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

Unity中如何终止嵌套协程?解决推箱游戏的移动终止需求

俯视解谜推箱游戏:同时终止角色与箱子移动协程的可行方案

问题描述

我正在开发一款俯视视角解谜游戏,采用网格移动机制。已实现角色移动及推箱逻辑,但希望当箱子后方存在障碍物时,同时终止角色和箱子的移动协程。目前编写了PlayerMove和Move两个协程函数,尝试用异常终止协程但无效,请问有什么可行方法?

角色移动协程原代码

private IEnumerator PlayerMove(Vector2 direction)
{
    // check for wall collisions
    if (Physics2D.Raycast(playerTransform.position, direction, raycastDistance, wallLayer).collider is not null)
    {
        Animate(Animations.CantMove);
        yield break;
    }

    // check for box collisions
    else if (Physics2D.Raycast(playerTransform.position, direction, raycastDistance, box.boxLayer).collider is not null)
    {
        if ()
    }

    // changing fields
    isMoving = true;
    isPlayersTurn = false;

    // actual moving
    float elapsedTime = 0f;

    origPos = playerTransform.position;
    targetPos = origPos + direction;

    while (elapsedTime < timeToMove)
    {
        playerTransform.position = Vector2.Lerp(origPos, targetPos, (elapsedTime / timeToMove));
        elapsedTime += Time.deltaTime;
        yield return null;
    }

    playerTransform.position = targetPos;

    // changing fields
    isMoving = false;
    isBotTurn = true;
}

箱子移动协程原代码

public IEnumerator Move(Vector2 direction, float raycastDistance, LayerMask wallLayer, float timeToMove)
{             
    // check for wall collisions
    if (Physics2D.Raycast(transform.position, direction, raycastDistance, wallLayer).collider is not null)
    {
        //do something
    }

    // actual moving
    float elapsedTime = 0f;

    Vector2 origPos, targetPos;

    origPos = transform.position;
    targetPos = origPos + direction;

    while (elapsedTime < timeToMove)
    {
        transform.position = Vector2.Lerp(origPos, targetPos, (elapsedTime / timeToMove));
        elapsedTime += Time.deltaTime;
        yield return null;
    }

    transform.position = targetPos;
}

解决方案

Unity协程不推荐用异常终止,最稳妥的方式是布尔标记+手动协程引用管理,以下是具体实现:

核心思路

  1. 给角色和箱子类添加终止标记,用于实时检测是否需要停止移动
  2. 启动箱子协程时保存引用,方便后续手动终止
  3. 移动前预检查箱子后方障碍物,移动过程中实时检测终止标记
  4. 终止后重置角色和箱子的位置,保证网格对齐

修改后的代码实现

Player类修改

private bool _shouldStopMove = false;
private Coroutine _currentBoxCoroutine;

private IEnumerator PlayerMove(Vector2 direction)
{
    _shouldStopMove = false;
    _currentBoxCoroutine = null;

    // 检测墙碰撞
    if (Physics2D.Raycast(playerTransform.position, direction, raycastDistance, wallLayer).collider != null)
    {
        Animate(Animations.CantMove);
        yield break;
    }

    // 检测箱子碰撞
    RaycastHit2D boxHit = Physics2D.Raycast(playerTransform.position, direction, raycastDistance, box.boxLayer);
    if (boxHit.collider != null)
    {
        Box targetBox = boxHit.collider.GetComponent<Box>();
        if (targetBox != null)
        {
            // 预检查箱子后方是否有障碍物,有则直接终止角色移动
            if (targetBox.CheckObstacleBehind(direction, raycastDistance, wallLayer))
            {
                Animate(Animations.CantMove);
                yield break;
            }
            // 启动箱子协程并保存引用
            _currentBoxCoroutine = StartCoroutine(targetBox.Move(direction, raycastDistance, wallLayer, timeToMove));
        }
    }

    isMoving = true;
    isPlayersTurn = false;

    float elapsedTime = 0f;
    Vector2 origPos = playerTransform.position;
    Vector2 targetPos = origPos + direction;

    // 移动循环中加入终止标记检测
    while (elapsedTime < timeToMove && !_shouldStopMove)
    {
        playerTransform.position = Vector2.Lerp(origPos, targetPos, elapsedTime / timeToMove);
        elapsedTime += Time.deltaTime;
        yield return null;
    }

    // 若因终止标记退出,重置角色位置并停止箱子协程
    if (_shouldStopMove)
    {
        playerTransform.position = origPos;
        if (_currentBoxCoroutine != null)
        {
            StopCoroutine(_currentBoxCoroutine);
            box.transform.position = box.OrigPos;
        }
    }
    else
    {
        playerTransform.position = targetPos;
    }

    isMoving = false;
    isBotTurn = true;
}

// 公开方法供箱子触发终止
public void TriggerMoveStop()
{
    _shouldStopMove = true;
}

Box类修改

private bool _shouldStopMove = false;
public Vector2 OrigPos { get; private set; }

// 预检查后方障碍物的方法
public bool CheckObstacleBehind(Vector2 direction, float raycastDistance, LayerMask wallLayer)
{
    return Physics2D.Raycast(transform.position, direction, raycastDistance, wallLayer).collider != null;
}

public IEnumerator Move(Vector2 direction, float raycastDistance, LayerMask wallLayer, float timeToMove)
{
    _shouldStopMove = false;
    OrigPos = transform.position;

    // 移动中检测到障碍物,触发角色终止
    if (CheckObstacleBehind(direction, raycastDistance, wallLayer))
    {
        Player player = FindObjectOfType<Player>();
        player?.TriggerMoveStop();
        yield break;
    }

    float elapsedTime = 0f;
    Vector2 targetPos = OrigPos + direction;

    // 箱子移动循环加入终止标记检测
    while (elapsedTime < timeToMove && !_shouldStopMove)
    {
        transform.position = Vector2.Lerp(OrigPos, targetPos, elapsedTime / timeToMove);
        elapsedTime += Time.deltaTime;
        yield return null;
    }

    // 终止时重置箱子位置
    transform.position = _shouldStopMove ? OrigPos : targetPos;
}

关键说明

  • 避免用异常终止协程:Unity协程的设计逻辑不支持可靠的异常终止,布尔标记是官方推荐的控制方式
  • 协程引用必须保存:只有持有Coroutine对象,才能调用StopCoroutine手动终止
  • 预检查+实时检查结合:移动前的预检查避免无效移动,实时检查应对动态障碍物等突发情况
  • 位置重置必须执行:终止后将角色和箱子放回移动起点,保证网格移动的准确性

内容的提问来源于stack exchange,提问作者Fase Play

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 09:33:20