Steering
steering creatures (Vehicle): a group follows a wavy loop while keeping their distance from each other, wanderers roam and leave trails, and a pursuer intercepts the lead follower
- Run
swift run --package-path Examples Example-Motion-Steering- Source
- Examples/Motion/Steering/Sketch.swift
import Ollin
/// Steering behaviors: creatures moved by composable forces. A troop of
/// followers moves along a wavy loop (`follow(path:)`), wanderers roam the middle
/// leaving trails (`wander` plus `contain`), and one pursuer hunts the lead
/// follower by aiming where it *will* be (`pursue`). Every motion here is the
/// same move with a different target: aim at full speed toward what you want,
/// subtract where you're already going, cap the turn.
@main
final class Steering: Sketch {
private var path: [Vector2] = []
private var followers: [Vehicle] = []
private var wanderers: [Vehicle] = []
private var trails: [[Vector2]] = []
private var pursuer: Vehicle?
override func setup() {
seed(11)
// A wavy closed loop built on looping noise, so it meets itself.
let center = center
path = (0 ..< 160).map { i in
let t = Double(i) / 160
let wobble = signedNoise(0, loop: t, radius: 1.6) * 90 * scale
return center + Vector2(angle: t * .tau) * (360 * scale + wobble)
}
followers = (0 ..< 7).map { i in
let start = path[i * 20]
return Vehicle(at: start, velocity: Vector2(angle: Double(i), length: 2),
maxSpeed: (3.4 + 0.2 * Double(i % 3)) * scale, maxForce: 0.16 * scale,
seed: i)
}
wanderers = (0 ..< 5).map { i in
Vehicle(at: center + Vector2(angle: Double(i) * 1.3, length: 120 * scale),
velocity: Vector2(angle: Double(i) * 2.1, length: 2),
maxSpeed: 2.6 * scale, maxForce: 0.1 * scale, seed: 100 + i)
}
trails = wanderers.map { _ in [] }
pursuer = Vehicle(at: center, maxSpeed: 3.1 * scale, maxForce: 0.09 * scale, seed: 42)
}
override func draw() {
background(Color(hex: 0x0E1016))
// The loop the followers travel, drawn as a faint corridor.
noFill()
stroke(Color(hex: 0x2A3040))
strokeWeight(30 * scale)
drawPolygon(path)
stroke(Color(hex: 0x4A5468))
strokeWeight(1.5 * scale)
drawPolygon(path)
// Wanderers roam the middle and leave trails.
for (i, creature) in wanderers.enumerated() {
creature.applyForce(creature.wander(radius: 30 * scale, distance: 90 * scale))
creature.applyForce(creature.contain(in: bounds.inset(by: .all(220 * scale))) * 1.6)
creature.step()
trails[i].append(creature.position)
if trails[i].count > 90 { trails[i].removeFirst() }
}
noFill()
strokeWeight(2 * scale)
for (i, trail) in trails.enumerated() where trail.count > 1 {
stroke(Color(hue: 0.52 + Double(i) * 0.03, saturation: 0.5, brightness: 0.9).withAlpha(0.35))
drawPolyline(trail)
}
noStroke()
for (i, creature) in wanderers.enumerated() {
fill(Color(hue: 0.52 + Double(i) * 0.03, saturation: 0.5, brightness: 0.95))
drawVehicle(creature, size: 10 * scale)
}
// Followers stay in the corridor; each also keeps a little personal
// space so the troop reads as creatures, not a train.
for creature in followers {
creature.applyForce(creature.follow(path: path, radius: 14 * scale,
lookAhead: 60 * scale, closed: true))
creature.applyForce(creature.separate(from: followers, radius: 26 * scale) * 1.2)
creature.step()
}
for (i, creature) in followers.enumerated() {
fill(Color(hue: 0.09 + Double(i) * 0.012, saturation: 0.72, brightness: 0.97))
drawVehicle(creature, size: 12 * scale)
}
// The pursuer aims where the lead follower will be, not where it is.
if let pursuer, let prey = followers.first {
pursuer.applyForce(pursuer.pursue(prey))
pursuer.step()
fill(Color(hex: 0xE8586B))
drawVehicle(pursuer, size: 15 * scale)
}
}
}