diff --git a/demo/demo/game/entities/Player.java b/demo/demo/game/entities/Player.java index 7eb097c..537e773 100644 --- a/demo/demo/game/entities/Player.java +++ b/demo/demo/game/entities/Player.java @@ -51,7 +51,9 @@ public class Player extends DemoEntity { public Player(float x, float y) { super(x, y, WIDTH, HEIGHT, DemoEntity.TYPE_PLAYER); - hitbox.setAirFrictionCoefficient(10f); + // Player should slow down dramatically in the air + hitbox.airFrictionCoefficient = 10f; + hitbox.setMaxSpeedX(MAX_SPEED_X); } diff --git a/docs/TODO.md b/docs/TODO.md index 76b7a6d..f5f05d8 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,12 +1,27 @@ # To Do -## Engine +## Features - Support multiple Tile layers + - Support slopes with different gradients + + - Support large entities on slopes (currently untested) + +## Bugs + + - Slopes: + - Non-slope-traversing Entities (e.g. projectiles) collide at the wrong place + - Occasional jittering at the bottom of ceiling slopes + - Jittering when a slope leads into a ceiling + - Strange behaviour when colliding with the wrong side of a slope tile + - Strange behaviour when colliding with the left/right corners of a diamond + +## Tech Debt + - Move more code from demo project to engine? - - Improve test coverage + - Improve test coverage (use reflection to test private methods) ## Demo diff --git a/src/engine/game/GameUtils.java b/src/engine/game/GameUtils.java index 2bb42c1..a278261 100644 --- a/src/engine/game/GameUtils.java +++ b/src/engine/game/GameUtils.java @@ -224,12 +224,12 @@ public static float pxToWorld(int px) { //////////////////////////////////////////////////////////////////////////// /** - * Returns either -1 or 1 at random. + * Multiplies the given value by either -1 or 1 at random. * * @param i * @return */ - public static int randomSign(int i) { + public static float randomSign(float i) { return i * (Math.random() < 0.5 ? -1 : 1); } @@ -258,7 +258,7 @@ public static double randBetween(double min, double max, Random random) { } /** - * Returns a random int between the 2 limits. + * Returns a random int between the 2 limits (inclusive). * * @param min * @param max @@ -281,4 +281,40 @@ public static int randBetween(int min, int max, Random random) { return min + (int)(random.nextDouble() * ((max - min) + 1)); } + /** + * Clamps an integer between 2 limits. + * + * @param val + * @param min + * @param max + * @return + */ + public static int clamp(int val, int min, int max) { + if (val < min) { + return min; + } + if (val > max) { + return max; + } + return val; + } + + /** + * Clamps a float between 2 limits. + * + * @param val + * @param min + * @param max + * @return + */ + public static float clamp(float val, float min, float max) { + if (val < min) { + return min; + } + if (val > max) { + return max; + } + return val; + } + } diff --git a/src/engine/game/Level.java b/src/engine/game/Level.java index eb394e7..2187387 100644 --- a/src/engine/game/Level.java +++ b/src/engine/game/Level.java @@ -55,6 +55,7 @@ public boolean doesTileExist_X(int tileX) { /** * Determines if a tile co-ordinate is valid. + * * @param tileY * @return */ @@ -62,6 +63,17 @@ public boolean doesTileExist_Y(int tileY) { return tileY >= 0 && tileY < getNumTilesY(); } + /** + * Determines if a tile co-ordinate is valid. + * + * @param tileX + * @param tileY + * @return + */ + public boolean doesTileExist(int tileX, int tileY) { + return doesTileExist_X(tileX) && doesTileExist_Y(tileY); + } + /** * Gets the level width, in tiles. * diff --git a/src/engine/game/entities/CameraSettings.java b/src/engine/game/entities/CameraSettings.java index f07c5aa..6774ac1 100644 --- a/src/engine/game/entities/CameraSettings.java +++ b/src/engine/game/entities/CameraSettings.java @@ -35,8 +35,8 @@ public void entityTeleported() { } @Override - public void notify(ComponentEvent event) { - if (event instanceof EntityTeleported) { + public void notify(ComponentEvent eventBeforeCast) { + if (eventBeforeCast instanceof EntityTeleported) { entityTeleported(); } } diff --git a/src/engine/game/entities/Entity.java b/src/engine/game/entities/Entity.java index 416a850..d68a0dc 100644 --- a/src/engine/game/entities/Entity.java +++ b/src/engine/game/entities/Entity.java @@ -32,6 +32,11 @@ public abstract class Entity implements HitboxListener { */ public ComponentStore components = new ComponentStore<>(); + /** + * This Entity's physical presence within the game world. + */ + public Hitbox hitbox; + /** * Unique identifier used to refer to this Entity. * @@ -39,11 +44,6 @@ public abstract class Entity implements HitboxListener { */ protected int id = -1; - /** - * This Entity's physical presence within the game world. - */ - protected Hitbox hitbox; - /** * Flag set when this Entity is marked for deletion. */ @@ -72,15 +72,6 @@ public Entity(float x, float y, float width, float height) { // Getters //////////////////////////////////////////////////////////////////////////// - /** - * Gets this Entity's {@link Hitbox}. - * - * @return - */ - public Hitbox hitbox { - return hitbox; - } - /** * Determines whether this Entity has been deleted. * diff --git a/src/engine/game/physics/Collision.java b/src/engine/game/physics/Collision.java index dae7f4e..79068b6 100644 --- a/src/engine/game/physics/Collision.java +++ b/src/engine/game/physics/Collision.java @@ -1,5 +1,6 @@ package engine.game.physics; +import engine.game.physics.Hitbox.CollisionNode; import engine.game.tiles.ForegroundTile; /** @@ -9,36 +10,48 @@ */ public class Collision implements Comparable { + /** + * Absolute position of the collision (x or y), in world units. + */ + public final float collisionPos; + /** * Distance to the collision, in world units. */ - private float distanceToCollision; + public final float distanceToCollision; /** - * Absolute position of the collision (either x or y), in world units. + * The CollisionNode that triggered this Collision. */ - private float collisionPos; + public final CollisionNode node; /** * Tile with which the collision occurred. */ - private ForegroundTile tile; + public final ForegroundTile tile; + + /** + * Whether this Collision is valid. + */ + protected boolean valid = true; /** - * Constructor for a Collision. + * Constructs a Collision. * - * @param hitboxPos - * Position of the relevant hitbox edge before the collision (x or y). - * Used to calculate the distance to the collision. * @param collisionPos - * Position in the world where the collision occurred (x or y). - * @param tile Tile with which the collision occurred. + * @param node + * @param tile + * @param distanceToCollision */ - public Collision(float hitboxPos, float collisionPos, ForegroundTile tile) { + private Collision( + float collisionPos, + CollisionNode node, + ForegroundTile tile, + float distanceToCollision) { this.collisionPos = collisionPos; + this.node = node; this.tile = tile; - - distanceToCollision = collisionPos - hitboxPos; + this.distanceToCollision = distanceToCollision; } /** @@ -49,27 +62,33 @@ public Collision(float hitboxPos, float collisionPos, ForegroundTile tile) { @Override public int compareTo(Collision other) { float myDist = Math.abs(distanceToCollision); - float otherDist = Math.abs(other.getDistanceToCollision()); + float otherDist = Math.abs(other.distanceToCollision); - if (myDist < otherDist) { - return -1; - } else if (myDist == otherDist) { - return 0; - } else { - return 1; - } + return Float.compare(myDist, otherDist); } - public float getDistanceToCollision() { - return distanceToCollision; - } - - public float getCollisionPos() { - return collisionPos; - } + //////////////////////////////////////////////////////////////////////////// + // Factory Methods + //////////////////////////////////////////////////////////////////////////// - public ForegroundTile getTile() { - return tile; + /** + * Creates a Collision. + * + * @param posBefore + * Absolute position of the relevant node before the collision. + * @param posAfter + * Absolute position of the relevant node after the collision. + * @param node The Node involved in this Collision. + * @param tile Tile with which the collision occurred. + * @return + */ + public static Collision create( + float posBefore, + float posAfter, + CollisionNode node, + ForegroundTile tile) { + float distanceToCollision = posAfter - posBefore; + return new Collision(posAfter, node, tile, distanceToCollision); } } diff --git a/src/engine/game/physics/CollisionResult.java b/src/engine/game/physics/CollisionResult.java index ccfb64b..41735e3 100644 --- a/src/engine/game/physics/CollisionResult.java +++ b/src/engine/game/physics/CollisionResult.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import engine.game.tiles.Slope; +import engine.game.physics.Hitbox.CollisionNode; /** * Class designed to hold a number of collisions during physics processing. @@ -12,6 +12,14 @@ */ public class CollisionResult { + /** + * Hitbox involved in this collision. + * + *

Until the CollisionResult is applied, this can be used to get the + * initial position of the Hitbox, before any movement takes place. + */ + public Hitbox hitbox; + /** * All Collisions that have occurred in the x-axis. */ @@ -23,14 +31,20 @@ public class CollisionResult { private List collisionsY = new ArrayList<>(); /** - * Nearest Collisions detected by this CollisionResult. + * All PostProcessCollision that have occurred. */ - private Collision nearestCollisionX, nearestCollisionY; + private List postProcessCollisions + = new ArrayList<>(); /** - * Hitbox involved in this Collision. + * Nearest x-Collision detected by this CollisionResult. */ - private Hitbox hitbox; + private Collision nearestCollisionX; + + /** + * Nearest y-Collision detected by this CollisionResult. + */ + private Collision nearestCollisionY; /** * Attempted movement distance. @@ -48,9 +62,13 @@ public class CollisionResult { private float newY; /** - * Flag used for a special case involving ceiling slopes. + * Whether Collisions still need to be resolved. */ - private boolean maintainSpeedY; + private boolean needsResolve = true; + + //////////////////////////////////////////////////////////////////////////// + // Lifecycle + //////////////////////////////////////////////////////////////////////////// /** * Creates a CollisionResult by attempting to move the given Hitbox by the @@ -64,404 +82,377 @@ public CollisionResult(Hitbox hitbox, float dx, float dy) { this.hitbox = hitbox; attempted_dx = dx; attempted_dy = dy; - newX = hitbox.left() + dx; - newY = hitbox.top() + dy; } /** - * Returns the original hitbox used in the collision detection. + * Must be called at the end of collision processing. * - * @return + *

After this, no more collisions should be added. */ - public Hitbox hitbox { - return hitbox; + public void finish() { + resolvePostProcessCollisions(); } - /** - * Getter for the new Hitbox left. - * - *

Before resolveCollisions_X() is called, this will return the new left - * position of the Hitbox assuming no collisions; afterwards, it will - * return the new position with the nearest collision applied. - * - * @return - */ - public float left() { - return newX; - } + //////////////////////////////////////////////////////////////////////////// + // Movement + //////////////////////////////////////////////////////////////////////////// /** - * Getter for the new Hitbox right. - * - * See {@link #left}. + * Getter for the attempted movement distance in the x-axis. * * @return */ - public float right() { - return newX + hitbox.width - Physics.SMALLEST_DISTANCE; + public float getAttemptedDx() { + return attempted_dx; } /** - * Getter for the new Hitbox centre. - * - * See {@link #left}. + * Getter for the attempted movement distance in the y-axis. * * @return */ - public float centreX() { - return newX + hitbox.width / 2; + public float getAttemptedDy() { + return attempted_dy; } - /** - * Getter for the new Hitbox centre. - * - * See {@link #left}. - * - * @return - */ - public float centreY() { - return newY + hitbox.height / 2; - } + //////////////////////////////////////////////////////////////////////////// + // Adding / Invalidating / Getting Collisions + //////////////////////////////////////////////////////////////////////////// /** - * Getter for the new Hitbox top. - * - * See {@link #left}. + * Returns the nearest Collision in the x-axis. * * @return */ - public float top() { - return newY; + public Collision getNearestCollisionX() { + resolve(); + return nearestCollisionX; } /** - * Getter for the new Hitbox bottom. - * - * See {@link #left}. + * Returns the nearest Collision in the y-axis. * * @return */ - public float bottom() { - return newY + hitbox.height - Physics.SMALLEST_DISTANCE; + public Collision getNearestCollisionY() { + resolve(); + return nearestCollisionY; } /** - * Returns the nearest Collision in the x-axis. + * Gets all Collisions that have occurred in the x-axis. * - *

This must be called after resolveCollisions_X(). + *

Changes to the resulting list will have no effect. * * @return */ - public Collision getNearestCollisionX() { - return nearestCollisionX; + public List getCollisionsX() { + return new ArrayList(collisionsX); } /** - * Returns the nearest Collision in the y-axis. + * Gets all Collisions that have occurred in the y-axis. * - *

This must be called after resolveCollisions_Y(). + *

Changes to the resulting list will have no effect. * * @return */ - public Collision getNearestCollisionY() { - return nearestCollisionY; + public List getCollisionsY() { + return new ArrayList(collisionsY); } /** - * Adds a Collision in the x-axis. + * Records a Collision in the x-axis. * * @param collision */ public void addCollision_X(Collision collision) { collisionsX.add(collision); + needsResolve = true; } /** - * Adds a Collision in the y-axis. + * Records a Collision in the y-axis. * * @param collision */ public void addCollision_Y(Collision collision) { collisionsY.add(collision); + needsResolve = true; } /** - * Adds the given Collision in the x-axis, overriding all other Collisions. + * Records a PostProcessCollision. * * @param collision */ - public void setCollision_X(Collision collision) { - collisionsX.clear(); - collisionsX.add(collision); + public void addPostProcessCollision(PostProcessCollision collision) { + postProcessCollisions.add(collision); } /** - * Adds the given Collision in the y-axis, overriding all other Collisions. + * Renders a Collision invalid. * * @param collision */ - public void setCollision_Y(Collision collision) { - collisionsY.clear(); - collisionsY.add(collision); + public void invalidateCollision(Collision collision) { + collision.valid = false; + needsResolve = true; } /** - * Getter for the attempted movement distance in the x-axis. + * Determines if a Collision occurred in the x-axis. * * @return */ - public float getAttemptedDx() { - return attempted_dx; + public boolean hasCollisionOccurredX() { + resolve(); + return nearestCollisionX != null; } /** - * Getter for the attempted movement distance in the y-axis. + * Determines if a Collision occurred in the y-axis. * * @return */ - public float getAttemptedDy() { - return attempted_dy; + public boolean hasCollisionOccurredY() { + resolve(); + return nearestCollisionY != null; } + //////////////////////////////////////////////////////////////////////////// + // Collision Resolution + //////////////////////////////////////////////////////////////////////////// + /** - * Returns the position of the colliding edge of the Hitbox. - * - *

When moving left, the colliding edge is the left of the Hitbox, and - * when moving right it is the right of the Hitbox. - * - * @return + * Resolves all registered Collisions. */ - public float getCollisionEdgeX() { - return wasCollisionWithLeftEdge() ? - hitbox.left() : - hitbox.right(); + private void resolve() { + if (needsResolve) { + needsResolve = false; + resolveCollisions_X(); + resolveCollisions_Y(); + } } /** - * Returns the position of the colliding edge of the Hitbox. + * Determines the nearest Collision in the x-axis. * - *

When moving up, the colliding edge is the top of the Hitbox, and when - * moving down it is the bottom of the Hitbox. + *

Must be called whenever a Collision is added or invalidated. + */ + private void resolveCollisions_X() { + + // Find the nearest valid collision + nearestCollisionX = collisionsX + .stream() + .filter(c -> c.valid) + .sorted() + .findFirst() + .orElse(null); + + if (nearestCollisionX == null) { + newX = hitbox.left() + attempted_dx; + return; + } + + if (nearestCollisionX.node.isOnLeftEdge()) { + // We add a small distance because we want the Hitbox to be placed + // NEXT to the colliding Tile, not inside it. + newX = nearestCollisionX.collisionPos + Physics.SMALLEST_DISTANCE; + } else { + newX = nearestCollisionX.collisionPos - hitbox.width; + } + } + + /** + * Determines the nearest Collision in the y-axis. * - * @return + * See {@link #resolveCollisions_X}. */ - public float getCollisionEdgeY() { - return wasCollisionWithTopEdge() ? - hitbox.top() : - hitbox.bottom(); + private void resolveCollisions_Y() { + + // Find the nearest valid collision + nearestCollisionY = collisionsY + .stream() + .filter(c -> c.valid) + .sorted() + .findFirst() + .orElse(null); + + if (nearestCollisionY == null) { + newY = hitbox.top() + attempted_dy; + return; + } + + if (nearestCollisionY.node.isOnTopEdge()) { + // We add a small distance because we want the Hitbox to be placed + // NEXT to the colliding Tile, not inside it. + newY = nearestCollisionY.collisionPos + Physics.SMALLEST_DISTANCE; + } else { + newY = nearestCollisionY.collisionPos - hitbox.height; + } } /** - * Determines if the nearest y-Collision was with a Slope. + * Resolves all PostProcessCollisions. * - * @return + *

Must be called after all collisions have been added. */ - public boolean isCollisionWithSlope() { - return nearestCollisionY != null && - nearestCollisionY.getTile() instanceof Slope; + private void resolvePostProcessCollisions() { + + // Defer to the PostProcessingTiles to resolve their own collisions + for (PostProcessCollision collision : postProcessCollisions) { + collision.tile.postProcessing(this, collision); + } + + // New collisions may have been added, which need to be resolved + needsResolve = true; } + //////////////////////////////////////////////////////////////////////////// + // Node Position Calculations + //////////////////////////////////////////////////////////////////////////// + /** - * Determines if the nearest y-Collision was with a floor Slope. + * Calculates the initial absolute x-position of a CollisionNode. * + * @param node * @return */ - private boolean isCollisionWithFloorSlope() { - return isCollisionWithSlope() && - ((Slope) nearestCollisionY.getTile()).isFloorSlope(); + public float initialNodeX(CollisionNode node) { + return hitbox.x + node.x; } /** - * Determines if the nearest y-Collision was with a ceiling Slope. + * Calculates the initial absolute y-position of a CollisionNode. * + *

This should be used to find the y-position used in an x-collision, + * since x-collisions are generated before any y-movement is applied. + * + * @param node * @return */ - private boolean isCollisionWithCeilingSlope() { - return isCollisionWithSlope() && - ((Slope) nearestCollisionY.getTile()).isCeilingSlope(); + public float initialNodeY(CollisionNode node) { + return hitbox.y + node.y; } /** - * Determines if a Collision occurred in the x-axis. + * Calculates the desired absolute x-position of a CollisionNode, that is, + * the position of this node if no x-collisions occurred. * + * @param node * @return */ - public boolean hasCollisionOccurredX() { - return nearestCollisionX != null; + public float desiredNodeX(CollisionNode node) { + return hitbox.x + node.x + attempted_dx; } /** - * Determines if a Collision occurred in the y-axis. + * Calculates the desired absolute y-position of a CollisionNode, that is, + * the position of this node if no y-collisions occurred. * + * @param node * @return */ - public boolean hasCollisionOccurredY() { - return nearestCollisionY != null; + public float desiredNodeY(CollisionNode node) { + return hitbox.y + node.y + attempted_dy; } /** - * Determines the nearest Collision in the x-axis. + * Calculates the new absolute x-position of a CollisionNode. * - *

Must be called after all Collisions are added. After this is called, - * no more collisions can be added. + * @param node + * @return */ - public void resolveCollisions_X() { - - if (collisionsX.isEmpty()) { - return; - } - - collisionsX.sort(null); - nearestCollisionX = collisionsX.get(0); - collisionsX.clear(); // No longer needed - - if (wasCollisionWithLeftEdge()) { - // We add a small distance because we want the Hitbox to be placed - // NEXT to the colliding Tile, not inside it. - newX = nearestCollisionX.getCollisionPos() + - Physics.SMALLEST_DISTANCE; - } else if (wasCollisionWithRightEdge()) { - newX = nearestCollisionX.getCollisionPos() - hitbox.width; - } + public float newNodeX(CollisionNode node) { + return left() + node.x; } /** - * Determines the nearest Collision in the y-axis. + * Calculates the new absolute y-position of a CollisionNode. * - * See {@link #resolveCollisions_X}. + *

This should NOT be used to find the y-position used in an x-collision, + * since x-collisions are generated before any y-movement is applied. + * + * @param node + * @return */ - public void resolveCollisions_Y() { - - if (collisionsY.isEmpty()) { - return; - } - - collisionsY.sort(null); - nearestCollisionY = collisionsY.get(0); - collisionsY.clear(); // No longer needed - - if (wasCollisionWithTopEdge()) { - // We add a small distance because we want the Hitbox to be placed - // NEXT to the colliding Tile, not inside it. - newY = nearestCollisionY.getCollisionPos() + - Physics.SMALLEST_DISTANCE; - if (isCollisionWithCeilingSlope() && attempted_dy > 0) { - /* - * Special case: Entities pressing into a ceiling slope while - * falling need to snap to the slope, but should maintain their - * vertical velocity. It doesn't make sense for Entities to - * suddenly stop falling just because they bump into a ceiling - * slope. - */ - maintainSpeedY = true; - } - } else if (wasCollisionWithBottomEdge()) { - newY = nearestCollisionY.getCollisionPos() - hitbox.height; - } + public float newNodeY(CollisionNode node) { + return top() + node.y; } + //////////////////////////////////////////////////////////////////////////// + // New Hitbox Positions + //////////////////////////////////////////////////////////////////////////// + /** - * Determines if this collision was with the left edge of the Hitbox. + * Gets the new Hitbox left, with the nearest Collision applied. * * @return */ - private boolean wasCollisionWithLeftEdge() { - // If the Entity was moving left, the left edge must have collided - return attempted_dx < 0; + public float left() { + resolve(); + return newX; } /** - * Determines if this collision was with the right edge of the Hitbox. + * Gets the new Hitbox right. + * + * See {@link #left}. * * @return */ - private boolean wasCollisionWithRightEdge() { - // If the Entity was moving right, the right edge must have collided - return attempted_dx > 0; + public float right() { + resolve(); + return newX + hitbox.width - Physics.SMALLEST_DISTANCE; } /** - * Determines if this collision was with the top edge of the Hitbox. + * Gets the new Hitbox centre. + * + * See {@link #left}. * * @return */ - private boolean wasCollisionWithTopEdge() { - - /* - * We have to include these special cases for floor and ceiling slopes, - * because we cannot rely on the direction of travel alone; it is - * possible to collide with either slope while moving up or down. - */ - if (isCollisionWithCeilingSlope()) { - // Collisions with ceiling slopes always involve the top edge of the - // Hitbox. - return true; - } - - if (isCollisionWithFloorSlope()) { - // Collisions with floor slopes always involve the bottom edge of - // the Hitbox. - return false; - } - - // Otherwise... - // If the Entity was moving up, the top edge must have collided - return attempted_dy < 0; + public float centreX() { + resolve(); + return newX + hitbox.width / 2; } /** - * Determines if this collision was with the bottom edge of the Hitbox. + * Gets the new Hitbox centre. + * + * See {@link #left}. * * @return */ - private boolean wasCollisionWithBottomEdge() { - - /* - * We have to include these special cases for floor and ceiling slopes, - * because we cannot rely on the direction of travel alone; it is - * possible to collide with either slope while moving up or down. - */ - if (isCollisionWithFloorSlope()) { - // Collisions with floor slopes always involve the bottom edge of - // the Hitbox. - return true; - } - - if (isCollisionWithCeilingSlope()) { - // Collisions with ceiling slopes always involve the top edge of the - // Hitbox. - return false; - } - - // Otherwise... - // If the Entity was moving down, the bottom edge must have collided - return attempted_dy > 0; + public float centreY() { + resolve(); + return newY + hitbox.height / 2; } /** - * Determines if the Hitbox's y-speed should be maintained, even if a - * collision has occurred. + * Gets the new Hitbox top. + * + * See {@link #left}. * * @return */ - public boolean shouldMaintainSpeedY() { - return maintainSpeedY; + public float top() { + resolve(); + return newY; } /** - * Determines if the Hitbox should "stick" to a floor slope. - * - *

If Hitboxes didn't "stick" to slopes, then with enough forward - * momentum an Entity running down a slope would fly off the slope and then - * fall down onto it in an arc (the "stairs effect"). + * Gets the new Hitbox bottom. * - *

To counter this, we ensure that a Hitbox that is on the ground remains - * "stuck" to a slope unless some upward momentum (e.g. a jump) is applied. + * See {@link #left}. * * @return */ - public boolean shouldHitboxStickToSlope() { - return hitbox.isGrounded() && attempted_dy > 0; + public float bottom() { + resolve(); + return newY + hitbox.height - Physics.SMALLEST_DISTANCE; } } diff --git a/src/engine/game/physics/Hitbox.java b/src/engine/game/physics/Hitbox.java index 3ff2905..7c309d8 100644 --- a/src/engine/game/physics/Hitbox.java +++ b/src/engine/game/physics/Hitbox.java @@ -1,11 +1,12 @@ package engine.game.physics; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import engine.game.GameUtils; import engine.game.Logic; -import engine.game.tiles.Slope; import engine.game.tiles.Tile; import engine.launcher.Launcher; @@ -19,12 +20,90 @@ */ public class Hitbox { + //////////////////////////////////////////////////////////////////////////// + // Node + //////////////////////////////////////////////////////////////////////////// + + public class CollisionNode { + + /** + * x-position of this Node relative to the left of the Hitbox. + */ + public final float x; + + /** + * y-position of this Node relative to the top of the Hitbox. + */ + public final float y; + + public CollisionNode(float x, float y) { + this.x = x; + this.y = y; + } + + /** + * Determines if this Node is on the left edge of a Hitbox. + * + * @return + */ + public boolean isOnLeftEdge() { + return x == 0; + } + + /** + * Determines if this Node is on the top edge of a Hitbox. + * + * @return + */ + public boolean isOnTopEdge() { + return y == 0; + } + + // Auto-generated + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Float.floatToIntBits(x); + result = prime * result + Float.floatToIntBits(y); + return result; + } + + // Auto-generated + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CollisionNode other = (CollisionNode) obj; + if (Float.floatToIntBits(x) != Float.floatToIntBits(other.x)) + return false; + if (Float.floatToIntBits(y) != Float.floatToIntBits(other.y)) + return false; + return true; + } + + } + + //////////////////////////////////////////////////////////////////////////// + // Hitbox + //////////////////////////////////////////////////////////////////////////// + /** - * Collision flag that allows a Hitbox to travel up and down Slopes. + * Collision flag that allows a Hitbox to walk up and down slopes. + */ + public static final int SUPPORTS_SLOPE_TRAVERSAL = 0; + + /** + * Starting index for custom collisions flags. * - *

If set to false, a Hitbox will collide with slopes instead. + *

When defining custom collision flags, they should be offset by this + * value to prevent conflicts with flags added to the engine in future. */ - public static final int SUPPORT_SLOPES = 0; + public static final int CUSTOM_FLAG_INDEX = 1; /** * Listener to inform whenever significant events occur. @@ -72,16 +151,29 @@ public class Hitbox { (GameUtils.worldUnits(1) - Physics.SMALLEST_DISTANCE); /** - * Distances along this Hitbox's horizontal edges at which to check for - * collision. + * Collision Nodes along the left edge of the Hitbox. + */ + private CollisionNode[] leftNodes; + + /** + * Collision Nodes along the right edge of the Hitbox. + */ + private CollisionNode[] rightNodes; + + /** + * Collision Nodes along the top edge of the Hitbox. */ - private float[] horizontalCollisionNodes; + private CollisionNode[] topNodes; /** - * Distances along this Hitbox's vertical edges at which to check for - * collision. + * Collision Nodes along the bottom edge of the Hitbox. */ - private float[] verticalCollisionNodes; + private CollisionNode[] bottomNodes; + + /** + * Collision Nodes along all edges of the Hitbox. + */ + private Set allNodes = new HashSet<>(); /** * Flags that can be used to control the outcome of collisions. @@ -91,7 +183,7 @@ public class Hitbox { /** * Flag set whenever this Hitbox is touching the ground. */ - private boolean onGround; + private boolean grounded; /** * Milliseconds since this Hitbox was last touching the ground. @@ -102,25 +194,25 @@ public class Hitbox { * Multiplier that determines how strongly this Hitbox is affected by * gravity. */ - private float gravityCoefficient = 1; + public float gravityCoefficient = 1; /** * Multiplier that determines how colliding with a surface affects this * Hitbox's speed. */ - private float bounceCoefficient = 0; + public float bounceCoefficient = 0; /** * Multiplier that determines how strongly this Hitbox is affected by ground * friction. */ - private float groundFrictionCoefficient = 1; + public float groundFrictionCoefficient = 1; /** * Multiplier that determines how strongly this Hitbox is affected by air * friction. */ - private float airFrictionCoefficient = 1; + public float airFrictionCoefficient = 1; /** * Whether this Hitbox is affected by collisions. @@ -150,8 +242,7 @@ public Hitbox(float x, float y, float width, float height, this.height = height; this.listener = listener; - horizontalCollisionNodes = createCollisionNodes(width); - verticalCollisionNodes = createCollisionNodes(height); + createCollisionNodes(); lastCollisionResult = new CollisionResult(this, 0, 0); } @@ -164,9 +255,49 @@ public void destroy() { } //////////////////////////////////////////////////////////////////////////// - // Collision + // Collision Nodes //////////////////////////////////////////////////////////////////////////// + /** + * Creates the Nodes used by this Hitbox for collision detection. + */ + private void createCollisionNodes() { + + // Calculate collision node positions + float[] horizontalCollisionNodes = getCollisionNodesPositions(width); + float[] verticalCollisionNodes = getCollisionNodesPositions(height); + float rightNode = + horizontalCollisionNodes[horizontalCollisionNodes.length - 1]; + float bottomNode = + verticalCollisionNodes[verticalCollisionNodes.length - 1]; + + // Initialise edge Nodes + leftNodes = new CollisionNode[verticalCollisionNodes.length]; + rightNodes = new CollisionNode[verticalCollisionNodes.length]; + topNodes = new CollisionNode[horizontalCollisionNodes.length]; + bottomNodes = new CollisionNode[horizontalCollisionNodes.length]; + + // Create Nodes for left / right edges + for (int i = 0; i < verticalCollisionNodes.length; i++) { + CollisionNode left = new CollisionNode(0, verticalCollisionNodes[i]); + CollisionNode right = new CollisionNode(rightNode, verticalCollisionNodes[i]); + leftNodes[i] = left; + rightNodes[i] = right; + allNodes.add(left); + allNodes.add(right); + } + + // Create Nodes for top / bottom edges + for (int i = 0; i < horizontalCollisionNodes.length; i++) { + CollisionNode top = new CollisionNode(horizontalCollisionNodes[i], 0); + CollisionNode bottom = new CollisionNode(horizontalCollisionNodes[i], bottomNode); + topNodes[i] = top; + bottomNodes[i] = bottom; + allNodes.add(top); + allNodes.add(bottom); + } + } + /** * Determines the points along the given edge at which to check for * collisions. @@ -178,7 +309,7 @@ public void destroy() { * * @param edgeLength Length of the collision edge, in world units. */ - private float[] createCollisionNodes(float edgeLength) { + private float[] getCollisionNodesPositions(float edgeLength) { /* * The number of nodes is equal to: @@ -201,21 +332,65 @@ private float[] createCollisionNodes(float edgeLength) { return nodes; } + /** + * Gets the Nodes along the left edge of the Hitbox. + * + * @return + */ + public CollisionNode[] getLeftNodes() { + return leftNodes; + } + + /** + * Gets the Nodes along the right edge of the Hitbox. + * + * @return + */ + public CollisionNode[] getRightNodes() { + return rightNodes; + } + + /** + * Gets the Nodes along the top edge of the Hitbox. + * + * @return + */ + public CollisionNode[] getTopNodes() { + return topNodes; + } + + /** + * Gets the Nodes along the bottom edge of the Hitbox. + * + * @return + */ + public CollisionNode[] getBottomNodes() { + return bottomNodes; + } + + /** + * Gets all the Nodes along the edges of the Hitbox. + * + * @return + */ + public Set getAllNodes() { + return allNodes; + } + + //////////////////////////////////////////////////////////////////////////// + // Collision Handling + //////////////////////////////////////////////////////////////////////////// + /** * Moves this Hitbox according to its current speed, and handles any * collisions with the Level along the way. * - *

When colliding with a surface, this Hitbox will bounce off it (change - * direction), and its speed will be multiplied by the bounce coefficient. - * A bounce coefficient of zero (the default) will cause the Hitbox to stop. - * - * @see Hitbox#setBounceCoefficient * @param logic * @param delta */ public void moveWithCollision(Logic logic, int delta) { - if (!onGround) { + if (!grounded) { msSinceGrounded += delta; } @@ -226,39 +401,8 @@ public void moveWithCollision(Logic logic, int delta) { // Move to the nearest collision CollisionResult result = Physics.getCollisionResult(logic, this, dx, dy); - setPos(result.left(), result.top()); - - // Collide with slopes if this Hitbox doesn't support them - if (!getCollisionFlag(SUPPORT_SLOPES) - && result.isCollisionWithSlope()) { - Slope slope = (Slope) result.getNearestCollisionY().getTile(); - slope.collide(this, bounceCoefficient); - } - // Adjust speed according to x-collisions - if (result.hasCollisionOccurredX()) { - setSpeedX(-speedX * bounceCoefficient); - } - - // Adjust speed according to y-collisions - if (result.hasCollisionOccurredY()) { - if (!result.shouldMaintainSpeedY()) { - setSpeedY(-speedY * bounceCoefficient); - } - if (result.getAttemptedDy() > 0 && - Math.abs(speedY) < Physics.MOVING_SPEED) { - // Hitbox has hit the ground - setSpeedY(0); - if (!onGround) { - setGrounded(true); - } - } - } else { - if (onGround) { - // Hitbox has left the ground - setGrounded(false); - } - } + apply(result); // Check if this Hitbox is now out-of-bounds if (y > logic.getLevel().getWorldHeight()) { @@ -275,32 +419,54 @@ public void moveWithCollision(Logic logic, int delta) { } /** - * Gets the most recent CollisionResult computed by this Hitbox. + * Applies the given CollisionResult. * - * @return + * @param result */ - public CollisionResult getLastCollisionResult() { - return lastCollisionResult; + private void apply(CollisionResult result) { + + // Move to the new position + setPos(result.left(), result.top()); + + if (result.hasCollisionOccurredX()) { + // Let the tile affect the Hitbox after an x-collision + result.getNearestCollisionX().tile.hitboxCollidedX(result); + } + + if (result.hasCollisionOccurredY()) { + // Let the tile affect the Hitbox after an y-collision + result.getNearestCollisionY().tile.hitboxCollidedY(result); + + // Landing + if (hasLanded(result)) { + setSpeedY(0); + setGrounded(true); + } + + } else if (grounded) { + // Hitbox has left the ground + setGrounded(false); + } } /** - * Gets the distances along this Hitbox's horizontal edges at which to check - * for collisions. + * Determines if this Hitbox has landed as the result of a collision. * + * @param result * @return */ - public float[] getHorizontalCollisionNodes() { - return horizontalCollisionNodes; + private boolean hasLanded(CollisionResult result) { + // Hitbox has landed if it has hit the ground and stopped + return result.getAttemptedDy() > 0 && !isMovingY(); } /** - * Gets the distances along this Hitbox's vertical edges at which to check - * for collisions. + * Gets the most recent CollisionResult computed by this Hitbox. * * @return */ - public float[] getVerticalCollisionNodes() { - return verticalCollisionNodes; + public CollisionResult getLastCollisionResult() { + return lastCollisionResult; } /** @@ -349,7 +515,7 @@ public void setSolid(boolean solid) { * @return */ public boolean isGrounded() { - return onGround; + return grounded; } /** @@ -365,16 +531,16 @@ public int getMsSinceGrounded() { * to be on the ground. * * @see Hitbox#isGrounded - * @param nowOnGround + * @param nowGrounded */ - public void setGrounded(boolean nowOnGround) { - if (!onGround && nowOnGround) { + public void setGrounded(boolean nowGrounded) { + if (!grounded && nowGrounded) { msSinceGrounded = 0; listener.hitboxLanded(); - } else if (onGround && !nowOnGround) { + } else if (grounded && !nowGrounded) { listener.hitboxLeftGround(); } - onGround = nowOnGround; + grounded = nowGrounded; } //////////////////////////////////////////////////////////////////////////// @@ -384,7 +550,6 @@ public void setGrounded(boolean nowOnGround) { /** * Adjusts this Hitbox's speed according to ground friction. * - * @see Hitbox#setGroundFrictionCoefficient * @param delta */ public void applyGroundFriction(int delta) { @@ -395,7 +560,6 @@ public void applyGroundFriction(int delta) { /** * Adjusts this Hitbox's x-speed according to air friction. * - * @see Hitbox#setAirFrictionCoefficient * @param delta */ public void applyAirFrictionX(int delta) { @@ -406,7 +570,6 @@ public void applyAirFrictionX(int delta) { /** * Adjusts this Hitbox's y-speed according to air friction. * - * @see Hitbox#setAirFrictionCoefficient * @param delta */ public void applyAirFrictionY(int delta) { @@ -417,53 +580,12 @@ public void applyAirFrictionY(int delta) { /** * Adjusts this Hitbox's y-speed according to gravity. * - * @see Hitbox#setGravityCoefficient * @param delta */ public void applyGravity(int delta) { setSpeedY(Physics.applyGravity(speedY, delta, gravityCoefficient)); } - /** - * Sets the multiplier used to determine the strength of gravity, as applied - * to this Hitbox. - * - * @param gravityCoefficient - */ - public void setGravityCoefficient(float gravityCoefficient) { - this.gravityCoefficient = gravityCoefficient; - } - - /** - * Sets the multiplier used to determine the strength of ground friction, as - * applied to this Hitbox. - * - * @param groundFrictionCoefficient - */ - public void setGroundFrictionCoefficient(float groundFrictionCoefficient) { - this.groundFrictionCoefficient = groundFrictionCoefficient; - } - - /** - * Sets the multiplier used to determine the strength of air friction, as - * applied to this Hitbox. - * - * @param airFrictionCoefficient - */ - public void setAirFrictionCoefficient(float airFrictionCoefficient) { - this.airFrictionCoefficient = airFrictionCoefficient; - } - - /** - * Sets the multiplier used to determine this Hitbox's speed after a - * collision with a surface. - * - * @param bounceCoefficient - */ - public void setBounceCoefficient(float bounceCoefficient) { - this.bounceCoefficient = bounceCoefficient; - } - //////////////////////////////////////////////////////////////////////////// // Utilities //////////////////////////////////////////////////////////////////////////// @@ -504,7 +626,25 @@ public boolean contains(float px, float py) { * @return */ public boolean isFalling() { - return !onGround && speedY > 0; + return !grounded && speedY > 0; + } + + /** + * Determines if this Hitbox is moving in the x-axis. + * + * @return + */ + public boolean isMovingX() { + return Math.abs(speedX) >= Physics.MOVING_SPEED; + } + + /** + * Determines if this Hitbox is moving in the y-axis. + * + * @return + */ + public boolean isMovingY() { + return Math.abs(speedY) >= Physics.MOVING_SPEED; } /** @@ -513,8 +653,7 @@ public boolean isFalling() { * @return */ public boolean isMoving() { - return Math.abs(speedX) >= Physics.MOVING_SPEED - || Math.abs(speedY) >= Physics.MOVING_SPEED; + return isMovingX() || isMovingY(); } /** diff --git a/src/engine/game/physics/Physics.java b/src/engine/game/physics/Physics.java index 0f98a66..b73edac 100644 --- a/src/engine/game/physics/Physics.java +++ b/src/engine/game/physics/Physics.java @@ -1,9 +1,13 @@ package engine.game.physics; +import java.util.Set; + import engine.game.GameUtils; import engine.game.Level; import engine.game.Logic; +import engine.game.physics.Hitbox.CollisionNode; import engine.game.tiles.ForegroundTile; +import engine.game.tiles.PostProcessingTile; import engine.game.tiles.Tile; import engine.launcher.Logger; @@ -15,10 +19,10 @@ public abstract class Physics { /** - * Minimum speed at which Entities are considered to be moving. + * Minimum speed at which Hitboxes are considered to be moving. * *

Speeds lower than this are negligible, so it is simpler to consider - * Entities stationary. + * Hitboxes stationary. * *

Measured in world units per second. */ @@ -52,13 +56,13 @@ public abstract class Physics { * (tile left + tile width) gives us the position of the right edge of the * tile. * - *

This is important for a number of reasons. For example, an Entity the + *

This is important for a number of reasons. For example, a Hitbox the * height of a tile should be able to fit inside a single tile without its * hitbox overlapping the tile below. However, as soon as any gravity is * applied whatsoever, a collision should be registered with the tile below. * *

This value ensures that such a collision will happen every frame, and - * the entity will be continually repositioned in the correct position atop + * the Hitbox will be continually repositioned in the correct position atop * the ground tile. */ public static final float SMALLEST_DISTANCE = @@ -168,12 +172,12 @@ public static float applyAirFriction( * @param dy Attempted distance travelled in y-direction. * @return */ - public static CollisionResult getCollisionResult(Logic logic, - Hitbox hitbox, float dx, float dy) { + public static CollisionResult getCollisionResult( + Logic logic, Hitbox hitbox, float dx, float dy) { if (Math.abs(dx) > MAX_MOVE_DISTANCE) { /* - * Entity has attempted to move further than a single Tile, which + * Hitbox has attempted to move further than a single Tile, which * can be problematic for collision detection. This can happen if * the game is lagging. * @@ -200,19 +204,47 @@ public static CollisionResult getCollisionResult(Logic logic, if (hitbox.isSolid()) { - // Move in each axis separately + /* + * Move in each axis independently and resolve collisions along the + * way. + * + * If the Hitbox intersects a PostProcessingTile at any point during + * the movement, a PostProcessingCollision will be registered. After + * the movement is finished, all PostProcessingCollisions will be + * resolved. + */ + + // Move in the x-axis + if (dx < 0) { + detectCollisionsX(result, logic, hitbox.getLeftNodes()); + } else if (dx > 0) { + detectCollisionsX(result, logic, hitbox.getRightNodes()); + } + + // Check for collisions with any PostProcessingTiles at the new + // Hitbox position; we have to check this now because after the + // y-movement is applied, the Hitbox may no longer be colliding with + // a PostProcessingTile, so we will have missed the collision! if (dx != 0) { - detectCollisionsX(result, logic, - hitbox.getVerticalCollisionNodes()); + detectPostProcessCollisions( + result, logic, hitbox.getAllNodes(), false); } - if (dy != 0) { - detectCollisionsY(result, logic, - hitbox.getHorizontalCollisionNodes()); + + // Move in the y-axis + if (dy < 0) { + detectCollisionsY(result, logic, hitbox.getTopNodes()); + } else if (dy > 0) { + detectCollisionsY(result, logic, hitbox.getBottomNodes()); } - // Adjust the CollisionResult for Slopes - SlopeUtils.doSlopePostProcessing(result, logic); + // Check for collisions with any PostProcessingTiles at the final + // Hitbox position + if (dy != 0) { + detectPostProcessCollisions( + result, logic, hitbox.getAllNodes(), true); + } + result.finish(); } return result; @@ -223,37 +255,29 @@ public static CollisionResult getCollisionResult(Logic logic, * * @param result CollisionResult to update after detecting collisions. * @param logic - * @param collisionNodesY + * @param nodesY */ - private static void detectCollisionsX(CollisionResult result, Logic logic, - float[] collisionNodesY) { + private static void detectCollisionsX( + CollisionResult result, Logic logic, CollisionNode[] nodesY) { Level level = logic.getLevel(); - // Get collision results for each node along the Entity's edge - for (float node : collisionNodesY) { + // Get collision results for each node along the Hitbox's edge + for (CollisionNode node : nodesY) { - float y = result.hitbox.top() + node; - float xBefore = result.getCollisionEdgeX(); - float xAfter = xBefore + result.getAttemptedDx(); + // Find the new position of this CollisionNode + float nodeX = result.newNodeX(node); + float nodeY = result.initialNodeY(node); - int tileX = Tile.getTileX(xAfter); - int tileY = Tile.getTileY(y); + // Find the tile which this node will intersect + int tileX = Tile.getTileX(nodeX); + int tileY = Tile.getTileY(nodeY); int tileId = level.getForeground().getTile(tileX, tileY); ForegroundTile tile = (ForegroundTile) logic.getTile(tileId); - if (SlopeUtils.isTileBehindSlope(result, tileX, tileY, logic) || - SlopeUtils.isTileAtBottomOfFloorSlope(result, tileX, tileY, logic) || - SlopeUtils.isTileAtTopOfCeilingSlope(result, tileX, tileY, logic)) { - // Don't collide with Tiles behind or at the bottom of slopes, - // lest they interfere with the Slope physics. - continue; - } else if (tile.hasCollisionX(result, logic, tileX, tileY)) { - tile.collisionOccurredX(result); - } + // Let the tile handle this collision + tile.checkForCollision_X(result, nodeX, node); } - - result.resolveCollisions_X(); } /** @@ -261,36 +285,74 @@ private static void detectCollisionsX(CollisionResult result, Logic logic, * * @param result CollisionResult to update after detecting collisions. * @param logic - * @param collisionNodesX + * @param nodesX */ - private static void detectCollisionsY(CollisionResult result, Logic logic, - float[] collisionNodesX) { + private static void detectCollisionsY( + CollisionResult result, Logic logic, CollisionNode[] nodesX) { Level level = logic.getLevel(); - // Get collision results for each node along the Entity's edge - for (float node : collisionNodesX) { + // Get collision results for each node along the Hitbox's edge + for (CollisionNode node : nodesX) { - // Use the already-calculated x-collision result - float x = result.left() + node; - float yBefore = result.getCollisionEdgeY(); - float yAfter = yBefore + result.getAttemptedDy(); + // Find the new position of this CollisionNode, + // using the already-calculated x-collision result + float nodeX = result.newNodeX(node); + float nodeY = result.newNodeY(node); - int tileX = Tile.getTileX(x); - int tileY = Tile.getTileY(yAfter); + // Find the tile which this node will intersect + int tileX = Tile.getTileX(nodeX); + int tileY = Tile.getTileY(nodeY); int tileId = level.getForeground().getTile(tileX, tileY); ForegroundTile tile = (ForegroundTile) logic.getTile(tileId); - if (SlopeUtils.isTileBelowFloorSlope(result, tileX, tileY, logic)) { - // Don't collide with Tiles underneath slopes, lest they - // interfere with the Slope physics. - continue; - } else if (tile.hasCollisionY(result, logic, tileX, tileY)) { - tile.collisionOccurredY(result); - } + // Let the tile handle this collision + tile.checkForCollision_Y(result, nodeY, node); } + } - result.resolveCollisions_Y(); + /** + * Detects collisions with PostProcessingTiles at each Node of the Hitbox. + * + *

If any part of the Hitbox intersects a PostProcessingTiles, the tile + * should be informed of the collision. + * + * @param result CollisionResult to update after detecting collisions. + * @param logic + * @param nodes + * @param afterYMovement + */ + private static void detectPostProcessCollisions( + CollisionResult result, + Logic logic, + Set nodes, + boolean afterYMovement) { + + Level level = logic.getLevel(); + + for (CollisionNode node : nodes) { + + // Find the desired position of this CollisionNode + // (this ignores any previously-detected collisions, since they may + // be overridden by a PostProcessingCollision) + float nodeX = result.desiredNodeX(node); + float nodeY = afterYMovement + ? result.desiredNodeY(node) + : result.initialNodeY(node); + + // Find the tile which this node will intersect + int tileX = Tile.getTileX(nodeX); + int tileY = Tile.getTileY(nodeY); + int tileId = level.getForeground().getTile(tileX, tileY); + ForegroundTile tile = (ForegroundTile) logic.getTile(tileId); + + // If it is a PostProcessingTile, add a PostProcessCollision + if (tile instanceof PostProcessingTile) { + PostProcessCollision collision = new PostProcessCollision( + (PostProcessingTile) tile, tileX, tileY, node); + result.addPostProcessCollision(collision); + } + } } } diff --git a/src/engine/game/physics/PostProcessCollision.java b/src/engine/game/physics/PostProcessCollision.java new file mode 100644 index 0000000..3afc36d --- /dev/null +++ b/src/engine/game/physics/PostProcessCollision.java @@ -0,0 +1,78 @@ +package engine.game.physics; + +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.tiles.PostProcessingTile; +import engine.game.tiles.Tile; + +/** + * Represents a collision with a PostProcessingTile. + * + * @author Dan Bryce + */ +public class PostProcessCollision { + + public final PostProcessingTile tile; + + public final int tileX; + + public final int tileY; + + public final CollisionNode node; + + /** + * Constructs a PostProcessCollision. + * + * @param tile Tile involved in this collision. + * @param tileX x-index of the tile within the level. + * @param tileY y-index of the tile within the level. + * @param node CollisionNode involved in this collision. + */ + public PostProcessCollision( + PostProcessingTile tile, + int tileX, + int tileY, + CollisionNode node) { + + this.tile = tile; + this.tileX = tileX; + this.tileY = tileY; + this.node = node; + } + + /** + * Gets the absolute position of the left edge of the tile. + * + * @return + */ + public float getTileLeft() { + return Tile.getLeft(tileX * Tile.WIDTH); + } + + /** + * Gets the absolute position of the top edge of the tile. + * + * @return + */ + public float getTileTop() { + return Tile.getTop(tileY * Tile.HEIGHT); + } + + /** + * Gets the absolute position of the right edge of the tile. + * + * @return + */ + public float getTileRight() { + return Tile.getRight(tileX * Tile.WIDTH); + } + + /** + * Gets the absolute position of the bottom edge of the tile. + * + * @return + */ + public float getTileBottom() { + return Tile.getBottom(tileY * Tile.HEIGHT); + } + +} diff --git a/src/engine/game/physics/SlopeUtils.java b/src/engine/game/physics/SlopeUtils.java deleted file mode 100644 index ad384cf..0000000 --- a/src/engine/game/physics/SlopeUtils.java +++ /dev/null @@ -1,381 +0,0 @@ -package engine.game.physics; - -import engine.game.Level; -import engine.game.Logic; -import engine.game.TileLayer; -import engine.game.tiles.ForegroundTile; -import engine.game.tiles.Slope; -import engine.game.tiles.Tile; - -/** - * Class containing static methods used in Slope handling. - * - * @author Dan Bryce - */ -public class SlopeUtils { - - /** - * Adjusts the given CollisionResult to account for any Slopes. - * - *

Slope tiles are marked as non-solid, but Hitboxes that enter the solid - * part of a slope are adjusted by this step so that they sit atop the - * slope. - * - * @param result - * @param logic - */ - public static void doSlopePostProcessing(CollisionResult result, - Logic logic) { - - // Look for slopes at both of the Hitbox's "slope nodes" (horizontal - // centres of the top and bottom edges). - float slopeNodeX = result.centreX(); - - boolean onSlope = handleFloorSlopeCollisions( - result, logic, slopeNodeX, result.bottom()); - - if (!onSlope) { - // No need to check the top node if we already have a result from - // the bottom node. - handleCeilingSlopeCollisions( - result, logic, slopeNodeX, result.top()); - } - - // Need to resolve collisions again as they may be affected by slope - // processing. - result.resolveCollisions_Y(); - } - - /** - * Checks for and handles Slope collision at the bottom slope node. - * - * @param result CollisionResult to update. - * @param logic - * @param nodeX - * @param nodeY - * @return True if slope node should be snapped onto a slope. - */ - private static boolean handleFloorSlopeCollisions(CollisionResult result, - Logic logic, float nodeX, float nodeY) { - - TileLayer foreground = logic.getLevel().getForeground(); - - int tileX = (int) (nodeX / Tile.WIDTH); - int tileY = (int) (nodeY / Tile.HEIGHT); - int tileId = foreground.getTile(tileX, tileY); - ForegroundTile tile = (ForegroundTile) logic.getTile(tileId); - - if (tile instanceof Slope) { - // Slope node is inside a Slope tile - Slope slope = (Slope) tile; - float distIntoTileX = nodeX - Tile.getLeft(nodeX); - float distIntoTileY = nodeY - (tileY * Tile.HEIGHT); - // Only snap if the Hitbox is colliding with the solid part of the - // slope, or is supposed to be "stuck" to it. - if (slope.isFloorSlope() && - (slope.isPointInSlope(distIntoTileX, distIntoTileY) || - result.shouldHitboxStickToSlope())) { - snapToFloorSlope(result, nodeX, tileY, slope); - return true; - } - - } else if (isTileBelowFloorSlope(result, tileX, tileY, logic)) { - // Slope node is inside the Tile *under* a slope. - // We need to get this slope tile, and snap to it. - tileY -= 1; - tileId = foreground.getTile(tileX, tileY); - Slope slope = (Slope) logic.getTile(tileId); - snapToFloorSlope(result, nodeX, tileY, slope); - return true; - - } else if (isTileAboveFloorSlope(result, tileX, tileY, logic) && - result.shouldHitboxStickToSlope()) { - // Hitbox is supposed to be "stuck" to the slope - tileY += 1; - tileId = foreground.getTile(tileX, tileY); - Slope slope = (Slope) logic.getTile(tileId); - snapToFloorSlope(result, nodeX, tileY, slope); - return true; - } - - return false; - } - - /** - * Checks for and handles Slope collision at the top slope node. - * - * @param result CollisionResult to update. - * @param logic - * @param nodeX - * @param nodeY - */ - private static void handleCeilingSlopeCollisions(CollisionResult result, - Logic logic, float nodeX, float nodeY) { - - TileLayer foreground = logic.getLevel().getForeground(); - - int tileX = (int) (nodeX / Tile.WIDTH); - int tileY = (int) (nodeY / Tile.HEIGHT); - int tileId = foreground.getTile(tileX, tileY); - ForegroundTile tile = (ForegroundTile) logic.getTile(tileId); - - if (tile instanceof Slope) { - // Slope node is intersecting a Slope tile - Slope slope = (Slope) tile; - float distIntoTileX = nodeX - Tile.getLeft(nodeX); - float distIntoTileY = nodeY - (tileY * Tile.HEIGHT); - // Only snap if Hitbox is colliding with the solid part of the slope - if (slope.isCeilingSlope() && - slope.isPointInSlope(distIntoTileX, distIntoTileY)) { - /* - * Note that unlike floor slopes where we don't snap to the - * slope if the Hitbox is jumping, we SHOULD snap to ceiling - * slopes even when falling; otherwise, players can glitch INTO - * a ceiling slope if they press into it with enough momentum - * while falling. - */ - snapToCeilingSlope(result, nodeX, tileY, slope); - } - - } else if (isTileAboveCeilingSlope(tileX, tileY, logic)) { - // Slope node is in the Tile *above* a slope. - // We need to get this slope tile, and "snap" to it. - tileY += 1; - tileId = foreground.getTile(tileX, tileY); - Slope slope = (Slope) logic.getTile(tileId); - snapToCeilingSlope(result, nodeX, tileY, slope); - } - } - - /** - * Updates the CollisionResult after a collision with the given floor slope. - * - * @param result - * @param nodeX X-position of the "slope node". - * @param tileY Tile co-ordinate of the Slope. - * @param slope Slope tile with which the collision occurred. - */ - private static void snapToFloorSlope(CollisionResult result, float nodeX, - int tileY, Slope slope) { - float distIntoTileX = nodeX - Tile.getLeft(nodeX); - float yOnSlope = slope.getSlopeY_At_X(distIntoTileX); - float collisionY = (tileY * Tile.HEIGHT) + Tile.HEIGHT - yOnSlope; - result.setCollision_Y(new Collision( - result.hitbox.bottom(), collisionY, slope)); - } - - /** - * Updates the CollisionResult after a collision with the given ceiling - * slope. - * - * @param result - * @param nodeX X-position of the "slope node". - * @param tileY Tile co-ordinate of the Slope. - * @param slope Slope tile with which the collision occurred. - */ - private static void snapToCeilingSlope(CollisionResult result, float nodeX, - int tileY, Slope slope) { - float distIntoTileX = nodeX - Tile.getLeft(nodeX); - float yOnSlope = slope.getSlopeY_At_X(distIntoTileX); - float collisionY = (tileY * Tile.HEIGHT) + yOnSlope; - result.setCollision_Y(new Collision( - result.hitbox.top(), collisionY, slope)); - } - - /** - * Determines if the given Tile is "behind" a slope. - * - *

A Tile is considered to be "behind" a slope if it is: - *
1) A tile immediately to the left of a left slope. - *
2) A tile immediately to the right of right slope. - * - *

We check for (1) or (2) depending on if the given CollisionResult was - * an attempt to move left or right, respectively. - * - * @param result - * @param tileX - * @param tileY - * @param logic - * @return - */ - public static boolean isTileBehindSlope(CollisionResult result, - int tileX, int tileY, Logic logic) { - - Level level = logic.getLevel(); - TileLayer foreground = level.getForeground(); - - int possibleSlopeTileX = result.getAttemptedDx() > 0 ? - tileX - 1 : tileX + 1; - if (!level.doesTileExist_X(possibleSlopeTileX)) { - // Don't try to check outside the Level bounds - return false; - } - - int tileId = foreground.getTile(possibleSlopeTileX, tileY); - Tile possibleSlopeTile = logic.getTile(tileId); - return possibleSlopeTile instanceof Slope; - } - - /** - * Determines if the given Tile is immediately below a floor slope. - * - * @param result - * @param tileX - * @param tileY - * @param logic - * @return - */ - public static boolean isTileBelowFloorSlope(CollisionResult result, - int tileX, int tileY, Logic logic) { - - Level level = logic.getLevel(); - TileLayer foreground = level.getForeground(); - - int possibleSlopeTileY = tileY - 1; - if (!level.doesTileExist_Y(possibleSlopeTileY)) { - // Don't try to check outside the Level bounds - return false; - } - - int tileId = foreground.getTile(tileX, possibleSlopeTileY); - Tile possibleSlopeTile = logic.getTile(tileId); - if (possibleSlopeTile instanceof Slope) { - Slope slope = (Slope) possibleSlopeTile; - return slope.isFloorSlope(); - } - - return false; - } - - /** - * Determines if the given Tile is immediately above a floor slope. - * - * @param result - * @param tileX - * @param tileY - * @param logic - * @return - */ - public static boolean isTileAboveFloorSlope(CollisionResult result, - int tileX, int tileY, Logic logic) { - - Level level = logic.getLevel(); - TileLayer foreground = level.getForeground(); - - int possibleSlopeTileY = tileY + 1; - if (!level.doesTileExist_Y(possibleSlopeTileY)) { - // Don't try to check outside the Level bounds - return false; - } - - int tileId = foreground.getTile(tileX, possibleSlopeTileY); - Tile possibleSlopeTile = logic.getTile(tileId); - if (possibleSlopeTile instanceof Slope) { - Slope slope = (Slope) possibleSlopeTile; - return slope.isFloorSlope(); - } - - return false; - } - - /** - * Determines if the given Tile is immediately above a ceiling slope. - * - * @param tileX - * @param tileY - * @param logic - * @return - */ - private static boolean isTileAboveCeilingSlope(int tileX, int tileY, - Logic logic) { - - Level level = logic.getLevel(); - TileLayer foreground = level.getForeground(); - - int possibleSlopeTileY = tileY + 1; - if (!level.doesTileExist_Y(possibleSlopeTileY)) { - // Don't try to check outside the Level bounds - return false; - } - - int tileId = foreground.getTile(tileX, possibleSlopeTileY); - Tile possibleSlopeTile = logic.getTile(tileId); - if (possibleSlopeTile instanceof Slope) { - Slope slope = (Slope) possibleSlopeTile; - return slope.isCeilingSlope(); - } - - return false; - } - - /** - * Determines if the given Tile is "at the bottom of" a floor slope. - * - *

This is true for tiles that are diagonally underneath a floor slope. - * - * @param result - * @param tileX - * @param tileY - * @param logic - * @return - */ - public static boolean isTileAtBottomOfFloorSlope(CollisionResult result, - int tileX, int tileY, Logic logic) { - - Level level = logic.getLevel(); - TileLayer foreground = level.getForeground(); - - int possibleSlopeTileX = result.getAttemptedDx() > 0 ? - tileX - 1 : tileX + 1; - if (!level.doesTileExist_X(possibleSlopeTileX) || - !level.doesTileExist_Y(tileY - 1)) { - // Don't try to check outside the Level bounds - return false; - } - - int tileId = foreground.getTile(possibleSlopeTileX, tileY - 1); - Tile possibleSlopeTile = logic.getTile(tileId); - if (possibleSlopeTile instanceof Slope) { - Slope slope = (Slope) possibleSlopeTile; - return slope.isFloorSlope(); - } - - return false; - } - - /** - * Determines if the given Tile is "at the top of" a ceiling slope. - * - *

This is true for tiles that are diagonally above a ceiling slope. - * - * @param result - * @param tileX - * @param tileY - * @param logic - * @return - */ - public static boolean isTileAtTopOfCeilingSlope(CollisionResult result, - int tileX, int tileY, Logic logic) { - - Level level = logic.getLevel(); - TileLayer foreground = level.getForeground(); - - int possibleSlopeTileX = result.getAttemptedDx() > 0 ? - tileX - 1 : tileX + 1; - if (!level.doesTileExist_X(possibleSlopeTileX)|| - !level.doesTileExist_Y(tileY - 1)) { - // Don't try to check outside the Level bounds - return false; - } - - int tileId = foreground.getTile(possibleSlopeTileX, tileY + 1); - Tile possibleSlopeTile = logic.getTile(tileId); - if (possibleSlopeTile instanceof Slope) { - Slope slope = (Slope) possibleSlopeTile; - return slope.isCeilingSlope(); - } - - return false; - } - -} diff --git a/src/engine/game/tiles/ForegroundTile.java b/src/engine/game/tiles/ForegroundTile.java index 6379ce2..89b6aab 100644 --- a/src/engine/game/tiles/ForegroundTile.java +++ b/src/engine/game/tiles/ForegroundTile.java @@ -1,7 +1,8 @@ package engine.game.tiles; -import engine.game.Logic; import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox; +import engine.game.physics.Hitbox.CollisionNode; /** * Class representing a Tile within the foreground layer of the Level. @@ -50,51 +51,76 @@ public ForegroundTile(int id) { } /** - * Determines whether or not Entities can collide with this Tile when - * attempting to move according to the given CollisionResult. + * Checks for an x-collision and adds it to the CollisionResult. + * + * @param result CollisionResult to which Collisions should be added. + * @param dstX + * The absolute x-position of the collision node after the attempted + * movement. + * @param node Node that has collided. + */ + public void checkForCollision_X( + CollisionResult result, float dstX, CollisionNode node) { + // No collision by default + } + + /** + * Checks for a y-collision and adds it to the CollisionResult. * * @param result - * @param logic - * @param tileX - * @param tileY + * @param dstY + * The absolute y-position of the collision node after the attempted + * movement. + * @param node Node that has collided. + */ + public void checkForCollision_Y( + CollisionResult result, float dstY, CollisionNode node) { + // No collision by default + } + + /** + * Determines whether or not this Tile is completely solid. + * * @return */ - public boolean hasCollisionX(CollisionResult result, Logic logic, int tileX, - int tileY) { + public boolean isSolid() { return false; } /** - * Determines whether or not Entities can collide with this Tile when - * attempting to move according to the given CollisionResult. + * Determines whether or not this Tile has special collision properties. * - * @param result - * @param logic - * @param tileX - * @param tileY * @return */ - public boolean hasCollisionY(CollisionResult result, Logic logic, int tileX, - int tileY) { + public boolean hasSpecialCollisionProperties() { return false; } /** - * Modifies the given CollisionResult after a collision with this Tile. + * Called when a Hitbox collides with this Tile in the x-axis. * - * @param collision + *

For tiles that have collision, this will cause the Hitbox to bounce + * off the tile according to its bounce coefficient. A bounce coefficient of + * zero (the default) will cause the Hitbox to stop. + * + * @param result */ - public void collisionOccurredX(CollisionResult collision) { - // Solid tiles should override this + public void hitboxCollidedX(CollisionResult result) { + Hitbox hitbox = result.hitbox; + float newSpeedX = -hitbox.getSpeedX() * hitbox.bounceCoefficient; + hitbox.setSpeedX(newSpeedX); } /** - * Modifies the given CollisionResult after a collision with this Tile. + * Called when a Hitbox collides with this Tile in the y-axis. * - * @param collision + * @see ForegroundTile#hitboxCollidedX(CollisionResult) + * @param result */ - public void collisionOccurredY(CollisionResult collision) { - // Solid tiles should override this + public void hitboxCollidedY(CollisionResult result) { + Hitbox hitbox = result.hitbox; + float newSpeedY = -hitbox.getSpeedY() * hitbox.bounceCoefficient; + hitbox.setSpeedY(newSpeedY); } } diff --git a/src/engine/game/tiles/LeftCeilingSlope.java b/src/engine/game/tiles/LeftCeilingSlope.java new file mode 100644 index 0000000..48b66d6 --- /dev/null +++ b/src/engine/game/tiles/LeftCeilingSlope.java @@ -0,0 +1,95 @@ +package engine.game.tiles; + +import engine.game.physics.Collision; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Left Ceiling Slope. + * + *

+ *  #####
+ *  ###
+ *  #
+ * 
+ */ +public class LeftCeilingSlope extends Slope { + + public LeftCeilingSlope(int id) { + super(id); + } + + @Override + protected boolean isCollisionValid_Y( + CollisionResult result, + PostProcessCollision slopeCollision, + Collision collision) { + + // Allow y-collisions with the ceiling at the top of the Slope + if (getSlopeNodeX(result) > slopeCollision.getTileRight()) { + return true; + } + + // Disable other y-collisions while on the Slope + return false; + } + + @Override + protected boolean shouldCollide( + CollisionResult result, PostProcessCollision collision) { + + if (result.hasCollisionOccurredX() + && getSlopeNodeX(result) > collision.getTileRight()) { + // The Hitbox has collided with a wall + // (for example, if this Slope leads up to a vertical wall, and the + // Hitbox collided with the solid tile above the slope) + return false; + } + + return super.shouldCollide(result, collision); + } + + @Override + protected float calculateNodeYAfterCollision( + CollisionResult result, CollisionNode node, float collisionY) { + return collisionY + node.y; + } + + @Override + protected float getSlopeNodeY(CollisionResult result) { + return result.top(); + } + + @Override + protected boolean isPointInSlope(float x, float y) { + return x <= Tile.HEIGHT - y; + } + + @Override + protected float calculateY(float distIntoTileX) { + /* + * _____ B + * | ./ + * | ./ + * |/ + * ' + * A + * + * (A) x = 0, y = 1 + * (B) x = 1, y = 0 + */ + return Tile.HEIGHT - distIntoTileX; + } + + @Override + protected float getBouceMultiplierX() { + return -1; + } + + @Override + protected float getBouceMultiplierY() { + return -1; + } + +} diff --git a/src/engine/game/tiles/LeftSlope.java b/src/engine/game/tiles/LeftSlope.java new file mode 100644 index 0000000..a9c7641 --- /dev/null +++ b/src/engine/game/tiles/LeftSlope.java @@ -0,0 +1,105 @@ +package engine.game.tiles; + +import engine.game.physics.Collision; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Left Slope. + * + *
+ *  #
+ *  ###
+ *  #####
+ * 
+ */ +public class LeftSlope extends Slope { + + public LeftSlope(int id) { + super(id); + } + + @Override + protected boolean isCollisionValid_Y( + CollisionResult result, + PostProcessCollision slopeCollision, + Collision collision) { + + // Allow y-collisions with the floor at the bottom of the Slope + if (getSlopeNodeX(result) > slopeCollision.getTileRight()) { + return true; + } + + // Disable y-collisions while on the Slope + return false; + } + + @Override + protected boolean shouldCollide( + CollisionResult result, PostProcessCollision collision) { + + if (result.hasCollisionOccurredX() + && getSlopeNodeX(result) > collision.getTileRight()) { + // The Hitbox has collided with a wall + // (for example, if this Slope leads down to a vertical drop, and + // the Hitbox collided with the solid tile under the slope) + return false; + } + + if (result.hitbox.isGrounded() + && result.getAttemptedDx() > 0 + && result.getAttemptedDy() > 0 + && result.bottom() <= collision.getTileBottom()) { + // The Hitbox was just grounded, so even if it's not intersecting + // the solid part of the slope, we should "pull" it onto the slope. + // This prevents fast-moving Hitboxes from flying off slopes. + return true; + } + + return super.shouldCollide(result, collision); + } + + @Override + protected float calculateNodeYAfterCollision( + CollisionResult result, CollisionNode node, float collisionY) { + return (collisionY - result.hitbox.height) + node.y; + } + + @Override + protected float getSlopeNodeY(CollisionResult result) { + return result.bottom(); + } + + @Override + protected boolean isPointInSlope(float x, float y) { + return x <= y; + } + + @Override + protected float calculateY(float distIntoTileX) { + /* + * A + * . + * |\. + * | \. + * |____\ + * B + * + * (A) x = 0, y = 0 + * (B) x = 1, y = 1 + */ + return distIntoTileX; + } + + @Override + protected float getBouceMultiplierX() { + return 1; + } + + @Override + protected float getBouceMultiplierY() { + return 1; + } + +} diff --git a/src/engine/game/tiles/PostProcessingTile.java b/src/engine/game/tiles/PostProcessingTile.java new file mode 100644 index 0000000..7b97d95 --- /dev/null +++ b/src/engine/game/tiles/PostProcessingTile.java @@ -0,0 +1,17 @@ +package engine.game.tiles; + +import engine.game.physics.CollisionResult; +import engine.game.physics.PostProcessCollision; + +/** + * A special tile that can inspect and modify a CollisionResult after all x- and + * y-collisions have been added. + * + * @author Dan Bryce + */ +public interface PostProcessingTile { + + void postProcessing( + CollisionResult result, PostProcessCollision thisCollision); + +} diff --git a/src/engine/game/tiles/RightCeilingSlope.java b/src/engine/game/tiles/RightCeilingSlope.java new file mode 100644 index 0000000..c873cca --- /dev/null +++ b/src/engine/game/tiles/RightCeilingSlope.java @@ -0,0 +1,95 @@ +package engine.game.tiles; + +import engine.game.physics.Collision; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Right Ceiling Slope. + * + *
+ *  #####
+ *    ###
+ *      #
+ * 
+ */ +public class RightCeilingSlope extends Slope { + + public RightCeilingSlope(int id) { + super(id); + } + + @Override + protected boolean isCollisionValid_Y( + CollisionResult result, + PostProcessCollision slopeCollision, + Collision collision) { + + // Allow y-collisions with the ceiling at the top of the Slope + if (getSlopeNodeX(result) < slopeCollision.getTileLeft()) { + return true; + } + + // Disable other y-collisions while on the Slope + return false; + } + + @Override + protected boolean shouldCollide( + CollisionResult result, PostProcessCollision collision) { + + if (result.hasCollisionOccurredX() + && getSlopeNodeX(result) < collision.getTileLeft()) { + // The Hitbox has collided with a wall + // (for example, if this Slope leads up to a vertical wall, and the + // Hitbox collided with the solid tile above the slope) + return false; + } + + return super.shouldCollide(result, collision); + } + + @Override + protected float calculateNodeYAfterCollision( + CollisionResult result, CollisionNode node, float collisionY) { + return collisionY + node.y; + } + + @Override + protected float getSlopeNodeY(CollisionResult result) { + return result.top(); + } + + @Override + protected boolean isPointInSlope(float x, float y) { + return x >= y; + } + + @Override + protected float calculateY(float distIntoTileX) { + /* + * A _____ + * \. | + * \. | + * \| + * ' + * B + * + * (A) x = 0, y = 0 + * (B) x = 1, y = 1 + */ + return distIntoTileX; + } + + @Override + protected float getBouceMultiplierX() { + return 1; + } + + @Override + protected float getBouceMultiplierY() { + return 1; + } + +} diff --git a/src/engine/game/tiles/RightSlope.java b/src/engine/game/tiles/RightSlope.java new file mode 100644 index 0000000..c792f7c --- /dev/null +++ b/src/engine/game/tiles/RightSlope.java @@ -0,0 +1,105 @@ +package engine.game.tiles; + +import engine.game.physics.Collision; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Right Slope. + * + *
+ *      #
+ *    ###
+ *  #####
+ * 
+ */ +public class RightSlope extends Slope { + + public RightSlope(int id) { + super(id); + } + + @Override + protected boolean isCollisionValid_Y( + CollisionResult result, + PostProcessCollision slopeCollision, + Collision collision) { + + // Allow y-collisions with the floor at the bottom of the Slope + if (getSlopeNodeX(result) < slopeCollision.getTileLeft()) { + return true; + } + + // Disable other y-collisions while on the Slope + return false; + } + + @Override + protected boolean shouldCollide( + CollisionResult result, PostProcessCollision collision) { + + if (result.hasCollisionOccurredX() + && getSlopeNodeX(result) < collision.getTileLeft()) { + // The Hitbox has collided with a wall + // (for example, if this Slope leads down to a vertical drop, and + // the Hitbox collided with the solid tile under the slope) + return false; + } + + if (result.hitbox.isGrounded() + && result.getAttemptedDx() < 0 + && result.getAttemptedDy() > 0 + && result.bottom() <= collision.getTileBottom()) { + // The Hitbox was just grounded, so even if it's not intersecting + // the solid part of the slope, we should "pull" it onto the slope. + // This prevents fast-moving Hitboxes from flying off slopes. + return true; + } + + return super.shouldCollide(result, collision); + } + + @Override + protected float calculateNodeYAfterCollision( + CollisionResult result, CollisionNode node, float collisionY) { + return (collisionY - result.hitbox.height) + node.y; + } + + @Override + protected float getSlopeNodeY(CollisionResult result) { + return result.bottom(); + } + + @Override + protected boolean isPointInSlope(float x, float y) { + return x >= Tile.HEIGHT - y; + } + + @Override + protected float calculateY(float distIntoTileX) { + /* + * B + * . + * ./| + * ./ | + * /____| + * A + * + * (A) x = 0, y = 1 + * (B) x = 1, y = 0 + */ + return Tile.HEIGHT - distIntoTileX; + } + + @Override + protected float getBouceMultiplierX() { + return -1; + } + + @Override + protected float getBouceMultiplierY() { + return -1; + } + +} diff --git a/src/engine/game/tiles/SemiSolidPlatform.java b/src/engine/game/tiles/SemiSolidPlatform.java deleted file mode 100644 index 97af7b8..0000000 --- a/src/engine/game/tiles/SemiSolidPlatform.java +++ /dev/null @@ -1,52 +0,0 @@ -package engine.game.tiles; - -import engine.game.Logic; -import engine.game.physics.Collision; -import engine.game.physics.CollisionResult; - -/** - * A tile that is only solid from above. - * - * @author Dan Bryce - */ -public class SemiSolidPlatform extends ForegroundTile { - - public SemiSolidPlatform(int id) { - super(id); - } - - @Override - public boolean hasCollisionY(CollisionResult result, Logic logic, int tileX, - int tileY) { - - if (result.hitbox.bottom() >= tileY * Tile.HEIGHT) { - // We were already inside the SemiSolidPlatform before this collision check - return false; - } - - return true; - } - - /** - * Process vertical collisions with a SemiSolidPlatform. - * - *

This allows Entities to jump up through a SemiSolidPlatform, but - * prevents them from falling through it. - */ - @Override - public void collisionOccurredY(CollisionResult collision) { - - if (collision.getAttemptedDy() > 0) { - // Moving down; collision is between the bottom of the hitbox and - // the top of the tile. - float hitboxY = collision.bottom(); - float collisionY = Tile.getTop(hitboxY); - collision.addCollision_Y(new Collision(hitboxY, collisionY, this)); - - } else if (collision.getAttemptedDy() < 0) { - // Moving up; no collision! - return; - } - } - -} diff --git a/src/engine/game/tiles/Slope.java b/src/engine/game/tiles/Slope.java index 321bcf7..a51760e 100644 --- a/src/engine/game/tiles/Slope.java +++ b/src/engine/game/tiles/Slope.java @@ -1,74 +1,237 @@ package engine.game.tiles; +import engine.game.GameUtils; +import engine.game.physics.Collision; +import engine.game.physics.CollisionResult; import engine.game.physics.Hitbox; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.Physics; +import engine.game.physics.PostProcessCollision; /** - * Abstract Slope class, to be subclassed by specific Slope types. + * Base class for Slopes. + * + *

There are a lot of problems with implementing slopes in a tile-based game: + * + * 1) Hitboxes may at times intersect the tile behind or below a slope. + * Collisions with such tiles must be disabled. + * + * 2) Regular collisions must still be respected outside the slope, for example, + * if a slope leads into a wall. + * + * 3) It looks strange if a Hitbox is positioned such that only its corner sits + * on the slope, as the rest of the Hitbox will be floating. Thus, when a + * Hitbox collides with a slope, it should be positioned such that its + * horizontal centre sits atop the slope. + * + * 4) Unlike regular collisions which affect the speed of a Hitbox, collisions + * with ceiling slopes should not slow a falling Hitbox. + * + * 5) Hitboxes that do not support slope traversal (for example, projectiles) + * should take into account the angle of the slope when bouncing off the + * slope. + * + * 6) By default, fast-moving Hitboxes will fly off the slope, instead of + * sliding down it. + * + * These problems are addressed herein. * * @author Dan Bryce */ -public abstract class Slope extends ForegroundTile { +public abstract class Slope extends ForegroundTile + implements PostProcessingTile { - /* - * Slope flag constants. - * - * These can be used (with the '&' operator) to create conditions that apply - * only to certain Slope tiles, e.g. + /** + * Constructs a Slope with the given Slope flags. * - * if (slope.getSlopeFlags() & SLOPE_LEFT > 0) + * @param id */ - public static final int SLOPE_LEFT = 0xff000000; - public static final int SLOPE_RIGHT = 0x00ff0000; - public static final int SLOPE_CEILING_LEFT = 0x0000ff00; - public static final int SLOPE_CEILING_RIGHT = 0x000000ff; - public static final int SLOPE_LEFT_ANY = SLOPE_LEFT | SLOPE_CEILING_LEFT; - public static final int SLOPE_RIGHT_ANY = SLOPE_RIGHT | SLOPE_CEILING_RIGHT; - public static final int SLOPE_FLOOR_ANY = SLOPE_LEFT | SLOPE_RIGHT; - public static final int SLOPE_CEILING_ANY = SLOPE_CEILING_LEFT | SLOPE_CEILING_RIGHT; - public static final int NOT_ON_SLOPE = 0; + protected Slope(int id) { + super(id); + } + + @Override + public boolean hasSpecialCollisionProperties() { + return true; + } /** - * Slope flags of this Slope tile. + * Resolves a PostProcessCollision with this Slope. */ - protected int slopeFlags; + @Override + public void postProcessing( + CollisionResult result, PostProcessCollision slopeCollision) { + + // Filter out invalid x-collisions + for (Collision collision : result.getCollisionsX()) { + if (!isCollisionValid_X(result, slopeCollision, collision)) { + result.invalidateCollision(collision); + } + } + + // Filter out invalid y-collisions + for (Collision collision : result.getCollisionsY()) { + if (!isCollisionValid_Y(result, slopeCollision, collision)) { + result.invalidateCollision(collision); + } + } + + // If the desired destination is in the Slope, add a Collision + if (shouldCollide(result, slopeCollision)) { + collideWithSlope(result, slopeCollision); + } + } /** - * Constructs a Slope with the given Slope flags. + * Determines if the given x-Collision is valid, in light of a collision + * with this Slope. * - * @param id - * @param slopeFlags + * @param result + * @param slopeCollision + * @param collision + * @return */ - protected Slope(int id, int slopeFlags) { - super(id); + protected boolean isCollisionValid_X( + CollisionResult result, + PostProcessCollision slopeCollision, + Collision collision) { + + int tileXBefore = Tile.getTileX(result.initialNodeX(collision.node)); + int tileXAfter = Tile.getTileX(result.desiredNodeX(collision.node)); + if (tileXBefore == tileXAfter) { + // Disable x-collisions if the CollisionNode was already + // intersecting the Tile in question + // (e.g. having been on a neighbouring slope) + return false; + } - this.slopeFlags = slopeFlags; - } + if (result.initialNodeY(collision.node) < slopeCollision.getTileTop()) { + // Allow x-collisions triggered by CollisionNodes above the Slope + // (e.g. if a Slope leads into a wall) + return true; + } - public boolean isFloorSlope() { - return (slopeFlags & SLOPE_FLOOR_ANY) != 0; + if (result.initialNodeY(collision.node) > slopeCollision.getTileBottom()) { + // Allow x-collisions triggered by CollisionNodes below the Slope + // (e.g. if a Slope leads to a vertical drop) + return true; + } + + // Disable other x-collisions while on the Slope + return false; } - public boolean isCeilingSlope() { - return (slopeFlags & SLOPE_CEILING_ANY) != 0; + /** + * Determines if the given x-Collision is valid, in light of a collision + * with this Slope. + * + * @param result + * @param slopeCollision + * @param collision + * @return + */ + protected abstract boolean isCollisionValid_Y( + CollisionResult result, + PostProcessCollision slopeCollision, + Collision collision); + + /** + * Determines if a CollisionResult should collide with this Slope. + * + * @param result + * @param collision + * @return + */ + protected boolean shouldCollide( + CollisionResult result, PostProcessCollision collision) { + + // Determine the position of the slope node relative to this Slope tile + float xInSlope = getSlopeNodeX(result) - collision.getTileLeft(); + float yInSlope = getSlopeNodeY(result) - collision.getTileTop(); + + // A Hitbox is only considered to be intersecting the Slope if its slope + // node is inside the *solid* part of the Slope + return isPointInSlope(xInSlope, yInSlope); } - public boolean isLeftSlope() { - return (slopeFlags & SLOPE_LEFT_ANY) != 0; + /** + * Determines the absolute x-position of the "slope node", that is, the + * point on the Hitbox which should sit atop the slope. + * + * @param result + * @return + */ + protected float getSlopeNodeX(CollisionResult result) { + return result.centreX(); } - public boolean isRightSlope() { - return (slopeFlags & SLOPE_RIGHT_ANY) != 0; + /** + * Determines the absolute y-position of the "slope node", that is, the + * point on the Hitbox which should sit atop the slope. + * + * @param result + * @return + */ + protected abstract float getSlopeNodeY(CollisionResult result); + + /** + * Causes the CollisionResult to collide with this Slope. + * + * @param result + * @param slopeCollision + */ + protected void collideWithSlope( + CollisionResult result, PostProcessCollision slopeCollision) { + Collision collision = createCollision(result, slopeCollision); + result.addCollision_Y(collision); } /** - * Gets the y-position of the Slope at the given x-position. + * Creates a y-collision based on a PostProcessCollision with this Slope. * - *

Positions range from 0 - Tile.SIZE. + * @param result + * @param slopeCollision + * @return + */ + protected Collision createCollision( + CollisionResult result, PostProcessCollision slopeCollision) { + + // Determine the "correct" y-position of the slope node on the Slope + float xInSlope = getSlopeNodeX(result) - slopeCollision.getTileLeft(); + float yInSlopeCorrect = calculateY(xInSlope); + + // Keep this position within acceptable bounds + yInSlopeCorrect = GameUtils.clamp(yInSlopeCorrect, + 0, + Tile.HEIGHT - Physics.SMALLEST_DISTANCE); + + // Find the absolute y-position of this point + float collisionY = slopeCollision.getTileTop() + yInSlopeCorrect; + + // Calculate the initial and corrected position of the CollisionNode + float yBefore = result.initialNodeY(slopeCollision.node); + float yAfter = calculateNodeYAfterCollision( + result, slopeCollision.node, collisionY); + + // Create the Collision + return Collision.create( + yBefore, + yAfter, + slopeCollision.node, + this); + } + + /** + * Given the point of a collision, calculates the new position of the + * CollisionNode that triggered this collision. * - * @param distIntoTileX + * @param result + * @param node + * @param collisionY * @return */ - public abstract float getSlopeY_At_X(float distIntoTileX); + protected abstract float calculateNodeYAfterCollision( + CollisionResult result, CollisionNode node, float collisionY); /** * Determines if a point is inside the solid part of this Slope. @@ -79,10 +242,20 @@ public boolean isRightSlope() { * @param y Position from 0 - Tile.HEIGHT. * @return */ - public abstract boolean isPointInSlope(float x, float y); + protected abstract boolean isPointInSlope(float x, float y); + + /** + * Gets the y-position of the Slope at the given x-position. + * + *

Positions range from 0 - Tile.SIZE. + * + * @param distIntoTileX x-position relative to the left of the Tile. + * @return y-position relative to the top of the Tile. + */ + protected abstract float calculateY(float distIntoTileX); /** - * Changes the given Hitbox's speed to cause it to bounce off this Slope. + * Causes a Hitbox to bounce after a collision with this Slope. * *

If you imagine 2 lines protruding from a Slope, one horizontal and one * vertical, those lines create 3 different sectors. These correspond to the @@ -103,146 +276,36 @@ public boolean isRightSlope() { *

Fortunately, after some experimentation, it would appear that these * different cases can all be handled in the same way - just swap the x- and * y-speeds, and possibly invert them (depending on the slope). - * - * @param hitbox - * @param bounceCoefficient */ - public abstract void collide(Hitbox hitbox, float bounceCoefficient); - - //////////////////////////////////////////////////////////////////////////// - // Slope Subclasses - //////////////////////////////////////////////////////////////////////////// - - /** - * Left Slope. - * - *

-     *  #
-     *  ###
-     *  #####
-     * 
- */ - public static class Left extends Slope { - - public Left(int id) { - super(id, SLOPE_LEFT); - } - - @Override - public boolean isPointInSlope(float x, float y) { - return y >= x; - } - - @Override - public float getSlopeY_At_X(float distIntoTileX) { - return Tile.HEIGHT - distIntoTileX; + @Override + public void hitboxCollidedY(CollisionResult result) { + + // Collisions with Slopes are always in the y-axis, + // but they affect the Hitbox speed in BOTH axes + + Hitbox hitbox = result.hitbox; + float prevSpeedX = hitbox.getSpeedX(); + float prevSpeedY = hitbox.getSpeedY(); + + // The new y-speed is the old x-speed + float newSpeedY = prevSpeedX + * hitbox.bounceCoefficient + * getBouceMultiplierY(); + hitbox.setSpeedY(newSpeedY); + + // The x-speed is only affected if a Hitbox supports slope traversal + if (!hitbox.getCollisionFlag(Hitbox.SUPPORTS_SLOPE_TRAVERSAL)) { + + // The new x-speed is the old y-speed + float newSpeedX = prevSpeedY + * hitbox.bounceCoefficient + * getBouceMultiplierX(); + hitbox.setSpeedX(newSpeedX); } - - @Override - public void collide(Hitbox hitbox, float bounceCoefficient) { - hitbox.setSpeedX(hitbox.getSpeedY() * bounceCoefficient); - hitbox.setSpeedY(hitbox.getSpeedX() * bounceCoefficient); - } - } - /** - * Right Slope. - * - *
-     *      #
-     *    ###
-     *  #####
-     * 
- */ - public static class Right extends Slope { - - public Right(int id) { - super(id, SLOPE_RIGHT); - } - - @Override - public boolean isPointInSlope(float x, float y) { - return x + y >= Tile.HEIGHT; - } - - @Override - public float getSlopeY_At_X(float distIntoTileX) { - return distIntoTileX; - } - - @Override - public void collide(Hitbox hitbox, float bounceCoefficient) { - hitbox.setSpeedX(-hitbox.getSpeedY() * bounceCoefficient); - hitbox.setSpeedY(-hitbox.getSpeedX() * bounceCoefficient); - } + protected abstract float getBouceMultiplierX(); - } - - /** - * Left Ceiling Slope. - * - *
-     *  #####
-     *  ###
-     *  #
-     * 
- */ - public static class LeftCeiling extends Slope { - - public LeftCeiling(int id) { - super(id, SLOPE_CEILING_LEFT); - } - - @Override - public boolean isPointInSlope(float x, float y) { - return x + y <= Tile.HEIGHT; - } - - @Override - public float getSlopeY_At_X(float distIntoTileX) { - return Tile.HEIGHT - distIntoTileX; - } - - @Override - public void collide(Hitbox hitbox, float bounceCoefficient) { - hitbox.setSpeedX(-hitbox.getSpeedY() * bounceCoefficient); - hitbox.setSpeedY(-hitbox.getSpeedX() * bounceCoefficient); - } - - } - - /** - * Right Ceiling Slope. - * - *
-     *  #####
-     *    ###
-     *      #
-     * 
- */ - public static class RightCeiling extends Slope { - - public RightCeiling(int id) { - super(id, SLOPE_CEILING_RIGHT); - } - - @Override - public boolean isPointInSlope(float x, float y) { - return y <= x; - } - - @Override - public float getSlopeY_At_X(float distIntoTileX) { - return distIntoTileX; - } - - @Override - public void collide(Hitbox hitbox, float bounceCoefficient) { - hitbox.setSpeedX(hitbox.getSpeedY() * bounceCoefficient); - hitbox.setSpeedY(hitbox.getSpeedX() * bounceCoefficient); - } - - } + protected abstract float getBouceMultiplierY(); } diff --git a/src/engine/game/tiles/SolidBlock.java b/src/engine/game/tiles/SolidBlock.java index 22e7214..5b9da07 100644 --- a/src/engine/game/tiles/SolidBlock.java +++ b/src/engine/game/tiles/SolidBlock.java @@ -1,8 +1,8 @@ package engine.game.tiles; -import engine.game.Logic; import engine.game.physics.Collision; import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox.CollisionNode; /** * A solid block tile. @@ -16,53 +16,38 @@ public SolidBlock(int id) { } @Override - public boolean hasCollisionX(CollisionResult result, Logic logic, int tileX, - int tileY) { + public boolean isSolid() { return true; } @Override - public boolean hasCollisionY(CollisionResult result, Logic logic, int tileX, - int tileY) { - return true; - } + public void checkForCollision_X( + CollisionResult result, float dstX, CollisionNode node) { - @Override - public void collisionOccurredX(CollisionResult collision) { + float xBefore = result.hitbox.x + node.x; - if (collision.getAttemptedDx() > 0) { - // Moving right; collision is between the right of the hitbox and - // the left of the tile. - float hitboxX = collision.right(); - float collisionX = Tile.getLeft(hitboxX); - collision.addCollision_X(new Collision(hitboxX, collisionX, this)); + // The Tile edge we collide with depends on the direction of travel + float xAfter = node.isOnLeftEdge() + ? Tile.getRight(dstX) + : Tile.getLeft(dstX); - } else if (collision.getAttemptedDx() < 0) { - // Moving left; collision is between the left of the hitbox and the - // right of the tile. - float hitboxX = collision.left(); - float collisionX = Tile.getRight(hitboxX); - collision.addCollision_X(new Collision(hitboxX, collisionX, this)); - } + result.addCollision_X( + Collision.create(xBefore, xAfter, node, this)); } @Override - public void collisionOccurredY(CollisionResult collision) { + public void checkForCollision_Y( + CollisionResult result, float dstY, CollisionNode node) { + + float yBefore = result.hitbox.y + node.y; - if (collision.getAttemptedDy() > 0) { - // Moving down; collision is between the bottom of the hitbox and - // the top of the tile. - float hitboxY = collision.bottom(); - float collisionY = Tile.getTop(hitboxY); - collision.addCollision_Y(new Collision(hitboxY, collisionY, this)); + // The Tile edge we collide with depends on the direction of travel + float yAfter = node.isOnTopEdge() + ? Tile.getBottom(dstY) + : Tile.getTop(dstY); - } else if (collision.getAttemptedDy() < 0) { - // Moving up; collision is between the top of the hitbox and the - // bottom of the tile. - float hitboxY = collision.top(); - float collisionY = Tile.getBottom(hitboxY); - collision.addCollision_Y(new Collision(hitboxY, collisionY, this)); - } + result.addCollision_Y( + Collision.create(yBefore, yAfter, node, this)); } } diff --git a/src/engine/game/tiles/Tile.java b/src/engine/game/tiles/Tile.java index e3124f2..317fed6 100644 --- a/src/engine/game/tiles/Tile.java +++ b/src/engine/game/tiles/Tile.java @@ -56,6 +56,18 @@ public Tile(int id) { this.id = id; } + /** + * Attaches a TileComponent. + * + *

Results in a callback to {@link TileComponent#onAttach}. + * + * @param component + */ + public void attach(TileComponent component) { + components.add(component); + component.onAttach(this); + } + /** * Gets this Tile's unique identifier. * @@ -73,7 +85,7 @@ public int getId() { * @return Tile edge in world units. */ public static float getLeft(float x) { - return x - (x % WIDTH); + return (int) (x / WIDTH); } /** @@ -96,7 +108,7 @@ public static float getRight(float x) { * @return Tile edge in world units. */ public static float getTop(float y) { - return y - (y % HEIGHT); + return (int) (y / HEIGHT); } /** diff --git a/src/engine/game/tiles/TileComponent.java b/src/engine/game/tiles/TileComponent.java index d375ccf..885c3d3 100644 --- a/src/engine/game/tiles/TileComponent.java +++ b/src/engine/game/tiles/TileComponent.java @@ -8,4 +8,8 @@ public TileComponent(String key) { super(key); } + public void onAttach(Tile tile) { + // Do nothing by default + } + } diff --git a/src/engine/launcher/Launcher.java b/src/engine/launcher/Launcher.java index e473d04..dcf4a39 100644 --- a/src/engine/launcher/Launcher.java +++ b/src/engine/launcher/Launcher.java @@ -105,7 +105,7 @@ public void start() { Logger.log(ex); } } - + exit(status); } diff --git a/test/engine/game/FramerateTest.java b/test/engine/game/FramerateTest.java index d1e88fa..105f158 100644 --- a/test/engine/game/FramerateTest.java +++ b/test/engine/game/FramerateTest.java @@ -29,12 +29,12 @@ public void testPhysicsAt60Fps() { GameUtils.worldUnits(0), GameUtils.worldUnits(1)); Hitbox hitbox = e.hitbox; - hitbox.setSpeedX(GameUtils.worldUnits(0.5f)); logic.addEntity(e); // WHEN 2 seconds have passed int msPerFrame = 15; for (int msPassed = 0; msPassed < 2000; msPassed += msPerFrame) { + hitbox.setSpeedX(GameUtils.worldUnits(0.5f)); logic.updateEntities(msPerFrame); } @@ -56,12 +56,12 @@ public void testPhysicsAt20Fps() { GameUtils.worldUnits(0), GameUtils.worldUnits(1)); Hitbox hitbox = e.hitbox; - hitbox.setSpeedX(GameUtils.worldUnits(0.5f)); logic.addEntity(e); // WHEN 2 seconds have passed int msPerFrame = 45; for (int msPassed = 0; msPassed < 2000; msPassed += msPerFrame) { + hitbox.setSpeedX(GameUtils.worldUnits(0.5f)); logic.updateEntities(msPerFrame); } diff --git a/test/engine/game/GameUtilsTest.java b/test/engine/game/GameUtilsTest.java index 1b07863..2c368d0 100644 --- a/test/engine/game/GameUtilsTest.java +++ b/test/engine/game/GameUtilsTest.java @@ -6,7 +6,7 @@ /** * Tests of the static methods in the GameUtils class. - * + * * @author Dan Bryce */ public class GameUtilsTest { diff --git a/test/engine/game/PhysicsTest.java b/test/engine/game/PhysicsTest.java index dd69261..07f5096 100644 --- a/test/engine/game/PhysicsTest.java +++ b/test/engine/game/PhysicsTest.java @@ -31,11 +31,11 @@ public void testCollision_None() { Entity entity = new TestEntity( GameUtils.worldUnits(1), GameUtils.worldUnits(1)); - + // WHEN trying to move down by half a tile, right by half a tile CollisionResult collision = Physics.getCollisionResult(logic, entity.hitbox, 0.5f, 0.5f); - + // THEN no collision is detected assertEquals(false, collision.hasCollisionOccurredX()); assertEquals(false, collision.hasCollisionOccurredY()); @@ -57,14 +57,14 @@ public void testCollision_Floor() { Entity entity = new TestEntity( GameUtils.worldUnits(1), GameUtils.worldUnits(1)); - + // WHEN trying to move down by half a tile CollisionResult collision = Physics.getCollisionResult(logic, entity.hitbox, 0, 0.5f); - + // THEN the nearest collision detected is at the top of the floor assertEquals(GameUtils.worldUnits(2), - collision.getNearestCollisionY().getCollisionPos(), + collision.getNearestCollisionY().collisionPos, Physics.SMALLEST_DISTANCE); } @@ -84,14 +84,14 @@ public void testCollision_Ceiling() { Entity entity = new TestEntity( GameUtils.worldUnits(1), GameUtils.worldUnits(1)); - + // WHEN trying to move up by half a tile CollisionResult collision = Physics.getCollisionResult(logic, entity.hitbox, 0, -0.5f); - + // THEN the nearest collision detected is at the bottom of the ceiling assertEquals(GameUtils.worldUnits(1) - Physics.SMALLEST_DISTANCE, - collision.getNearestCollisionY().getCollisionPos(), + collision.getNearestCollisionY().collisionPos, Physics.SMALLEST_DISTANCE); } @@ -111,14 +111,14 @@ public void testCollision_LeftWall() { Entity entity = new TestEntity( GameUtils.worldUnits(1), GameUtils.worldUnits(1)); - + // WHEN trying to move left by half a tile CollisionResult collision = Physics.getCollisionResult(logic, entity.hitbox, -0.5f, 0); - + // THEN the nearest collision detected is at the right edge of the wall assertEquals(GameUtils.worldUnits(1) - Physics.SMALLEST_DISTANCE, - collision.getNearestCollisionX().getCollisionPos(), + collision.getNearestCollisionX().collisionPos, Physics.SMALLEST_DISTANCE); } @@ -138,14 +138,14 @@ public void testCollision_RightWall() { Entity entity = new TestEntity( GameUtils.worldUnits(1), GameUtils.worldUnits(1)); - + // WHEN trying to move left by half a tile CollisionResult collision = Physics.getCollisionResult(logic, entity.hitbox, 0.5f, 0); - + // THEN the nearest collision detected is at the left edge of the wall assertEquals(GameUtils.worldUnits(2), - collision.getNearestCollisionX().getCollisionPos(), + collision.getNearestCollisionX().collisionPos, Physics.SMALLEST_DISTANCE); } diff --git a/test/engine/game/tiles/LeftCeilingSlopeTest.java b/test/engine/game/tiles/LeftCeilingSlopeTest.java new file mode 100644 index 0000000..940c660 --- /dev/null +++ b/test/engine/game/tiles/LeftCeilingSlopeTest.java @@ -0,0 +1,182 @@ +package engine.game.tiles; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import engine.game.GameUtils; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Left Ceiling Slope. + * + *

+ *  #####
+ *  ###
+ *  #
+ * 
+ */ +public class LeftCeilingSlopeTest { + + private Slope slope = new LeftCeilingSlope(0); + + @Test + public void resolveCollisions_NoCollisionWhenNotIntersectingSlope() { + /* + * GIVEN: + * + * Slope is at (1, 1) + * Hitbox is at (1, 2) and moving by (0, -0.1f) + * + * #### + * #/`` + * `E + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(2); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(-0.1f); + int slopeTileX = 1; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[0]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN no collision is added + assertEquals(0, result.getCollisionsY().size()); + assertEquals(null, result.getNearestCollisionY()); + } + + @Test + public void resolveCollisions_CollideWhenJumpingIntoSlope_Y() { + /* + * GIVEN: + * + * Slope is at (1, 1) + * Hitbox is at (1, 2) and moving by (0, -0.75f) + * + * #### + * #/`` + * `E + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(0); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(-0.75f); + int slopeTileX = 1; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[0]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at the middle of the Slope + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + GameUtils.worldUnits(1.5f), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWhenJumpingIntoSlope_XAndY() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (3, 2) and moving by (-0.75f, -0.75f) + * + * #### + * #/`` + * ` E + */ + float hX = GameUtils.worldUnits(3); + float hY = GameUtils.worldUnits(2); + float dx = GameUtils.worldUnits(-0.75f); + float dy = GameUtils.worldUnits(-0.75f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[0]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added 0.25f world units into the Slope + // => Initial slopeNodeX = 3.5f + // => Destination slopeNodeX = 2.75f + // => This is 0.75f world units into the Slope + // => For a left ceiling slope: 0.75 (x-axis) -> 0.25 (y-axis) + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + GameUtils.worldUnits(1.25f), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWhenIntersectingTileAboveSlope() { + /* + * GIVEN: + * + * Slope is at (1, 1) + * Hitbox is at (2, 1) and moving by (-0.5f, -0.25f) + * + * #### + * #/E` + * ` + */ + float hX = GameUtils.worldUnits(2); + float hY = GameUtils.worldUnits(1); + float dx = GameUtils.worldUnits(-0.5f); + float dy = GameUtils.worldUnits(-0.25f); + int slopeTileX = 1; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collision with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[0]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at ceiling level + // (because the slope node has only just entered the Slope) + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + Tile.getTop(GameUtils.worldUnits(1)), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void testPointInSlope() { + // 4 corners of the Tile + assertEquals(true, slope.isPointInSlope(0, 0)); + assertEquals(true, slope.isPointInSlope(Tile.WIDTH, 0)); + assertEquals(false, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); + assertEquals(true, slope.isPointInSlope(0, Tile.HEIGHT)); + } + + @Test + public void testSlopeY_At_X() { + // When 1/4 of the way into the Tile, we should be 3/4 from the top + assertEquals(3 * Tile.HEIGHT / 4, + slope.calculateY(Tile.WIDTH / 4), + 0.1); + } + +} diff --git a/test/engine/game/tiles/LeftSlopeTest.java b/test/engine/game/tiles/LeftSlopeTest.java new file mode 100644 index 0000000..28c3341 --- /dev/null +++ b/test/engine/game/tiles/LeftSlopeTest.java @@ -0,0 +1,189 @@ +package engine.game.tiles; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import engine.game.GameUtils; +import engine.game.physics.Collision; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Left Slope. + * + *
+ *  #
+ *  ###
+ *  #####
+ * 
+ */ +public class LeftSlopeTest { + + private Slope slope = new LeftSlope(0); + + @Test + public void resolveCollisions_NoCollisionWhenNotIntersectingSlope() { + /* + * GIVEN: + * + * Slope is at (1, 1) + * Hitbox is at (1, 0) and moving by (0, 0.1f) + * + * _E + * #\__ + * #### + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(0); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(0.1f); + int slopeTileX = 1; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[0]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN no collision is added + assertEquals(0, result.getCollisionsY().size()); + assertEquals(null, result.getNearestCollisionY()); + } + + @Test + public void resolveCollisions_CollideWhenFallingIntoSlope() { + /* + * GIVEN: + * + * Slope is at (1, 1) + * Hitbox is at (1, 0) and moving by (0, 0.75f) + * + * _E + * #\__ + * #### + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(0); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(0.75f); + int slopeTileX = 1; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[0]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at the middle of the Slope + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + GameUtils.worldUnits(1.5f), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWhenIntersectingTileUnderSlope() { + /* + * GIVEN: + * + * Slope is at (1, 1) + * Hitbox is at (2, 1) and moving by (-0.5f, 0.25f) + * _ + * #\E_ + * #### + */ + float hX = GameUtils.worldUnits(2); + float hY = GameUtils.worldUnits(1); + float dx = GameUtils.worldUnits(-0.5f); + float dy = GameUtils.worldUnits(0.25f); + int slopeTileX = 1; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[0]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at floor level + // (because the slope node has only just entered the Slope) + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + Tile.getBottom(GameUtils.worldUnits(1)), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWithGroundBeforeSlopeNodeEntersSlope() { + /* + * GIVEN: + * + * Slope is at (1, 1) + * Hitbox is at (2, 1) and moving by (-0.25f, 0.25f) + * _ + * #\E_ + * #### + * + * Thus, the Hitbox will intersect the Slope, but its slode node will + * lie outside the slope. + */ + float hX = GameUtils.worldUnits(2); + float hY = GameUtils.worldUnits(1); + float dx = GameUtils.worldUnits(0.25f); + float dy = GameUtils.worldUnits(0.25f); + int slopeTileX = 1; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // AND the Hitbox has already collided with the ground + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[0]; + Collision groundCollision = Collision.create( + hY, + Tile.getTop(hY + GameUtils.worldUnits(1)), + node, + new SolidBlock(0)); + result.addCollision_Y(groundCollision); + + // WHEN resolving collisions with this Slope + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN the ground collision is still present + assertEquals(1, result.getCollisionsY().size()); + assertEquals(groundCollision, result.getNearestCollisionY()); + } + + @Test + public void isPointInSlope() { + // 4 corners of the Tile + assertEquals(true, slope.isPointInSlope(0, 0)); + assertEquals(false, slope.isPointInSlope(Tile.WIDTH, 0)); + assertEquals(true, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); + assertEquals(true, slope.isPointInSlope(0, Tile.HEIGHT)); + + // Below the Tile + assertEquals(true, slope.isPointInSlope(0, 2 * Tile.HEIGHT)); + } + + @Test + public void getSlopeY_At_X() { + // When 1/4 of the way into the Tile, we should be 1/4 from the top + assertEquals(Tile.HEIGHT / 4, + slope.calculateY(Tile.WIDTH / 4), + 0.1); + } + +} diff --git a/test/engine/game/tiles/RightCeilingSlopeTest.java b/test/engine/game/tiles/RightCeilingSlopeTest.java new file mode 100644 index 0000000..1546c8c --- /dev/null +++ b/test/engine/game/tiles/RightCeilingSlopeTest.java @@ -0,0 +1,182 @@ +package engine.game.tiles; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import engine.game.GameUtils; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Right Ceiling Slope. + * + *
+ *  #####
+ *    ###
+ *      #
+ * 
+ */ +public class RightCeilingSlopeTest { + + private Slope slope = new RightCeilingSlope(0); + + @Test + public void resolveCollisions_NoCollisionWhenNotIntersectingSlope() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (2, 2) and moving by (0, -0.1f) + * + * #### + * ``\ + * E` + */ + float hX = GameUtils.worldUnits(2); + float hY = GameUtils.worldUnits(2); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(-0.1f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[1]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN no collision is added + assertEquals(0, result.getCollisionsY().size()); + assertEquals(null, result.getNearestCollisionY()); + } + + @Test + public void resolveCollisions_CollideWhenJumpingIntoSlope_Y() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (2, 2) and moving by (0, -0.75f) + * + * #### + * ``\ + * E` + */ + float hX = GameUtils.worldUnits(2); + float hY = GameUtils.worldUnits(2); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(-0.75f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[1]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at the middle of the Slope + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + GameUtils.worldUnits(1.5f), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWhenJumpingIntoSlope_XAndY() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (1, 2) and moving by (0.75f, -0.75f) + * + * #### + * ``\ + * E ` + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(2); + float dx = GameUtils.worldUnits(0.75f); + float dy = GameUtils.worldUnits(-0.75f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[1]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added 0.25f world units into the Slope + // => Initial slopeNodeX = 1.5f + // => Destination slopeNodeX = 2.25f + // => This is 0.25f world units into the Slope + // => For a right ceiling slope: 0.25 (x-axis) -> 0.25 (y-axis) + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + GameUtils.worldUnits(1.25f), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWhenIntersectingTileAboveSlope() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (1, 1) and moving by (0.5f, -0.25f) + * + * #### + * `E\ + * ` + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(1); + float dx = GameUtils.worldUnits(0.5f); + float dy = GameUtils.worldUnits(-0.25f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getTopNodes()[1]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at ceiling level + // (because the slope node has only just entered the Slope) + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + Tile.getTop(GameUtils.worldUnits(1)), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void testPointInSlope() { + // 4 corners of the Tile + assertEquals(true, slope.isPointInSlope(0, 0)); + assertEquals(true, slope.isPointInSlope(Tile.WIDTH, 0)); + assertEquals(true, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); + assertEquals(false, slope.isPointInSlope(0, Tile.HEIGHT)); + } + + @Test + public void testSlopeY_At_X() { + // When 1/4 of the way into the Tile, we should be 1/4 from the top + assertEquals(Tile.HEIGHT / 4, + slope.calculateY(Tile.WIDTH / 4), + 0.1); + } + +} diff --git a/test/engine/game/tiles/RightSlopeTest.java b/test/engine/game/tiles/RightSlopeTest.java new file mode 100644 index 0000000..829d504 --- /dev/null +++ b/test/engine/game/tiles/RightSlopeTest.java @@ -0,0 +1,189 @@ +package engine.game.tiles; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import engine.game.GameUtils; +import engine.game.physics.Collision; +import engine.game.physics.CollisionResult; +import engine.game.physics.Hitbox; +import engine.game.physics.Hitbox.CollisionNode; +import engine.game.physics.PostProcessCollision; + +/** + * Right Slope. + * + *
+ *      #
+ *    ###
+ *  #####
+ * 
+ */ +public class RightSlopeTest { + + private Slope slope = new RightSlope(0); + + @Test + public void resolveCollisions_NoCollisionWhenNotIntersectingSlope() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (2, 0) and moving by (0, 0.1f) + * + * E_ + * __/ + * #### + */ + float hX = GameUtils.worldUnits(2); + float hY = GameUtils.worldUnits(0); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(0.1f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[1]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN no collision is added + assertEquals(0, result.getCollisionsY().size()); + assertEquals(null, result.getNearestCollisionY()); + } + + @Test + public void resolveCollisions_CollideWhenFallingIntoSlope() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (2, 0) and moving by (0, 0.75f) + * + * E_ + * __/ + * #### + */ + float hX = GameUtils.worldUnits(2); + float hY = GameUtils.worldUnits(0); + float dx = GameUtils.worldUnits(0); + float dy = GameUtils.worldUnits(0.75f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[1]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at the middle of the Slope + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + GameUtils.worldUnits(1.5f), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWhenIntersectingTileUnderSlope() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (1, 1) and moving by (0.5f, 0.25f) + * _ + * _E/ + * #### + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(1); + float dx = GameUtils.worldUnits(0.5f); + float dy = GameUtils.worldUnits(0.25f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // WHEN resolving collisions with this Slope + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[1]; + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN a collision is added at floor level + // (because the slope node has only just entered the Slope) + assertEquals(1, result.getCollisionsY().size()); + assertEquals( + Tile.getBottom(GameUtils.worldUnits(1)), + result.getNearestCollisionY().collisionPos, 0.001); + } + + @Test + public void resolveCollisions_CollideWithGroundBeforeSlopeNodeEntersSlope() { + /* + * GIVEN: + * + * Slope is at (2, 1) + * Hitbox is at (1, 1) and moving by (0.25f, 0.25f) + * _ + * _E/ + * #### + * + * Thus, the Hitbox will intersect the Slope, but its slode node will + * lie outside the slope. + */ + float hX = GameUtils.worldUnits(1); + float hY = GameUtils.worldUnits(1); + float dx = GameUtils.worldUnits(0.25f); + float dy = GameUtils.worldUnits(0.25f); + int slopeTileX = 2; + int slopeTileY = 1; + Hitbox hitbox = new Hitbox(hX, hY, 1, 1, null); + + // AND the Hitbox has already collided with the ground + CollisionResult result = new CollisionResult(hitbox, dx, dy); + CollisionNode node = hitbox.getBottomNodes()[1]; + Collision groundCollision = Collision.create( + hY, + Tile.getTop(hY + GameUtils.worldUnits(1)), + node, + new SolidBlock(0)); + result.addCollision_Y(groundCollision); + + // WHEN resolving collisions with this Slope + PostProcessCollision collision = + new PostProcessCollision(slope, slopeTileX, slopeTileY, node); + slope.postProcessing(result, collision); + + // THEN the ground collision is still present + assertEquals(1, result.getCollisionsY().size()); + assertEquals(groundCollision, result.getNearestCollisionY()); + } + + @Test + public void testPointInSlope() { + // 4 corners of the Tile + assertEquals(false, slope.isPointInSlope(0, 0)); + assertEquals(true, slope.isPointInSlope(Tile.WIDTH, 0)); + assertEquals(true, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); + assertEquals(true, slope.isPointInSlope(0, Tile.HEIGHT)); + + // Below the Tile + assertEquals(true, slope.isPointInSlope(0, 2 * Tile.HEIGHT)); + } + + @Test + public void testSlopeY_At_X() { + // When 1/4 of the way into the Tile, we should be 3/4 from the top + assertEquals(3 * Tile.HEIGHT / 4, + slope.calculateY(Tile.WIDTH / 4), + 0.1); + } + +} diff --git a/test/engine/game/tiles/SlopeTest.java b/test/engine/game/tiles/SlopeTest.java deleted file mode 100644 index 4d8a1ac..0000000 --- a/test/engine/game/tiles/SlopeTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package engine.game.tiles; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -/** - * Tests that the various Slope tiles are correctly defined. - * - * @author Dan Bryce - */ -public class SlopeTest { - - /** - * Left Slope. - * - *
-     *  #
-     *  ###
-     *  #####
-     * 
- */ - public static class TestSlopeLeft { - - private Slope slope = new Slope.Left(0); - - @Test - public void testPointInSlope() { - // 4 corners of the Tile - assertEquals(true, slope.isPointInSlope(0, 0)); - assertEquals(false, slope.isPointInSlope(Tile.WIDTH, 0)); - assertEquals(true, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); - assertEquals(true, slope.isPointInSlope(0, Tile.HEIGHT)); - - // Below the Tile - assertEquals(true, slope.isPointInSlope(0, 2 * Tile.HEIGHT)); - } - - @Test - public void testSlopeY_At_X() { - // When 1/4 of the way into the Tile, we should be 3/4 up the slope - assertEquals(3 * Tile.HEIGHT / 4, - slope.getSlopeY_At_X(Tile.WIDTH / 4), - 0.1); - } - - } - - /** - * Right Slope. - * - *
-     *      #
-     *    ###
-     *  #####
-     * 
- */ - public static class TestSlopeRight { - - private Slope slope = new Slope.Right(0); - - @Test - public void testPointInSlope() { - // 4 corners of the Tile - assertEquals(false, slope.isPointInSlope(0, 0)); - assertEquals(true, slope.isPointInSlope(Tile.WIDTH, 0)); - assertEquals(true, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); - assertEquals(true, slope.isPointInSlope(0, Tile.HEIGHT)); - - // Below the Tile - assertEquals(true, slope.isPointInSlope(0, 2 * Tile.HEIGHT)); - } - - @Test - public void testSlopeY_At_X() { - // When 1/4 of the way into the Tile, we should be 1/4 up the slope - assertEquals(Tile.HEIGHT / 4, - slope.getSlopeY_At_X(Tile.WIDTH / 4), - 0.1); - } - - } - - /** - * Left Ceiling Slope. - * - *
-     *  #####
-     *  ###
-     *  #
-     * 
- */ - public static class TestSlopeLeftCeiling { - - private Slope slope = - new Slope.LeftCeiling(0); - - @Test - public void testPointInSlope() { - // 4 corners of the Tile - assertEquals(true, slope.isPointInSlope(0, 0)); - assertEquals(true, slope.isPointInSlope(Tile.WIDTH, 0)); - assertEquals(false, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); - assertEquals(true, slope.isPointInSlope(0, Tile.HEIGHT)); - } - - @Test - public void testSlopeY_At_X() { - // When 1/4 of the way into the Tile, we should be 3/4 up the slope - assertEquals(3 * Tile.HEIGHT / 4, - slope.getSlopeY_At_X(Tile.WIDTH / 4), - 0.1); - } - - } - - /** - * Right Ceiling Slope. - * - *
-     *  #####
-     *    ###
-     *      #
-     * 
- */ - public static class TestSlopeRightCeiling { - - private Slope slope = new Slope.RightCeiling(0); - - @Test - public void testPointInSlope() { - // 4 corners of the Tile - assertEquals(true, slope.isPointInSlope(0, 0)); - assertEquals(true, slope.isPointInSlope(Tile.WIDTH, 0)); - assertEquals(true, slope.isPointInSlope(Tile.WIDTH, Tile.HEIGHT)); - assertEquals(false, slope.isPointInSlope(0, Tile.HEIGHT)); - } - - @Test - public void testSlopeY_At_X() { - // When 1/4 of the way into the Tile, we should be 1/4 up the slope - assertEquals(Tile.HEIGHT / 4, - slope.getSlopeY_At_X(Tile.WIDTH / 4), - 0.1); - } - - } - -}