Предисловие


В настоящее время я работаю над собственным игровым движком. С использованием минимального количества сторонних библиотек, после реализации игрового цикла (game loop), отрисовки кадра, функции «update», загрузки текстур и пр., основная «начинка» движка была готова. Пришло время реализации еще одной важной составляющей — сцены (scene).

Введение


В статье я предполагаю, что движок уже оснащен игровым циклом с «callback»-функциями. Весь код будет написан на Java, но может быть легко перенесен на любой другой язык, поддерживающий garbage collection. Что-ж, приступим.

Что уже есть


Как упоминалось ранее, мы уже располагаем игровым циклом. Пусть он выглядит примерно так:

void awake() {
    RenderUtil.init(); // настройка параметров OpenGL
    run();
}

void run() {
    // game loop
    // ...
    // прочитать input, обновить frame rate и т. п.
    //
    if (Window.isCloseRequested()) {   // если игру закрыли
        stop();
        return;
    }
    update();
    render();
}

Реализацию методов render() и stop() я приведу чуть позже.

Определяем сцену


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

Что я подразумеваю под игровым объектом? Это объект, реализующий «callback»-функции игрового цикла. Для тех, кто знаком с Unity3D: аналогия с объектом, класс которого реализует MonoBehaviour.

Пусть этот самый игровой объект представляется интерфейсом (или абстрактным классом — в зависимости от нужного функционала), который мы назовем GameListener (опять же, в данной статье я подразумеваю, что этот класс уже так или иначе реализован).
Примитивная реализация интерфейса может выглядить следующим образом:

public interface GameListener {
    void start() ;     // вызывается, когда игра началась
    void update();  // вызывается каждый фрейм
    void draw();     // аналогично update()
    void destroy(); // вызывается, когда объект "отключается"
}

Количество функций зависит от нужной степени контроля, например, у Unity3D их достаточно много.

Реализуем класс Scene


После определения архитектуры и строения нашей сцены, можно наконец-таки приступить к её реализации.

public abstract class Scene implements GameListener {

    ArrayList<GameListener> gameListeners = new ArrayList<>();

    public abstract void initializeScene();

    public final void AddToScene(GameListener gameListener) {
        gameListeners.add(gameListener);
    }

    public final void onInitializeScene() {
        if (gameListeners.isEmpty())
            initializeScene();
    }

    @Override
    public final void start() {
        for (GameListener gameListener : gameListeners)
            gameListener.start();
    }
    
    @Override
    public final void update() {
        for (GameListener gameListener : gameListeners)
            gameListener.update();
    }

    @Override
    public final void draw() {
        for (GameListener gameListener : gameListeners)
            gameListener.draw();
    }
    
    public final void onDestroy() {
        for (GameListener gameListener : gameListeners)
            gameListener.destroy();

        gameListeners.clear();
    }
    
    @Override
    public final void destroy() {}

Комментарии я распишу по пунктам:

  • Наших игровых объектов сцена хранит в коллекции ArrayList
  • Метод initializeScene() — абстрактный. В нём мы будем добавлять игровых объектов в сцену, используя метод AddToScene() в нашем конкретном классе-сцене;
  • Метод onDestroy() мы будем вызывать после смены/перезапуска сцены или закрытия игры. В нём мы очищаем сцену от игровых объектов, garbage collector позаботится об остальном (можно намекнуть JVM провести очистку, вызвав System.gc());

Стоит заметить, что все методы (кроме initializeScene() естественно) помечены ключевым словом final, таким образом, в классе Scene пользователь движка может только добавить своих игровых объектов (такое ограничение пока вполне меня устраивает).

Преобразования в игровом цикле


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

Scene runningScene;

void awake() {
    RenderUtil.init();
    runningScene = SceneManager.getScene(0);
    run();
}

void run() {
    if (Window.isCloseRequested()) { 
        stop();
        return;
    }
    runningScene.update();
    runningScene.render();
}

void stop() { runningScene.onDestroy(); }

void render() {  runningScene.draw(); }

Все наши созданные сцены мы можем добавить в массив, содержащийся, к примеру, в каком-нибудь классе, под названием SceneManager. Тогда он будет выступать в качестве контроллера нашей системой сцен, представляя методы getScene(), setScene() и т. п.

На данном этапе реализация системы очень сильно напоминает паттерн «Состояние». Так оно и есть.

Смена сцен


Для смены сцен мы можем определить аналогичный экземпляр класса Scene в SceneManager:

private static Scene currentScene;

Далее напишем setter setCurrentScene(Scene):

public static void setCurrentScene(Scene scene) { currentScene = scene; }

Тогда в игровом цикле мы сравниваем runningScene с currentScene и, если они не совпадают, меняем сцену:

void run() {
    if (Window.isCloseRequested()) { 
        stop();
        return;
    }
    runningScene.update();
    if (runningScene != SceneManager.getCurrentScene()) {
       runningScene.onDestroy();
       runningScene = SceneManager.getCurrentScene();
    }
    runningScene.render();
}

Важно не забыть вызвать метод onDestroy() текущей сцены для удаления её игровых объектов.

Реализация additive loading


В той же Unity3D есть возможность «аддитивной» загрузки сцен. При таком методе объекты «старой» сцены не удаляются (в нашем случае — не вызывается метод onDestroy()), а новая сцена загружается «поверх» старой.

Этого можно достичь, например, создав контейнер, хранящий список загруженных аддитивно сцен. Тогда наряду с вызовом

runningScene.update();

нужно будет сказать что-то типа

for (Scene additive : additives) additive.update();

и так далее.

Вызвать onDestroy() придется в случае перезагрузки/смены основной сцены (runningScene) или закрытия игры.

Архитектура, процедура добавления игровых объектов и самой сцены остаются такими же.

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


  1. Zoolander
    03.12.2015 10:22

    Спасибо за статью.

    Хотелось бы вкратце узнать, чем обосновано решение «строить свой движок», а не «расширять уже имеющийся» — к примеру, Flixel-GDX + libGDX?

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


    1. ibe
      03.12.2015 15:40

      Спасибо.
      Мною движет желание разобраться в самой архитектуре игр, на низком уровне. Отсюда начинается конкретное понимание, как работают уже готовые фреймворки.