Skip to content

Commit

Permalink
Implement serialization.
Browse files Browse the repository at this point in the history
  • Loading branch information
player-03 committed Jan 7, 2025
1 parent c172a0c commit 3e48c61
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 0 deletions.
42 changes: 42 additions & 0 deletions src/echoes/ComponentStorage.hx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import echoes.utils.ComponentTypes;
import echoes.utils.ReadOnlyData;
import echoes.View;
import haxe.Exception;
import haxe.Serializer;
import haxe.Unserializer;

/**
* A central location to store all components of a given type. For example, the
Expand Down Expand Up @@ -61,6 +63,7 @@ class ComponentStorage<T> {
/**
* All components of this type.
*/
@:allow(echoes.Echoes)
#if (echoes_storage == "Map")
private final storage:Map<Int, T> = new Map();
#else
Expand Down Expand Up @@ -220,9 +223,48 @@ class ComponentStorage<T> {
}
}

/**
* Saves all components of this type to string.
* @see `Echoes.serialize()` to save all components at once.
*/
public function serialize():String {
return Serializer.run(storage);
}

private inline function toString():String {
return name;
}

/**
* Restores all components of this type from string, overwriting any
* existing components. No `@:remove` or `@:add` events are dispatched.
*
* Caution: serializing and unserializing are not well-tested. Use this at
* your own risk, and especially avoid unserializing if the component type
* could have changed. Even a minor change, such as changing `Int` to
* `Float`, can cause errors on some targets.
* @see `Echoes.unserialize()` to restore all components at once.
*/
public function unserialize(data:String):Void {
for(components in EntityComponents.components) {
if(components != null) {
components.removeComponentStorage(this);
}
}

unserializeFromData(Unserializer.run(data));
}

@:allow(echoes.Echoes)
private function unserializeFromData(data:#if (echoes_storage == "Map") Map<Int, T> #else Array<Null<T>> #end) {
clear();

if(data != null) {
for(entity => component in data) {
add(cast entity, component);
}
}
}
}

/**
Expand Down
45 changes: 45 additions & 0 deletions src/echoes/Echoes.hx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import echoes.Entity;
import echoes.utils.Clock;
import echoes.utils.ReadOnlyData;
import echoes.View;
import haxe.Serializer;
import haxe.Unserializer;

#if macro
import haxe.macro.Expr;
Expand Down Expand Up @@ -221,6 +223,49 @@ class Echoes {
$view;
};
}

public static function serialize():String {
final data:Dynamic = {
"echoes.Echoes.activeEntities": activeEntities
};

for(storage in componentStorage) {
final components = (cast storage:ComponentStorage<Dynamic>).storage;

//Omit empty arrays. It isn't as easy to check if a map is empty, so
//just include all of them.
if(#if (echoes_storage == "Map") true #else components.length > 0 #end) {
Reflect.setField(data, storage.componentType, components);
}
}

return Serializer.run(data);
}

/**
* Restores all entities and components recorded by `serialize()`,
* overwriting any existing entities or components.
*
* Caution: serializing and unserializing are not well-tested. Use this at
* your own risk, and especially avoid unserializing if the component types
* could have changed. Even a minor change, such as changing `Int` to
* `Float`, can cause errors on some targets.
*/
public static function unserialize(data:String):Void {
activeEntityIndices.resize(0);
_activeEntities.resize(0);
EntityComponents.components.resize(0);

final data:Dynamic = Unserializer.run(data);
for(entity in (Reflect.field(data, "echoes.Echoes.activeEntities"):Array<Entity>)) {
activeEntityIndices[entity.id] = _activeEntities.length;
_activeEntities.push(entity);
}

for(storage in componentStorage) {
(cast storage:ComponentStorage<Dynamic>).unserializeFromData(Reflect.field(data, storage.componentType));
}
}
}

typedef AppStatistics = {
Expand Down
61 changes: 61 additions & 0 deletions test/AdvancedFunctionalityTest.hx
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,67 @@ class AdvancedFunctionalityTest extends Test {
Assert.equals("update, update2, pre_update, post_update", updateOrder.join(", "));
}

private function testSerialization():Void {
final entity0:Entity = new Entity();
final entity1:Entity = new Entity();
final entity2:Entity = new Entity();

entity0.add(("zero":Name));
entity0.add((0xFFFFFF:Color));
entity0.deactivate();

entity1.add(("one":Name));
entity1.add((0.5:Alias<Float>));
entity1.add(["red", "green", "blue"]);
entity1.deactivate();
entity1.activate();

entity2.add(("two":Name));
entity2.add((4:Alias<Float>));

Assert.same([2, 1], @:privateAccess Echoes.activeEntities);
Assert.same([null, 1, 0], @:privateAccess Echoes.activeEntityIndices);

//Bulk serialization

final data:String = Echoes.serialize();
Echoes.reset();
Echoes.unserialize(data);

Assert.same([2, 1], @:privateAccess Echoes.activeEntities);
Assert.same([null, 1, 0], @:privateAccess Echoes.activeEntityIndices);
Assert.isFalse(entity0.active);
Assert.isTrue(entity1.active && entity2.active);

Assert.equals("zero", entity0.get(Name));
Assert.equals(0xFFFFFF, entity0.get(Color));

Assert.equals("one", entity1.get(Name));
Assert.equals(0.5, entity1.get((_:Alias<Float>)));
Assert.same(["red", "green", "blue"], entity1.get((_:Array<String>)));

Assert.equals("two", entity2.get(Name));
Assert.equals(4.0, entity2.get((_:Alias<Float>)));

//Single-component serialization

entity2.remove(Name);
final data:String = Echoes.getComponentStorage(Name).serialize();

entity0.remove(Name);
entity1.add(("entity1":Name));
entity2.add(("":Name));
Echoes.getComponentStorage(Name).unserialize(data);

Assert.equals("zero", entity0.get(Name));
Assert.equals("one", entity1.get(Name));
Assert.isNull(entity2.get(Name));

Assert.isTrue(entity0.getComponents().contains(Name));
Assert.isTrue(entity1.getComponents().contains(Name));
Assert.isFalse(entity2.getComponents().contains(Name));
}

private function testSignals():Void {
count1 = 0;
var count2:Int = 0;
Expand Down

0 comments on commit 3e48c61

Please sign in to comment.