加载中...
基于 ET 框架的 MMO 项目架构笔记
发表于:2024-03-22 | 分类: 作品展示

基于 ET 框架的 MMO 项目架构笔记

一款基于 ET(Entity Technology)框架的 MMO 实践项目。ET 是国产开源的双端游戏框架,采用 Actor 模型 + 组件化 + 分布式 的设计,服务端与客户端共用同一套 C# 代码,非常适合中小团队搭建 MMO / MMORPG。


项目概述

MMO 相比单机或休闲游戏,核心难点在于服务器需要同时承载大量在线玩家、维持一套可信的世界状态、并保证客户端与服务端的一致。本项目用 ET 搭建了从「登录 → 网关 → 角色 → 战斗/场景」的完整闭环,重点实践了服务端的分布式拆分与客户端的基础操作手感。

整体流程可概括为:

1
客户端 → 登录服务器(认证) → 网关(维持连接) → 排队(限流) → 世界/战斗服务(逻辑) → 客户端(渲染 + 输入)

服务端核心模块

1. 分布式服务器

单进程服务器存在天然的承载上限与单点故障风险。ET 的分布式思路是把一个”大世界”拆成多个职责单一的进程,彼此通过内部消息通信,对外表现为一个整体。

本项目按职责划分了登录、网关、世界、战斗等进程:

进程 职责
登录服务器 账号认证、发放入场凭证
网关服务器 维持客户端长连接、消息路由
世界服务器 场景/角色/世界状态逻辑
战斗服务器 战斗结算等高算力模块

要点:进程间通信必须无状态化、可横向扩容,这样才能在开服高峰时通过加机器扛住流量。

2. 登录模块

登录是玩家的第一道门,也是最容易被攻击/压垮的入口。标准流程:

  1. 客户端连接网关,发起登录请求;
  2. 登录服务器校验账号密码(或第三方 token);
  3. 校验通过后,生成会话凭证并绑定该客户端;
  4. 将客户端分流到指定网关/世界服,完成入场。

设计考量:登录阶段不应承载游戏逻辑,越轻越好;凭证要有时效性并防止重放/伪造。

▎真实代码(Server/Hotfix/Demo/Login/Realm/Handler/C2R_AccountLoginHandler.cs 精简)

登录 handler 里做了三件事:按账号 hash 分配到对应 Realm 进程(天然做负载均衡)、协程锁防重复登录、密码校验(开发模式自动注册):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. 账号 hash 取模,把登录请求路由到指定的 Realm 进程
int modCount = (int)((ulong)request.Account.GetLongHashCode()
% (uint)StartSceneConfigCategory.Instance.Realms.Count);
if (session.DomainScene().InstanceId
!= StartSceneConfigCategory.Instance.Realms[modCount].InstanceId)
{
response.Error = ErrorCode.ERR_RealmAdressError; // 请求落在错误的 Realm
reply(); session.DisConnect().Coroutine(); return;
}

// 2. 协程锁 + 数据库查询,防同一账号并发登录
using (await CoroutineLockComponent.Instance.Wait(
CoroutineLockType.AccountLogin, account.GetLongHashCode()))
{
var list = await session.GetDirectDb().Query<AccountDB>(db => db.Account == account);
if (list.Count > 0) accountDB = list[0];

// 正式模式严格校验;开发模式首次登录自动建号
if (Game.Options.Develop == 0)
{
if (accountDB == null) { response.Error = ErrorCode.Err_Login_AccountNotExist; reply(); return; }
if (accountDB.Password != request.Password) { response.Error = ErrorCode.Err_Login_PassWordWrong; reply(); return; }
}
// 校验通过 → 挂 RealmAccountComponent,后续发凭证、分网关
}

关键点:取模路由让 Realm 进程可水平扩展;CoroutineLock 保证同一账号的登录串行,避免重复/顶号竞态。

3. 网关处理

网关是所有客户端连接的”总闸”,职责包括:

  • 维持连接:长连接的生命周期管理、心跳检测;
  • 消息路由:把客户端的请求转发给对应逻辑服务器,再把响应回传;
  • Session 管理:客户端 ↔ 服务器的映射关系,登出/断线时的清理。

网关做的是转发而非逻辑,因此可以多开、水平扩展,是分布式架构的”薄层”。

4. 排队服务器

开服/活动高峰时玩家涌入,若所有逻辑服务器直接放行,会瞬间被打垮。排队服务器的作用是限流缓冲:

  • 维护一个入场队列,超过承载上限的玩家进入排队;
  • 按队列顺序、结合负载动态放行;
  • 对玩家展示”排队人数/预计等待”,缓解焦虑。

要点:排队要无状态可水平扩展,放行策略要平滑,避免”一拥而上”。

▎真实代码(Server/Hotfix/Demo/Login/Queue/Handler/G2Queue_EnqueueHandler.cs)

网关把玩家请求转发给排队服,排队服判定是否入队,并回传排队人数与我的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class G2Queue_EnqueueHandler : AMActorRpcHandler<Scene, G2Queue_Enqueue, Queue2G_Enqueue>
{
protected override async ETTask Run(Scene scene, G2Queue_Enqueue request,
Queue2G_Enqueue response, Action reply)
{
QueueMgrComponent queueMgrComponent = scene.GetComponent<QueueMgrComponent>();
if (queueMgrComponent.TryEnterQueue(request.Account, request.UnitId, request.GateActorId))
{
response.NeedQueue = true; // 需要排队
response.Count = queueMgrComponent.Queue.Count; // 当前排队人数
response.Index = queueMgrComponent.GetIndex(request.UnitId); // 我在队列中的位置
}
reply();
await ETTask.CompletedTask;
}
}

排队服作为独立进程(SceneType.Queue)承接入队/取消/更新等 RPC,网关与地图服通过 Queue2G_EnterMap 等消息协调放行,实现峰值限流与平滑入场。

5. 角色创建

角色是玩家在游戏世界中的”身份载体”。创建流程涉及:

  • 角色数据建模(名字、外观、属性、起始装备等);
  • 与数据库交互(建号唯一性校验、持久化);
  • 创建成功后,将角色数据同步到世界服务器,玩家以该角色进入世界。

要点:角色名唯一性要在数据库层面加约束,避免并发重复;角色数据应独立于账号,支持多角色。

▎真实代码(Server/Hotfix/Demo/Login/RoleInfo/Handler/C2G_CreatRoleHandler.cs 精简)

角色创建在网关层校验身份后,调 Name 场景做全局重名校验,再生成 UnitId 并落库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 校验网关用户与区服数据
GateUSer gateUSer = session.GetComponent<SessionUserComponent>()?.USer;
if (gateUSer == null) { response.Error = ErrorCode.ERR_Login_NoneGateUser; reply(); return; }
AccountZoneDB accountDB = gateUSer.GetComponent<AccountZoneDB>();
if (accountDB == null) { response.Error = ErrorCode.ERR_Login_NoneAccountZone; reply(); return; }

// 2. 生成新 UnitId,调用独立的 Name 场景校验角色名唯一性
long unitId = IdGenerater.Instance.GenerateUnitId(accountDB.LoginZoneId);
Name2G_CheckName check = (Name2G_CheckName)await MessageHelper.CallActor(accountDB.LoginZoneId,
SceneType.Name, new G2Name_CheckName() { Name = request.name, UnitId = unitId });
if (check.Error != ErrorCode.ERR_Success) { response.Error = check.Error; reply(); return; }

// 3. 落库保存角色(账号下可挂多个角色)
RoleInfoDB roleInfoDB = accountDB.AddChildWithId<RoleInfoDB>(unitId);
roleInfoDB.Name = request.name;
roleInfoDB.Level = 1;
await session.GetDirectDb().Save(roleInfoDB);

名字唯一性没有在网关本地查表,而是调用 SceneType.Name 这个独立 Name 进程做跨服校验——避免分布式环境下并发重复取名。

6. 服务端导航数据

在 MMO 中,服务器需要掌握可移动区域数据,用于校验移动合法性(防作弊)。本项目在服务端维护导航/阻挡数据:

  • 服务端加载并维护地图阻挡数据 / 寻路网格;
  • 客户端移动请求到达后,服务端校验目标点是否合法、路径是否可达;
  • 通过校验才广播位置更新,保证服务器权威。

要点:阻挡数据应在服务器端校验,不能信任客户端自报坐标;数据规模大时需做分区/流式处理。


客户端核心模块

7. 客户端 UI

  • 基于 UI 框架(ET 自带 UI 组件化 + 基于消息驱动的 UI 状态管理);
  • 将登录、创建角色、主城等界面按模块拆分,UI 逻辑与业务解耦;
  • 通过消息/事件驱动 UI 刷新,避免直接对象引用造成的强耦合。

8. Touch 移动按钮与摄像机跟随

移动手感是 MMO 客户端体验的基础,本项目实现了虚拟摇杆(Touch 移动)与摄像机跟随:

  • 虚拟摇杆:监听触摸/拖拽,把摇杆偏移量换算成移动方向与速度,驱动角色移动并上报服务器;
  • 摄像机跟随:相机平滑跟随主角,处理旋转、缩放、碰撞避让,避免穿模。

要点:客户端移动做本地预测保证手感,同时与服务端权威位置做插值平滑,避免抖跳。


小结

这套 MMO 骨架覆盖了登录、网关、排队、角色、服务器权威移动、客户端操作的完整链路。对于想入门”分布式游戏服务器 + 双端”的开发者,以 ET 为骨架、按上述模块逐个落地,是一条很清晰的实践路径。

后续可以继续完善的方向:战斗结算上服务器、AOI 视野管理、技能/ Buff 系统、持久化与存档、压测与扩容。

更多作品见:本人作品

上一篇:
渲染管线Render Pipeline个人理解总结
下一篇:
基于 ET 的双端战斗系统
本文目录
本文目录