moba类游戏 unity3d

2018-11-07 22:11:28 qq563129582 阅读数 5684

一、前言
  《码神联盟》是一款为技术人做的开源情怀游戏,每一种编程语言都是一位英雄。客户端和服务端均使用C#开发,客户端使用Unity3D引擎,数据库使用MySQL。这个MOBA类游戏是笔者在学习时期和客户端美术策划的小伙伴一起做的游戏,笔者主要负责游戏服务端开发,客户端也参与了一部分,同时也是这个项目的发起和负责人。这次主要分享这款游戏的服务端相关的设计与实现,从整体的架构设计,到服务器网络通信底层的搭建,通信协议、模型定制,再到游戏逻辑的分层架构实现。同时这篇博客也沉淀了笔者在游戏公司实践五个月后对游戏架构与设计的重新审视与思考。

这款游戏自去年完成后笔者曾多次想写篇博客来分享,也曾多次停笔,只因总觉得灵感还不够积淀还不够思考还不够,现在终于可以跨过这一步和大家分享,希望可以带来的是干货与诚意满满。由于目前关于游戏服务端相关的介绍文章少之又少,而为数不多的几篇也都是站在游戏服务端发展历史和架构的角度上进行分享,很少涉及具体的实现,这篇文章我将尝试多从实现的层面上加以介绍,所附的代码均有详尽注释,篇幅较长,可以关注收藏后再看。学习时期做的项目可能无法达到工业级,参考了github上开源的C#网络框架,笔者在和小伙伴做这款游戏时农药还没有现在这般火。 : )

二、服务器架构
在这里插入图片描述

上图为这款游戏的服务器架构和主要逻辑流程图,笔者将游戏的代码实现分为三个主要模块: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等;

逻辑处理流程如下:
在这里插入图片描述
六、优化思路
  思考了一些优化思路,自文章发布后也收到了许多来自朋友圈或留言评论中大神们给出的优化思路,大多数建议都质量很高,极具参考价值和学习意义,大概这就是开源的魅力所在吧。现在把这些思路整理出来分享给大家:

(1)在原有架构基础上,可以进一步考虑下:协议的自动化生成,托管内存的gc消耗控制,更小的网络延迟和更大的网络并发;

(2)如果用上异步消息机制和Nosql 单服承载人数或许还能够上升一些,目前Nosql中MongoDB在游戏服务端中有较多应用,Redis是笔者个人很喜欢的一个开源Nosql数据库,也有一些游戏项目已经在尝试集成;

(3).net 自带的二进制序列化性能偏差,文章中代码里数据接收发送时的内存拷贝次数偏多,序列化可以尝试Google开源的protobuf,目前很多线上游戏都在应用;

(4)用.net framework其实就把服务器绑定到windows上了,同时mono性能堪忧,如果非要用c#的话,可以尝试.net core + docker ,网络库可以libuv ,这个方案不管是从扩展还是性能监控管理上都比windows要优秀许多,业界的游戏服务器也确实大多在Linux上部署;

(5)收发消息部分太复杂,使用现成的RPC框架性能、安全性会更好。

2017-07-13 16:01:32 tingting14054765 阅读数 11976

由于最近工作忙,之前一直想写的王者荣耀教程直接就忘记了,最新才记起来,现在继续更新~。

上一篇起始大概介绍了一下我对这个工程的简单思路现在开始一步步实现,首先先创建一个Unity3d工程,这里我先用5.4.0吧,因为项目里要用到一些新的插件,老版本可能对新版本插件不是很支持。

新建完工程后,首先我们先把思路理一下,要完成一个完整的王者荣耀类MOBA游戏,以下几点需要我们去做

(1)场景(Scene),场景是游戏的基本模块,首先我们先要新建几个场景去进行游戏的建模,目前用到的只有4个场景,登录,大厅,加载,战斗。像商城,匹配,符文等子场景都是包含在大厅场景。新建好场景后,我们把我们要的模型资源导入到工程中,然后把所有的资源制作成Prefab,在游戏中通过程序在合适的时候载入到场景当中,就可以看到效果了。

(2)资源管理(ResourceManager),由于一个MOBA游戏包含大量的人物模型,动作,特效,地图等,资源特别多,如果不对这些资源进行有效的管理,在游戏运行过程中可能因为某个时候资源内存达到峰值而造成游戏闪退,要知道闪退对于一个MOBA游戏是致命的,所以资源管理对MOBA游戏来说十分重要。

(3)客户端逻辑脚本编写,在第一篇我简单提了一下客户端逻辑的基本构思,例如UI框架,UI控件,消息通知,网络交互等逻辑,之后的文章我会每一个都详细介绍,通过这些部分组合可以使客户端有效运行

(4)服务器逻辑,这次的服务器逻辑就用一个现成的moba游戏服务器框架,就不自己写了,之后也会为大家简单介绍一下

(5)数据库,数据库用MySQL存储

(6)数据传输协议,上述服务器框架用的是protobuf

大概知道,我们这个项目要做些什么内容之后,开始干!


2016-11-17 16:50:54 tingting14054765 阅读数 13087

最近在公司搬砖事情不多,因为最近在玩鹅厂游戏《王者荣耀》,是一款目前市场上最火爆的moba类手游,所以抽空想做一个类似的demo~本文纯灌水记录一下开发计划,客户端打算采用Unity3d,服务器打算先用C++的一套现成的改改(其实用photon会更方便),数据库使用mysql。

客户端UI准备使用Unity3d的UI插件 NGUI,因为之前实习对这个插件比较实习,UI框架自己写一套,所有的UI都做成prefab,不依赖场景,prefab都通过配置文件去配置,然后在游戏里进行动态加载。用一个资源管理类ResourceManager进行场景切换时UI prefab的销毁和加载。UI控件写一个基类BaseWindow实现一些基本的方法并且写几个抽象函数让子类去继承,所有的UI控件都要继承BaseWindow,然后要有一个WindowManager类去管理所有的子窗口,例如隐藏,显示,销毁某个子窗口等等。

UI的触发事件用C#的delegate机制去实现,先把所有的UI触发事件定义一个枚举,然后在各UI子控件cs文件里进行绑定一个方法,然后在需要的时候直接触发事件从而实现跨场景跨类去调用各种UI事件。

其他的暂时还在考虑~~~明天开始写第一篇开发记录博客,大家有什么建议欢迎留言~

2017-11-19 14:04:20 u013108312 阅读数 11098

原文链接http://blog.csdn.net/u013108312/article/details/78574139

支付宝捐赠

打赏红包

1.ResourceManager 这个是我们进入的第一个游戏场景,我们可以看到游戏一种挂载了2个脚本,一个是游戏资源管理类,一个是游戏资源的更新。开始主要是验证服务器资源版本和本地的资源是否一致,一致的话就不在更新,进入游戏登录界面。资源更新大家可以把资源放在CDN加速器上。
2.在GameLogin场景会加载LoginWindow 界面管理类,LoinCtr.cs。那我们就从用户点击登录服务器按钮开始,

//登陆
        public void Login(string account, string pass)
        {
            Debugger.Log("### LoginCtrl - >  Login");

            SelectServerData.Instance.SetServerInfo((int)SdkManager.Instance.GetPlatFrom(), account, pass);
            NetworkManager.Instance.canReconnect = false;
            NetworkManager.Instance.Close();
            NetworkManager.Instance.Init(JxBlGame.Instance.LoginServerAdress, 49996, NetworkManager.ServerType.LoginServer);

            Debugger.Log("### LoginCtrl - > Login " + SdkManager.Instance.GetPlatFrom() + account + pass);
            Debugger.Log("### LoginCtrl - > Login " + JxBlGame.Instance.LoginServerAdress + NetworkManager.ServerType.LoginServer);
        }

这个地方是点击后触发的事件,让我们先来分析一下,这一步都做了什么事情,首先是初始化登录账号和密码。初始化NetworkManager - > NetworkManager.ServerType.LoginServer。然后进入

public void Init(string ip, Int32 port, ServerType type)
        {
            Debugger.Log("set network ip:" + ip + " port:" + port + " type:" + type);

            m_IP = ip;
            m_Port = port;
            serverType = type;
            m_n32ConnectTimes = 0;
            canReconnect = true;
            m_RecvPos = 0;

#if UNITY_EDITOR
            mRecvOverDelayTime = 20000f;
#endif
        }

当canReconnect = true的时候。执行

public void Connect()
        {
            
            if (!canReconnect) return;
            //Debug.LogError("------");

            if (m_CanConnectTime > Time.time) return;

            if (m_Client != null)
                throw new Exception("fuck, The socket is connecting, cannot connect again!");

            if (m_Connecting != null)
                throw new Exception("fuck, The socket is connecting, cannot connect again!");

            Debugger.Log("IClientSession Connect");

            IPAddress ipAddress = IPAddress.Parse(m_IP);

            try
            {
                m_Connecting = new TcpClient();

                mConnectResult = m_Connecting.BeginConnect(m_IP, m_Port, null, null);

                m_ConnectOverCount = 0;

                m_ConnectOverTime = Time.time + 2;
            }
            catch (Exception exc)
            {
                Debugger.LogError(exc.ToString());

                m_Client = m_Connecting;

                m_Connecting = null;

                mConnectResult = null;

                OnConnectError(m_Client, null);
            }
        }
public void OnConnected(object sender, EventArgs e)
        {

            switch (serverType)
            {
                case ServerType.BalanceServer:
                    {
                        CGLCtrl_GameLogic.Instance.BsOneClinetLogin();
                    }
                    break;
                case ServerType.GateServer:
                    {
                        ++m_n32ConnectTimes;
                        if (m_n32ConnectTimes > 1)
                        {
                            CGLCtrl_GameLogic.Instance.EmsgTocsAskReconnect();
                        }
                        else
                        {
                            CGLCtrl_GameLogic.Instance.GameLogin();
                        }
                        EventCenter.Broadcast(EGameEvent.eGameEvent_ConnectServerSuccess);
                    }
                    break;
                case ServerType.LoginServer:
                    {
                        CGLCtrl_GameLogic.Instance.EmsgToLs_AskLogin();
                    }
                    break;
            }
        }

向服务器发送消息。

public void GameLogin()
    {
        GCToCS.Login pMsg = new GCToCS.Login
        {
            sdk = (Int32)SdkManager.Instance.GetPlatFrom(),                                  //ToReview 平台写死为10?
           
            #if UNITY_STANDALONE_WIN || UNITY_EDITOR || SKIP_SDK
             platform = 0,
            #elif UNITY_IPHONE
             platform = 1,
            #elif UNITY_ANDROID
             platform = 2,
            #endif

            equimentid = MacAddressIosAgent.GetMacAddressByDNet(),
            name =  SelectServerData.Instance.gateServerUin,
            passwd = SelectServerData.Instance.GateServerToken,
        };
        NetworkManager.Instance.SendMsg(pMsg, (int)pMsg.msgnum);


     #if UNITY_STANDALONE_WIN || UNITY_EDITOR || SKIP_SDK
     #else
          //talkgame 初始化
        string ip = JxBlGame.Instance.LoginServerAdress;
        string curtSdk = SdkManager.Instance.GetPlatFrom().ToString();
        string cdkey = SelectServerData.Instance.gateServerUin; 
        TalkGame.Instance.InitTalkGame(curtSdk, cdkey,  ip); 
#endif

    }

发送消息后接收服务器消息。

CGLCtrl_GameLogic.Instance.HandleNetMsg(iostream, type);

判断消息类型。

public partial class CGLCtrl_GameLogic : UnitySingleton<CGLCtrl_GameLogic>
{
    const int PROTO_DESERIALIZE_ERROR = -1;

    public void HandleNetMsg(System.IO.Stream stream, int n32ProtocalID)
    {
   
        //Debugger.Log("### n32ProtocalID   " + (GSToGC.MsgID)n32ProtocalID);
        switch (n32ProtocalID)
        {
            case (Int32)GSToGC.MsgID.eMsgToGCFromGS_GCAskPingRet:
                OnNetMsg_NotifyPing(stream);
                break;
                ·······
                }
                }

501.6216 发送消息: 40961 eMsgToLSFromGC_AskLogin
501.7052 收到消息:513->eMsgToGCFromLS_NotifyServerBSAddr

//接收GateServer信息
        public void RecvGateServerInfo(Stream stream)
        {
            Debugger.Log("### LoginCtrl - >  RecvGateServerInfo");

            BSToGC.AskGateAddressRet pMsg = ProtoBuf.Serializer.Deserialize<BSToGC.AskGateAddressRet>(stream);
            SelectServerData.Instance.GateServerAdress = pMsg.ip;
            SelectServerData.Instance.GateServerPort = pMsg.port;
            SelectServerData.Instance.GateServerToken = pMsg.token;
            SelectServerData.Instance.SetGateServerUin(pMsg.user_name);
            NetworkManager.Instance.canReconnect = false;
            NetworkManager.Instance.Close();
            NetworkManager.Instance.Init(pMsg.ip, pMsg.port, NetworkManager.ServerType.GateServer); 

            EventCenter.Broadcast(EGameEvent.eGameEvent_LoginSuccess);
        }

截止到目前为止,是客户端和服务器的第一次通信,让我们来分析一下,这里使用了一个protobuf,没有听说过的可以先谷歌搜一下这个工具的使用。在本游戏中主要是完成C#和c++之间的通信。
3.让我们来看看服务器都干了什么事,在LoginServer.cpp中进入到主函数的死循环,判断是否有消息发送过来。

int _tmain(int argc, _TCHAR* argv[])
{
	SetConsoleTitle(_T("ls id=1"));

	if (!Init()) 
	{
		system("pause");
		return -1;
	}

	while(true)
	{
		if (kbhit())
		{
			static char CmdArray[1024] = {0};
			static int CmdPos = 0;
			char CmdOne = getche();
			CmdArray[CmdPos++] = CmdOne;
			bool bRet = 0;
			if (CmdPos>=1024 || CmdOne==13) { CmdArray[--CmdPos]=0; bRet = DoUserCmd(CmdArray); CmdPos=0; if (bRet) break; }
		}
		SdkConnector::GetInstance().Update();
		Run();
		::Sleep(1);
	}
	UnInit();
	google::protobuf::ShutdownProtobufLibrary();
	return 0;
}
#include "../stdafx.h"
#include "ClientSession.h"
#include "..\..\..\Share\Net\INetSessionMgr.h"
#include "../SDKAsynHandler.h"
#include "../SdkConnector.h"

CMsgHandle_Client CClientSession::mHandles;

CMsgHandle_Client::CMsgHandle_Client()
	:IMsgHandle(GCToLS::eMsgToLSFromGC_Begin, GCToLS::eMsgToLSFromGC_End-GCToLS::eMsgToLSFromGC_Begin)
{
	RegisterMsgFunc(GCToLS::eMsgToLSFromGC_AskLogin, CClientSession::Msg_Handle_Init, true);
	SetUnKnownMsgHandle(CClientSession::Msg_Handle_Dispath);
}

CClientSession::CClientSession()
{
	mLogicInited = false;
}

CClientSession::~CClientSession()
{

}

void CClientSession::SendInitData()
{

}

void CClientSession::OnRealEstablish()
{
}

void CClientSession::OnClose()
{
}

bool CClientSession::Msg_Handle_Init(const char* pMsg, int n32MsgLength, INetSession* vthis, int n32MsgID)
{
	// 收到第1消息:请求登录,放入登录队列
	ELOG(LOG_DBBUG,"收到第1消息:请求登录,放入登录队列");
	boost::shared_ptr<GCToLS::AskLogin> sLogin = ParseProtoMsg<GCToLS::AskLogin>(pMsg, n32MsgLength);
	if (!sLogin){
		//ELOG(LOG_ERROR, "Login Fail With Msg Analysis Error.");
		SDKAsynHandler::GetInstance().PostToLoginFailQueue(eEC_TBInvalidToken, vthis->GetID());
		return 0;
	}

	SDKAsynHandler::GetInstance().CheckLogin(*sLogin, vthis->GetID());
	vthis->SetInited(true,true);
	return true;
}

bool CClientSession::Msg_Handle_Dispath(const char* pMsg, int n32MsgLength, INetSession* vthis, int n32MsgID)
{
	return true;
}

int SDKAsynHandler::CheckLogin(GCToLS::AskLogin& sAskLogin, int gcnetid){
	string str_uin = sAskLogin.uin();
	const char *uin = str_uin.c_str();
	const char *sessionid = sAskLogin.sessionid().c_str();
	UINT32 un32platform = sAskLogin.platform();
	INT32 retflag = eNormal;

	UserLoginData pData;
	pData.platFrom = un32platform;
	pData.sessionid = sAskLogin.sessionid();
	pData.uin = sAskLogin.uin();
	{
		boost::mutex::scoped_lock lock(m_UserLoginDataMapMutex);
		if (m_UserLoginDataMap.find(gcnetid) != m_UserLoginDataMap.end()){
			ELOG(LOG_WARNNING, "玩家(%d)多次登录!!但服务器数据还没返回数据给客户端", sAskLogin.uin());
			return eNormal;
		}
		m_UserLoginDataMap[gcnetid] = pData;
	}

	ELOG(LOG_DBBUG, "GC Try To Login with uin:%s(%u), sessionid:%s, platform:%d", uin, gcnetid, sessionid, un32platform);

	string sSendData;
	switch(un32platform)
	{
	case ePlatformiOS_91:
		sSendData = SdkConnector::GetInstance().HttpSendData_91(uin, sessionid);
		break;
	case  ePlatformiOS_TB:
		sSendData = SdkConnector::GetInstance().HttpSendData_TB(sessionid);
		break;
	case ePlatformiOS_PP:
		sSendData = SdkConnector::GetInstance().HttpSendData_PP(sessionid);
		break;
	case ePlatformiOS_CMGE:
		sSendData = SdkConnector::GetInstance().HttpSendData_Cmge_JB(sessionid);
		break;
	case ePlatformAndroid_UC:
		sSendData = SdkConnector::GetInstance().HttpSendData_UC(sessionid);
		break;
	case ePlatformiOS_iTools:
		sSendData = SdkConnector::GetInstance().HttpSendData_iTools(sessionid);
		break;
	case ePlatformiOS_OnlineGame:
		sSendData = SdkConnector::GetInstance().HttpSendData_OnlineGame(sessionid);
		break;
	case ePlatformiOS_As:
		sSendData = SdkConnector::GetInstance().HttpSendData_As(sessionid);
		break;
	case ePlatformiOS_XY:
		sSendData = SdkConnector::GetInstance().HttpSendData_XY(uin, sessionid);
		break;
	case ePlatformAndroid_CMGE:
		sSendData = SdkConnector::GetInstance().HttpSendData_Cmge_Android(sessionid);
		break;
	case ePlatform_PC:
		sSendData = "PCTest";
		break;
	default:
		PostToLoginFailQueue(eEC_UnknowPlatform, gcnetid);
		break;
	}
	ELOG(LOG_SpecialDebug, "%s", sSendData.c_str());
	PostMsg(sSendData.c_str(), sSendData.size(), sAskLogin.msgid(), gcnetid, (EUserPlatform)un32platform);
	return 0;
}
void SdkConnector::PostMsgToGC_NotifyServerList(int gcnetid)
{
	// 发送第2消息:登录成功,bs服务器列表
	LSToGC::ServerBSAddr ServerList;
	map<UINT32,sServerAddr>::iterator iter = gAllServerAddr.begin();
	for(; iter != gAllServerAddr.end(); iter ++){
		LSToGC::ServerInfo *pInfo = ServerList.add_serverinfo();
		pInfo->set_servername(iter->second.str_name);
		pInfo->set_serveraddr(iter->second.str_addr);
		pInfo->set_serverport(iter->second.str_port);
	}
	ELOG(LOG_DBBUG, "Post Server List To User.");
	INetSessionMgr::GetInstance()->SendMsgToSession(ST_SERVER_LS_OnlyGC, gcnetid, ServerList, ServerList.msgid());
}

void SdkConnector::PostMsgToGC_NotifyLoginFail(int errorcode, int gcnetid)
{
	// 发送第1消息:登录失败
	LSToGC::LoginResult sMsg;
	sMsg.set_result(errorcode);
	INetSessionMgr::GetInstance()->SendMsgToSession(ST_SERVER_LS_OnlyGC, gcnetid, sMsg, sMsg.msgid());
}

好了,这个就是目前客户端通过tcp连接到服务器后,发送消息,服务器收到消息后向客户端发送消息的流程。有不懂的,欢迎在下面留言。

原文链接http://blog.csdn.net/u013108312/article/details/78574139

2017-11-19 00:16:54 isakwong 阅读数 5314

基本概念

游戏物体,是所有游戏中最重要的一个概念,所有出现在游戏中的物体,对玩家或者二次开发者可见的,就是游戏物体。

在魔兽争霸3的 Word Editor 的物体编辑器中可以看到有如下几种基本物体

  • 单位
  • 物品
  • 可破坏物
  • 技能
  • 魔法效果/特效
  • 升级

这里写图片描述

这些物体出现在单位的状态栏,建筑的升级栏,或者是商店的购买栏,或者是掉落在地上的物品。

可以看到这些都是组成游戏的基本元素,游戏内所有交互都是基于这些基本物体之间的。

而魔兽争霸3的 JASS 脚本以及 JASS 脚本封装一层后带有 GUI 的触发编辑器所进行的二次开发,都是基于暴雪所提供的魔兽争霸3的一些 API 。

这里写图片描述

在设计游戏的基本物体的时候,我们就需要考虑,这个物体是什么(IsA),这个物体有什么(HasA),这个物体能做什么(CanDo)。

同时我们要从这个物体给外界提供出接口,以方便外界组合出新功能。比如对某个物体提供了设置坐标的接口提供之后,便可以配合定时器做出冲锋,位移等效果。

设计物体

首先是IsA,开发游戏,大部分是需要选用面向对象语言的,比较符合游戏开发的需求。IsA是面向对象中重要的思想,IsA 决定了这个物体的基本属性。

举个例子:大法师是一个英雄,是一个单位。那么他就会继承所有单位的属性,同时,所有单位的操作也对大法师有效,比如杀死一个单位的操作,也是可以杀死一个大法师的。

这里写图片描述

其次是 HasA,HasA着重表示他拥有什么属性,IsA着重表示了他的父类是谁,那么HasA表示了他拥有了哪些属性。

大法师有名字,图标,攻击投射物,技能等属性,其中部分属性是从单位类继承而来。

当然在魔兽争霸3的世界编辑器中,这里的游戏物体显得比较扁平化

(可以看到在世界编辑器这里分类出来的游戏物体是非常特异化的,比如技能和单位,是几乎无法找到共同属性的,这也是为什么它俩要分开,但是在游戏引擎底层,应该会是属于同一个类,因为都要进行资源的读取,加载以及释放)

特异化

魔兽争霸3中的单位,大部分具有相同的属性,比如会有右边的操作栏,生命值,移动速度等等。而魔兽争霸3还分有:召唤物,建筑,普通单位,英雄等概念。

如果从编辑器这里的角度来看,魔兽争霸3做到特异化这些单位的方法,便是给属性设置不同的值。

从这里看起来很扁平化的原因便是如此。其实考虑到英雄可能是个召唤物,英雄也可能是个建筑这些概念的话,用属性控制特异化而不是用IsA来控制特异化是比较好的选择。因为现在支持虚继承,支持多重继承的面向对象语言不多了(其实有C++一个也就够了)

操作栏是魔兽争霸3中能够直接让玩家感受到不同的区域,英雄单位通过技能的不同组合,获得了特异的能力。