解决简易TypeScript游戏引擎中的玩家碰撞异常问题
解决方案:修复玩家碰撞单向推动问题
问题核心是按顺序更新对象+仅修正被碰撞对象位置导致的:先处理的玩家移动后碰撞到后处理玩家时,会修改后者位置,但后者处理时前者已在新位置,无法反向修正前者,最终出现单向推动。要实现碰撞时双方均停止,需要调整碰撞处理的流程,确保碰撞对被统一处理,且双方位置都被修正。
核心思路
- 分离位置计算与更新:先计算所有玩家的目标位置(不实际修改),避免顺序更新带来的干扰。
- 唯一处理每对碰撞:避免同一碰撞对被重复处理(A→B和B→A各处理一次)。
- 同时修正双方位置:碰撞时要么让双方回到碰撞前的位置(实现“立即停止”),要么各退一半重叠量(更自然的碰撞反馈)。
代码实现步骤
1. 修改玩家位置更新逻辑
不再直接修改当前位置,而是计算并存储目标位置:
// Player类的update方法 update(deltaTime: number) { // moveX/moveY为玩家输入方向(-1/0/1) const moveX = this.input.getHorizontal(); const moveY = this.input.getVertical(); // 计算目标位置,暂不实际更新 this.targetPosition = { x: this.transform.position().x + moveX * Player.speed * deltaTime, y: this.transform.position().y + moveY * Player.speed * deltaTime }; }
2. 全局碰撞处理流程
在引擎的update函数中,分三步处理:
// 引擎全局update函数 function engineUpdate(deltaTime: number) { // 第一步:收集所有对象的目标位置 const objectTargets = new Map<GameObject, {x: number, y: number}>(); gameObjects.forEach(obj => { obj.update(deltaTime); objectTargets.set(obj, {...obj.targetPosition}); }); // 第二步:处理所有碰撞对,避免重复处理 const processedPairs = new Set<string>(); for (let i = 0; i < gameObjects.length; i++) { const objA = gameObjects[i]; for (let j = i + 1; j < gameObjects.length; j++) { const objB = gameObjects[j]; // 生成唯一键标记已处理的碰撞对 const pairKey = `${Math.min(objA.id, objB.id)}-${Math.max(objA.id, objB.id)}`; if (processedPairs.has(pairKey)) continue; processedPairs.add(pairKey); // 检测目标位置是否碰撞 const posA = objectTargets.get(objA)!; const posB = objectTargets.get(objB)!; if (isColliding(objA, posA, objB, posB)) { // 方案1:碰撞时双方立即停止,回到上一帧位置 objectTargets.set(objA, {...objA.transform.position()}); objectTargets.set(objB, {...objB.transform.position()}); // 方案2:碰撞时双方各退一半重叠量(更自然) // const overlap = calculateOverlap(objA, posA, objB, posB); // adjustPositions(objA, objB, objectTargets, overlap); } } } // 第三步:统一更新所有对象的位置 gameObjects.forEach(obj => { const target = objectTargets.get(obj)!; obj.transform.updateX(target.x); obj.transform.updateY(target.y); }); }
3. 辅助函数实现
// 轴对齐碰撞检测 function isColliding(objA: GameObject, posA: {x: number, y: number}, objB: GameObject, posB: {x: number, y: number}): boolean { const aRight = posA.x + objA.getSize().width; const aBottom = posA.y + objA.getSize().height; const bRight = posB.x + objB.getSize().width; const bBottom = posB.y + objB.getSize().height; return posA.x < bRight && aRight > posB.x && posA.y < bBottom && aBottom > posB.y; } // 计算碰撞重叠量(与原代码逻辑一致) function calculateOverlap(objA: GameObject, posA: {x: number, y: number}, objB: GameObject, posB: {x: number, y: number}) { return { left: posA.x - (posB.x + objB.getSize().width), top: posA.y - (posB.y + objB.getSize().height), right: (posA.x + objA.getSize().width) - posB.x, bottom: (posA.y + objA.getSize().height) - posB.y, }; } // 方案2:调整双方位置各退一半重叠量 function adjustPositions(objA: GameObject, objB: GameObject, targets: Map<GameObject, {x: number, y: number}>, overlap: any) { const absOverlap = { left: Math.abs(overlap.left), top: Math.abs(overlap.top), right: Math.abs(overlap.right), bottom: Math.abs(overlap.bottom), }; const minOverlap = Math.min(...Object.values(absOverlap)); if (minOverlap === absOverlap.left) { const correction = absOverlap.left / 2; targets.get(objA)!.x += correction; targets.get(objB)!.x -= correction; } else if (minOverlap === absOverlap.top) { const correction = absOverlap.top / 2; targets.get(objA)!.y += correction; targets.get(objB)!.y -= correction; } else if (minOverlap === absOverlap.right) { const correction = absOverlap.right / 2; targets.get(objA)!.x -= correction; targets.get(objB)!.x += correction; } else if (minOverlap === absOverlap.bottom) { const correction = absOverlap.bottom / 2; targets.get(objA)!.y -= correction; targets.get(objB)!.y += correction; } }
关键说明
- 给每个
GameObject添加唯一id属性,用于生成碰撞对的唯一键,避免重复处理。 - 方案1直接让碰撞双方回到上一帧位置,完全符合“碰撞时立即停止”的需求;方案2则提供更平滑的碰撞反馈,可根据游戏风格选择。
- 这种“先计算所有目标,再处理碰撞,最后统一更新”的模式,从根源上解决了顺序更新导致的单向推动问题。
内容的提问来源于stack exchange,提问作者Sudashi
相关产品推荐
相关产品推荐

