继续之前的内容:

战斗系统

无需多言,整个项目中最复杂的部分,也是代码量最大的部分。

属性系统

首先我们要定义一系列属性,毕竟所谓的战斗就是不断地扣血对吧。

属性系统是战斗系统的核心模块,负责管理角色的所有属性数据,包括初始属性、成长属性、装备加成和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 处理退出竞技场逻辑,加载回原场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/913124.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/913124.shtml
英文地址,请注明出处:http://en.pswp.cn/news/913124.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux入门篇学习——Linux 帮助手册

目录 一、Linux 帮助手册 1.怎么打开帮助手册 2.安装依赖 3.使用手册查看命令 一、Linux 帮助手册 1.怎么打开帮助手册 打开 ubuntu &#xff0c;输入 man 命令打开帮助手册&#xff0c;直接在控制台输入 man 就可以了&#xff0c; man 手册一共有 9 页&#xff0c…

2025年后端主流框架对比和竞争格局及趋势发展

2025年的后端开发呈现出云原生主导、性能革命、AI深度融合的技术格局&#xff0c;主流框架在细分领域持续分化&#xff0c;新兴技术快速渗透关键场景。以下是基于行业实践与技术演进的深度解析&#xff1a; 一、主流框架竞争态势与核心能力 1. Java生态&#xff1a;企业级市场的…

bRPC简介

bRPC基础介绍。 什么是RPC? 互联网上的机器大都通过TCP/IP协议相互访问&#xff0c;但TCP/IP只是往远端发送了一段二进制数据&#xff0c;为了建立服务还有很多问题需要抽象&#xff1a; 数据以什么格式传输&#xff1f;不同机器间&#xff0c;网络间可能是不同的字节序&am…

力扣网C语言编程题:在数组中查找目标值位置之二分查找法

一. 简介 上一篇文章对力扣网上"有序数组中查找目标值范围"题目进行了普通的解法。文章如下&#xff1a; 力扣网C语言编程题&#xff1a;在数组中查找目标值位置之暴力解法-CSDN博客 本文使用二分查找法进行实现&#xff0c;因为二分查找法符合题目要求&#xff0…

前端查询条件加密传输方案(SM2加解密)

一、需求背景 控台项目甲方进行安全测试&#xff0c;测试报告其中一条&#xff1a;敏感信息明文传输 1 敏感信息明文传输 中危 查询接口传输手机号、银行卡号等敏感信息时未加密/脱敏处理。 二、解决方案 讨论出的方案是通过前端查询条件加密&#xff0c;后端对加密的…

【Python】Flask网页

Flask第三方库安装命令&#xff1a;pip install flask代码&#xff1a;from flask import Flask app Flask(__name__)app.route("/") def hello():return "Hello world!"if __name__ "__main__":app.run()其中的"Hello world!"可以改…

数字资产革命中的信任之锚:RWA法律架构的隐形密码

首席数据官高鹏团队律师创作&#xff0c;AI辅助 在数字经济的浪潮中&#xff0c;资产的边界正在被重新定义。当一块地产、一笔应收账款、甚至一份碳配额被转化为链上的数字代币时&#xff0c;技术的光芒固然耀眼&#xff0c;但真正决定其生命力的&#xff0c;是背后隐匿的“信…

mobaxterm终端sqlplus乱码问题解决

背景。使用mobaxterm终端连接linux。在查询数据库表注释时发现**&#xff1f;**中文乱码。影响对表的分析。完成以下三个编码设置再打开sqlplus查询含中文的数据就正常了 总结。需要查看sqlplus的编码是什么 SELECT parameter, value FROM nls_database_parameters WHERE pa…

一个简单的分布式追踪系统

1. 准备工作 导入必要的库 import contextvars import time from typing import Any, Optional, Dict, List, Union from dataclasses import dataclass, field2. 定义上下文变量 # 定义两个上下文变量&#xff0c;存储当前 Span 和 Trace _current_span: contextvars.Conte…

【Qt】事件处理、事件分发器、事件过滤器

事件处理 一. 事件事件处理鼠标事件处理按键事件处理定时器事件处理窗口事件处理 二. 事件分发器三. 事件过滤器 虽然 Qt 是跨平台的 C 开发框架&#xff0c;Qt 的很多能力其实是操作系统提供的&#xff0c;只不过 Qt 封装了系统 API&#xff0c;程序是运行在操作系统上的&…

广东省省考备考(第三十八天7.4)——言语理解:逻辑填空(题目训练)

错题解析 本题可从第二空入手&#xff0c;横线处搭配“理论”&#xff0c;且根据“使得”可知&#xff0c;横线处与前文构成因果关系&#xff0c;即“遗传学的空白和古生物证据的缺乏”导致他的理论在某些方面存在不足&#xff0c;A项“捉襟见肘”指拉一拉衣襟&#xff0c;就露…

5G网络切片技术

5G中的网络切片技术是一种通过虚拟化将单一物理网络划分为多个独立、可定制的虚拟网络的技术&#xff0c;旨在满足不同应用场景对网络性能、带宽、时延等需求的差异化要求。以下从技术原理、核心价值、应用场景、实现方式及未来趋势五个维度展开分析&#xff1a;一、技术原理&a…

算法学习笔记:7.Dijkstra 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题

在计算机科学领域&#xff0c;图论算法一直占据着重要地位&#xff0c;其中 Dijkstra 算法作为求解单源最短路径问题的经典算法&#xff0c;被广泛应用于路径规划、网络路由等多个场景。无论是算法竞赛、实际项目开发&#xff0c;还是计算机考研 408 的备考&#xff0c;Dijkstr…

汇编 函数调用栈

前言 网上很多对函数栈的解释&#xff0c;说的不是很清楚感觉&#xff0c;尤其是对到底是谁的栈&#xff0c;以及指令的微小但是很致命的细节没说&#xff0c;特写本文&#xff0c;一是帮助自己记忆&#xff0c;二是为了帮助大家&#xff0c;如有疏忽错误请指正。 核心概念 首先…

基于Apache MINA SSHD配置及应用

Apache MINA SSHD 是一个基于 Java 的 SSH 服务器和客户端实现&#xff0c;它是 Apache MINA 项目的一部分&#xff0c;提供了完整的 SSH 协议支持。 主要特性 SSH 协议支持&#xff1a; 支持 SSH2 协议 兼容大多数 SSH 客户端 支持多种加密算法和密钥交换方法 服务器功能…

Excel 如何让数据自动按要求排序或筛选?

让数据按要求排序和筛选是Excel数据处理的基础核心功能&#xff0c;也是进行有效分析前必做的准备工作。下面我们分开讲解这两个功能。 一、排序 (Sort)&#xff1a;让数据井井有条 排序的目的是重新排列数据行的顺序&#xff0c;以便更好地观察和比较。 1. 快速单列排序 (最…

Django 安装使用教程

一、Django 简介 Django 是一个高级 Python Web 框架&#xff0c;鼓励快速开发和简洁实用的设计。它内置 ORM、认证系统、后台管理、表单处理、路由控制等功能&#xff0c;广泛用于开发企业级网站、内容管理系统、电商平台等。 二、环境准备 2.1 安装 Python Django 基于 Py…

前沿交叉:Fluent与深度学习驱动的流体力学计算体系

基础模块 流体力学方程求解 1、不可压缩N-S方程数值解法&#xff08;有限差分/有限元/伪谱法&#xff09; Fluent工业级应用&#xff1a;稳态/瞬态流、两相流仿真&#xff08;圆柱绕流、入水问题&#xff09; Tecplot流场可视化与数据导出 2、CFD数据的AI预处理 基于P…

五、Flutter动画

目录1. Flutter 中动画的基本概念是什么&#xff1f;2. 解释 AnimationController 和 Tween 的作用3. 如何实现一个补间&#xff08;Tween&#xff09;动画&#xff1f;4. 什么是隐式动画&#xff1f;举例说明5. 如何实现自定义复杂动画&#xff1f;1. Flutter 中动画的基本概念…

全网唯一/Qt结合ffmpeg实现手机端采集摄像头推流到rtsp或rtmp/可切换前置后置摄像头/指定分辨率帧率

一、前言说明 之前已经实现了Qt结合ffmpeg在安卓上运行&#xff0c;所有在win上的功能&#xff0c;在安卓上都已经实现&#xff0c;比如编码保存到MP4文件&#xff0c;正常解码音视频文件播放等&#xff0c;唯独还差一个功能&#xff0c;尽管用的不多&#xff0c;但是还是有一…