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

