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

A Reactive 3D Game Engine in Scala

Aleksandar Prokopec
June 23, 2014
7.8k

A Reactive 3D Game Engine in Scala

Most modern 3D game engines are written close to the metal in C++ to achieve smooth performance and stunning. Managed languages and runtimes are usually avoided for this task since they incur garbage collection lags and other performance penalties.

We decided to put this conventional wisdom to test with an experiment - we used an Rx-style reactive programming framework enriched with reactive collections and isolates in unison with a high-level OpenGL framework to build a modern 3D engine.

Game engines are traditionally written in low-level imperative style to achieve optimal performance. Such code can be hard to understand and maintain - the uprising reactive programming is much more natural for writing games, since game engines are in essence discrete event simulations. However, reactive programming comes with performance penalties that we overcome using Scala Specialization, inlining and efficient reactive data containers. Similarly, the OpenGL API exposes a plethora of low-level routines unfit for large scale development - a more structured approach to graphics programming with higher level programming abstractions is desired, but yields a higher performance cost. Through the use of Scala Macros we eliminate these inefficiencies while in the same time retaining the advantages of a structured graphics programming framework.

Result? A high-throughput reactive 3D real-time game engine achieving smooth 60FPS on modern hardware with high polygon counts, texture blending, GPU-based object instancing, and effects like ambient occlusion, shadow mapping and image filtering.

Aleksandar Prokopec

June 23, 2014
Tweet

Transcript

  1. 1
    A Reactive 3D Game Engine in Scala
    Aleksandar Prokopec
    @_axel22_

    View Slide

  2. 2
    What’s a game engine?

    View Slide

  3. 3
    Simulation

    View Slide

  4. 4
    Real-time simulation

    View Slide

  5. 5
    15 ms
    Real-time simulation

    View Slide

  6. 6
    Demo first!
    http://youtu.be/pRCzSRhifLs

    View Slide

  7. 7
    Input Simulator
    Interaction
    Renderer

    View Slide

  8. Reactive Collections
    http://reactive-collections.com
    8

    View Slide

  9. 9
    Reactive values

    View Slide

  10. Reactive[T]
    10

    View Slide

  11. val ticks: Reactive[Long]
    11
    ticks 1
    1
    2
    2
    3
    3
    4
    4
    60
    60
    61
    61

    View Slide

  12. ticks onEvent { x =>
    log.debug(s”tick no.$x”)
    }
    12
    1 2 3 4 60 61
    tick no.1
    tick no.2
    tick no.3
    tick no.4
    tick no.60
    tick no.61
    ...

    View Slide

  13. ticks foreach { x =>
    log.debug(s”tick no.$x”)
    }
    13
    1 2 3 4 60 61

    View Slide

  14. 14
    for (x <- ticks) {
    log.debug(s”tick no.$x”)
    }

    View Slide

  15. 15
    Reactive combinators

    View Slide

  16. for (x <- ticks) yield {
    x / 60
    }
    16

    View Slide

  17. val seconds: Reactive[Long] =
    for (x <- ticks) yield {
    x / 60
    }
    17

    View Slide

  18. 60
    61
    val seconds: Reactive[Long] =
    for (x <- ticks) yield {
    x / 60
    }
    18
    ticks 1
    1
    2
    2
    3
    3 60 61
    seconds
    0 0 0 1 1
    ticks
    seconds
    0
    0
    0
    1
    1

    View Slide

  19. val days: Reactive[Long] =
    seconds.map(_ / 86400)
    19

    View Slide

  20. val days: Reactive[Long] =
    seconds.map(_ / 86400)
    val secondsToday =
    20

    View Slide

  21. val days: Reactive[Long] =
    seconds.map(_ / 86400)
    val secondsToday =
    (seconds zip days) {
    (s, d) =>
    s – d * 86400
    }
    21

    View Slide

  22. val days: Reactive[Long] =
    seconds.map(_ / 86400)
    val secondsToday =
    (seconds zip days) {
    _ – _ * 86400
    }
    22
    seconds days
    secondsToday

    View Slide

  23. val angle =
    secondsInDay.map(angleFunc)
    23

    View Slide

  24. val angle =
    secondsInDay.map(angleFunc)
    val light =
    secondsInDay.map(lightFunc)
    24

    View Slide

  25. 25
    https://www.youtube.com/watch?v=5g7DvNEs6K8&feature=youtu.be
    Preview

    View Slide

  26. 26
    val rotate =
    keys
    a ↓
    shift ↓ a ↑ shift ↑
    pgup ↓ pgup ↑
    keys

    View Slide

  27. 27
    val rotate =
    keys.filter(_ == PAGEUP)
    a ↓
    shift ↓ a ↑ shift ↑
    pgup ↓ pgup ↑
    keys
    pgup ↓ pgup ↑
    filter

    View Slide

  28. 28
    val rotate =
    keys.filter(_ == PAGEUP)
    .map(_.down)
    a ↓
    shift ↓ a ↑ shift ↑
    pgup ↓ pgup ↑
    keys
    pgup ↓ pgup ↑
    filter
    true false
    map

    View Slide

  29. 29
    if (rotate()) viewAngle += 1
    true false
    map

    View Slide

  30. 30
    Signals

    View Slide

  31. 31
    Reactives are
    discrete

    View Slide

  32. 32
    Signals are
    continuous

    View Slide

  33. 33
    trait Signal[T]
    extends Reactive[T] {
    def apply(): T
    }

    View Slide

  34. 34
    val rotate =
    keys.filter(_ == PAGEUP)
    .map(_.down)
    .signal(false)
    true false
    map
    signal

    View Slide

  35. 35
    val rotate: Signal[Boolean] =
    keys.filter(_ == PAGEUP)
    .map(_.down)
    .signal(false)
    true false
    map
    signal

    View Slide

  36. 36
    val rotate: Signal[Boolean]
    val ticks: Reactive[Long]
    ticks

    View Slide

  37. 37
    val rotate: Signal[Boolean]
    val ticks: Reactive[Long]
    ticks
    rotate

    View Slide

  38. 38
    val rotate: Signal[Boolean]
    val ticks: Reactive[Long]
    val viewAngle: Signal[Double] =
    ticks
    rotate
    viewAngle

    View Slide

  39. 39
    List(1, 2, 3).scanLeft(0)(_ + _)

    View Slide

  40. 40
    List(1, 2, 3).scanLeft(0)(_ + _)
    → List(0, 1, 3, 6)

    View Slide

  41. 41
    def scanLeft[S](z: S)(f: (S, T) => S)
    : List[S]

    View Slide

  42. 42
    def scanLeft[S](z: S)(f: (S, T) => S)
    : List[S]
    def scanPast[S](z: S)(f: (S, T) => S)
    : Signal[S]

    View Slide

  43. 43
    val rotate: Signal[Boolean]
    val ticks: Reactive[Long]
    val viewAngle: Signal[Double] =
    ticks.scanPast(0.0)
    ticks
    rotate
    viewAngle

    View Slide

  44. 44
    val rotate: Signal[Boolean]
    val ticks: Reactive[Long]
    val viewAngle: Signal[Double] =
    ticks.scanPast(0.0) {
    (a, _) =>
    if (rotate()) a + 1 else a
    }
    ticks
    rotate
    viewAngle

    View Slide

  45. 45
    http://youtu.be/blG95W5uWQ8
    Preview

    View Slide

  46. 46
    val velocity =
    ticks.scanPast(0.0) {
    (v, _) =>
    }
    val viewAngle =

    View Slide

  47. 47
    val velocity =
    ticks.scanPast(0.0) {
    (v, _) =>
    if (rotate()) v + 1
    }
    val viewAngle =

    View Slide

  48. 48
    val velocity =
    ticks.scanPast(0.0) {
    (v, _) =>
    if (rotate()) v + 1
    else v – 0.5
    }
    val viewAngle =

    View Slide

  49. 49
    val velocity =
    ticks.scanPast(0.0) {
    (v, _) =>
    if (rotate()) v + 1
    else v – 0.5
    }
    val viewAngle =
    velocity.scanPast(0.0)(_ + _)

    View Slide

  50. 50
    http://youtu.be/NMVhirZLWmA
    Preview

    View Slide

  51. 51
    Higher-order
    reactive values

    View Slide

  52. 52
    (T => S) => (List[S] => List[T])

    View Slide

  53. 53
    (T => S) => (List[S] => List[T])
    Reactive[Reactive[S]]

    View Slide

  54. 54
    val mids = mouse
    .filter(_.button == MIDDLE)
    mids ↓ ↓ ↓
    ↑ ↑ ↑

    View Slide

  55. 55
    val mids = mouse
    .filter(_.button == MIDDLE)
    val up = mids.filter(!_.down)
    val down = mids.filter(_.down)
    mids ↓ ↓ ↓
    up
    down ↓ ↓ ↓
    ↑ ↑ ↑
    ↑ ↑ ↑

    View Slide

  56. 56
    val mids = mouse
    .filter(_.button == MIDDLE)
    val up = mids.filter(!_.down)
    val down = mids.filter(_.down)
    val drags = down
    .map(_ => mouse)
    up
    down ↓ ↓ ↓
    ↑ ↑ ↑

    View Slide

  57. 57
    val mids = mouse
    .filter(_.button == MIDDLE)
    val up = mids.filter(!_.down)
    val down = mids.filter(_.down)
    val drags = down
    .map(_ => mouse)
    up
    down ↓ ↓ ↓
    ↑ ↑ ↑
    Reactive[Reactive[MouseEvent]]

    View Slide

  58. 58
    val mids = mouse
    .filter(_.button == MIDDLE)
    val up = mids.filter(!_.down)
    val down = mids.filter(_.down)
    val drags = down
    .map(_ => mouse)
    up
    down ↓ ↓ ↓
    ↑ ↑ ↑
    drags

    View Slide

  59. drags
    up ↑ ↑ ↑
    59
    val mids = mouse
    .filter(_.button == MIDDLE)
    val up = mids.filter(!_.down)
    val down = mids.filter(_.down)
    val drags = down
    .map(_ => mouse.until(up))
    down ↓ ↓ ↓
    mouse.until(up)

    View Slide

  60. 60
    val mids = mouse
    .filter(_.button == MIDDLE)
    val up = mids.filter(!_.down)
    val down = mids.filter(_.down)
    val drags = down
    .map(_ => mouse.until(up))
    down ↓ ↓ ↓
    up ↑ ↑ ↑
    drags

    View Slide

  61. 61
    val drags = down
    .map(_ => mouse.until(up))
    .map(_.map(_.xy))
    drags
    1, 1
    2, 3
    3, 5
    4, 6
    6, 9
    9, 9

    View Slide

  62. 62
    val drags = down
    .map(_ => mouse.until(up))
    .map(_.map(_.xy))
    .map(_.diffPast(_.xy - _.xy))
    drags
    0, 0
    1, 2
    1, 2
    0, 0
    2, 3
    0, 0

    View Slide

  63. 63
    val drags = down
    .map(_ => mouse.until(up))
    .map(_.map(_.xy))
    .map(_.diffPast(_.xy - _.xy))
    .concat()
    drags 0, 0 1, 2 1, 2 0, 0 2, 3 0, 0

    View Slide

  64. 64
    val drags = down
    .map(_ => mouse.until(up))
    .map(_.map(_.xy))
    .map(_.diffPast(_.xy - _.xy))
    .concat()
    val pos =
    drags.scanPast((0, 0))(_ + _)
    drags 0, 0 1, 2 1, 2 0, 0 2, 3 0, 0
    pos 0, 0 1, 2 2, 4 2, 4 4, 7 4, 7

    View Slide

  65. 65
    http://youtu.be/RsMSZ7OH2fo
    Preview
    However, a lot of object
    allocations lead to GC issues.

    View Slide

  66. 66
    Reactive mutators

    View Slide

  67. 67
    class Matrix {
    def apply(x: Int, y: Int): Double
    def update(x: Int, y: Int, v: Double)
    }

    View Slide

  68. 68
    val screenMat: Signal[Matrix] =
    (projMat zip viewMat)(_ * _)
    val invScreenMat =
    screenMat.map(_.inverse)

    View Slide

  69. 69
    Reactive[immutable.Matrix[T]]

    View Slide

  70. 70
    val screenMat: Signal[Matrix] =
    (projMat zip viewMat)(_ * _)
    val invScreenMat =
    screenMat.map(_.inverse)
    (4*4*8 + 16 + 16)*4*100 = 64 kb/s

    View Slide

  71. 71
    val screenMat = Mutable(new Matrix)
    (projMat, viewMat).mutate(screenMat) {
    (p, v) => screenMat().assignMul(p, v)
    }
    val invScreenMat = Mutable(new Matrix)
    screenMat.mutate(invScreenMat) {
    m => invScreenMat().assignInv(m)
    }

    View Slide

  72. 72
    Reactive collections

    View Slide

  73. 73
    http://youtu.be/ebbrAHNsexc
    Preview
    How do we model that a
    character is selected?

    View Slide

  74. 74
    val selected: Reactive[Set[Character]]

    View Slide

  75. 75
    val selected: ReactSet[Character]

    View Slide

  76. 76
    trait ReactSet[T]
    extends ReactContainer[T] {
    def apply(x: T): Boolean
    }

    View Slide

  77. 77
    trait ReactContainer[T] {
    def inserts: Reactive[T]
    def removes: Reactive[T]
    }

    View Slide

  78. 78
    A reactive collection
    is a pair
    of reactive values

    View Slide

  79. 79
    val selected =
    new ReactHashSet[Character]

    View Slide

  80. 80

    View Slide

  81. 81
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))

    View Slide

  82. 82

    View Slide

  83. 83
    class ReactContainer[T] { self =>
    def inserts: Reactive[T]
    def removes: Reactive[T]
    def map[S](f: T => S) =
    new ReactContainer[S] {
    def inserts: Reactive[T] =
    self.inserts.map(f)
    def removes: Reactive[T] =
    self.removes.map(f)
    }
    }

    View Slide

  84. 84
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .to[ReactHashMap]

    View Slide

  85. 85
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .to[ReactHashMap]

    View Slide

  86. 86
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .to[ReactHashMap]

    View Slide

  87. 87
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .to[ReactHashMap]

    View Slide

  88. 88
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .react.to[ReactHashMap]

    View Slide

  89. 89
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .react.to[ReactHashMap]

    View Slide

  90. 90
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .react.to[ReactHashMap]

    View Slide

  91. 91
    val selected =
    new ReactHashSet[Character]
    val decorations = selected
    .map(c => (c, decoFor(c)))
    .react.to[ReactHashMap]

    View Slide

  92. 92
    • reactive mutators
    • reactive collections
    • @specialized
    • Scala Macros
    • shipping computations to the GPU

    View Slide

  93. 93
    http://storm-enroute.com/macrogl/
    MacroGL

    View Slide

  94. 94
    https://www.youtube.com/watch?v=UHCeXdxkx70
    GC Preview

    View Slide

  95. 95
    Is Scala Ready?

    View Slide

  96. 96
    YES!

    View Slide

  97. 97
    Thank you!

    View Slide