Transform Component in GameplayKit

johansteen

Johan Steen

Posted on May 21, 2022

Transform Component in GameplayKit

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
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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. So 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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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. So 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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]))
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }()
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
johansteen
Johan Steen

Posted on May 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related