Over a million developers have joined DZone.

A Look at Agents, Goals, and Behaviours in GameplayKit

A closer look at how you can utilize one of Apple's newest frameworks.

· Mobile Zone

Visually compose APIs with easy-to-use tooling. Learn how IBM API Connect provides near-universal access to data and services both on-premises and in the cloud, brought to you in partnership with IBM.

GameplayKit is one of the new frameworks introduced at WWDC 2015. It includes some interesting functionality that isn't just for games such as random number generationstate machines and pathfinding. 

In this post, I'm going to look at agents, goals and behaviours which allows the creation of agents  that follow simple dynamic rules such as cohesion and separation. To demonstrate the effects of rules and goals on agents, I've built a little demo app, GameplayKitAgents, which creates a system of 250 agents with a user interface to move obstacles and seek targets and vary the weight of different goals.


A GameplayKit component system starts with an instance of GKComponentSystem which is instantiated with the component class we want to use as its members. GKComponentSystem is homogenous and this component class is immutable. In the case of my app, I'll be using GKAgent2D  so my opening line looks like this:

    let agentSystem =  GKComponentSystem(componentClass: GKAgent2D.self)

I'm going to create an array of 250 agents, since GKAgent2D is a class and passed by reference, if I do this:

    let agents = [GKAgent2D](count: 250, repeatedValue: GKAgent2D())

I actually only get one agent, referenced 250 times in the array, so I populate the agent system's components in a loop:

    for _ in 0 ... 250


Next, I create some goals. These are instances of GKGoal and define the forces that act upon each agent. Rather than creating a multitude of sub-classes, Apple have chosen to implement different goals with different constructors, for example wandering and cohesion are created with these initialisers:

public convenience init(toCohereWithAgents agents: [GKAgent], 
 maxDistance: Float, 
 maxAngle: Float)

public convenience init(toWander speed: Float)

Since there's no way to interrogate a goal after creation to see what type it is, I've created my own class, NamedGoal, which includes name and weight properties. So my cohesion and wander goals look like this:

   lazy var cohesionGoal: NamedGoal =
        [unowned self] in
        NamedGoal(name: "Cohesion",
            goal: GKGoal(toCohereWithAgents: self.agentSystem.getGKAgent2D(),
                maxDistance: 20,
                maxAngle: Float(2 * M_PI)),
            weight: 50)


    let wanderGoal = NamedGoal(name: "Wander",
        goal: GKGoal(toWander: 25),

        weight: 60)

I've also added an extension to GKComponentSystem so that I can get a typed array of GKAgent2D to use when constructing goals that reference the agents, for example, the cohesion goal:

    extension GKComponentSystem
        func getGKAgent2D() -> [GKAgent2D]
            return components
                .filter({ $0 is GKAgent2D })
                .map({ $0 as! GKAgent2D })


My system also includes a seek target (in blue) and obstacles to avoid (in red). These are also defined as goals. For the obstacles, I've created an array of four GKCircleObstacle, 

  let obstacles = [GKCircleObstacle(radius: 100),
        GKCircleObstacle(radius: 100),
        GKCircleObstacle(radius: 100),

        GKCircleObstacle(radius: 100)]

...and passed that to an avoid goal:

   lazy var avoidGoal: NamedGoal =
        [unowned self] in
        NamedGoal(name: "Avoid",
            goal: GKGoal(toAvoidObstacles: self.obstacles,
                maxPredictionTime: 2),
            weight: 100)


The seek goal uses an agent, 

  let targets = [GKAgent2D()]

    lazy var seekGoal: NamedGoal =
        [unowned self] in
        NamedGoal(name: "Seek",
            goal: GKGoal(toSeekAgent: self.targets.first!),
            weight: 50,
            weightMultiplier: 0.01)


Once I've crafted all of my NamedGoal instances, I create an array to hold them:

lazy var namedGoals: [NamedGoal] =
        [unowned self] in
        [self.wanderGoal, self.cohesionGoal, self.avoidGoal, self.seekGoal]



To apply the goals to the system, I use a GKBehavior. 

    let behaviour = GKBehavior()

By looping over my namedGoals array, I can add each goal to the behaviour by simply invoking setWeight which adds or changes the weight of a goal on that behaviour:

  for namedGoal in namedGoals
         behaviour.setWeight(namedGoal.weight, forGoal: namedGoal.goal)

Then a loop over each of the agents in the system allows me to apply my behaviour to each one:

    for agent in agentSystem.getGKAgent2D()
        agent.behavior = behaviour


Animation and Rendering

To animate the system, I use a CADisplayLink which will fire step with each screen refresh:

 lazy var displayLink: CADisplayLink =
        [unowned self] in
        return CADisplayLink(target: self, selector: Selector("step"))


    // in init()

        forMode: NSDefaultRunLoopMode)

I'm drawing the agents by constructing a single UIBezierPath with a small circle for each agent and setting that as the path for my view's layer. So, my step method does two things, first updates the system so each agent has its position changed by the forces of the goals acting upon it, then second it creates the path and applies it: 

    func step()

        let bezierPath = UIBezierPath()

        for agent in agentSystem.getGKAgent2D()
                atPosition: agent.position,
                inFrame: frame)

        agentsLayer.path = bezierPath.CGPath


appendCircleOfRadius is an extension to UIBezierPath that, unsurprisingly, appends a circle to a path and is happy accepting GameplayKit's vector_float2 and Float rather than CoreGraphics CGPoint and CGFloat:

    extension UIBezierPath
        func appendCircleOfRadius(radius: Float, atPosition position: vector_float2, inFrame frame: CGRect)
            let position = CGPoint(x: frame.width / 2 + CGFloat(position.x),
                y: frame.height / 2 + CGFloat(position.y))

            let circle = UIBezierPath(ovalInRect: CGRect(origin: position.offset(dx: radius, dy: radius),
                size: CGSize(width: CGFloat(radius * 2), height: CGFloat(radius * 2))))



User Interface

Finally, the user interface is built from a few components. 

  • The main view, AgentsView, contains the system, the display link and pretty much everything discussed in this post.
  • The editor view, AgentsEditor, contains a UITableView with a row for each NamedGoal, when the sliders in the table view change, the editor invokes goalWeightDidChange on its AgentsEditorDelegate. In my demo app, the view controller acts as the AgentsEditorDelegate and invokes setWeight on the AgentsView.

The obstacles and target can be moved around with a touch/drag which is handled inside AgentsView.

In Conclusion

GameplayKit offers some pretty high level dynamic behaviours with a very simple API. I've coded similar systems in the past (see my Swarm Chemistry stuff) and it can be tricky to get right. Performance is pretty impressive, on my iPad Pro, this app breezes along with 500 agent, but I've set the default to 250 agents.

I don't think this framework is limited to games, my first thought for agents, goals and behaviours was for a simple crowd simulation application.

As always, the source code for this demo is available at my GitHub repository here. Enjoy!

The Mobile Zone is brought to you in partnership with Strongloop and IBM.  Visually compose APIs with easy-to-use tooling. Learn how IBM API Connect provides near-universal access to data and services both on-premises and in the cloud.


Published at DZone with permission of Simon Gladman, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}