本文介绍Inception中MISSILE弹道系统的来历和实现思路,仅仅是思路和大致的做法。

起因

2016年4月7日,无意中看到LOL团队介绍其弹道系统的一篇文章:BEHIND LEAGUE'S NEW MISSILE SYSTEM。感觉LOL开发团队用新的弹道系统制作的几个测试技能很有意思:

当然,对其中吐槽旧弹道系统的图片也是深有同感:

对,Inception的bullet系统也是这个样子的,面目全非。

每当接到bullet的需求需要修改其代码时,我都对这团意大利面居然能运行正常深感震惊。

借用LOL一文中的一句话,描述我对inception的bullet系统的感受:

In short, the missile system in League of Legends was unnecessarily complex, tangled inward on itself, and rather fragile.

那么这团意大利面能不能做到LOL的这种效果呢?答案是否定的:

是时候将Inception的bullet弹道系统升级为MISSILE弹道系统了。我采取的方式,也与LOL类似:

The method we chose to accomplish this was to nuke the system and rewrite it from scratch. It's the only way to be sure.

但我不敢nuke它,那会增加策划同学的工作量,因为bullet系统的复杂从配表阶段就开始了。

所以,我选择让bullet自生自灭,并重写一个MISSILE。

思考

具体怎么做呢,LOL一文中只讲解了重新设计的目标,并没有说具体的做法。不过我从维鲁斯心形弹道的图片描述中,捕获到几个关键字:

And with a slight modification to the center spline control point, we add some love to Varus

没错,spline,control point。

再结合LOL一文中的几张演示图片,做法基本就确定了:需要一个弹道曲线编辑器。

旧的bullet系统只支持直线、抛物线,而且都是代码中写死的路径计算公式,要想实现LOL的这种效果,还不知道要在配置表中增加多少个表头字段,代码要复杂成什么样。

那么用什么曲线合适呢?Google了一下,貌似贝塞尔曲线比较合适。

要我说贝塞尔曲线,就是几个控制点控制一条曲线的形状,而且又可以很方便的对应到数学公式。因此对策划和程序都很友好,适合用来做弹道系统。

本文不是贝塞尔曲线的教程,网上太多了,随便列一些参考资料:

实现

基础组件

Inception客户端使用的Unity引擎,插件丰富,本着快速实现、快速验证的原则,去Asset Store找一个方便使用的来用即可:Bezier Curve Editor

不过这个插件有bug,需要自己修掉,具体什么bug记不得了。

服务器代码就需要自己来写了,照着Unity插件的C#代码翻译成C++然后整合到服务器代码中:

sceneobj/object/bezier_curve.cpp
sceneobj/object/bezier_curve.h
sceneobj/object/curve_mng.cpp
sceneobj/object/curve_mng.h

MISSILE系统设计

有了贝塞尔曲线的支持,仅仅是让策划可以自定义投射物的运行轨迹了而已,但实际的投射物技能效果远非沿着一个固定路径从头走到尾这么简单。

就拿龙姬的大招“影刃旋风”来说,其效果描述如下:

甩出旋转刀刃,并进入4秒隐身,隐身结束收回刀刃,并对沿途所有目标眩晕2秒,冷却90秒

可以看到,整个技能有三个阶段:

  • 第一阶段:释放五把飞刀,沿直线飞行指定距离
  • 第二阶段:飞刀在原地停留(停留时间不固定)
  • 第三阶段:飞刀飞向施法者

因此,在MISSILE的设计上,我采用了分阶段的做法,首先定义MISSLE阶段的概念,用来描述MISSILE在某个阶段的行为。行为的类型可随意扩展,由唯一的阶段类型来标识,但一般以下三种基本就足够了:

  • 曲线类型:MISSILE沿着指定的曲线运动。
  • 停留类型:MISSILE原地不动,可自转。
  • 追踪类型:MISSILE追击指定目标。

多个MISSILE阶段组合成一个完整的MISSILE,因此一个MISSILE的行为就是按顺序依次执行其包含的MISSILE阶段。通过对MISSILE阶段的各种组合,可以配置出各种丰富的投射物技能效果。

使用贝塞尔曲线

曲线阶段由贝塞尔曲线定义MISSILE在此阶段的运行轨迹,服务器和客户端均保存定义曲线的数据,两边同时按照固定的轨迹行进。

怎么使用呢?说下Inception的用法:

  • Unity中使用Bezier Curve Editor插件编辑曲线,保存为prefab。
  • 定义一张曲线表,策划同学为每个prefab配置一个曲线ID。
  • 提供曲线导表工具,遍历曲线表,将每条曲线的控制点数据导入到服务器的配置文件中。
  • 服务器开机时,读取并创建所有曲线对象(数量比较少,多的话可以考虑按需创建)。
  • 玩家释放技能时,客户端加载曲线prefab,服务器直接使用创建好的曲线对象。

位置同步

MISSILE的位置同步看似非常简单,只需要同步起点即可。

其实不然,对于多阶段的MISSILE,其后续阶段的准确与否依赖前一阶段的终点的准确性。而服务器和客户端即便保证阶段的初始位置一致,其结束位置和角度也会因为浮点数误差而稍有不同,而且这个误差会在后续阶段被放大。

那么每个阶段结束后,由服务器向客户端同步一次位置不就可以了么?

效果不好,由于网络延迟的存在:

  • 如果客户端等待服务器的阶段结束通知才开始下一阶段,则会有明显的滞留感。
  • 如果客户端不等待通知,直接开始下一阶段,而收到服务器的通知后再同步位置,则会有明显的抖动。

显然,应该使用位置同步的一大法宝——预测,准确的说是服务端预测

  • 在服务器端,每当某一阶段快结束时,服务器提前计算其结束位置并通知客户端。
  • 在客户端,当前阶段结束时,根据服务器下发的位置,修正下一阶段的起始位置。

对于曲线阶段和停留阶段来说,预测的位置是严格准确的,而追踪阶段一般作为MISSILE的最后一个阶段,不需要预测。

效果

模仿维鲁斯的心形弹道,艾希的六边形弹道:

新项目中自娱自乐的“拆”字:

说下“拆”字投射物的实现方法:

  • “拆”的每一笔都是一个Trail Renderer,一共8个MISSILE。
  • 每个MISSILE都有两个阶段
  • 第一阶段:曲线阶段,用贝塞尔曲线制作“拆”字的每一笔,MISSILE沿曲线前进,并且带有搜索目标的功能。
  • 第二阶段:追踪阶段,MISSILE追踪第一阶段锁定的目标。

展望

就一般游戏中需要的运动轨迹,我个人觉得贝塞尔曲线足够了。但其功能确实有限,有些复杂轨迹的制作比较难,比如说螺旋曲线。甚至有些简单轨迹贝塞尔曲线也不是特别擅长,比如说标准圆。

因此,MISSILE系统存在很大的可改进空间,我的想法是,不仅支持贝塞尔曲线,而是直接支持用任意数学公式来定义曲线。