2
\$\begingroup\$

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

\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

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)

\$\endgroup\$
4
  • \$\begingroup\$ But wouldnt your code make an error if I move diagonally? \$\endgroup\$
    – zanzoken
    Commented Jan 5, 2014 at 0:27
  • \$\begingroup\$ In theory, diagonal movement is an illusion. There are two axis, the x-axis and y-axis. If both your X velocity and Y velocity is 5, and you perform the check along the x-axis first, then y-axis, (or vice versa) it should work. \$\endgroup\$
    – oxysoft
    Commented Jan 5, 2014 at 0:30
  • \$\begingroup\$ True. But I have to look if my tiles have a OnCollide method. \$\endgroup\$
    – zanzoken
    Commented Jan 5, 2014 at 0:32
  • \$\begingroup\$ You can add them or if your tiles are just integers in a tile array of some sort and they don't have a class for different tiles, make a new onCollideWithTile method or something and have the tile id/integer or whatever as a parameter and then return appropriate boolean based on that. You can put handling code in there too. \$\endgroup\$
    – oxysoft
    Commented Jan 5, 2014 at 0:51

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .