Skip to content

Instantly share code, notes, and snippets.

@JAForbes
Last active January 13, 2021 00:06
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save JAForbes/99c15c0995b87a22b95a to your computer and use it in GitHub Desktop.
Save JAForbes/99c15c0995b87a22b95a to your computer and use it in GitHub Desktop.
Data Driven Entity Component System

Entities and Components

An entity is just a number.

There is a graph of components, indexed by the entity id.

All state is stored in one place.

var components = { Position: {}, Velocity:{}, Spite: {}, Timer: {}, ScreenShake: {}, RemoveComponents: {} }

var player = 1
var enemy = 2
var potplants = [3,4,5]
var game = 6

components.Position[player] = { x:100, y: 100 }
components.Velocity[player] = { x: 0.5, y: 0 }
components.Sprite[player] = sprites.player

components.Position[enemy] = { x: 0, y: 100}
components.Velocity[enemy] = { x: 1, y: 0}
components.Sprite[enemy] = sprites.enemy

potplants.forEach(function(potplant, i){
   components.Position[potplant] = { x: i * 100, y: 200 }
   components.Sprite[potplant] = sprites.potplant
})

components.Timer[6] = { count: 0, interval: 1000, on: { ScreenShake: { x: 1, y: 0 } } }

Systems iterate over a specific component type. If that component type is empty for that game loop, those systems do not run.

This is very different to the traditional iteration over all entities, which is wasteful and grounded in thinking every entity is an object instead of just an identifier.

A potplant never moves, so it just has a Position and a Sprite. Because it has no Velocity component the code that handles Movement will never be activated for those entities.

Also notice the game entity has no Position or Sprite. It is just an identifier with some associated data. In this case it has a Timer component that will make the screen shake every 1000 milliseconds. There are no callbacks, or event listeners, it is just an object literal with some numbers in it.

//look ma, no callbacks
components.Timer[6] = { count: 0, interval: 1000, on: { ScreenShake: { x: 1, y: 0 } } }

Systems

//gameloop systems

[
   [Move,  components.Velocity]
   [Timer, components.Timer],
   [Screenshake, components.Screenshake],
   [Render, components.Sprite],
   [RemoveComponents, components.RemoveComponents],
].forEach(callSystem)

function callSystem([system, components]){
    _.each(components, system)
}

function Move(velocity, entity){
   var p = component.Position[entity]
   p.x += velocity.x
   p.y += velocity.y
}

function Render(sprite, entity){
   var p = component.Position[entity]
   context.drawImage(p.x, p.y, ..., sprite.img )
}

function Timer(timer, entity){
   if(timer.count > timer.timer){
       if(timer.repeat){
           timer.count = 0
       } else {
           components.RemoveComponents[entity] = { components: ['Timer']  }
       }
       
       
       _.each(components.on, function(data, name){
            components[name] = data
       })

   }
   timer.count ++
}

function ScreenShake(screenshake, entity){
    //shake the screen
    components.RemoveComponents[entity] = { components: ['ScreenShake']}
}

function RemoveComponents(remove, entity){
    remove.components.forEach(function(){
        delete components.ScreenShake[entity]
    })
}

Each system accepts a single component and the entity id for that component. The id allows the system to pull in other components for that entity.

function Move(velocity, entity){
   var p = component.Position[entity]
   p.x += velocity.x
   p.y += velocity.y
}

See how the Move system pulls in the position data by accessing the component graph directly. Because all references are local to a function call, deleting a component from the graph will immediately release it into memory unlike class/prototype based approaches.

Also notice how focused each system is. You could easily imaginee reusing any of these systems across many applications. But you get the banana without the gorilla and the rest of the jungle.

State Graph

Because all your state is stored in one place, you can easily implement quicksave/quickload functionality.

QuickSave(quicksave, entity){
   components.SaveState[entity] = JSON.stringify(components)
}

QuickLoad(quickload, entity){
   components = JSON.parse(components.SaveSate[entity])
}

Live programming

You can also interact with your game live in a REPL with ease. By modifiying the state you can test elaborate scenarios. It's trivial to clone an entity n times, or adjust the strength of a particular force and see the results immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment