Skip to content

Scene and Entity Serialization #18

@grischaerbe

Description

@grischaerbe

Scene and Entity Serialization

Currently, when working with Phatty, you probably maintain models next to your entities which hold initial data, state or configuration, such as:

class Ball {
  constructor(public x: number, public y: number) {}
}

and use these models to build a scene:

const ball = new Ball(10, 20)
const ballEntity = this.entities.create()
ballEntity.components.add(TransformComponent, ball.x, ball.y)

It would be great to be able to serialize and deserialize (i.e. save and load) certain/all entities of a scene and let phatty do the job of setting up a scene once (e.g. in a level designer), saving that state and loading the state later:

// load the initial state of a scene
const level = scene.entities.load(serializedScene)

// later, save the game state
const savegame = scene.entities.save()

// or make a prefab
const prefab = ballEntity.save()

// load a prefab
scene.entities.add(prefab)

On top of that, serialization would allow a proper visual entity editor.

How would that work?

This issue is a first sketch of the public facing API and some thoughts about how to get there.

Internally, we need a place where all used components are stored. You may register your components with a defineComponents function:

import { defineComponents } from 'phatty'

defineComponents({
  TransformComponent,
  EnemyComponent,
  PathfindingComponent
})

The Component class would need an overhaul. In the style of Unity, public properties which are of a certain type (numbers and strings first? or properties marked as serializable with a @serialize class property decorator? bring your own serializer?) are serialized, meaning this is how a TransformComponent would look like:

import { Component } from 'phatty'

export class TransformComponent extends Component {
  @serialize()
  public x = 0

  @serialize()
  public y = 0

  public container!: Phaser.GameObjects.Container

  create() {
    this.container = this.entity.scene.add.container(this.x, this.y)
  }

  public destroy(): void {
    this.container.destroy()
  }
}

We can serialize this (roughly) as follows:

const constructor = getConstructorFromComponentMap(transformComponentInstance)
// → 'TransformComponent'
const data = {}
Object.keys(serializableKeysOfConstructor(constructor)).forEach((key) => {
  data[key] =   serialize(transformComponentInstance[key])
})
// → { x: 0, y: 0 }

Internally, a serialized version of this would be:

const serializedComponent = {
  constructor: 'TransformComponent', // the key in the component map
  data: {
    x: 0,
    y: 0,
  }
}

and it would be loaded like this:

const componentInstance = new componentMap[serializedComponent.constructor]()
Object.entries(serializedComponent).forEach(([key, value]) => {
  componentInstance[key] = deserialize(value)
})

The serialization of an entity is just an array of serialized components:

{
  "components": [
    {
      "constructor": "TransformComponent",
      "data": {
        "x": 0,
        "y": 0
      }
    },
    {
      "constructor": "RigidBodyComponent",
      "data": {
        "shape": "circle",
        "radius": 35
      }
    }
  ]
}

Now because we cannot really make use of constructor arguments, a constructor is not allowed in components and create is the new constructor (as seen above); all public properties have been assigned at this point. This isn't the best DX but it's as good as it gets.

Now iterating over all entities of a scene and serializing all components of each entity gives us a proper scene representation. Iterating over a single entity and serializing that outputs a prefab, neat!

Visual editing

Now that we know how to create entities including components and their data, we can make use of that and offer visual editing in the style of a scene outline showing all entities and an inspector, offering an editing experience just like the unity editor which allows you to create entities, add components to them and edit the public/serialized properties of a component. It's much work for sure but serialization is the first step towards that.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions