Previous 11 Next

Memory management

TypeScript is transpiled into JavaScript, and JavaScript is a garbage collected language. This is not a problem for most applications, however it can be for games. When the garbage collector starts to run it might use up the time we have to update and draw the game, causing the game to stutter.

Games often allocate a large number of short lived objects. This causes the memory usage to look like a sawtooth where the garbadge collector needs to run frequently.

Graph of memory usage showing a sawtooth pattern
Sawtooth pattern

Object Pooling

Object Pooling tries to reuse already created objects.

src/ObjectPool.ts
export class ObjectPool<T, V extends any[]> {
  private objects: T[] = []
  constructor(
    private newObject: (...v: V) => T,
    private initializeObject: (t: T, ...v: V) => void
  ) {}

  get(...args: V): T {
    let object = this.objects.pop()
    if (object == null) return this.newObject(...args)
    this.initializeObject(object, ...args)
    return object
  }

  add(object: T) {
    this.objects.push(object)
  }
}

Instead of creating a new object we get one from the pool, however, when we are done with the object it should be added back to the pool.

const vector2Pool = new ObjectPool(
  () => new Vector2,
  (v, x: number, y: number) => {
    v.x = x
    v.y = y
  })
const vector = vector2Pool.get(50, 50)
// ...
// ...
vector2Pool.add(vector)

Managed pools

Having to remember to add back all the objects we get from a object pool is error prone. It can be easier to just say that all the objects that have been taken out, should be added back.

src/ObjectPoolManager.ts
import { ObjectPool } from "./ObjectPool";

export class ObjectPoolManager<T, V extends any[]> {
    private objects: T[] = []
    constructor(
      private objectPool: ObjectPool<T, V>
    ) {}
  
    get(...args: V) {
      const object = this.objectPool.get(...args)
      this.objects.push(object)
      return object
    }
  
    release() {
      while(true) {
        let object = this.objects.pop()
        if (object == undefined) return
        this.objectPool.add(object)
      }
    }
  }

Temporary objects

Temporary objects are only valid for a single frame. We are not allowed to keep refrences them across frames, since the they are released at the end of the frame.

src/temporaryObjects.ts
import { ObjectPoolManager } from "./ObjectPoolManager";
import { ObjectPool } from "./ObjectPool";
import { Vector2 } from "./Vector2";
import { Rectangle } from "./Rectangle";
import { Line } from "./Line";

export const tVector2 = new ObjectPoolManager(
  new ObjectPool(
    (x: number, y: number) => new Vector2(x, y),
    (vector, x: number, y: number) => {
      vector.x = x
      vector.y = y
    }
  )
)

export const tRectangle = new ObjectPoolManager(
  new ObjectPool(
    (x: number, y: number, width: number, height: number) => new Rectangle(x, y, width, height),
    (rectangle, x: number, y: number, width: number, height: number) => {
      rectangle.x = x
      rectangle.y = y
      rectangle.width = width
      rectangle.height = height
    }
  )
)

export const tLine = new ObjectPoolManager(
  new ObjectPool(
    (p0: Vector2, p1: Vector2) => new Line(p0, p1),
    (rectangle, p0: Vector2, p1: Vector2) => {
      rectangle.p0 = p0
      rectangle.p1 = p1
    }
  )
)

const temporaryPools = [
  tVector2, tRectangle, tLine
]

export function releaseTemporaryPools() {
  for (const pool of temporaryPools) pool.release()
}

Links

Previous 11 Next