关闭

Unity 回合制战斗系统(中级篇)

标签: Unity回合制战斗战斗系统游戏游戏设计
71人阅读 评论(0) 收藏 举报
分类:

本人游戏策划一枚,爱好游戏设计开发

上一篇文章里实现了较为初级的回合制战斗系统,仅限与1v1的战斗,且目标固定,比较low,昨晚又研究了一种进阶的回合制战斗。


中级篇

回合制战斗系统实现效果简介

1. 多目标战斗,不管你放多少个战斗单位都OK(只要给参战单位设置相应的tag,PlayerUnit或EnemyUnit);

2. 加入了攻击速度排序,初始读取参战单位时会对列表进行一次出手排序;

3. 玩家手动选择技能及攻击目标:先在UI上选择技能(影响伤害系数),再通过射线选择攻击目标;

4.实时血条,单位头顶显示血条并实时更新;

5.战败界面及小动画,使用UGUI做了个结束动画(为方便,战败和战胜用了同一个)



准备工作:

1. 还是先准备模型资源

下载自AssetStore,资源名:Animated Knight and Slime Monster(免费)

下载自AssetStore,资源名:Toon RTS Units - Demo(免费)

2. 场景添加模型并为模型添加Animator

我从模型中选出了骑士作为玩家角色,小僵尸作为怪物,分别添加了待机、攻击、受击、死亡动画片段

(这几步和初级篇实现一致)

3. 为参战单位添加tag

玩家单位设置为PlayerUnit,怪物单位设置为EnemyUnit

顺便把场景中的位置和相机视角调整到比较合理的位置,可以参考截图角度

4. 创建空物体BattleManager和BattleUIManager

分别用于挂载回合控制脚本和血条UI脚本

5. 之前漏了关于血条预制体的说明

创建一个Image命名为“BloodBar”作为血条底图,下面包含2个子物体:

BloodFill,Image类型作为血条(红色会变化的图),这张图的锚点设置为(0,0.5),并设置Image Type为Filled(后面的脚本可以通过修改FillAmout直接改变长度)

OwnerName,Text类型,用于显示血条主人的名字

然后给血条上添加一个脚本BloodUpdate(),脚本内容如下:


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

public class BloodUpdate : MonoBehaviour {

    public GameObject owner;

    private Image ownerBloodFill;

    private BattleUIManager uiManager;

    private Vector3 playerBlood3DPosition;
    private Vector2 playerBlood2DPosition;

    void Start()
    {
        //显示血条主人的名字
        Text ownerText = gameObject.transform.Find("OwnerName").GetComponent<Text>();
        ownerText.text = owner.name;
        
        //获取UI控制脚本的引用
        uiManager = GameObject.Find("BattleUIManager").GetComponent<BattleUIManager>();
    }

    void Update()
    {
        if (owner.tag=="PlayerUnit" || owner.tag == "EnemyUnit")
        {
            //更新血条长度
            ownerBloodFill = gameObject.transform.Find("BloodFill").GetComponent<Image>();
            ownerBloodFill.fillAmount = owner.GetComponent<UnitStats>().bloodPercent;

            //更新血条位置
            playerBlood3DPosition = owner.transform.position + new Vector3(uiManager.bloodXOffeset, uiManager.bloodYOffeset, uiManager.bloodZOffeset);
            playerBlood2DPosition = Camera.main.WorldToScreenPoint(playerBlood3DPosition);
            gameObject.GetComponent<RectTransform>().position = playerBlood2DPosition;
        }
        if (owner.GetComponent<UnitStats>().IsDead())
        {
            gameObject.SetActive(false);
        }
    }

}
添加完脚本后把血条拖到Prefabs文件夹中生成为预制体。


完整项目层级视图结构如下:



接下来就是脚本

脚本一共3个,也不多,作用分别如下:

UnitStats,参战单位公用的脚本,用于保存角色战斗属性,并包含了承受伤害、判断死亡这些供外部调用的函数;

BattleTurnSystem,回合制逻辑控制的核心脚本

BattleUIManager,绘制血条UI的脚本,这个写的比较丑,仅仅为了实现功能,未做优化,其中也包含了供结束界面按钮调用的场景切换函数


UnitStats,添加到所有玩家和怪物对象上,并通过Unity编辑器界面赋值(生命、攻击、防御、速度)

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

public class UnitStats : MonoBehaviour {

    public float health;
    public float attack;
    public float defense;
    public float speed;
    public float attackTrun;            //根据速度计算的出手速度,速度越高出手速度越快

    public float intialBlood;
    public float bloodPercent;

    private bool dead = false;
    // Use this for initialization
    void Start () {
        intialBlood = health;
        bloodPercent = health / intialBlood;

        attackTrun = 100 / speed;
    }

    public void ReceiveDamage(float damage)
    {
        health -= damage;
        bloodPercent = health / intialBlood;

        if (health <= 0)
        {
            dead = true;
            gameObject.tag = "DeadUnit";
            //gameObject.SetActive(false);
            //Destroy(this.gameObject);
        }
        //Debug.Log(gameObject.name + "掉血" + damage + "点,剩余生命值" + health);

    }

    public bool IsDead()
    {
        return dead;
    }


}

BattleTurnSystem,添加到之前创建的BattleManager物体上

这个脚本内容较多,先拆解说明下:

    /// <summary>
    /// 创建初始参战列表,存储参战单位,并进行一次出手排序
    /// </summary>
    void Start ()

    /// <summary>
    /// 判断战斗进行的条件是否满足,取出参战列表第一单位,并从列表移除该单位,单位行动
    /// 行动完后重新添加单位至队列,继续ToBattle()
    /// </summary>
    public void ToBattle()

    /// <summary>
    /// 查找攻击目标,如果行动者是怪物则从剩余玩家中随机
    /// 如果行动者是玩家,则获取鼠标点击对象
    /// </summary>
    /// <returns></returns>
    void FindTarget()

    /// <summary>
    /// 攻击者移动到攻击目标前(暂时没有做这块)
    /// </summary>
    void RunToTarget()

    /// <summary>
    /// 绘制玩家选择技能的窗口
    /// </summary>
    void OnGUI()

    /// <summary>
    /// 技能选择窗口的回调函数
    /// </summary>
    /// <param name="ID"></param>
    void PlayerSkillChoose(int ID)

    /// <summary>
    /// 用于控制玩家选择目标状态的开启
    /// </summary>
    void Update()

    /// <summary>
    /// 当前行动单位执行攻击动作
    /// </summary>

    public void LaunchAttack()

    /// <summary>
    /// 对参战单位根据攻速计算值进行出手排序
    /// </summary>
    void listSort()

    /// <summary>
    /// 延时操作函数,避免在怪物回合操作过快
    /// </summary>
    /// <returns></returns>
    IEnumerator WaitForTakeDamage()

完整脚本如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class BattleTurnSystem : MonoBehaviour {

    private List<GameObject> battleUnits;           //所有参战对象的列表
    private GameObject[] playerUnits;           //所有参战玩家的列表
    private GameObject[] enemyUnits;            //所有参战敌人的列表
    private GameObject[] remainingEnemyUnits;           //剩余参战对敌人的列表
    private GameObject[] remainingPlayerUnits;           //剩余参战对玩家的列表

    private GameObject currentActUnit;          //当前行动的单位
    private GameObject currentActUnitTarget;            //当前行动的单位的目标

    public bool isWaitForPlayerToChooseSkill = false;            //玩家选择技能UI的开关
    public bool isWaitForPlayerToChooseTarget = false;            //是否等待玩家选择目标,控制射线的开关
    private Ray targetChooseRay;            //玩家选择攻击对象的射线
    private RaycastHit targetHit;           //射线目标

    public string attackTypeName;           //攻击技能名称
    public float attackDamageMultiplier;           //攻击伤害系数
    public float attackData;            //伤害值

    private GameObject endImage;            //游戏结束画面

    /// <summary>
    /// 创建初始参战列表,存储参战单位,并进行一次出手排序
    /// </summary>
    void Start ()
    {
        //禁用结束菜单
        endImage = GameObject.Find("ResultImage");
        endImage.SetActive(false);

        //创建参战列表
        battleUnits = new List<GameObject>();

        //添加玩家单位至参战列表
        playerUnits = GameObject.FindGameObjectsWithTag("PlayerUnit");
        foreach (GameObject playerUnit in playerUnits)
        {
            battleUnits.Add(playerUnit);
        }

        //添加怪物单位至参战列表
        enemyUnits = GameObject.FindGameObjectsWithTag("EnemyUnit");
        foreach (GameObject enemyUnit in enemyUnits)
        {
            battleUnits.Add(enemyUnit);
        }

        //对参战单位列表进行排序
        listSort();

        //开始战斗
        ToBattle();
    }

    /// <summary>
    /// 判断战斗进行的条件是否满足,取出参战列表第一单位,并从列表移除该单位,单位行动
    /// 行动完后重新添加单位至队列,继续ToBattle()
    /// </summary>
    public void ToBattle()
    {
        remainingEnemyUnits = GameObject.FindGameObjectsWithTag("EnemyUnit");
        remainingPlayerUnits = GameObject.FindGameObjectsWithTag("PlayerUnit");

        //检查存活敌人单位
        if (remainingEnemyUnits.Length == 0)
        {
            Debug.Log("敌人全灭,战斗胜利");
            endImage.SetActive(true);           //显示战败界面
        }
        //检查存活玩家单位
        else if (remainingPlayerUnits.Length == 0)
        {
            Debug.Log("我方全灭,战斗失败");
            endImage.SetActive(true);           //显示胜利界面
        }
        else
        {
            //取出参战列表第一单位,并从列表移除
            currentActUnit = battleUnits[0];
            battleUnits.Remove(currentActUnit);
            //重新将单位添加至参战列表末尾
            battleUnits.Add(currentActUnit);

            //Debug.Log("当前攻击者:" + currentActUnit.name);

            //获取该行动单位的属性组件
            UnitStats currentActUnitStats = currentActUnit.GetComponent<UnitStats>();

            //判断取出的战斗单位是否存活
            if (!currentActUnitStats.IsDead())
            {
                //选取攻击目标
                FindTarget();
            }
            else
            {
                //Debug.Log("目标死亡,跳过回合");
                ToBattle();
            }
        }
    }

    /// <summary>
    /// 查找攻击目标,如果行动者是怪物则从剩余玩家中随机
    /// 如果行动者是玩家,则获取鼠标点击对象
    /// </summary>
    /// <returns></returns>
    void FindTarget()
    {
        if (currentActUnit.tag == "EnemyUnit")
        {
            //如果行动单位是怪物则从存活玩家对象中随机一个目标
            int targetIndex = Random.Range(0, remainingPlayerUnits.Length);
            currentActUnitTarget = remainingPlayerUnits[targetIndex];
            LaunchAttack();
        }
        else if (currentActUnit.tag == "PlayerUnit")
        {
            isWaitForPlayerToChooseSkill = true;
        }
    }

    /// <summary>
    /// 攻击者移动到攻击目标前(暂时没有做这块)
    /// </summary>
    void RunToTarget()
    {

    }

    /// <summary>
    /// 绘制玩家选择技能的窗口
    /// </summary>
    void OnGUI()
    {
        if (isWaitForPlayerToChooseSkill == true)
        {
            GUI.Window(1, new Rect(Screen.width / 2 + 300, Screen.height / 2+100, 100, 100), PlayerSkillChoose, "选择技能");
        }
    }

    /// <summary>
    /// 技能选择窗口的回调函数
    /// </summary>
    /// <param name="ID"></param>
    void PlayerSkillChoose(int ID)
    {
        if (GUI.Button(new Rect(10, 20, 80, 30), "普通攻击"))
        {
            isWaitForPlayerToChooseSkill = false;
            isWaitForPlayerToChooseTarget = true;
            attackTypeName = "普通攻击";
            attackDamageMultiplier = 1f;
            Debug.Log("请选择攻击目标......");
        }
        if (GUI.Button(new Rect(10, 60, 80, 30), "英勇打击"))
        {
            isWaitForPlayerToChooseSkill = false;
            isWaitForPlayerToChooseTarget = true;
            attackTypeName = "英勇打击";
            attackDamageMultiplier = 1.5f;
            Debug.Log("请选择攻击目标......");
        }
    }

    /// <summary>
    /// 用户控制玩家选择目标状态的开启
    /// </summary>
    void Update()
    {
        if (isWaitForPlayerToChooseTarget)
        {
            targetChooseRay = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(targetChooseRay, out targetHit))
            {
                if (Input.GetMouseButtonDown(0) && targetHit.collider.gameObject.tag == "EnemyUnit")
                {
                    currentActUnitTarget = targetHit.collider.gameObject;
                    //Debug.Log("攻击目标为:" + currentActUnitTarget.name);
                    LaunchAttack();
                }
            }
        }
    }
    
    /// <summary>
    /// 当前行动单位执行攻击动作
    /// </summary>
    public void LaunchAttack()
    {
        //存储攻击者和攻击目标的属性脚本
        UnitStats attackOwner = currentActUnit.GetComponent<UnitStats>();
        UnitStats attackReceiver = currentActUnitTarget.GetComponent<UnitStats>();
        //根据攻防计算伤害
        attackData = (attackOwner.attack - attackReceiver.defense + Random.Range(-2, 2)) * attackDamageMultiplier;
        //播放攻击动画
        currentActUnit.GetComponent<Animator>().SetTrigger("Attack");
        currentActUnit.GetComponent<AudioSource>().Play();

        Debug.Log(currentActUnit.name + "使用技能(" + attackTypeName + ")对" + currentActUnitTarget.name+"造成了"+ attackData + "点伤害");
        //在对象承受伤害并进入下个单位操作前前添加1s延迟
        StartCoroutine("WaitForTakeDamage");
    }

    /// <summary>
    /// 对参战单位根据攻速计算值进行出手排序
    /// </summary>
    void listSort()
    {
        GameObject temp = battleUnits[0];
        for (int i = 0; i < battleUnits.Count - 1; i++)
        {
            float minVal = battleUnits[i].GetComponent<UnitStats>().attackTrun;       //假设i下标的是最小的值
            int minIndex = i;       //初始认为最小的数的下标

            for (int j = i + 1; j < battleUnits.Count; j++)
            {
                if (minVal > battleUnits[j].GetComponent<UnitStats>().attackTrun)
                {
                    minVal = battleUnits[j].GetComponent<UnitStats>().attackTrun;
                    minIndex = j;
                }
            }
            temp = battleUnits[i];       //把本次比较的第一个位置的值临时保存起来
            battleUnits[i] = battleUnits[minIndex];       //把最终我们找到的最小值赋给这一趟的比较的第一个位置
            battleUnits[minIndex] = temp;        //把本次比较的第一个位置的值放回这个数组的空地方,保证数组的完整性
        }

        for (int x = 0; x < battleUnits.Count; x++)
        {
            Debug.Log(battleUnits[x].name);
        }
    }

    /// <summary>
    /// 延时操作函数,避免在怪物回合操作过快
    /// </summary>
    /// <returns></returns>
    IEnumerator WaitForTakeDamage()
    {
        //被攻击者承受伤害
        currentActUnitTarget.GetComponent<UnitStats>().ReceiveDamage(attackData);
        if (!currentActUnitTarget.GetComponent<UnitStats>().IsDead())
        {
            currentActUnitTarget.GetComponent<Animator>().SetTrigger("TakeDamage");
        }
        else
        {
            currentActUnitTarget.GetComponent<Animator>().SetTrigger("Dead");
        }
        
        yield return new WaitForSeconds(1);
        ToBattle();
    }
}


BattleUIManager,同样添加到之前创建的空物体上

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class BattleUIManager : MonoBehaviour {

    public GameObject bloodBar;

    private GameObject[] playerUnits;
    private GameObject[] enemyUnits;

    public float bloodXOffeset;
    public float bloodYOffeset;
    public float bloodZOffeset;

    void Start () {
        playerUnits = GameObject.FindGameObjectsWithTag("PlayerUnit");
        foreach (GameObject playerUnit in playerUnits)
        {
            GameObject playerBloodBar = Instantiate(bloodBar) as GameObject;
            playerBloodBar.transform.SetParent(GameObject.Find("BloodBarGroup").transform, false);
            
            //设置血条的主人
            playerBloodBar.GetComponent<BloodUpdate>().owner = playerUnit;
        }

        enemyUnits = GameObject.FindGameObjectsWithTag("EnemyUnit");
        foreach (GameObject enemyUnit in enemyUnits)
        {
            GameObject enemyBloodBar = Instantiate(bloodBar) as GameObject;
            enemyBloodBar.transform.SetParent(GameObject.Find("BloodBarGroup").transform, false);

            //设置血条的主人
            enemyBloodBar.GetComponent<BloodUpdate>().owner = enemyUnit;
        }
    }

    public void GoToScene(string name)
    {
        SceneManager.LoadScene(name);
    }
}

如果对UGUI创建界面和动画这块不是很熟悉,可以忽略血条相关脚本,以及把回合制逻辑控制脚本BattleTurnSystem中关于

private GameObject endImage;            //游戏结束画面
这块的内容注视掉,直接Debug.Log()输出文字校验即可;

结束按钮的回调也可以不用添加;

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:4112次
    • 积分:123
    • 等级:
    • 排名:千里之外
    • 原创:6篇
    • 转载:13篇
    • 译文:0篇
    • 评论:1条
    文章分类
    文章存档
    最新评论