NateTheGreatt / bitECS

Flexible, minimal, data-oriented ECS library for Typescript
Mozilla Public License 2.0
942 stars 84 forks source link

Using serialization for saving and loading #67

Closed luetkemj closed 2 days ago

luetkemj commented 2 years ago

Serializing a world seems to work but deserializing throws the following error:

Uncaught TypeError: Cannot read properties of undefined (reading 'Symbol(storeBase)')
at bitecs.v0.3.34.js:472

Here's some repro code:

import { createWorld, defineSerializer, defineDeserializer } from "bitecs";

let packet;

export const save = (world) => {
  const serialize = defineSerializer(world);
  packet = serialize(world);
};

export const load = () => {
  let newWorld = createWorld();
  const deserialize = defineDeserializer(newWorld);
  deserialize(newWorld, packet);
  return newWorld;
};

I don't get an error if I use defineSerializer with same world that I serialized but I'm trying to figure out how that's useful. If I have the world already, there's no reason to deserialize.

Not sure if bug or I'm missing something...

NateTheGreatt commented 2 years ago

seems like a bug! thanks for reporting, i'll work on a fix ASAP

NateTheGreatt commented 2 years ago

@luetkemj it seems this error indicates that newWorld does not know about a component that world knows about. components are normally automatically registered with worlds when they are either added to an entity or a query with that component is called on a world, but it can also be done explicitly. i created an example which reproduces the error:

import { 
  Types, 
  createWorld,
  defineSerializer,
  defineDeserializer,
  defineComponent,
  registerComponent,
  addEntity,
  addComponent
} from "bitecs"

const save = (world) => {
  const serialize = defineSerializer(world)
  return serialize(world)
}

const load = (world, packet) => {
  const deserialize = defineDeserializer(world)
  deserialize(world, packet)
}

const worldA = createWorld()
const worldB = createWorld()

const C = defineComponent({
  x: Types.f32,
  y: Types.f32,
  z: Types.f32,
})

registerComponent(worldA, C)
// uncomment this line and the error goes away
// registerComponent(worldB, C)

const eidA = addEntity(worldA)

addComponent(worldA, C, eidA)

const packet = save(worldA)

load(worldB, packet)

because serializers and deserializers need to have the same exact config up-front, using worlds creates a bit of an edge case if those worlds don't know about the same exact components. to avoid this error one should explicitly pass in all components to both the serializer and deserializer for now.

however, i will be introducing a new function to a future release which should make this easier: getWorldComponents

i'm now wondering if it's a bad idea to allow a world to be passed into the initial serializer config, and instead if something like this should replace that feature:

const serialize = defineSerializer(getWorldComponents(world))

what do you think?

luetkemj commented 2 years ago

const serialize = defineSerializer(getWorldComponents(world))

@NateTheGreatt That feels a lot more intuitive to me. If there's a legitimate usecase for passing in the world directly that I'm missing, good documentation should make things clear about when to do what.

O4epegb commented 2 years ago

Would also love to see some in-build solution for that!

So far I've been using this to keep track of all components (to avoid forgetting to register one):

const definedComponents: ComponentType<any>[] = [];

const defineComponent: typeof defineComponentBitecs = (schema, size) => {
  const component = defineComponentBitecs(schema, size);

  definedComponents.push(component);

  return component;
};

// component definitions with custom `defineComponent`
// ...

export const registerAllComponents = (world: IWorld) => {
  registerComponents(world, definedComponents);
};
lunarnet76 commented 5 months ago

Thank you Nate for your code, I didn't realise you code register a component against a world, if I register all components then it works!!!

also, in case anybody needs it, it took me a while how to actually save it to a file (in nodejs, I am using electron)

fs.writeFileSync(this.FilePath(saveName), Buffer.from(data));

and to read
this.typedArrayToBuffer(fs.readFileSync(this.FilePath(saveName)));

with
typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
    return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset)
}

(I am unsure why it works but it works for me, which is good enough for now!)

NateTheGreatt commented 2 days ago

https://github.com/NateTheGreatt/bitECS/tree/rc-0-4-0/docs/Serialization.md