Skip to content

Commit

Permalink
Add Object Pooling (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
jthomperoo authored Oct 18, 2020
1 parent ac3c380 commit 0462888
Show file tree
Hide file tree
Showing 101 changed files with 2,968 additions and 657 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ and this project adheres to [Semantic
Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Object pooling, allows reusing objects in memory to avoid the garbage collection churn of create -> delete -> create.
This can help prevent stuttering due to minor and major garbage collection occurring between frames by reducing the
volume of objects that need garbage collected.
- `Vector` object is now poolable, with helper static functions added to `Vector`
- `New` -> provisions a `Vector` from the object pool if available, if not it creates a new instance.
- `Free` -> releases a `Vector` back into the object pool if available.
- `Init` -> initializes the `Vector` object pool to a specified size.
- `Renderable` object is now poolable, with similar helper static functions as `Vector`.
- `DispatchUntilEmpty` method added to `MessageBus`, allows repeated dispatching until the message queue is empty.
### Changed
- In `Polygon` -> `RectangleByDimensions`, `QuadByDimensions`, and `EllipseEstimation` all now represent center/origin
using two X and Y numbers rather than a vector object to avoid unneeded object creation.
- In `Ellipse` -> `Circle` represnts center using two X and Y numbers rather than a vector object to avoid unneeded
object creation.
- `Component` implements `IFreeable`, allowing component values to be freed upon removal/entity destruction. This
must be implemented on a per `Component` basis by overriding this function.
- The game loop now ensures all game logic messages are processed using `DispatchUntilEmpty` before executing any
render logic.

## [v0.9.0] - 2020-09-05
### Added
Expand Down
109 changes: 109 additions & 0 deletions docs/architecture/pooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Pooling

JamJar supports object pooling ([see the pooling documentation for an overview](../../documentation/pooling)), this
page will outline how pooling works in the engine, alongside how pooling can be added to another object.

Pooling in JamJar works with the following process:

1. Pool is initialized, set to a maximum size and filled with empty/blank objects.
2. An object is requested from the pool.

- If the pool is not initialized the normal object constructor is used and a new instance is created.

- If the pool does not have objects available the normal object constructor is used and a new instance is created.

- If the pool has objects available, it shifts the first item out of the pool, it calls that pooled objects
`Recycle` method, providing the required arguments. The object is marked as not in the pool anymore.

3. Object is used.
4. The object is freed.

- If the object is marked as already in the pool this does nothing.

- If the pool is not initialized this does nothing.

- If the pool is already full (at maximum size) this does nothing.

- If the pool has room the object is pushed into the pool and it is marked as in the pool to avoid duplicate
entries.

This approach means that the pool is a dynamic size, with the only limitation being the maximum size of the pool. This
strategy also means that any object can be pushed into the pool and reused, it does not have to be provisioned from
the pool in the first place. This allows a hybrid approach, with pooling used where available, and a fallback to
standard garbage collection if the pool is full or pooling is disabled.

## Making an Object Poolable

An object can be made poolable by extending the [Pooled] base class, which provides helper functions for object pooling,
and also implementing the [IPoolable] interface to add in `Recycle` and `Free` methods. The `Recycle` method should
take in the same arguments as the constructor, allowing objects to be reused; for example in the [Vector] object it is:

```typescript
public Recycle(x: number, y: number): Vector {
this.x = x;
this.y = y;
return this;
}
```

It is also good practice to provide some static helper methods for initialising the object pool, creating new objects
from the pool and freeing objects into the pool, for example the [Vector] class provides `New`, `Free`, and `Init`:

```typescript
private static POOL_KEY = "jamjar_vector";

public static New(x: number, y: number): Vector {
return this.new<Vector>(Vector.POOL_KEY, Vector, x, y);
}

public static Free(obj: Vector): void {
this.free(Vector.POOL_KEY, obj);
}

public static Init(size: number): void {
this.init(Vector.POOL_KEY, () => {
return Vector.New(0, 0);
}, size);
}
```

These static methods use the underlying methods provided by the [Pooled] base class. The `POOL_KEY` specifies the
unique key that should be used when specifying the pool to use.

## Object with Poolable Constituents/Subparts

If an object contains constituent parts that are poolable should provide a `Free` method, if the object itself is not
poolable it should implement the [IFreeable] interface to specify this. For example, [Component] objects are not
themselves poolable, but can contain pooled data, so it implements [IFreeable] to allow any pooled constituent objects
to be freed:

```typescript
abstract class Component implements IFreeable {
...
public Free(): void {
return;
}
}
```

The [Transform] class extends the [Component] class, it contains 3 poolable pieces of data (`position`, `scale` and
`previous`) so it overrides the `Free` method and calls each of these poolable objects' `Free` methods:

```typescript
class Transform extends Component {
...
public Free(): void {
this.position.Free();
this.previous.Free();
this.scale.Free();
}
}

```

[Pooled]: ../../reference/classes/pooled
[IPoolable]: ../../reference/interfaces/ipoolable
[IFreeable]: ../../reference/interfaces/ifreeable
[Vector]: ../../reference/classes/vector
[Component]: ../../reference/classes/component
[Transform]: ../../reference/classes/transform
12 changes: 6 additions & 6 deletions docs/documentation/camera.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Cameras are important parts of the JamJar game engine, they define how the game
world is viewed and rendered. A camera is a
[`Component`](../../reference/classes/component), any entity can be made into a
camera by simply adding a [`Camera`](../../reference/classes/camera) to it.
camera by simply adding a [`Camera`](../../reference/classes/camera) to it.

## Camera World Position

Expand Down Expand Up @@ -32,7 +32,7 @@ rendered. This is defined with two properties of the
[`viewportScale`](../../reference/classes/camera#viewportscale).
The [`viewportPosition`](../../reference/classes/camera#viewportposition) is the
position of the camera's viewport on the screen, with from the bottom left
`(-1,-1)` to top right `(1,1)` with `(0,0)` as the center.
`(-1,-1)` to top right `(1,1)` with `(0,0)` as the center.
The [`viewportScale`](../../reference/classes/camera#viewportscale) is the scale
of the camera's viewport, relative to the canvas/rendering surface. A viewport
scale of `(1,1)` would take up the entire canvas, while a scale of `(0.5, 0.5)`
Expand All @@ -44,11 +44,11 @@ would only take up half of the screen (width and height).

```typescript
const cameraEntity = new Entity(messageBus);
cameraEntity.Add(new Transform(new Vector(0, 0), new Vector(5, 5)));
cameraEntity.Add(new Transform(Vector.New(0, 0), Vector.New(5, 5)));
cameraEntity.Add(new Camera(
new Color(1, 0, 0, 1), // Red
new Vector(0.5, 0.5), // Top right
new Vector(0.5, 0.5), // Half width and height of canvas/screen
new Vector(160, 90) // 16*9 screen, show 160 * 90 world units
Vector.New(0.5, 0.5), // Top right
Vector.New(0.5, 0.5), // Half width and height of canvas/screen
Vector.New(160, 90) // 16*9 screen, show 160 * 90 world units
));
```
20 changes: 10 additions & 10 deletions docs/documentation/custom-shaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,11 @@ class ShaderGame extends Game {
ShaderAsset.FRAGMENT_TYPE,
`#version 300 es
precision mediump float;
in vec2 vTextureCoordinate;
out vec4 outColor;
void main() {
outColor = vec4(0,1,0,1);
}
Expand Down Expand Up @@ -111,11 +111,11 @@ class ShaderGame extends Game {
ShaderAsset.FRAGMENT_TYPE,
`#version 300 es
precision mediump float;
in vec2 vTextureCoordinate;
out vec4 outColor;
void main() {
outColor = vec4(0,1,0,1);
}
Expand All @@ -139,7 +139,7 @@ class ShaderGame extends Game {

// Create example entity
const example = new Entity(this.messageBus);
example.Add(new Transform(new Vector(0,0), new Vector(10,10)));
example.Add(new Transform(Vector.New(0,0), Vector.New(10,10)));

// Create sprite using a material with the previously loaded texture,
// alongside using our custom fragment shader with the default
Expand All @@ -148,8 +148,8 @@ class ShaderGame extends Game {
new Material({
texture: new Texture("example", Polygon.RectangleByDimensions(1,1)),
shaders: [ "example-shader", ShaderAsset.DEFAULT_TEXTURE_VERTEX_SHADER_NAME ]
}),
0,
}),
0,
Polygon.RectangleByDimensions(1,1)
));
}
Expand All @@ -162,4 +162,4 @@ class ShaderGame extends Game {

The full code for this guide can be found in
[`/example/custom_fragment_shader`](https://github.com/jamjarlabs/JamJar/tree/master/example/custom_fragment_shader)
in the main JamJar repository.
in the main JamJar repository.
14 changes: 7 additions & 7 deletions docs/documentation/image-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ attached [ImageRequest] to specify the image.

```typescript
this.messageBus.Publish(new Message<ImageRequest>(
ImageRequest.MESSAGE_REQUEST_LOAD,
ImageRequest.MESSAGE_REQUEST_LOAD,
new ImageRequest("bullet", "assets/bullet.png")
));
```
Expand All @@ -31,11 +31,11 @@ such as in a sprite:

```typescript
const bullet = new Entity();
bullet.Add(new Transform(new Vector(0,0), new Vector(5,5)));
bullet.Add(new Transform(Vector.New(0,0), Vector.New(5,5)));
bullet.Add(new Sprite(new Material(
new Texture(
"bullet",
Polygon.RectangleByPoints(new Vector(0,0), new Vector(1,1)).GetFloat32Array()
"bullet",
Polygon.RectangleByPoints(Vector.New(0,0), Vector.New(1,1)).GetFloat32Array()
)), 1)
);
```
Expand All @@ -49,9 +49,9 @@ options provided to the [ImageRequest] in the form of [ITextureOptions].

```typescript
this.messageBus.Publish(new Message<ImageRequest>(
ImageRequest.MESSAGE_REQUEST_LOAD,
ImageRequest.MESSAGE_REQUEST_LOAD,
new ImageRequest(
"bullet",
"bullet",
"assets/bullet.png",
{
minFilter: TextureFiltering.NEAREST,
Expand All @@ -71,4 +71,4 @@ on texture customisation choices.
[Sprite]:../../reference/classes/sprite
[ITextureOptions]:../../reference/interfaces/itextureoptions
[TextureFiltering]:../../reference/classes/texturefiltering
[Texture Options]:../texture-options
[Texture Options]:../texture-options
101 changes: 101 additions & 0 deletions docs/documentation/pooling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Pooling

Object pooling allows for reuse of objects, keeping free objects in a centralised *pool* of objects. Instead of
creating a new instance of the object and requesting memory for it, instead the existing object is overwritten in place
and used instead.

## Benefits

Object pooling is intended to reduce/avoid stuttering caused by garbage collection between frames. This can be a
problem if there are many object being created and destroyed per frame, object pooling addresses this by using global
static memory that allows objects to be created once and reused, rather than created, destroyed, and then created.

## Drawbacks

Object pooling can have some drawbacks:

- If the initialised object pool is large the game will use a big chunk of memory, this could be inefficient.
- Object pooling can come with a performance (CPU) hit, as new object creation requires more logic.

## Considerations

JamJar is designed to allow the optional use of object pools, they can be disabled and enabled easily - by choosing to
initialise the pools or not. If an object pool is not initialised then object pooling is skipped and normal instance
creation is used.

With the design of the JamJar pooling architecture if some logic forgets to free the object back into the object pool
this is not likely to cause a memory leak, the object will simply be garbage collected as normal. Since this fallback
uses normal garbage collection it is still recommended to free poolable objects when finished with them.

## Best Practices

When designing systems/game logic to support object pooling make sure any objects that are no longer used are freed up
and released into the object pool. This freeing of objects back into the pool is essential to allow object pooling to
work without using up the entire object pool wastefully.

When using pooled data inside a [Component] the pooled data can easily be released upon component removal/entity
destruction by implementing and overriding the `Free` method to ensure all pooled data is freed. For example in the
[Transform] component there are three poolable [Vector] data points (`position`, `previous`, and `scale`) which are
freed up:

```typescript
public Free(): void {
this.position.Free();
this.previous.Free();
this.scale.Free();
}
```

When requesting new objects it is good practice to use the pool method to provision them rather than directly using
the constructor - as this allows object pooling to be enabled or disabled by deciding to initialise the object pool or
not, rather than having to manually switch between the two methods of initialising the objects. See the examples below
for a more indepth explanation of creating objects using pooling vs using a constructor.

## Examples

Pooling is slightly abstract, and it mostly handled behind the scenes by the engine, so the interface for using object
pooling in your game is quite simple.

### Vector

Instead of using the constructor for creating a [Vector] instead use the `Vector.New` static method:

```typescript
const unpooled = new Vector(5, 3);

const pooled = Vector.New(5, 3);
```

Using `Vector.New` is preferred in all instances, as this will work even if object pooling is disabled - this means
that object pooling can be enabled or disabled in a single place by removing/adding pool initialization calls.

To initialize the [Vector] object pool, use `Vector.Init`:

```typescript
Vector.Init(500);
```

This initializes the [Vector] object pool with `500` blank objects, with the maximum pool size set to `500`.

To free a [Vector] after it has been used, use `Vector.Free`:

```typescript
// Create
const position = Vector.New(2, 1);

// Use the vector in some way
position.x += 5;

// Free
position.Free();
```

### Renderable

[Renderable] objects are lower level objects that support pooling, they follow the same static methods as the `Vector`
with `New`, `Free`, and `Init` all available.

[Component]: ../../reference/classes/component
[Transform]: ../../reference/classes/transform
[Vector]: ../../reference/classes/vector
[Renderable]: ../../reference/classes/renderable
Loading

0 comments on commit 0462888

Please sign in to comment.