问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

用Unity实现简单的撤销/重做(undo/redo)功能(附源项目)

创作时间:
作者:
@小白创作中心

用Unity实现简单的撤销/重做(undo/redo)功能(附源项目)

引用
CSDN
1.
https://blog.csdn.net/LJR_LJRRR/article/details/143527736

在Unity开发中,撤销(Undo)和重做(Redo)功能是许多项目中常见的需求。本文将详细介绍如何使用栈(Stack)数据结构在Unity中实现这一功能,并通过一个简单的示例项目进行演示。

一、了解撤销重做实现原理

查阅资料,发现基本上的实现都是基于栈(stack)这个数据结构,栈具有先入后出的特点,这个特点就很好的适应了撤消重做的需求,因为我们总是会撤回到离我们最近的前一步,而不是最久远、最先前的那一步。

我们在项目中将设置两个栈,分别存储undo的内容和redo的内容,也就是两个栈并行的来工作。

下面我将通过一个简单的操作流程,随之编写代码和界面,实现一个很简单的撤消重做demo。

二、在unity中编辑UI交互界面

这一步相信大家自己就可以完成了,我也是初学者,所以我还是大体说一下,因为后面会用到按钮点击事件调用函数。

功能分析:点哪个按钮,小物块就移动到哪个按钮的下面。创建UI-Legacy-Button,复制两个出来,依次摆好。

小物块reset一下位置(0,0,0)

三、分析工作流与编写脚本

(一)分析工作流

现在,假装你的老师/上司给了你一个任务,你要做的是将小物块按左-中-右的顺序移动,是的,假装你有这样一个很简单的任务。

第一步我要点left按钮,可是,我不小心点错了,点成了right,这时候我需要撤回一下,让他回到原位置,也就是回到中心,这是一步错误,而在现实中,人们不一定只走错一步路,还可能连续错好几次,这时候用户就会疯狂点撤回,也就是说,我们的undo栈是需要保存许多步的。(此处先不分析最大步数)这样用户在连续撤回的时候,才能逐步后退,直到他退到了某个之前的状态。

同理,假如我们做错了5步,但是不小心按了六下撤回按钮,多按了一次,那我想再重做一次,回到第五次按撤回按钮那个时候的状态,这就是redo栈。也就是说,你按了撤回,回到了上一步,但现在,此时这个状态,是要被保存进redo栈的。以下是完整的图示:

注意每当我们按下Left等位移按钮时,除了记录上一个状态(并压入undo栈)和设置新状态时,还需要将redo栈清空,因为我这一步是最新添加的操作,我没有“下一步”,所以redo为空很好理解。还有其他操作redo栈中的数据以保证逻辑正确的方法,但我觉得这是最通用且最好理解的一个。你可以用office或者什么绘画软件试一下,看是不是你引入新操作后,重做的那个按钮变灰了。

其中六、七步是逐步分析了我们连续撤回和重做时的出入栈情况,注意,这个过程中一直没有新操作引入。

(二)位置移动(新操作)的代码

现在我们大体清楚了工作流,开始写脚本。

我们create一个C#脚本,命名为UndoRedoManager.cs,挂载到场景中的小物块上。

定义两个栈,注意,c#要求Stack,尖括号内必须有一个类型,这个demo只涉及到位置,所以我用v3,你可以根据自己的项目更改类型,比如bool型、int甚至你自己定义的类型。并且定义一个current,存放当前的位置信息。

private Stack<Vector3> undoStack = new Stack<Vector3>();
private Stack<Vector3> redoStack = new Stack<Vector3>();  
private Vector3 currentPosition;  

别忘了在start中初始化一下我们的current。

void Start()
{
    currentPosition = transform.position;
}  

我们先来写left函数,并绑定到button上,另外两个按钮你可以照猫画虎。

让我们看看他做了什么?首先,把小物块的位置改了,保证你显示上没问题,他现在确实显示在屏幕左侧了;其次,将当前的current压入undo栈,请问现在的current值是什么?还是(0,0,0),因为你没让他变过。

压完了,我们才修改current的值,改为当前的,如你所见,现在的current应该是(-7,0,0)了。

最后,不要忘记,这是一个新操作!我们需要清空整个redo栈!

public void CubeTransLeft()
{
    gameObject.transform.position = new Vector3(-7, 0, 0);
    //一定要先压入,然后再改变current数值
    undoStack.Push(currentPosition);
    currentPosition = gameObject.transform.position;
    redoStack.Clear();
}  

在button的onclick中,拖拽、选择刚刚编写的函数,如此以配置按钮交互事件。

你可以自己试着写写center和right,不想写也没关系,我附上了源文件。

(三)undo&redo的代码

让我们用快捷键Z和Y来实现,在update中:

void Update()
{
    //使用键盘快捷键触发撤销和重做
    if (Input.GetKeyDown(KeyCode.Z))
    {
        Undo();
    }
    if (Input.GetKeyDown(KeyCode.Y))
    {
        Redo();
    }
}  

所调用的undo方法:

如果只看代码不好理解,可以对照刚刚的图示一起看。

分为非空和空的情况,这里如果为空的话我直接debug.log了,其实你完全可以做一个UI窗口弹出,或者改成按钮,当空的时候按钮就禁用的模式。

首先把当前状态压入重做栈,因为这是你“上一步”的“下一步”,它存在,必须保留下来以防用户点击“重做”。

在undo栈中,取出最顶层的,放到current里面,并将物体的position设置为current。

public void Undo()
{
    if (undoStack.Count > 0)
    {
        // 将当前状态推入重做堆栈
        redoStack.Push(currentPosition);
        // 从撤销堆栈弹出上一个状态并应用
        currentPosition = undoStack.Pop();
        transform.position = currentPosition;
    }
    else
    {
        Debug.Log("No actions to undo");
    }
}  

redo方法:

同理。current压入undo栈,因为这是你“下一步”的“上一步”。同样current取出最顶层,位置也改变。

public void Redo()
{
    if (redoStack.Count > 0)
    {
        // 将当前状态推入撤销堆栈
        undoStack.Push(currentPosition);
        // 从重做堆栈弹出下一个状态并应用
        currentPosition = redoStack.Pop();
        transform.position = currentPosition;
    }
    else
    {
        Debug.Log("No actions to redo");
    }
}  

测试运行一下!完成!你可以在此流程的基础上添加任何你需要的变量类型、操作类型。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号