Monday, March 6, 2017

java - Collision detection in libgdx


I am currently writing a little zelda like game and have a little bit of a problem with collision detection.


It works but the the character stands to far away from the object.


Here is my player class:


public class Player extends Sprite {


/** the movement velocity */
private Vector2 velocity = new Vector2();


private float speed = 60 * 2, gravity = 6 * 1.8f, animationTime = 0;

private Animation still, left, right, up , down;
private TiledMapTileLayer collisionLayer;

private String blockedKey = "blocked";

public Player(Animation still, Animation left, Animation right,
Animation up, Animation down, TiledMapTileLayer collisionLayer){

super(still.getKeyFrame(0));
this.still = still;
this.left = left;
this.right = right;
this.up = up;
this.down = down;
this.collisionLayer = collisionLayer;
}

public void draw(SpriteBatch spriteBatch){

update(Gdx.graphics.getDeltaTime());
super.draw(spriteBatch);
}

public void update(float delta){

//save old position
float oldX = getX(), oldY = getY();
boolean collisionX = false, collisionY = false;


//move on x
setX(getX() + velocity.x * delta);

if(velocity.x < 0) // going left
collisionX = collidesLeft();
else if(velocity.x > 0) // going right
collisionX = collidesRight();

//react to x collision
if(collisionX){

setX(oldX);
velocity.x = 0;
}

//move on y
setY(getY() + velocity.y * delta);

if(velocity.y < 0) // going down
collisionY = collidesBottom();
else if(velocity.y > 0) // going up

collisionY = collidesTop();

//react to y collision
if(collisionY){
setY(oldY);
velocity.y = 0;
}

// update animation
animationTime += delta;

setRegion(velocity.x < 0 ? left.getKeyFrame(animationTime) : velocity.x > 0 ? right.getKeyFrame(animationTime) : velocity.y > 0 ? up.getKeyFrame(animationTime) : velocity.y < 0 ? down.getKeyFrame(animationTime) : still.getKeyFrame(animationTime));
if(velocity.x == 0 && velocity.y == 0) animationTime = 0;

}

private boolean isCellBlocked(float x, float y) {
Cell cell = collisionLayer.getCell((int) (x / collisionLayer.getTileWidth()), (int) (y / collisionLayer.getTileHeight()));
return cell != null && cell.getTile() != null && cell.getTile().getProperties().containsKey(blockedKey);
}


public boolean collidesRight() {
for(float step = 0; step < getHeight(); step += collisionLayer.getTileHeight() / 2)
if(isCellBlocked(getX() + getWidth() , getY() + step))
return true;
return false;
}

public boolean collidesLeft() {
for(float step = 0; step < getHeight(); step += collisionLayer.getTileHeight() / 2)
if(isCellBlocked(getX(), getY() + step))

return true;
return false;
}

public boolean collidesTop() {
for(float step = 0; step < getWidth(); step += collisionLayer.getTileWidth() / 2)
if(isCellBlocked(getX() + step, getY() + getHeight()))
return true;
return false;
}


public boolean collidesBottom() {
for(float step = 0; step < getWidth(); step += collisionLayer.getTileWidth() / 2)
if(isCellBlocked(getX() + step, getY()))
return true;
return false;
}

here you can see the collision


I guess it has something to do with the sprite sheet. Here is my Spritesheet:



enter image description here


Here is the pack file for the spritesheet:



linksprite.png format: RGBA8888 filter: Nearest, Nearest repeat: none stillup rotate: false xy: 0, 0 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 still rotate: false xy: 0, 64 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 stillleft rotate: false xy: 0, 32 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 up rotate: false xy: 0, 0 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 up rotate: false xy: 32, 0 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 up rotate: false xy: 64, 0 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 up rotate: false xy: 96, 0 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 up rotate: false xy: 128, 0 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 up rotate: false xy: 160, 0 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 left rotate: false xy: 0, 32 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 left rotate: false xy: 32, 32 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 left rotate: false xy: 64, 32 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 left rotate: false xy: 96, 32 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 left rotate: false xy: 128, 32 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 left rotate: false xy: 160, 32 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 down rotate: false xy: 0, 64 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 down rotate: false xy: 32, 64 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 down rotate: false xy: 64, 64 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 down rotate: false xy: 96, 64 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 down rotate: false xy: 128, 64 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 down rotate: false xy: 160, 64 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 right rotate: false xy: 0, 96 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 right rotate: false xy: 32, 96 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 right rotate: false xy: 64, 96 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 right rotate: false xy: 96, 96 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 right rotate: false xy: 128, 96 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1 right rotate: false xy: 160, 96 size: 32, 32 orig: 16, 16 offset: 0, 0 index: -1




Answer



I haven't read into your code too much but the collision logic appears to be flawed. Imagine a projectile moving at over 200 pixels every updates. The projectile will just fly right through everything.


In order to fix this problem, loop from 0 to the amount of pixels to move in whichever direction you entity is moving towards. On top of that, you need to loop from 0 to [HEIGHT_OF_ENTITY] for horizontal movement and WIDTH_OF_ENTITY for vertical movement, to ensure that the entity does not get stuck inside blocked tiles. This will ensure correctly working collisions in any scenario, and should never fail.


Here is some old code I wrote a few months back. I cleaned it a little bit and documented it so it would be easier to understand.


It's my base collision code, I rewrite this every time I want tile based collisions and it works perfectly.



It's in C# but it should be very easy to understand for you since Java's syntax is almost exactly the same.


public bool resolveCollisions() {
if (xa == 0 && ya == 0) return false;
if (xa != 0 && ya != 0) throw new ArgumentException("Can only resolve collisions along one axis at a time");

if (xa != 0) { //horizontal movement
if (xa < 0) { //movement towards the left
for (int i = 0; i > xa; i--) {
for (int j = OffsetY; j < OffsetY + Height; j++) { //OffsetX and OffsetY is in case you want to adjust the hitbox of your entity a bit
Vector2 cVec = new Vector2(Location.X + OffsetX + i, Location.Y + j) / Tile.Size;

Tile t = level.GetTile((int) cVec.X, (int) cVec.Y);
if (t.OnCollide(this)) { //t.OnCollide is the tile's handling when collided with an entity. (ie, damage if spikes etc.)
Velocity.X = i + 1;
return true; //COLLISION!
}
}
}
} else if (xa > 0) { //movement towards the right
for (int i = 0; i < xa; i++) {
for (int j = OffsetY; j < OffsetY + Height; j++) { //OffsetX and OffsetY is in case you want to adjust the hitbox of your entity a bit

Vector2 cVec = new Vector2(Location.X + OffsetX + Width + i, Location.Y + j) / Tile.Size;
Tile t = level.GetTile((int) cVec.X, (int) cVec.Y);
if (t.OnCollide(this)) { //t.OnCollide is the tile's handling when collided with an entity. (ie, damage if spikes etc.)
Velocity.X = i; //stop movement!
return true; //COLLISION!
}
}
}
}
return false;

} else if (ya != 0) { //vertical movement
if (ya < 0) { //up movement
for (int i = 0; i > ya; i--) {
for (int j = OffsetX; j < OffsetX + Width; j++) { //OffsetX and OffsetY is in case you want to adjust the hitbox of your entity a bit
Vector2 cVec = new Vector2(Location.X + j, Location.Y + OffsetY + i) / Tile.Size;
Tile t = level.GetTile((int) cVec.X, (int) cVec.Y);
if (t.OnCollide(this)) { //t.OnCollide is the tile's handling when collided with an entity. (ie, damage if spikes etc.)
Velocity.Y = i + 1;
return true; //COLLISION!
}

}
}
InAir = true; //A boolean which indicts that the entity is not on ground!
} else if (ya > 0) { //down movement
for (int i = 0; i < ya; i++) {
for (int j = OffsetX; j < OffsetX + Width; j++) { //OffsetX and OffsetY is in case you want to adjust the hitbox of your entity a bit
Vector2 cVec = new Vector2((int) (Location.X + j), (int) (Location.Y + OffsetY + Height + i)) / Tile.Size;
Tile t = level.GetTile((int) cVec.X, (int) cVec.Y);
if (t.OnCollide(this)) { //t.OnCollide is the tile's handling when collided with an entity. (ie, damage if spikes etc.)
Velocity.Y = i;

if (InAir) //Is the player in the airs?
OnLand(); //Entity is landing, make sure you set InAir to false inside this method
return true; //COLLISION!
}
}
}
}
return false;
} else {
//sanity check

throw new ArgumentException("Can only resolve collisions along one axis at a time");
}
}

Plug that method into your entity class and then these two calls into your update method


resolveCollisions(velocity.x, 0);
resolveCollisions(0, velocity.y);

The code loops complicated and very math/logic intensive but it really isn't and is fairly straightforward once you get the meaning behind it. The same logic is repeated 4 times essentially. (4 different directions)


A few clarifications





  • This code was written for a platformer, with gravity. But it can easily be edited to fit the needs of a top-down view game.




  • OffsetX and OffsetY are variables to help define a better hitbox in conjunction of the Width and Height variables




  • The tile classes should have a method OnCollide with a parameter of type Entity that will return true if this tile should block passage or false if not. You can also add special handling like damaging the entity if it's spikes or whatever.





  • I'm not entirely sure why you shouldn't check collisions along both x and y axis at the same time but I remember I had problems when doing it that way, so I only check along x and y, separately. You can't check along x-axis and y-axis both in the same method call because we can't return two booleans, duh! (Unless we returned a Tuple of two booleans of course)




No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...