剔除投影是什么意思,在虚幻虚幻4引擎和unity3d中?

&figure&&img src=&https://pic4.zhimg.com/v2-aa55d25b08f0264ead19_b.jpg& data-rawwidth=&403& data-rawheight=&266& class=&content_image& width=&403&&&/figure&&p&
终于要写帧同步这块了,这块很难讲清楚,细枝末节有很多优化点,也有一些不同的优化方向,根据不同项目类型,对操作手感的要求,对联机玩家的个数等,会有不同的难点和痛点。不同的优化方向,优化手法的差异,可能导致一些争论。并且,帧同步,本身也有很多变种,以应对不同的需求。所以,我一切都是基于我们的项目类型(ACT)来做的方案和优化,并不一定适合其它也需要帧同步的游戏,故在此提前说一下,以免引起一些不必要的误解。&/p&&p&
有些东西,可能会写得详细些,有些可能会简略些,不可能照顾到所有程度的读者,见谅。&/p&&h2&&b&帧同步的几个难点&/b&&/h2&&p&
帧同步的基础原理,以及和状态同步的区别,已经有很多文章介绍,我就不再赘述,大家可以自行google。以下只说几个难点。&/p&&ul&&li&&b&保证客户端独自计算的正确,即一致性&/b&&/li&&/ul&&p&
帧同步的基础,是不同的客户端,基于相同的操作指令顺序,各自执行逻辑,能得到相同的效果。就如大家所知道的,在unity下,不同的调用顺序,时序,浮点数计算的偏差,容器的排序不确定性,coroutine内写逻辑带来的不确定性,物理浮点数,随机数值带来的不确定性等等。&/p&&p&
有些比较好解决,比如随机数值,只需要做随机种子即可。&/p&&p&
有些注意代码规范,比如在帧同步的战斗中,逻辑部分不使用Coroutine,不依赖类似Dictionary等不确定顺序的容器的循环等。&/p&&p&
还有最基础的,要通过一个统一的逻辑tick入口,来更新整个战斗逻辑,而不是每个逻辑自己去Update。保证每次tick都从上到下,每次执行的顺序一致。&/p&&p&
物理方面,因为我们战斗逻辑不需要物理,碰撞都是自己做的碰撞逻辑,所以,跳过不说,这块可以参考别的文章。&/p&&p&
最后,说一下,浮点数计算无法保证一致性,我们需要转换为定点数。关于定点数的实现,比较简单的方式是,在原来浮点数的基础上乘,对应地方除以,这种做法最为简单,再辅以三角函数查表,能解决一些问题,减少计算不一致的概率,但是,这种做法是治标不治本的方式,存在一些隐患(举个例子,例如一个int和一个float做乘法,如果原数值就要*1000,那最后算出来的数值,可能会非常大,有越界的风险。)。&/p&&p&
最佳的解决办法,是使用实现更加精确和严谨,并经过验证的定点数数学库,在c#上,有一个定点数的实现,&a href=&https://link.zhihu.com/?target=https%3A//www.photonengine.com/en-US/Photon& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Photon网络&/a&的早期版本,&a href=&https://link.zhihu.com/?target=https%3A//github.com/suzuke/TrueSync/tree/master/Assets/TrueSync/Engine/Math& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Truesync有一个很不错的定点数实现&/a&。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-54df1a2a2c4db77fdab09e9_b.jpg& data-size=&normal& data-rawwidth=&669& data-rawheight=&852& class=&origin_image zh-lightbox-thumb& width=&669& data-original=&https://pic1.zhimg.com/v2-54df1a2a2c4db77fdab09e9_r.jpg&&&figcaption&定点数的实现&/figcaption&&/figure&&p&
其中FP,就可以完全代替float,我们只需要将我们自己的逻辑部分,float等改造为FP,就可以轻松解决。并且,能够很好的和我们protobuf的序列化方式集成(注意代码中的Attribute,如下图),保证我们的配置文件,也是定点数的。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-ae47c66a1dbb7ce73207c7_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&437& data-rawheight=&383& class=&origin_image zh-lightbox-thumb& width=&437& data-original=&https://pic1.zhimg.com/v2-ae47c66a1dbb7ce73207c7_r.jpg&&&/figure&&p&TSVector对应Vector3,只要基于FP,可以自己扩展自己的数据结构。(当然,如果用到了复杂的插件,并且不开源,那么对于定点数的改造,就会困难很多)&/p&&figure&&img src=&https://pic4.zhimg.com/v2-de61f21d96bf38aad9a499ee30becd42_b.jpg& data-size=&normal& data-rawwidth=&973& data-rawheight=&288& class=&origin_image zh-lightbox-thumb& width=&973& data-original=&https://pic4.zhimg.com/v2-de61f21d96bf38aad9a499ee30becd42_r.jpg&&&figcaption&三角函数通过查表方式实现,保证了定点数的准确&/figcaption&&/figure&&p&我个人认为,这一套的实现,是优于简单的乘10000,除10000的方式。带来的坏处,可能就是计算性能略差一点点,但是我们大量测试下来,对计算性能的影响很小,应该是能胜任绝大部分项目的需求。&/p&&p&
对于计算的不确定性,我们也有一些小的隐患,就是,我们用到了Physics.Raycast来检测地面和围墙,让人物可以上下坡,走楼梯等高低不平的路,也可以有形状不规则的墙。这里会获得一个浮点数的位置,可能会导致不确定性,这里,我们用了数值截断等方式,尽量规避,经过反复测试,没有出现过不一致。但是这种方式,毕竟在逻辑上,存在隐患,更好的方式,是实现一套基于定点数的raycast机制,我们人力有限,就没时间精力去做了。这块,有篇文章讲得更细致一些,大家可以参看 &a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&帧同步:浮点精度测试&/a&。&/p&&ul&&li&&b&帧同步网络协议的实现&/b&&/li&&/ul&&p&在处理好了基础的计算一致性问题后,我们就要考虑网络如何通信。这里,我不谈p2p方式了,我们以下谈的,都是多client,一个server的模式,server负责统一tick,并转发client的指令,通知其他client,可以参看文章&a href=&https://link.zhihu.com/?target=http%3A//www.10tiao.com/html/255/.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&网游流畅基础:帧同步游戏开发&/a&。&/p&&p&首先,是网络协议的选择。TCP和UDP的选择,我就不多说了,帧同步肯定要基于UDP才能保证更低的延迟。在UDP的选择上,我看网上有些文章,容易导入一个误区,即,我们是要用可靠传输的UDP,还是冗余信息的UDP。&/p&&p&&b&基于可靠传输的UDP&/b&,是指在UDP上加一层封装,自己去实现丢包处理,消息序列,重传等类似TCP的消息处理方式,保证上层逻辑在处理数据包的时候,不需要考虑包的顺序,丢包等。类似的实现有Enet,KCP等。&/p&&p&&b&冗余信息的UDP&/b&,是指需要上层逻辑自己处理丢包,乱序,重传等问题,底层直接用原始的UDP,或者用类似Enet的Unsequenced模式。常见的处理方式,就是两端的消息里面,带有确认帧信息,比如客户端(C)通知服务器(S)第100帧的数据,S收到后通知C,已收到C的第100帧,如果C一直没收到S的通知(丢包,乱序等原因),就会继续发送第100帧的数据给S,直到收到S的确认信息。&/p&&p&
有些文章介绍的时候,没有明确这两者的区别,但是这两种方式,区别是巨大的。可靠传输的UDP,在帧同步中,个人认为是不合适的,因为他为了保证包的顺序和处理丢包重传等,在网络不佳的情况下,delay很大,将导致收发包处理都会变成类似tcp的效果,只是比TCP会好一些。必须要用冗余信息的UDP的方式,才能获得好的效果。并且实现并不复杂,只要和服务器商议好确认帧和如何重传即可,自己实现,有很大的优化空间。例如,我们的协议定义类似如下:&/p&&figure&&img src=&https://pic3.zhimg.com/v2-a79eca856ce184fd70cd80d_b.jpg& data-size=&normal& data-rawwidth=&607& data-rawheight=&499& class=&origin_image zh-lightbox-thumb& width=&607& data-original=&https://pic3.zhimg.com/v2-a79eca856ce184fd70cd80d_r.jpg&&&figcaption&双方都要通知对方,已经接受哪一帧的通知了,并通过cmd list重发没有收到的指令&/figcaption&&/figure&&p&这里简单说一下,对于这种收发频繁的消息,如果使用protobuf,会造成每个逻辑帧的GC,这是非常不好的,解决方案,要么对&a href=&https://link.zhihu.com/?target=https%3A//www.cnblogs.com/SChivas/p/7898166.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&protobuf做无GC改造&/a&,要么就自己实现一个简单的byte[]读写。无GC改造工程太大,感觉无必要,我们只是在战斗的几个频繁发送的消息,需要自己处理一下byte[]读写即可。&/p&&p&-----------------------------------&/p&&p&此处补充一下,看到KCP作者&a href=&https://www.zhihu.com/people/skywind3000/activities& class=&internal&&韦易笑&/a&在评论区的留言,提到&a href=&https://link.zhihu.com/?target=https%3A//github.com/skywind3000/kcp/wiki/Network-Layer& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&kcp+fec的模式&/a&,可以比冗余方式,有更好的效果,我之前并没有仔细研究过这个模式,不过可以推荐大家看一下,如果有用过朋友分享下结论就更好了。&/p&&p&因为我们项目早期,服务器定下了使用enet,我评估了一下,反正使用冗余包的方式,所以没有纠结enet或kcp,后续其实想改成kcp,服务器不想再动,也就放下了。&/p&&p&enet麻烦的地方是,&a href=&https://link.zhihu.com/?target=https%3A//github.com/lsalzman/enet/pull/73& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&enet的ipv6版本&/a&,是一个不成熟的pull request,enet作者没有merge(并且存在好几个ipv6的pull request),我不确定稳定性,还好看了下commit,加上测试下来,没有太大问题。KCP我没有评估过ipv6的问题,不过github上有C#版本,改一下ipv6支持应该很简单。&/p&&p&------------------------------------&/p&&ul&&li&&b&逻辑和显示的分离&/b&&/li&&/ul&&p&这块很多讲帧同步的文章都提过了。我在前面讲&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&技能编辑器&/a&的时候,也提过,配置的数据和显示要分离,在战斗中,战斗的逻辑,也要和显示做到分离。&/p&&p&
例如,最基本,我们动作切换的逻辑,是基于自己抽象的逻辑帧,而不是基于animator中一个clip的播放。比如一个攻击动作,当第10帧的时候,开始出现攻击框,并开始检测和敌人受击框的碰撞,这个时候的第10帧,必须是独立的逻辑,不能依赖于animator播放的时间,或者AnimatorStateInfo的normalizedTime等。甚至,当我们不加载角色的模型,一样可以跑战斗的逻辑。如果抽离得好,还可以放到服务器跑,做为战斗的验证程序,王者荣耀就是这样做的。&/p&&ul&&li&&b&联机如何做到流畅战斗&/b&&/li&&/ul&&p&
前面所有的准备,最终的目的,都是为了战斗的流畅。特别是我们这种Act游戏,或者格斗类游戏,对按键以后操作反馈的即时性,要求非常高,一点点延迟,都会影响玩家的手感,导致玩家的连招操作打断,非常影响体验。我们对延迟的敏感性,甚至比MOBA类游戏还要高,我们要做到好的操作手感,还要联机战斗(PVP,组队PVE),都需要把帧同步做到极致,不能因为延迟卡住或者操作反馈出现变化。&/p&&p&
因为这个原因,我们不能用lockstep的方式,lockstep更适合网络环境良好的内网,或者对操作延迟不敏感的类型(例如我听过还有项目用来做卡牌类的帧同步)。&/p&&p&
我们也不能用缓存服务器确认操作的方式,也就是一些游戏做的指令buffer。具体描述,&a href=&https://link.zhihu.com/?target=http%3A//youxiputao.com/articles/11842& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&王者荣耀的分析文章&/a&,讲得很具体了。这也是他们说的模式,这个模式能解决一些小的网络波动,对一些操作反馈不需要太高的游戏,例如有些游戏攻击前会有一个比较长的前摇动作,这类游戏,用这种方式,应该就能解决大部分问题。但是这种方式还是存在隐患,即使通过策略能很好地动态调整buffer,也还是难以解决高延迟下的卡顿和不流畅。王者荣耀优化得很好,他们说能让buffer长度为0,文章只提到通过平滑插值和逻辑表现分离来优化,更细节的没有提到,我不确定他们是否只是基于这个方式来优化的。目前也没有看到更具体的分析。&/p&&p&
指令buffer的方式,也不能满足我们的需求,或者说,我没有找到基于此方式,能优化到王者荣耀的效果的办法。我也测试过其他moba和act,arpg类游戏的联机,在高延迟,网络波动情况下,没有比王者表现更好的了。&/p&&p&
最后,在仔细研究了我们的需求后,找到一篇指导性的文章,非常适合我们。&/p&&p&
就是&a href=&https://link.zhihu.com/?target=http%3A//mauve.mizuumi.net//understanding-fighting-game-networking/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Understanding Fighting Game Networking&/a&,这篇文章非常详细地介绍了各种方式,最终回滚逻辑(rollback)是终极的解决方案,国内也有文章提到过,即&a href=&https://link.zhihu.com/?target=http%3A//www.skywind.me/blog/archives/1343%23more-1343& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Skywind Inside & 再谈网游同步技术&/a&里面提到的Time Warp方式,我理解回滚逻辑,和Time Warp是一个概念。&/p&&ul&&li&&b&游戏逻辑的回滚&/b&&/li&&/ul&&p&
回滚逻辑,就是我们解决问题的方案。可以这样理解,客户端的时间,领先服务器,客户端不需要服务器确认帧返回才执行指令,而是玩家输入,立刻执行(其他玩家的输入,按照其最近一个输入做预测,或者其他更优化的预测方案),然后将指令发送给服务器,服务器收到后给客户端确认,客户端收到确认后,如果服务确认的操作,和之前执行的一样(自己和其他玩家预测的操作),将不做任何改变,如果不一样(预测错误),就会将游戏整体逻辑回滚到最后一次服务器确认的正确帧,然后再追上当前客户端的帧。&/p&&p&
此处逻辑较为复杂,我尝试举个例子说明下。&/p&&p&
当前客户端(A,B)执行到100帧,服务器执行到97帧。在100帧的时候,A执行了移动,B执行了攻击,A和B都通知服务器:我已经执行到100帧,我的操作是移动(A),攻击(B)。服务器在自己的98帧或99帧收到了A,B的消息,存在对应帧的操作数据中,等服务器执行到100帧的时候(或提前),将这个数据广播给AB。&/p&&p&
然后A和B立刻开始执行100帧,A执行移动,预测B不执行操作。而B执行攻击,预测A执行攻击(可能A的99帧也是攻击),A和B各自预测对方的操作。&/p&&p&
在A和B执行完100帧后,他们会各自保存100帧的状态快照,以及100帧各自的操作(包括预测的操作),以备万一预测错误,做逻辑回滚。&/p&&p&
执行几帧后,A,B来到了103帧,服务器到了100帧,他开始广播数据给AB,在一定延迟后,AB收到了服务器确认的100帧的数据,这时候,AB可能已经执行到104了。A和B各自去核对服务器的数据和自己预测的数据是否相同。例如A核对后,100帧的操作,和自己预测的一样,A不做任何处理,继续往前。而B核对后,发现在100帧,B对A的预测,和服务器确认的A的操作,是不一样的(B预测的是攻击,而实际A的操作是移动),B就回滚到上一个确认一样的帧,即99帧,然后根据确认的100帧操作去执行100帧,然后快速执行101~103的帧逻辑,之后继续执行104帧,其中(101~104)还是预测的逻辑帧。&/p&&p&
因为客户端对当前操作的立刻执行,这个操作手感,是完全和pve(不联网状态)是一样的,不存在任何delay。所以,能做到绝佳的操作手感。当预测不一样的时候,做逻辑回滚,快速追回当前操作。&/p&&p&
这样,对于网络好的玩家,和网络不好的玩家,都不会互相影响,不会像lockstep一样,网络好的玩家,会被网络不好的玩家lock住。也不会被网络延迟lock住,客户端可以一直往前预测。&/p&&p&
对于网络好的玩家(A),可以动态调整(根据动态的latency),让客户端领先服务器少一些,尽量减少预测量,就会尽量减少回滚,例如网络好的,可能客户端只领先2~3帧。&/p&&p&
对于网络不好的玩家(B),动态调整,领先服务器多一些,根据latency调整,例如领先5帧。&/p&&p&
那么,A可能预测错的情况,只有2~3帧,而网络不好的B,可能预测错误的帧有5帧。通过优化的预测技术,和消息通知的优化,可以进一步减少A和B的预测错误率。对于A而言,战斗是顺畅的,手感很好,少数情况的回滚,优化好了,并不会带来卡顿和延迟感。&/p&&p&
重点优化的是B,即网络不好的玩家,他的操作体验。因为客户端不等待服务器确认,就执行操作,所以B的操作手感,和A是一致的,区别只在于,B因为延迟,预测了比较多的帧,可能导致预测错,回滚会多一些。比如按照B的预测,应该在100帧击中A,但是因为预测错误A的操作,回滚重新执行后,B可能在100帧不会击中A。这对于B来说,通过插值和一些平滑方式,B的感受是不会有太大区别的,因为B看自己,操作自己都是及时反馈的,他感觉自己是平滑的。&/p&&p&
这种方式,保证了网络不好的B的操作手感,和A一致。回滚导致的一些轻微的抖动,都是B看A的抖动,通过优化(插值,平滑等),进一步减少这些后,B的感受是很好的。我们测试在200~300毫秒随机延迟的情况下,B的操作手感良好。&/p&&p&
这里,客户端提前服务器的方式,并且在延迟增大的情况下,客户端将加速,和&a href=&https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&守望先锋的处理方式&/a&是一样的。当然,他们肯定比我做得好很多。&/p&&p&
希望我已经大致讲清楚了这个逻辑,大家参看几篇链接的文章,能体会更深。&/p&&p&
这里,我要强调的一点是,我们这里的预测执行,是真实逻辑的预测,和很多介绍帧同步文章提到的预测是不同的。有些文章介绍的预测执行,只是view层面的预测,例如前摇动作和位移,但是逻辑是不会提前执行的,还是要等服务器的返回。这两种预测执行(View的预测执行,和真实逻辑的预测执行)是完全不是一个概念的,这里需要仔细地区分。&/p&&p&
这里有很多的可以优化的点,我就不一一介绍了,以后可能零散地再谈。&/p&&ul&&li&&b&游戏逻辑的快照(snapshot)&/b&&/li&&/ul&&p&我们的逻辑之所以能回滚,都是基于对每一帧状态可以处理快照,存储下每一帧的状态,并可以回滚到任何一帧的状态。在&a href=&https://link.zhihu.com/?target=http%3A//mauve.mizuumi.net//understanding-fighting-game-networking/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Understanding Fighting Game Networking&/a& 文章和&a href=&https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&守望先锋网络&/a& 文章中,都一笔带过了快照的说明。他们说的快照,可能略有不同,但是思路,都是能保存下每一帧的状态。如果去处理快照(Understanding那篇文章做的是模拟器游戏,可以方便地以内存快照的方式来做),是一个难点,这也是我&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&前面文章&/a&提到ECS在这个方式下的应用,云风的解释:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-dd9ed3cab5_b.jpg& data-size=&normal& data-rawwidth=&737& data-rawheight=&87& class=&origin_image zh-lightbox-thumb& width=&737& data-original=&https://pic4.zhimg.com/v2-dd9ed3cab5_r.jpg&&&figcaption&云风博客截图,地址https://blog.codingnow.com/2017/06/overwatch_ecs.html&/figcaption&&/figure&&p&ECS是一个好的处理方式,并且我找到&a href=&https://link.zhihu.com/?target=https%3A//www.kisence.com//guan-yu-zheng-tong-bu-de-xie-xin-de/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&一篇文章&/a&,也这样做了(我看过他开源的demo,做得还不够好,应该还是demo阶段,不太像是一个成型的项目)。这篇文章的思路是很清晰的,并且也点到了一些实实在在的痛点,解决思路也基本是正确的,可以参看。&/p&&p&这块,我做得比较早了,当时守望先锋的文章还没出,我的战斗也没有基于ECS,所以,在处理快照上,只有自己理顺逻辑来做了。&/p&&p&我的思路是,通过一个回滚接口,需要数据回滚的部分,实现接口,各自处理自己的保存快照和回滚。就像我们序列化一个复杂的配置,每个配置各自序列化自己的部分,最终合并成一个序列化好的文件。&/p&&p&首先,定义接口,和快照数据的reader和writer&/p&&figure&&img src=&https://pic3.zhimg.com/v2-c12b7e87d2f452b29fdef61_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&388& data-rawheight=&185& class=&content_image& width=&388&&&/figure&&figure&&img src=&https://pic1.zhimg.com/v2-b666f214abd2a804ebcc119b_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&407& data-rawheight=&499& class=&content_image& width=&407&&&/figure&&figure&&img src=&https://pic2.zhimg.com/v2-2f49a68d6fdbdf3ffdfb283_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&470& data-rawheight=&483& class=&origin_image zh-lightbox-thumb& width=&470& data-original=&https://pic2.zhimg.com/v2-2f49a68d6fdbdf3ffdfb283_r.jpg&&&/figure&&p&然后,就是每个模块,自己去处理自己的takeSnapshot和rollback,例如:&/p&&figure&&img src=&https://pic2.zhimg.com/v2-96bbeff404d_b.jpg& data-size=&normal& data-rawwidth=&400& data-rawheight=&254& class=&content_image& width=&400&&&figcaption&简单的数值回滚&/figcaption&&/figure&&figure&&img src=&https://pic2.zhimg.com/v2-eeec5b5d8e7e663a8e06f487_b.jpg& data-size=&normal& data-rawwidth=&700& data-rawheight=&597& class=&origin_image zh-lightbox-thumb& width=&700& data-original=&https://pic2.zhimg.com/v2-eeec5b5d8e7e663a8e06f487_r.jpg&&&figcaption&复制的列表回滚和调用子模块回滚&/figcaption&&/figure&&p&思路理顺以后,就可以很方便地处理了,注意write和read的顺序,注意处理好list,就解决了大部分问题。当然,在实现逻辑的过程中,时刻要注意,一个模块如何回滚(例如获取随机数也需要回滚)。&/p&&p&有一个更简单的方式,就是给属性打Attribute,然后写通用的方法。例如,我早&b&期的实现方案&/b&:&/p&&figure&&img src=&https://pic3.zhimg.com/v2-05fee97a51c743b54d8dcaf_b.jpg& data-size=&normal& data-rawwidth=&471& data-rawheight=&340& class=&origin_image zh-lightbox-thumb& width=&471& data-original=&https://pic3.zhimg.com/v2-05fee97a51c743b54d8dcaf_r.jpg&&&figcaption&给属性打标签&/figcaption&&/figure&&p&根据标签,通用的读写方法,通过反射来读写,就不需要每个模块自己去实现自己的方法了:&/p&&figure&&img src=&https://pic1.zhimg.com/v2-c2adc282fe621f2357e3_b.jpg& data-size=&normal& data-rawwidth=&649& data-rawheight=&375& class=&origin_image zh-lightbox-thumb& width=&649& data-original=&https://pic1.zhimg.com/v2-c2adc282fe621f2357e3_r.jpg&&&figcaption&部分代码&/figcaption&&/figure&&p&这种方法,能很好地解决大部分问题,甚至前面提到的&a href=&https://link.zhihu.com/?target=https%3A//github.com/suzuke/TrueSync/tree/master/Assets/TrueSync/Engine/Math& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Truesync&/a&,也是用的这种方式来做。&/p&&p&但是这种方法有个难以回避的问题,就是GC,因为基于反射,当我们调用field的GetValue和SetValue的时候,GC难以避免。并且,因为全自动,不方便处理一些特殊逻辑,调试优化也不方便,最后改成了现有的方式,虽然看起来笨重一些,但是可控性更强,我后续做的很多优化,都方便很多。&/p&&p&关于快照,也有很多可以优化的点,无论是GC内存上的,还是运行效率上的,都需要优化好,否则,可能带来性能问题。这块优化,有空另辟文章再细谈吧。&/p&&p&当我们有了快照,就可以支持回滚,甚至跳转。例如我们要看战斗录像,如果没有快照,我们要跳到1000帧,就需要从第一帧,根据保存的操作指令,一直快速执行到1000帧,而有了快照,可以直接跳到1000帧,不需要执行中间的过程,如果需要在不同的帧之间切换,只需要跳转即可,这将带来巨大的帮助。&/p&&ul&&li&&b&自动测试&/b&&/li&&/ul&&p&由于帧同步需要测试一致性的问题,对我们来说,回滚也是需要大量测试的问题。自动测试是必须要做的一步,这块没有什么特别的点,主要就是保存好操作,快照,log,然后对不同客户端的数据做比对,找到不同的地方,查错改正。&/p&&p&我们现在做到,一步操作,自动循环战斗,将每一盘战斗数据上传内网log服务器。&/p&&p&当有很多盘战斗的数据后,通过工具自动解析比对数据,找到不同步的点。也是还可以优化得更好,只是现在感觉已经够用了。经过大量的内部自动测试,目前战斗的一致性,是很好的。&/p&&ul&&li&&b&总结&/b&&/li&&/ul&&p&我们现在的帧同步方案,总结下来,就是预测,快照,回滚。当把这些有机地结合起来,优化好,就有了非常不错的帧同步联网效果,无论网络速度如何,只要不是延迟大到变态,都保证了非常好的操作手感。&/p&&p&快照回滚的方式,也不是所有游戏都适用,例如&a href=&https://link.zhihu.com/?target=http%3A//www.skywind.me/blog/archives/1343%23more-1343& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Skywind Inside & 再谈网游同步技术&/a&文章中对此模式(Time warp或Rollback)的缺点,也说明了。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-fb6abdadfb1ce4ce31ec6_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&651& data-rawheight=&106& class=&origin_image zh-lightbox-thumb& width=&651& data-original=&https://pic4.zhimg.com/v2-fb6abdadfb1ce4ce31ec6_r.jpg&&&/figure&&p&如图所述,这种模式不适合太多人的联网玩法,例如MOBA,可能就不太适用。我们最多三人联机,目前优化测试下来,效果也没有太大问题。但是联机人数越多,预测操作的错误可能性越大,导致的回滚也会越多。&/p&&p&&br&&/p&&p&一篇文章,难以讲得面面俱到,很多地方可能描述也不一定明确,并且,个人能力有限,团队人员有限(3个客户端)的情况下,必定有很多设计实现不够好的地方,大家见谅。后续有空,再补充一些细节和优化等,这里同样是希望抛砖引玉,希望看到更多好的方案。&/p&&p&最后,也要感谢我们项目的服务器哥们的技术支持和帮助。&/p&&p&一些有帮助的文章再列一下:&/p&&ul&&li&&a href=&https://link.zhihu.com/?target=http%3A//mauve.mizuumi.net//understanding-fighting-game-networking/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Understanding Fighting Game Networking&/a& &/li&&li&&a href=&https://link.zhihu.com/?target=http%3A//www.skywind.me/blog/archives/1343%23more-1343& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Skywind Inside & 再谈网游同步技术&/a& &/li&&li&&a href=&https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&《守望先锋》回放技术:阵亡镜头、全场最佳和亮眼表现&/a& &/li&&li&&a href=&https://link.zhihu.com/?target=http%3A//youxiputao.com/articles/11842& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&《王者荣耀》技术总监复盘回炉历程&/a& &/li&&li&&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&帧同步:浮点精度测试&/a&&/li&&li&&a href=&https://link.zhihu.com/?target=https%3A//www.gamereplays.org/overwatch/portals.php%3Fshow%3Dpage%26name%3Doverwatch-a-guide-to-understanding-netcode& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&A guide to understanding netcode&/a& &/li&&li&&a href=&https://link.zhihu.com/?target=http%3A//www.10tiao.com/html/255/.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&网游流畅基础:帧同步游戏开发&/a& &/li&&/ul&&p&最后啰嗦一句,如最开始所述,帧同步有很多变种和实现方式,优化方向。有时候,可能不同文章提到帧同步这个术语的时候,里面的意思,可能都有区别,大家需要仔细理清和区分。&/p&
终于要写帧同步这块了,这块很难讲清楚,细枝末节有很多优化点,也有一些不同的优化方向,根据不同项目类型,对操作手感的要求,对联机玩家的个数等,会有不同的难点和痛点。不同的优化方向,优化手法的差异,可能导致一些争论。并且,帧同步,本身也有很多…
&figure&&img src=&https://pic2.zhimg.com/v2-6cdd9a97b8212871fdcec66_b.jpg& data-rawwidth=&1600& data-rawheight=&807& class=&origin_image zh-lightbox-thumb& width=&1600& data-original=&https://pic2.zhimg.com/v2-6cdd9a97b8212871fdcec66_r.jpg&&&/figure&&p&
如果之前大家看过我们的技能编辑器,可以看到,大部分玩家通过按键操作主动触发的技能,都可以通过编辑器实现了,我心里大大地出了一口气,感觉问题处理得差不多了。&/p&&p&
但是大家都知道,一个战斗类的游戏,有很大一部分战斗元素,是角色的被动技能,buffer。我之前做过MMORPG的被动技能系统,MMORPG通常是服务器来处理这块逻辑,客户端相对轻松,我也做过战棋类型的战斗,被动buffer这块,也不算很复杂。&/p&&p&策划列出的被动技能,buffer方面的部分需求如下:&/p&&figure&&img src=&https://pic3.zhimg.com/v2-d51a1dbc4d3c7707cdf8e_b.jpg& data-size=&normal& data-rawwidth=&661& data-rawheight=&765& class=&origin_image zh-lightbox-thumb& width=&661& data-original=&https://pic3.zhimg.com/v2-d51a1dbc4d3c7707cdf8e_r.jpg&&&figcaption&需求示例:1&/figcaption&&/figure&&figure&&img src=&https://pic3.zhimg.com/v2-ecda76dbc60329ebd6bc4db8efcfdacd_b.jpg& data-size=&normal& data-rawwidth=&760& data-rawheight=&791& class=&origin_image zh-lightbox-thumb& width=&760& data-original=&https://pic3.zhimg.com/v2-ecda76dbc60329ebd6bc4db8efcfdacd_r.jpg&&&figcaption&需求示例:2&/figcaption&&/figure&&figure&&img src=&https://pic2.zhimg.com/v2-5def34a36157b8bcc5e1c_b.jpg& data-size=&normal& data-rawwidth=&942& data-rawheight=&794& class=&origin_image zh-lightbox-thumb& width=&942& data-original=&https://pic2.zhimg.com/v2-5def34a36157b8bcc5e1c_r.jpg&&&figcaption&需求示例:3&/figcaption&&/figure&&p&&br&&/p&&p&大家如果仔细看一下这个需求(只列出部分),已经相当复杂了,后续新需求添加后,我感觉,达到了最复杂的MOBA类游戏的80%以上,MOBA类游戏,应该是需求最为复杂的了。&/p&&p&我的考虑是:&/p&&ul&&li&不使用程序暴露接口,让策划根据需求写lua脚本的方式。我对策划能写好lua存疑,也许dota2的开发团队能有很强力的lua脚本策划,但是我们不能苛求我们有这样能力的策划。&/li&&li&不使用excel来配置逻辑,excel难以描述复杂的逻辑和前置后置条件,策划处理逻辑难以配置。excel对数据友好,对配置逻辑不够友好。&/li&&li&要给策划提供一个编辑器处理逻辑,而不是文本配置方式(xml,json等)&/li&&/ul&&p&我感觉需求复杂,仔细研究了很久,希望找到合适的,易扩展的解决方案,也查找了很多文章和资料,并没有找到很详细的,参考性强的内容。&/p&&p&最后,在参考了&a href=&https://link.zhihu.com/?target=https%3A//developer.valvesoftware.com/wiki/Dota_2_Workshop_Tools%3Azh-cn/Scripting%3Azh-cn/Abilities_Data_Driven%3Azh-cn& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Dota2的被动技能设计逻辑&/a&后,逐渐找到了思路。&/p&&h2&&b&对需求在逻辑语义上进行分析,将需求分为几个模块&/b&&/h2&&p&&b&1. 触发事件。&/b&常见的有,当xxx的时候,执行yyy的逻辑。触发条件,是一块逻辑。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-0eb3ce3ca49b_b.jpg& data-size=&normal& data-rawwidth=&420& data-rawheight=&475& class=&content_image& width=&420&&&figcaption&当xxx的事件发生&/figcaption&&/figure&&p&&b&2. 触发前置条件。&/b&例如,当xxx时,&b&&i&有20%概率&/i&&/b&执行yyy逻辑。&/p&&p&
当触发,并达成一定条件的时候,才执行yyy。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-cafa9dc4d102e_b.jpg& data-size=&normal& data-rawwidth=&369& data-rawheight=&532& class=&content_image& width=&369&&&figcaption&触发事件的前置条件&/figcaption&&/figure&&p&&b&
3. 执行的具体逻辑。&/b&就是当xxx时,执行yyy,这里,我整体抽象yyy为buffer,分为持续性buffer和瞬时的buffer。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-4dfedaeb635e3_b.jpg& data-size=&normal& data-rawwidth=&352& data-rawheight=&605& class=&content_image& width=&352&&&figcaption&部分buffer类型。&/figcaption&&/figure&&p&&b& 4. 对什么人添加buffer(执行操作),即目标选择。&/b&例如,当xxx时,&b&&i&对半径内敌人&/i&&/b&,执行yyy。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-be714ddf43b0_b.jpg& data-size=&normal& data-rawwidth=&256& data-rawheight=&248& class=&content_image& width=&256&&&figcaption&目标选择&/figcaption&&/figure&&p&&b&5. 对目标的过滤。&/b&例如,当xxx时,对半径内&b&&i&血量大于50%&/i&&/b&的敌人,执行yyy。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-39cbf549aa60f9efd5d908_b.jpg& data-size=&normal& data-rawwidth=&324& data-rawheight=&340& class=&content_image& width=&324&&&figcaption&过滤条件&/figcaption&&/figure&&p&&b& 6. buffer 的叠加类型。&/b&例如,火焰buff,可叠加xxx层。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-2c0d629c1d4_b.jpg& data-size=&normal& data-rawwidth=&375& data-rawheight=&162& class=&content_image& width=&375&&&figcaption&叠加类型&/figcaption&&/figure&&p&&b&7. 数值类buff的数值类型。&/b&例如给玩家增加攻击,防御,暴击率等等。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-6d540a5a09b4765dea5f3_b.jpg& data-size=&normal& data-rawwidth=&307& data-rawheight=&604& class=&content_image& width=&307&&&figcaption&对角色数值的处理&/figcaption&&/figure&&p&&b& 8. 数值来源。&/b&例如,增加&b&&i&当前攻击力10%的&/i&&/b&防御力。数值可能是需要根据条件计算出来的&/p&&figure&&img src=&https://pic3.zhimg.com/v2-ff30c692c5ed42ce302aac_b.jpg& data-size=&normal& data-rawwidth=&514& data-rawheight=&367& class=&origin_image zh-lightbox-thumb& width=&514& data-original=&https://pic3.zhimg.com/v2-ff30c692c5ed42ce302aac_r.jpg&&&figcaption&数值来源&/figcaption&&/figure&&p&
数值来源这里,可以考虑小部分来自脚本公式。但是我们还是选择处理少量数特殊需求。&/p&&p&&b& 9. buffer 的生效条件。&/b&例如,添加一个buff,持续10秒,过程中,&b&&i&当血量低于10%&/i&&/b&,提升暴击率50%。这种一般是,一个持续性buff,需要某些条件下,才生效。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-e2bfe0c18cdef7b_b.jpg& data-size=&normal& data-rawwidth=&401& data-rawheight=&610& class=&content_image& width=&401&&&figcaption&各种各样的生效条件&/figcaption&&/figure&&p&&b&通过上述的分析,我们基本上可以在结构上还原出需求。根据细分的模块,根据需求添加模块的点,然后策划通过编辑器去组合各个模块,就可以得到需求的效果。后续的扩展,就是主要对以上几个模块的分别扩展。&/b&&/p&&p&&br&&/p&&h2&&b&大体的数据结构(数据class关系),分为被动技能,和buffer两大部分&/b&&/h2&&p&
1&b&. 被动技能部分&/b&&/p&&figure&&img src=&https://pic2.zhimg.com/v2-9a171bae9b6b618bbd330_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&433& data-rawheight=&329& class=&origin_image zh-lightbox-thumb& width=&433& data-original=&https://pic2.zhimg.com/v2-9a171bae9b6b618bbd330_r.jpg&&&/figure&&p&
PassiveSkillCfg:一个被动技能的根,包含多个事件的配置&/p&&p&
PassiveSkillEventCfg_base:一个事件的配置,包含多个前置条件,以及条件达成后触发什么(通常是添加buffer和添加其他被动事件)&/p&&figure&&img src=&https://pic4.zhimg.com/v2-04ea01ce1da65750c77ef_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&920& data-rawheight=&536& class=&origin_image zh-lightbox-thumb& width=&920& data-original=&https://pic4.zhimg.com/v2-04ea01ce1da65750c77ef_r.jpg&&&/figure&&p&
PassiveSkillEventPreConditionCfg_base:各种不同的前置条件&/p&&figure&&img src=&https://pic1.zhimg.com/v2-18e302c263c7cd43453ac5abbbbf7374_b.jpg& data-size=&normal& data-rawwidth=&815& data-rawheight=&725& class=&origin_image zh-lightbox-thumb& width=&815& data-original=&https://pic1.zhimg.com/v2-18e302c263c7cd43453ac5abbbbf7374_r.jpg&&&figcaption&不同前置条件,处理自己是否达成的逻辑&/figcaption&&/figure&&p&对应的被动技能编辑器展示:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-025da4abdaec22b9be10b729e9fcc1a0_b.jpg& data-size=&normal& data-rawwidth=&600& data-rawheight=&535& data-thumbnail=&https://pic4.zhimg.com/v2-025da4abdaec22b9be10b729e9fcc1a0_b.jpg& class=&origin_image zh-lightbox-thumb& width=&600& data-original=&https://pic4.zhimg.com/v2-025da4abdaec22b9be10b729e9fcc1a0_r.jpg&&&figcaption&被动技能编辑器&/figcaption&&/figure&&p&&b&2. buffer部分&/b&&/p&&figure&&img src=&https://pic3.zhimg.com/v2-45a00ad802f4e_b.jpg& data-size=&normal& data-rawwidth=&474& data-rawheight=&274& class=&origin_image zh-lightbox-thumb& width=&474& data-original=&https://pic3.zhimg.com/v2-45a00ad802f4e_r.jpg&&&figcaption&基本的类型&/figcaption&&/figure&&p&BufferCfg_base:buffer类型
&/p&&figure&&img src=&https://pic3.zhimg.com/v2-2a34b17df298b62addd7_b.jpg& data-size=&normal& data-rawwidth=&578& data-rawheight=&426& class=&origin_image zh-lightbox-thumb& width=&578& data-original=&https://pic3.zhimg.com/v2-2a34b17df298b62addd7_r.jpg&&&figcaption&buffer不同类型的扩展&/figcaption&&/figure&&figure&&img src=&https://pic2.zhimg.com/v2-e87f072cb445e7df984b_b.jpg& data-size=&normal& data-rawwidth=&605& data-rawheight=&521& class=&origin_image zh-lightbox-thumb& width=&605& data-original=&https://pic2.zhimg.com/v2-e87f072cb445e7df984b_r.jpg&&&figcaption&持续性buffer的扩展&/figcaption&&/figure&&p&SelectTargetCfg:选择目标&/p&&figure&&img src=&https://pic3.zhimg.com/v2-b73cc479e58a2952c1affc5fb02de729_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&632& data-rawheight=&302& class=&origin_image zh-lightbox-thumb& width=&632& data-original=&https://pic3.zhimg.com/v2-b73cc479e58a2952c1affc5fb02de729_r.jpg&&&/figure&&p&SelectTargetFilter_base:选择目标过滤&/p&&figure&&img src=&https://pic1.zhimg.com/v2-0a564de8aa51a933b831fa7ebc797ef3_b.jpg& data-size=&normal& data-rawwidth=&571& data-rawheight=&709& class=&origin_image zh-lightbox-thumb& width=&571& data-original=&https://pic1.zhimg.com/v2-0a564de8aa51a933b831fa7ebc797ef3_r.jpg&&&figcaption&每个过滤条件处理自己的逻辑&/figcaption&&/figure&&p&
我们以设置数值buffer举例&/p&&figure&&img src=&https://pic3.zhimg.com/v2-c9f0033cf5_b.jpg& data-size=&normal& data-rawwidth=&692& data-rawheight=&198& class=&origin_image zh-lightbox-thumb& width=&692& data-original=&https://pic3.zhimg.com/v2-c9f0033cf5_r.jpg&&&figcaption&设置数值buffer&/figcaption&&/figure&&p&BufferSetValueType:buffer改变的数值类型&/p&&p&BufferValueSourceCfg_Base:数据来源&/p&&figure&&img src=&https://pic3.zhimg.com/v2-ba30a3bd445b3d84b6019_b.jpg& data-size=&normal& data-rawwidth=&681& data-rawheight=&705& class=&origin_image zh-lightbox-thumb& width=&681& data-original=&https://pic3.zhimg.com/v2-ba30a3bd445b3d84b6019_r.jpg&&&figcaption&每个类型只需要根据逻辑返回数值&/figcaption&&/figure&&p&BufferValidCfg_base:buffer的生效条件&/p&&figure&&img src=&https://pic4.zhimg.com/v2-aa36752cbec_b.jpg& data-size=&normal& data-rawwidth=&643& data-rawheight=&693& class=&origin_image zh-lightbox-thumb& width=&643& data-original=&https://pic4.zhimg.com/v2-aa36752cbec_r.jpg&&&figcaption&每个生效条件处理自己的逻辑&/figcaption&&/figure&&p&&br&&/p&&p&每种buffer的逻辑可能不同,根据配置不同的数据,处理不同的逻辑即可,非常方便扩展。&/p&&p&对应的Buffer编辑器演示:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-7eb3f081ae9bab919aec1aa4_b.jpg& data-size=&normal& data-rawwidth=&600& data-rawheight=&535& data-thumbnail=&https://pic4.zhimg.com/v2-7eb3f081ae9bab919aec1aa4_b.jpg& class=&origin_image zh-lightbox-thumb& width=&600& data-original=&https://pic4.zhimg.com/v2-7eb3f081ae9bab919aec1aa4_r.jpg&&&figcaption&buffer编辑器演示&/figcaption&&/figure&&p&&b&以上基本就是整体的思路,主要就是根据逻辑,在语义上,研究划分,将需求划分成每一个块,每一个块可以单独扩展,之后将逻辑整合起来,策划通过编辑器去组合不同的模块逻辑。&/b&&/p&&p&这里之所以说我们要在语义上划分,是因为,如果我们按别的方式去抽象逻辑,策划配置的时候,可能需要一个思维转换的过程。而在语义上划分的话,策划基本是根据他们自己定义的描述(就像他们给出的需求描述),一步一步在编辑器里去配置,在思维上,是顺畅的。&/p&&p&我们的代码设计和编辑器,还是有很多做得不够好的地方,并且根据策划的需求,经过了几次重构和整理,才有了现在的结构。&/p&&h2&&b&在Excel表里,配置数据,编辑器里,配置逻辑&/b&&/h2&&p&在最初的设计,我考虑漏了一点,就是如何在excel里面配置数值,编辑器配置逻辑,将两者分离。当策划提出被动技能和buffer需要升级,例如cd,数值,条件的数值等等,各个数值部分都可能会变,这时候如何配置,1~10级的数值,但是逻辑一样,总不能复制10个出来吧。&/p&&p&后面我们的补救措施(为了兼容之前的配置),是在excel里面,配置每一个buffer id对应的逻辑id(即编辑器里面的逻辑),然后配置1~20个数值参数(20够用了)。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-32fb36f6da3a07d93407e_b.jpg& data-size=&normal& data-rawwidth=&1385& data-rawheight=&796& class=&origin_image zh-lightbox-thumb& width=&1385& data-original=&https://pic3.zhimg.com/v2-32fb36f6da3a07d93407e_r.jpg&&&figcaption&同一个逻辑配置id,不同的参数&/figcaption&&/figure&&p&编辑器里面,配置数值的地方,配置1~20的参数索引,初始化的时候,读取配置的数值。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-4f452bf98d_b.jpg& data-size=&normal& data-rawwidth=&438& data-rawheight=&253& class=&origin_image zh-lightbox-thumb& width=&438& data-original=&https://pic2.zhimg.com/v2-4f452bf98d_r.jpg&&&figcaption&配置1~20的参数索引&/figcaption&&/figure&&p&这个处理方式不是太优雅,如果重新设计,可能专门设计一个数值类,可以选择直接设置,还是读取配置。当前有点积重难返,暂时这样处理了,也能解决问题。&/p&&p&&br&&/p&&h2&&b&最后的总结&/b&&/h2&&p&经过几次整理和修改后,现在成型的这套方式,已经能配制出非常多样的被动技能和buffer,后续的扩展,总体来说是非常方便的,少数特殊逻辑,可能还是需要一些改动。&/p&&p&借助之前做技能编辑器的经验,我们直接使用了protobuf来序列化配置,并借助Advanced Inspector这个插件来处理配置的UI界面。因为不需要显示角色模型等,我们用EditorWindow的方式来做被动技能和buffer的编辑器。&/p&&p&还要说说为什么一定要处理这个编辑器。&/p&&p&
我见过有些项目通过多张Excel表相互映射等方式来做技能配置,可视化太差了,当一个逻辑分散在各处,将难以维护,我们程序不能光考虑自己维护方便,更要考虑策划用起来是否方便。&/p&&p&
用xml可以将逻辑很好地组织在一个node下,通过child node来配置子逻辑,也是不错。但是xml的编辑,很大问题是,不能方便地根据不同的逻辑,自动展示不同逻辑需要配置的属性,策划很难记得住这个逻辑,是否需要这个字段,某些字段,例如需要二进制与或操作的数值字段(如选择角色类型enum),策划将难以处理。而且某些字段,不好查错。&/p&&p&通过编辑器,大大提高了策划的工作效率,再在runtime辅以一些方便的测试方法,这个编辑,测试的流程就很通畅了。通过&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&主动技能编辑器&/a&的配合,主动技能,被动技能,buffer完成了整套战斗核心逻辑的编辑配置。&/p&&p&
文章篇幅有限,无法非常详细地介绍整个系统和编辑器。整个系统还有很多其他的功能点,就不一一介绍了,毕竟那是和项目本身关联更深入。这里只是抛砖引玉了。&/p&
如果之前大家看过我们的技能编辑器,可以看到,大部分玩家通过按键操作主动触发的技能,都可以通过编辑器实现了,我心里大大地出了一口气,感觉问题处理得差不多了。 但是大家都知道,一个战斗类的游戏,有很大一部分战斗元素,是角色的被动技能,buffer。…
&figure&&img src=&https://pic3.zhimg.com/v2-d06e7d92cdd8e7_b.jpg& data-rawwidth=&1374& data-rawheight=&938& class=&origin_image zh-lightbox-thumb& width=&1374& data-original=&https://pic3.zhimg.com/v2-d06e7d92cdd8e7_r.jpg&&&/figure&&p&
先简单演示下技能编辑器的功能。我们最初使用unity版本为5.4.0,现升级到5.6.4p4。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-4bca2de69_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&700& data-rawheight=&452& data-thumbnail=&https://pic2.zhimg.com/v2-4bca2de69_b.jpg& class=&origin_image zh-lightbox-thumb& width=&700& data-original=&https://pic2.zhimg.com/v2-4bca2de69_r.jpg&&&/figure&&ul&&li&&b&
缘起&/b&&/li&&/ul&&p&
在一个新组建的团队里,只有一个策划兼制作人,加我一个客户端(公司美术内包),需要在一个月之内快速出一个ACT动作的demo,demo需要有至少三个角色和若干小兵,demo通过评审会正式立项。当时我并没有做过这类act游戏,对这类游戏所知甚少,在策划的讲解,以及恶补了一些文章后,我们定下了计划:&/p&&p&
1,在半个月内做一个技能编辑器,要支持编辑动作每一帧的攻击受击框,配置按键指令的招式转换,以及各种攻击数据的配置。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-f2eaec9b2c2d4ab2131c59_b.jpg& data-size=&normal& data-rawwidth=&746& data-rawheight=&181& class=&origin_image zh-lightbox-thumb& width=&746& data-original=&https://pic2.zhimg.com/v2-f2eaec9b2c2d4ab2131c59_r.jpg&&&figcaption&为什么要要做技能编辑器?&/figcaption&&/figure&&p&
为了提高后续策划的工作效率,技能编辑器,必不可少。
2,同时制定美术制作动作的规范,以及我们如何切分动作,动作的复用和衔接&/p&&p&
3,第三周做runtime的战斗逻辑,第四周整合资源,编辑器的配置,出demo包&/p&&ul&&li&&b&
计划&/b&&/li&&/ul&&p&时间紧,任务重,我给自己的任务是&b&不加班完成&/b&,并尽可能将编辑器功能提前完成,给后续留出更多的调整和debug的时间,所以,需要:&/p&&p&
1,尽可能和新策划搞清楚需求,做prototype的推演,推演editor下如何配置,runtime如何执行。有经验的程序,会花更多的时间在前期的需求整理和逻辑推演上。&/p&&p&
2,和美术梳理工作流程,让美术可以立刻开始做事情,后续能和我们的编辑器很好地对接。所以,在demo阶段,美术只提供模型fbx和动画fbx,剩下由程序整合。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-118d13bd188ffd76a96e1ba_b.jpg& data-size=&normal& data-rawwidth=&209& data-rawheight=&246& class=&content_image& width=&209&&&figcaption&常用的动画分割方式&/figcaption&&/figure&&ul&&li&&b&
分析和选择&/b&&/li&&/ul&&p&接下来,有几个技术点,需要做出选择:&/p&&p&1,是使用animator 的状态机来做逻辑,还是自己做切换逻辑?&/p&&figure&&img src=&https://pic3.zhimg.com/v2-d7f0c49bd8aaec5aef13d8fb23a0b0f2_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&467& data-rawheight=&178& class=&origin_image zh-lightbox-thumb& width=&467& data-original=&https://pic3.zhimg.com/v2-d7f0c49bd8aaec5aef13d8fb23a0b0f2_r.jpg&&&/figure&&p&最终选择了自己做逻辑切换,核心就是工作流可定制。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-41f29ec12fc1a26ef334c62a66455d34_b.jpg& data-size=&small& data-rawwidth=&615& data-rawheight=&518& class=&origin_image zh-lightbox-thumb& width=&615& data-original=&https://pic1.zhimg.com/v2-41f29ec12fc1a26ef334c62a66455d34_r.jpg&&&figcaption&animator状态机自动生成,最简化,不包含任何逻辑和数据&/figcaption&&/figure&&p&2,是使用unity的GUI来做编辑器,还是UGUI做?&/p&&figure&&img src=&https://pic4.zhimg.com/v2-d03bf1e02c7dceff6cebcb_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&817& data-rawheight=&247& class=&origin_image zh-lightbox-thumb& width=&817& data-original=&https://pic4.zhimg.com/v2-d03bf1e02c7dceff6cebcb_r.jpg&&&/figure&&p&最终选择用UGUI,主要是怕用GUI时间来不及。另一个原因是我希望策划能在一个技能编辑器下做所有的操作,流程一体化不要那种在A prefab上挂个脚本,再在B prefab上挂个脚本,最后把A和B拖到C prefab脚本的field上这种编辑方式,这不利于策划理顺思路。(可对比白鹭引擎一堆Editor和Unity All in one)&/p&&p&3,如何做编辑器数据的序列化?&/p&&figure&&img src=&https://pic4.zhimg.com/v2-cdce3bcb7591ed4cfec8913b_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&928& data-rawheight=&259& class=&origin_image zh-lightbox-thumb& width=&928& data-original=&https://pic4.zhimg.com/v2-cdce3bcb7591ed4cfec8913b_r.jpg&&&/figure&&p&最终选择了自己做序列化,最主要的是数据和资源分离,以及热更新。&/p&&p&4,是使用unity自带的Collider(物理系统)来做碰撞,还是自己做碰撞检测?&/p&&figure&&img src=&https://pic1.zhimg.com/v2-edceefe418f0_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&929& data-rawheight=&128& class=&origin_image zh-lightbox-thumb& width=&929& data-original=&https://pic1.zhimg.com/v2-edceefe418f0_r.jpg&&&/figure&&p&因为我们是横版游戏,所以,按传统的格斗游戏做法,只需要做box,而且是不需要旋转的box,自己实现的复杂度大大降低,那肯定自己做更可控。&/p&&p&简单总结下选择方案的思路:&/p&&ul&&li&没有方案是对所有项目都合适的,选择适合自己项目类型以及开发周期,人员配置的方案&/li&&li&要着重考虑数据和资源的分离,热更新。考虑好和美术,策划的衔接工作流程,这些比逻辑本身更重要&/li&&li&可控性和开源,尽量使用自己熟悉和可控的实现方式,尽量选择开源的插件和工具&/li&&/ul&&p&&br&&/p&&ul&&li&&b&
关于ACT游戏,我的理解&/b&&/li&&/ul&&p&
我作为非act游戏玩家,我理解的act游戏和mmorg,arpg这类游戏的战斗表现上,区别如下:&/p&&figure&&img src=&https://pic2.zhimg.com/v2-c6b75bdf2fa49f94993a19_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&945& data-rawheight=&235& class=&origin_image zh-lightbox-thumb& width=&945& data-original=&https://pic2.zhimg.com/v2-c6b75bdf2fa49f94993a19_r.jpg&&&/figure&&p&所以,arpg是介于act游戏和mmorpg之间,arpg和act游戏,有着重大的区别(按策划的话:市面上太多所谓动作游戏,只能说是arpg)。&/p&&p&act游戏,需要:&/p&&ul&&li&快速的操作反馈,需要玩家衔接好操作,才能输出最大化&/li&&li&招式衔接的丰富,不同招式下,不同时刻,不同按键,要切换到不同的招式&/li&&li&打击反馈的丰富,被击需要有动作反馈,轻中重,浮空等各种击中方式,被击者需要不同的动作反馈&/li&&/ul&&p&以上是一个非act玩家,做为程序开发者的理解,方便我区分当年做MMO的经验。&/p&&p&&br&&/p&&ul&&li&&b&
开动:以数据为基础来搭建框架&/b&&/li&&/ul&&p&编辑器的目的是为了数据,所以,对于技能(这里只包含按键触发的主动技能)的数据组织如下:&/p&&figure&&img src=&https://pic3.zhimg.com/v2-a1bc1cac989b41b4738182_b.jpg& data-size=&normal& data-rawwidth=&593& data-rawheight=&403& class=&origin_image zh-lightbox-thumb& width=&593& data-original=&https://pic3.zhimg.com/v2-a1bc1cac989b41b4738182_r.jpg&&&figcaption&角色数据图&/figcaption&&/figure&&ul&&li&ActorCfg:角色数据的根,包含所有角色相关数据&/li&&li&ActorAttr:基础数据,包含资源prefab id,移动速度,重力,指令起始招式id等&/li&&/ul&&figure&&img src=&https://pic3.zhimg.com/v2-ff538f2f1dece1c2987db2_b.jpg& data-size=&normal& data-rawwidth=&295& data-rawheight=&325& class=&content_image& width=&295&&&figcaption&指令&/figcaption&&/figure&&ul&&li&ActInfo:一个动作的信息,对应美术制作的一个动画(animation)&/li&&/ul&&figure&&img src=&https://pic3.zhimg.com/v2-220a65bc4efe76e485e9c6_b.jpg& data-size=&normal& data-rawwidth=&719& data-rawheight=&319& class=&origin_image zh-lightbox-thumb& width=&719& data-original=&https://pic3.zhimg.com/v2-220a65bc4efe76e485e9c6_r.jpg&&&figcaption&动作列表&/figcaption&&/figure&&ul&&li&FrameInfo:每一帧的信息,一般包含攻击,被击框的信息(position,scale),以及一些复杂逻辑使用的标记&/li&&/ul&&figure&&img src=&https://pic4.zhimg.com/v2-0ad9bdee1f7_b.jpg& data-size=&normal& data-rawwidth=&598& data-rawheight=&448& class=&origin_image zh-lightbox-thumb& width=&598& data-original=&https://pic4.zhimg.com/v2-0ad9bdee1f7_r.jpg&&&figcaption&动作游戏,需要给攻击和被击都打上框,没有被击框的帧,就不会被击中。&/figcaption&&/figure&&ul&&li&SkillInfo:招式信息。每个招式可以由一个或多个动作(ActInfo)组成,并可以选择动作的帧范围,这样最大程度地复用美术动作,并可以由策划自由发挥,组合出新的动画。&/li&&/ul&&figure&&img src=&https://pic1.zhimg.com/v2-e71aedae328fa5a09890_b.jpg& data-size=&normal& data-rawwidth=&600& data-rawheight=&423& data-thumbnail=&https://pic1.zhimg.com/v2-e71aedae328fa5a09890_b.jpg& class=&origin_image zh-lightbox-thumb& width=&600& data-original=&https://pic1.zhimg.com/v2-e71aedae328fa5a09890_r.jpg&&&figcaption&不同的ActInfo(animation)组合出不同的招式&/figcaption&&/figure&&ul&&li&BoxInfo:不同类型的box,不同信息。比如攻击,被击,霸体等不同box的信息不同。&/li&&/ul&&figure&&img src=&https://pic1.zhimg.com/v2-a5ced5f0bc72d8_b.jpg& data-size=&normal& data-rawwidth=&1165& data-rawheight=&473& class=&origin_image zh-lightbox-thumb& width=&1165& data-original=&https://pic1.zhimg.com/v2-a5ced5f0bc72d8_r.jpg&&&figcaption&攻击类box数据&/figcaption&&/figure&&ul&&li&HitInfo:攻击类box,击中以后的数据配置&/li&&/ul&&figure&&img src=&https://pic3.zhimg.com/v2-9fb27cda972d213e2dfb1f763f6b8862_b.jpg& data-size=&normal& data-rawwidth=&622& data-rawheight=&560& class=&origin_image zh-lightbox-thumb& width=&622& data-original=&https://pic3.zhimg.com/v2-9fb27cda972d213e2dfb1f763f6b8862_r.jpg&&&figcaption&当击中后,需要的数据,根据策划的需求来&/figcaption&&/figure&&ul&&li&ChangeCtrl:切换招式的数据。比如在帧范围(0~10内)触发了指令(Up),切换到招式xx&/li&&/ul&&figure&&img src=&https://pic2.zhimg.com/v2-b9f30ec88d6bd05bed1d889b_b.jpg& data-size=&normal& data-rawwidth=&559& data-rawheight=&380& class=&origin_image zh-lightbox-thumb& width=&559& data-original=&https://pic2.zhimg.com/v2-b9f30ec88d6bd05bed1d889b_r.jpg&&&figcaption&change To 招式 id&/figcaption&&/figure&&ul&&li&SkillCtrl:各种技能处理,播放音效,特效,设置速度等等&/li&&/ul&&figure&&img src=&https://pic3.zhimg.com/v2-bb2ba2e46f57bb4cd8db800_b.jpg& data-size=&normal& data-rawwidth=&600& data-rawheight=&281& data-thumbnail=&https://pic1.zhimg.com/v2-bb2ba2e46f57bb4cd8db800_b.jpg& class=&origin_image zh-lightbox-thumb& width=&600& data-original=&https://pic1.zhimg.com/v2-bb2ba2e46f57bb4cd8db800_r.jpg&&&figcaption&各种控制&/figcaption&&/figure&&ul&&li&Trigger:各种判断条件,条件达成,才会执行ChangeCtrl或SkillCtrl&/li&&/ul&&figure&&img src=&https://pic2.zhimg.com/v2-71dcddc88febfae3_b.jpg& data-size=&normal& data-rawwidth=&600& data-rawheight=&281& data-thumbnail=&https://pic4.zhimg.com/v2-71dcddc88febfae3_b.jpg& class=&origin_image zh-lightbox-thumb& width=&600& data-original=&https://pic4.zhimg.com/v2-71dcddc88febfae3_r.jpg&&&figcaption&各种Trigger&/figcaption&&/figure&&p&以上是主要的数据模块,ActInfo主要保存每一帧框的位置缩放信息,SkillInfo保存各种ChangeCtrl和SkillCtrl,并用Trigger来做为生效条件。&/p&&p&后续按策划需求对技能编辑器的扩展,更多是SkillCtrl的添加和Trigger的添加。&/p&&p&通过这一套技能编辑处理,策划可以配置出丰富的表现效果。只要数据组织好了,同一数据,有不同的表现形式,这也是基础的MVC的适用。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-ffbf9f1a157a8a2ede591ea_b.jpg& data-size=&normal& data-rawwidth=&600& data-rawheight=&384& data-thumbnail=&https://pic3.zhimg.com/v2-ffbf9f1a157a8a2ede591ea_b.jpg& class=&origin_image zh-lightbox-thumb& width=&600& data-original=&https://pic3.zhimg.com/v2-ffbf9f1a157a8a2ede591ea_r.jpg&&&figcaption&对于习惯使用类似TimeLine编辑方式的,可以用frameline方式(gif压缩有点糊了)&/figcaption&&/figure&&p&上半部分,就先写到这里。&/p&&p&下半部分,再分享一些:&/p&&ul&&li&序列化数据&/li&&li&编辑器和excel表数据的配合&/li&&li&SkillCtrl 和 Trigger的设计&/li&&li&一些做得还不够好的地方,以及一些思考&/li&&/ul&&hr&&p&更新,加一句,我们编辑器的界面,选项,提示,尽可能用中文。大多数策划的英语水平,大家懂的。&/p&
先简单演示下技能编辑器的功能。我们最初使用unity版本为5.4.0,现升级到5.6.4p4。 缘起 在一个新组建的团队里,只有一个策划兼制作人,加我一个客户端(公司美术内包),需要在一个月之内快速出一个ACT动作的demo,demo需要有至少三个角色和若干小兵,demo…
&figure&&img src=&https://pic3.zhimg.com/v2-eccd5e7d72d8e9ed646b6_b.jpg& data-rawwidth=&600& data-rawheight=&600& class=&origin_image zh-lightbox-thumb& width=&600& data-original=&https://pic3.zhimg.com/v2-eccd5e7d72d8e9ed646b6_r.jpg&&&/figure&&p&在写实类游戏制作时,常需要下雨场景的制作,由于日常生活中几乎所有物体都会被淋湿,所以下雨的制作其实需要考虑的方面有很多,我们将从粒子,材质,脚本控制等方面,分析一下应该如何渲染一个下雨的场景。&/p&&ol&&li&材质高光:&/li&&/ol&&p&Unity的Standard Lighting中,使用GGX作为BRDF的高光算法,GGX具有拖尾感,可以较好的模拟潮湿物体表面的反光效果,首先我们来看一下这张故宫下雨时的照片(图侵删):&/p&&figure&&img src=&https://pic1.zhimg.com/v2-27f6becc57f57a71a6e4dec_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1024& data-rawheight=&683& class=&origin_image zh-lightbox-thumb& width=&1024& data-original=&https://pic1.zhimg.com/v2-27f6becc57f57a71a6e4dec_r.jpg&&&/figure&&p&在普通游客的眼里,地面变得“湿滑”了,然而在渲染工程师的眼里,我们应该将这个“湿滑”的效果在PBR中分成四部分:Smoothness的提高,Specular Color的变亮以及GI Occlusion的降低和法线贴图比重的降低。首先,提高Smoothness是毫无疑问的,要让物体表面光滑首先要降低粗糙度,但是仅仅降低粗糙度是不行的,水停留在物体表面,这时反射光线的是水而不是物体本身,因此物体本身的漫反射会被降低,因此被淋水的物体看起来颜色会变深,根据物理反射定律,物体本身色彩无变化,即光线反射比例无变化的情况下,高光程度应提高,所以会表现出高光率提高的现象。同样,因为积水在物体表面的停留,物体受环境光影响会增大,这时我们应该适当降低Occlusion Map的影响,并降低法线贴图的偏移强度,这些在Unity的Standard Shader中都有提供,不需要手动写Shader,当然,如果要处理一个大场景,希望通过全局变量控制,还是应该手写shader进行精确优化的,因此,希望读者能够不依赖Shader Forge, Unity Surface Shader等辅助功能,独立编写基于PBR Shader。至于反射图像的处理,我们一般通过Reflection Probe, Screen Space Reflection & Planar Reflection等方法实现,这并不在本文讨论的范围内,我们将会在本专栏的其他文章中详细讨论。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-59b25a9ab27c64dce9e7a77d18d94681_b.jpg& data-size=&normal& data-rawwidth=&767& data-rawheight=&369& class=&origin_image zh-lightbox-thumb& width=&767& data-original=&https://pic2.zhimg.com/v2-59b25a9ab27c64dce9e7a77d18d94681_r.jpg&&&figcaption&不同反射率的材质&/figcaption&&/figure&&p&接下来就是雨水的制作了,雨水本身的粒子效果制作虽然属于比较初级的粒子制作,甚至Assets Store上也有大量的资源,但是对美术制作能力有比较高的要求。如果像我一样,美术功底奇差无比,完全可以直接买一个效果实现#手动斜眼#,然后在粒子发射器上绑定一个脚本,使其始终在摄像机上方悬停,可以看到在示例中,我们使用粒子碰撞防止穿帮:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-b35fff00ba6e30aa06705a25eea70697_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&737& data-rawheight=&592& class=&origin_image zh-lightbox-thumb& width=&737& data-original=&https://pic4.zhimg.com/v2-b35fff00ba6e30aa06705a25eea70697_r.jpg&&&/figure&&p&在摄像机第一视角效果如下:&/p&&figure&&img src=&https://pic1.zhimg.com/v2-f97fffcc89ec0_b.jpg& data-size=&normal& data-rawwidth=&1250& data-rawheight=&769& class=&origin_image zh-lightbox-thumb& width=&1250& data-original=&https://pic1.zhimg.com/v2-f97fffcc89ec0_r.jpg&&&figcaption&雨水落在平面&/figcaption&&/figure&&p&到此,一个简单的雨水渲染就出来了,然而,整个画面看起来僵硬死板,这是因为我们没有表现出雨滴打在地上的效果,因此,我们需要模拟一个动态的法线贴图,让地面的法线“动起来”,解决的方法有许多,最简单的方法就是序列帧,在CG软件(如Houdini,Substance Painter等)中制作序列帧并且渲染,然后在Unity中播放,这当然是一种比较简单的方法,但是同样也无法实现真正的实时与随机,我们这里则是使用Unity自带的CommandBuffer进行比较底层的图形绘制,实现随机的雨点特效。&/p&&p&学习过渲染管线基础的朋友都知道,Unity的摄像机其实并不是绘制RenderTexture的唯一方法,它只是封装的比较上层的方法,其实摄像机的工作流程就是(剔除-&绘制网格-&后处理)这三部分,无论是Forward path或是Deferred Shading path,亦或是Unity 2018最新提供的HDRP和LWRP,本质上都是这三部,区别仅仅在于,deferred shading会将光照作为后处理运算,而forward path会直接将灯光信息传入shader中进行光影运算并直接输出色彩,而我们这里并不需要动态剔除,只需要使用command buffer在一个指定的Render Target上进行GPU Instance,使用指定的材质绘制大量面片即可。有朋友问我为何不使用Unity 2017推出的CustomRenderTexture进行绘制,我认为,CustomRenderTexture只是给不会渲染底层的程序提供的一个上层封装,实际功能不如使用Graphics类或CommandBuffer直接进行绘制,后者虽然门槛较高但是功能更加强大,大概相当于美图秀秀和PhotoShop的关系(只是个人看法,别怼别怼)。&/p&&p&首先我们需要手动生成一个正方形Mesh,并将indexBuffer设置为四边形绘制,实现非常简单,代码如下:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-37b39cbebdb_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&691& data-rawheight=&543& class=&origin_image zh-lightbox-thumb& width=&691& data-original=&https://pic4.zhimg.com/v2-37b39cbebdb_r.jpg&&&/figure&&p&由于我们是直接往屏幕上绘制的,所以根本不需要考虑ViewProjectionMatrix的问题,直接用NDC坐标(-1, 1)进行绘制即可,如果直接将这个mesh绘制到RenderTarget上,就是一个覆盖全屏的Mesh。&/p&&p&接下来我们要让这个mesh缩小并且随机分布在RenderTarget上,实现雨滴随机散落的效果,这时候就需要使用矩阵进行变换了,然而,雨滴数量众多,在本例中我们绘制了1023个雨点,所以很难依靠CPU进行迭代绘制,无论是计算还是Drawcall,消耗都是难以接受的。所以我们使用Compute Shader与Gpu Instance进行绘制,大幅度提高运算效率。&/p&&p&首先是Compute Shader,这里不赘述如何使用Compute Shader,只是提供Compute Shader的实现目标与过程。实现目标:生成1023个随机分配位置的矩阵并执行1023个计时器。为何要用计时器呢,原因很简单,当一个雨点散落到地上时,涟漪应该是越来越浅直到消失的,在涟漪消失时更新位置信息,使面片在另一个位置绘制。实现代码如下:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-43e7ab8c887e152df880ec55de52614f_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&894& data-rawheight=&692& class=&origin_image zh-lightbox-thumb& width=&894& data-original=&https://pic4.zhimg.com/v2-43e7ab8c887e152df880ec55de52614f_r.jpg&&&/figure&&p&这里来解释一下这段代码的意义,MatrixBuffer是我们需要使用的1023个坐标矩阵,而timeSliceBuffer则是我们需要使用的计时器,其中float2的x值是计时器数值而y值是计时器速度。_DeltaFlashSpeed则是由脚本传入的每帧的更新,即Time.DeltaTime * X; 然后是两个LocalRand函数,使用魔数运算输出一个伪随机数。其中第一个函数会输出一个(-1, 1)区间的float2随机数,用于随机生成一个平面位置,而第二个函数则会输出一个(0, 1)区间的float随机数,用于生成一个随机的计时器速度。&/p&&p&下面的CSMain函数就比较简单了,当计时器数值&1时,归0并重新生成随机的速度与位置。根据线代基础,矩阵的M03, M13决定了xy轴的位置,M00,M11则决定了xy轴的Scale,而这里为了偷懒,果断省略了雨滴大小的随机,直接用同样大小的面片。&/p&&p&在ComputeShader中运算完毕后,就可以在脚本里获取计算的结果,并且使用运算结果进行绘制了,当然,在此之前我们需要先进行初始化:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-be7a33a26aeabbca1766f_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&763& data-rawheight=&781& class=&origin_image zh-lightbox-thumb& width=&763& data-original=&https://pic4.zhimg.com/v2-be7a33a26aeabbca1766f_r.jpg&&&/figure&&p&这里初始化了Compute Shader,Compute Buffer以及需要用到的GPU Instance材质与高斯模糊材质(之后会用到)。&/p&&p&接下来就是调用Compute Shader并使用CommandBuffer进行绘制:&/p&&figure&&img src=&https://pic3.zhimg.com/v2-31c6ed5ce6_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&729& data-rawheight=&366& class=&origin_image zh-lightbox-thumb& width=&729& data-original=&https://pic3.zhimg.com/v2-31c6ed5ce6_r.jpg&&&/figure&&p&首先,指定renderTarget并初始化为(0.5,0.5,1)也就是标准的法线贴图格式,然后使用Compute Shader输出的矩阵进行Gpu Instance,最后经过高斯高斯模糊,使画面顺滑一些。&/p&&p&有了输入的计时器与输入的矩阵,就可以开始绘制波纹了,波纹绘制实际非常简单,直接用Alpha Blend实现减弱效果,用三角函数实现波动即可,直接上代码:&/p&&div class=&highlight&&&pre&&code class=&language-glsl&&&span&&/span&&span class=&n&&Shader&/span& &span class=&err&&&&/span&&span class=&n&&Unlit&/span&&span class=&o&&/&/span&&span class=&n&&Wave&/span&&span class=&err&&&&/span&
&span class=&p&&{&/span&
&span class=&n&&SubShader&/span&
&span class=&p&&{&/span&
&span class=&n&&Tags&/span& &span class=&p&&{&/span& &span class=&err&&&&/span&&span class=&n&&RenderType&/span&&span class=&err&&&&/span&&span class=&o&&=&/span&&span class=&err&&&&/span&&span class=&n&&Opaque&/span&&span class=&err&&&&/span& &span class=&p&&}&/span&
&span class=&n&&ZWrite&/span& &span class=&n&&Off&/span&
&span class=&n&&ZTest&/span& &span class=&n&&Always&/span&
&span class=&n&&Cull&/span& &span class=&n&&Off&/span&
&span class=&n&&Blend&/span& &span class=&n&&oneMinusSrcAlpha&/span& &span class=&n&&srcAlpha&/span&
&span class=&n&&Pass&/span&
&span class=&p&&{&/span&
&span class=&n&&CGPROGRAM&/span&
&span class=&err&&#&/span&&span class=&n&&pragma&/span& &span class=&n&&vertex&/span& &span class=&n&&vert&/span&
&span class=&err&&#&/span&&span class=&n&&pragma&/span& &span class=&n&&fragment&/span& &span class=&n&&frag&/span&
&span class=&err&&#&/span&&span class=&n&&pragma&/span& &span class=&n&&multi_compile_instancing&/span&
&span class=&err&&#&/span&&span class=&n&&include&/span& &span class=&err&&&&/span&&span class=&n&&UnityCG&/span&&span class=&p&&.&/span&&span class=&n&&cginc&/span&&span class=&err&&&&/span&
&span class=&err&&#&/span&&span class=&n&&pragma&/span& &span class=&n&&target&/span& &span class=&mf&&5.0&/span&
&span class=&err&&#&/span&&span class=&n&&define&/span& &span class=&n&&MAXCOUNT&/span& &span class=&mi&&1023&/span&
&span class=&n&&StructuredBuffer&/span&&span class=&o&&&&/span&&span class=&n&&float2&/span&&span class=&o&&&&/span& &span class=&n&&timeSliceBuffer&/span&&span class=&p&&;&/span&
&span class=&k&&struct&/span& &span class=&n&&appdata&/span&
&span class=&p&&{&/span&
&span class=&n&&float4&/span& &span class=&n&&vertex&/span& &span class=&o&&:&/span& &span class=&n&&POSITION&/span&&span class=&p&&;&/span&
&span class=&n&&float4&/span& &span class=&n&&uv&/span& &span class=&o&&:&/span& &span class=&n&&TEXCOORD0&/span&&span class=&p&&;&/span&
&span class=&n&&UNITY_VERTEX_INPUT_INSTANCE_ID&/span&
&span class=&p&&};&/span&
&span class=&k&&struct&/span& &span class=&n&&v2f&/span&
&span class=&p&&{&/span&
&span class=&n&&float4&/span& &span class=&n&&vertex&/span& &span class=&o&&:&/span& &span class=&n&&SV_POSITION&/span&&span class=&p&&;&/span&
&span class=&k&&float&/span& &span class=&n&&timeSlice&/span& &span class=&o&&:&/span& &span class=&n&&TEXCOORD0&/span&&span class=&p&&;&/span&
&span class=&n&&float2&/span& &span class=&n&&uv&/span& &span class=&o&&:&/span& &span class=&n&&TEXCOORD1&/span&&span class=&p&&;&/span&
&span class=&p&&};&/span&
&span class=&n&&v2f&/span& &span class=&n&&vert&/span&&span class=&p&&(&/span&&span class=&n&&appdata&/span& &span class=&n&&v&/span&&span class=&p&&,&/span& &span class=&n&&uint&/span& &span class=&n&&instanceID&/span& &span class=&o&&:&/span& &span class=&n&&SV_InstanceID&/span&&span class=&p&&)&/span&
&span class=&p&&{&/span&
&span class=&n&&v2f&/span& &span class=&n&&o&/span&&span class=&p&&;&/span&
&span class=&n&&UNITY_SETUP_INSTANCE_ID&/span&&span class=&p&&(&/span&&span class=&n&&v&/span&&span class=&p&&);&/span&
&span class=&n&&o&/span&&span class=&p&&.&/span&&span class=&n&&vertex&/span& &span class=&o&&=&/span& &span class=&n&&mul&/span&&span class=&p&&(&/span&&span class=&n&&unity_ObjectToWorld&/span&&span class=&p&&,&/span& &span class=&n&&v&/span&&span class=&p&&.&/span&&span class=&n&&vertex&/span&&span class=&p&&);&/span&
&span class=&n&&o&/span&&span class=&p&&.&/span&&span class=&n&&timeSlice&/span& &span class=&o&&=&/span& &span class=&n&&timeSliceBuffer&/span&&span class=&p&&[&/span&&span class=&n&&instanceID&/span&&span class=&p&&].&/span&&span class=&n&&x&/span&&span class=&p&&;&/span&
&span class=&n&&o&/span&&span class=&p&&.&/span&&span class=&n&&uv&/span& &span class=&o&&=&/span& &span class=&n&&v&/span&&span class=&p&&.&/span&&span class=&n&&uv&/span&&span class=&p&&;&/span&
&span class=&k&&return&/span& &span class=&n&&o&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&err&&#&/span&&span class=&n&&define&/span& &span class=&n&&PI&/span& &span class=&mf&&18.76&/span&
&span class=&n&&float4&/span& &span class=&n&&frag&/span&&span class=&p&&(&/span&&span class=&n&&v2f&/span& &span class=&n&&i&/span&&span class=&p&&)&/span& &span class=&o&&:&/span& &span class=&n&&SV_Target&/span&
&span class=&p&&{&/span&
&span class=&n&&float4&/span& &span class=&n&&c&/span& &span class=&o&&=&/span& &span class=&mi&&1&/span&&span class=&p&&;&/span&
&span class=&n&&float2&/span& &span class=&n&&dir&/span& &span class=&o&&=&/span& &span class=&n&&i&/span&&span class=&p&&.&/span&&span class=&n&&uv&/span& &span class=&o&&-&/span& &span class=&mf&&0.5&/span&&span class=&p&&;&/span&
&span class=&k&&float&/span& &span class=&n&&len&/span& &span class=&o&&=&/span& &span class=&n&&length&/span&&span class=&p&&(&/span&&span class=&n&&dir&/span&&span class=&p&&);&/span&
&span class=&k&&bool&/span& &span class=&n&&ignore&/span& &span class=&o&&=&/span& &span class=&n&&len&/span& &span class=&o&&&&/span& &span class=&mf&&0.5&/span&&span class=&p&&;&/span&
&span class=&n&&dir&/span& &span class=&o&&/=&/span& &span class=&n&&max&/span&&span class=&p&&(&/span&&span class=&n&&len&/span&&span class=&p&&,&/span& &span class=&mi&&1&/span&&span class=&n&&e&/span&&span class=&o&&-&/span&&span class=&mi&&5&/span&&span class=&p&&);&/span&
&span class=&n&&c&/span&&span class=&p&&.&/span&&span class=&n&&xy&/span& &span class=&o&&=&/span& &span class=&p&&(&/span&&span class=&n&&dir&/span& &span class=&o&&*&/span& &span class=&n&&sin&/span&&span class=&p&&(&/span&&span class=&o&&-&/span&&span class=&n&&i&/span&&span class=&p&&.&/span&&span class=&n&&timeSlice&/span& &span class=&o&&*&/span& &span class=&n&&PI&/span& &span class=&o&&+&/span& &span class=&n&&len&/span& &span class=&o&&*&/span& &span class=&mi&&20&/span&&span class=&p&&))&/span& &span class=&o&&*&/span& &span class=&mf&&0.5&/span& &span class=&o&&+&/span& &span class=&mf&&0.5&/span&&span class=&p&&;&/span&
&span class=&n&&c&/span&&span class=&p&&.&/span&&span class=&n&&a&/span& &span class=&o&&=&/span& &span class=&n&&ignore&/span& &span class=&o&&?&/span& &span class=&mi&&1&/span& &span class=&o&&:&/span& &span class=&n&&i&/span&&span class=&p&&.&/span&&span class=&n&&timeSlice&/span&&span class=&p&&;&/span&
&span class=&k&&return&/span& &span class=&n&&c&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&n&&ENDCG&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&shader非常简单,只是绘制了一个大致效果,最后生成的法线贴图效果如下:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-6b924d8eb13c302f3ab4d762f890b8ef_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&779& data-rawheight=&780& class=&origin_image zh-lightbox-thumb& width=&779& data-original=&https://pic4.zhimg.com/v2-6b924d8eb13c302f3ab4d762f890b8ef_r.jpg&&&/figure&&p&可以看到,这张对密集恐患者非常友好的图片,已经有了深浅不一的涟漪花纹(虽然比较难看),我们将这张renderTarget放到地面上,效果如下:&/p&&figure&&img src=&https://pic1.zhimg.com/v2-0db2187f0eae4a9caaf8_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1887& data-rawheight=&899& class=&origin_image zh-lightbox-thumb& width=&1887& data-original=&https://pic1.zhimg.com/v2-0db2187f0eae4a9caaf8_r.jpg&&&/figure&&p&可以看到,地面已经有了法线的涟漪,最近放假回国探亲,只能用家里的古董笔记本写文章,不过从粒子到动图绘制,在这台古董上也只需要4ms左右的运算时间,drawcall也因为gpu instance的原因并没有额外增加,可以说性能表现比较令人满意。&/p&&p&当然,这只是比较基础的雨水表现,其他丰富的细节与最终的开源实现将会在之后的几篇文章中公布。&/p&&p&最后照例宣传一波Unity图形交流群:&/p&
在写实类游戏制作时,常需要下雨场景的制作,由于日常生活中几乎所有物体都会被淋湿,所以下雨的制作其实需要考虑的方面有很多,我们将从粒子,材质,脚本控制等方面,分析一下应该如何渲染一个下雨的场景。材质高光:Unity的Standard Lighting中,使用GGX…
&figure&&img src=&https://pic2.zhimg.com/v2-b50f151ea570108bfc1044_b.jpg& data-rawwidth=&1936& data-rawheight=&1296& class=&origin_image zh-lightbox-thumb& width=&1936& data-original=&https://pic2.zhimg.com/v2-b50f151ea570108bfc1044_r.jpg&&&/figure&&p&体积光是现实中常见的因丁达尔效应而产生的一种大气现象,文人墨客常用“慵懒的阳光泄下”描绘}

我要回帖

更多关于 虚幻4引擎 unity 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信