summaryrefslogtreecommitdiff
path: root/core/src/main/java/coffee/liz/ecs
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2026-02-28 15:19:31 -0800
committerElizabeth Alexander Hunt <me@liz.coffee>2026-02-28 15:19:31 -0800
commit2bc1cb77dfe8edf29ccb4064be23078cdd06038b (patch)
tree916bcccba0a0b7c5794e7b8b78e75469f0fa9e19 /core/src/main/java/coffee/liz/ecs
parent8412efda977c1c76885eae1d0b4a721cf71162f2 (diff)
downloaddyl-2bc1cb77dfe8edf29ccb4064be23078cdd06038b.tar.gz
dyl-2bc1cb77dfe8edf29ccb4064be23078cdd06038b.zip
Simplify event emissions
Diffstat (limited to 'core/src/main/java/coffee/liz/ecs')
-rw-r--r--core/src/main/java/coffee/liz/ecs/ComponentCache.java15
-rw-r--r--core/src/main/java/coffee/liz/ecs/DAGWorld.java34
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/ComponentRemoved.java4
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/EventBus.java49
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/Hook.java13
-rw-r--r--core/src/main/java/coffee/liz/ecs/events/Observable.java34
-rw-r--r--core/src/main/java/coffee/liz/ecs/math/Vec2f.java4
-rw-r--r--core/src/main/java/coffee/liz/ecs/math/Vec2i.java4
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Entity.java39
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/System.java1
10 files changed, 82 insertions, 115 deletions
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;
/**