GameplayKit Tutorial: Entity-Component System, Agents, Goals, and Behaviors
In this GameplayKit tutorial, you will learn how to create flexible and scalable games by using the Entity-Component system with Agents, Goals and Behaviors. By Ryan Ackermann.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
GameplayKit Tutorial: Entity-Component System, Agents, Goals, and Behaviors
35 mins
Spawning The Monsters
This game is ready for some monsters! Let’s modify the game so you can spawn Quirk monsters.
Right-click your Entities group, select New File.., select the iOS/Source/Swift File template, and click Next. Name the new file Quirk and click Create.
Open Quirk.swift and replace the contents with the following:
import SpriteKit
import GameplayKit
class Quirk: GKEntity {
init(team: Team) {
super.init()
let texture = SKTexture(imageNamed: "quirk\(team.rawValue)")
let spriteComponent = SpriteComponent(texture: texture)
addComponent(spriteComponent)
addComponent(TeamComponent(team: team))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This is very similar to how you set up the castle entity. Here you set the texture according to the team and add the sprite component to the entity. Additionally you also add a team component to complete all this entity needs.
Now it’s time to create an instance of the Quirk entity. Last time, you created the Castle entity directly in GameScene
, but this time you’ll move the code to spawn a quirk monster into EntityManager
.
To do this, switch to EntityManager.swift and add this method to the bottom of the class:
func spawnQuirk(team: Team) {
// 1
guard let teamEntity = castle(for: team),
let teamCastleComponent = teamEntity.component(ofType: CastleComponent.self),
let teamSpriteComponent = teamEntity.component(ofType: SpriteComponent.self) else {
return
}
// 2
if teamCastleComponent.coins < costQuirk {
return
}
teamCastleComponent.coins -= costQuirk
scene.run(SoundManager.sharedInstance.soundSpawn)
// 3
let monster = Quirk(team: team)
if let spriteComponent = monster.component(ofType: SpriteComponent.self) {
spriteComponent.node.position = CGPoint(x: teamSpriteComponent.node.position.x, y: CGFloat.random(min: scene.size.height * 0.25, max: scene.size.height * 0.75))
spriteComponent.node.zPosition = 2
}
add(monster)
}
Let's review this section by section:
- Monsters should be spawned near their team's castle. To do this, you need the position of the castle's sprite, so this is some code to look up that information in a dynamic way.
- This checks to see if there are enough coins to spawn the monster, and if so subtracts the appropriate coins and plays a sound.
- This is the code to create a
Quirk
entity and position it near the castle (at a random y-value).
Finally, switch to GameScene.swift and add this to the end of quirkPressed()
:
entityManager.spawnQuirk(team: .team1)
Build and run. You can now tap the Quirk button to spawn some monsters!
Agents, Goals, and Behaviors
So far, the quirk monsters are just sitting right there doing nothing. This game needs movement!
Luckily, GameplayKit comes with a set of classes collectively known as "agents, goals, and behaviors" that makes moving objects in your game in complex ways super easy. Here's how it works:
-
GKAgent2D
is a subclass ofGKComponent
that handles moving objects in your game. You can set different properties on it like max speed, acceleration, and so on, and theGKBehavior
to use. -
GKBehavior
is a class that contains a set ofGKGoals
, representing how you would like your objects to move. -
GKGoal
represents a movement goal you might have for your agents - for example to move toward another agent.
So basically, you configure these objects and add the GKAgent
component to your class, and GameplayKit will move everything for you from there!
GKAgent2D
doesn't move your sprites directly, it just updates its own position appropriately. You need to write a bit of glue code to match up the sprite position with the GKAgent
position.Let's start by creating the behavior and goals. Right-click your Components group, select New File.., select the iOS/Source/Swift File template, and click Next. Name the new file MoveBehavior and click Create.
Open MoveBehavior.swift and replace the contents with the following:
import GameplayKit
import SpriteKit
// 1
class MoveBehavior: GKBehavior {
init(targetSpeed: Float, seek: GKAgent, avoid: [GKAgent]) {
super.init()
// 2
if targetSpeed > 0 {
// 3
setWeight(0.1, for: GKGoal(toReachTargetSpeed: targetSpeed))
// 4
setWeight(0.5, for: GKGoal(toSeekAgent: seek))
// 5
setWeight(1.0, for: GKGoal(toAvoid: avoid, maxPredictionTime: 1.0))
}
}
}
There's a lot of new stuff here, so let's review this section by section:
- You create a
GKBehavior
subclass here so you can easily configure a set of movement goals. - If the speed is less than 0, don't set any goals as the agent should not move.
- To add a goal to your behavior, you use the
setWeight(_:for:)
method. This allows you to specify a goal, along with a weight of how important it is - larger weight values take priority. In this instance, you set a low priority goal for the agent to reach the target speed. - Here you set a medium priority goal for the agent to move toward another agent. You will use this to make your monsters move toward the closest enemy.
- Here you set a high priority goal to avoid colliding with a group of other agents. You will use this to make your monsters stay away from their allies so they are nicely spread out.
Now that you've created your behavior and goals, you can set up your agent. Right-click your Components group, select New File.., select the iOS/Source/Swift File template, and click Next. Name the new file MoveComponent and click Create.
Open MoveComponent.swift and replace the contents with the following:
import SpriteKit
import GameplayKit
// 1
class MoveComponent: GKAgent2D, GKAgentDelegate {
// 2
let entityManager: EntityManager
// 3
init(maxSpeed: Float, maxAcceleration: Float, radius: Float, entityManager: EntityManager) {
self.entityManager = entityManager
super.init()
delegate = self
self.maxSpeed = maxSpeed
self.maxAcceleration = maxAcceleration
self.radius = radius
print(self.mass)
self.mass = 0.01
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// 4
func agentWillUpdate(_ agent: GKAgent) {
guard let spriteComponent = entity?.component(ofType: SpriteComponent.self) else {
return
}
position = float2(spriteComponent.node.position)
}
// 5
func agentDidUpdate(_ agent: GKAgent) {
guard let spriteComponent = entity?.component(ofType: SpriteComponent.self) else {
return
}
spriteComponent.node.position = CGPoint(position)
}
}
There's lots of new stuff here as well, so let's review this section by section:
- Remember that
GKAgent2D
is a subclass ofGKComponent
. You subclass it here so customize its functionality. Also, you implementGKAgentDelegate
- this is how you'll match up the position of the sprite with the agent's position. - You'll need a reference to the
entityManger
so you can access the other entities in the game. For example, you need to know about your closest enemy (so you can seek to it) and your full list of allies (so you can spread apart from them). -
GKAgent2D
has various properties like max speed, acceleration, and so on. Here you configure them based on passed in parameters. You also set this class as its own delegate, and make the mass very small so objects respond to direction changes more easily. - Before the agent updates its position, you set the position of the agent to the sprite component's position. This is so that agents will be positioned in the correct spot to start. Note there's some funky conversions going on here - GameplayKit uses
float2
instead ofCGPoint
, gah! - Similarly, after the agent updates its position
agentDidUpdate(_:)
is called. You set the sprite's position to match the agent's position.
You still have a bit more to do in this file, but first you need to add some helper methods. Start by opening EntityManager.swift and add these new methods:
func entities(for team: Team) -> [GKEntity] {
return entities.flatMap{ entity in
if let teamComponent = entity.component(ofType: TeamComponent.self) {
if teamComponent.team == team {
return entity
}
}
return nil
}
}
func moveComponents(for team: Team) -> [MoveComponent] {
let entitiesToMove = entities(for: team)
var moveComponents = [MoveComponent]()
for entity in entitiesToMove {
if let moveComponent = entity.component(ofType: MoveComponent.self) {
moveComponents.append(moveComponent)
}
}
return moveComponents
}
entities(for:)
returns all entities for a particular team, and moveComponents(for:)
returns all move components for a particular team. You'll need these shortly.
Switch back to MoveComponent.swift and add this new method:
func closestMoveComponent(for team: Team) -> GKAgent2D? {
var closestMoveComponent: MoveComponent? = nil
var closestDistance = CGFloat(0)
let enemyMoveComponents = entityManager.moveComponents(for: team)
for enemyMoveComponent in enemyMoveComponents {
let distance = (CGPoint(enemyMoveComponent.position) - CGPoint(position)).length()
if closestMoveComponent == nil || distance < closestDistance {
closestMoveComponent = enemyMoveComponent
closestDistance = distance
}
}
return closestMoveComponent
}
This is some code to find the closest move component on a particular team from the current move component. You will use this to find the closest enemy now.
Add this new method to the bottom of the class:
override func update(deltaTime seconds: TimeInterval) {
super.update(deltaTime: seconds)
// 1
guard let entity = entity,
let teamComponent = entity.component(ofType: TeamComponent.self) else {
return
}
// 2
guard let enemyMoveComponent = closestMoveComponent(for: teamComponent.team.oppositeTeam()) else {
return
}
// 3
let alliedMoveComponents = entityManager.moveComponents(for: teamComponent.team)
// 4
behavior = MoveBehavior(targetSpeed: maxSpeed, seek: enemyMoveComponent, avoid: alliedMoveComponents)
}
This is the update loop that puts it all together.
- Here you find the team component for the current entity.
- Here you use the helper method you wrote to find the closest enemy.
- Here you use the helper method you wrote to find all your allies move components.
- Finally, you reset the behavior with the updated values.
Almost done; just a few cleanup items to do. Open EntityManager.swift and update the line that sets up the componentSystems
property as follows:
lazy var componentSystems: [GKComponentSystem] = {
let castleSystem = GKComponentSystem(componentClass: CastleComponent.self)
let moveSystem = GKComponentSystem(componentClass: MoveComponent.self)
return [castleSystem, moveSystem]
}()
Remember, this is necessary so that your update(_:)
method gets called on your new MoveComponent
.
Next open Quirk.swift and modify your initializer to take the entityManager
as a parameter:
init(team: Team, entityManager: EntityManager) {
Then add this to the bottom of init(team:entityManager:)
:
addComponent(MoveComponent(maxSpeed: 150, maxAcceleration: 5, radius: Float(texture.size().width * 0.3), entityManager: entityManager))
This creates your move component with some values that work well for the quick Quirk monster.
You need a move component for the castle too - this way they can be one of the agents considered for the "closest possible enemy". To do this, open Castle.swift and modify your initializer to take the entityManager
as a parameter:
init(imageName: String, team: Team, entityManager: EntityManager) {
Then add this to the bottom of init(imageName:team:entityManager:)
:
addComponent(MoveComponent(maxSpeed: 0, maxAcceleration: 0, radius: Float(spriteComponent.node.size.width / 2), entityManager: entityManager))
Finally, move to EntityManager.swift and inside spawnQuirk(team:)
, modify the line that creates the Quirk
instance as follows:
let monster = Quirk(team: team, entityManager: self)
Also open GameScene.swift and modify the line in didMove(to:)
that creates the humanCastle
:
let humanCastle = Castle(imageName: "castle1_atk", team: .team1, entityManager: entityManager)
And similarly for aiCastle
:
let aiCastle = Castle(imageName: "castle2_atk", team: .team2, entityManager: entityManager)
Build and run, and enjoy your moving monsters:
Congratulations! At this point you have a good understanding of how to use the new Entity-Component system in GameplayKit, along with using Agents, Goals, and Behaviors for movement.