战斗系统Part1
网游常见的战斗模式
客户端战斗:客户端执行核心逻辑,服务端验证逻辑,分为数据验证(属性验证)和战斗回放验证,两个人以上比较好验证。
服务器战斗:服务器执行核心逻辑,客户端处理表现,这样就不需要验证。
我们将要做服务器战斗。
所以每一个战斗都用一场BattleManager来管理,战斗管理可以分为战斗对象管理,战斗状态管理,战斗结算。
战斗数据分为基本属性,战斗属性,计算公式
单位战斗流程:
- 目标选择:主动选择,被动选择
- 进入战斗状态:攻击行为产生
- 战斗过程
- 战斗结束:目标死亡,脱离战斗
- 非战斗状态
战斗表现: - 输入模块:技能栏,BUFF栏,目标指示,战斗飘字
- 动作:攻击动作,受击动作
- 音效:攻击,受击
- 特效:技能特效,BUFF特效,受击特效
具体细节请查看策划文档。
属性类型:
基础属性:初始属性、成长属性
附加属性:装备属性等
动态属性:BUFF属性等
Character属性结构:
Attributes:
InitAttribute
BaseAttribute
EquipAttribute
BuffAttribute
FinalAttribute
框架
客户端:
- 表现层:InputManager、Amin、Effect、Audio、BattleUI、EntityController
- 逻辑层:Monster、Characeter、BuffManager、SkillManager
- 网络层:BattleService
服务器: - 逻辑层:AI、Buff、Skill、Monster、Character、BuffManager、SkillManager
- 网络层:BattleService
大型系统的实现原则
- 由低向上
- 由简及繁
- 由外而内
- 有整体到局部
- 线性流程
- 由不变到多变
战斗层次拆解
表现:特效、音效、UI
技能BUFF:技能逻辑、Buff逻辑
战斗流程:目标选择、发起战斗、执行战斗
战斗框架:战斗管理、单位管理
属性配置:配置表、数值存储、属性成长和同步
战斗流程拆解
角色成长:经验获取、等级成长、装备加成、最终属性运算、属性同步
发起战斗:目标选择、条件检查、发起战斗
技能释放:技能消耗、技能逻辑、技能表现
Buff效果:对属性影响、Buff逻辑、Buff表现
角色属性成长
建立基础数据
首先是建立属性面板,在Common项目中新建一个Battle文件夹,然后新建类如下
namespace Common.Battle
{
public enum AttributeType
{
None = -1,
MaxHP = 0,
MaxMP = 1,
STR = 2,
INT = 3,
DEX = 4,
AD = 5,
AP = 6,
DEF = 7,
MDEF = 8,
SPD = 9,
CRI = 10,
MAX
}
public enum SkillType
{
Normal = 0,
Skill = 1
}
public enum TargetType
{
None = 0,
Target = 1,
Self = 2,
Position
}
public enum BuffEffect
{
None = 0,
Stun = 1
}
}
using System;
using System.Collections.Generic;
using SkillBridge.Message;
using Common.Data;
namespace Common.Battle
{
public class Attributes
{
AttributeData Initial = new AttributeData();
AttributeData Growth = new AttributeData();
AttributeData Equip = new AttributeData();
AttributeData Basic = new AttributeData();
AttributeData Buff = new AttributeData();
public AttributeData Final = new AttributeData();
int Level;
private NAttributeDynamic dynamic;
public float HP
{
get { return dynamic.Hp; }
set { dynamic.Hp = (int)Math.Min(MaxHP, value); }
}
public float MP
{
get { return dynamic.Mp; }
set { dynamic.Mp = (int)Math.Min(MaxMP, value); }
}
public float MaxHP { get { return this.Final.MaxHP; } }
public float MaxMP { get { return this.Final.MaxMP; } }
public float STR { get { return this.Final.STR; } }
public float INT { get { return this.Final.INT; } }
public float DEX { get { return this.Final.DEX; } }
public float AD { get { return this.Final.AD; } }
public float AP { get { return this.Final.AP; } }
public float DEF { get { return this.Final.DEF; } }
public float MDEF { get { return this.Final.MDEF; } }
public float SPD { get { return this.Final.SPD; } }
public float CRI { get { return this.Final.CRI; } }
public void Init(CharacterDefine define,int level,List<EquipDefine> equips,NAttributeDynamic dynamicAttr)
{
this.dynamic = dynamicAttr;
this.LoadInitAttribute(this.Initial, define);
this.LoadGrowthAttribute(this.Growth, define);
this.LoadEquipAttributes(this.Equip, equips);
this.Level = level;
this.InitBasicAttributes();
this.InitSecondaryAttributes();
this.InitFinalAttributes();
this.HP = dynamicAttr.Hp;
this.MP = dynamicAttr.Mp;
}
private void LoadInitAttribute(AttributeData attr, CharacterDefine define)
{
attr.MaxHP = define.MaxHP;
attr.MaxMP = define.MaxMP;
attr.STR = define.STR;
attr.INT = define.INT;
attr.DEX = define.DEX;
attr.AD = define.AD;
attr.AP = define.AP;
attr.DEF = define.DEF;
attr.MDEF = define.MDEF;
attr.SPD = define.SPD;
attr.CRI = define.CRI;
}
private void LoadGrowthAttribute(AttributeData attr, CharacterDefine define)
{
attr.STR = define.GrowthSTR;
attr.INT = define.GrowthINT;
attr.DEX = define.GrowthDEX;
}
private void LoadEquipAttributes(AttributeData attr, List<EquipDefine> equips)
{
attr.Reset();
foreach (var define in equips)
{
attr.MaxHP += define.MaxHP;
attr.MaxMP += define.MaxMP;
attr.STR += define.STR;
attr.INT += define.INT;
attr.DEX += define.DEX;
attr.AD += define.AD;
attr.AP += define.AP;
attr.DEF += define.DEF;
attr.MDEF += define.MDEF;
attr.SPD += define.SPD;
attr.CRI += define.CRI;
}
}
private 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 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 + this.Equip.SPD;
this.Basic.CRI = this.Basic.DEX * 0.0002f + this.Initial.CRI + this.Equip.CRI;
}
private 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];
}
}
}
}
namespace Common.Battle
{
public class AttributeData
{
public float[] Data = new float[(int)AttributeType.MAX];
public float MaxHP { get { return Data[(int)AttributeType.MaxHP]; } set { Data[(int)AttributeType.MaxHP] = value; } }
public float MaxMP { get { return Data[(int)AttributeType.MaxMP]; } set { Data[(int)AttributeType.MaxMP] = value; } }
public float STR { get { return Data[(int)AttributeType.STR]; } set { Data[(int)AttributeType.STR] = value; } }
public float INT { get { return Data[(int)AttributeType.INT]; } set { Data[(int)AttributeType.INT] = value; } }
public float DEX { get { return Data[(int)AttributeType.DEX]; } set { Data[(int)AttributeType.DEX] = value; } }
public float AD { get { return Data[(int)AttributeType.AD]; } set { Data[(int)AttributeType.AD] = value; } }
public float AP { get { return Data[(int)AttributeType.AP]; } set { Data[(int)AttributeType.AP] = value; } }
public float DEF { get { return Data[(int)AttributeType.DEF]; } set { Data[(int)AttributeType.DEF] = value; } }
public float MDEF { get { return Data[(int)AttributeType.MDEF]; } set { Data[(int)AttributeType.MDEF] = value; } }
public float SPD { get { return Data[(int)AttributeType.SPD]; } set { Data[(int)AttributeType.SPD] = value; } }
public float CRI { get { return Data[(int)AttributeType.CRI]; } set { Data[(int)AttributeType.CRI] = value; } }
public void Reset()
{
for (int i = 0; i < (int)AttributeType.MAX; i++)
{
this.Data[i] = 0;
}
}
}
}
服务端
建立了一个GM指令系统,用来测试程序:
using System;
using System.Linq;
namespace GameServer
{
// GM指令
class CommandHelper
{
public static void Run()
{
bool run = true;
while (run)
{
Console.Write(">");
string line = Console.ReadLine().ToLower().Trim();
try
{
string[] cmd = line.Split(" ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
if (cmd.Count() == 0) continue;
switch (cmd[0])
{
case "addexp":
AddExp(int.Parse(cmd[1]),int.Parse(cmd[2]));
break;
case "exit":
run = false;
break;
default:
Help();
break;
}
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.ToString());
}
}
}
public static void AddExp(int characterId,int exp)
{
var cha = Managers.CharacterManager.Instance.GetCharacter(characterId);
if (cha == null)
{
Console.WriteLine("characterId {0} not existed", characterId);
return;
}
cha.AddExp(exp);
}
public static void Help()
{
Console.Write(@"
Help:
addexp <characterId> <exp> Add exp for character
exit Exit Game Server
help Show Help
");
}
}
}
Character新增:
public int Level
{
get { return this.Data.Level; }
private set
{
if (this.Data.Level == value)
return;
this.StatusManager.AddLevelUp((int)(value - this.Data.Level));
this.Data.Level = value;
}
}
public long Exp
{
get { return this.Data.Exp; }
private set
{
if (this.Data.Exp == value)
return;
this.StatusManager.AddExpChange((int)(value - this.Data.Exp));
this.Data.Exp = value;
}
}
public Character(CharacterType type,TCharacter cha):
base(new Core.Vector3Int(cha.MapPosX, cha.MapPosY, cha.MapPosZ),new Core.Vector3Int(100,0,0)) // 初始化血量蓝量
this.Info.attrDynamic = new NAttributeDynamic();
this.Info.attrDynamic.Hp = cha.HP;
this.Info.attrDynamic.Mp = cha.MP;
}
// 增加经验的方法,检查升级
internal void AddExp(int exp)
{
this.Exp += exp;
this.CheckLevelUp();
}
void CheckLevelUp()
{
long needExp = (long)Math.Pow(this.Level, 3) * 10 + this.Level * 40 + 50;
if (this.Exp >= needExp)
{
this.LevelUp();
//this.Exp -= needExp;
}
}
void LevelUp()
{
this.Level += 1;
Log.InfoFormat("Character[{0}:{1}] LevelUp:{2}", this.Id, this.Info.Name, this.Level);
CheckLevelUp();
}
客户端
Character新增:
public Attributes Attributes;
public Character(NCharacterInfo info) : base(info.Entity)
{
// 拉取服务器角色数据,对本地的角色进行数据的初始化
this.Info = info;
this.Define = DataManager.Instance.Characters[info.ConfigId];
this.Attributes = new Attributes();
var equips = EquipManager.Instance.GetEquipedDefines();
this.Attributes.Init(this.Define, this.Info.Level, equips, this.Info.attrDynamic);
}
UICharEquip新增:
void InitAttributes()
{
// 获取当前角色属性数据
var charattr = User.Instance.CurrentCharacter.Attributes;
// 更新血条和血条文本
this.hp.text = string.Format("{0}/{1}", charattr.HP, charattr.MaxHP);
this.mp.text = string.Format("{0}/{1}", charattr.MP, charattr.MaxMP);
this.hpBar.maxValue = charattr.MaxHP;
this.hpBar.value = charattr.HP;
this.mpBar.maxValue = charattr.MaxMP;
this.mpBar.value = charattr.MP;
// 更新UI文本
for (int i = (int)AttributeType.STR; i < (int)AttributeType.MAX; i++)
{
if (i == (int)AttributeType.CRI)
this.attrs[i - 2].text = string.Format("{0:f2}%", charattr.Final.Data[i] * 100);
else
this.attrs[i - 2].text = ((int)charattr.Final.Data[i]).ToString();
}
}
技能系统设计
技能元素拆分
- 释放:目标、范围、时间
- 条件:MP消耗、CD
- AOE:范围
- 持续:持续事件、间隔时间
- 行为:伤害、Buff
- 表现:名称、图标特效
Entity结构优化
Entity:
Creature:
NPC:
Monster:
Character: Player
Projectile:Trap
代码重构
服务端
using Common.Data;
using GameServer.Core;
using GameServer.Managers;
using SkillBridge.Message;
using System;
using System.Collections.Generic;
using GameServer.Battle;
namespace GameServer.Entities
{
class Creature : Entity
{
public int Id { get; set; }
public NCharacterInfo Info;
public string Name { get { return this.Info.Name; } }
public CharacterDefine Define;
public SkillManager SkillMgr;
public Creature(CharacterType type, int configId, int level, Vector3Int pos, Vector3Int dir) :
base(pos, dir)
{
this.Define = DataManager.Instance.Characters[configId];
this.Info = new NCharacterInfo();
this.Info.Type = type;
this.Info.Level = level;
this.Info.ConfigId = configId;
this.Info.Entity = this.EntityData;
this.Info.EntityId = this.entityId;
this.Info.Name = this.Define.Name;
this.InitSkills();
}
void InitSkills()
{
SkillMgr = new SkillManager(this);
this.Info.Skills.AddRange(this.SkillMgr.Infos);
}
}
}
using GameServer.Core;
using SkillBridge.Message;
namespace GameServer.Entities
{
class Monster : Creature
{
public Monster(int tid, int level, Vector3Int pos, Vector3Int dir) : base(CharacterType.Monster, tid, level, pos, dir)
{
}
}
}
客户端
Creature用于集中处理战斗方面的逻辑:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SkillBridge.Message;
using UnityEngine;
using Common.Data;
using Common.Battle;
using Managers;
using Battle;
namespace Entities
{
public class Creature : Entity
{
// 角色的网络信息和配置表信息
public NCharacterInfo Info;
public CharacterDefine Define;
public Attributes Attributes;
public SkillManager SkillMgr;
bool battleState = false;
public Skill CastringSkill = null;
public bool BattleStats
{
get { return battleState; }
set
{
if (battleState != value)
{
battleState = value;
this.SetStandby(value);
}
}
}
public int id
{
get { return this.Info.Id; }
}
public string Name
{
get
{
if (this.Info.Type == CharacterType.Player)
return this.Info.Name;
else
return this.Define.Name;
}
}
public bool IsPlayer
{
get { return this.Info.Type == CharacterType.Player; }
}
public bool IsCurrentPlayer
{
get
{
if (!IsPlayer) return false;
return this.Info.Id == Models.User.Instance.CurrentCharacterInfo.Id;
}
}
public Creature(NCharacterInfo info) : base(info.Entity)
{
// 拉取服务器角色数据,对本地的角色进行数据的初始化
this.Info = info;
this.Define = DataManager.Instance.Characters[info.ConfigId];
this.Attributes = new Attributes();
UpdateAttributes();
this.SkillMgr = new SkillManager(this);
}
// 默认没有装备,角色才需要实现这个虚函数
public virtual List<EquipDefine> GetEquips()
{
return null;
}
public void UpdateAttributes()
{
this.Attributes.Init(this.Define, this.Info.Level, this.GetEquips(), this.Info.attrDynamic);
}
// 下面是几种移动方法
public void MoveForward()
{
Debug.LogFormat("MoveForward");
this.speed = this.Define.Speed;
}
public void MoveBack()
{
Debug.LogFormat("MoveBack");
this.speed = -this.Define.Speed;
}
public void Stop()
{
Debug.LogFormat("Stop");
this.speed = 0;
}
public void SetDirection(Vector3Int direction)
{
//Debug.LogFormat("SetDirection:{0}", direction);
this.direction = direction;
}
public void SetPosition(Vector3Int position)
{
//Debug.LogFormat("SetPosition:{0}", position);
this.position = position;
}
public void CastSkill(int skillId,Creature target,NVector3 pos)
{
// 设置为战斗状态
this.SetStandby(true);
var skill = this.SkillMgr.GetSkill(skillId);
skill.BeginCast();
}
// 这里播放的是触发器类型的动画
public void PlayAnim(string name)
{
if (this.Controller != null)
this.Controller.PlayAnim(name);
}
// 播放进入战斗状态的动画
public void SetStandby(bool standby)
{
if (this.Controller != null)
this.Controller.SetStandby(standby);
}
public override void OnUpdate(float delta)
{
base.OnUpdate(delta);
this.SkillMgr.OnUpdate(delta);
}
}
}
using System;
using System.Collections.Generic;
using SkillBridge.Message;
using Common.Battle;
using Common.Data;
using Managers;
using UnityEngine;
namespace Entities
{
public class Character : Creature
{
public Character(NCharacterInfo info) : base(info)
{
}
public override List<EquipDefine> GetEquips()
{
return EquipManager.Instance.GetEquipedDefines();
}
}
}
本节任务说明
本节完成了战斗系统技能部分的转发和展示,可以释放技能特效并在其他客户端看到了,但是还没有技能的伤害逻辑。
服务器代码
Service层:
using SkillBridge.Message;
using Common;
using Common.Data;
using Network;
using GameServer.Entities;
using GameServer.Managers;
namespace GameServer.Services
{
class BattleService : Singleton<BattleService>
{
public BattleService()
{
MessageDistributer<NetConnection<NetSession>>.Instance.Subscribe<SkillCastRequest>(this.OnSkillCast);
}
public void Init()
{
ChatManager.Instance.Init();
}
private void OnSkillCast(NetConnection<NetSession> sender, SkillCastRequest request)
{
Character character = sender.Session.Character;
Log.InfoFormat("OnSkillCast: skill:{0} caster:{1} target:{2} position:{3}", request.castInfo.skillId, request.castInfo.casterId, request.castInfo.targetId, request.castInfo.Position);
sender.Session.Response.skillCast = new SkillCastResponse();
sender.Session.Response.skillCast.Result = Result.Success;
sender.Session.Response.skillCast.castInfo = request.castInfo;
// 这里只涉及了转发,但是还没在服务端实现技能逻辑
MapManager.Instance[character.Info.mapId].BroadcastBattleResponse(sender.Session.Response);
}
}
}
Map新增:
internal void BroadcastBattleResponse(NetMessageResponse response)
{
foreach (var kv in this.MapCharacters)
{
kv.Value.connection.Session.Response.skillCast = response.skillCast;
kv.Value.connection.SendResponse();
}
}
客户端代码
Service层:
using Entities;
using Managers;
using Network;
using SkillBridge.Message;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace Services
{
class BattleService : Singleton<BattleService>,IDisposable
{
public void Init()
{
}
public BattleService()
{
MessageDistributer.Instance.Subscribe<SkillCastResponse>(this.OnSkillCast);
}
public void Dispose()
{
MessageDistributer.Instance.Unsubscribe<SkillCastResponse>(this.OnSkillCast);
}
public void SendSkillCast(int skillId, int casterId, int targetId, NVector3 position)
{
if (position == null) position = new NVector3();
Debug.LogFormat("SendSkillCast: Skill:{0} caster:{1} target:{2} pos:{3}", skillId, casterId, targetId, position.ToString());
NetMessage message = new NetMessage();
message.Request = new NetMessageRequest();
message.Request.skillCast = new SkillCastRequest();
message.Request.skillCast.castInfo = new NSkillCastInfo();
message.Request.skillCast.castInfo.skillId = skillId;
message.Request.skillCast.castInfo.casterId = casterId;
message.Request.skillCast.castInfo.targetId = targetId;
message.Request.skillCast.castInfo.Position = position;
NetClient.Instance.SendMessage(message);
}
private void OnSkillCast(object sender, SkillCastResponse message)
{
Debug.LogFormat("OnSkillCast: Skill:{0} caster:{1} target:{2} pos:{3}", message.castInfo.skillId, message.castInfo.casterId, message.castInfo.targetId, message.castInfo.Position.ToString());
if (message.Result == Result.Success)
{
Creature caster = EntityManager.Instance.GetEntity(message.castInfo.casterId) as Creature;
if (caster != null)
{
Creature target = EntityManager.Instance.GetEntity(message.castInfo.targetId) as Creature;
caster.CastSkill(message.castInfo.skillId, target, message.castInfo.Position);
}
}
// 服务器出现异常的时候通知
else
ChatManager.Instance.AddSystemMessage(message.Errormsg);
}
}
}
Manager层:
using System;
using System.Collections.Generic;
using UnityEngine;
using Battle;
using Entities;
using SkillBridge.Message;
using Services;
namespace Managers
{
class BattleManager : Singleton<BattleManager>
{
private Creature currentTarget;
public Creature CurrentTarget
{
get { return currentTarget; }
set { this.SetTarget(value); }
}
private void SetTarget(Creature target)
{
this.currentTarget = target;
Debug.LogFormat("BattltManager.SetTarget:[{0} {1}]", target.entityId, target.Name);
}
private NVector3 currentPosition;
public NVector3 CurrentPosition
{
get { return currentPosition; }
set { this.SetPosition(value); }
}
private void SetPosition(NVector3 position)
{
this.currentPosition = position;
Debug.LogFormat("BattltManager.SetPosition:[{0}]", position);
}
public void CastSkill(Skill skill)
{
int target = currentTarget != null ? currentTarget.entityId : 0;
BattleService.Instance.SendSkillCast(skill.Define.ID, skill.Owner.entityId, target, currentPosition);
}
}
}
using System;
using System.Collections.Generic;
using Entities;
using UnityEngine;
namespace Battle
{
public class SkillManager
{
Creature Owner;
public List<Skill> Skills { get; private set; }
public SkillManager(Creature owner)
{
this.Owner = owner;
this.Skills = new List<Skill>();
this.InitSkills();
}
void InitSkills()
{
this.Skills.Clear();
foreach (var skillInfo in this.Owner.Info.Skills)
{
Skill skill = new Skill(skillInfo, this.Owner);
this.AddSkill(skill);
}
}
public void AddSkill(Skill skill)
{
this.Skills.Add(skill);
}
public Skill GetSkill(int skillId)
{
for (int i = 0; i < this.Skills.Count; i++)
{
if (this.Skills[i].Define.ID == skillId)
return this.Skills[i];
}
return null;
}
public void OnUpdate(float delta)
{
for (int i = 0; i < Skills.Count; i++)
{
this.Skills[i].OnUpdate(delta);
}
}
}
}
using System;
using System.Collections.Generic;
using Common.Data;
using Entities;
using SkillBridge.Message;
using Common.Battle;
using Managers;
using UnityEngine;
namespace Battle
{
public class Skill
{
public NSkillInfo Info;
public Creature Owner;
public SkillDefine Define;
private float cd;
public bool IsCasting = false;
private float castTime;
public float CD
{
get { return cd; }
}
public Skill(NSkillInfo info,Creature owner)
{
this.Info = info;
this.Owner = owner;
this.Define = DataManager.Instance.Skills[(int)this.Owner.Define.Class][this.Info.Id];
this.cd = 0;
}
public SkillResult CanCast(Creature target)
{
if (this.Define.CastTarget == TargetType.Target)
{
if (target == null || target == this.Owner)
return SkillResult.InvalidTarget;
}
if (this.Define.CastTarget == TargetType.Position && BattleManager.Instance.CurrentPosition == null)
return SkillResult.InvalidTarget;
if (this.Owner.Attributes.MP < this.Define.MPCost)
return SkillResult.OutOfMp;
if (this.CD > 0)
return SkillResult.CoolDown;
return SkillResult.Ok;
}
public void BeginCast()
{
this.IsCasting = true;
this.castTime = 0; // 蓄力时间
this.cd = this.Define.CD;
this.Owner.PlayAnim(this.Define.SkillAnim);
}
public void OnUpdate(float delta)
{
if (this.IsCasting) { }
UpdateCD(delta);
}
private void UpdateCD(float delta)
{
if (this.CD > 0)
this.cd -= delta;
if (this.CD < 0)
this.cd = 0;
}
}
}
UI层:
using Models;
using UnityEngine;
class UISkillSlots : MonoBehaviour
{
public UISkillSlot[] slots;
private void Start()
{
RefreshUI();
}
void RefreshUI()
{
var Skills = User.Instance.CurrentCharacter.SkillMgr.Skills;
for (int i = 0; i < Skills.Count; i++)
{
slots[i].SetSkill(Skills[i]);
}
// int skillIdx = 0;
// foreach (var kv in Skills)
// {
// slots[skillIdx].SetSkill(kv.Value);
// skillIdx++;
// }
}
}
using Models;
using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Battle;
using Common.Battle;
using SkillBridge.Message;
using Managers;
class UISkillSlot : MonoBehaviour,IPointerClickHandler
{
public Image icon;
public Image overlay;
public Text cdText;
Skill skill;
void Start()
{
overlay.enabled = false;
cdText.enabled = false;
}
void Update()
{
if (skill == null) return;
if (this.skill.CD > 0)
{
if(!overlay.enabled) overlay.enabled = true;
if (!cdText.enabled) cdText.enabled = true;
overlay.fillAmount = this.skill.CD / this.skill.Define.CD;
this.cdText.text = ((int)Math.Ceiling(this.skill.CD)).ToString();// 向上取整
}
else
{
if (overlay.enabled) overlay.enabled = false;
if (this.cdText.enabled) this.cdText.enabled = false;
}
}
// 释放技能的开始
public void OnPointerClick(PointerEventData eventData)
{
SkillResult result = this.skill.CanCast(BattleManager.Instance.CurrentTarget);
switch (result)
{
case SkillResult.InvalidTarget:
MessageBox.Show("技能[" + this.skill.Define.Name + "]目标无效");
return;
case SkillResult.OutOfMp:
MessageBox.Show("技能[" + this.skill.Define.Name + "]MP不足");
return;
case SkillResult.CoolDown:
MessageBox.Show("技能[" + this.skill.Define.Name + "]正在冷却");
return;
}
BattleManager.Instance.CastSkill(this.skill);
}
internal void SetSkill(Skill value)
{
this.skill = value;
if (this.icon != null) this.icon.overrideSprite = Resloader.Load<Sprite>(this.skill.Define.Icon);
}
}
调用流程
BattleManager唯一,SkillManager人手一个
主线
客户端(按键并判断):
BattleManager创建战斗条件
UISkillSlot.OnPointerClick -> CanCast(判断条件) -> BattleManager.CastSkill -> BattleService ->
服务端(广播所有人):
BattleService-> BattleManager.BroadcastBattleResponse ->
客服端(所有客户端对应的角色执行动画):
BattleService -> Creature.CastSkill -> Creature.SkillManager.GetSkill -> Skill.BeginCast -> Creature.PlayAnim
持续更新
EntityController.FixUpdate -> entity.OnUpdate -> Creature.OnUpdate -> Creature.SkillManager.OnUpdate -> Skill.OnUpdate -> UpdateCD
战斗系统Part2

子弹技能
分类:
真实命中:需要实现子弹的方向、速度、位置同步。
无真实命中:必定命中,客户端进行时间预测,命中时间同步,同步方案有预同步和实时同步。
完善技能行为
- 技能状态:无、蓄力,执行
- 释放类型:瞬发、非瞬发、吟唱、持续
- 伤害类型:单次、多次
- 伤害范围:单体、AOE
范围技能
Projector
// 激活Projector
TargetSelecter.ShowSelector(User.Instance.CurrentCharacter.position, this.skill.Define.CastRange, this.skill.Define.AOERange, OnPositionSelected);
using UnityEngine;
using System;
public class TargetSelecter : MonoSingleton<TargetSelecter>
{
Projector projector;
bool actived = false;
public Vector3 center;
private float range;
private float size;
Vector3 offset = new Vector3(0f, 2f, 0f);
protected Action<Vector3> selectPoint;
protected override void OnStart()
{
projector = this.GetComponentInChildren<Projector>();
projector.gameObject.SetActive(actived);
}
public void Active(bool active)
{
this.actived = active;
if (projector == null) return;
projector.gameObject.SetActive(this.actived);
// 半径大小
projector.orthographicSize = this.size * 0.5f;
}
void Update()
{
if (!actived) return;
if (this.projector == null) return;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if (Physics.Raycast(ray,out hitInfo, 100f,LayerMask.GetMask("Terrain"))) // 只对地面进行碰撞检测
{
Vector3 hitPoint = hitInfo.point;
Vector3 dist = hitPoint - this.center;
// 对光圈范围进行限制
if (dist.magnitude > this.range)
{
hitPoint = this.center + dist.normalized * this.range;
}
this.projector.gameObject.transform.position = hitPoint + offset;
if (Input.GetMouseButton(0))
{
// 调用事件
this.selectPoint(hitPoint);
this.Active(false);
}
if (Input.GetMouseButton(1))
{
this.Active(false);
}
}
}
// 提供给第三方使用
public static void ShowSelector(Vector3Int center, int range,int size,Action<Vector3> onPositionSelected)
{
if (TargetSelecter.Instance == null) return;
TargetSelecter.Instance.center = GameObjectTool.LogicToWorld(center);
TargetSelecter.Instance.range = GameObjectTool.LogicToWorld(range);
TargetSelecter.Instance.size = GameObjectTool.LogicToWorld(size);
TargetSelecter.Instance.selectPoint = onPositionSelected;
TargetSelecter.Instance.Active(true);
}
}
Buff系统
基础元素:
目标:自己、敌人、队友
触发类型:技能释放、技能命中
效果:眩晕、STATUS
持续:持续时间、间隔
行为:伤害、增益
表现:名称、图标、特效
战斗系统Part3
怪物AI
怪物AI在服务器实现,客户端值负责表现
服务端:
Monster.cs:
namespace GameServer.Entities
{
public class Monster : Creature
{
List<Skill> SkillsCanCast = new List<Skill>();
AIAgent AI;
private Vector3Int moveTarget;
Vector3 movePosition;
public Spawner Owner;
float deadTimer = GameDefine.MonsterDeadTime;
public BattleState BattleState; // 战斗状态
public CharacterState State; // 移动状态,Idle代表不能动,Move代表能动
public Monster(int tid, int level, Vector3Int pos, Vector3Int dir, Spawner owner) : base(CharacterType.Monster, tid, level, pos, dir)
{
this.AI = new AIAgent(this);
this.Attributes.Init(this.Define, this.Info.Level, this.GetEquips(), this.Info.attrDynamic); // 这里attrDynamic为空,默认满血满蓝
this.Owner = owner;
deadTimer = GameDefine.MonsterDeadTime;
}
// 常驻更新
public void OnUpdate()
{
if (startDeathCount)
{
deadTimer -= Time.deltaTime;
if(deadTimer <= 0)
{
this.Map.MonsterLeave(this);
IsDeath = true;
startDeathCount = false;
}
}
}
// 战斗状态更新(在Battle类的AllUnit中),战斗信息(技能,buff等放这里,否则消息发不出去
public override void Update()
{
if (startDeathCount || IsDeath) return;
base.Update();
this.UpdateMovement();
if (this.AI != null)
this.AI.Update();
}
public Skill FindSkill(BattleContext context, SkillType type)
{
SkillsCanCast.Clear();
foreach (var skill in SkillMgr.Skills)
{
if ((skill.Define.Type & type) != skill.Define.Type) continue; // 这种位运算可以支持多个种类的type查找
var result = skill.CanCast(context);
if (result == SkillResult.Casting)
return null;
if (result == SkillResult.Ok)
SkillsCanCast.Add(skill);
}
if(SkillsCanCast.Count == 0) return null;
Random rd = new Random();
int castidx = rd.Next(0, SkillsCanCast.Count);
return SkillsCanCast[castidx];
}
internal void StopMove()
{
this.State = CharacterState.Idle;
this.moveTarget = Vector3Int.zero;
this.Speed = 0;
SendEntityMsg();
}
// 更新怪物移动位置,这里反编译了Unity中的Vector3用来进行浮点运算
private void UpdateMovement()
{
if (State == CharacterState.Move)
{
if (this.Distance(this.moveTarget) < 50)
{
this.StopMove();
}
if (this.Speed > 0)
{
Vector3 dir = this.Direction; // 注意,这里反编译了Unity的Vector3拿来用,因为是一个连续变量
this.movePosition += dir * this.Speed * Time.deltaTime / 100f;
this.Position = this.movePosition;
}
}
}
// 这个函数会被agent持续调用
internal void MoveTo(Vector3Int position)
{
if (State == CharacterState.Idle)
{
State = CharacterState.Move;
}
if (this.moveTarget != position)
{
this.moveTarget = position;
this.movePosition = this.Position;
var dist = (this.moveTarget - this.Position);
this.Direction = dist.normalized;
this.Speed = this.Define.Speed;
SendEntityMsg(EntityEvent.MoveFwd);
}
}
internal override void DoDamage(NDamageInfo damage, Creature source)
{
base.DoDamage(damage, source);
this.BattleState = BattleState.InBattle;
}
protected override void OnDamage(NDamageInfo damage, Creature source)
{
base.OnDamage (damage, source);
if (this.AI != null)
{
this.AI.OnDamage(damage, source);
}
}
}
}
AIAgent.cs:
using GameServer.Entities;
using SkillBridge.Message;
namespace GameServer.AI
{
class AIAgent
{
private Monster monster;
AIBase ai;
public AIAgent(Monster monster)
{
this.monster = monster;
string ainame = monster.Define.AI;
if (string.IsNullOrEmpty(ainame)) ainame = AIMonsterPassive.ID;
switch (ainame)
{
case AIMonsterPassive.ID:
this.ai = new AIMonsterPassive(monster);
break;
case AIBoss.ID:
this.ai = new AIBoss(monster);
break;
}
}
public void Init()
{
}
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);
}
}
}
}
AIMonsterPassive:
using GameServer.Entities;
namespace GameServer.AI
{
class AIMonsterPassive : AIBase
{
public const string ID = "AIMonsterPassive";
public AIMonsterPassive(Monster monster) : base(monster)
{
}
}
}
AIBase.cs:
using System;
using System.Collections.Generic;
using Common;
using Common.Battle;
using GameServer.Battle;
using GameServer.Entities;
using SkillBridge.Message;
namespace GameServer.AI
{
class AIBase
{
private Monster owner;
private Creature Target;
Skill normalSkill;
float SkillInterval = 2f;// 释放技能后的僵直时间
float SkillIntervallTimer = 0;
public AIBase(Monster monster)
{
this.owner = monster;
normalSkill = this.owner.SkillMgr.NormalSkill;
}
internal void Update()
{
if (this.owner.BattleState == BattleState.InBattle)
{
this.UpdateBattle();
}
else if (this.owner.BattleState == BattleState.Idle)
{
this.owner.Attributes.HP += 5; // 脱战回血
}
}
private void UpdateBattle()
{
if (this.Target == null)
{
this.owner.BattleState = BattleState.Idle;
return;
}
if(SkillIntervallTimer > 0)
{
SkillIntervallTimer -= Time.deltaTime;
if(SkillIntervallTimer > 0)
{
return;
}
}
if (!TryCastSkill())
{
if (!TryCastNormal())
{
FollowTarget();
}
}
}
private void FollowTarget()
{
if (Target == null) return;
int distance = this.owner.Distance(this.Target);
if (distance > normalSkill.Define.CastRange - 20) // 这里需要减去目标半径
{
this.owner.MoveTo(this.Target.Position);
//Log.InfoFormat("Monster:{0} Move", this.owner.Name);
}
else
{
//Log.InfoFormat("Monster:{0} Stop", this.owner.Name);
this.owner.StopMove();
}
}
private bool TryCastNormal()
{
if (this.Target != null)
{
BattleContext context = new BattleContext(this.owner.Map.Battle)
{
Target = this.Target,
Caster = this.owner,
};
var result = normalSkill.CanCast(context);
if (result == SkillResult.Ok)
{
Log.InfoFormat("Monster:{0} CastNormalAtk", this.owner.Name);
this.owner.CastSkill(context, normalSkill.Define.ID);
this.owner.StopMove();
SkillIntervallTimer = SkillInterval;
return true;
}
else if(result == SkillResult.OutOfRange)
{
return false;
}
}
return false;
}
private bool TryCastSkill()
{
if (this.Target != null)
{
BattleContext context = new BattleContext(this.owner.Map.Battle)
{
Target = this.Target,
Caster = this.owner,
};
Skill skill = this.owner.FindSkill(context, SkillType.Skill); // 只查找技能
if (skill != null)
{
Log.InfoFormat("Monster:{0} TryCastSkill:{1}", this.owner.Name, skill.Define.Name);
this.owner.CastSkill(context, skill.Define.ID);
this.owner.StopMove();
SkillIntervallTimer = SkillInterval;
return true;
}
}
return false;
}
internal void OnDamage(NDamageInfo damage, Creature source)
{
if (Target == null)
{
Target = source;
source.OnDead += LossTarget;
source.OnLeave += LossTarget;
}
}
//
private void LossTarget()
{
this.Target.OnDead -= LossTarget;
this.Target.OnLeave -= LossTarget;
this.Target = null;
}
}
}
竞技场系统和副本系统
Timeline组件:实现CG播放,摄像机漫游等