Unity turn-based combat system (advanced)

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

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

The title of today’s article has been upgraded to “Advanced Chapter” because the combat aspect has achieved the original learning expectations. Next, I am going to work on character creation, data storage and main scene control.

Please see the Gif for the effect of this advanced chapter:

The new content is as follows:

1. Added a long-range unit - Archer

2. Added special effects during the unit waiting for action phase (showing which unit is currently waiting for instructions)

3. The most obvious thing is that some modifications have been added to the scene.

4. Because the Gif does not have any sound effects, it actually has scenes and various attack sound effects.

The special effects resources are also the free KTK Effect Sample Set.unitypackage found in the Assest Store. Regarding the use of special effects prefabs and parameter modifications, just read the tutorials, and I will not introduce them here.

The changes involved this time are as follows:

The UnitStats script adds a variable to distinguish unit attack types.

    public int attackType = 0;          //单位攻击类型,0为近战,1远程

BattleTurnSystem script,

···When the operating unit is selected, the special effects prefab is instantiated according to the current position of the unit.

···Delete the special effects after obtaining the attack target information (the player's instructions have been entered)

Below are the several functions that have been changed. The first is the modification in the ToBattle function when obtaining the current action object.

public void ToBattle()
    {
        remainingEnemyUnits = GameObject.FindGameObjectsWithTag("EnemyUnit");
        remainingPlayerUnits = GameObject.FindGameObjectsWithTag("PlayerUnit");

        //检查存活敌人单位
        if (remainingEnemyUnits.Length == 0)
        {
            Debug.Log("敌人全灭,战斗胜利");
            endImageText.text = "战斗胜利";
            endImageText.color = Color.green;
            endImage.SetActive(true);           //显示胜利界面
        }
        //检查存活玩家单位
        else if (remainingPlayerUnits.Length == 0)
        {
            Debug.Log("我方全灭,战斗失败");
            endImageText.text = "战斗失败";
            endImageText.color = Color.red;
            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())
            {
                //在该单位脚底下显示特效
                thisPartical = Instantiate(partical_show, currentActUnit.transform.position, Quaternion.identity) as GameObject;
                //选取攻击目标
                FindTarget();
            }
            else
            {
                //Debug.Log("目标死亡,跳过回合");
                ToBattle();
            }
        }
    }

Then there is the destruction special effect after obtaining the attack target. Since players and monsters select targets in different ways, there are two destruction commands that need to be added.

void FindTarget()
    {
        if (currentActUnit.tag == "EnemyUnit")
        {
            //如果行动单位是怪物则从存活玩家对象中随机一个目标
            int targetIndex = Random.Range(0, remainingPlayerUnits.Length);
            currentActUnitTarget = remainingPlayerUnits[targetIndex];

            Destroy(thisPartical);          //选择完目标后删除标识当前行动单位的特效

            attackTypeName = "无情撕咬";
            //如果是远程单位直接在这里LaunchAttack,就不需要RunToTarget
            if (currentActUnit.GetComponent<UnitStats>().attackType == 1)
            {
                LaunchAttack();
            }
            else
            {
                
                RunToTarget();
            }
        }
        else if (currentActUnit.tag == "PlayerUnit")
        {
            isWaitForPlayerToChooseSkill = true;
        }
    }

as well as

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;
                    isWaitForPlayerToChooseTarget = false;

                    Destroy(thisPartical);          //选择完目标后删除标识当前行动单位的特效

                    //如果是远程单位直接在这里LaunchAttack,就不需要RunToTarget
                    if (currentActUnit.GetComponent<UnitStats>().attackType == 1)
                    {
                        LaunchAttack();
                    }
                    else
                    {
                        RunToTarget();
                    }
                }
            }
        }

        if (isUnitRunningToTarget)
        {
            currentActUnit.transform.LookAt(currentActUnitTargetPosition);           //单位移动的朝向
            distanceToTarget = Vector3.Distance(currentActUnitTargetPosition, currentActUnit.transform.position);           //到目标的距离,需要实时计算
            //避免靠近目标时抖动
            if (distanceToTarget > 1)
            {
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", true);
                currentActUnit.transform.Translate(Vector3.forward * unitMoveSpeed * Time.deltaTime, Space.Self);         //Time.deltaTime保证速度单位是每秒
            }
            else
            {
                //停止移动
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", false);
                //关闭移动状态
                isUnitRunningToTarget = false;
                //记录停下的位置
                currentactUnitStopPosition = currentActUnit.transform.position;
                //开始攻击
                LaunchAttack();
            }

        }

        if (isUnitRunningBack)
        {
            currentActUnit.transform.LookAt(currentActUnitInitialPosition);            //回来的朝向
            distanceToInitial=Vector3.Distance(currentActUnit.transform.position, currentActUnitInitialPosition);           //离初始位置的距离
            if (distanceToInitial > 1)
            {
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", true);
                currentActUnit.transform.Translate(Vector3.forward * unitMoveSpeed * Time.deltaTime, Space.Self);         //Time.deltaTime保证速度单位是每秒
            }
            else
            {
                //停止移动
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", false);
                //关闭移动状态
                isUnitRunningBack = false;
                //修正到初始位置和朝向
                currentActUnit.transform.position = currentActUnitInitialPosition;
                currentActUnit.transform.rotation = currentActUnitInitialRotation;

                //攻击单位回原位后行动结束,到下一个单位
                ToBattle();
            }
        }
    }


The complete BattleTurnSystem script is posted below. There are some small optimizations in the middle (such as dynamically delaying the attack according to the duration of the attack animation after the attack action). I can’t remember clearly, so I won’t list them one by one.

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;          //当前行动的单位
    public GameObject currentActUnitTarget;            //当前行动的单位的目标

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

    private Vector3 currentActUnitInitialPosition;         //当前行动单位的初始位置
    private Quaternion currentActUnitInitialRotation;         //当前行动单位的初始朝向
    private Vector3 currentActUnitTargetPosition;           //当前行动单位目标的位置
    public bool isUnitRunningToTarget = false;            //玩家是否为移动至目标状态
    public bool isUnitRunningBack = false;          //玩家是否为移动回原点状态
    private float distanceToTarget;         //当前行动单位到攻击目标的距离
    private float distanceToInitial;         //当前行动单位到初始位置的距离
    public float unitMoveSpeed = 1f;         //单位战斗中的移动速度
    private Vector3 currentactUnitStopPosition;         //当前行动单位的移动后停下的位置

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

    private GameObject endImage;            //游戏结束画面
    private Text endImageText;          //结束画面文本内容

    public GameObject bloodText;           //保存血条预制体

    public GameObject partical_show;            //展示当前行动单位的特效
    private GameObject thisPartical;            //存储生成的特效

    /// <summary>
    /// 创建初始参战列表,存储参战单位,并进行一次出手排序
    /// </summary>
    void Start ()
    {
        //获取结束菜单的引用,并禁用结束菜单
        endImage = GameObject.Find("ResultImage");
        endImageText = endImage.transform.Find("ResultText").GetComponent<Text>();
        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("敌人全灭,战斗胜利");
            endImageText.text = "战斗胜利";
            endImageText.color = Color.green;
            endImage.SetActive(true);           //显示胜利界面
        }
        //检查存活玩家单位
        else if (remainingPlayerUnits.Length == 0)
        {
            Debug.Log("我方全灭,战斗失败");
            endImageText.text = "战斗失败";
            endImageText.color = Color.red;
            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())
            {
                //在该单位脚底下显示特效
                thisPartical = Instantiate(partical_show, currentActUnit.transform.position, Quaternion.identity) as GameObject;
                //选取攻击目标
                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];

            Destroy(thisPartical);          //选择完目标后删除标识当前行动单位的特效

            attackTypeName = "无情撕咬";
            //如果是远程单位直接在这里LaunchAttack,就不需要RunToTarget
            if (currentActUnit.GetComponent<UnitStats>().attackType == 1)
            {
                LaunchAttack();
            }
            else
            {
                
                RunToTarget();
            }
        }
        else if (currentActUnit.tag == "PlayerUnit")
        {
            isWaitForPlayerToChooseSkill = true;
        }
    }

    /// <summary>
    /// 攻击者移动到攻击目标前
    /// </summary>
    void RunToTarget()
    {
        currentActUnitInitialPosition = currentActUnit.transform.position;
        currentActUnitInitialRotation = currentActUnit.transform.rotation;         //保存移动前的位置和朝向,因为跑回来还要用
        currentActUnitTargetPosition = currentActUnitTarget.transform.position;         //目标的位置
        //开启移动状态
        isUnitRunningToTarget = true;
        //移动的控制放到Update里,因为要每一帧判断离目标的距离
    }

    /// <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;
                    isWaitForPlayerToChooseTarget = false;

                    Destroy(thisPartical);          //选择完目标后删除标识当前行动单位的特效

                    //如果是远程单位直接在这里LaunchAttack,就不需要RunToTarget
                    if (currentActUnit.GetComponent<UnitStats>().attackType == 1)
                    {
                        LaunchAttack();
                    }
                    else
                    {
                        RunToTarget();
                    }
                }
            }
        }

        if (isUnitRunningToTarget)
        {
            currentActUnit.transform.LookAt(currentActUnitTargetPosition);           //单位移动的朝向
            distanceToTarget = Vector3.Distance(currentActUnitTargetPosition, currentActUnit.transform.position);           //到目标的距离,需要实时计算
            //避免靠近目标时抖动
            if (distanceToTarget > 1)
            {
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", true);
                currentActUnit.transform.Translate(Vector3.forward * unitMoveSpeed * Time.deltaTime, Space.Self);         //Time.deltaTime保证速度单位是每秒
            }
            else
            {
                //停止移动
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", false);
                //关闭移动状态
                isUnitRunningToTarget = false;
                //记录停下的位置
                currentactUnitStopPosition = currentActUnit.transform.position;
                //开始攻击
                LaunchAttack();
            }

        }

        if (isUnitRunningBack)
        {
            currentActUnit.transform.LookAt(currentActUnitInitialPosition);            //回来的朝向
            distanceToInitial=Vector3.Distance(currentActUnit.transform.position, currentActUnitInitialPosition);           //离初始位置的距离
            if (distanceToInitial > 1)
            {
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", true);
                currentActUnit.transform.Translate(Vector3.forward * unitMoveSpeed * Time.deltaTime, Space.Self);         //Time.deltaTime保证速度单位是每秒
            }
            else
            {
                //停止移动
                currentActUnit.GetComponent<Animator>().SetBool("Is_move", false);
                //关闭移动状态
                isUnitRunningBack = false;
                //修正到初始位置和朝向
                currentActUnit.transform.position = currentActUnitInitialPosition;
                currentActUnit.transform.rotation = currentActUnitInitialRotation;

                //攻击单位回原位后行动结束,到下一个单位
                ToBattle();
            }
        }
    }
    
    /// <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();

        //获取攻击动画长度
        float attackAnimationTime=currentActUnit.GetComponent<Animator>().GetCurrentAnimatorStateInfo(0).length;
        float damageAnimationTime = attackAnimationTime*0.4f;

        Debug.Log(currentActUnit.name + "使用技能(" + attackTypeName + ")对" + currentActUnitTarget.name+"造成了"+ attackData + "点伤害");
        
        //在对象承受伤害前添加延迟(伤害在动作砍下去就得出现)
        StartCoroutine(WaitForTakeDamage(damageAnimationTime));

        //下一个单位行动前延时
        StartCoroutine(WaitForRunBack(attackAnimationTime));
    }

    /// <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(float time)
    {
        yield return new WaitForSeconds(time);

        //被攻击者承受伤害
        currentActUnitTarget.GetComponent<UnitStats>().ReceiveDamage(attackData);

        //实例化伤害字体并设置到画布上(字体位置和内容的控制放在它自身的脚本中)
        GameObject thisText = Instantiate(bloodText) as GameObject;
        thisText.transform.SetParent(GameObject.Find("BloodTextGroup").transform, false);

        if (!currentActUnitTarget.GetComponent<UnitStats>().IsDead())
        {
            currentActUnitTarget.GetComponent<Animator>().SetTrigger("TakeDamage");
        }
        else
        {
            currentActUnitTarget.GetComponent<Animator>().SetTrigger("Dead");
        }

    }

    IEnumerator WaitForRunBack(float time)
    {
        yield return new WaitForSeconds(time);
        //远程单位不需要跑回来,延迟后直接下个单位行动
        if (currentActUnit.GetComponent<UnitStats>().attackType == 1)
        {
            ToBattle();
        }
        else
        {
            //此时开启跑回状态
            isUnitRunningBack = true;
        }
        
    }
}

At this point, the learning and implementation of the combat part comes to an end.

Comrades who are also learning Unity, if you have any questions, you can leave a message and learn and progress together.


 

Guess you like

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