![Unity MOBA 多人竞技手游制作教程](https://wfqqreader-1252317822.image.myqcloud.com/cover/974/32435974/b_32435974.jpg)
3.5 英雄选择逻辑实现
3.5.1 基础知识
英雄选择模块的实现包含的内容相对于前边课程较多,涉及的知识点也有很多。为此,在实现英雄选择模块前,首先介绍相关知识点,主要包含5点。
◎生成对象(Instantiate函数)
Instantiate函数是Unity3D中实例化的函数,也是对一个对象进行复制操作的函数。利用此函数可以将模板对象的所有子物体和子组件完全复制,成为一个新的对象。实例化后的新对象拥有与模板对象完全一样的属性,包括坐标值等。模板对象可以是场景中的对象,也可以是一个预制体(Prefab)。下面来看如何利用此函数在场景中生成一个对象。
Step 01 新建一个Test场景,在场景中创建一个对象Cube。
Step 02 创建一个脚本InstantiateTest,挂载到场景中的Cube对象上。
Step 03 打开脚本,定义一个公有变量cube,类型为GameObject并在场景中赋值。
Step 04 利用GameObject.Instantiate重新生成一个对象。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_66_1.jpg?sign=1739284073-Rel0ARBJF58y8K23rXLyy7tCwj1ksHAL-0-ac41ef42521597f31e47ccde70e2f2db)
Step 05 保存脚本,并单击Unity运行按钮,在Hierarchy面板上,出现了一个Cube(Clone)对象,如图3-18所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_66_2.jpg?sign=1739284073-gCFH7u9ETI0pwqREWofCwVTRHZCO1TLA-0-b6f677758c64c0de3d378ae6350afb97)
图3-18
由于创建的对象(Cube(Clone))与原对象(Cube)的所有属性均一致,因此在Game视图中直观上只能看到一个Cube对象,为了在观察时更为便捷,引入一种新的创建对象方式,将Cube作为资源加载到场景中,下面就来介绍资源加载。
◎资源加载(Resources.load)
Resources.load是将物体加载到内存中去,并非直接在场景中显示出来,因此,在利用此资源生成对象时经常会搭配Instantiate。使用这种方式加载资源时,需要先在Assets目录下创建一个名为Resources的文件夹(名称不能改),然后把资源文件放进去,当然也可以在Resources中再创建子文件夹,动态加载时需要添加相应的资源路径。下面来看如何利用此函数加载资源并生成对象。
Step 01 在Test场景中将Cube拖曳到Project视图中制成预制体,并拖曳到Resources文件夹中。
Step 02 创建一个脚本ResourceTest,挂载到场景中Camera对象上。
Step 03 打开脚本,利用Resources.load去加载资源文件夹中的对象。加载时,直接根据资源的名称加载。
Step 04 获取资源后,可以利用Instantiate函数创建资源对象。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_67_1.jpg?sign=1739284073-ZkptvN38xS0et80P4kdV4Fv01rdkZ44P-0-b8eb2eca3a8090de741d346d4526c717)
Step 05 保存脚本,并单击“运行”按钮。在Hierarchy面板上,可以看到多了一个Cube(Clone)对象,并且在Game视图中出现了相应的对象,如图3-19所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_67_2.jpg?sign=1739284073-4jpesUyrL23zgUJAdyUeMUnKf6McpAUv-0-63c0a6f2ce877f864d9f1445f4fd7356)
图3-19
◎事件监听器(UI EventListener)
玩家可以与界面进行交互,因为添加了UI Button等组件。但是如果动态生成的对象想要实现点击事件功能,动态绑定执行函数就比较麻烦了,因此利用NGUI中的UIEventListener为UI对象绑定执行函数。具体操作过程如下。
Step 01 在Test场景中新建一个UI Sprite,并添加一个BoxCollider组件,BoxCollider的大小与UI Sprite大小一致。
Step 02 重新定义一个变量sprite,类型为UI Sprite,并且在函数外部定义。
Step 03 利用NGUI中的UIEventListener为对象添加一个点击事件。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_68_1.jpg?sign=1739284073-uAOKO3X4vzONqKDlvdyArcsyFlJfnIPq-0-f523a2e0a91922f12a179b7ab6cd67df)
Step 04 鼠标点击场景中的图片,在控制台中输出“点击了此对象”。如图3-20所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_68_2.jpg?sign=1739284073-5ORF0ps3qZ6WKZ6DAE3oLzolvA236jeO-0-6ff7947aea6b142652616672b2e10ac6)
图3-20
◎精灵图片(UI Sprite)
游戏中的界面是通过NGUI来搭建的。搭建界面时,图片是通过设置组件中的Sprite(名称)来设置的,如图3-21所示。因此更改Sprite图片时,只需要重新指定图片的名称即可,但是指定的图片必须是当前图集中的图片。具体操作过程如下。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_69_1.jpg?sign=1739284073-17ziRL5QC7SHJuOce9l9sgTPoK9VAp1u-0-a5e5bca6c92f93d567aba92ad7aeb923)
图3-21
Step 01 利用NGUI创建一个Sprite,并为此设置一个精灵图片。
Step 02 在脚本中新建一个变量sprite,类型为UI Sprite,并在场景中赋值。
Step 03 通过重新设置精灵图片的名称可以更新图片。指定图集后,单击Sprite便可以查看所有图片的名称。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_69_2.jpg?sign=1739284073-y87DXyEZWCZ3LzajZ3BG6JISi4buGv8a-0-d5740f05381967939d4f8d9b14f1b641)
Step 04 场景中的图片更新完成,如图3-22所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_69_3.jpg?sign=1739284073-zhSXSXbfU9XlGuT289pmX82uzXUfCGNt-0-c7c9279e90a9c47eb23ca12ef1a22cad)
图3-22
◎场景加载(SceneManager)
游戏中经常会包含不同的场景,会涉及场景的跳转。随着Unity的不断更新,之前的场景加载Application.LoadLevel已经被弃用,而新的场景加载的用法会使用到SceneManager中的函数。下面来看如何利用SceneManager切换场景。具体操作过程如下。
Step 01 创建两个场景:Scene1和Scene2。在第二个场景中添加Label来进行标识,便于两个场景的区分。
Step 02 新建脚本,挂载到Scene1中的Camera对象上,双击打开。利用SceneManager.LoadScene函数去加载场景。参数为要加载的场景的名称。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_70_1.jpg?sign=1739284073-iohCyieZLWfoArXVWjsiPRcAYMMOm0JL-0-a3f1911d2359da255ea9142ac794f3ee)
Step 03 单击File→Build Settings命令,将新创建的场景添加到Scenes In Build中,如图3-23所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_70_2.jpg?sign=1739284073-XduO3Ngl0cwgPd5THbCnFHEjsX3kjsV4-0-dca24451ab20dd8398c962d96ce30282)
图3-23
Step 04 运行后,场景由Scene1切换到Scene2中,如图3-24所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_70_3.jpg?sign=1739284073-yXJDJTa1U0lLLhOjElnMKJiZNrWKL4pJ-0-1dae1b74dc1d89deeb35472cf9810fd4)
图3-24
3.5.2 完善英雄选择
游戏匹配结束后,服务器端进入英雄选择模块,模块中简化了英雄选择的过程。
主要包含下面5个部分:
□ 切换选择界面。
□ 英雄列表。
□ 选择英雄。
□ 确定英雄。
□ 场景切换。
英雄选择的进行是由消息驱动的。在此模块中,服务器端返回了可选英雄的列表、敌方选择信息、我方信息等。客户端根据不同的信息进行处理。消息的接收同样在HandleNetMsg中,消息处理在MessageHandler中,如果有必要可以将消息广播回来,广播回来时定义相关函数进行处理。
◎切换选择界面
客户端处理的第一个消息是eMsgToGCFromGS_NotifyBattleSeatPosInfo,接收到此消息时,定义一个函数来显示选择界面。在函数中将其他界面隐藏,仅将选择英雄的界面显示出来,代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_71_1.jpg?sign=1739284073-w0BlvtrQ9m2ZFoLiAlxUiBHzwsSZTVix-0-0dbe98a134c2ff8b0bc1f585cf82fcfd)
服务器端返回的消息中包含每个队员的位置信息,本游戏只有一对一模式,所以没有涉及所有队员的位置信息。后期课程优化时会利用此消息对所有对象进行排列。
◎英雄列表
第二个要处理的消息是英雄选择列表,进入选择界面后首先要显示可选英雄的列表,英雄列表信息包含在服务器返回的消息体中,在接收到eMsgToGCFromGS_NotifyHeroList的消息时,定义一个函数onNotifyHeroList处理英雄列表。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_71_2.jpg?sign=1739284073-USZt0BJwcbToXLmNtgzlHMMatrGAWtH5-0-533d2c6fb98be40a8359e4590df497a0)
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_72_1.jpg?sign=1739284073-XtPIJBgt9CRN3NYsEYU7woY8EuDTuYFC-0-337284858b2b0a0309471119799c0b75)
返回的消息体中包含着每个玩家的ID,因为不同玩家所用有的英雄是不同的,所以根据玩家的ID去加载可选英雄的配置文件。heroInfo就是根据玩家ID加载的可选英雄的信息,如果加载成功,则显示所有的可选英雄的列表,并加载所有的英雄模型。重新定义两个函数:LoadSelectHero与LoadModel。LoadSelectHero负责显示所有的英雄列表,LoadModel负责加载列表中英雄对应的模型,但是并不在此时显示,而是在点击某个英雄时显示,如图3-25所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_72_2.jpg?sign=1739284073-R7fSCBXJKQQe4szcToKL8pqqSto2ipow-0-771e7149bf240fec780036175670e8d3)
图3-25
● 创建英雄图标
创建英雄图标的整个流程如图3-26所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_72_3.jpg?sign=1739284073-b6CxFkPZ6dz2EsM2f3wzPAfmI36SwDpQ-0-d2e8132116f811c628bde0922d16ee86)
图3-26
LoadSelectHero负责将所有可选英雄图标显示出来,并为所有的英雄图标添加点击事件。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_73_1.jpg?sign=1739284073-rJcNIWZLctvZSUNY72OYSDSlfswUe4N6-0-d47df36d9685dbec41423a81c677d1fa)
mMidShow是选择英雄界面SelectHeroBG下的HeroList,此变量在GameStart中定义完成后,在外部指定。获取UIGrid组件时就是在此对象下获取Grid对象再获取组件的。
生成英雄图标时利用mHeroItem,此对象是一个预制体,预制体目录为Resources/Prefab/Hero1,根据此模板生成英雄图标后重新设置它的大小比例以及头像图片。这里需要注意的是,在设置头像时只要将UISprite组件的图片名称更新便可以重置头像,头像的名称从参数中获取,所以在调用此函数时,第一个参数指的就是头像图片的名称。创建完成之后,利用Grid组件将生成的位置自动重置。
此函数中最重要的内容就是为生成的英雄图标添加点击事件,当玩家点击了某一个英雄时,要向服务器发送消息,通知服务器客户端的选择。所以此事件就是向服务器端发送选择英雄的消息。
● 加载模型
LoadModel函数将模型ID与路径传过来,利用这两个参数去加载模型并生成,但是创建完成后所有的模型都隐藏了,并没有显示出来。最后将生成的模型添加到heroModelTable字典中。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_74_1.jpg?sign=1739284073-htBNHKUMTh8kvUV6oox0x33dyCYzkX9M-0-da31bd7398dc3e14469d8281387c4a25)
heroModelTable是一个字典,定义方式如下所示。其中,第一个参数是模型的ID,第二个参数就是模型本身。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_74_2.jpg?sign=1739284073-P9P2f2RPUKymoQi6ZkIt7IYdSy42guzf-0-11130d8c2df432ae42d7143738c9570e)
◎选择英雄
英雄列表创建完成后,在点击某一个英雄时,客户端向服务器端发送选择英雄的消息。服务器端接收到此消息后返回eMsgToGCFromGS_NotifyTryToChooseHero的消息,接收到此消息时,经过消息的接收与反序列化,在MessageHandler中广播回来。定义一个函数用来处理此消息,代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_74_3.jpg?sign=1739284073-ONoAQFdq4LvSHlvIJ9OrP9n6T384MTaz-0-d3769d07598724d6ea8da8c2999ee727)
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_75_1.jpg?sign=1739284073-i152Hr1ke2YaLK7SmbAlwZQu7ycjEUgX-0-1f84a994c3ba2bbe8165ff91923e6379)
此函数的功能是更新选择英雄的头像并显示选择的模型。在传回的消息体中,包含了玩家的位置pos。pos等于1代表更新我方的英雄,所以当pos等于1时更新右边的头像,并在中间显示模型,模型在之前已经加载出来并被存储在heroModelTable中,可以直接通过消息体中的模型ID获取并显示。
◎确定英雄
玩家选择某一个英雄后,可以通过单击“确定”按钮来通知服务器,定义一个函数并绑定到“确定”按钮上。消息体的类型为SelectHero,利用NetWorkManager中的SendMsg将此消息发送,并在单击“确定”按钮后显示加载界面,隐藏其他的界面。mLoadingUI是加载界面,定义完成后在外指定,指定的是UI Root下的LoadingBG。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_75_2.jpg?sign=1739284073-nNX9UuWoVe38SHmPn1qmJf8nJ37pl9TJ-0-4d98a8449dd07aba561bd76bbdbf02c4)
小提示
“确定”按钮上必须添加BoxCollider组件与UIBtton组件。
◎场景切换
确定英雄时,客户端发送消息通知服务器端已确定的英雄。服务器端接收到消息处理后返回,此时界面处于加载界面,如图3-27所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_76_1.jpg?sign=1739284073-OiLkSPiZRDjcnJtNvVFSH60fMQiKDQO7-0-d0e6e3e51a94ec69cc16ea2b0fdd2bb4)
图3-27
此界面主要为更新加载场景的敌我双方选择英雄的精灵图片,返回的消息体中包含敌我双方的位置信息。所以需要定义一个函数onNotifyEnsureHero来更新敌我双方选择的英雄图标。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_76_2.jpg?sign=1739284073-kbKuPEc58nm1RYRbypdDMQE6nFKHJw50-0-7919a0ddd344b15028c99016a116e8c6)
此函数内容直接分析if-else语句。在这两段代码中,目标就是更新图标。因此,主要步骤就是获取UISprite,并重新设置精灵图片的名称。需要注意的是,名称的获取是通过配置文件读取的,而配置文件读取是根据选择的英雄ID来进行的。英雄头像的ID包含在eMsgToGCFromGS_NotifyTryToChooseHero的消息体中,所以在接收此消息体时,将此消息体保存下来。为什么在接收确定英雄消息中不包含选择英雄的ID呢?原本的游戏在选择英雄时是同时显示敌我双方的选择情况,类似于英雄联盟。而本游戏中在游戏选择时仅显示我方的选择情况,敌方的选择情况是在加载界面中显示的,所以敌方选择英雄的ID包含在上一个消息体中。也因此,在上一个消息体中将选择英雄的消息体获取并保存。
加载完成敌我双方选择英雄的功能后,服务器通过状态判断客户端的进程。服务器端游戏一开局即进行状态改变。比如匹配状态、英雄选择状态、加载状态等,每种状态的转换都会通过发送消息来驱动,发送的消息类型为eMsgToGCFromGS_NotifyBattleStateChange。定义一个函数用来处理此消息。消息体中包含着游戏的状态。当服务器端的状态转换为战斗场景时,客户端便要加载场景了。因为现在本游戏中并没有涉及状态机的问题,所以在处理场景加载时只关心服务器端的状态是否为转换战斗场景,版本优化时会逐渐完善客户端的问题。
消息体中的state值为2时,代表场景加载。所以在此时去加载场景。但是加载场景要考虑一些问题,场景加载后,原场景的对象全部销毁,处理消息接收的GameStart也会销毁。那么在转换场景前先要停止消息的接收并移除消息接收的监听器。代码如下所示。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_77_1.jpg?sign=1739284073-7V9xC1sd8jkV4gLlTu9cd0xfj8FEqrw5-0-9b15de5c76ba00c5bda6d1f9af99834e)
定义一个新的变量mHandleMsg。只有在它打开时才会接收消息。在场景切换时关闭。在Update函数中,将连接服务器端的语句加上判断条件,只有mHandleMsg为true时才会进行连接,代码如下所示(一开始游戏就会连接服务器端,所以在Awake函数中将此值设置为true)。
![](https://epubservercos.yuewen.com/104103/17517092107475806/epubprivate/OEBPS/Images/36590_78_1.jpg?sign=1739284073-ACLa2UL6WnANWeia8YFiaOMzRbgyPBJT-0-09099a47687583e2fe656672e27840ef)
加载场景时利用SceneManager中的异步加载函数LoadSceneAsync,加载完成后返回加载结果。pvp_001是场景的名称,此场景存在于Scenes文件夹中。场景加载完成后一定要通知服务器端加载完成。所以最后要发送LoadComplete消息,服务器端才能返回战斗场景的信息。
小提示
使用SceneManager时,一定要引用命名空间UnityEngine.SceneManagement。