package me.hammerle.supersnuvi.entity; import me.hammerle.supersnuvi.entity.components.ai.Controller; import me.hammerle.supersnuvi.entity.components.Energy; import me.hammerle.supersnuvi.entity.components.Health; import me.hammerle.supersnuvi.entity.components.ItemCollector; import me.hammerle.supersnuvi.entity.components.Movement; import me.hammerle.supersnuvi.tiles.Tile; import me.hammerle.supersnuvi.util.Face; import me.hammerle.supersnuvi.util.Utils; import me.hammerle.supersnuvi.gamelogic.Level; import me.hammerle.supersnuvi.math.Vector; import me.hammerle.supersnuvi.entity.components.IDespawn; public final class Entity { public static final float GRAVITY = 8.0f * Tile.SIZE_SCALE; public static final float STEP = 0.015625f; // this one is a little bit bigger to prevent wrong calculation // while going upwards public static final float UP_STEP = STEP + 0.00390625f; // the last position is used for interpolation during rendering private final Vector lastPos = new Vector(); // the current position of the entity private final Vector pos = new Vector(); // the width of the entity private final float width; // the height of the entity private final float height; // own force used by the controller component private final Vector ownForce = new Vector(); // the final motion of the last tick private final Vector lastMotion = new Vector(); // the motion, reduced by the movement collision check private final Vector motion = new Vector(); // the friction reducing motion each tick private final Vector baseFriction = new Vector(0.0f, 1.0f); private final Vector friction = new Vector(0.0f, 1.0f); // a flag indicating that the entity is on the ground private boolean onGround = true; // entity components private final Controller controller; private final Health health; private final Energy energy; private final Movement move; private final ItemCollector itemCollector; private final IDespawn onDespawn; // the type of the entity, used by snuvi script private final String type; private Face direction = Face.NULL; protected Entity(float width, float height, Controller c, Health h, Energy e, Movement m, ItemCollector ic, IDespawn d, String type) { this.width = width; this.height = height; this.controller = c; this.health = h; this.energy = e; this.move = m; this.itemCollector = ic; this.onDespawn = d; this.type = type; } public String getType() { return type; } //-------------------------------------------------------------------------- // components //-------------------------------------------------------------------------- public boolean isAnimated() { return controller.isAnimated(); } public Controller getController() { return controller; } public Health getHealth() { return health; } public Energy getEnergy() { return energy; } public Movement getMovement() { return move; } public ItemCollector getItemCollector() { return itemCollector; } public void onDespawn() { onDespawn.onDespawn(this); } //-------------------------------------------------------------------------- // basic stuff //-------------------------------------------------------------------------- public float getSquaredDistance(Entity e) { return Utils.getSquaredDistance(getCenterX(), getCenterY(), e.getCenterX(), e.getCenterY()); } public float getX() { return pos.getX(); } public float getY() { return pos.getY(); } public void setPosition(float x, float y) { lastPos.set(x, y); pos.set(x, y); } public float getLastX() { return lastPos.getX(); } public float getLastY() { return lastPos.getY(); } public float getCenterX() { return pos.getX() + width * 0.5f; } public float getCenterY() { return pos.getY() + height * 0.5f; } public float getWidth() { return width; } public float getHeight() { return height; } public float getMotionX() { return motion.getX(); } public float getMotionY() { return motion.getY(); } public float getOwnForceX() { return ownForce.getX(); } public float getOwnForceY() { return ownForce.getY(); } public void applyOwnForce(float x, float y) { ownForce.add(x, y); } public void applyForce(float x, float y) { motion.add(x, y); } public void setBaseFriction(float fx, float fy) { baseFriction.set(fx, fy); } public Vector getFriction() { return friction; } public boolean isAt(float x, float y) { return Math.abs(x - pos.getX()) < STEP && Math.abs(y - pos.getY()) < STEP; } public boolean isColliding(float minX, float minY, float maxX, float maxY) { return maxX > getX() && getX() + width > minX && maxY > getY() && getY() + height > minY; } public boolean isColliding(Entity ent) { return isColliding(ent.getX(), ent.getY(), ent.getX() + ent.width, ent.getY() + ent.height); } private Face getCollidingFace(Entity o) { return Utils.getCollidingFace( o.getX(), o.getY(), o.getX() + o.getWidth(), o.getY() + o.getHeight(), getX(), getY(), getX() + getWidth(), getY() + getHeight()); } public boolean jump() { if(onGround) { ownForce.addY(-move.getJumpPower()); return true; } return false; } public Face getDirection() { return direction; } //-------------------------------------------------------------------------- // ticking //-------------------------------------------------------------------------- public void tick(Level level) { // last motion is current motion, last motion will be set in move motion.set(lastMotion); lastMotion.set(0.0f, 0.0f); // reset own force ownForce.set(0.0f, 0.0f); // update onGround before controller tick onGround = isOnTileGround(level); if(onGround) { if(!move.canMoveOnTiles()) { motion.setY(0.0f); } } else { friction.set(0.2f, 1.0f); } // tick components controller.tick(this, level); energy.tick(this); health.tick(); // apply gravity if needed if(move.hasGravity(this)) { motion.addY(GRAVITY * move.getGravityFactor()); } // apply friction motion.mul(friction); if(move.isInWater()) { motion.mul(friction); move.setInWater(false); } friction.set(baseFriction); // modify current motion by own force motion.add(ownForce); if(ownForce.getX() < 0.0f) { direction = Face.LEFT; } else if(ownForce.getX() > 0.0f) { direction = Face.RIGHT; } } public void tickCollision(Level level) { // (entity <-> tile) collision doTileCollision(level); // (entity <-> entity) collision level.forEachCollidingEntity(this, (ent) -> { controller.onCollideWithEntity(this, ent, getCollidingFace(ent)); }); slideOnEdge(level); // remeber last position for rendering lastPos.set(pos); } public int move(Level level) { if(motion.getX() == 0.0f && motion.getY() == 0.0f) { return 0; } Vector maxMotion = new Vector(); maxMotion.set(motion); if(!move.canMoveOnEntities() || !move.canMoveOnTiles()) { reduceMotion(level); } pos.add(motion); int r = (motion.getX() == 0.0f && motion.getY() == 0.0f) ? 0 : 1; lastMotion.add(motion); maxMotion.sub(motion); motion.set(maxMotion); return r; } private boolean isOnTileGround(Level level) { float minX = pos.getX(); float minY = pos.getY() + height; float maxX = pos.getX() + width; float maxY = pos.getY() + height + STEP * 2; int startX = Utils.toBlock(minX); int startY = Utils.toBlock(minY); int endX = Utils.toBlock(maxX); int endY = Utils.toBlock(maxY); for(int x = startX; x <= endX; x++) { for(int y = startY; y <= endY; y++) { Tile t = level.getInteractionTile(x, y); if(t.isMoveColliding(minX, minY, maxX, maxY, x, y, level)) { return true; } } } for(Entity ent : level.getEntities()) { if(ent == this) { continue; } if(ent.getMovement().isSolid() && ent.isColliding(minX, minY, maxX, maxY)) { return true; } } return false; } private void doTileCollision(Level level) { float minX = pos.getX() - STEP; float minY = pos.getY() - STEP; float maxX = pos.getX() + width + STEP; float maxY = pos.getY() + height + STEP; int startX = Utils.toBlock(minX); int startY = Utils.toBlock(minY); int endX = Utils.toBlock(maxX); int endY = Utils.toBlock(maxY); for(int x = startX; x <= endX; x++) { for(int y = startY; y <= endY; y++) { Tile t = level.getInteractionTile(x, y); if(t.isColliding(minX, minY, maxX, maxY, x, y, level)) { Face f = t.getCollidingFace(minX, minY, maxX, maxY, x, y, level); controller.onCollideWithTile(this, x, y, level, t, f); t.onEntityCollide(this, x, y, f.getOpposite(), level); } } } } private boolean isCollidingWithTiles(int startX, int startY, int endX, int endY, Level level) { if(move.canMoveOnTiles()) { return false; } float minX = getX(); float minY = getY(); float maxX = minX + width; float maxY = minY + height; for(int x = startX; x <= endX; x++) { for(int y = startY; y <= endY; y++) { if(level.getInteractionTile(x, y).isMoveColliding(minX, minY, maxX, maxY, x, y, level)) { return true; } } } return false; } private boolean isCollidingWithEntities(Level level) { if(move.canMoveOnEntities()) { return false; } float minX = pos.getX(); float minY = pos.getY(); float maxX = pos.getX() + width; float maxY = pos.getY() + height; for(Entity ent : level.getEntities()) { if(ent == this) { continue; } if(ent.isColliding(minX, minY, maxX, maxY) && (move.isSolid() || ent.move.isSolid())) { return true; } } for(Entity ent : level.getEntitiesInQueue()) { if(ent == this) { continue; } if(ent.isColliding(minX, minY, maxX, maxY) && (move.isSolid() || ent.move.isSolid())) { return true; } } return false; } public void reduceMotion(Level level) { if(motion.getX() == 0.0f && motion.getY() == 0.0f) { return; } float mx = motion.getX(); float my = motion.getY(); int startX; int endX; if(mx < 0.0f) { startX = Utils.toBlock(pos.getX() + mx); endX = Utils.toBlock(pos.getX() + width); } else { startX = Utils.toBlock(pos.getX()); endX = Utils.toBlock(pos.getX() + width + mx); } int startY; int endY; if(my < 0.0f) { startY = Utils.toBlock(pos.getY() + my); endY = Utils.toBlock(pos.getY() + height); } else { startY = Utils.toBlock(pos.getY()); endY = Utils.toBlock(pos.getY() + height + my); } float oldPosX = pos.getX(); float oldPosY = pos.getY(); while(mx != 0.0 || my != 0.0) { if(mx != 0.0f) { float oldX = pos.getX(); if(mx < 0.0) { if(mx > -STEP) { pos.addX(mx); mx = 0.0f; } else { pos.addX(-STEP); mx += STEP; } } else if(mx > 0.0) { if(mx < STEP) { pos.addX(mx); mx = 0.0f; } else { pos.addX(STEP); mx -= STEP; } } if(isCollidingWithTiles(startX, startY, endX, endY, level) || isCollidingWithEntities(level)) { pos.addY(-UP_STEP); if(isCollidingWithTiles(startX, startY, endX, endY, level) || isCollidingWithEntities(level)) { pos.addY(UP_STEP); pos.setX(oldX); mx = 0.0f; } } } if(my != 0.0f) { float oldY = pos.getY(); if(my < 0.0) { if(my > -STEP) { pos.addY(my); my = 0.0f; } else { pos.addY(-STEP); my += STEP; } } else if(my > 0.0) { if(my < STEP) { pos.addY(my); my = 0.0f; } else { pos.addY(STEP); my -= STEP; } } if(isCollidingWithTiles(startX, startY, endX, endY, level) || isCollidingWithEntities(level)) { my = 0.0f; pos.setY(oldY); } } } motion.set(pos.getX() - oldPosX, pos.getY() - oldPosY); pos.set(oldPosX, oldPosY); } private void slideOnEdge(Level level) { float minY = pos.getY() + height; float maxY = pos.getY() + height + STEP * 2; float minX = pos.getX(); float midX = pos.getX() + width * 0.5f; float maxX = pos.getX() + width; int startX = Utils.toBlock(minX); int startY = Utils.toBlock(minY); int endX = Utils.toBlock(maxX); int endY = Utils.toBlock(maxY); int leftEdges = 0; int rightEdges = 0; for(int x = startX; x <= endX; x++) { for(int y = startY; y <= endY; y++) { Tile t = level.getInteractionTile(x, y); if(t.isMoveColliding(minX, minY, midX, maxY, x, y, level)) { rightEdges++; } if(t.isMoveColliding(midX, minY, maxX, maxY, x, y, level)) { leftEdges++; } } } if(leftEdges == 0) { applyForce(STEP * 25, 0.0f); } if(rightEdges == 0) { applyForce(-STEP * 25, 0.0f); } } public void renderTick(float lag) { controller.renderTick(this, lag); } //-------------------------------------------------------------------------- // gravity, friction //-------------------------------------------------------------------------- public boolean isOnGround() { return onGround; } @Override public String toString() { return String.format("(%f, %f, %f, %f)", pos.getX(), pos.getY(), pos.getX() + width, pos.getY() + height); } }