引
言
3A级游戏中,镜头是一个很重要的因素,跟战斗体验和剧情演出都紧密相关。对于一个有激烈打击感的动作游戏,镜头演出更是十分重要。本文将参考动作游戏鬼泣5的相关镜头功能,进行鬼泣5镜头规则的分析。并在虚幻4引擎下,给出一个基本框架实现,尽量还原一个跟战斗体验强相关的动态镜头效果。
· 正 · 文 · 来 · 啦 ·
1 需求目标分析 镜头通常有以下几种分类:1.自动追踪镜头:常规第三人称镜头,镜头跟随玩家;
2.轨道镜头:根据静态轨道信息控制移动的镜头;
3.战斗镜头:考量流程战斗的效果时,需要考虑的镜头控制;
4.定点镜头:固定位置,固定旋转或仅变动旋转的镜头。
在剧情演出时,常出现轨道镜头的运用。而固定场景时,又较常出现定点镜头和轨道镜头的组合。 而我们的目标要实现的是:1.非战斗下镜头表现;
2.战斗场景镜头表现;
3.战斗场景锁定敌人时的镜头表现;
4.特殊主角动作触发的镜头表现。
2 镜头简要说明非战斗下镜头表现
鬼泣5希望在没有敌人时,能充分地向玩家展示场景。因此当玩家操控视野摇杆时,镜头部分参数会发生自动修正,表现(称为表现A)如下:可以看到此时,尼禄默认回到了偏左下角的位置,而将更大的视野留给了场景,当玩家操作移动摇杆时,又将触发镜头默认的动态修正,使其恢复到常规镜头。摇杆操作的默认镜头修正到常规镜头
无敌人时的场景镜头修正
战斗场景镜头表现出现敌人时,为保证玩家能更加直观看到尼禄的技能释放和敌人方位,玩家操控视野摇杆时,不会触发表现A,同时会给予视野控制的pitch角更大的限制。其他的镜头表现跟非战斗场景的常规镜头类似。
有敌人出场时的镜头表现,pitch受到了更多限制
战斗场景锁定敌人时的镜头表现
锁定状态下,鬼泣5希望玩家和锁定镜头都在屏幕范围内,同时由于鬼泣5几乎所有攻击技都依赖锁定,因此在鬼泣5在在近距离跟敌人发生战斗时会有较多的镜头规则约束。鬼泣对玩家锁定敌人时的场景进行了分区,玩家踩在不同的区域做出不同的逻辑操作会触发不同的镜头动画。
鬼泣5分区简易示意图
中心平移区进入缓冲区缓冲区进入中心平移区敌人在屏幕区外时进行锁定边缘平移区进入屏幕外区域
分区没有采用复杂的计算方式,使用向量夹角判断即可。 敌人的前向向量取镜头前向向量的反向,同时计算敌我向量在XY平面的投影向量,两个直线取夹角(非向量夹角)。 这样就可以获取主角相当于敌人的方位来判定所站分区。特殊主角动作触发的镜头表现
某些动画可能也为影响镜头表现,此时需要动画帧配置通知,通过控制行为树黑板变量以事件驱动的方式控制镜头。
跳跃动作触发镜头
另外,需要注意,三个镜头状态间进行的切换是柔性的,存在缓冲,目的是间接平滑镜头效果。解除锁定几秒后,镜头才会恢复到非锁定的镜头状态
3 UE4相关镜头模块简介UE4 内部自带PlayerCameraManager对玩家相机进行管理,PlayerCameraManager被playerController引用,PlayerCameraManager管理着所有镜头的修改器。镜头修改器基类为UCameraModifier。ue4自带了镜头抖动的功能,见UCameraModifier_CameraShake,因此自定义扩展UCameraModifier,如控制springarm的UCameraModifier_SpringArmSetter,控制viewRotate的UCameraModifier_ControlRotate,UCameraModifier_CameraShake对shake动画实例进行了管理,相关的代码可以作为我们实现其他类型modifier的参考。
SpringArm弹簧臂是ue4第三人称相机常用组件,用来辅助控制相机的移动,具体可参考官网:https://docs.unrealengine.com/en-US/Gameplay/HowTo/UsingCameras/SpringArmComponents/index.html。
因此我们可以选择在ue4的基础上实现自定义扩展,同时能复用springArm组件的相关功能。
UE4摄像机相关架构
4 UE4 CameraShake的管理和实践UCameraModifier_CameraShake作为控制相机抖动的修改器类型,管理着所有相机抖动动画相关的实例。下面对相机抖动的管理做个简介。
AddCameraShake负责增加一个相机抖动效果,ue4允许同时存在多个抖动动画实例,使用了TArray进行管理。抖动动画是短时生命周期的实例,所以内部使用了对象池进行优化。ue4选择在拿出对象池中的动画实例时进行参数重置,NewObject支持重新初始化。 在定制SpringArm弹簧臂和相机rotate等相关设置时可以使用了类似方式实现,唯一的约束在于自定义镜头modifier实例,一个类型只能一个处于running,使用了指针维护链接即可。Modifier层级自定义扩展
5 Camera和Spring Arm的独立管理默认ue4第三人称相机的实现方式是把弹簧臂挂接在主角身上,主角位移带动弹簧臂位移,但是这样会有很大的缺点,即无法灵活地解绑或绑定其他场景对象,难以实现定点相机或轨道相机等功能。因此抽离springarm和followCamera到单独的actor类进行管理是必要的。
另外,由于战斗镜头使用了行为树相关的AIi功能,因此AIcontroller的继承和扩展必不可少,借鉴ue4的CameraActor,实现了自己的CameraPawn。
CameraPawn必须拥有自己的AIcontroller,同时又需要从依赖的playercontroller获取rotation或设置ViewTarget。
6 镜头业务逻辑的管理: 行为树 ? 状态机? 或朴素的数组管理?思考一下我们的需求,同一时刻只有一个镜头动画触发,镜头动画按镜头动画按时间划分又可以分为单帧动画和长时动画,一个是单帧触发一次则镜头参数修正一次,一个是触发动画runing状态,使之持续一段时间。容易想到,同一时刻,其实可能存在多条镜头动画路径满足条件,因此需要做优先级管理,使同一时刻只触发执行一个节点。
镜头的一大特点就是各个状态之间都可能发生跳转,简言之就是anystate->anystate,因此n个状态的状态机来描述一定会出现n*n的transition,但好在transition同样可以做通用的优先级管理,状态的Exit函数来实现状态本身的中断。大致的思路是,每个状态是否触发都做先置检查,先置检查将check多个条件bool值(每个判定条件value可能通过外部事件驱动设置,也可能通过内部update设置,视具体情况而定)。Transition在条件判定值设定完毕后通过优先级选取当前时刻满足条件判定的最高优先级节点执行。
当然,使用优先队列(或可变数组)管理也是可以实现,大致思路是:将当前时刻满足条件的执行节点全部扔进队列(数组)后,在执行具体镜头动画的时刻从优先队列中弹出优先级最高的节点(或直接数组遍历找出最高优先级的节点)执行即可,如果当前执行节点就是弹出节点直接返回,否则进行跳转,同时对上一执行节点实施中断。每一个执行节点可能组合了几种类型的镜头动画(如同时设置location和rotation)。
其实数组的管理中提到的将当前时刻满足条件的执行节点全部扔进队列(数组)并选出最高优先级执行的操作类似状态机方法中提到的update通用执行方法,而能抽离通用执行的前提是某状态需要满足其跳入条件只依赖自身,不依赖跳出状态,依赖上一跳出状态的跳转也被称作带有上下文的状态跳转方式。状态机的存在主要是能解耦这类复杂逻辑,如果不使用状态机,上下文跳转多半会出现下述情况:
Switch(CurrentState){Case StateA:If(conditionA2B) SwitchState(StateB)Elseif(conditionA2C) SwitchState(StateC)Case StateB:...}
可以预见,如果采用常规数组管理,一旦出现带有上下文的状态跳转方式,在选择执行节点时,难免会出现上述格式的代码。
同时我们也能发现常规状态机的不足,即条件判定难以层次化,如下所示:
Switch(CurrentState){Case StateA:If(condition1&&condition2) SwitchState(StateB)elseif(condition1&&condition3) SwitchState(StateC)elseif(condition1&&condition4) SwitchState(StateD)Case StateB:...}
采用层次化状态机可以将公共变量提到上层作为更前置的判定(类似行为树的黑板装饰节点),附带的福利是还可以把公共逻辑也抽离到上层(类似行为树的Service)。至此我们发现层次化状态机的行为跟行为树已经越来越相似,如果不使用带有上下文依赖的跳转,当你完成行为树的连接时,会发现除了连接线路不同,他们的节点配置高度相同(层次状态机中,非叶子节点也被称为状态,而行为树中,根据功能不同,可能叫服务,也可能叫装饰节点或其他,是比状态更细的粒度)。
如果使用行为树,值得一提的是为了确保最终镜头动画效果可控,尽量减少并行节点的使用。在必须使用的情况下,也务必要使并行节点的放置层级尽量低,越接近叶节点越好,通过这种方式进行约束有利于提高镜头效果的稳定性。
行为树相关指南下可见参考文献列出的官方链接。可以看到,行为树更加强大,支持更复杂的逻辑支持,灵活性极高,但是代价就是容易滥用,导致树的复杂度不受控制,特别对于上下文依赖的跳转似乎也没有优美的解决方案。如果形成了过于复杂的树,再作后续开发的时候可能因为分层过于复杂,粒度过细,导致无法实现线性扩展。
综上,三种方式都能实现目前的需求。当出现上下文依赖跳转的情况,状态机的实现更稳定,可以减少对原代码大刀阔斧修改的概率。如果出现以下情况:1.没有出现上下文依赖 2.能控制行为树层级复杂度(树的层级不宜过深) 3.可视化状态机的跳转连线已成稠密网状结构。在可控的范围内,规范的使用行为树是可行的。另外上述为了严谨起见,用的同一时刻而非同一帧,因为ue4行为树各个路径的条件检查并非是在一帧内完成。
下面简述Ue4下通过行为树进行实现的大致思路。由于镜头控制器使用行为树作为逻辑管理,因此镜头必须依赖自定义AIController,AIController借助BehaviorTree实现控制逻辑管理,BehaviorTree则在内部节点(如服务)简单封装AIController节点。因此,复杂的逻辑可以存放在自定义AIController中,再由行为树进一步调用。下面是一种行为树实现,可以看到除开并行节点层级已经达到4层,所以从复杂度上来说还可以有进一步调整树的层级的空间,这里就不再赘述。
7 关于镜头动画: 近似的阻尼运动镜头运动近似模拟一个阻尼运动,例如旋转量0→50,在旋转量接近目标50的时候,tick时每帧的旋转变化量受到离目标的差值作为倍率影响,形成一个越接近目标越慢的移动。因此,目前tick时每帧旋转量的计算方式如下:DesiredRot为目标rotatation,OutViewRotation为当前viewRotation,ActionInterpSpeed为策划配置的速度。
float scale = (DesiredRot - OutViewRotation).GetNormalized().Yaw;float speed = FMath::Clamp(ActionInterpSpeed*DeltaTime, 0, 1);OutDeltaRot.Yaw = speed * scale ;
此处的speed为相对速率,含义是此帧移动的距离占总距离的比例。
接近目标时函数收敛很慢,为了避免逻辑上的bug,在防止overshoot的误差量设置上需要小心。
防止overshoot的通常做法是:
if (FMath::IsNearlyZero(Delta.Yaw, RotateTolerance)){ OutViewRotation.Yaw = DesiredRot.Yaw; bIsFnished = true;}
因此,RotateTolerance设置的精度直接影响了此动作是否完成的逻辑状态。
8
参考文献资料
1.游戏设计的236个技巧 第5章:让3D游戏更有趣的镜头技巧
2https://www.youtube.com/watch?v=C7307qRmlMI&t=246s GDC关于3A游戏的镜头设计3.https://cowlevel.net/article/1882558
4.https://blog.csdn.net/u012999985/article/details/68947410 ue4摄像机系统解析
5.https://docs.unrealengine.com/zh-CN/Engine/AI/BehaviorTrees/QuickStart/index.html 行为树快速入门指南
6.相关图片视频素材皆来自游戏《鬼泣5》
数天技术
让技术·更有趣