Player.java
package com.skloch.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.skloch.game.AchievementSystem.AchievementSystem;
/**
* A class handling everything needed to control and draw a player, including animation, movement and collision
*/
public class Player {
// Achievements
AchievementSystem achievementSystem;
// Hitboxes
public Rectangle sprite, feet, eventHitbox;
public float centreX, centreY;
public int direction = 2; // 0 = up, 1 = right, 2 = down, 3 = left (like a clock)
private TextureRegion currentFrame;
private float stateTime = 0;
private final Array<Animation<TextureRegion>> walkingAnimation, idleAnimation;
// Stats
public float speed = 600f;
public Array<GameObject> collidables;
public int scale = 4;
private Rectangle bounds;
private GameObject closestObject;
public boolean frozen, moving;
private float totalDistanceTraveled;
private float distanceSinceLastProgress = 0;
private static final float DISTANCE_THRESHOLD = 10; // Adjust this value as needed
/**
* A player character, contains methods to move the player and update animations, also includes collision handling
* and can be used to trigger events of objects near the player.
* Includes a feet hitbox for collision and an event hitbox for triggering objects.
* Call move() then draw the result of getCurrentAnimation() to use
*
* @param avatar "avatar1" for the more masculine character, "avatar2" for the more feminine character,
* player animations are packed in the player_sprites atlas
*/
public Player(String avatar) {
// Load the player's textures from the atlas
TextureAtlas playerAtlas = new TextureAtlas(Gdx.files.internal("Sprites/Player/player_sprites.atlas"));
// Load the achievement system
achievementSystem = AchievementSystem.getInstance();
walkingAnimation = new Array<Animation<TextureRegion>>(4);
idleAnimation = new Array<Animation<TextureRegion>>(4);
// Load walking animation from Sprite atlas
walkingAnimation.add(
new Animation<TextureRegion>(0.25f, playerAtlas.findRegions(avatar + "_walk_back"), Animation.PlayMode.LOOP),
new Animation<TextureRegion>(0.25f, playerAtlas.findRegions(avatar + "_walk_right"), Animation.PlayMode.LOOP),
new Animation<TextureRegion>(0.25f, playerAtlas.findRegions(avatar + "_walk_front"), Animation.PlayMode.LOOP),
new Animation<TextureRegion>(0.25f, playerAtlas.findRegions(avatar + "_walk_left"), Animation.PlayMode.LOOP));
// Load idle animation
idleAnimation.add(
new Animation<TextureRegion>(0.40f, playerAtlas.findRegions(avatar + "_idle_back"), Animation.PlayMode.LOOP),
new Animation<TextureRegion>(0.40f, playerAtlas.findRegions(avatar + "_idle_right"), Animation.PlayMode.LOOP),
new Animation<TextureRegion>(0.40f, playerAtlas.findRegions(avatar + "_idle_front"), Animation.PlayMode.LOOP),
new Animation<TextureRegion>(0.40f, playerAtlas.findRegions(avatar + "_idle_left"), Animation.PlayMode.LOOP)
);
collidables = new Array<GameObject>();
// Sprite is a rectangle covering the whole player
sprite = new Rectangle(0, 0, 17 * scale, 28 * scale);
// Feet is a rectangle just covering the player's feet, so is better for collision
feet = new Rectangle(4 * scale, 0, 9 * scale, 7 * scale);
// Hitbox for triggering events with objects
float hitboxScaleX = 2.2f;
float hitboxScaley = 1.7f;
eventHitbox = new Rectangle(
sprite.getX() - (sprite.getWidth() * hitboxScaleX - sprite.getWidth()) / 2,
sprite.getY() - (sprite.getHeight() * hitboxScaley - sprite.getHeight()) / 2,
sprite.getWidth() * hitboxScaleX,
sprite.getHeight() * hitboxScaley
);
}
/**
* Handles all the logic involved in moving the player given keyboard inputs
* If the player encounters an object, they will not be alowed to move into the space, but will attempt to
* 'slide' off of it.
* Also updates the player's animation
*
* <p></p>
* <p>
* Also locates the nearest object after moving, which can be used to trigger events
*
* @param delta The time passed since the previous render
*/
public void move(float delta) {
// Updates the player's position based on keys being pressed
// Also updates the direction they are facing, and whether they are currently moving
// And also does collision
moving = false;
// To check collision, store the player's current position
float oldX = sprite.x;
float oldY = sprite.y;
float oldFeetX = feet.x;
// If not frozen, react to keyboard input presses
if (!frozen) {
// Move the player and their 2 other hitboxes
moving = false;
if (Gdx.input.isKeyPressed(Input.Keys.LEFT) || Gdx.input.isKeyPressed(Input.Keys.A)) {
this.setX(sprite.getX() - speed * delta); // Note: Setting all the values with a constant delta removes hitbox desyncing issues
direction = 3;
moving = true;
}
if (Gdx.input.isKeyPressed(Input.Keys.RIGHT) || Gdx.input.isKeyPressed(Input.Keys.D)) {
this.setX(sprite.getX() + speed * delta);
direction = 1;
moving = true;
}
if (Gdx.input.isKeyPressed(Input.Keys.UP) || Gdx.input.isKeyPressed(Input.Keys.W)) {
this.setY(sprite.getY() + speed * delta);
direction = 0;
moving = true;
}
if (Gdx.input.isKeyPressed(Input.Keys.DOWN) || Gdx.input.isKeyPressed(Input.Keys.S)) {
this.setY(sprite.getY() - speed * delta);
direction = 2;
moving = true;
}
// Check if the player's feet are inside an object, if they are, move them back in that axis
for (GameObject object : this.collidables) {
if (feet.overlaps(object)) {
// Find the direction that the player needs to be moved back to
// Reset x
if (!(oldFeetX < object.x + object.width && oldFeetX + feet.width > object.x)) {
this.setX(oldX);
}
// If overlapping in y direction
if (!(oldY < object.y + object.height && oldY + feet.height > object.y)) {
this.setY(oldY);
}
// The above two are essentially the same code as Rectangle.overlaps()
// Just separated into the x and y dimensions
}
}
// Check the player is in bounds
if (bounds != null) {
// If player is out of bounds, move them back
if (feet.getX() < bounds.getX()) {
sprite.x = bounds.getX() - 4 * scale;
feet.x = sprite.x + 4 * scale;
}
if (feet.getX() + feet.getWidth() > bounds.getWidth()) {
sprite.x = (bounds.getWidth() - feet.getWidth()) - (4 * scale);
feet.x = sprite.x + 4 * scale;
}
if (feet.getY() < bounds.getY()) {
sprite.y = bounds.getY();
feet.y = bounds.getY();
}
if (feet.getY() + feet.getHeight() > bounds.getHeight()) {
sprite.y = bounds.getHeight() - feet.getHeight();
feet.y = sprite.y;
}
}
}
// Calculate the distance traveled in this frame
float deltaX = sprite.getX() - oldX;
float deltaY = sprite.getY() - oldY;
float distanceTraveled = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
totalDistanceTraveled += distanceTraveled;
// Check if the achievement should be incremented
AchievementSystem.getInstance().getDistanceTraveledAchievement().checkProgress((int) totalDistanceTraveled);
// Find the closest object to the player so they can interact with it
recalcCentre(); // Just recalculates the centre of the player now we have moved them
float distance = -1;
closestObject = null;
for (GameObject object : this.collidables) {
// Check if this object is even interactable
if (object.get("event") != null || object.get("text") != null) {
if (eventHitbox.overlaps(object)) {
// Check if this is the closest object to the player
if (distance == -1 || distanceFrom(object) < distance) {
closestObject = object;
distance = distanceFrom(object);
}
}
}
}
// Increment the animation
updateAnimation();
}
/**
* Advances the current animation based on the time since the last render
* The animation frame of the player can be grabbed with getCurrentFrame
*/
public void updateAnimation() {
stateTime += Gdx.graphics.getDeltaTime();
// Set the current frame of the animation
// Show a different animation if the player is moving vs idling
if (moving) {
currentFrame = walkingAnimation.get(direction).getKeyFrame(stateTime);
} else {
currentFrame = idleAnimation.get(direction).getKeyFrame(stateTime);
}
}
/**
* Returns whether the player's eventHitbox overlaps an object
* Call getClosestObject to get the nearest
*
* @return true if a player is near enough an object to interact with it
*/
public boolean nearObject() {
return closestObject != null;
}
/**
* Returns the object that is closest to the player, calculated during move()
*
* @return A GameObject that is closest
*/
public GameObject getClosestObject() {
return closestObject;
}
/**
* Returns if the player is moving or not
*
* @return true if the player is moving
*/
public boolean isMoving() {
return moving;
}
/**
* Sets the player's state to moving or not moving, a not moving character will just display an idle animation
*
* @param moving The boolean to set moving to
*/
public void setMoving(boolean moving) {
this.moving = moving;
}
/**
* Returns the current frame the player's animation is on
*
* @return TextureRegion the frame of the player's animation
*/
public TextureRegion getCurrentFrame() {
// Returns the current frame the player animation is on
return currentFrame;
}
/**
* Sets the objects the player cannot move into as an Array of GameObjects
*
* @param collidables An array of GameObjects that the player should collide with
*/
public void setCollidables(Array<GameObject> collidables) {
this.collidables = collidables;
}
/**
* Adds a GameObeject to the player's list of collidable objects
*
* @param object a GameObject for the player to collide with
*/
public void addCollidable(GameObject object) {
this.collidables.add(object);
}
/**
* @return The X coordinate of the player
*/
public float getX() {
return sprite.getX();
}
/**
* @return The Y coordinate of the player
*/
public float getY() {
return sprite.getY();
}
/**
* @return The X coordinate of the centre point of the player's sprite rectangle
*/
public float getCentreX() {
return centreX;
}
/**
* @return The Y coordinate of the centre point of the player's sprite rectangle
*/
public float getCentreY() {
return centreY;
}
/**
* @return The Vector3 representation of the bottom left corner of the player's sprite hitbox
*/
public Vector3 getPosAsVec3() {
return new Vector3(
sprite.getX(),
sprite.getY(),
0
);
}
/**
* Sets the x coordinate of the player, updating all 3 hitboxes at once as opposed to just the sprite rectangle
*/
public void setX(float x) {
this.sprite.setX(x);
this.feet.setX(x + 4 * scale);
this.eventHitbox.setX(this.sprite.getX() - (this.eventHitbox.getWidth() - sprite.getWidth()) / 2);
this.recalcCentre();
}
/**
* Sets the Y coordinate of the player, updating all 3 hitboxes at once as opposed to just the sprite rectangle
*/
public void setY(float y) {
this.sprite.setY(y);
this.feet.setY(y);
this.eventHitbox.setY(this.sprite.getY() - (this.eventHitbox.getHeight() - sprite.getHeight()) / 2);
this.recalcCentre();
}
/**
* @param x The X coordinate to set the player to
* @param y The Y coordinate to set the player to
*/
public void setPos(float x, float y) {
this.setX(x);
this.setY(y);
}
/**
* Set a large rectangle that the player should be kept inside, set to null to set no bounds
*
* @param bounds The bounds of the playable map
*/
public void setBounds(Rectangle bounds) {
// Set a rectangle that the player should not leave
this.bounds = bounds;
}
/**
* Returns the euclidian distance from a GameObject to the centre of the player
*
* @param object The object to get the distance from
* @return The distance from the object
*/
public float distanceFrom(GameObject object) {
return (float) Math.sqrt((Math.pow((centreX - object.centreX), 2) + Math.pow((centreY - object.centreY), 2)));
}
/**
* Recalculates the centre of the player, useful after moving the player
*/
private void recalcCentre() {
centreX = sprite.getX() + sprite.getWidth() / 2;
centreY = sprite.getY() + sprite.getHeight() / 2;
}
/**
* Sets the player to frozen, a frozen player can be set to ignore keyboard inputs in render
*
* @param freeze The value to set frozen to
*/
public void setFrozen(boolean freeze) {
this.frozen = freeze;
if (freeze) {
// Set to non-moving frame
currentFrame = idleAnimation.get(direction).getKeyFrame(stateTime);
}
}
/**
* @return true if the player is frozen
*/
public boolean isFrozen() {
return this.frozen;
}
}