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.9k

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. 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 ...
  2. 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
  3. val days: Reactive[Long] = seconds.map(_ / 86400) val secondsToday =

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

    (seconds zip days) { _ – _ * 86400 } 22 seconds days secondsToday
  5. 26 val rotate = keys a ↓ shift ↓ a

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

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

    shift ↓ a ↑ shift ↑ pgup ↓ pgup ↑ keys pgup ↓ pgup ↑ filter true false map
  8. 42 def scanLeft[S](z: S)(f: (S, T) => S) : List[S]

    def scanPast[S](z: S)(f: (S, T) => S) : Signal[S]
  9. 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
  10. 47 val velocity = ticks.scanPast(0.0) { (v, _) => if

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

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

    (rotate()) v + 1 else v – 0.5 } val viewAngle = velocity.scanPast(0.0)(_ + _)
  13. 55 val mids = mouse .filter(_.button == MIDDLE) val up

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

    = mids.filter(!_.down) val down = mids.filter(_.down) val drags = down .map(_ => mouse) up down ↓ ↓ ↓ ↑ ↑ ↑
  15. 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]]
  16. 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
  17. 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)
  18. 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
  19. 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
  20. 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
  21. 67 class Matrix { def apply(x: Int, y: Int): Double

    def update(x: Int, y: Int, v: Double) }
  22. 68 val screenMat: Signal[Matrix] = (projMat zip viewMat)(_ * _)

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

    val invScreenMat = screenMat.map(_.inverse) (4*4*8 + 16 + 16)*4*100 = 64 kb/s
  24. 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) }
  25. 80

  26. 82

  27. 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) } }
  28. 88 val selected = new ReactHashSet[Character] val decorations = selected

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

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

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

    .map(c => (c, decoFor(c))) .react.to[ReactHashMap]
  32. 92 • reactive mutators • reactive collections • @specialized •

    Scala Macros • shipping computations to the GPU