之前看过关于回放系统的实现,于是为了深入该系统的学习,便实现了一个简单的回放系统,下文是该系统的思路。

该文以《【游戏设计模式】之二 论撤消重做、回放系统的实现:命令模式》为主要思路,毛大的这类文章都是很好的设计模式思路在游戏上的应用,很推荐看看。

简介

回放系统主要是应用命令模式来实现,关于命令模式我就不再详细描述,可以先看上面的推荐文章。

先了解回放系统的需求,简单来说就是可以让用户查看到某一时间游戏的状态,首先要知道游戏一开始的状态是确定的,而游戏状态的变化很大程度上都是玩家的操作(即命令),那么我们只要保存玩家输出的操作,就可以从初始状态一直执行玩家在对应时间的操作,到达目标时间时,该状态也就是某一时间游戏状态,从而实现了回放系统。

为了方便了解该项目,下为该项目的类图:

命令模式实例类图.png

由于项目主要是为了体现命令模式实现如何制作回放系统,所以仅实现了二维棋盘本地双人移动的简单操作。

基本功能

主界面如图:

界面图示.png

该项目可以通过W/S/A/D或者小键盘的上下左右实现两个小球的移动,移动后可以点击【Save Commands】按钮,来将命令集保存为Json文件,再点击【Load Commands】按钮便进入回放模式,如下图:

回放模式界面图示

回放模式可以自动播放或者拖拽进度条来实现调整播放进度,具体可以查看带代码。

Rest】按钮可以返回至游戏初始状态。

以上为项目的基本功能。

思路

在初始化阶段需要将所有Controller以id存入核心类的字典中,方便后续回放时通过id获取Controller实现命令。

//Controller在Start函数时会将自己存入
private void Start()
{        
    //初始化输入模块
    circleInput.SetController(this);

    //存入ReplayManager中
    id=ReplayManager.GetInstance().InputController(this)
    
    //位置归到地图原点
    Rest();
}

再是关于命令记录,在玩家输入按键时会生成一个命令对象,执行命令后会将该命令转换为CommandData类对象存入,该类包含由创建该命令的Controller的id值,创建时的相对帧数,该命令属于什么命令的枚举对象,以及多个参数转换string,用于存储命令的数据信息。

记录完后可以通过将CommandData类转换为Json实现本地化存储,当需要回放时则读取Json重新转换为CommandData,再根据CommandData的信息转换为Command实现到Controller中。

其中为了绑定当前帧与当前状态,将当前帧的数据curFrame设置为属性,并在该属性修改时调用函数让当前Controller状态一同变化,代码如下:

    /// <summary>
    /// 当前播放帧
    /// </summary>
    private int _curFrame;
    /// <summary>
    /// 当前播放帧属性
    /// </summary>
    public int curFrame
    {
        get => _curFrame;
        set => GoTargetFrame(value);
    }
    
    /// <summary>
    /// 到达目标帧
    /// </summary>
    /// <param name="targetFrame"></param>
    private void GoTargetFrame(int targetFrame)
    {
        //判断是否使用Excute或者Undo
        bool isExcute = targetFrame > _curFrame;
        while (_curFrame != targetFrame)
        {
            _curFrame += (isExcute?1:-1);
            if (isExcute &&_curFrame >= commands[curIndex].frame || 
                !isExcute && _curFrame <= commands[curIndex].frame)
            {
                //对对应id的controller使用CommandData来进行操作
                //ControlByCommandData就是实现上述的函数,具体可以查看项目源码
                controllerDic[commands[curIndex].id].
                    ControlByCommandData(commands[curIndex],isExcute);
                curIndex=Mathf.Clamp(curIndex+(isExcute?1:-1),0,commands.Count-1);
            }
        }
    }

以上就是比较主要的思路,实际上比较麻烦的主要是UI方面,也就是进度条,这里就不细讲了,具体可以查看项目源码。

参考

  1. 《【游戏设计模式】之二 论撤消重做、回放系统的实现:命令模式》

项目源码

Github仓库