本人游戏策划一枚,爱好游戏设计开发
上一篇文章里实现了较为初级的回合制战斗系统,仅限与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();
ownerText.text = owner.name;
//获取UI控制脚本的引用
uiManager = GameObject.Find("BattleUIManager").GetComponent();
}
void Update()
{
if (owner.tag=="PlayerUnit" || owner.tag == "EnemyUnit")
{
//更新血条长度
ownerBloodFill = gameObject.transform.Find("BloodFill").GetComponent();
ownerBloodFill.fillAmount = owner.GetComponent().bloodPercent;
//更新血条位置
playerBlood3DPosition = owner.transform.position + new Vector3(uiManager.bloodXOffeset, uiManager.bloodYOffeset, uiManager.bloodZOffeset);
playerBlood2DPosition = Camera.main.WorldToScreenPoint(playerBlood3DPosition);
gameObject.GetComponent().position = playerBlood2DPosition;
}
if (owner.GetComponent().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;
}
}
这个脚本内容较多,先拆解说明下:
///
/// 创建初始参战列表,存储参战单位,并进行一次出手排序
///
void Start ()
///
/// 判断战斗进行的条件是否满足,取出参战列表第一单位,并从列表移除该单位,单位行动
/// 行动完后重新添加单位至队列,继续ToBattle()
///
public void ToBattle()
///
/// 查找攻击目标,如果行动者是怪物则从剩余玩家中随机
/// 如果行动者是玩家,则获取鼠标点击对象
///
///
void FindTarget()
///
/// 攻击者移动到攻击目标前(暂时没有做这块)
///
void RunToTarget()
///
/// 绘制玩家选择技能的窗口
///
void OnGUI()
///
/// 技能选择窗口的回调函数
///
///
void PlayerSkillChoose(int ID)
///
/// 用于控制玩家选择目标状态的开启
///
void Update()
///
/// 当前行动单位执行攻击动作
///
public void LaunchAttack()
///
/// 对参战单位根据攻速计算值进行出手排序
///
void listSort()
///
/// 延时操作函数,避免在怪物回合操作过快
///
///
IEnumerator WaitForTakeDamage()
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class BattleTurnSystem : MonoBehaviour {
private List 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; //游戏结束画面
///
/// 创建初始参战列表,存储参战单位,并进行一次出手排序
///
void Start ()
{
//禁用结束菜单
endImage = GameObject.Find("ResultImage");
endImage.SetActive(false);
//创建参战列表
battleUnits = new List();
//添加玩家单位至参战列表
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();
}
///
/// 判断战斗进行的条件是否满足,取出参战列表第一单位,并从列表移除该单位,单位行动
/// 行动完后重新添加单位至队列,继续ToBattle()
///
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();
//判断取出的战斗单位是否存活
if (!currentActUnitStats.IsDead())
{
//选取攻击目标
FindTarget();
}
else
{
//Debug.Log("目标死亡,跳过回合");
ToBattle();
}
}
}
///
/// 查找攻击目标,如果行动者是怪物则从剩余玩家中随机
/// 如果行动者是玩家,则获取鼠标点击对象
///
///
void FindTarget()
{
if (currentActUnit.tag == "EnemyUnit")
{
//如果行动单位是怪物则从存活玩家对象中随机一个目标
int targetIndex = Random.Range(0, remainingPlayerUnits.Length);
currentActUnitTarget = remainingPlayerUnits[targetIndex];
LaunchAttack();
}
else if (currentActUnit.tag == "PlayerUnit")
{
isWaitForPlayerToChooseSkill = true;
}
}
///
/// 攻击者移动到攻击目标前(暂时没有做这块)
///
void RunToTarget()
{
}
///
/// 绘制玩家选择技能的窗口
///
void OnGUI()
{
if (isWaitForPlayerToChooseSkill == true)
{
GUI.Window(1, new Rect(Screen.width / 2 + 300, Screen.height / 2+100, 100, 100), PlayerSkillChoose, "选择技能");
}
}
///
/// 技能选择窗口的回调函数
///
///
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("请选择攻击目标......");
}
}
///
/// 用户控制玩家选择目标状态的开启
///
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();
}
}
}
}
///
/// 当前行动单位执行攻击动作
///
public void LaunchAttack()
{
//存储攻击者和攻击目标的属性脚本
UnitStats attackOwner = currentActUnit.GetComponent();
UnitStats attackReceiver = currentActUnitTarget.GetComponent();
//根据攻防计算伤害
attackData = (attackOwner.attack - attackReceiver.defense + Random.Range(-2, 2)) * attackDamageMultiplier;
//播放攻击动画
currentActUnit.GetComponent().SetTrigger("Attack");
currentActUnit.GetComponent().Play();
Debug.Log(currentActUnit.name + "使用技能(" + attackTypeName + ")对" + currentActUnitTarget.name+"造成了"+ attackData + "点伤害");
//在对象承受伤害并进入下个单位操作前前添加1s延迟
StartCoroutine("WaitForTakeDamage");
}
///
/// 对参战单位根据攻速计算值进行出手排序
///
void listSort()
{
GameObject temp = battleUnits[0];
for (int i = 0; i < battleUnits.Count - 1; i++)
{
float minVal = battleUnits[i].GetComponent().attackTrun; //假设i下标的是最小的值
int minIndex = i; //初始认为最小的数的下标
for (int j = i + 1; j < battleUnits.Count; j++)
{
if (minVal > battleUnits[j].GetComponent().attackTrun)
{
minVal = battleUnits[j].GetComponent().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);
}
}
///
/// 延时操作函数,避免在怪物回合操作过快
///
///
IEnumerator WaitForTakeDamage()
{
//被攻击者承受伤害
currentActUnitTarget.GetComponent().ReceiveDamage(attackData);
if (!currentActUnitTarget.GetComponent().IsDead())
{
currentActUnitTarget.GetComponent().SetTrigger("TakeDamage");
}
else
{
currentActUnitTarget.GetComponent().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().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().owner = enemyUnit;
}
}
public void GoToScene(string name)
{
SceneManager.LoadScene(name);
}
}
private GameObject endImage; //游戏结束画面
这块的内容注视掉,直接Debug.Log()输出文字校验即可;
结束按钮的回调也可以不用添加;