講師: Joe
這堂課我想改變一下以往的教學模式,比起步驟式的講解或說明,我希望能詳細講解一個小專案的各個部分,並增加你們實作練習的時間與方便性
今天我們要來完成一個簡單的俯視角射殭屍遊戲,主要是運用以前教過的東西,新的內容比較少(其實原本有要教PathFinding,
但是我發現我自己也弄不好所以就擇日再說)
以後的課都會以這堂課的專案做延伸喔!記得把專案留下來
然後VS Code的問題,有聰明的學弟找到解決方法了
以下引述我的某次DC對話,我就懶! (解果對方用Mac QAQ)
我猜還是會有人沒辦法用,如果還有問題,請一樣讓我知道
另外如果你用Mac請參考這個影片
先來總覽一下這個遊戲
我們來看看這個遊戲需要些什麼
玩家
殭屍
牆壁
子彈
殭屍生成器
背景
對這堂課差不多就教這些
前置作業,如果你不嫌棄我用十分鐘化的美術以及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相關的東西
不過可能沒有簡報就是了
有興趣的要來喔
建北電資Unitu小社課第五堂
By rain0130
建北電資Unitu小社課第五堂
- 185