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

解决简易TypeScript游戏引擎中的玩家碰撞异常问题

解决方案:修复玩家碰撞单向推动问题

问题核心是按顺序更新对象+仅修正被碰撞对象位置导致的:先处理的玩家移动后碰撞到后处理玩家时,会修改后者位置,但后者处理时前者已在新位置,无法反向修正前者,最终出现单向推动。要实现碰撞时双方均停止,需要调整碰撞处理的流程,确保碰撞对被统一处理,且双方位置都被修正。

核心思路

  1. 分离位置计算与更新:先计算所有玩家的目标位置(不实际修改),避免顺序更新带来的干扰。
  2. 唯一处理每对碰撞:避免同一碰撞对被重复处理(A→B和B→A各处理一次)。
  3. 同时修正双方位置:碰撞时要么让双方回到碰撞前的位置(实现“立即停止”),要么各退一半重叠量(更自然的碰撞反馈)。

代码实现步骤

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 15:02:27