You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

2D俯视角RPG游戏中实体速度引发的碰撞检测提前触发问题

2D俯视角RPG游戏中实体速度引发的碰撞检测提前触发问题

嘿,我看了你的问题和代码,完全懂你碰到的麻烦——角色速度一调高,明明还没碰到瓷砖就被硬生生挡住了,对吧?这问题其实出在你当前碰撞检测的逻辑上,咱们一步步拆解修复它。

问题根源分析

你的checkTile方法里,判断碰撞时直接用entity.speed偏移实体的边界坐标,再计算对应的tile行列。比如向上移动时,你直接把顶部行算成(entityTopWorldY - entity.speed)/wm.TILE_SIZE——这相当于你在检测角色移动整个speed距离后会进入的tile,而不是检测角色在移动过程中是否真的会碰到tile。

举个实际例子:假设tile大小是32像素,角色当前顶部离上方瓷砖底部只剩3像素,但角色speed设成了5。这时候你的代码会直接计算移动5像素后的顶部行,跳到上方的瓷砖,如果那是个实心tile,就直接把isColliding设为true,完全阻止角色移动。但实际上角色本来可以移动3像素到瓷砖边缘,而不是停在原地,这就造成了“碰撞提前触发”的错觉。

解决方案

我们需要把“预判整个speed距离的tile”改成精确计算角色能移动的最大距离,或者逐小步检测碰撞,确保角色能贴到瓷砖边缘,而不是提前被挡住。这里给你两种可行的修改思路:

思路1:精确计算可移动距离(推荐,效率更高)

修改碰撞检测逻辑,不再简单判断“是否会碰撞”,而是计算角色在当前方向上最多能移动多少像素而不碰到实心tile,然后只移动这个距离。

首先调整CollisionManager的方法,让它返回最大可移动距离:

public int getMaxMoveDistance(Entity entity) {
    int entityLeftWorldX = entity.worldX + entity.boundingBox.x;
    int entityRightWorldX = entityLeftWorldX + entity.boundingBox.width;
    int entityTopWorldY = entity.worldY + entity.boundingBox.y;
    int entityBottomWorldY = entityTopWorldY + entity.boundingBox.height;

    int maxDistance = entity.speed;

    switch (entity.currentDirection) {
        case UP:
            // 计算当前顶部到上方tile底部的剩余距离
            int currentTopRow = entityTopWorldY / wm.TILE_SIZE;
            int tileBottomY = currentTopRow * wm.TILE_SIZE;
            int distanceToTile = entityTopWorldY - tileBottomY;
            // 检测上方tile是否实心,若是则最多移动到tile边缘
            if (isTileSolidAt(entityLeftWorldX, tileBottomY - wm.TILE_SIZE) || 
                isTileSolidAt(entityRightWorldX - 1, tileBottomY - wm.TILE_SIZE)) {
                maxDistance = Math.min(maxDistance, distanceToTile);
            }
            break;
        case DOWN:
            int currentBottomRow = entityBottomWorldY / wm.TILE_SIZE;
            int tileTopY = (currentBottomRow + 1) * wm.TILE_SIZE;
            int distanceToTileDown = tileTopY - entityBottomWorldY;
            if (isTileSolidAt(entityLeftWorldX, tileTopY) || 
                isTileSolidAt(entityRightWorldX - 1, tileTopY)) {
                maxDistance = Math.min(maxDistance, distanceToTileDown);
            }
            break;
        case LEFT:
            int currentLeftCol = entityLeftWorldX / wm.TILE_SIZE;
            int tileRightX = currentLeftCol * wm.TILE_SIZE;
            int distanceToTileLeft = entityLeftWorldX - tileRightX;
            if (isTileSolidAt(tileRightX - wm.TILE_SIZE, entityTopWorldY) || 
                isTileSolidAt(tileRightX - wm.TILE_SIZE, entityBottomWorldY - 1)) {
                maxDistance = Math.min(maxDistance, distanceToTileLeft);
            }
            break;
        case RIGHT:
            int currentRightCol = entityRightWorldX / wm.TILE_SIZE;
            int tileLeftX = (currentRightCol + 1) * wm.TILE_SIZE;
            int distanceToTileRight = tileLeftX - entityRightWorldX;
            if (isTileSolidAt(tileLeftX, entityTopWorldY) || 
                isTileSolidAt(tileLeftX, entityBottomWorldY - 1)) {
                maxDistance = Math.min(maxDistance, distanceToTileRight);
            }
            break;
    }

    return maxDistance;
}

// 辅助方法:检测指定世界坐标点是否在实心tile上
private boolean isTileSolidAt(int worldX, int worldY) {
    int col = worldX / wm.TILE_SIZE;
    int row = worldY / wm.TILE_SIZE;
    // 边界外视为不可进入的实心区域
    if (col < 0 || col >= wm.firstLayerMap.GAME_MAP.length || row < 0 || row >= wm.firstLayerMap.GAME_MAP[0].length) {
        return true;
    }
    int tileNum = wm.firstLayerMap.GAME_MAP[col][row];
    return isTileSolid(tileNum, wm.firstLayerMap);
}

然后修改Entity的updatePosition方法,用这个最大距离来移动:

private void updatePosition() {
    if(currentState == State.WALKING) {
        int maxMove = cm.getMaxMoveDistance(this);
        if(maxMove > 0) {
            switch(currentDirection) {
                case UP -> worldY -= maxMove;
                case DOWN -> worldY += maxMove;
                case LEFT -> worldX -= maxMove;
                case RIGHT -> worldX += maxMove;
            }
        }
    }
}

思路2:小步迭代检测(直观易理解)

如果觉得上面的计算有点复杂,也可以把大的speed拆成多个1像素的小步,每移动一步就检测一次碰撞,直到用完所有步数或者碰到障碍:

先修改CollisionManager的checkTile,去掉用speed偏移行列的逻辑,只检测当前位置的碰撞:

public void checkTile(Entity entity) {
    int entityLeftWorldX = entity.worldX + entity.boundingBox.x;
    int entityRightWorldX = entityLeftWorldX + entity.boundingBox.width;
    int entityTopWorldY = entity.worldY + entity.boundingBox.y;
    int entityBottomWorldY = entityTopWorldY + entity.boundingBox.height;

    int entityLeftColumn = entityLeftWorldX / wm.TILE_SIZE;
    int entityRightColumn = entityRightWorldX / wm.TILE_SIZE;
    int entityTopRow = entityTopWorldY / wm.TILE_SIZE;
    int entityBottomRow = entityBottomWorldY / wm.TILE_SIZE;

    // 移除原来用speed偏移行列的switch块

    int[] topTiles = {
            wm.firstLayerMap.GAME_MAP[entityLeftColumn][entityTopRow],
            wm.firstLayerMap.GAME_MAP[entityRightColumn][entityTopRow]
    };

    int[] bottomTiles = {
            wm.firstLayerMap.GAME_MAP[entityLeftColumn][entityBottomRow],
            wm.firstLayerMap.GAME_MAP[entityRightColumn][entityBottomRow]
    };

    entity.isColliding = isTileColliding(topTiles) || isTileColliding(bottomTiles);
}

再修改Entity的updatePosition方法:

private void updatePosition() {
    if(currentState == State.WALKING) {
        int remainingSpeed = speed;
        while(remainingSpeed > 0) {
            isColliding = false;
            cm.checkTile(this);
            if(isColliding) {
                break;
            }
            // 每次移动1像素
            switch(currentDirection) {
                case UP -> worldY -= 1;
                case DOWN -> worldY += 1;
                case LEFT -> worldX -= 1;
                case RIGHT -> worldX += 1;
            }
            remainingSpeed--;
        }
    }
}

两种思路对比

  • 思路1效率更高,适合速度较大的场景,不会因为speed高而增加太多计算量;
  • 思路2更直观,适合小项目或者调试阶段,代码改动更小。

不管选哪种,核心都是让碰撞检测精确到像素级,而不是提前预判整个speed距离后的tile位置。

备注:内容来源于stack exchange,提问作者Luca Facchini

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 10:24:34