Marker
is a structural design pattern that is used to provide run-time metadata with objects. The associated information then used to indicate that the marked type requires specific treatment or belongs to a particular category that needs to be processed uniquely.
In Swift the pattern is implemented using protocols
. We simply create an empty protocol and a target type that needs to be marked. Then we can retrieve that information at run-time and perform some custom logic around the corresponding types.
As an example we build a simple skeleton structure for game objects and game systems that process types that are marked with particular marker
.
We start off from declaring marker
protocols for our needs:
protocol Interactable { /* empty */ }
protocol Destructible { /* empty */ }
protocol PositionUpdatable { /* empty */ }
We introduced three difference protocols with empty implementation. The first one, called Interactable
will be used to mark game objects that can be touched. Destructible
protocol will be used to mark game objects that can be destroyed and PositionUpdatable
protocol will mark types that can be move around a game level.
The first type that will conform to all of the protocols will be BunnyEnemy
class:
class BunnyEnemy: SKSpriteNode, Interactable, Destructible {
// MARK: - Properties
var movementSpeed: CGFloat
// MARK: - Initializers
init(position: CGPoint, texture: SKTexture, movementSpeed: CGFloat) {
self.movementSpeed = movementSpeed
super.init(texture: texture, color: .clear, size: texture.size())
self.position = position
}
required init?(coder aDecoder: NSCoder) {
fatalError("required init?(coder aDecoder: NSCoder) has not been implemented")
}
}
We used SpriteKit
framework to build this custom node. The implementation is fairly trivial:
- The class has just a single custom property for movement speed
- Designated initializer
- And the required initializer
We conformed to all two of the marker
protocols: Interactable
and Destructible
since we designed our game in a way where the bunny enemies can not move around - they guard particular areas.
The next step is to create an another type for Player
:
class Player: SKSpriteNode, Destructible, PositionUpdatable {
// MARK: - Initializers
init(position: CGPoint, texture: SKTexture) {
super.init(texture: texture, color: .clear, size: texture.size())
self.position = position
}
required init?(coder aDecoder: NSCoder) {
fatalError("required init?(coder aDecoder: NSCoder) has not been implemented")
}
}
This class is even simpler and has just the designated and required initializers. However our player conforms to only two protocols: Destructible
- since player character can die and PositionUpdatable
- since the player can move around.
Marked game objects will be processed by systems
that are represented and structs that are responsible for processing nodes with specific markers.
struct InteractionSystem {
// MARK: - Properties
typealias InteractableSprite = SKSpriteNode & Interactable
private(set) var interactables: [InteractableSprite]
// MARK: - Initializers
init() {
interactables = []
}
// MARK: - Methods
mutating func add(interactable: InteractableSprite) {
interactables += [interactable]
}
func touchesBegan(touches: Set<UITouch>, with event: UIEvent?) {
for interactable in interactables {
interactable.touchesBegan(touches, with: event)
}
}
}
The first system is for Interactable
marker. InteractionSystem
delegates touch handling to the objects contained in interactables
array. In SpriteKit
scene this system in combination with marker
design pattern is used to decompose responsibilities for delegating touch events, which simplifies scenes and makes the touch handling code more easily maintained.
The next system will be DestructionSystem
:
struct DestructionSystem {
// MARK: - Properties
typealias DestructibleSprite = SKSpriteNode & Destructible
private(set) var destructibles: [DestructibleSprite]
// MARK: - Initializers
init() {
destructibles = []
}
private lazy var animationSequence: SKAction = {
let fadeOut = SKAction.fadeOut(withDuration: 1.0)
let remove = SKAction.removeFromParent()
let sequence = SKAction.sequence([fadeOut, remove])
return sequence
}()
// MARK: - Methods
mutating func add(destructible: DestructibleSprite) {
destructibles += [destructible]
}
mutating func destroy() {
for destructible in destructibles {
destructible.run(animationSequence)
}
}
}
The presented system works slightly differently: we append game objects that need to be destroyed and then at some point we call destroy
method to apply animation sequence to all of them. The system simplifies management of a scene, allows for custom extensions related to game object destruction and decomposes responsibilities into dedicated objects (hello Single Responsibility
principle).
And the final system that we are about to present will be PositionUpdatableSystem
:
struct PositionUpdatableSystem {
// MARK: - Properties
typealias PositionUpdatableSprite = SKSpriteNode & PositionUpdatable
private(set) var updatables: [PositionUpdatableSprite]
var inputSource: InputSource
// MARK: - Initializers
init(inputSource: InputSource) {
self.inputSource = inputSource
updatables = []
}
// MARK: - Methods
mutating func add(updatable: PositionUpdatableSprite) {
updatables += [updatable]
}
mutating func update() {
for updatable in updatables {
updatable.position = inputSource.move(currentPosition: updatable.position)
}
}
}
The final system works a bit differently than the others: it updates positions of the game objects with respect to the InputSource
, which can be an another subsystem that is responsible for handling movement of characters.
In order to use the systems we create them and our game objects:
// Systems
var interactionSystem = InteractionSystem()
var positionUpdatableSystem = PositionUpdatableSystem(inputSource: characterMovementController)
var destructionSystem = DestructionSystem()
// Game objects
let player = Player(position: CGPoint(x: 50, y: 120), texture: SKTexture(imageNamed: "player.png"))
let bunny = BunnyEnemy(position: CGPoint(x: 140, y: 165), texture: SKTexture(imageNamed: "bunny-mad.png"), movementSpeed: 3.24)
let bunnyFat = BunnyEnemy(position: CGPoint(x: 180, y: 165), texture: SKTexture(imageNamed: "bunny-fat"), movementSpeed: 1.03)
Then add them to the corresponding systems:
positionUpdatableSystem = PositionUpdatableSystem(inputSource: characterMovementController)
positionUpdatableSystem?.add(updatable: player)
interactionSystem.add(interactable: bunny)
interactionSystem.add(interactable: bunnyFat)
Interaction logic can be delegated using the corresponding method of SKScene
class:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
interactionSystem.touchesBegan(touches: touches, with: event)
}
And the other systems can process marked objects using their API
, like so:
override func update(_ currentTime: TimeInterval) {
positionUpdatableSystem?.update()
// ...
// At some point, enemies are added to the Destruction System, this piece of code simulates that:
if currentTime == 3840578 {
destructionSystem.add(destructible: bunny)
destructionSystem.add(destructible: bunnyFat)
}
// Later on, a bunny killed our player, so we add the player to the Destruction System to be destroyed:
destructionSystem.add(destructible: player)
// At the end of the update loop we call the destroy method to delegate the destruction to the corresponding system
destructionSystem.destroy()
}
By using marker
/system
approach we have separated different responsibilities into systems by creating types that are marked by corresponding marker
protocols. Marker
protocols provided run-time metadata which then was used.
We could achieve similar results by various ways: implementing common functionality into protocols, instead of making them empty. However we would need to provide a common handler type anyways, that would store and delegate method calls to the destination points.
The main issue with Marker
pattern is that it's implemented using protocols, that defines contract to be conformed or implemented. Then all the types that inherit from the marked type also have that metadata associated with them. Basically the marker conformance cannot be undone for the child types which may lead to unexpected run-time issues, that are hard to debug.
The described issue can be eliminated with custom, user-defined attributes which are not yet supported by Swift - only pre-defined attributes can be used.
Marker
pattern may be used in cases when we don't need to actually implement functionality in protocols, instead implementing it in separate types that process the marked types. Such separation makes it very easy to create many processing types for multiple markers without the need to implement it in the protocol itself and then executing each instance somewhere else.
It provides run-time metadata along with types and can be easily, later on converted to regular protocols with methods, properties and extensions. However the pattern has some issues as well, in cases when the marked type has children that don't suppose to hold unneeded metadata.