entity component system - for app developers
TRANSCRIPT
Entity Component System
for App developers@iceX33
History & Facts
4 First known game 1998 Thief: The Dark Project
4 My first encounter 2012 With ECS
4 Articles that convinced me "What is an entity system framework for game development?" by Richard Lord
4 ObjectiveC library I used: Entitas
4 Two Apps I built with Entitas UIKonf-App & Resi App
DEMO
Entity | Component | System----> State <------> Behaviour
What is a Component
public protocol Component {}
public protocol UniqueComopnent : Component {}
Component is a value type
struct TickComponent : UniqueComopnent{ let value : UInt64}struct ElixirComponent : UniqueComopnent{ let value : Float}struct ConsumeElixirComponent : Component{ let value : Int}struct PauseComponent : UniqueComopnent{}struct JumpIntTimeComponent : UniqueComopnent{ let value : UInt64}
Entity is a collection of components
public final class Entity { private var _components : [ComponentId:Component] ...}
e.set(ConsumeElixirComponent(value:2))let amount = e.get(ConsumeElixirComponent.self)?.valuee.remove(ConsumeElixirComponent.self)
Components = Lego pieces
Entites = things you build with them
let ctx = Context()
let e = ctx.createEntity()e.set(NameComponent(value:"Maxim"))
let g = ctx.getEntityGroup(NameComponent.matcher)assert(g.count == 1)
e.set(NameComponent(value:"Leo"), overwrite:true)assert(g.count == 1)
e.remove(NameComponent.self)assert(g.count == 0)
What about behaviour
What is a System
public protocol ASystem : class {}
public protocol InitialiseSystem : ASystem { func initialize()}
public protocol ExecuteSystem : ASystem { func execute()}
public protocol CleanupSystem : ASystem { func cleanup()}
System Example
class TickUpdateSystem : InitialiseSystem, ExecuteSystem { let ctx : Context init(ctx : Context) { self.ctx = ctx } func initialize() { ctx.setUniqueEntityWith(TickComponent(value: 0)) } func execute() { guard ctx.hasUniqueComponent(PauseComponent.self) == false, let currentTick = ctx.uniqueComponent(TickComponent.self)?.value else { return } ctx.setUniqueEntityWith(TickComponent(value: currentTick + 1)) }}
Reactive System
Reactive System
public protocol ReactiveSystem : ExecuteSystem { var collector : Collector! {get set} var limit : Int {get} func execute(input : ArraySlice<Entity>)}public extension ReactiveSystem { func execute(){ if let collector = collector { let entities = collector.pull(limit) if entities.count > 0 { execute(input: entities) } } } var limit : Int { return -1 }}
Reactive System Exampleclass ElixirProduceSystem : InitialiseSystem, ReactiveSystem { var collector: Collector! let ctx : Context private let productionFrequency = 3 private let elixirCapacity : Float = 10 private let productionStep : Float = 0.01 private(set) var limit: Int = 1 init(ctx : Context) { self.ctx = ctx collector = Collector(group: ctx.entityGroup(TickComponent.matcher), changeType: .added) } func initialize() { ctx.setUniqueEntityWith(ElixirComponent(value: 0)) } func execute(input: ArraySlice<Entity>) { guard let tick = input.first?.get(TickComponent.self)?.value, (tick % UInt64(productionFrequency)) == 0, let elixirAmount = ctx.uniqueComponent(ElixirComponent.self)?.value else{ return } let newAmount = min(elixirCapacity, elixirAmount + productionStep) ctx.setUniqueEntityWith(ElixirComponent(value: newAmount)) }}
What about UI
UI Action
@IBAction func pauseResume(_ sender: UIButton) { if ctx.hasUniqueComponent(PauseComponent.self) { ctx.destroyUniqueEntity(PauseComponent.matcher) } else { ctx.setUniqueEntityWith(PauseComponent()) }}@IBAction func consumeAction(_ sender: UIButton) { ctx.createEntity().set(ConsumeElixirComponent(value: sender.tag))}@IBAction func timeTravel(_ sender: UISlider) { ctx.setUniqueEntityWith(JumpIntTimeComponent(value: UInt64(sender.value)))}
What about Reactive UI
protocol TickListener { func tickChanged(tick : UInt64)}struct TickListenerComponent : Component { let ref : TickListener}
protocol PauseListener { func pauseStateChanged(paused : Bool)}struct PauseListenerComponent : Component { let ref : PauseListener}
protocol ElixirListener { func elixirChanged(amount : Float)}struct ElixirListenerComponent : Component { let ref : ElixirListener}
ViewController is a listener
class ViewController: UIViewController, TickListener, ElixirListener, PauseListener {
...
override func viewDidLoad() { ctx.createEntity() .set(TickListenerComponent(ref: self)) .set(ElixirListenerComponent(ref: self)) .set(PauseListenerComponent(ref: self)) }
...}
Or if you like it less monolithic
struct ConsumeButtonController : PauseListener, ElixirListener { let consumeButton : UIButton let consumeButtonProgress: UIProgressView let ctx : Context
func pauseStateChanged(paused: Bool) { consumeButton.isEnabled = !paused }
func elixirChanged(amount: Float) { let paused = ctx.hasUniqueComponent(PauseComponent.self) consumeButton.isEnabled = consumeButton.tag <= Int(amount) && !paused consumeButtonProgress.progress = 1 - min(1, (amount / Float(consumeButton.tag))) }}
Reactive System + UIKitclass NotifyPauseListenersSystem : ReactiveSystem { var collector: Collector! let ctx : Context var limit: Int = 1 let listeners : Group init(ctx : Context) { self.ctx = ctx collector = Collector(group: ctx.entityGroup(PauseComponent.matcher), changeType: .addedAndRemoved) listeners = ctx.entityGroup(PauseListenerComponent.matcher) }
func execute(input: ArraySlice<Entity>) { let paused = ctx.hasUniqueComponent(PauseComponent.self) for e in listeners { e.get(PauseListenerComponent.self)? .ref.pauseStateChanged(paused: paused) } }}
Show me the code & tests
ECSvs.
MVC | MVVM | Rx | ReSwift
Bonus material
Questions?@iceX33
Thank you!