diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2026-03-07 15:01:14 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2026-03-07 15:10:27 -0800 |
| commit | f643f0afb8c7d91a7a39ff96f58b95baac985ce0 (patch) | |
| tree | cfc13e8b8a53d968929b02224e3a7c8c01e35ec5 | |
| parent | f2135acc9accdc938035d753ee6e79c865fd44e2 (diff) | |
| download | dyl-f643f0afb8c7d91a7a39ff96f58b95baac985ce0.tar.gz dyl-f643f0afb8c7d91a7a39ff96f58b95baac985ce0.zip | |
Some really good refactoring happening in here
36 files changed, 435 insertions, 262 deletions
diff --git a/core/src/main/java/coffee/liz/dyl/actions/Action.java b/core/src/main/java/coffee/liz/dyl/actions/Action.java new file mode 100644 index 0000000..224cfc6 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/actions/Action.java @@ -0,0 +1,4 @@ +package coffee.liz.dyl.actions; + +public interface Action { +} diff --git a/core/src/main/java/coffee/liz/dyl/actions/JumpAction.java b/core/src/main/java/coffee/liz/dyl/actions/JumpAction.java new file mode 100644 index 0000000..0d8eda5 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/actions/JumpAction.java @@ -0,0 +1,4 @@ +package coffee.liz.dyl.actions; + +public class JumpAction implements Action { +} diff --git a/core/src/main/java/coffee/liz/dyl/actions/MoveAction.java b/core/src/main/java/coffee/liz/dyl/actions/MoveAction.java new file mode 100644 index 0000000..ad56387 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/actions/MoveAction.java @@ -0,0 +1,14 @@ +package coffee.liz.dyl.actions; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class MoveAction implements Action { + private final Direction direction; + + public enum Direction { + LEFT, RIGHT + } +} diff --git a/core/src/main/java/coffee/liz/dyl/components/control/ActionQueue.java b/core/src/main/java/coffee/liz/dyl/components/control/ActionQueue.java new file mode 100644 index 0000000..35b936c --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/control/ActionQueue.java @@ -0,0 +1,62 @@ +package coffee.liz.dyl.components.control; + +import coffee.liz.dyl.actions.Action; +import coffee.liz.ecs.model.Component; +import jakarta.annotation.Nullable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +public class ActionQueue implements Component { + private final Map<Class<? extends Action>, ExpiringAction> actionQueue = new HashMap<>(); + + public boolean has(final Class<? extends Action> actionType) { + return actionQueue.containsKey(actionType); + } + + public void queue(final Action action, final float currentTime) { + queue(action, currentTime, -1f); + } + + public void queue(final Action action, final float currentTime, final float expiresAfterSeconds) { + actionQueue.put(action.getClass(), new ExpiringAction(action, currentTime, expiresAfterSeconds)); + } + + @SuppressWarnings("unchecked") + @Nullable + public <T extends Action> T consume(final Class<T> actionClass, final float currentTime, final Predicate<T> consumed) { + final ExpiringAction expiringAction = actionQueue.get(actionClass); + if (expiringAction == null) { + return null; + } + if (expiringAction.isExpired(currentTime)) { + actionQueue.remove(actionClass); + return null; + } + + final Action action = expiringAction.getAction(); + if (action != null && actionClass.isAssignableFrom(action.getClass()) && consumed.test((T) action)) { + actionQueue.remove(actionClass); + return (T) action; + } + return null; + } + + @RequiredArgsConstructor + @Getter + private static class ExpiringAction { + private final Action action; + private final float queuedAt; + private final float expiresAfterSeconds; + + public boolean isExpired(final float currentTime) { + if (expiresAfterSeconds < 0f) { + return false; + } + return (currentTime - queuedAt) > expiresAfterSeconds; + } + } +} diff --git a/core/src/main/java/coffee/liz/dyl/components/Controllable.java b/core/src/main/java/coffee/liz/dyl/components/control/Controllable.java index c476c91..f63a1c2 100644 --- a/core/src/main/java/coffee/liz/dyl/components/Controllable.java +++ b/core/src/main/java/coffee/liz/dyl/components/control/Controllable.java @@ -1,4 +1,4 @@ -package coffee.liz.dyl.components; +package coffee.liz.dyl.components.control; import coffee.liz.ecs.model.Component; diff --git a/core/src/main/java/coffee/liz/dyl/components/FacingDirection.java b/core/src/main/java/coffee/liz/dyl/components/control/FacingDirection.java index d517fc1..280bfb0 100644 --- a/core/src/main/java/coffee/liz/dyl/components/FacingDirection.java +++ b/core/src/main/java/coffee/liz/dyl/components/control/FacingDirection.java @@ -1,12 +1,15 @@ -package coffee.liz.dyl.components; +package coffee.liz.dyl.components.control; import coffee.liz.ecs.math.Vec2; import coffee.liz.ecs.model.Component; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; -@RequiredArgsConstructor +@AllArgsConstructor @Getter +@Setter public class FacingDirection implements Component { - private final Vec2<Float> unitDirection; + private Vec2<Float> unitDirection; } diff --git a/core/src/main/java/coffee/liz/dyl/components/AnimationState.java b/core/src/main/java/coffee/liz/dyl/components/graphic/AnimationState.java index 04505c5..ad183da 100644 --- a/core/src/main/java/coffee/liz/dyl/components/AnimationState.java +++ b/core/src/main/java/coffee/liz/dyl/components/graphic/AnimationState.java @@ -1,9 +1,7 @@ -package coffee.liz.dyl.components; +package coffee.liz.dyl.components.graphic; -import coffee.liz.dyl.components.graphic.AnimationGraphic; import coffee.liz.ecs.model.Component; import jakarta.annotation.Nullable; -import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; diff --git a/core/src/main/java/coffee/liz/dyl/components/DamageGraceTime.java b/core/src/main/java/coffee/liz/dyl/components/health/DamageGraceTime.java index 8424639..ca16f63 100644 --- a/core/src/main/java/coffee/liz/dyl/components/DamageGraceTime.java +++ b/core/src/main/java/coffee/liz/dyl/components/health/DamageGraceTime.java @@ -1,4 +1,4 @@ -package coffee.liz.dyl.components; +package coffee.liz.dyl.components.health; import coffee.liz.ecs.model.Component; import lombok.RequiredArgsConstructor; diff --git a/core/src/main/java/coffee/liz/dyl/components/health/RelativeDamageBox.java b/core/src/main/java/coffee/liz/dyl/components/health/RelativeDamageBox.java new file mode 100644 index 0000000..e290d4f --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/components/health/RelativeDamageBox.java @@ -0,0 +1,10 @@ +package coffee.liz.dyl.components.health; + +import coffee.liz.ecs.model.Component; + +public class RelativeDamageBox implements Component { + // TODO: Bounding box relative to sprite bounding box + // A damage system will check that the damaging entity collides with the physics bounding box and then that it + // also collides with this component, so we don't have to index the grid twice. + // private final Vec2<Float> relativeDimensions; // To fill the center +} diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java b/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java index 49ab7f6..033c3b6 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/CollisionContacts.java @@ -12,16 +12,20 @@ import java.util.List; @Getter public class CollisionContacts implements Component { - @Getter - @RequiredArgsConstructor - public class Contact { - private final Entity contactEntity; - private final Vec2<Float> penetrationVector; + private final List<Contact> contacts = new ArrayList<>(); + + public void clear() { + contacts.clear(); } - private final List<Contact> contacts = new ArrayList<>(); + public void add(final Entity entity, final Vec2<Float> separationVector) { + contacts.add(new Contact(entity, separationVector)); + } - public void add(final Entity entity, final Vec2<Float> penetrationVector) { - contacts.add(new Contact(entity, penetrationVector)); + @Getter + @RequiredArgsConstructor + public static class Contact { + private final Entity contactEntity; + private final Vec2<Float> separationVector; } } diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java b/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java index 79d2f78..c791040 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/Jump.java @@ -4,15 +4,12 @@ import coffee.liz.ecs.model.Component; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; import lombok.Setter; -import java.time.Instant; - @NoArgsConstructor @AllArgsConstructor @Getter @Setter public class Jump implements Component { - private Instant jumpStart; + private float startTime; } diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java b/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java index d35464a..4ce8e74 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/Jumpable.java @@ -1,16 +1,6 @@ package coffee.liz.dyl.components.physics; import coffee.liz.ecs.model.Component; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; -import java.time.Instant; - -@AllArgsConstructor -@Getter -@Setter public class Jumpable implements Component { - private boolean canPhysicallyJump = false; - private Instant jumpRequestTime; } diff --git a/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java b/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java index 57af1e3..c9d4a78 100644 --- a/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java +++ b/core/src/main/java/coffee/liz/dyl/components/physics/Solid.java @@ -2,4 +2,5 @@ package coffee.liz.dyl.components.physics; import coffee.liz.ecs.model.Component; -public class Solid implements Component {} +public class Solid implements Component { +} diff --git a/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java b/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java index 3019160..6414629 100644 --- a/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java +++ b/core/src/main/java/coffee/liz/dyl/config/PhysicsConstants.java @@ -1,14 +1,12 @@ package coffee.liz.dyl.config; -import java.time.Duration; - public final class PhysicsConstants { public static final float GRAVITY = 40f; public static final float ADDITIONAL_JUMP_OVER_GRAVITY = 1.05f; public static final float MOVE_SPEED = 8.0f; public static final float JUMP_INITIAL_VEL = 10.0f; - public static final Duration MAX_JUMP = Duration.ofMillis(100); - public static final Duration JUMP_BUFFER = Duration.ofMillis(400); + public static final float MAX_JUMP_SECONDS = 0.1f; + public static final float JUMP_BUFFER_SECONDS = 0.2f; public static final float JUMP_CUT_FACTOR = 0.5f; public static final float DAMAGE_TIME = 0.25f; 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 ffd55f6..e5e8855 100644 --- a/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java +++ b/core/src/main/java/coffee/liz/dyl/entities/PlayerFactory.java @@ -1,17 +1,12 @@ package coffee.liz.dyl.entities; -import coffee.liz.dyl.components.AnimationState; -import coffee.liz.dyl.components.Controllable; -import coffee.liz.dyl.components.FacingDirection; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.graphic.AnimationState; +import coffee.liz.dyl.components.control.Controllable; +import coffee.liz.dyl.components.control.FacingDirection; import coffee.liz.dyl.components.graphic.AnimationGraphic; -import coffee.liz.dyl.components.physics.Jumpable; +import coffee.liz.dyl.components.physics.*; import coffee.liz.dyl.config.PhysicsConstants; -import coffee.liz.dyl.components.physics.BoundingBox; -import coffee.liz.dyl.components.physics.Forces; -import coffee.liz.dyl.components.physics.Gravity; -import coffee.liz.dyl.components.physics.Jump; -import coffee.liz.dyl.components.physics.Mass; -import coffee.liz.dyl.components.physics.Velocity; import coffee.liz.ecs.math.Vec2; import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.math.Vec2i; @@ -41,12 +36,14 @@ public class PlayerFactory { .add(animationState.getAnimationStates().get(AnimationState.State.IDLE)) .add(new Controllable()) .add(new BoundingBox(new Vec2f(1f, 1f), PlayerAssets.PLAYER_DIMS.floatValue().scale(1/6f, 1/6f))) + .add(new CollisionContacts()) + .add(new ActionQueue()) .add(new Velocity(Vec2f.ZERO)) .add(new Mass(0.8f)) .add(new Forces()) .add(new FacingDirection(Vec2f.ZERO)) .add(new Gravity(-20f)) - .add(new Jumpable(false, null)); + .add(new Jumpable()); } private static class PlayerAssets { diff --git a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java deleted file mode 100644 index ea0e574..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/InputSystem.java +++ /dev/null @@ -1,79 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.components.Controllable; -import coffee.liz.dyl.components.FacingDirection; -import coffee.liz.dyl.components.physics.Jumpable; -import coffee.liz.dyl.config.KeyBinds; -import coffee.liz.dyl.components.physics.Jump; -import coffee.liz.dyl.components.physics.Velocity; -import coffee.liz.ecs.math.Vec2f; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; -import lombok.RequiredArgsConstructor; - -import java.time.Duration; -import java.time.Instant; -import java.util.Collection; -import java.util.Set; -import java.util.function.Supplier; - -import static coffee.liz.dyl.config.PhysicsConstants.*; - -@RequiredArgsConstructor -public class InputSystem implements System { - private final Supplier<Set<KeyBinds.Action>> activeActions; - - @Override - public Collection<Class<? extends System>> getDependencies() { - return Set.of(); - } - - @Override - public void update(final World world, final float deltaSeconds) { - final Set<KeyBinds.Action> actions = activeActions.get(); - - world.queryable().allOf(Controllable.class, Velocity.class).forEach(entity -> { - final Velocity velocity = entity.get(Velocity.class); - - float dx = 0f; - if (actions.contains(KeyBinds.Action.MOVE_LEFT)) dx -= MOVE_SPEED; - if (actions.contains(KeyBinds.Action.MOVE_RIGHT)) dx += MOVE_SPEED; - if (dx != 0) { - entity.add(new FacingDirection(new Vec2f(dx / MOVE_SPEED, 0f))); - } - - final Instant now = Instant.now(); - final Jumpable jumpable = entity.get(Jumpable.class); - final boolean canRequestJump = jumpable.getJumpRequestTime() == null; - final boolean jumpRequested = actions.contains(KeyBinds.Action.JUMP); - final boolean hasCurrentJump = entity.has(Jump.class); - - float dy = velocity.getVelocity().getY(); - - // buffer only fires on a released press — holding never re-triggers - final boolean withinBuffer = !canRequestJump && !jumpRequested - && Duration.between(jumpable.getJumpRequestTime(), now).compareTo(JUMP_BUFFER) <= 0; - if (jumpable.isCanPhysicallyJump() && !hasCurrentJump && (canRequestJump && jumpRequested || withinBuffer)) { - entity.add(new Jump(now)); - dy = JUMP_INITIAL_VEL; - jumpable.setJumpRequestTime(now); // mark consumed; keeps canRequestJump false while held - } else if (!jumpRequested && hasCurrentJump && dy > 0) { - dy *= JUMP_CUT_FACTOR; - } - - // record an aerial press for buffering (ground presses are recorded above on initiation) - if (!jumpable.isCanPhysicallyJump() && canRequestJump && jumpRequested && !entity.has(Jump.class)) { - jumpable.setJumpRequestTime(now); - } else if (!jumpRequested && !entity.has(Jump.class)) { - if (jumpable.isCanPhysicallyJump()) { - jumpable.setJumpRequestTime(null); // released on ground — ready for next press - } else if (!canRequestJump - && Duration.between(jumpable.getJumpRequestTime(), now).compareTo(JUMP_BUFFER) > 0) { - jumpable.setJumpRequestTime(null); // aerial, buffer expired - } - } - - velocity.setVelocity(new Vec2f(dx, dy)); - }); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/JumpSystem.java b/core/src/main/java/coffee/liz/dyl/systems/JumpSystem.java deleted file mode 100644 index d1552f5..0000000 --- a/core/src/main/java/coffee/liz/dyl/systems/JumpSystem.java +++ /dev/null @@ -1,43 +0,0 @@ -package coffee.liz.dyl.systems; - -import coffee.liz.dyl.components.physics.CollisionContacts; -import coffee.liz.dyl.components.physics.Jump; -import coffee.liz.dyl.components.physics.Jumpable; -import coffee.liz.dyl.components.physics.Velocity; -import coffee.liz.dyl.systems.physics.CollisionSystem; -import coffee.liz.ecs.model.System; -import coffee.liz.ecs.model.World; - -import java.util.Collection; -import java.util.List; - -public class JumpSystem implements System { - @Override - public Collection<Class<? extends System>> getDependencies() { - return List.of(CollisionSystem.class); - } - - @Override - public void update(final World world, final float deltaSeconds) { - world.queryable().allOf(Jumpable.class).forEach(entity -> { - entity.get(Jumpable.class).setCanPhysicallyJump(false); - }); - world.queryable().allOf(Jumpable.class, CollisionContacts.class).forEach(entity -> { - final CollisionContacts contacts = entity.get(CollisionContacts.class); - final Jumpable jumpable = entity.get(Jumpable.class); - contacts.getContacts().forEach(contact -> { - final boolean solidUnderUs = contact.getPenetrationVector().getY() <= 0; - if (!solidUnderUs) { - return; - } - jumpable.setCanPhysicallyJump(true); - - final boolean currentJumpOver = entity.has(Jump.class) && entity.has(Velocity.class) - && entity.get(Velocity.class).getVelocity().getY() <= 0; - if (currentJumpOver) { - entity.remove(Jump.class); - } - }); - }); - } -} diff --git a/core/src/main/java/coffee/liz/dyl/systems/collision/SolidCollisionSystem.java b/core/src/main/java/coffee/liz/dyl/systems/collision/SolidCollisionSystem.java new file mode 100644 index 0000000..6f3697f --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/collision/SolidCollisionSystem.java @@ -0,0 +1,58 @@ +package coffee.liz.dyl.systems.collision; + +import coffee.liz.dyl.components.physics.BoundingBox; +import coffee.liz.dyl.components.physics.CollisionContacts; +import coffee.liz.dyl.components.physics.Solid; +import coffee.liz.dyl.components.physics.Velocity; +import coffee.liz.dyl.systems.physics.CollisionSystem; +import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.math.Vec2f; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class SolidCollisionSystem implements System { + @Override + public Collection<Class<? extends System>> getDependencies() { + return List.of(CollisionSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + world.queryable().allOf(CollisionContacts.class, BoundingBox.class, Velocity.class).forEach(me -> { + final CollisionContacts contacts = me.get(CollisionContacts.class); + contacts.getContacts().forEach(contact -> { + final Entity them = contact.getContactEntity(); + if (!them.has(Solid.class)) { + return; + } + + applyNormalImpulse(me, contact); + }); + }); + } + + private void applyNormalImpulse(final Entity me, final CollisionContacts.Contact contact) { + final float sLen = contact.getSeparationVector().length(); + if (sLen < 0.0001f) { + return; // Just touching, no separation needed + } + + final BoundingBox meBox = me.get(BoundingBox.class); + meBox.setPosition(meBox.getPosition().plus(contact.getSeparationVector())); + final float nx = contact.getSeparationVector().getX() / sLen; + final float ny = contact.getSeparationVector().getY() / sLen; + + final Vec2<Float> vel = me.get(Velocity.class).getVelocity(); + final float velIntoWall = -(vel.getX() * nx + vel.getY() * ny); + if (velIntoWall > 0f) { + me.get(Velocity.class).setVelocity(new Vec2f( + vel.getX() + nx * velIntoWall, + vel.getY() + ny * velIntoWall + )); + } + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/control/InputSystem.java b/core/src/main/java/coffee/liz/dyl/systems/control/InputSystem.java new file mode 100644 index 0000000..ca114ee --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/control/InputSystem.java @@ -0,0 +1,51 @@ +package coffee.liz.dyl.systems.control; + +import coffee.liz.dyl.actions.JumpAction; +import coffee.liz.dyl.actions.MoveAction; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.control.Controllable; +import coffee.liz.dyl.components.physics.Jumpable; +import coffee.liz.dyl.config.KeyBinds; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.Set; +import java.util.function.Supplier; + +import static coffee.liz.dyl.config.PhysicsConstants.*; + +@RequiredArgsConstructor +public class InputSystem implements System { + private final Supplier<Set<KeyBinds.Action>> heldActions; + private final Supplier<Set<KeyBinds.Action>> justPressedActions; + + @Override + public Collection<Class<? extends System>> getDependencies() { + return Set.of(); + } + + @Override + public void update(final World world, final float deltaSeconds) { + final float time = world.getTime(); + final Set<KeyBinds.Action> held = heldActions.get(); + final Set<KeyBinds.Action> justPressed = justPressedActions.get(); + + world.queryable().allOf(Controllable.class, ActionQueue.class).forEach(entity -> { + final ActionQueue actionQueue = entity.get(ActionQueue.class); + + final boolean moveLeft = held.contains(KeyBinds.Action.MOVE_LEFT); + final boolean moveRight = held.contains(KeyBinds.Action.MOVE_RIGHT); + if (moveLeft != moveRight) { + final MoveAction action = moveLeft ? new MoveAction(MoveAction.Direction.LEFT) + : new MoveAction(MoveAction.Direction.RIGHT); + actionQueue.queue(action, time); + } + + if (entity.has(Jumpable.class) && justPressed.contains(KeyBinds.Action.JUMP)) { + actionQueue.queue(new JumpAction(), time, JUMP_BUFFER_SECONDS); + } + }); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/control/JumpActionConsumerSystem.java b/core/src/main/java/coffee/liz/dyl/systems/control/JumpActionConsumerSystem.java new file mode 100644 index 0000000..eadc402 --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/control/JumpActionConsumerSystem.java @@ -0,0 +1,58 @@ +package coffee.liz.dyl.systems.control; + +import coffee.liz.dyl.actions.JumpAction; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.physics.*; +import coffee.liz.dyl.config.PhysicsConstants; +import coffee.liz.dyl.systems.collision.SolidCollisionSystem; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class JumpActionConsumerSystem implements System { + @Override + public Collection<Class<? extends System>> getDependencies() { + return List.of(InputSystem.class, SolidCollisionSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + final float time = world.getTime(); + + world.queryable().allOf(Jumpable.class, Velocity.class, CollisionContacts.class, ActionQueue.class).forEach(entity -> { + final ActionQueue queue = entity.get(ActionQueue.class); + final Velocity velocity = entity.get(Velocity.class); + final CollisionContacts contacts = entity.get(CollisionContacts.class); + final boolean solidOverUs = contacts.getContacts().stream() + .anyMatch(contact -> contact.getSeparationVector().getY() < 0 && contact.getContactEntity().has(Solid.class)); + final boolean solidUnderUs = contacts.getContacts().stream() + .anyMatch(contact -> contact.getSeparationVector().getY() > 0 && contact.getContactEntity().has(Solid.class)); + final boolean hasCurrentJump = entity.has(Jump.class); + + final boolean bonkedIntoCeiling = solidOverUs && hasCurrentJump; + final boolean justHitFloor = solidUnderUs && hasCurrentJump; + final boolean notJumping = !hasCurrentJump || bonkedIntoCeiling || justHitFloor; + if (notJumping) { + entity.remove(Jump.class); + } + + queue.consume(JumpAction.class, time, _action -> { + if (notJumping && solidUnderUs) { + entity.add(new Jump(time)); + } + if (entity.has(Jump.class)) { + final Jump jump = entity.get(Jump.class); + final boolean inTimeWindowToApplyJump = (time - jump.getStartTime()) <= PhysicsConstants.MAX_JUMP_SECONDS; + if (inTimeWindowToApplyJump) { + velocity.setVelocity(velocity.getVelocity() + .transform(x -> x, y -> PhysicsConstants.JUMP_INITIAL_VEL)); + return true; + } + } + return false; + }); + }); + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/control/MovementActionConsumerSystem.java b/core/src/main/java/coffee/liz/dyl/systems/control/MovementActionConsumerSystem.java new file mode 100644 index 0000000..9791cfb --- /dev/null +++ b/core/src/main/java/coffee/liz/dyl/systems/control/MovementActionConsumerSystem.java @@ -0,0 +1,48 @@ +package coffee.liz.dyl.systems.control; + +import coffee.liz.dyl.actions.MoveAction; +import coffee.liz.dyl.components.control.ActionQueue; +import coffee.liz.dyl.components.control.FacingDirection; +import coffee.liz.dyl.components.physics.Velocity; +import coffee.liz.dyl.config.PhysicsConstants; +import coffee.liz.ecs.math.Vec2f; +import coffee.liz.ecs.model.Entity; +import coffee.liz.ecs.model.System; +import coffee.liz.ecs.model.World; + +import java.util.Collection; +import java.util.List; + +public class MovementActionConsumerSystem implements System { + @Override + public Collection<Class<? extends System>> getDependencies() { + return List.of(InputSystem.class); + } + + @Override + public void update(final World world, final float deltaSeconds) { + final float time = world.getTime(); + + world.queryable().allOf(ActionQueue.class, Velocity.class, FacingDirection.class).forEach(entity -> { + final ActionQueue actionQueue = entity.get(ActionQueue.class); + if (!actionQueue.has(MoveAction.class)) { + final Velocity velocity = entity.get(Velocity.class); + velocity.setVelocity(velocity.getVelocity().transform(_x -> 0f, y -> y)); + return; + } + actionQueue.consume(MoveAction.class, time, (a) -> consumeAction(entity, a)); + }); + } + + private boolean consumeAction(final Entity entity, final MoveAction action) { + final Velocity velocity = entity.get(Velocity.class); + final FacingDirection facingDirection = entity.get(FacingDirection.class); + final float dx = switch(action.getDirection()) { + case LEFT -> -PhysicsConstants.MOVE_SPEED; + case RIGHT -> PhysicsConstants.MOVE_SPEED; + }; + facingDirection.setUnitDirection(new Vec2f(dx / PhysicsConstants.MOVE_SPEED, 0f)); + velocity.setVelocity(velocity.getVelocity().transform(_x -> dx, y -> y)); + return true; + } +} diff --git a/core/src/main/java/coffee/liz/dyl/systems/AnimationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/graphics/AnimationSystem.java index a932eea..a1e4874 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/AnimationSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/graphics/AnimationSystem.java @@ -1,11 +1,12 @@ -package coffee.liz.dyl.systems; +package coffee.liz.dyl.systems.graphics; -import coffee.liz.dyl.components.AnimationState; -import coffee.liz.dyl.components.FacingDirection; +import coffee.liz.dyl.components.graphic.AnimationState; +import coffee.liz.dyl.components.control.FacingDirection; import coffee.liz.dyl.components.graphic.AnimationGraphic; -import coffee.liz.dyl.components.graphic.Graphic; import coffee.liz.dyl.components.physics.Jump; import coffee.liz.dyl.components.physics.Velocity; +import coffee.liz.dyl.systems.health.DamageSystem; +import coffee.liz.dyl.systems.control.InputSystem; import coffee.liz.dyl.systems.physics.CollisionSystem; import coffee.liz.ecs.model.System; import coffee.liz.ecs.model.World; diff --git a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java b/core/src/main/java/coffee/liz/dyl/systems/graphics/RenderSystem.java index 1624d0f..5897c65 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/RenderSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/graphics/RenderSystem.java @@ -1,4 +1,4 @@ -package coffee.liz.dyl.systems; +package coffee.liz.dyl.systems.graphics; import coffee.liz.dyl.components.graphic.Graphic; import coffee.liz.dyl.components.physics.BoundingBox; diff --git a/core/src/main/java/coffee/liz/dyl/systems/DamageSystem.java b/core/src/main/java/coffee/liz/dyl/systems/health/DamageSystem.java index 60db760..b8ac306 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/DamageSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/health/DamageSystem.java @@ -1,4 +1,4 @@ -package coffee.liz.dyl.systems; +package coffee.liz.dyl.systems.health; import coffee.liz.dyl.systems.physics.CollisionSystem; import coffee.liz.ecs.model.System; diff --git a/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java b/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java index 519cc3f..c3c61d0 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/physics/AccelerationSystem.java @@ -7,7 +7,7 @@ import coffee.liz.dyl.components.physics.Jump; import coffee.liz.dyl.components.physics.Mass; import coffee.liz.dyl.components.physics.Velocity; import coffee.liz.dyl.config.PhysicsConstants; -import coffee.liz.dyl.systems.InputSystem; +import coffee.liz.dyl.systems.control.InputSystem; import coffee.liz.ecs.math.Vec2; import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.model.System; diff --git a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java index fb14b70..617fc22 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java +++ b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionGrid.java @@ -2,22 +2,29 @@ package coffee.liz.dyl.systems.physics; import coffee.liz.dyl.components.physics.BoundingBox; import coffee.liz.ecs.math.Vec2; +import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.math.Vec2i; import coffee.liz.ecs.model.Entity; import lombok.Getter; -import lombok.Setter; +import lombok.RequiredArgsConstructor; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; -@Setter @Getter +@RequiredArgsConstructor public class CollisionGrid { + private final Vec2<Float> cellSize; private final Map<Vec2i, Set<Entity>> cells = new HashMap<>(); private Vec2<Float> origin; - private Vec2<Float> cellSize; + + public void updateOrigin(final Collection<Vec2<Float>> points) { + float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; + for (final Vec2<Float> pos : points) { + minX = Math.min(minX, pos.getX()); + minY = Math.min(minY, pos.getY()); + } + this.origin = new Vec2f(minX, minY); + } public void clear() { cells.clear(); diff --git a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java index fb98151..099839e 100644 --- a/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java +++ b/core/src/main/java/coffee/liz/dyl/systems/physics/CollisionSystem.java @@ -2,9 +2,7 @@ package coffee.liz.dyl.systems.physics; import coffee.liz.dyl.components.physics.BoundingBox; import coffee.liz.dyl.components.physics.CollisionContacts; -import coffee.liz.dyl.components.physics.Solid; import coffee.liz.dyl.components.physics.Velocity; -import coffee.liz.dyl.systems.InputSystem; import coffee.liz.ecs.math.Vec2; import coffee.liz.ecs.math.Vec2f; import coffee.liz.ecs.model.Entity; @@ -13,12 +11,14 @@ import coffee.liz.ecs.model.World; import lombok.RequiredArgsConstructor; import java.util.Collection; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; @RequiredArgsConstructor public class CollisionSystem implements System { - private float cellSize = 3f; - private final CollisionGrid grid = new CollisionGrid(); + private static final Vec2<Float> CELL_SIZE = new Vec2f(3f, 3f); + private final CollisionGrid grid = new CollisionGrid(CELL_SIZE); @Override public Collection<Class<? extends System>> getDependencies() { @@ -27,20 +27,19 @@ public class CollisionSystem implements System { @Override public void update(final World world, final float deltaSeconds) { - world.queryable().allOf(CollisionContacts.class) - .forEach(e -> e.remove(CollisionContacts.class)); - final Set<Entity> allBoxEntities = world.queryable().allOf(BoundingBox.class); if (allBoxEntities.isEmpty()) { return; } + final List<Entity> allCollidingEntities = allBoxEntities.stream().filter(e -> e.has(CollisionContacts.class)).toList(); + allCollidingEntities.forEach(e -> e.get(CollisionContacts.class).clear()); grid.clear(); - final Vec2<Float> origin = getOrigin(allBoxEntities.stream() + + final List<Vec2<Float>> entityPositions = allBoxEntities.stream() .map(e -> e.get(BoundingBox.class).getPosition()) - .toList()); - grid.setOrigin(origin); - grid.setCellSize(new Vec2f(cellSize, cellSize)); + .toList(); + grid.updateOrigin(entityPositions); for (final Entity entity : allBoxEntities) { grid.insert(entity); } @@ -51,69 +50,31 @@ public class CollisionSystem implements System { if (me.equals(them)) { continue; } - if (!meBox.isCollidingWith(them.get(BoundingBox.class))) { + final BoundingBox themBox = them.get(BoundingBox.class); + if (!meBox.isCollidingWith(themBox)) { continue; } - if (them.has(Solid.class)) { - resolveCollision(me, them); + if (me.has(CollisionContacts.class)) { + me.get(CollisionContacts.class).add(them, getSeparationVector(meBox, themBox)); + } + if (them.has(CollisionContacts.class)) { + them.get(CollisionContacts.class).add(me, getSeparationVector(themBox, meBox)); } } } } - - private void resolveCollision(final Entity me, final Entity them) { - final BoundingBox meBox = me.get(BoundingBox.class); - final BoundingBox themBox = them.get(BoundingBox.class); - - final Vec2<Float> mtv = getPenetrationVector(meBox, themBox); - contacts(me).add(them, getPenetrationVector(themBox, meBox)); - contacts(them).add(me, mtv); - meBox.setPosition(meBox.getPosition().plus(mtv)); - - // Cancel the velocity component pressing into the solid (normal impulse) - final float mtvLen = mtv.length(); - final float nx = mtv.getX() / mtvLen; - final float ny = mtv.getY() / mtvLen; - - final Vec2<Float> vel = me.get(Velocity.class).getVelocity(); - final float velIntoWall = -(vel.getX() * nx + vel.getY() * ny); - if (velIntoWall > 0f) { - me.get(Velocity.class).setVelocity(new Vec2f( - vel.getX() + nx * velIntoWall, - vel.getY() + ny * velIntoWall - )); - } - } - - private Vec2<Float> getPenetrationVector(final BoundingBox meBox, final BoundingBox themBox) { + private Vec2<Float> getSeparationVector(final BoundingBox meBox, final BoundingBox themBox) { final float fromLeft = meBox.getRight() - themBox.getPosition().getX(); final float fromRight = themBox.getRight() - meBox.getPosition().getX(); final float fromBottom = meBox.getTop() - themBox.getPosition().getY(); final float fromTop = themBox.getTop() - meBox.getPosition().getY(); - final float penetrationX = fromLeft < fromRight ? -fromLeft : fromRight; - final float penetrationY = fromBottom < fromTop ? -fromBottom : fromTop; - - final Vec2<Float> minimumTranslationVector = Math.abs(penetrationX) <= Math.abs(penetrationY) - ? new Vec2f(penetrationX, 0f) - : new Vec2f(0f, penetrationY); - return minimumTranslationVector; - } - - private CollisionContacts contacts(final Entity entity) { - if (!entity.has(CollisionContacts.class)) { - entity.add(new CollisionContacts()); - } - return entity.get(CollisionContacts.class); - } + final float separationX = fromLeft < fromRight ? -fromLeft : fromRight; + final float separationY = fromBottom < fromTop ? -fromBottom : fromTop; - private Vec2<Float> getOrigin(final Collection<Vec2<Float>> positions) { - float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; - for (final Vec2<Float> pos : positions) { - minX = Math.min(minX, pos.getX()); - minY = Math.min(minY, pos.getY()); - } - return new Vec2f(minX, minY); + return Math.abs(separationX) <= Math.abs(separationY) + ? new Vec2f(separationX, 0f) + : new Vec2f(0f, separationY); } } 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 fd8e282..fb1f847 100644 --- a/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java +++ b/core/src/main/java/coffee/liz/dyl/world/DylGameWorld.java @@ -4,11 +4,13 @@ import coffee.liz.dyl.DylGame; import coffee.liz.dyl.config.PhysicsConstants; import coffee.liz.dyl.entities.FloorFactory; import coffee.liz.dyl.entities.PlayerFactory; -import coffee.liz.dyl.systems.AnimationSystem; -import coffee.liz.dyl.systems.DamageSystem; -import coffee.liz.dyl.systems.InputSystem; -import coffee.liz.dyl.systems.JumpSystem; -import coffee.liz.dyl.systems.RenderSystem; +import coffee.liz.dyl.systems.collision.SolidCollisionSystem; +import coffee.liz.dyl.systems.control.MovementActionConsumerSystem; +import coffee.liz.dyl.systems.graphics.AnimationSystem; +import coffee.liz.dyl.systems.health.DamageSystem; +import coffee.liz.dyl.systems.control.InputSystem; +import coffee.liz.dyl.systems.control.JumpActionConsumerSystem; +import coffee.liz.dyl.systems.graphics.RenderSystem; import coffee.liz.ecs.DAGWorld; import coffee.liz.dyl.components.physics.BoundingBox; import coffee.liz.dyl.components.physics.Solid; @@ -21,14 +23,18 @@ import com.badlogic.gdx.Gdx; public class DylGameWorld extends DAGWorld { public DylGameWorld(final DylGame game) { super( - new InputSystem(() -> game.getSettings().getKeyBinds().filterActiveActions(Gdx.input::isKeyPressed)), + new InputSystem( + () -> game.getSettings().getKeyBinds().filterActiveActions(Gdx.input::isKeyPressed), + () -> game.getSettings().getKeyBinds().filterActiveActions(Gdx.input::isKeyJustPressed)), new AccelerationSystem(PhysicsConstants.GRAVITY), new MovementSystem(), new CollisionSystem(), + new SolidCollisionSystem(), new DamageSystem(), new AnimationSystem(), new RenderSystem(game.getBatch(), game.getViewport()), - new JumpSystem() + new JumpActionConsumerSystem(), + new MovementActionConsumerSystem() ); PlayerFactory.addTo(this); FloorFactory.addTo(this); diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java index f941dba..f9801c2 100644 --- a/core/src/main/java/coffee/liz/ecs/DAGWorld.java +++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java @@ -34,6 +34,7 @@ public class DAGWorld implements World { protected final Map<Class<? extends System>, System> systems; private final List<System> systemExecutionOrder; private final QueryBuilder queryBuilder = new QueryBuilder(this); + private float elapsedTime = 0f; public DAGWorld(final System... systems) { this.systems = singletonClazzMap(systems); @@ -74,9 +75,15 @@ public class DAGWorld implements World { @Override public void update(final float deltaSeconds) { + elapsedTime += deltaSeconds; systemExecutionOrder.forEach(system -> system.update(this, deltaSeconds)); } + @Override + public float getTime() { + return elapsedTime; + } + @SuppressWarnings("unchecked") @Override public <S extends System> S getSystem(final Class<S> system) { diff --git a/core/src/main/java/coffee/liz/ecs/events/EventBus.java b/core/src/main/java/coffee/liz/ecs/events/EventBus.java index d814281..1ed04c5 100644 --- a/core/src/main/java/coffee/liz/ecs/events/EventBus.java +++ b/core/src/main/java/coffee/liz/ecs/events/EventBus.java @@ -28,13 +28,10 @@ public class EventBus<E> { /** * Remove a previously registered hook. * - * @param hook - * callback to remove - * @return true - * if the hook was removed. + * @param hook callback to remove */ - public boolean unsubscribe(final Consumer<E> hook) { - return subscriptions.remove(hook); + public void unsubscribe(final Consumer<E> hook) { + subscriptions.remove(hook); } /** diff --git a/core/src/main/java/coffee/liz/ecs/model/Query.java b/core/src/main/java/coffee/liz/ecs/model/Query.java index 679ea49..8b52d42 100644 --- a/core/src/main/java/coffee/liz/ecs/model/Query.java +++ b/core/src/main/java/coffee/liz/ecs/model/Query.java @@ -5,10 +5,6 @@ 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); 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 63335c8..88602fc 100644 --- a/core/src/main/java/coffee/liz/ecs/model/World.java +++ b/core/src/main/java/coffee/liz/ecs/model/World.java @@ -15,6 +15,8 @@ public interface World { void update(float deltaSeconds); + float getTime(); + <S extends System> S getSystem(Class<S> system); default void dispose() { diff --git a/design/bugs.txt b/docs/bugs.txt index e65b3ca..e65b3ca 100644 --- a/design/bugs.txt +++ b/docs/bugs.txt diff --git a/design/design.txt b/docs/design.txt index 74c720d..d0926ad 100644 --- a/design/design.txt +++ b/docs/design.txt @@ -50,7 +50,10 @@ Science Strategy manifests in maintaining the order of upgrades you choose to stay ahead of the threat. For instance, a distant civilization that is really really advanced in science might take less precidence over a medium advanced military civilization close by attacking soon. -The Oracles are key to your combat experiences. There are a few oracles. It marks on your map the positions of civilizations and when they plan on attacking. But it doesn't reveal what kind of civilization (magic, military, science, etc.) it is. Thus, you need to go scout the attackers to see what you need to plan / gather for. +The Oracles are key to your combat experiences. There are a few oracles. It marks on your map the positions of civilizations and when they plan on attacking (or just "rumors"). But it doesn't reveal what kind of civilization (magic, military, science, etc.) it is. Thus, you need to go scout the attackers to see what you need to plan / gather for. + +The actual combat will be driven by using your grappling hook to swing around and dodge enemies by grappling onto +ledges or trees? ===Game=== diff --git a/docs/input.txt b/docs/input.txt new file mode 100644 index 0000000..d5bc1a7 --- /dev/null +++ b/docs/input.txt @@ -0,0 +1,18 @@ +Entities will have an ActionQueue component. The ActionQueue is a set of actions and durations within which those +actions may be applied. This will solve the issue of buffering inputs as well as having multiple systems control which +actions an entity might take (for a future AI system, perhaps?). + +Let's introduce the jumping mechanics of this game as an example of the system interaction with entities. + +When the player presses a key mapped to a Jumping action, a Jump action is queued and expires in 80 milliseconds. +The JumpSystem consumes the action if the entity is able to "physically" jump: +- it is in contact with a solid underneath and has zero velocity on the y axis (?) and there is no current Jump component +- or, there is a Jump component but the "availableJump" counter is greater than zero (it is decremented on consumption + of the Jump action), to account for the future of a double jump being possible. + +If the JumpSystem does not consume() the action within the allotted 80 milliseconds, the action expires and is removed +from the queue. + +The queue is an action-singleton queue. Meaning, enqueueing an action has the effect of replacing any action of the same +type whose expiration is earlier than the enqueued action. + diff --git a/design/todo.txt b/docs/todo.txt index 023868e..023868e 100644 --- a/design/todo.txt +++ b/docs/todo.txt |
