If you thought writing 2,000 words about opening Photoshop or taking four weeks to start coding in Xcode were frustratingly slow, then this week’s post might make you want to set fire to your computer.
As I continue to build a jigsaw puzzle game in ridiculous detail, I want to spend the next 1,500 words getting the game to look and function exactly the same as it does right now.
Before you reach for the matches, let me explain.
In the last post, I added the puzzle pieces as children of the main game scene, GameScene.swift
.
The implication was that this file will be managing them and all of their functionality. For a straightforward game like this one, this would probably be fine. However, in general it’s not a great way to build a game and this series is all about doing things the right way.
I want to use Entity Component Systems to create a flexible game architecture. This will make adding new systems easier and it will also help keep the project organised by keeping related pieces of functionality together in separate files.
Adding a Sprite Component
I’ll start by opening up Xcode where I left off last time and then I’ll add a new Swift file in the Shared
group. I’ll call this SpriteComponent
and, after making sure that both targets are selected, I’ll add this definition:
import GameplayKit import SpriteKit // 1. class SpriteComponent : GKComponent { // 2. let sprite : SKSpriteNode // 3. init( name : String ) { self.sprite = SKSpriteNode(imageNamed: name) super.init() } // 4. override func willRemoveFromEntity() { self.sprite.removeFromParent() } required init?(coder aDecoder: NSCoder) { fatalError("Not implmented") } }
- SpriteComponent is a subclass of the GameplayKit class
GKComponent
which provides methods for common component operations - This property will hold a reference to the sprite, although the sprite itself gets added to the main game scene directly (see below)
- I’ll initialise the component with the name of the sprite, which should correspond to an asset in the asset catalog. If the asset isn’t there, SpriteKit will show a red X on a white background instead.
- If the sprite component is removed from the entity for any reason, I will assume that this is because the entity should no longer be represented on screen by a sprite. This method gives me an opportunity to remove the sprite from the parent node without involving the main game scene.
Adding a Position Component
I’ll add another new Swift file to the Shared group called PositionComponent
, again making sure that both targets are selected, then replace the contents with:
import GameplayKit import SpriteKit class PositionComponent : GKComponent { // 1. var currentPosition : CGPoint let targetPosition : CGPoint // 2. init( currentPosition : CGPoint, targetPosition : CGPoint ) { self.currentPosition = currentPosition self.targetPosition = targetPosition super.init() } required init?(coder aDecoder: NSCoder) { fatalError("Not implmented") } }
- The current position is a
var
property as it can be changed by any of the systems (see below). ThetargetPosition
is alet
constant as it describes the final, winning location of the puzzle piece and should never be changed during the lifetime of the piece. - I initialise this component with a current position and a target position. When this component is initialised, the current position should be some randomised point that represents the initial state of the entity. The target position is the winning position, which should come from the puzzle JSON file and correspond to this piece’s place in the finished puzzle.
Creating an Entity and Adding Components
In GameScene.swift
, I’ll replace the current implementation of didMove(to:)
with the following:
override func didMove(to view: SKView) { for piece in puzzle.pieces { // 1. let puzzlePiece = GKEntity() // 2. let spriteComponent = SpriteComponent(name: piece.name) // 3. let positionComponent = PositionComponent(currentPosition: piece.position, targetPosition: piece.position) // 4. puzzlePiece.addComponent(spriteComponent) puzzlePiece.addComponent(positionComponent) self.addChild(spriteComponent.sprite) self.entities.append(puzzlePiece) } }
- I initialise a new, generic GKEntity object. Entities are ultimately just objects with an ID, so the generic implementation of the class is fine here. Anything that describes the nature of this entity should be represented as a component.
- I create a SpriteComponent object and pass the piece’s name to the initialiser.
- I create a position component. For the moment, I’m passing the piece’s winning position to both parameters until I’ve added player interaction.
- I then add both of these components to the entity object
- Finally, I’ll add the sprite property of the SpriteComponent to the game scene and I’ll keep a reference to the entity object in the scene’s
entities
array.
Whereas previously all of the pieces appeared in their correct location, if I build and run this now, all the pieces appear at 0,0:
This is because the position information held by the position component is not being applied to the sprite component.
For this I need my first system.
Adding a Render System
I covered my application of Entity Component Systems within SpriteKit in this post but, in summary:
- SpriteKit doesn’t do a “pure” implementation of ECS: Apple recommends that logic goes in
GKComponent
subclasses whereas in a “pure” implementation components should be dumb containers of state only. Systems should hold logic. - To get around this and for organisational purposes, I use extensions on the
GKComponent
most closely associated with the system I want to create rather than a subclass of GameplayKit’s built-inGKComponentSystem
. If I find myself overriding theupdate(deltaTime:)
method of a component, it’s a good indication I’m actually developing a system. - I then use the actual
GKComponentSystem
classes as simply buckets of related components and use these systems to define execution order.
With that in mind, I’ll create another new Swift file in the Shared group called SpriteComponent+Render.swift
and add the following:
import Foundation extension SpriteComponent { // 1. override func update(deltaTime seconds: TimeInterval) { // 2. guard let hasPositionComponent = entity?.component(ofType: PositionComponent.self) else { return } // 3. self.sprite.position = hasPositionComponent.currentPosition } }
- This method will get called every frame and its parameter will be the fraction of a second that has passed since the last update
- I’ll make sure that the entity that the SpriteComponent belongs to also has a PositionComponent otherwise there’s no work for this system to do.
- If it does, I’ll apply the PositionComponent’s
currentPosition
property to the sprite, updating its display on screen
Seeing The Changes
Right now, this system does nothing as the update(deltaTime:)
method is never called.
I’ll return to GameScene.swift
and add the following property:
lazy var componentSystems : [GKComponentSystem] = { let spriteCompSystem = GKComponentSystem(componentClass: SpriteComponent.self) return [spriteCompSystem] }()
This property will hold an array of component systems. Each component system will contain a reference to every component of a specified type that gets added to that system.
I created a new GKComponentSystem
object specifying that I’m only interested in the SpriteComponent
class then added this to the array.
Next, in didMove(to:)
under the current for
loop, I’ll add this additional loop:
for system in componentSystems { for entity in entities { system.addComponent(foundIn: entity) } }
For each component system (which is bound to a particular GKComponent
subclass), it will go through and grab a reference to that GKComponent subclass if it appears in any entity in the entities
array.
In this instance, if any of the entities have a SpriteComponent installed, then the system will keep a reference of that SpriteComponent class.
Finally, in the update(:)
method of GameScene.swift
, I’ll add the following underneath let dt = currentTime - self.lastUpdateTime
:
for system in componentSystems { system.update(deltaTime: dt) }
Each component system will forward the delta time to every reference of the GKComponent subclass that it holds. In other words, every SpriteComponent instance that was added to the system will now have its update(deltaTime:)
method called.
The purpose of having an array of systems like this is to define a clear order of execution.
Right now, the only system I have is the render system. However, in future I will want some sort of snapping system that will snap a piece into place if the player drops it close to their final positions.
I will want that snapping position to update the position component of the entity being moved before it is rendered. The flow will look something like on iOS:
- Player lifts finger after dragging a piece.
- The position component belonging to that piece’s entity is updated by the player interaction system with the location of the finger when it was lifted.
- Every entity with a snapping system will then check to see if its current location is close enough to the final position to be moved automatically. If it is, then the snapping system will update the position component again with the final target location of the puzzle piece (this is all happening within a single frame—at this point, the sprite on screen is still showing the last position it was before the player lifted their finger).
- Finally, the render system updates the piece on screen based on the last change to the position component. It doesn’t care who last updated the position component (in this case, either the interaction system or the snapping system), its job is just to apply this position information to the sprite.
For this to work as planned, the snapping system has to be guaranteed to fire before the render system.
Using an array of GKComponentSystems in the above code snippet ensures that every entity that has a snapping component will be updated first. Only after this has completed will every every entity that has a render system then be updated.
For this game, system order is not actually very important. If the snapping comes in and updates the position information after the rendering of the current frame has taken place, then the piece will “snap” a fraction of a second later in the next frame. A delay which will probably go unnoticed by most players.
However, for some games (especially games with high velocity entities like racing or shooting games) this split second difference between frames is important which is why controlling the updating of components is important.
Without a clear order of update, other weird bugs or effects can arise as systems write over each other’s updates to component properties.
Testing It Out
In order to make sure this is all working, I’ll add the following temporary method:
#if os(OSX) override func mouseUp(with event: NSEvent) { let random = arc4random_uniform(UInt32(entities.count)) let entity = entities[Int(random)] entity.component(ofType: PositionComponent.self)?.currentPosition = event.location(in: self) } #endif
This gets the last location the user clicks and then sets the position component of a random puzzle piece to that location. It does this, not by updating the sprite, but updating the position component.
Already we begin to see the potential of ECS. This method doesn’t care if the entity has a position component or not. If it does, it’ll set the position. If not, nothing happens.
It also doesn’t care about what type of entity the PositionComponent belongs to. This position component could be attached to an entity that represents a particle system, or an SKShapeNode
, or an SKLabelNode
.
This can make adding new gameplay systems or effects a simple matter of adding or removing components.
While this post ends in (almost) the same place it started, the architectural changes made in it have already made things easier to manage.
They also create an environment that encourage experimentations and iteration (as we’ll see in the next post) and they allow for bonus gameplay changes for very little work down the line.
ECS FTW!