diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-27 20:00:05 -0800 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-27 20:00:05 -0800 |
| commit | 6e2fb9b12870a24f99071ea726d1c49ed57593ad (patch) | |
| tree | ffd77e884206ddd2f8312dce4ce3191524df0796 /core/src/main/java/coffee | |
| parent | c9c7e69714707262e8bdea25853bf33447a57951 (diff) | |
| download | dyl-6e2fb9b12870a24f99071ea726d1c49ed57593ad.tar.gz dyl-6e2fb9b12870a24f99071ea726d1c49ed57593ad.zip | |
Moving penguin
Diffstat (limited to 'core/src/main/java/coffee')
25 files changed, 928 insertions, 56 deletions
diff --git a/core/src/main/java/coffee/liz/dyl/DylGame.java b/core/src/main/java/coffee/liz/dyl/DylGame.java new file mode 100644 index 0000000..aa25617 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/DylGame.java @@ -0,0 +1,53 @@ +package coffee.liz.dyl; + +import coffee.liz.dyl.config.Settings; +import coffee.liz.dyl.screen.GameScreen; +import com.badlogic.gdx.Game; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.InputMultiplexer; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.utils.viewport.FitViewport; +import com.badlogic.gdx.utils.viewport.Viewport; +import lombok.Getter; + +@Getter +public class DylGame extends Game { + public static final float WORLD_WIDTH = 10f; + public static final float WORLD_HEIGHT = 10f; + public static final float UNIT_SCALE = 1/10f; + + private InputMultiplexer inputMultiplexer; + private Batch batch; + private OrthographicCamera camera; + private Viewport viewport; + private Settings settings = Settings.builder().build(); + + @Override + public void create() { + inputMultiplexer = new InputMultiplexer(); + Gdx.input.setInputProcessor(inputMultiplexer); + + batch = new SpriteBatch(); + camera = new OrthographicCamera(); + viewport = new FitViewport(WORLD_WIDTH, WORLD_HEIGHT, camera); + + setScreen(new GameScreen(this)); + } + + @Override + public void render() { + Gdx.gl.glClearColor(0, 0, 0, 1); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + + super.render(); + } + + @Override + public void resize(final int width, final int height) { + viewport.update(width, height, true); + super.resize(width, height); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/FirstScreen.java b/core/src/main/java/coffee/liz/dyl/FirstScreen.java deleted file mode 100644 index 4901283..0000000 --- a/core/src/main/java/coffee/liz/dyl/FirstScreen.java +++ /dev/null @@ -1,45 +0,0 @@ -package coffee.liz.dyl; - -import com.badlogic.gdx.Screen; - -/** First screen of the application. Displayed after the application is created. */ -public class FirstScreen implements Screen { - @Override - public void show() { - // Prepare your screen here. - } - - @Override - public void render(float delta) { - // Draw your screen here. "delta" is the time since last render in seconds. - } - - @Override - public void resize(int width, int height) { - // If the window is minimized on a desktop (LWJGL3) platform, width and height are 0, which causes problems. - // In that case, we don't resize anything, and wait for the window to be a normal size before updating. - if(width <= 0 || height <= 0) return; - - // Resize your screen here. The parameters represent the new window size. - } - - @Override - public void pause() { - // Invoked when your application is paused. - } - - @Override - public void resume() { - // Invoked when your application is resumed after pause. - } - - @Override - public void hide() { - // This method is called when another screen replaces this one. - } - - @Override - public void dispose() { - // Destroy screen's assets here. - } -}
\ No newline at end of file diff --git a/core/src/main/java/coffee/liz/dyl/FrameState.java b/core/src/main/java/coffee/liz/dyl/FrameState.java new file mode 100644 index 0000000..7d70cde --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/FrameState.java @@ -0,0 +1,16 @@ +package coffee.liz.dyl; + +import coffee.liz.dyl.config.Settings; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.function.Predicate; + +@Builder +@Getter +@RequiredArgsConstructor +public class FrameState { + private final Settings settings; + private final Predicate<Integer> isKeyPressed; +} diff --git a/core/src/main/java/coffee/liz/dyl/Main.java b/core/src/main/java/coffee/liz/dyl/Main.java deleted file mode 100644 index 24fa3de..0000000 --- a/core/src/main/java/coffee/liz/dyl/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package coffee.liz.dyl; - -import com.badlogic.gdx.Game; - -/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */ -public class Main extends Game { - @Override - public void create() { - setScreen(new FirstScreen()); - } -}
\ No newline at end of file diff --git a/core/src/main/java/coffee/liz/dyl/components/Controllable.java b/core/src/main/java/coffee/liz/dyl/components/Controllable.java new file mode 100644 index 0000000..c476c91 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/Controllable.java @@ -0,0 +1,6 @@ +package coffee.liz.dyl.components; + +import coffee.liz.ecs.model.Component; + +public class Controllable implements Component { +} diff --git a/core/src/main/java/coffee/liz/dyl/components/Transform.java b/core/src/main/java/coffee/liz/dyl/components/Transform.java new file mode 100644 index 0000000..b2d5a60 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/Transform.java @@ -0,0 +1,18 @@ +package coffee.liz.dyl.components; + +import coffee.liz.ecs.model.Component; +import com.badlogic.gdx.math.Vector2; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class Transform implements Component, Comparable<Transform> { + private Vector2 position; + private int z; + + @Override + public int compareTo(final Transform other) { + return Integer.compare(z, other.getZ()); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/components/Velocity.java b/core/src/main/java/coffee/liz/dyl/components/Velocity.java new file mode 100644 index 0000000..eb981f7 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/Velocity.java @@ -0,0 +1,16 @@ +package coffee.liz.dyl.components; + +import coffee.liz.ecs.model.Component; +import com.badlogic.gdx.math.Vector2; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class Velocity implements Component { + private Vector2 velocity = Vector2.Zero; +} diff --git a/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java b/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java new file mode 100644 index 0000000..648a930 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java @@ -0,0 +1,13 @@ +package coffee.liz.dyl.components.graphic; + +import coffee.liz.dyl.components.Transform; +import coffee.liz.ecs.model.Component; +import com.badlogic.gdx.graphics.g2d.Batch; + +public interface Graphic extends Component { + default Class<? extends Component> getKey() { + return Graphic.class; + } + + void draw(final Batch batch, final Transform transform); +} diff --git a/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java b/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java new file mode 100644 index 0000000..0d12f1a --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java @@ -0,0 +1,21 @@ +package coffee.liz.dyl.components.graphic; + +import coffee.liz.dyl.components.Transform; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.TextureRegion; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TextureGraphic implements Graphic { + private Color color; + private TextureRegion textureRegion; + + @Override + public void draw(final Batch batch, final Transform transform) { + batch.setColor(color); + batch.draw(textureRegion, transform.getPosition().x, transform.getPosition().y); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java b/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java new file mode 100644 index 0000000..762dc48 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java @@ -0,0 +1,41 @@ +package coffee.liz.dyl.config; +import com.badlogic.gdx.Input; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +@Builder +@AllArgsConstructor +@RequiredArgsConstructor +@Getter +public class KeyBinds { + @Builder.Default + private Set<Integer> moveUpKeys = Set.of(Input.Keys.K, Input.Keys.UP, Input.Keys.W); + @Builder.Default + private Set<Integer> moveDownKeys = Set.of(Input.Keys.J, Input.Keys.DOWN, Input.Keys.S); + @Builder.Default + private Set<Integer> moveLeftKeys = Set.of(Input.Keys.H, Input.Keys.LEFT, Input.Keys.A); + @Builder.Default + private Set<Integer> moveRightKeys = Set.of(Input.Keys.L, Input.Keys.RIGHT, Input.Keys.D); + + public Set<Action> filterActiveActions(final Predicate<Integer> isDown) { + final Set<Action> actions = new HashSet<>(); + Map.of(moveUpKeys, Action.MOVE_UP, moveDownKeys, Action.MOVE_DOWN, moveRightKeys, Action.MOVE_RIGHT, + moveLeftKeys, Action.MOVE_LEFT).forEach((keys, action) -> { + if (keys.stream().anyMatch(isDown)) { + actions.add(action); + } + }); + return actions; + } + + public enum Action { + MOVE_UP, MOVE_LEFT, MOVE_DOWN, MOVE_RIGHT; + } +} diff --git a/core/src/main/java/coffee/liz/dyl/config/Settings.java b/core/src/main/java/coffee/liz/dyl/config/Settings.java new file mode 100644 index 0000000..13fbb3c --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/config/Settings.java @@ -0,0 +1,16 @@ +package coffee.liz.dyl.config; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Builder +@AllArgsConstructor +@Getter +public class Settings { + @Builder.Default + private boolean skipIntro = true; + @Builder.Default + private boolean playMusic = true; + @Builder.Default + private KeyBinds keyBinds = KeyBinds.builder().build(); +} diff --git a/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java new file mode 100644 index 0000000..f7344c7 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java @@ -0,0 +1,31 @@ +package coffee.liz.dyl.entities; + +import coffee.liz.dyl.components.Controllable; +import coffee.liz.dyl.components.Velocity; +import coffee.liz.dyl.components.graphic.TextureGraphic; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.World; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.files.FileHandle; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.TextureRegion; + +public class PlayerFactory { + private static final int PENGUIN_FRAMES_Y = 4; + private static final int PENGUIN_FRAMES_X = 8; + private static final FileHandle FILE = Gdx.files.internal("player.png"); + + public static Entity addTo(final World<?> world) { + final Texture texture = new Texture(FILE); + final TextureRegion[][] tmp = TextureRegion.split(texture, + texture.getWidth() / PENGUIN_FRAMES_X, + texture.getHeight() / PENGUIN_FRAMES_Y); + return world.createEntity() + .add(new TextureGraphic(Color.PINK, tmp[0][0])) + .add(new Controllable()) + .add(new Velocity()); + } + + private PlayerFactory() { } +} diff --git a/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java b/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java new file mode 100644 index 0000000..54dfda3 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java @@ -0,0 +1,51 @@ +package coffee.liz.dyl.screen; + +import coffee.liz.dyl.DylGame; +import coffee.liz.dyl.FrameState; +import coffee.liz.dyl.world.DylGameWorld; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Screen; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class GameScreen implements Screen { + private final DylGame game; + + private DylGameWorld dylGameWorld; + private FrameState frameState; + + @Override + public void show() { + dylGameWorld = new DylGameWorld(game); + frameState = FrameState.builder() + .settings(game.getSettings()) + .isKeyPressed(Gdx.input::isKeyPressed) + .build(); + } + + @Override + public void render(final float delta) { + dylGameWorld.update(frameState, delta); + } + + @Override + public void resize(final int width, final int height) { + } + + @Override + public void pause() { + } + + @Override + public void resume() { + } + + @Override + public void hide() { + } + + @Override + public void dispose() { + dylGameWorld.dispose(); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java new file mode 100644 index 0000000..8a357ba --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java @@ -0,0 +1,41 @@ +package coffee.liz.dyl.systems; + +import coffee.liz.dyl.FrameState; +import coffee.liz.dyl.components.Transform; +import coffee.liz.dyl.components.Velocity; +import coffee.liz.dyl.config.KeyBinds; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import com.badlogic.gdx.math.Vector2; + +import java.util.Collection; +import java.util.Set; + +public class InputSystem implements System<FrameState> { + @Override + public Collection<Class<? extends System<FrameState>>> getDependencies() { + return Set.of(); + } + + @Override + public void update(final World<FrameState> world, final FrameState state, final float deltaSeconds) { + final Set<KeyBinds.Action> currentlyActive = state.getSettings().getKeyBinds() + .filterActiveActions(state.getIsKeyPressed()); + + final Vector2 momentum = Vector2.Zero.cpy(); + currentlyActive.stream().map(InputSystem::actionToMovement).forEach(momentum::add); + + world.queryable().allOf(Velocity.class, Transform.class).forEach(e -> { + e.get(Velocity.class).getVelocity().set(momentum); + }); + } + + private static Vector2 actionToMovement(final KeyBinds.Action action) { + return switch (action) { + case MOVE_UP -> new Vector2(0, 1); + case MOVE_DOWN -> new Vector2(0, -1); + case MOVE_LEFT -> new Vector2(-1, 0); + case MOVE_RIGHT -> new Vector2(1, 0); + }; + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java new file mode 100644 index 0000000..3fb9b76 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java @@ -0,0 +1,30 @@ +package coffee.liz.dyl.systems; + +import coffee.liz.dyl.FrameState; +import coffee.liz.dyl.components.Transform; +import coffee.liz.dyl.components.Velocity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import com.badlogic.gdx.math.Vector2; +import lombok.extern.log4j.Log4j2; + +import java.util.Collection; +import java.util.List; + +@Log4j2 +public class IntegrationSystem implements System<FrameState> { + @Override + public Collection<Class<? extends System<FrameState>>> getDependencies() { + return List.of(InputSystem.class); + } + + @Override + public void update(final World<FrameState> world, final FrameState state, final float deltaSeconds) { + world.queryable().allOf(Velocity.class, Transform.class) + .forEach(e -> { + final Vector2 velocity = e.get(Velocity.class).getVelocity(); + final Vector2 position = e.get(Transform.class).getPosition(); + position.add(velocity.cpy().scl(deltaSeconds)); + }); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java b/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java new file mode 100644 index 0000000..2f185cd --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java @@ -0,0 +1,53 @@ +package coffee.liz.dyl.systems; + +import coffee.liz.dyl.FrameState; +import coffee.liz.dyl.components.graphic.Graphic; +import coffee.liz.dyl.components.Transform; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.graphics.OrthographicCamera; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.utils.viewport.Viewport; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; + +public class RenderSystem implements System<FrameState> { + private final Batch batch; + private final OrthographicCamera camera; + private final Viewport viewport; + + public RenderSystem(final Batch batch, final Viewport viewport) { + this.batch = batch; + this.viewport = viewport; + this.camera = (OrthographicCamera) viewport.getCamera(); + } + + @Override + public Collection<Class<? extends System<FrameState>>> getDependencies() { + return Set.of(IntegrationSystem.class); + } + + @Override + public void update(final World<FrameState> world, final FrameState state, final float deltaSeconds) { + viewport.apply(); + camera.update(); + + batch.setProjectionMatrix(camera.combined); + batch.begin(); + batch.setColor(Color.WHITE); + + world.queryable().allOf(Transform.class, Graphic.class).stream() + .sorted(Comparator.comparing(e -> e.get(Transform.class))) + .forEach(e -> { + final Transform transform = e.get(Transform.class); + final Graphic graphic = e.get(Graphic.class); + graphic.draw(batch, transform); + }); + + batch.setColor(Color.WHITE); + batch.end(); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java new file mode 100644 index 0000000..b04a4df --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java @@ -0,0 +1,23 @@ +package coffee.liz.dyl.world; + +import coffee.liz.dyl.DylGame; +import coffee.liz.dyl.FrameState; +import coffee.liz.dyl.components.Transform; +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.ecs.DAGWorld; +import com.badlogic.gdx.math.Vector2; + +public class DylGameWorld extends DAGWorld<FrameState> { + public DylGameWorld(final DylGame game) { + super( + new InputSystem(), + new IntegrationSystem(), + new RenderSystem(game.getBatch(), game.getViewport()) + ); + PlayerFactory.addTo(this) + .add(new Transform(Vector2.One, 1)); + } +} 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..1075e5e --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java @@ -0,0 +1,207 @@ +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; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +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; +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; + + @SafeVarargs + public DAGWorld(final System<T>... systems) { + this.systems = singletonClazzMap(systems); + this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems)); + 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> resolve(final Query query) { + final Set<Class<? extends Component>> components = query.queryingComponents(); + return switch (query.filter()) { + case ALL_OF -> resolveAllOf(components); + case ANY_OF -> resolveAnyOf(components); + case NONE_OF -> resolveNoneOf(components); + }; + } + + @Override + public void update(final T state, final float deltaSeconds) { + systemExecutionOrder.forEach(system -> { + refreshComponentCache(); + system.update(this, state, deltaSeconds); + }); + refreshComponentCache(); + } + + @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, _comp -> new HashSet<>()).add(entity))); + } + + private Set<Entity> resolveAllOf(final Set<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()); + } + + private Set<Entity> resolveAnyOf(final Set<Class<? extends Component>> components) { + if (components.isEmpty()) { + return Collections.emptySet(); + } + + return entities.stream().filter(entity -> components.stream().anyMatch(entity::has)) + .collect(Collectors.toSet()); + } + + private Set<Entity> resolveNoneOf(final Set<Class<? extends Component>> components) { + if (components.isEmpty()) { + return Set.copyOf(entities); + } + + return entities.stream().filter(entity -> components.stream().noneMatch(entity::has)) + .collect(Collectors.toSet()); + } + + 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, (_sys, b) -> b, LinkedHashMap::new)); + final Map<Class<?>, Integer> inDegree = new LinkedHashMap<>(); + final Map<Class<?>, Set<Class<?>>> adjacencyList = new LinkedHashMap<>(); + + 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); + } + + @Override + public void dispose() { + for (final System<T> system : systemExecutionOrder) { + system.dispose(); + } + componentCache.clear(); + entities.clear(); + } + + @SuppressWarnings("unchecked") + private static <T> Map<Class<? extends T>, T> singletonClazzMap(final T... singletons) { + final boolean areSingletons = Arrays.stream(singletons).map(t -> (Class<? extends System<T>>) 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<? extends T>) t.getClass(), t)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } +} 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..2d3a8e7 --- /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..7dab667 --- /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/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<Class<? extends Component>> queryingComponents, QueryFilter filter) { + public Query { + queryingComponents = Set.copyOf(queryingComponents); + } + + @SafeVarargs + public static Query allOf(final Class<? extends Component>... components) { + return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ALL_OF); + } + + @SafeVarargs + public static Query anyOf(final Class<? extends Component>... components) { + return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ANY_OF); + } + + @SafeVarargs + public static Query noneOf(final Class<? extends Component>... 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<T> { + private final World<T> world; + + @SafeVarargs + public final Set<Entity> allOf(final Class<? extends Component>... components) { + return world.resolve(Query.allOf(components)); + } + + @SafeVarargs + public final Set<Entity> anyOf(final Class<? extends Component>... components) { + return world.resolve(Query.anyOf(components)); + } + + @SafeVarargs + public final Set<Entity> noneOf(final Class<? extends Component>... 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 new file mode 100644 index 0000000..c00334a --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/model/System.java @@ -0,0 +1,32 @@ +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 deltaSeconds + * Is the timestamp. + */ + void update(final World<T> world, final T state, final float deltaSeconds); + + 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 new file mode 100644 index 0000000..82e01a7 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/model/World.java @@ -0,0 +1,64 @@ +package coffee.liz.ecs.model; + +import java.time.Duration; +import java.util.Set; + +/** + * The game 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 query + * to query. + * @return All entities satisfying {@param query}. + */ + Set<Entity> resolve(final Query query); + + default QueryBuilder<T> queryable() { + return new QueryBuilder<>(this); + } + + /** + * Integrate the {@link World}. + * + * @param state + * Is the state outside the world. + * @param deltaSeconds + * Is the time step. + */ + void update(T state, float deltaSeconds); + + /** + * 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); + + default void dispose() { + } +} diff --git a/core/src/main/java/coffee/liz/ecs/utils/FunctionUtils.java b/core/src/main/java/coffee/liz/ecs/utils/FunctionUtils.java new file mode 100644 index 0000000..25e81f0 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/utils/FunctionUtils.java @@ -0,0 +1,28 @@ +package coffee.liz.ecs.utils; + + +public final class FunctionUtils { + /** + * Runs the provided action and wraps failures into a {@link RuntimeException}. + * + * @param run + * action to execute + */ + public static <E extends Throwable> void wrapCheckedException(final ThrowableRunnable<E> run) { + try { + run.run(); + } catch (final Throwable e) { + throw new RuntimeException(e); + } + } + + @FunctionalInterface + public interface ThrowableRunnable<E extends Throwable> { + /** + * Run. + */ + void run() throws E; + } + + private FunctionUtils() { } +} |
