Presentation Experiment: 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.

No need to spread Fear, Uncertainty and Doubt about performance anymore - Scala is ready for modern real-time game engines, delivering great performance with the convenience of the modern managed language.

Speakers


PDF: slides.pdf

Slides

A Reactive 3D Game Engine in

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

Parallel

Parallel Collections ScalaMeter ScalaBlitz

What’s a game engine?

What’s a game engine?

Simulation

Simulation

Real-time simulation

Real-time simulation

Real-time simulation

Real-time simulation 15 ms

Demo first!

Demo first!

Renderer

Renderer Interaction Input Simulator

Reactive Collections

Reactive Collections http://reactive-collections.com

Reactive mutators

Reactive mutators

Reactive[T]

Reactive[T]

1

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

1

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

1

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

for (x <- ticks) {

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

Reactive combinators

Reactive combinators

for (x <- ticks) yield {

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

val seconds: Reactive[Long] =

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

ticks

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

val days: Reactive[Long] =

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

val days: Reactive[Long] =

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

val days: Reactive[Long] =

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

seconds

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

val angle =

val angle = secondsInDay.map(angleFunc)

val angle =

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

keys

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

keys

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

keys

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

map

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

Signals

Signals

Reactives are

Reactives are discrete

Signals are

Signals are continuous

trait Signal[T]

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

map

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

map

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

ticks

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

ticks

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

ticks

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

ticks

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

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

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

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

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

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

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

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

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

ticks

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

ticks

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

val velocity =

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

val velocity =

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

val velocity =

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

val velocity =

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

Higher-order

Higher-order reactive values

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

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

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

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

mids

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

mids

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

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

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

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

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

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

drags

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

drags

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

drags

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

drags

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 val drags = down .map(_ => mouse.until(up)) .map(_.map(_.xy)) .map(_.diffPast(_.xy - _.xy)) .concat() val pos = drags.scanPast((0, 0))(_ + _)

Reactive mutators

Reactive mutators

class Matrix {

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

val screenMat: Signal[Matrix] =

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

Reactive[immutable.Matrix[T]]

Reactive[immutable.Matrix[T]]

val screenMat: Signal[Matrix] =

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

val screenMat = Mutable(new Matrix)

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) }

Reactive collections

Reactive collections

val selected: Reactive[Set[Character]]

val selected: Reactive[Set[Character]]

val selected: ReactSet[Character]

val selected: ReactSet[Character]

trait ReactSet[T]

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

trait ReactContainer[T] {

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

A reactive collection

A reactive collection is a pair of reactive values

val selected =

val selected = new ReactHashSet[Character]

val selected =

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

val selected =

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

class ReactContainer[T] { self =>

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) } }

val selected =

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

val selected =

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

val selected =

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

val selected =

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

val selected =

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

val selected =

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

val selected =

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

• reactive mutators

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

MacroGL

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

YES!

YES!

Thank you!

Thank you!