Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Creating games with entities and components

Creating games with entities and components

Ruby is an object-oriented language, and that is the paradigm we mainly use to write software.

Object orientation is not always the best solution for some situations. In real-time simulations, such as real-time games, techniques such as inheritance, modules, traits, etc break down.

This talk explains the entity-component-system architecture, a common pattern in modern game architecture, which explicitly breaks away from object orientation to achieve large gains in flexibility and speed.

Denis Defreyne

December 04, 2014
Tweet

More Decks by Denis Defreyne

Other Decks in Technology

Transcript

  1. Creating games with
    entities and components
    Denis Defreyne / RUG::B / December 4, 2014
    1

    View Slide

  2. I make games*!
    2
    * Or at least I try.

    View Slide

  3. 3

    View Slide

  4. 4

    View Slide

  5. 5

    View Slide

  6. 6

    View Slide

  7. Less about games.
    More about paradigms.
    7

    View Slide

  8. I am not a professional
    game developer.
    8
    DISCLAIMER

    View Slide

  9. 9

    View Slide

  10. Put your data first.
    10

    View Slide

  11. If all you have is the data,

    you know what it’s about.

    If all you have is the behavior,

    you’re still in the dark.
    11

    View Slide

  12. In object orientation,
    objects encapsulate data.
    12

    View Slide

  13. Objects are defined
    by their methods,
    not by their data.
    13

    View Slide

  14. Encapsulation
    is not data-driven.
    14

    View Slide

  15. Object orientation
    is not data-driven.
    15

    View Slide

  16. You can be data-driven
    in an object-oriented language.
    16

    View Slide

  17. Objects combine
    data with behavior.
    17

    View Slide

  18. Games have data
    position, velocity, health, …
    Games have behaviors
    movement, combat, AI, …
    18

    View Slide

  19. How does the game data
    get its behavior?
    19

    View Slide

  20. class Spaceship
    end
    20

    View Slide

  21. class Spaceship
    def update
    # TODO: Write me
    end
    end
    21

    View Slide

  22. class Spaceship
    def update
    # Move
    @position_x += @velocity_x
    @position_y += @velocity_y
    # …
    end
    end
    22

    View Slide

  23. class Spaceship
    def update
    # Move
    @position_x += @velocity_x
    @position_y += @velocity_y
    # Calculate acceleration
    @acceleration = 0.0
    if Keyboard.key_down?('w')
    @acceleration = 10.0
    # TODO: Play accelerate sound
    elsif Keyboard.key_down?('s')
    @acceleration = -3.0
    end
    # …
    end
    end
    23

    View Slide

  24. class Spaceship
    def update
    # Move
    @position_x += @velocity_x
    @position_y += @velocity_y
    # Calculate acceleration
    @acceleration = 0.0
    if Keyboard.key_down?('w')
    @acceleration = 10.0
    # TODO: Play accelerate sound
    elsif Keyboard.key_down?('s')
    @acceleration = -3.0
    end
    # Rotate
    if Keyboard.key_down?('a')
    @rotation -= 5
    elsif Keyboard.key_down?('d')
    @rotation += 5
    end
    # TODO: Add rotational velocity
    # …
    end
    end
    24

    View Slide

  25. class Spaceship
    def update
    # Move
    @position_x += @velocity_x
    @position_y += @velocity_y
    # Calculate acceleration
    @acceleration = 0.0
    if Keyboard.key_down?('w')
    @acceleration = 10.0
    # TODO: Play accelerate sound
    elsif Keyboard.key_down?('s')
    @acceleration = -3.0
    end
    # Rotate
    if Keyboard.key_down?('a')
    @rotation -= 5
    elsif Keyboard.key_down?('d')
    @rotation += 5
    end
    # TODO: Add rotational velocity
    # Accelerate
    @velocity_x += Math.cos(@rotation) * @acceleration
    @velocity_y += Math.sin(@rotation) * @acceleration
    # …
    end
    end
    25

    View Slide

  26. class Spaceship
    def update
    # Move
    @position_x += @velocity_x
    @position_y += @velocity_y
    # Calculate acceleration
    @acceleration = 0.0
    if Keyboard.key_down?('w')
    @acceleration = 10.0
    # TODO: Play accelerate sound
    elsif Keyboard.key_down?('s')
    @acceleration = -3.0
    end
    # Rotate
    if Keyboard.key_down?('a')
    @rotation -= 5
    elsif Keyboard.key_down?('d')
    @rotation += 5
    end
    # TODO: Add rotational velocity
    # Accelerate
    @velocity_x += Math.cos(@rotation) * @acceleration
    @velocity_y += Math.sin(@rotation) * @acceleration
    # Cap speed
    @velocity_x = [@velocity_x, @max_velocity_x].min
    @velocity_y = [@velocity_y, @max_velocity_y].min
    # …
    end
    end
    26

    View Slide

  27. class Spaceship
    def update
    # Move
    @position_x += @velocity_x
    @position_y += @velocity_y
    # Calculate acceleration
    @acceleration = 0.0
    if Keyboard.key_down?('w')
    @acceleration = 10.0
    # TODO: Play accelerate sound
    elsif Keyboard.key_down?('s')
    @acceleration = -3.0
    end
    # Rotate
    if Keyboard.key_down?('a')
    @rotation -= 5
    elsif Keyboard.key_down?('d')
    @rotation += 5
    end
    # TODO: Add rotational velocity
    # Accelerate
    @velocity_x += Math.cos(@rotation) * @acceleration
    @velocity_y += Math.sin(@rotation) * @acceleration
    # Cap speed
    @velocity_x = [@velocity_x, @max_velocity_x].min
    @velocity_y = [@velocity_y, @max_velocity_y].min
    # Render
    Graphics.translate(@position_x, @position_y) do
    Graphics.rotate(@rotation) do
    @sprite.draw
    end
    end
    end
    end
    27

    View Slide

  28. class Spaceship
    def update
    # Move
    @position_x += @velocity_x
    @position_y += @velocity_y
    # Calculate acceleration
    @acceleration = 0.0
    if Keyboard.key_down?('w')
    @acceleration = 10.0
    # TODO: Play accelerate sound
    elsif Keyboard.key_down?('s')
    @acceleration = -3.0
    end
    # Rotate
    if Keyboard.key_down?('a')
    @rotation -= 5
    elsif Keyboard.key_down?('d')
    @rotation += 5
    end
    # TODO: Add rotational velocity
    # Accelerate
    @velocity_x += Math.cos(@rotation) * @acceleration
    @velocity_y += Math.sin(@rotation) * @acceleration
    # Cap speed
    @velocity_x = [@velocity_x, @max_velocity_x].min
    @velocity_y = [@velocity_y, @max_velocity_y].min
    # Render
    Graphics.translate(@position_x, @position_y) do
    Graphics.rotate(@rotation) do
    if @acceleration > 0.0
    @flame_animation.step
    @flame_sprite = @flame_animation.sprite
    @flame_sprite.draw
    end
    @sprite.draw
    end
    end
    end
    end
    28

    View Slide

  29. class Spaceship < Movable

    end
    29
    ?

    View Slide

  30. class Spaceship
    include Movable
    include Decoration

    end
    30
    ?

    View Slide

  31. 31

    View Slide

  32. How can you be data-driven
    in an object oriented language?
    32

    View Slide

  33. A better approach:
    entities, components, systems.
    33

    View Slide

  34. Objects can have logic.
    That doesn’t mean they should.
    34

    View Slide

  35. class Spaceship
    attr_reader :position_x, :position_y
    attr_reader :velocity_x, :velocity_y
    attr_reader :acceleration_x, :acceleration_y
    attr_reader :rotation
    attr_reader :shield_cur, :shield_max, :shield_rate
    attr_reader :armor_cur, :armor_max
    def initialize(params = {})

    end
    end
    35

    View Slide

  36. Entities are stupid objects;
    they’re just structs.
    36

    View Slide

  37. How do these entities
    get their behaviors?
    37

    View Slide

  38. Behaviors are handled
    by systems.
    38

    View Slide

  39. A system is essentially
    a procedure* that is called
    thirty times per second.
    39
    * A function with only side effects

    View Slide

  40. If you mutate state,
    you might as well
    be explicit about it.
    40

    View Slide

  41. spaceship = Spaceship.new(

    position_x: 200,

    position_y: 150,

    velocity_x: 10,

    velocity_y: -5,

    armor_cur: 100,

    armor_max: 100)

    movement_system = MovementSystem.new
    movement_system.update(spaceship)

    p [spaceship.position_x, spaceship.position_y]
    # => [210, 145]
    41

    View Slide

  42. class MovementSystem

    def update(entity)

    entity.position_x += entity.velocity_x
    entity.position_y += entity.velocity_y
    end
    end
    42

    View Slide

  43. class MovementSystem
    class HealthSystem
    class InputSystem
    class AISteeringSystem
    class RenderingSystem
    class …
    43

    View Slide

  44. Systems are decoupled.
    44

    View Slide

  45. Data is stored in components.
    45

    View Slide

  46. class Spaceship
    attr_reader :position_x, :position_y
    attr_reader :velocity_x, :velocity_y
    attr_reader :acceleration_x, :acceleration_y
    attr_reader :rotation
    attr_reader :shield_cur, :shield_max, :shield_rate
    attr_reader :armor_cur, :armor_max
    def initialize(params = {})

    end
    end
    46

    View Slide

  47. class Spaceship
    attr_reader :position
    attr_reader :velocity
    attr_reader :acceleration
    attr_reader :rotation
    attr_reader :shield
    attr_reader :armor
    def initialize(params = {})

    end
    end
    47

    View Slide

  48. Position = Struct.new(:x, :y)
    Velocity = Struct.new(:x, :y)
    Acceleration = Struct.new(:x, :y)
    Rotation = Struct.new(:rad)
    Shield = Struct.new(:cur, :max, :rate)
    Armor = Struct.new(:cur, :max)
    48

    View Slide

  49. Components have no behavior.
    49

    View Slide

  50. Entities are essentially just
    collections of components.
    50

    View Slide

  51. class Spaceship
    attr_reader :position
    attr_reader :velocity
    attr_reader :acceleration
    attr_reader :rotation
    attr_reader :shield
    attr_reader :armor
    def initialize(params = {})

    end
    end
    51

    View Slide

  52. spaceship = Entity.new
    52

    View Slide

  53. spaceship = Entity.new
    spaceship.add(Position.new(400, 200))
    spaceship.add(Velocity.new(0, 0))
    spaceship.add(Acceleration.new(0, 0))
    spaceship.add(Rotation.new(Math::PI / 4))
    spaceship.add(Armor.new(100, 100))
    spaceship.add(Shield.new(100, 100, 10))
    53

    View Slide

  54. class MovementSystem
    def update(entity)
    entity[Position] += entity[Velocity]
    end
    end
    54

    View Slide

  55. Entities group components.
    Components contain data.
    Systems update components.
    55

    View Slide

  56. What are the advantages
    being data-driven?
    56

    View Slide

  57. It gives us the power to
    change behavior at runtime!
    57
    WIN #1

    View Slide

  58. # Become invisible
    spaceship.remove(Sprite)
    # Become mortal
    angel.add(Health.new(100))
    # Become a ghost
    asteroid.remove(CollisionShape)
    # Mind control enemy
    enemy.remove(AISteering)
    enemy.add(PlayerSteering)
    58

    View Slide

  59. It gives us data locality*!
    59
    * Not so much in Ruby.
    WIN #2

    View Slide

  60. 60
    HD
    RAM
    Cache
    ~ 1 000 000 ns
    ~ 1-10 ns
    ~ 500 ns

    View Slide

  61. 61

    View Slide

  62. 62

    View Slide

  63. To avoid cache misses,
    keep similar data
    together in memory.
    63

    View Slide

  64. 64
    position velocity rotation armor shield

    View Slide

  65. 65
    position velocity
    14 used bytes / 32 total bytes = 44% efficiency
    (for movement system)
    rotation armor shield

    View Slide

  66. 66

    View Slide

  67. 67

    View Slide

  68. Store components
    in contiguous arrays.
    68

    View Slide

  69. positions[47] = Position.new(400, 200)
    velocities[47] = Velocity.new(0, 0)
    shields[47] = Shield.new(cur: 100, max: 100, rate: 2)
    armors[47] = Armor.new(cur: 50, max: 50)
    69

    View Slide

  70. Smart use of the CPU cache
    can lead to a 50x speedup!
    70
    * Source: Game Programming Patterns by Robert Nystrom

    View Slide

  71. Saving and loading
    becomes trivial!
    71
    WIN #3

    View Slide

  72. # Save
    File.write('save.json', JSON.dump(@world))
    # Load
    @world = JSON.parse(File.read('save.json'))
    72

    View Slide

  73. Level data becomes plaintext!
    73
    WIN #4

    View Slide

  74. {
    "position": [400, 200],
    "velocity": [0, 0],
    "shield": {"cur": 100, "max": 100, "rate": 2},
    "armor": {"cur": 50, "max": 50}
    }
    74

    View Slide

  75. In-game editors
    become easy to write!
    75
    WIN #5

    View Slide

  76. 76

    View Slide

  77. Entities group components.
    Components contain data.
    Systems update components.
    77

    View Slide

  78. DEMO
    78

    View Slide

  79. 79
    @ddfreyne
    [email protected]
    Denis Defreyne,
    expert game dev newbie.


    View Slide

  80. 80
    This talk would not have been the same without some great assets that I
    could use, either for free or for a well-deserved donation.
    The fonts in this presentation are Clear Sans by Intel (01.org/clear-sans)
    and Ubuntu Mono by Canonical Ltd (font.ubuntu.com).
    Most of the sprites are by Kenney Vleugels (kenney.nl) and are part of the
    Kenney Donation Pack (kenney.itch.io/kenney-donation). The planet
    sprite in the 2D space shooter example is by Justin Nichol. Assets in the
    initial screenshot are by Sven Ahlgrimm.

    View Slide