Предыдущие части: Часть 0Часть 1, Часть 2, Часть 3

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

4.6 Даем объектам свойство материальности

Теперь поговорим о том, как сделать так, чтобы спрайты обросли плотью внутри нашей маленькой вселенной. Если в предыдущих разделах я более-менее понимал, что творю, то сейчас вступил на территорию, которая для меня является достаточно новой. Я много писал на C#, но очень мало писал игры и даже приложения, где необходимо так или иначе обсчитывать «физику» объектов. По роду деятельности я в основном работаю с дискретными системами, когда пространство поделено на одинаковые клетки, и в клетке в один момент времени может быть только один объект. В таких системах сама клетка выступает формочкой, поэтому не нужно особо заморачиваться с обсчетом столкновений. При моделировании «непрерывного» пространства приходится вводить новую сущность, которая отвечает за форму - коллайдер.

Создаем класс RectangleCollider, который будет придавать объектам прямоугольную форму:

public class RectangleCollider
{
    public Rectangle Boundary { get; set; }
    public RectangleCollider(int x, int y, int width, int height)
    {
        Boundary = new Rectangle(x, y, width, height);
    }

    public static bool IsCollided(RectangleCollider r1, RectangleCollider r2)
    {
        return r1.Boundary.Intersects(r2.Boundary);
    }
}

В Monogame есть класс Rectangle, который можно использовать для обнаружения пересечений между объектами. Я сделал над ним еще один класс для удобства работы. Класс RectangleCollider содержит сам Rectangle, конструктор для его создания и статический метод IsCollided, который вызывает уже существующий метод Intersects. Я так сделал, потому что мне удобнее вызывать метод столкновения без привязки к конкретному прямоугольнику.

Теперь добавим нашим объектам способность быть материальными. Создаем интерфейс, который говорит нам о том, что объект имеет твердую форму:

public interface ISolid
{
    RectangleCollider Collider { get; set; }
    void MoveCollider(Vector2 newPos);
}

Метод MoveCollider нужен, чтобы передвигать наш прямоугольник вместе с объектом. Теперь прилепим коллайдер к машинке и соответствующим образом изменим код:

public class Car : IObject, ISolid
{
    private Vector2 _speed;

    public Vector2 Pos { get; set; }
    RectangleCollider Collider { get; set; }


    public Car(Vector2 position)
    {
        Pos = position;
        Collider = new RectangleCollider((int)Pos.X, (int)Pos.Y, 77, 100);
    }
    public void Update()
    {
        Pos += Speed;        
        Speed = new Vector2(0, Speed.Y);
    }

    public void MoveCollider(Vector2 newPos)
    {
        Collider.Boundary.Offset(newPos);
    }
}

Так как машинка реализует интерфейс ISolid, то я добавил поле прямоугольного коллайдера, определил метод его сдвига, прописал инициализацию в конструкторе так, чтобы коллайдер по размерам соответствовал спрайту, и добавил вызов движения коллайдера в Update().

Теперь подробнее о том, как происходит сдвиг коллайдера. Объект Rectangle меняет позицию через метод Offset, напрямую его координату не поменять. У него есть три перегрузки, я выбрал ту, которая работает через Vector2, так как позиция коллайдера всегда такая же, как позиция объекта, а ее мы задаем так же через Vector2. Сейчас мы видим две особенности – во-первых, размеры нашего прямоугольника хардкодятся, потому что ему пока неоткуда взять размер машинки, во-вторых, теперь мы не можем менять поле Pos от балды, извне, иначе изображение и коллайдер машинки разойдутся.

Однако далее при проверке я выяснил, что метод Offset не работает как надо - он создает новый прямоугольник, который надо куда-то записать. Мне не понравился синтаксис для этого, тем более, никакого выигрыша по производительности не будет, потому что мы все равно создаем новый объект. Поэтому я поправил этот кусок и метод MoveCollider стал выглядеть следующим образом:

public void MoveCollider(Vector2 newPos)
{
    Collider = new RectangleCollider((int)Pos.X, (int)Pos.Y, 77, 100);
}

Итак, в машинку мы добавили свойство твердости, а что с ней делать будем решать уже в игровом цикле. Пока достаточно сделать так, чтобы объекты просто не входили друг в друга. И, нужно сказать, уже на этом этапе такая задача для меня оказалась нетривиальной. Методом проб и ошибок я осуществил две реализации при разработке первой версии игры, и они работали, но никогда меня не удовлетворяли полностью. Поэтому в этот раз я решил придумать что-то новое и более простое по сравнению с предыдущими реализациями. И, чтобы показать, что все не так просто, как кажется (хотя, для кого-то проблема окажется не стоящей выеденного яйца), я продемонстрирую весь путь до решения, включая неудачные шаги.

Задачу обсчета столкновений можно разбить на два этапа – обнаружение того, что столкновение вообще произошло, и что с этим дальше делать.

И уже на первом этапе начинается нечто интересное. Если бы мы делали «дискретную» игру, подобную пошаговым стратегиям типа Цивилизации или Героев Меча и Магии (да даже шахмат), где все объекты могут находиться только внутри клеток и не могут иметь промежуточных координат, то проблемы бы не стояло вообще – мы знаем, в какую клетку объект собирается шагать и можем посмотреть, есть ли там что-то. Это легко реализовать даже в лоб с помощью массива. По сути, в таких играх нет физики.

В нашем случае, так как у нас нет жесткой привязки объектов в какой-либо позиции (тайлы сейчас, по сути, служат для упрощения размещения объектов при инициализации) мы сталкиваемся с проблемой того, как понять, а произошло ли столкновение? Напрашивается решение в лоб – у нас есть словарь объектов, почему бы их просто попарно не проверить на столкновение? Оно крайне неоптимальное, но работать будет, поэтому начнем с него. Кроме того, помним – проверять на столкновение можно и нужно не все объекты, а только имеющие свойство твердости. Следовательно, предварительно нам нужно проверить - а нужно ли вообще у этого объекта обсчитывать столкновения?

public void Update()
{
    Vector2 playerInitPos = ObjectSecurity[PlayerId].Pos;
    foreach (var o in Objects.Values)
    {
        o.Update();
        if (o is ISolid && o is Car p1)
        {
            foreach (var ao in Objects.Values)
                if (ao is ISolid p2)
                {
                    if (RectangleCollider.IsCollided(p1.Collieder, p2.Collider))
                    {
                        p1.Speed = new Vector2(0, 0);
                    }
                }
        }
    }
  Vector2 playerShift = Objects[PlayerId].Pos - playerInitPos;
  Updated.Invoke(this, new GameplayEventArgs 
                 { 
                   Objects = Objects, 
                     POVShift = playerShift
                 });
}

Итак, мы проверяем, является ли текущий объект твердым (и машинкой), дальше циклом проверяем все остальные объекты на карте, так же смотрим, является ли второй объект твердым и, если эти две сущности нашли друг друга, то останавливаем текущий объект. Двойная проверка здесь является ошибкой, так как если объект - машина, то он уже твердый, но далее я это поправлю.

Сложность алгоритма только что возросла, потому что вместо одного мы проходимся по словарю объектов два раза. Теперь, если добавить два новых объекта, количество шагов каждый игровой цикл увеличится на 4. Это очень плохо, но потом решим, что с этим делать – пока объектов мало и на скорости это не скажется.

Немного поменяем карту, чтобы генерировать машинки рядом.

private char [,] _map = new char [11, 500];
private int _tileSize = 100;
public int PlayerId { get ;set; }
public Dictionary<int, IObject> Objects { get; set; }

public void Initialize()
{    
    Objects = new Dictionary<int, IObject>();
    _map[5, 7] = 'С';
    _map[6, 2] = 'P';
}

Если запустим наш код, то машинка не будет двигаться, потому что сталкивается сама с собой. Следует прописать, что не нужно проверять в этом случае столкновение собой. Для этого заменим foreach на for и будем определять одинаковость объектов по ключу. Попутно у второго цикла сделаем отсчет не с 1-го ключа, а с текущего+1, чтобы два раза не проверять столкновение одних и тех же объектов (спойлер – работает некорректно). Тем самым заодно и решается проблема столкновения объекта с самим собой (если вам показалось, что здесь может быть ошибка, если какой-то объект будет удален из словаря, то вам не показалось):

public void Update()
{
    Vector2 playerInitPos = Objects[PlayerId].Pos;
    for (int i = 1; i <= Objects.Keys.Count; i++)
    {
        Objects[i].Update();
        if (Objects[i] is Car p1)
        {
            for (int j = i+1; j <= Objects.Keys.Count; j++)
            {
                if (Objects[j] is ISolid p2)
                {
                    if (RectangleCollider.IsCollided(p1.Collieder, p2.Collider))
                    {
                        p1.Speed = new Vector2(0, 0);
                    }
                }
            }                
        }
    }
    Vector2 playerShift = Objects[PlayerId].Pos - playerInitPos;
    Updated.Invoke(this, new GameplayEventArgs 
                 { 
                   Objects = Objects, 
                     POVShift = playerShift
                 });
}

Если мы запустим этот код, то увидим, что ничего не работает:

Это происходит потому, что в нашем алгоритме имеет значение порядок того, кто с кем столкнулся. Так как неподвижная машинка имеет меньший Id, чем машинка игрока (так как генерируется по карте раньше), то получается, что при столкновении объектов p1 и p2 зануляется скорость именно неподвижной машинки, а не игрока.

В порядке эксперимента, можно попробовать остановить оба столкнувшихся объекта:

Но, вот незадача – объект p2 является ISolid, а у ISolid нет поля скорости. Печально. Можно было бы p2 тоже привести к типу Car, но кто сказал, что в нашей игре могут двигаться только машины? Сделать еще один интерфейс IMovable? Возможно, придется, но в этом месте я подумал, что логично сделать поле скорости не только у машинки, а у всех классов, которые реализуют IObject, потому что нам может понадобиться двигать любой объект вплоть до элементов пользовательского интерфейса и даже стенок. В принципе, наверное, более правильно было бы все-таки навернуть интерфейс IMovable, но мне откровенно не нравится возиться с этими бесконечными привидениями к типу. Сейчас я пытаюсь сделать так, чтобы объекты были более-менее автономными, поэтому все их поведение вынесено в метод Update, который есть у всех объектов, так как у меня словарь элементов IObject. Игровой цикл работает с взаимодействиями между объектами, например, заведует обсчетом столкновений. Если скорость будет в IMovable, то придется либо делать отдельную коллекцию для подвижных объектов, либо пытаться привести объект к типу IMovable... короче, я это вижу не так. Поэтому просто сделаю возможность наличия скорости у всех IObject, а что они будут с ней делать решат при конкретной реализации. Итак, добавляем IObject поле скорости:

public interface IObject
{    
    int ImageId { get; set; }

    Vector2 Pos { get; set; }

    Vector2 Speed { get; set; }

    void Update();    
}

И реализуем скорость у стенки – у машинки она уже есть:

class Wall : IObject
{    
    public int ImageId { get; set; }

    public Vector2 Pos { get; set; }

    public Vector2 Speed { get; set; }

    public void Update()
    {

    }
}

Мне это нравится больше, чем возиться с гигантским количеством интерфейсов. Все хорошо в меру. Теперь кусок обсчета столкновений в GameCycle мы можем сделать так:

<..........................>
if (Objects[i] is Car p1)
{
    for (int j = i + 1; j <= Objects.Keys.Count; j++)
    {
        if (Objects[j] is ISolid p2)
        {
            if (RectangleCollider.IsCollided(p1.Collieder, p2.Collider))
            {
                Objects[i].Speed = new Vector2(0, 0);
                Objects[j].Speed = new Vector2(0, 0);
            }
        }
    }
}
<..........................>
       

Вместо Car в первой строке можно аналогично приводить к ISolid.

Такое решение будет работать (я немного изменил движение машинки, чтобы легче было работать — теперь у них нет ускорения):

Но работает не совсем так, как нужно. Машинки въезжают друг в друга и намертво сцепляются, так как во время сдачи назад все еще есть пересечение их коллайдеров, поэтому скорость обнуляется. Нужно придумать что-нибудь, чтобы после въезжания коллайдеров друг в друга машинки выезжали друг из друга. Здесь я опять застопорился. Можно было бы просто вернуть машинку на изначальную позицию, но тут я подумал – ведь это не универсальное решение! Оно не будет работать в тех же платформерах, где есть гравитация, потому что персонажи не смогут двигаться по горизонтали, их будет откатывать назад. Мне же хотелось сделать универсальное решение, потому что я люблю универсальные решения! Но так как родить своей головой в этот раз я ничего не смог, то наступил на горло своей гордыне, полез на ютуб и посмотрел, как делают обсчет столкновений другие люди.

Собственно, именно по Monogame самые полные видеообучения лежат на этом канале.

Посмотрел 9-й урок – обсчет коллизий. Залез на гитхаб автора, и увидел там вот это:

То есть, у нас есть 4 метода, которые проверяют, сталкивается ли текущий коллайдер, если его сдвинуть на шаг в определенном направлении с тем коллайдером, для которого мы проверяем столкновение, и еще и смотрим, где они столкнулись относительно текущего – сверху, снизу, по бокам. Мне стало плохо, я стал искать дальше. Второй ролик, на котором я остановил внимание, оказался интереснее. Там даже человек, вроде как, делает целиковую игру, но это RPG с видом сверху. То есть – правильно – он просто откатывает персонажа на предыдущий шаг, если произошло пересечение с препятствием, гравитации тут нет. Моя мечта о единой модели для всего рухнула, поэтому я сдался и тупо стал откатывать машинку на шаг назад.

Комментарий: в самой первой статье я говорил, что не встречал материалов, где целиком создают законченную игру. Я раньше не видел этот ролик и, возможно, на этом канале автор делает именно это, но утверждать не могу.

<......................................>
Vector2 playerInitPos = Objects[PlayerId].Pos;
for (int i = 1; i <= Objects.Keys.Count; i++)
{
    //Добавляем сохранение начальной позиции проверяемого объекта
    Vector2 objInitPos = Objects[i].Pos;
    Objects[i].Update();
    //Приводим объект не к Car, а к ISolid, что более правильно,
    //и смотрим, сдвинулся ли он после Update
    if (Objects[i] is ISolid p1 && objInitPos != Objects[i].Pos)
    {
        //Отсчитываем все ключи, чтобы избежать ситуации, когда
        //при определенном порядке ключей столкновения нет
        for (int j = 1; j <= Objects.Keys.Count; j++)
        {
            //Сам с собой объект не сталкивается
            if (i == j)
              continue;
            if (Objects[j] is ISolid p2)
            {
                if (RectangleCollider.IsCollided(p1.Collieder, p2.Collider))
                {
                    Objects[i].Pos = objInitPos;
                    p1.MoveCollider(Objects[i].Pos);
                }
            }
        }
    }
}
<......................................>

Теперь мы сохраняем начальную позицию объекта и, если он с чем-то столкнется, возвращаем его назад. Кстати, обратите внимание, что я передвигаю и объект, и его коллайдер в игровом цикле. Это плохое решение (так как в суматохе можно забыть после сдвига объекта сдвинуть и его коллайдер), которое мы потом поправим. Запускаем — работает:

Кажется, что работает. Но, на самом деле нет. Вы же помните, что я убрал механику ускорения у машинок? А что, если ее вернуть?

public void Update()
{
    Pos += Speed;
    MoveCollider(Pos);
    Speed = new Vector2 (0, Speed.Y);
    //Speed = new Vector2(0,0);
}

Если скорость отличается от 1, то столкновение будет не вплотную:

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

<......................................>
Vector2 playerInitPos = Objects[PlayerId].Pos;
for (int i = 1; i <= Objects.Keys.Count; i++)
{    
    Vector2 objInitPos = Objects[i].Pos;
    Objects[i].Update();    
    if (Objects[i] is ISolid p1 && objInitPos != Objects[i].Pos)
    {        
        for (int j = 1; j <= Objects.Keys.Count; j++)
        {            
            if (i == j)
              continue;
            if (Objects[j] is ISolid p2)
            {
                bool isCollided = false;
                while (RectangleCollider.IsCollided(p1.Collieder, p2.Collider))
                {
                    var oppositeDirection = Objects[i].Speed;
                    oppositeDirection.Normalize();
                    Objects[i].Pos = Objects[i].Pos-oppositeDirection;
                    p1.MoveCollider(Objects[i].Pos);
                }
            }
        }
    }
}
<......................................>

Вместо if делаем цикл while, который будем крутить до тех пор, пока объекты не перестанут пересекаться. Внутри самого цикла мы сохраняем текущую скорость в oppositeDirection. Далее, мы нормализуем этот вектор (для Vector2 в Monogame есть такая процедура), после чего он становится единичным, но смотрит по направлению скорости объекта. Далее ставим объект в точке (Текущая позиция — вектор скорости), тем самым сместив объект обратно на единицу длины. Кроме того, вводим переменную isCollided — если объекты столкнулись, то тормозим текущий объект. В общем, первая версия обработчика закончена, но сначала немного порефакторим. Идем в класс машинки и создаем метод Move, чтобы коллайдер двигался в связке с самим объектом автоматически:

<..........................>
public void Update()
{            
    Move(Pos + Speed);
    Speed = new Vector2(0, Speed.Y);
}
public void Move(Vector2 newPos)
{
    Pos = newPos;
    MoveCollider(Pos);
}
<..........................>

Чтобы можно работать с более общими конструкциями, вынесем метод Move в IObject и реализуем метод Move еще и в стенке (так как она тоже реализует IObject), но оставим его пустым (код со стенкой я выносить сюда не буду, чтобы не захламлять статью). Теперь немного поинкапсулируем. Дело в том, что мы добавили метод Move, но никто не мешает нам поменять позицию объекта, напрямую обращаясь к свойству, вызвав рассинхронизацию позиции коллайдера и объекта. Для этого просто уберем из свойства Pos интерфейса IObject сеттер. Новый код объекта сейчас выглядит следующим образом:

public interface IObject
{    
    int ImageId { get; set; }
    //Убрали сеттер из свойства Pos
    Vector2 Pos { get; }
    Vector2 Speed { get; set; }
    void Update();
    //Добавили метод Move
    void Move(Vector2 pos);
}

Таким образом мы определяем у свойства интерфейса публичный геттер, а сеттер уже можем реализовывать у конкретного класса, как нам хочется, при этом если экземпляр класса скрыт в интерфейсе IObject, мы менять свойство Pos не сможем. У машинки сеттер сделаем приватным:

public class Car : IObject, ISolid
{
    private Vector2 _speed;
    //Теперь позицию менять можно только внутри самой машинки
    public Vector2 Pos { get; private set; }
    RectangleCollider Collider { get; set; }
    //Заодно сделаем свойство скорости, чтобы не ускоряться до бесконечности
    public Vector2 Speed
    {
        get
        {
            return _speed;
        }
        set
        {
            _speed = value;
            if (_speed.Y > 20)
                _speed.Y = 20;
            else if (_speed.Y < -20)
                _speed.Y = -20;
        }
    }
    public Car(Vector2 position)
    {
        Pos = position;
        Collider = new RectangleCollider((int)Pos.X, (int)Pos.Y, 77, 100);
    }
    public void Update()
    {
        Pos += Speed;
        MoveCollider(Pos);
        Speed = new Vector2(0, Speed.Y);
    }
    
    public void MoveCollider(Vector2 newPos)
    {
        Collider = new RectangleCollider((int)Pos.X, (int)Pos.Y, 77, 100);
    }
}

Теперь мы не можем двигать машинку извне (например, в GameCycle) просто так — только через метод Move. Аналогично можно заприватить сеттер стенки на всякий случай.

Однако, у нас есть следующий баг– если скорость обнуляется к моменту обсчета коллизий, то объект не сдвигается обратно, так как мы сделали привязку к вектору скорости. На самом деле, программу вообще выкидывает, потому что в этом случае мы делим на модуль вектора, равный 0. Сделаем привязку направления возврата не к скорости, а к предыдущей позиции:

<.........................................>
bool isCollided = false;
while (RectangleCollider.IsCollided(p1.Collieder, p2.Collider))
{
    //Теперь вектор возврата высчитывается как разность
    //векторов текущей и начальной позиций
    Vector2 oppositeDirection = Objects[i].Pos - objInitPos;
    oppositeDirection.Normalize();
    Objects[i].Pos = Objects[i].Pos - oppositeDirection;
    p1.MoveCollider(Objects[i].Pos);
}
<.........................................>

И, если запустить, то все, наконец-то, работает!

Теперь снова немного порефакторим – сделаем код менее кривым, а также создадим задел на будущее – у нас могут быть разные категории столкновений, и пихать все это в несчастный метод Update будет некрасиво. Нужно разделение труда. Кроме того, здесь будет незаметный пока баг – у нас имеет значение порядок обсчета – тормозится и сдвигается только тот объект, который стоит на месте p1. Так как вторая машинка неподвижна, то все в порядке, но если у нас будет происходить столкновение движущихся объектов, то тормозиться от столкновения будет только тот, у которого айдишник меньше.

Сама напрашивается мысль вынести процедуру обсчета в отдельный метод. Но не все так просто – мы не можем просто взять и вырезать кусок кода с обсчетом в отдельный метод, так как, если посмотреть внимательнее, то у нас в одном и том же цикле вызывается Update объекта, затем вызывается еще один цикл, и для обсчета столкновения используются оба индекса. Иными словами, в одном и том же цикле все накидано в кучу, он отвечает сразу за несколько вещей, что неправильно.

Поэтому я решил сделать в Update два цикла – первый просто вызывает Update у объектов, как и раньше, а во втором мы уже перебираем объекты, которые должны посталкиваться. А сталкиваться они уже будут с помощью вызова в отдельного метода. С него и начнем. По моей задумке этот метод должен заниматься всем, что относится к «физике» - проверяет, ISolid ли объекты, столкнулись ли, и что с этим делать:

private void CalculateObstacleCollision(
  (Vector2 initPos, int Id) obj1, 
  (Vector2 initPos, int Id) obj2
)
{    
    bool isCollided = false;
    if (Objects[obj1.Id] is ISolid p1 && Objects[obj2.Id] is ISolid p2)
    {
        Vector2 oppositeDirection = new Vector2 (0, 0);
        while (RectangleCollider.IsCollided(p1.Collider, p2.Collider))
        {
            isCollided = true;
            if (obj1.initPos != Objects[obj1.Id].Pos)
            {
                oppositeDirection = Objects[obj1.Id].Pos - obj1.initPos;
                oppositeDirection.Normalize();
                Objects[obj1.Id].Move(Objects[obj1.Id].Pos - oppositeDirection);
            }
            if (obj2.initPos != Objects[obj2.Id].Pos)
            {
                oppositeDirection = Objects[obj2.Id].Pos - obj2.initPos;
                oppositeDirection.Normalize();
                Objects[obj2.Id].Move(Objects[obj2.Id].Pos - oppositeDirection);
            }  
        }
    }
    if (isCollided)
    {
        Objects.[obj1.Id].Speed = new Vector2(0, 0);
        Objects.[obj2.Id].Speed = new Vector2(0, 0);
    }
}

Я его назвал CalculateObstacleCollision, так как он при столкновении будет просто не давать объектам входить друг в друга. Позднее я хочу сделать другие типы столкновений, например нечто более «физичное», с учетом массы объектов. В качестве аргументов наш метод принимает два кортежа, которые содержат начальную позицию объекта на начало игрового цикла и айдишник этого объекта. Начальную позицию знать необходимо, иначе мы не будем понимать, в какую сторону толкать объект обратно. Все же данные по текущему состоянию объекта мы можем получить из списка Objects, поэтому методу нужен только его Id.

Дальше немного упорядочиваем проверку. Сначала нужно проверить, чтобы оба объекта реализовывали интерфейс ISolid. После этого крутим тот же цикл while, что и до этого в Update, но теперь мы для обоих объектов проверяем, двигались ли они на этом ходу, и, если да, то обоих сдвигаем в обратно направлении на единичный вектор. В конце обнуляем скорость тоже у обоих. Теперь не имеет значения порядок обсчета объектов.

Приступим в модернизации метода Update:

public void Update()
{
    Vector2 playerInitPos = ObjectSecurity[PlayerId].Pos;
    Dictionary<int, Vector2> collisionObjects = new Dictionary<int, Vector2>();
    foreach (var i in Objects.Keys)
    {
        Vector2 initPos = Objects[i].Pos;
        Objects[i].Update();
        collisionObjects.Add(i, initPos);
    }
    foreach (var i in collisionObjects.Keys)
    {
        foreach (var j in collisionObjects.Keys)
        {
            if (i==j)
              continue;
            CalculateObstacleCollision(
              (collisionObjects[i],i), 
              (collisionObjects[j],j)
            );
        }
    }
    Vector2 playerShift = Objects[PlayerId].Pos - playerInitPos;
    Updated.Invoke(this, new GameplayEventArgs 
                 { 
                   Objects = Objects, 
                     POVShift = playerShift
                 });
}

Создаем еще один словарь, который по ключу хранит айдишник объекта, а по значению – его начальную позицию на начало игрового цикла. После вызова Update у каждого объекта сохраняем новый элемент в этом словаре. Кроме того, я поменял цикл – теперь я прохожусь с использованием foreach. Дело в том, что если порядок айдишников нарушится, например, мы удалим какой-то объект из списка, то вся наша стройная система навернется, так как цикл for будет пытаться обратиться к несуществующему объекту. С foreach такой проблемы не будет.

Теперь смотрим на кусок обсчета столкновений. Мы добавили двойной цикл по нашему новому словарю collisionObjects, делаем в нем проверку на неравенство ключей, чтобы объект не сталкивался сам с собой, на вход метода собираем кортежи из наших ключей. Если запустим, то увидим, что ничего не поменялось – это хорошо.

Вернем обратно нашу большую карту для тестирования, со стенками и прочим:

private char [,] _map = new char [11, 500];
private int _tileSize = 100;
public int PlayerId { get ;set; }
public Dictionary<int, IObject> Objects { get; set; }

public void Initialize()
{    
    Objects = new Dictionary<int, IObject>();
    _map[5, 7] = 'P';
    _map[4, 4] = 'C';
    _map[6, 4] = 'C';
    _map[5, 2] = 'W';
    for (int y = 0; y < _map.GetLength(1); y++)
    {
        _map[0, y] = 'W';
        _map[_map.GetLength(0) - 1, y] = 'W';
    }
}

И видим, что игра безбожно тормозит. Ибо у нас 1000 стенок, каждая из них отдельный объект и, даже несмотря на то, что мы стенки пока не сделали твердыми, в цикле collisionObjects они крутятся. Мы проверяем столкновение всех объектов со всеми независимо от их местоположения. Если поставить поле хотя бы 11х100, а не 11х500, то все работает прекрасно. Добавим стенке свойство твердости - реализуем в ней интерфейс ISolid; MoveCollider и Move продублируем с машинки.

При размере трассы 11х100 работает неплохо, а проблему неоптимального обсчета столкновений мы решим в следующей статье. Пока же на маленькой трассе видно, что наш мир обрел форму:

На сегодня все, большое спасибо за внимание!

Комментарии (0)