Posted by & filed under Game Development.

I am happy to hit the next milestone in development of the Zerk game engine. With version 0.2.0 Zerk is based on an Entity-Component-System architecture.

ecs_key_256

In the first run I created a game engine prototype with the basic features to run a jump and run game. Now I have shaped this prototype into an Entity-Component-System (ECS) architecture.
I wont talk about ECS architectures in general in this post. There are plenty of good explanations in the web (see links at the end of the post). I will talk about the implementation details in case of the Zerk game engine. Before i go into detail lets look at the reasons that made up my decision to use an ECS architecture:

Composition over inheritance
With an ECS architecture entities are build through composition instead of inheritance. Inheritance can be limiting to game design. Composition offers the flexibility needed for proper recombination of tons of game logic.
Data driven design
When using an ECS architecture all logic is in the systems. The entities are only data and can be stored in JSON files. That hides the code from the entity developer as long as only existing properties/behavior (components) are recombined. Entities can be created inside a world editor.
Separation of concerns
The classic entity is split into entity, components and systems. In general ECS architectures have a high object granularity. Smaller objects for better re-usability and separation of responsibilities.
Performance
When most things are data there is more potential for performance optimization. Software that uses an ECS architecture is often threaded as high performance software.
Concurrency
The separation of game logic into systems offers a layer to control the execution in detail. Systems can run in different threads. Simulation and rendering can be separated. Specific systems can run in threads with other frequencies.

Engine

The engine object holds everything together. It has two main responsibilities:

  • Manage a collection of systems
  • Manage a collection of entities

Once the engine is started systems and entities can be added. This is done manually or by loading a world. The engine iterates over the registered systems and updates them. All that remains in the classic game loop is the iteration over the systems. The engine itself does not contain any game code. Its only a system and entity player. Everything, including the viewport is gone into the systems. The engine would not even render a black screen without the viewport system.

Systems

The code of the classic game loop is divided into the systems. Each system is a unique piece of code to implement a certain game logic. Systems have a process like interface:

  • start
  • stop
  • update

Systems and their descendant libraries are the only way to extend game logic. Systems may use inheritance. The update order of the systems is managed with priorities. Systems communicate with each other by calling public methods and registering event handlers.

In case of the web browser requestAnimationFrame has to be used for rendering while setTimeout has to be used for the game loop. The engine handles that by threads in which the systems run. All visual systems run in the render thread which gets updated by requestAnimationFrame. When the browser window/tab is not visible requestAnimationFrame stops firing while the simulation thread is independent updated in setTimeout. Removing all visual systems makes the engine run invisible even without the need of a canvas element. That comes in handy when putting things under tests or when creating a server side simulation for multiplayer purposes.

Here is a list of systems that build the heart of the engine. In most cases other systems are build around the functionality of one or more of these systems.

Name Thread Description
control simulation Interface to human input devices
message render Render messages on the display
physics simulation Physics simulation (Box2d)
sprite render Renders sprites attached to entities
viewport render Interface for visualization
wireframe render Render the world as wireframes for debugging purposes

This is a list of other systems that I used for the implementation of my jump and run demo. These systems are related to concrete game implementations, but the functionality that they offer is so basic that they are contained in the engine package.

Name Thread Description
damager simulation Damages or destroys other entities on contact
elevator simulation Provides elevator like movement for entities
fall simulation Enables entities to start falling on touch
player simulation Provides player controls for jump and run style games
trigger simulation Enables entities to act as trigger area

This is how a simple system looks like. The “fall” system adds the behaviour to an entity that it starts falling when it gets in touch with another entity.

zerk.define({

	name: 'zerk.game.engine.system.fall',
	extend: 'zerk.game.engine.system'

},{

	// Name of the system
	_name: 'fall',

	// Thread that runs this system
	_thread: 'simulation',

	// Physics system instance
	_physics: null,

	// Class constructor
	init: function(engine,config) {

		zerk.parent('zerk.game.engine.system.fall').init.apply(
			this,
			arguments
		);

		this._physics=this._getSystem('physics');

	},

	// Returns true when the system is interested in given component
	useComponent: function(name) {

		return (name=='fall');

	},

	// Starts the system
	start: function() {

		zerk.parent('zerk.game.engine.system.fall').start.apply(
			this,
			arguments
		);

		this._physics.on(
			'contactbegin',
			this._onContactBegin,
			this
		);

	},

	// Stops the system
	stop: function() {

		zerk.parent('zerk.game.engine.system.fall').stop.apply(
			this,
			arguments
		);

		this._physics.un(
			'contactbegin',
			this._onContactBegin
		);

	},

	// Fires when two fixtures collide
	_onContactBegin: function(source,target) {

		if (!zerk.isDefined(target.entity.components.fall)) {
			return true;
		}

		var self=this;

		window.setTimeout(
			function() {

				self._physics.setBodyMoveable(target.entity,'main',true);

			},
			target.entity.components.fall.releaseDelay
		);

	}

});

Entities

Every object in the game world is an entity or a descendant of an entity. Entities can be created (spawned) from entity definitions or composed at run-time. An entity is an identifier and a list of components with specific configurations. The assignments of components gives the entity its properties/behaviours. Entities only store state data and have no methods. Entity definitions are identified by unique names. Entity instances are identified by autogenerated ids and custom tags.

List of entities that i created for the jump and run demo:

Name Description
balancePlatform Platform that can rotate
box Resizeable box shaped object
damager Resizeable area that does damage to passing entities
elevatorPlatform Elevator like moving platform
fallingPlatform Platform that starts falling when getting in touch with other entities
groundDown Polygon shaped ground
madStone Falling stone that kills the player on touch
player Character entity to be controlled by player
trigger Resizeable area that acts as trigger zone

This matrix shows the assignments of components to some of the entities used in the jump and run demo:

position physics sprite elevator fall player madStone damager
box      
elevatorPlatform        
fallingPlatform        
player        
madStone        
damager        

Example entity data:

{
	name: 'player',
	components: {
		position: {},
		player: {
			enableControl: false
		},
		physics: {
			bodies: {
				main: {
					x: 0,
					y: 0,
					width: 1,
					height: 2,
					angle: 0,
					moveable: true,
					origin: true,
					fixedRotation: true,
					fixtures: {
						torso: {
							shape: 'box',
							x: 0,
							y: 0,
							angle: 0,
							width: 1,
							height: 2,
							friction: 0.2
						},
						foot: {
							shape: 'box',
							x: 0,
							y: 1,
							angle: 0,
							width: 0.9,
							height: 0.25,
							isSensor: true
						}
					}
				}
			}
		},
		sprite: {
			/* Not implemented yet */
		}
	}
}

Components

The entity state data is divided into small pieces, the components. Each component has a minimal set of data needed for a specific purpose. A component is represented by a class that holds the logic to build a component state from a configuration. A component class validates a given configuration and creates run-time data structures. At run-time a component is only a dead data object without methods. Its data is used by one or more systems.

Example component code:

zerk.define({

	name: 'zerk.game.engine.component.fall',
	extend: 'zerk.game.engine.component'

},{

	_name: 'fall',

	build: function(entityConfig,worldConfig) {

		var defaultConfig={
			releaseDelay: 500
		};

		// Create new state
		var state={};

		// Apply default configuration
		zerk.apply(
			state,
			defaultConfig
		);

		// Apply enitity configuration
		zerk.apply(
			state,
			entityConfig
		);

		// Apply world configuration
		zerk.apply(
			state,
			worldConfig
		);

		return state;

	}

});

Worlds

With worlds everything comes together. Worlds are pre-configurations of systems and entities. A world can store maps and savegames. In general a world contains a game state.

Example of a world, containing only the player entity and a ground:

{
	name: 'level1',
	config: {
		systems: {
			physics: {
				gravityX: 0,
				gravityY: 40
			}
		}
	},
	entities: [
		{
			name: 'jumpandrun.entity.player',
			tags: [
				'player'
			],
			components: {
				position: {
					x: 0,
					y: 0
				}
			}
		},
		{
			name: 'jumpandrun.entity.box',
			components: {
				position: {
					x: 0,
					y: 2
				},
				physics: {
					bodies: {
						main: {
							width: 15,
							height: 1
						}
					}
				}
			}
		}
	]
}

Changelog

Here is a list of all changes:

  • Entity-Component-System architecture
  • Data driven entities and worlds (JSON)
  • Entity ids and tags
  • Rendering backend abstraction (first step to use WebGL)
  • Physics engine abstraction (Another physics engine than Box2D can be used)
  • Wireframe renderer system (Seperate wireframe renderer system)
  • Advanced error handling
  • Advanced log system
  • Game configuration file (JSON)
  • Refactored helper and cross browser functions
  • JSON resource loader
  • Sandbox and procedural generation demo

Next steps

The implementation of the ECS architecture felt very right to me and was fun to do. When it comes to reusability and data driven design its really worth the effort. After all this the engine is still in state of a prototype. There are dozens of changes under the hut but its not possible to create real games right now. The biggest issue is that graphics are still missing. That’s the next thing iam working on.

Related Links

Leave a Reply

  • (will not be published)