Как можно отделить игровую логику от анимации и отрисовки?

9

Я только ранее делал флэш-игры, используя MovieClips и тому подобное, чтобы отделить анимацию от игровой логики. Сейчас я пытаюсь попробовать создать игру для Android, но теория программирования игр вокруг разделения этих вещей все еще смущает меня. Я пришел из опыта разработки неигровых веб-приложений, поэтому я разбираюсь в шаблонах, похожих на MVC, и застрял в этом мышлении, приближаясь к программированию игр.

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

Как я могу выйти из этого мышления и начать думать о шаблонах, которые имеют больше смысла для игр?

ВТМ
источник

Ответы:

6

Анимация все еще может быть идеально разделена между логикой и рендерингом. Абстрактное состояние данных анимации будет той информацией, которая необходима вашему графическому API для визуализации анимации.

Например, в 2D-играх это может быть прямоугольная область, которая отмечает область, отображающую текущую часть вашего спрайт-листа, которую нужно нарисовать (если у вас есть лист, состоящий из 30 рисунков 80x80, содержащих различные шаги вашего персонажа) прыгать, садиться, двигаться и т. д.). Это также могут быть любые данные, которые вам не нужны для рендеринга, но, возможно, для управления самими состояниями анимации, например, время, оставшееся до истечения текущего шага анимации, или название анимации («ходьба», «стоя»). и т.д.) Все это можно представить так, как вы хотите. Это часть логики.

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

В коде (здесь используется синтаксис C ++):

class Sprite //Model
{
    private:
       Rectangle subrect;
       Vector2f position;
       //etc.

    public:
       Rectangle GetSubrect() 
       {
           return subrect;
       }
       //etc.
};

class AnimatedSprite : public Sprite, public Updatable //arbitrary interface for classes that need to change their state on a regular basis
{
    AnimationController animation_controller;
    //etc.
    public:
        void Update()
        {
            animation_controller.Update(); //Good OOP design ;) It will take control of changing animations in time etc. for you
            this.SetSubrect(animation_controller.GetCurrentAnimation().GetRect());
        }
        //etc.
};

Это данные. Ваш рендерер возьмет эти данные и нарисует их. Поскольку и обычные, и анимированные спрайты нарисованы одинаково, вы можете использовать полиморф здесь!

class Renderer
{
    //etc.
    public:
       void Draw(const Sprite &spr)
       {
           graphics_api_pointer->Draw(spr.GetAllTheDataThatINeed());
       }
};

ВТМ:

Я придумал другой пример. Скажем, у вас есть РПГ. Например, вашей модели, представляющей карту мира, вероятно, потребуется сохранить положение персонажа в мире в виде координат плитки на карте. Однако когда вы перемещаете персонажа, они переходят на несколько пикселей за раз к следующему квадрату. Вы сохраняете это положение «между плитками» в анимационном объекте? Как вы обновляете модель, когда персонаж, наконец, «прибыл» в следующую координату плитки на карте?

Карта мира не знает непосредственно о позиции игроков (у нее нет Vector2f или чего-то подобного, которая напрямую хранит позицию игроков =, вместо этого она имеет прямую ссылку на сам объект игрока, который, в свою очередь, происходит от AnimatedSprite так что вы можете легко передать его рендереру и получить от него все необходимые данные.

В целом, однако, ваша карта тайлов не должна быть в состоянии сделать только все - у меня был бы класс "TileMap", который заботится об управлении всеми тайлами, и, возможно, он также обнаруживает столкновения между объектами, которые я передаю ему и плитки на карте. Затем у меня был бы другой класс "RPGMap", или как бы вы его не называли, который имеет как ссылку на вашу карту тайлов, так и ссылку на игрока и выполняет фактические вызовы Update () для вашего игрока и вашего tilemap.

То, как вы хотите обновить модель, когда игрок движется, зависит от того, что вы хотите сделать.

Разрешено ли вашему игроку самостоятельно перемещаться между тайлами (стиль Zelda)? Просто обработайте ввод и переместите игрока соответственно каждый кадр. Или вы хотите, чтобы игрок нажимал «вправо», и ваш персонаж автоматически перемещал одну плитку вправо? Позвольте вашему классу RPGMap интерполировать положение игроков, пока он не прибудет в пункт назначения, и тем временем заблокировать всю обработку ввода с помощью клавиш движения.

В любом случае, если вы хотите облегчить себе задачу, все ваши модели будут иметь методы Update (), если им действительно нужна некоторая логика для обновления (вместо простого изменения значений переменных) - вы не отдаете контроллер таким образом, в шаблоне MVC вы просто перемещаете код из «одного шага выше» (контроллера) в модель, и все, что делает контроллер, - это вызывает метод Update () модели (в нашем случае контроллер будет RPGMap). Вы все еще можете легко поменять код логики - вы можете просто напрямую изменить код класса или, если вам нужно совершенно другое поведение, вы можете просто наследовать от своего класса модели и только переопределить метод Update ().

Такой подход значительно сокращает вызовы методов и тому подобное - что раньше было одним из главных недостатков чистого шаблона MVC (в конечном итоге вы часто вызываете GetThis () GetThat ()) - он делает код длиннее и немного сложнее для чтения, а также медленнее - даже если об этом позаботится ваш компилятор, который оптимизирует множество подобных вещей.

TravisG
источник
Будете ли вы хранить данные анимации в классе, содержащем игровую логику, в классе, содержащем игровой цикл, или отдельно от обоих? Кроме того, все зависит от цикла или класса, содержащего цикл, чтобы понять, как преобразовать данные анимации в фактическое рисование экрана, верно? Часто бывает не так просто получить прямоугольник, представляющий часть листа спрайта, и использовать его для обрезки растрового изображения из листа спрайта.
TMV
Я придумал другой пример. Скажем, у вас есть РПГ. Например, вашей модели, представляющей карту мира, вероятно, потребуется сохранить положение персонажа в мире в виде координат плитки на карте. Однако когда вы перемещаете персонажа, они переходят на несколько пикселей за раз к следующему квадрату. Вы сохраняете это положение «между плитками» в анимационном объекте? Как вы обновляете модель, когда персонаж, наконец, «прибыл» в следующую координату плитки на карте?
TMV
Я отредактировал ответ на ваш вопрос, так как в комментариях недостаточно символов для этого.
TravisG
Если я все правильно понимаю:
TMV
Вы можете иметь экземпляр класса «Animator» внутри вашего View, и у него будет публичный метод «update», который вызывается каждым кадром представлением. Метод update вызывает методы «update» экземпляров различных видов отдельных объектов анимации внутри него. Аниматор и анимации внутри него имеют ссылку на модель (передаваемую через их конструкторы), поэтому они могут обновить данные модели, если анимация изменит их. Затем, в цикле рисования, вы получаете данные из анимации внутри аниматора таким образом, чтобы они могли быть поняты представлением и нарисованы.
TMV
2

Я могу развить это, если хотите, но у меня есть центральный рендер, которому говорят рисовать в цикле. Скорее, чем

handle input

for every entity:
    update entity

for every entity:
    draw entity

У меня система больше похожа

handle input (well, update the state. Mine is event driven so this is null)

for every entity:
    update entity //still got game logic here

renderer.draw();

Класс рендерера просто содержит список ссылок на рисуемые компоненты объектов. Они назначены в конструкторах для простоты.

Для вашего примера у меня будет класс GameBoard с несколькими тайлами. Каждая плитка, очевидно, знает свою позицию, и я предполагаю некоторую анимацию. Разложите это в некоторый класс Animation, которым владеет плитка, и пусть он передает ссылку на себя в класс Renderer. Там все разделено. Когда вы обновляете Tile, он вызывает Update для анимации .. или обновляет его сам. Когда Renderer.Draw()вызывается, он рисует анимацию.

Не зависящая от кадра анимация не должна слишком сильно зависеть от цикла рисования.

Коммунистическая утка
источник
0

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

Методология, которая кажется наиболее подходящей для игрового дизайна, заключается в отделении логики от вывода на экран.

В большинстве случаев вы хотели бы использовать многопоточный подход, если вы не знакомы с этой темой, это вопрос сам по себе, вот учебник для начинающих вики . По сути, вы хотите, чтобы игровая логика выполнялась в одном потоке, блокируя переменные, к которым она должна обращаться для обеспечения целостности данных. Если ваш логический цикл невероятно быстр (супер-мега-анимированный 3d-понг?), Вы можете попытаться зафиксировать частоту, которую выполняет цикл, засыпая поток на небольшие промежутки времени (на этом форуме для игровых циклов физики было предложено 120 Гц). Одновременно другой поток перерисовывает экран (60 Гц было предложено в других разделах) с обновленными переменными, снова запрашивая блокировку переменных до того, как он получит к ним доступ.

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

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

Надеюсь это поможет :)

[edit] В системах, которые не поддерживают многопоточность, анимация все еще может переходить в цикл рисования, но вы хотите установить состояние таким образом, чтобы сигнализировать логике, что происходит что-то другое, и не продолжать обрабатывать текущий уровень / карта / и т.д ...

Стивен
источник
1
Я не согласен здесь. В большинстве случаев вы не хотите многопоточности, особенно если это маленькая игра.
Коммунистическая утка
@TheCommunistDuck Достаточно справедливо, накладные расходы и сложность для многопоточности могут определенно сделать это излишним, плюс, если игра небольшая, она сможет быстро обновляться.
Стивен