Unity-MMORPG内容笔记-其三
继续之前的内容:
战斗系统
无需多言,整个项目中最复杂的部分,也是代码量最大的部分。
属性系统
首先我们要定义一系列属性,毕竟所谓的战斗就是不断地扣血对吧。
属性系统是战斗系统的核心模块,负责管理角色的所有属性数据,包括初始属性、成长属性、装备加成和Buff效果,并通过多阶段计算得出最终属性值。系统支持属性实时更新,当角色等级提升、装备变化或Buff增减时,会自动重新计算并同步属性数据。
属性含义说明
- MaxHP/MaxMP : 角色的最大生命值和法力值,决定角色的生存能力和技能释放能力
- STR(力量) : 影响物理攻击和物理防御
- INT(智力) : 影响魔法攻击和魔法防御
- DEX(敏捷) : 影响攻击速度和暴击概率
- AD(物理攻击) : 决定物理技能和普通攻击的伤害
- AP(魔法攻击) : 决定魔法技能的伤害
- DEF(物理防御) : 降低受到的物理伤害
- MDEF(魔法防御) : 降低受到的魔法伤害
- SPD(攻击速度) : 影响攻击间隔和技能施放速度
- CRI(暴击概率) : 攻击产生暴击的几率
public class AttributeData{ public float[] Data = new float[(int)AttributeType.MAX]; ///  /// 最大生命 ///   public float MaxHP { get { return Data[(int)AttributeType.MaxHP]; } set { Data[(int)AttributeType.MaxHP] = value; } } /// /// 最大法力 ///  public float MaxMP { get { return Data[(int)AttributeType.MaxMP]; } set { Data[(int)AttributeType.MaxMP] = value; } } /// /// 力量 ///  public float STR { get { return Data[(int)AttributeType.STR]; } set { Data[(int)AttributeType.STR] = value; } } /// /// 智力 ///  public float INT { get { return Data[(int)AttributeType.INT]; } set { Data[(int)AttributeType.INT] = value; } } /// /// 敏捷 ///  public float DEX { get { return Data[(int)AttributeType.DEX]; } set { Data[(int)AttributeType.DEX] = value; } } /// /// 物理攻击 ///  public float AD { get { return Data[(int)AttributeType.AD]; } set { Data[(int)AttributeType.AD] = value; } } /// /// 魔法攻击 ///  public float AP { get { return Data[(int)AttributeType.AP]; } set { Data[(int)AttributeType.AP] = value; } } /// /// 物理防御 ///  public float DEF { get { return Data[(int)AttributeType.DEF]; } set { Data[(int)AttributeType.DEF] = value; } } /// /// 魔法防御 ///  public float MDEF { get { return Data[(int)AttributeType.MDEF]; } set { Data[(int)AttributeType.MDEF] = value; } } /// /// 攻击速度 ///  public float SPD { get { return Data[(int)AttributeType.SPD]; } set { Data[(int)AttributeType.SPD] = value; } } /// /// 暴击概率 ///  public float CRI { get { return Data[(int)AttributeType.CRI]; } set { Data[(int)AttributeType.CRI] = value; } }}
属性计算流程
- 初始属性加载 :通过 LoadInitAttribute 方法从角色定义中加载基础属性
- 成长属性加载 :通过 LoadGrowthAttribute 方法加载成长系数
- 装备属性加载 :通过 LoadEquipAttribute 方法汇总所有装备的属性加成
- 基础属性计算 :结合初始属性、成长属性和装备属性计算基础属性值
- 二级属性计算 :根据基础属性计算出生命值、攻击力等战斗属性
- 最终属性计算 :叠加Buff效果得到最终属性值
////// 初始化角色属性/// public void Init(CharacterDefine define, int level,List equips,NAttributeDynamic dynamicAttr){ this.DynamicAttr = dynamicAttr; this.LoadInitAttribute(this.Initial, define); this.LoadGrowthAttribute(this.Growth, define); this.LoadEquipAttribute(this.Equip, equips); this.Level = level; this.InitBasicAttributes(); this.InitSecondaryAttributes(); this.InitFinalAttributes(); if (this.DynamicAttr == null) { this.DynamicAttr = new NAttributeDynamic(); this.HP = this.MaxHP; this.MP = this.MaxMP; } else { this.HP = dynamicAttr.Hp; this.MP = dynamicAttr.Mp; }}////// 计算基础属性/// public void InitBasicAttributes(){ for (int i = (int)AttributeType.MaxHP; i < (int)AttributeType.MAX; i++) { this.Basic.Data[i] = this.Initial.Data[i]; } for (int i = (int)AttributeType.STR; i < (int)AttributeType.DEX; i++) { this.Basic.Data[i] = this.Initial.Data[i] + this.Growth.Data[i] * (this.Level - 1); this.Basic.Data[i] += this.Equip.Data[i]; }}////// 计算二级属性/// public void InitSecondaryAttributes(){ this.Basic.MaxHP = this.Basic.STR * 10 + this.Initial.MaxHP + this.Equip.MaxHP; this.Basic.MaxMP = this.Basic.INT * 10 + this.Initial.MaxMP + this.Equip.MaxMP; this.Basic.AD = this.Basic.STR * 5 + this.Initial.AD + this.Equip.AD; this.Basic.AP = this.Basic.INT * 5 + this.Initial.AP + this.Equip.AP; this.Basic.DEF = this.Basic.STR * 2 + this.Basic.DEX * 1 + this.Initial.DEF + this.Equip.DEF; this.Basic.MDEF = this.Basic.INT * 2 + this.Basic.DEX * 1 + this.Initial.MDEF + this.Equip.MDEF; this.Basic.SPD = this.Basic.DEX * 0.2f + this.Initial.SPD * 1 + this.Equip.SPD; this.Basic.CRI = this.Basic.DEX * 0.0002f + this.Initial.CRI * 1 + this.Equip.CRI;}public void InitFinalAttributes(){ for (int i = (int)AttributeType.MaxHP; i < (int)AttributeType.MAX; i++) { this.Final.Data[i] = this.Basic.Data[i] + this.Buff.Data[i]; }}
属性实时更新逻辑
- 客户端发起操作 :玩家在客户端进行升级、更换装备或使用Buff等操作
- 服务器验证和处理 :服务器接收这些操作请求,进行合法性验证,然后执行相应的业务逻辑
- 服务器更新属性 :在服务器端,当角色升级、更换装备或Buff变化时,会调用 Attributes.Init 方法重新计算属性
- 服务器同步数据 :属性更新后,服务器会将新的属性数据(通过 DynamicAttr )同步给客户端
- 客户端更新显示 :客户端接收并处理服务器同步的属性数据,然后更新UI显示
成长属性实现
- 加载成长系数 :通过 `Attributes.LoadGrowthAttribute` 从角色定义中加载STR、INT、DEX(各种属性)的成长系数
- 计算成长值 :基础属性 = 初始属性 + 成长系数 × (当前等级 - 1)
- 叠加装备加成 :将装备提供的属性直接累加到基础属性上
- 计算二级属性 :根据基础属性通过公式计算出AD、AP等战斗属性
- 应用Buff效果 :最终属性 = 基础属性 + Buff加成
////// 计算基础属性/// public void InitBasicAttributes(){ for (int i = (int)AttributeType.MaxHP; i < (int)AttributeType.MAX; i++) { this.Basic.Data[i] = this.Initial.Data[i]; } for (int i = (int)AttributeType.STR; i < (int)AttributeType.DEX; i++) { this.Basic.Data[i] = this.Initial.Data[i] + this.Growth.Data[i] * (this.Level - 1);// 一级属性成长 this.Basic.Data[i] += this.Equip.Data[i]; // 装备一级属性加成在计算属性前 }}private void LoadGrowthAttribute(AttributeData attr, CharacterDefine define){ attr.STR = define.GrowthSTR; attr.INT = define.GrowthINT; attr.DEX = define.GrowthDEX;}
Buff系统
Buff 系统主要用于临时修改角色的属性或状态,给角色带来增益或减益效果,从而影响游戏的战斗体验和策略性。例如,增加攻击力、防御力,或者减少移动速度、受到的伤害等。主要分为三个类:Buff类,BuffManager类,EffectManager类。
Buff类
Buff 类代表具体的 Buff 效果,包含了 Buff 的 ID、拥有者、定义和上下文等信息。它提供了添加属性和效果的方法,并在 Buff 结束时移除这些效果。
// ... existing code ...class Buff{ public int BuffID; private Creature Owner; private BuffDefine Define; private BattleContext Context; public bool Stoped; // ... existing code ... private void OnAdd() { if (this.Define.Effect != BuffEffect.None) { this.Owner.EffectMgr.AddEffect(this.Define.Effect); } AddAttr(); // ... existing code ... } private void AddAttr() { if (this.Define.DEFRatio != 0) { this.Owner.Attributes.Buff.DEF += this.Owner.Attributes.Basic.DEF * this.Define.DEFRatio; } if (this.Define.AD != 0) { this.Owner.Attributes.Buff.AD += this.Define.AD; } if (this.Define.AP != 0) { this.Owner.Attributes.Buff.AP += this.Define.AP; } // ... existing code ... this.Owner.Attributes.InitFinalAttributes(); }}// ... existing code ...
BuffManager类
BuffManager 是 Buff 系统的管理器,负责添加和更新 Buff。它维护了一个 Buff 列表,并在更新时移除已停止的 Buff。
// ... existing code ...class BuffManager{ private Creature Owner; List Buffs = new List(); // ... existing code ... internal void AddBuff(BattleContext context, BuffDefine buffDefine) { Buff buff = new Buff(this.BuffID,this.Owner, buffDefine, context); Buffs.Add(buff); } public void Upate() { for (int i = 0; i  b.Stoped); }}// ... existing code ...
EffectManager类
EffectManager 类负责管理 Buff 的效果,维护了一个效果字典,记录了每种效果的数量。它提供了添加、移除和检查效果的方法。
// ... existing code ...class EffectManager{ private Creature Owner; Dictionary Effects = new Dictionary(); // ... existing code ... public bool HasEffect(BuffEffect effect) { if (this.Effects.TryGetValue(effect,out int val)) { return val > 0; } return false; } public void AddEffect(BuffEffect effect) { Log.InfoFormat(\"[{0}].AddEffect {1}\", this.Owner.Name, effect); if (!this.Effects.ContainsKey(effect)) { this.Effects[effect] = 1; } else { this.Effects[effect]++; } } public void RemoveEffect(BuffEffect effect) { Log.InfoFormat(\"[{0}].AddEffect {1}\", this.Owner.Name, effect); if (this.Effects[effect] > 0) { this.Effects[effect]--; } }}// ... existing code ...
- BuffManager 类 BuffManager 是 Buff 系统的管理器,负责 Buff 的生命周期管理。它的主要职责包括:
- 维护一个 Buff 列表
- 添加新的 Buff
- 更新 Buff 的状态
- 移除已停止的 Buff
- Buff 类 Buff 类代表具体的 Buff 效果,是一个定义类。它的主要职责包括:
- 存储 Buff 的基本信息(ID、拥有者、定义和上下文等)
- 处理 Buff 添加时的逻辑(如添加效果、修改属性等)
- 处理 Buff 移除时的逻辑(如移除效果、恢复属性等)
- EffectManager 类 EffectManager 类负责管理 Buff 的效果,维护了一个效果字典,记录了每种效果的数量。它的主要职责包括:
- 检查角色是否拥有某种效果
- 添加效果
- 移除效果
客户端发起添加Buff请求,服务器验证后,BuffManager创建Buff实例;Buff类通过EffectManager添加效果并修改属性,服务器同步给客户端显示;BuffManager定期更新Buff状态,到期时,Buff类移除效果并恢复属性,服务器同步给客户端移除显示。
技能系统
技能系统是游戏中管理角色技能释放、效果生效和状态同步的核心系统,负责处理技能的整个生命周期,包括技能的学习、释放、冷却、命中、伤害计算以及视觉表现等环节。
大致上分为三类:Skill类、SkillMananger类、SkillDefine类。
Skill类
public class Skill{ public NSkillInfo Info { get; set; } public Creature Owner { get; set; } public SkillDefine Define { get; set; } public SkillStatus Status { get; set; } public float CD { get; set; } public float castingTime { get; set; } public float skillTime { get; set; } public int Hit { get; set; } public BattleContext BattleContext { get; set; } public List Bullets { get; set; } public bool CanCast() { /* 实现技能施放条件判断 */ } public void Cast() { /* 实现技能施放逻辑 */ } public void AddBuff(Creature target, int buffId) { /* 实现添加Buff逻辑 */ } public void DoHit() { /* 实现技能命中逻辑 */ } public int CalcSkillDamage(Creature target) { /* 实现伤害计算 */ } public void Update(float deltaTime) { /* 实现技能状态更新 */ }}
定义了技能的属性和行为,包括技能信息、所属角色、技能定义、状态、冷却时间等,以及技能施放、命中、伤害计算等核心逻辑。
SkillMananger类
public class SkillMananger{ public Creature Owner { get; set; } public Skill NormalSkill { get; set; } public List Skills { get; set; } public void InitSkills() { /* 从数据管理器加载技能定义并创建Skill实例 */ } public void Update(float deltaTime) { /* 遍历并更新所有技能的状态 */ } public Skill GetSkill(int skillId) { /* 根据技能ID获取技能 */ } public void AddSkill(NSkillInfo skillInfo) { /* 添加新技能 */ }}
管理角色的技能列表,负责技能的初始化、更新、获取和添加等操作,是角色与技能之间的桥梁。
SkillDefine类
public class SkillDefine{ public int ID { get; set; } public string Name { get; set; } public string Icon { get; set; } public string Animation { get; set; } public int Type { get; set; } public int Damage { get; set; } public int MPCost { get; set; } public float CD { get; set; } public float Range { get; set; } public int BulletId { get; set; } public int HitEffectId { get; set; } /* 其他技能定义属性 */}
存储技能的静态定义数据,如图标、动画、伤害、消耗、冷却时间等,这些数据通常从配置文件中加载。
值得一提的是:
- SkillDefine类存储的是 静态数据 ,这些数据通常是从配置文件(如SkillDefine.txt)中加载的,不会在运行时发生变化,比如技能的ID、名称、图标、伤害值、冷却时间等。
- Skill类存储的是 动态数据 ,这些数据会在运行时根据游戏状态发生变化,比如技能的当前冷却时间、施放状态、所属角色等。
使用方法和流程
技能释放流程
- 客户端检测用户输入,调用 Skill.BeginCast 方法
- 客户端通过 BattleService.SendSkillCast 向服务器发送技能释放请求
- 服务器端接收请求,调用 Skill.Cast 方法验证并执行技能
- 服务器端计算技能伤害并向客户端发送技能命中消息
- 客户端接收消息,播放技能特效并更新UI
技能状态管理
- 技能有三种状态:未使用( None )、施法中( Casting )、运行中( Running )
- 技能释放后进入施法状态,施法完成后进入运行状态
- 技能运行结束后回到未使用状态,开始冷却计时
敌人AI系统
敌人AI系统是游戏中控制怪物行为的核心系统,它负责决定怪物如何移动、攻击、释放技能以及对玩家行为做出反应,从而提高游戏的挑战性和趣味性,为玩家创造出丰富多样的战斗体验。
目前游戏中的敌人AI主要分为两类:
- 普通怪物AI( AIMonsterPassive ):这是默认的怪物AI类型,适用于大多数普通怪物。
- BOSS怪物AI( AIBoss ):专门为BOSS怪物设计的AI类型,可能具有更复杂的行为模式。
这里我们需要先提一嘴关于代理模式:因为我们的敌人AI是基于代理模式来做的:
代理模式是一种设计模式,它通过引入一个代理类来控制对原始类(被代理类)的访问,在不修改原始类代码的情况下扩展或增强其功能。
我们需要代理模式的原因主要有以下几点:一是实现职责分离,让被代理类专注于核心逻辑,代理类负责额外的控制和管理;二是增强扩展性,能够轻松添加新的功能或实现,而不需要修改现有代码;三是控制对被代理类的访问,可以在调用前后添加额外的逻辑(如验证、日志等);四是简化客户端使用,隐藏底层实现的复杂性。
在我们的项目中,代理模式的实现主要体现在 AIAgent 和 AIBase 类上。 AIBase 是被代理类,定义了AI的核心行为(如战斗状态更新、技能施放、跟随目标等); AIAgent 是代理类,它持有 AIBase 的引用,并根据怪物定义中的AI名称实例化对应的 AIBase 子类(如 AIMonsterPassive 或 AIBoss )。 AIAgent 会将收到的调用转发给 AIBase 实例,同时可能在转发前后添加额外的功能。这种实现方式使得我们能够轻松地添加新的AI行为,而不需要修改 AIAgent 或 Monster 类的代码,增强了系统的扩展性和灵活性。
// ... existing code ...class AIAgent{ private Monster owner; private AIBase ai; public AIAgent(Monster owner) { this.owner = owner; string ainame = owner.Define.AI; if (string.IsNullOrEmpty(ainame)) { ainame = AIMonsterPassive.ID; } switch (ainame) { case AIMonsterPassive.ID: this.ai = new AIMonsterPassive(owner); break; case AIBoss.ID: this.ai = new AIBoss(owner); break; default: break; } } internal void Update() { if (this.ai != null) { this.ai.Update(); } } internal void OnDamage(NDamageInfo damage, Creature source) { if (this.ai != null) { this.ai.OnDamage(damage, source); } }}// ... existing code ...
普通怪物AI
// ... existing code ...class AIMonsterPassive : AIBase{ public const string ID = \"AIMonsterPassive\"; public AIMonsterPassive(Monster monster):base(monster) { }}// ... existing code ...
- 继承自 AIBase 类,没有添加额外的行为
- 当怪物定义中没有指定AI类型时,默认使用这种类型
- 遵循基类的战斗逻辑:尝试释放技能 -> 尝试普通攻击 -> 跟随目标
BOSS怪物AI
// ... existing code ...class AIBoss :AIBase{ public const string ID = \"AIBoss\"; public AIBoss(Monster monster):base(monster) { }}// ... existing code ...
- 同样继承自 AIBase 类,目前没有添加额外的行为
- 专为BOSS怪物设计,可以在后续扩展中添加更复杂的行为逻辑
以下是敌人AI系统运作的简化示例代码:
// 怪物创建Monster monster = new Monster(tid, level, pos, dir);// 自动创建AI代理AIAgent agent = monster.AI;// 游戏循环更新while (gameRunning){ // 更新怪物 monster.Update(); { // 内部调用AI更新 agent.Update(); { // AI检查战斗状态 if (monster.BattleState == BattleState.InBattle) { // 处理战斗逻辑 UpdateBattle(); {  // 尝试释放技能  if (!TryCastSkill())  { // 尝试普通攻击 if (!TryCastNormal()) { // 跟随目标 FollowRarfet(); }  } } } } }}// 怪物受到伤害monster.OnDamage(damage, source);{ // 通知AI agent.OnDamage(damage, source); { // 设置目标 ai.OnDamage(damage, source); { target = source; } }}
副本系统
接下来是我们的副本系统:
主要就是这个PVP竞技场。
PVP竞技场
基础架构设计
首先需要明确竞技场的核心要素:
- 参与双方 :两个玩家(或队伍)
- 独立地图 :竞技场作为独立场景,与主城、野外地图分离
- 战斗规则 :回合制/即时制、胜利条件(如击败对方、比分领先等)
- 状态管理 :挑战发起、接受、准备、战斗、结算等状态
地图与场景设计
在 MapDefine.txt 中配置地图信息,指定类型为 Arena ,如项目中:
\"Name\": \"竞技场\",\"Type\": \"Arena\",\"SubType\": \"Arena\",\"Resource\": \"Arena\"
网络通信与消息定义
- 定义消息结构 :使用 Protocol Buffers 定义竞技场相关的消息,如项目中的 message.proto 包含:
- ArenaChallengeRequest (挑战请求)
- ArenaChallengeResponse (挑战响应)
- ArenaReadyRequest (准备请求)
- ArenaBeginResponse (开始响应)
- ArenaRoundStartResponse (回合开始)
- ArenaRoundEndResponse (回合结束)
- ArenaEndResponse (结束响应)
- 消息分发 :通过 MessageDistributer 分发消息,如 MessageDispatch.cs 中处理各种竞技场消息
核心逻辑实现
客户端代码
ArenaService.cs
负责处理客户端与服务器之间的竞技场消息通信,包括订阅消息、发送挑战请求和响应等。
using Managers;using Models;using Network;using SkillBridge.Message;using System;using System.Collections.Generic;using System.Linq;using System.Text;using UnityEngine;namespace Services{ class ArenaService : Singleton, IDisposable { public void Init() { } public ArenaService() { MessageDistributer.Instance.Subscribe(this.OnArenaBegin); MessageDistributer.Instance.Subscribe(this.OnArenaChallengeResponse); MessageDistributer.Instance.Subscribe(this.OnArenaEnd); MessageDistributer.Instance.Subscribe(this.OnArenaChallengeRequest); MessageDistributer.Instance.Subscribe(this.OnArenaReady); MessageDistributer.Instance.Subscribe(this.OnArenaRoundStart); MessageDistributer.Instance.Subscribe(this.OnArenaRoundEnd); } public void Dispose() { MessageDistributer.Instance.Unsubscribe(this.OnArenaBegin); MessageDistributer.Instance.Unsubscribe(this.OnArenaChallengeResponse); MessageDistributer.Instance.Unsubscribe(this.OnArenaEnd); MessageDistributer.Instance.Unsubscribe(this.OnArenaChallengeRequest); MessageDistributer.Instance.Unsubscribe(this.OnArenaReady); MessageDistributer.Instance.Unsubscribe(this.OnArenaRoundStart); MessageDistributer.Instance.Unsubscribe(this.OnArenaRoundEnd); } private void OnArenaChallengeRequest(object sender, ArenaChallengeRequest request) { Debug.Log(\"OnArenaChallengeRequest\"); var confirm = MessageBox.Show(string.Format(\"{0} 邀请你竞技场对战\",request.ArenaInfo.Red.Name),\"竞技场对战\",MessageBoxType.Confirm,\"接受\",\"拒绝\"); confirm.OnNo = () => { this.SendArenaChallengeResponse(false, request); }; confirm.OnYes = () => { this.SendArenaChallengeResponse(true, request); };  } private void OnArenaBegin(object sender, ArenaBeginResponse message) { Debug.Log(\"OnArenaBegin\"); ArenaManager.Instance.EnterArena(message.ArenaInfo); } private void OnArenaEnd(object sender, ArenaEndResponse message) { Debug.Log(\"OnArenaEnd\"); ArenaManager.Instance.ExitArena(message.ArenaInfo); } ///  /// 发起挑战 ///   ///  ///  public void SendArenaChallengeRequest(int targetId, string name) { Debug.Log(\"SendTeamInviteRequest\"); NetMessage message = new NetMessage(); message.Request = new NetMessageRequest(); message.Request.arenaChallengeReq = new ArenaChallengeRequest(); message.Request.arenaChallengeReq.ArenaInfo = new ArenaInfo(); message.Request.arenaChallengeReq.ArenaInfo.Red = new ArenaPlayer() { EntityId = User.Instance.CurrentCharacterInfo.Id, Name = User.Instance.CurrentCharacterInfo.Name }; message.Request.arenaChallengeReq.ArenaInfo.Blue = new ArenaPlayer() { EntityId = targetId, Name = name }; NetClient.Instance.SendMessage(message); } private void OnArenaChallengeResponse(object accept, ArenaChallengeResponse message) { Debug.Log(\"OnArenaChallengeResponse\"); if (message.Resul != Result.Success) { MessageBox.Show(message.Errormsg, \"对方拒绝挑战\"); } } ///  /// 发起挑战的响应 ///   ///  ///  public void SendArenaChallengeResponse(bool accept,ArenaChallengeRequest request) { Debug.Log(\"SendArenaChallengeResponse\"); NetMessage message = new NetMessage(); message.Request = new NetMessageRequest(); message.Request.arenaChallengeRes = new ArenaChallengeResponse(); message.Request.arenaChallengeRes.Resul = accept ? Result.Success : Result.Failed; message.Request.arenaChallengeRes.Errormsg = accept ? \"\" : \"对方拒绝了挑战请求\"; message.Request.arenaChallengeRes.ArenaInfo = request.ArenaInfo; NetClient.Instance.SendMessage(message); } public void SendArenaReadyRequest(int arenaId) { Debug.Log(\"SendArenaChallengeResponse\"); NetMessage message = new NetMessage(); message.Request = new NetMessageRequest(); message.Request.arenaReady = new ArenaReadyRequest(); message.Request.arenaReady.entityId = User.Instance.CurrentCharacter.entityId; message.Request.arenaReady.arenaId = arenaId; NetClient.Instance.SendMessage(message); } private void OnArenaRoundEnd(object sender, ArenaRoundEndResponse message) { ArenaManager.Instance.OnRoundEnd(message.Round, message.ArenaInfo); } private void OnArenaRoundStart(object sender, ArenaRoundStartResponse message) { ArenaManager.Instance.OnRoundStart(message.Round, message.ArenaInfo); } private void OnArenaReady(object sender, ArenaReadyResponse message) { ArenaManager.Instance.OnReady(message.Round, message.ArenaInfo); } }}
ArenaManager.cs
管理客户端的竞技场状态,如进入/退出竞技场、准备状态、回合开始/结束等,并通知UI更新。
using Services;using SkillBridge.Message;using System;using System.Collections.Generic;using System.Linq;using System.Text;using UnityEngine;namespace Managers{ class ArenaManager : Singleton { ArenaInfo ArenaInfo; public int Round; internal void EnterArena(ArenaInfo arenaInfo) { Debug.LogFormat(\"ArenaManager.EnterArena : {0}\", arenaInfo.ArenaId); this.ArenaInfo = arenaInfo; } internal void ExitArena(ArenaInfo arenaInfo) { Debug.LogFormat(\"ArenaManager.ExitArena : {0}\", arenaInfo.ArenaId); this.ArenaInfo = null; } internal void SenReady() { Debug.LogFormat(\"ArenaManager.SendReady: {0}\", this.ArenaInfo.ArenaId); ArenaService.Instance.SendArenaReadyRequest(this.ArenaInfo.ArenaId); } public void OnReady(int round,ArenaInfo arenaInfo) { Debug.LogFormat(\"ArenaManager.OnReady:{0} Round:{1}\", arenaInfo.ArenaId, round); this.Round = round; if (UIArena.Instance != null) { UIArena.Instance.ShowCountDown(); } } public void OnRoundStart(int round,ArenaInfo arenaInfo) { Debug.LogFormat(\"ArenaManager.OnRoundStart:{0} Round:{1}\", arenaInfo.ArenaId, round); if (UIArena.Instance != null) { UIArena.Instance.ShowRoundStart(round,arenaInfo); } } public void OnRoundEnd(int round, ArenaInfo arenaInfo) { Debug.LogFormat(\"ArenaManager.OnRoundEnd:{0} Round:{1}\", arenaInfo.ArenaId, round); if (UIArena.Instance != null) { UIArena.Instance.ShowRoundResult(round, arenaInfo); } } }}
服务器端代码
Arena.cs
维护竞技场的核心逻辑,包括玩家进入、准备、战斗、结算等状态管理,以及回合计时、胜负判定等。
using Common;using Common.Data;using GameServer.Managers;using GameServer.Services;using Network;using SkillBridge.Message;using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace GameServer.Models{ class Arena { const float READY_TIME = 11f; const float ROUND_TIME = 60f; const float RESULT_TIME = 5f; public Map Map; public ArenaInfo ArenaInfo; public NetConnection Red; public NetConnection Blue; Map SourceMapRed; Map SourceMapBlue; int RedPoint = 9; int BluePoint = 10; private bool redReady; private bool blueReady; private ArenaStatus ArenaStatus; private ArenaRoundStatus RoundStatus; private float timer = 0; public int Round { get; internal set; } private bool Redy { get { return this.redReady && this.blueReady; } } public Arena(Map map, ArenaInfo arena, NetConnection red, NetConnection blue) { this.Map = map; arena.ArenaId = map.InstabceID; this.ArenaInfo = arena; this.Red = red; this.Blue = blue; this.ArenaStatus = ArenaStatus.Wait; this.RoundStatus = ArenaRoundStatus.None; this.Round = 0; } internal void PlayerEnter() { this.SourceMapRed = PlayerLeaveMap(this.Red); this.SourceMapBlue = PlayerLeaveMap(this.Blue); this.PlayerEnterArena(); } private void PlayerEnterArena() { TeleporterDefine redPoint = DataManager.Instance.Teleporters[this.RedPoint]; this.Red.Session.Character.Position = redPoint.Position; this.Red.Session.Character.Direction = redPoint.Direction; TeleporterDefine bluePoint = DataManager.Instance.Teleporters[this.BluePoint]; this.Blue.Session.Character.Position = bluePoint.Position; this.Blue.Session.Character.Direction = bluePoint.Direction; this.Map.AddCharacter(this.Red, this.Red.Session.Character); this.Map.AddCharacter(this.Blue, this.Blue.Session.Character); this.Map.CharacterEnter(this.Blue, this.Blue.Session.Character); this.Map.CharacterEnter(this.Red, this.Red.Session.Character); EntityManager.Instance.AddMapEntity(this.Map.ID, this.Map.InstabceID, this.Red.Session.Character); EntityManager.Instance.AddMapEntity(this.Map.ID, this.Map.InstabceID, this.Blue.Session.Character); } public void Update() { if (this.ArenaStatus == ArenaStatus.Game) { UpdateRound(); } } private void UpdateRound() { if (this.RoundStatus == ArenaRoundStatus.Ready) { this.timer -= Time.deltaTime; if (timer < 0) {  this.RoundStatus = ArenaRoundStatus.Fight;  this.timer = ROUND_TIME;  Log.InfoFormat(\"Arena :[{0}] Round Start\", this.ArenaInfo.ArenaId);  ArenaService.Instance.SendArenaRoundStart(this); } } else if(this.RoundStatus == ArenaRoundStatus.Fight) { this.timer -= Time.deltaTime; if (timer < 0) {  this.RoundStatus = ArenaRoundStatus.Result;  this.timer = ROUND_TIME;  Log.InfoFormat(\"Arena:[{0}] Round End\", this.ArenaInfo.ArenaId);  ArenaService.Instance.SendArenaRoundEnd(this); } } else if(this.RoundStatus == ArenaRoundStatus.Result) { this.timer -= Time.deltaTime; if (timer = 3)  { ArenaResult();  }  else  { NextRound();  } } } } private void ArenaResult() { this.ArenaStatus = ArenaStatus.Result; //执行结算 } private Map PlayerLeaveMap(NetConnection player) { var currentMap = MapManager.Instance[player.Session.Character.Info.mapId]; currentMap.CharacterLeve(player.Session.Character); EntityManager.Instance.RemoveMapEntity(currentMap.ID, currentMap.InstabceID, player.Session.Character); return currentMap; } internal void EntityReady(int entityId) { if (this.Red.Session.Character.entityId == entityId) { this.redReady = true; } if (this.Blue.Session.Character.entityId == entityId) { this.blueReady = true; } if (this.Redy) { this.ArenaStatus = ArenaStatus.Game; this.Round = 0; NextRound(); } } private void NextRound() { this.Round++; this.timer = READY_TIME; this.RoundStatus = ArenaRoundStatus.Ready; Log.InfoFormat(\"Srena:[{0}] Round[{1}] Ready\", this.ArenaInfo.ArenaId, this.Round); ArenaService.Instance.SendArenaReady(this); } }}
ArenaService.cs
处理服务器端的竞技场消息,如挑战请求、响应、准备请求等,并负责创建竞技场实例、发送状态更新等。
using Common;using GameServer.Entities;using GameServer.Managers;using Network;using SkillBridge.Message;using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace GameServer.Services{ class ArenaService : Singleton { public ArenaService() { MessageDistributer<NetConnection>.Instance.Subscribe(this.OnArenaChallengeRequest); MessageDistributer<NetConnection>.Instance.Subscribe(this.OnArenaChallengeResponse); MessageDistributer<NetConnection>.Instance.Subscribe(this.OnArenaReady); } public void Dispose() { MessageDistributer<NetConnection>.Instance.Unsubscribe(this.OnArenaChallengeRequest); MessageDistributer<NetConnection>.Instance.Unsubscribe(this.OnArenaChallengeResponse); MessageDistributer<NetConnection>.Instance.Unsubscribe(this.OnArenaReady); } public void Init() { ArenaManager.Instance.Init(); } private void OnArenaChallengeRequest(NetConnection sender, ArenaChallengeRequest request) { Character character = sender.Session.Character; Log.InfoFormat(\"OnArenaChallengeRequest::RedId:[{0}] RedName :[{1}] BlueID[{2}] BlueName:[{3}]\", request.ArenaInfo.Red.EntityId, request.ArenaInfo.Red.Name, request.ArenaInfo.Blue.EntityId, request.ArenaInfo.Blue.Name); NetConnection blue = null; if (request.ArenaInfo.Blue.EntityId > 0) {//如果没有传入ID,则使用名称查找 blue = SessionManager.Instance.GetSession(request.ArenaInfo.Blue.EntityId); } if (blue == null) { sender.Session.Response.arenaChallengeRes = new ArenaChallengeResponse(); sender.Session.Response.arenaChallengeRes.Resul = Result.Failed; sender.Session.Response.arenaChallengeRes.Errormsg = \"好友不存在或者不在线\"; sender.SendResponse(); } Log.InfoFormat(\"OnArenaChallengeRequest:: RedId:{0} RedName:{1} BlueID:{2} BlueName:{3}\", request.ArenaInfo.Red.EntityId, request.ArenaInfo.Red.Name, request.ArenaInfo.Blue.EntityId, request.ArenaInfo.Blue.Name); blue.Session.Response.arenaChallengeReq = request; blue.SendResponse(); } private void OnArenaChallengeResponse(NetConnection sender, ArenaChallengeResponse response) { Character character = sender.Session.Character; Log.InfoFormat(\"OnArenaChallengeResponse::RedId:[{0}] RedName :[{1}] BlueID[{2}] BlueName:[{3}]\", response.ArenaInfo.Red.EntityId, response.ArenaInfo.Red.Name, response.ArenaInfo.Blue.EntityId, response.ArenaInfo.Blue.Name); var requester = SessionManager.Instance.GetSession(response.ArenaInfo.Red.EntityId); if (requester == null) { sender.Session.Response.arenaChallengeRes.Resul = Result.Failed; sender.Session.Response.arenaChallengeRes.Errormsg = \"挑战者已经下线\"; sender.SendResponse(); return; } if (response.Resul == Result.Failed) { requester.Session.Response.arenaChallengeRes = response; requester.Session.Response.arenaChallengeRes.Resul = Result.Failed; requester.SendResponse(); return; } var arena = ArenaManager.Instance.NewArena(response.ArenaInfo, requester,sender); this.SendArenaBegin(arena); } private void SendArenaBegin(Models.Arena arena) { var arenaBegin = new ArenaBeginResponse(); arenaBegin.Result = Result.Failed; arenaBegin.Errormsg = \"对方不在线\"; arenaBegin.ArenaInfo = arena.ArenaInfo; arena.Red.Session.Response.arenaBegin = arenaBegin; arena.Red.SendResponse(); arena.Blue.Session.Response.arenaBegin = arenaBegin; arena.Blue.SendResponse(); } private void OnArenaReady(NetConnection sender, ArenaReadyRequest message) { var arena = ArenaManager.Instance.GetArena(message.arenaId); arena.EntityReady(message.entityId); } public void SendArenaReady(Models.Arena arena) { var arenaReady = new ArenaReadyResponse(); arenaReady.Round = arena.Round; arenaReady.ArenaInfo = arena.ArenaInfo; arena.Red.Session.Response.arenaReady = arenaReady; arena.Red.SendResponse(); arena.Blue.Session.Response.arenaReady = arenaReady; arena.Blue.SendResponse(); } public void SendArenaRoundStart(Models.Arena arena) { var roundStart = new ArenaRoundStartResponse(); roundStart.Round = arena.Round; roundStart.ArenaInfo = arena.ArenaInfo; arena.Red.Session.Response.arenaRoundStart = roundStart; arena.Red.SendResponse(); arena.Blue.Session.Response.arenaRoundStart = roundStart; arena.Blue.SendResponse(); } public void SendArenaRoundEnd(Models.Arena arena) { var roundEnd = new ArenaRoundEndResponse(); roundEnd.Round = arena.Round; roundEnd.ArenaInfo = arena.ArenaInfo; arena.Red.Session.Response.arenaRoundEnd = roundEnd; arena.Red.SendResponse(); arena.Blue.Session.Response.arenaRoundEnd = roundEnd; arena.Blue.SendResponse(); } }}
PVP竞技场的工作流程主要分为以下几个阶段:
1. 挑战发起 客户端通过 UIFriends.cs 中的UI逻辑发起竞技场挑战,调用 ArenaService.SendChallengeRequest 方法向服务器发送挑战请求。
2. 挑战响应 服务器端 ArenaService 接收挑战请求,处理后向挑战双方发送响应。若被挑战方接受,进入下一步;若拒绝,则流程终止。
3. 进入竞技场 接受挑战后,服务器通过 ArenaManager.CreateArena 创建竞技场实例,客户端通过 MapService 调用 SceneManager.Instance.LoadScene 加载竞技场场景( Arena.unity )。
4. 准备阶段 客户端加载场景完成后, ArenaManager 处理进入竞技场逻辑, UIArena 显示倒计时。客户端发送 ArenaService.SendReadyRequest 表示准备就绪,服务器端 Arena 类中的 Update 方法计时准备阶段(通常几秒)。
5. 战斗阶段 准备阶段结束后,服务器触发回合开始,向客户端发送 ArenaStart 消息,客户端 UIArena 更新UI显示战斗开始。双方玩家在竞技场中进行战斗,服务器通过 Battle 类管理战斗逻辑,同步双方状态。
6. 回合结束 战斗持续一定时间或一方达到胜利条件后,服务器 Arena 类中的 UpdateRoundResult 方法计算回合结果,向客户端发送 RoundEnd 消息, UIArena 更新回合结果信息。
7. 竞技场结束 达到设定的回合数或一方累计胜利次数满足条件后,服务器 Arena 类中的 UpdateArenaResult 方法判定最终胜负,向客户端发送 ArenaEnd 消息,客户端 ArenaManager 处理退出竞技场逻辑,加载回原场景。
一些问题和补充
Q:
我会说这个项目实现了TCP+ProtoBuf网络通信框架,支持异步消息处理、断线重连等完整网络解决方案,你能先介绍这个TCP+ProtoBuf网络通信框架的概念,内容,如何实现,以及后续的通信相关功能如何实现的吗?
A:
TCP+ProtoBuf网络通信框架是一种高性能的网络通信解决方案,结合了TCP协议的可靠传输特性和Protocol Buffers的高效序列化能力。
关于TCP的特点:面向连接的可靠传输、流量控制和拥塞控制、具有重传机制等。
关于Protocol Buffers的特点:跨语言、跨平台;体积小,速度快;强类型定义(每个字段都有明确的类型声明),编译时检查(一般的序列化方式是动态的,也就是在运行时才执行,而protocol buffers则是在编译时就执行并进行语法和语义检查);向前兼容性好(指新版本的协议能够处理旧版本的数据)。
这个框架可以分为三层:网络层、协议层和应用层。网络层负责底层的TCP连接管理,包括客户端连接、服务器监听、数据收发等基础功能。协议层定义了消息的格式和结构,使用Protocol Buffers定义消息协议,包含消息头(消息ID、长度、序列号等元信息)和消息体(序列化的业务数据)。应用层提供消息路由、会话管理、断线重连等高级功能。

网络层采用异步Socket实现,支持连接管理和断线重连:
using System;using System.Net;using System.Net.Sockets; // .NET框架提供的Socket类using System.IO;  // .NET框架提供的MemoryStream类using UnityEngine;namespace Network{ ///  /// 网络客户端 - 负责与服务器建立TCP连接 /// 继承自MonoSingleton,这是Unity中常用的单例模式 ///   public class NetClient : MonoSingleton { // 常量定义 const int DEF_RECV_BUFFER_SIZE = 64 * 1024; // 接收缓冲区大小:64KB const int DEF_TRY_CONNECT_TIMES = 3; // 重连次数:3次 const int NetConnectTimeout = 10000; // 连接超时:10秒 // 错误码定义 public const int NET_ERROR_SEND_EXCEPTION = 1000; // 发送异常 public const int NET_ERROR_ILLEGAL_PACKAGE = 1001; // 非法数据包 public const int NET_ERROR_ZERO_BYTE = 1002; // 零字节错误 public const int NET_ERROR_FAIL_TO_CONNECT = 1005; // 连接失败 // 事件委托定义 - 用于通知连接状态变化 public delegate void ConnectEventHandler(int result, string reason); public event ConnectEventHandler OnConnect; // 连接成功事件 public event ConnectEventHandler OnDisconnect; // 断开连接事件 // 网络相关字段 private IPEndPoint address;// 服务器地址和端口 private Socket clientSocket;  // TCP Socket对象 private MemoryStream sendBuffer;  // 发送缓冲区 private MemoryStream receiveBuffer;  // 接收缓冲区 private Queue sendQueue; // 发送消息队列 // 连接状态管理 private bool connecting = false;  // 是否正在连接 private int retryTimes = 0;// 当前重试次数 private int retryTimesTotal = DEF_TRY_CONNECT_TIMES; // 最大重试次数 private int sendOffset = 0;// 发送缓冲区偏移量 public bool running { get; set; }  // 网络服务运行状态 // 包处理器 - 负责消息的序列化和反序列化 public PackageHandler packageHandler; ///  /// 初始化网络客户端 ///   protected override void OnStart() { running = true; // 初始化缓冲区 sendBuffer = new MemoryStream(); receiveBuffer = new MemoryStream(DEF_RECV_BUFFER_SIZE); sendQueue = new Queue(); // 初始化包处理器 packageHandler = new PackageHandler(null); // 设置消息分发器抛出异常 MessageDistributer.Instance.ThrowException = true; } ///  /// 初始化服务器地址 ///   /// 服务器IP地址 /// 服务器端口 public void Init(string serverIP, int port) { // IPAddress.Parse() - .NET框架方法,将字符串IP转换为IPAddress对象 // IPEndPoint - .NET框架类,表示IP地址和端口的组合 this.address = new IPEndPoint(IPAddress.Parse(serverIP), port); } ///  /// 连接到服务器 ///   public void Connect() { if (this.connecting) return; // 关闭之前的连接 if (this.clientSocket != null) { this.clientSocket.Close(); } this.connecting = true; this.DoConnect(); } ///  /// 执行连接操作 ///   void DoConnect() { try { // Socket构造函数参数说明: // AddressFamily.InterNetwork - IPv4协议 // SocketType.Stream - 流式Socket(TCP) // ProtocolType.Tcp - TCP协议 this.clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 设置为阻塞模式进行连接 this.clientSocket.Blocking = true; // 异步连接 // BeginConnect - 开始异步连接 // AsyncWaitHandle.WaitOne - 等待连接完成,最多等待10秒 IAsyncResult result = this.clientSocket.BeginConnect(this.address, null, null); bool success = result.AsyncWaitHandle.WaitOne(NetConnectTimeout); if (success) {  // EndConnect - 完成异步连接  this.clientSocket.EndConnect(result);  // 连接成功后设置为非阻塞模式  this.clientSocket.Blocking = false;  RaiseConnected(0, \"Success\"); } } catch (SocketException ex) { // SocketException - .NET框架异常类,表示Socket操作异常 if (ex.SocketErrorCode == SocketError.ConnectionRefused) {  CloseConnection(NET_ERROR_FAIL_TO_CONNECT); } } catch (Exception e) { Debug.Log(\"DoConnect Exception:\" + e.ToString()); } this.connecting = false; } ///  /// 检查连接状态,如果断开则尝试重连 ///   bool KeepConnect() { if (this.connecting) return false; if (this.address == null) return false; if (this.Connected) return true; // 重连逻辑 if (this.retryTimes < this.retryTimesTotal) { this.Connect(); } return false; } ///  /// 连接状态属性 ///   public bool Connected { get { // 检查Socket是否存在且已连接 return (clientSocket != default(Socket)) ? clientSocket.Connected : false; } } ///  /// 发送消息到服务器 ///   public void SendMessage(NetMessage message) { if (!running) return; if (!this.Connected) { // 如果未连接,先连接再发送 this.Connect(); return; } // 将消息加入发送队列 sendQueue.Enqueue(message); } ///  /// 处理数据接收 ///   bool ProcessRecv() { try { // Poll - 检查Socket状态 // SelectMode.SelectError - 检查是否有错误 bool error = this.clientSocket.Poll(0, SelectMode.SelectError); if (error) {  CloseConnection(NET_ERROR_SEND_EXCEPTION);  return false; } // SelectMode.SelectRead - 检查是否有数据可读 bool ret = this.clientSocket.Poll(0, SelectMode.SelectRead); if (ret) {  // Receive - 接收数据到缓冲区  int n = this.clientSocket.Receive(this.receiveBuffer.GetBuffer(), 0, this.receiveBuffer.Capacity, SocketFlags.None);  if (n <= 0)  { CloseConnection(NET_ERROR_ZERO_BYTE); return false;  }  // 将接收到的数据交给包处理器处理  this.packageHandler.ReceiveData(this.receiveBuffer.GetBuffer(), 0, n); } } catch (Exception e) { Debug.LogError(\"ProcessReceive exception:\" + e.ToString()); CloseConnection(NET_ERROR_ILLEGAL_PACKAGE); return false; } return true; } ///  /// 处理数据发送 ///   bool ProcessSend() { try { bool error = this.clientSocket.Poll(0, SelectMode.SelectError); if (error) {  CloseConnection(NET_ERROR_SEND_EXCEPTION);  return false; } // SelectMode.SelectWrite - 检查是否可以发送数据 bool ret = this.clientSocket.Poll(0, SelectMode.SelectWrite); if (ret) {  // 发送缓冲区中的数据  if (this.sendBuffer.Position > this.sendOffset)  { int bufsize = (int)(this.sendBuffer.Position - this.sendOffset); int n = this.clientSocket.Send(this.sendBuffer.GetBuffer(), this.sendOffset, bufsize, SocketFlags.None); if (n = this.sendBuffer.Position) { this.sendOffset = 0; this.sendBuffer.Position = 0; this.sendQueue.Dequeue(); // 移除已发送的消息 }  }  else  { // 从发送队列中取出消息进行发送 if (this.sendQueue.Count > 0) { NetMessage message = this.sendQueue.Peek(); // PackMessage - 将消息打包成字节数组 byte[] package = PackageHandler.PackMessage(message); this.sendBuffer.Write(package, 0, package.Length); }  } } } catch (Exception e) { Debug.Log(\"ProcessSend exception:\" + e.ToString()); CloseConnection(NET_ERROR_SEND_EXCEPTION); return false; } return true; } ///  /// 处理消息分发 ///   void ProcessMessage() { MessageDistributer.Instance.Distribute(); } ///  /// Unity的Update方法,每帧调用 /// 在这里处理网络IO ///   public void Update() { if (!running) return; // 保持连接 if (this.KeepConnect()) { // 处理接收 if (this.ProcessRecv()) {  if (this.Connected)  { // 处理发送 this.ProcessSend(); // 处理消息分发 this.ProcessMessage();  } } } } ///  /// 关闭连接 ///   public void CloseConnection(int errCode) { this.connecting = false; if (this.clientSocket != null) { this.clientSocket.Close(); } // 清空缓冲区 MessageDistributer.Instance.Clear(); this.sendQueue.Clear(); this.receiveBuffer.Position = 0; this.sendBuffer.Position = sendOffset = 0; // 根据错误码进行不同处理 switch (errCode) { case NET_ERROR_FAIL_TO_CONNECT:  // 连接失败,可以在这里添加重连逻辑  break; default:  this.RaiseDisonnected(errCode);  break; } } ///  /// 触发连接成功事件 ///   protected virtual void RaiseConnected(int result, string reason) { OnConnect?.Invoke(result, reason); } ///  /// 触发断开连接事件 ///   public virtual void RaiseDisonnected(int result, string reason = \"\") { OnDisconnect?.Invoke(result, reason); } }}
使用了.NET框架提供的Socket类来实现TCP网络连接,创建了一个网络客户端类,它负责与服务器建立TCP连接、管理连接状态、处理数据的发送和接收。具体来说,我们用Socket类建立连接,用MemoryStream作为数据缓冲区来存储接收和发送的数据,用Queue来管理待发送的消息队列,还实现了自动重连机制,当检测到连接断开时会自动尝试重新连接。整个网络层本质上就是包装了.NET的Socket类,让它更适合游戏开发使用,提供了更友好的接口和错误处理。
我们使用ProtoBuf进行高效的消息序列化,并设计了自定义的包格式:
using System;using System.IO;using SkillBridge.Message; // 这是我们项目自定义的消息定义namespace Network{ ///  /// 包处理器 - 负责消息的序列化和反序列化 /// 使用ProtoBuf进行高效的数据序列化 ///   public class PackageHandler { // 内存流 - 用于存储接收到的原始数据 private MemoryStream stream = new MemoryStream(64 * 1024); private int readOffset = 0; // 读取偏移量 private T sender; // 消息发送者 public PackageHandler(T sender) { this.sender = sender; } ///  /// 接收原始数据到包处理器 ///   /// 原始字节数据 /// 数据偏移量 /// 数据长度 public void ReceiveData(byte[] data, int offset, int count) { // 检查缓冲区是否会溢出 if (stream.Position + count > stream.Capacity) { throw new Exception(\"PackageHandler write buffer overflow\"); } // 将数据写入内存流 stream.Write(data, offset, count); // 解析数据包 ParsePackage(); } ///  /// 打包消息 - 将NetMessage对象序列化为字节数组 ///   /// 要发送的消息 /// 打包后的字节数组 public static byte[] PackMessage(NetMessage message) { byte[] package = null; using (MemoryStream ms = new MemoryStream()) { // ProtoBuf序列化 - 将对象序列化为字节流 // Serializer.Serialize - ProtoBuf库的方法 ProtoBuf.Serializer.Serialize(ms, message); // 创建包含长度头的完整包 // 包格式:[4字节长度][ProtoBuf数据] package = new byte[ms.Length + 4]; // 写入4字节长度头 // BitConverter.GetBytes - .NET框架方法,将int转换为字节数组 Buffer.BlockCopy(BitConverter.GetBytes(ms.Length), 0, package, 0, 4); // 写入ProtoBuf数据 Buffer.BlockCopy(ms.GetBuffer(), 0, package, 4, (int)ms.Length); } return package; } ///  /// 解包消息 - 将字节数组反序列化为NetMessage对象 ///   /// 字节数组 /// 偏移量 /// 长度 /// 反序列化后的消息对象 public static NetMessage UnpackMessage(byte[] packet, int offset, int length) { NetMessage message = null; using (MemoryStream ms = new MemoryStream(packet, offset, length)) { // ProtoBuf反序列化 // Serializer.Deserialize - ProtoBuf库的方法 message = ProtoBuf.Serializer.Deserialize(ms); } return message; } ///  /// 解析数据包 - 处理TCP粘包问题 /// TCP是流式协议,可能会将多个包粘在一起 ///   /// 是否解析成功 bool ParsePackage() { // 检查是否有足够的数据读取长度头(4字节) if (readOffset + 4 < stream.Position) { // 读取包长度 int packageSize = BitConverter.ToInt32(stream.GetBuffer(), readOffset); // 检查是否有完整的包 if (packageSize + readOffset + 4 <= stream.Position) {  // 解包消息  NetMessage message = UnpackMessage(stream.GetBuffer(), this.readOffset + 4, packageSize);  if (message == null)  { throw new Exception(\"PackageHandler ParsePackage failed, invalid package\");  }  // 将消息交给消息分发器处理  MessageDistributer.Instance.ReceiveMessage(this.sender, message);  // 更新读取偏移量  this.readOffset += (packageSize + 4);  // 递归处理下一个包  return ParsePackage(); } } return false; } }}
使用了ProtoBuf这个第三方序列化库来处理数据的序列化和反序列化,设计了简单的数据包格式,每个包包含4字节的长度头加上ProtoBuf序列化后的数据。这个层主要负责将C#对象转换成可以在网络上传输的字节数组,以及将接收到的字节数组重新转换成C#对象。我们还处理了TCP协议特有的粘包问题,因为TCP是流式协议,可能会将多个数据包粘在一起传输,所以需要解析出每个完整的包。整个协议层就是利用ProtoBuf的高效序列化能力,加上自定义的包格式,实现了可靠的数据传输。
为什么需要这个长度头呢?粘包问题又如何解决?
长度头就是在每个数据包前面加上4个字节,用来表示后面数据的长度。因为TCP是流式协议,它只负责传输字节流,不关心数据的边界,如果不加长度头无法获取到消息的边界。
粘包就是多个数据包被TCP合并成一个包发送,或者一个数据包被TCP分割成多个包发送。当接收到数据时,首先检查是否有足够的数据读取4字节的长度头,如果有就读取长度值,然后检查是否有足够的数据构成完整的包(长度头+数据),如果有就提取出完整的包进行处理,并更新读取位置到下一个包的位置,然后递归处理剩余的数据。这样无论TCP如何合并或分割数据包,我们都能根据长度头准确识别出每个完整的消息边界,确保每个消息都能被完整接收和处理,不会出现数据丢失或消息混乱的问题。
实现了事件驱动的消息分发机制,支持异步多线程处理:
using System;using System.Collections.Generic;using System.Threading;using Common; // 这是我们项目的公共库namespace Network{ ///  /// 消息分发器 - 负责将消息分发到对应的业务处理器 /// 采用事件驱动模式,支持异步多线程处理 ///   public class MessageDistributer : Singleton<MessageDistributer> { ///  /// 消息参数类 - 包含发送者和消息内容 ///   class MessageArgs { public T sender;  // 消息发送者 public NetMessage message; // 消息内容 } // 消息队列 - 存储待处理的消息 private Queue messageQueue = new Queue(); // 消息处理器字典 - 存储不同类型消息的处理器 // Dictionary - .NET框架的字典类 // Delegate - .NET框架的委托类型,用于存储函数指针 private Dictionary messageHandlers = new Dictionary(); // 线程控制 private bool Running = false;  // 是否正在运行 private AutoResetEvent threadEvent; // 线程事件,用于线程同步 public int ThreadCount = 0; // 工作线程数量 public int ActiveThreadCount = 0; // 活跃线程数量 public bool ThrowException = false; // 是否抛出异常 ///  /// 消息处理器委托定义 ///   /// 消息类型 /// 发送者 /// 消息内容 public delegate void MessageHandler(T sender, Tm message); public MessageDistributer() { threadEvent = new AutoResetEvent(true); } ///  /// 订阅消息处理器 ///   /// 消息类型 /// 消息处理函数 public void Subscribe(MessageHandler messageHandler) { // 获取消息类型名称作为键 string type = typeof(Tm).Name; // 如果该类型还没有处理器,初始化为null if (!messageHandlers.ContainsKey(type)) { messageHandlers[type] = null; } // 将新的处理器添加到委托链中 // += 操作符用于组合委托 messageHandlers[type] = (MessageHandler)messageHandlers[type] + messageHandler; } ///  /// 取消订阅消息处理器 ///   /// 消息类型 /// 要取消的消息处理函数 public void Unsubscribe(MessageHandler messageHandler) { string type = typeof(Tm).Name; if (!messageHandlers.ContainsKey(type)) { messageHandlers[type] = null; } // 从委托链中移除处理器 // -= 操作符用于移除委托 messageHandlers[type] = (MessageHandler)messageHandlers[type] - messageHandler; } ///  /// 触发消息事件 ///   /// 消息类型 /// 发送者 /// 消息内容 public void RaiseEvent(T sender, Tm msg) { string key = msg.GetType().Name; if (messageHandlers.ContainsKey(key)) { // 获取对应的消息处理器 MessageHandler handler = (MessageHandler)messageHandlers[key]; if (handler != null) {  try  { // 调用消息处理器 handler(sender, msg);  }  catch (Exception ex)  { Log.ErrorFormat(\"Message handler exception:{0}, {1}, {2}, {3}\", ex.InnerException, ex.Message, ex.Source, ex.StackTrace); if (ThrowException) throw ex;  } } else {  Log.Warning(\"No handler subscribed for \" + msg.ToString()); } } } ///  /// 接收消息并加入队列 ///   /// 发送者 /// 消息内容 public void ReceiveMessage(T sender, NetMessage message) { // 将消息加入队列 this.messageQueue.Enqueue(new MessageArgs() { sender = sender, message = message }); // 设置事件,唤醒等待的线程 threadEvent.Set(); } ///  /// 清空消息队列 ///   public void Clear() { this.messageQueue.Clear(); } ///  /// 分发队列中的所有消息 /// 单线程模式,在Unity主线程中调用 ///   public void Distribute() { if (this.messageQueue.Count == 0) { return; } // 处理队列中的所有消息 while (this.messageQueue.Count > 0) { MessageArgs package = this.messageQueue.Dequeue(); // 分发请求消息 if (package.message.Request != null)  MessageDispatch.Instance.Dispatch(package.sender, package.message.Request); // 分发响应消息 if (package.message.Response != null)  MessageDispatch.Instance.Dispatch(package.sender, package.message.Response); } } ///  /// 启动消息处理器 - 多线程模式 ///   /// 工作线程数量 public void Start(int threadNum) { this.ThreadCount = threadNum; if (this.ThreadCount  1000) this.ThreadCount = 1000; Running = true; // 启动多个工作线程 for (int i = 0; i < this.ThreadCount; i++) { // ThreadPool.QueueUserWorkItem - .NET框架方法,在线程池中执行任务 ThreadPool.QueueUserWorkItem(new WaitCallback(MessageDistribute)); } // 等待所有线程启动 while (ActiveThreadCount < this.ThreadCount) { Thread.Sleep(100); } } ///  /// 停止消息处理器 ///   public void Stop() { Running = false; this.messageQueue.Clear(); // 唤醒所有等待的线程 while (ActiveThreadCount > 0) { threadEvent.Set(); } Thread.Sleep(100); } ///  /// 消息处理线程函数 ///   /// 线程参数(未使用) private void MessageDistribute(object stateInfo) { Log.Warning(\"MessageDistribute thread start\"); try { // 增加活跃线程计数 ActiveThreadCount = Interlocked.Increment(ref ActiveThreadCount); while (Running) {  if (this.messageQueue.Count == 0)  { // 等待新消息 threadEvent.WaitOne(); continue;  }  // 取出消息进行处理  MessageArgs package = this.messageQueue.Dequeue();  if (package.message.Request != null) MessageDispatch.Instance.Dispatch(package.sender, package.message.Request);  if (package.message.Response != null) MessageDispatch.Instance.Dispatch(package.sender, package.message.Response); } } catch { // 异常处理 } finally { // 减少活跃线程计数 ActiveThreadCount = Interlocked.Decrement(ref ActiveThreadCount); Log.Warning(\"MessageDistribute thread end\"); } } }}
用C#标准库实现了一个事件驱动的消息分发系统,它负责将接收到的消息分发到对应的业务处理函数。具体来说,我们用Dictionary来存储不同类型消息的处理器,用Queue来管理待处理的消息队列,用ThreadPool来实现多线程处理,用委托(Delegate)来实现事件驱动模式。当网络层接收到消息后,会交给消息分发器,分发器根据消息类型找到对应的处理器并调用它。整个应用层就是C#的事件系统加上多线程和队列,实现了高效的消息路由和处理,让业务逻辑与网络通信完全解耦。
总的来说:

具体的实现中,我们在客户端和服务器端实现的内容不同:
客户端主要包含连接管理器、消息发送接收模块和消息处理器:连接管理器负责建立和维护与服务器的TCP连接,处理连接状态变化;消息发送接收模块负责将业务数据序列化为Protocol Buffers格式,添加消息头后发送,同时接收服务器消息并反序列化;消息处理器根据消息ID将消息分发到对应的业务处理函数。
服务器端实现包括连接监听器、会话管理器和消息分发器:连接监听器持续监听客户端连接请求,为每个连接创建独立的会话对象。会话管理器维护所有活跃连接的状态信息,包括用户身份、连接时间、最后活跃时间等。消息分发器接收客户端消息,根据消息类型路由到相应的业务逻辑处理器。
一般来说客户端会优先发起请求,这个请求会被protocol buffers协议进行序列化转变成二进制格式之后通过客户端和服务器的TCP连接进行传输,服务器接收到这个信息之后再反序列化得到相关的请求之后内部执行相关逻辑,最后返回响应;响应传输的过程和请求是一样的,客户端接收到响应之后再在本地执行相关逻辑。
以用户登录为例,UserService创建LoginRequest消息对象,调用NetClient.Instance.SendMessage()发送消息。NetClient使用protobuf-net将LoginRequest序列化为二进制格式,通过TCP连接发送到服务器。服务器端的NetConnection接收到数据后,PackageHandler解析数据包并反序列化为NetMessage对象,MessageDistributer将消息分发到UserHandler进行业务逻辑处理。UserHandler处理完成后创建LoginResponse消息,通过NetConnection.SendResponse()发送响应。客户端NetClient接收到响应数据,PackageHandler反序列化为NetMessage,MessageDistributer分发到UserService的对应处理方法,更新本地状态并通知UI层更新界面。
异步消息处理机制则是客户端和服务器都维护消息队列,接收到的消息先入队,然后由专门的线程池异步处理,这种设计避免了网络IO阻塞业务逻辑,保证了游戏主线程的流畅运行。MessageDistributer支持配置工作线程数量,默认使用8个线程处理消息,每个线程独立处理队列中的消息,提高了系统并发处理能力,能够同时处理多个客户端的请求而不会相互影响。消息处理采用事件驱动模式,每个消息类型都有对应的处理器函数,当消息到达时,系统根据消息类型查找对应的处理器并异步执行,这种设计使得业务逻辑与网络通信完全解耦,便于维护和扩展。
客户端通过Connected属性(调用底层Socket对象的Connected属性来判断连接是否活跃)监控连接状态,在Update方法中定期检查连接状态,当检测到连接断开时,自动启动重连流程,无需用户手动干预。项目实现了重连次数限制和重连间隔控制,避免频繁重连对服务器造成压力,通过指数退避算法逐步增加重连间隔时间,既保证了重连的成功率又避免了对服务器的过度负载。重连成功后,客户端会发送会话恢复请求,确保游戏状态的连续性和数据的一致性,实现断线重连。
Q:
这个项目中似乎涉及到了数据库,可以告诉我具体是怎么使用的吗?涉及到了哪些知识点?
A:
这个项目使用了完整的Microsoft数据库技术栈,包括SQL Server作为数据库服务器,Entity Framework 6作为ORM框架,以及LINQ作为查询语言。项目通过App.config配置文件管理数据库连接字符串,连接到特定名称的SQL Server数据库。
项目采用了完整的MMORPG数据库架构,包含多个核心表:Users表存储用户账户信息,Players表作为用户和角色之间的中间表,Characters表存储角色详细信息,CharacterItems表管理角色物品,TCharacterBags表存储背包数据,CharacterQuests表记录任务状态,TCharacterFriends表管理好友关系,TGuilds表存储公会信息,TGuildMembers表管理公会成员,TSkills表记录角色技能。这些表通过外键约束建立了完整的数据关系,确保了数据的一致性和完整性。
Entity Framework 6是微软的ORM框架,将数据库表映射为.NET对象,让开发者用面向对象方式操作数据库。在这个项目中,通过Entities.edmx定义数据模型,自动生成ExtremeWorldEntities上下文类和TUser、TCharacter等实体类。无需编写SQL,直接操作.NET对象完成数据库操作,Entity Framework自动转换为SQL语句并处理数据同步。
LINQ是.NET的统一查询语言,提供一致的方式查询各种数据源。这个项目中主要用于查询Entity Framework实体集合,支持方法语法和查询语法。LINQ具有延迟执行特性,查询链在需要结果时才执行,Entity Framework会将LINQ查询转换为SQL语句。LINQ提供强类型查询,编译器能在编译时检查语法正确性,提高代码可靠性。
Q:
如何理解这个项目采用模块化架构设计,构建事件驱动的Manager-Service分层系统?
A:
模块化架构设计指各个功能模块之间尽可能避免直接依赖和引用关系,每个模块都有明确的职责边界,通过接口或事件进行通信。
这个项目中的事件驱动的Manager-Service分层架构指的是Manager层负责游戏逻辑管理和状态维护,包括角色管理、实体管理、数据管理等核心功能,采用单例模式提供全局访问点;Service层负责网络通信和业务逻辑处理,包括用户服务、战斗服务、聊天服务等,订阅网络消息并处理具体的业务逻辑;两层之间通过UnityAction事件实现通信机制,Service层处理完网络请求和业务逻辑后触发相应事件,Manager层订阅这些事件来更新本地状态和游戏对象,这种事件驱动设计实现了网络层和游戏逻辑层的完全解耦,提高了系统的响应性、可维护性和扩展性。
Q:
介绍对象池模式,为何可以优化内存?
A:
对象池模式是一种内存管理设计模式,通过预先创建一定数量的对象并存储在池中,当需要使用时从池中获取,使用完毕后归还到池中而不是销毁,从而避免频繁的对象创建和销毁操作。
频繁创建和销毁对象会产生大量垃圾,触发垃圾回收器频繁工作,导致游戏卡顿。对象池模式通过复用对象,大大减少了垃圾产生,降低了垃圾回收的频率和压力;对象创建需要分配内存空间,这个过程相对耗时。对象池中的对象已经分配好内存,获取时只需要重置状态,比重新分配内存快得多。本质上来说,对象池是一种以空间换时间。
Q:
讲一下这个技能系统是怎么设计的吧。
A:
首先玩家在客户端通过UI点击或按键触发技能释放逻辑,客户端会进行第一次技能检验,包括检查MP是否足够、距离是否在施法范围内、技能是否在冷却中以及目标类型验证(区分目标技能和位置技能),如果验证通过客户端就发送包含技能ID和目标信息的网络请求到服务器。服务器接收到请求后会从数据库获取该技能的详细配置信息,然后进行第二次技能检验,这次检验主要是为了防作弊和确保状态同步,因为客户端可能被修改、服务器与客户端状态可能不同步、网络延迟可能导致状态变化,或者多人环境中其他玩家可能影响目标状态。验证通过后服务器负责所有逻辑计算和数值处理,因为这是一个状态同步的项目,所有核心逻辑都在服务器端执行。计算完成后服务器将结果通过消息分发器发送给所有相关客户端,实现多人游戏的同步。最后客户端接收到服务器消息后负责视觉效果、音效播放、UI更新等表现层内容的实现,这样就形成了服务器负责逻辑计算、客户端负责视觉表现的完整架构。
Q:
讲一下背包类怎么实现
A:
背包类的实现是这样的:首先定义一个背包类作为容器,它包含一个背包插槽数组来存储所有的物品格子,每个插槽都是一个独立的类,包含物品图标、数量文本、物品名称、是否占用状态等属性。背包类会维护背包容量、当前可用插槽索引等基本信息,并提供添加物品、移除物品、查找空插槽、查找可堆叠插槽等核心方法。当需要添加物品时,背包会先查找空插槽,如果没有空插槽就查找可以堆叠的同类物品,如果都没有就返回背包已满。当玩家进行拖拽操作时,背包会通过插槽的OnDrop事件来处理物品位置交换,如果是右键点击插槽,会显示物品操作菜单让玩家选择装备、使用或丢弃等操作。每次物品变化时,背包都会更新本地数据并发送网络请求同步到服务器,同时触发相关事件来更新UI显示和角色属性。
Q:
任务系统怎么实现的?
A:
首先在数据库中存储任务配置表,包含任务ID、任务名称、任务描述、任务类型、任务目标、奖励物品等信息。当玩家进入游戏时,客户端会发送请求获取该角色的任务列表,服务器从数据库查询玩家已接受的任务和可接受的任务,返回给客户端显示在任务UI界面中。当玩家点击接受任务时,客户端发送任务接受请求到服务器,服务器验证任务接受条件(如等级要求、前置任务等),如果满足条件就在数据库中创建任务记录,更新玩家任务状态并返回响应。当玩家完成任务目标时(如击杀怪物、收集物品等),客户端发送任务进度更新请求,服务器验证任务完成条件,如果完成就更新任务状态为已完成,计算并发放任务奖励(经验、金币、物品等),同时更新玩家属性数据,最后返回新的任务状态和奖励信息给客户端,客户端更新任务UI显示和角色属性界面。整个任务系统通过这种设计实现了任务的接受、进度跟踪、完成验证和奖励发放的完整流程。
Q:
什么是公会系统?这个项目中公会系统是怎么实现的?
A:
公会系统是MMORPG中的一种社交功能,允许玩家组成一个固定的团队组织。简单来说,公会就是游戏中的\"帮派\"或\"工会\",玩家可以创建或加入公会,在公会中与其他成员进行交流、协作、共同参与游戏活动。公会通常有会长、副会长、普通成员等不同职位,会长拥有管理公会的权限,比如可以邀请新成员、踢出成员、设置公会公告等。公会成员之间可以聊天、组队、分享资源,有些游戏还会提供公会专属的副本、活动或奖励。公会系统增强了游戏的社交性,让玩家能够建立稳定的游戏社交圈,形成长期的游戏合作关系。
怎么实现的话,首先在数据库中存储公会相关的配置表,包含公会ID、公会名称、会长信息、成员列表、申请记录等数据。当玩家想要创建公会时,客户端会发送创建公会请求到服务器,服务器验证公会名称是否已存在,如果不存在就在数据库中创建新的公会记录,将创建者设为会长并返回创建结果。当玩家想要加入公会时,客户端发送加入申请请求,服务器创建申请记录并通知公会会长,会长可以选择接受或拒绝申请,服务器根据会长的选择更新数据库中的成员信息。公会系统还包含成员管理功能,会长可以踢出成员、提升职位、转让会长等操作。整个公会系统通过这种设计实现了公会的创建、成员管理、权限控制、实时通知等功能,所有核心逻辑都在服务器端执行,客户端负责用户交互和界面显示,数据库负责数据的持久化存储。
Q:
请问在这个项目中哪里会导致内存泄漏呢?如何避免内存泄漏的问题呢?
A:
一些常见的导致内存泄漏的情况比如我们的事件/委托注册后忘记注销,导致对象无法被GC回收;Socket连接忘记关闭或Dispose;长时间没有释放缓存内存;全局单例持有临时对象的引用等;
如何避免的话,可以使用内存分析工具(如dotMemory、Unity Profiler)定期检测内存泄漏。
Q:
请问这个项目中如何保证网络通信的可靠性?是否有实现性能优化?
A:
首先这个项目网络通信的底层传输层协议是TCP——这本身就是一个面向连接的可靠协议,TCP本身包含重传机制;然后我们还添加了心跳包和Socket监听等方法来实时检测网络连接状态,并实现了断线重连功能,重连时通过Token机制恢复会话,保证数据一致性。
性能优化方面,首先我们的网络通信是异步消息处理:通过消息队列+多线程分发,避免主线程阻塞,提高通信效率;我们还在ProtoBuf协议对消息的格式定义的基础上添加了长度头,相当于人为的划分了消息边界,避免粘包问题以提高网络吞吐量;我们还可以批量消息合并:对频繁小消息进行合并,减少网络包数量;对象池复用:消息对象、临时数据等采用对象池,减少GC压力。
Q:
为了保证游戏体验的流畅性,你还做了哪些其他的优化?例如,你是如何减少网络延迟的?或者如何处理丢包的情况?
A:
针对高延迟有专门的办法:如果是帧同步我们可以采取预测回滚,如果是状态同步我们可以采取延迟补偿。

Q:
假设你需要设计一个排行榜系统,该系统需要存储大量玩家的排名信息,并能够快速查询Top N的玩家。你会选择什么数据结构和算法来实现这个功能?
A:
跳表或者小根堆都可以。

Q:
你能详细地描述一下这个项目中你是如何实现断线重连的吗?例如,你使用了什么机制来检测断线,以及如何保证重连后数据的完整性和一致性?
A:
在我们的MMORPG项目中,断线重连功能主要包括断线检测和断线后的重连处理。首先,断线检测采用了两种方式:一是客户端和服务器会在特定的时间间隔内互相发送心跳包,通过心跳包的收发情况来判断连接是否正常;二是实时监听Socket的状态,通过捕获连接重置、超时、发送或接收失败等异常,及时发现网络断开。当检测到断线后,客户端会自动发起重连尝试,通常会设置最大重试次数和重试间隔,避免频繁重连对服务器造成压力。如果在规定次数内重连成功,客户端会携带上一次会话时服务器分配的Token(SessionID)进行重连,服务器根据Token校验并恢复玩家的会话和状态(如角色位置、背包、任务进度等),并将最新数据同步给客户端,客户端据此刷新UI和场景,实现无缝恢复游戏体验;如果多次重连仍失败,则会提示用户网络异常或连接失败,放弃本次重连。
为了保证数据的完整性和一致性,对于交易等关键操作,客户端在发送请求时会附带一个唯一的操作ID,服务器收到后会先检查该ID是否已经处理过,已处理则直接返回结果,未处理则执行并记录,防止断线重连后重复执行。此外,关键数据操作在服务器端会用数据库事务包裹,确保要么全部成功要么全部失败,防止出现中间状态。客户端在断线期间如果未收到服务器确认的关键操作,重连后会自动重发,服务器根据操作ID判断是否需要再次执行。服务器还可以定期保存玩家状态快照,断线重连时可回滚到最近一次一致状态,进一步防止数据丢失。通过以上机制,我们有效保障了断线重连过程中的数据安全和玩家体验。
如果有人不知道Token和数据库事务包裹是什么的话:


Q:
你提到了使用数据库来存储玩家属性信息,并使用EF框架来进行数据库操作。既然你提到了数据库,我想进一步了解你在数据库设计和优化方面的经验。
A:
我们首先会根据游戏的业务需求,设计出合理的数据表结构。比如有玩家表(Player)、背包表(BagItem)、任务表(Task)、好友表(Friend)、公会表(Guild)等,每张表都用来存储一类数据。每张表都会有一个主键(比如玩家ID、物品记录ID等),用来唯一标识每一条数据,这样查找和管理数据都非常方便。表里的每一列就是一个字段,比如玩家表会有昵称、等级、金币等字段。
为了提升查询效率,我们会为高频查询的字段建立索引。索引就像书的目录,可以让数据库快速定位到需要的数据。常见的索引有主键索引(自动为主键字段建立)、联合索引(比如玩家ID+物品ID,适合经常联合查询的场景)、覆盖索引(查询用到的字段都在索引里,查询更快)。不过索引不能建太多,否则会影响写入和更新的速度,所以我们会根据实际业务需求合理设计。
在高并发和大数据量的场景下,我们还会采用分表分库(比如按区服或玩家ID范围分表),避免单表过大导致性能下降。对于关键操作(如交易、物品变更),我们会用数据库事务包裹,保证这些操作要么全部成功,要么全部失败,防止出现只执行一半的“中间状态”,确保数据一致性。
此外,我们还会结合Redis缓存来加速热点数据的访问。比如排行榜、在线玩家等高频数据会放到Redis里,查询时先查缓存,查不到再查数据库,这样可以大大减轻数据库压力,提升系统整体性能。
在开发模式上,EF支持Code First(代码优先)和Database First(数据库优先)两种方式。Code First适合新项目,先写代码类再自动生成数据库表;Database First适合已有数据库,先有表再生成代码。我们会根据项目实际情况选择合适的模式。
Q:
在异步消息处理方面,你们是如何避免主线程阻塞的?具体的消息分发流程是怎样的?
A:
我们采用了消息队列+线程池的架构来避免主线程阻塞。具体实现是主线程只负责接收网络消息并将其放入消息队列中,然后立即返回继续处理其他任务,而实际的消息处理工作则由线程池中的工作线程来完成。我们的消息分发流程是这样的:当网络层接收到消息后,首先将消息封装成Message对象并加入线程安全的队列,然后通过Task.Run()将消息处理任务提交给线程池,线程池会自动分配空闲的工作线程来执行消息的解析、业务逻辑处理和响应发送,这样主线程就不会被耗时的业务处理阻塞,可以继续处理新的网络消息和UI更新。我们还使用了事件驱动机制,让各个功能模块通过订阅相应的事件来实现解耦,比如技能释放、背包操作等都会触发对应的事件,工作线程处理完消息后会发布相应的事件,主线程或其他模块可以监听这些事件来更新UI或执行后续操作。
在我们的异步消息处理中,Task是.NET中处理异步操作的核心类型,它是对线程的更高层次抽象。当我们使用Task.Run()将消息处理任务提交给线程池时,Task会自动管理线程的生命周期,避免频繁创建和销毁线程的开销。Task还提供了丰富的功能,比如我们可以使用Task.ContinueWith()来链式处理多个相关任务,使用Task.WhenAll()来等待多个任务完成,或者使用async/await语法来编写更清晰的异步代码。在我们的消息处理流程中,当主线程接收到网络消息后,会通过Task.Run(() => ProcessMessage(message))将消息处理工作交给线程池,线程池会自动分配空闲的工作线程来执行具体的业务逻辑,处理完成后可以通过Task的返回值或回调机制来通知主线程更新UI或发送响应。Task还支持取消操作,我们可以通过CancellationToken来优雅地取消正在执行的任务,这在处理用户断线或服务器关闭时非常有用。
Q:
单例模式的Manager类如何实现?如何避免多线程环境下的单例安全问题?
A:
单例模式的Manager类主要通过双重检查锁定机制来实现,具体做法是定义一个私有的静态实例变量和私有的构造函数,然后提供一个公共的静态Instance属性来获取唯一实例。在Instance属性中,我们首先进行第一重检查来判断实例是否为空,如果为空则进入锁块,在锁块内进行第二重检查再次确认实例为空后才创建新实例,这样既保证了线程安全又避免了不必要的锁开销。第一重检查的作用是性能优化,避免每次获取实例都要加锁,而第二重检查的作用是确保在多个线程同时通过第一重检查后,只有一个线程能创建实例,防止重复创建。我们还使用了volatile关键字来确保实例变量的可见性,防止编译器优化导致的问题。对于更重要的Manager类,我们还采用了Lazy模式,它内部已经实现了线程安全的延迟初始化,使用起来更加简洁和安全,只需要定义Lazy类型的静态字段,然后在Instance属性中返回_lazyInstance.Value即可,这种方式在C#中是实现单例模式的最佳实践。
Q:
如何保证账号安全?
A:
账号安全主要通过密码加密存储、登录限流防爆破、Token机制和加密传输等手段实现。用户登录后服务器会下发Token,后续用Token验证身份,避免频繁用密码登录,降低被会话劫持风险。
这里涉及到两个问题:Token为什么能防止会话被劫持?Session(会话)为什么可以劫持网络通信?
Token机制是通过在用户登录后生成一个唯一的令牌,客户端后续请求都带上这个Token,服务器通过验证Token来识别用户身份,这样可以避免每次都用账号密码登录,降低被劫持的风险。Session(会话)则是在服务器端保存用户的登录状态和部分数据,客户端只需传递SessionID或Token即可,减少了重复验证和数据传输,提升了通信效率。会话劫持是指攻击者盗取用户的SessionID或Token冒充用户操作,我们可以通过加密传输、绑定设备、定期刷新Token等方式来防止会话被劫持。
Q:
战斗系统和技能系统如何防止外挂和作弊?
A:
在我们的MMORPG项目中,战斗系统和技能系统采用了双重判定机制:第一次判定发生在客户端,主要用于判断技能是否可以释放,比如检测技能是否冷却结束、角色当前状态是否允许释放、目标是否在有效范围内以及资源(如魔法值)是否充足,这样可以让玩家操作更加流畅、及时反馈,减少延迟感;第二次判定发生在服务器端,服务器会对客户端发来的技能释放请求进行严格校验,包括再次检查技能冷却、角色状态、目标合法性、资源消耗等,并且会验证技能释放的时机和位置是否合理,只有全部通过后才会真正执行技能效果并同步给所有相关玩家。通过这种双重判定机制,既保证了玩家体验的流畅性,又能有效防止外挂和作弊,因为即使客户端被篡改或伪造技能释放请求,服务器端的严格校验可以拦截和拒绝所有不合法的操作,从而保障游戏的公平性和安全性。
Q:
聊天系统如何实现频道、私聊、公会聊天?如何防止刷屏和垃圾信息?
A:
在我们的MMORPG项目中,聊天系统主要通过消息队列和频道管理来实现不同类型的聊天功能。具体实现是服务器为每个聊天频道(如世界频道、公会频道、私聊等)维护独立的消息队列,当玩家发送消息时,客户端会将消息内容和目标频道信息发送到服务器,服务器根据消息类型进行分发:私聊消息只转发给指定的目标玩家,公会频道消息广播给该公会的所有在线成员,世界频道消息则广播给所有在线玩家。为了防止刷屏和垃圾信息,我们采用了多层防护机制:首先在客户端设置消息发送间隔限制,防止玩家快速连续发送消息;其次在服务器端实现消息频率检测,对短时间内发送过多消息的玩家进行警告或临时禁言;同时我们还建立了敏感词过滤系统,自动检测和屏蔽包含违规内容的消息;对于严重违规的玩家,系统会记录其行为并实施相应的处罚措施,如延长禁言时间或封号处理,从而维护良好的游戏聊天环境。
Q:
在数据库的设计方面有哪些优化的手段?
A:
在我们的MMORPG项目中,数据库优化主要通过多个层面来实现性能提升:首先在索引优化方面,我们为常用的查询字段如用户ID、物品ID、角色ID等建立了合适的索引,避免全表扫描,大幅提升查询效率;其次采用读写分离架构,主数据库负责写操作,从数据库负责读操作,通过负载均衡分散数据库压力,提升并发处理能力;同时我们还使用Redis等缓存系统对热点数据如排行榜、在线玩家信息、公会数据等进行缓存,减少对数据库的直接访问;对于大数据量的表,我们实施了分表分库策略,按照用户ID或时间等规则将大表拆分为多个小表,分布到不同的数据库实例中,提升查询和写入性能;在SQL优化方面,我们避免使用SELECT 等全字段查询,合理使用分页查询和批量操作,减少数据传输量;此外我们还定期进行数据库维护,如清理过期数据、优化表结构、更新统计信息等,确保数据库始终保持最佳性能状态。
Q:
DrawCall优化具体做了哪些?合批、图集、对象池等技术如何落地?
A:
首先,我们将场景中大量使用的UI元素和角色、怪物等资源进行图集(Atlas)打包,把多个小图片合成一张大图,减少材质切换次数,从而大幅降低DrawCall数量;其次,利用合批渲染技术,将同一材质、同一渲染状态的对象批量提交给GPU一次性渲染,避免频繁的渲染指令调用;此外,对于频繁生成和销毁的游戏对象(如子弹、特效、掉落物等),我们通过对象池技术进行复用,避免频繁创建和销毁带来的性能损耗,同时也减少了DrawCall的波动。通过这些优化手段,我们有效提升了渲染效率,保证了大场景和高并发下的流畅画面表现。


