dogse入门指南
Dogse作为游戏服务端引擎,目前只包含游戏服务端的核心部分,但这也是最核心的部分。它全部使用.net c#开发,充分兼顾了程序性能与代码编写的准确性与易用性,再加上以vs作为开发工具,极大的提升代码的编写效率,以及方便的调试性。
在使用Dogse之前,我们还需要先了解它能做什么,在游戏服务端里处于什么位置。
Dogse简单的说,实现了游戏服务器的基本框架,包含最基础的网络通讯,消息协议,任务调度等基本的功能。和业务逻辑相关的代码都不包含在dogse里,还需要使用者自己来编写。
我们以demo 《TradeAge》为例子,说说一个基本的游戏项目的划分方式。
重要事情说3遍,dogse下载地址:
https://github.com/dogvane/DogSE
https://github.com/dogvane/DogSE
https://github.com/dogvane/DogSE
项目基本结构
服务器端结构
首先整个服务端可以划分为以下几个项目
TradeAge.Server.Entity 实体类项目是游戏里用到的各种数据对象,包括玩家的游戏数据,游戏的配置数据,以及一些服务器存储数据
TradeAge.Server.Interface 接口类定义的则是服务器与客户端的交互接口,客户端对服务器的一些收发数据的格式内容定义在这里,它依赖实体类项目
TradeAge.Server.Protocol 通讯协议项目,这个项目里保存的是对接口类项目的通讯交互协议的实现,这个项目里的代码是通过代码生成器来生成的,一般不需要手工去修改他。
TradeAge.Server.Logic 整个游戏的业务逻辑都写在这个项目里,具体的一些编写规则会后面描述
TradeAge.Server.Game 是整个游戏的启动项目,可以是控制台,可以是nt服务,或者是winform窗体。在这里完成一些简单的初始化工作就ok了。
基本上来说,整个游戏服务端的项目可以按照整个模块划分,当然根据项目类型的不同,也可以自己根据需要去修改项目的模块划分结构,只不过对于小项目而言,可以从整个结构开始了解整个服务器开发的过程。
客户端结构
客户端部分可以先分为以下几个部分
TradeAge.Client.Entity 客户端的实体类,基本与服务器项目里的 TradeAge.Server.Entity 里的文件保持一致,甚至可以共用一份文件,通过一些项目配置来对实体数据根据客户度和服务端做一些差异化的配置,这部分后面会详细说
TradeAge.Client.Controller 客户端的控制器,传说中的mvc杀器,在负责和游戏服务端对接,大部分的代码是自动生成的,视图层通过这个模块公开的事件(.net的event事件)来获得服务器对客户端的请求操作,并通过这里的一些方法来向服务器发起操作请求。
TradeAge.Client.Simulator 客户端模拟器,可以方便的不启动Unity3D等客户端情况下测试游戏里的某个功能。后期还可完成压力测试的机器人。
登陆模块实战
有了基本结构后,我们先来补充一下所有游戏都会用到的基本功能(登陆模块),并以这个功能来详细描述如何使用dogse构建一个游戏。
对象定义
首先为了实现登陆,必须有一个账号类,记录里登陆玩家的账号名与密码(如果是第三方平台接入,或者单点登陆,除了账号外,还会有一些其他的内容,具体情况根据实际碰到的问题在做增减)。
账号对象只对应着平台登陆,通常是放在Mysql里,demo里为了减少对系统的依赖,则放在一个xml文件系统里。
客户端在向服务器发起登陆请求后,服务器应该返回这次的登陆结果(成功或者失败)。如果成功成功,则还需要返回玩家在服务器里是否创建过角色,客户端再根据这个返回信息决定,是直接进入游戏,还是打开创建角色界面。当然,这个是目前页游和手游常见的做法,对于mmo网游,通常返回的就是一个玩家列表,一个账号在一个服务器里存在多个游戏角色。
我们先将账号类Account和玩家的角色类Player定义好放在TradeAge.Server.Entity项目下。
接口定义
对象定义好了,我们接着看一下客户端与服务器是如何交互的。
最开始描述功能的时候,说了,对于一个登陆模块来说,我们用一个简单的序列图来描述整个登陆流程
序列图很简单吧,实际在dogse里定义这个交互过程也很简单。
我们在TradeAge.Server.Interface项目里增加两个目录
Client放的是服务器给客户端发数据(消息)的接口
Server 放的是客户端给服务器发送数据(消息)的接口
当然,目录不是强制的,你们想怎么定义都行。
服务器接口定义
在Server目录下,增加第一个服务器登陆模块的接口文件ILogin
打开文件加入以下代码
大家可以注意看一下标注的几个地方。
1.服务器的接口需要继承自ILogicModule
2.每个网络消息对应一个接口的方法(类型wcf,webservice的接口定义)
3.每个方法上面需要有一个NetMethod的Attribute的属性,并填写上正确的参数
ILogicModule接口
这是Dogse里,对于每一个服务端的业务逻辑模块必须要实现的接口,包含一个属性,四个方法。这四个方法都会在整个游戏的生命周期里的不同时段被调用。大家可以在里面做一些必要的初始化。
接口里的消息方法
再看看每个消息方法
在Dogse之前使用mmose的时候,每个消息的处理函数都是这样定义的
在业务逻辑代码里再对这个reader里的数据做解析(参考DogseExample4和 DgoseExample5)。
现在Dogse只要将通信的数据写在每个方法里就能自动生成通讯协议代码,当然一些必要的约束还是要有的
1.方法的第一个参数必须是NetState类型,这个是用来表示是那个客户端接入服务端的。
2.方法允许空参数,方法实际上还是得有NetState这一个参数。
3.参数的基本类型有:int long byte float double bool string DateTime(DateTime实际按照long类型传输,取值DateTime.Ticks)
4.参数运行数组与List<T>类型
5.参数运行对象,但对象里的属性也只能是基本类型,数组与对象
方法的定义就这些要求,接下来再说说方法的属性标签
消息码
消息码作为前后通讯的唯一标识,大家应该都可以理解了,dogse用ushort类型(双字节)在实际项目里,可以定义一个枚举在记录通讯的消息id。
一般来说,ushort最大长度是65535,对于一般的游戏绝对够用了,当然,你还有什么特殊的做法或者想法,那就需要自己扩展了。
方法类型
这个看注解基本可以了解,
PackaetReader需要自己来解析数据,适合当你有非常复杂的数据结构时使用,偶尔为之即可,不可常用。
SimpleMethod Dogse建议的方法,基本可以满足需求
ProtocolStruct 当初设计的目标是让.net下的结构体和cc++下的结构体数据在字节上做序列化,不过不建议使用,除非你真的能保证内存结构的一致性
是否进行登录验证:
这是一个很神奇的参数,一般默认都是true,他可以在读写数据包操作前,对这个包做一个简单的过滤。简单的说,执行这个方法的客户端必须在完成登陆操作后才能被执行到。如何验证在今后的登陆模块里会说到。
当然,在Login的方法里,这个值必须是false。
任务类型:
今后在说Dogse底层的时候会详细的说明。
客户端接口定义
服务端的接口说完了,我们来看看客户端的接口
这里有2个类,一个是ILogin,和之前的服务端的ILogin类一样,里面写了和登陆相关的接口。ClientProxy顾名思义,就是客户端代理类,也就是服务器如果要往客户端发送数据,则需要通过这个类来发送
先看看ILogin的设计:
接口文件和服务端接口文件类似,只不过客户端的接口没有继承自ILogicModule,而是给接口加了ClientInterface的属性标签。这个标签的作用也仅仅是说明这个接口是为了服务器往客户端发送用的。剩下的方法与方法上的属性标签和服务器定义的需求一致。
ClientProxy则是对客户端接口模块做一个汇总的静态类,方便服务器找到对应的接口来发送数据。
消息代理类以及代理类生成工具
TradeAge.Server.Interface 的项目就介绍到这来,下面开始dogse的黑科技部分--代理类生成。
打开DogSE.Tools.CodeGeneration项目下的Program.cs文件
我们可以看到这个方法,他的作用就是将TradeAge.Server.Interface项目里的接口,生成2个代理类到TradeAge.Server.Protocol 项目的目录下,文件名分别是ServerLogicProtocol.cs和ClientProxyProtocol.cs。在最开始的时候,dogse也采用这样的代码生成技术,只不过是可以在服务器启动的时候就自动生成,并在内存里完成编译。但是这样的代码非常不利于debug,所以最后还是决定生成到文件里。当然,最终文件的输出路径(项目)和文件名都是可以换的,只不过我个人觉得,还是有一个独立的项目来放这些自动生成的代码会比较好。
让我们来看一下这两个类分别干了什么事情
ServerLogicProtocal.cs
ILoginAccess1这个是对ILogin这个接口的包装,注意,是包装,不是实现。
它的作用是承上启下,从Dogse的网络层获得消息包事件,然后再转发给逻辑模块里的对应方法。
这个方法就是用来绑定ILogin接口所对应个的逻辑模块实例
这个方法则是向Dogse注册和登陆相关的消息id所绑定的方法
而这两个方法这是用来解析消息包,并调用对应的游戏业务逻辑方法。
而整个游戏所有的业务接口类的包装类,都在这个方法进行统一的注册,到时候在程序启动的时候,只需要这样
一调用,服务器这个小伙伴就可以愉快的玩耍了。
当然这个类里的东西,大家基本上了解他是干啥的就行了,除非你不打算使用目前Dogse这套对数据进行序列化/反序化的方法可以,先对这里的代码做修改,然后形成一套规则,再修改DogSE.Tools.CodeGeneration 项目里的代码生成器。今后有空应该会补上protobuf的处理方法。
ClientProxyProtocol.cs
有了对服务器端的了解,客户端代理类就更简单一些了,基本上就是对方法参数做序列化后,转为消息包的格式通过Netstate暴露出来的方法把数据发送给客户端。
服务器端逻辑处理
有了底层的了解,我们来瞅瞅服务器最关键的部分,TradeAge.Server.Logic项目的结构
和之前的服务器端接口文件定义一样,一个接口文件对应一个业务模块,每个业务模块可以在这里就会对应一个对这个接口的实现类
这个是登陆模块的基本代码,我们先从接口方法到初始化方法过一次。
登陆的第一步自然是根据账号名去数据库里查玩家的账号在不在,在就验证密码,不在则创建一个新账号。这里说一下,为了演示,我没有做服务器连接,而是将数据保存到本地的xml文件里,一个账号对一根文件。实际项目应该是根据实际的场景连接对应的数据库,要么是mysql,要么是mongodb。
账号通过验证,做一些数据的基础赋值
注意这个操作,这里需要将netstate里的IsVerifyLogin的属性设置为true,说明,这个客户端连接通过了登陆验证,今后这个客户端再发过来的各种数据包才能触发对应的服务器方法。
没错,就是之前的ServerLogicProtocol.cs里的方法,在这里对网络连接预先做了判断,如果没有完成登陆,是不会触发服务器的创建角色方法。
基本属性设置好了,接着就是从数据库里加载与账号对应的角色数据。
这里做了一个判断,先读内存里的缓存数据,再读数据库,如果数据库里也没数据,则说明这个账号没创建过角色,那么返回给客户端的登陆成功消息里,是否创建角色的参数就只能是false,让客户端先创建角色才能继续下面的游戏。
这里就是当玩家创建过角色后,进行一些判断,第一步,先看看这个角色之前是否已经登录过,并且现在还在线。
这里的 player 对象在上面是先从缓存里读,再从db里读,如果玩家之前在线,那么缓存里一定会有这个玩家对象,同时和这个玩家所绑定的NetState的就是一个有效的对象,只需要判断这个对象在不在就知道此刻有没有另外一个客户端正在连接服务器,我们需要做的也仅仅是把这个客户端断开。(实际的项目里,还需要向客户端补发一条消息,告诉玩家你被其它地方登陆的客户端顶下线了)
接下来就是让 player对象和 netstate 对象互相关联起来。
而PlayerEnterGame独立出一个方法来,主要是为了让OnCreatePlayer里当玩家创建角色后,走同一套进入游戏的流程,目前这个方法只有2个函数,但随着项目的演进,这个方法会越来越膨胀的。
触发玩家事件
PlayerEvents则是另外另外一种初始化玩家进入游戏的方法,这个今后在各种模块里会详细跟大家说明的。这里简单的说一下,就是一些公共事件,例如登陆,离线,升级,解锁啥的,一些模块可能都需要在那个时刻做一些与自己相关的处理,这时候,如果都写在Login模块里的PlayerEnterGame里,会让代码在今后维护起来相当麻烦,所以,可以在游戏里设计一个静态的PlayerEvents的事件类,凡是有模块需要在登陆时做一些自己的特殊处理的时候,监听这个事件,然后模块自己的初始化代码就放在自己的模块里了。
关于登录的函数就说完了,剩下还有一个创建角色的代码,大家可以自己看看,基本逻辑和登录差不多。
服务器启动代码
底层的模块好了,我们下一步是让服务器端跑起来。
TradeAge.Server.Game项目就是用来启动服务端的程序,只所以要另外建立一个exe的宿主项目,是为了今后扩展用(动态更新服务端)。
这里的ServerConfig 对象是服务器连接用的,这里是示例代码,所以在这里初始化,实际项目里,应该是需要从本地的配置文件里读取的,这部分今后会详细介绍。
Logs 是dogse的日志模块,这里初始化了文件和控制台的日志
GameServerService 是整个服务器的一个总控,用于控制整个服务器的初始化流程,以及服务器状态的维护。一般来说服务器的初始化流程,先完服务配置文件的初始化,再完成游戏配置文件的初始化,接着初始化游戏模块,等游戏模块初始化完成后,就可以开启服务端socket接入客户端的连接请求了。目前项目是控制台程序,那么服务器启动好以后,需要接收管理员在控制台的一些指令输入(如退出,查询状态,重新加载某些数据等)。如果管理员在控制台输入了exit后,整个游戏会进入退出流程,先关闭客户端的连接,然后等待数据保存到数据库,最后才关闭整个进程。这些流程上的功能都是GameServerService进行控制的。
WorldBase 是游戏世界的实例,这部分和GameServerService一样,属于dogse的底层部分,这里我们简单的可以认为WorldBase就是控制着游戏运行逻辑的模块,它负责开启网络连接,客户端socket过来的网络消息,会转换为一个个网络任务,然后通知到对应业务逻辑处理代码。理论上说,一个服务端里可以开启多个游戏世界的实例,但目前来说还没这样做,更多的时候是开启多个游戏进程。
这里说一下,当游戏业务逻辑模块初始化完成后(所有继承自ILogicModule接口的对象的Initializationed方法被执行过)会触发GameServerService的AfterModuleInit的事件,这个时候,我们需要向系统注册模块的处理函数,和客户端代理类的注册。今后加入动态更新功能的话,在新的逻辑模块加载完成后,对新的逻辑模块重新注册后,就可以将客户端的操作请求切换到新的代码里了。
客户端部分解析
生成代理类
Dogse在生成服务器代理类的同时,针对u3d客户端,也会生成对应的接口的协议代码,最大程度上方便客户端的开发,生成方法类似服务器的生成方式。
客户端部分的代理类生成和服务器差不多,只不过这次不想服务器端那样,指定一个文件了,而是指定一个目录。我们再看看终客户端通过代码生成器生成的目录结构:
首先是LogicInterface.cs
客户端业务逻辑
这个是TradeAge.Server.Interface项目下Client目录下服务器往客户端发送数据的接口生成的代码,基本上是将接口的第一个Netstate参数移除后生成生成的。这里没有采用Interface而是抽象类作为客户端的接口,主要目的是让非TradeAge.Client.Contrller模块,也就是Unity3d,模拟器的项目在使用该模块模块的时候,不要看到这些由网络消息触发的方法。
和服务端一样,会生成2个协议代理文件客户端也会按照模块分别生成文件名为“模块.Net.cs”和“模块.Proxy.cs”文件,这分别是服务器通知客户端触发代码(.Net.cs),以及客户端向服务器通知的代码(.Proxy.cs),而我们还需要另外自己新建一个文件“模块.Logic.cs”(名字可以随意起,这个不做命名要求)。
模块.Logic.cs
控制器的名字为TradeAge.Server.Interface项目里对应个的IModule 接口的名字+Controller组成,这个是固定的,类必须继承自客户端LogicInterface.cs文件里定义的“Base模块Controller”命名的抽象类。完成定义后,就会想集成Interface一样,会要求你补完哪些响应网络消息的函数。今后客户端控制器的重点就在这个模块里了。我们需要在这里,将网络过来的消息,进行一些适度的包装,再通过事件的方式让View层知道有怎样的消息过来了。这部分代码
最后就是在GameController.cs文件里,把刚才的登陆模块加到里面。
这样客户端的Controller模块就算基本完成了,剩下的就是view层的事情了,在还view层还没开始做之前,我们先来做一个登陆模拟器。
登陆模拟器
BaseLoginTest.cs是我们的第一个客户端模拟器,完成了登录游戏,并创建第一个角色的功能。
一开始监听了3个事件,分别是网络连接,登陆完成,创建角色完成。接着你们就启动服务端,并让客户端连接到服务器
如果连接成功,则向服务器发送登陆请求。
登陆成功了,则看看角色是否创建过,没有的话就向服务器发送创建角色的请求。
好了,到这来登陆测试代码就算完成了。
最后,回到模拟器的程序的Program.cs处完成启动代码
这个代码就比较简单了,今后如果有啥移动测试的代码,可以再新建一个类,去做移动的数据包发送,更高级的还有战斗啥的,到时候在这里替换BaseLoginTest的对象就可以了。
整个TradeAge的登陆Demo介绍就到这里结束了。让我们把客户端和服务器都跑来器看看吧。