繼續之前的內容:
戰斗系統
無需多言,整個項目中最復雜的部分,也是代碼量最大的部分。
屬性系統
首先我們要定義一系列屬性,畢竟所謂的戰斗就是不斷地扣血對吧。
屬性系統是戰斗系統的核心模塊,負責管理角色的所有屬性數據,包括初始屬性、成長屬性、裝備加成和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 處理退出競技場邏輯,加載回原場景。