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()
}
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 }
}
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
}
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
}
}
}
}
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
}
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
}
We use that when we instantiate a sprite component in an entity.
let sprite = SpriteComponent(layer: .fx)
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)
}
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()
}
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)
}
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)
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)
}
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)
}
}
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)