生活随笔
收集整理的這篇文章主要介紹了
用Unity开发一款塔防游戏(一):攻击方设计
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
大家好。偶爾想起了這個手把手教學的、但現已長滿雜草的坑,還是來挖幾鏟子。
?
這一期的游戲是最常見的類型之一——塔防。
塔防游戲相信大家并不陌生,幾個主要元素如下:
1、敵方士兵
2、我方防御塔
3、我方主城
emmmmmmm好像就沒了。
玩法就是建立防御塔阻擊前往我方主城的敵兵,可以通過視頻直觀感受下:
演示視頻:https://www.zhihu.com/video/1110139144373776384?autoplay=false&useMSE=
人越狠,話越不多。不多說,接下來我們一步步把這幾個功能做完。
素材準備:
網上隨便找一些資源就行,不一定要和我一樣。這里再次強調:
網上獲取的資源一定不能用作商業用途!!!!!!
就本工程而言,資源有一下幾種:
敵人2個,分別擁有移動,攻擊,待機,死亡四種動畫
?
?
防御塔3個,擁有待機,攻擊兩種動畫
?
人形防御塔可還行
主城1個,主地形1組(內含各種雜草亂石)
?
敵人地形(敵人能用來走的路)1種,防御塔地形(防御塔能放置的地方)1種
?
箭矢1個
?
弓兵模型中自帶
場景搭建:
先從簡單的功能做起:讓敵人從生成點走到主城,看見主城就攻擊。
搭建一個簡單場景:
?
為了檢測敵人尋路,最好是能轉彎的道路
敵人和主城有一個都有血量的屬性,都會被攻擊,這里為它們做能顯示在頭上的血條。
以主城為例,在主城的子節點層創建一個Sprite做黃血條,設為黃色,取名“BloodStrip”,調整好大小:
?
然后在BloodStrip的子節點層創建一個空物體,取名“Hp”,在Hp的子節點層再創建一個Sprite做紅血條,名字“Red”,設為紅色,大小和黃血條一樣,把黃血色覆蓋:
?
接下來就移動紅血條位置,讓它左邊邊緣與父物體Hp的Y軸重合:
?
然后再將Hp往右移動,讓Y軸與黃血條左邊緣重合(紅血條剛好覆蓋黃血條):
?
這樣我們只需要設置H的X軸大小,手機靚號賣號就可以控制紅血條長度了:
***這里請初學者注意,如果你選取的紅血條圖片資源不是純色的、是有其他花紋的,則不能用這個方法。原因很簡單,這種方法會把花紋拉長或壓扁。大家可以下來想一下:這種情況下應該怎樣來設置?
后面在代碼中只需要將當前血量與總血量的比值賦給Hp的X軸,就可以將血量信息顯示在界面上了。敵人血條做法一樣。
做好后讓BloodStrip處于禁用狀態,受傷后才顯示(這是游戲UI顯示的一個約定俗成的規則)。
代碼編寫:
為主城與敵人創建一個基類腳本Character:
?
public class Character : MonoBehaviour{? ? public float totalHp = 100; //總血量? ? float surHp; //剩余血量? ? protected Transform hpObj; //黃血條? ? protected Transform redHp; //血條紅條? ? protected Transform mainCamera; //主攝像機? ? 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) //受傷方法,參數為受到的傷害值? ? {? ?? ???if (surHp > damage) //當前血量大于受傷血量,正常扣血? ?? ???{? ?? ?? ?? ?surHp -= damage;? ?? ?? ?? ?//受傷后開始顯示血條? ?? ?? ?? ?if (surHp < totalHp)? ?? ?? ?? ?? ? hpObj.gameObject.SetActive(true);? ?? ?? ?? ?Vector3 hpScale = redHp.localScale;? ?? ?? ?? ?hpScale.x = surHp / totalHp;? ?? ?? ?? ?redHp.localScale = hpScale;? ?? ???}? ?? ???else //當前血量不夠,調用死亡方法? ?? ?? ?? ?? ?? ?? ?Death();? ? }? ? public virtual void Death() //死亡方法? ? {? ?? ???surHp = 0;? ?? ???hpObj.gameObject.SetActive(false); //血條不再顯示? ? }} 復制代碼
創建主調腳本:用于游戲初始化和記錄游戲死亡,掛在一個場景物體上:
?
public class GameMain : MonoBehaviour{? ? public static GameMain instance;? ? public bool gameOver;? ? void Start()? ? {? ?? ???InitGame();? ? }? ? //初始化游戲? ? void InitGame()? ? {? ?? ???instance = this; //單例? ?? ???gameOver = false;? ? }} 復制代碼
創建主城腳本,繼承自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; //游戲結束? ? }} 復制代碼
敵人的腳本也繼承自Charater,除了受傷和死亡之外還能攻擊與移動:
?
public class Enemy : Character{? ? Animator anim;? ? public float damage; //傷害? ? public float speed; //移動速度? ? MainCity target; //主城? ? public override void Init()? ? {? ?? ???base.Init();? ?? ???anim = GetComponent<Animator>();? ? }? ? private void Update()? ? {? ?? ???hpObj.rotation = mainCamera.rotation; //血條始終面向鏡頭? ? }? ? //前進方法? ? private void EnemyForward()? ? {? ? }? ? //攻擊方法(放在攻擊動畫事件中)? ? private void EnemyAttack()? ? {? ?? ???if (target != null)? ?? ?? ?? ?target.Damage(damage);? ? }? ? //死亡方法? ? public override void Death()? ? {? ?? ???base.Death();? ?? ???anim.Play("death");? ? }? ? //尸體消失? ? private void DestroySelf()? ? {? ?? ???Destroy(gameObject);? ? }} 復制代碼
重點在移動方法上。因為敵人的移動帶有尋路功能,這里沒有采取Unity自帶的NavMeshAgent,而是用腳本來實現,主要思路仿照盲人的行進方式,利用射線充當導盲棍,發現前方道路中斷再從兩邊找新的行進路線:
?
拐杖就是射線
要利用好這個思路,場景中道路的搭建也有一定要求,道路都要掛上MeshCollider組件,方便射線檢測。
?
所有道路的Z軸指向路線前進方向
道路的物體層設置為“Way”,主城也掛上碰撞器,物體層設為“City”。
?
在敵人模型身上創建一個空物體為眼睛,取名為“Eye”,主要作用是從此為射線起始點,位置合適即可,注意,因為所有敵人都用的相同腳本,所以所有敵人的眼睛高度距離地面相同:
?
正面看這些模型真特么驚悚
當然每個敵人也請掛上碰撞器和剛體以及Animator組件:
?
創建一個敵人狀態機:
?
public enum EnemyState //狀態機{? ? forward,? ? attack,? ? death} 復制代碼
重寫初始化方法:
?
??Animator anim;? ? Rigidbody rigid;? ? public EnemyState state;? ? Transform eye; //眼睛:用于觀測道路和攻擊目標? ? List<Collider> ways; //記錄走過的路(不走回頭路)? ? //重新初始化方法? ? public override void Init()? ? {? ?? ???base.Init();? ?? ?? ???anim = GetComponent<Animator>();? ?? ???rigid = GetComponent<Rigidbody>();? ?? ???gameObject.layer = LayerMask.NameToLayer("Enemy"); //敵人層設置為"Enemy"? ?? ???state = EnemyState.forward;? ?? ???eye = transform.Find("Eye");? ?? ???ways = new List<Collider>();? ? } 復制代碼
編寫移動方法,并在Update中調用:
?
private void Update()? ? {? ?? ???hpObj.rotation = mainCamera.rotation; //血條始終面向鏡頭? ?? ???if (GameMain.instance.gameOver) //游戲結束播放待機動畫? ?? ?? ?? ?anim.Play("idle");? ?? ???else if (state == EnemyState.forward)? ?? ?? ?? ?EnemyForward();? ? }? ? public int view; //視野? ? Quaternion wayDir; //前進方向? ? MainCity target; //主城? ? Transform way; //正在走的路? ? public float speed;? ? //前進方法? ? private void EnemyForward()? ? {? ?? ???RaycastHit hit;? ?? ???//看見攻擊目標則攻擊? ?? ???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°打射線檢測前方道路? ?? ???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);? ?? ?? ?? ?//發現未走過的道路,獲取該道路,朝向該路通往的方向? ?? ?? ?? ?if (!ways.Contains(hit.collider))? ?? ?? ?? ?{? ?? ?? ?? ?? ? ways.Add(hit.collider);? ?? ?? ?? ?? ? way = hit.transform;? ?? ?? ?? ?? ? wayDir = Quaternion.LookRotation(way.forward);? ?? ?? ?? ?}? ?? ???}? ?? ???else //前方沒路了發射球形射線檢測周圍是否有路? ?? ???{? ?? ?? ?? ?Collider[] colliders = Physics.OverlapSphere(transform.position, 8, LayerMask.GetMask("Way"));? ?? ?? ?? ?for (int i = 0; i < colliders.Length; i++)? ?? ?? ?? ?{? ?? ?? ?? ?? ? //發現未走過的道路,獲取該道路,朝向該路通往的方向? ?? ?? ?? ?? ? 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);? ?? ???}? ?? ???//面向該路指向的方向前進? ?? ???transform.rotation = Quaternion.RotateTowards(transform.rotation, wayDir, speed * 20 * Time.deltaTime);? ?? ???transform.Translate(-offset * Time.deltaTime, 0, speed * Time.deltaTime);? ? } 復制代碼
暫時把初始化方法放在Start中調用(后面我們會在創建的時候初始化),然后設置好血量、視野、速度、傷害,主城也設置好血量:
?
先來看下尋路運行效果:
?
藍線檢測前方道路,紅圈檢測周圍道路
尋路沒有問題了,將攻擊動畫設為循環播放,然后將攻擊方法放入攻擊動畫事件中,敵人看到主城就會自動攻擊了:
?
敵人主要功能就已經完成。現在我們來做敵人生成器。
塔防游戲的敵人生成方式一般都是比較有規律的,比如先生成一組a敵人,跟著生成一組b敵人,每組敵人的生成間隔也恒定(當然,讀者也可以自己嘗試更豐富的出兵方法,比如讓“某些特定敵人的血量減到某個閾值”作為觸發條件等等):
?
為了生成方便,我們來做一個定時器,可以重復并規律地調用一個生成敵人方法:
?
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 //定時事件類? ? {? ?? ???public Action callback; //回調函數? ?? ???public float delayTime; //延遲長度? ?? ???public float destTime; //延遲后的目標時間? ?? ???public int count; //重復次數? ? }? ?? ?? ?? ?? ?? ? List<TimeTask> timeTaskList = new List<TimeTask>(); //保存所有的定時事件? ?? ? //增加定時回調的方法? ? 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++) //實時監測所有定時事件? ?? ???{? ?? ?? ?? ?TimeTask task = timeTaskList[i];? ?? ?? ?? ?if (Time.realtimeSinceStartup >= task.destTime) //時間到了,則執行? ?? ?? ?? ?{? ?? ?? ?? ?? ? task.callback?.Invoke();? ?? ?? ?? ?? ? if (task.count == 1) //當次數為1,執行完移除該定時事件? ?? ?? ?? ?? ?? ???timeTaskList.RemoveAt(i);? ?? ?? ?? ?? ? else if (task.count > 1) //當次數大于1,執行完次數減1? ?? ?? ?? ?? ?? ???task.count--;? ?? ?? ?? ?? ? task.destTime += task.delayTime; //執行完一次后,重新定出下次執行時間? ?? ?? ?? ?}? ?? ???}? ? }} 復制代碼
把所有敵人放入一個路徑中:
?
創建一個空物體做敵人生成器,放在敵人生成點,創建腳本掛上去:
?
public class EnemySystem : MonoBehaviour{? ? //根據名稱保存所有敵人Dictionary<string, Enemy> enemyDict = new Dictionary<string, Enemy>();//初始化,放在主調腳本GameMain中執行? ? public void Init()? ? {? ?? ???//保存所有種類敵人,可以根據名字獲取? ?? ???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]);? ?? ???}? ? }? ? //生成敵人,參數中設置敵人種類,生成間隔,生成數量(默認為1)? ? public void CreateEnemy(string name, float delay, int count = 1)? ? {? ?? ???if (GameMain.instance.gameOver == false)? ?? ?? ?? ?//使用定時器,生成敵人? ?? ?? ?? ?Util.Instance.AddTimeTask(() => Instantiate(? ?? ?? ?? ?enemyDict[name], transform.position, transform.rotation).Init(),? ?? ?? ?? ?delay, count);}? ? //點擊按鈕生成敵人(掛在按鈕事件中)? ? public void ClickButtonDispatchTroops()? ? {? ?? ???//每秒生成一個敵人,生成5次,第一次生成在1秒后執行? ?? ???CreateEnemy("Zombie1", 1, 5);? ?? ???//沒0.5秒生成一個敵人,生成10次,第一次生成在5.5秒后執行? ?? ???Util.Instance.AddTimeTask(() => CreateEnemy("Zombie2", 0.5f, 10), 5);? ? }} 復制代碼
做到這一步就可以像演示視頻中那樣點擊按鈕出兵了。
?
總結
以上是生活随笔為你收集整理的用Unity开发一款塔防游戏(一):攻击方设计的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。