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)?.value
e.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 Example
class 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 + UIKit
class 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
ECS
vs.
MVC | MVVM | Rx | ReSwift
Bonus material
Questions?
@iceX33
Thank you!

Entity Component System - for App developers

  • 1.
    Entity Component System forApp developers @iceX33
  • 2.
    History & Facts 4First 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
  • 3.
  • 4.
    Entity | Component| System ----> State <------> Behaviour
  • 5.
    What is aComponent public protocol Component {} public protocol UniqueComopnent : Component {}
  • 6.
    Component is avalue 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 }
  • 7.
    Entity is acollection of components public final class Entity { private var _components : [ComponentId:Component] ... } e.set(ConsumeElixirComponent(value:2)) let amount = e.get(ConsumeElixirComponent.self)?.value e.remove(ConsumeElixirComponent.self)
  • 8.
    Components = Legopieces Entites = things you build with them
  • 14.
    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)
  • 15.
  • 17.
    What is aSystem public protocol ASystem : class {} public protocol InitialiseSystem : ASystem { func initialize() } public protocol ExecuteSystem : ASystem { func execute() } public protocol CleanupSystem : ASystem { func cleanup() }
  • 18.
    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)) } }
  • 19.
  • 20.
    Reactive System public protocolReactiveSystem : 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 } }
  • 21.
    Reactive System Example classElixirProduceSystem : 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)) } }
  • 22.
  • 24.
    UI Action @IBAction funcpauseResume(_ 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))) }
  • 25.
  • 27.
    protocol TickListener { functickChanged(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 }
  • 28.
    ViewController is alistener class ViewController: UIViewController, TickListener, ElixirListener, PauseListener { ... override func viewDidLoad() { ctx.createEntity() .set(TickListenerComponent(ref: self)) .set(ElixirListenerComponent(ref: self)) .set(PauseListenerComponent(ref: self)) } ... }
  • 29.
    Or if youlike 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))) } }
  • 30.
    Reactive System +UIKit class 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) } } }
  • 31.
    Show me thecode & tests
  • 32.
    ECS vs. MVC | MVVM| Rx | ReSwift
  • 33.
  • 34.
  • 35.