aboutsummaryrefslogtreecommitdiff
path: root/core/src/main/java/coffee/liz/ecs
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2026-01-23 20:22:30 -0800
committerElizabeth Hunt <me@liz.coffee>2026-01-23 20:22:30 -0800
commit52864cb701e59a1d847fd5586245519eb5e3b3bc (patch)
tree1d3df85b939e2c50ebf154ab4fcac6f02ad087c2 /core/src/main/java/coffee/liz/ecs
downloadthe-abstraction-engine-v2-52864cb701e59a1d847fd5586245519eb5e3b3bc.tar.gz
the-abstraction-engine-v2-52864cb701e59a1d847fd5586245519eb5e3b3bc.zip
Move code over
Diffstat (limited to 'core/src/main/java/coffee/liz/ecs')
-rw-r--r--core/src/main/java/coffee/liz/ecs/DAGWorld.java158
-rw-r--r--core/src/main/java/coffee/liz/ecs/math/Mat2.java91
-rw-r--r--core/src/main/java/coffee/liz/ecs/math/Vec2.java65
-rw-r--r--core/src/main/java/coffee/liz/ecs/math/Vec2f.java54
-rw-r--r--core/src/main/java/coffee/liz/ecs/math/Vec2i.java57
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Component.java8
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Entity.java105
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/System.java29
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/World.java58
9 files changed, 625 insertions, 0 deletions
diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
new file mode 100644
index 0000000..716808a
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
@@ -0,0 +1,158 @@
+package coffee.liz.ecs;
+
+import coffee.liz.ecs.model.Component;
+import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+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<>());
+
+ /** Cache mapping component types to entities having that component. */
+ private final Map<Class<? extends Component>, Set<Entity>> componentCache = Collections
+ .synchronizedMap(new HashMap<>());
+
+ /** Deterministic ID's for spawned entities. */
+ 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;
+
+ public DAGWorld(final Map<Class<? extends System<T>>, System<T>> systems) {
+ this.systems = systems;
+ this.systemExecutionOrder = buildExecutionOrder(systems.values().stream().toList());
+ log.debug("Executing in order: {}", systemExecutionOrder);
+ }
+
+ @Override
+ public Entity createEntity() {
+ final Entity entity = Entity.builder().id(nextEntityId.incrementAndGet()).build();
+ entities.add(entity);
+ return entity;
+ }
+
+ @Override
+ public void removeEntity(final Entity entity) {
+ entity.getComponentMap().keySet().forEach(componentType -> {
+ final Set<Entity> cachedEntities = componentCache.get(componentType);
+ if (cachedEntities != null) {
+ cachedEntities.remove(entity);
+ }
+ });
+ entities.remove(entity);
+ }
+
+ @Override
+ public Set<Entity> query(final Collection<Class<? extends Component>> components) {
+ if (components.isEmpty()) {
+ return Set.copyOf(entities);
+ }
+
+ final Class<? extends Component> firstType = components.iterator().next();
+ final Set<Entity> candidates = componentCache.get(firstType);
+ if (candidates == null) {
+ return Collections.emptySet();
+ }
+
+ return candidates.stream().filter(entity -> components.stream().allMatch(entity::has))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public void update(final T state, final Duration duration) {
+ refreshComponentCache();
+ systemExecutionOrder.forEach(system -> {
+ system.update(this, state, duration);
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <S extends System<T>> S getSystem(final Class<S> system) {
+ return (S) systems.get(system);
+ }
+
+ private void refreshComponentCache() {
+ componentCache.clear();
+ entities.forEach(entity -> {
+ entity.getComponentMap().keySet().forEach(componentType -> {
+ componentCache.computeIfAbsent(componentType, k -> new HashSet<>()).add(entity);
+ });
+ });
+ }
+
+ private List<System<T>> buildExecutionOrder(final Collection<System<T>> systems) {
+ if (systems.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final Map<Class<?>, System<T>> systemMap = systems.stream()
+ .collect(Collectors.toMap(System::getClass, system -> system));
+ final Map<Class<?>, Integer> inDegree = new HashMap<>();
+ final Map<Class<?>, Set<Class<?>>> adjacencyList = new HashMap<>();
+
+ systems.forEach(system -> {
+ final Class<?> systemClass = system.getClass();
+ inDegree.put(systemClass, 0);
+ adjacencyList.put(systemClass, new HashSet<>());
+ });
+
+ systems.forEach(system -> {
+ system.getDependencies().forEach(dependency -> {
+ if (systemMap.containsKey(dependency)) {
+ adjacencyList.get(dependency).add(system.getClass());
+ inDegree.merge(system.getClass(), 1, Integer::sum);
+ }
+ });
+ });
+
+ // Kahn's algorithm
+ final List<System<T>> result = new ArrayList<>();
+
+ final Queue<Class<?>> queue = new LinkedList<>(
+ inDegree.entrySet().stream().filter(entry -> entry.getValue() == 0).map(Map.Entry::getKey).toList());
+
+ while (!queue.isEmpty()) {
+ final Class<?> currentClass = queue.poll();
+ result.add(systemMap.get(currentClass));
+
+ adjacencyList.getOrDefault(currentClass, Collections.emptySet()).forEach(dependent -> {
+ final int newInDegree = inDegree.get(dependent) - 1;
+ inDegree.put(dependent, newInDegree);
+ if (newInDegree == 0) {
+ queue.add(dependent);
+ }
+ });
+ }
+
+ if (result.size() != systems.size()) {
+ throw new IllegalStateException("Circular dependency detected in systems");
+ }
+
+ return Collections.unmodifiableList(result);
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/math/Mat2.java b/core/src/main/java/coffee/liz/ecs/math/Mat2.java
new file mode 100644
index 0000000..8be945c
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/math/Mat2.java
@@ -0,0 +1,91 @@
+package coffee.liz.ecs.math;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public final class Mat2 {
+ private Mat2() {
+ }
+
+ /**
+ * Initializes a mutable 2d matrix of given type.
+ *
+ * @param dimensions
+ * the dimensions
+ * @param constructor
+ * the constructor
+ * @return row-indexed 2d matrix of {@param dimensions}
+ */
+ public static <T> List<List<T>> init(final Vec2<Integer> dimensions, final Function<Vec2<Integer>, T> constructor) {
+ final List<List<T>> rows = new ArrayList<>();
+ for (int y = 0; y < dimensions.getY(); y++) {
+ final List<T> row = new ArrayList<>(dimensions.getX());
+ for (int x = 0; x < dimensions.getX(); x++)
+ row.add(constructor.apply(Vec2i.builder().y(y).x(x).build()));
+ rows.add(row);
+ }
+ return rows;
+ }
+
+ /**
+ * Convolves a {@link Convolver} across a matrix.
+ *
+ * @param mat
+ * is the row-indexed 2d matrix to convolve.
+ * @param axes
+ * are the x/y major/minor axes.
+ * @param init
+ * is the initial value of the convolution.
+ * @param convolver
+ * is the {@link Convolver}.
+ * @param finalReduction
+ * to apply after {@param convolver}.
+ * @return result of {@param convolver} applied along axes at each cell.
+ * @param <T>
+ * is the type of the matrix to convolve.
+ * @param <R>
+ * is the type of the resulting type of each convolution.
+ */
+ public static <T, R, U> List<List<U>> convolve(final List<List<T>> mat, final Vec2<Integer> axes,
+ final Supplier<R> init, final Convolver<T, R> convolver, final BiFunction<T, R, U> finalReduction) {
+ final List<List<R>> rows = new ArrayList<>();
+ for (int y = 0; y < mat.size(); y++) {
+ final List<R> row = new ArrayList<>(mat.get(y).size());
+ for (int x = 0; x < mat.get(y).size(); x++) {
+ final T center = mat.get(y).get(x);
+ R result = init.get();
+ for (int dy = -axes.getY(); dy <= axes.getY(); dy++) {
+ final int ry = y + dy;
+ if (ry < 0 || ry >= mat.size())
+ continue;
+ for (int dx = -axes.getX(); dx <= axes.getX(); dx++) {
+ final int rx = x + dx;
+ if (rx < 0 || rx >= mat.get(ry).size())
+ continue;
+ result = convolver.convolve(mat.get(ry).get(rx), Vec2i.builder().x(dx).y(dy).build(), result);
+ }
+ }
+ row.add(result);
+ }
+ rows.add(row);
+ }
+
+ final List<List<U>> reductions = new ArrayList<>();
+ for (int y = 0; y < mat.size(); y++) {
+ final List<U> reduction = new ArrayList<>(mat.get(y).size());
+ for (int x = 0; x < mat.get(y).size(); x++) {
+ reduction.add(finalReduction.apply(mat.get(y).get(x), rows.get(y).get(x)));
+ }
+ reductions.add(reduction);
+ }
+ return reductions;
+ }
+
+ @FunctionalInterface
+ public interface Convolver<T, R> {
+ R convolve(final T center, final Vec2<Integer> rel, final R reduction);
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/math/Vec2.java b/core/src/main/java/coffee/liz/ecs/math/Vec2.java
new file mode 100644
index 0000000..ec7e531
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/math/Vec2.java
@@ -0,0 +1,65 @@
+package coffee.liz.ecs.math;
+
+/**
+ * Cartesian vectors.
+ *
+ * @param <T>
+ * the numeric type of vector components
+ */
+public interface Vec2<T> {
+ /**
+ * @return the x coordinate
+ */
+ T getX();
+
+ /**
+ * @return the y coordinate
+ */
+ T getY();
+
+ /**
+ * Adds another vector to this vector.
+ *
+ * @param other
+ * the vector to add
+ * @return a new vector with the result
+ */
+ Vec2<T> plus(final Vec2<T> other);
+
+ /**
+ * Subtracts another vector from this vector.
+ *
+ * @param other
+ * the vector to subtract
+ * @return a new vector with the result
+ */
+ Vec2<T> minus(final Vec2<T> other);
+
+ /**
+ * Scales this vector by the given factors.
+ *
+ * @param scaleX
+ * the x scale factor
+ * @param scaleY
+ * the y scale factor
+ * @return a new scaled vector
+ */
+ Vec2<T> scale(final T scaleX, final T scaleY);
+
+ /**
+ * Length of the vector.
+ *
+ * @return length.
+ */
+ float length();
+
+ /**
+ * @return Vec2<Integer> components of {@link Vec2<T>}
+ */
+ Vec2<Integer> intValue();
+
+ /**
+ * @return Vec2<Float> components of {@link Vec2<T>}
+ */
+ Vec2<Float> floatValue();
+}
diff --git a/core/src/main/java/coffee/liz/ecs/math/Vec2f.java b/core/src/main/java/coffee/liz/ecs/math/Vec2f.java
new file mode 100644
index 0000000..46f3fb8
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/math/Vec2f.java
@@ -0,0 +1,54 @@
+package coffee.liz.ecs.math;
+
+import static java.lang.Math.sqrt;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/** Float impl of {@link Vec2}. */
+@Getter
+@RequiredArgsConstructor
+@Data
+@Builder
+public final class Vec2f implements Vec2<Float> {
+ /** X coordinate. */
+ private final Float x;
+
+ /** Y coordinate. */
+ private final Float y;
+
+ @Override
+ public Vec2<Float> plus(final Vec2<Float> other) {
+ return new Vec2f(x + other.getX(), y + other.getY());
+ }
+
+ @Override
+ public Vec2<Float> minus(final Vec2<Float> other) {
+ return new Vec2f(x - other.getX(), y - other.getY());
+ }
+
+ @Override
+ public Vec2<Float> scale(final Float scaleX, final Float scaleY) {
+ return new Vec2f(x * scaleX, y * scaleY);
+ }
+
+ @Override
+ public float length() {
+ return (float) sqrt(x * x + y * y);
+ }
+
+ @Override
+ public Vec2<Float> floatValue() {
+ return this;
+ }
+
+ @Override
+ public Vec2<Integer> intValue() {
+ return Vec2i.builder().x(this.x.intValue()).y(this.y.intValue()).build();
+ }
+
+ /** Zero float vec */
+ public static Vec2<Float> ZERO = Vec2f.builder().x(0f).y(0f).build();
+}
diff --git a/core/src/main/java/coffee/liz/ecs/math/Vec2i.java b/core/src/main/java/coffee/liz/ecs/math/Vec2i.java
new file mode 100644
index 0000000..dbe246e
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/math/Vec2i.java
@@ -0,0 +1,57 @@
+package coffee.liz.ecs.math;
+
+import static java.lang.Math.sqrt;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/** Integer impl of {@link Vec2}. */
+@Getter
+@RequiredArgsConstructor
+@Builder
+@Data
+public final class Vec2i implements Vec2<Integer> {
+ /** X coordinate. */
+ private final Integer x;
+
+ /** Y coordinate. */
+ private final Integer y;
+
+ @Override
+ public Vec2<Integer> plus(final Vec2<Integer> other) {
+ return new Vec2i(x + other.getX(), y + other.getY());
+ }
+
+ @Override
+ public Vec2<Integer> minus(final Vec2<Integer> other) {
+ return new Vec2i(x - other.getX(), y - other.getY());
+ }
+
+ @Override
+ public Vec2<Integer> scale(final Integer scaleX, final Integer scaleY) {
+ return new Vec2i(x * scaleX, y * scaleY);
+ }
+
+ @Override
+ public Vec2<Float> floatValue() {
+ return Vec2f.builder().x(this.x.floatValue()).y(this.y.floatValue()).build();
+ }
+
+ @Override
+ public Vec2<Integer> intValue() {
+ return this;
+ }
+
+ @Override
+ public float length() {
+ return (float) sqrt(x * x + y * y);
+ }
+
+ public static final Vec2i NORTH = new Vec2i(0, -1);
+ public static final Vec2i SOUTH = new Vec2i(0, 1);
+ public static final Vec2i EAST = new Vec2i(1, 0);
+ public static final Vec2i WEST = new Vec2i(-1, 0);
+ public static final Vec2i ZERO = new Vec2i(0, 0);
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/Component.java b/core/src/main/java/coffee/liz/ecs/model/Component.java
new file mode 100644
index 0000000..f96ba95
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/Component.java
@@ -0,0 +1,8 @@
+package coffee.liz.ecs.model;
+
+/** Component of an {@link Entity}. */
+public interface Component {
+ default Class<? extends Component> getKey() {
+ return getClass();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/Entity.java b/core/src/main/java/coffee/liz/ecs/model/Entity.java
new file mode 100644
index 0000000..e820e57
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/Entity.java
@@ -0,0 +1,105 @@
+package coffee.liz.ecs.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+@Getter
+@Builder
+@RequiredArgsConstructor
+@AllArgsConstructor
+@Data
+public class Entity {
+ /** Unique id. */
+ private final int id;
+
+ /** Instances of {@link Component}s. */
+ @Builder.Default
+ private Map<Class<? extends Component>, Component> componentMap = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Check if entity has component type.
+ *
+ * @param componentType
+ * the {@link Component} class
+ * @return true if component exists
+ */
+ public boolean has(final Class<? extends Component> componentType) {
+ return componentMap.containsKey(componentType);
+ }
+
+ /**
+ * Check if entity has all component types.
+ *
+ * @param components
+ * collection of {@link Component} classes
+ * @return true if all components exist
+ */
+ public boolean hasAll(final Collection<Class<? extends Component>> components) {
+ return components.stream().allMatch(this::has);
+ }
+
+ /**
+ * Get component by type.
+ *
+ * @param componentType
+ * the {@link Component} class
+ * @param <C>
+ * component type
+ * @return the component or throw {@link IllegalArgumentException}
+ */
+ @SuppressWarnings("unchecked")
+ public <C extends Component> C get(final Class<C> componentType) {
+ final C component = (C) componentMap.get(componentType);
+ if (component == null) {
+ throw new IllegalArgumentException(
+ "Entity with id " + getId() + " does not have required component " + componentType.getSimpleName());
+ }
+ return component;
+ }
+
+ /**
+ * Add component to entity.
+ *
+ * @param component
+ * the {@link Component} to add
+ * @param <C>
+ * component type
+ * @return this {@link Entity} for chaining
+ */
+ public <C extends Component> Entity add(final C component) {
+ componentMap.put(component.getKey(), component);
+ return this;
+ }
+
+ /**
+ * Remove component from entity.
+ *
+ * @param componentType
+ * the {@link Component} class to remove
+ * @param <C>
+ * component type
+ * @return this {@link Entity} for chaining
+ */
+ public <C extends Component> Entity remove(final Class<C> componentType) {
+ componentMap.remove(componentType);
+ return this;
+ }
+
+ /**
+ * Get all component types.
+ *
+ * @return set of {@link Component} classes
+ */
+ public Set<Class<? extends Component>> componentTypes() {
+ return componentMap.keySet();
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/System.java b/core/src/main/java/coffee/liz/ecs/model/System.java
new file mode 100644
index 0000000..220b917
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/System.java
@@ -0,0 +1,29 @@
+package coffee.liz.ecs.model;
+
+import java.time.Duration;
+import java.util.Collection;
+
+/**
+ * Updates the {@link World} state.
+ *
+ * @param <T>
+ * is the state of the stuff outside the {@link World}.
+ */
+public interface System<T> {
+ /**
+ * {@link System} clazzes that must run before this system.
+ *
+ * @return {@link Collection} of dependencies.
+ */
+ Collection<Class<? extends System<T>>> getDependencies();
+
+ /**
+ * @param world
+ * Is the {@link World}.
+ * @param state
+ * Is the {@link T} state outside the {@param world}.
+ * @param dt
+ * Is the timestep.
+ */
+ void update(final World<T> world, final T state, final Duration dt);
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/World.java b/core/src/main/java/coffee/liz/ecs/model/World.java
new file mode 100644
index 0000000..96c7a74
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/World.java
@@ -0,0 +1,58 @@
+package coffee.liz.ecs.model;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * The state of the world.
+ *
+ * @param <T>
+ * is the state of the stuff outside the world.
+ */
+public interface World<T> {
+ /**
+ * Create unique {@link Entity} in the {@link World}.
+ *
+ * @return created {@link Entity}.
+ */
+ Entity createEntity();
+
+ /**
+ * Remove an entity from the {@link World}.
+ *
+ * @param entity
+ * to remove.
+ */
+ void removeEntity(final Entity entity);
+
+ /**
+ * Get entities with {@link Component}s.
+ *
+ * @param components
+ * to query for.
+ * @return All entities with all {@param components}.
+ */
+ Set<Entity> query(final Collection<Class<? extends Component>> components);
+
+ /**
+ * Integrate the {@link World}.
+ *
+ * @param state
+ * Is the state outside the world.
+ * @param duration
+ * Is the time step.
+ */
+ void update(final T state, final Duration duration);
+
+ /**
+ * Get world {@link System}.
+ *
+ * @param system
+ * is the Clazz.
+ * @param <S>
+ * is the {@link System} type.
+ * @return {@link System} instance of {@param system}.
+ */
+ <S extends System<T>> S getSystem(final Class<S> system);
+}