Unity终极实战之MMORPG-Part3


战斗系统Part1

网游常见的战斗模式

客户端战斗:客户端执行核心逻辑,服务端验证逻辑,分为数据验证(属性验证)和战斗回放验证,两个人以上比较好验证。
服务器战斗:服务器执行核心逻辑,客户端处理表现,这样就不需要验证。

我们将要做服务器战斗。
所以每一个战斗都用一场BattleManager来管理,战斗管理可以分为战斗对象管理,战斗状态管理,战斗结算。
战斗数据分为基本属性,战斗属性,计算公式
单位战斗流程:

  1. 目标选择:主动选择,被动选择
  2. 进入战斗状态:攻击行为产生
  3. 战斗过程
  4. 战斗结束:目标死亡,脱离战斗
  5. 非战斗状态
    战斗表现:
  6. 输入模块:技能栏,BUFF栏,目标指示,战斗飘字
  7. 动作:攻击动作,受击动作
  8. 音效:攻击,受击
  9. 特效:技能特效,BUFF特效,受击特效

具体细节请查看策划文档。
属性类型:
基础属性:初始属性、成长属性
附加属性:装备属性等
动态属性:BUFF属性等
Character属性结构:
Attributes:
InitAttribute
BaseAttribute
EquipAttribute
BuffAttribute
FinalAttribute

框架

客户端:

  1. 表现层:InputManager、Amin、Effect、Audio、BattleUI、EntityController
  2. 逻辑层:Monster、Characeter、BuffManager、SkillManager
  3. 网络层:BattleService
    服务器:
  4. 逻辑层:AI、Buff、Skill、Monster、Character、BuffManager、SkillManager
  5. 网络层: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();

    }
}

技能系统设计

技能元素拆分

  1. 释放:目标、范围、时间
  2. 条件:MP消耗、CD
  3. AOE:范围
  4. 持续:持续事件、间隔时间
  5. 行为:伤害、Buff
  6. 表现:名称、图标特效

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

子弹技能

分类:
真实命中:需要实现子弹的方向、速度、位置同步。
无真实命中:必定命中,客户端进行时间预测,命中时间同步,同步方案有预同步和实时同步。

完善技能行为

  1. 技能状态:无、蓄力,执行
  2. 释放类型:瞬发、非瞬发、吟唱、持续
  3. 伤害类型:单次、多次
  4. 伤害范围:单体、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播放,摄像机漫游等


文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
评论
  目录