Game loop, View system, and Scene Management for Lumi2d
First week of Lumi2d development. I set up a mono repo using pnpm workspaces. Different packages will reuse the same configs and libraries, so I think it makes sense to use a mono repo. I have set up some linting and config files and added busted for unit testing. Then, I started working on the actual engine. The source code is at the bottom of the post.
The Game Loop
I added a Game class with a start, update, and draw function. You call start in your main.ts file to start your game. You pass in the design width, height, and the first scene to load. I explain the design size in the view system section below.
In LÖVE, you can override love.run
to get a custom game loop. I use most of the default run loop LÖVE provides. I have taken out the code for drawing and call Game.update
and Game.draw
instead. I also added code to limit the delta time:
if (love.timer !== undefined) {
dt = love.timer.step();
if (dt > MAX_DT) {
dt = MAX_DT;
}
}
MAX_DT
is set to 1.0 / 30
to make sure the delta time never gets too high. When the game goes to the background or minimizes and then comes back into focus, the delta time will be calculated from the last call before the window lost focus. If you don't clamp the delta time, it can cause weird glitches in the game if you use delta time for physics or movement, for example.
View System
When developing a game that needs to support a variety of different resolutions, one of the solutions is to decide on one resolution with a ratio that will fit most screens and then scale that up or down depending on the screen resolution automatically.
This is the solution I'm going with. When starting the game, we pass in a design width and height that will be used as the base view size. It is then scaled based on the screen or window resolution.
The way the scaling is done depends on the scale mode. There are multiple scale modes:
Fit view - fill as much of the screen as possible. The game's top, bottom, or sides can be cut off to make it fit. Keeps the aspect ratio.
Fit width - fill the width of the screen. The top or bottom of the game can be cut off. Keeps the aspect ratio.
Fit height - fill the height of the screen. The sides of the game can be cut off. Keeps the aspect ratio.
No scale - Don't scale the design size. Black bars will be around the view.
Stretch - Stretch the view to fit the screen. Does not keep the aspect ratio.
The scale mode is just a function, so custom scale modes can be used:
/**
* Helper type for the scale function returns.
* Returns [viewWidth, viewHeight, scaleFactorX, scaleFactorY, xOffset, yOffset].
*/
type ScaleModeReturn = LuaMultiReturn<[number, number, number, number, number, number]>;
/** Scale mode type used for every scale mode function */
export type ScaleMode = (
designWidth: number,
designHeight: number,
anchorX: number,
anchorY: number
) => ScaleModeReturn;
The anchor parameters offset the view inside the screen for some scale modes. A scale mode will return x and y offsets based on the anchors as part of the return values. Anchor values should be between 0 and 1.
When the window is resized, the view is updated automatically and to scale to the new size.
Scene Management
Most games will need a way to change between different parts. For example, a menu, a cut scene, and the main game. Lumi2d has a scene manager to handle that. Each part is a scene instance that extends from the Scene class.
When you start the game, you pass the first scene in as a parameter. When you want to switch scenes, you call the functions in the Scenes class to push, pop, or replace a scene.
The scene manager stores the scenes you add in an array used as a stack. The top one is active and receives update and draw calls. If the top scene is an overlay scene, the scene below also gets draw calls, but no update calls. This can be useful for message overlays or pause screens.
The three main scene manager functions are:
Push - Will push a new active scene on top. The previous one will stay in memory.
Pop - Pops the top scene from the top and removes it from memory, activating the one below.
Replace - Replace the top scene with a new Scene and remove it from memory.
Push and pop can be used for a game like Pokemon when you enter a building. You can push the building scene and pop it off when you leave, returning to the outside one. Or when you have a mini-game, for example.
Each scene will have events, tweens, and timers of its own. These are only updated when a scene is on top.
Scene instance
The scene is one of the main parts of the engine when building a game. Entities get added, and it updates and draws everything on the screen. There are a few functions in the base scene that get called in certain events that can be overridden to use them:
Load - Gets called after the scene is created. Set up the scene. Load assets, add entities, set up logic, etc.
Update - Called every frame. Entities are updated here automatically, but other logic can be added here.
LateUpdate - Gets called every frame after the update function. Update entities again after everything in the update function. For example, after the physics update.
Draw - Draws the entities on the screen automatically.
Resize - When the window resizes, this function gets called.
Destroy - Called when a scene is removed from the manager. Asset cleanup can happen here, for example.
Delta time and Time scale
Delta time is the time that has passed since the last update. This can be used to make the game run at the same speed if the FPS is lower or higher than expected. Delta time is passed as dt
in the update function of the scene. It is in seconds. There is a TimeStep class that handles updating the delta time.
One of the fields in the TimeStep class is timeScale
. This is 1.0
by default. If you make this higher, you can speed the game up. When you make it lower, you can slow the game down. Useful for slow-motion effects, for example.
Next steps
Next up are the event system and user input.
Source code
Subscribe to my newsletter
Read articles from Juriën Meerlo directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Juriën Meerlo
Juriën Meerlo
I'm a senior software engineer using TypeScript, C#, and Python in web and application development. As a hobby, I make games using custom game engines. You can find my latest game Gravity Golfing on iOS. I'm Working on and posting about my new engine called Lilo2d.