基于Unity3D的网络多人对战策略游戏的开发与实_第1页
基于Unity3D的网络多人对战策略游戏的开发与实_第2页
基于Unity3D的网络多人对战策略游戏的开发与实_第3页
基于Unity3D的网络多人对战策略游戏的开发与实_第4页
基于Unity3D的网络多人对战策略游戏的开发与实_第5页
已阅读5页,还剩85页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

摘要在如今的互联网时代影响下,人们的生活交往方式产生了变化。在学习工作的时间之外,游戏娱乐已经成为人们休闲娱乐的主要方式之一,尤其是在互联网高速发展的今天,网络游戏成为了越来越多人的选择。网络游戏不仅是人们放松身心的娱乐方式,也是沟通交友的有效途径,网络游戏已然成为人们休闲之时的很好选择,网络游戏的发展越来越受到重视。Unity3D是目前最流行的游戏开发引擎之一,以其低成本与高效率被多数游戏开发者所使用。本文主要描述了使用Unity3D开发基于网络的多人策略放置类型游戏的实际开发过程,以及开发过程中所应用到的网络连接与信息交流的原理与方法。在具体的开发流程中,主要描述了使用Unity3D作为开发引擎的游戏客户端的开发,随后实现基于TCP的服务端的简单实现,以及服务端与客户端进行交流的实现,然后会实现游戏热更新的相关步骤,最后进行游戏的打包与功能的测试。为后续的同类型游戏开发提供借鉴与参考。关键词:Unity3D;策略类游戏;网络游戏

AbstractUndertheinfluenceoftheInternetera,people'swayoflifeandcommunicationhaschanged.Inadditiontostudyandworktime,gameentertainmenthasbecomeoneofthemainwaysofpeople'sleisureandentertainment,especiallyintherapiddevelopmentoftheInternettoday,onlinegameshavebecomemoreandmorepeople'schoice.Onlinegamesarenotonlyawayforpeopletorelaxphysicallyandmentally,butalsoaneffectivewaytocommunicateandmakefriends.Onlinegameshavebecomeagoodchoiceforpeopleintheirleisuretime,andthedevelopmentofonlinegameshasattractedmoreandmoreattention.Unity3Disoneofthemostpopulargamedevelopmentengines,whichisusedbymostgamedeveloperswithitslowcostandhighefficiency.Thispapermainlydescribestheactualdevelopmentprocessofdevelopingnetwork-basedmultiplayerstrategyplacementtypegameswithUnity3D,andtheprinciplesandmethodsofnetworkconnectionandinformationexchangeappliedinthedevelopmentprocess.Inthespecificdevelopmentprocess,itmainlydescribesthedevelopmentofthegameclientwhichUSESUnity3Dasthedevelopmentengine,thenrealizesthesimpleimplementationoftheserverbasedonTCP,aswellasthecommunicationbetweentheserverandtheclient,thenrealizestherelevantstepsofthegamehotupdate,andfinallycarriesoutthepackagingandfunctionaltestofthegame.Itprovidesreferenceforthefollowingdevelopmentofthesametypeofgames.Keywords:Unity3D;Strategygame;Networkgame

目录第一章 绪论 绪论选题的目的和意义随着人民生活水平的提高和互联网终端硬件设施的改善,更多的人选择在线上游戏里释放生活中的压力,为自己的生活增添更多的乐趣,游戏产业因此飞速发展。在众多不同类型的游戏中,人们更趋向于选择网络游戏,所以网络游戏占据绝对主体地位。选择开发一款网络游戏能够获得更大的市场需求和更多的机会。计算机的持续普及和网络技术的不断发展,为网络游戏的发展提供了硬件基础和技术支持。一款好的游戏开发引擎可以帮助开发者快速将游戏构想付诸实现。在众多游戏开发引擎中的Unity3D,以它的多平台开发、上手难度低等特点脱颖而出。选用Unity3D作为开发引擎,可以帮助开发者更加便捷、高效地开发出一款完整的游戏。由于Unity3D游戏开发的成本较低、周期较短、适应不同终端平台、支持多种游戏类型开发等特点,是许多游戏开发公司的首选,因此Unity3D开发等相关岗位拥有比较广阔的市场和发展前景。研究现状随着互联网技术和终端硬件的发展与完善、人们生活水平的提高,游戏市场越来越受到重视,游戏产业高速发展。2019年全球数字游戏总营收高达1201亿美元,其中的移动端游戏营收644亿美元。移动端游戏的收入占免费游戏营收的比重高达74%,在这其中腾讯的王者荣耀稳定强劲的营收占据了较大比重。这也体现了如今的游戏产业中,移动端网络游戏是比较受欢迎,有较大的市场和前景。论文组织结构论文从最初的构思到最终的完成,结合应用了游戏开发与网络交流相关的知识和技术,成功实现了一个基于Unity3D的网络多人策略游戏的开发,其中具体介绍了游戏框架设计以及游戏制作的关键步骤,也包括游戏的系统功能设计和一系列代码的编写。论文分为六章,各章节安排如下: 第一部分:绪论,主要阐述了该毕业设计制作的背景、相关课题的研究现状以及本设计的大概介绍; 第二部分:介绍了本文制作游戏使用的各类开发工具和Unity3D的大致介绍;第三部分:介绍游戏流程设计;第四部分:游戏实践开发的关键步骤; 第五部分:对游戏进行打包并测试;第六部分:进行最终总结,回顾游戏的缺陷以及改进方法。

开发工具介绍Unity3D游戏引擎Unity3D是一款3D跨平台次时代游戏引擎。作为一款专业的游戏开发引擎,它有资深技术团队的支持、友善的图形化界面、丰富的拓展插件和多平台的导出配置。Unity降低了游戏开发门槛,引擎的上手难度较小,对新手比较友好。使用Unity开发游戏的成本低,开发周期较短,是大多数游戏公司和游戏开发者的最佳选择。C#开发语言C#是微软公司发布的一种运行于.NETFramework和.NetCore上的高级程序设计语言,是由C和C++衍生出来的面向对象的编程语言,拥有较高的运行效率和较强的操作能力,因为其创新性的语言特性以及面向组件的编程支持,成为了Unity3D开发选用的主流语言。C#不仅能用于开发传统Windows环境中的应用程序,还可以用来开发原生的Android、iOS、WindowsPhone和MacApp应用程序,甚至还能整合Azure或Hadoop技术开发云计算和大数据应用系统。[5]C#也提供了网络编程相关类库,因此也可被用于服务端的开发。VisualStudio2017VisualStudio2017(以下简称VS)是微软公司推出的集成开发环境。提供强大的调试和性能分析工具,快捷地查找和修复错误的方式,强大的库调用和云集成,为Unity3D游戏开发提高了效率。MySQL数据库MySQL是由瑞典MySQLAB公司开发的一个关系型数据库管理系统。因为其关系型的特点,存储时将数据保存在不同的表中,提高了读写速度和整个数据库的灵活性。也拥有体积小、速度快、运营成本低等优点,是中小型项目开发时数据库使用的较好选择。Lua开发语言Lua是由巴西里约热内卢天主教大学里的一个由RobertoIerusalimschy、WaldemarCeles和LuizHenriquedeFigueiredo三人所组成的研究小组于1993年开发的一个小巧的脚本语言,该脚本语言设计之初就是为了嵌入到应用程序中,为其提供扩展和定制等功能。由于C#是预编译语言,在手机端的热更新上不太兼容,所以使用Lua语言嵌入C#实现游戏代码的热更新。

游戏设计游戏玩法设计本游戏是根据目前比较流行的策略放置类型游戏:自走棋,进行设计和开发的。在游戏中,玩家有金币栏、生命值和经验栏。每一小局系统会给玩家若干金币,提供5个英雄让玩家购买,玩家通过点击UI使用金币购买英雄、刷新英雄或提升自身的等级,通过卖出英雄获得金币。玩家收集不同的英雄搭配,为英雄佩戴装备,提升自己的阵容强度,在对战时刻来临时,对战双方的英雄阵营进行自动对战,失败一方扣除生命。玩家的生命值小于等于0,就淘汰出局,游戏一直持续至出现唯一存活下来的胜利玩家。游戏热更新在游戏以及上架后,如果我们发现游戏中有Bug或者想要修改游戏中的模型贴图甚至是发行DLC等等,我们可以选择在Unity3D编辑器中修改,然后再打包后发行。当是这样操作后,游戏就得重新花费时间进行审核,玩家也得重新下载整个游戏。这就会造成玩家的流失,尤其在手机玩家的流失方面更为严重。因此游戏热更新应运而生。游戏热更,只需下载需要更新的部分覆盖旧的部分,极大的降低了游戏更新地代价。尤其在网络游戏方面,如果出现一个BUG可能会破坏游戏平衡甚至导致整个游戏的终结,热更新的及时性,可以降低游戏修复的时间和难度。在每次打开游戏的时候,都会进行更新检查,如果发现新的版本,就会进行热更新。图3-1游戏进行热更新游戏登录界面在热更新完成之后,就会进入游戏登录界面。玩家可以在登录界面进行账号注册和登录。登录及注册的内容会通过协议发送到服务端,服务端解析后,通过SQL语句访问数据库,完成玩家的操作。图3-2游戏登录界面游戏大厅及房间在玩家成功登录后,会进入游戏大厅。大厅会显示出所有的游戏房间。左侧为玩家的账号信息,右侧为大厅界面,提供创建房间和刷新界面的按钮。玩家可以创建房间或者进入别人的房间进行游戏。图3-3游戏大厅界面当玩家进入一个房间后,游戏会切换至房间界面。房间界面提供了离开、添加机器人玩家、开始游戏三个按钮。一个房间最多可以进入8位玩家,当房间内的玩家数量至少为2名时,才能进行游戏。图3-4房间界面游戏流程英雄和装备购买房主点击开始游戏按钮后发送开始游戏协议到服务端,服务端接收到消息后在派送给所有客户端,客户端接收到开始游戏协议后,开始加载游戏资源,当资源加载完成后,发送完成协议到服务端,服务端收集到全部的协议后,广播正式开始游戏协议,所有玩家进入游戏界面。通过点击游戏右下角的棋篓按钮可进入英雄和装备购买界面,英雄购买界面显示英雄的价格、羁绊等信息。玩家通过购买英雄和装备并使用,从而与其它玩家进行对战。图3-5英雄和装备购买界面玩家点击英雄后进行购买,购买的英雄会出现在等待区域,通过拖拽方式将英雄放置到战斗区域,拖拽时会出现绿色的UI提示可放置位置。购买后的装备会出现在左下角的仓库内,拖拽仓库内的装备至英雄身上进行装备。图3-6拖拽英雄至战斗区域图3-7拖拽装备至英雄身上英雄羁绊在游戏画面的左侧有英雄羁绊界面,玩家将购买后的英雄放置到战斗区后,系统会统计战斗区域英雄的羁绊组合,将羁绊组合显示在羁绊界面并为英雄增加相应的属性,玩家可以通过不同的羁绊组合实现属于自己的英雄阵容,游戏的可玩性得到提升。图3-8羁绊界面升级英雄当玩家购买且集齐三个同等级同类型的英雄后,可以点击英雄属性界面的升级按钮,将三个一级英雄升级为一个二级英雄,获得更高的英雄属性,更少的位置占用。图3-9英雄升级画面战斗阶段当游戏画面上方的倒计时结束后,进入战斗阶段,战斗区域的双方英雄进行战斗。战斗阶段结束后,又会回到准备阶段。玩家需要不断调整自己的英雄组合和装备,确保自己赢得更多的战斗。图3-10英雄战斗画面结算界面每个战斗阶段结束后,如果任何一方有英雄存活,就会对对方玩家造成基于英雄数量的伤害。在游戏画面的右方是游戏中玩家的生命值,当某位玩家的生命值为0时,就表示失败,不再继续参加游戏。当只剩下一位玩家存活时,这位玩家就是胜利者,同时游戏画面会切换至结算画面。结算界面会根据玩家们游戏失败时的时间长短,显示玩家们的名次,对应增加胜败次数。图3-11结算画面在游戏结算后,玩家可以在结算界面查看自己的名次及胜负场次。然后可以点击游戏画面右下角的返回大厅按钮,返回到游戏大厅,继续进入房间进行游戏,或是退出游戏。

游戏实现UI界面的实现UI界面框架PureMVC是一个基于MVC模式建立的轻量级框架,实现了经典MVC设计模式中的Model、View、Controller三层分离的方式,有利于减少应用程序与视图间的依赖,降低整个程序的耦合程度。PureMVC框架非常适用于UI界面的构造。UI视图、数据和控制命令间的解耦,有利于UI界面的管理和更新。图4-1PureMVC框架的结构图PureMVC大致分为四大部分:View:由具体的UI组件和对应的Mediator组成。UI组件只负责UI的获取。Mediator负责接收操作UI组件的命令和对UI组件进行具体操作。Model:由Data和Proxy组成。Data保存了View层的数据,可以是本地的或是下载得到的数据。Proxy是Data的封装层,负责Data的获取和操作。Controller:由自己编写的许多Command组成,通过这些Command在其它两层之间进行交互,或是MVC系统外部对内部的访问。Façade:是整个PureMVC体系的唯一外部接口,外部通过Façade和对应的Command对内部进行访问。所有的Mediator、Proxy、Command都要在Façade内进行注册和删除。UI子物体的获取在Unity的的UI交互设计中,初学者通常是在预先编写的脚本内,通过Find或GetChild获取到UIPanel下的子物体进行逻辑设计。当我们在游戏开发中后期,要对游戏UI进行修改调整时,如调整UI的层级或是添加新的子物体,原本编写好的代码就无法获取到UI子物体,重新编写脚本就显得耗时耗力。所以可以使用一种便捷地获取需要进行使用的UI子物体的方式。在制作UIPanel之初,我们可以把需要使用的UI子物体的名称进行修改添加标记,如“Button_”,在所有子物体的名称后添加“_”。使用一个单例类UIManager,存放一个键值对为父节点的名称和所有需要使用的子物体,类型为Dictionary<string,Dictionary<string,Transform>>的字典;以及对应该字典的添加、移除和获取方法。在每个UIPanel的初始化时,遍历自身的所有节点,就能根据标记获取到所有需要的子物体,保存到字典内。使用这种方式后,我们只需要知道父物体的名称,该物体的名称,以及该物体的类型,就可以根据获得的transform得到对应的组件。当我们调整子物体的层级时,原脚本无需修改;而当添加新物体时,只需要根据模板进行获取UI。GameObject

root

=

skin.gameObject;

Transform[]

allChild

=

root.GetComponentsInChildren<Transform>();

foreach

(var

i

in

allChild)

{

if

(.EndsWith("_"))

{

UIManager.Instance.AddUI(,

,

i);

}

}

UI界面流程控制和管理在所有父UI的层面上,我们也应该进行管理;使用类PanelManager和BasePanel组成界面管理。BasePanel作为所有Panel的基类,与UIManager和PureMve相结合。在OnShow中预先编写之前的UI子物体获取保存方式,每个Panel在OnShow时自动运行。在MVC的表现上:Panel脚本和其相互对应的Mediator和作为UI界面的View层,对应的Proxy和Data作为Model层,作为两层间交互的控制命令作为Controller层,其实例在之后UIPanel的实际开发中体现。Panel继承BasePanel并重写OnInit、OnShow、OnClose三个虚函数,编写各自在不同情况下的逻辑;保存了SkinName和Skin,根据SkinName加载对应的UIPanel游戏物体。 public

class

BasePanel

:

MonoBehaviour

{

//皮肤路径

public

string

skinName;

//皮肤

public

GameObject

skin;

//层级

public

PanelManager.Layer

layer

=

PanelManager.Layer.Panel;

public

SceneEnum

SceneIndex;

public

int

itemIndex;//如果是子面板,索引

public

PanelManager.CanvasEnum

canvasIndex

=

0;//画布索引

//初始化

public

void

Init(Transform

rootLayer,

Dictionary<string,

BasePanel>

panels,

object[]

para)

public

void

Init(Transform

rootLayer,

List<BasePanel>

childrenPanels,

object[]

para)

//关闭

public

void

Close()

//初始化时

public

virtual

void

OnInit()

//显示时

public

virtual

void

OnShow(object[]

para)

//关闭时

public

virtual

void

OnClose()

}

PanelManager作为所有Panel的管理类,存储并管理所有的UI界面和子UI界面。提供界面的打开和关闭,并控制UI界面在其生命周期内的运行逻辑。public

static

class

PanelManager

{

//Layer

public

enum

Layer

public

enum

CanvasEnum

//层级列表

private

static

Dictionary<Layer,

Dictionary<int,

Transform>>

layers;

//面板列表

public

static

Dictionary<string,

BasePanel>

panels

=

new

Dictionary<string,

BasePanel>();

public

static

Dictionary<GameObject,

List<BasePanel>>

childPanels

=

new

Dictionary<GameObject,

List<BasePanel>>();

//结构

public

static

Transform

root;

public

static

List<Transform>

canvas;

public

static

FirstFacade

firstFacade;

public

static

SecondFacade

secondFacade;

//初始化

public

static

void

Init()

//打开面板

public

static

void

Open<T>(params

object[]

para)

where

T

:

BasePanel

public

static

void

AddChildPanel<T>(GameObject

parent,

object[]

para)

where

T

:

BasePanel

//关闭面板

public

static

void

Close(string

name)

public

static

void

Close<T>()

where

T

:

BasePanel

public

static

void

RemoveChildPanels(GameObject

parent)

public

static

void

RemoveChildPanelSelf(GameObject

parent,

string

Name)

}

UI界面是一个优秀游戏中最基础的、最不可获取的部分,是玩家更直观地游玩游戏地依托。为了更好地UI界面的管理和维护,使用了基于PureMVC框架实现的UI界面设计,因此需要有View、Model、Command三层分离的实现方式。接下来以游戏中的英雄羁绊界面为例,进行UI界面实例的实现。View层的实现HeroTypePanel创建脚本HeroTypePanel继承BasePanel,重写基类的OnInit、OnShow、OnClose三个方法。在OnInit中,调用基类的OnInit的多态实现的另一个同名方法,填入参数分别为:SkinName(UI界面的GameObject的名称,用于加载UI界面的图形化表示的实例到游戏中),SceneIndex(GameObject加载时需要的参数,表示来源于哪个资源包),PanelIndex(表示将这个UI界面添加到哪个层级(或是当前UI界面是什么类型的):例如弹窗类型是永远覆盖于所有的普通界面类型之上的),CanvasIndex(表示UI界面的目标画布)。在OnShow方法中:调用基类的OnShow方法,遍历UI界面的所有子物体,保存到UIManager中。调用Façade的Register进行与这个UI界面相关的Mediator、Proxy、Command的注册。在OnClose方法中:调用Façade的SendNotification方法进行广播消息,移除该UI界面的子UI界面。调用Façade的Remove进行与该UI界面相关的MVC三层的移除。public

class

HeroTypePanel

:

BasePanel

{

public

override

void

OnInit()

{

base.OnInit("HeroTypePanel",

SceneEnum.GameScene,

PanelManager.Layer.Panel,

PanelManager.CanvasEnum.World);

}

public

override

void

OnShow(params

object[]

para)

{

base.OnShow(para);

PanelManager.secondFacade.Register(SecondFacade.RegisterPanel.HeroTypePanel,

skin.gameObject,

para);

}

public

override

void

OnClose()

{

base.OnClose();

PanelManager.secondFacade.SendNotification(NotificationCons.RemoveHeroTypeItemPanelMediator);

PanelManager.secondFacade.Remove(SecondFacade.RegisterPanel.HeroTypePanel);

}

}

HeroTypePanelMediator创建脚本HeroTypePanelMediator继承Mediator。在构造函数中:接收需要的参数,并为Base传入mediatorName(此Mediator类的索引)用于注册。content是该UI界面的子UI界面的根结点,通过UIManager获取。重写ListNotificationInterests和HandleNotification。用于Notification(PureMVC内部提供的消息系统)的注册和接收。编写其它的方法进行UI界面的具体操作,通过消息接收触发。public

class

HeroTypePanelMetiator

:

Mediator

{

public

const

string

mediatorName="HeroTypePanelMetiator";

private

GameObject

content;

private

string

curId;

public

HeroTypePanelMetiator(GameObject

root)

:

base(mediatorName)

{

content

=

UIManager.Instance.GetUI(,

"content_").gameObject;

}

public

override

string[]

ListNotificationInterests()

public

override

void

HandleNotification(INotification

notification)

public

void

UpdateHeroTypePanel(string

id,

Dictionary<int,int>

dict)

private

void

UpdateHeroTypeItem(string

id,

int

type,

int

count,

bool

newItem

=

false)

private

void

RemoveAllChildItemPanel()

private

void

OnChangePlayerForHeroTypePanelDisplay(string

id,

Dictionary<int,

int>

dict)

}

当HeroTypePanelMediator触发添加子界面的时候,会实例化一个HeroTypeItemPanel界面和实例化其对应的Mediator、Proxy、Command。在HeroTypeItemPanelMediator的构造函数中,通过UIManager获取UI组件用于使用。public

HeroTypeItemPanelMediator(GameObject

_root,int

_heroTypeIndex)

:

base(string.Format("{0}{1}",

mediatorName,

_heroTypeIndex.ToString()))

{

heroTypeIndex

=

_heroTypeIndex;

root

=

_root;

heroTypeImg

=

UIManager.Instance.GetUI(,

"heroTypeImg_").GetComponent<Image>();

currentCountText

=

UIManager.Instance.GetUI(,

"currentCountText_").GetComponent<TextMeshProUGUI>();

heroTypeNameText

=

UIManager.Instance.GetUI(,

"heroTypeNameText_").GetComponent<TextMeshProUGUI>();

heroTypeCountText

=

UIManager.Instance.GetUI(,

"heroTypeCountText_").GetComponent<TextMeshProUGUI>();

scriptPanel

=

UIManager.Instance.GetUI(,

"scriptPanel_").gameObject;

scriptText

=

UIManager.Instance.GetUI(,

"scriptText_").GetComponent<TextMeshProUGUI>();

scriptMask

=

UIManager.Instance.GetUI(,

"scriptMask_").GetComponent<RectTransform>();

UIManager.Instance.RemoveUI();

MessageMgr.AddMessageListener<object[]>(MessageMgr.MessageType.DisplayHeroTypeScriptPanel,

DisplayHeroTypeScriptPanel);

InitPanel();

}Model层的实现1)HeroTypePanelProxy创建脚本HeroTypePanelProxy继承Proxy,ProxyName是该Proxy类的索引名称。在其构造函数中,初始化data并监听更新界面的命令。当接收到更新命令,获取新数据后,把数据更新到data中。至于View层的更新,可以是Proxy自己在更新data后随即调用界面更新方法,或是其它方式通过Command调用Proxy中的界面更新方法,通过该方法可以调用Mediator注册好的的Notification传送data数据作为参数进行View层的更新。public

class

HeroTypePanelProxy

:

Proxy

{

public

const

string

proxyName

=

"HeroTypePanelProxy";

private

HeroTypePanelData

data;

public

HeroTypePanelProxy()

:

base(proxyName)

{

data=new

Data();

MessageMgr.AddMessageListener<MsgBase>(MessageMgr.MessageType.OnMsgUpdateHeroTypePanel,

OnMsgUpdateHeroTypePanel);

}

public

override

void

OnRemove()

{

MessageMgr.RemoveMessageListener<MsgBase>(MessageMgr.MessageType.OnMsgUpdateHeroTypePanel,

OnMsgUpdateHeroTypePanel);

}

public

void

OnMsgUpdateHeroTypePanel(MsgBase

msgBase)

public

void

OnChangePlayerForHeroTypePanelDisplay(string

id)

}

2)HeroTypePanelDataHeroTypePanelData类的实例只是数据的存放,不做其它操作。public

class

HeroTypePanelData

{

Public

string

id;

public

Dictionary<string

,Dictionary<int,int>>

heroTypeIndexsDict;

public

HeroTypePanelData()

{

HeroTypeIndexsDict=new

Dictionary<string

,Dictionary<int,int>>();

}

}

Command层UpdateHeroTypePanelCommand继承SimpleCommand,Execute方法实例化时便执行,该命令执行了调用HeroTypePanelProxy的OnMsgUpdateHeroTypePanel方法,进行Model层数据的更新。public

class

UpdateHeroTypePanelCommand

:

SimpleCommand

{

public

override

void

Execute(INotification

notification)

{

MsgUpdateHeroTypePanel

data

=

notification.Body

as

MsgUpdateHeroTypePanel;

HeroTypePanelProxy

proxy

=

Facade.RetrieveProxy(HeroTypePanelPxyName)

as

HeroTypePanelProxy;

proxy.OnMsgUpdateHeroTypePanel(data);

}

}

UpdateHeroTypeItemCommand命令进行View层数据的更新。public

class

UpdateHeroTypeItemCommand

:

SimpleCommand

{

public

override

void

Execute(INotification

notification)

{

HeroTypeItemData

data

=

notification.Body

as

HeroTypeItemData;

SendNotification(NotificationCons.UpdateHeroTypeItemInfoMediator,

data);

}

}ChangePlayerForHeroTypePanelCommand命令调用方法,执行当该界面的显示目标ID发生改变时的行为。public

class

ChangePlayerForHeroTypePanelCommand

:

SimpleCommand

{

public

override

void

Execute(INotification

notification)

{

HeroTypePanelProxy

proxy

=

Facade.RetrieveProxy(HeroTypePanelPxyName)

as

HeroTypePanelProxy;

proxy.OnChangePlayerForHeroTypePanelDisplay(notification.Body.ToString());

}

}

全局管理层Façade新建类SecondFacade继承Façade管理第二个场景内的所有UI界面的Façade,方便不同场景内UI界面的控制。在SecondFacade中编写Register方法,做Switch-case判断不同UI进行不同的注册内容编写。通过RegisterMediator、RegisterProxy、RegisterCommand进行三层的注册。removeFun是一个委托,为委托添加对应的三层注销方法并存储到字典内,当该界面移除时,触发该委托执行,从而进行对应三层的注销。以英雄羁绊UI界面为例,在初始化界面时,Façade的注册方法执行,注册进行羁绊界面相关的Mediator、Proxy和Command,并且提前将卸载命令放入委托字典,在卸载UI界面时调用委托进行卸载相关类。case

RegisterPanel.HeroTypePanel:

RegisterMediator(new

HeroTypePanelMetiator(root));

RegisterProxy(new

HeroTypePanelProxy());

RegisterCommand(NotificationCons.UpdateHeroTypeItemCommand,

()

=>

{

return

new

UpdateHeroTypeItemCommand();

});

RegisterCommand(NotificationCons.UpdateHeroTypePanelCommand,

()

=>

{

return

new

UpdateHeroTypePanelCommand();

});

RegisterCommand(NotificationCons.ChangePlayerForHeroTypePanelCommand,

()

=>

{

return

new

ChangePlayerForHeroTypePanelCommand();

});

removeFun

+=

()

=>

{

RemoveMediator(HeroTypePanelMetiator.mediatorName);

RemoveProxy(HeroTypePanelPxyName);

RemoveCommand(NotificationCons.UpdateHeroTypeItemCommand);

RemoveCommand(NotificationCons.UpdateHeroTypePanelCommand);

RemoveCommand(NotificationCons.ChangePlayerForHeroTypePanelCommand);

};

AddToRemoveDict(RegisterPanel.HeroTypePanel,removeFun);

break;

游戏资源游戏属性数值游戏的人物是进行对战的英雄们,英雄们都有着相同的属性类型(生命,力量,攻速等等),但是不同类型英雄的基础属性数值是不同的;英雄穿戴不同装备获得的属性加成是不同的;不同英雄的上场组合形成的羁绊对英雄们的加成也是有区别的。为了更直观全面地统揽游戏数值、更便捷地进行游戏数值的修改和完善、更平衡舒适的游戏体验,所以使用Excel进行游戏数据的编辑。先在Excel表格中按照预先规定好的格式编写好Excel表格。分别对英雄属性、羁绊属性、装备属性的表格进行数据填写。图4-2游戏人物数值设定图4-3羁绊属性的设定图4-4装备的属性设定在数据设定完成后Excel的数据保存为xlsx文件,通过LitJson类库,将不同表格依托不同的模板类转换成对应的json文件。json文件就是游戏程序可以直接读取的游戏数据,不同的模板类是表格格式设定的依托。表格的第2行的是模板类的属性名称,第3行代表属性的类型,由此可以形成xlsx数据于C#类的转换。xlsx数据转C#类的过程:获取要转换Excel表格数据和对应的模板类获取模板类实例判断模板类实例属性并填入表格数据将该实例保存成json文件当需要加载数据时,通过json文件转换成模板类(属性类) private

static

void

ReadSingleSheet(Type

type,

DataTable

dataTable,

string

jsonPath)

{

int

rows

=

dataTable.Rows.Count;

int

Columns

=

dataTable.Columns.Count;

//UnityEngine.Debug.Log(rows

+

"行,"

+

Columns

+

"列");

//

工作表的行数据

DataRowCollection

collect

=

dataTable.Rows;

//

xlsx对应的数据字段,规定是第二行

string[]

jsonFileds

=

new

string[Columns];

//

要保存成Json的obj

List<object>

objsToSave

=

new

List<object>();

for

(int

i

=

0;

i

<

Columns;

i++)

{

jsonFileds[i]

=

collect[1][i].ToString();

UnityEngine.Debug.Log(jsonFileds[i]);

}

//

从第三行开始

for

(int

i

=

3;

i

<

rows;

i++)

{

//

生成一个实例

object

objIns

=

type.Assembly.CreateInstance(type.ToString());

for

(int

j

=

0;

j

<

Columns;

j++)

{

//

获取字段

FieldInfo

field

=

type.GetField(jsonFileds[j]);

if

(field

!=

null)

{

object

value

=

null;

try

//

赋值

{

value

=

Convert.ChangeType(collect[i][j],

field.FieldType);

}

catch

(InvalidCastException

e)

{

Console.WriteLine(e.Message);

string

str

=

collect[i][j].ToString();

string[]

strs

=

str.Split(',');

int[]

ints

=

new

int[strs.Length];

for

(int

k

=

0;

k

<

strs.Length;

k++)

{

ints[k]

=

int.Parse(strs[k]);

}

value

=

ints;

}

field.SetValue(objIns,

value);

}

else

{

UnityEngine.Debug.LogFormat("有无法识别的字符串:{0}",

jsonFileds[j]);

}

}

objsToSave.Add(objIns);

}

//

保存为Json

string

content

=

JsonMapper.ToJson(objsToSave);

SaveFile(content,

jsonPath);

}

英雄属性的模板类如下,存储英雄的基础属性数据:

public

class

HeroInfoTMP

{

public

int

index;

public

string

type_name;

public

string

hero_name;

public

int

max_life;

public

int

mana_recover_value;

public

int

power;

public

int

spell_power;

public

int

power_defend;

public

int

spell_defend;

public

int

atk_cd;

public

bool

isNearAtk;

public

int

atk_distance;

public

int

price;

public

string

type;

}

每个不同的英雄的基础属性都是不同数值的模板类,为了便于管理,创建HeroInfoMgr单例类作为英雄属性管理类,存储了所有英雄的基础属性类型,提供提供使用索引查找某个英雄属性的方法,便于游戏开发。以同样的方式创建HeroTypeMgr和EquipInfoMgr作为英雄羁绊管理类和装备管理类。准备游戏资源人物模型作为英雄在游戏中的GameObject,为AI脚本、寻路脚本及其它继承Mono(需要挂载在场景物体)的脚本提供搭乘实体。英雄在攻击和释放技能时,都会生成不同的特效、触发不同的音效。在英雄攻击时实例化到场景内。为了更好地区分各个资源,以及方便使用Addressables进行调用。必须规范化地修改Prefabs的名称(该名称是英雄属性填写的依托),并添加可寻址标记。图4-5游戏人物模型图4-6技能预设命名资源的管理Unity3D里有场景、模型、声音、视频、文本等许多不同类型的游戏资源,为了方便加载使用,我们需要对这些游戏资源进行很好地管理。在之前的资源管理行为上,我们需要把资源添加标记并打包成AssetBundle包,在加载资源时,解压AssetBundle包后读取资源,在这种方式的操作过程中,繁琐的操作步骤极易产生错误。一个小小的错误可能会导致资源无法加载,而且难以发现并修正。Unity3D官方也因此推出了新的资源管理系统Addressables。Addressables是对AssetBundle的高层封装。优化了游戏资源的打包和加载,极大地提升了游戏开发的整体效率。Addressables会对实例化的资源的个数进行标记,当实例化的资源减少时,减少标记数,如果标记数为零,表示没有该资源的引用,就会卸载掉该资源的内存,从而也帮助开发者实现了内存的优化管理。由于Addressables并非Unity3D内置的系统,所以需要进行导入,打开Unity3D的Window>PackageManager标签找到Addressable包后进行导入。图4-7Addressables包导入添加一个资源到Addressables时,打开资源的Inspector界面,打上Addressables标记并修改标记名称。在Addressables窗口中就会出现对应的资源,我们还可以为不同的资源添加不同的标签。在资源加载时,就是根据标记名称和标签进行不同资源或是不同的资源包进行的。图4-8Addressables的标记和标签要对标记好的资源进行加载时,引用UnityEngine.AddressableAssets,Addressables类提供了多种不同的加载方式。通过Addressables.LoadAssetsAsync可以异步地加载多个labels下的同类型资源。Addressables.LoadAssetsAsync<T>(labels,

null,

loadmode).Completed

+=

(handles)

=>

{

handle

=

handles;

foreach

(var

item

in

handle.Result)

{

result.Add(item);

}

if

(++loadCounter

>=

loadCount)

{

loadFinish();

}

};

人物动画控制器Unity3D中Animator系统可以对游戏的模型动画进行控制和管理。新建一个AnimatorController作为一个英雄的动画控制器,添加Idle、Move、Skill、Atk、Death四种状态。将Idle状态设置为默认状态,设置好状态间的过度条件:Move、Skill、Atk状态只能与Idle状态进行转换,任何状态都能进入死亡状态。图4-9动画控制器逻辑连接当动画控制器进行不同的状态时,触发对应的不同角色动画。为了区分游戏人物的个性,每个游戏的人物动画应是不同的。可以使用Animator系统中的AnimatorOverrideController对基础的AnimatortControll进行覆盖,这样就可以在不会改变原有的动画间的逻辑情况下,改变具体的模型动画作为另一个英雄的动画控制器。图4-10动画控制器的覆盖游戏人物的创建游戏中使用单例类HeroMgr作为英雄的管理类,负责英雄的创建和管理。在英雄购买UI界面进行英雄购买后,会创建相应英雄到游戏场景中的等待区域供玩家操作。通过HeroMgr的CreateHeroToWait方法创建英雄,通过英雄索引加载对应的属性,依托英雄属性的内容,通过Addressables异步加载模型和技能,为英雄添加不同的AI控制脚本脚本等等。public

void

CreateHeroToWait(string

id,

int

hero_type_index)

{

//根据编号获得英雄属性信息

HeroInfoTMP

heroInfoTmp

=

heroInfoMag.GetHeroModel(hero_type_index);

string

model_name

=

heroInfoTmp.heroModel_name;

//加载英雄模型

ResManager.LoadInstantiateAsync(model_name,

hero_model

=>

{

hero_model.AddComponent<AIPath>();

hero_model.AddComponent<AIPathFinder>();

//加入字典

hero_dict[id].Add(hero_model);

//不同归属英雄添加不同的基础脚本

string[]

ids

=

id.Split('_');

if

(id.Equals(NetManager.id)

||

ids[0].Equals(NetManager.id))

(GameManager.AddComponent(hero_model,

"HeroPositive")

as

HeroPositive).OnInit(id,

(sbyte)(indexTarget[id]++),

heroInfoTmp);

else

(GameManager.AddComponent(hero_model,

"HeroNegative")

as

HeroNegative).OnInit(id,

(sbyte)(indexTarget[id]++),

heroInfoTmp);

//英雄的血条,其它特效

ResManager.LoadInstantiateAsync(MainDefine.LifeBarPrefab,

hero_life_bar

=>

{……});

});

//在等待区放置hero

bool

isPosi

=

true;

PlacePlaneFind

parentPlane

=

PlacePlaneMag.placePlaneDict[id];

PlacePlaneFind

placePlaneTmp

=

parentPlane;

if

(progressMag.gameProgressState

!=

(byte)ProgressMag.GameProgressState.PlaceState)

{

for

(int

i

=

0;

i

<

progressMag.playerId.Count;

i++)

{

if

(NetManager.id.Equals(progressMag.playerId[i])

&&

!progressMag.enemyId[i].Equals("posi"))

{

isPosi

=

false;

placePlaneTmp

=

PlacePlaneMag.placePlaneDict[progressMag.enemyId[i]];

}

}

}

……

});

}

AI控制脚本游戏中的人物主要是自动战斗或者在等待区域的英雄们。玩家在游戏中扮演的是决策者,因此在游戏创建英雄时就会为英雄添加AI脚本,以便游戏中的英雄在战斗阶段进行自动战斗。由于采用了单一客户端进行逻辑演算,所以AI脚本也会有主动和被动之分。使用HeroBase作为不同类型AI脚本的基类,保存了脚本对应英雄的属性数据、组件引用和其它控制脚本的引用等,为对单个英雄的相关操作提供了方法。HeroBase创建了虚函数方法表示英雄在不同时期的逻辑,方便控制英雄在其生命周期内的行为。public

class

HeroBase

:MonoBehaviour

{

public

string

id

=

"";

private

sbyte

index;

public

sbyte

Index{}

public

string

only_index;//id-index

protected

float

syncInterval;//同步帧率

protected

bool

isNormalAtking

=

false;

protected

bool

isSkilling

=

false;

public

short

originalPos;//初始位置代码:index*10+1/0

:准备/等待

public

sbyte

ready_index_map=-1;//在映射数组中的位置,用于战斗寻路

public

int

ready_index_map_before=-1;//预先占据位置

public

bool

canLevelUp;

public

HeroInfo

heroInfo;

public

AIPathFinder

aiPath;

public

EquipBase[]

equipArray;

public

Transform

target_enemy;

public

HeroLifeBarCtr

lifeBarCtr;

public

List<Transform>

enemy_list;

private

OtherFXCtr

fxCtr;

public

OtherFXCtr

otherFxCtr

protected

Animator

ani;

protected

AnimationClip[]

clips;

protected

Collider

col;

public

virtual

void

Init(HeroInfo

_heroInfo,string

_id,sbyte

_index)

……

}

主动AI基础脚本HeroBasePositive:继承于HeroBase,在主机客户端创建英雄时添加的AI脚本。负责在此客户端执行英雄的逻辑决策、在规定的时间间隔发送脚本对应英雄的位置、状态信息到其它被动客户端内进行同步。HeroBasePosition发送同步消息方法如下:public

void

SyncUpdate()

{

//时间间隔判断

if

(Time.time

-

lastSendSyncTime

<

syncInterval)

{

return;

}

lastSendSyncTime

=

Time.time;

//发送同步协议

MsgMgr.SendMsgSyncTransform(id,transform,

(sbyte)(ani.GetBool("Move")

?

1

:

0),ready_index_map==-1?UpdateSelfReadyMapIndex():ready_index_map);

}

被动AI基础脚本HeroBaseNegative:继承于HeroBase,在主机客户端以外的客户端创建英雄时添加的AI脚本,不会进行游戏人物的逻辑决策,只是通过解析接收到的协议对场景中的人物的行为状态进行调整。HeroBaseNegative的接收并解析人物移动协议的方法如下: public

void

OnMsgSyncTransform(MsgBase

msgBase)

{

MsgSyncTransform

msg

=

(MsgSyncTransform)msgBase;

if

(msg.id.Equals(id)&&msg.index.Equals(Index)&&heroInfo.isPositive==false)

{

ani.SetBool("Move",

msg.move.Equals(0)

?

false

:

true);

//预测位置

Vector3

pos

=

new

Vector3(msg.x

,

msg.y,

msg.z);

Vector3

rot

=

new

Vector3(transform.rotation.x,

msg.ey,

transform.rotation.z);

forecastPos

=

pos

+

2

*

(pos

-

lastPos);

forecastRot

=

rot

+

2

*

(rot

-

lastRot);

//更新

lastPos

=

pos;

lastRot

=

rot;

forecastTime

=

Time.time;

PlacePlaneFind

plane

=

PlacePlaneMag.placePlaneDict[PlacePlaneMag.GetTargetPlaneId(msg.id)];

if

(ready_index_map

!=

-1)

{

plane.all_ready_place_map[ready_index_map]

=

null;

}

ready_index_map

=

msg.ready_index_map;

plane.all_ready_place_map[ready_index_map]

=

only_index;

}

}

游戏人物的逻辑决策只在主动AI类HeroBasePositive的有限状态机内进行判断和控制,在HeroBasePosition中初始化有限状态机,为状态机添加多个不同时刻的状态,对应HeroBase中的不同行为。通过SetCurrentIndex方法使状态机进行空状态,为运行做准备。public

void

InitFsmsys()

{

fsmsys

=

new

FSMSystemN();

NullState

nullState

=

new

NullState(this);

FightStateBase

fightStateBase

=

new

FightStateBase(this);

IdleState

idleState

=

new

IdleState(this);

MoveState

moveState

=

new

MoveState(this);

AtkState

atkState

=

new

AtkState(this);

DeathState

deathState

=

new

DeathState(this);

fsmsys.AddState(nullState);

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论