Unity快速上手系列之4:《塔防》續(xù)

作者:四五二十
大家好。
最近狀態(tài)比較佳,趁這個(gè)機(jī)會(huì)繼續(xù)豐富咱們的塔防游戲。

對(duì)了,如大家覺(jué)得哪里沒(méi)能理解,歡迎在評(píng)論區(qū)給我留言。
在上一篇中,我們已經(jīng)能夠通過(guò)生成器產(chǎn)生敵人,這些敵人能自動(dòng)尋路到達(dá)主城所在位置進(jìn)行攻擊。主城被攻破后游戲結(jié)束。攻擊方已經(jīng)具備。
接下來(lái)是防御方了。這里,咱們建立防御塔阻止敵人的進(jìn)攻。首先說(shuō)說(shuō)本例中三種防御塔的攻擊方式:
弓箭手:遠(yuǎn)程攻擊,對(duì)敵人射出弓箭造成傷害,弓箭可以插在敵人身上;

錘兵:群體攻擊,錘擊地面,原地?fù)麸w敵人,減慢敵人移動(dòng)速度;

劍士:近程攻擊。

那么從弓箭手開(kāi)始,來(lái)做他的攻擊功能:

弓箭手攻擊時(shí)會(huì)從“槍口”處發(fā)出弓箭,所以先在弓箭手模型上創(chuàng)建一個(gè)槍口Muzzle:

槍口要隨弓箭移動(dòng),在Hierarchy面板中大概在這里:

為了獲取這個(gè)未知層級(jí)的子物體,也為了讓其它類(lèi)也方便調(diào)用,我們先寫(xiě)一個(gè)工具類(lèi),里面創(chuàng)建一個(gè)查找未知層級(jí)子物體的方法:
public class ToolsMethod
{
??? private static ToolsMethod _Instance;
??? public static ToolsMethod Instance //單例
??? {
??????? get
??????? {
??????????? if (_Instance == null)
??????????????? _Instance = new ToolsMethod();
??????????? return _Instance;
??????? }
??? }
??? //根據(jù)名稱獲取未知層級(jí)子物體
??? public Transform FindChildByName(Transform currentTF, string childName)
??? {
??????? Transform childTF = currentTF.Find(childName);
??????? if (childTF != null) return childTF;
??????? for (int i = 0; i < currentTF.childCount; i++)
??????? {
??????????? childTF = FindChildByName(currentTF.GetChild(i), childName);
??????????? if (childTF != null) return childTF;
??????? }
??????? return null;
??? }
}
?
弓箭手的腳本中還需要拿到箭矢的預(yù)制體,先把箭矢預(yù)制體放入路徑中:

為箭矢預(yù)制體創(chuàng)建一個(gè)腳本Bullet掛上去:
public class Bullet : MonoBehaviour
{
}
?
為了讓箭矢和其它物體可以使用對(duì)象池,我們先創(chuàng)建一個(gè)空物體,作為所有對(duì)象池的管理器。取名“PoolManager”,創(chuàng)建對(duì)象池類(lèi):
public class GameObjectPool
{
??? private static GameObjectPool _Instance;
? ??public static GameObjectPool Instance //單例模式
??? {
??????? get
??????? {
??????????? if (_Instance == null)
??????????????? _Instance = new GameObjectPool();
??????????? return _Instance;
??????? }
??? }
??? //用于保存所有對(duì)象池
??? public Dictionary<string, Transform> poolDict = new Dictionary<string, Transform>();
??? //獲取對(duì)象池
??? public Transform GetPool(string poolName)
??? {
??????? if (poolDict.ContainsKey(poolName))
??????????? return poolDict[poolName];
??????? //字典中沒(méi)有重新創(chuàng)建
??????? Transform poolObj = new GameObject(poolName + "_Pool").transform;
??????? //創(chuàng)建的對(duì)象池放入對(duì)象池管理器的子物體中
??????? poolObj.SetParent(GameObject.Find("PoolManager").transform);
??????? poolObj.gameObject.SetActive(false);
??????? poolDict.Add(poolName, poolObj);
??????? return poolObj;
??? }
}
?
其它物體需要對(duì)象池也可以調(diào)用該類(lèi)的方法。
弓箭手需要有一個(gè)攻擊范圍。敵人進(jìn)入該范圍后才會(huì)被認(rèn)定為目標(biāo)并展開(kāi)攻擊。
首先將敵人都放在Enemy層,創(chuàng)建弓箭手的腳本:
public class Pagoda : MonoBehaviour
{
??? protected Animator anim;
??? protected Transform muzzle;
??? Bullet Arrow;
??? Transform arrowPool; //箭矢對(duì)象池
??? //初始化
??? public void initPagoda()
???? {
??????? enabled = true; //啟用腳本
??????? anim = GetComponentInChildren<Animator>();
??????? muzzle = ToolsMethod.Instance.FindChildByName(transform, "Muzzle");
??????? Arrow = Resources.Load<Bullet>("Prefab/Bullet/Arrow");
??????? //為箭矢創(chuàng)建一個(gè)以它命名的對(duì)象池
??????? arrowPool = GameObjectPool.Instance.GetPool(Arrow.name);
???? }
??? private void Update()
???? {
??????? //游戲結(jié)束,停止攻擊
??????? if (GameMain.instance.gameOver)
??????? {
??????????? anim.SetBool("Attack", false);
??????????? return;
??????? }
??????? GetTarget();
??? }
??? public float attactRange; //攻擊范圍
??? public float damage; //傷害值
??? protected Enemy target; //攻擊目標(biāo)
??? //獲取攻擊目標(biāo)
??? void GetTarget()
??? {
??????? if (target == null) //攻擊目標(biāo)為空時(shí),用球形射線檢測(cè)Enemy層找尋攻擊目標(biāo)
??????? {
??????????? Collider[] enemys = Physics.OverlapSphere(transform.position, attactRange, LayerMask.GetMask("Enemy"));
??????????? if (enemys.Length == 0)
??????????????? anim.SetBool("Attack", false);
??????????? //發(fā)現(xiàn)敵人,設(shè)為目標(biāo),進(jìn)行攻擊(播放攻擊動(dòng)畫(huà))
??????????? for (int i = 0; i < enemys.Length;)
??????????? {
??????????????? target = enemys[i].GetComponent<Enemy>();
??????????????? anim.SetBool("Attack", true);
??????????????? break;
??????????? }
??????? }
??????? else
??????? {
??????????? //面向攻擊目標(biāo)
??????????? Vector3 pos = target.transform.position;
??????????? Quaternion dir = Quaternion.LookRotation(new Vector3(pos.x, transform.position.y, pos.z) - transform.position);
??????????? transform.rotation = Quaternion.Lerp(transform.rotation, dir, 0.1f);
??????????? //攻擊目標(biāo)離開(kāi)攻擊范圍或死亡,重新獲取攻擊目標(biāo)
??????????? if (Vector3.Distance(target.transform.position, transform.position) >= attactRange || target.state == EnemyState.death)
??????????????? target = null;
??????? }
??? }
??? //攻擊方法(放在攻擊動(dòng)畫(huà)事件中)
??? public virtual void PagodaAttack()
??? {
??????? //在槍口位置創(chuàng)建箭矢
??? }
}
?
弓箭手的初始化在弓箭手創(chuàng)建時(shí)調(diào)用。
如果弓箭手已經(jīng)能檢測(cè)敵人并發(fā)射箭矢,接下來(lái)就是箭矢的功能了。主要如下:
1. 飛向目標(biāo)(始終面向目標(biāo),并向前飛);
2. 打到目標(biāo),調(diào)用目標(biāo)受傷方法(用距離判斷是否打到);
3. 插在目標(biāo)身上(認(rèn)目標(biāo)做父物體,停止移動(dòng))
要讓箭矢能插在敵人身上,需要在敵人模型上創(chuàng)建一個(gè)空物體做打擊點(diǎn),取名HitPos,為了效果逼真,HitPos最好放在模型骨骼上,并可以在敵人腳本中聲明一個(gè)hitPos,使用查找未知層級(jí)子物體的方法來(lái)獲?。?/p>
Transform hitPos = ToolsMethod.Instance.FindChildByName(transform, "HitPos");
?
當(dāng)弓箭手檢測(cè)到敵人創(chuàng)建箭矢時(shí),同時(shí)將敵人信息、傷害值、箭矢對(duì)象池賦給箭矢,由箭矢去做接下來(lái)的工作,如傷害敵人,它的腳本可以這樣寫(xiě):
public class Bullet : MonoBehaviour
{
??? public float speed;
?
??? Enemy target; //攻擊目標(biāo)
??? float damage; //傷害值
??? Transform pool; //對(duì)象池
??? Vector3 initPos; //初始位置
??? //初始化
??? public void InitBullet(Vector3 position, Quaternion rotation, Enemy _target, float _damage, Transform _pool)
??? {
??????? transform.SetParent(null);
??????? transform.position = position;
??????? transform.rotation = rotation;
??????? target = _target;
??????? damage = _damage;
??????? pool = _pool;
??????? initPos = transform.position;
??? }
??? private void Update()
??? {
??????? if (transform.parent == null) //沒(méi)有射中目標(biāo),繼續(xù)飛
??????? {
??????????? transform.Translate(0, 0, speed * Time.deltaTime);
??????????? if (Vector3.Distance(initPos, transform.position) > 500) //飛出一定范圍自動(dòng)銷(xiāo)毀
??????????????? DestroySelf();
?
??????????? if (target != null && target.state != EnemyState.death) //如果目標(biāo)活著朝向目標(biāo)
??????????? {
??????????????? transform.LookAt(target.hitPos);
??????????????? //到達(dá)有效范圍,調(diào)用目標(biāo)受傷方法,成為目標(biāo)子物體(插在目標(biāo)身上)
??????????????? if (Vector3.Distance(target.hitPos.position, transform.position) <= 1)
??????????????? {
??????????????????? target.Damage(damage);
??????????????????? transform.SetParent(target.hitPos);
??????????????? }
??????????? }
??????? }
??????? else if (target.state == EnemyState.death) //射中后,只要目標(biāo)一死就銷(xiāo)毀
??????????? DestroySelf();
??? }
??? //銷(xiāo)毀自身(進(jìn)入對(duì)象池)
??? private void DestroySelf()
??? {
??????? transform.SetParent(pool);
??? }
}
?
箭矢的移動(dòng)速度在編輯器界面自行設(shè)定,在弓箭手的攻擊方法中,就可以在創(chuàng)建箭矢的同時(shí)把相關(guān)信息賦給它:
??? //攻擊方法(放在攻擊動(dòng)畫(huà)事件中)
??? public virtual void PagodaAttack()
??? {
??????? //如果對(duì)象池有,則從對(duì)象池取子彈,否則重新實(shí)例化
??????? //設(shè)定位置,方向,攻擊目標(biāo),傷害值,所在對(duì)象池
??????? if (arrowPool.childCount > 0)
??????????? arrowPool.GetChild(0).GetComponent<Bullet>().InitBullet(muzzle.position, muzzle.rotation, target, damage, arrowPool);
??????? else
??????????? Instantiate(Arrow).InitBullet(muzzle.position, muzzle.rotation, target, damage, arrowPool);
??? }
?
弓箭手做完,接下來(lái)是錘子兵的功能:

錘子兵的功能和弓箭手非常相似。除了攻擊方式不同,其它都一樣。所以我們創(chuàng)建錘子兵的腳本可以繼承自弓箭手的腳本:
public class Pagoda2 : Pagoda
{
??? public float force; //擊飛力度
??? public ParticleSystem effect; //擊飛特效
??? //重寫(xiě)攻擊方法(在攻擊動(dòng)畫(huà)事件中調(diào)用)
??? public override void PagodaAttack()
??? {
??????? //群體攻擊,作用范圍始攻擊范圍的一半
??????? Collider[] enemys = Physics.OverlapSphere(muzzle.position, attactRange / 2, LayerMask.GetMask("Enemy"));
??????? for (int i = 0; i < enemys.Length; i++)
??????? {
??????????? //傷害作用范圍內(nèi)的每個(gè)敵人
??????????? Enemy enemy = enemys[i].GetComponent<Enemy>();
??????????? enemy.Damage(damage);???????
??????????? //播放特效
??????????? effect.transform.position = muzzle.position;
??????????? effect.Play();
??????????? //擊飛方法
??????? }
??? }
}
?
除了對(duì)敵人造成傷害之外,需要專(zhuān)門(mén)寫(xiě)一個(gè)擊飛的方法,擊飛方法可以寫(xiě)在錘子兵的腳本里,也可以在敵人腳本(Enemy)中寫(xiě)一個(gè)被擊飛方法:
??? //被擊飛方法
??? bool isFly; //是否被擊飛(處于擊飛狀態(tài)時(shí)不能再被擊飛)
??? public void StrikeFly(float force)
??? {
??????? if (isFly == false) //未被擊飛狀態(tài)下才可以被擊飛
??????? {
??????????? isFly = true;
??????????? rigid.AddForce(Vector3.up * force, ForceMode.Impulse);
??????????? float initSpeed = speed; //初始速度
??????????? speed = 0;
??????????? //0.5秒后恢復(fù)
??????????? Util.Instance.AddTimeTask(() =>
??????????? {
??????????????? speed = initSpeed;
??????????????? isFly = false;
??????????? }, 0.5f);
??????? }
??? }
?
該方法是公開(kāi)屬性,在錘子兵那邊調(diào)用。
然后是劍士的功能:

劍士功能最簡(jiǎn)單,但增加了一個(gè)暴擊的屬性,將暴擊率代入攻擊力的計(jì)算就好,腳本也繼承自弓箭手:
public class Pagoda3 : Pagoda
{
??? public float critChance = 0.2f; //暴擊率
??? //重新攻擊方法
??? public override void PagodaAttack()
??? {
??????? if (target != null)
??????? {
??????????? //代入暴擊率,計(jì)算最終傷害(暴擊是雙倍傷害)
??????????? int crit = (int)(critChance * 100);
??????????? target.Damage(damage * (Random.Range(0, 100) < crit ? 2 : 1), this);
??????? }
??? }
}
?
是不是很簡(jiǎn)單?
好了,三個(gè)人形防御塔的功能都做完了?,F(xiàn)在正式做安放防御塔的功能,用Image搭建一個(gè)防御塔菜單UI界面,放入精靈圖片,取名“PagodaMenu”:

在PagodaMenu下創(chuàng)建三個(gè)Image做頭像:

在道路旁擺上若干的防御塔地形,將層設(shè)為Pagoda:

我們先來(lái)看下放置的過(guò)程:

通過(guò)演示,我們大概可以理清創(chuàng)建的邏輯:
1. 點(diǎn)擊頭像實(shí)例化一個(gè)防御塔,并顯示攻擊范圍;
2. 按住鼠標(biāo)不放防御塔會(huì)跟隨鼠標(biāo)移動(dòng);
3. 攻擊范圍的顏色在可放置位置顯示為綠色,其余地方為紅色;
4. 在可放置位置彈起鼠標(biāo)時(shí),會(huì)將防御塔放在地形上,且同時(shí)為防御塔初始化。
根據(jù)以上的邏輯順序,我們首先要讓圖片具有可點(diǎn)擊事件與彈起事件。為頭像Image創(chuàng)建一個(gè)腳本,引入相應(yīng)接口:
public class IconElement : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
??? //點(diǎn)擊事件
??? public void OnPointerDown(PointerEventData eventData)
??? {
??? }
??? //彈起事件
??? public void OnPointerUp(PointerEventData eventData)
??? {
??? }
}
?
然后可以從主攝像打出射線,將射線檢測(cè)點(diǎn)的坐標(biāo)賦給防御塔,可以放在在Update中調(diào)用。攻擊范圍的顯示可以通過(guò)創(chuàng)建一個(gè)普通球形物體來(lái)實(shí)現(xiàn),后面在代碼中調(diào)整顏色就行了。位置也跟隨射線檢測(cè)點(diǎn)移動(dòng),平時(shí)處于禁用狀態(tài),只有在擺放防御塔過(guò)程中調(diào)用:

現(xiàn)在將所有防御塔預(yù)制體放入路徑中:

預(yù)制體名字和頭像圖片的名字相同,且一一對(duì)應(yīng):

為Icon的腳本創(chuàng)建初始化方法:
??? string pagodaName; //防御塔名,用來(lái)加載防御塔
??? Camera mainCamera; //主攝像
??? Transform attRange; //攻擊范圍顯示器
??? Material ria; //范圍顯示器的材質(zhì)球
??? LayerMask layer; //射線可照射層
??? //初始化
??? public void Init(Camera _mainCamera, Transform _attRange)
??? {
??????? pagodaName = GetComponent<Image>().sprite.name; //自身圖片的名字就是對(duì)應(yīng)防御塔名字
??????? mainCamera = _mainCamera;
??????? attRange = _attRange;
??????? ria = attRange.GetComponent<MeshRenderer>().material;
??????? layer = LayerMask.GetMask("Ground") | LayerMask.GetMask("Way") | LayerMask.GetMask("Pagoda");
??? }
?
然后在點(diǎn)擊事件中寫(xiě)入點(diǎn)擊時(shí)要執(zhí)行的功能:
??? Pagoda pagodaObj; //防御塔實(shí)例???
//點(diǎn)擊頭像實(shí)例化防御塔
??? public void OnPointerDown(PointerEventData eventData)
??? {
??????? //加載防御塔模型
??????? pagodaObj = Instantiate(Resources.Load<Pagoda>("Prefab/Chara/PagodaChara/" + pagodaName));
??????? //啟用攻擊范圍顯示器并將防御塔攻擊方位反映在尺寸上
??????? attRange.gameObject.SetActive(true);
??????? attRange.localScale = new Vector3(pagodaObj.attactRange * 2, 10, pagodaObj.attactRange * 2);
??????? GetComponent<Image>().color = new Color(0, 1, 0); //頭像變色
??? }
?
彈起事件中根據(jù)條件判斷當(dāng)前是否可放置防御塔,判斷邏輯放在Update中:
??? bool isPlace; //是否可放置
??? Transform terrain; //可放置地形
??? //抬起鼠標(biāo)放置或刪除防御塔
??? public void OnPointerUp(PointerEventData eventData)
??? {
??????? if (isPlace) //可放置時(shí)
??????? {
??????????? //放置在該地形并成為地形子物體,然后初始化
??????????? pagodaObj.transform.position = terrain.position;
??????????? pagodaObj.transform.SetParent(terrain);
??????????? pagodaObj.initPagoda();
??????? }
??????? else //不可放置則銷(xiāo)毀
??????????? Destroy(pagodaObj.gameObject);
?
??????? attRange.gameObject.SetActive(false); //禁用范圍顯示器
??????? pagodaObj = null;
??????? GetComponent<Image>().color = new Color(1, 1, 1); //頭像變色
??? }
??? void Update()
??? {
??????? //如果防御塔實(shí)例化,則找尋可以放置的位置
??????? if (pagodaObj != null)
??????? {
??????????? //攝像機(jī)向鼠標(biāo)位置發(fā)射線
??????????? Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
??????????? RaycastHit hit;
??????????? if (Physics.Raycast(ray, out hit, 500, layer))
??????????? {
??????????????? pagodaObj.transform.position = hit.point; //防御塔模型根據(jù)鼠標(biāo)移動(dòng)
??????????????? attRange.position = hit.point; //范圍顯示器根據(jù)鼠標(biāo)移動(dòng)
??????????????? int index = hit.collider.gameObject.layer; //獲取照射到物體的層
??????????????? //如果是可以放置的地形,并且該地形上沒(méi)有其它防御塔,就可以放置
??????????????? if (LayerMask.LayerToName(index) == "Pagoda" && hit.collider.transform.childCount == 0)
??????????????? {
??????????????????? isPlace = true;
??????????????????? terrain = hit.collider.transform;
??????????????????? ria.color = new Color(0, 1, 0, 0.3f);
?????????????? ?}
??????????????? else
??????????????? {
??????????????????? isPlace = false;
??????????????????? ria.color = new Color(1, 0, 0, 0.3f);
??????????????? }
??????????? }
??????? }
??? }
?
好的,頭像功能的腳本就做完了。頭像的數(shù)量可以根據(jù)防御塔具體數(shù)量增減。我們注意到每個(gè)頭像的初始化方法沒(méi)地方調(diào)用,可以創(chuàng)建一個(gè)管理類(lèi)來(lái)對(duì)它們統(tǒng)一初始化,將它掛在PagodaMenu上:
public class PagodaMenu : MonoBehaviour
{
??? public Camera mainCamera; //主攝像機(jī)
??? public Transform attRange; //攻擊范圍顯示器
??? public void Init()
??? {
??????? IconElement[] icons = GetComponentsInChildren<IconElement>();
??????? for (int i = 0; i < icons.Length; i++)
??????? {
??????????? icons[i].Init(mainCamera, attRange);
? ??????}
??? }
}
?
主攝像機(jī)和范圍顯示器在編輯器界面直接拖入。
到這里,我們之前在第一篇文章演示視頻里的功能就做完了。之后可能會(huì)做一些經(jīng)濟(jì)系統(tǒng)方面的功能,如消滅敵人可以獲得金錢(qián)、使用金錢(qián)購(gòu)買(mǎi)和升級(jí)防御塔等等。
工程鏈接:https://pan.baidu.com/share/init?surl=e8UOSkOtG7hr93t2kl3xnA
提取碼:oshk
有意向參與線下游戲開(kāi)發(fā)學(xué)習(xí)的童鞋,歡迎訪問(wèn):http://levelpp.com/
皮皮關(guān)的游戲開(kāi)發(fā)QQ群也歡迎各位強(qiáng)勢(shì)插入:869551769