From d6a294e14604aecd9046cbc19dc1837ca694fbb0 Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Sun, 15 Feb 2026 10:08:30 -0800 Subject: feat: Adding ValueEmitter, LambdaEvaluatorSystem --- core/src/main/java/coffee/liz/ecs/DAGWorld.java | 85 ++++++++++++++++------ core/src/main/java/coffee/liz/ecs/model/Query.java | 30 ++++++++ .../java/coffee/liz/ecs/model/QueryBuilder.java | 25 +++++++ .../src/main/java/coffee/liz/ecs/model/System.java | 5 +- core/src/main/java/coffee/liz/ecs/model/World.java | 20 +++-- 5 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 core/src/main/java/coffee/liz/ecs/model/Query.java create mode 100644 core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java (limited to 'core/src/main/java/coffee/liz/ecs') diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java index 24d5adc..5d052ab 100644 --- a/core/src/main/java/coffee/liz/ecs/DAGWorld.java +++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java @@ -2,6 +2,7 @@ package coffee.liz.ecs; import coffee.liz.ecs.model.Component; import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.Query; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; @@ -10,10 +11,12 @@ import lombok.extern.log4j.Log4j2; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -42,9 +45,10 @@ public class DAGWorld implements World { /** Ordered list of systems for execution. */ private final List> systemExecutionOrder; - public DAGWorld(final Map>, System> systems) { - this.systems = systems; - this.systemExecutionOrder = buildExecutionOrder(systems.values().stream().toList()); + @SafeVarargs + public DAGWorld(final System... systems) { + this.systems = singletonClazzMap(systems); + this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems)); log.debug("Executing in order: {}", systemExecutionOrder); } @@ -67,19 +71,13 @@ public class DAGWorld implements World { } @Override - public Set query(final Collection> components) { - if (components.isEmpty()) { - return Set.copyOf(entities); - } - - final Class firstType = components.iterator().next(); - final Set candidates = componentCache.get(firstType); - if (candidates == null) { - return Collections.emptySet(); - } - - return candidates.stream().filter(entity -> components.stream().allMatch(entity::has)) - .collect(Collectors.toSet()); + public Set resolve(final Query query) { + final Set> components = query.queryingComponents(); + return switch (query.filter()) { + case ALL_OF -> resolveAllOf(components); + case ANY_OF -> resolveAnyOf(components); + case NONE_OF -> resolveNoneOf(components); + }; } @Override @@ -103,15 +101,48 @@ public class DAGWorld implements World { componentType -> componentCache.computeIfAbsent(componentType, _ -> new HashSet<>()).add(entity))); } + private Set resolveAllOf(final Set> components) { + if (components.isEmpty()) { + return Set.copyOf(entities); + } + + final Class firstType = components.iterator().next(); + final Set candidates = componentCache.get(firstType); + if (candidates == null) { + return Collections.emptySet(); + } + + return candidates.stream().filter(entity -> components.stream().allMatch(entity::has)) + .collect(Collectors.toSet()); + } + + private Set resolveAnyOf(final Set> components) { + if (components.isEmpty()) { + return Collections.emptySet(); + } + + return entities.stream().filter(entity -> components.stream().anyMatch(entity::has)) + .collect(Collectors.toSet()); + } + + private Set resolveNoneOf(final Set> components) { + if (components.isEmpty()) { + return Set.copyOf(entities); + } + + return entities.stream().filter(entity -> components.stream().noneMatch(entity::has)) + .collect(Collectors.toSet()); + } + private List> buildExecutionOrder(final Collection> systems) { if (systems.isEmpty()) { return Collections.emptyList(); } final Map, System> systemMap = systems.stream() - .collect(Collectors.toMap(System::getClass, system -> system)); - final Map, Integer> inDegree = new HashMap<>(); - final Map, Set>> adjacencyList = new HashMap<>(); + .collect(Collectors.toMap(System::getClass, system -> system, (_, b) -> b, LinkedHashMap::new)); + final Map, Integer> inDegree = new LinkedHashMap<>(); + final Map, Set>> adjacencyList = new LinkedHashMap<>(); systems.forEach(system -> { final Class systemClass = system.getClass(); @@ -155,11 +186,23 @@ public class DAGWorld implements World { } @Override - public void close() throws Exception { + public void dispose() { for (final System system : systemExecutionOrder) { - system.close(); + system.dispose(); } componentCache.clear(); entities.clear(); } + + @SuppressWarnings("unchecked") + private static Map, T> singletonClazzMap(final T... singletons) { + final boolean areSingletons = Arrays.stream(singletons).map(t -> (Class>) t.getClass()) + .distinct().count() == singletons.length; + if (!areSingletons) { + throw new IllegalArgumentException("Only one instance may be used per clazz"); + } + + return Arrays.stream(singletons).map(t -> Map.entry((Class) t.getClass(), t)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } } diff --git a/core/src/main/java/coffee/liz/ecs/model/Query.java b/core/src/main/java/coffee/liz/ecs/model/Query.java new file mode 100644 index 0000000..679ea49 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/model/Query.java @@ -0,0 +1,30 @@ +package coffee.liz.ecs.model; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +public record Query(Set> queryingComponents, QueryFilter filter) { + public Query { + queryingComponents = Set.copyOf(queryingComponents); + } + + @SafeVarargs + public static Query allOf(final Class... components) { + return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ALL_OF); + } + + @SafeVarargs + public static Query anyOf(final Class... components) { + return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ANY_OF); + } + + @SafeVarargs + public static Query noneOf(final Class... components) { + return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.NONE_OF); + } + + public enum QueryFilter { + ALL_OF, ANY_OF, NONE_OF + } +} diff --git a/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java new file mode 100644 index 0000000..eba5021 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java @@ -0,0 +1,25 @@ +package coffee.liz.ecs.model; + +import lombok.RequiredArgsConstructor; + +import java.util.Set; + +@RequiredArgsConstructor +public class QueryBuilder { + private final World world; + + @SafeVarargs + public final Set allOf(final Class... components) { + return world.resolve(Query.allOf(components)); + } + + @SafeVarargs + public final Set anyOf(final Class... components) { + return world.resolve(Query.anyOf(components)); + } + + @SafeVarargs + public final Set noneOf(final Class... components) { + return world.resolve(Query.noneOf(components)); + } +} 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 c4b0bc0..8138a0b 100644 --- a/core/src/main/java/coffee/liz/ecs/model/System.java +++ b/core/src/main/java/coffee/liz/ecs/model/System.java @@ -9,7 +9,7 @@ import java.util.Collection; * @param * is the state of the stuff outside the {@link World}. */ -public interface System extends AutoCloseable { +public interface System { /** * {@link System} clazzes that must run before this system. * @@ -27,7 +27,6 @@ public interface System extends AutoCloseable { */ void update(final World world, final T state, final Duration dt); - @Override - default void close() { + default void dispose() { } } diff --git a/core/src/main/java/coffee/liz/ecs/model/World.java b/core/src/main/java/coffee/liz/ecs/model/World.java index c59a0d7..668c3d5 100644 --- a/core/src/main/java/coffee/liz/ecs/model/World.java +++ b/core/src/main/java/coffee/liz/ecs/model/World.java @@ -1,16 +1,15 @@ package coffee.liz.ecs.model; import java.time.Duration; -import java.util.Collection; import java.util.Set; /** - * The state of the world. + * The game world. * * @param * is the state of the stuff outside the world. */ -public interface World extends AutoCloseable { +public interface World { /** * Create unique {@link Entity} in the {@link World}. * @@ -29,11 +28,15 @@ public interface World extends AutoCloseable { /** * Get entities with {@link Component}s. * - * @param components - * to query for. - * @return All entities with all {@param components}. + * @param query + * to query. + * @return All entities satisfying {@param query}. */ - Set query(final Collection> components); + Set resolve(final Query query); + + default QueryBuilder queryable() { + return new QueryBuilder<>(this); + } /** * Integrate the {@link World}. @@ -55,4 +58,7 @@ public interface World extends AutoCloseable { * @return {@link System} instance of {@param system}. */ > S getSystem(final Class system); + + default void dispose() { + } } -- cgit v1.3