找回密碼
 注冊帳號

掃一掃,訪問微社區

士郎 用Unity開發一款塔防游戲(二):防御方設計

14
回復
1824
查看
打印 上一主題 下一主題
[ 復制鏈接 ]
9以壇為家
33145/50000
排名
1
昨日變化

8252

主題

8810

帖子

3萬

積分

Rank: 9Rank: 9Rank: 9

UID
1231
好友
186
蠻牛幣
227
威望
30
注冊時間
2013-7-29
在線時間
4217 小時
最后登錄
2019-11-20

活力之星原創精華達人突出貢獻獎財富之證游戲蠻牛QQ群會員蠻牛妹VIP

馬上注冊,結交更多好友,享用更多功能,讓你輕松玩轉社區。

您需要 登錄 才可以下載或查看,沒有帳號?注冊帳號

x
在上一篇中,我們已經能夠通過生成器產生敵人,這些敵人能自動尋路到達主城所在位置進行攻擊。主城被攻破后游戲結束。攻擊方已經具備。
接下來是防御方了。這里,咱們建立防御塔阻止敵人的進攻。首先說說本例中三種防御塔的攻擊方式:
弓箭手:遠程攻擊,對敵人射出弓箭造成傷害,弓箭可以插在敵人身上;




錘兵:群體攻擊,錘擊地面,原地擊飛敵人,減慢敵人移動速度;



劍士:近程攻擊。


那么從弓箭手開始,來做他的攻擊功能:



弓箭手攻擊時會從“槍口”處發出弓箭,所以先在弓箭手模型上創建一個槍口Muzzle:


槍口一Z軸指向發射方向
槍口要隨弓箭移動,在Hierarchy面板中大概在這里:


為了獲取這個未知層級的子物體,也為了讓其它類也方便調用,我們先寫一個工具類,里面創建一個查找未知層級子物體的方法:

[AppleScript] 純文本查看 復制代碼
public class ToolsMethod
{
    private static ToolsMethod _Instance;
    public static ToolsMethod Instance //單例
    {
        get
        {
            if (_Instance == null)
                _Instance = new ToolsMethod();
            return _Instance;
        }
    }
    //根據名稱獲取未知層級子物體
    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;
    }
}

弓箭手的腳本中還需要拿到箭矢的預制體,先把箭矢預制體放入路徑中:



為箭矢預制體創建一個腳本Bullet掛上去:

[AppleScript] 純文本查看 復制代碼
public class Bullet : MonoBehaviour
{
}

為了讓箭矢和其它物體可以使用對象池,我們先創建一個空物體,作為所有對象池的管理器。取名“PoolManager”,創建對象池類:

[AppleScript] 純文本查看 復制代碼
public class GameObjectPool
{
    private static GameObjectPool _Instance;
    public static GameObjectPool Instance //單例模式
    {
        get
        {
            if (_Instance == null)
                _Instance = new GameObjectPool();
            return _Instance;
        }
    }
    //用于保存所有對象池
    public Dictionary<string, Transform> poolDict = new Dictionary<string, Transform>();
    //獲取對象池
    public Transform GetPool(string poolName) 
    {
        if (poolDict.ContainsKey(poolName))
            return poolDict[poolName];
        //字典中沒有重新創建
        Transform poolObj = new GameObject(poolName + "_Pool").transform;
        //創建的對象池放入對象池管理器的子物體中
        poolObj.SetParent(GameObject.Find("PoolManager").transform);
        poolObj.gameObject.SetActive(false);
        poolDict.Add(poolName, poolObj);
        return poolObj;
    }
}

其它物體需要對象池也可以調用該類的方法。
弓箭手需要有一個攻擊范圍。敵人進入該范圍后才會被認定為目標并展開攻擊。
首先將敵人都放在Enemy層,創建弓箭手的腳本:
[AppleScript] 純文本查看 復制代碼
public class Pagoda : MonoBehaviour
{
    protected Animator anim;
    protected Transform muzzle;
    Bullet Arrow;
    Transform arrowPool; //箭矢對象池
    //初始化
    public void initPagoda()
     {
        enabled = true; //啟用腳本
        anim = GetComponentInChildren<Animator>();
        muzzle = ToolsMethod.Instance.FindChildByName(transform, "Muzzle");
        Arrow = Resources.Load<Bullet>("Prefab/Bullet/Arrow");
        //為箭矢創建一個以它命名的對象池
        arrowPool = GameObjectPool.Instance.GetPool(Arrow.name);
     }
    private void Update()
     {
        //游戲結束,停止攻擊
        if (GameMain.instance.gameOver)
        {
            anim.SetBool("Attack", false);
            return;
        }
        GetTarget();
    }
    public float attactRange; //攻擊范圍
    public float damage; //傷害值
    protected Enemy target; //攻擊目標
    //獲取攻擊目標
    void GetTarget()
    {
        if (target == null) //攻擊目標為空時,用球形射線檢測Enemy層找尋攻擊目標
        {
            Collider[] enemys = Physics.OverlapSphere(transform.position, attactRange, LayerMask.GetMask("Enemy"));
            if (enemys.Length == 0)
                anim.SetBool("Attack", false);
            //發現敵人,設為目標,進行攻擊(播放攻擊動畫)
            for (int i = 0; i < enemys.Length;)
            {
                target = enemys.GetComponent<Enemy>();
                anim.SetBool("Attack", true);
                break;
            }
        }
        else
        {
            //面向攻擊目標
            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);
            //攻擊目標離開攻擊范圍或死亡,重新獲取攻擊目標
            if (Vector3.Distance(target.transform.position, transform.position) >= attactRange || target.state == EnemyState.death)
                target = null;
        }
    }
    //攻擊方法(放在攻擊動畫事件中)
    public virtual void PagodaAttack()
    {
        //在槍口位置創建箭矢
    }
}

弓箭手的初始化在弓箭手創建時調用。
如果弓箭手已經能檢測敵人并發射箭矢,接下來就是箭矢的功能了。主要如下:
1. 飛向目標(始終面向目標,并向前飛);
2. 打到目標,調用目標受傷方法(用距離判斷是否打到);
3. 插在目標身上(認目標做父物體,停止移動)
要讓箭矢能插在敵人身上,需要在敵人模型上創建一個空物體做打擊點,取名HitPos,為了效果逼真,HitPos最好放在模型骨骼上,并可以在敵人腳本中聲明一個hitPos,使用查找未知層級子物體的方法來獲取:
[AppleScript] 純文本查看 復制代碼
Transform hitPos = ToolsMethod.Instance.FindChildByName(transform, "HitPos");

當弓箭手檢測到敵人創建箭矢時,同時將敵人信息、傷害值、箭矢對象池賦給箭矢,由箭矢去做接下來的工作,如傷害敵人,它的腳本可以這樣寫:
[AppleScript] 純文本查看 復制代碼
public class Bullet : MonoBehaviour
{
    public float speed;

    Enemy target; //攻擊目標
    float damage; //傷害值
    Transform pool; //對象池
    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) //沒有射中目標,繼續飛
        {
            transform.Translate(0, 0, speed * Time.deltaTime);
            if (Vector3.Distance(initPos, transform.position) > 500) //飛出一定范圍自動銷毀
                DestroySelf();

            if (target != null && target.state != EnemyState.death) //如果目標活著朝向目標
            {
                transform.LookAt(target.hitPos);
                //到達有效范圍,調用目標受傷方法,成為目標子物體(插在目標身上)
                if (Vector3.Distance(target.hitPos.position, transform.position) <= 1)
                {
                    target.Damage(damage);
                    transform.SetParent(target.hitPos);
                }
            }
        }
        else if (target.state == EnemyState.death) //射中后,只要目標一死就銷毀
            DestroySelf();
    }
    //銷毀自身(進入對象池)
    private void DestroySelf()
    {
        transform.SetParent(pool);
    }
}

箭矢的移動速度在編輯器界面自行設定,在弓箭手的攻擊方法中,就可以在創建箭矢的同時把相關信息賦給它:
  
[AppleScript] 純文本查看 復制代碼
  //攻擊方法(放在攻擊動畫事件中)
    public virtual void PagodaAttack()
    {
        //如果對象池有,則從對象池取子彈,否則重新實例化
        //設定位置,方向,攻擊目標,傷害值,所在對象池
        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);
    }

弓箭手做完,接下來是錘子兵的功能:
想象每一下都是LOL里石頭人的大招


錘子兵的功能和弓箭手非常相似。除了攻擊方式不同,其它都一樣。所以我們創建錘子兵的腳本可以繼承自弓箭手的腳本:

[AppleScript] 純文本查看 復制代碼
public class Pagoda2 : Pagoda
{
    public float force; //擊飛力度
    public ParticleSystem effect; //擊飛特效
    //重寫攻擊方法(在攻擊動畫事件中調用)
    public override void PagodaAttack()
    {
        //群體攻擊,作用范圍始攻擊范圍的一半
        Collider[] enemys = Physics.OverlapSphere(muzzle.position, attactRange / 2, LayerMask.GetMask("Enemy"));
        for (int i = 0; i < enemys.Length; i++)
        {
            //傷害作用范圍內的每個敵人
            Enemy enemy = enemys.GetComponent<Enemy>();
            enemy.Damage(damage);        
            //播放特效
            effect.transform.position = muzzle.position;
            effect.Play();
            //擊飛方法
        }
    }
}

除了對敵人造成傷害之外,需要專門寫一個擊飛的方法,擊飛方法可以寫在錘子兵的腳本里,也可以在敵人腳本(Enemy)中寫一個被擊飛方法:
   
[AppleScript] 純文本查看 復制代碼
 //被擊飛方法
    bool isFly; //是否被擊飛(處于擊飛狀態時不能再被擊飛)
    public void StrikeFly(float force)
    {
        if (isFly == false) //未被擊飛狀態下才可以被擊飛
        {
            isFly = true;
            rigid.AddForce(Vector3.up * force, ForceMode.Impulse);
            float initSpeed = speed; //初始速度
            speed = 0;
            //0.5秒后恢復
            Util.Instance.AddTimeTask(() =>
            {
                speed = initSpeed;
                isFly = false;
            }, 0.5f);
        }
    }

該方法是公開屬性,在錘子兵那邊調用。
然后是劍士的功能:



劍士功能最簡單,但增加了一個暴擊的屬性,將暴擊率代入攻擊力的計算就好,腳本也繼承自弓箭手:
[AppleScript] 純文本查看 復制代碼
public class Pagoda3 : Pagoda
{
    public float critChance = 0.2f; //暴擊率
    //重新攻擊方法
    public override void PagodaAttack()
    {
        if (target != null)
        {
            //代入暴擊率,計算最終傷害(暴擊是雙倍傷害)
            int crit = (int)(critChance * 100);
            target.Damage(damage * (Random.Range(0, 100) < crit ? 2 : 1), this);
        }
    }
}

是不是很簡單?
好了,三個人形防御塔的功能都做完了。現在正式做安放防御塔的功能,用Image搭建一個防御塔菜單UI界面,放入精靈圖片,取名“PagodaMenu”:


在PagodaMenu下創建三個Image做頭像:

我是直接把模型放在紅色背景板前截圖



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



我們先來看下放置的過程:



通過演示,我們大概可以理清創建的邏輯:
1. 點擊頭像實例化一個防御塔,并顯示攻擊范圍;
2. 按住鼠標不放防御塔會跟隨鼠標移動;
3. 攻擊范圍的顏色在可放置位置顯示為綠色,其余地方為紅色;
4. 在可放置位置彈起鼠標時,會將防御塔放在地形上,且同時為防御塔初始化。
根據以上的邏輯順序,我們首先要讓圖片具有可點擊事件與彈起事件。為頭像Image創建一個腳本,引入相應接口:
[AppleScript] 純文本查看 復制代碼
public class IconElement : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
    //點擊事件
    public void OnPointerDown(PointerEventData eventData)
    {
    }
    //彈起事件
    public void OnPointerUp(PointerEventData eventData)
    {
    }
}

然后可以從主攝像打出射線,將射線檢測點的坐標賦給防御塔,可以放在在Update中調用。攻擊范圍的顯示可以通過創建一個普通球形物體來實現,后面在代碼中調整顏色就行了。位置也跟隨射線檢測點移動,平時處于禁用狀態,只有在擺放防御塔過程中調用:



現在將所有防御塔預制體放入路徑中:



預制體名字和頭像圖片的名字相同,且一一對應:


為Icon的腳本創建初始化方法:

  
[AppleScript] 純文本查看 復制代碼
  string pagodaName; //防御塔名,用來加載防御塔 
    Camera mainCamera; //主攝像
    Transform attRange; //攻擊范圍顯示器
    Material ria; //范圍顯示器的材質球
    LayerMask layer; //射線可照射層
    //初始化
    public void Init(Camera _mainCamera, Transform _attRange)
    {
        pagodaName = GetComponent<Image>().sprite.name; //自身圖片的名字就是對應防御塔名字
        mainCamera = _mainCamera;
        attRange = _attRange;
        ria = attRange.GetComponent<MeshRenderer>().material;
        layer = LayerMask.GetMask("Ground") | LayerMask.GetMask("Way") | LayerMask.GetMask("Pagoda");
    }

然后在點擊事件中寫入點擊時要執行的功能:
[AppleScript] 純文本查看 復制代碼
Pagoda pagodaObj; //防御塔實例    
//點擊頭像實例化防御塔
    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); //頭像變色
    }

彈起事件中根據條件判斷當前是否可放置防御塔,判斷邏輯放在Update中:
[AppleScript] 純文本查看 復制代碼
 bool isPlace; //是否可放置
    Transform terrain; //可放置地形
    //抬起鼠標放置或刪除防御塔
    public void OnPointerUp(PointerEventData eventData)
    {
        if (isPlace) //可放置時
        {
            //放置在該地形并成為地形子物體,然后初始化
            pagodaObj.transform.position = terrain.position;
            pagodaObj.transform.SetParent(terrain); 
            pagodaObj.initPagoda();
        }
        else //不可放置則銷毀
            Destroy(pagodaObj.gameObject);

        attRange.gameObject.SetActive(false); //禁用范圍顯示器
        pagodaObj = null;
        GetComponent<Image>().color = new Color(1, 1, 1); //頭像變色
    }
    void Update()
    {
        //如果防御塔實例化,則找尋可以放置的位置
        if (pagodaObj != null)
        {
            //攝像機向鼠標位置發射線
            Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 500, layer))
            {
                pagodaObj.transform.position = hit.point; //防御塔模型根據鼠標移動
                attRange.position = hit.point; //范圍顯示器根據鼠標移動
                int index = hit.collider.gameObject.layer; //獲取照射到物體的層
                //如果是可以放置的地形,并且該地形上沒有其它防御塔,就可以放置
                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);
                }
            }
        }
    }

好的,頭像功能的腳本就做完了。頭像的數量可以根據防御塔具體數量增減。我們注意到每個頭像的初始化方法沒地方調用,可以創建一個管理類來對它們統一初始化,將它掛在PagodaMenu上:

[AppleScript] 純文本查看 復制代碼
public class PagodaMenu : MonoBehaviour
{
    public Camera mainCamera; //主攝像機
    public Transform attRange; //攻擊范圍顯示器
    public void Init()
    {
        IconElement[] icons = GetComponentsInChildren<IconElement>();
        for (int i = 0; i < icons.Length; i++)
        {
            icons.Init(mainCamera, attRange);
        }
    }
}


主攝像機和范圍顯示器在編輯器界面直接拖入。

到這里,我們之前在第一篇文章演示視頻里的功能就做完了。之后可能會做一些經濟系統方面的功能,如消滅敵人可以獲得金錢、使用金錢購買和升級防御塔等等。

工程鏈接:


提取碼:oshk

知乎@四五二十

回復

使用道具 舉報

7日久生情
1588/5000
排名
1338
昨日變化

8

主題

167

帖子

1588

積分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
215462
好友
1
蠻牛幣
4021
威望
0
注冊時間
2017-3-30
在線時間
501 小時
最后登錄
2019-12-6
沙發
2019-6-13 17:20:01 只看該作者
期待經濟系統
回復

使用道具 舉報

0

主題

56

帖子

77

積分

Rank: 2Rank: 2

UID
321303
好友
0
蠻牛幣
97
威望
0
注冊時間
2019-5-6
在線時間
21 小時
最后登錄
2019-7-11
板凳
2019-6-14 09:05:36 只看該作者
回復

使用道具 舉報

5熟悉之中
523/1000
排名
12482
昨日變化

0

主題

83

帖子

523

積分

Rank: 5Rank: 5

UID
313384
好友
0
蠻牛幣
1436
威望
0
注冊時間
2019-2-4
在線時間
366 小時
最后登錄
2019-12-10
地板
2019-6-14 09:11:14 只看該作者
不錯,講的很細
回復

使用道具 舉報

0

主題

56

帖子

77

積分

Rank: 2Rank: 2

UID
321303
好友
0
蠻牛幣
97
威望
0
注冊時間
2019-5-6
在線時間
21 小時
最后登錄
2019-7-11
5#
2019-6-17 08:57:12 只看該作者
回復

使用道具 舉報

2初來乍到
139/150
排名
19949
昨日變化

2

主題

42

帖子

139

積分

Rank: 2Rank: 2

UID
82201
好友
0
蠻牛幣
3
威望
0
注冊時間
2015-3-20
在線時間
67 小時
最后登錄
2019-11-7
QQ
6#
2019-6-17 23:24:08 只看該作者

好東西,感謝分享!
回復

使用道具 舉報

6蠻牛粉絲
1059/1500
排名
3929
昨日變化

0

主題

329

帖子

1059

積分

Rank: 6Rank: 6Rank: 6

UID
251353
好友
0
蠻牛幣
7681
威望
0
注冊時間
2017-10-29
在線時間
350 小時
最后登錄
2019-12-10
7#
2019-6-18 11:56:08 只看該作者
回復

使用道具 舉報

3偶爾光臨
157/300
排名
64938
昨日變化

0

主題

33

帖子

157

積分

Rank: 3Rank: 3Rank: 3

UID
259926
好友
0
蠻牛幣
454
威望
0
注冊時間
2017-12-16
在線時間
123 小時
最后登錄
2019-12-10
8#
2019-6-24 16:59:30 只看該作者
回復

使用道具 舉報

6蠻牛粉絲
1055/1500
排名
10708
昨日變化

0

主題

761

帖子

1055

積分

Rank: 6Rank: 6Rank: 6

UID
301976
好友
1
蠻牛幣
1562
威望
0
注冊時間
2018-10-31
在線時間
196 小時
最后登錄
2019-12-10
9#
2019-6-25 10:08:28 只看該作者
感謝樓主大大...
回復

使用道具 舉報

3偶爾光臨
274/300
排名
14500
昨日變化

0

主題

36

帖子

274

積分

Rank: 3Rank: 3Rank: 3

UID
265586
好友
0
蠻牛幣
492
威望
0
注冊時間
2018-1-21
在線時間
182 小時
最后登錄
2019-12-10
10#
2019-6-26 09:19:59 只看該作者
贊一個贊一個
回復

使用道具 舉報

6蠻牛粉絲
1055/1500
排名
10708
昨日變化

0

主題

761

帖子

1055

積分

Rank: 6Rank: 6Rank: 6

UID
301976
好友
1
蠻牛幣
1562
威望
0
注冊時間
2018-10-31
在線時間
196 小時
最后登錄
2019-12-10
11#
2019-6-26 10:59:05 只看該作者
回復

使用道具 舉報

3偶爾光臨
244/300
排名
14745
昨日變化

0

主題

82

帖子

244

積分

Rank: 3Rank: 3Rank: 3

UID
236535
好友
0
蠻牛幣
413
威望
0
注冊時間
2017-8-8
在線時間
108 小時
最后登錄
2019-12-10
12#
2019-7-2 10:50:55 只看該作者
回復

使用道具 舉報

4四處流浪
353/500
排名
8825
昨日變化

1

主題

30

帖子

353

積分

Rank: 4

UID
236737
好友
0
蠻牛幣
502
威望
0
注冊時間
2017-8-9
在線時間
188 小時
最后登錄
2019-11-26
13#
2019-7-3 10:34:08 只看該作者
感覺有點難
回復

使用道具 舉報

4四處流浪
353/500
排名
8825
昨日變化

1

主題

30

帖子

353

積分

Rank: 4

UID
236737
好友
0
蠻牛幣
502
威望
0
注冊時間
2017-8-9
在線時間
188 小時
最后登錄
2019-11-26
14#
2019-7-3 10:36:20 只看該作者
很向學,就是看起來有點難。
回復 支持 反對

使用道具 舉報

排名
39864
昨日變化

0

主題

11

帖子

43

積分

Rank: 1

UID
231105
好友
0
蠻牛幣
22
威望
0
注冊時間
2017-7-9
在線時間
26 小時
最后登錄
2019-10-18
15#
2019-7-9 15:50:58 只看該作者
感謝分享
回復

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 注冊帳號

本版積分規則

女校游泳队彩金 分分彩定位胆大小玩法 360彩票网官网下载 山西福彩快乐10分开奖 云南快乐十分投注技巧 初中毕业女生最赚钱的职业 东方6+1开奖时间 湖北30选5开奖查询结果 淘宝快3吧 小猪佩琪童装店赚钱吗 山西快乐10分钟玩法 u购彩首页 一个喜欢赚钱的男孩 江苏快3是否真实 6538彩票首页 六肖中特期期准免费期 打麻将可以提现的平台