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];/// <summary>/// 最大生命/// </summary>public float MaxHP { get { return Data[(int)AttributeType.MaxHP]; } set { Data[(int)AttributeType.MaxHP] = value; } }///<summary>/// 最大法力///</summary>public float MaxMP { get { return Data[(int)AttributeType.MaxMP]; } set { Data[(int)AttributeType.MaxMP] = value; } }///<summary>/// 力量///</summary>public float STR { get { return Data[(int)AttributeType.STR]; } set { Data[(int)AttributeType.STR] = value; } }///<summary>/// 智力///</summary>public float INT { get { return Data[(int)AttributeType.INT]; } set { Data[(int)AttributeType.INT] = value; } }///<summary>/// 敏捷///</summary>public float DEX { get { return Data[(int)AttributeType.DEX]; } set { Data[(int)AttributeType.DEX] = value; } }///<summary>/// 物理攻击///</summary>public float AD { get { return Data[(int)AttributeType.AD]; } set { Data[(int)AttributeType.AD] = value; } }///<summary>/// 魔法攻击///</summary>public float AP { get { return Data[(int)AttributeType.AP]; } set { Data[(int)AttributeType.AP] = value; } }///<summary>/// 物理防御///</summary>public float DEF { get { return Data[(int)AttributeType.DEF]; } set { Data[(int)AttributeType.DEF] = value; } }///<summary>/// 魔法防御///</summary>public float MDEF { get { return Data[(int)AttributeType.MDEF]; } set { Data[(int)AttributeType.MDEF] = value; } }///<summary>/// 攻击速度///</summary>public float SPD { get { return Data[(int)AttributeType.SPD]; } set { Data[(int)AttributeType.SPD] = value; } }///<summary>/// 暴击概率///</summary>public float CRI { get { return Data[(int)AttributeType.CRI]; } set { Data[(int)AttributeType.CRI] = value; } }
}
属性计算流程
- 初始属性加载 :通过 LoadInitAttribute 方法从角色定义中加载基础属性
- 成长属性加载 :通过 LoadGrowthAttribute 方法加载成长系数
- 装备属性加载 :通过 LoadEquipAttribute 方法汇总所有装备的属性加成
- 基础属性计算 :结合初始属性、成长属性和装备属性计算基础属性值
- 二级属性计算 :根据基础属性计算出生命值、攻击力等战斗属性
- 最终属性计算 :叠加Buff效果得到最终属性值
///<summary>
/// 初始化角色属性
///</summary>
public void Init(CharacterDefine define, int level,List<EquipDefine> 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;}
}///<summary>
/// 计算基础属性
///</summary>
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];}
}///<summary>
/// 计算二级属性
///</summary>
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加成
///<summary>
/// 计算基础属性
///</summary>
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<Buff> Buffs = new List<Buff>();// ... 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 < Buffs.Count; i++){if (!this.Buffs[i].Stoped){this.Buffs[i].Update();}}this.Buffs.RemoveAll((b) => b.Stoped);}
}
// ... existing code ...
EffectManager类
EffectManager 类负责管理 Buff 的效果,维护了一个效果字典,记录了每种效果的数量。它提供了添加、移除和检查效果的方法。
// ... existing code ...
class EffectManager
{private Creature Owner;Dictionary<BuffEffect, int> Effects = new Dictionary<BuffEffect, int>();// ... 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<Bullet> 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<Skill> 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);
{// 通知AIagent.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<ArenaService>, IDisposable{public void Init(){}public ArenaService(){MessageDistributer.Instance.Subscribe<ArenaBeginResponse>(this.OnArenaBegin);MessageDistributer.Instance.Subscribe<ArenaChallengeResponse>(this.OnArenaChallengeResponse);MessageDistributer.Instance.Subscribe<ArenaEndResponse>(this.OnArenaEnd);MessageDistributer.Instance.Subscribe<ArenaChallengeRequest>(this.OnArenaChallengeRequest);MessageDistributer.Instance.Subscribe<ArenaReadyResponse>(this.OnArenaReady);MessageDistributer.Instance.Subscribe<ArenaRoundStartResponse>(this.OnArenaRoundStart);MessageDistributer.Instance.Subscribe<ArenaRoundEndResponse>(this.OnArenaRoundEnd);}public void Dispose(){MessageDistributer.Instance.Unsubscribe<ArenaBeginResponse>(this.OnArenaBegin);MessageDistributer.Instance.Unsubscribe<ArenaChallengeResponse>(this.OnArenaChallengeResponse);MessageDistributer.Instance.Unsubscribe<ArenaEndResponse>(this.OnArenaEnd);MessageDistributer.Instance.Unsubscribe<ArenaChallengeRequest>(this.OnArenaChallengeRequest);MessageDistributer.Instance.Unsubscribe<ArenaReadyResponse>(this.OnArenaReady);MessageDistributer.Instance.Unsubscribe<ArenaRoundStartResponse>(this.OnArenaRoundStart);MessageDistributer.Instance.Unsubscribe<ArenaRoundEndResponse>(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);}/// <summary>/// 发起挑战/// </summary>/// <param name="targetId"></param>/// <param name="name"></param>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, "对方拒绝挑战");}}/// <summary>/// 发起挑战的响应/// </summary>/// <param name="sender"></param>/// <param name="message"></param>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<ArenaManager>{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<NetSession> Red;public NetConnection<NetSession> 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<NetSession> red, NetConnection<NetSession> 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 < 0){if (this.Round >= 3){ArenaResult();}else{NextRound();}}}}private void ArenaResult(){this.ArenaStatus = ArenaStatus.Result;//执行结算}private Map PlayerLeaveMap(NetConnection<NetSession> 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<ArenaService>{public ArenaService(){MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<ArenaChallengeRequest>(this.OnArenaChallengeRequest);MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<ArenaChallengeResponse>(this.OnArenaChallengeResponse);MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<ArenaReadyRequest>(this.OnArenaReady);}public void Dispose(){MessageDistributer<NetConnection<NetSession>>.Instance.Unsubscribe<ArenaChallengeRequest>(this.OnArenaChallengeRequest);MessageDistributer<NetConnection<NetSession>>.Instance.Unsubscribe<ArenaChallengeResponse>(this.OnArenaChallengeResponse);MessageDistributer<NetConnection<NetSession>>.Instance.Unsubscribe<ArenaReadyRequest>(this.OnArenaReady);}public void Init(){ArenaManager.Instance.Init();}private void OnArenaChallengeRequest(NetConnection<NetSession> 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<NetSession> 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<NetSession> 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<NetSession> 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 处理退出竞技场逻辑,加载回原场景
游戏优化
最后的最后我们来聊聊游戏的优化吧:
AOI(Area Of Interest)
资源优化
CPU优化
GPU优化
内存优化
多线程和线程安全
未完待续。