用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);
- }
- }
做到這一步就可以像演示視訊中那樣點選按鈕出兵了。
放上工程連結:
https://pan.baidu.com/s/1T2nZ_FrIk9DaTvem-YH8nQ提取碼:n61s
下一篇文章我們將做UI介面點選頭像在場景中生成防禦塔,以及不同的防禦塔與敵人的互動。
有意向參與線下游戲開發學習的童鞋,歡迎訪問http://levelpp.com/
作者:四五二十
專欄地址:https://zhuanlan.zhihu.com/p/65206955
相關文章
- Unity《ATD》塔防RPG類3D遊戲架構設計(一)Unity3D遊戲架構
- Unity《ATD》塔防RPG類3D遊戲架構設計(二)Unity3D遊戲架構
- API設計中防重放攻擊API
- 遊戲伺服器防ddos攻擊,三招搞定ddos攻擊遊戲伺服器
- Unity遊戲示例來了,用Unity開源遊戲資源做遊戲,遊戲開發不再難!Unity遊戲開發
- 塔防PVP手遊設計的困局(下):防守、進攻兩難全
- 《變數》的策劃日誌(上):如何設計一個塔防遊戲?變數遊戲
- Spring Boot介面如何設計防篡改、防重放攻擊Spring Boot
- 簡單塔防小遊戲遊戲
- 【Unity3D開發小遊戲】《戰棋小遊戲》Unity開發教程Unity3D遊戲
- 塔防PVP手遊設計的困局(上):關於“逆塔防”與“正反饋”
- 俄羅斯方塊+塔防 國產獨立遊戲《方境戰記》5月25日發售遊戲
- React 防注入攻擊 XSS攻擊 (放心大膽的用吧)React
- 氣球塔防6 for Mac(BloonsTD6 塔防闖關遊戲)Mac遊戲
- 遊戲開發與設計遊戲開發
- 如何開發一款棋牌遊戲?棋牌遊戲平臺搭建遊戲
- 鄒偉:如何開發一款小遊戲遊戲
- 研發一款VR射擊遊戲,需要注意這些VR遊戲
- 逃離塔科夫:一款不是血賺就是白給的硬核生存射擊遊戲遊戲
- DDoS攻擊、CC攻擊的攻擊方式和防禦方法
- 《Splitgate》開發者:怎樣做一款有新意的競技射擊遊戲?遊戲
- 3、攻擊防範
- XXE攻擊攻擊原理是什麼?如何防禦XXE攻擊?
- Unity遊戲框架設計之單例MonoUnity遊戲框架單例Mono
- DDoS攻擊頻發,科普防禦DDoS攻擊的幾大有效方法
- 釣魚攻擊防不勝防,該如何預防網路釣魚攻擊?
- 遊戲開發—協議設計遊戲開發協議
- 如何有效防禦DDoS攻擊和CC攻擊?
- 攻擊面管理預防網路攻擊原理?
- 納稅塔防射擊遊戲《熔鐵少女》主機版今日在多平臺正式發售遊戲
- 從零開始手把手教你使用javascript+canvas開發一個塔防遊戲01地圖建立JavaScriptCanvas遊戲地圖
- Akamai:過去一年遊戲中共發生了3072次DDoS攻擊AI遊戲
- csrf攻擊與防範
- CSRF 攻擊與防禦
- WEB攻擊與防禦Web
- CSRF攻擊與防禦
- 什麼是SSRF攻擊?如何防禦SSRF攻擊?
- 什麼是DDoS攻擊?如何防範DDoS攻擊?