Идеальные до пикселя коллизии в Monogame с плавающими позициями

Я хочу обнаружить пиксельные столкновения между двумя спрайтами.

Я использую следующую функцию, которую я нашел в Интернете, но она имеет для меня смысл.

    static bool PerPixelCollision(Sprite a, Sprite b)
    {
        // Get Color data of each Texture
        Color[] bitsA = new Color[a.Width * a.Height];
        a.Texture.GetData(0, a.CurrentFrameRectangle, bitsA, 0, a.Width * a.Height);
        Color[] bitsB = new Color[b.Width * b.Height];
        b.Texture.GetData(0, b.CurrentFrameRectangle, bitsB, 0, b.Width * b.Height);

        // Calculate the intersecting rectangle
        int x1 = (int)Math.Floor(Math.Max(a.Bounds.X, b.Bounds.X));
        int x2 = (int)Math.Floor(Math.Min(a.Bounds.X + a.Bounds.Width, b.Bounds.X + b.Bounds.Width));

        int y1 = (int)Math.Floor(Math.Max(a.Bounds.Y, b.Bounds.Y));
        int y2 = (int)Math.Floor(Math.Min(a.Bounds.Y + a.Bounds.Height, b.Bounds.Y + b.Bounds.Height));

        // For each single pixel in the intersecting rectangle
        for (int y = y1; y < y2; ++y)
        {
            for (int x = x1; x < x2; ++x)
            {
                // Get the color from each texture
                Color colorA = bitsA[(x - (int)Math.Floor(a.Bounds.X)) + (y - (int)Math.Floor(a.Bounds.Y)) * a.Texture.Width];
                Color colorB = bitsB[(x - (int)Math.Floor(b.Bounds.X)) + (y - (int)Math.Floor(b.Bounds.Y)) * b.Texture.Width];

                if (colorA.A != 0 && colorB.A != 0) // If both colors are not transparent (the alpha channel is not 0), then there is a collision
                {
                    return true;
                }
            }
        }
        //If no collision occurred by now, we're clear.
        return false;
    }

(все Math.floor бесполезны, я скопировал эту функцию из моего текущего кода, где я пытаюсь заставить ее работать с поплавками).

Он считывает цвет спрайтов в прямоугольной части, которая является общей для обоих спрайтов.

На самом деле это работает нормально, когда я отображаю спрайты в координатах x/y, где x и y являются целыми (.Bounds.X и .Bounds.Y):

Посмотреть пример

Проблема с отображением спрайтов в координатах int заключается в том, что это приводит к очень неровному движению по диагоналям:

Посмотреть пример

Поэтому в конечном итоге я хотел бы не приводить позицию спрайта к int при их рисовании, что приводит к плавному (er) движению:

Посмотреть пример

Проблема в том, что PerPixelCollision работает с целыми числами, а не с плавающей запятой, поэтому я добавил все эти Math.Floor. Как есть, это работает в большинстве случаев, но отсутствует одна строка и одна строка проверки внизу и справа (я думаю) общего прямоугольника из-за округления, вызванного Math.Floor:

Посмотреть пример

Когда я думаю об этом, я думаю, что это имеет смысл. Если x1 равно 80, а x2 на самом деле будет 81,5, но равно 81 из-за приведения, то цикл будет работать только для x = 80 и, следовательно, пропустит последний столбец (в примере gif фиксированный спрайт имеет прозрачный столбец на слева от видимых пикселей).

Проблема в том, что как бы я ни думал об этом, или что бы я ни пытался (я пробовал много вещей) - я не могу заставить это работать должным образом. Я почти убежден, что x2 и y2 должны иметь Math.Ceiling вместо Math.Floor, чтобы «включить» последний пиксель, который в противном случае не учитывался, но тогда он всегда дает мне индекс из массивов bitsA или bitsB.

Кто-нибудь сможет настроить эту функцию так, чтобы она работала, когда Bounds.X и Bounds.Y являются числами с плавающей запятой?

PS - может ли проблема возникнуть из-за BoxingViewportAdapter? Я использую это (из MonoExtended) для «увеличения» моей игры, которая на самом деле 144p.


person Cool Cascade    schedule 16.01.2019    source источник
comment
Границы измеряются в пикселях. И поскольку нет 0,2 пикселя, здесь не имеет смысла использовать float. Есть ли причина, по которой вы хотите использовать float вместо int?   -  person Pavel Slesinger    schedule 17.01.2019


Ответы (2)


Помните, что не существует такой вещи, как дробный пиксель. Для целей движения имеет смысл использовать числа с плавающей запятой для значений и приводить их к целым пикселям при отрисовке. Проблема не в дробных значениях, а в том, как они нарисованы.

Основная причина, по которой коллизии не работают правильно, - это масштабирование. Цвета новых пикселей между диагоналями получаются путем усреднения* окружающих пикселей. Эффект заставляет изображение казаться больше, чем оригинал, особенно по диагонали.

* существует несколько методов масштабирования, бикубический и линейный являются наиболее распространенными.

Единственное прямое (пиксельное) решение – сравнить фактический результат после масштабирования. Это требует двойного рендеринга всего экрана и дополнительных вычислений коэффициента масштабирования. (не рекомендуется)

Поскольку вы сравниваете немасштабированные изображения, ваши столкновения кажутся отключенными.

Другой вопрос — скорость движения. Если вы двигаетесь быстрее, чем на один пиксель за Update(), обнаружения столкновений по пикселям недостаточно, если движение должно быть ограничено препятствием. Вы должны разрешить конфликт.

Для врагов или опасностей окружающей среды достаточно исходного кода, и разрешение коллизий не требуется. Это даст игроку небольшое преимущество.

Простой алгоритм разрешения (см. ниже математическое решение) состоит в том, чтобы раскрутить движение наполовину, проверить на столкновение. Если он все еще сталкивается, раскрутите механизм на четверть, в противном случае увеличьте его на четверть и проверьте на столкновение. Повторяйте, пока движение не станет меньше 1 пикселя. Это запускает журнал скорости раз.

Что касается верхней стены, которая не сталкивается идеально: если начальное значение Y не кратно скорости вертикального движения, вы не приземлитесь идеально на ноль. Я предпочитаю решать эту проблему, устанавливая Y = 0, когда Y отрицательно. То же самое для X, а также когда X и Y > границы экрана - начало координат, для нижней и правой части экрана.

Я предпочитаю использовать математические решения для разрешения столкновений. На ваших примерах изображений вы показываете коробку, сталкивающуюся с бриллиантом, форма ромба математически представлена ​​​​как манхэттенское расстояние (Math.Abs(x1-x2) + Math.Abs(y1-y2))). Из этого факта легко напрямую рассчитать разрешение столкновения.

По оптимизациям:

Перед вызовом этого метода убедитесь, что ограничивающие прямоугольники перекрываются.

Как вы сказали, удалите все Math.Floor, так как броска достаточно. Сократите все вычисления внутри циклов, не зависящие от переменной цикла вне цикла.

(int)a.Bounds.Y * a.Texture.Width и (int)b.Bounds.Y * b.Texture.Width не зависят от переменных x или y и должны быть рассчитаны и сохранены перед циклами. Вычитания 'y-[переменная выше]` должны храниться в цикле "y".

Я бы рекомендовал использовать битовую доску (1 бит на квадрат 8 на 8) для коллизий. Это уменьшает широкие (8x8) проверки столкновений до O (1). Для разрешения 144х144 все пространство поиска становится 18х18.

person Strom    schedule 18.01.2019

вы можете обернуть свой спрайт прямоугольником и использовать его функцию Intersect, которая обнаруживает столкновения.

Intersect — XNA< /а>

person Ben Efrat    schedule 19.01.2019