
Creating a scene system for the game engine
Foreword
I am currently working on my own game engine. Using the minimum number of third-party libraries, after the implementation of the game loop (game loop), rendering the frame, the “update” function, loading textures, etc., the main “filling” of the engine was ready. It's time to implement another important component - the scene (scene).
Introduction
In the article, I assume that the engine is already equipped with a game loop with “callback” functions. All code will be written in Java, but can be easily ported to any other language that supports the garbage collection . Well, let's get started.
What is already there
As mentioned earlier, we already have a game loop. Let it look something like this:
void awake() {
RenderUtil.init(); // настройка параметров OpenGL
run();
}
void run() {
// game loop
// ...
// прочитать input, обновить frame rate и т. п.
//
if (Window.isCloseRequested()) { // если игру закрыли
stop();
return;
}
update();
render();
}
The implementation of the render () and stop () methods will be given a bit later.
Define the scene
Before you start writing a class for a scene, you need to decide what it will be. In my case, this is an object that includes many game objects. Take a break from reading for a second and look around you: we will call everything that you see (almost) game objects, and where you are - a scene.
What do I mean by a game object? This is an object that implements the "callback" function of the game loop. For those familiar with Unity3D : an analogy with an object whose class implements MonoBehaviour.
Let this same game object be represented by an interface (or an abstract class, depending on the desired functionality), which we will call GameListener (again, in this article I mean that this class is already implemented one way or another).
A primitive interface implementation may look like this:
public interface GameListener {
void start() ; // вызывается, когда игра началась
void update(); // вызывается каждый фрейм
void draw(); // аналогично update()
void destroy(); // вызывается, когда объект "отключается"
}
The number of functions depends on the desired degree of control, for example, Unity3D has a lot of them .
We implement the class Scene
After defining the architecture and structure of our scene, we can finally proceed to its implementation.
public abstract class Scene implements GameListener {
ArrayList 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() {}
I will write comments on the items:
- The scene stores our game objects in the ArrayList collection.
- The initializeScene () method is abstract. In it, we will add game objects to the scene using the AddToScene () method in our particular scene class;
- We will call the onDestroy () method after changing / restarting the scene or closing the game. In it, we clear the scene of game objects, the garbage collector takes care of the rest (you can hint the JVM to clean it up by calling System.gc () );
It is worth noting that all methods (except for initializeScene () naturally) are marked with the final keyword , so in the Scene class the user of the engine can only add their game objects (this restriction suits me so far).
Transformations in the game cycle
Now you need to carry out the transformation in the game loop. All of them are, in fact, intuitive.
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(); }
We can add all of our created scenes to an array contained, for example, in some class called SceneManager . Then it will act as a controller by our scene system, presenting the getScene () , setScene () , etc. methods .
At this stage, the implementation of the system very much resembles the “State” pattern . The way it is.
Scene change
To change scenes, we can define a similar instance of the Scene class in SceneManager: Next, write setter setCurrentScene (Scene): Then in the game loop we compare runningScene with currentScene and, if they do not match, change the scene:
private static Scene currentScene;
public static void setCurrentScene(Scene scene) { currentScene = scene; }
void run() {
if (Window.isCloseRequested()) {
stop();
return;
}
runningScene.update();
if (runningScene != SceneManager.getCurrentScene()) {
runningScene.onDestroy();
runningScene = SceneManager.getCurrentScene();
}
runningScene.render();
}
It is important not to forget to call the onDestroy () method of the current scene to delete its game objects.
Implementation of additive loading
In the same Unity3D there is the possibility of "additive" loading scenes. With this method, objects of the "old" scene are not deleted (in our case, the onDestroy () method is not called ), and the new scene is loaded "on top" of the old one.
This can be achieved, for example, by creating a container that stores a list of additively loaded scenes. Then, along with the call, you will need to say something like and so on. Cause onDestroy () is necessary in the event of a reboot / change the main scene (runningScene) or closing the game. The architecture, the procedure for adding game objects and the scene itself remain the same.
runningScene.update();
for (Scene additive : additives)
additive.update();