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.

Be732ee41fd3038aa98a0a7e7b7be081?s=128

Denis Defreyne

December 04, 2014
Tweet

Transcript

  1. Creating games with entities and components Denis Defreyne / RUG::B

    / December 4, 2014 1
  2. I make games*! 2 * Or at least I try.

  3. 3

  4. 4

  5. 5

  6. 6

  7. Less about games. More about paradigms. 7

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

  9. 9

  10. Put your data first. 10

  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
  12. In object orientation, objects encapsulate data. 12

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

    13
  14. Encapsulation is not data-driven. 14

  15. Object orientation is not data-driven. 15

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

  17. Objects combine data with behavior. 17

  18. Games have data position, velocity, health, … Games have behaviors

    movement, combat, AI, … 18
  19. How does the game data get its behavior? 19

  20. class Spaceship end 20

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

    21
  22. class Spaceship def update # Move @position_x += @velocity_x @position_y

    += @velocity_y # … end end 22
  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
  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
  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
  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
  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
  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
  29. class Spaceship < Movable … end 29 ?

  30. class Spaceship include Movable include Decoration … end 30 ?

  31. 31

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

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

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

  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
  36. Entities are stupid objects; they’re just structs. 36

  37. How do these entities get their behaviors? 37

  38. Behaviors are handled by systems. 38

  39. A system is essentially a procedure* that is called thirty

    times per second. 39 * A function with only side effects
  40. If you mutate state, you might as well be explicit

    about it. 40
  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
  42. class MovementSystem
 def update(entity)
 entity.position_x += entity.velocity_x entity.position_y += entity.velocity_y

    end end 42
  43. class MovementSystem class HealthSystem class InputSystem class AISteeringSystem class RenderingSystem

    class … 43
  44. Systems are decoupled. 44

  45. Data is stored in components. 45

  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
  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
  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
  49. Components have no behavior. 49

  50. Entities are essentially just collections of components. 50

  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
  52. spaceship = Entity.new 52

  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
  54. class MovementSystem def update(entity) entity[Position] += entity[Velocity] end end 54

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

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

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

    57 WIN #1
  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
  59. It gives us data locality*! 59 * Not so much

    in Ruby. WIN #2
  60. 60 HD RAM Cache ~ 1 000 000 ns ~

    1-10 ns ~ 500 ns
  61. 61

  62. 62

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

    63
  64. 64 position velocity rotation armor shield

  65. 65 position velocity 14 used bytes / 32 total bytes

    = 44% efficiency (for movement system) rotation armor shield
  66. 66

  67. 67

  68. Store components in contiguous arrays. 68

  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
  70. Smart use of the CPU cache can lead to a

    50x speedup! 70 * Source: Game Programming Patterns by Robert Nystrom
  71. Saving and loading becomes trivial! 71 WIN #3

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

  73. Level data becomes plaintext! 73 WIN #4

  74. { "position": [400, 200], "velocity": [0, 0], "shield": {"cur": 100,

    "max": 100, "rate": 2}, "armor": {"cur": 50, "max": 50} } 74
  75. In-game editors become easy to write! 75 WIN #5

  76. 76

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

  78. DEMO 78

  79. 79 @ddfreyne denis@stoneship.org Denis Defreyne, expert game dev newbie.


  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.