DEV Community

Cover image for Game engine in web - part 2 - data oriented system
Ioannis Noukakis
Ioannis Noukakis

Posted on

Game engine in web - part 2 - data oriented system

Introduction

Hey hey folks, its been a while. But here I am again with the continuation of this series of posts about this 3D web engine. If you missed the first part: get it here: https://dev.to/ioannisnoukakis/3d-game-engine-in-web-part-1-4i4k.

To a data driven system

In the last part, we registered systems and components like this:

const threeJsContext = new ThreeJSContext();

scene.registerSystem(new TransformSystem());
scene.registerSystem(new ThreeJsDynamicMeshSystem(threeJsContext));
scene.registerSystem(new UpAndDownSinSystem());

// cube
const cubeId = "cube";
scene.addEntityForSystem<Transform>(TransformSystem.TYPE, {
    position: {x: 10, y: 10, z: 10,},
    rotation: {x: 0, y: 0, z: 0, w: 1,}
}, cubeId);
Enter fullscreen mode Exit fullscreen mode

Not really flexible if you ask me. Since we are on web we are going to refactor all of this into generic args and declarations so we can supply a JSON file to our engine that will then spawn the elements of our scene.

So first we need to define generic parameters for each system so those parameters do not rely on the implementation of said system.

This mean that rather than passing a

new BoxGeometry(5, 5, 5)
Enter fullscreen mode Exit fullscreen mode

but rather a

{
    type: "BoxGeometry",
    width: 5,
    height: 5,
    depth: 5,
}
Enter fullscreen mode Exit fullscreen mode

This has the advantage of decloupling the declarating part of the scene from the actual implementation. Meaning we can swap ThreeJs with our custom webgl adapter our change our physic library if we fancy.

I'm not going to show all definitions here but it looks generally something like this:

import {PackagedQuaternion} from "./Transform";

export type BodyType = "STATIC" | "DYNAMIC" | "KINEMATIC";

export interface PhysicsBase {
    type: BodyType;
}

export interface PhysicsPlane extends PhysicsBase {
    shape: "PLANE";
    rotation: PackagedQuaternion;
}

export interface PhysicsBox extends PhysicsBase {
    shape: "CUBE";
    sizeInMeter: number;
    massInKG: number;
}

export interface PhysicsSphere extends PhysicsBase {
    shape: "SPHERE";
    radiusInMeter: number;
    massInKG: number;
}

export type PhysicsArgs = {
    type: "PHYSIC",
    arg:
        | PhysicsPlane
        | PhysicsBox
        | PhysicsSphere
}
Enter fullscreen mode Exit fullscreen mode

So now we have generic definitions of how to build our scene objects. (see the whole code here: https://gitlab.noukakis.ch/voidbattlesengine/voidbattlesengineweb/-/tree/chapter-2/src/engine/systems/_meta)

Just something to note with quaternions here:
Because of numerical precision of JS and how its exported/red from JSON its better to export them as Euler angles like this:

export type PackagedQuaternion = {
    yaw: number,
    pitch: number,
    roll: number
}
Enter fullscreen mode Exit fullscreen mode

A parser and typed JSON

Now we have to parse the JSON containing our scene and validate that this JSON is valid according to what we expect. For that we are going to use the JSON Schema along with ts-json-schema-generator to generate said schema automatically and ajv to validate JSON against the generated schema.

We can get the schema using this command:

./node_modules/.bin/ts-json-schema-generator --path 'src/**/*.ts' --type 'SceneType'
Enter fullscreen mode Exit fullscreen mode

For now we are going to save the schema in a ts file but latter we will at some point automate the schema generation and bundling. Schema looks something like this

{
    "$ref": "#/definitions/SceneType",
    "$schema": "http://json-schema.org/draft-07/schema#",
    "definitions": {
        "BodyType": {
            "enum": [
                "STATIC",
                "DYNAMIC",
                "KINEMATIC"
            ],
            "type": "string"
            ...
Enter fullscreen mode Exit fullscreen mode

And now we are ready to write our "parser" usecase. This parser will have the responsibility to download the scene JSON, validate it against the schema and populate the existing systems with whatever entities are described in the scene JSON.

See the usecase, tests and adapters here: https://gitlab.noukakis.ch/voidbattlesengine/voidbattlesengineweb/-/tree/chapter-2/src/parser

Now we can just supply a JSON file to the system and get a scene out of it.

Now this:

{
  "cube": [
    {
      "type": "TRANSFORM",
      "position": {
        "x": 10,
        "y": 10,
        "z": 10
      },
      "rotation": {
        "yaw": 0,
        "pitch": 0,
        "roll": 0
      }
    },
    {
      "type": "RENDER",
      "geometry": {
        "type": "BoxGeometry",
        "width": 5,
        "height": 5,
        "depth": 5
      },
      "material": {
        "type": "MeshBasicMaterial",
        "color": 15131077
      }
    },
    {
      "type": "PHYSIC",
      "arg": {
        "type": "DYNAMIC",
        "shape": "CUBE",
        "sizeInMeter": 5,
        "massInKG": 1
      }
    }
  ],
  "floor": [
    {
      "type": "TRANSFORM",
      "position": {
        "x": 0,
        "y": -5,
        "z": 0
      },
      "rotation": {
        "yaw": 0,
        "pitch": 0,
        "roll": 0
      }
    },
    {
      "type": "RENDER",
      "geometry": {
        "type": "BoxGeometry",
        "width": 200,
        "height": 1,
        "depth": 200
      },
      "material": {
        "type": "MeshBasicMaterial",
        "color": 12375026
      },
      "corrections": [
        {
          "yaw": 0,
          "pitch": 0,
          "roll": -90
        }
      ]
    },
    {
      "type": "PHYSIC",
      "arg": {
        "type": "STATIC",
        "shape": "PLANE",
        "rotation": {
          "yaw": 0,
          "pitch": 0,
          "roll": -60
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Yields this:

Image description

Testing our renderer with cypress and screenshots

Now for the neat part (and also why we made all those refactors) is that we can test this app in an automated manner using testing automation tool such as cypress.

Using it we can define scenarios like "display a cube rotated 45 degrees on the X axis" and use screenshots to check for regression.

Image description

And that tests the renderer. Now I think we are ready to tacle on more advanced topics such as data driven animations, shaders, etc.

Until next time!
Image description

Top comments (0)