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 處理退出競技場邏輯,加載回原場景。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處: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;但是還是有一…