From 52864cb701e59a1d847fd5586245519eb5e3b3bc Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Fri, 23 Jan 2026 20:22:30 -0800 Subject: Move code over --- core/src/main/java/coffee/liz/ecs/DAGWorld.java | 158 +++++++++++++++++++++ core/src/main/java/coffee/liz/ecs/math/Mat2.java | 91 ++++++++++++ core/src/main/java/coffee/liz/ecs/math/Vec2.java | 65 +++++++++ core/src/main/java/coffee/liz/ecs/math/Vec2f.java | 54 +++++++ core/src/main/java/coffee/liz/ecs/math/Vec2i.java | 57 ++++++++ .../main/java/coffee/liz/ecs/model/Component.java | 8 ++ .../src/main/java/coffee/liz/ecs/model/Entity.java | 105 ++++++++++++++ .../src/main/java/coffee/liz/ecs/model/System.java | 29 ++++ core/src/main/java/coffee/liz/ecs/model/World.java | 58 ++++++++ 9 files changed, 625 insertions(+) create mode 100644 core/src/main/java/coffee/liz/ecs/DAGWorld.java create mode 100644 core/src/main/java/coffee/liz/ecs/math/Mat2.java create mode 100644 core/src/main/java/coffee/liz/ecs/math/Vec2.java create mode 100644 core/src/main/java/coffee/liz/ecs/math/Vec2f.java create mode 100644 core/src/main/java/coffee/liz/ecs/math/Vec2i.java create mode 100644 core/src/main/java/coffee/liz/ecs/model/Component.java create mode 100644 core/src/main/java/coffee/liz/ecs/model/Entity.java create mode 100644 core/src/main/java/coffee/liz/ecs/model/System.java create mode 100644 core/src/main/java/coffee/liz/ecs/model/World.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 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 implements World { + /** All entities in the world. */ + protected final Set entities = Collections.synchronizedSet(new HashSet<>()); + + /** Cache mapping component types to entities having that component. */ + private final Map, Set> componentCache = Collections + .synchronizedMap(new HashMap<>()); + + /** Deterministic ID's for spawned entities. */ + private final AtomicInteger nextEntityId = new AtomicInteger(0); + + /** All registered systems. */ + protected final Map>, System> systems; + + /** 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()); + 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 cachedEntities = componentCache.get(componentType); + if (cachedEntities != null) { + cachedEntities.remove(entity); + } + }); + entities.remove(entity); + } + + @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()); + } + + @Override + public void update(final T state, final Duration duration) { + refreshComponentCache(); + systemExecutionOrder.forEach(system -> { + system.update(this, state, duration); + }); + } + + @SuppressWarnings("unchecked") + @Override + public > S getSystem(final Class 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> 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<>(); + + 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> result = new ArrayList<>(); + + final Queue> 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 List> init(final Vec2 dimensions, final Function, T> constructor) { + final List> rows = new ArrayList<>(); + for (int y = 0; y < dimensions.getY(); y++) { + final List 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 + * is the type of the matrix to convolve. + * @param + * is the type of the resulting type of each convolution. + */ + public static List> convolve(final List> mat, final Vec2 axes, + final Supplier init, final Convolver convolver, final BiFunction finalReduction) { + final List> rows = new ArrayList<>(); + for (int y = 0; y < mat.size(); y++) { + final List 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> reductions = new ArrayList<>(); + for (int y = 0; y < mat.size(); y++) { + final List 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 { + R convolve(final T center, final Vec2 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 + * the numeric type of vector components + */ +public interface Vec2 { + /** + * @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 plus(final Vec2 other); + + /** + * Subtracts another vector from this vector. + * + * @param other + * the vector to subtract + * @return a new vector with the result + */ + Vec2 minus(final Vec2 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 scale(final T scaleX, final T scaleY); + + /** + * Length of the vector. + * + * @return length. + */ + float length(); + + /** + * @return Vec2 components of {@link Vec2} + */ + Vec2 intValue(); + + /** + * @return Vec2 components of {@link Vec2} + */ + Vec2 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 { + /** X coordinate. */ + private final Float x; + + /** Y coordinate. */ + private final Float y; + + @Override + public Vec2 plus(final Vec2 other) { + return new Vec2f(x + other.getX(), y + other.getY()); + } + + @Override + public Vec2 minus(final Vec2 other) { + return new Vec2f(x - other.getX(), y - other.getY()); + } + + @Override + public Vec2 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 floatValue() { + return this; + } + + @Override + public Vec2 intValue() { + return Vec2i.builder().x(this.x.intValue()).y(this.y.intValue()).build(); + } + + /** Zero float vec */ + public static Vec2 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 { + /** X coordinate. */ + private final Integer x; + + /** Y coordinate. */ + private final Integer y; + + @Override + public Vec2 plus(final Vec2 other) { + return new Vec2i(x + other.getX(), y + other.getY()); + } + + @Override + public Vec2 minus(final Vec2 other) { + return new Vec2i(x - other.getX(), y - other.getY()); + } + + @Override + public Vec2 scale(final Integer scaleX, final Integer scaleY) { + return new Vec2i(x * scaleX, y * scaleY); + } + + @Override + public Vec2 floatValue() { + return Vec2f.builder().x(this.x.floatValue()).y(this.y.floatValue()).build(); + } + + @Override + public Vec2 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 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, 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 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> components) { + return components.stream().allMatch(this::has); + } + + /** + * Get component by type. + * + * @param componentType + * the {@link Component} class + * @param + * component type + * @return the component or throw {@link IllegalArgumentException} + */ + @SuppressWarnings("unchecked") + public C get(final Class 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 + * component type + * @return this {@link Entity} for chaining + */ + public 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 + * component type + * @return this {@link Entity} for chaining + */ + public Entity remove(final Class componentType) { + componentMap.remove(componentType); + return this; + } + + /** + * Get all component types. + * + * @return set of {@link Component} classes + */ + public Set> 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 + * is the state of the stuff outside the {@link World}. + */ +public interface System { + /** + * {@link System} clazzes that must run before this system. + * + * @return {@link Collection} of dependencies. + */ + Collection>> 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 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 + * is the state of the stuff outside the world. + */ +public interface World { + /** + * 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 query(final Collection> 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 + * is the {@link System} type. + * @return {@link System} instance of {@param system}. + */ + > S getSystem(final Class system); +} -- cgit v1.2.3-70-g09d2