SdlDotNet 平台游戏:Collision Rectangle A(玩家)与 Collision Rectangle B(关卡)相交

SdlDotNet Platformer Game: Collision Rectangle A (player) ntersectswith Collision Rectangle B (level)

我正在努力解决我的玩家或敌人的碰撞矩形与 level/wall 之间的碰撞问题。

关卡是一维数组。

这画得很好:一个带有平台的屏幕和关卡周围的一个盒子,所以玩家和敌人应该不能离开竞技场。

在我的 Game_Manager class(处理游戏、更新和碰撞)中绘制关卡、玩家和一个敌人。效果很好,但玩家和敌人可以离开该区域,因为碰撞检测不正确:

在我的 Game_Manager class 中,玩家检测到敌人并在他们碰撞时重置其位置:

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

同样:效果很好。

但是:我似乎无法正确地与一维数组(我的关卡)发生碰撞。 而且我似乎在获取关卡中每个 block/tile 的碰撞矩形时遇到问题 - 我的玩家可以出去,我的敌人也可以。

敌人是粉红色的,玩家是绿色的。 现在不使用红色块: See here what my level looks like and whats happening (.gif)

我试过:

player.PlayerCollRect.IntersectsWith(enemy.EnemyCollRect)

我应该尝试与我的瓷砖碰撞吗?还是以我的完整水平?又如何?

我的代码 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);
            }
        }
    }
}

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 },
        };
    }
}

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);
    }
}

--编辑-- 好吧,让我换个说法: 我如何检查我的玩家是否 touches/collides/intersectswith 我关卡中 tileArray 中的一个图块? 我有一个 tileArray,如果有碰撞,我想检查与任何 1(见上文)的瓷砖的碰撞。

假设关卡不移动,只有玩家移动,实际上可以通过玩家的X、Y坐标进行碰撞。这使得制作基本上无限大小的关卡成为可能,而不会因必须为每个图块测试碰撞而导致延迟。

您要做的是仅在紧邻玩家的方块中测试碰撞。由于您的玩家与每个方块一样大,因此提供了九个方块来测试碰撞(三个在玩家上方,三个与玩家相同,三个在玩家下方)。

我不知道你的播放器 X 和 Y 是如何存储的,但假设它在 xPos 和 yPos 下的播放器 class 中。对于所有瓷砖,瓷砖宽度为 40,瓷砖高度为 40,但如果您想更改它,请将 allWidth 声明为 40,将 allHeight 声明为 40。现在让我们设置碰撞测试:

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;
    }
}

那我刚才做了什么?

Math.Floor(player.xPos/allWidth):假设你的玩家的 X 为 62。Math.Floor(player.xPos/allWidth) = Math.Floor(62/40) = 1。因此对于 for 循环,这意味着我们正在查看 byteArray[0,y]、byteArray[1,y] 和 byteArray[2,y]。现在假设你的玩家 Y 在 152。然后 Math.Floor(player.yPos/allHeight) = Math.Floor(152/40) = 3。现在有了循环,我们只想碰撞与 byteArray[0,2], byteArray[0,3], byteArray[0,4], byteArray[1,2], byteArray[1,3], byteArray[1,4], byteArray[2,2], byteArray[2,3] 和 byteArray[2,4].

基本上,您的角色的 X 和 Y 通过四舍五入并除以图块大小转换为 byteArray 上的 X 和 Y 位置:0 到 39 之间的玩家 X 为 0,40 到 79 之间的玩家 X 为1、80到119之间的选手X为2,以此类推。然后你的玩家周围的九个方块只不过是数字 - 只需检查相应的 byteArray[x,y] > 0,如果是,那么你的玩家需要回到他们的起始位置。

我希望这个解释足够好!

在你的管理器中 class 应该有一个不断更新游戏的循环,这意味着关卡、玩家、敌人、射弹……任何在游戏过程中可能发生变化的东西.我知道您使用您使用的库中的 Thick 事件。

与其在管理器中创建和更新所有内容,不如在其中执行属于级别的所有内容。因此,例如在您的经理中而不是:

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();
    ...
}

由于精灵(让抛射物留在后面)必须与关卡中的图块交互,因此让它们以这种方式交互可能会有点混乱。为了使它更简单和更干净,您应该在存储 tileArray 的级别 class 中执行此操作。这使得与其交互和更新属于关卡的所有内容变得容易。

所以在你的水平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();
    }
}

从这一点来看,精灵可以更轻松地与关卡及其中的所有内容进行交互。然后在你的经理 class 你可以这样做,并像这样保持干净:

Level level = new Level();

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

因此,为了检查碰撞,我们必须将精灵的坐标与图块的坐标进行比较。在这种情况下,由于您使用库中的函数:'InteractWith',您必须创建碰撞矩形,因为它不适用于您的自定义对象 'Tile'。这意味着我们必须从 'tileArray' 中的所有图块中获取坐标并将它们放在矩形中。让我们调用包含矩形的新数组 'tileCollisionArray'.

在你的关卡中这样 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);
        }
    }

!!!确保您在 Tile class 中创建吸气剂,以便我们可以访问这些坐标和维度。!!!

所以现在我们可以使用这个 InteractWith 函数,这使得碰撞更容易。接下来我们必须确保 sprite 可以与 tiles 碰撞,所以它们需要以某种方式链接起来。

同样有多种方法可以做到这一点,但由于关卡中可能有多个 sprite 会与图块发生碰撞,因此最佳做法是检查 sprite 本身是否发生碰撞。根据 DRY,有一个抽象的 class Sprite 是很好的,玩家和敌人从中继承。所以当我在抽象 superclass 中的一个方法中检查碰撞时,那么所有的 subclass 也将具有检查碰撞的能力。

是这样的:

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

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

以便我们可以在class关卡中进行下一步:

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

这就是精灵如何访问碰撞对象,接下来是如何检查碰撞是否为真。我们已经在 Sprite class 中创建了函数 CheckCollisionSurrounding,因此我们将在此处检查实际碰撞。

(更好的做法是保护此方法并将其放在精灵本身的更新函数中,但是您需要通过注入或构造函数将 tileCollisionArray 传递给精灵。但是如果上面的方法有效, 这一步应该很简单。)

接下来,也是最后一步,我们可以做:

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.
                }
            }
        }

    }

}

最后一段代码不是很干净而且很忙,所以如果它以这种方式工作,最好进行一些重构。

通常这是它应该如何工作并使您的代码干净易懂。希望对你有帮助。

编辑:我看到你现在对你的编辑有疑问。您已经注意只与包含 1 的索引进行交互。它在您的 tileArray 中;如果 byteArray 中该索引为 1,则向 tileArray 添加一个 tile。因此,所有包含 0 的字节都不再使用。

感谢您的投入。我用过 Rectangle1.InteractsWith(Reactangle2) 并且效果很好。 此外,我的播放器不断下落,当它与 Rectangle2 交互时,它得到它的 Position.Y 坐标,因此进入矩形。

谢谢!