講師: Joe

這堂課我想改變一下以往的教學模式,比起步驟式的講解或說明,我希望能詳細講解一個小專案的各個部分,並增加你們實作練習的時間與方便性

今天我們要來完成一個簡單的俯視角射殭屍遊戲,主要是運用以前教過的東西,新的內容比較少(其實原本有要教PathFinding,

但是我發現我自己也弄不好所以就擇日再說)

以後的課都會以這堂課的專案做延伸喔!記得把專案留下來

然後VS Code的問題,有聰明的學弟找到解決方法了

以下引述我的某次DC對話,我就懶! (解果對方用Mac QAQ)

我猜還是會有人沒辦法用,如果還有問題,請一樣讓我知道

另外如果你用Mac請參考這個影片

https://www.youtube.com/watch?v=3GVGyooZ8jk

先來總覽一下這個遊戲

我們來看看這個遊戲需要些什麼

玩家

殭屍

牆壁

子彈

殭屍生成器

背景

對這堂課差不多就教這些

前置作業,如果你不嫌棄我用十分鐘化的美術以及AI生成的背景的話: 可以下載下面的Package,裡面有今天會用到的圖片(我有幫你們設定好)

https://drive.google.com/drive/folders/11yZAPZWqQRsSl2vzW1QwNTm0_smDhvlP?usp=sharing

還有這個設定要確認一下不然Sprite中心會跑掉

背景  Background

Components /Scripts Sprite Renderer
功能說明/備註/設置 可以調顏色
把Order in layer設成負的,確保背景在最下層

總結: 就是個背景,沒啥好說的

牆壁 Wall (是個TileMap)

Components /Scripts Tilemap
/Tilemap Renderer
Tilemap Collider
/Composite Collider
/RigidBody2D
其他
功能說明/備註/設置 就創建時預設的,不用動 給Tilemap碰撞功能,
請參閱上節簡報。
鋼體記得調成Static。
Tilemap collider的Used by composite記得勾選。
記得新增一個Tag叫做Wall並指派給Tilemap,另外這一頁講的是Tilemap跟父物件Grid沒關係
 

總結: 就是個牆壁,放在最外圍,沒啥好說的

如果你忘記怎麼添加/使用Tilemap請參閱上節簡報

攝影機 MainCamera

Components /Scripts Camera Camera Move(腳本) Audio Listener
功能說明/備註/設置 可以調Size和BackgroundColor(不是剛剛講的背景) 跟隨玩家 預設就存在,以後用到
 

下一頁有腳本

總結: 就是個攝影機,一開始就有,沒啥好說的

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraMove : MonoBehaviour
{
    Transform player;
    private void Start() 
    {
        player = GameObject.FindGameObjectWithTag("Player").transform; //用Tag找到Player
    }
    void Update()
    {
        transform.position = new Vector3(player.position.x, player.position.y, -10);
    }
}

子彈 Bullet (是個Prefab)

Components /Scripts Sprite Renderer RigidBody2D BoxCollider2D Bullet(腳本)
功能說明/備註/設置 不用動 讓子彈飛。
Gravity Scale調成0
 
調成適合的大小。
isTrigger記得勾選
 
碰到牆壁會消失,
碰到僵屍會呼叫僵屍身上的傷害腳本的函式,然後消失

下一頁有腳本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D other) //觸發器偵測
    {
        if(other.CompareTag("Enemy")) //檢查觸發對象的Tag是否為敵人,CompareTag和 == 的字串比較效果相同,但據官方說比較省效能
        {
            other.GetComponent<EnemyDamage>().TakeDamage(); //呼叫觸發對象身上的腳本中的受傷函式
            GameObject.Destroy(this.gameObject); //摧毀自身
        }
        else if(other.CompareTag("Wall")) //檢查觸發對象的Tag是否為牆壁
        {
            GameObject.Destroy(this.gameObject); //摧毀自身
        }
    }
}

總結: 記得它是一個Prefab,不知道Prefab是啥怎麼存請看上節簡報

玩家 Player

Components /Scripts Sprite Renderer RigidBody2D BoxCollider2D PlayerMove(腳本)
功能說明/備註/設置 不用動 Gravity Scale調成0,勾選凍結Z軸旋轉
 
調成適合的大小。
也可以用圓的
 
處理簡單的平面移動,之後可能會修改優化
表格塞不下 PlayerDamage
(腳本)
PlayerShoot
(腳本)
ShootingPiont
(子物件)
其他
負責處理玩家被僵屍打到後發生的事 負責處理滑鼠瞄準跟子彈射擊 一個用來標記子彈位置的空的GameObject 記得將Tag設成Player

PlayerMove

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    [SerializeField] float speed;

    Vector2 moveinput;
    Rigidbody2D rb;
    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        moveinput.x = Input.GetAxis("Horizontal");
        moveinput.y = Input.GetAxis("Vertical"); //垂直輸入
    }

    void FixedUpdate() 
    {
        rb.velocity = moveinput.normalized * speed * Time.deltaTime; //normalize會將向量的長度變為1,避免斜向移動速度變快
    }
}

PlayerDamage

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerDamage : MonoBehaviour
{
    [SerializeField] int playerMaxHP = 5; //滿血血量
    int playerHP;//實際血量,之後會做血量條
    void Start()
    {
        playerHP = playerMaxHP;
    }

    public void TakeDamage()
    {
        playerHP--;
        Debug.Log("PlayerGetDamage");
        if(playerHP <= 0)
        {
            //暫時先這樣,之後會有更多功能(如死亡動畫和選單)
            Debug.Log("Player is dead");
            this.gameObject.SetActive(false);
        }
    }
}

PlayerShoot

下一頁繼續

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerShoot : MonoBehaviour
{
    [SerializeField] GameObject bulletPrefab; //子彈預置件,記得拖入腳本的空位中
    [SerializeField] Transform shootingPoint; //生成子彈的位置(子物件位置),記得拖入腳本的空位中
    [SerializeField] float shootingCD = 0.5f; //冷卻時間
    float cdTimer; //冷卻計時器
    Vector3 mousePos; //儲存滑鼠游標的座標位置
    Vector2 mouseDir; //儲存玩家到滑鼠游標的向量(方向和距離長度)
    bool shootCmd; //有發出射擊的指令
    [SerializeField] float bulletForce; //射擊強度

    void Start() 
    {
        shootCmd = false;
        cdTimer = 0;
    }

    void Update()
    {
        Vector3 mousePosInScreen = Input.mousePosition; //注意Input.mousePosition存的是在螢幕中的位置
        mousePos = Camera.main.ScreenToWorldPoint(mousePosInScreen); //攝影機有轉換成世界座標的函式
        if(Input.GetMouseButtonDown(0)) //0是左鍵
        {
            shootCmd = true;
        }
    }

    void FixedUpdate()
    {
        mouseDir = mousePos - transform.position; //計算玩家到鼠標的向量
        transform.up = mouseDir; //將玩家上方面向的方向定義為和鼠標方向相同,使玩家面向鼠標
        if(shootCmd && cdTimer <= 0) //冷卻時間跑完以後才能射擊
        {
            shootCmd = false; //指令重置
            cdTimer = shootingCD; //冷卻時間重置
            Shoot();
        }
        else
        {
            shootCmd = false;
            cdTimer -= Time.deltaTime; //冷卻時間和現實時間一樣減少
        }
    }

    void Shoot()
    {
        GameObject bullet = Instantiate(bulletPrefab, shootingPoint.position, transform.rotation); 
        //生成子彈物件,並獲得其參考叫bullet,生成位置等於ShootingPoint的位置,角度等於玩家的角度

        bullet.GetComponent<Rigidbody2D>().AddForce(mouseDir.normalized * bulletForce, ForceMode2D.Impulse);
        //施予生成子彈的鋼體一個朝向鼠標的力
    }
}

總結: 一個簡單的2D射擊遊戲角色,各項數值記得手動調整,物件參考記得拖入腳本中

小補充: 空物件可以調整它在Scene中的Icon,讓它可以被看到

僵屍 Zombie(是Prefab)

Components /Scripts Sprite Renderer RigidBody2D BoxCollider2D Enemy
Damage
(腳本)
EnemyAI
(腳本)
功能說明/備註/設置 不用動 Gravity Scale調成0,勾選凍結Z軸旋轉 調成適合的大小。
也可以用圓的
處理被子彈打到之後的事情 處理僵屍的偵測、移動和攻擊

原本要給它自動尋路功能,但是發現我太久沒碰那個模組有點忘記怎麼用,之後有機會再說

表格放不下,Tag要設成Enemy

EnemyDamage

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyDamage : MonoBehaviour
{
    [SerializeField] int enemyMaxHP = 3;
    int enemyHP;
    private void Start() 
    {
        enemyHP = enemyMaxHP;
    }
    public void TakeDamage()
    {
        enemyHP--;
        if(enemyHP <= 0)
        {
            //暫時這樣,之後會有更多功能和機制
            GameObject.Destroy(this.gameObject);
        }
    }
}

EnemyAI

下一頁繼續

下一頁繼續

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyAI : MonoBehaviour
{
    [SerializeField] float speed = 80;
    [SerializeField] float rotationSpeed; //轉身時的每秒度數
    [SerializeField] float awareRange; //查覺到玩家的距離
    [SerializeField] float attactCD = 0.2f; //攻擊冷卻時間
    float cdTimer; //冷卻計時器
    Transform playerTransform; //玩家位置
    Rigidbody2D rb;
    bool awarePlayer; //是否感知到玩家
    Vector3 enemyToPlayer; //儲存僵屍到玩家的向量
    void Start()
    {
        playerTransform = GameObject.FindGameObjectWithTag("Player").transform; //用Tag找到玩家
        rb = GetComponent<Rigidbody2D>();
        awarePlayer = false;
    }

    private void FixedUpdate() 
    {
        DetectPlayr(); //偵測玩家的距離和方位

        if(awarePlayer)
        {
            Move();
            Rotate();
        }

        cdTimer -= Time.deltaTime;
    }

    void DetectPlayr()
    {
        enemyToPlayer = playerTransform.position - transform.position;
        if(!awarePlayer)
        {
            if(enemyToPlayer.sqrMagnitude <= awareRange * awareRange) //sqrMagnitude是向量的平方和,用來和感知範圍的平方比較,不用開根號,比較省資源
            {
                awarePlayer = true; //如果玩家在範圍內則將awarePlayer設為True
            }
        }
    }

    void Move()
    {
        rb.velocity = enemyToPlayer.normalized * speed * Time.deltaTime;
    }

    void Rotate()
    {
        //這斷根轉動相關的程式有點複雜,有興趣可以針對每個函式查手冊
        Quaternion targetRotation = Quaternion.LookRotation(transform.forward, enemyToPlayer.normalized); //將目標向量轉換成旋轉角度(四元數)
        Quaternion rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime); //處理每一幀的轉動角度
        rb.SetRotation(rotation); //鋼體轉動特定角度
    }

    private void OnCollisionStay2D(Collision2D other) //和玩家的碰撞檢測,注意是Stay不是Enter
    {
        if(other.collider.CompareTag("Player") && cdTimer <= 0) //冷卻時間跑完才能攻擊
        {
            other.gameObject.GetComponent<PlayerDamage>().TakeDamage(); //調用玩家身上的傷害函式
            cdTimer = attactCD; //重置冷卻
        }
    }
}

總結: 敵人AI的程式稍微比較複雜,請確保你有讀懂每一個部分在幹嘛

僵屍生成器Enemy Spawner

Components /Scripts EnemySpawner(腳本)
功能說明/備註/設置 自動在地圖中生成僵屍的腳本,會偵測玩家的位置並防止僵屍直接生成在玩家附近

 

(沒有圖片因為它就只是一個用來放腳本的空物件)

EnemySpawner

下一頁繼續

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] GameObject zombiePrefab; //僵屍預置件
    [SerializeField] float spawnCD; //生成僵屍的冷卻時間
    [SerializeField] float playerAvoidingRange; //玩家周圍不可生成殭屍的範圍
    Transform playerTra; //玩家位置
    float cdTimer; //冷卻計時器
    private void Start() 
    {
        playerTra = GameObject.FindGameObjectWithTag("Player").transform;
    }

    private void Update() 
    {
        if(cdTimer <= 0)
        {
            Vector3 spawnPos = Vector3.zero;
            int tryLimit = 100; //限制隨機生成的常識次數,避免卡頓

            while(tryLimit > 0)
            {
                spawnPos = new Vector3(Random.Range(-24f, 24f), Random.Range(-24f, 24f), 0); //依據地圖大小隨機決定生成點
                Vector3 toPlayerPos = playerTra.position - spawnPos; //計算生成點至玩家位置的向量
                if(toPlayerPos.sqrMagnitude > playerAvoidingRange * playerAvoidingRange) //檢查生成點是否罹玩家太近,沒有的話就跳出迴圈
                {
                    break;
                }
                tryLimit--;
            }
            Instantiate(zombiePrefab, spawnPos, Quaternion.identity); //生成殭屍
            cdTimer = spawnCD; //重製冷卻時間
        }
        else
        {
            cdTimer -= Time.deltaTime;
        }
      
    }

}

總結: 這個生成器的功能十分陽春,之後可能會再改進,另外生成速度和不可生成的距離半徑可以用來調整難度

這樣就完成啦

你們可以開始實作了

以後的課都會以這個專案做延伸喔

有任何問題都可以問我

提醒一下下一堂課是明天

會教Animation相關的東西

不過可能沒有簡報就是了

有興趣的要來喔

Made with Slides.com