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

原创 2017年08月13日 13:59:30

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

上一篇文章里实现了较为初级的回合制战斗系统,仅限与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()输出文字校验即可;

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

版权声明:本文为博主原创文章,未经博主允许不得转载。

Unity3d即时战斗之敌人AI和角色攻击

角色控制脚本Character.cs,为角色添加角色控制器Character controller。 using UnityEngine; using System.Collections; pu...

Unity3D 战斗技能系统架构

大家在玩游戏的时候,经常会看到各种技能特效,远近攻击,非常绚丽,角色的打击感非常吸引玩家,其实这就是我们通常说的战斗技能系统,大家如果第一次去实现这个战斗系统,可能感觉无处下手,再被策划的文案一搞,云...

MMORPG中技能战斗系统的技术分享

 (1)技能施法时,client只有一个上行的请求施法包,后续施法的过程全由server来驱动下发施法各阶段的结果信息,如吟唱、效果伤害、命中信息等等;     (2)弹道类技能是由serve...
  • wfziyou
  • wfziyou
  • 2015年05月05日 18:01
  • 888

Unity快速实现回合制游戏

对少时步步高RPG回合制游戏的甚是怀念,但是现在找一些当时的游戏来玩发现画面已经完全吸引不了我了,特别是最原始的黑白版伏魔记、侠客行之类的,剧情甚是不错但是却没有了要玩的激情,所以就想在自己业余的时间...

【Unity3D】回合制游戏

回合制游戏一直在游戏史,至少是在中国的游戏历史上扮演很重要的角色。从仙剑到梦幻,这类游戏深受玩家喜爱。那么在Unity3D中怎么实现呢?下面用一个比较简单Unity3D的一对一回合制游戏来说明这个问题...

[Unity3D]Unity3D游戏开发之回合制游戏原型的实现

大家好,欢迎大家关注我的博客,我是秦元培,我的博客地址是blog.csdn.net/qinyuanpei。       今天呢,我们来说说回合制。博主曾经坦言自己是一个喜欢国产RPG游戏的人,《仙剑奇...

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

本人游戏策划一枚,爱好游戏设计开发. 今天的这篇名字升级到“高级篇”了,因为战斗这块已经实现了原本的学习预期,接下来准备去搞搞角色创建、数据存储和主场景控制这些东西了。 这次高级篇的效果请看Gif: ...

今天开始做战斗,回合制战斗代码实现第三篇,特殊的回合制游戏Slg(策略战棋)

喜欢玩火焰纹章吗?【这个是我最喜欢的游戏】fc时期放假的时候,可以一次玩一天的游戏,连续玩几周,梦幻模拟战,最早接触的电脑游戏《天使帝国》(我这个人比较奇怪,人家都是先玩红警,我最早接触的游戏除了48...

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

本人游戏策划一枚,爱好游戏设计开发 最近学习了Unity之后,结合各种网上的教材和资料后自己实现了一套简单的回合制战斗系统,如下 系统实现效果简介: 1. 目前为1V1的固定回合战斗 ...

#游戏unity-音之国度#战斗系统中的回合制

#游戏unity-音之国度#战斗系统中的回合制转载需标明出处在上一篇博客中,我们介绍了实现回合制的代码中的新内容——协程,这篇博客我们就真正的来实现一个回合制游戏;首先,一般的回合制游戏都是自己与敌人...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Unity 回合制战斗系统(中级篇)
举报原因:
原因补充:

(最多只允许输入30个字)