DEV Community

Cover image for Sprite Component in GameplayKit
Johan Steen
Johan Steen

Posted on • Originally published at blog.bitbebop.com

Sprite Component in GameplayKit

Let's continue our journey to get a few useful core components for GameplayKit built, that almost every game will have use for that we write in Swift and using Apple's game frameworks.

Last time we looked at making a GameplayKit Transform Component, so let's pair that component up with a Sprite Component. A sprite and a transform component complement each other and provide a powerful, yet manageable, system to get things moving on screen.

By having a dedicated component for rendering the visuals of our entities, we get a clear separation of concerns between transformation and the visual representation.

The Sprite Component

To start off the sprite component, we will have an empty node that will serve as a root that holds one or many child nodes that make up the entity's visuals.

/// Handles visual representation of an `Entity`.
class SpriteComponent: GKComponent {
  /// The root node to add children that will be rendered in the scene.
  private let node = SKNode()
}
Enter fullscreen mode Exit fullscreen mode

This makes the sprite component completely stand alone. In my own production code I shared this with my transform component. If you've read my previous article about building a transform component, you might remember that we use an SKNode there to hold the transformation data. As every entity in my setup always has a transform component, my sprite component references the same SKNode.

But for simplicity, in the example we are building here, we will let the sprite have its own. But keep in mind that you can get a very lean and elegant setup by sharing that node between some of the core components.

Sprite Properties

Let's move on to the basic properties, we are using Swift's computed properties to wrap around some of the built-in properties of SKNode, and then we add a few additional properties on top of that.

/// Computed property that manages the name of the root node.
var name: String? {
  get { node.name }
  set { node.name = newValue }
}

/// Computed property that manages the alpha of the root node.
var alpha: CGFloat {
  get { node.alpha }
  set { node.alpha = newValue }
}

/// Computed property that manages the zPosition of the root node.
var zPosition: CGFloat {
  get { node.zPosition }
  set { node.zPosition = newValue }
}
Enter fullscreen mode Exit fullscreen mode

To start it off, we basically wrap some common properties from SKNode that I've found useful to be able to modify from other components that provide the unique behavior for the entity and are by that convenient to have exposed in the component.

Now let's make it a bit more interesting.

/// Determines if the node is visible in camera.
var inCamera: Bool {
  if let camera = scene?.camera {
    if let child = node.children.first {
      return camera.contains(child)
    }
  }

  return false
}
Enter fullscreen mode Exit fullscreen mode

I often need to know if an entity actually is in view or not and can make behavioral decisions for the entity based on that. I've found it incredibly useful to have an inCamera property available to always be able to check the current state for any entity in the game.

The next one is isHidden.

var isHidden: Bool = false {
  didSet {
    node.isHidden = isHidden

    // Disable physicsbody while hidden to not trigger collisions.
    if isHidden {
      node.physicsBody = nil
    } else {
      if let physics = entity?.component(ofType: PhysicsComponent.self) {
        node.physicsBody = physics.physicsBody
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This could also be a simple wrapper around the property in SKNode as the first 3 properties were. As many of my entities also have a physics component with a physics body attached to them, I've found this to be the best location to handle that too.

Setting a sprite to hidden, does not disable its physics body. So by hooking into didSet in the isHidden property we can handle enabling and disabling the physics for the entity here, so that is taken care of automatically when we need to hide our entities for one reason or another.

PhysicsComponent is part of my core components that I include in every game, so I can pretty much assume in my other core components that the component exists.

Layering the Sprites

Another crucial part of working with sprites is depth sorting to let SpriteKit know, in which order to stack overlapping sprites on screen.

I've learned the hard way that manually assigning values to zPosition for each sprite is a sure way towards losing track of the sorting order. Instead I prefer using a Swift enum to setup predetermined sprite layers.

/// The names and zPositions of layers in the game scene.
enum GameLayer: CGFloat {
  case bullet       = 400
  case pod          = 500
  case enemy        = 600
  case player       = 700
  case fx           = 800
  case hud          = 1000
  case debug        = 1500
  case touchControl = 2000
  case overlay      = 3000
}
Enter fullscreen mode Exit fullscreen mode

By using enum cases for the sprites, I ensure that a certain entity type gets consistent depth positioning, and if I want to reorder the layers, I can simply update the enum instead of having to go through every single affected entity.
I might want to move bullets to be in front instead of being behind the player, which I can do directly in the enum.

The constructor of the sprite component takes GameLayer as its signature.

init(layer: GameLayer) {
  super.init()
  node.zPosition = layer.rawValue
}
Enter fullscreen mode Exit fullscreen mode

We use that when we instantiate a sprite component in an entity.

let sprite = SpriteComponent(layer: .fx)
Enter fullscreen mode Exit fullscreen mode

We pass in the game layer, using Swift's dot syntax, to determine which layer the entity will be rendered on in our gameplay scene.

The Life Cycle

We are using the built-in life cycle of GKComponent that is called when the component is added and removed from an entity, to be able to do some relevant setup and cleanup.

override func didAddToEntity() {
  super.didAddToEntity()

  // Assign the entity to the node's entity property.
  node.entity = entity

  // Set the sprite root as the entity's node for transformations.
  (entity as? Entity)?.transform.set(node: node)
}
Enter fullscreen mode Exit fullscreen mode

SKNode has an entity property that takes a reference to a GKEntity. To be able to get a reference to the entity directly from the sprite node is very convenient and useful.

So we use this location to assign our entity to SKNode so we can take advantage of that reference through the property.

Optionally, if planning to share the SKNode with the transform component, this would be a good place to handle that. In this example I update the transform component to use the node from the sprite component as the root for all transformations.

override func willRemoveFromEntity() {
  super.willRemoveFromEntity()

  node.entity = nil
  node.removeAllActions()
  node.removeAllChildren()
  node.removeFromParent()
}
Enter fullscreen mode Exit fullscreen mode

Once it's time to remove the entity and its components from the game, we are good citizens and clean up after ourselves. We remove the entity reference as we don't want to cause any reference retain cycles. And then we stop any actions and clean up the sprite hierarchy.

That should let us leave the game world in the same pristine condition as when we entered.

Sprite Component Methods

With the sprite properties setup, and the life cycle management and initialization in place, we can start adding some useful methods to interact with the component.

First, we have the add() methods.

/// Adds a node as a child to the Sprite's root node.
func add(node: SKNode) {
  self.node.addChild(node)
}
Enter fullscreen mode Exit fullscreen mode

This is the essential method to actually get some visuals assigned to the sprite component. I use this method to pass in the sprite to render in the component.

You can keep calling this method to stack up the visuals in side the component.

let sprite = SpriteComponent(layer: .fx)
sprite.add(node: debris)
sprite.add(node: flame)
sprite.add(node: sparks)
addComponent(sprite)
Enter fullscreen mode Exit fullscreen mode

In the example above, when I define an entity for an explosion effect, I push in 3 different layers to my sprite node to build the entity's visuals.

Then I've recently come to like to have an additional add(to:) method.

/// Adds the sprite as a child to provided node.
func add(to node: SKNode) {
  node.addChild(self.node)
}
Enter fullscreen mode Exit fullscreen mode

The essential add method doesn't build a hierarchy, and I've had a few cases where I need to keep nesting, and then this method does the trick of adding that option to the component.

And finally, we need to be able to adjust the hierarchy during runtime.

/// Moves provided node from its previous parent to be a child of this root node.
func move(child node: SKNode) {
  node.move(toParent: self.node)
}

/// Moves the root node to be a child of the provided new parent node.
func move(to parent: SKNode) {
  node.move(toParent: parent)
}

/// Moves the root node to be a child of the root node in another entity.
func move(to parent: Entity) {
  // Get the SpriteComponent of the Entity to move to.
  if let sprite = parent.component(ofType: SpriteComponent.self) {
    // Move this root node to be a child of the other entity's root node.
    sprite.move(child: node)
  }
}
Enter fullscreen mode Exit fullscreen mode

The first two move methods are simply wrapping around the move method in SKNode which are useful to change the hierarchy on demand.

Then we have our own more unique move method where we can move the visuals to another Entity. So we pass in the other entity, get a reference to that entity's sprite component, and then move our complete visual hierarchy to that entity instead.

I initially thought this was a special case scenario when I needed it in one game. But when I've started new projects, I've kept reaching for this method, so I've decided to make it a permanent member of the sprite component.

Conclusion

To place the game visuals in a dedicated sprite component helps separate concerns and keep the code decoupled.

Theoretically we should be able to add a Swift protocol for rendering, RenderingProtocol, that the sprite component would conform to and we should then be be able to implement a mesh component and swap the game play over to using 3D meshes instead of sprites, without having to modify any other logic in our game entities. Well, theoretically.

Anyway, to have a dedicated component for the sprite, we get a logical place where to house things like the in camera property.

Continuing on, the sprite component would pair up nicely with an animation component that would manage animating the textures or handling flip book like animations on top of the sprite component.

And of course, a physics component that manages the physics properties of the entity and that grabs a reference to the sprite component to add and remove itself to the physicsBody property of the root node of the sprite component.

Top comments (0)