Unity MOBA 多人竞技手游制作教程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

4.2 玩家控制

4.2.1 虚拟摇杆的使用

在游戏中应用虚拟摇杆是很常见的,尤其是格斗类游戏。很多开发者初期都会因为虚拟摇杆的控制烦恼。下面先来介绍如何通过虚拟摇杆控制英雄的移动,本质上是将摇杆移动方向实时通知给控制的英雄对象。其详细实现过程可以分为3步。

Step 01 创建摇杆对象。

Step 02 摄像机设置。

Step 03 实现控制逻辑。

4.2.1.1 创建摇杆对象

在游戏中,摇杆是用NGUI搭建的。创建时需要注意两点:位置显示在屏幕的左下角;摄像机在渲染时,要同时显示场景元素与摇杆控件。

□ 通过NGUI→Create→Panel菜单命令创建一个根节点,如图4-13所示。

图4-13

□ 在UI Root下创建一个空物体,将其命名为VirtualPanel,并在此对象上添加Anchor组件,用于控制子对象的位置,Side属性设置为Bottom Left,如图4-14所示。

图4-14

□ 在VirtualPanel下创建一个空物体,命名为VirtualStick,此对象负责控制摇杆。在此对象上添加BoxCollider(用于触发),大小可以设置为400×380。再添加一个ButtonOnPress组件,用于监听点击事件。

□ 在VirtualStick下创建两个Sprite,分别命名为stick与underpan。stick指的是摇杆中心,图片设置为图集11中的名称为8的图片,underpan指的是背景层,可以设置为120×120。图片设置为图集11中名称为7的图片,大小可以设置为200×200,如图4-15所示。

图4-15

4.2.1.2 摄像机设置

创建UI Root会自动添加一个摄像机。此摄像机负责渲染UI界面。由于界面始终显示在最上层。因此,这个摄像机的深度值总是比场景中的摄像机的深度值要大。如果场景中的MainCamera的深度值为-1,那么此摄像机的深度值为0。

4.2.1.3 实现控制逻辑

摇杆的作用是控制英雄移动。当摇杆中心Stick向某个方向偏移时,英雄也会向同样的方向移动。因此控制英雄移动的关键点就是获取Stick方向。控制脚本已经包含在工程中,学习过程中可参考VirtualStickUI脚本。

(1)创建VirtualStick脚本并挂载到摇杆对象上,主要用来连接摇杆与英雄。

(2)打开脚本,将VirtualStick类继承VirtualStickUI类。父类中控制英雄移动的函数为SendMove,但是父类中的英雄是动态加载的。因此,VirtualStick类中重写SendMove函数。代码如下所示。

(3)SendMove控制英雄的移动。代码中的player指的就是要控制的英雄。定义一个公开的变量player,类型为GameObject,并在Unity编辑器中指定。player的朝向与摇杆的朝向始终是相同的,因此需要获取遥杆的方向,使之与摇杆保持相同的方向。方向设置完成后,英雄移动时便可以使英雄向前移动。打开移动控制的按钮isMove,便可以根据此状态更新英雄的位置。

(4)Update函数的功能是实时更新英雄的位置。当isMove为true时,将英雄Player的位置向前移动。移动的原理:新位置=自身位置+方向*距离(时间*速度)。ani表示英雄的Animation组件,在Awake函数中获取,当英雄移动的时候可以播放移动动画,如果要更改播放的动画可以在此处更改。代码如下所示。

(5)回到Unity编辑器中,在VirtualStick对象的Virtal Stick组件下,为Player指定对象,如图4-16所示。此时运行脚本,可以控制英雄的移动。

图4-16

4.2.2 英雄移动状态

介绍了虚拟摇杆的使用后,将虚拟摇杆添加到工程中去。本地英雄的移动控制依靠虚拟摇杆,但是摇杆控制的英雄并不是静态加载到场景中,而是动态加载的对象。在英雄移动时需要实时通知服务器端英雄所在的位置。SendMove函数只是向服务器端发送消息,然后由服务器端来通知英雄的移动。整个流程可以分为以下3步。

Step 01 摇杆设置。

Step 02 发送移动消息。

Step 03 移动处理。

4.2.2.1 摇杆设置

将摇杆的预制体拖曳到场景中,如图4-17所示。现在要利用摇杆来控制本地英雄的移动。在UI Root父节点下,找到VirtualPanel对象。此对象就是图中左下角显示的摇杆,摇杆下还有一个子对象VirtualStickUI,在此对象上挂载着名为VirtualStickUI的组件,双击打开此脚本。

图4-17

VirtualStickUI是控制摇杆的脚本,主要负责遥杆的关闭显示、移动等。与实例中的摇杆是一样的。现在要利用此脚本来控制模型的移动。在此脚本中找到OnDrag函数。此函数在移动摇杆时会自动调用。代码如下所示。

4.2.2.2 发送移动消息

SendMove函数在移动摇杆时记录了摇杆的位置并设置摇杆状态,代码如下所示。

此函数实现了以下两个功能:

(1)获取英雄模型。

(2)向服务器发送请求移动消息。

英雄模型怎么获取呢?

首先,在Player中定义一个属性RealEntity存储本地玩家模型,代码如下所示。

然后,在GamePlay类中的onNotifyGameObjectAppear函数中为此属性赋值。代码如下所示。

4.2.2.3 移动处理

当服务器端接收到请求移动的消息后,经过计算并返回进入移动状态的消息,在HandleNetMsg中进行消息接收。在MessageHandler中定义函数进行消息解析,如果需要用到Gameplay中的变量,则广播回来,这与GameStart中消息处理的模式相同。广播前主要在GamePlay中注册监听器,绑定消息类型与对应的处理函数。

服务器端返回的消息类型为eMsgToGCFromGS_NotifyGameObjectRunState,在GamePlay中定义一个处理英雄移动的函数。这里主要接收移动消息返回的数据,具体的移动逻辑请参考工程文件,代码如下所示。

返回移动状态的消息体中包含着移动的方向、位置、速度等,将这些数据存储起来,并打开移动的开关isRuning,当isRuning为true时,在玩家的Update函数中调用处理移动的函数OnRunState。

OnRunState函数的主要功能就是控制对象向某个方向移动。在这里只介绍原理,具体函数请参考原文件。OnRunState定义在Player中,是一个虚函数,子类在MyPlayer中进行重写。因为在移动时,本地玩家与敌方的控制是不同的,这里先来介绍本地玩家的移动原理。

小提示

虚函数,又称虚方法,若一个实例方法声明前带有virtual关键字,那么这个方法就是虚方法。

虚方法与非虚方法的最大不同是,虚方法的实现可以由派生类所取代,这种取代是通过方法的重写实现的。

虚方法的特点:

虚方法前不允许有static、abstract或override修饰符。

虚方法不能是私有的,因此不能使用private修饰符。

如何从原位置移动到一个新的位置呢?在这里设计一个移动算法:目标位置=原位置+方向*距离间隔(速度*时间间隔)。获取到新位置后,将此值赋给本地玩家,从而实现英雄的移动。而距离间隔=速度*时间,在获取距离间隔后还要对此值进行判断,如果短时间的距离间隔过大,客户端有可能出现闪现或者同步错误的情况,此时以客户端的位置为准。如果间隔较小,以服务器端传送的位置为准。而且英雄在移动时,本地玩家要更新摄像机的位置。这是本地玩家的移动处理,敌方玩家移动处理在父类Player中,原理是相同的。如果大家感兴趣,可以参考项目源代码文件,或者关注微信公众号“Unity一站学”。

小结

移动处理属于玩家的一个状态,本游戏中并未通过状态机来控制英雄的行为,后期优化过程中会为玩家添加状态机,使英雄的行为更丰满。

4.2.3 英雄自由状态

英雄移动状态的控制简单说就是将摇杆偏移的结果发送给服务器端,服务器端处理后返回。客户端根据返回的数值控制对象移动。当摇杆复原时,英雄又会处于什么样的状态呢?接下来介绍移动结束后的处理过程。

打开VirtualStickUI脚本,如果找不到此脚本,在VS的Solution Explorer窗口中直接输入脚本名称即可快速找到(菜单栏View→Solution Explorer命令)。摇杆的启动通过PressVirtual控制。按下摇杆时,ShowStick函数被调用。松开摇杆时,CloseStick函数被调用。

CloseStick关闭摇杆代码如下所示。读者也可以通过工程源文件进行学习。这里实现移动的过程很简单。首先向服务器发送通知移动的消息,与此同时播放free动画、设置摇杆属性使之处于禁用状态。这是客户端处理移动停止的第一步。因为服务器端接收到消息后依然会返回消息给客户端。所以,第二步就是处理服务器端返回的消息。

打开GamePlay脚本,消息接收同样是在HandleNetMsg中进行,接收到的消息类型是eMsgToGCFromGS_NotifyGameObjectFreeState,与之对应的在MessageHandler中定义一个处理此消息的函数。处理这个消息时,调用了GamePlay中的变量,因此将消息体广播回GamePlay中。GamePlay中注册了处理自由状态的事件,代码如下所示。消息体中包含的所有数据解析出来后,存储在Player中的GOSSI中,等待调用。处理自由状态最重要的就是调整英雄的位置和方向等信息。前边都是数据的解析处理,而真正处理位置相关的功能包含在OnFreeState函数中。因此Player脚本中的OnFreeState函数就是处理相关内容。

英雄自由状态的处理就是为玩家重新设置位置,位置的信息是从服务器端解析出来的数据。在位置更新时,如果玩家真实的位置与服务器端传过来的位置相差太大,则使用服务器端传来的数据,否则直接播放free动画。在此函数开始时,重置血条。如果血条不存在,则再次显示血条。在英雄重生时,会进入此状态,因此在这里进行了血条的重置。

小结

英雄的移动包含移动与停止两部分。相对应的两个处理函数OnRunState与OnFreeState包含在Player中,MyPlayer中重写了这两个函数,负责处理本地玩家的状态。

4.2.4 技能控制

4.2.4.1 英雄攻击实例讲解

玩家控制中技能控制是一大模块,技能的释放通过按钮攻击敌方,实际上攻击就是播放动画并产生特效。攻击的流程可以分为3步:

Step 01 绑定攻击事件。

Step 02 播放动画。

Step 03 生成特效。

◎绑定攻击事件

在UI Root下创建一个空物体,命名为SkillWindow。并为此对象添加Anchor组件,将side设置为Bottom Right,这样可以将此对象下的子物体位置保持在屏幕的右下方。在此对象下创建两个Sprite,并为其指定攻击图片,图集为UIatlas11,图片名称为85和14,如图4-18所示。这两个对象作为攻击按钮,因此在按钮上添加BoxCollider与UI Button。左边为技能攻击按钮,右边为普通攻击按钮。

图4-18

新建一个脚本SkillTest,挂载到SkillWindow上。在脚本中创建两个公开的函数Attack和Skill,并分别绑定到场景中的两个按钮上,如图4-19所示。Attack为普通攻击函数,Skill为技能攻击函数。

图4-19

◎播放动画

普通攻击是每个英雄都拥有的被动技能,在普通攻击时主要是播放攻击动画。先定义一个变量player,并且在Unity中指定。然后获取到玩家上的Animatioan组件。在攻击时分别播放对象的动画。Attack中播放普通攻击的动画,Skill中播放技能攻击的动画。代码如下所示。

小提示

关于动画的高级操作,比如动画状态机、行为树、动画帧事件、动画融合、混合树等,请读者参考视频(http://books.insideria.cn/101/12)。

◎生成特效

当英雄进行技能攻击时,还会生成特效,因此要在技能攻击函数中加载一个技能特效。代码如下所示。

利用Resources.Load与Instantiate函数生成特效。特效是英雄产生的,因此产生特效的位置设置在英雄的位置上。如图4-20所示。

图4-20

小结

英雄攻击的原理是很简单的,游戏中服务器知道每个英雄的当前状态,释放技能只是向服务器发送消息,技能动画的播放与特效的产生是通过服务器返回的消息控制的。下面介绍游戏中攻击的整个流程。

小提示

关于高级的粒子特效内容,比如创建炫酷的烟雾、气流、火焰、涟漪等,请读者参考http://books.insideria.cn/101/13的视频学习。

4.2.4.2 攻击逻辑完善

攻击的原理是:当单击按钮时,客户端向服务器端发送消息,请求攻击,服务器端根据客户端的请求返回消息。这是英雄攻击的原理。完成此功能可以分为3步:

Step 01 按钮攻击事件。

Step 02 释放技能。

Step 03 产生特效。

◎按钮攻击事件

攻击分为技能攻击与普通攻击,它们的区别在于发送的消息类型不同。这里以技能攻击为例,在GamePlay中定义一个按钮绑定事件OnReleaseSkill_1。代码如下所示。

此事件的主要目的是向服务器发送消息请求攻击。但是用哪一个技能进行攻击呢?每个英雄的每一个技能都有不同的ID,因此需要获取技能的ID,才能通知服务器。

ShortCutBarBtnEnum是技能类型的枚举,包含技能1、技能2、自动攻击与改变锁定这4种类型。每个类型代表不同的按钮,比如BTN_SKILL_1代表技能1,在单击技能1的按钮时也是通过枚举类型转换得到技能类型。在获取到技能的ID后便可以通知服务器了。现在问题又来了,每一个技能的ID怎么获取呢?

技能ID是从配置文件中读取的,每一个技能都有不同的ID。首先在玩家Player中定义一个字典,用来存储技能的ID。代码如下所示。

其次,定义一个函数InitSkillDic,主要用来初始化技能列表。每个玩家选择的英雄都有自身的ID。此ID值在Player中定义,并根据显示对象消息中的数据赋值。根据英雄的ID获取英雄配置信息。接着将技能类型与对应技能的ID进行映射,添加在技能字典中。由此根据技能类型来获取技能的ID。InitSkillDic函数在显示英雄模型时就要进行初始化,所以在onNotifyGameObjectAppear函数中调用此方法。代码如下所示。

小提示

(1)配置文件的读取请参考项目源代码。

(2)有关本地存储的知识,比如xml、二进制文件的读取与存储、数据持久化等。请参考视频文件http://books.insideria.cn/101/14。

获取技能ID之后,客户端向服务器端发送消息,通知服务器请求技能的释放。技能发送的消息体封装在EmsgToss_AskUseSkill中,发送的消息体类型为UseSkill。

◎释放技能

服务器端接收到客户端发送的消息之后,返回两个消息。第一个消息负责播放动画,第二个消息负责产生特效。首先来看动画播放的处理过程。OnNotifyGameObjectReleas eSkillState函数用来处理释放技能的消息。此函数体逻辑非常简单,主要调用Player中释放技能的函数OnEntityReleaseSkill,但是释放技能首要条件有敌我双方,且需要设定我方的位置方向等,因此将消息体中的这些数据解析出来为Player中的变量赋值。代码如下所示。

EntityChangeDataOnPrepareSkill函数是在Player中定义设置数据的函数,函数中涉及的变量都是在Player类中定义的有关英雄的属性。将属性赋值后,可以在释放技能时直接调用。代码如下所示。

在Player中定义一个函数OnEntityReleaseSkill,负责处理技能的释放。处理此消息时主要是去播放动画。普通攻击与技能攻击会返回相同的消息进行处理。在处理过程时先要判断是否为普通攻击。技能攻击与普通攻击所播放的动画不同,所做的处理也不同。如果是技能ID,每个英雄释放的技能动画是不同的。因此,根据返回的技能ID获取技能动画的名称,再根据名称进行播放。

◎产生特效

攻击有两个过程,第一个是动画播放,此消息处理完后,便是第二个过程——产生动画特效。在接收到第二个消息时,在MessageHandler中定义一个函数OnNotifySkillModelEmit进行处理。代码如下所示。

在生成特效时,消息体中包含了攻击者与被攻击者的ID、位置等信息。在创建特效时利用这些数据设置特效产生的位置与移动方向。

需要注意的是,在接收到产生特效的消息后,并没有直接处理,而是通过一个协程来完成,为什么这么做呢?因为特效在生成之后需要从源点移动到目标点,每一帧都要进行刷新,所以生成的特效都会调用自身的Update函数进行刷新。返回到登录场景,在登录场景中新建一个空物体,命名为HolyTechGameBase,并为此对象挂载一个组件——HolyTechGameBase,此脚本中包含着更新特效的机制,并且在场景转换时此对象不会被销毁。

小提示

(1)特效创建详细机制包含在框架的EffectManager.cs脚本中,详细处理过程可以参考框架源代码。

(2)后期课程会深入讲解执行协程的原理、协程的状态控制、协程锁定,以及对yield return、IEnumerator和StartCoroutine的深入理解等。有兴趣的读者请参考http://books.insideria.cn/101/15。

(3)当Unity对象互动小时,可以使用协程。CPU需要处理大量计算时,需要使用多线程并行处理,有关多线程的认识与使用、线程同步、线程锁定等,请参考视频http://books.insideria.cn/101/16。

4.2.5 血条处理

4.2.5.1 血条实例讲解

游戏中角色相互的攻击过程带有血量以及能量等相关数值的消耗,这样会让角色更有真实感。

现在通过实例介绍如何生成并控制血条,在Update函数中模拟血条随机减少,让血条数值产生变化。具体操作如下。

Step 01 打开测试场景,拖曳Prefab(Resources/Prefab/HeroLifePlateGreen)到Hierachy列表中UI Root下,创建一个血条实例。如图4-21所示。

图4-21

Step 02 在目录Study/Test下创建heroLifeBar的脚本,脚本中加入游戏对象mPlayerGO,用来保存角色对象的实例。如图4-22所示。加入游戏对象mHeroLife,用来保存血条HeroLifePlateGreen实例。代码如下所示。

图4-22

Step 03 创建两个变量Sprite对象——mpGreenSprite、hpGreenSprite,用来保存HP与MP的Sprite实例。通过UISprite实例的fillAmount属性,可以对血条进度进行设置更新。

Step 04增加两个float的变量保存HP和MP的当前值,在Update中进行修改。代码如下所示。

Step 05 血条要随着角色移动,首先得获取角色的位置,但是血条的位置与角色的位置在高度上有一定的差距,因此,在获得角色的位置后,重新设置y值。在进行坐标系转换后,更新血条在界面中的显示位置。代码如下所示。

Step 06 启动测试场景,可以发现蓝绿条在不断地减少。如图4-23所示。

图4-23

在实际项目中,血条的处理要远比这个复杂。下面对1V1游戏中血条的实现做简单说明。

4.2.5.2 完善血条逻辑

◎血条生成

游戏中血条生成是根据路径动态加载预制体并创建出来,路径根据英雄的ID设置,本地玩家加载绿色血条,敌方玩家加载红色血条。如图4-24所示。血条生成后添加到Player类存储血条的字典中,首先要在Player中创建一个字典。在创建血条时要保留血条背景UISprite,以便后面使用。显示血条的函数在创建对象时一起创建。因此,此函数在onNotifyGameObjectAppear中调用。代码如下所示。

图4-24

血条并非显示在三维坐标中,而是显示在屏幕坐标系统中。在生成血条后还需要对血条的位置进行设置。血条是跟随英雄一起移动的,因此要在Player脚本的Update函数中更新位置。代码如下所示。

将英雄血条的二维坐标转换为三维坐标的函数WorldToUI,已经在上面的例子中实现过,具体代码也可以参考项目源代码。

◎减血效果

血条生成之后,血条值的改变是由消息控制的,当收到攻击或者在基地进行恢复时,服务器端返回HP改变的消息,消息体中包含当前英雄的HP。那么该如何表示血条的值呢?在生成血条时,获取了血条的背景UI,在UISprite中包含着FillAmount属性,此属性的范围值从0到1。由此属性控制血条的改变,如图4-25所示。因此只需要更改FillAmount即可。

图4-25

在进入战斗场景时,每个英雄都会接收到HP的初始值,在此消息体中可以直接设置英雄的最大HP的值。此部分内容请参考项目源代码。当客户端接收到HP改变的消息后,MessageHandler中定义一个函数处理HP的改变,代码如下所示(消息体中包含了英雄ID与当前血条的值,因此直接调用英雄更新血条的函数来改变血条)。

血条的更新与魔法值MP的更新是一样的,这里不再对魔法值的更新进行赘述。想了解具体实现过程,请参考项目源代码或者关注微信公众号“Unity一站学”进行咨询。

4.2.6 死亡处理

4.2.6.1 英雄死亡示例

在游戏战斗过程中,随着英雄生命值的变化,总会出现生命值为0的现象,也就是英雄的死亡状态。下面就来介绍如何设置英雄死亡条件、死亡动画、功能的禁用以及英雄的重生设置。

◎死亡条件设置

由于在实战过程中血条的变化代表了英雄生命值的变化,因此可以根据血条的fillAmount属性值作为判断英雄是否死亡的依据,并把fillAmount为0时的值作为死亡的临界值。

首先,获取控制血条fillAmount值的对象。创建DeadState脚本,并挂载到当前英雄身上。

其次,设置死亡判断条件,当fillAmount为0时,进入死亡状态。在这里,利用Update函数的逐帧执行特性,模拟血量减少状态。

◎死亡动画设置

当英雄进入死亡状态时,播放相应的死亡动画,需要在死亡条件内实施动画的播放。首先获取当前英雄的动画播放器,然后在规定的死亡时间内通过Play函数播放动画,代码如下所示。

◎禁用功能设置

英雄在死亡状态时,除了播放相应的死亡动画之外,其所携带的血条也要隐藏掉。代码如下所示。

◎重生设置

死亡必会伴随着重生,那重生又是如何设置的呢?这里通过计时器来设置重生时间,而在实际工程中,重生时间会由服务器将数据传递过来。当到达重生时间时,英雄位置恢复到初始位置,英雄的所有属性都回到初始值。

4.2.6.2 死亡逻辑完善

在实际项目中,示例中的数据都是通过解析服务器传来的数据实现功能的,因此采取消息驱动的方式实现英雄的死亡与重生。

当英雄血条低于0时,服务器进行计算后返回英雄死亡状态的消息,客户端接收到此消息后,在MessageHandler中定义一个函数进行处理。死亡处理的内容很简单,根据消息体中的英雄ID获取英雄后,调用实体的OnDeadState函数。此函数定义在Player中,同时也在MyPlayer中进行了重写。代码如下所示。

敌我双方对于死亡状态的处理是不同的,比如说本地玩家死亡禁用虚拟摇杆,而敌方玩家则不需要。因此死亡处理时则调用不同的处理函数。代码如下所示。

英雄的重生设置由服务器来控制,当服务器返回显示游戏对象的消息时,重新生成英雄;返回自由状态的消息时,重新设置位置。

小结:英雄的每种状态在子类中都会重写基类的函数,比如移动状态、死亡状态等。对于英雄状态的控制还有很多细节可以进行优化,后期开发过程中会为英雄添加状态机,这样可以更精确地控制英雄。游戏的结算、战斗数据以及箭塔的攻击等后期课程会陆续添加。相关内容请关注微信公众号“Unity一站学”进行咨询。

小提示

(1)在英雄死亡时,整个屏幕画面会通过Shader渲染为灰色。有关Shader计算机图形渲染的内容请读者参考http://books.insideria.cn/101/17。

(2)游戏开发完成后,通过打包发布到各个平台,有关游戏打包涉及的关键技术,比如存储空间管理、加密/解密和数据压缩等,请读者参考视频http://books.insideria.cn/101/18。

本章任务

□ 练习使用Unity地形系统创建地图。

□ 练习使用摇杆控制模型移动。

□ 练习使用播放动画。

□ 练习英雄各个状态的控制。