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

项目文件找出来了,老版本的脚本有报错,我在新版2019.4.21f1c1下解决了报错,战斗场景可以正常跑的。

需要的同学点下面地址下载(关注就行啦不用积分),祝大家都早日学成

项目包下载

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

今天的这篇名字升级到“高级篇”了,因为战斗这块已经实现了原本的学习预期,接下来准备去搞搞角色创建、数据存储和主场景控制这些东西了。

这次高级篇的效果请看Gif:

新增内容如下:

1. 增加了远程兵种--弓箭手

2. 在单位等待行动阶段添加了特效(显示当前等待指令的是那个单位)

3. 最明显的就是场景加了些修饰

4. Gif因为没有音效不带感,实际是有场景和各种攻击音效的

特效资源也是从Assest Store找的免费的KTK Effect Sample Set.unitypackage,关于特效预制体的使用和参数修改看看教程就行,这里就不介绍啦。

本次涉及修改的内容如下:

UnitStats脚本,增加了一个用于区别单位攻击类型的变量。

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

BattleTurnSystem脚本,

···在选中操作单位时根据单位当前position实例化特效预制体

···在得到攻击目标信息后(玩家的指令已经输入完毕)删除特效

下面先标出改动的几个函数,首先是ToBattle函数中获取当前行动对象时的修改

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

然后是获取到攻击目标后销毁特效,由于玩家和怪物的选取目标方式不一样,所以有2处要增加销毁命令

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

以及

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


下面贴一下完整的BattleTurnSystem脚本,中间有些小的优化(像攻击动作之后,根据攻击动画时长动态延迟受击)记不太清楚了,就不一一列出来了

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

至此战斗部分的学习和实现告一段落。

同样在学习Unity的同志们如果有疑问可以留言哈,共同学习进步


 

猜你喜欢

转载自blog.csdn.net/c252270036/article/details/77270062