初学者,自用笔记

把“要执行的动作/请求”封装成一个对象(Command),从而让你能把请求的发起者(Invoker)和实际执行者(Receiver)解耦,并且支持:排队、延迟执行、撤销/重做、录制回放、网络同步、宏命令等

  • Command(命令接口/抽象类):定义统一执行入口(如 Execute(),可选 Undo())。
  • ConcreteCommand(具体命令):持有执行者 Receiver 及参数,实现具体执行/撤销逻辑。
  • Receiver(接收者):真正干活的对象(角色控制器、背包系统、战斗系统等)。
  • Invoker(调用者/触发者):保存命令并在合适时机触发(输入系统、UI按钮、回放系统、网络消息处理等)。
  • (可选)CommandQueue/History:命令队列与历史栈,用于排队与 Undo/Redo。

基本命令模式

指令接口

public interface ICommand
{
    // 执行命令
    void Execute();
    
    // 撤销命令(可选,用于需要撤销功能的场景)
    void Undo();
}

接收者

public class CommandReceiver : MonoBehaviour
{
    // 示例:移动接收者
    public void Move(Vector3 direction, float distance)
    {
        transform.Translate(direction.normalized * distance);
        Debug.Log($"移动: {direction} 距离: {distance}");
    }
    
    // 示例:旋转接收者
    public void Rotate(Vector3 axis, float angle)
    {
        transform.Rotate(axis, angle);
        Debug.Log($"旋转: 轴 {axis} 角度 {angle}");
    }
    
    // 示例:缩放接收者
    public void Scale(Vector3 scaleFactor)
    {
        transform.localScale *= scaleFactor;
        Debug.Log($"缩放: 因子 {scaleFactor}");
    }
}

移动指令

public class MoveCommand : ICommand
{
    private CommandReceiver receiver;
    private Vector3 direction;
    private float distance;
    
    // 构造函数注入接收者和命令参数
    public MoveCommand(CommandReceiver receiver, Vector3 direction, float distance)
    {
        this.receiver = receiver;
        this.direction = direction;
        this.distance = distance;
    }
    
    public void Execute()
    {
        receiver.Move(direction, distance);
    }
    
    public void Undo()
    {
        // 撤销移动:向相反方向移动相同距离
        receiver.Move(-direction, distance);
    }
}

命令管理

public class CommandManager : MonoBehaviour
{
    // 存储已执行的命令(用于撤销)
    private Stack<ICommand> commandHistory = new Stack<ICommand>();
    
    // 存储已撤销的命令(用于重做)
    private Stack<ICommand> undoneCommands = new Stack<ICommand>();
    
    /// 执行命令并记录到历史
    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        commandHistory.Push(command);
        // 执行新命令后,清空重做栈(避免混淆)
        undoneCommands.Clear();
    }
    
    /// 撤销上一个命令
    public void Undo()
    {
        if (commandHistory.Count == 0)
        {
            Debug.Log("没有可撤销的命令");
            return;
        }
        
        ICommand command = commandHistory.Pop();
        command.Undo();
        undoneCommands.Push(command);
    }
    
    /// 重做上一个被撤销的命令
    public void Redo()
    {
        if (undoneCommands.Count == 0)
        {
            Debug.Log("没有可重做的命令");
            return;
        }
        
        ICommand command = undoneCommands.Pop();
        command.Execute();
        commandHistory.Push(command);
    }
    
    /// 清空命令历史(用于重置场景等)
    public void ClearHistory()
    {
        commandHistory.Clear();
        undoneCommands.Clear();
        Debug.Log("命令历史已清空");
    }
}

命令存储

public class CommandInvoker : MonoBehaviour
{
    public CommandManager commandManager;
    public CommandReceiver receiver;
    
    [Header("控制参数")]
    public float moveDistance = 1f;
    public float rotateAngle = 90f;
    public Vector3 scaleFactor = new Vector3(1.1f, 1.1f, 1.1f);
    
    private void Update()
    {
        // 移动命令(WASD)
        if (Input.GetKeyDown(KeyCode.W))
        {
            ICommand moveCommand = new MoveCommand(receiver, Vector3.forward, moveDistance);
            commandManager.ExecuteCommand(moveCommand);
        }
        if (Input.GetKeyDown(KeyCode.S))
        {
            ICommand moveCommand = new MoveCommand(receiver, Vector3.back, moveDistance);
            commandManager.ExecuteCommand(moveCommand);
        }
        if (Input.GetKeyDown(KeyCode.A))
        {
            ICommand moveCommand = new MoveCommand(receiver, Vector3.left, moveDistance);
            commandManager.ExecuteCommand(moveCommand);
        }
        if (Input.GetKeyDown(KeyCode.D))
        {
            ICommand moveCommand = new MoveCommand(receiver, Vector3.right, moveDistance);
            commandManager.ExecuteCommand(moveCommand);
        }
    }

命令模式实例(角色动作回放,记录状态)

指令接口

// 时间回溯命令接口:定义了"记录状态"和"执行回溯"的规范
// 所有需要支持时间回溯的命令(如移动、旋转等)都需实现此接口
public interface ITimeBackCommand
{
    // 记录源对象的当前状态(用于后续回溯)
    // source:需要记录状态的对象(如玩家、物体)
    void Record(GameObject source);

    // 在目标对象上执行回溯操作(恢复到记录时的状态)
    // target:需要被回溯的对象
    void Execute(GameObject target);
}
    public void Record(GameObject source)
    {
        // ActionCommand在创建时就已经确定了动作类型,所以Record方法是空的
    }
}

public class MoveCommand : ITimeBackCommand
{
    // 记录的状态数据:速度、角速度、时间间隔
    private Vector3 _velocity;         // 线性速度
    private Vector3 _angularVelocity;  // 角速度
    private float _deltaTime;          // 记录状态时的物理帧时间间隔
    public StateType curType;
    private Quaternion _rotation;

    // 执行回溯:将目标对象的刚体状态恢复到记录时的状态
    public void Execute(GameObject target)
    {
        // 获取目标对象的刚体组件
        Rigidbody rb = target.GetComponent<Rigidbody>();
        if (rb != null)
        {
            // 恢复记录的速度和角速度
            rb.velocity = _velocity;
            rb.angularVelocity = _angularVelocity;
        }

        target.transform.rotation = _rotation;

        CloneC clone;
        if(clone=target.GetComponent<CloneC>())//切换动画状态
        {
            if(clone.curType!=curType)
            {
                clone.animator.SetBool(clone.curType.ToString(), false);
                if (curType.ToString() == "Find"|| curType.ToString() == "Die")
                {
                    clone.GetComponent<CloneC>().animator.SetBool("Idle", true);
                }
                else
                {
                    clone.animator.SetBool(curType.ToString(), true);
                }
            }

            clone.curType=curType;
        }
    }

    // 记录状态:保存源对象刚体的当前运动状态
    public void Record(GameObject source)
    {
        Rigidbody rb = source.GetComponent<Rigidbody>();
        if (rb != null)
        {
            // 记录当前的线性速度、角速度和物理帧时间
            _velocity = rb.velocity;
            _angularVelocity = rb.angularVelocity;
            _deltaTime = Time.fixedDeltaTime;  // 物理更新的时间间隔(用于精确回溯)

        }

        _rotation = source.transform.rotation;

        if (source.GetComponent<Player>())
        {
            curType = source.GetComponent<Player>().PlayerFSM.curType;
        }

    }
}

执行逻辑

public class TimeBackManager : TheManager<TimeBackManager>
{
    public class TimeBackManager : TheManager<TimeBackManager>
{
    public GameObject player; // 玩家对象(要记录状态的主体)
    public GameObject clon; // 克隆体对象(回溯时执行状态的目标)

    public float recordTime = 5f; // 最大记录时长(最多记录5秒内的玩家状态)
    public float invincibilityDuration = 2f; // 无敌时间

    private List<ITimeBackCommand> commands = new List<ITimeBackCommand>(); // 存储所有记录的状态命令(如移动状态)
    private float recordTimer = 0f; // 记录时长计时器
    private bool isRecording = false; // 是否正在记录状态的标记

    private Vector3 playerPosition; // 记录开始时玩家的初始位置
    private Quaternion playerRotation; // 记录开始时玩家的初始旋转
    public bool isDie;

    private void Start()
    {
        EventCenter.Instance.AddEventList<bool>("Die",SetDieBool);
    }

    private void Update()
    {
        if (isDie) return;

        // 按Tab键:切换记录状态(未记录则开始,正在记录则停止)
        if (Input.GetKeyDown(KeyCode.Tab)&&!clon.activeSelf)
        {
            if (!isRecording)
            {
                StartRecording(); // 开始记录玩家状态
            }
            else
            {
                StopRecording(); // 停止记录玩家状态
            }
        }

        // 按E键:触发时间回溯(需满足:未在记录、已有记录的状态)
        if (Input.GetKeyDown(KeyCode.E) && !isRecording && commands.Count > 0)
        {
            if (!clon.activeSelf)
            {
                StartTimeBack(); // 启动回溯逻辑
                MusicManager.Instance.PlaySFX("e",1);
            }
        }
    }

    // FixedUpdate:物理帧更新,记录玩家每帧的运动状态(确保物理逻辑同步)
    private void FixedUpdate()
    {
        if (isRecording) // 仅在记录状态时执行
        {
            if (isDie)
            {
                commands.Clear();
                isRecording = false;
                return;
            }

            RecordFrame(); // 记录当前帧的玩家状态
            recordTimer += Time.fixedDeltaTime; // 累加记录时间

            // 记录时长达到上限,自动停止记录
            if (recordTimer >= recordTime)
            {
                StopRecording();
            }
        }
    }

    // 开始记录:初始化记录参数,保存玩家初始位置和旋转
    public void StartRecording()
    {
        if (player == null) return; // 玩家为空则不执行

        isRecording = true; // 标记为正在记录
        commands.Clear(); // 清空之前的记录(避免残留旧状态)
        recordTimer = 0f; // 重置记录计时器

        MusicManager.Instance.PlaySFX("Music/ping");

        EventCenter.Instance.TriggerEvent<bool>("SetTimeBackUI",true);
        EventCenter.Instance.TriggerEvent("Setaura_flat",player.transform.position);
        // 保存记录开始时玩家的位置和旋转(用于回溯后克隆体的初始位置)
        playerPosition = player.transform.position;
        playerRotation = player.transform.rotation;
    }

    // 停止记录:结束记录状态,玩家回到记录开始时的位置和旋转
    public void StopRecording()
    {
        isRecording = false; // 标记为停止记录
        // 使用DOTween实现平滑的“高速倒带”效果

        player.GetComponent<Collider>().enabled = false;

        EventCenter.Instance.TriggerEvent<Player>("DeletePlayer",player.GetComponent<Player>());

        var playerComponent = player.GetComponent<Player>();
        var playerCollider = player.GetComponent<Collider>();
        var collidersList = playerComponent.colliders;
        for (int i = 0; i < collidersList.Count; i++)
        {
            var a = collidersList[i];
            var parent = a.transform.parent;
            if (parent != null)
            {
                for (int j = 0; j < parent.childCount; j++)
                {
                    var child = parent.GetChild(j);
                    var ppInteract = child.GetComponent<PressurePlateInteract>();
                    if (ppInteract != null)
                    {
                        ppInteract.colliders.Remove(playerCollider);
                        playerComponent.colliders.Remove(a);
                        if (ppInteract.colliders.Count == 0)
                        {
                            ppInteract.isTrigger = false;
                            ppInteract.AniSetBool(false);
                        }
                        break; // 找到后就退出循环
                    }
                }
            }
        }
        player.transform.DOMove(playerPosition, 0.3f).SetEase(Ease.InOutQuad);//回归动画
        player.transform.DORotateQuaternion(playerRotation, 0.3f)
            .OnComplete(() => player.GetComponent<Collider>().enabled = true); // 动画结束后恢复碰撞体

        MusicManager.Instance.PlaySFX("Music/tap回归");

        EventCenter.Instance.TriggerEvent<bool>("SetTimeBackUI", false);
        EventCenter.Instance.TriggerEvent("Clearaura_flat");
    }

    // 记录当前帧状态:创建移动命令,记录玩家当前的刚体运动状态(速度等)
    private void RecordFrame()
    {
        MoveCommand cmd = new MoveCommand(); // 实例化移动命令(记录刚体状态)
        cmd.Record(player); // 记录玩家当前的运动状态(速度、角速度)
        commands.Add(cmd); // 将命令添加到列表,保存帧状态
    }

    public void RecordAction(ActionCommand cmd)
    {
        if (isRecording)
        {
            commands.Add(cmd);
        }
    }

    public void StartTimeBack()
    {
        if (clon == null || player == null) return;


        clon.GetComponent<CloneC>().onCloneCheck(); // 克隆体激活相关逻辑(自定义脚本方法)
        clon.transform.position = playerPosition; // 克隆体移动到记录开始时的玩家位置
        clon.transform.rotation = playerRotation; // 克隆体旋转到记录开始时的玩家旋转

        // 重置克隆体刚体状态(避免初始速度影响回溯效果)
        Rigidbody rb = clon.GetComponent<Rigidbody>();
        rb.velocity = Vector3.zero; // 线性速度归零
        rb.angularVelocity = Vector3.zero; // 角速度归零

        StartCoroutine(TimeBack()); // 启动协程,执行逐帧回溯
    }

    // 回溯协程:逐帧执行记录的状态命令,让克隆体复现玩家的运动轨迹
    private IEnumerator TimeBack()
    {
        // 遍历所有记录的帧命令,逐帧执行
        foreach (var cmd in commands)
        {
            cmd.Execute(clon);
            if (cmd is MoveCommand)
            {
                yield return new WaitForFixedUpdate();
            }
        }

        // 回溯结束后,重置克隆体刚体状态(停止运动)
        Rigidbody rb = clon.GetComponent<Rigidbody>();
        rb.velocity = Vector3.zero;
        rb.angularVelocity = Vector3.zero;
        clon.GetComponent<CloneC>().animator.SetBool(clon.GetComponent<CloneC>().curType.ToString(), false);
        clon.GetComponent<CloneC>().animator.SetBool("Idle",true);

        //commands.Clear();
        yield return new WaitForSeconds(1f); // 等待1秒后,关闭克隆体相关状态
        clon.GetComponent<CloneC>().outCloneCheck(); // 克隆体取消激活相关逻辑(自定义脚本方法)
    }

    public void SetDieBool(bool _bool)
    {
        isDie = _bool;
    }

}
}