Introducing strongforce: an infrastructure for game engines

The strongforce library is a light-weight framework to build game engines. It provides a backbone for a powerful game loop and helper classes for efficiently implement new game components such as models, renders and simulators. Main framework features include:

  •  Model oriented.
  •  Decoupled simulation and render loops.
  •  Real time simulation loop for integration with physics engines.
  •  Separated read and write stages during simulation to avoid expensive model copies.
  •  Versatile and dynamic visitor pattern to implement the Game Model Hierarchy.
  •  Use of JavaScript functors to quickly implement simple renders or simulators.
  •  Helper classes to ease implementation of Renders and Simulators.
  •  Events for Model to allow asynchronous programming.
  •  Fully control of the loop step and execution.
  •  Careful separation of responsibilities allowing the developer to focus on data model, business logic and rendering process separately.

Strongforce is not a graphic engine, not even a game engine. It is only a proposal for a game architecture and a main loop. You can read more about the framework internals and philosophy of design in the wiki pages of the repository or further in this post. Please feel free to provide any feedback by opening new issues on GitHub.

The strong interaction or strong force is the physical force keeping quarks together in order to form hadrons.

Hadron was the name of a full HTML5 isometric game engine I started around a year ago. As the project was too big, I decided to split it into several smaller projects. What I realized was the main loop of Hadron and the basic infrastructure suited very well for each of these little cases so I preferred to isolate this specific functionality.

So the framework keeping Hadron engine’s and all the side projects’ infrastructure was called after the mentioned phenomenon.

As the lead developer, when I was designing the engine, my main focus was to provide an agile development cycle where more than one team could work simultaneously on the different aspects of the game. Specifically, one team could focus on designing the game abstractions working side by side with game designers while others would implement business logic and rendering systems in parallel. If suddenly we choose to change the graphic engine or game rules, these changes should only affect specific and contained parts of the source code.

The data model

From strongforce perspective, a game is no more than a state being simulated and rendered. The state holds the properties of the fictional world where the game unfolds, the render reads the model and realizes it in a physical medium and the simulator alters the state through time applying the business logic, i.e. the game rules. You can see an overview of the split of responsibilities in the presentation I gave to the development team. Notice there were no separation between Hadron and strongforce entities at that moment.

There is only one model per loop but this model can be arbitrary complex. Strongforce assumes a graph-like structure and the loop traverses the nodes in pre-order. For such end, the base class Model implements a traverse() visitor method which receives a method name and pass through the model calling that method on the parent node and its descendants. The specified method is called twice, once before visiting children nodes and once again after all children have been visited. First and second calls are designated as pre-call and post-call respectively and are distinguished by a flag argument passed to the function call.

Suppose you call traverse() for the node F with ‘render’ as the method to traverse with. The list of calls is as follows: F, B, A, A’, D, C, C’, E, E’ D’, B’, G, I, H, H’, I’, G’, F’ where prime denotes post-call invocations.

The game loop uses traverse() to recursively call simulate() or render() on the model and its children. Lists of children models are not meant to be JavaScript arrays but objects implementing Array.prototype.forEach() signature so the lists can be as dynamic as required. In the same way, simulate() and render() are not intended to be implemented by JavaScript simple functions. Instead of simply call each method from the model, traverse() uses the function’s method apply() so any object implementing the signature of Function.prototype.apply() can be used instead of a real function. This object pretending to be a function is what is called a functor object.

This allows us to think about renders or simulators as complete and separated objects / classes with its own state. The model is split into three responsibilities following the original design: a state, a simulator and a render. In terms of responsibility and inside strongforce, each is called a facet. The game loop reveals one facet or another by executing the proper functor in its respective stage.

The execution model

Although this design is suitable for any game loop, after much reading I choose the one proposed by the article Fix your TimeStep! at gafferongames.com. It allows to decouple the render and simulation loops allowing as many FPS as possible while keeping a constant rate for simulation. Nevertheless, current implementation of strongforce uses requestAnimationFrame() to schedule the next game step so FPS are clamped to 60.

Taking advantage of JavaScript closures and first citizen functions, each time the model is simulated, a function to schedule an update on the model is provided allowing the simulator first read the model and delay changes after all the traverse is complete.

And last but not least, due to the asynchronous nature of JavaScript, models are able to dispatch events for allowing renders or simulators to react upon asynchronous causes. These events are marked with a a timestamps for synchronization reasons and the associated listeners are executed synchronously when the event happens. Asynchronous events have usually immediate effects over the model like the apparition of a force or the discrete displacement of an actor and they have not noticeable impact on the synchronous game loop if it is running at the expected high rates.

Conclusions

For me, most important thing about this design is that it provides two independent dimensions of the single responsibility principle. In one hand, there is a vertical split aimed to provide a top-down definition at the level of which actors are involved in the game model (for instance a world with objects, the hero party with the hero characters and enemy patrols with the enemies themselves) while in the other hand, there is a horizontal split to provide the specific behaviors for such actors while simulating or rendering (how the enemies act or are rendered? how the hero react to user input or is rendered? …).

The split allows the team to simultaneously evolve the number of entities of the game while refining a specific render and simulation aspects in a predictable and regular, OO-friendly and dynamic way.

A note about performance

In order to keep the illusion of fluid animations, the complete game loop can take no more than 16.6 ms. Profiling current overhead introduced by the loop and model traverse reveals that no more than the 4% of time is spend in the game loop.

Considering the game step as the 100% of the execution, notice the low overhead introduced by the game step itself and the traverse steps.
Considering the game step as the 100% of the execution, notice the low overhead introduced by the game step itself and the traverse steps.

Of course, traversing the model is directly dependent on the model complexity (nesting level) itself.

The flame chart reveals the low impact of the overhead introduced by Hadron-Loop.
The flame chart reveals the low impact of the overhead introduced by Hadron-Loop.

Hope I will be able to improve performance and provide even less overhead. Sure there is a lot of room for improvement!

And that’s all. Along this post I talked about the motivation, internals and design choices of one of the more ambitious projects I’m developing right now. Hope you enjoyed and learn. Remember you can use comments to provide as much feedback and opinion as you want.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s