diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-28 15:19:31 -0800 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-28 15:19:31 -0800 |
| commit | 2bc1cb77dfe8edf29ccb4064be23078cdd06038b (patch) | |
| tree | 916bcccba0a0b7c5794e7b8b78e75469f0fa9e19 | |
| parent | 8412efda977c1c76885eae1d0b4a721cf71162f2 (diff) | |
| download | dyl-2bc1cb77dfe8edf29ccb4064be23078cdd06038b.tar.gz dyl-2bc1cb77dfe8edf29ccb4064be23078cdd06038b.zip | |
Simplify event emissions
15 files changed, 111 insertions, 177 deletions
diff --git a/core/src/main/java/coffee/liz/dyl/components/StressComponent.java b/core/src/main/java/coffee/liz/dyl/components/StressComponent.java deleted file mode 100644 index f22dfaa..0000000 --- a/core/src/main/java/coffee/liz/dyl/components/StressComponent.java +++ /dev/null @@ -1,16 +0,0 @@ -package coffee.liz.dyl.components; - -import coffee.liz.ecs.model.Component; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; - -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -@RequiredArgsConstructor -public class StressComponent implements Component { - private final String componentData; - public StressComponent() { - componentData = IntStream.range(0, 10).mapToObj(Integer::toString).collect(Collectors.joining()); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java index d5dfe0b..1519b3e 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java @@ -11,6 +11,7 @@ import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; import java.util.Collection; +import java.util.Map; import java.util.Set; public class InputSystem implements System<FrameState> { @@ -24,7 +25,7 @@ public class InputSystem implements System<FrameState> { final Set<KeyBinds.Action> currentlyActive = state.getSettings().getKeyBinds() .filterActiveActions(state.getIsKeyPressed()); - final Vec2<Float> momentum = currentlyActive.stream().map(InputSystem::actionToMovement) + final Vec2<Float> momentum = currentlyActive.stream().map(movementVectors::get) .reduce(Vec2::plus) .orElse(Vec2f.ZERO); @@ -32,12 +33,10 @@ public class InputSystem implements System<FrameState> { .forEach(e -> e.get(Velocity.class).setVelocity(momentum)); } - private static Vec2<Float> actionToMovement(final KeyBinds.Action action) { - return switch (action) { - case MOVE_UP -> Vec2i.NORTH.floatValue(); - case MOVE_DOWN -> Vec2i.SOUTH.floatValue(); - case MOVE_LEFT -> Vec2i.WEST.floatValue(); - case MOVE_RIGHT -> Vec2i.EAST.floatValue(); - }; - } + private static Map<KeyBinds.Action, Vec2<Float>> movementVectors = Map.of( + KeyBinds.Action.MOVE_UP, Vec2i.NORTH.floatValue(), + KeyBinds.Action.MOVE_DOWN, Vec2i.SOUTH.floatValue(), + KeyBinds.Action.MOVE_LEFT, Vec2i.WEST.floatValue(), + KeyBinds.Action.MOVE_RIGHT, Vec2i.EAST.floatValue() + ); } diff --git a/core/src/main/java/coffee/liz/dyl/systems/StressSystem.java b/core/src/main/java/coffee/liz/dyl/systems/StressSystem.java deleted file mode 100644 index 850a9f3..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/StressSystem.java +++ /dev/null @@ -1,31 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.FrameState; -import coffee.liz.dyl.components.StressComponent; -import coffee.liz.dyl.components.Velocity; -import coffee.liz.ecs.math.Vec2f; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; - -import java.util.Collection; -import java.util.List; - -public class StressSystem implements System<FrameState> { - - @Override - public Collection<Class<? extends System<FrameState>>> getDependencies() { - return List.of(); - } - - @Override - public void update(World<FrameState> world, FrameState state, float deltaSeconds) { - world.queryable().allOf().forEach(e -> { - if (Math.random() > 0.5) { - e.add(new StressComponent()); - e.add(new Velocity(new Vec2f(0.5f - (float) Math.random(), 0.5f - (float) Math.random()))); - } else if (e.has(StressComponent.class)){ - e.remove(StressComponent.class); - } - }); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java index 475694e..a52ea0f 100644 --- a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java +++ b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java @@ -7,7 +7,6 @@ import coffee.liz.dyl.entities.PlayerFactory; import coffee.liz.dyl.systems.InputSystem; import coffee.liz.dyl.systems.IntegrationSystem; import coffee.liz.dyl.systems.RenderSystem; -import coffee.liz.dyl.systems.StressSystem; import coffee.liz.ecs.DAGWorld; import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.math.Vec2i; @@ -17,10 +16,9 @@ public class DylGameWorld extends DAGWorld<FrameState> { super( new InputSystem(), new IntegrationSystem(), - new RenderSystem(game.getBatch(), game.getViewport()), - new StressSystem() + new RenderSystem(game.getBatch(), game.getViewport()) ); - for (int i = 0; i < 16_000; i++) { + for (int i = 0; i < 2_000; i++) { PlayerFactory.addTo(this) .add(new BoundingBox(new Vec2f(i / 200f, i/ 200f), new Vec2i(1, 1), 1)); } diff --git a/core/src/main/java/coffee/liz/ecs/ComponentCache.java b/core/src/main/java/coffee/liz/ecs/ComponentCache.java index 70487d6..1e6788f 100644 --- a/core/src/main/java/coffee/liz/ecs/ComponentCache.java +++ b/core/src/main/java/coffee/liz/ecs/ComponentCache.java @@ -45,14 +45,9 @@ public final class ComponentCache { */ public void onEntityEvent(final EntityEvent event) { switch (event) { - case ComponentAdded componentAdded: - addComponentEntry(componentAdded.getComponentClazz(), componentAdded.getEntity()); - break; - case ComponentRemoved componentRemoved: - removeComponentEntry(componentRemoved.getComponentClazz(), componentRemoved.getEntity()); - break; - default: - break; + case ComponentAdded a -> addComponentEntry(a.getComponentClazz(), a.getEntity()); + case ComponentRemoved r -> removeComponentEntry(r.getComponentType(), r.getEntity()); + default -> throw new IllegalStateException("Unexpected value: " + event); } } @@ -116,6 +111,10 @@ public final class ComponentCache { private void removeComponentEntry(final Class<? extends Component> componentType, final Entity entity) { final Set<Entity> entities = entitiesByComponent.get(componentType); + if (entities == null) { + return; + } + entities.remove(entity); if (entities.isEmpty()) { entitiesByComponent.remove(componentType); diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java index a6a11db..0cc7d5d 100644 --- a/core/src/main/java/coffee/liz/ecs/DAGWorld.java +++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java @@ -1,10 +1,10 @@ package coffee.liz.ecs; import coffee.liz.ecs.events.EntityEvent; -import coffee.liz.ecs.events.Hook; import coffee.liz.ecs.model.Component; import coffee.liz.ecs.model.Entity; import coffee.liz.ecs.model.Query; +import coffee.liz.ecs.model.QueryBuilder; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; @@ -23,29 +23,20 @@ import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.stream.Collectors; /** World that updates in {@link System#getDependencies()} topological order. */ @Log4j2 @RequiredArgsConstructor public class DAGWorld<T> implements World<T> { - /** All entities in the world. */ - protected final Set<Entity> entities = Collections.synchronizedSet(new HashSet<>()); - - /** Incremental cache mapping component types to entities having that component. */ + private final Set<Entity> entities = Collections.synchronizedSet(new HashSet<>()); private final ComponentCache componentCache = new ComponentCache(); - - /** Shared world listener to keep cache synchronized with entity component changes. */ - private final Hook<EntityEvent> cacheUpdateHook = componentCache::onEntityEvent; - - /** Deterministic ID's for spawned entities. */ + private final Consumer<EntityEvent> entityEventConsumer = componentCache::onEntityEvent; private final AtomicInteger nextEntityId = new AtomicInteger(0); - - /** All registered systems. */ protected final Map<Class<? extends System<T>>, System<T>> systems; - - /** Ordered list of systems for execution. */ private final List<System<T>> systemExecutionOrder; + private final QueryBuilder<T> queryBuilder = new QueryBuilder<>(this); @SafeVarargs public DAGWorld(final System<T>... systems) { @@ -57,7 +48,7 @@ public class DAGWorld<T> implements World<T> { @Override public Entity createEntity() { final Entity entity = Entity.builder().id(nextEntityId.incrementAndGet()).build(); - entity.subscribe(cacheUpdateHook); + entity.subscribe(entityEventConsumer); entities.add(entity); componentCache.addEntity(entity); return entity; @@ -65,7 +56,7 @@ public class DAGWorld<T> implements World<T> { @Override public void removeEntity(final Entity entity) { - entity.unsubscribe(cacheUpdateHook); + entity.unsubscribe(entityEventConsumer); componentCache.removeEntity(entity); entities.remove(entity); } @@ -81,6 +72,11 @@ public class DAGWorld<T> implements World<T> { } @Override + public QueryBuilder<T> queryable() { + return queryBuilder; + } + + @Override public void update(final T state, final float deltaSeconds) { systemExecutionOrder.forEach(system -> system.update(this, state, deltaSeconds)); } @@ -93,7 +89,7 @@ public class DAGWorld<T> implements World<T> { private Set<Entity> resolveAllOf(final Set<Class<? extends Component>> components) { if (components.isEmpty()) { - return Set.copyOf(entities); + return entities; } return componentCache.entitiesMatchingAllOf(components); } @@ -110,7 +106,7 @@ public class DAGWorld<T> implements World<T> { private Set<Entity> resolveNoneOf(final Set<Class<? extends Component>> components) { if (components.isEmpty()) { - return Set.copyOf(entities); + return entities; } final Set<Entity> excluded = new HashSet<>(); @@ -181,7 +177,7 @@ public class DAGWorld<T> implements World<T> { for (final System<T> system : systemExecutionOrder) { system.dispose(); } - entities.forEach(entity -> entity.unsubscribe(cacheUpdateHook)); + entities.forEach(entity -> entity.unsubscribe(entityEventConsumer)); componentCache.clear(); entities.clear(); } diff --git a/core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java b/core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java index 25047b6..b9ef853 100644 --- a/core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java +++ b/core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java @@ -10,12 +10,12 @@ import lombok.RequiredArgsConstructor; * * @param entity * entity that lost a component - * @param componentClazz + * @param componentType * class key of the component that was removed */ @RequiredArgsConstructor @Getter public class ComponentRemoved implements EntityEvent { private final Entity entity; - private final Class<? extends Component> componentClazz; + private final Class<? extends Component> componentType; } diff --git a/core/src/main/java/coffee/liz/ecs/events/EventBus.java b/core/src/main/java/coffee/liz/ecs/events/EventBus.java new file mode 100644 index 0000000..d814281 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/events/EventBus.java @@ -0,0 +1,49 @@ +package coffee.liz.ecs.events; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; + +/** + * Minimal observable contract for event emission. + * + * @param <E> + * emitted event type + */ +public class EventBus<E> { + protected final Set<Consumer<E>> subscriptions = new CopyOnWriteArraySet<>(); + + /** + * Register a hook to receive future events. + * + * @param hook + * callback to invoke for each emitted event + * @return subscribed hook. + */ + public Consumer<E> subscribe(final Consumer<E> hook) { + subscriptions.add(hook); + return hook; + } + + /** + * Remove a previously registered hook. + * + * @param hook + * callback to remove + * @return true + * if the hook was removed. + */ + public boolean unsubscribe(final Consumer<E> hook) { + return subscriptions.remove(hook); + } + + /** + * Publish an event to all currently subscribed hooks. + * + * @param event + * event to publish + */ + public void emit(final E event) { + subscriptions.forEach(s -> s.accept(event)); + } +} diff --git a/core/src/main/java/coffee/liz/ecs/events/Hook.java b/core/src/main/java/coffee/liz/ecs/events/Hook.java deleted file mode 100644 index 8f91aad..0000000 --- a/core/src/main/java/coffee/liz/ecs/events/Hook.java +++ /dev/null @@ -1,13 +0,0 @@ -package coffee.liz.ecs.events; - -import java.util.function.Consumer; - -/** - * Event callback used by {@link Observable}. - * - * @param <E> - * consumed event type - */ -@FunctionalInterface -public interface Hook<E> extends Consumer<E> { -} diff --git a/core/src/main/java/coffee/liz/ecs/events/Observable.java b/core/src/main/java/coffee/liz/ecs/events/Observable.java deleted file mode 100644 index d849407..0000000 --- a/core/src/main/java/coffee/liz/ecs/events/Observable.java +++ /dev/null @@ -1,34 +0,0 @@ -package coffee.liz.ecs.events; - -/** - * Minimal observable contract for event emission. - * - * @param <E> - * emitted event type - */ -public interface Observable<E> { - /** - * Register a hook to receive future events. - * - * @param hook - * callback to invoke for each emitted event - * @return the registered hook - */ - Hook<E> subscribe(Hook<E> hook); - - /** - * Remove a previously registered hook. - * - * @param hook - * callback to remove - */ - void unsubscribe(Hook<E> hook); - - /** - * Publish an event to all currently subscribed hooks. - * - * @param event - * event to publish - */ - void emit(E event); -} diff --git a/core/src/main/java/coffee/liz/ecs/math/Vec2f.java b/core/src/main/java/coffee/liz/ecs/math/Vec2f.java index 42b73e7..4bd0529 100644 --- a/core/src/main/java/coffee/liz/ecs/math/Vec2f.java +++ b/core/src/main/java/coffee/liz/ecs/math/Vec2f.java @@ -3,7 +3,7 @@ package coffee.liz.ecs.math; import static java.lang.Math.sqrt; import lombok.Builder; -import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -12,8 +12,8 @@ import java.util.function.Function; /** Float impl of {@link Vec2}. */ @Getter @RequiredArgsConstructor -@Data @Builder +@EqualsAndHashCode public final class Vec2f implements Vec2<Float> { /** X coordinate. */ private final Float x; diff --git a/core/src/main/java/coffee/liz/ecs/math/Vec2i.java b/core/src/main/java/coffee/liz/ecs/math/Vec2i.java index 326a5df..0e612ec 100644 --- a/core/src/main/java/coffee/liz/ecs/math/Vec2i.java +++ b/core/src/main/java/coffee/liz/ecs/math/Vec2i.java @@ -3,7 +3,7 @@ package coffee.liz.ecs.math; import static java.lang.Math.sqrt; import lombok.Builder; -import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -13,7 +13,7 @@ import java.util.function.Function; @Getter @RequiredArgsConstructor @Builder -@Data +@EqualsAndHashCode public final class Vec2i implements Vec2<Integer> { /** X coordinate. */ private final Integer x; diff --git a/core/src/main/java/coffee/liz/ecs/model/Entity.java b/core/src/main/java/coffee/liz/ecs/model/Entity.java index 4984d5d..bbd417e 100644 --- a/core/src/main/java/coffee/liz/ecs/model/Entity.java +++ b/core/src/main/java/coffee/liz/ecs/model/Entity.java @@ -3,20 +3,16 @@ package coffee.liz.ecs.model; import coffee.liz.ecs.events.ComponentAdded; import coffee.liz.ecs.events.ComponentRemoved; import coffee.liz.ecs.events.EntityEvent; -import coffee.liz.ecs.events.Hook; -import coffee.liz.ecs.events.Observable; +import coffee.liz.ecs.events.EventBus; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; import lombok.Getter; import lombok.RequiredArgsConstructor; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.Map; import java.util.Set; @@ -24,19 +20,12 @@ import java.util.Set; @Builder @RequiredArgsConstructor @AllArgsConstructor -@Data -public class Entity implements Observable<EntityEvent> { - /** Unique id. */ +public class Entity extends EventBus<EntityEvent> { private final int id; - /** Instances of {@link Component}s. */ @Builder.Default private Map<Class<? extends Component>, Component> componentMap = Collections.synchronizedMap(new HashMap<>()); - @Builder.Default - /** Event subscribers for entity mutation events. */ - private transient Set<Hook<EntityEvent>> hooks = Collections.newSetFromMap(new IdentityHashMap<>()); - /** * Check if entity has component type. * @@ -89,8 +78,9 @@ public class Entity implements Observable<EntityEvent> { */ public <C extends Component> Entity add(final C component) { final Class<? extends Component> componentType = component.getKey(); - componentMap.put(componentType, component); - emit(new ComponentAdded(this, componentType)); + if (componentMap.put(componentType, component) == null) { + emit(new ComponentAdded(this, componentType)); + } return this; } @@ -118,23 +108,4 @@ public class Entity implements Observable<EntityEvent> { public Set<Class<? extends Component>> componentTypes() { return componentMap.keySet(); } - - /** Subscribe to component mutation events emitted by this entity. */ - @Override - public Hook<EntityEvent> subscribe(final Hook<EntityEvent> hook) { - hooks.add(hook); - return hook; - } - - /** Remove a previously subscribed mutation hook. */ - @Override - public void unsubscribe(final Hook<EntityEvent> hook) { - hooks.remove(hook); - } - - /** Emit a mutation event to all current subscribers. */ - @Override - public void emit(final EntityEvent event) { - new ArrayList<>(hooks).forEach(hook -> hook.accept(event)); - } } diff --git a/core/src/main/java/coffee/liz/ecs/model/System.java b/core/src/main/java/coffee/liz/ecs/model/System.java index c00334a..f16cdba 100644 --- a/core/src/main/java/coffee/liz/ecs/model/System.java +++ b/core/src/main/java/coffee/liz/ecs/model/System.java @@ -1,6 +1,5 @@ package coffee.liz.ecs.model; -import java.time.Duration; import java.util.Collection; /** diff --git a/core/src/test/java/coffee/liz/ecs/model/EntityTest.java b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java index bb17296..5be4cdb 100644 --- a/core/src/test/java/coffee/liz/ecs/model/EntityTest.java +++ b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import coffee.liz.ecs.events.ComponentAdded; import coffee.liz.ecs.events.ComponentRemoved; import coffee.liz.ecs.events.EntityEvent; -import coffee.liz.ecs.events.Hook; import lombok.RequiredArgsConstructor; @@ -21,6 +20,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.util.Collection; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.stream.Stream; final class EntityTest { @@ -70,6 +70,23 @@ final class EntityTest { } @Test + public void replacingExistingComponentDoesNotEmitAddedEvent() { + final Entity entity = Entity.builder().id(18).build(); + final AtomicInteger addedCount = new AtomicInteger(0); + final Consumer<EntityEvent> hook = entity.subscribe(event -> { + if (event instanceof ComponentAdded) { + addedCount.incrementAndGet(); + } + }); + + entity.add(new AlphaComponent("initial")); + entity.add(new AlphaComponent("replacement")); + + assertEquals(1, addedCount.get()); + entity.unsubscribe(hook); + } + + @Test public void removeClearsComponentPresence() { final Entity entity = Entity.builder().id(45).build(); entity.add(new BetaComponent(2)); @@ -87,7 +104,7 @@ final class EntityTest { final AtomicInteger addedCount = new AtomicInteger(0); final AtomicInteger removedCount = new AtomicInteger(0); - final Hook<EntityEvent> hook = entity.subscribe(event -> { + final Consumer<EntityEvent> hook = entity.subscribe(event -> { if (event instanceof ComponentAdded) { addedCount.incrementAndGet(); } |
