Предисловие
В настоящее время я работаю над собственным игровым движком. С использованием минимального количества сторонних библиотек, после реализации игрового цикла (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) или закрытия игры.
Архитектура, процедура добавления игровых объектов и самой сцены остаются такими же.
Zoolander
Спасибо за статью.
Хотелось бы вкратце узнать, чем обосновано решение «строить свой движок», а не «расширять уже имеющийся» — к примеру, Flixel-GDX + libGDX?
Просто это сложный путь, его тяжело будет пройти в одиночку. В любом случае, я желаю вам удачи и жду следующей статьи, как интересующийся разработкой игр на Java
ibe
Спасибо.
Мною движет желание разобраться в самой архитектуре игр, на низком уровне. Отсюда начинается конкретное понимание, как работают уже готовые фреймворки.