Unity turn-based combat system (intermediate level)

I found the project file. The old version of the script reported an error. I solved the error under the new version 2019.4.21f1c1, and the battle scene can run normally.

Students who need it can click the address below to download (just follow it, no points are required). I wish everyone will succeed in learning as soon as possible.

Project package download

————————————————————————

In the previous article, we implemented a relatively rudimentary turn-based combat system, which was limited to 1v1 combat and had a fixed target, which was relatively low. Last night, we studied an advanced turn-based combat system.

Intermediate level

Introduction to the implementation effects of turn-based combat system

1. Multi-target combat, no matter how many combat units you put in, it will be OK (just set the corresponding tag, PlayerUnit or EnemyUnit for the participating units);

2. Added attack speed sorting, and when initially reading the units participating in the battle, the list will be sorted once;

3. Players manually select skills and attack targets: first select skills on the UI (which affects the damage coefficient), and then select attack targets through rays;

4. Real-time blood bar, the blood bar is displayed above the unit head and updated in real time;

5. For the defeat interface and small animation, UGUI was used to make an ending animation (for convenience, the same one was used for defeat and victory)

Preparation:

1. It is better to prepare model resources first

Downloaded from AssetStore, resource name: Animated Knight and Slime Monster (free)

Downloaded from AssetStore, resource name: Toon RTS Units - Demo (free)

2. Add a model to the scene and add Animator to the model

I selected the knight as the player character and the little zombie as the monster from the model, and added standby, attack, hit, and death animation clips respectively.

(These steps are consistent with the basic implementation)

3. Add tags to participating units

The player unit is set to PlayerUnit and the monster unit is set to EnemyUnit

By the way, adjust the position and camera angle in the scene to a more reasonable position. You can refer to the screenshot angle.

4. Create empty objects BattleManager and BattleUIManager

Used to mount the round control script and the health bar UI script respectively.

5. I missed the description about the health bar prefab before.

Create an Image named "BloodBar" as the health bar base map, which contains 2 sub-objects:

BloodFill, the Image type is used as the blood bar (the red color changes), the anchor point of this picture is set to (0,0.5), and the Image Type is set to Filled (the subsequent script can directly change the length by modifying FillAmout)

OwnerName, Text type, used to display the name of the owner of the health bar

Then add a script BloodUpdate() to the health bar. The script content is as follows:

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);
        }
    }

}

After adding the script, drag the health bar to the Prefabs folder to generate a prefab.

The complete project hierarchy view structure is as follows:

Next is the script

There are 3 scripts in total, not many. Their functions are as follows:

UnitStats, a common script for units participating in the battle, is used to save character combat attributes, and includes functions for external calls such as taking damage and determining death;

BattleTurnSystem, the core script for turn-based logic control

BattleUIManager, a script for drawing the health bar UI. This script is relatively ugly. It is just to implement the function and is not optimized. It also includes a scene switching function called by the end interface button.

UnitStats, added to all player and monster objects and assigned values ​​(health, attack, defense, speed) through the Unity editor interface

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, added to the previously created BattleManager object

This script has a lot of content, so I’ll break it down and explain it first:

    /// <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()


The complete script is as follows:

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, also added to the previously created empty object

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);
    }
}

If you are not very familiar with the UGUI creation interface and animation, you can ignore the health bar related scripts and the turn-based logic control script BattleTurnSystem.

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

Pay attention to the content of this block and directly output text verification through Debug.Log();

The callback for the end button does not need to be added;
 

Guess you like

Origin blog.csdn.net/c252270036/article/details/77141708