Unity快速上手系列之4:《塔防》

作者:四五二十
大家好。偶爾想起了這個(gè)手把手教學(xué)的、但現(xiàn)已長(zhǎng)滿雜草的坑,還是來(lái)挖幾鏟子。

這一期的游戲是最常見(jiàn)的類(lèi)型之一——塔防。
塔防游戲相信大家并不陌生,幾個(gè)主要元素如下:
1、敵方士兵
2、我方防御塔
3、我方主城
emmmmmmm好像就沒(méi)了。
玩法就是建立防御塔阻擊前往我方主城的敵兵,可以通過(guò)視頻直觀感受下:

人越狠,話越不多。不多說(shuō),接下來(lái)我們一步步把這幾個(gè)功能做完。
素材準(zhǔn)備:
網(wǎng)上隨便找一些資源就行,不一定要和我一樣。這里再次強(qiáng)調(diào):
網(wǎng)上獲取的資源一定不能用作商業(yè)用途?。。。。?!
就本工程而言,資源有一下幾種:
敵人2個(gè),分別擁有移動(dòng),攻擊,待機(jī),死亡四種動(dòng)畫(huà)

防御塔3個(gè),擁有待機(jī),攻擊兩種動(dòng)畫(huà)

主城1個(gè),主地形 1組(內(nèi)含各種雜草亂石 )

敵人地形(敵人能用來(lái)走的路)1種,防御塔地形(防御塔能放置的地方)1種

箭矢1個(gè)

場(chǎng)景搭建:
先從簡(jiǎn)單的功能做起:讓敵人從生成點(diǎn)走到主城,看見(jiàn)主城就攻擊。
搭建一個(gè)簡(jiǎn)單場(chǎng)景:

敵人和主城有一個(gè)都有血量的屬性,都會(huì)被攻擊,這里為它們做能顯示在頭上的血條。
以主城為例,在主城的子節(jié)點(diǎn)層創(chuàng)建一個(gè)Sprite做黃血條,設(shè)為黃色,取名“BloodStrip”,調(diào)整好大?。?/p>
然后在BloodStrip的子節(jié)點(diǎn)層創(chuàng)建一個(gè)空物體,取名“Hp”,在Hp的子節(jié)點(diǎn)層再創(chuàng)建一個(gè)Sprite做紅血條,名字“Red”,設(shè)為紅色,大小和黃血條一樣,把黃血色覆蓋:

接下來(lái)就移動(dòng)紅血條位置,讓它左邊邊緣與父物體Hp的Y軸重合:

然后再將Hp往右移動(dòng),讓Y軸與黃血條左邊緣重合(紅血條剛好覆蓋黃血條):

這樣我們只需要設(shè)置Hp的X軸大小,就可以控制紅血條長(zhǎng)度了:

***這里請(qǐng)初學(xué)者注意,如果你選取的紅血條圖片資源不是純色的、是有其他花紋的,則不能用這個(gè)方法。原因很簡(jiǎn)單,這種方法會(huì)把花紋拉長(zhǎng)或壓扁。大家可以下來(lái)想一下:這種情況下應(yīng)該怎樣來(lái)設(shè)置?
后面在代碼中只需要將當(dāng)前血量與總血量的比值賦給Hp的X軸,就可以將血量信息顯示在界面上了。敵人血條做法一樣。
做好后讓BloodStrip處于禁用狀態(tài),受傷后才顯示(這是游戲UI顯示的一個(gè)約定俗成的規(guī)則)。
代碼編寫(xiě):
為主城與敵人創(chuàng)建一個(gè)基類(lèi)腳本Character:
public class Character : MonoBehaviour
{
??? public float totalHp = 100; //總血量
??? float surHp; //剩余血量
??? protected Transform hpObj; //黃血條
??? protected Transform redHp; //血條紅條
??? protected Transform mainCamera; //主攝像機(jī)
?
??? public virtual void Init() //初始化
??? {
??????? surHp = totalHp;
??????? hpObj = transform.Find("BloodStrip");
??????? redHp = hpObj.Find("Hp");
??????? mainCamera = GameObject.Find("Main Camera").transform;
??? }
??? public void Damage(float damage) //受傷方法,參數(shù)為受到的傷害值
??? {
??????? if (surHp > damage) //當(dāng)前血量大于受傷血量,正??垩?/p>
??????? {
??????????? surHp -= damage;
??????????? //受傷后開(kāi)始顯示血條
??????????? if (surHp < totalHp)
??????????????? hpObj.gameObject.SetActive(true);
??????????? Vector3 hpScale = redHp.localScale;
??????????? hpScale.x = surHp / totalHp;
??????????? redHp.localScale = hpScale;
??????? }
??????? else //當(dāng)前血量不夠,調(diào)用死亡方法?????????
??????????? Death();
??? }
??? public virtual void Death() //死亡方法
??? {
??????? surHp = 0;
??????? hpObj.gameObject.SetActive(false); //血條不再顯示
??? }
}
?
創(chuàng)建主調(diào)腳本:用于游戲初始化和記錄游戲死亡,掛在一個(gè)場(chǎng)景物體上:
public class GameMain : MonoBehaviour
{
??? public static GameMain instance;
??? public bool gameOver;
??? void Start()
??? {
??????? InitGame();
??? }
??? //初始化游戲
??? void InitGame()
??? {
??????? instance = this; //單例
??????? gameOver = false;
??? }
}
?
創(chuàng)建主城腳本,繼承自Character腳本:
public class MainCity : Character
{
??? void Start()
??? {
??????? Init();
??? }
??? private void Update()
??? {
??????? hpObj.rotation = mainCamera.rotation; //血條始終面向鏡頭
??? }
??? public override void Death() //重新死亡方法
??? {
??????? base.Death();
????? ??GameMain.instance.gameOver = true; //游戲結(jié)束
??? }
}
?
敵人的腳本也繼承自Charater,除了受傷和死亡之外還能攻擊與移動(dòng):
public class Enemy : Character
{
??? Animator anim;
??? public float damage; //傷害
??? public float speed; //移動(dòng)速度
??? MainCity target; //主城
??? public override void Init()
??? {
??????? base.Init();
??? ????anim = GetComponent<Animator>();
??? }
??? private void Update()
??? {
??????? hpObj.rotation = mainCamera.rotation; //血條始終面向鏡頭
??? }
??? //前進(jìn)方法
??? private void EnemyForward()
??? {
??? }
??? //攻擊方法(放在攻擊動(dòng)畫(huà)事件中)
??? private void EnemyAttack()
??? {
?? ?????if (target != null)
??????????? target.Damage(damage);
??? }
??? //死亡方法
??? public override void Death()
??? {
??????? base.Death();
??????? anim.Play("death");
??? }
??? //尸體消失
??? private void DestroySelf()
??? {
??????? Destroy(gameObject);
??? }
}
?
重點(diǎn)在移動(dòng)方法上。因?yàn)閿橙说囊苿?dòng)帶有尋路功能,這里沒(méi)有采取Unity自帶的NavMeshAgent,而是用腳本來(lái)實(shí)現(xiàn),主要思路仿照盲人的行進(jìn)方式,利用射線充當(dāng)導(dǎo)盲棍,發(fā)現(xiàn)前方道路中斷再?gòu)膬蛇呎倚碌男羞M(jìn)路線:

要利用好這個(gè)思路,場(chǎng)景中道路的搭建也有一定要求,道路都要掛上MeshCollider組件,方便射線檢測(cè)。

道路的物體層設(shè)置為“Way”,主城也掛上碰撞器,物體層設(shè)為“City”。

在敵人模型身上創(chuàng)建一個(gè)空物體為眼睛,取名為“Eye”,主要作用是從此為射線起始點(diǎn),位置合適即可,注意,因?yàn)樗袛橙硕加玫南嗤_本,所以所有敵人的眼睛高度距離地面相同:

當(dāng)然每個(gè)敵人也請(qǐng)掛上碰撞器和剛體以及Animator組件:

創(chuàng)建一個(gè)敵人狀態(tài)機(jī):
public enum EnemyState //狀態(tài)機(jī)
{
??? forward,
??? attack,
??? death
}
?
重寫(xiě)初始化方法:
??? Animator anim;
??? Rigidbody rigid;
??? public EnemyState state;
??? Transform eye; //眼睛:用于觀測(cè)道路和攻擊目標(biāo)
??? List<Collider> ways; //記錄走過(guò)的路(不走回頭路)
??? //重新初始化方法
??? public override void Init()
??? {
??????? base.Init();
???
??????? anim = GetComponent<Animator>();
??????? rigid = GetComponent<Rigidbody>();
??????? gameObject.layer = LayerMask.NameToLayer("Enemy"); //敵人層設(shè)置為"Enemy"
??????? state = EnemyState.forward;
????? ??eye = transform.Find("Eye");
??????? ways = new List<Collider>();
??? }
?
編寫(xiě)移動(dòng)方法,并在Update中調(diào)用:
private void Update()
??? {
??????? hpObj.rotation = mainCamera.rotation; //血條始終面向鏡頭
??????? if (GameMain.instance.gameOver) //游戲結(jié)束播放待機(jī)動(dòng)畫(huà)
??????????? anim.Play("idle");
??????? else if (state == EnemyState.forward)
??????????? EnemyForward();
??? }
??? public int view; //視野
??? Quaternion wayDir; //前進(jìn)方向
??? MainCity target; //主城
??? Transform way; //正在走的路
??? public float speed;
??? //前進(jìn)方法
??? private void EnemyForward()
??? {
??????? RaycastHit hit;
??????? //看見(jiàn)攻擊目標(biāo)則攻擊
??????? if (Physics.Raycast(eye.position, transform.forward, out hit, view, LayerMask.GetMask("City")))
??????? {
??????????? state = EnemyState.attack;
??????????? anim.Play("attack");
??? ????????target = hit.collider.GetComponent<MainCity>();
??????? }
?
??????? //斜下方30°打射線檢測(cè)前方道路
??????? if (Physics.Raycast(eye.position, Quaternion.AngleAxis(30, transform.right)
??????????? * transform.forward, out hit, 50, LayerMask.GetMask("Way")))
???? ???{
??????????? Debug.DrawLine(eye.position, hit.point, Color.blue);
??????????? //發(fā)現(xiàn)未走過(guò)的道路,獲取該道路,朝向該路通往的方向
??????????? if (!ways.Contains(hit.collider))
??????????? {
??????????????? ways.Add(hit.collider);
??????????????? way = hit.transform;
???????? ???????wayDir = Quaternion.LookRotation(way.forward);
??????????? }
??????? }
??????? else //前方?jīng)]路了發(fā)射球形射線檢測(cè)周?chē)欠裼新?/p>
??????? {
??????????? Collider[] colliders = Physics.OverlapSphere(transform.position, 8, LayerMask.GetMask("Way"));
??????????? for (int i = 0; i < colliders.Length; i++)
??????????? {
??????????????? //發(fā)現(xiàn)未走過(guò)的道路,獲取該道路,朝向該路通往的方向
??????????????? if (!ways.Contains(colliders[i]))
??????????????? {
??????????????????? way = colliders[i].transform;
??????????????????? wayDir = Quaternion.LookRotation(way.forward);
??????????????????? break;
??????????????? }
??????????? }
??????? }
??????? //獲取與腳下道路x軸上偏差值,好讓自身走在路中間
??????? float offset = 0;
??????? if (way != null)
??????? {
??????????? Vector3 distance = transform.position - way.position;
???????? ???offset = Vector3.Dot(distance, way.right.normalized);
??????? }
??????? //面向該路指向的方向前進(jìn)
??????? transform.rotation = Quaternion.RotateTowards(transform.rotation, wayDir, speed * 20 * Time.deltaTime);
??????? transform.Translate(-offset * Time.deltaTime, 0, speed * Time.deltaTime);
??? }
?
暫時(shí)把初始化方法放在Start中調(diào)用(后面我們會(huì)在創(chuàng)建的時(shí)候初始化),然后設(shè)置好血量、視野、速度、傷害,主城也設(shè)置好血量:

先來(lái)看下尋路運(yùn)行效果:

尋路沒(méi)有問(wèn)題了,將攻擊動(dòng)畫(huà)設(shè)為循環(huán)播放,然后將攻擊方法放入攻擊動(dòng)畫(huà)事件中,敵人看到主城就會(huì)自動(dòng)攻擊了:

敵人主要功能就已經(jīng)完成。現(xiàn)在我們來(lái)做敵人生成器。
塔防游戲的敵人生成方式一般都是比較有規(guī)律的,比如先生成一組a敵人,跟著生成一組b敵人,每組敵人的生成間隔也恒定(當(dāng)然,讀者也可以自己嘗試更豐富的出兵方法,比如讓“某些特定敵人的血量減到某個(gè)閾值”作為觸發(fā)條件等等):

為了生成方便,我們來(lái)做一個(gè)定時(shí)器,可以重復(fù)并規(guī)律地調(diào)用一個(gè)生成敵人方法:
public class Util : MonoBehaviour
{
??? private static Util _Instance = null;
??? public static Util Instance //單例模式,依附GameObject
??? {
??????? get
??????? {
??????????? if (_Instance == null)
??????????? {
??????????????? GameObject obj = new GameObject("Util");
??????????? ????_Instance = obj.AddComponent<Util>();
??????????? }
??????????? return _Instance;
??????? }
??? }
??? public class TimeTask //定時(shí)事件類(lèi)
??? {
??????? public Action callback; //回調(diào)函數(shù)
??????? public float delayTime; //延遲長(zhǎng)度
??????? public float destTime; //延遲后的目標(biāo)時(shí)間
??????? public int count; //重復(fù)次數(shù)
??? }???????????????
??? List<TimeTask> timeTaskList = new List<TimeTask>(); //保存所有的定時(shí)事件??
??? //增加定時(shí)回調(diào)的方法
??? public void AddTimeTask(Action _callback, float _delayTime, int _count = 1)????
??? {
??????? timeTaskList.Add(new TimeTask()
??????? {
??????????? callback = _callback,
??????????? delayTime = _delayTime,
??????????? destTime = Time.realtimeSinceStartup + _delayTime,
??????????? count = _count
??????? });
??? }
??? private void Update()
??? {
??????? for (int i = 0; i < timeTaskList.Count; i++) //實(shí)時(shí)監(jiān)測(cè)所有定時(shí)事件
??????? {
??????????? TimeTask task = timeTaskList[i];
??????????? if (Time.realtimeSinceStartup >= task.destTime) //時(shí)間到了,則執(zhí)行
??????????? {
??????????????? task.callback?.Invoke();
??????????????? if (task.count == 1) //當(dāng)次數(shù)為1,執(zhí)行完移除該定時(shí)事件
??????????????????? timeTaskList.RemoveAt(i);
??????????????? else if (task.count > 1) //當(dāng)次數(shù)大于1,執(zhí)行完次數(shù)減1
??????????????????? task.count--;
??????????????? task.destTime += task.delayTime; //執(zhí)行完一次后,重新定出下次執(zhí)行時(shí)間
??????????? }
??????? }
??? }
}
?
把所有敵人放入一個(gè)路徑中:

創(chuàng)建一個(gè)空物體做敵人生成器,放在敵人生成點(diǎn),創(chuàng)建腳本掛上去:
public class EnemySystem : MonoBehaviour
{
??? //根據(jù)名稱(chēng)保存所有敵人
Dictionary<string, Enemy> enemyDict = new Dictionary<string, Enemy>();
//初始化,放在主調(diào)腳本GameMain中執(zhí)行
??? public void Init()
??? {
?????? ?//保存所有種類(lèi)敵人,可以根據(jù)名字獲取
??????? Enemy[] enemys = Resources.LoadAll<Enemy>("Prefab/Chara/EnemyChara");
??????? for (int i = 0; i < enemys.Length; i++)
??????? {
??????????? if (!enemyDict.ContainsKey(enemys[i].name))
??????????????? enemyDict.Add(enemys[i].name, enemys[i]);
??????? }
??? }
??? //生成敵人,參數(shù)中設(shè)置敵人種類(lèi),生成間隔,生成數(shù)量(默認(rèn)為1)
??? public void CreateEnemy(string name, float delay, int count = 1)
??? {
??????? if (GameMain.instance.gameOver == false)
??????????? //使用定時(shí)器,生成敵人
??????????? Util.Instance.AddTimeTask(() => Instantiate(
??????????? enemyDict[name], transform.position, transform.rotation).Init(),
??????????? delay, count);
}
??? //點(diǎn)擊按鈕生成敵人(掛在按鈕事件中)
??? public void ClickButtonDispatchTroops()
??? {
??????? //每秒生成一個(gè)敵人,生成5次,第一次生成在1秒后執(zhí)行
??????? CreateEnemy("Zombie1", 1, 5);
??????? //沒(méi)0.5秒生成一個(gè)敵人,生成10次,第一次生成在5.5秒后執(zhí)行
??????? Util.Instance.AddTimeTask(() => CreateEnemy("Zombie2", 0.5f, 10), 5);
??? }
}
?
做到這一步就可以像演示視頻中那樣點(diǎn)擊按鈕出兵了。
放上工程鏈接:https://pan.baidu.com/share/init?surl=T2nZ_FrIk9DaTvem-YH8nQ
提取碼:n61s
下一篇文章我們將做UI界面點(diǎn)擊頭像在場(chǎng)景中生成防御塔,以及不同的防御塔與敵人的交互。
想系統(tǒng)學(xué)習(xí)游戲開(kāi)發(fā)的童鞋,歡迎訪問(wèn):http://levelpp.com/????
另有專(zhuān)業(yè)開(kāi)發(fā)交(gao)流(ji)群等待大家強(qiáng)勢(shì)插入:869551769