I couldn’t help drafting up another example of how generic protocols could be useful. In this case, CollidesWith<T> is a generic protocol, and a generic extension implements weapon-collision logic for all entities that have hit points:
//MARK: Collision Detection
/// A generic protocol that describes how one kind of thing reacts to colliding with another kind of thing.
protocol CollidesWith<Other> {
var position: Vec3 { get, private(set) }
var velocity: Vec3 { get, private(set) }
func handleCollision(with other: Other)
}
/// If T: CollidesWith<U>, then implicitly U: CollidesWith<T>.
extension<T, U> T: CollidesWith<U> where U: CollidesWith<T> {
func handleCollision(with other: U) {
// Nothing happens by default.
// Can be refined by conformers.
}
}
// Entry point for collision system.
extension CollidesWith<Other> {
func collide(with other: Other) {
handleCollision(with: other)
Other.handleCollision(with: self)
}
}
/// All the Entities that can collide with each other.
var objects: [any CollidesWith]
func physicsTick(deltaT: Int) {
// Integrate object velocities and test for collisions.
let collisions = gatherIntersections(among: objects, over: deltaT)
for (object1, object2) in collisions {
// This single call will handle both aspects of the collision, even if one object doesn’t react.
object1.collide(with: object2)
}
// Apply (potentially modified) velocity.
for object in objects {
object.integratePosition(over: deltaT)
}
}
//MARK: Game Specific Logic
// The world has a ground plane.
struct Ground {
let elevation: Float
}
// Things can’t fall past the ground.
extension CollidesWith<Ground> {
func handleCollision(with ground: Ground) {
velocity.z = 0
}
}
// A Weapon is anything that can cause damage.
struct Weapon {
var damage: Int
}
// A protocol for anything that can be damaged or healed.
protocol HasHitPoints {
var hp: Int { get, private(set) }
}
// When a weapon strikes, it causes damage.
// This is a generic extension, implemented on all types that conform to CollidesWith<Weapon>.
extension<T: HasHitPoints> T where T: CollidesWith<Weapon> {
func handleCollision(with weapon: Weapon) {
hp -= weapon.damage
}
}
// A player loses when they run out of hit points.
// Notice how struct Player _only_ implements the game-over condition; weapon handling is handled by the generic extension above, but the hp setter is kept private.
struct Player: HasHitPoints, CollidesWith<Weapon>, CollidesWith<Ground> {
private var _hp: Int
var hp {
get { _hp }
private(set) {
// No resurrecting dead players!
if (_hp >= 0) {
_hp = newValue
}
}
didSet {
if _hp <= 0 {
gameOver()
}
}
}
}