Transform Component in GameplayKit
Create a transform component for GameplayKit in Swift to get a clean and robust API for everything related to position, rotation, and scale.
In every new game project I have a few components which are part of my reusable core functionality that I always include. To have a set of GameplayKit based components that provide common functionality most games require can save a great deal of time once implemented.
+-- Shared
+-- Entities
+-- Components
+-- Core
InputComponent.swift
PhysicsComponent.swift
RemoveComponent.swift
SpriteComponent.swift
TransformComponent.swift
Last time we looked at implementing a GameplayKit Remove Component and now the time has come to see how we can implement a transform component.
Almost every entity I create needs transformations in one way or another, so to have a rock-solid and robust transform component that is easily accessible everywhere where it makes sense, has turned out to be incredible helpful.
The purpose of the transform component is to give ourselves a clean and logical API that wraps around useful transform related functions similar to how the major game engines do it.
This will provide us with a system where the transform interaction is fully decoupled from the visual representation of the game entity. Clean, single responsible components, help us to get better structured and more maintainable code.
Apart from handling the obvious properties position, rotation and scale, we also get a location in the game code where we can put anything related to overall transformation handling that is useful for more than one entity.
The Transform Component
To start off the transform component we will have an empty node that will serve as a container that holds the transform properties.
/// Handles transformations for an `Entity`.
class TransformComponent: GKComponent {
/// Reference to the node that holds the transformations.
///
/// We assign an empty node by default so an entity can have transformations
/// without having a sprite node assigned to it.
private lazy var node = SKNode()
}
To ensure that our transform component can always hold transformations,
we instantiate an empty SpriteKit SKNode
by default. We do it lazily as we only want to
create the empty node if we actually need it.
The node
property can be used differently, depending on the game and the entities
needs. Many times I let my SpriteComponent
set itself as the node in the
transform component, which is the reason why we want it to be lazy.
Other times I might keep the empty SKNode
and use it as a parent for the sprites
it transforms. It's a case-by-case scenario.
Transform Properties
Let's move on to the basic properties, where we wrap the transform component
around the properties of SKNode
with some added sugar on top.
First, we have the position.
/// The position of the entity's node in its parent's coordinate system.
var position: CGPoint {
get { node.position }
set { node.position = newValue }
}
/// Computed world position from parent's coordinate system.
var worldPosition: CGPoint {
get {
guard let parent = node.parent,
let entity = self.entity as? Entity,
let scene = entity.scene
else { return position }
/*
Use parent's coord system.
Convert this node's local position to the world node's coord system.
*/
return parent.convert(node.position, to: scene.world)
}
set {
guard let parent = node.parent,
let entity = self.entity as? Entity,
let scene = entity.scene
else { return }
node.position = scene.world.convert(newValue, to: parent)
}
}
The position property is simply a wrapper around the position property in
SKNode
.
More interesting is the world position. I use the worldPosition
property
all the time and I find it very useful to have a wrapper around reading and
setting the position in world space.
So by having both a position
and a worldPosition
property as part of the
transform component, it becomes dead simple to work with the position of
entities in both local and world space.
Next, moving on to rotation.
/// The Euler rotation about the z axis (in radians).
var rotation: CGFloat {
get { node.zRotation }
set { node.zRotation = newValue }
}
Not much to say here; the rotation property simply wraps around the zRotation
property in SKNode
. I haven't yet had any reason to add any other options here.
If you prefer working with degrees instead of radians, you could add a conversion for doing that here.
And finally, we have the scale.
/// A scaling factor that multiplies the size of an entity.
var scale: CGSize {
get {
return .init(width: node.xScale, height: node.yScale)
}
set {
node.xScale = newValue.width
node.yScale = newValue.height
}
}
/// Get accumulated size of the entity.
var size: CGSize { node.calculateAccumulatedFrame().size }
I prefer to work with scale by having it contained in one CGSize
value instead
of working with each axis separately. So the scale property wraps a CGSize
value
around xScale
and yScale
in SKNode
.
Then the additional size
attribute calculates the total size of everything
added to the hierarchy of the node. One use for this is to get the size in
Physics Component when setting up the physics properties.
Transform Methods
With the transform properties setup, we can start adding useful methods to interact with the component.
I've a set(node:)
method that I use for updating which node that the transform
component uses for holding its properties.
/// Set the node that holds the transformation values.
func set(node: SKNode) {
// Transfer attributes from the previous referenced node to the new.
node.position = position
node.zRotation = zRotation
node.xScale = scale.width
node.yScale = scale.height
// Set self.node to reference the new node.
self.node = node
}
The transform component might already have values assigned to its properties, so when setting a new node I transfer the properties from the node that previously was responsible for holding the properties to the new node.
This is a design consideration that I've been juggling a bit back and forth with. And I'm still not 100% sure if this is the way I'm going to keep it or if I might revise it in the future.
I've been considering using another approach where the transform class has its own properties for position, rotation and scale instead of using a node to hold those properties. But so far I've found it more useful to have it in the node, and also simpler to work with. For now, I'll stick to this approach.
And then finally, we can start decorating the component with useful methods to simplify making transforms on our game entities.
/// Moves the transform in the direction and distance of translation.
func translate(_ direction: CGVector) {
node.position += direction
}
/// Run an animation SKAction on the node.
func run(_ action: SKAction) {
node.run(action)
}
With the structure for the transform component in place, there is no limit to different methods that are useful for transformations in game development that we can keep adding to this class.
I'll get the ball rolling with two example methods.
In any component that handles movement logic for our game entities, we can use the translate method to quickly execute the movement on screen. If the component has a direction and speed, we can simply pass in the formula to the transform component.
transform.translate(direction * speed * Time.deltaTime)
This is something I use all the time. To be able to simple get the delta time anywhere in your game code as in my example above, you can check out my article on writing a class for Game Time in Swift.
Another method that I find useful is to have a run()
method where I can execute
SKActions
on the entity.
transform.run(.sequence([actionA, actionB]))
I use this for animations, but I also use it quite often to quickly be able to run time-based logic on a game entity. For instance, on an explosion entity I would start a run action with a completion action that adds a remove component when the explosion is done.
Accessing the Transform Component
With the transform component done and ready to be used in our game entities, we need to make sure we can easily get hold of a reference to it.
The transform component is something I use all the time. Not only for moving the objects on the screen, but I also need to read the transformation data for all kinds of different logic. It could be anything from a behavior system to handling trigger-based events.
More or less every game entity will require the transform component, so I've made it a part of my base class for entities. Which ensures that the rest of the game systems can count on that there will always be a transform component available.
I create my own base class for that all game entities inherit from, which is on
top of GKEntity
in GameplayKit.
class Entity: GKEntity {
/// Transform Component.
let transform = TransformComponent()
override init() {
super.init()
// Every entity has a transform component.
addComponent(transform)
}
}
As every entity in the game inherits this Entity
class instead of using
GKEntity
directly, they all automatically get the transform component added.
We also want to make sure that every game component has quick and easy access
to its entity's transforms. So just as we did our own Entity
class instead
of using GKEntity
, we will use our own Component
class instead of using GKComponent
in GameplayKit directly.
class Component: GKComponent {
/// Hold a reference to the entity's TransformComponent.
lazy var transform: TransformComponent = {
guard let entity = entity as? Entity
else { fatalError("The component must be added to an Entity.") }
return entity.transform
}()
}
We want it to be dead simple to get hold of the transform component everywhere where it makes sense, and that is in other game components.
By having a lazy property we get the reference to the transform component first time it's used, and then the reference is kept for in the property, which is good for performance.
Now every component in an entity has direct access to its transform, and we can write code like this anywhere without having to first find the transform component or having to worry about which SpriteKit node to interact with.
transform.position = newPosition
Powerful and convenient at the same time.
Where to Next
With the basic structure for the transform component in place, we can continue decorating it with methods to keep increasing its usefulness. We've just scratched the surface of what methods to include.
I'll put some ideas on the table that I believe would be useful. What first comes to mind would be to add a property and related methods to simplify the handling of the parent-child relationship between game entities.
I can also see that including methods for both local and world space operations for rotations and scale would be of use. It would probably also be convenient to add methods that rotate towards a direction and/or move towards a specific position. And the list could go on.
Enjoy leveling up your gamedev with a powerful transform component.