日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

用Unity开发一款塔防游戏(一):攻击方设计

發(fā)布時間:2024/8/26 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 用Unity开发一款塔防游戏(一):攻击方设计 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.


大家好。偶爾想起了這個手把手教學的、但現(xiàn)已長滿雜草的坑,還是來挖幾鏟子。
?


這一期的游戲是最常見的類型之一——塔防。

塔防游戲相信大家并不陌生,幾個主要元素如下:

1、敵方士兵

2、我方防御塔

3、我方主城

emmmmmmm好像就沒了。

玩法就是建立防御塔阻擊前往我方主城的敵兵,可以通過視頻直觀感受下:

演示視頻:https://www.zhihu.com/video/1110139144373776384?autoplay=false&useMSE=

人越狠,話越不多。不多說,接下來我們一步步把這幾個功能做完。

素材準備:

網(wǎng)上隨便找一些資源就行,不一定要和我一樣。這里再次強調(diào):

網(wǎng)上獲取的資源一定不能用作商業(yè)用途!!!!!!

就本工程而言,資源有一下幾種:

敵人2個,分別擁有移動,攻擊,待機,死亡四種動畫
?

?


防御塔3個,擁有待機,攻擊兩種動畫
?

人形防御塔可還行


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


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


箭矢1個
?

弓兵模型中自帶


場景搭建:

先從簡單的功能做起:讓敵人從生成點走到主城,看見主城就攻擊。

搭建一個簡單場景:
?

為了檢測敵人尋路,最好是能轉(zhuǎn)彎的道路


敵人和主城有一個都有血量的屬性,都會被攻擊,這里為它們做能顯示在頭上的血條。

以主城為例,在主城的子節(jié)點層創(chuàng)建一個Sprite做黃血條,設(shè)為黃色,取名“BloodStrip”,調(diào)整好大小:
?


然后在BloodStrip的子節(jié)點層創(chuàng)建一個空物體,取名“Hp”,在Hp的子節(jié)點層再創(chuàng)建一個Sprite做紅血條,名字“Red”,設(shè)為紅色,大小和黃血條一樣,把黃血色覆蓋:
?


接下來就移動紅血條位置,讓它左邊邊緣與父物體Hp的Y軸重合:
?


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


這樣我們只需要設(shè)置H的X軸大小,手機靚號賣號就可以控制紅血條長度了:


***這里請初學者注意,如果你選取的紅血條圖片資源不是純色的、是有其他花紋的,則不能用這個方法。原因很簡單,這種方法會把花紋拉長或壓扁。大家可以下來想一下:這種情況下應(yīng)該怎樣來設(shè)置?

后面在代碼中只需要將當前血量與總血量的比值賦給Hp的X軸,就可以將血量信息顯示在界面上了。敵人血條做法一樣。

做好后讓BloodStrip處于禁用狀態(tài),受傷后才顯示(這是游戲UI顯示的一個約定俗成的規(guī)則)。

代碼編寫:

為主城與敵人創(chuàng)建一個基類腳本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) //受傷方法,參數(shù)為受到的傷害值
  • ? ? {
  • ? ?? ???if (surHp > damage) //當前血量大于受傷血量,正??垩?/span>
  • ? ?? ???{
  • ? ?? ?? ?? ?surHp -= damage;
  • ? ?? ?? ?? ?//受傷后開始顯示血條
  • ? ?? ?? ?? ?if (surHp < totalHp)
  • ? ?? ?? ?? ?? ? hpObj.gameObject.SetActive(true);
  • ? ?? ?? ?? ?Vector3 hpScale = redHp.localScale;
  • ? ?? ?? ?? ?hpScale.x = surHp / totalHp;
  • ? ?? ?? ?? ?redHp.localScale = hpScale;
  • ? ?? ???}
  • ? ?? ???else //當前血量不夠,調(diào)用死亡方法? ?? ?? ?
  • ? ?? ?? ?? ?Death();
  • ? ? }
  • ? ? public virtual void Death() //死亡方法
  • ? ? {
  • ? ?? ???surHp = 0;
  • ? ?? ???hpObj.gameObject.SetActive(false); //血條不再顯示
  • ? ? }
  • }
  • 復(fù)制代碼


    創(chuàng)建主調(diào)腳本:用于游戲初始化和記錄游戲死亡,掛在一個場景物體上:
    ?

  • public class GameMain : MonoBehaviour
  • {
  • ? ? public static GameMain instance;
  • ? ? public bool gameOver;
  • ? ? void Start()
  • ? ? {
  • ? ?? ???InitGame();
  • ? ? }
  • ? ? //初始化游戲
  • ? ? void InitGame()
  • ? ? {
  • ? ?? ???instance = this; //單例
  • ? ?? ???gameOver = false;
  • ? ? }
  • }
  • 復(fù)制代碼


    創(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é)束
  • ? ? }
  • }
  • 復(fù)制代碼


    敵人的腳本也繼承自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);
  • ? ? }
  • }
  • 復(fù)制代碼


    重點在移動方法上。因為敵人的移動帶有尋路功能,這里沒有采取Unity自帶的NavMeshAgent,而是用腳本來實現(xiàn),主要思路仿照盲人的行進方式,利用射線充當導盲棍,發(fā)現(xiàn)前方道路中斷再從兩邊找新的行進路線:
    ?

    拐杖就是射線


    要利用好這個思路,場景中道路的搭建也有一定要求,道路都要掛上MeshCollider組件,方便射線檢測。
    ?

    所有道路的Z軸指向路線前進方向


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


    在敵人模型身上創(chuàng)建一個空物體為眼睛,取名為“Eye”,主要作用是從此為射線起始點,位置合適即可,注意,因為所有敵人都用的相同腳本,所以所有敵人的眼睛高度距離地面相同:
    ?

    正面看這些模型真特么驚悚


    當然每個敵人也請掛上碰撞器和剛體以及Animator組件:
    ?


    創(chuàng)建一個敵人狀態(tài)機:
    ?

  • public enum EnemyState //狀態(tài)機
  • {
  • ? ? forward,
  • ? ? attack,
  • ? ? death
  • }
  • 復(fù)制代碼


    重寫初始化方法:
    ?

  • ??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"); //敵人層設(shè)置為"Enemy"
  • ? ?? ???state = EnemyState.forward;
  • ? ?? ???eye = transform.Find("Eye");
  • ? ?? ???ways = new List<Collider>();
  • ? ? }
  • 復(fù)制代碼


    編寫移動方法,并在Update中調(diào)用:
    ?

  • private void Update()
  • ? ? {
  • ? ?? ???hpObj.rotation = mainCamera.rotation; //血條始終面向鏡頭
  • ? ?? ???if (GameMain.instance.gameOver) //游戲結(jié)束播放待機動畫
  • ? ?? ?? ?? ?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);
  • ? ?? ?? ?? ?//發(fā)現(xiàn)未走過的道路,獲取該道路,朝向該路通往的方向
  • ? ?? ?? ?? ?if (!ways.Contains(hit.collider))
  • ? ?? ?? ?? ?{
  • ? ?? ?? ?? ?? ? ways.Add(hit.collider);
  • ? ?? ?? ?? ?? ? way = hit.transform;
  • ? ?? ?? ?? ?? ? wayDir = Quaternion.LookRotation(way.forward);
  • ? ?? ?? ?? ?}
  • ? ?? ???}
  • ? ?? ???else //前方?jīng)]路了發(fā)射球形射線檢測周圍是否有路
  • ? ?? ???{
  • ? ?? ?? ?? ?Collider[] colliders = Physics.OverlapSphere(transform.position, 8, LayerMask.GetMask("Way"));
  • ? ?? ?? ?? ?for (int i = 0; i < colliders.Length; i++)
  • ? ?? ?? ?? ?{
  • ? ?? ?? ?? ?? ? //發(fā)現(xiàn)未走過的道路,獲取該道路,朝向該路通往的方向
  • ? ?? ?? ?? ?? ? 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);
  • ? ? }
  • 復(fù)制代碼


    暫時把初始化方法放在Start中調(diào)用(后面我們會在創(chuàng)建的時候初始化),然后設(shè)置好血量、視野、速度、傷害,主城也設(shè)置好血量:
    ?


    先來看下尋路運行效果:
    ?


    藍線檢測前方道路,紅圈檢測周圍道路

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

    敵人主要功能就已經(jīng)完成?,F(xiàn)在我們來做敵人生成器。


    塔防游戲的敵人生成方式一般都是比較有規(guī)律的,比如先生成一組a敵人,跟著生成一組b敵人,每組敵人的生成間隔也恒定(當然,讀者也可以自己嘗試更豐富的出兵方法,比如讓“某些特定敵人的血量減到某個閾值”作為觸發(fā)條件等等):
    ?


    為了生成方便,我們來做一個定時器,可以重復(fù)并規(guī)律地調(diào)用一個生成敵人方法:
    ?

  • 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; //回調(diào)函數(shù)
  • ? ?? ???public float delayTime; //延遲長度
  • ? ?? ???public float destTime; //延遲后的目標時間
  • ? ?? ???public int count; //重復(fù)次數(shù)
  • ? ? }? ?? ?? ?? ?? ?
  • ? ? List<TimeTask> timeTaskList = new List<TimeTask>(); //保存所有的定時事件? ?
  • ? ? //增加定時回調(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++) //實時監(jiān)測所有定時事件
  • ? ?? ???{
  • ? ?? ?? ?? ?TimeTask task = timeTaskList[i];
  • ? ?? ?? ?? ?if (Time.realtimeSinceStartup >= task.destTime) //時間到了,則執(zhí)行
  • ? ?? ?? ?? ?{
  • ? ?? ?? ?? ?? ? task.callback?.Invoke();
  • ? ?? ?? ?? ?? ? if (task.count == 1) //當次數(shù)為1,執(zhí)行完移除該定時事件
  • ? ?? ?? ?? ?? ?? ???timeTaskList.RemoveAt(i);
  • ? ?? ?? ?? ?? ? else if (task.count > 1) //當次數(shù)大于1,執(zhí)行完次數(shù)減1
  • ? ?? ?? ?? ?? ?? ???task.count--;
  • ? ?? ?? ?? ?? ? task.destTime += task.delayTime; //執(zhí)行完一次后,重新定出下次執(zhí)行時間
  • ? ?? ?? ?? ?}
  • ? ?? ???}
  • ? ? }
  • }
  • 復(fù)制代碼


    把所有敵人放入一個路徑中:
    ?


    創(chuàng)建一個空物體做敵人生成器,放在敵人生成點,創(chuàng)建腳本掛上去:
    ?

  • public class EnemySystem : MonoBehaviour
  • {
  • ? ? //根據(jù)名稱保存所有敵人
  • Dictionary<string, Enemy> enemyDict = new Dictionary<string, Enemy>();
  • //初始化,放在主調(diào)腳本GameMain中執(zhí)行
  • ? ? public void Init()
  • ? ? {
  • ? ?? ???//保存所有種類敵人,可以根據(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è)置敵人種類,生成間隔,生成數(shù)量(默認為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秒后執(zhí)行
  • ? ?? ???CreateEnemy("Zombie1", 1, 5);
  • ? ?? ???//沒0.5秒生成一個敵人,生成10次,第一次生成在5.5秒后執(zhí)行
  • ? ?? ???Util.Instance.AddTimeTask(() => CreateEnemy("Zombie2", 0.5f, 10), 5);
  • ? ? }
  • }
  • 復(fù)制代碼


    做到這一步就可以像演示視頻中那樣點擊按鈕出兵了。
    ?

    總結(jié)

    以上是生活随笔為你收集整理的用Unity开发一款塔防游戏(一):攻击方设计的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。