Skip to content

Commit

Permalink
Implement DynamicView.
Browse files Browse the repository at this point in the history
  • Loading branch information
player-03 committed Oct 8, 2024
1 parent 2edf56a commit db6f959
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 3 deletions.
12 changes: 9 additions & 3 deletions src/echoes/ComponentStorage.hx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class ComponentStorage<T> {
private final storage:Array<Null<T>> = [];
#end

private inline function new(componentType:String) {
public inline function new(componentType:String) {
this.componentType = componentType;
Echoes.componentStorage.push(this);

Expand Down Expand Up @@ -184,8 +184,14 @@ class ComponentStorage<T> {
}

/**
* A version of `ComponentStorage` that stores components of unknown type. As
* this makes it unsafe to call `add()`, that function is disabled.
* A version of `ComponentStorage` that stores components of unknown type.
* `add()` is disabled because there's no way to make sure the added component
* is the correct type. `remove()` is still available because it doesn't need to
* check any types.
*
* If you're creating the `ComponentStorage` at runtime and want to be able to
* add components, use `new ComponentStorage<Dynamic>()` instead. Obviously, no
* type checking will be performed.
*/
@:forward(clear, componentType, exists, get, name, relatedViews, remove)
abstract DynamicComponentStorage(ComponentStorage<Dynamic>) {
Expand Down
102 changes: 102 additions & 0 deletions src/echoes/View.hx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package echoes;
import echoes.ComponentStorage;
import echoes.Entity;
import echoes.utils.ReadOnlyData;
import echoes.utils.Signal;
import haxe.Exception;

#if !macro
@:genericBuild(echoes.macro.ViewBuilder.build())
Expand Down Expand Up @@ -113,3 +115,103 @@ class ViewBase {
return "View<" + [for(storage in componentStorage) storage.name].join(", ") + ">";
}
}

/**
* A `View` that can be created at runtime.
*
* Sample usage:
*
* ```haxe
* //Storage for a custom component type. Because `entity.add(x)` only works at
* //compile time, you'll have to call `customComponent.add(entity, x)`.
* public final customComponent:ComponentStorage<Any>;
*
* //A view of `customComponent` and `String`; it'll dispatch events for any
* //entity that has both components.
* public final view:DynamicView;
*
* public function new() {
* customComponent = new ComponentStorage<Any>("CustomComponent");
*
* view = new DynamicView(customComponent, Echoes.getComponentStorage(String));
*
* //Important: `DynamicView` doesn't activate itself.
* view.activate();
*
* //Add/remove listeners work normally, except the components are untyped.
* view.onAdded.add((entity:Entity, components:Array<Any>) -> trace('Entity $entity now has $components'));
* view.onRemoved.add((entity:Entity, components:Array<Any>) -> trace('Entity $entity no longer has all of $components'));
* }
*
* public function update(time:Float):Void {
* //Like with any other view, `iter()` doesn't allow for a time argument.
* //Here's one way to pass it in, but you could also simply leave it out.
* view.iter(updateEntity.bind(time));
* }
*
* private function updateEntity(time:Float, entity:Entity, components:Array<Any>):Void {
* trace('Updating entity $entity that has $components ($time seconds elapsed)')
* }
* ```
*/
class DynamicView extends ViewBase {
public final onAdded:Signal<(Entity, Array<Any>) -> Void> = new Signal<(Entity, Array<Any>) -> Void>();
public final onRemoved:Signal<(Entity, Array<Any>) -> Void> = new Signal<(Entity, Array<Any>) -> Void>();

public inline function new(...componentStorage:DynamicComponentStorage) {
super(componentStorage);
}

private override function dispatchAddedCallback(entity:Entity):Void {
var index:Int = entities.lastIndexOf(entity);
for(callback in onAdded) {
callback(entity, [for(storage in componentStorage) storage.get(entity)]);

//If the callback removed the entity, stop. Cache the index to save
//time in most cases. HashLink is known to return 0 when reading out
//of bounds, so it has to check length too.
if(#if hl macro index >= entities.length || #end entities[index] != entity) {
index = entities.lastIndexOf(entity);
if(index < 0) {
break;
}
}
}
}

private override function dispatchRemovedCallback(entity:Entity, ?removedComponentStorage:DynamicComponentStorage, ?removedComponent:Any):Void {
var exception:Exception = null;
for(callback in onRemoved) {
try {
callback(entity, [for(storage in componentStorage)
storage == removedComponentStorage ? removedComponent : storage.get(entity)]);
} catch(e:Exception) {
exception = e;
}
}

if(exception != null) {
throw exception;
}
}

private override function reset():Void {
super.reset();
onAdded.clear();
onRemoved.clear();
}

public function iter(callback:(Entity, Array<Any>) -> Void):Void {
var i:Int = 0;
while(i < entities.length) {
final entity:Entity = entities[i];
callback(entity, [for(storage in componentStorage) storage.get(entity)]);

if(entity != entities[i] && !entities.contains(entity)) {
//Entity was removed; don't increment.
} else {
i++;
}
}
}
}
39 changes: 39 additions & 0 deletions test/AdvancedFunctionalityTest.hx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,45 @@ class AdvancedFunctionalityTest extends Test {
Assert.isFalse(types.contains(String));
}

private function testDynamicViews():Void {
final component0:ComponentStorage<Any> = new ComponentStorage<Any>("component0");
final component1:ComponentStorage<Any> = new ComponentStorage<Any>("component1");

final view:DynamicView = new DynamicView(component0, component1);
view.activate();
var added:String = "";
view.onAdded.add((entity, components) -> added += components.join(""));
var removed:String = "";
view.onRemoved.add((entity, components) -> removed += components.join(""));

final entity0:Entity = new Entity();
component0.add(entity0, "---");
component0.remove(entity0);
Assert.equals("", added);
Assert.equals("", removed);

component1.add(entity0, "b");
component0.add(entity0, "a");
Assert.equals("ab", added);
Assert.equals("", removed);

final entity1:Entity = new Entity();
entity1.add("string");
component0.add(entity1, 0);
component1.add(entity1, 1);
Assert.equals("ab01", added);
Assert.equals("", removed);

var updated:String = "";
view.iter((entity, components) -> updated += components.join(""));
Assert.equals("ab01", updated);

component1.remove(entity1);
component1.remove(entity0);
Assert.equals("ab01", added);
Assert.equals("01ab", removed);
}

private function testEntityTemplates():Void {
new NameSystem().activate();
new AppearanceSystem().activate();
Expand Down
15 changes: 15 additions & 0 deletions test/EdgeCaseTest.hx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,21 @@ class EdgeCaseTest extends Test {
Echoes.getView(String).iter(system.removeString);
assertTimesCalled(3, "RemoveStringSystem.removeString");
Assert.equals(1, Echoes.getView(String).entities.length);

final view:DynamicView = new DynamicView(Echoes.getComponentStorage(Bool));
view.activate();
var count:Int = 0;
entity0.add(false);
entity1.add(true);
entity2.add(true);
view.iter((entity, components) -> {
count++;
if(components[0] == true) {
entity.remove(Bool);
}
});
Assert.equals(1, view.entities.length);
Assert.equals(3, count);
}

private function testSystemLists():Void {
Expand Down

0 comments on commit db6f959

Please sign in to comment.