【Unity】uNet游戏服务端框架(一)服务端架构设计
更新日期:2025年10月9日。
项目源码:获取源码。
索引
- uNet游戏服务端框架
- 一、uNet源码结构
- 二、uNet架构设计
- 1.网络玩家 NetworkPlayer
- 2.网络实体 NetworkEntity
- 3.网络地图 NetworkMap
- 4.网络房间 NetworkRoom
- 5.启动服务端
uNet游戏服务端框架
uNet
游戏服务端框架为使用.Net Core
开发的高性能、高并发网络游戏服务端框架,基于async / await
的多线程架构天然适应IO高并发
环境,使用Protobuf
进行网络数据交换能够极大的降低带宽和网络延迟,同时使用对象池
技术管理服务端中的大多数实例能够显著的降低GC开销。
一、uNet源码结构
在uNet
解决方案目录中,存在4个项目:
uNet.Core:uNet框架的核心代码模块。
uNet.Engine:uNet框架中与Unity引擎进行同步交互的功能实现模块。
uNet.Example.ChineseChess:中国象棋的游戏服务端。
uNet.Example.MMO:MMORPG的游戏服务端。
二、uNet架构设计
在uNet
中,将一个游戏服务端程序中可能出现的所有实例划分为了如下的四类:
类型 | 描述 |
---|---|
网络玩家 | 代表网络中的玩家客户端实例 |
网络实体 | 代表存在于地图、房间中的具备网络同步需求的实体(比如NPC、副本中的BOSS) |
网络地图 | 代表可同时容纳大量玩家、大量实体的游戏场景(也即是游戏中的主城) |
网络房间 | 代表可同时容纳少量玩家、少量实体的游戏场景(也即是游戏中的副本) |
1.网络玩家 NetworkPlayer
网络玩家由NetworkPlayer
类表示,一个NetworkPlayer
实例即代表了存在于当前服务器中的一名玩家客户端,他的基础属性如下:
/// <summary>/// 网络玩家/// </summary>public abstract class NetworkPlayer : IObjectPoolable{/// <summary>/// 玩家的ID/// </summary>public long ID { get; private set; }/// <summary>/// 玩家姓名/// </summary>public string? Name { get; protected set; }/// <summary>/// 在本次同步中是否已改变/// </summary>public bool IsDirty { get; set; } = false;/// <summary>/// 心跳包校验码/// </summary>public abstract int HEARTBEAT { get; }/// <summary>/// 常规信息校验码/// </summary>public abstract int NORMAL { get; }//........}
ID:玩家的ID,玩家在当前游戏服务器中的唯一标识符,重新登录后会改变。
Name:玩家的姓名。
IsDirty:在本次同步中是否已改变,在执行网络同步时,只有标记为改变的玩家才会被同步。
HEARTBEAT:心跳包校验码,在心跳机制中,用于鉴别心跳包的校验码。
NORMAL:常规信息校验码,区别于心跳包的常规信息数据包的校验码。
由于NetworkPlayer
兼顾了与客户端进行双向通信的使命,所以他自身具备了区别心跳包
和常规信息数据包
的能力。
在NetworkPlayer
中已封装了用于收、发数据的相关方法(使用Socket
收、发),直接调用即可:
/// <summary>/// 发送数据到客户端/// </summary>/// <param name="bytes">数据内容</param>/// <param name="token">用于取消异步的token</param>/// <returns>是否发送成功</returns>protected async Task<bool> SendDataAsync(byte[]? bytes, CancellationToken token){//代码后续讲解......}/// <summary>/// 从客户端接收数据/// </summary>/// <param name="token">用于取消异步的token</param>/// <returns>网络消息</returns>protected async Task<NetworkMessage?> ReceiveDataAsync(CancellationToken token){//代码后续讲解......}
2.网络实体 NetworkEntity
网络实体由NetworkEntity
类表示,除了玩家以外的,存在于地图或副本中的其他需要进行网络同步
的实例,都统称为网络实体。
比如副本中的小怪和BOSS,他们需要
进行网络同步,以在不同的玩家客户端中表现为相同状态(比如相同的血量,相同的攻击姿态),所以他们是
网络实体。
但某些NPC,站在原地不动,玩家仅仅能点击他进行对话或接受任务,他们不需要
进行网络同步,所以他们不是
网络实体。
NetworkEntity
的基础属性如下:
/// <summary>/// 网络实体/// </summary>public abstract class NetworkEntity : IObjectPoolable{/// <summary>/// 实体的ID/// </summary>public long ID { get; private set; }/// <summary>/// 在本次同步中是否已改变/// </summary>public bool IsDirty { get; set; } = false;/// <summary>/// 实体名称/// </summary>public abstract string Name { get; }/// <summary>/// 实体类型/// </summary>public abstract string Type { get; }//......}
ID:实体的ID,实体在当前地图或副本中的唯一标识符,不同类型的实体可能重复。
IsDirty:在本次同步中是否已改变,在执行网络同步时,只有标记为改变的实体才会被同步。
Name:实体的名称,同一类型的实体,他们一般具有相同的名称。
Type:实体的类型,用于在网络环境中区分实体(比如服务端的某个实体Type为Enemy
,客户端的相同实体的Type也应该为Enemy
,确保在网络同步时能够正确定位到该实体)。
3.网络地图 NetworkMap
网络地图由NetworkMap
类表示,一个NetworkMap
实例即代表了存在于当前服务器中的一个地图场景,在地图中可容纳大量玩家或实体,玩家与实体之间产生交互并经过网络同步,最终完成一整套的游戏玩法。
他的基础属性如下:
/// <summary>/// 网络地图(用于容纳大量网络玩家和大量网络实体)/// </summary>public abstract class NetworkMap : IObjectPoolable{/// <summary>/// 地图名称/// </summary>public abstract string Name { get; }/// <summary>/// 地图类型/// </summary>public abstract string Type { get; }/// <summary>/// 执行网络同步的间隔时间(毫秒)/// </summary>public abstract int SyncInterval { get; }}
Name:地图的名称,同一类型的地图,他们一般具有相同的名称,且地图实例本身就是唯一的。
Type:地图的类型,用于在网络环境中区分地图(比如服务端的某个地图Type为Map1
,客户端的相同地图的Type也应该为Map1
,确保在网络同步时能够正确定位到该地图)。
SyncInterval:执行网络同步的间隔时间(毫秒),地图拥有自动对其中的玩家和实体进行网络同步的功能,此为2次同步之间的间隔时间,设置得越低同步频率越高,但同时服务端压力也越大,可能导致网络延迟卡顿。
注:通常情况下,地图的同步间隔时间可以设置较长,以降低性能开销。
4.网络房间 NetworkRoom
网络房间由NetworkRoom
类表示,一个NetworkRoom
实例即代表了存在于当前服务器中的一个房间场景,在房间中可容纳少量玩家或实体,玩家与实体之间产生交互并经过网络同步,最终完成一整套的游戏玩法。
由于同一类型的NetworkRoom
是可以创建多个的,所以他们在大多数时候也被叫做副本
。
他的基础属性如下:
/// <summary>/// 网络房间(用于容纳少量网络玩家和少量网络实体)/// </summary>public abstract class NetworkRoom : IObjectPoolable{/// <summary>/// 房间ID/// </summary>public long ID { get; private set; }/// <summary>/// 房间名称/// </summary>public abstract string Name { get; }/// <summary>/// 房间类型/// </summary>public abstract string Type { get; }/// <summary>/// 执行网络同步的间隔时间(毫秒)/// </summary>public abstract int SyncInterval { get; }}
ID:房间的ID,在当前服务器中的唯一标识符,不同类型的房间可能重复。
Name:房间的名称,同一类型的房间,他们一般具有相同的名称。
Type:房间的类型,用于在网络环境中区分房间(比如服务端的某个房间Type为Room1
,客户端的相同房间的Type也应该为Room1
,确保在网络同步时能够正确定位到该房间)。
SyncInterval:执行网络同步的间隔时间(毫秒),房间拥有自动对其中的玩家和实体进行网络同步的功能,此为2次同步之间的间隔时间,设置得越低同步频率越高,但同时服务端压力也越大,可能导致网络延迟卡顿。
注:通常情况下,房间的同步间隔时间可以设置较短,以带来更流畅的体验。
uNet
的网络同步采用的是状态同步
的方式,而网络同步的时机为固定频率同步
,这不一定适合所有游戏,只是我们本系列教程所采用的方式,当然,其他方式(比如帧同步
)后续可能也会考虑。
5.启动服务端
启动服务端的话将非常简单,比如通过查看MMORPG
的服务端入口函数Main
,将看到极简的代码:
namespace uNet.Example.MMO
{class Program{static void Main(string[] args){//定义服务端配置信息,比如监听的IP地址、端口号,玩家类型(继承至NetworkPlayer,同一游戏服务端中只能存在一种玩家类型)ServerConfig config = new ServerConfig("127.0.0.1", 11000, typeof(MMO_Player), 10, 20);//创建服务端入口ServerEntry entry = new ServerEntry(config);//创建一个地图(比如这里是初始地图:暴风要塞)entry.CreateMap<MMO_BeginnerMap>();//启动服务端entry.Start();//启用常规日志的打印显示Log.IsEnableInfo = true;//按下ESC键退出程序ConsoleKeyInfo consoleKeyInfo = Console.ReadKey();while (consoleKeyInfo.Key != ConsoleKey.Escape){consoleKeyInfo = Console.ReadKey();}}}
}
服务端为控制台程序
,启动完成后如下: