继续之前的内容:
战斗系统
无需多言,整个项目中最复杂的部分,也是代码量最大的部分。
属性系统
首先我们要定义一系列属性,毕竟所谓的战斗就是不断地扣血对吧。
属性系统是战斗系统的核心模块,负责管理角色的所有属性数据,包括初始属性、成长属性、装备加成和Buff效果,并通过多阶段计算得出最终属性值。系统支持属性实时更新,当角色等级提升、装备变化或Buff增减时,会自动重新计算并同步属性数据。
属性含义说明
-
MaxHP/MaxMP : 角色的最大生命值和法力值,决定角色的生存能力和技能释放能力
-
STR(力量) : 影响物理攻击和物理防御
-
INT(智力) : 影响魔法攻击和魔法防御
-
DEX(敏捷) : 影响攻击速度和暴击概率
-
AD(物理攻击) : 决定物理技能和普通攻击的伤害
-
AP(魔法攻击) : 决定魔法技能的伤害
-
DEF(物理防御) : 降低受到的物理伤害
-
MDEF(魔法防御) : 降低受到的魔法伤害
-
SPD(攻击速度) : 影响攻击间隔和技能施放速度
-
CRI(暴击概率) : 攻击产生暴击的几率
cs
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效果得到最终属性值
cs
///<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加成
cs
///<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 结束时移除这些效果。
cs
// ... 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。
cs
// ... 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 的效果,维护了一个效果字典,记录了每种效果的数量。它提供了添加、移除和检查效果的方法。
cs
// ... 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类
cs
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类
cs
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类
cs
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 类的代码,增强了系统的扩展性和灵活性。
cs
// ... 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
cs
// ... existing code ...
class AIMonsterPassive : AIBase
{
public const string ID = "AIMonsterPassive";
public AIMonsterPassive(Monster monster):base(monster)
{
}
}
// ... existing code ...
-
继承自 AIBase 类,没有添加额外的行为
-
当怪物定义中没有指定AI类型时,默认使用这种类型
-
遵循基类的战斗逻辑:尝试释放技能 -> 尝试普通攻击 -> 跟随目标
BOSS怪物AI
cs
// ... existing code ...
class AIBoss :AIBase
{
public const string ID = "AIBoss";
public AIBoss(Monster monster):base(monster)
{
}
}
// ... existing code ...
-
同样继承自 AIBase 类,目前没有添加额外的行为
-
专为BOSS怪物设计,可以在后续扩展中添加更复杂的行为逻辑
以下是敌人AI系统运作的简化示例代码:
cs
// 怪物创建
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 ,如项目中:
bash
"Name": "竞技场",
"Type": "Arena",
"SubType": "Arena",
"Resource": "Arena"
网络通信与消息定义
-
定义消息结构 :使用 Protocol Buffers 定义竞技场相关的消息,如项目中的 message.proto 包含:
-
ArenaChallengeRequest (挑战请求)
-
ArenaChallengeResponse (挑战响应)
-
ArenaReadyRequest (准备请求)
-
ArenaBeginResponse (开始响应)
-
ArenaRoundStartResponse (回合开始)
-
ArenaRoundEndResponse (回合结束)
-
ArenaEndResponse (结束响应)
-
消息分发 :通过 MessageDistributer 分发消息,如 MessageDispatch.cs 中处理各种竞技场消息
核心逻辑实现
客户端代码
ArenaService.cs
负责处理客户端与服务器之间的竞技场消息通信,包括订阅消息、发送挑战请求和响应等。
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更新。
cs
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
维护竞技场的核心逻辑,包括玩家进入、准备、战斗、结算等状态管理,以及回合计时、胜负判定等。
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
处理服务器端的竞技场消息,如挑战请求、响应、准备请求等,并负责创建竞技场实例、发送状态更新等。
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竞技场的工作流程主要分为以下几个阶段:
-
挑战发起 客户端通过 UIFriends.cs 中的UI逻辑发起竞技场挑战,调用 ArenaService.SendChallengeRequest 方法向服务器发送挑战请求。
-
挑战响应 服务器端 ArenaService 接收挑战请求,处理后向挑战双方发送响应。若被挑战方接受,进入下一步;若拒绝,则流程终止。
-
进入竞技场 接受挑战后,服务器通过 ArenaManager.CreateArena 创建竞技场实例,客户端通过 MapService 调用 SceneManager.Instance.LoadScene 加载竞技场场景( Arena.unity )。
-
准备阶段 客户端加载场景完成后, ArenaManager 处理进入竞技场逻辑, UIArena 显示倒计时。客户端发送 ArenaService.SendReadyRequest 表示准备就绪,服务器端 Arena 类中的 Update 方法计时准备阶段(通常几秒)。
-
战斗阶段 准备阶段结束后,服务器触发回合开始,向客户端发送 ArenaStart 消息,客户端 UIArena 更新UI显示战斗开始。双方玩家在竞技场中进行战斗,服务器通过 Battle 类管理战斗逻辑,同步双方状态。
-
回合结束 战斗持续一定时间或一方达到胜利条件后,服务器 Arena 类中的 UpdateRoundResult 方法计算回合结果,向客户端发送 RoundEnd 消息, UIArena 更新回合结果信息。
-
竞技场结束 达到设定的回合数或一方累计胜利次数满足条件后,服务器 Arena 类中的 UpdateArenaResult 方法判定最终胜负,向客户端发送 ArenaEnd 消息,客户端 ArenaManager 处理退出竞技场逻辑,加载回原场景。