基于unity的回合制网游战斗系统的研究与实现_第1页
基于unity的回合制网游战斗系统的研究与实现_第2页
基于unity的回合制网游战斗系统的研究与实现_第3页
基于unity的回合制网游战斗系统的研究与实现_第4页
基于unity的回合制网游战斗系统的研究与实现_第5页
已阅读5页,还剩46页未读 继续免费阅读

下载本文档

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

文档简介

-

-

摘要本文主要阐述网络回合制战斗系统的实现,包括回合制战斗系统的整个流程(战斗开始、玩家下达战斗指令、战斗动画、战斗结束),和网络回合制战斗系统常见的主要功能(攻击、使用技能、使用物品)的实现,并且在游戏项目《PKer》中进行实践和测试。在开发环境方面,服务器采用国内IOCP高性能开源框架“HP-Socket”(Windows平台),并且连接到Mysql数据库,客户端采用近年比较火热的强大跨平台引擎Unity3D(2D,C#)并且使用Sqlite作为嵌入式数据库。除此以外,本文还对战斗系统实现所涉及到的相关技术如Unity引擎的协同程序、单例设计模式和分包算法进行简要介绍。

关键词回合制战斗系统,网络游戏,Unity3D,协同程序,游戏编程

ABSTRACTThisarticlemainlyexpoundstheimplementationofnetworkturn-basedcombatsystem,includingthewholeprocessofturn-basedcombatsystem(battlebegins,theplayersfightinginstructions,combatanimation,battleends),andthemainfunctionofcommon(attack,usingskills,usingitems)implementation,andinthegame"PKer"inpracticeandtestoftheproject.Intheaspectofdevelopmentenvironment,C++IOCPserverwithWindowsplatformandconnecttotheMysqldatabase,theclientusesUnity3D(2D,C#)andusesSqliteasanembeddeddatabase.Inadditiontothis,thisarticlealsoinvolvedtocombatsystemrelatedtechnologiessuchastheUnityenginecoroutines,singletondesignpatternandthesubcontractalgorithmarebrieflyintroduced.

KEYWORDSTurn-basedcombatsystem,onlinegame,Unity3D,Coroutine,Singletondesignpattern

目录

TOC\f\h\z\t"样式1,1,样式2,2,样式3,3"

前言

1

第1章绪论

2

1.1回合制游戏简介

2

1.1.1广义上的回合制游戏

2

1.1.2狭义上的回合制游戏

2

1.2回合制战斗系统简介

2

1.3实践项目《PKer》简介

2

第2章开发环境和部分涉及技术简介

4

2.1开发环境简介

4

2.2Unity协同程序

4

2.2.1Unity协同程序简介

4

2.2.2Unity协同程序的运用

4

2.3单例设计模式

5

2.3.1单例设计模式

5

2.3.2单例设计模式的运用

6

2.3.3单例设计模式在Unity中的运用

7

2.4分包算法

7

2.4.1分包的原因

7

2.4.2环形缓冲区(CircularBuffer)

7

2.4.3分包算法流程图

9

第3章战斗动画实现方案的研究与对比

11

3.1研究背景

11

3.2实现方案一:有限状态机

11

3.3实现方案二:协同程序(Coroutine)

12

3.4方案抉择结论

13

第4章回合制网游战斗系统设计

14

4.1回合制网游战斗系统设计

14

4.2回合制网游战斗系统服务器实现思路

15

4.3回合制网游战斗系统客户端实现思路

15

4.4战斗系统框架结构(以实践项目《PKer》为例)

16

第5章回合制战斗系统实现

18

5.1战斗系统相关消息结构体的定义

18

5.2服务器战斗系统的实现

20

5.2.1服务器战斗系统相关结构体

20

5.2.2初始化一个战局

23

5.2.3接收客户端的战斗指令消息

23

5.2.4处理战斗指令并发送动画消息

24

5.2.5说明

27

5.3客户端战斗系统的实现

27

5.3.1客户端战斗系统功能模块

28

5.3.2客户端战斗系统相关数据类型

28

5.3.3进入战斗后初始化战斗角色

29

5.3.4下达战斗指令并发送战斗指令消息

29

5.3.5接收战斗动画消息

30

5.3.6收到战斗动画播放消息并播放战斗动画

30

5.3.7战斗动画实现细述

35

第6章项目测试

39

6.1进入战斗测试

39

6.2普通攻击功能测试

39

6.3使用技能功能测试

40

6.4使用物品功能测试

41

结论

43

参考文献

44

致谢

45

前言

随着网络的普及与迅速发展,网络游戏已经成为电子游戏中的主流。网络游戏的战斗系统主要可分为即时制和回合制两种模式。即时制战斗系统侧重于刺激、反应、操作性,战斗节奏快。而回合制战斗系统侧重于休闲、战略、配合,战斗节奏慢。早期的电子游戏由于设备硬件条件有限,大多采用回合制战斗系统。

随着科技的发展和设备硬件的提升,如今回合制游戏的数量比例有所下降,但回合制游戏在国内游戏市场依旧占据着想当大的份额,仍有大量玩家热衷这种战斗模式,几款众所周知的国产单机游戏如《轩辕剑》系列、《仙剑奇侠传》系列、《古剑奇谭》系列以及国内在线人数最多的MMORPG《梦幻西游》均是采用回合制战斗系统的游戏。

本人也是热衷于回合制战斗系统的玩家之一,可纵观近年来国内回合制游戏的发展,国内回合制游戏一直止步不前,《轩辕剑》系列、《仙剑奇侠传》系列、《古剑奇谭》销量下降,MMORPG《梦幻西游》也有降温迹象,甚至国内大多回合制网游只是复制在《梦幻西游》或“换皮”,缺少创新和突破。本人希望在研究和实现回合制战斗系统的基础上,能够找到突破和创新点。同时在游戏编程方面,即时制逻辑较为简单明了清晰,而回合制则比较繁琐复杂,而且在Unity引擎上实现回合制战斗系统的相关资料较少。综上原因,本人撰写本文,希望能对志同道合者有所启发,同时也寻求学术交流。

项目将在2017年6月开源,开源地址

/share/home?uk=1017424337

By江正觊

2016.6

第1章绪论

1.1回合制游戏简介

1.1.1广义上的回合制游戏

凡“我方与敌方在单个回合内轮流行动,只有轮到我方(自己)的回合或者是新的回合开始时,才可进行行动”的游戏,都可归类为广义上的回合制游戏,而且绝大大多数情况下,单个回合内敌我双方行动次数相同。从广义上来说,回合制游戏范围非常广,棋牌、卡牌、战棋策略、回合制战斗模式都能归为此类。

1.1.2狭义上的回合制游戏

狭义上的回合制游戏,是从广义回合制游戏中细分,特指回合制战斗模式的游戏,与即时制战斗模式相对。

1.2回合制战斗系统简介

在回合制战斗模式下,每个回合开始时,敌我双方各自为双方战斗角色下达战斗指令,待双方所有角色下达完战斗指令或超过时限(回合制网络游戏均会设置下达战斗指令的时间限制)后双方所有角色开始行动,一般根据角色自身“速度(敏捷)”的属性数值轮流行动,期间若果某一方符合战败条件(某方全体阵亡或全体人物阵亡)则战斗结束,如所有角色行动完后敌我双方均没符合战败条件,则进入下回合,如此循环。

1.3实践项目《PKer》简介

《PKer》是一个以回合制竞技和社交为卖点的PC和移动端跨平台网络游戏,是游戏与社交APP的融合体。游戏主要玩法是玩家与玩家之间的回合制战斗PK。

通过移动设备GPS定位功能(PC端无法使用),能够快速搜索在你身边的游戏房间和玩家,与其开展战斗。

游戏提供一定数量的基础装备和宠物让玩家任意领取,故玩家可以随时更换装备和宠物,新玩家也能以此为基础投入到战斗中。更强的装备和稀有的宠物通过合成、付费租用等渠道获得。角色的属性和职业也可以随时更改,以便随时改变战术和改变在队伍里中的定位。

第2章开发环境和部分涉及技术简介

2.1开发环境简介

本文实践项目的服务器采用国内IOCP开源框架“HP-Socket”,并且连接到Mysql数据库,客户端采用近年比较火热的强大跨平台引擎Unity3D(2D,C#)并且使用Sqlite作为嵌入式数据库。

2.2Unity协同程序

2.2.1Unity协同程序简介

协同程序(Coroutine),通常简称“协程”,顾名思义,是一段协助的程序(方法),很多人以为它是另开一个线程执行一段程序,其不然,实际上它是从主线程每帧或每隔一定时间调用的程序。当协程创建后,主线程中创建协程的语句后面的代码块会“挂起”,直到协同程序结束后,才会继续执行创建协程语句后面的代码。当协程中的代码执行完或者使用yieldbreak语句时,协程才会结束,并且返回到主线程中的创建该协程的语句的位置,继续执行后面的代码。协程中使用yieldreturn帧数/newWaitForSeconds(秒数)语句可以实现隔多少帧或多少秒后再执行后续代码。另外协程可以嵌套协程,利用协程的特点和嵌套,可以实现很多复杂和有趣的功能,十分强大。

2.2.2Unity协同程序的运用

协程广泛地运用在计时、延迟、控制物体运动、等待物体状态的改变、有顺序地让物体执行一系列动作等方面上。

根据协程的特点,协程中可以使用“循环+条件判断+yieldreturn帧数/newWaitForSeconds(秒数)”实现每隔多少帧或者多少秒后再次执行条件判断语句,当判断语句满足跳出循环,从而让协程代码执行完结束,回到主线程继续执行后续代码,利用此功能,可以很方便地实现某物体达到某状态后再执行程序。

示例代码:

主线程代码

StartCoroutine(actor.Move(getAttackPosition(nTargetIndex)));

Inti=0;

协同程序Move方法

publicIEnumeratorMove(Vector3destPos)

{

while(transform.localPosition!=destPos)

{

transform.localPosition=Vector3.MoveTowards(transform.localPosition,destPos,GlobalData.BATTLE_SPRITE_MOVE_SPEED*Time.deltaTime);

yieldreturn0;

}

}

上述代码中,主线程StartCoroutine方法创建一个协程Move,待协程Move执行完成返回后,主线程才执行i=0语句。可是协程Move的返回条件有点特殊,yieldreturn0语句表示协程运行到此处挂起,等下一帧再从本语句继续运行,由于yieldreturn0在while循环内,所以不管yieldreturn0多少次,都依然在while循环内,而且while循环每一帧循环一次。直到while不满足循环条件(即transform.localPosition==destPos),则协程能够完成使命执行完成并返回。可以猜测到,所控制的物体可能通过Update方法或者其他方法每帧在移动,直到移动到目标地点,才执行主线程后面的代码。

2.3单例设计模式

2.3.1单例设计模式

单例设计模式是常见和简单的一种软件设计模式,单例模式下的类能确保在整个工程项目中只允许存在最多一个实例对象。单例模式下的类通常不能直接通过构造函数new出一个实例,而且构造函数通常会设置成私有,外部不可访问。只有通过类的一个静态方法可以创建或获取有且只有唯一一个的对象实例。

2.3.2单例设计模式的运用

单例设计模式下的类往往具有通用性或全局性,用于保存全局数据,需要访问其变量的外部类只需要通过单例类的静态方法即可获得其唯一的实例,从而访问其内部变量。

示例代码:

publicclassClientSocket//单例类ClientSocket

{

privatestaticClientSocket_client_socket;//保存本类的唯一实例

privatestaticMessageManager_msg_mgr;

privatestaticSocket_socket;

privatestring_web="";

privateIPAddress_ip;

privateint_port=8000;

privatebyte[]_buf=newbyte[1024];

privateCircularBuffer_circularBuf=newCircularBuffer(4096);

privateClientSocket(){

}

publicstaticClientSocketgetInstance()

{

if(_client_socket==null)//若实例不存在则创建,实例已存在则直接返回

{

_client_socket=newClientSocket();

_msg_mgr=MessageManager.getInstance();

}

return_client_socket;

}

……

}

2.3.3单例设计模式在Unity中的运用

在Unity中使用单例设计模式,有时会有额外的意义。在Unity中,通常一个类会作为一个组件挂载到一个物体上,可是如果进行了场景切换,当前场景所有物体以及物体所挂载的所有组件都会被销毁,这时如果想要所有场景都能访问一个类的实例以及其变量,则需要把该类设置成单例类,并且不能继承MonoBehaviour,而且不能挂载到任何一个物体上,这样就可以保证整个游戏中不管场景怎么切换,该类的实例一直存在,而且只有唯一一个实例,所有场景均可通过静态方法获取实例并且访问。

2.4分包算法

2.4.1分包的原因

TCP在发送消息时,如果发送间隔太短,发送方有可能出现一次发送多个消息的情况,而接收方便会一次接收到多个消息,另外,很多时候网络情况较差或不稳定,接收方迟迟收不到消息,过一段时间有可能一次过接收到此前该接收的多个消息,这些现象就是粘包。

由于粘包现象的存在,如果不进行分包,接收方可能只能读取到粘包数据的头一个消息,后面的消息将会丢失,甚至影响后续接收的消息,严重影响程序的运行。因此,我们需要一个分包算法来检测粘包情况并进行分包,以便正确读取所有来自发送方的消息。

2.4.2环形缓冲区(CircularBuffer)

在分包算法中,我们需要用到一个数据结构——环形缓冲区。之所以要用到环形缓冲区,是因为分包前我们要把接收到的所有数据存放在一个缓冲区中,分包时根据消息的长度从缓冲区里取数据,取出后缓冲区相应地移除对应的数据,这样反复频繁进行添加和删除操作如果只用普通的缓冲区(byte数组/char数组)的话,则要频繁地移动数据存放的位置,十分影响程序运行效率,为此需要引入环形缓冲区。

下面是环形缓冲区(CricularBuffer)类的UML类图(C++):

图2-1环形缓冲区UML类图

说明:

buf:指向缓冲区的指针

front:缓冲区的数据的起始位置

rear:缓冲区的数据的尾部位置

lock:用于多线程同步的临界区,防止多个线程访问修改缓冲区数据

size:缓冲区数据的长度

capacity:缓冲区的容量

CircularBuffer:构造函数,创建一个指定容量为参数大小的缓冲区

~CircularBuffer:析构函数,释放缓冲区

clear:清空缓冲区

pushBuf:往缓冲区尾部添加数据

popBuf:从缓冲区头部得到数据,并且缓冲区移除相应数据

getBuf:从缓冲区头部得到数据,但缓冲区不移除数据

isEmpty:判断缓冲区是否为空

由于环形缓冲区使用front起始位置和rear尾部位置记录数据存放的位置,添加或移除数据不需要移动数据的储存位置,运行效率高。

2.4.3分包算法流程图

图2-2分包算法流程图

说明:

定义或获得一个容量足够大的环形缓冲区(具体大小请根据实际情况考虑);

接收到数据后,往环形缓冲区尾部添加数据;

判断缓冲区的数据长度是否大于等于4(消息头的长度),本文实践项目的结构体消息头由消息类型(ushort)和消息长度(ushort)组成,共4字节。若缓冲区数据长度小于消息头长度,则无法获取第一条消息的长度,也无法分包,也表明数据接收不全,回到(2)继续接收数据,不进行任何操作;若缓冲区数据长度大于等于消息头长度,则开始进行分包;

从缓冲区头部getBuf获得4字节数据(消息头),不改变缓冲区数据,从消息头得到第一包消息的长度;

判断缓冲区数据长度,若缓冲区数据长度大于第一包消息的长度,则第一包消息接收完并可取,根据第一包消息的长度popBuf得到第一包消息数据,并且缓冲区移除相应数据,处理该消息;若缓冲区数据长度小于第一包消息长度(此时缓冲区数据长度大于等于4,但是小于第一包消息的长度),则说明第一包消息还没接收完,回到(2)继续接收,不进行任何操作;

回到(4),一直循环。

第3章战斗动画实现方案的研究与对比

3.1研究背景

笔者认为回合制战斗系统实现的最大难点,那无疑就是客户端战斗动画的实现。回合制战斗系统的战斗动画,不是平常我们说的“动画”,它是由数据转换过来的动态动画,动画内容由数据决定。例如动画数据是“某个角色攻击某个角色”,动画内容便是“攻击者移动到目标面前并播放攻击动作动画,目标播放受伤动作动画,显示扣血,攻击者返回到本来的位置”;再例如动画数据是“某个角色使用技能攻击某个角色”,动画内容便是“施法者播放施法动作动画,然后在目标身上播放技能特效,等到特效播放到某一帧,目标播放受伤动作动画,显示扣血”。

上述例子已经是回合制战斗系统最基本的战斗动画,更不用说多次攻击或者多次施法,可谓难上加难。更何况,战斗动画的质量极大影响回合制游戏的可玩性,而且回合制战斗除了玩家下达战斗指令外,其余时间全部都在播放战斗动画上。

所以,笔者认为客户端的战斗动画实现是整个回合制战斗系统的重点和难点,为了正确谨慎地抉择实现方案,少走弯路,额外对战斗动画的实现展开了研究。

3.2实现方案一:有限状态机

这是笔者最先想到的方案,毕竟有限状态机在游戏编程中实在极为常用,相信不少读者也会率先联想到这种方式。

有限状态机代码示例:

voidUpdate(){

switch(state)

{

case(int)State.Standby:

……

break;

case(int)State.Attack:

……

break;

case(int)State.Magic:

……

break;

……

}

……

}

有限状态机方案下,Update方法每帧检测记录状态的变量state,从而决定究竟要执行何种行为,当行为执行完或者某个事件触发时,通过修改状态变量state,改变执行的行为。

优点:

(1)灵活可变,随时随地都可以改变状态,从而改变执行的行为

缺点:

每帧都需要检测状态变量,影响程序运行效率;

如果要实现一系列复杂的行为,代码量大而且凌乱,逻辑复杂,例如要实现“攻击者移动到目标面前,攻击者播放攻击动画,目标播放受伤动画,显示扣血,攻击者返回原本位置”这样的功能,可能需要不止一个状态机;

Update方法变得臃肿,很多时候Update还需要做其他事情;

3.3实现方案二:协同程序(Coroutine)

在感觉方案一可行性不高的情况下,本人开始寻求其他实现方案,直到发现协同程序(Coroutine)。若读者不了解协程可阅本文第二章,协同程序在本文第2章有简略介绍,此处不再作介绍。个人感觉Unity的协同程序跟Cocos2d的CCSequence动作有点像,都能很方便实现按顺序执行一系列动作,而且协同程序比CCSequence更灵活。

协同程序示例代码:

IEnumeratorAttack(BATTLE_ANIMbattleAnim)

{

BattleSpritesprite=battleSprites[battleAnim.nActorIndex-1];

yieldreturnStartCoroutine(sprite.Move(getAttackPosition(nTargetIndex)));

yieldreturnStartCoroutine(sprite.AttackAnim());

yieldreturnStartCoroutine(sprite.Back(actor.vOrigPos));

}

协程Attack()能够简单清晰实现“控制某个物体移动到某个地点,完成后进行攻击,攻击完成后返回到某个地点”,一步完成之后再进行下一步。

优点:

不需要在Update每帧调用,协程开启后根据代码逻辑等待(每)n帧/n秒自动调用;

代码逻辑清晰

能够很好实现按顺序执行一系列复杂的行为

缺点:

如要中途终止协程,有一定的终止条件

没状态机灵活,难以临时转变行为

3.4方案抉择结论

显而易见,经过笔者的研究和深思熟虑,本文采用了协同程序方案。理由很简单,回合制战斗动画几乎全避开了协程的缺点,体现了协程的优点。因为回合制战斗动画是由服务器发来的动画数据转换的,数据自始至终没有改变,客户端只负责播放动画效果,并没有修改数据,那么从数据转化为动画那刻开始,动画就不可能有变化或者中途终止,并不需要程序的灵活性。这么看来,协程用在回合制战斗动画的播放很是适合。

另外,读者也可考虑行为树方案,由于笔者时间和能力有限,截至目前未能深入学习研究行为树,不知是否能比协程更好地实现战斗动画,所以本文未能提及,恳请体谅。

第4章回合制网游战斗系统设计

4.1回合制网游战斗系统设计

我们从回合制网游中不难发现,在客户端面前,我们可以看到一场回合制战斗可划分为以下阶段:进入战斗-玩家下达战斗指令(很多情况下需要下达人物和宠物两个战斗指令)-战斗动画-战斗结束。经过本人对上述战斗过程的研究、思考和分析,得出个人的回合制战斗系统实现思路,战斗系统流程图如图:

图4-1回合制网游战斗系统流程图

4.2回合制网游战斗系统服务器实现思路

(1)战斗开始发送“战斗初始化消息”给客户端,“战斗初始化消息”包含所有战斗角色(人物和宠物)的名字、图形及动画控制器ID、最大Hp、当前Hp、最大Mp、当前Mp等数据,客户端只关心需要显示的内容;

(2)根据战斗角色的数量,等待接收并保存数量与角色数量想当的来自客户端的“战斗指令消息”,即等待所有战斗角色都下达完战斗指令;

(3)接收到足够数量的“角色战斗指令消息”后,根据角色的“速度”属性数值,倒序(即速度高者先行动)处理每一个战斗指令所造成的影响,并更新服务器各个角色的数值,发送对应的“动画消息”给所有客户端,每处理一个“战斗指令消息”就发送一个对应的“动画消息”;

(4)待所有“战斗指令消息”处理完并发送完对应的“动画消息”后,发送“动画播放消息”;

(5)判断双方角色的阵亡情况,若一方符合战败条件(所有角色阵亡或者所有人物阵亡,具体根据游戏设定决定)则结束战斗,否则回到步骤(2)进入下一个回合,如此循环。

4.3回合制网游战斗系统客户端实现思路

(1)若接收到“战斗初始化消息”,则进入战斗,并根据消息里的数据,初始化每一个战斗角色;

(2)显示人物战斗指令菜单,根据玩家的操作,发送“战斗指令消息”,人物战斗指令菜单消失;

(3)若没有参战宠物,直接跳到下一步,若有参战宠物,显示宠物战斗指令菜单,根据玩家的操作,发送“战斗指令消息”,宠物战斗指令菜单消失;

(4)接收并保存每一个来自服务器的“动画消息”或“动画播放消息”;

(5)收到“动画播放消息”后,读取并处理每一个“动画消息”,转化为对应等量的战斗动画类并用容器保存,遍历容器播放所有动画;

(6)当前回合所有战斗动画播放完后,判断双方阵亡情况,若一方符合战败条件(所有角色阵亡或者所有人物阵亡,具体根据游戏设定决定)则结束战斗,否则回到步骤(2)进入下一个回合,如此循环。

4.4战斗系统框架结构(以实践项目《PKer》为例)

虽然本文主要阐述战斗系统,但是由于战斗系统较为复杂,为了让读者更好的理解本文所述战斗系统的实现思路,以实践项目《PKer》为例,对游戏的战斗系统架构和相关功能架构进行简单的罗列和说明。

图4-2实践项目《PKer》战斗系统及相关功能架构

说明:

玩家从游戏大厅可以搜索加入房间或创建房间,与房间内其他玩家进行对战进入战斗(读者可根据自身游戏玩法考虑如何触发战斗,如常见的回合制游戏通过暗雷或明雷遇敌、玩家对点触发战斗);

《PKer》中玩家可以直接在玩家属性配置界面中通过点选“宠物库”内的宠物即可获得对应宠物,并且通过设置“参战宠物”可让宠物在战斗中出战(读者可根据自身游戏玩法考虑宠物的获得途径,常见的途径有战斗捕捉、任务奖励等);

《PKer》中玩家可以直接在玩家属性配置界面中通过点选“装备物品库”内的装备/物品即可获得对应装备/物品,装上装备将提高角色的属性使角色更具战斗力,放在物品栏的物品可以在战斗中使用回复Hp/Mp(读者可根据自身游戏玩法考虑装备物品的获得途径,常见的途径有战斗奖励、任务奖励等);

《PKer》中玩家可以直接在玩家属性配置界面中更改职业,从而获得在战斗中使用的技能;

进入战斗后,参战角色的属性由各个玩家配置(属性加点、装备、参战宠物、职业等)所决定;

各玩家通过指令菜单下达指令从而控制自身角色和自身参战宠物(如有设置参战宠物)的行动。其中,使用技能能够选择使用自身职业的技能,使用物品能够选择使用物品栏的物品;

战斗动画主要由多个协同程序组成来实现,根据各个参战人物/宠物的下达的战斗指令对应的播放其行为的动画,不同的行为动画内容不一样;

而在技能动画中,为了增加回合制战斗的画面感和可玩性,对不同的技能配置了不同的特效播放方式,让特效动画更华丽更多元化,详细实现方式在本文5.3.6和5.3.7小节有阐述;

第5章回合制战斗系统实现

5.1战斗系统相关消息结构体的定义

结构体消息是服务器与客户端沟通的“共同语言”,结构体消息在客户端和服务器分别定义并且数据结构是相同,发送方填写结构体数据,接收方先获取消息头的消息类型ID和长度,再判断是哪种类型的消息并转换成该类型结构体,然后读取内容并进行对应的处理。

以下是Unity客户端(C#)中定义的相关结构体消息,服务器的定义与其一致,但由于服务器(C++)与客户端(C#)的编程语言不同所以定义语法有些许区别。

//战斗初始化消息

publicstructMSG_BATTLE_INIT

{

publicushortnMsgID;//消息类型ID

publicushortnLen;//结构体长度

publicuintnBattleID;//战局ID

[MarshalAs(UnmanagedType.ByValArray,SizeConst=20)]

publicBATTLE_SPRITE_INIT[]battleSprites;//所有战斗角色基本数据

};

//战斗角色(人物/宠物)基本数据消息

publicstructBATTLE_SPRITE_INIT

{

publicuintnID;//玩家/宠物ID(客户端用此值判断是否有战斗成员,可能还会用来标识自身和自身宠物)

[MarshalAs(UnmanagedType.ByValArray,SizeConst=22)]

publicbyte[]strName;//战斗人物/宠物名字

publicuintnImageID;//图形及动画控制器ID

publicshortnHpMax;//最大HP

publicshortnHp;//当前HP

publicshortnMpMax;//最大MP

publicshortnMp;//当前MP

};

//战斗指令消息

publicstructMSG_BATTLE_COMMAND

{

publicushortnMsgID;//消息类型ID

publicushortnLen;//结构体长度

publicushortnActorType;//行动者类型(1=人物2=宠物,由服务端赋值,客户端不需要赋值)

publicushortnActorIndex;//行动者索引(由服务端赋值,客户端不需要赋值)

publicushortnActionType;//行动的类型

publicushortnTargetIndex;//目标索引

publicushortnParam;//参数(根据行动类型而定,如使用技能则是技能ID,使用物品则是物品栏位置,切换战宠则是宠物栏位置)

};

//战斗动画播放消息(客户端收到此消息后开始播放战斗动画)

publicstructMSG_BATTLE_ANIM_BEGIN

{

publicushortnMsgID;//消息类型ID

publicushortnLen;//结构体长度

};

//战斗动画消息

publicstructMSG_BATTLE_ANIM

{

publicushortnMsgID;//消息类型ID

publicushortnLen;//结构体长度

publicushortnActorIndex;//行动者索引

[MarshalAs(UnmanagedType.ByValArray,SizeConst=10)]

publicushort[]nTargetIndex;//目标索引(初始值为0,0则无目标)

publicushortaction_type;//行动类型

publicushortnParam;//参数(根据行动类型而定,如使用技能则是技能ID,使用物品则是物品栏位置)

[MarshalAs(UnmanagedType.ByValArray,SizeConst=10)]

publicshort[]nActorHp;//行动者Hp影响(<0:扣血,>0:加血,0=无任何影响)

[MarshalAs(UnmanagedType.ByValArray,SizeConst=10)]

publicshort[]nActorMp;//行动者Mp影响

[MarshalAs(UnmanagedType.ByValArray,SizeConst=10)]

publicshort[]nTargetHp;//目标Hp影响(<0:扣血,>0:加血,0=无任何影响)

[MarshalAs(UnmanagedType.ByValArray,SizeConst=10)]

publicshort[]nTargetMp;//目标Mp影响

};

5.2服务器战斗系统的实现

5.2.1服务器战斗系统相关结构体

图5-1服务器战斗系统的相关结构体的关系图

//战局结构体

structBATTLE

{

UINTnBattleID;//战局ID

BATTLE_SPRITEbattleSprites[20];//战斗角色(人物/宠物)数组

std::list<USHORT>team1_survivors_index;//队伍1存活者索引list

std::list<USHORT>team2_survivors_index;//队伍2存活者索引list

USHORTnSpriteTotal;//参战角色总数

std::multimap<float,MSG_BATTLE_COMMAND*>m_CommandMap;//玩家战斗指令字典,用于保存每回合接收的战斗指令消息,Key为角色速度数值

}

说明:

battleSprites数组的索引0-4位置分别为队伍1人物1、队伍1人物2……,索引5-9位置分别为队伍1人物1的宠物、队伍1人物2的宠物……,类似的,索引10-14为队伍2人物,索引15-19为队伍2宠物,人物索引+5即该人物的宠物的索引,根据人物索引即可获得其宠物索引

//战斗角色(人物/宠物)

structBATTLE_SPRITE

{

BATTLE_SPRITE_DATA*pBattleData;//战斗数据(包含Hp、Mp、攻击力等)

CLIENT_DATA*pClient;//客户端数据(CLIENT_DATA包含客户端IOCP完成键、套接字、玩家游戏数据等数据,本文不阐述,请读者根据自身游戏服务器架构设计)

USHORTnType;//角色类型,1=人,2=宠物

USHORTbCommand;//是否已经下命令,0=没下命令,1=已下命令

}

//战斗数据(人物与宠物通用)

structBATTLE_SPRITE_DATA

{

UINTnID;//玩家/宠物ID

charstrName[22];//战斗人物/宠物名字

UINTnImageID;//图形及动画控制器ID

USHORTnLevel;//等级

shortnHpMax;//最大Hp

shortnHp;//当前Hp

shortnMpMax;//最大Mp

shortnMp;//当前Mp

shortnAtk;//物理攻击力

shortnDef;//物理防御力

shortnMat;//魔法攻击力

shortnMdf;//魔法防御力

shortnSpd;//速度

}

5.2.2初始化一个战局

即初始化一个战局BATTLE实例,并根据参战玩家的数据对战局所有成员进行赋值,由于成员较多,并且游戏逻辑复杂,读者可根据自身游戏逻辑赋值,此处初始化的方式不影响后续的实现,所以这里不进行详细阐述,最后在客户端数据存放指向战局的指针,以便后面通过完成键以及客户端数据能够获得战局数据。

本文假设一个战局最多能5位玩家对战5位玩家,包括每位玩家可出战的一个战斗宠物,所以一个战局最多能有20个战斗角色,故battleSprites战斗角色数组长度为20,读者可根据自身游戏逻辑进行变更。另外,如果战局不足20个战斗角色,本文通过检查battleSprites中的战斗数据pBattleData是否为NULL进行判断并计数,用成员nSpriteTotal保存战斗角色总数。战局初始时所有战斗角色理应是存活的,battleSprites中所有存在的战斗角色的索引相应的存放到队伍1存活索引list或队伍2存活索引list中,本文假设索引0-9为队伍1(0-4队伍1人物,5-9队伍1宠物),10-19为队伍2(10-14为队伍2人物,15-19为队伍2宠物),并且人物和其宠物的索引相差5(即0索引上的人物其宠物索引为5,如此类推)。

5.2.3接收客户端的战斗指令消息

图5-2接收战斗指令消息流程图

收到客户端发来的消息后,通过消息头和消息类型ID(可用宏定义或定义枚举类型)的判断,确定是否为战斗指令消息后,然后处理战斗指令消息。

本文实践项目从IOCP完成键可以获取玩家数据进而获取玩家所在的战局指针(BATTLE*)。收到战斗指令消息后,通过消息中的行动者索引nActorIndex从战局的战斗角色battleSprites数组中获取该角色,根据该角色的“速度”数值和战斗指令消息一并加入到战斗指令字典m_CommandMap中,以便之后遍历逐个处理。检测m_CommandMap的元素个数是否等于战斗角色总数(即检测是否所有战斗角色都已下达战斗指令,若等于则开始遍历m_CommandMap处理本回合的所有战斗指令。

5.2.4处理战斗指令并发送动画消息

图5-3服务器处理战斗指令消息流程图

当本回合所有战斗角色都下达战斗指令(服务器收到的指令数等于角色数量)后,开始倒序遍历(倒序的原因是,multimap按从小到大排序,而我们的逻辑是速度高的先行动)处理每一条战斗指令。

倒序遍历时,先判断发出该战斗指令消息的角色是否已经死亡,若死亡则不能进行任何行为,直接跳出进入下一次循环。若发出指令的角色没有死亡,根据指令的nActionType行动类型(攻击、使用技能、使用物品、更换宠物等)进行不同的处理,计算每一条战斗指令所造成的影响,并发送对应的动画消息。

若在计算完每一条战斗指令所造成的影响后,检查双方存活状况,若一方全员阵亡则战斗结束,并发送战斗动画播放消息,客户端开始播放动画。

若计算完所有战斗指令后战斗并没有结束,发送战斗动画播放消息,客户端开始播放动画,进入下一回合,清空m_CommandMap所有元素,服务器继续接收新回合的来自客户端的战斗指令消息。

下面是不同的行动类型的不同处理方式:

攻击:

声明一个战斗动画消息,nActionType赋值为“攻击(枚举或自定义宏)”,根据战斗指令消息里的nActorIndex行动者索引和nTargetIndex目标索引从战局中获取对应的战斗角色(battleSprites[nActorIndex]和battleSprites[nTargetIndex]),若目标死亡则从对应队伍的存活者列表中随机一个新目标索引。把目标索引值赋值给战斗动画消息的nTargetIndex[0]。

根据行动者攻击和目标防御力计算伤害值,赋值给nTargetHp[0],并且目标扣除对应Hp,判断目标扣除Hp后Hp是否小于等于0(即死亡),若死亡则从对应队伍的存活者列表中移除。

最后,发送战斗动画消息给战局所有玩家。

使用技能:

本文的实践项目通过数据库配置所有技能的相关数据,包括技能的ID、作用目标数量、特效动画ID、特效播放方式(仅在客户端使用)、Mp消耗、伤害公式等。

声明一个战斗动画消息,nActionType赋值为“使用技能(枚举或自定义宏)”,通过战斗指令消息的nParam查找数据库的技能表获取该技能的相关数据,根据战斗指令消息的nActorIndex和nTargetIndex从战局获得对应的角色battleSprites[nActorIndex]和battleSprites[nTargetIndex]),根据技能Mp消耗数值赋值到动画消息的nActorMp上,并且减少使用者角色的Mp。如果目标角色死亡,则从目标角色的队伍存活者列表中随机得到新的目标索引,并赋值多体战斗动画消息nTargetIndex数组的对应索引位置上,再根据行动者的魔法攻击力和目标的魔法防御力以及技能的伤害公式计算伤害数值,赋值nTargetHp数组对应的索引位置上,并且目标扣除对应Hp,判断目标扣除Hp后Hp是否小于等于0(即死亡),若死亡则从对应队伍的存活者列表中移除。之后再随机目标,并计算伤害数值,直到目标数量达到技能的作用目标数量。

最后,发送战斗动画消息给战局所有玩家。

使用物品:

本文的实践项目通过数据库配置所有物品的相关数据,包括物品的ID、作用于单体/全体、Hp回复量、Mp回复量等。

声明一个战斗动画消息,nActionType赋值为“使用物品(枚举或自定义宏)”,通过战斗指令消息的nParam获取玩家对应物品栏位置的物品ID(物品栏的实现本文不阐述),把物品ID赋值给战斗动画消息的nParam,根据物品ID查找数据库的物品表获取该物品的相关数据,根据战斗指令消息的nActorIndex赋值给动画消息的nActorIndex,根据物品是作用于单体还是全体对动画消息的nTargetIndex数组进行赋值,根据物品的Hp回复量和Mp回复量分别对nTargetHp和nTargetMp数组进行赋值。根据物品回复量更新目标的Hp/Mp,注意不能超出最大Hp上限/最大Mp上限。

最后,发送战斗动画消息给战局所有玩家。

5.2.5说明

本文为了尽量简短地阐述实现方式,战斗指令消息的行动者索引nActorIndex由客户端直接赋值,服务器并没有对此进行检查而直接使用。若客户端并没有使用自身角色的索引赋值而对nActorIndex进行修改作弊,则能够冒充其他玩家角色下达战斗指令,存在隐患,所以战斗指令消息的nActorIndex应该由服务器赋值并检查,但由于本文篇幅所限并且考虑到游戏逻辑的复杂性和不一性,上述实现方式并没有过多考虑防范客户端作弊,恳请读者体谅。

而实际上,服务器在很多地方都应该对客户端发来的消息进行合法性检查,防范客户端外挂或作弊,还请读者根据自身游戏逻辑另外考虑。

5.3客户端战斗系统的实现

5.3.1客户端战斗系统功能模块

图5-4客户端战斗系统功能模块

说明:

战斗系统主要包含战斗角色、战斗动画和战斗指令菜单三个模块;

战斗指令菜单类型分为人物和宠物两种,分别用于下达人物和宠物战斗指令,并发送战斗指令消息给服务器;

战斗角色由角色图形、动画控制器和血条组成,通过动画控制器切换不同动作的动画,主要负责战斗角色的图形显示;

战斗动画模块主要有动画计数、把接收到的所有动画消息转换为战斗动画类以及播放战斗动画三个功能;

动画计数主要是为了等待战斗中所有正在播放的角色动作动画、特效动画播放完,角色动作动画或特效动画开始时会+1动画计数,结束时会-1动画计数,动画计数为0后,当前单位行动结束,下一个行动单位开始行动;

播放战斗动画模块根据战斗动画类数据播放不同行为的动画(主要使用协程来控制战斗角色的移动、切换战斗角色的动作动画、添加特效等)。

5.3.2客户端战斗系统相关数据类型

//战斗动画类

publicclassBATTLE_ANIM

{

publicushortnActorIndex;//行动方索引

publicushort[]nTargetIndex=newushort[10];//目标索引

publicushortaction_type;//行动类型

publicushortnParam;//参数(根据行动类型而定,如使用技能则是技能ID,使用物品则是物品栏位置)

publicushortnAnimType;//技能动画类型

publicushortnEffectID;//技能特效ID

publicushortnTargetCount;//目标总数(nTargetIndex数组中值不为0的个数)

publicushortnReactionIndex;//当前受影响的目标索引

publicshortnActorMp;//行动方Mp影响(多用于计算技能使用者的Mp消耗)

publicshort[]nTargetHp=newshort[10];//目标索引对应的目标Hp影响

}

5.3.3进入战斗后初始化战斗角色

收到来自服务器的战斗初始化消息后,切换到战斗场景,并根据战斗初始化消息中的battleSprites数组(包含战斗角色的玩家ID、图形及动画控制器ID、名字、Hp和Mp等数据)动态生成对应的战斗角色。

并且从战斗初始化消息中的battleSprites数组中,对比玩家ID(客户端在登录游戏后,理应早已得到玩家ID,那么在这里与battleSprites数组的玩家ID进行对比)查找自身角色和宠物的索引,用变量保存该索引,用于下达战斗指令。

5.3.4下达战斗指令并发送战斗指令消息

下达战斗指令实质是地对战斗指令消息的赋值。

一般是先下达人物战斗指令,如有参战宠物,再下达战斗指令。在本文4.3.2中,进入战斗时,客户端已能获得自身人物和宠物在战斗中的索引,若是轮到人物下达战斗指令,则战斗指令消息的nActorIndex是人物的索引,若是轮到宠物下达战斗指令,则指令消息的nActorIndex是宠物的索引,分情况赋值即可,然后根据不同的行动类型对指令消息的nActionType行动类型赋值,最后通过点选目标对指令消息的nTargetIndex进行赋值。

当下达完人物和宠物的战斗指令后,分别发送对应的战斗指令消息。

战斗指令菜单的UI实现千变万化,本文不作阐述,使用最简单的按钮即可实现。

下图为实践项目的指令菜单,其中玩家选择了使用技能的行动,并即将点选技能:

图5-5指令菜单

5.3.5接收战斗动画消息

根据每一条接收到的战斗动画消息对new出来的战斗动画类赋值,并push_back到一个顺序容器(可考虑List)。

5.3.6收到战斗动画播放消息并播放战斗动画

当接收到来自服务器的战斗动画播放消息后,客户端开始从头到尾遍历存放了多个战斗动画类的顺序容器,根据战斗动画类的数据,逐个播放动画。

战斗动画的实现方式主要是依靠协同程序和动画计数器。角色播放动作动画(除待机)或每往场景中添加一个特效动画,动画计数加1,角色播放动作动画(除待机)结束时调用方法切回待机动画并且动画计数-1,特效动画播放结束时销毁自身并且调用方法把动画计数减1,当动画所有协程结束返回并且动画计数为0时,当前动画播放完毕,开始播放下一个动画。

当播放完所有战斗动画类后,清空容器,若一方全体阵亡,则战斗结束退出战斗场景,否则进入下一回合,玩家开始下达新回合的战斗指令。

根据战斗动画类的行动类型和特效播放方式的不同,动画播放实现方式也不同,由于动画的实现较为复杂,本文在5.3.7小节单独以普通攻击动画进行较详细的阐述,读者可结合本小节和5.3.7小节进行理解。

下面是各种战斗动画的实现方式:

普通攻击动画协程

①行动者角色执行协程Move(),从自身位置移动到目标角色位置,到达后协程结束返回;

②行动者播放攻击动作动画,目标播放受伤动作动画并显示目标扣血数值,更新角色Hp/Mp,若角色Hp为0则播放死亡动作动画;

③使用循环和yieldreturn0语句直到动画计数为0(即攻击动画、受伤动画播放完)跳出循环;

④行动者角色执行协程Back(),回到原本的位置,到达原本的位置后协程结束返回;

⑤协程结束返回。

攻击技能动画协程

①行动者角色执行协程Move(),从自身位置移动到目标角色位置,到达后协程结束返回;

②行动者播放攻击动作动画,添加并播放技能特效动画,目标播放受伤动作动画并显示目标扣血数值,更新角色Hp/Mp,若角色Hp为0则播放死亡动作动画;

③使用while循环和yieldreturn0语句直到动画计数为0(即攻击动画、受伤动画、特效动画播放完),后跳出循环;

④行动者角色执行协程Back(),回到原本的位置,到达原本的位置后Back()协程结束返回;

⑤协程结束返回。

魔法技能(同步型特效)动画协程

①行动者播放施法动作动画,遍历所有目标,并在目标位置上添加并播放特效动画,目标播放受伤动作动画并显示扣血数值,更新角色Hp/Mp,若角色Hp为0则播放死亡动作动画;

②使用while循环和yieldreturn0语句直到动画计数为0(即攻击动画、受伤动画、特效动画播放完),后跳出循环;

③协程结束返回。

动画效果如下图:

图5-6魔法技能(同步型特效)动画效果

魔法技能(异步型特效)动画协程

①行动者播放施法动作动画,遍历所有目标,并在目标位置上添加并播放特效动画,目标播放受伤动作动画并显示扣血数值,更新角色Hp/Mp,若角色Hp为0则播放死亡动作动画,遍历中使用yieldreturn帧数/秒数实现延迟,从而达到每个目标身上出现的特效动画时间不一样;

②使用while循环和yieldreturn0语句直到动画计数为0(即攻击动画、受伤动画、特效动画播放完),后跳出循环;

③协程结束返回。

动画效果如下图:

图5-7魔法技能(异步型特效)动画效果

魔法技能(弹射型特效)动画协程

①行动者播放施法动作动画,行动者身上添加一个循环播放的特效动画;

②遍历目标,使用协程控制特效动画的移动,使特效移动到目标的位置,到达位置后,目标播放受伤动作动画并显示扣血数值,更新角色Hp/Mp,若角色Hp为0则播放死亡动作动画,再让特效移动到下一个目标的位置,如此类推,直到最后一个目标,手动摧毁特效;

③协程结束返回。

动画效果如下图:

图5-8魔法技能(弹射型特效)动画效果

魔法技能(发射型特效)动画协程

①行动者播放施法动作动画,遍历目标,行动者身上添加数量与目标数相同循环播放的特效动画,并增加同等的动画计数;

②每个特效自身执行协程让自身移向对应目标的位置,到达后动画计数减1,特效销毁自身,目标播放受伤动作动画并显示扣血数值,更新角色Hp/Mp,若角色Hp为0则播放死亡动作动画;

③使用while循环和yieldreturn0语句直到动画计数为0(即攻击动画、受伤动画、特效动画播放完),后跳出循环;

④协程结束返回。

动画效果如下图:

图5-9魔法技能(发射型特效)动画效果

使用物品动画协程

①行动者播放施法动作动画,并在所有目标身上添加并播放用于恢复的特效动画并显示数值,更新角色Hp/Mp;

②使用while循环和yieldreturn0语句直到动画计数为0(即攻击动画、受伤动画、特效动画播放完),后跳出循环;

③协程结束返回。

5.3.7战斗动画实现细述

本小节以魔法技能动画(弹射型特效)的实现为例进行细述,以便读者理解实现原理。

相关伪代码如下:

//动画播放协程

IEnumeratorPlayAnim()

{

while(l_AnimList不为空)//存放战斗动画类的list容器不为空

{

//从容器中获得头一个战斗动画类

BATTLE_ANIMbattleAnim=l_AnimList[0];

switch(battleAnim.action_type)//判断行动类型

{

case普通攻击:

yieldreturnStartCoroutine(Attack(battleAnim));

break;

case使用技能:

根据battleAnim.nParam查询数据库技能表获得技能动画类型

battleAnim.nAnimType=技能动画类型

switch(battleAnim.nAnimType)//判断技能动画类型

{

case攻击技能动画:

yieldreturnStartCoroutine(MagicAttack(battleAnim));

break;

case魔法技能动画(弹射型特效):

yieldreturnStartCoroutine(MagicBounce(battleAnim));

break;

……

}

break;

case使用物品:

yieldreturnStartCoroutine(Item(battleAnim));

break;

}

while(动画计数器!=0)//(一帧一次循环)

{

//下一帧再回到这里执行下一次循环

yieldreturn1;

}

l_AnimList.Remove(0);//清除本次播放完的战斗动画类

}

//所有动画播放完

if(一方全体死亡)

战斗结束

else

进入下一回合,玩家下达指令

}

说明:

PlayAnim协同程序主要是遍历存放战斗动画类的list容器,通过遍历逐个播放战斗动画类数据所包含的动画,通过类型的判断,执行不同的协同程序实现不同类型动画的播放。

//魔法技能动画(弹射型特效)协程

IEnumeratorMagicBounce(BATTLE_ANIMbattleAnim)

{

行动者(施法者)播放施法动作动画;

根据特效ID动态加载特效预设;

由特效预设动态生成特效effect;

把effect移到到施法者的位置上;

动画计数+1;

for(inti=0;i<battleAnim.nTargetCount;i++)//遍历目标

{

获得battleAnim.nTargetIndex[i]目标索引并根据索引获取目标角色

destPos=目标角色的位置

while(effect的位置!=destPos)

{

特效的位置=Vector3.MoveTowards(特效的位置,destPos,400*Time.deltaTime);//即根据速度移向目标位置

yieldreturn0;

}

目标播放受伤动作动画并显示扣血

}

遍历完所有目标后,销毁特效

动画计数-1

}

说明:

MagicBounce协同

温馨提示

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

评论

0/150

提交评论