0
votes

I'm struggling with collision between the collision rectangles of my player or enemy and the level/wall.

The level is a 1D array.

This draws well: a screen with a platform and a box around the level, so the player and enemy shoudn't be able to leave the arena.

In my Game_Manager class (which handles the game, it's updates and the collision) draws the level, the player and one enemy. Works well, but the player and enemy can leave the area, because the collision detection is not right:

In my Game_Manager class the player detects the enemy and it's position is reset when they collide:

          if(player.PlayerCollRect.IntersectsWith(enemy.enemyCollRect))
        {
            player.playerPosition = playerStartPosition;
        }

Again: works well.

But: I can't seem to get the collision with the 1D array (my level) right. And I seem to have a problem with getting the collision rectangle of each block/tile in my level - my player can get out, and so can my enemy.

The enemy is pink, the player green. The red block isn't used right now: See here what my level looks like and whats happening (.gif)

I've tried:

player.PlayerCollRect.IntersectsWith(enemy.EnemyCollRect)

Should I try to collide with my tiles? Or with my complete level? And how?

My code for Level.cs

public abstract class Level
{
    protected byte[,] byteArray;
    public Tile[,] tileArray;

    public Rectangle levelTileColl;

    public Level()
    {
        CreateTileArray();
        tileArray = new Tile[byteArray.GetLength(0), byteArray.GetLength(1)];

        CreateWorld();
    }

    protected abstract void CreateTileArray();

    private void CreateWorld()
    {
        for (int r = 0; r < byteArray.GetLength(0); r++)
        {
            for (int c = 0; c < byteArray.GetLength(1); c++)
            {
                if(byteArray[r, c] == 1)
                {
                    tileArray[r, c] = new Tile(new Point(c * 40, r * 40));
                    //tileArray[r, c] = new Tile(new Point(c * tile.TileWidth, r * tile.TileHeight));
                }
            }
        }
    }

    public void Draw(Surface showTiles)
    {
        for (int r = 0; r < byteArray.GetLength(0); r++)
        {
            for (int c = 0; c < byteArray.GetLength(1); c++)
            {
                if (tileArray[r, c] != null)
                    tileArray[r, c].Draw(showTiles);
            }
        }
    }
}

Code for Level01.cs:

    public class Level01 : Level
{
    protected override void CreateTileArray()
    {
        byteArray = new Byte[,]
        {
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
            { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
        };
    }
}

Code for Tile.cs:

public class Tile
{
    private Surface tileImage;
    private Point tilePosition;

    public Rectangle tileColl;

    private int tileWidth = 40;
    private int tileHeight = 40;

    public int TileWidth
    {
        get { return tileWidth; }
        set { tileWidth = value; }
    }

    public int TileHeight
    {
        get { return tileHeight; }
    }

    public Rectangle TileColl
    {
        get { return tileColl; }
        set { tileColl = value; }
    }

    public Tile(Point position)
    {
        tileImage = new Surface("tile.png");
        tilePosition = position;
        tileColl = new Rectangle(tilePosition.X, tilePosition.Y, tileWidth, tileHeight);
    }

    public void Draw(Surface showTiles)
    {
        showTiles.Blit(tileImage, tilePosition);
    }
}

-- edit -- Okay, let me state it differently: How do I check if my player touches/collides/intersectswith a tile in my tileArray in my Level? I have a tileArray and I want to check collision with any tile which is 1 (see above) if there is collision.

3

3 Answers

0
votes

In your manager class there should be a loop that constanlty updates the game, which means the levels, the player, the enemies, projectiles, ... just anything that could undergo any change during the game. I know you use the Thick event from the library you use.

Instead of creating and updating everything in the manager, it is better to do everything that belongs in the level in there. So for example in your manager instead of:

Level level = new Level();
Sprite player = new Player();
Sprite enemy = new Enemy();
Projectile[] projectiles =  new... (and then filled with projectiles)

private void Events_Tick(object sender, TickEventArgs e){
    player.update();
    enemy.update();
    projectiles.update();
    ...
}

Since the sprites (lets leave behind the projectiles) have to interact with tiles in the level, it can be a bit messy to let them interact this way. To make this simpler and more clean, you should do this in the level class where the tileArray is stored. This makes it easy to interact it with and to update everything that belongs to the level.

So in your level class:

public abstract class Level
{
    protected byte[,] byteArray;
    public Tile[,] tileArray;

    Sprite player = new Player();
    Sprite enemy = new Enemy();

    public Level()
    {
        CreateTileArray();
        tileArray = new Tile[byteArray.GetLength(0), byteArray.GetLength(1)];

        CreateWorld();
    }

    protected abstract void CreateTileArray();
    private void CreateWorld() {...}
    public void Draw(Surface showTiles) {...}

    public void Update() {
        player.Update();
        enemy.Update();
    }
}

From this point it's easier for the sprites to interact with the level and everything in it. Then in your manager class you can do, and keep it clean like this:

Level level = new Level();

private void Events_Tick(object sender, TickEventArgs e){
    level.Update();
}

So in order to check for a collision, we have to compare the coördinates of the sprite to those of the tiles. In this case, since you use the function from the library: 'InteractWith', you have to create collision rectangles because it won't work with your custom object 'Tile'. This means we gotta take the coördinates from all tiles in the 'tileArray' and put them in rectangles. Let's call the new array containing the rectangles 'tileCollisionArray'.

Like this in your level class:

public Tile[,] tileArray;
public Rectangle[,] tileCollisionArray;

public Level()
    {
        CreateTileArray();
        tileArray = new Tile[byteArray.GetLength(0), byteArray.GetLength(1)];
        tileCollisionArray = new Rectangle[tileArray.GetLength(0), tileArray.GetLength(1)];

        CreateWorld();

        //here I convert the coördinates from the tiles to the collision rectangles
        for (int x = 0; x < tileArray.GetLength(0); x++) {
            for (int y = 0; y < tileArray.GetLength(1); y++) {
                tileCollisionArray[x,y] = new Rectangle(tileArray[x,y].GetPosX, tileArray[x,y].GetPosY, tileArray[x,y].GetTileWidth, tileArray[x,y].GetTileHeight);
        }
    }

!!!Make sure you make getters in the Tile class so we have access to these coördinates and dimensions.!!!

So now we made it possible for using this InteractWith function which makes collision much easier. Next we have to make sure sprites can collide with tiles, so they need to be linked somehow.

There are again multiple ways to do this, but since we might have multiple sprites in the level that can collide with the tiles, the best practice is to check for collision in the sprite itself. According DRY, it's good to have an abstract class Sprite where the player and enemy inherit from. So when I check for collision in a method in the abstract superclass, then all subclasses will also have the ability to check for collision.

Goes like this:

public abstract class Sprite {
    public void CheckCollisionSurrounding(Rectangle[,] collisionObjects) { ... }
}

public class Player: Sprite { ... }
public class Enemy: Sprite { ... }

So that we can do next in the level class:

public abstract class Level {
    ...
    public void Update() {
        player.Update();
        player.CheckCollisionSurrounding(tileCollisionArray);
        enemy.Update();
        enemy.CheckCollisionSurrounding(tileCollisionArray);
    }
    ...
}

So this is how the sprite can get access to the collision objects, next is how to check if the collision is true. We already created the function CheckCollisionSurrounding in the Sprite class, so this is where we will check the actual collision.

(A better practice is to make this method protected and put it in the Update function of the sprite itself, but then you'll need to pass the tileCollisionArray to the sprite through injection or constructor. But if this above works, this step should be easy.)

So next, and the last step, we can do:

public abstract class Sprite {

    public void CheckCollisionSurrounding(Rectangle[,] collisionObjects) {

        for (int x = 0; x < collisionObjects.GetLength(0); x++)
        {
            for (int y = 0; y < collisionObjects.GetLength(1); y++)
            {
                if (collisionObjects[x, y] != null) //saves us from possible null reference exceptions
                {
                 //so now we iterate through every collision object so see if the sprite collides with it and call an action to it if it does collide.

                    if(PlayerCollRect.IntersectsWithTop(collisionObjects[x,y].tileCollRect)) {
                        //whatever you want to happen, but let's say you want the sprite at the position of the object when colliding with it:
                        playerPosition.y = collisionObjects[x,y].GetPosY;
                    }
                    //and so on for InteractWithBottom, -Right and -Left
                    //also keep in to account the dimensions of your own sprite which I haven't here. If the sprites hits left side of tile with it's right side, you have to add the width of the sprite to the calculation.
                }
            }
        }

    }

}

This last piece of code is not really clean and quite hectic, so if it works this way it's good to do some refactoring.

Normally this is how it should work and keeps your code clean and understandable. I hope it helps.

EDIT: I see you question now on your edit. You already take care of interacting only with the indexes that contain 1. It's in your tileArray; if in the byteArray is a 1 on that index, add a tile to the tileArray. And so all the bytes that contain a 0, aren't used any further from there on out.

0
votes

Assuming that the level does not move, only the players does, you can actually do collision by the player X and Y coordinates. This makes it possible to make a basically infinitely-sized level without lag caused by having to test collision for every single tile.

What you do is test collision only in tiles that are immediately adjacent to the player. As your player is as big as each block, that gives nine tiles to test collisions for (three above the player, three at the same level as the player, and three below the player).

I don't know how your player X and Y are stored, but let's say it's in a Player class under xPos and yPos. For all the tiles, tile width is 40 and tile height is 40, but in case you want to change it, declare allWidth to be 40 and allHeight to be 40. Now let's set up the collision tests:

for (int x = Math.Floor(player.xPos/allWidth) - 1; x < Math.Floor(player.xPos/allWidth) + 2; x++){
    for (int y = Math.Floor(player.yPos/allHeight) - 1; y < Math.Floor(player.xPos/allHeight) + 2; y++){
        if (byteArray[x, y] > 0)
            player.playerPosition = player.StartPosition;
    }
}

So what did I just do there?

Math.Floor(player.xPos/allWidth): Let's say that your player is at an X of 62. Math.Floor(player.xPos/allWidth) = Math.Floor(62/40) = 1. So with the for loop, this means that we are looking at byteArray[0,y], byteArray[1,y], and byteArray[2,y]. Now let's say your player Y is at 152. Then Math.Floor(player.yPos/allHeight) = Math.Floor(152/40) = 3. Now with the loop, we are only looking to collide with byteArray[0,2], byteArray[0,3], byteArray[0,4], byteArray[1,2], byteArray[1,3], byteArray[1,4], byteArray[2,2], byteArray[2,3], and byteArray[2,4].

Basically, your character's X and Y are converted into X and Y places on the byteArray by rounding down and dividing by the tile size: player X of between 0 and 39 is 0, player X of between 40 and 79 is 1, player X of between 80 and 119 is 2, and so on. Then the nine tiles surrounding your player are nothing more than numbers - just check if the corresponding byteArray[x,y] > 0, and if so, then your player needs to go back to their starting position.

I hope this was explained well enough!

0
votes

Thanks for the input. I've used Rectangle1.InteractsWith(Reactangle2) and it works fine. Also my player is contantly falling and when it interacts with Rectangle2, it gets it's Position.Y coordinate and therefore get onto the Rectangle.

Thanks!