Skip to content

Commit

Permalink
Fix some issues with slopes, and some refactoring
Browse files Browse the repository at this point in the history
 - Fix Hitboxes sometimes colliding at the bottom of a ceiling slope

 - Fix ceiling slopes not preserving downward momentum

 - Remove references to AWT (not supported by LWJGL3 on MacOS)

 - Improve Logic construction (Level should not be required at construction)
  • Loading branch information
Danjb1 committed Feb 16, 2020
1 parent ee43fb8 commit 14b0a03
Show file tree
Hide file tree
Showing 16 changed files with 263 additions and 52 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A 2D tile-based game engine using axis-aligned bounding boxes.

Used by [Abacus](http://www.danjb.com/abacus).

![Screenshot](docs/demo.png)

## Features
Expand All @@ -16,12 +18,12 @@ with no external dependencies.
Supports 45 degree floor and ceiling slopes, and new tile types can easily be
added.

### **:electric_plug: Extendible component-based entity system**
### **:electric_plug: Extensible**

Entities can easily be extended with additional properties and behaviour using a
flexible component-based system.

### **:books: Easily integrate with any GUI / rendering / input library**
### **:books: Library-agnostic**

Window creation, rendering and input handling are abstracted; the engine is not
tied to any existing libraries.
Expand Down
3 changes: 2 additions & 1 deletion demo/demo/launcher/DemoLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ public static void main(String[] args) {

DemoLauncher launcher = new DemoLauncher();

Logic logic = new Logic();
Level level = createLevel();
Logic logic = new Logic(level);
logic.setLevel(level);
launcher.setState(new GameState(launcher, logic));

launcher.start();
Expand Down
13 changes: 9 additions & 4 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Features

- Support multiple Tile layers
- Support level extensions (e.g. multiple tile layers)

- Support slopes with different gradients

Expand All @@ -11,14 +11,19 @@
## Bugs

- Slopes:
- Occasional jittering at the bottom of ceiling slopes
- Strange behaviour when a slope leads into a ceiling
- Strange behaviour when colliding with the "back" of a slope tile
- Hitbox can clip through a right ceiling slope if jumping into the very top of it
- Hitbox can drop off a right slope and land on the solid block 2 tiles below it
- Hitbox clips through solid blocks if "wedged" between a slope and a solid block
- Hitbox can clip through the "back" of a slope (not yet supported)

## Tech Debt

- Don't log to System.out

- Move more code from demo project to engine?

- Reduce duplication between the various slope classes

- Improve test coverage (use reflection to test private methods)

## Demo
Expand Down
23 changes: 11 additions & 12 deletions src/engine/game/Camera.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package engine.game;

import java.awt.geom.Rectangle2D;

import engine.game.entities.CameraSettings;
import engine.game.entities.Entity;
import engine.game.physics.Hitbox;
import engine.game.tiles.Tile;
import engine.util.Rectangle;

/**
* Camera capable of tracking an Entity within the game world.
Expand Down Expand Up @@ -40,7 +39,7 @@ public enum TrackingMode {
/**
* Rectangle of the world that is visible to this camera, in world units.
*/
private Rectangle2D.Float visibleRegion = new Rectangle2D.Float();
private Rectangle visibleRegion = new Rectangle();

/**
* Entity this Camera is tracking.
Expand Down Expand Up @@ -132,8 +131,8 @@ public void teleportToDestination() {
settings.entityTeleported();
float targetX = hitbox.centreX() + settings.getTargetOffsetX();
float targetY = hitbox.centreY() + settings.getTargetOffsetY();
float x = (float) (targetX - visibleRegion.getWidth() / 2);
float y = (float) (targetY - visibleRegion.getHeight() / 2);
float x = targetX - visibleRegion.width / 2;
float y = targetY - visibleRegion.height / 2;
setPos(x, y);
}

Expand Down Expand Up @@ -182,14 +181,14 @@ public void update(int delta) {
private float getDistToTargetX() {

// If the full width of the level is visible, there is no need to move
if (level.getWorldWidth() <= visibleRegion.getWidth()) {
if (level.getWorldWidth() <= visibleRegion.width) {
return 0;
}

// Calculate how far the camera is from the target
Hitbox hitbox = targetEntity.hitbox;
float targetPos = hitbox.centreX() + settings.getTargetOffsetX();
return (float) (targetPos - visibleRegion.getCenterX());
return targetPos - visibleRegion.getCenterX();
}

/**
Expand All @@ -200,14 +199,14 @@ private float getDistToTargetX() {
private float getDistToTargetY() {

// If the full height of the level is visible, there is no need to move
if (level.getWorldHeight() <= visibleRegion.getHeight()) {
if (level.getWorldHeight() <= visibleRegion.height) {
return 0;
}

// Calculate how far the camera is from the target
Hitbox hitbox = targetEntity.hitbox;
float targetPos = hitbox.centreY() + settings.getTargetOffsetY();
return (float) (targetPos - visibleRegion.getCenterY());
return targetPos - visibleRegion.getCenterY();
}

/**
Expand Down Expand Up @@ -260,7 +259,7 @@ private float keepWithinBoundsX(float cameraX) {
float maxVisibleX = minVisibleX + level.getNumTilesX() * Tile.WIDTH;
float maxCameraX = maxVisibleX - visibleRegion.width;

if (level.getWorldWidth() <= visibleRegion.getWidth()) {
if (level.getWorldWidth() <= visibleRegion.width) {
// The full width of the level is visible; keep camera at left edge
cameraX = minVisibleX;
} else if (cameraX < minVisibleX) {
Expand All @@ -285,7 +284,7 @@ private float keepWithinBoundsY(float cameraY) {
float maxVisibleY = minVisibleY + level.getNumTilesY() * Tile.HEIGHT;
float maxCameraY = maxVisibleY - visibleRegion.height;

if (level.getWorldHeight() <= visibleRegion.getHeight()) {
if (level.getWorldHeight() <= visibleRegion.height) {
// The full height of the level is visible; keep camera at top edge
cameraY = minVisibleY;
} else if (cameraY < minVisibleY) {
Expand Down Expand Up @@ -380,7 +379,7 @@ private int getNumVisibleTilesY() {
return (int) (visibleRegion.height / Tile.HEIGHT) + 2;
}

public Rectangle2D.Float getVisibleRegion() {
public Rectangle getVisibleRegion() {
return visibleRegion;
}

Expand Down
11 changes: 11 additions & 0 deletions src/engine/game/GameUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,15 @@ public static float clamp(float val, float min, float max) {
return val;
}

/**
* Determines if 2 values have the same sign.
*
* @param a
* @param b
* @return
*/
public static boolean sameSign(int a, float b) {
return a == b || (a > 0) == (b > 0);
}

}
24 changes: 18 additions & 6 deletions src/engine/game/Logic.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,36 @@ public class Logic {
protected List<Entity> entitiesToDelete = new ArrayList<>();

/**
* Constructs the Logic using the given Level.
*
* @param level
* Constructs the Logic.
*/
public Logic(Level level) {
this.level = level;

public Logic() {
// Add the always-available Tile types
addTileType(new Air(ForegroundTile.ID_AIR));
addTileType(new SolidBlock(ForegroundTile.ID_SOLID_BLOCK));
}

/**
* Sets the current level.
*
* <p>This must be called before {@link #update}.
*
* @param level
*/
public void setLevel(Level level) {
this.level = level;
}

/**
* Updates the Logic using the given delta value.
*
* @param delta Number of milliseconds since the last update.
*/
public void update(int delta) {

if (level == null) {
throw new IllegalStateException("No Level loaded");
}

updateEntities(delta);
processCollisions();
deleteEntities();
Expand Down
5 changes: 4 additions & 1 deletion src/engine/game/physics/Hitbox.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ public boolean equals(Object obj) {
////////////////////////////////////////////////////////////////////////////

/**
* Collision flag that allows a Hitbox to walk up and down slopes.
* Collision flag that allows a Hitbox to slide up and down slopes.
*
* <p>Hitboxes that do not have this flag set will bounce off slopes
* instead. This is the default behaviour.
*/
public static final int SUPPORTS_SLOPE_TRAVERSAL = 0;

Expand Down
13 changes: 13 additions & 0 deletions src/engine/game/tiles/LeftCeilingSlope.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ && getSlopeNodeX(result) > collision.getTileRight()) {
return super.shouldBeOnSlope(result, collision);
}

@Override
protected float getMaxCollisionY() {
// See comments in RightCeilingSlope
return Tile.HEIGHT;
}

@Override
protected float calculateNodeYAfterCollision(
CollisionResult result, CollisionNode node, float collisionY) {
Expand Down Expand Up @@ -113,4 +119,11 @@ protected float getBounceMultiplierY() {
return -1;
}

@Override
protected boolean shouldRemoveSpeedOnCollision(CollisionResult result) {
// Remove y-speed if the Hitbox was moving up
// (but not when hitting the slope while falling)
return result.getAttemptedDy() < 0;
}

}
16 changes: 16 additions & 0 deletions src/engine/game/tiles/LeftSlope.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import engine.game.physics.Collision;
import engine.game.physics.CollisionResult;
import engine.game.physics.Hitbox.CollisionNode;
import engine.game.physics.Physics;
import engine.game.physics.PostProcessCollision;

/**
Expand Down Expand Up @@ -76,6 +77,14 @@ && getSlopeNodeX(result) > collision.getTileRight()) {
return super.shouldBeOnSlope(result, collision);
}

@Override
protected float getMaxCollisionY() {
// For floor slopes, we have to be careful that the collision does not
// occur outside the bounds of the tile, otherwise the Hitbox can
// become embedded in the floor
return Tile.HEIGHT - Physics.SMALLEST_DISTANCE;
}

@Override
protected float calculateNodeYAfterCollision(
CollisionResult result, CollisionNode node, float collisionY) {
Expand Down Expand Up @@ -123,4 +132,11 @@ protected float getBounceMultiplierY() {
return 1;
}

@Override
protected boolean shouldRemoveSpeedOnCollision(CollisionResult result) {
// Remove y-speed if the Hitbox was moving down
// (but not when hitting the slope on the ascent of a jump)
return result.getAttemptedDy() > 0;
}

}
40 changes: 40 additions & 0 deletions src/engine/game/tiles/RightCeilingSlope.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,39 @@ && getSlopeNodeX(result) < collision.getTileLeft()) {
return super.shouldBeOnSlope(result, collision);
}

@Override
protected float getMaxCollisionY() {
// For ceiling slopes, we have to ensure that when a collision occurs
// at the bottom of the slope, the Hitbox will end up strictly below
// the slope tile when the collision resolves, otherwise the Hitbox
// may become embedded in the ceiling.
//
// Technically it is more correct to say that the collision occurs at
// (Tile.HEIGHT - Physics.SMALLEST_DISTANCE), as that signifies the far
// edge of the tile. We add Physics.SMALLEST_DISTANCE back on later in
// `Hitbox.resolveCollisions_Y()`, which should result in the Hitbox
// being placed below the tile.
//
// However, rounding errors can occur when working with such precise
// values, leading to the Hitbox becoming embedded in the ceiling, so
// we err on the side of caution instead.
//
// Example (buggy):
// - Slope tile is at y=3
// - Physics.SMALLEST_DISTANCE is 0.0001f
// - getMaxCollisionY() returns 0.9999f
// - collisionY = 3.0f + 0.9999f = 3.9998999f (rounding error)
// - Hitbox is placed at 3.9998999f + 0.0001f = 3.9999998f (bug)
//
// Example (fixed):
// - Slope tile is at y=3
// - Physics.SMALLEST_DISTANCE is 0.0001f
// - getMaxCollisionY() returns 1.0f
// - collisionY = 3.0f + 1.0f = 4.0f
// - Hitbox is placed at 4.0f + 0.0001f = 4.0001f (ok!)
return Tile.HEIGHT;
}

@Override
protected float calculateNodeYAfterCollision(
CollisionResult result, CollisionNode node, float collisionY) {
Expand Down Expand Up @@ -113,4 +146,11 @@ protected float getBounceMultiplierY() {
return 1;
}

@Override
protected boolean shouldRemoveSpeedOnCollision(CollisionResult result) {
// Remove y-speed if the Hitbox was moving up
// (but not when hitting the slope while falling)
return result.getAttemptedDy() < 0;
}

}
16 changes: 16 additions & 0 deletions src/engine/game/tiles/RightSlope.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import engine.game.physics.Collision;
import engine.game.physics.CollisionResult;
import engine.game.physics.Hitbox.CollisionNode;
import engine.game.physics.Physics;
import engine.game.physics.PostProcessCollision;

/**
Expand Down Expand Up @@ -76,6 +77,14 @@ && getSlopeNodeX(result) < collision.getTileLeft()) {
return super.shouldBeOnSlope(result, collision);
}

@Override
protected float getMaxCollisionY() {
// For floor slopes, we have to be careful that the collision does not
// occur outside the bounds of the tile, otherwise the Hitbox can
// become embedded in the floor
return Tile.HEIGHT - Physics.SMALLEST_DISTANCE;
}

@Override
protected float calculateNodeYAfterCollision(
CollisionResult result, CollisionNode node, float collisionY) {
Expand Down Expand Up @@ -123,4 +132,11 @@ protected float getBounceMultiplierY() {
return -1;
}

@Override
protected boolean shouldRemoveSpeedOnCollision(CollisionResult result) {
// Remove y-speed if the Hitbox was moving down
// (but not when hitting the slope on the ascent of a jump)
return result.getAttemptedDy() > 0;
}

}
Loading

0 comments on commit 14b0a03

Please sign in to comment.