Systemless Components with Excalibur.js
Excalibur.js is a Typescript 2D game engine for the web. It is designed using an ECS architecture, but that doesn’t mean your game has to be too.
I’d like to share an alternative pattern to ECS that I’ve found to be very helpful in my personal game development. The specifics and code examples will be particular to Excalibur but the concepts should apply to any game engine, so even if you have not used Excalibur before I hope this can still be useful for you.
What is ECS?
ECS (Entity Component System) is a design pattern consisting of “entities” and “components” which are then iterated over with a “system” every game loop. It’s often used because of its compositional approach (as opposed to inheritance) and performance benefits (sometimes).
A basic implementation of ECS might look like this:
// entity
class Entity {
components = []
}
// components
class PositionComponent {
x = 0
y = 0
constructor(x, y) {
this.x = x
this.y = y
}
}
class GraphicsComponent {
image = null
constructor(image) {
this.image = image
}
}
// system
class GraphicsSystem {
update(entities) {
for (const entity of entities) {
// find the component's instance on the entity
const position = entity.components.find(
(c) => c instanceof PositionComponent
)
const graphics = entity.componentes.find(
(c) => c instanceof GraphicsComponent
)
// only act on entities that have this component
if (position && graphics) {
this.draw(position.x, position.y, graphics.image)
}
}
}
draw(x, y, image) {
/* some code to draw to a canvas */
}
}
// usage
class Player extends Entity {
constructor(x, y) {
this.components.push(new PositionComponent(x, y))
this.components.push(
new GraphicsComponent(new Image('/images/my-sprite.png'))
)
}
}
const entities = [new Player(100, 100)]
const graphicsSystem = new GraphicsSystem()
function gameLoop() {
graphicsSystem.update(entities)
}
Here we have a “PositionComponent” and a “GraphicsComponent”. Our Player entity is given both components to represent its current position in the world and the graphic to be drawn on canvas. The “GraphicsSystem” runs every game loop, finds all entities with both components, and uses them to draw on a graphic the canvas at its position.
For additional game logic the same philosophy would be applied (physics, collision, movement, etc). Each system would run one a time, effectively batching the logic by component type.
function gameLoop() {
// process all movement on entities
motionSystem.update(entities)
// solve any potential collisions as a result of the movement
collisionSystem.update(entities)
// draw to canvas
graphicsSystem.update(entities)
}
You’ll often hear that ECS provides performance benefits and this is the reason why. Without going into too much detail, the essence of it is because you’re processing one system at a time you can make better presumptions on memory allocation, CPU caching, and whatnot. Unfortunately Javascript manages all of that for you so these benefits are largely lost (or at least harder to realize). Something to keep in mind when evaluating ECS.
An entire explanation of ECS could be its own blog post so I hope this suffices for now!
Excalibur and ECS
Excalibur uses ECS internally to run the core functionality of the engine. It has components and systems for physics, collisions, drawing, amongst others. You really don’t need to be concerned with this if you don’t want to. The Actor class – which is what most people use as the basis for their game objects – is just a preset entity with components. It actually fits pretty well into inheritance, so if you wanted to follow OOP patterns it’s a great basis to do that and totally fine to do.
class Animal extends ex.Actor {}
class Dog extends Animal {}
Breaking the rules
When I first heard about ECS it made so much sense on paper. But when it came to using it in practice, it just didn’t jive with me. Implementing a general system like movement made sense, but implementing smaller, more specific logic was cumbersome. It felt like I was splitting simple logic into components and systems for the sake of following rules and not for the benefits it supposedly offered.
Bob Nystrom has a really good talk called “Is There More to Game Architecture than ECS”? If you have the time, I highly recommend watching it before continuing. It’s one of my all-time favourite game dev resources. After watching it for the first time it was a total “aha!” moment for me (specifically at 10:30 - 12:45). I realized that I don’t have to follow the rules and I can tailor patterns like this to my own use case if it’s working for me. This is how I landed on a pattern that I’m calling “Systemless Components” (I’m sure I’m not the first person to discover this concept but I had to call it something for this post).
Systemless Components
The idea behind Systemless Components is that components have no system counterparts and are able to run update logic themselves. This allows you to keep the logic for the component all in one spot, at the cost of any benefits gained by using system (which, as outlined earlier, are usually minimal in Javascript).
Health component example
Here is a demonstration for a Health component and a Death system using traditional ECS. An entity can have a Health component with a value, and the Death system will check if that value falls below 0. When it does, it kills the entity (removes them from the game).
class HealthComponent {
constructor(value) {
this.value = value
}
}
class DeathSystem {
update(entities) {
for (const entity of entities) {
// excalibur entity's have a get(ComponentType) method
const health = entities.get(HealthComponent)
if (health && health.value <= 0) {
entity.kill()
}
}
}
}
class Player {
constructor() {
super()
this.addComponent(new HealthComponent(10))
}
}
It’s a little bit cumbersome for a simple feature isn’t it? If I were doing this on an Actor without components, it would look like this:
class Player extends ex.Actor {
health = 10
onPostUpdate() {
if (this.health < 0) {
this.kill()
}
}
}
It feels much simpler this way. However if I want to re-use this health logic I would need to create a base class to extend from, which would be fine… but I’d prefer to have a fully decoupled health feature. This way it could be added to any entity in the future without worrying about extending the right base class.
Here’s how it would look as a Systemless Component:
class HealthComponent extends ex.Component {
constructor(value) {
this.value = value
}
// called when the component is added to the entity
onAdd(owner) {
// setup an event listener for the postupdate event
owner.on('postupdate', this.onPostUpdate.bind(this))
}
onPostUpdate() {
if (this.value < 10) {
this.owner.kill()
}
}
}
class Player extends ex.Actor {
constructor() {
super()
this.addComponent(new HealthComponent(10))
}
}
This is much nicer, in my opinion. Components have access to their owner (entity) when added, so we can use that to listen to events on the entity. It’s essentially just moving logic out of an entity class and into a component, but by doing that we gain the benefits of composition.
Bounce component example
We’ve added logic to the entity’s update event, but what if we wanted to add some physics functionality? Let’s create a component that will make an entity bounce off the ground:
class BounceComponent extends ex.Component {
onAdd(owner) {
owner.on('precollision', this.onPreCollision.bind(this))
}
onPreCollision(self, other) {
// get the ex.MotionComponent instance from the entity
const motion = this.owner.get(ex.MotionComponent)
if (motion) {
motion.vel = motion.vel.negate().scale(0.5)
}
}
}
class Ball extends ex.Actor {
constructor(args) {
super({
...args,
radius: 40,
color: ex.Color.Green,
collisionType: ex.CollisionType.Active
})
this.addComponent(new BounceComponent())
}
}
I used an Actor in the above example, but this would work for any entity that contains an ex.MotionComponent
and ex.ColliderComponent
(Actors come preconfigured with both). I could have accessed velocity from owner.vel
(because Actor’s expose vel
as an alias to the motion component’s velocity
), but then this component would only work on Actors. By using this.owner.get()
to get the motion component, we keep this component agnostic of what the owner is other than being an instance of Entity.
Composing
Things really start coming together once you have a few components and begin mixing and matching. While I was working on the Jelly Jumper sample game, I made these two components:
OneWayCollisionComponent
Provides capability to be a “one-way” platform, allowing entity’s to pass through it from below. Common for 2D platformers.
class OneWayCollisionComponent extends ex.Component {
onAdd(owner) {
owner.on('precollision', this.onPreCollisionResolve.bind(this))
}
onPreCollisionResolve({ other }) {
const otherMotion = other.get(ex.MotionComponent)
if (otherMotion && otherMotion.vel.y < 0) {
// prevents the collision from happening
contact.cancel()
}
}
}
CarrierComponent
Provides capability for a moving platform to carry entities on top of it.
(Without this component, if an entity were to land on top of a moving platform, the entity would slide off if it when moving horizontally).
class CarrierComponent extends ex.Component {
onAdd(owner) {
owner.on('collisionstart', this.onCollisionStart.bind(this))
owner.on('collisionend', this.onCollisionEnd.bind(this))
}
onCollisionStart({ other }) {
if (!this.owner.children.includes(other)) {
const otherTransform = other.get(ex.TransformComponent)
const selfTransform = this.owner.get(ex.TransformComponent)
if (otherTransform && selfTransform) {
// add other as a child so that its position
// becomes local to this.owner
// e.g. (0,0) = same position as this.owner
this.owner.addChild(other)
// since other's position is now local to us
// we need to convert their position from global
// to local coordinates
otherTransform.pos.x -= selfTransform.owner.pos.x
otherTransform.pos.y -= selfTransform.owner.pos.y
}
}
}
onCollisionEnd({ other }) {
if (this.owner.children.includes(other)) {
const otherTransform = other.get(ex.TransformComponent)
const selfTransform = this.owner.get(ex.TransformComponent)
if (otherTransform && selfTransform) {
this.owner.removeChild(other)
// removeChild removes them from scene too, add them back
this.owner.scene.add(other)
// now that the child is no longer a child we
// need to adjust its position back to global coordinates
otherTransform.pos.x += selfTransform.owner.pos.x
otherTransform.pos.y += selfTransform.owner.pos.y
}
}
}
}
Using them together
Both of these components are very useful on their own, but the magic happens when we start using them together:
class MovingPlatform extends ex.Actor {
constructor() {
super({ ... })
this.addComponent(new OneWayCollisionComponent())
this.addComponent(new CarrierComponent())
// move back and forth
this.actions.repeatForever(ctx => {
ctx
.moveBy(ex.vec(100, 0), 100)
.moveBy(ex.vec(-100, 0), 100)
})
}
}
Hopefully this is a good demonstration of how useful it can be to split out logic into components. When I stumbled upon this pattern, it felt like everything in my head clicked. I finally had the benefits of composition without the boilerplate of systems. Creating new kinds of entities is a matter of using new combinations of components, whereas with inheritance it would mean trying to find the right inheritance tree. It’s also fine to create a component that’s only used for one kind of entity too, if it makes your code cleaner. They don’t always have to be re-usable!
When to use a System
I want to add that there are times where it still makes sense to use a system over logic in the component even if following a Systemless Components pattern. Usually this is when the ordering of the update logic is important.
For example, you may want to execute your logic only after all entities in the world have been updated by the Motion and Collision systems, or after the Motion system but before the Collision system. It would be very appropriate to use a system in this scenario so remain open to that as a solution.
Conclusion
Be sure to checkout the components used in the Jelly Jumper sample code for more real-world use cases, and if you want to, play it here. The components shown in this post are also used in Jelly Jumper, but they are a bit more complex as to handle edge cases. They also aren’t as nicely written as I was still dogfooding this pattern to find what worked, so please excuse inconsistencies. I’ve followed what I think are best practices now with my examples here.
At the end of the day, you can design your game any which way you like. You could follow inheritance patterns for some parts, ECS for others, and Systemless Components for the rest. If you were to look at some of my projects you’d probably find me using a combo of the 3. It’s all up to your preference and what you feel the best tool for the job is.