title: (1) 基础的战斗实现 - RE:从0开始的RPG游戏制作
date: 2024-06-26 15:39:54
description: 从最简单的回合制逻辑开始
tags: [Unity3D, 游戏开发, 玩法设计, C#]
categories: [RE:从0开始的RPG游戏制作]

cover: https://s21.ax1x.com/2024/06/27/pkyxyHU.png

0/基本结构

在上一章我们已经想好我们要做什么事了,那么从现在开始就要落实下去。根据我们之前的设想,战斗部分的逻辑可以用这张图来表示。

最初的战斗逻辑

乍一看在玩家眼中确实是这样的,我们不妨想的更详细一些,把buff之类的东西考虑进去,之后我们就能得到下面这张图

优化过后的战斗逻辑

虽然这个流程图还没有详细到我们实际实现的部分,但是我们可以根据这个流程图来搭建一个框架

1/战斗管理器

根据我之前那堪称贫瘠的项目经验,把逻辑与数据分开是最好的实现方式。这种思想就有点类似于工厂模式,我们在定义好“战斗工厂”以后,每次想要开启战斗就让“战斗工厂”去“生产”一个出来就好了。

所以我们要创建脚本Script\System\BattleManager.cs来实现。

{% note info %}
强烈推荐在创建代码的时候,预先设计好项目的目录结构,不然代码多了之后就会非常折磨人
所以我比较推荐的目录结构是下面这样

D:\U3DPROJ\RPG\ASSETS        
├─Scenes
└─Scripts
    ├─DataDef
    ├─System
    └─UI

{% endnote %}

这里我就直接贴出代码,之后再一段一段讲解

1.0/枚举定义

首先我们要将一些枚举提前定义下来

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 所有与战斗有关的常量都在这定义
/// </summary>

public class BattleDef
{


}

/// <summary>
/// 战斗的状态标志
/// </summary>
public enum BattleState : byte
{
    Enter = 0,      // 进入战斗
    BeforeAct = 1,  // 行动前
    SelectAct = 2,  // 选择行动中
    Acting = 3,     // 行动表演中
    AfterAct = 4,   // 行动后
    ResultWin = 5,  // 战斗胜利
    ResultLose = 6, // 战斗失败
}

/// <summary>
/// Buff触发的时机
/// </summary>
public enum BuffTime : byte
{
    None = 0,
    Enter = 1,          // 自身入场时
    Exit = 2,           // 自身离场时
    BeforeAct = 3,      // 自身行动前
    AfterAct = 4,       // 自身行动后
    AddBuff = 5,        // 添加其他增益时
    RemoveBuff = 6,     // 移除其他增益时
    AddDebuff = 7,      // 添加其他减益时
    RemoveDebuff = 8,   // 移除其他减益时
    DeadSelf = 9,       // 自身死亡时
    ReviveSelf = 10,    // 自身复活时
    DeadAlly = 11,      // 友方死亡时
    ReviveAlly = 12,    // 友方复活时
    Dead = 13,          // 其他单位死亡时
    Revive = 14,        // 其他单位复活时
    OnCrit = 15,        // 暴击时
    OnCrited = 16,      // 受到暴击时
    OnKill = 17,        // 造成击杀时
    OnAttack = 18,      // 使用普攻时
    BeforeAllyCast = 19,// 友方使用技能前
    AfterAllyCast = 20, // 友方使用技能后
    BeforeEnemyCast = 21,// 敌方使用技能前
    AfterEnemyCast = 22,// 敌方使用技能后
    BeforeCast = 23,    // 自身使用技能前
    AfterCast = 24,     // 自身使用技能后
    GetCost = 25,       // 获得资源时
    ConsumeCost = 26,   // 消耗资源时
    OnAllyDamaged = 27, // 友方受伤时
    OnAllyHeal = 28,    // 友方承受治疗时
    OnEnemyDamaged = 29,// 敌方受伤时
    OnEnemyHeal = 30,   // 敌方承受治疗时
}

public enum BuffType : byte
{
    Buff,   // 正常Buff,可被驱散
    Mark,   // 印记,不可被驱散
}

1.1/角色定义

分析我们先前的系统设计,对于角色,我们大体能得出以下图

// Assets\Scripts\DataDef\RoleData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RoleData : ScriptableObject
{
    // 基础属性
    public float Hp;

    public float Atk;

    public float Def;

    public float Spd;

    public float Crit;

    public float CritDmg;

    public float Hit;

    public float Res;

    // 技能配置

    // 深拷贝,将Data转换为实际的对象
    public Role ToRole()
    {
        // 调用对应的函数生成并返回
        Role ret = new Role(Hp, Atk, Def, Spd, Crit, CritDmg, Hit, Res);

        return ret;
    }
}
```csharp
// Assets\Scripts\DataDef\Role.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 实际的Role
/// </summary>
public class Role
{
    // Id,只会在战斗中生成
    public int Id;

    // 基础属性
    public float hpMax;
    public float hp;
    public float attack;
    public float defense;
    public float speed;
    public float critChance;
    public float critDamage;
    public float hitChance;
    public float resistance;

    // 倍率部分
    public float attackRate;        // 攻击力倍率
    public float defenseRate;       // 防御倍率
    public float speedRate;         // 速度倍率
    public float critRate;          // 暴击机率倍率
    public float critDmgRate;       // 暴击伤害倍率
    public float hitRate;           // 命中倍率
    public float resistanceRate;    // 抵抗倍率

    // 固定部分
    public float attackExtra;       // 额外攻击力
    public float defenseExtra;      // 额外防御
    public float speedExtra;        // 额外速度
    public float critExtra;         // 额外暴击机率
    public float critDmgExtra;      // 额外暴击伤害
    public float hitExtra;          // 额外效果命中
    public float resistanceExtra;   // 额外效果抵抗

    public List<Skill> skillList;   // 角色的技能

    public Skill curSkill;          // 当前回合释放的技能

    public List<Role> targetList;   // 技能选择的目标

    public Role()
    {
    }

    /// <summary>
    /// 给全数据生成的Role对象
    /// </summary>
    /// <param name="hpMax">初始生命值</param>
    /// <param name="attack">攻击力</param>
    /// <param name="defense">防御</param>
    /// <param name="speed">速度</param>
    /// <param name="critChance">暴击几率</param>
    /// <param name="critDamage">暴击伤害</param>
    /// <param name="hitChance">效果命中</param>
    /// <param name="resistance">效果抵抗</param>
    /// <param name="attackRate">攻击倍率</param>
    /// <param name="defenseRate">防御倍率</param>
    /// <param name="speedRate">速度倍率</param>
    /// <param name="critRate">暴击机率倍率</param>
    /// <param name="critDmgRate">暴击伤害倍率</param>
    /// <param name="hitRate">效果命中倍率</param>
    /// <param name="resistanceRate">效果抵抗倍率</param>
    /// <param name="attackExtra">额外攻击</param>
    /// <param name="defenseExtra">额外防御</param>
    /// <param name="speedExtra">额外速度</param>
    /// <param name="critExtra">额外暴击</param>
    /// <param name="critDmgExtra">额外爆伤</param>
    /// <param name="hitExtra">额外效果命中</param>
    /// <param name="resistanceExtra">额外效果抵抗</param>
    public Role(float hpMax, float attack, float defense, float speed, float critChance, float critDamage, float hitChance, float resistance, float attackRate, float defenseRate, float speedRate, float critRate, float critDmgRate, float hitRate, float resistanceRate, float attackExtra, float defenseExtra, float speedExtra, float critExtra, float critDmgExtra, float hitExtra, float resistanceExtra)
    {
        this.hpMax = hpMax;
        this.hp = hpMax;
        this.attack = attack;
        this.defense = defense;
        this.speed = speed;
        this.critChance = critChance;
        this.critDamage = critDamage;
        this.hitChance = hitChance;
        this.resistance = resistance;
        this.attackRate = attackRate;
        this.defenseRate = defenseRate;
        this.speedRate = speedRate;
        this.critRate = critRate;
        this.critDmgRate = critDmgRate;
        this.hitRate = hitRate;
        this.resistanceRate = resistanceRate;
        this.attackExtra = attackExtra;
        this.defenseExtra = defenseExtra;
        this.speedExtra = speedExtra;
        this.critExtra = critExtra;
        this.critDmgExtra = critDmgExtra;
        this.hitExtra = hitExtra;
        this.resistanceExtra = resistanceExtra;
    }

    /// <summary>
    /// 只有初始数据生成的Role对象
    /// </summary>
    /// <param name="hpMax">初始生命</param>
    /// <param name="attack">攻击力</param>
    /// <param name="defense">防御</param>
    /// <param name="speed">速度</param>
    /// <param name="critChance">暴击几率</param>
    /// <param name="critDamage">暴击伤害</param>
    /// <param name="hitChance">效果命中</param>
    /// <param name="resistance">效果抵抗</param>
    public Role(float hpMax, float attack, float defense, float speed, float critChance, float critDamage, float hitChance, float resistance)
    {
        this.hpMax = hpMax;
        this.hp = hpMax;
        this.attack = attack;
        this.defense = defense;
        this.speed = speed;
        this.critChance = critChance;
        this.critDamage = critDamage;
        this.hitChance = hitChance;
        this.resistance = resistance;
        this.attackRate = 0f;
        this.defenseRate = 0f;
        this.speedRate = 0f;
        this.critRate = 0f;
        this.critDmgRate = 0f;
        this.hitRate = 0f;
        this.resistanceRate = 0f;
        this.attackExtra = 0f;
        this.defenseExtra = 0f;
        this.speedExtra = 0f;
        this.critExtra = 0f;
        this.critDmgExtra = 0f;
        this.hitExtra = 0f;
        this.resistanceExtra = 0f;
    }

    #region 角色的行为

    /// <summary>
    /// 释放当前回合选择的技能
    /// </summary>
    /// <returns>是否成功释放</returns>
    public bool CastCurSkill()
    {
        curSkill.Cast(this, targetList);
        return true;
    }

    #endregion

    #region 获取计算好的属性

    // 获取计算好的速度
    public float GetSpeed() { return (this.speed + this.speedExtra) * (1f + this.speedRate); }

    #endregion
}

1.2/技能定义

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SkillData : ScriptableObject
{

}
```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Skill
{
    public void Cast(Role caster, List<Role> targets)
    {

    }
}

1.3/Buff定义

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BuffData : ScriptableObject
{
    public string buffName;

    public string description;

    public BuffTime triggerTime;
}
```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Buff
{

}

1.4/行动条管理

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 将行动条单独封装成一个类,好操作
/// </summary>
public class ActStrip
{
    // 在场的所有单位
    private List<ActUnit> allUnits;
    // 行动条上的所有单位
    private List<ActUnit> units;
    // 行动条的长度,由一速决定
    private float actLength;

    public ActStrip()
    {
        this.allUnits = new List<ActUnit>();
        this.units = new List<ActUnit>();
        this.actLength = 0f;
    }

    /// <summary>
    /// 根据给定角色生成行动队列
    /// </summary>
    /// <param name="roles">参与战斗的角色</param>
    /// <returns></returns>
    public void GenerateActStrip(List<Role> roles)
    {
        float tempSpeed = 0f;   // 最快一速
        foreach (Role role in roles)
        {
            if(role.speed > tempSpeed)
                tempSpeed = role.speed;
            this.allUnits.Add(new ActUnit(role));
        }
        this.actLength = tempSpeed;
        // 根据curProgress排序
        this.allUnits.Sort((ActUnit x, ActUnit y) =>
        {
            Role roleX = BattleManager.GetRoleById(x.roleId);
            Role roleY = BattleManager.GetRoleById(y.roleId);
            return roleX.speed.CompareTo(roleY.speed);
        });
        this.units = allUnits;
    }

    /// <summary>
    /// 行动至下一个角色并返回它
    /// </summary>
    /// <returns>行动条推进后进行行动的角色</returns>
    public Role ActProgress()
    {
        // 只用看行动条的第一个就知道整体的情况了
        ActUnit unit = this.units[0];
        if (unit.curProgress >= actLength)
        {
            // 复制一份到队尾,删除unit并返回这个角色
            unit.Duplicate(unit.curProgress - actLength);
            Role ret = BattleManager.GetRoleById(unit.roleId);
            units.Remove(unit);
            return ret;
        }
        else
        {
            // 所有人都按照自己的速度行动
            foreach(ActUnit u in units)
            {
                Role role = BattleManager.GetRoleById(u.roleId);
                if(role != null)
                {
                    u.curProgress += role.GetSpeed();
                }
            }
            // 行动完递归调用一次
            return ActProgress();
        }
    }
}

public class ActUnit
{
    // 在本场战斗中的唯一Id
    public int roleId = 0;
    // 当前位于行动条的哪个位置
    public float curProgress;

    public ActUnit()
    {
        this.roleId = 0;
        this.curProgress = 0f;
    }

    // 初始生成角色
    public ActUnit(Role role)
    {
        // 读取角色的Id
        this.roleId = role.Id;
        this.curProgress = role.speed;
    }

    // 生成同一个单位的不同速度副本
    public ActUnit Duplicate(float extraProgress)
    {
        if(this.roleId == 0)
        {
            Debug.LogError("复制了一个空目标");
            return null;
        }
        this.curProgress = extraProgress;
        return this;
    }
}

1.5/战斗管理器

// Assets\Scripts\System\Battle\BattleManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;


public class BattleManager : MonoBehaviour
{
    // 所有的等待都用这个来实现
    private readonly WaitForSeconds _delaySeconds = new WaitForSeconds(0.3f);

    #region 战斗相关数据
    // 存放用来深拷贝的双方数据
    /// <summary>
    /// 玩家队伍的角色数据
    /// </summary>
    List<RoleData> playerTeam;
    /// <summary>
    /// 敌方队伍的角色数据
    /// </summary>
    List<RoleData> enemyTeam;

    /// <summary>
    /// 玩家队伍的角色
    /// </summary>
    public static List<Role> playerRoles;
    /// <summary>
    /// 敌方队伍的角色
    /// </summary>
    public static List<Role> enemyRoles;
    /// <summary>
    /// 当前回合正在行动的角色
    /// </summary>
    Role curActRole;
    /// <summary>
    /// 当前的行动队列和之后的行动队列
    /// </summary>
    ActStrip actStrip;
    /// <summary>
    /// 当前战斗的状态
    /// </summary>
    private BattleState _battleState;

    #endregion

    void Start()
    {
        StartCoroutine(Fight(() => { Debug.Log("战斗胜利"); }, () => { Debug.Log("战斗失败"); }));
    }

    void Update()
    {

    }

    IEnumerator Fight(Action winCallback, Action loseCallback)
    {
        // 从这里开始就是进入战斗逻辑的初始化部分
        foreach(RoleData playerData in playerTeam)
        {
            // 初始化玩家阵营的人物
            playerRoles.Add(playerData.ToRole());
        }
        foreach (RoleData enemyData in enemyTeam)
        {
            enemyRoles.Add(enemyData.ToRole());
        }
        // 读取所有单位的速度,初始化行动条
        actStrip.GenerateActStrip(playerRoles.Concat(enemyRoles).ToList());

        // 初始化战斗结果
        int result = 0;

        // 战斗主循环
        while ((int)_battleState < 5 )
        {
            // 处理先机事件,只有刚进入战斗会触发
            if (_battleState == BattleState.Enter)
            {
                // TODO:遍历单位的先机相关属性
                Debug.Log("触发了所有人的先机效果");
                while(_battleState == BattleState.Acting) { yield return _delaySeconds; }
            }

            result = CheckBattleResult();
            if(result == 1) {yield return winCallback; }
            else if(result == 2) { yield return loseCallback; }

            curActRole = actStrip.ActProgress();
            _battleState = BattleState.BeforeAct;
            // TODO: 回合前BUFF检测

            if(playerRoles.Contains(curActRole) && _battleState == BattleState.BeforeAct)
            {
                _battleState = BattleState.SelectAct;
                // 等待玩家操作
                while(_battleState == BattleState.SelectAct) { yield return _delaySeconds; }
            }else if(enemyRoles.Contains(curActRole) && _battleState == BattleState.BeforeAct)
            {
                // TODO: 调用敌人的AI选择技能
            }

            // 技能演出
            if(curActRole.curSkill != null)
            {
                curActRole.CastCurSkill();
                while(_battleState == BattleState.Acting) { yield return _delaySeconds; }
            }

            _battleState = BattleState.AfterAct;
            // TODO: 行动后的逻辑

            // 判断是否满足结束条件
            result = CheckBattleResult();
            if (result == 1) { yield return winCallback; }
            else if (result == 2) { yield return loseCallback; }
        }
    }

    /// <summary>
    /// 检查当前是否满足结束条件
    /// </summary>
    /// <returns>0 - 还没结束,1 - 胜利,2 - 失败</returns>
    public int CheckBattleResult()
    {
        // 玩家阵营全部阵亡 -- 失败
        if(playerRoles.Count == 0) { return 2; }
        // 敌方阵营全部阵亡 -- 胜利
        if(enemyRoles.Count == 0) { return 1; }
        // 否则就是还没结束
        return 0;
    }

    #region 外界获取数据的接口

    /// <summary>
    /// 根据角色的Id获取角色对象
    /// </summary>
    /// <param name="Id">角色的Id</param>
    /// <returns>成功返回角色对象,失败返回null</returns>
    public static Role GetRoleById(int Id)
    {
        foreach (Role role in playerRoles)
        {
            if (role.Id == Id){ return role; }
        }
        foreach(Role role in enemyRoles)
        {
            if (role.Id == Id) { return role; }
        }
        return null;
    }

    #endregion
}

一个还在寻找自己的三流开发者