【Unity】动作游戏开发实战详细分析-15-可扩展的战斗系统


系统设计

攻击信息传递

通常情况下,伤害、属性、判定都会被封装到类中,在触发动画事件后将战斗信息发送给受击者。

我们可以结合Unity碰撞/触发,在发生事件后获取对应信息,而非主动将战斗信息发送给目标,这有利于后期受击判定的调试。

unity 战斗框架 代码 unity战斗系统_初始化

战斗系统设计

通过下图的组件模式设计战斗系统,并通过统一的回调函数进行战斗信息发送

只需要将所有组件的信息函数绑定到BattleObject的回调函数进行统一调用。

通过这样的组件模式构建可扩展的战斗系统,以及高度自定义的战斗系统。

设计所有组件的基类,并让所有组件通过它进行泛化,即可实现扩展。

unity 战斗框架 代码 unity战斗系统_初始化_02

战斗组件基类

基类是一个抽象类,它是所有组件的抽象。

具备基本字段,战斗对象(存储挂载的战斗对象本身),并在初始化函数中进行赋值。

public abstract class BattleObjectComponentBase : MonoBehaviour
{
  protected BattleObject mBattleObject;


  public virtual void Initialization(BattleObject battleObject)//初始化
  {
    mBattleObject = battleObject;
  }
}
战斗阵营

这是一个静态类,设计所有的战斗阵营,依次对队友与敌人进行区分。

public static class EasyFactionConst
{
  public const int PLAYER = 1;//玩家
  public const int ENEMY = 2;//敌人
}
战斗对象类

战斗对象类是个十分重要的类。

首先是基本字段

  • 战斗组件数组,存储该战斗对象存储的所有战斗组件
  • 战斗阵营
  • 接触其他阵营的回调事件,用于传递战斗信息
  • 常规回调事件,可用于处理同阵营的Buff等,这是一个基础回调事件。在本示例中,该事件会对所有阵营反馈调用。

其次是函数

  • 获取组件函数,这是一个泛型函数,用于获取该战斗对象上的任意战斗组件。
  • 遍历所有组件,返回对应的组件
  • 在Awake函数中初始化所有战斗组件
  • 触发器函数
  • 首先尝试获取战斗对象组件,并判断战斗阵营,其次就是回调事件的调用。
public class BattleObject : MonoBehaviour
{
  [SerializeField]
  BattleObjectComponentBase[] battleObjectComponents = new BattleObjectComponentBase[0];
  public int faction = EasyFactionConst.PLAYER;
  public event Action<BattleObject, BattleObject> OnContactOtherFaction;//接触到其他阵营
  public event Action<BattleObject, BattleObject> OnBattleTriggerEnter;//常规碰撞回调


  public T GetBattleObjectComponent<T>()//获取战斗对象组件
    where T : BattleObjectComponentBase
    {
      var result = default(T);
      for (int i = 0; i < battleObjectComponents.Length; i++)
      {
        var item = battleObjectComponents[i];
        if (item.GetType() == typeof(T))//匹配对应类型
        {
          result = item as T;
          break;
        }
      }
      return result;
    }
  protected virtual void Awake()
  {
    for (int i = 0; i < battleObjectComponents.Length; i++)
      battleObjectComponents[i].Initialization(this);//初始化战斗对象组件
  }

  void OnTriggerEnter(Collider collider)
  {
    var otherBattleObject = collider.transform.GetComponent<BattleObject>();
    if (otherBattleObject == null) return;//战斗对象过滤
    if (otherBattleObject.faction != faction)
    {
      if (OnContactOtherFaction != null)
        OnContactOtherFaction(this, otherBattleObject);//接触到其他阵营
    }
    if (OnBattleTriggerEnter != null)
      OnBattleTriggerEnter(this, otherBattleObject);//常规进入回调
  }
}
伤害传递组件

我们来添加伤害传递组件

该组件具有基本字段

  • 生命值,战斗数值比较复杂可自行定义类
  • 攻击状态判断
  • 伤害值
  • 上帝模式
  • 死亡判断
  • 事件(可根据自定义添加更多的回调事件)
  • HP改变回调事件
  • 死亡回调事件
  • 受伤回调事件
  • 攻击成功回调事件
  • 击杀回调事件

这些事件,只需要通过其他脚本进行回调绑定即可完成基本动作行为,包括:死亡,受伤,攻击,击杀等等,并且可以通过回调函数完成很多能力判定,例如,生命值改变触发能力增强,击杀触发生命值吸收等等。

函数说明

  • 重写初始化事件,并绑定战斗对象的阵营接触回调事件
  • 生命值改变函数
  • 阵营接触回调函数
  • 伤害传递,调用其他阵营对象的生命值改变函数,受伤事件,判断死亡事件等等。

这里有一点需要重点说明,如果攻击方式触发动画事件创造了带有伤害信息的预制体来造成伤害,那么那个预制体上就必须要带有伤害传递组件,因为这是一个战斗对象。

其次还需要配置动画事件,初始化预制体的战斗阵营(可以在之前的动画事件接收脚本里完成),以及自我销毁脚本等等功能。

伤害传递组件具备了受伤功能,伤害功能,死亡,受伤等功能。

public class DamageBattleComponent : BattleObjectComponentBase
{
  public int hp;//生命值
  public bool isAttackState;//是否为攻击状态
  public int toEnemyDamage;//给予敌人伤害
  public bool godMode;//上帝模式
  bool mIsDied;

  /// <summary>
  /// 参数1 - 事件发送者, 参数2 - 旧的HP值, 参数3 - 新的HP值
  /// </summary>
  public event Action<BattleObject, int, int> OnHPChanged;//HP改变事件
  /// <summary>
  /// 参数1 - 事件发送者。
  /// </summary>
  public event Action<BattleObject> OnDied;//死亡事件
  /// <summary>
  /// 参数1 - 事件发送者,参数2 - 攻击者, 参数3 - 伤害值
  /// </summary>
  public event Action<BattleObject, BattleObject, int> OnHurt;//受伤事件
  /// <summary>
  /// 参数1 - 事件发送者, 参数2 - 受击者
  /// </summary>
  public event Action<BattleObject, BattleObject> OnAttackCompleted;//攻击成功事件
  /// <summary>
  /// 参数1 - 事件发送者, 参数2 - 受击者
  /// </summary>
  public event Action<BattleObject, BattleObject> OnKilled;//击杀事件


  public override void Initialization(BattleObject battleObject)//初始化函数
  {
    base.Initialization(battleObject);
    battleObject.OnContactOtherFaction += OnContactOtherFactionCallback;
  }

  public void ChangeHP(int newHP)
  {
    newHP = Math.Max(newHP, 0);

    if (hp == newHP) return;

    if (OnHPChanged != null)
      OnHPChanged(mBattleObject, hp, newHP);

    hp = newHP;

    if (hp <= 0 && !mIsDied)
    {
      if (OnDied != null)
        OnDied(mBattleObject);

      mIsDied = true;
    }
  }
  void OnContactOtherFactionCallback(BattleObject sender, BattleObject other)//接触不同阵营后的处理
  {
    var anotherDamageComponent = other.GetBattleObjectComponent<DamageBattleComponent>();
    if (anotherDamageComponent != null)
    {
      if (toEnemyDamage <= 0) return;//没有伤害信息则跳出
      if (anotherDamageComponent.godMode) return;//上帝模式则跳出

      var isAlive_Before = anotherDamageComponent.mIsDied;
      anotherDamageComponent.ChangeHP(anotherDamageComponent.hp - toEnemyDamage);//扣血处理
      var isAlive_After = anotherDamageComponent.mIsDied;

      if (anotherDamageComponent.OnHurt != null)//敌方触发受伤回调
        anotherDamageComponent.OnHurt(other, sender, toEnemyDamage);

      if (anotherDamageComponent.OnAttackCompleted != null)//己方触发攻击成功回调
        anotherDamageComponent.OnAttackCompleted(sender, other);

      if (!isAlive_Before && isAlive_After)//若扣血之后死亡则己方触发击杀回调
      {
        if (OnKilled != null)
          OnKilled(mBattleObject, other);
      }
    }
  }
}
受击僵直组件

受击僵直组件

基本字段

  • 僵直时间
  • 僵直回调事件

方法说明

  • 初始化函数
  • 回调函数

该组件具备造成僵直能力与受到僵直的功能。

public class HitStopBattleComponent : BattleObjectComponentBase
{
  public float toEnemyHitStopTime;//赋予敌人的僵直时间
  /// <summary>
  /// 进入僵直状态事件,参数1 - 僵直时间
  /// </summary>
  public event Action<float> OnHitStopTriggered;


  public override void Initialization(BattleObject battleObject)//初始化函数
  {
    base.Initialization(battleObject);
    battleObject.OnContactOtherFaction += OnContactOtherFactionCallback;
  }

  void OnContactOtherFactionCallback(BattleObject sender, BattleObject other)//接触不同阵营后的处理
  {
    var anotherHitStopComponent = other.GetBattleObjectComponent<HitStopBattleComponent>();
    if (anotherHitStopComponent != null)
    {
      if (toEnemyHitStopTime <= 0) return;//没有僵直信息则跳出
      if (anotherHitStopComponent.OnHitStopTriggered != null)//触发僵直事件,并传入僵直时间
        anotherHitStopComponent.OnHitStopTriggered(toEnemyHitStopTime);
    }
  }
}

这是一个与受击僵直组件耦合的脚本,它具有会绑定受击僵直组件的回调事件,它会单独处理僵直逻辑。

public class GenericHitStopProcess : MonoBehaviour
{
  public HitStopBattleComponent hitStopBattleComponent;
  public Animator animator;
  float mHitRecoverTimer;
  int mIsHitAnimatorHash;
  public bool HitStop { get { return mHitRecoverTimer > 0; } }//是否处于僵直状态


  void Awake()
  {
    hitStopBattleComponent.OnHitStopTriggered += OnHitStopTriggered;
    mIsHitAnimatorHash = Animator.StringToHash("IsHit");
  }

  void Update()
  {
    if (mHitRecoverTimer > 0)//简易的僵直度反馈
      animator.SetBool(mIsHitAnimatorHash, true);
    else
      animator.SetBool(mIsHitAnimatorHash, false);

    mHitRecoverTimer = Mathf.Max(0f, mHitRecoverTimer - Time.deltaTime);//僵直恢复
  }

  void OnHitStopTriggered(float hitStopValue)
  {
    mHitRecoverTimer = Mathf.Max(mHitRecoverTimer, hitStopValue);//更新僵直时间
  }
}
物理位移组件

物理位移组件,它负责所有的战斗相关的物理移动能力:击退,浮空等等。

基本字段

  • 力的类型
  • 力度大小
  • 角色驱动
  • 物理行为回调事件

方法说明

  • 初始化函数
  • 施加力量函数(这是被动调用的函数,也就是受到物理位移攻击)
  • 阵营接触回调函数
public class PhysicsBattleComponent : BattleObjectComponentBase
{
  public enum EType { Push, VerticalPush, AirPush, Max }
  public EType type;//力的类型
  public Vector3 forceValue;//力的值

  public CharacterMotor characterMotor;

  /// <summary>
  /// 当物理行为触发,参数1 - 类型
  /// </summary>
  public event Action<EType> OnPhysicBehaviourTriggered;


  public override void Initialization(BattleObject battleObject)//初始化函数
  {
    base.Initialization(battleObject);
    battleObject.OnContactOtherFaction += OnContactOtherFactionCallback;
  }

  public void SetForce(Vector3 forceVector, EType type)//设置力的传入
  {
    if (characterMotor == null) return;

    var upAxis = -Physics.gravity.normalized;
    switch (type)
    {
      case EType.Push:
        characterMotor.SetForce(Vector3.ProjectOnPlane(forceVector, upAxis));//消除Y轴力
        break;
      case EType.VerticalPush:
        characterMotor.SetForce(Vector3.Project(forceVector, upAxis));//消除平面方向力
        break;
      case EType.AirPush:
        characterMotor.SetForce(forceVector);//直接将力赋予Motor
        break;
    }

    OnPhysicBehaviourTriggered(type);
  }

  void OnContactOtherFactionCallback(BattleObject sender, BattleObject other)//接触不同阵营处理
  {
    var anotherPhysicsBattleComponent = other.GetBattleObjectComponent<PhysicsBattleComponent>();
    if (anotherPhysicsBattleComponent != null)
    {
      forceValue = mBattleObject.transform.rotation * forceValue;
      anotherPhysicsBattleComponent.SetForce(forceValue, type);//传入力
    }
  }
}