找回密码
 立即注册
查看: 239|回复: 0

Unity录制回放

[复制链接]
发表于 2023-8-21 13:47 | 显示全部楼层 |阅读模式
本文的目的是为了介绍Unity手游自动化中录制回放功能的简单实现,实际上Unity 有一个官方东西 Automated QA用来实现录制回放,只不外该东西于21年12月遏制维护,官方的理由说是筹备把精力放在CI 管线上。目前该东西对某些自定义的UI是不撑持的,会呈现一些问题。因此在这里聊下录制回放的简单实现。
    凡是来说用户的输入按输入源可以分为以下两类:

  • Pointer操作

    • Pointer Down
    • Pointer Up
    • Pointer Move
    • Pointer Drag

  • 按键操作
    如果按接受源分类的话可以分为以下两类:

  • UI接受
  • 游戏逻辑接受
    从输入源的角度分析,如果我们想要获取用户的输入,只需要简单的在Update里面监听Input类的输入即可,记录下时间和输入内容。录制的问题很轻松就能解决了。
    如果要实现回放的话,首先我们需要知道输入的传递路径,简单来说就是系统底层接受硬件输入然后将其传递给应用,应用接受到输入信息后再将其分发。那么从接受源的角度来分析,就会有两条路径:


    那么我们就能想到几种改削方式:

  • 伪造系统底层输入
  • 改削Input类,在Input这里伪造输入
  • 对StandaloneInputModule.cs进行改削
    对于第一种方式我们需要按照平台去分袂实现,目前我调研到的方案斗劲有限,仅供参考:

  • Windows平台的话可以使用win32 api去伪造输入
  • Android平台可以使用adb,只不外感觉adb会很慢,在回放拖拽时可能会有精度问题
    理论上讲是可以实现的,只不外一看就很麻烦,我们还是考虑从C#的层面去解决。
    对于第二种方式,Input类是C++实现的,因此我们无法简单对其进行改削,目前能考虑的手段就是将Input封装一下,之后获取输入的时候都使用封装好的Input类就行,而且还需要把其他处所使用的Input给替换了。实际上Unity的StandaloneInputModule就是这么干的,这个脚本获取到的所有输入都来自于一个叫BaseInput.cs的脚本,这个脚本就是把Input简单封装了下。
    如果不想改削代码,可以考虑第三种方式。实际上我们前面也说了是为了解决手游自动化的录制回放,手游有个特点就是只接受触摸输入,也就是说我们可以不用考虑游戏逻辑的按键输入措置,转而存眷Pointer和UI文本输入。
    对于Unity来说,Pointer的操作默认是交给StandaloneInputModule.cs去措置,这个脚本凡是会自动挂载在EventSystem上,而且全局独一。这个类的有一个核心方式Process会被EventSystem在Update中不竭调用(当游戏掉去Focus或暂停时不会调用),这个方式简单来说就是按照当前鼠标的信息(这些信息会从Input类获取)来创建一个PointerEvent,并按照条件将其分发给鼠标下方的UI,触发各种回调(OnPointerDown之类的)。下面这段是Process的代码,可以看到核心的部门就是ProcessTouchEvents和ProcessMouseEvent,后面的部门是措置UI Navi的,这个可以不用管。
        public override void Process()
        {
            if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
                return;

            bool usedEvent = SendUpdateEventToSelectedObject();

            // case 1004066 - touch / mouse events should be processed before navigation events in case
            // they change the current selected gameobject and the submit button is a touch / mouse button.

            // touch needs to take precedence because of the mouse emulation layer
            if (!ProcessTouchEvents() && input.mousePresent)
                ProcessMouseEvent();

            if (eventSystem.sendNavigationEvents)
            {
                if (!usedEvent)
                    usedEvent |= SendMoveEventToSelectedObject();

                if (!usedEvent)
                    SendSubmitEventToSelectedObject();
            }
        }
    我们继续看ProcessTouchEvents的代码,简单来说就是遍历所有的Touch数据,这个input就是上文提到过的BaseInput。StandaloneInputModule的父类PointerInputModule会维护一个dic,存储PointerEventData,也就是说点击开始的时候就会创建一个PointerEventData并添加到dic,松开后就会移除掉。获取到Touch数据后会调用GetTouchPointerEventData方式,这个方式就是去dic里面查找。得到PointerEventData后就会走Press、Move、Drag、Release的措置流程。
        private bool ProcessTouchEvents()
        {
            if (State == RecordState.Replay)
                return true;

            for (int i = 0; i < input.touchCount; ++i)
            {
                Touch touch = input.GetTouch(i);

                if (touch.type == TouchType.Indirect)
                    continue;

                bool released;
                bool pressed;
                var pointer = GetTouchPointerEventData(touch, out pressed, out released);

                ProcessTouchPress(pointer, pressed, released);

                if (!released)
                {
                    ProcessMove(pointer);
                    ProcessDrag(pointer);
                }
                else
                    RemovePointerData(pointer);
            }
            return input.touchCount > 0;
        }
    顺带一提PointerEventData还会记录当前按压的GameObj,用的是射线检测获取的。在阅读了ProcessDrag这些函数的代码后会发现基本都是使用ExecuteEvents.ExecuteHierarchy这个方式去把PointerEventData传递给Obj对应的措置函数。
    在了解这些后,我们很容易就能想到在这部门代码里面去记录PointerEventData的数据,对比起在Update里面去监听输入,好处就是能过滤很多无效的点击。这里需要注意的是不能简单的在ProcessTouchEvent这里记录,应该到PrcoessDrag这些函数里面成功措置了的部门去记录。凡是来说我们需要记录的数据是位置、时间以及操作,并不需要把整个PointerEventData记录下来。时间的话建议直接用Time.time去获取,最后只需要记录每个操作之间的相隔时间即可。
    由于StandaloneInputModule是Unity自带的脚本,我们并不便利改削,因此我们需要创建个新的Module,这里建议直接把StandaloneInputModule的代码复制粘贴一份,再做些改削就行。之前我们提到InputModule全局独一,因此我们还需要把场景中现有的Module禁用掉。回放这些操作的时候也只需要重写ProcessTouchEvent即可。这里建议把回放的逻辑放到Update里面,并在回放时把之前的措置逻辑禁用掉。需要注意的是不要在回放的逻辑里面写费事操作,容易影响拖拽精度。
    在回放的时候我们要按照记录的数据生成Touch,而不是PointerEventData。在生成Touch的时候填入pos以及TouchPhase即可,fingerId的话默认填0就行,如果有多指操作的话这里需要注意下。
    到目前为止我们已经措置触摸操作,接下来还需要措置输入操作。
    对于Unity来说,输入操作一般都是由InputField进行措置,那么我们可以监听所有常用按键的输入,在监听到按键按下时就去场景中搜索focused的InputField,凡是全局只会有独一一个InputField被Focused。找到后注册onEndEdit回调,记录最后的输入与InputField的UI路径。我们还可以额外记录InputField的位置以防UI路径变换,到时候就可以按照位置去搜索UI。
    回放的时候就简单多了,而且这种文本输入的回放也不像拖拽那样要求精确的时间。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-3 04:35 , Processed in 0.099990 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表