MOBA游戏的术语都有哪些?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MOBA游戏的术语都有哪些?相关的知识,希望对你有一定的参考价值。
1、feed
送人头/经验书。经常死掉从而养肥敌人的玩家(敌人通过杀他而获得了很多金钱)。被叫做经验书可不是一种赞美。杀经验书的玩家会获得很多杀人数。
2、Gank
gank是一个游戏用语,MOBA类游戏中的一种常用战术,用来指在游戏中一个或几个的游戏角色行动,对对方的游戏角色进行偷袭、包抄、围杀,或者说以人数或技能优势有预谋地击杀对手以起到压制作用。
3、KS
Kill Steal的简称,指专门对敌方英雄造成最后一下伤害获得金钱和杀人数,而实际上对这个英雄的多数伤害是由队友造成的,又称“抢人头”。
4、CD
技能冷却时间,技能使用后只能经过一段固定时间后再次使用,这就是冷却。有的技能冷却时间非常短,甚至低于一秒,它们可以经常使用。另外一些技能的冷却时间则很长,几分钟才能使用一次。
5、BUFF
杀死特定的野怪后获得增益魔法,持续一段时间后消失,若持有BUFF的同时被杀死,那么敌人将获得这个BUFF的增益效果。
6、FB
FB是1血的意思,也是DOTA类游戏的魅力之一,FB的全称是firstblood,指第一个杀死的英雄,不光有击杀英雄的奖励,还会额外的奖励的100元(杀英雄本身获得300)
7、拉野
拉野是MOBA游戏的一种战术名称,即指英雄去吸引野怪到线上,与正在行进中的小兵相遇,然后小兵帮助英雄打野怪。拉野能够消耗线上己方小兵兵力,还能通过击杀野怪所获得前期高额的经济与经验回报。
另一种形式是英雄去吸引野怪,让野怪走出野怪家,当野怪走出野怪家而且同时到了野怪刷新的时间,会在这个野怪家又刷新一波野怪,通过不断的囤积,每一个野怪家都能变成一个经济与经验的金矿。
8、辅助
主要辅助ADC的一个角色,不补兵,带眼,加血,主要保护ADC为目的。提示队友各大BUFF刷新时间,辅助有两种打法,一种是传统的打法,即纯辅助。辅助的作用仅在于给团队提供帮助,比如加血和插眼。另一种打法是将辅助作为全队的核心。此时的辅助能肉能输出,布眼掌控全局。
9、DPS
Damage Per Second,每秒输出伤害。同时也指能够高输出高攻的单位。一般情况下DPS后边应是数字代表平均每秒对目标造成的伤害数值,比如DPS 3000 就表示在一段时间内某人对某目标平均每秒造成3000点的伤害,在很多在线游戏中玩家组队召唤队友时所说的“来T 来DPS”中的DPS,实际是DPSer,可以理解成负责输出伤害者。
10、补刀
补刀又称补兵,MOBA游戏的术语,主要用于以dota为例的一类游戏术语,指的是抢最后一次攻击杀死小兵的技术。补刀分为正补与反补(Deny),正补是指抢最后一次攻击杀死敌方小兵而获取金钱,反补是指通过抢最后一次攻击杀死己方小兵影响对方的正补和经验
参考技术A术语翻译
T:指Tank,肉盾,护甲高,血量多,能够承受大量伤害的英雄。
DPS:damage per second,伤害每秒,特指能够对敌人造成大量伤害的英雄。
AD:是指以物理伤害为主的一类英雄。
ADC:是指以远程物理伤害为主的一类英雄。
AP:是指以法系伤害为主的一类英雄。
APC:是指以远程法系伤害为主的一类英雄。
Carry:后期,核心,需要大量的金钱去堆积装备的英雄,成型后威力很大,起决定性作用。
Gank:GangbangKill的缩写,游戏中的一种常用战术,指两个以上的英雄并肩作战,对敌方英雄进行偷袭、包抄、围杀。通常是以多打少,又称“抓人”。
Stun:带有眩晕效果的技能的总称,也指打断对手持续性施法和施法动作的打断技。
Solo:一条线路上一个人,指英雄单独处于一路兵线上与敌人对峙,经验高升级速度远超其他两路。两名玩家一对一单独对抗,无其它人干预。
Aoe :area of effect,效果范围,引申为有范围效果的技能
KS:Kill Steal的简称,指专门对敌方英雄造成最后一下伤害获得金钱和杀人数,而实际上对这个英雄的多数伤害是由队友造成的,又称“抢人头”。
CD:技能冷却时间,技能使用后只能经过一段固定时间后再次使用,这就是冷却。有的技能冷却时间非常短,甚至低于一秒,它们可以经常使用。另外一些技能的冷却时间则很长,几分钟才能使用一次。
BUFF:杀死特定的野怪后获得增益魔法,持续一段时间后消失,若持有BUFF的同时被杀死,那么敌人将获得这个BUFF的增益效果。
DEBUFF:相对于Buff的增益,Debuff就是减益效果了。有的英雄会有控制技能,比如减速、禁锢等等。
ULT:大招
ID:你的游戏角色名字
HP:指自己的生命值
FB:1血,也是DOTA类游戏的魅力之一,FB的全称是firstblood,指第一个杀死的英雄,不光有击杀英雄的奖励,还会额外的奖励的100元(杀英雄本身获得300)
3:闪人,撤退
b:闪人,撤退
卡:机器卡,网络延迟,服务器卡
Lag:一般是指网络延迟,当然有时候也是因为电脑的原因,总体意思和“卡”差不多。
Combo:由2个或者2个以上的英雄进行配合,打出的连招或者组合技。
Aoe:范围伤害,范围攻击。
Gank:游走杀人,限制对方英雄发展(一般简称抓人)
KS:抢人头
补刀:对方的小兵要没血的时候,砍他最后一下,获取金钱
ult:大招
push:推进,进攻。
farm:打兵赚钱
afk:人不在电脑面前,离开,英雄挂机等等。
bd:偷塔
mia:敌人在地图上消失,让队友提高警惕
miss:对线英雄在所在路消失,同上
noob:菜鸟
Jungling:打野
打野:杀地图上的中立怪物
拉野:依靠走位视野阴影等方式对野怪进行击杀,通常这种杀怪方式比正常的杀怪方式要慢一些,但是耗血比较少。
黑店:指游戏平台上几个认识的人在一起玩
集火:集中火力攻击一个人
先手控/先手技能:指向性的控制技能
后手控:在第一轮技能或者攻击完以后进行第二轮技能控制
Tank:肉盾,坦克,专门用于保护己方重要英雄或者吸收对方伤害。
奶妈:治疗
超神:在自己不死的情况下连续杀死对方多个英雄
超鬼:自己死了很多很多次
BLINK:闪现技能
stun:带有晕眩效果的技能
slow:带有减速效果的技能
brb:马上回来
弹道:远程英雄普通攻击出手到攻击到敌方单位的时间
沉默:对对方英雄使用禁魔技能
持续施法:需要站在一个地方吟唱,直到法术释放完毕。
CD:冷却时间
神装:能将自己攻击或防御达到极限的装备,最大化输出和生存
红药:加血回血
蓝药:回复魔法值
top:上路
bot:下路
mid:中路
base:基地/老家
care:小心
oom:没魔了
dd :直接伤害技能。先点技能,后点目标,就可以看到效果,这就是DD的好处。比如黑暗之子安妮的Q键,火球。
feed :送人头/经验书。经常死掉从而养肥敌人的玩家(敌人通过杀他而获得了很多金钱)。被叫做经验书可不是一种赞美。杀经验书的玩家会获得很多杀人数。
ms:移动速度
gl hf:good luck have fun=祝你好运。通常会在游戏开始打这句话进行礼貌性的问候。
gg:good game。大部分时候是在游戏结束的时候,由输的一方打出来。称赞对方打的好,自己认输的一种表现。
辅助:主要辅助ADC的一个角色,不补兵,带眼,加血,主要保护ADC为目的。提示队友各大BUFF刷新时间
小学生:指那些刚入门的玩家,对游戏的理解让你大跌眼镜,没有操作,没有意识.
团控:团体控制
点控:单人控制
shat down:击杀对面长时间未死亡并已经至少连杀3人。可获得500金
控线:就是把自己的小兵控制在自己的塔附近,防止自己被对方gank,也便于让自己人gank对方。
全屏流:是指技能释放的覆盖面非常大。当然全屏传送也应该算做全屏流
包眼:整场比赛买眼睛。一般是由辅助英雄来做。
PUB:路人局
BAN人:特定模式中(排位赛)禁用对方英雄
ARM/Armor:物理护甲
MR/Magic Resistance:魔法抗性
谈一款MOBA类游戏《码神联盟》的服务端架构设计与实现
-
一、前言
《码神联盟》是一款为技术人做的开源情怀游戏,每一种编程语言都是一位英雄。客户端和服务端均使用C#开发,客户端使用Unity3D引擎,数据库使用MySQL。这个MOBA类游戏是笔者在学习时期和客户端美术策划的小伙伴一起做的游戏,笔者主要负责游戏服务端开发,客户端也参与了一部分,同时也是这个项目的发起和负责人。这次主要分享这款游戏的服务端相关的设计与实现,从整体的架构设计,到服务器网络通信底层的搭建,通信协议、模型定制,再到游戏逻辑的分层架构实现。同时这篇博客也沉淀了笔者在游戏公司实践五个月后对游戏架构与设计的重新审视与思考。
这款游戏自去年完成后笔者曾多次想写篇博客来分享,也曾多次停笔,只因总觉得灵感还不够积淀还不够思考还不够,现在终于可以跨过这一步和大家分享,希望可以带来的是干货与诚意满满。由于目前关于游戏服务端相关的介绍文章少之又少,而为数不多的几篇也都是站在游戏服务端发展历史和架构的角度上进行分享,很少涉及具体的实现,这篇文章我将尝试多从实现的层面上加以介绍,所附的代码均有详尽注释,篇幅较长,可以关注收藏后再看。学习时期做的项目可能无法达到工业级,单服承载大概9000人,笔者在和小伙伴做这款游戏时农药还没有现在这般火。 : )
-
二、服务器架构
上图为这款游戏的服务器架构和主要逻辑流程图,笔者将游戏的代码实现分为三个主要模块:Protocol通信协议、NetFrame服务器网络通信底层的搭建以及LOLServer游戏的具体逻辑分层架构实现,下面将针对每个模块进行分别介绍。
-
三、通信协议
先从最简单也最基本的通信协议部分说起,我们可以看到这部分代码主要分为xxxProtocol、xxxDTO和xxxModel、以及xxxData四种类型,让我们来对它们的作用一探究竟。
-
1.Protocol协议
LOLServer\Protocol\Protocol.cs
using System; using System.Collections.Generic; using System.Text; namespace GameProtocol { public class Protocol { public const byte TYPE_LOGIN = 0;//登录模块 public const byte TYPE_USER = 1;//用户模块 public const byte TYPE_MATCH = 2;//战斗匹配模块 public const byte TYPE_SELECT = 3;//战斗选人模块 public const byte TYPE_FIGHT = 4;//战斗模块 } }
从上述的代码举例可以看到,在Protocol协议部分,我们主要是定义了一些常量用于模块通信,在这个部分分别定义了用户协议、登录协议、战斗匹配协议、战斗选人协议以及战斗协议。
-
2.DTO数据传输对象
DTO即数据传输对象,表现层与应用层之间是通过数据传输对象(DTO)进行交互的,需要了解的是,数据传输对象DTO本身并不是业务对象。数据传输对象是根据UI的需求进行设计的,而不是根据领域对象进行设计的。比如,User领域对象可能会包含一些诸如name, level, exp, email等信息。但如果UI上不打算显示email的信息,那么UserDTO中也无需包含这个email的数据。
简单来说Model面向业务,我们是通过业务来定义Model的。而DTO是面向界面UI,是通过UI的需求来定义的。通过DTO我们实现了表现层与Model之间的解耦,表现层不引用Model,如果开发过程中我们的模型改变了,而界面没变,我们就只需要改Model而不需要去改表现层中的东西。
using System; using System.Collections.Generic; using System.Text; namespace GameProtocol.dto { [Serializable] public class UserDTO { public int id;//玩家ID 唯一主键 public string name;//玩家昵称 public int level;//玩家等级 public int exp;//玩家经验 public int winCount;//胜利场次 public int loseCount;//失败场次 public int ranCount;//逃跑场次 public int[] heroList;//玩家拥有的英雄列表 public UserDTO() { } public UserDTO(string name, int id, int level, int win, int lose, int ran,int[] heroList) { this.id = id; this.name = name; this.winCount = win; this.loseCount = lose; this.ranCount = ran; this.level = level; this.heroList = heroList; } } }
-
3.Data属性配置表
这部分的实现主要是为了将程序功能与属性配置分离,后面可以由策划来配置这部分内容,由导表工具自动生成配表,从而减轻程序的开发工作量,扩展游戏的功能。
using System; using System.Collections.Generic; using System.Text; namespace GameProtocol.constans { /// <summary> /// 英雄属性配置表 /// </summary> public class HeroData { public static readonly Dictionary<int, HeroDataModel> heroMap = new Dictionary<int, HeroDataModel>(); /// <summary> /// 静态构造 初次访问的时候自动调用 /// </summary> static HeroData() { create(1, "西嘉迦[C++]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200,200, 1, 2, 3, 4); create(2, "派森[Python]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 1, 2, 3, 4); create(3, "扎瓦[Java]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 6, 2, 3, 4); create(4, "琵欸赤貔[PHP]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 3, 2, 3, 4); } /// <summary> /// 创建模型并添加进字典 /// </summary> /// <param name="code"></param> /// <param name="name"></param> /// <param name="atkBase"></param> /// <param name="defBase"></param> /// <param name="hpBase"></param> /// <param name="mpBase"></param> /// <param name="atkArr"></param> /// <param name="defArr"></param> /// <param name="hpArr"></param> /// <param name="mpArr"></param> /// <param name="speed"></param> /// <param name="aSpeed"></param> /// <param name="range"></param> /// <param name="eyeRange"></param> /// <param name="skills"></param> private static void create(int code, string name, int atkBase, int defBase, int hpBase, int mpBase, int atkArr, int defArr, int hpArr, int mpArr, float speed, float aSpeed, float range, float eyeRange, params int[] skills) { HeroDataModel model = new HeroDataModel(); model.code = code; model.name = name; model.atkBase = atkBase; model.defBase = defBase; model.hpBase = hpBase; model.mpBase = mpBase; model.atkArr = atkArr; model.defArr = defArr; model.hpArr = hpArr; model.mpArr = mpArr; model.speed = speed; model.aSpeed = aSpeed; model.range = range; model.eyeRange = eyeRange; model.skills = skills; heroMap.Add(code, model); } } public partial class HeroDataModel { public int code;//策划定义的唯一编号 public string name;//英雄名称 public int atkBase;//初始(基础)攻击力 public int defBase;//初始防御 public int hpBase;//初始血量 public int mpBase;//初始蓝 public int atkArr;//攻击成长 public int defArr;//防御成长 public int hpArr;//血量成长 public int mpArr;//蓝成长 public float speed;//移动速度 public float aSpeed;//攻击速度 public float range;//攻击距离 public float eyeRange;//视野范围 public int[] skills;//拥有技能 } }
-
四、服务器通信底层搭建
这部分为服务器的网络通信底层实现,也是游戏服务器的核心内容,下面将结合具体的代码以及代码注释一一介绍底层的实现,可能会涉及到一些C#的网络编程知识,对C#语言不熟悉没关系,笔者对C#的运用也仅仅停留在使用阶段,只需通过C#这门简单易懂的语言来窥探整个服务器通信底层搭建起来的过程,来到我们的NetFrame网络通信框架,这部分干货很多,我将用完整的代码和详尽的注释来阐明其意。
-
1.四层Socket模型
将SocketModel分为了四个层级,分别为:
(1)type:一级协议 用于区分所属模块,如用户模块
(2)area:二级协议 用于区分模块下的所属子模块,如用户模块的子模块为道具模块1、装备模块2、技能模块3等
(3)command:三级协议 用于区分当前处理逻辑功能,如道具模块的逻辑功能有“使用(申请/结果),丢弃,获得”等,技能模块的逻辑功能有“学习,升级,遗忘”等;
(4)message:消息体 当前需要处理的主体数据,如技能书
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NetFrame.auto { public class SocketModel { /// <summary> /// 一级协议 用于区分所属模块 /// </summary> public byte type {get;set;} /// <summary> /// 二级协议 用于区分 模块下所属子模块 /// </summary> public int area { get; set; } /// <summary> /// 三级协议 用于区分当前处理逻辑功能 /// </summary> public int command { get; set; } /// <summary> /// 消息体 当前需要处理的主体数据 /// </summary> public object message { get; set; } public SocketModel() { } public SocketModel(byte t,int a,int c,object o) { this.type = t; this.area = a; this.command = c; this.message = o; } public T GetMessage<T>() { return (T)message; } } }
同时封装了一个消息封装的方法,收到消息的处理流程如图所示:
-
2.对象序列化与反序列化为对象
序列化: 将数据结构或对象转换成二进制串的过程。
反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Threading.Tasks; namespace NetFrame { public class SerializeUtil { /// <summary> /// 对象序列化 /// </summary> /// <param name="value"></param> /// <returns></returns> public static byte[] encode(object value) { MemoryStream ms = new MemoryStream();//创建编码解码的内存流对象 BinaryFormatter bw = new BinaryFormatter();//二进制流序列化对象 //将obj对象序列化成二进制数据 写入到 内存流 bw.Serialize(ms, value); byte[] result=new byte[ms.Length]; //将流数据 拷贝到结果数组 Buffer.BlockCopy(ms.GetBuffer(), 0, result, 0, (int)ms.Length); ms.Close(); return result; } /// <summary> /// 反序列化为对象 /// </summary> /// <param name="value"></param> /// <returns></returns> public static object decode(byte[] value) { MemoryStream ms = new MemoryStream(value);//创建编码解码的内存流对象 并将需要反序列化的数据写入其中 BinaryFormatter bw = new BinaryFormatter();//二进制流序列化对象 //将流数据反序列化为obj对象 object result= bw.Deserialize(ms); ms.Close(); return result; } } }
-
3.消息体序列化与反序列化
相应的,我们利用上面写好的序列化和反序列化方法将我们再Socket模型中定义的message消息体进行序列化与反序列化
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NetFrame.auto { public class MessageEncoding { /// <summary> /// 消息体序列化 /// </summary> /// <param name="value"></param> /// <returns></returns> public static byte[] encode(object value) { SocketModel model = value as SocketModel; ByteArray ba = new ByteArray(); ba.write(model.type); ba.write(model.area); ba.write(model.command); //判断消息体是否为空 不为空则序列化后写入 if (model.message != null) { ba.write(SerializeUtil.encode(model.message)); } byte[] result = ba.getBuff(); ba.Close(); return result; } /// <summary> /// 消息体反序列化 /// </summary> /// <param name="value"></param> /// <returns></returns> public static object decode(byte[] value) { ByteArray ba = new ByteArray(value); SocketModel model = new SocketModel(); byte type; int area; int command; //从数据中读取 三层协议 读取数据顺序必须和写入顺序保持一致 ba.read(out type); ba.read(out area); ba.read(out command); model.type = type; model.area = area; model.command = command; //判断读取完协议后 是否还有数据需要读取 是则说明有消息体 进行消息体读取 if (ba.Readnable) { byte[] message; //将剩余数据全部读取出来 ba.read(out message, ba.Length - ba.Position); //反序列化剩余数据为消息体 model.message = SerializeUtil.decode(message); } ba.Close(); return model; } } }
-
4.将数据写入成二进制
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace NetFrame { /// <summary> /// 将数据写入成二进制 /// </summary> public class ByteArray { MemoryStream ms = new MemoryStream(); BinaryWriter bw; BinaryReader br; public void Close() { bw.Close(); br.Close(); ms.Close(); } /// <summary> /// 支持传入初始数据的构造 /// </summary> /// <param name="buff"></param> public ByteArray(byte[] buff) { ms = new MemoryStream(buff); bw = new BinaryWriter(ms); br = new BinaryReader(ms); } /// <summary> /// 获取当前数据 读取到的下标位置 /// </summary> public int Position { get { return (int)ms.Position; } } /// <summary> /// 获取当前数据长度 /// </summary> public int Length { get { return (int)ms.Length; } } /// <summary> /// 当前是否还有数据可以读取 /// </summary> public bool Readnable{ get { return ms.Length > ms.Position; } } /// <summary> /// 默认构造 /// </summary> public ByteArray() { bw = new BinaryWriter(ms); br = new BinaryReader(ms); } public void write(int value) { bw.Write(value); } public void write(byte value) { bw.Write(value); } public void write(bool value) { bw.Write(value); } public void write(string value) { bw.Write(value); } public void write(byte[] value) { bw.Write(value); } public void write(double value) { bw.Write(value); } public void write(float value) { bw.Write(value); } public void write(long value) { bw.Write(value); } public void read(out int value) { value= br.ReadInt32(); } public void read(out byte value) { value = br.ReadByte(); } public void read(out bool value) { value = br.ReadBoolean(); } public void read(out string value) { value = br.ReadString(); } public void read(out byte[] value,int length) { value = br.ReadBytes(length); } public void read(out double value) { value = br.ReadDouble(); } public void read(out float value) { value = br.ReadSingle(); } public void read(out long value) { value = br.ReadInt64(); } public void reposition() { ms.Position = 0; } /// <summary> /// 获取数据 /// </summary> /// <returns></returns> public byte[] getBuff() { byte[] result = new byte[ms.Length]; Buffer.BlockCopy(ms.GetBuffer(), 0, result, 0, (int)ms.Length); return result; } } }
-
5.粘包长度编码与解码
粘包出现原因:在流传输中出现(UDP不会出现粘包,因为它有消息边界)
1 发送端需要等缓冲区满才发送出去,造成粘包
2 接收方不及时接收缓冲区的包,造成多个包接收
所以这里我们需要对粘包长度进行编码与解码,具体的代码如下:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NetFrame.auto { public class LengthEncoding { /// <summary> /// 粘包长度编码 /// </summary> /// <param name="buff"></param> /// <returns></returns> public static byte[] encode(byte[] buff) { MemoryStream ms = new MemoryStream();//创建内存流对象 BinaryWriter sw = new BinaryWriter(ms);//写入二进制对象流 //写入消息长度 sw.Write(buff.Length); //写入消息体 sw.Write(buff); byte[] result = new byte[ms.Length]; Buffer.BlockCopy(ms.GetBuffer(), 0, result, 0, (int)ms.Length); sw.Close(); ms.Close(); return result; } /// <summary> /// 粘包长度解码 /// </summary> /// <param name="cache"></param> /// <returns></returns> public static byte[] decode(ref List<byte> cache) { if (cache.Count < 4) return null; MemoryStream ms = new MemoryStream(cache.ToArray());//创建内存流对象,并将缓存数据写入进去 BinaryReader br = new BinaryReader(ms);//二进制读取流 int length = br.ReadInt32();//从缓存中读取int型消息体长度 //如果消息体长度 大于缓存中数据长度 说明消息没有读取完 等待下次消息到达后再次处理 if (length > ms.Length - ms.Position) { return null; } //读取正确长度的数据 byte[] result = br.ReadBytes(length); //清空缓存 cache.Clear(); //将读取后的剩余数据写入缓存 cache.AddRange(br.ReadBytes((int)(ms.Length - ms.Position))); br.Close(); ms.Close(); return result; } } }
-
6.delegate委托声明
delegate 是表示对具有特定参数列表和返回类型的方法的引用的类型。 在实例化委托时,可以将其实例与任何具有兼容签名和返回类型的方法相关联。通过委托实例调用方法。委托相当于将方法作为参数传递给其他方法,类似于 C++ 函数指针,但它们是类型安全的。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NetFrame { public delegate byte[] LengthEncode(byte[] value); public delegate byte[] LengthDecode(ref List<byte> value); public delegate byte[] encode(object value); public delegate object decode(byte[] value); }
-
7.用户连接对象UserToken
-
SocketAsyncEventArgs介绍
SocketAsyncEventArgs是微软提供的高性能异步Socket实现类,主要为高性能网络服务器应用程序而设计,主要是为了避免在在异步套接字 I/O 量非常大时发生重复的对象分配和同步。使用此类执行异步套接字操作的模式包含以下步骤:
(1)分配一个新的
SocketAsyncEventArgs 上下文对象,或者从应用程序池中获取一个空闲的此类对象。
(2)将该上下文对象的属性设置为要执行的操作(例如,完成回调方法、数据缓冲区、缓冲区偏移量以及要传输的最大数据量)。
(3)调用适当的套接字方法
(xxxAsync) 以启动异步操作。
(4)如果异步套接字方法 (xxxAsync)
返回 true,则在回调中查询上下文属性来获取完成状态。
(5)如果异步套接字方法 (xxxAsync)
返回 false,则说明操作是同步完成的。可以查询上下文属性来获取操作结果。
(6)将该上下文重用于另一个操作,将它放回到应用程序池中,或者将它丢弃。
- SocketAsyncEventArgs.UserToken 属性
获取或设置与此异步套接字操作关联的用户或应用程序对象。
命名空间: System.Net.Sockets
public object UserToken { get; set; }
备注:
此属性可以由应用程序相关联的应用程序状态对象与 SocketAsyncEventArgs 对象。 首先,此属性是一种将状态传递到应用程序的事件处理程序(例如,异步操作完成方法)的应用程序的方法。
此属性用于所有异步套接字 (xxxAsync) 方法。
UserToken类的完整实现代码如下,可以结合代码注释加以理解:
using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace NetFrame { /// <summary> /// 用户连接信息对象 /// </summary> public class UserToken { /// <summary> /// 用户连接 /// </summary> public Socket conn; //用户异步接收网络数据对象 public SocketAsyncEventArgs receiveSAEA; //用户异步发送网络数据对象 public SocketAsyncEventArgs sendSAEA; public LengthEncode LE; public LengthDecode LD; public encode encode; public decode decode; public delegate void SendProcess(SocketAsyncEventArgs e); public SendProcess sendProcess; public delegate void CloseProcess(UserToken token, string error); public CloseProcess closeProcess; public AbsHandlerCenter center; List<byte> cache = new List<byte>(); private bool isReading = false; private bool isWriting = false; Queue<byte[]> writeQueue = new Queue<byte[]>(); public UserToken() { receiveSAEA = new SocketAsyncEventArgs(); sendSAEA = new SocketAsyncEventArgs(); receiveSAEA.UserToken = this; sendSAEA.UserToken = this; //设置接收对象的缓冲区大小 receiveSAEA.SetBuffer(new byte[1024], 0, 1024); } //网络消息到达 public void receive(byte[] buff) { //将消息写入缓存 cache.AddRange(buff); if (!isReading) { isReading = true; onData(); } } //缓存中有数据处理 void onData() { //解码消息存储对象 byte[] buff = null; //当粘包解码器存在的时候 进行粘包处理 if (LD != null) { buff = LD(ref cache); //消息未接收全 退出数据处理 等待下次消息到达 if (buff == null) { isReading = false; return; } } else { //缓存区中没有数据 直接跳出数据处理 等待下次消息到达 if (cache.Count == 0) { isReading = false; return; } buff = cache.ToArray(); cache.Clear(); } //反序列化方法是否存在 if (decode == null) { throw new Exception("message decode process is null"); } //进行消息反序列化 object message = decode(buff); //TODO 通知应用层 有消息到达 center.MessageReceive(this, message); //尾递归 防止在消息处理过程中 有其他消息到达而没有经过处理 onData(); } public void write(byte[] value) { if (conn == null) { //此连接已经断开了 closeProcess(this, "调用已经断开的连接"); return; } writeQueue.Enqueue(value); if (!isWriting) { isWriting = true; onWrite(); } } public void onWrite() { //判断发送消息队列是否有消息 if (writeQueue.Count == 0) { isWriting = false; return; } //取出第一条待发消息 byte[] buff = writeQueue.Dequeue(); //设置消息发送异步对象的发送数据缓冲区数据 sendSAEA.SetBuffer(buff, 0, buff.Length); //开启异步发送 bool result = conn.SendAsync(sendSAEA); //是否挂起 if (!result) { sendProcess(sendSAEA); } } public void writed() { //与onData尾递归同理 onWrite(); } public void Close() { try { writeQueue.Clear(); cache.Clear(); isReading = false; isWriting = false; conn.Shutdown(SocketShutdown.Both); conn.Close(); conn = null; } catch (Exception e) { Console.WriteLine(e.Message); } } } }
-
8.连接池UserTokenPool
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NetFrame { public class UserTokenPool { private Stack<UserToken> pool; public UserTokenPool(int max) { pool = new Stack<UserToken>(max); } /// <summary> /// 取出一个连接对象 --创建连接 /// </summary> public UserToken pop() { return pool.Pop(); } //插入一个连接对象---释放连接 public void push(UserToken token) { if (token != null) pool.Push(token); } public int Size { get { return pool.Count; } } } }
-
9.抽象处理中心AbsHandlerCenter
在这里我们定义了客户端连接、收到客户端消息和客户端断开连接的抽象类,标记为抽象或包含在抽象类中的成员必须通过从抽象类派生的类来实现。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace NetFrame { public abstract class AbsHandlerCenter { /// <summary> /// 客户端连接 /// </summary> /// <param name="token">连接的客户端对象</param> public abstract void ClientConnect(UserToken token); /// <summary> /// 收到客户端消息 /// </summary> /// <param name="token">发送消息的客户端对象</param> /// <param name="message">消息内容</param> public abstract void MessageReceive(UserToken token, object message); /// <summary> /// 客户端断开连接 /// </summary> /// <param name="token">断开的客户端对象</param> /// <param name="error">断开的错误信息</param> public abstract void ClientClose(UserToken token, string error); } }
-
10.HandlerCenter实现类
接下来具体实现客户端连接、断开连接以及收到消息后的协议分发到具体的逻辑处理模块,代码如下:
using GameProtocol; using LOLServer.logic; using LOLServer.logic.fight; using LOLServer.logic.login; using LOLServer.logic.match; using LOLServer.logic.select; using LOLServer.logic.user; using NetFrame; using NetFrame.auto; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LOLServer { public class HandlerCenter:AbsHandlerCenter { HandlerInterface login; HandlerInterface user; HandlerInterface match; HandlerInterface select; HandlerInterface fight; public HandlerCenter() { login = new LoginHandler(); user = new UserHandler(); match = new MatchHandler(); select = new SelectHandler(); fight = new FightHandler(); } public override void ClientClose(UserToken token, string error) { Console.WriteLine("有客户端断开连接了"); select.ClientClose(token, error); match.ClientClose(token, error); fight.ClientClose(token, error); //user的连接关闭方法 一定要放在逻辑处理单元后面 //其他逻辑单元需要通过user绑定数据来进行内存清理 //如果先清除了绑定关系 其他模块无法获取角色数据会导致无法清理 user.ClientClose(token, error); login.ClientClose(token, error); } public override void ClientConnect(UserToken token) { Console.WriteLine("有客户端连接了"); } public override void MessageReceive(UserToken token, object message) { SocketModel model = message as SocketModel; switch (model.type) { case Protocol.TYPE_LOGIN: login.MessageReceive(token, model); break; case Protocol.TYPE_USER: user.MessageReceive(token, model); break; case Protocol.TYPE_MATCH: match.MessageReceive(token, model); break; case Protocol.TYPE_SELECT: select.MessageReceive(token, model); break; case Protocol.TYPE_FIGHT: fight.MessageReceive(token, model); break; default: //未知模块 可能是客户端作弊了 无视 break; } } } }
-
11.启动服务器
写到这里,服务器终于可以启来了,不管你激不激动,反正坐在这里写写画画了一天我是激动了,总算要大功告成了。 : )
启动服务器->监听IP(可选)->监听端口,服务器处理流程如下图:
让我们来具体看看代码实现,均给了详细的注释:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace NetFrame { public class ServerStart { Socket server;//服务器socket监听对象 int maxClient;//最大客户端连接数 Semaphore acceptClients; UserTokenPool pool; public LengthEncode LE; public LengthDecode LD; public encode encode; public decode decode; /// <summary> /// 消息处理中心,由外部应用传入 /// </summary> public AbsHandlerCenter center; /// <summary> /// 初始化通信监听 /// </summary> /// <param name="port">监听端口</param> public ServerStart(int max) { //实例化监听对象 server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //设定服务器最大连接人数 maxClient = max; } public void Start(int port) { //创建连接池 pool = new UserTokenPool(maxClient); //连接信号量 acceptClients = new Semaphore(maxClient, maxClient); for (int i = 0; i < maxClient; i++) { UserToken token = new UserToken(); //初始化token信息 token.receiveSAEA.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Comleted); token.sendSAEA.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Comleted); token.LD = LD; token.LE = LE; token.encode = encode; token.decode = decode; token.sendProcess = ProcessSend; token.closeProcess = ClientClose; token.center = center; pool.push(token); } //监听当前服务器网卡所有可用IP地址的port端口 // 外网IP 内网IP192.168.x.x 本机IP一个127.0.0.1 try { server.Bind(new IPEndPoint(IPAddress.Any, port)); //置于监听状态 server.Listen(10); StartAccept(null); } catch (Exception e) { Console.WriteLine(e.Message); } } /// <summary> /// 开始客户端连接监听 /// </summary> public void StartAccept(SocketAsyncEventArgs e) { //如果当前传入为空 说明调用新的客户端连接监听事件 否则的话 移除当前客户端连接 if (e == null) { e = new SocketAsyncEventArgs(); e.Completed += new EventHandler<SocketAsyncEventArgs>(Accept_Comleted); } else { e.AcceptSocket = null; } //信号量-1 acceptClients.WaitOne(); bool result= server.AcceptAsync(e); //判断异步事件是否挂起 没挂起说明立刻执行完成 直接处理事件 否则会在处理完成后触发Accept_Comleted事件 if (!result) { ProcessAccept(e); } } public void ProcessAccept(SocketAsyncEventArgs e) { //从连接对象池取出连接对象 供新用户使用 UserToken token = pool.pop(); token.conn = e.AcceptSocket; //TODO 通知应用层 有客户端连接 center.ClientConnect(token); //开启消息到达监听 StartReceive(token); //释放当前异步对象 StartAccept(e); } public void Accept_Comleted(object sender, SocketAsyncEventArgs e) { ProcessAccept(e); } public void StartReceive(UserToken token) { try { //用户连接对象 开启异步数据接收 bool result = token.conn.ReceiveAsync(token.receiveSAEA); //异步事件是否挂起 if (!result) { ProcessReceive(token.receiveSAEA); } } catch (Exception e) { Console.WriteLine(e.Message); } } public void IO_Comleted(object sender, SocketAsyncEventArgs e) { if (e.LastOperation == SocketAsyncOperation.Receive) { ProcessReceive(e); } else { ProcessSend(e); } } public void ProcessReceive(SocketAsyncEventArgs e) { UserToken token= e.UserToken as UserToken; //判断网络消息接收是否成功 if (token.receiveSAEA.BytesTransferred > 0 && token.receiveSAEA.SocketError == SocketError.Success) { byte[] message = new byte[token.receiveSAEA.BytesTransferred]; //将网络消息拷贝到自定义数组 Buffer.BlockCopy(token.receiveSAEA.Buffer, 0, message, 0, token.receiveSAEA.BytesTransferred); //处理接收到的消息 token.receive(message); StartReceive(token); } else { if (token.receiveSAEA.SocketError != SocketError.Success) { ClientClose(token, token.receiveSAEA.SocketError.ToString()); } else { ClientClose(token, "客户端主动断开连接"); } } } public void ProcessSend(SocketAsyncEventArgs e) { UserToken token = e.UserToken as UserToken; if (e.SocketError != SocketError.Success) { ClientClose(token, e.SocketError.ToString()); } else { //消息发送成功,回调成功 token.writed(); } } /// <summary> /// 客户端断开连接 /// </summary> /// <param name="token"> 断开连接的用户对象</param> /// <param name="error">断开连接的错误编码</param> public void ClientClose(UserToken token,string error) { if (token.conn != null) { lock (token) { //通知应用层面 客户端断开连接了 center.ClientClose(token, error); token.Close(); //加回一个信号量,供其它用户使用 pool.push(token); acceptClients.Release(); } } } } }
至此,服务器的通信底层已经搭建完毕,可以进一步进行具体的游戏逻辑玩法开发了。
-
五、游戏服务端逻辑分层实现
逻辑处理主要分层架构如下:
(1)logic逻辑层:逻辑处理模块,异步的逻辑处理,登录、用户处理、匹配、选人、战斗的主要逻辑都在这里,Moba类游戏是典型的房间服务器架构,AbsOnceHandler用于单体消息发送的处理,AbsMulitHandler用于群发;
AbsOnceHandler代码如下:
using LOLServer.biz; using LOLServer.dao.model; using NetFrame; using NetFrame.auto; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LOLServer.logic { public class AbsOnceHandler { public IUserBiz userBiz = BizFactory.userBiz; private byte type; private int area; public void SetArea(int area) { this.area = area; } public virtual int GetArea() { return area; } public void SetType(byte type) { this.type = type; } public new virtual byte GetType() { return type; } /// <summary> /// 通过连接对象获取用户 /// </summary> /// <param name="token"></param> /// <returns></returns> public USER getUser(UserToken token) { return userBiz.get(token); } /// <summary> /// 通过ID获取用户 /// </summary> /// <param name="token"></param> /// <returns></returns> public USER getUser(int id) { return userBiz.get(id); } /// <summary> /// 通过连接对象 获取用户ID /// </summary> /// <param name="token"></param> /// <returns></returns> public int getUserId(UserToken token){ USER user = getUser(token); if(user==null)return -1; return user.id; } /// <summary> /// 通过用户ID获取连接 /// </summary> /// <param name="id"></param> /// <returns></returns> public UserToken getToken(int id) { return userBiz.getToken(id); } #region 通过连接对象发送 public void write(UserToken token,int command) { write(token, command, null); } public void write(UserToken token, int command,object message) { write(token,GetArea(), command, message); } public void write(UserToken token,int area, int command, object message) { write(token,GetType(), GetArea(), command, message); } public void write(UserToken token,byte type, int area, int command, object message) { byte[] value = MessageEncoding.encode(CreateSocketModel(type,area,command,message)); value = LengthEncoding.encode(value); token.write(value); } #endregion #region 通过ID发送 public void write(int id, int command) { write(id, command, null); } public void write(int id, int command, object message) { write(id, GetArea(), command, message); } public void write(int id, int area, int command, object message) { write(id, GetType(), area, command, message); } public void write(int id, byte type, int area, int command, object message) { UserToken token= getToken(id); if(token==null)return; write(token, type, area, command, message); } public void writeToUsers(int[] users, byte type, int area, int command, object message) { byte[] value = MessageEncoding.encode(CreateSocketModel(type, area, command, message)); value = LengthEncoding.encode(value); foreach (int item in users) { UserToken token = userBiz.getToken(item); if (token == null) continue; byte[] bs = new byte[value.Length]; Array.Copy(value, 0, bs, 0, value.Length); token.write(bs); } } #endregion public SocketModel CreateSocketModel(byte type, int area, int command, object message) { return new SocketModel(type, area, command, message); } } }
AbsMulitHandler继承自AbsOnceHandler,实现代码如下:
using NetFrame; using NetFrame.auto; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace LOLServer.logic { public class AbsMulitHandler:AbsOnceHandler { public List<UserToken> list = new List<UserToken>(); /// <summary> /// 用户进入当前子模块 /// </summary> /// <param name="token"></param> /// <returns></returns> public bool enter(UserToken token) { if (list.Contains(token)) { return false; } list.Add(token); return true; } /// <summary> /// 用户是否在此子模块 /// </summary> /// <param name="token"></param> /// <returns></returns> public bool isEntered(UserToken token) { return list.Contains(token); } /// <summary> /// 用户离开当前子模块 /// </summary> /// <param name="token"></param> /// <returns></returns> public bool leave(UserToken token) { if (list.Contains(token)) { list.Remove(token); return true; } return false; } #region 消息群发API public void brocast(int command, object message,UserToken exToken=null) { brocast(GetArea(), command, message, exToken); } public void brocast(int area, int command, object message, UserToken exToken = null) { brocast(GetType(), area, command, message, exToken); } public void brocast(byte type, int area, int command, object message, UserToken exToken = null) { byte[] value = MessageEncoding.encode(CreateSocketModel(type, area, command, message)); value = LengthEncoding.encode(value); foreach (UserToken item in list) { if (item != exToken) { byte[] bs = new byte[value.Length]; Array.Copy(value, 0, bs, 0, value.Length); item.write(bs); } } } #endregion } }
(2)biz事务层:事务处理,保证数据安全的逻辑处理,如账号、用户信息相关的处理,impl是相关的实现类;
(3)cache缓存层:读取数据库中的内容放在内存中,加快访问速度;
(4)dao数据层:服务器和数据库之间的中间件;
(5)工具类:一些实用的工具类放在这里,如定时任务列表,用来实现游戏中的刷怪,buff等;
逻辑处理流程如下:
-
六、写在最后
好了,这篇文章就分享到这里,从项目的制作周期,到沉淀积累,到重构设计,到总结与反思,到怎么把整个架构的设计与实现分享出来,再到写出一篇文章确实经历了很长的一段时间,有些图由于比较长经常需要把电脑屏幕来回旋转绘制,希望对您有所帮助,感谢阅读,篇幅有限不能一一详述,如有问题欢迎留言讨论。下一篇可能会开始剖析开源MMORPG游戏服务端引擎KBEngine的源码,也可能写C++或python相关,如果您也对这些内容感兴趣,或者对笔者感兴趣,可以继续关注我的后续文章。^_^
上篇博客后有不少小伙伴给笔者发了私信,大多是技术生涯的一些迷茫与选择,最后有一句笔者很喜欢的话分享给大家:衡量一个人才的标准,在于一个人在有限的时间内所展现出来的成长速度。持续学习,持续进步,持续成长,才能持续幸运,持续实现价值。
以上是关于MOBA游戏的术语都有哪些?的主要内容,如果未能解决你的问题,请参考以下文章