package de.samdev.colorrunner.game.world.entities; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import de.samdev.colorrunner.CRGame; import de.samdev.colorrunner.game.world.CRGameWorld; public abstract class MovingEntity extends CRGameEntity { public Vector2 velocity = new Vector2(); public final static float EPSILON = 1e-4f; public final static float EPSILON_TOUCH = 5e-4f; private boolean face_TOP_isTouching = false; private boolean face_LEFT_isTouching = false; private boolean face_BOTTOM_isTouching = false; private boolean face_RIGHT_isTouching = false; public MovingEntity(CRGameWorld _owner, float width, float height) { this(_owner, 0, 0, width, height); } public MovingEntity(CRGameWorld _owner, float x, float y, float width, float height) { super(_owner, x, y, width, height); } protected boolean moveByX(float bx) { if (bx == 0) return false; boolean collided = false; Rectangle next = new Rectangle(bounds); next.x += bx; for (CRGameEntity ent : world.entities) { if (ent == this) continue; if (overlaps(next, ent.bounds) && ent.canCollide(true, this)) { if (overlaps(ent.bounds, bounds)) { Gdx.app.log("Collision", "Ignore in bounds collision [X]"); continue; } if (bx < 0) { // LEFT float correction = (ent.bounds.x + ent.bounds.height) - next.x + EPSILON; next.x += correction; bx = next.x - bounds.x; if (bx >= 0) return true; // collision but no movement collided = true; } else { // RIGHT float correction = (next.x + next.height) - ent.bounds.x + EPSILON; next.x -= correction; bx = next.x - bounds.x; if (bx <= 0) return true; // collision but no movement collided = true; } } } if (getFirstCollision() != null) return true; // collision but movement AND correction failed bounds.x = next.x; return collided; } protected boolean moveByY(float by) { if (by == 0) return false; boolean collided = false; Rectangle next = new Rectangle(bounds); next.y += by; for (CRGameEntity ent : world.entities) { if (ent == this) continue; if (overlaps(next, ent.bounds) && ent.canCollide(true, this)) { if (overlaps(bounds, ent.bounds)) { Gdx.app.log("Collision", "Ignore in bounds collision [Y]"); continue; } if (by < 0) { // DOWN float correction = (ent.bounds.y + ent.bounds.height) - next.y + EPSILON; next.y += correction; by = next.y - bounds.y; if (by >= 0) return true; // collision but no movement collided = true; } else { // UP float correction = (next.y + next.height) - ent.bounds.y + EPSILON; next.y -= correction; by = next.y - bounds.y; if (by <= 0) return true; // collision but no movement collided = true; } } } if (getFirstCollision() != null) return true; // collision but movement AND correction failed bounds.y = next.y; return collided; } protected boolean updateHitBox(float width, float height) { boolean succ_x = updateHitBoxWidth(width); if (CRGame.DEBUG && getFirstCollision() != null) Gdx.app.error("Collision", "Collision after updateHitBox [[Width]] - " + succ_x); boolean succ_y = updateHitBoxHeight(height); if (CRGame.DEBUG && getFirstCollision() != null) Gdx.app.error("Collision", "Collision after updateHitBox [[Height]] - " + succ_y); return succ_x && succ_y; } private boolean updateHitBoxWidth(float width) { Rectangle next = new Rectangle(bounds); float add = width - bounds.width; if (isTouching_LEFT() && ! isTouching_RIGHT()) { next.width += add; } else if (! isTouching_LEFT() && isTouching_RIGHT()) { next.width += add; next.x -= add; next.x -= EPSILON; } else { next.width += add; next.x -= add/2; } if (add < 0) { if (CRGame.DEBUG) { for (CRGameEntity ent2 : world.entities) { if (ent2 == this) continue; if (overlaps(next, ent2.bounds) && ent2.canCollide(false, this)) { Gdx.app.log("Entity Hitbox", "Expand failed X-"); return false; } } } bounds.set(next); return true; } else { for (CRGameEntity ent : world.entities) { if (ent == this) continue; if (overlaps(ent.bounds, next) && ent.canCollide(false, this)) { if (overlaps(ent.bounds, bounds)) { Gdx.app.log("Collision", "Ignore in bounds collision on HB Expand [X]"); continue; } if (ent.bounds.x > next.x) { next.x -= (next.x + next.width) - ent.bounds.x + EPSILON; for (CRGameEntity ent2 : world.entities) { if (ent2 == this) continue; if (overlaps(next, ent2.bounds) && ent2.canCollide(false, this)) { Gdx.app.log("Entity Hitbox", "Expand failed X+"); return false; } } } else if (ent.bounds.x < next.x) { next.x += (ent.bounds.x + ent.bounds.width) - next.x + EPSILON; for (CRGameEntity ent2 : world.entities) { if (ent2 == this) continue; if (overlaps(next, ent2.bounds) && ent2.canCollide(false, this)) { Gdx.app.log("Entity Hitbox", "Expand failed X-"); return false; } } } } } bounds.set(next); return true; } } private boolean updateHitBoxHeight(float height) { Rectangle next = new Rectangle(bounds); float add = height - bounds.height; if (isTouching_BOTTOM() && !isTouching_TOP()) { next.height += add; } else if (! isTouching_BOTTOM() && isTouching_TOP()) { next.height += add; next.y -= add; next.y -= EPSILON; } else { next.height += add; next.y -= add/2; } if (add < 0) { if (CRGame.DEBUG) { for (CRGameEntity ent2 : world.entities) { if (ent2 == this) continue; if (overlaps(next, ent2.bounds) && ent2.canCollide(false, this)) { Gdx.app.log("Entity Hitbox", "Expand failed Y-"); return false; } } } bounds.set(next); return true; } else { for (CRGameEntity ent : world.entities) { if (ent == this) continue; if (overlaps(ent.bounds, next) && ent.canCollide(false, this)) { if (overlaps(ent.bounds, bounds)) { Gdx.app.log("Collision", "Ignore in bounds collision on HB Expand [Y]"); continue; } if (ent.bounds.y > next.y) { next.y -= (next.y + next.height) - ent.bounds.y + EPSILON; for (CRGameEntity ent2 : world.entities) { if (ent2 == this) continue; if (overlaps(next, ent2.bounds) && ent2.canCollide(false, this)) { Gdx.app.log("Entity Hitbox", "Expand failed Y+"); return false; } } } else if (ent.bounds.y < next.y) { next.y += (ent.bounds.y + ent.bounds.height) - next.y + EPSILON; for (CRGameEntity ent2 : world.entities) { if (ent2 == this) continue; if (overlaps(next, ent2.bounds) && ent2.canCollide(false, this)) { Gdx.app.log("Entity Hitbox", "Expand failed Y-"); return false; } } } } } bounds.set(next); return true; } } private void updateTouchCollisions() { face_TOP_isTouching = false; face_LEFT_isTouching = false; face_BOTTOM_isTouching = false; face_RIGHT_isTouching = false; for (CRGameEntity ent : world.entities) { if (ent == this) continue; if (Math.abs((ent.bounds.y + ent.bounds.height) - bounds.y) < EPSILON_TOUCH && ent.bounds.x < (bounds.x + bounds.width) && (ent.bounds.x + ent.bounds.width) > bounds.x && ent.canCollide(false, this)) { face_BOTTOM_isTouching = true; } if (Math.abs((bounds.y + bounds.height) - ent.bounds.y) < EPSILON_TOUCH && ent.bounds.x < (bounds.x + bounds.width) && (ent.bounds.x + ent.bounds.width) > bounds.x && ent.canCollide(false, this)) { face_TOP_isTouching = true; } if (Math.abs((bounds.x + bounds.width) - ent.bounds.x) < EPSILON_TOUCH && ent.bounds.y < (bounds.y + bounds.height) && (ent.bounds.y + ent.bounds.height) > bounds.y && ent.canCollide(false, this)) { face_RIGHT_isTouching = true; } if (Math.abs((ent.bounds.x + ent.bounds.width) - bounds.x) < EPSILON_TOUCH && ent.bounds.y < (bounds.y + bounds.height) && (ent.bounds.y + ent.bounds.height) > bounds.y && ent.canCollide(false, this)) { face_LEFT_isTouching = true; } } } public Vector2 getVelocity() { return velocity; } @Override public void update(float delta) { if (velocity.x != 0) { if (moveByX(velocity.x * delta)) { velocity.x = 0; } if (CRGame.DEBUG && getFirstCollision() != null) Gdx.app.error("Collision", "Collision after move [[X]]"); } if (velocity.y != 0) { if (moveByY(velocity.y * delta)) { velocity.y = 0; } if (CRGame.DEBUG && getFirstCollision() != null) Gdx.app.error("Collision", "Collision after move [[Y]]"); } updateTouchCollisions(); } private CRGameEntity getFirstCollision() { for (CRGameEntity ent : world.entities) { if (ent == this) continue; if (overlaps(ent.bounds, bounds) && ent.canCollide(false, this)) return ent; } return null; } private boolean overlaps (Rectangle r1, Rectangle r2) { return r1.overlaps(r2) && r2.overlaps(r1); // make overlaps commutative (?) } public boolean isTouching_ANY() { return face_TOP_isTouching || face_LEFT_isTouching || face_BOTTOM_isTouching || face_RIGHT_isTouching; } public boolean isTouching_TOP() { return face_TOP_isTouching; } public boolean isTouching_LEFT() { return face_LEFT_isTouching; } public boolean isTouching_BOTTOM() { return face_BOTTOM_isTouching; } public boolean isTouching_RIGHT() { return face_RIGHT_isTouching; } }