版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
摘要本文主要阐述网络回合制战斗系统的实现,包括回合制战斗系统的整个流程〔战斗开场、玩家下达战斗指令、战斗动画、战斗完毕〕,和网络回合制战斗系统常见的主要功能〔攻击、使用技能、使用物品〕的实现,并且在游戏工程"PKer"中进展实践和测试。在开发环境方面,效劳器采用国IOCP高性能开源框架“HP-Socket〞(Windows平台),并且连接到Mysql数据库,客户端采用近年比拟炽热的强大跨平台引擎Unity3D〔2D,C*〕并且使用Sqlite作为嵌入式数据库。除此以外,本文还对战斗系统实现所涉及到的相关技术如Unity引擎的协同程序、单例设计模式和分包算法进展简要介绍。
关键词回合制战斗系统,网络游戏,Unity3D,协同程序,游戏编程
ABSTRACTThisarticlemainlye*poundstheimplementationofnetworkturn-basedbatsystem,includingthewholeprocessofturn-basedbatsystem(battlebegins,theplayersfightinginstructions,batanimation,battleends),andthemainfunctionofmon(attack,usingskills,usingitems)implementation,andinthegame"PKer"inpracticeandtestoftheproject.Intheaspectofdevelopmentenvironment,C++IOCPserverwithWindowsplatformandconnecttotheMysqldatabase,theclientusesUnity3D(2D,C*)andusesSqliteasanembeddeddatabase.Inadditiontothis,thisarticlealsoinvolvedtobatsystemrelatedtechnologiessuchastheUnityenginecoroutines,singletondesignpatternandthesubcontractalgorithmarebrieflyintroduced.
KEYWORDSTurn-basedbatsystem,onlinegame,Unity3D,Coroutine,Singletondesignpattern
目录
TOC\f\h\z\t"样式1,1,样式2,2,样式3,3"
前言
3
第1章绪论
3
1.1回合制游戏简介
3
1.1.1广义上的回合制游戏
3
1.1.2狭义上的回合制游戏
3
1.2回合制战斗系统简介
3
1.3实践工程"PKer"简介
3
第2章开发环境和局部涉及技术简介
3
2.1开发环境简介
3
2.2Unity协同程序
3
2.2.1Unity协同程序简介
3
2.2.2Unity协同程序的运用
3
2.3单例设计模式
3
2.3.1单例设计模式
3
2.3.2单例设计模式的运用
3
2.3.3单例设计模式在Unity中的运用
3
2.4分包算法
3
2.4.1分包的原因
3
2.4.2环形缓冲区〔CircularBuffer〕
3
2.4.3分包算法流程图
3
第3章战斗动画实现方案的研究与比照
3
3.1研究背景
3
3.2实现方案一:有限状态机
3
3.3实现方案二:协同程序(Coroutine)
3
3.4方案抉择结论
3
第4章回合制网游战斗系统设计
3
4.1回合制网游战斗系统设计
3
4.2回合制网游战斗系统效劳器实现思路
3
4.3回合制网游战斗系统客户端实现思路
3
4.4战斗系统框架构造〔以实践工程"PKer"为例〕
3
第5章回合制战斗系统实现
3
5.1战斗系统相关消息构造体的定义
3
5.2效劳器战斗系统的实现
3
5.2.1效劳器战斗系统相关构造体
3
5.2.2初始化一个战局
3
5.2.3接收客户端的战斗指令消息
3
5.2.4处理战斗指令并发送动画消息
3
5.2.5说明
3
5.3客户端战斗系统的实现
3
5.3.1客户端战斗系统功能模块
3
5.3.2客户端战斗系统相关数据类型
3
5.3.3进入战斗后初始化战斗角色
3
5.3.4下达战斗指令并发送战斗指令消息
3
5.3.5接收战斗动画消息
3
5.3.6收到战斗动画播放消息并播放战斗动画
3
5.3.7战斗动画实现细述
3
第6章工程测试
3
6.1进入战斗测试
3
6.2普通攻击功能测试
3
6.3使用技能功能测试
3
6.4使用物品功能测试
3
结论
3
参考文献
3
致
3
前言
随着网络的普及与迅速开展,网络游戏已经成为电子游戏中的主流。网络游戏的战斗系统主要可分为即时制和回合制两种模式。即时制战斗系统侧重于刺激、反响、操作性,战斗节奏快。而回合制战斗系统侧重于休闲、战略、配合,战斗节奏慢。早期的电子游戏由于设备硬件条件有限,大多采用回合制战斗系统。
随着科技的开展和设备硬件的提升,如今回合制游戏的数量比例有所下降,但回合制游戏在国游戏市场依旧占据着想当大的份额,仍有大量玩家热衷这种战斗模式,几款众所周知的国产单机游戏如"轩辕剑"系列、"仙剑奇侠传"系列、"古剑奇谭"系列以及国在线人数最多的MMORPG"梦幻西游"均是采用回合制战斗系统的游戏。
本人也是热衷于回合制战斗系统的玩家之一,可纵观近年来国回合制游戏的开展,国回合制游戏一直止步不前,"轩辕剑"系列、"仙剑奇侠传"系列、"古剑奇谭"销量下降,MMORPG"梦幻西游"也有降温迹象,甚至国大多回合制网游只是复制在"梦幻西游"或“换皮〞,缺少创新和突破。本人希望在研究和实现回合制战斗系统的根底上,能够找到突破和创新点。同时在游戏编程方面,即时制逻辑较为简单明了清晰,而回合制则比拟繁琐复杂,而且在Unity引擎上实现回合制战斗系统的相关资料较少。综上原因,本人撰写本文,希望能对志同道合者有所启发,同时也寻求学术交流。
工程将在2017年6月开源,开源地址s://pan.baidu./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(nTargetInde*)));
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.nActorInde*-1];
yieldreturnStartCoroutine(sprite.Move(getAttackPosition(nTargetInde*)));
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章回合制战斗系统实现
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
publicshortnHpMa*;//最大HP
publicshortnHp;//当前HP
publicshortnMpMa*;//最大MP
publicshortnMp;//当前MP
};
//战斗指令消息
publicstructMSG_BATTLE_MAND
{
publicushortnMsgID;//消息类型ID
publicushortnLen;//构造体长度
publicushortnActorType;//行动者类型〔1=人物2=宠物,由效劳端赋值,客户端不需要赋值〕
publicushortnActorInde*;//行动者索引〔由效劳端赋值,客户端不需要赋值〕
publicushortnActionType;//行动的类型
publicushortnTargetInde*;//目标索引
publicushortnParam;//参数〔根据行动类型而定,如使用技能则是技能ID,使用物品则是物品栏位置,切换战宠则是宠物栏位置〕
};
//战斗动画播放消息〔客户端收到此消息后开场播放战斗动画〕
publicstructMSG_BATTLE_ANIM_BEGIN
{
publicushortnMsgID;//消息类型ID
publicushortnLen;//构造体长度
};
//战斗动画消息
publicstructMSG_BATTLE_ANIM
{
publicushortnMsgID;//消息类型ID
publicushortnLen;//构造体长度
publicushortnActorInde*;//行动者索引
[MarshalAs(UnmanagedType.ByValArray,SizeConst=10)]
publicushort[]nTargetInde*;//目标索引〔初始值为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_inde*;//队伍1存活者索引list
std::list<USHORT>team2_survivors_inde*;//队伍2存活者索引list
USHORTnSpriteTotal;//参战角色总数
std::multimap<float,MSG_BATTLE_MAND*>m_mandMap;//玩家战斗指令字典,用于保存每回合接收的战斗指令消息,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=宠物
USHORTbmand;//是否已经下命令,0=没下命令,1=已下命令
}
//战斗数据(人物与宠物通用)
structBATTLE_SPRITE_DATA
{
UINTnID;//玩家/宠物ID
charstrName[22];//战斗人物/宠物名字
UINTnImageID;//图形及动画控制器ID
USHORTnLevel;//等级
shortnHpMa*;//最大Hp
shortnHp;//当前Hp
shortnMpMa*;//最大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*〕。收到战斗指令消息后,通过消息中的行动者索引nActorInde*从战局的战斗角色battleSprites数组中获取该角色,根据该角色的“速度〞数值和战斗指令消息一并参加到战斗指令字典m_mandMap中,以便之后遍历逐个处理。检测m_mandMap的元素个数是否等于战斗角色总数〔即检测是否所有战斗角色都已下达战斗指令,假设等于则开场遍历m_mandMap处理本回合的所有战斗指令。
5.2.4处理战斗指令并发送动画消息
图5-3效劳器处理战斗指令消息流程图
当本回合所有战斗角色都下达战斗指令〔效劳器收到的指令数等于角色数量〕后,开场倒序遍历〔倒序的原因是,multimap按从小到大排序,而我们的逻辑是速度高的先行动〕处理每一条战斗指令。
倒序遍历时,先判断发出该战斗指令消息的角色是否已经死亡,假设死亡则不能进展任何行为,直接跳出进入下一次循环。假设发出指令的角色没有死亡,根据指令的nActionType行动类型〔攻击、使用技能、使用物品、更换宠物等〕进展不同的处理,计算每一条战斗指令所造成的影响,并发送对应的动画消息。
假设在计算完每一条战斗指令所造成的影响后,检查双方存活状况,假设一方全员阵亡则战斗完毕,并发送战斗动画播放消息,客户端开场播放动画。
假设计算完所有战斗指令后战斗并没有完毕,发送战斗动画播放消息,客户端开场播放动画,进入下一回合,清空m_mandMap所有元素,效劳器继续接收新回合的来自客户端的战斗指令消息。
下面是不同的行动类型的不同处理方式:
攻击:
声明一个战斗动画消息,nActionType赋值为“攻击〔枚举或自定义宏〕〞,根据战斗指令消息里的nActorInde*行动者索引和nTargetInde*目标索引从战局中获取对应的战斗角色〔battleSprites[nActorInde*]和battleSprites[nTargetInde*]〕,假设目标死亡则从对应队伍的存活者列表中随机一个新目标索引。把目标索引值赋值给战斗动画消息的nTargetInde*[0]。
根据行动者攻击和目标防御力计算伤害值,赋值给nTargetHp[0],并且目标扣除对应Hp,判断目标扣除Hp后Hp是否小于等于0〔即死亡〕,假设死亡则从对应队伍的存活者列表中移除。
最后,发送战斗动画消息给战局所有玩家。
使用技能:
本文的实践工程通过数据库配置所有技能的相关数据,包括技能的ID、作用目标数量、特效动画ID、特效播放方式〔仅在客户端使用〕、Mp消耗、伤害公式等。
声明一个战斗动画消息,nActionType赋值为“使用技能〔枚举或自定义宏〕〞,通过战斗指令消息的nParam查找数据库的技能表获取该技能的相关数据,根据战斗指令消息的nActorInde*和nTargetInde*从战局获得对应的角色battleSprites[nActorInde*]和battleSprites[nTargetInde*]〕,根据技能Mp消耗数值赋值到动画消息的nActorMp上,并且减少使用者角色的Mp。如果目标角色死亡,则从目标角色的队伍存活者列表中随机得到新的目标索引,并赋值多体战斗动画消息nTargetInde*数组的对应索引位置上,再根据行动者的魔法攻击力和目标的魔法防御力以及技能的伤害公式计算伤害数值,赋值nTargetHp数组对应的索引位置上,并且目标扣除对应Hp,判断目标扣除Hp后Hp是否小于等于0〔即死亡〕,假设死亡则从对应队伍的存活者列表中移除。之后再随机目标,并计算伤害数值,直到目标数量到达技能的作用目标数量。
最后,发送战斗动画消息给战局所有玩家。
使用物品:
本文的实践工程通过数据库配置所有物品的相关数据,包括物品的ID、作用于单体/全体、Hp回复量、Mp回复量等。
声明一个战斗动画消息,nActionType赋值为“使用物品〔枚举或自定义宏〕〞,通过战斗指令消息的nParam获取玩家对应物品栏位置的物品ID〔物品栏的实现本文不阐述〕,把物品ID赋值给战斗动画消息的nParam,根据物品ID查找数据库的物品表获取该物品的相关数据,根据战斗指令消息的nActorInde*赋值给动画消息的nActorInde*,根据物品是作用于单体还是全体对动画消息的nTargetInde*数组进展赋值,根据物品的Hp回复量和Mp回复量分别对nTargetHp和nTargetMp数组进展赋值。根据物品回复量更新目标的Hp/Mp,注意不能超出最大Hp上限/最大Mp上限。
最后,发送战斗动画消息给战局所有玩家。
5.2.5说明
本文为了尽量简短地阐述实现方式,战斗指令消息的行动者索引nActorInde*由客户端直接赋值,效劳器并没有对此进展检查而直接使用。假设客户端并没有使用自身角色的索引赋值而对nActorInde*进展修改作弊,则能够冒充其他玩家角色下达战斗指令,存在隐患,所以战斗指令消息的nActorInde*应该由效劳器赋值并检查,但由于本文篇幅所限并且考虑到游戏逻辑的复杂性和不一性,上述实现方式并没有过多考虑防客户端作弊,恳请读者体谅。
而实际上,效劳器在很多地方都应该对客户端发来的消息进展合法性检查,防客户端外挂或作弊,还请读者根据自身游戏逻辑另外考虑。
5.3客户端战斗系统的实现
5.3.1客户端战斗系统功能模块
图5-4客户端战斗系统功能模块
说明:
战斗系统主要包含战斗角色、战斗动画和战斗指令菜单三个模块;
战斗指令菜单类型分为人物和宠物两种,分别用于下达人物和宠物战斗指令,并发送战斗指令消息给效劳器;
战斗角色由角色图形、动画控制器和血条组成,通过动画控制器切换不同动作的动画,主要负责战斗角色的图形显示;
战斗动画模块主要有动画计数、把接收到的所有动画消息转换为战斗动画类以及播放战斗动画三个功能;
动画计数主要是为了等待战斗中所有正在播放的角色动作动画、特效动画播放完,角色动作动画或特效动画开场时会+1动画计数,完毕时会-1动画计数,动画计数为0后,当前单位行动完毕,下一个行动单位开场行动;
播放战斗动画模块根据战斗动画类数据播放不同行为的动画〔主要使用协程来控制战斗角色的移动、切换战斗角色的动作动画、添加特效等〕。
5.3.2客户端战斗系统相关数据类型
//战斗动画类
publicclassBATTLE_ANIM
{
publicushortnActorInde*;//行动方索引
publicushort[]nTargetInde*=newushort[10];//目标索引
publicushortaction_type;//行动类型
publicushortnParam;//参数〔根据行动类型而定,如使用技能则是技能ID,使用物品则是物品栏位置〕
publicushortnAnimType;//技能动画类型
publicushortnEffectID;//技能特效ID
publicushortnTargetCount;//目标总数〔nTargetInde*数组中值不为0的个数〕
publicushortnReactionInde*;//当前受影响的目标索引
publicshortnActorMp;//行动方Mp影响〔多用于计算技能使用者的Mp消耗〕
publicshort[]nTargetHp=newshort[10];//目标索引对应的目标Hp影响
}
5.3.3进入战斗后初始化战斗角色
收到来自效劳器的战斗初始化消息后,切换到战斗场景,并根据战斗初始化消息中的battleSprites数组〔包含战斗角色的玩家ID、图形及动画控制器ID、名字、Hp和Mp等数据〕动态生成对应的战斗角色。
并且从战斗初始化消息中的battleSprites数组中,比照玩家ID〔客户端在登录游戏后,理应早已得到玩家ID,则在这里与battleSprites数组的玩家ID进展比照〕查找自身角色和宠物的索引,用变量保存该索引,用于下达战斗指令。
5.3.4下达战斗指令并发送战斗指令消息
下达战斗指令实质是地对战斗指令消息的赋值。
一般是先下达人物战斗指令,如有参战宠物,再下达战斗指令。在本文中,进入战斗时,客户端已能获得自身人物和宠物在战斗中的索引,假设是轮到人物下达战斗指令,则战斗指令消息的nActorInde*是人物的索引,假设是轮到宠物下达战斗指令,则指令消息的nActorInde*是宠物的索引,分情况赋值即可,然后根据不同的行动类型对指令消息的nActionType行动类型赋值,最后通过点选目标对指令消息的nTargetInde*进展赋值。
当下达完人物和宠物的战斗指令后,分别发送对应的战斗指令消息。
战斗指令菜单的UI实现千变万化,本文不作阐述,使用最简单的按钮即可实现。
下列图为实践工程的指令菜单,其中玩家选择了使用技能的行动,并即将点选技能:
图5-5指令菜单
5.3.5接收战斗动画消息
根据每一条接收到的战斗动画消息对new出来的战斗动画类赋值,并push_back到一个顺序容器〔可考虑List〕。
5.3.6收到战斗动画播放消息并播放战斗动画
当接收到来自效劳器的战斗动画播放消息后,客户端开场从头到尾遍历存放了多个战斗动画类的顺序容器,根据战斗动画类的数据,逐个播放动画。
战斗动画的实现方式主要是依靠协同程序和动画计数器。角色播放动作动画〔除待机〕或每往场景中添加一个特效动画,动画计数加1,角色播放动作动画〔除待机〕完毕时调用方法切回待机动画并且动画计数-1,特效动画播放完毕时销毁自身并且调用方法把动画计数减1,当动画所有协程完毕返回并且动画计数为0时,当前动画播放完毕,开场播放下一个动画。
当播放完所有战斗动画类后,清空容器,假设一方全体阵亡,则战斗完毕退出战斗场景,否则进入下一回合,玩家开场下达新回合的战斗指令。
根据战斗动画类的行动类型和特效播放方式的不同,动画播放实现方式也不同,由于动画的实现较为复杂,本文在小节单独以普通攻击动画进展较详细的阐述,读者可结合本小节和小节进展理解。
下面是各种战斗动画的实现方式:
普通攻击动画协程
①行动者角色执行协程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.nTargetInde*[i]目标索引并根据索引获取目标角色
destPos=目标角色的位置
while(effect的位置!=destPos)
{
特效的位置=Vector3.MoveTowards(特效的位置,destPos,400*Time.deltaTime);//即根据速度移向目标位置
yieldreturn0;
}
目标播放受伤动作动画并显示扣血
}
遍历完所有目标后,销毁特效
动画计数-1
}
说明:
MagicBounce协同程序主要是控制特效的移动,实现弹射型特效动画的播放。
最终的动画效果为:行动者施法,技能特效从行动者身上移向第一个目标,特效到达第一个目标的位置后目标受伤并扣血,特效继续移向下一个目标,如此类推,直到最后一个目标后,技能动画完毕,销毁特效,协程完毕返回到PlayAnim(),PlayAnim()继续遍历播放下一个动画。
其他动画类型与上
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 国际物流代理合同2026
- 著作权许可使用合同2026年版
- 油漆涂料原料采购合同2026
- 平台化数据标注质量保证协议2026
- 脊髓拴系患者的医疗服务投诉处理改进措施
- 2026年脂肪肝运动与饮食处方模板
- 2026年小学围棋教学的开展与思维品质培养
- 全球供应链运输协议2026
- 印刷包装设备租赁合同协议
- 物流配送2026年持续改进服务合同
- 2026中国铁塔夏季校园招聘备考题库附答案详解(轻巧夺冠)
- 2026年软考高级系统架构设计师真题及答案解析
- 2026重庆新华书店有限公司招聘工作人员47名备考题库及参考答案详解一套
- 2025年软考《数据库系统工程师》考试试题及答案
- 服装系毕业设计
- 2026年银行金融基础知识复习通关试题库带答案详解(完整版)
- 2026年湖北省黄冈市八年级地理生物会考真题试卷(+答案)
- 2026年部编版新教材语文一年级下册第四单元检测题(有答案)
- 江西省省宜春市袁州区重点名校2026届中考数学模拟预测题含解析
- 舞蹈类创新创业
- 部编版(2024)七年级下册 第六单元 单元测试题(含答案)
评论
0/150
提交评论