A Kernel Based Game Engine

Although the examples in this article are given in a C like syntax, they probably won't work out of the box. They're more for inspiration than actual working code.

This article explains an alternative approach to the game loop found in most game engines. A typical loop might look something like the following:

do {
    handle_input();
    update_animations();
    update_entities();
    draw_everything();
} while (!finished);

There's nothing wrong with this approach, but it can become unwieldy as time goes by and more things are added. What if you want to make the game support online multiplayer at some point? What about adding new features, such as a debug console? How about screen management, or changing how game objects are rendered?

Kernel-based Engine

The kernel-based game engine is designed to take care of these problems. It does add some overhead to the main loop, but it's still fast enough for most games out there, and the added flexibility really helps further down the line.

This model splits things into two sections: the Kernel object, and a collection of Service objects.

The Kernel takes care of running the loop, and dispatches messages to Service objects.

Each Service object takes care of a single piece of functionality. This can include updating game entities, managing game screens or handling input. Keeping the functionality of services limited helps to make things reusable as time goes on, and prevents things becoming too complex.

Our loop now looks something like this:

do {
    kernel.update();
    kernel.render();
} while (!kernel.isFinished());

Note: The "render" method is optional, but makes things much easier once different update & render priorities come into play. For example, a service may need to render last but be updated before other services update (see Render Method for a more detailed explanation).

The Kernel

A very basic Kernel class may look something like this:

class Kernel {

    private ServiceCollection _services;
    private Map<Class, Service> _serviceLookup;

    public void update();
    public void render();
    public void start();
    public void stop();
    public bool isFinished();

    public void addService(Service service, Class serviceType);
    public void removeService(Class serviceType);
    public void getService(Class serviceType);

}

When a service is added, its class type is also stored. In most cases this will be the same type as the service, but it can be used to extend a service class and then override it. This makes it easier when referencing a service elsewhere.

Other points:

  • The _services field is for iterating over services during update & render loops.
  • _serviceLookup is for looking up services in the add, remove and get methods.
  • update and render are called every loop.

The Game Service

class GameService {

    private int _priority;
    private int _updatePriority;
    private int _renderPriority;

    public void render();
    public void update();
    public void stop();
    public void start();
    public void onSuspend();
    public void onResume();

}

Not every service needs to render or update, so there should be a way of making these methods optional.

An example

Below is the updated version of the game loop from the beginning of the article.

// Create kernel
Kernel kernel = new Kernel();

// Add services
kernel.addService(new InputService)
kernel.addService(new UpdateEntitiesService)
kernel.addService(new RenderingService)

// Start kernel & run loop
kernel.start();
do {
   kernel.update()
   kernel.render()
} while (!kernel.isFinished());

There's not a huge difference, but hopefully you can see the advantages. In theory the same parts of the game could be reused in a server app by skipping the render calls and removing RenderingService.

Important Points

Render Method

The kernel doesn't necessarily need a render method, but it makes things easier to read and rendering is a pretty big part of most games.

Some things also render in a different order to being updated - for example, a debug console must be updated first so that it can reset keyboard events (to stop them being used in other services), but it must be rendered last so it appears on top of everything.

Overhead

Using this approach adds a small amount of overhead. If you desperately need every ounce of performance or memory, then the traditional method works just fine.

Reuse

By splitting things into separate services, it becomes almost trivial to use the same components for different games.

Flexibility

This is perhaps the biggest selling point. Games that require a central server game can remove unnecessary services whilst still reusing core components.

Services can also be enabled & disabled at runtime, so a debug console can be enabled for different builds, and can services can be switched out during program execution (via the debug console).

Example Services

Here are a few services from games under development:

ScreenManagerService

Handles GameScreen objects. This acts as a form of state management, and screens can be pushed onto a stack for menus and overlays.

DebugConsoleService

Handles the debug console. Lets a user enter commands and query things. This service usually has access to the kernel and can request other service objects.

DebugListenerService

This service listens for changes on a specific directory, and fires an event when a file is changed. The ResourceService listens for these events, and can reload assets when something is changed. This is particularly useful when tweaking graphics, as the engine can be running in one windows whilst things are being updated.

ResourcesService

Holds references to resources (images, sounds, animations etc) that can then be accessed inside other parts of the engine.

InputService

Handles user input, either from a game pad or the keyboard.

EventsService

Manages a simple event dispatcher.

ScriptEngineService

Acts as a common execution environment for scriptable objects.

SessionService

Manages gameplay sessions and handles save states.

RendererService

Handles 2D rendering.

SoundService

A single point for playing and sounds.

AmbienceService

Used to manage ambient effects in a game, such as weather or time of day effects. Although this can be done separately via several separate services, this wraps things up in a nicer package.

GameEntityService

Manages component based game entities (which is another large topic).

Post a comment

org-mode tags allowed: /italic/, *bold* and =code=