diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-28 17:23:42 -0800 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-28 17:23:42 -0800 |
| commit | c491b5cb08972ffc041fa0b968810373b9ed79a3 (patch) | |
| tree | 962a41f50fab58a026ab7c2c3e034fcda0733dfd | |
| parent | 2bc1cb77dfe8edf29ccb4064be23078cdd06038b (diff) | |
| download | dyl-c491b5cb08972ffc041fa0b968810373b9ed79a3.tar.gz dyl-c491b5cb08972ffc041fa0b968810373b9ed79a3.zip | |
Removing parameterization from World since it was getting complicated.
36 files changed, 741 insertions, 310 deletions
diff --git a/build.gradle b/build.gradle index 79053e4..2cf224f 100644 --- a/build.gradle +++ b/build.gradle @@ -27,8 +27,11 @@ allprojects { configure(subprojects) { apply plugin: 'java-library' - java.sourceCompatibility = JavaVersion.VERSION_24 - java.targetCompatibility = JavaVersion.VERSION_24 + java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } + } dependencies { compileOnly 'org.projectlombok:lombok:1.18.42' diff --git a/core/src/main/java/coffee/liz/dyl/FrameState.java b/core/src/main/java/coffee/liz/dyl/FrameState.java deleted file mode 100644 index 7d70cde..0000000 --- a/core/src/main/java/coffee/liz/dyl/FrameState.java +++ /dev/null @@ -1,16 +0,0 @@ -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/components/BoundingBox.java b/core/src/main/java/coffee/liz/dyl/components/BoundingBox.java deleted file mode 100644 index 481e91e..0000000 --- a/core/src/main/java/coffee/liz/dyl/components/BoundingBox.java +++ /dev/null @@ -1,21 +0,0 @@ -package coffee.liz.dyl.components; - -import coffee.liz.ecs.math.Vec2; -import coffee.liz.ecs.model.Component; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -@AllArgsConstructor -@Getter -@Setter -public class BoundingBox implements Component, Comparable<BoundingBox> { - private Vec2<Float> position; - private Vec2<Integer> dimensions; - private int z; - - @Override - public int compareTo(final BoundingBox other) { - return Integer.compare(z, other.getZ()); - } -} 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 index c70b382..7075755 100644 --- a/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java +++ b/core/src/main/java/coffee/liz/dyl/components/graphic/Graphic.java @@ -1,6 +1,6 @@ package coffee.liz.dyl.components.graphic; -import coffee.liz.dyl.components.BoundingBox; +import coffee.liz.ecs.common.components.physics.BoundingBox; import coffee.liz.ecs.model.Component; import com.badlogic.gdx.graphics.g2d.Batch; @@ -9,5 +9,7 @@ public interface Graphic extends Component { return Graphic.class; } + int getZ(); + void draw(final Batch batch, final BoundingBox boundingBox); } 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 index 6f29759..c2718b5 100644 --- a/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java +++ b/core/src/main/java/coffee/liz/dyl/components/graphic/TextureGraphic.java @@ -1,6 +1,6 @@ package coffee.liz.dyl.components.graphic; -import coffee.liz.dyl.components.BoundingBox; +import coffee.liz.ecs.common.components.physics.BoundingBox; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; @@ -10,6 +10,7 @@ import lombok.Getter; @Getter @AllArgsConstructor public class TextureGraphic implements Graphic { + private int z; private Color color; private Texture texture; @@ -18,8 +19,7 @@ public class TextureGraphic implements Graphic { batch.setColor(color); batch.draw(texture, boundingBox.getPosition().getX(), boundingBox.getPosition().getY(), - 0, 0, - boundingBox.getDimensions().getX(), boundingBox.getDimensions().getY() + boundingBox.getSize().getX(), boundingBox.getSize().getY() ); } } diff --git a/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java b/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java index 762dc48..0b33b26 100644 --- a/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java +++ b/core/src/main/java/coffee/liz/dyl/config/KeyBinds.java @@ -1,4 +1,5 @@ package coffee.liz.dyl.config; + import com.badlogic.gdx.Input; import lombok.AllArgsConstructor; import lombok.Builder; @@ -16,26 +17,24 @@ import java.util.function.Predicate; @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); + @Builder.Default + private Set<Integer> jumpKeys = Set.of(Input.Keys.W, Input.Keys.UP, Input.Keys.SPACE); 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); - } - }); + Map.of(moveLeftKeys, Action.MOVE_LEFT, moveRightKeys, Action.MOVE_RIGHT, jumpKeys, Action.JUMP) + .forEach((keys, action) -> { + if (keys.stream().anyMatch(isDown)) { + actions.add(action); + } + }); return actions; } public enum Action { - MOVE_UP, MOVE_LEFT, MOVE_DOWN, MOVE_RIGHT; + MOVE_LEFT, MOVE_RIGHT, JUMP; } } diff --git a/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java b/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java new file mode 100644 index 0000000..b0b1a31 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java @@ -0,0 +1,12 @@ +package coffee.liz.dyl.config; + +public final class PhysicsConstants { + public static final float GRAVITY = 9.8f; + public static final float MOVE_SPEED = 5.0f; + public static final float JUMP_INITIAL_VEL = 8.0f; + public static final float JUMP_ACC = 15.0f; + public static final long MAX_JUMP_MS = 150L; + + private PhysicsConstants() { + } +} diff --git a/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java index 54c5df7..a982bb9 100644 --- a/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java +++ b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java @@ -1,8 +1,15 @@ 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.common.components.physics.BoundingBox; +import coffee.liz.ecs.common.components.physics.Collidable; +import coffee.liz.ecs.common.components.physics.Forces; +import coffee.liz.ecs.common.components.physics.Gravity; +import coffee.liz.ecs.common.components.physics.Jump; +import coffee.liz.ecs.common.components.physics.Mass; +import coffee.liz.ecs.common.components.physics.Velocity; +import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.model.Entity; import coffee.liz.ecs.model.World; import com.badlogic.gdx.Gdx; @@ -15,16 +22,23 @@ 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"); - private static Texture texture = new Texture(FILE);; + private static Texture texture = new Texture(FILE); - public static Entity addTo(final World<?> world) { + public static Entity addTo(final World world) { 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].getTexture())) + .add(new TextureGraphic(0, Color.PINK, tmp[0][0].getTexture())) .add(new Controllable()) - .add(new Velocity()); + .add(new BoundingBox(new Vec2f(2f, 2f), new Vec2f(1f, 1f))) + .add(new Velocity(Vec2f.ZERO)) + .add(new Mass(1f)) + .add(new Forces()) + .add(new Gravity(20f)) + .add(new Jump(false, 0L)) + .add(new Collidable()); } - private PlayerFactory() { } + 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 index 39017c5..0797e3e 100644 --- a/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java +++ b/core/src/main/java/coffee/liz/dyl/screen/GameScreen.java @@ -1,9 +1,7 @@ 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; @@ -13,20 +11,15 @@ 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, Math.min(delta, MAX_DELTA_SECONDS)); + dylGameWorld.update(Math.min(delta, MAX_DELTA_SECONDS)); } @Override diff --git a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java index 1519b3e..d5acbaf 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java @@ -1,42 +1,65 @@ package coffee.liz.dyl.systems; -import coffee.liz.dyl.FrameState; -import coffee.liz.dyl.components.BoundingBox; -import coffee.liz.dyl.components.Velocity; +import coffee.liz.dyl.components.Controllable; import coffee.liz.dyl.config.KeyBinds; -import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.common.components.physics.Force; +import coffee.liz.ecs.common.components.physics.Forces; +import coffee.liz.ecs.common.components.physics.Jump; +import coffee.liz.ecs.common.components.physics.Mass; +import coffee.liz.ecs.common.components.physics.Velocity; import coffee.liz.ecs.math.Vec2f; -import coffee.liz.ecs.math.Vec2i; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; +import lombok.RequiredArgsConstructor; +import java.time.Instant; import java.util.Collection; -import java.util.Map; import java.util.Set; +import java.util.function.Supplier; + +import static coffee.liz.dyl.config.PhysicsConstants.JUMP_ACC; +import static coffee.liz.dyl.config.PhysicsConstants.JUMP_INITIAL_VEL; +import static coffee.liz.dyl.config.PhysicsConstants.MAX_JUMP_MS; +import static coffee.liz.dyl.config.PhysicsConstants.MOVE_SPEED; + +@RequiredArgsConstructor +public class InputSystem implements System { + private final Supplier<Set<KeyBinds.Action>> activeActions; -public class InputSystem implements System<FrameState> { @Override - public Collection<Class<? extends System<FrameState>>> getDependencies() { + public Collection<Class<? extends System>> 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()); + public void update(final World world, final float deltaSeconds) { + final Set<KeyBinds.Action> actions = activeActions.get(); - final Vec2<Float> momentum = currentlyActive.stream().map(movementVectors::get) - .reduce(Vec2::plus) - .orElse(Vec2f.ZERO); + world.queryable().allOf(Controllable.class, Velocity.class).forEach(entity -> { + final Velocity velocity = entity.get(Velocity.class); - world.queryable().allOf(Velocity.class, BoundingBox.class) - .forEach(e -> e.get(Velocity.class).setVelocity(momentum)); - } + float dx = 0f; + if (actions.contains(KeyBinds.Action.MOVE_LEFT)) dx = -MOVE_SPEED; + if (actions.contains(KeyBinds.Action.MOVE_RIGHT)) dx = MOVE_SPEED; + + float newDy = velocity.getVelocity().getY(); - private static Map<KeyBinds.Action, Vec2<Float>> movementVectors = Map.of( - KeyBinds.Action.MOVE_UP, Vec2i.NORTH.floatValue(), - KeyBinds.Action.MOVE_DOWN, Vec2i.SOUTH.floatValue(), - KeyBinds.Action.MOVE_LEFT, Vec2i.WEST.floatValue(), - KeyBinds.Action.MOVE_RIGHT, Vec2i.EAST.floatValue() - ); + if (entity.has(Jump.class) && entity.has(Mass.class) && entity.has(Forces.class)) { + final Jump jump = entity.get(Jump.class); + final Mass mass = entity.get(Mass.class); + + if (actions.contains(KeyBinds.Action.JUMP)) { + if (jump.isCanJump()) { + newDy = JUMP_INITIAL_VEL; + jump.setCanJump(false); + jump.setJumpStartMs(Instant.now().toEpochMilli()); + } else if (Instant.now().toEpochMilli() - jump.getJumpStartMs() < MAX_JUMP_MS) { + entity.get(Forces.class).add(new Force(new Vec2f(0f, mass.getMass() * JUMP_ACC))); + } + } + } + + velocity.setVelocity(new Vec2f(dx, newDy)); + }); + } } diff --git a/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java deleted file mode 100644 index cd7d7b1..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/IntegrationSystem.java +++ /dev/null @@ -1,32 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.FrameState; -import coffee.liz.dyl.components.BoundingBox; -import coffee.liz.dyl.components.Velocity; -import coffee.liz.ecs.math.Vec2; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; -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, BoundingBox.class) - .forEach(e -> { - final Vec2<Float> velocity = e.get(Velocity.class).getVelocity(); - final BoundingBox box = e.get(BoundingBox.class); - final Vec2<Float> position = box.getPosition() - .plus(velocity.scale(deltaSeconds, deltaSeconds)); - box.setPosition(position); - }); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java b/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java index 86c18fd..15e1f30 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java @@ -1,8 +1,8 @@ package coffee.liz.dyl.systems; -import coffee.liz.dyl.FrameState; import coffee.liz.dyl.components.graphic.Graphic; -import coffee.liz.dyl.components.BoundingBox; +import coffee.liz.ecs.common.components.physics.BoundingBox; +import coffee.liz.ecs.common.systems.physics.CollisionSystem; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; import com.badlogic.gdx.graphics.Color; @@ -14,7 +14,7 @@ import java.util.Collection; import java.util.Comparator; import java.util.Set; -public class RenderSystem implements System<FrameState> { +public class RenderSystem implements System { private final Batch batch; private final OrthographicCamera camera; private final Viewport viewport; @@ -26,12 +26,12 @@ public class RenderSystem implements System<FrameState> { } @Override - public Collection<Class<? extends System<FrameState>>> getDependencies() { - return Set.of(IntegrationSystem.class); + public Collection<Class<? extends System>> getDependencies() { + return Set.of(CollisionSystem.class); } @Override - public void update(final World<FrameState> world, final FrameState state, final float deltaSeconds) { + public void update(final World world, final float deltaSeconds) { viewport.apply(); camera.update(); @@ -40,7 +40,7 @@ public class RenderSystem implements System<FrameState> { batch.setColor(Color.WHITE); world.queryable().allOf(BoundingBox.class, Graphic.class).stream() - .sorted(Comparator.comparing(e -> e.get(BoundingBox.class))) + .sorted(Comparator.comparingDouble(e -> e.get(Graphic.class).getZ())) .forEach(e -> { final BoundingBox boundingBox = e.get(BoundingBox.class); final Graphic graphic = e.get(Graphic.class); diff --git a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java index a52ea0f..7f1cb62 100644 --- a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java +++ b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java @@ -1,26 +1,31 @@ package coffee.liz.dyl.world; import coffee.liz.dyl.DylGame; -import coffee.liz.dyl.FrameState; -import coffee.liz.dyl.components.BoundingBox; +import coffee.liz.dyl.config.PhysicsConstants; 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 coffee.liz.ecs.common.components.physics.BoundingBox; +import coffee.liz.ecs.common.components.physics.TopCollidable; +import coffee.liz.ecs.common.systems.physics.CollisionSystem; +import coffee.liz.ecs.common.systems.physics.ForceReductionSystem; +import coffee.liz.ecs.common.systems.physics.IntegrationSystem; import coffee.liz.ecs.math.Vec2f; -import coffee.liz.ecs.math.Vec2i; +import com.badlogic.gdx.Gdx; -public class DylGameWorld extends DAGWorld<FrameState> { +public class DylGameWorld extends DAGWorld { public DylGameWorld(final DylGame game) { super( - new InputSystem(), + new InputSystem(() -> game.getSettings().getKeyBinds().filterActiveActions(Gdx.input::isKeyPressed)), + new ForceReductionSystem(PhysicsConstants.GRAVITY), new IntegrationSystem(), + new CollisionSystem(PhysicsConstants.GRAVITY), new RenderSystem(game.getBatch(), game.getViewport()) ); - for (int i = 0; i < 2_000; i++) { - PlayerFactory.addTo(this) - .add(new BoundingBox(new Vec2f(i / 200f, i/ 200f), new Vec2i(1, 1), 1)); - } + PlayerFactory.addTo(this); + createEntity() + .add(new BoundingBox(new Vec2f(-50f, -1f), new Vec2f(200f, 1f))) + .add(new TopCollidable()); } } diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java index 0cc7d5d..f941dba 100644 --- a/core/src/main/java/coffee/liz/ecs/DAGWorld.java +++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java @@ -8,7 +8,6 @@ import coffee.liz.ecs.model.QueryBuilder; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; -import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import java.util.ArrayList; @@ -26,20 +25,17 @@ 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> { +public class DAGWorld implements World { private final Set<Entity> entities = Collections.synchronizedSet(new HashSet<>()); private final ComponentCache componentCache = new ComponentCache(); private final Consumer<EntityEvent> entityEventConsumer = componentCache::onEntityEvent; private final AtomicInteger nextEntityId = new AtomicInteger(0); - protected final Map<Class<? extends System<T>>, System<T>> systems; - private final List<System<T>> systemExecutionOrder; - private final QueryBuilder<T> queryBuilder = new QueryBuilder<>(this); + protected final Map<Class<? extends System>, System> systems; + private final List<System> systemExecutionOrder; + private final QueryBuilder queryBuilder = new QueryBuilder(this); - @SafeVarargs - public DAGWorld(final System<T>... systems) { + public DAGWorld(final System... systems) { this.systems = singletonClazzMap(systems); this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems)); log.info("Executing in order: {}", systemExecutionOrder); @@ -72,18 +68,18 @@ public class DAGWorld<T> implements World<T> { } @Override - public QueryBuilder<T> queryable() { + public QueryBuilder queryable() { return queryBuilder; } @Override - public void update(final T state, final float deltaSeconds) { - systemExecutionOrder.forEach(system -> system.update(this, state, deltaSeconds)); + public void update(final float deltaSeconds) { + systemExecutionOrder.forEach(system -> system.update(this, deltaSeconds)); } @SuppressWarnings("unchecked") @Override - public <S extends System<T>> S getSystem(final Class<S> system) { + public <S extends System> S getSystem(final Class<S> system) { return (S) systems.get(system); } @@ -98,7 +94,6 @@ public class DAGWorld<T> implements World<T> { if (components.isEmpty()) { return Collections.emptySet(); } - final Set<Entity> matches = new HashSet<>(); components.forEach(componentType -> matches.addAll(componentCache.entitiesWith(componentType))); return matches; @@ -108,10 +103,8 @@ public class DAGWorld<T> implements World<T> { if (components.isEmpty()) { return entities; } - final Set<Entity> excluded = new HashSet<>(); components.forEach(componentType -> excluded.addAll(componentCache.entitiesWith(componentType))); - final Set<Entity> result = new HashSet<>(); entities.forEach(entity -> { if (!excluded.contains(entity)) { @@ -121,41 +114,35 @@ public class DAGWorld<T> implements World<T> { return result; } - private List<System<T>> buildExecutionOrder(final Collection<System<T>> systems) { + private List<System> buildExecutionOrder(final Collection<System> systems) { if (systems.isEmpty()) { return Collections.emptyList(); } - final Map<Class<?>, System<T>> systemMap = systems.stream() + final Map<Class<?>, System> 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); - } - }); + inDegree.put(system.getClass(), 0); + adjacencyList.put(system.getClass(), new HashSet<>()); }); - // Kahn's algorithm - final List<System<T>> result = new ArrayList<>(); + systems.forEach(system -> system.getDependencies().forEach(dependency -> { + if (systemMap.containsKey(dependency)) { + adjacencyList.get(dependency).add(system.getClass()); + inDegree.merge(system.getClass(), 1, Integer::sum); + } + })); + final List<System> result = new ArrayList<>(); final Queue<Class<?>> queue = new LinkedList<>( - inDegree.entrySet().stream().filter(entry -> entry.getValue() == 0).map(Map.Entry::getKey).toList()); + inDegree.entrySet().stream().filter(e -> e.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); @@ -174,23 +161,24 @@ public class DAGWorld<T> implements World<T> { @Override public void dispose() { - for (final System<T> system : systemExecutionOrder) { - system.dispose(); - } + systemExecutionOrder.forEach(System::dispose); entities.forEach(entity -> entity.unsubscribe(entityEventConsumer)); 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; + private static Map<Class<? extends System>, System> singletonClazzMap(final System... singletons) { + final boolean areSingletons = Arrays.stream(singletons) + .map(Object::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)); + return Arrays.stream(singletons) + .collect(Collectors.toMap( + s -> (Class<? extends System>) s.getClass(), + s -> s)); } } diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/BoundingBox.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/BoundingBox.java new file mode 100644 index 0000000..7a31cae --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/BoundingBox.java @@ -0,0 +1,34 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.model.Component; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class BoundingBox implements Component { + private Vec2<Float> position; + private Vec2<Float> size; + + public float getTop() { + return position.getY() + size.getY(); + } + + public float getRight() { + return position.getX() + size.getX(); + } + + public boolean isCollidingWith(final BoundingBox other) { + return getRight() > other.getPosition().getX() + && position.getX() < other.getRight() + && getTop() > other.getPosition().getY() + && position.getY() < other.getTop(); + } + + public boolean isAbove(final BoundingBox other) { + return position.getY() >= other.getTop(); + } +} diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/Collidable.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/Collidable.java new file mode 100644 index 0000000..166428a --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/Collidable.java @@ -0,0 +1,6 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.model.Component; + +public class Collidable implements Component { +} diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/Force.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/Force.java new file mode 100644 index 0000000..7d5dab1 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/Force.java @@ -0,0 +1,12 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.model.Component; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class Force implements Component { + private final Vec2<Float> force; +} diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/Forces.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/Forces.java new file mode 100644 index 0000000..f6c9ee8 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/Forces.java @@ -0,0 +1,20 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.model.Component; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class Forces implements Component { + private final List<Force> forces = new ArrayList<>(); + + public void add(final Force force) { + forces.add(force); + } + + public void clear() { + forces.clear(); + } +} diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/Gravity.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/Gravity.java new file mode 100644 index 0000000..44d5caf --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/Gravity.java @@ -0,0 +1,11 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.model.Component; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class Gravity implements Component { + private final float terminalVelocity; +} diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/Jump.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/Jump.java new file mode 100644 index 0000000..5224b32 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/Jump.java @@ -0,0 +1,14 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.model.Component; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter +@Setter +public class Jump implements Component { + private boolean canJump; + private long jumpStartMs; +} diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/Mass.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/Mass.java new file mode 100644 index 0000000..f38ccc0 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/Mass.java @@ -0,0 +1,11 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.model.Component; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class Mass implements Component { + private final float mass; +} diff --git a/core/src/main/java/coffee/liz/ecs/common/components/physics/TopCollidable.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/TopCollidable.java new file mode 100644 index 0000000..76185bd --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/TopCollidable.java @@ -0,0 +1,6 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.model.Component; + +public class TopCollidable implements Component { +} diff --git a/core/src/main/java/coffee/liz/dyl/components/Velocity.java b/core/src/main/java/coffee/liz/ecs/common/components/physics/Velocity.java index f31e861..9b6a36b 100644 --- a/core/src/main/java/coffee/liz/dyl/components/Velocity.java +++ b/core/src/main/java/coffee/liz/ecs/common/components/physics/Velocity.java @@ -1,17 +1,14 @@ -package coffee.liz.dyl.components; +package coffee.liz.ecs.common.components.physics; import coffee.liz.ecs.math.Vec2; -import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.model.Component; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; @AllArgsConstructor -@NoArgsConstructor @Getter @Setter public class Velocity implements Component { - private Vec2<Float> velocity = Vec2f.ZERO; + private Vec2<Float> velocity; } diff --git a/core/src/main/java/coffee/liz/ecs/common/systems/physics/CollisionGrid.java b/core/src/main/java/coffee/liz/ecs/common/systems/physics/CollisionGrid.java new file mode 100644 index 0000000..94202b4 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/systems/physics/CollisionGrid.java @@ -0,0 +1,65 @@ +package coffee.liz.ecs.common.systems.physics; + +import coffee.liz.ecs.common.components.physics.BoundingBox; +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.math.Vec2i; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class CollisionGrid { + private final Map<Vec2i, Set<Integer>> cells = new HashMap<>(); + private Vec2<Float> origin; + private Vec2<Float> cellSize; + + public void setOrigin(final Vec2<Float> origin) { + this.origin = origin; + } + + public void setCellSize(final Vec2<Float> cellSize) { + this.cellSize = cellSize; + } + + public void clear() { + cells.clear(); + } + + public void insert(final int entityId, final BoundingBox bb) { + final int minCx = cellX(bb.getPosition().getX()); + final int minCy = cellY(bb.getPosition().getY()); + final int maxCx = cellX(bb.getRight()); + final int maxCy = cellY(bb.getTop()); + for (int cx = minCx; cx <= maxCx; cx++) { + for (int cy = minCy; cy <= maxCy; cy++) { + cells.computeIfAbsent(new Vec2i(cx, cy), _ -> new HashSet<>()).add(entityId); + } + } + } + + public Set<Integer> getNeighborIds(final BoundingBox bb) { + final Set<Integer> neighbors = new HashSet<>(); + final int minCx = cellX(bb.getPosition().getX()); + final int minCy = cellY(bb.getPosition().getY()); + final int maxCx = cellX(bb.getRight()); + final int maxCy = cellY(bb.getTop()); + for (int cx = minCx; cx <= maxCx; cx++) { + for (int cy = minCy; cy <= maxCy; cy++) { + final Set<Integer> cell = cells.get(new Vec2i(cx, cy)); + if (cell != null) { + neighbors.addAll(cell); + } + } + } + return neighbors; + } + + private int cellX(final float x) { + return (int) Math.floor((x - origin.getX()) / cellSize.getX()); + } + + private int cellY(final float y) { + return (int) Math.floor((y - origin.getY()) / cellSize.getY()); + } +} diff --git a/core/src/main/java/coffee/liz/ecs/common/systems/physics/CollisionSystem.java b/core/src/main/java/coffee/liz/ecs/common/systems/physics/CollisionSystem.java new file mode 100644 index 0000000..14f0a2b --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/systems/physics/CollisionSystem.java @@ -0,0 +1,107 @@ +package coffee.liz.ecs.common.systems.physics; + +import coffee.liz.ecs.common.components.physics.BoundingBox; +import coffee.liz.ecs.common.components.physics.Collidable; +import coffee.liz.ecs.common.components.physics.Force; +import coffee.liz.ecs.common.components.physics.Forces; +import coffee.liz.ecs.common.components.physics.Gravity; +import coffee.liz.ecs.common.components.physics.Jump; +import coffee.liz.ecs.common.components.physics.Mass; +import coffee.liz.ecs.common.components.physics.TopCollidable; +import coffee.liz.ecs.common.components.physics.Velocity; +import coffee.liz.ecs.math.Vec2f; +import coffee.liz.ecs.math.Vec2i; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@RequiredArgsConstructor +@AllArgsConstructor +public class CollisionSystem implements System { + private final float gravity; + private float cellSize = 64f; + private final CollisionGrid grid = new CollisionGrid(); + + @Override + public Collection<Class<? extends System>> getDependencies() { + return Set.of(IntegrationSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + final Set<Entity> collidableEntities = world.queryable().allOf(BoundingBox.class, Collidable.class); + final Set<Entity> surfaceEntities = world.queryable().allOf(BoundingBox.class, TopCollidable.class); + + if (collidableEntities.isEmpty() || surfaceEntities.isEmpty()) { + return; + } + + final Map<Integer, Entity> entityMap = new HashMap<>(); + collidableEntities.forEach(e -> entityMap.put(e.getId(), e)); + surfaceEntities.forEach(e -> entityMap.put(e.getId(), e)); + + float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; + for (final Entity e : entityMap.values()) { + final BoundingBox bb = e.get(BoundingBox.class); + minX = Math.min(minX, bb.getPosition().getX()); + minY = Math.min(minY, bb.getPosition().getY()); + } + + grid.clear(); + grid.setOrigin(new Vec2f(minX, minY)); + grid.setCellSize(new Vec2f(cellSize, cellSize)); + entityMap.forEach((id, e) -> grid.insert(id, e.get(BoundingBox.class))); + + final Set<Vec2i> checkedPairs = new HashSet<>(); + + for (final Entity entityA : collidableEntities) { + if (!entityA.has(Velocity.class)) { + continue; + } + final BoundingBox bbA = entityA.get(BoundingBox.class); + final Velocity velocity = entityA.get(Velocity.class); + + for (final int idB : grid.getNeighborIds(bbA)) { + final int idA = entityA.getId(); + if (idB == idA) { + continue; + } + if (!checkedPairs.add(new Vec2i(Math.min(idA, idB), Math.max(idA, idB)))) { + continue; + } + + final Entity entityB = entityMap.get(idB); + if (entityB == null || !entityB.has(TopCollidable.class)) { + continue; + } + + final BoundingBox bbB = entityB.get(BoundingBox.class); + if (!bbA.isCollidingWith(bbB)) { + continue; + } + if (velocity.getVelocity().getY() > 0 || bbA.isAbove(bbB)) { + continue; + } + + bbA.setPosition(new Vec2f(bbA.getPosition().getX(), bbB.getTop())); + velocity.setVelocity(new Vec2f(velocity.getVelocity().getX(), 0f)); + + if (entityA.has(Jump.class)) { + entityA.get(Jump.class).setCanJump(true); + } + if (entityA.has(Gravity.class) && entityA.has(Forces.class) && entityA.has(Mass.class)) { + entityA.get(Forces.class).add(new Force(new Vec2f(0f, entityA.get(Mass.class).getMass() * gravity))); + } + } + } + } +} diff --git a/core/src/main/java/coffee/liz/ecs/common/systems/physics/ForceReductionSystem.java b/core/src/main/java/coffee/liz/ecs/common/systems/physics/ForceReductionSystem.java new file mode 100644 index 0000000..4a51ba8 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/systems/physics/ForceReductionSystem.java @@ -0,0 +1,55 @@ +package coffee.liz.ecs.common.systems.physics; + +import coffee.liz.ecs.common.components.physics.Force; +import coffee.liz.ecs.common.components.physics.Forces; +import coffee.liz.ecs.common.components.physics.Gravity; +import coffee.liz.ecs.common.components.physics.Jump; +import coffee.liz.ecs.common.components.physics.Mass; +import coffee.liz.ecs.common.components.physics.Velocity; +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.math.Vec2f; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.Set; + +@RequiredArgsConstructor +public class ForceReductionSystem implements System { + private final float gravity; + + @Override + public Collection<Class<? extends System>> getDependencies() { + return Set.of(); + } + + @Override + public void update(final World world, final float deltaSeconds) { + world.queryable().allOf(Forces.class, Mass.class, Velocity.class).forEach(entity -> { + final Forces forces = entity.get(Forces.class); + final Mass mass = entity.get(Mass.class); + final Velocity velocity = entity.get(Velocity.class); + + if (entity.has(Gravity.class)) { + final Gravity gravityComponent = entity.get(Gravity.class); + if (velocity.getVelocity().getY() > -gravityComponent.getTerminalVelocity()) { + forces.add(new Force(new Vec2f(0f, -mass.getMass() * gravity))); + } + } + + Vec2<Float> netForce = Vec2f.ZERO; + for (final Force f : forces.getForces()) { + netForce = netForce.plus(f.getForce()); + } + forces.clear(); + + final Vec2<Float> acceleration = netForce.scale(1f / mass.getMass(), 1f / mass.getMass()); + velocity.setVelocity(velocity.getVelocity().plus(acceleration.scale(deltaSeconds, deltaSeconds))); + + if (entity.has(Jump.class) && acceleration.getY() < 0) { + entity.get(Jump.class).setCanJump(false); + } + }); + } +} diff --git a/core/src/main/java/coffee/liz/ecs/common/systems/physics/IntegrationSystem.java b/core/src/main/java/coffee/liz/ecs/common/systems/physics/IntegrationSystem.java new file mode 100644 index 0000000..ba38b70 --- /dev/null +++ b/core/src/main/java/coffee/liz/ecs/common/systems/physics/IntegrationSystem.java @@ -0,0 +1,25 @@ +package coffee.liz.ecs.common.systems.physics; + +import coffee.liz.ecs.common.components.physics.BoundingBox; +import coffee.liz.ecs.common.components.physics.Velocity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.Set; + +public class IntegrationSystem implements System { + @Override + public Collection<Class<? extends System>> getDependencies() { + return Set.of(ForceReductionSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + world.queryable().allOf(Velocity.class, BoundingBox.class).forEach(entity -> { + final BoundingBox bb = entity.get(BoundingBox.class); + final Velocity velocity = entity.get(Velocity.class); + bb.setPosition(bb.getPosition().plus(velocity.getVelocity().scale(deltaSeconds, deltaSeconds))); + }); + } +} diff --git a/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java index eba5021..bc7c4f7 100644 --- a/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java +++ b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java @@ -5,8 +5,8 @@ import lombok.RequiredArgsConstructor; import java.util.Set; @RequiredArgsConstructor -public class QueryBuilder<T> { - private final World<T> world; +public class QueryBuilder { + private final World world; @SafeVarargs public final Set<Entity> allOf(final Class<? extends Component>... 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 f16cdba..e9e0729 100644 --- a/core/src/main/java/coffee/liz/ecs/model/System.java +++ b/core/src/main/java/coffee/liz/ecs/model/System.java @@ -2,29 +2,10 @@ package coffee.liz.ecs.model; 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(); +public interface System { + Collection<Class<? extends System>> 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); + void update(World world, 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 index 82e01a7..63335c8 100644 --- a/core/src/main/java/coffee/liz/ecs/model/World.java +++ b/core/src/main/java/coffee/liz/ecs/model/World.java @@ -1,63 +1,21 @@ 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}. - */ +public interface World { Entity createEntity(); - /** - * Remove an entity from the {@link World}. - * - * @param entity - * to remove. - */ - void removeEntity(final Entity entity); + void removeEntity(Entity entity); - /** - * Get entities with {@link Component}s. - * - * @param query - * to query. - * @return All entities satisfying {@param query}. - */ - Set<Entity> resolve(final Query query); + Set<Entity> resolve(Query query); - default QueryBuilder<T> queryable() { - return new QueryBuilder<>(this); + default QueryBuilder 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); + void update(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); + <S extends System> S getSystem(Class<S> system); default void dispose() { } diff --git a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java index 4825e36..7f3dc2f 100644 --- a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java +++ b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Map; @@ -30,7 +29,7 @@ public class DAGWorldTest { @ParameterizedTest @MethodSource("queryScenarios") public void queryResolvesExpectedEntities(final Query query, final Set<String> expectedEntityKeys) { - final World<String> world = new DAGWorld<>(); + final World world = new DAGWorld(); final Entity both = world.createEntity(); both.add(new PositionComponent()); both.add(new VelocityComponent()); @@ -40,7 +39,7 @@ public class DAGWorldTest { velocityOnly.add(new VelocityComponent()); final Entity neither = world.createEntity(); - world.update("state", 0); + world.update(0); final Map<String, Entity> entities = Map.of("both", both, "positionOnly", positionOnly, "velocityOnly", velocityOnly, "neither", neither); @@ -60,16 +59,16 @@ public class DAGWorldTest { public void updateExecutesSystemsInTopologicalOrder() { final CopyOnWriteArrayList<String> executionLog = new CopyOnWriteArrayList<>(); - final DAGWorld<String> world = new DAGWorld<>(new SystemC(executionLog), new SystemA(executionLog), + final DAGWorld world = new DAGWorld(new SystemC(executionLog), new SystemA(executionLog), new SystemB(executionLog)); - world.update("state", 0); + world.update(0); assertEquals(List.of("A", "B", "C"), executionLog); } @Test public void cacheTracksComponentMutationsViaEntityEvents() { - final DAGWorld<String> world = new DAGWorld<>(); + final DAGWorld world = new DAGWorld(); final Entity subject = world.createEntity(); assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty()); @@ -83,7 +82,7 @@ public class DAGWorldTest { @Test public void removedEntityNoLongerMutatesWorldCache() { - final DAGWorld<String> world = new DAGWorld<>(); + final DAGWorld world = new DAGWorld(); final Entity subject = world.createEntity(); subject.add(new PositionComponent()); @@ -100,13 +99,12 @@ public class DAGWorldTest { public void queryFindsComponentsAddedByEarlierSystemInSameTick() { final List<Set<Entity>> queryResults = new CopyOnWriteArrayList<>(); - final DAGWorld<String> world = new DAGWorld<>(new ComponentAdderSystem(), - new ComponentReaderSystem(queryResults)); + final DAGWorld world = new DAGWorld(new ComponentAdderSystem(), new ComponentReaderSystem(queryResults)); final Entity entity = world.createEntity(); entity.add(new PositionComponent()); - world.update("state", 0); + world.update(0); assertEquals(1, queryResults.size()); assertEquals(Set.of(entity), queryResults.get(0)); @@ -114,7 +112,7 @@ public class DAGWorldTest { @Test public void circularDependencyDetectionThrowsIllegalStateException() { - assertThrows(IllegalStateException.class, () -> new DAGWorld<>(new SystemCycleA(), new SystemCycleB())); + assertThrows(IllegalStateException.class, () -> new DAGWorld(new SystemCycleA(), new SystemCycleB())); } private static final class PositionComponent implements Component { @@ -124,12 +122,12 @@ public class DAGWorldTest { } @RequiredArgsConstructor - private abstract static class RecordingSystem implements System<String> { + private abstract static class RecordingSystem implements System { private final List<String> log; private final String label; @Override - public final void update(final World<String> world, final String state, final float deltaSeconds) { + public final void update(final World world, final float deltaSeconds) { log.add(label); } } @@ -140,7 +138,7 @@ public class DAGWorldTest { } @Override - public Collection<Class<? extends System<String>>> getDependencies() { + public Collection<Class<? extends System>> getDependencies() { return List.of(); } } @@ -151,7 +149,7 @@ public class DAGWorldTest { } @Override - public Collection<Class<? extends System<String>>> getDependencies() { + public Collection<Class<? extends System>> getDependencies() { return Set.of(SystemA.class); } } @@ -162,56 +160,56 @@ public class DAGWorldTest { } @Override - public Collection<Class<? extends System<String>>> getDependencies() { + public Collection<Class<? extends System>> getDependencies() { return Set.of(SystemB.class); } } - private static final class SystemCycleA implements System<String> { + private static final class SystemCycleA implements System { @Override - public Collection<Class<? extends System<String>>> getDependencies() { + public Collection<Class<? extends System>> getDependencies() { return Set.of(SystemCycleB.class); } @Override - public final void update(final World<String> world, final String state, final float deltaSeconds) { + public void update(final World world, final float deltaSeconds) { } } - private static final class SystemCycleB implements System<String> { + private static final class SystemCycleB implements System { @Override - public Collection<Class<? extends System<String>>> getDependencies() { + public Collection<Class<? extends System>> getDependencies() { return Set.of(SystemCycleA.class); } @Override - public final void update(final World<String> world, final String state, final float deltaSeconds) { + public void update(final World world, final float deltaSeconds) { } } - private static final class ComponentAdderSystem implements System<String> { + private static final class ComponentAdderSystem implements System { @Override - public Collection<Class<? extends System<String>>> getDependencies() { + public Collection<Class<? extends System>> getDependencies() { return List.of(); } @Override - public final void update(final World<String> world, final String state, final float deltaSeconds) { + public void update(final World world, final float deltaSeconds) { world.resolve(Query.allOf(PositionComponent.class)).forEach(e -> e.add(new VelocityComponent())); } } @RequiredArgsConstructor - private static final class ComponentReaderSystem implements System<String> { + private static final class ComponentReaderSystem implements System { private final List<Set<Entity>> queryResults; @Override - public Collection<Class<? extends System<String>>> getDependencies() { + public Collection<Class<? extends System>> getDependencies() { return Set.of(ComponentAdderSystem.class); } @Override - public final void update(final World<String> world, final String state, final float deltaSeconds) { + public void update(final World world, final float deltaSeconds) { queryResults.add(world.resolve(Query.allOf(VelocityComponent.class, PositionComponent.class))); } } diff --git a/core/src/test/java/coffee/liz/ecs/common/components/physics/BoundingBoxTest.java b/core/src/test/java/coffee/liz/ecs/common/components/physics/BoundingBoxTest.java new file mode 100644 index 0000000..2372456 --- /dev/null +++ b/core/src/test/java/coffee/liz/ecs/common/components/physics/BoundingBoxTest.java @@ -0,0 +1,45 @@ +package coffee.liz.ecs.common.components.physics; + +import coffee.liz.ecs.math.Vec2f; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BoundingBoxTest { + @Test + public void isCollidingWith_overlapping() { + final BoundingBox a = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(2f, 2f)); + final BoundingBox b = new BoundingBox(new Vec2f(1f, 1f), new Vec2f(2f, 2f)); + assertTrue(a.isCollidingWith(b)); + assertTrue(b.isCollidingWith(a)); + } + + @Test + public void isCollidingWith_notOverlapping() { + final BoundingBox a = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(1f, 1f)); + final BoundingBox b = new BoundingBox(new Vec2f(2f, 2f), new Vec2f(1f, 1f)); + assertFalse(a.isCollidingWith(b)); + } + + @Test + public void isCollidingWith_touching_isNotColliding() { + final BoundingBox a = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(1f, 1f)); + final BoundingBox b = new BoundingBox(new Vec2f(1f, 0f), new Vec2f(1f, 1f)); + assertFalse(a.isCollidingWith(b)); + } + + @Test + public void isAbove_returnsTrue_whenBottomAtOrAboveOtherTop() { + final BoundingBox a = new BoundingBox(new Vec2f(0f, 2f), new Vec2f(1f, 1f)); + final BoundingBox b = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(1f, 1f)); + assertTrue(a.isAbove(b)); + } + + @Test + public void isAbove_returnsFalse_whenOverlapping() { + final BoundingBox a = new BoundingBox(new Vec2f(0f, 0.5f), new Vec2f(1f, 1f)); + final BoundingBox b = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(1f, 1f)); + assertFalse(a.isAbove(b)); + } +} diff --git a/core/src/test/java/coffee/liz/ecs/common/systems/physics/CollisionGridTest.java b/core/src/test/java/coffee/liz/ecs/common/systems/physics/CollisionGridTest.java new file mode 100644 index 0000000..d5670aa --- /dev/null +++ b/core/src/test/java/coffee/liz/ecs/common/systems/physics/CollisionGridTest.java @@ -0,0 +1,50 @@ +package coffee.liz.ecs.common.systems.physics; + +import coffee.liz.ecs.common.components.physics.BoundingBox; +import coffee.liz.ecs.math.Vec2f; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CollisionGridTest { + private final CollisionGrid grid = new CollisionGrid(); + + @BeforeEach + public void setup() { + grid.setOrigin(new Vec2f(0f, 0f)); + grid.setCellSize(new Vec2f(32f, 32f)); + } + + @Test + public void insertedEntityIsFoundAsNeighbor() { + final BoundingBox bb = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(1f, 1f)); + grid.insert(1, bb); + assertTrue(grid.getNeighborIds(bb).contains(1)); + } + + @Test + public void entityInDifferentCellIsNotNeighbor() { + final BoundingBox a = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(1f, 1f)); + final BoundingBox b = new BoundingBox(new Vec2f(100f, 100f), new Vec2f(1f, 1f)); + grid.insert(1, a); + assertFalse(grid.getNeighborIds(b).contains(1)); + } + + @Test + public void clearRemovesAllEntities() { + final BoundingBox bb = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(1f, 1f)); + grid.insert(1, bb); + grid.clear(); + assertTrue(grid.getNeighborIds(bb).isEmpty()); + } + + @Test + public void largeEntitySpanningMultipleCellsFoundByNeighborInAnyCell() { + final BoundingBox large = new BoundingBox(new Vec2f(0f, 0f), new Vec2f(64f, 64f)); + final BoundingBox corner = new BoundingBox(new Vec2f(50f, 50f), new Vec2f(1f, 1f)); + grid.insert(1, large); + assertTrue(grid.getNeighborIds(corner).contains(1)); + } +} diff --git a/core/src/test/java/coffee/liz/ecs/common/systems/physics/PhysicsSystemsTest.java b/core/src/test/java/coffee/liz/ecs/common/systems/physics/PhysicsSystemsTest.java new file mode 100644 index 0000000..13a4a1c --- /dev/null +++ b/core/src/test/java/coffee/liz/ecs/common/systems/physics/PhysicsSystemsTest.java @@ -0,0 +1,78 @@ +package coffee.liz.ecs.common.systems.physics; + +import coffee.liz.ecs.DAGWorld; +import coffee.liz.ecs.common.components.physics.BoundingBox; +import coffee.liz.ecs.common.components.physics.Collidable; +import coffee.liz.ecs.common.components.physics.Force; +import coffee.liz.ecs.common.components.physics.Forces; +import coffee.liz.ecs.common.components.physics.Gravity; +import coffee.liz.ecs.common.components.physics.Jump; +import coffee.liz.ecs.common.components.physics.Mass; +import coffee.liz.ecs.common.components.physics.TopCollidable; +import coffee.liz.ecs.common.components.physics.Velocity; +import coffee.liz.ecs.math.Vec2f; +import coffee.liz.ecs.model.Entity; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PhysicsSystemsTest { + private static final float GRAVITY = 9.8f; + + @Test + public void gravityAcceleratesEntityDownwardOverTime() { + final DAGWorld world = new DAGWorld(new ForceReductionSystem(GRAVITY), new IntegrationSystem()); + final Entity entity = world.createEntity(); + entity.add(new Mass(1f)); + entity.add(new Velocity(Vec2f.ZERO)); + entity.add(new Forces()); + entity.add(new Gravity(100f)); + entity.add(new BoundingBox(new Vec2f(0f, 10f), new Vec2f(1f, 1f))); + + world.update(1f); + + assertTrue(entity.get(Velocity.class).getVelocity().getY() < 0f); + assertTrue(entity.get(BoundingBox.class).getPosition().getY() < 10f); + } + + @Test + public void forcesAreClearedEachFrame() { + final DAGWorld world = new DAGWorld(new ForceReductionSystem(GRAVITY), new IntegrationSystem()); + final Entity entity = world.createEntity(); + entity.add(new Mass(1f)); + entity.add(new Velocity(Vec2f.ZERO)); + final Forces forces = new Forces(); + forces.add(new Force(new Vec2f(0f, 100f))); + entity.add(forces); + + world.update(1f); + + assertTrue(entity.get(Forces.class).getForces().isEmpty()); + } + + @Test + public void entityLandsOnPlatformAndStops() { + final DAGWorld world = new DAGWorld( + new ForceReductionSystem(GRAVITY), new IntegrationSystem(), new CollisionSystem(GRAVITY, 32f)); + + final Entity player = world.createEntity(); + player.add(new Mass(1f)); + player.add(new Velocity(new Vec2f(0f, -5f))); + player.add(new Forces()); + player.add(new Gravity(100f)); + player.add(new Jump(false, 0L)); + player.add(new BoundingBox(new Vec2f(0f, 1.1f), new Vec2f(1f, 1f))); + player.add(new Collidable()); + + final Entity floor = world.createEntity(); + floor.add(new BoundingBox(new Vec2f(-5f, 0f), new Vec2f(10f, 1f))); + floor.add(new TopCollidable()); + + world.update(0.1f); + + assertEquals(1f, player.get(BoundingBox.class).getPosition().getY(), 0.001f); + assertEquals(0f, player.get(Velocity.class).getVelocity().getY(), 0.001f); + assertTrue(player.get(Jump.class).isCanJump()); + } +} diff --git a/gradle.properties b/gradle.properties index 15acc17..8ffde07 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,8 +13,6 @@ org.gradle.configureondemand=false # the link to the scan won't get shown at all. # Documented at: https://docs.gradle.org/current/userguide/command_line_interface.html#sec:command_line_logging org.gradle.logging.level=lifecycle -aiVersion=1.8.2 -ashleyVersion=1.7.4 box2dlightsVersion=1.5 graalHelperVersion=2.0.1 enableGraalNative=false diff --git a/lwjgl3/build.gradle b/lwjgl3/build.gradle index 76f521b..33f6d9b 100644 --- a/lwjgl3/build.gradle +++ b/lwjgl3/build.gradle @@ -21,12 +21,8 @@ import io.github.fourlastor.construo.Target sourceSets.main.resources.srcDirs += [ rootProject.file('assets').path ] application.mainClass = 'coffee.liz.dyl.lwjgl3.Lwjgl3Launcher' application.applicationName = appName +application.applicationDefaultJvmArgs = ['-XX:+UseZGC', '-XX:+UseCompactObjectHeaders'] eclipse.project.name = appName + '-lwjgl3' -java.sourceCompatibility = JavaVersion.VERSION_24 -java.targetCompatibility = JavaVersion.VERSION_24 -if (JavaVersion.current().isJava9Compatible()) { - compileJava.options.release.set(24) -} dependencies { implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion" @@ -119,40 +115,34 @@ tasks.register("jarWin") { } construo { - // name of the executable name.set(appName) - // human-readable name, used for example in the `.app` name for macOS humanName.set(appName) + roast { + vmArgs.addAll('-XX:+UseCompactObjectHeaders') + } + targets.configure { register("linuxX64", Target.Linux) { architecture.set(Target.Architecture.X86_64) - jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/linux/x64/jdk/hotspot/normal/eclipse") - // Linux does not currently have a way to set the icon on the executable + jdkUrl.set("https://api.adoptium.net/v3/binary/latest/25/ga/linux/x64/jdk/hotspot/normal/eclipse") } register("macM1", Target.MacOs) { architecture.set(Target.Architecture.AARCH64) - jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/mac/aarch64/jdk/hotspot/normal/eclipse") - // macOS needs an identifier + jdkUrl.set("https://api.adoptium.net/v3/binary/latest/25/ga/mac/aarch64/jdk/hotspot/normal/eclipse") identifier.set("coffee.liz.dyl." + appName) - // Optional: icon for macOS, as an ICNS file macIcon.set(project.file("icons/logo.icns")) } register("macX64", Target.MacOs) { architecture.set(Target.Architecture.X86_64) - jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/mac/x64/jdk/hotspot/normal/eclipse") - // macOS needs an identifier + jdkUrl.set("https://api.adoptium.net/v3/binary/latest/25/ga/mac/x64/jdk/hotspot/normal/eclipse") identifier.set("coffee.liz.dyl." + appName) - // Optional: icon for macOS, as an ICNS file macIcon.set(project.file("icons/logo.icns")) } register("winX64", Target.Windows) { architecture.set(Target.Architecture.X86_64) - // Optional: icon for Windows, as a PNG icon.set(project.file("icons/logo.png")) - jdkUrl.set("https://api.adoptium.net/v3/binary/latest/24/ga/windows/x64/jdk/hotspot/normal/eclipse") - // Uncomment the next line to show a console when the game runs, to print messages. - //useConsole.set(true) + jdkUrl.set("https://api.adoptium.net/v3/binary/latest/25/ga/windows/x64/jdk/hotspot/normal/eclipse") } } } |
