Unity Game Development

第3~4話 基礎物理系統&簡易平台遊戲製作

Lecturer:水獺

目錄(可點)

本堂課內容

本堂課內容

Collider(碰撞器)

1

這是什麼?

Collider,翻譯成中文就是碰撞器

在場景中會以綠色線條顯示

顧名思義

這個Component可以偵測兩個GameObject之間的碰撞

並透過某些函數回傳碰撞資訊

當兩個有Collider的物件碰在一起時會有「撞到」的效果

會被擋下而無法前進

因此可以拿來製作平台遊戲中的站立平台

2

有什麼類型?

大致上可以用2D、3D以及形狀來做區分

 

2D形狀包含:

矩形、膠囊形、圓形、多邊形、邊、組合

 

3D形狀包含:

矩形、膠囊形、球形、網格狀、地形、車輪

3

有什麼類型?

  • 矩形Box

生成一個包住GameObject邊界的Collider

最不吃效能

可直接在Heirarchy裡用

拖曳的方式編輯形狀

是否為觸發器(之後會講)

是否被組合使用(之後會講)

相對中心偏移量

長寬

圓角半徑(可以自己試試看)

覆蓋層(之後會講)

4

物理材質

有什麼類型?

  • 膠囊形Capsule

圓滑版的矩形

(為了示範所以我把他拉長了(。_。))

因為如果太大會變圓形

可直接在Heirarchy裡用

拖曳的方式編輯形狀

是否為觸發器(之後會講)

相對中心偏移量

長寬

橫向或直向

覆蓋層(之後會講)

5

物理材質

有什麼類型?

  • 圓形Circle

生成一個圓形包住GameObject

可直接在Heirarchy裡用

拖曳的方式編輯形狀

是否為觸發器(之後會講)

相對中心偏移量

半徑

覆蓋層(之後會講)

6

物理材質

有什麼類型?

  • 多邊形Polygon

生成一個貼合GameObject邊緣的多邊形

蠻吃效能的

需要很精細的碰撞範圍可以用

可直接在Heirarchy裡用拖曳的方式編輯形狀

是否為觸發器(之後會講)

相對中心偏移量

組合成這個碰撞器的所有點座標

覆蓋層(之後會講)

是否被組合使用(之後會講)

Draw Mode為Tiled時讓所有複製的Sprite都有碰撞器

7

物理材質

有什麼類型?

  • 邊Edge

生成一個或多個邊

不常用

可以想想他能用在什麼地方

可直接在Heirarchy裡用拖曳的方式編輯形狀

是否為觸發器(之後會講)

相對中心偏移量

組合成這個碰撞器的所有點座標

覆蓋層(之後會講)

邊圓角半徑(可以自己試試看,會有膠囊的效果)

點數量,可以調整

8

物理材質

有什麼類型?

  • 組合Composite

9

=

+

將有勾選Used By Composite屬性的Collider形狀

結合在一起

Trigger

Collider是碰撞器

那如果我只想要偵測兩個東西相碰的效果

而不要碰撞的效果呢?

10

Trigger!也就是觸發器

只要把Is Trigger屬性勾起來

Collider就會變成一個Trigger

不會有碰撞讓東西不能動的效果

但一樣可以回傳資訊

平台製作

11

平台製作

  • 加入Collider

開啟之前的專案

在遊戲角色上加一個Collider 2D

(善用搜尋會快很多)

12

平台製作

  • 新增平台GameObject

在Hierarchy點右鍵 --> Create Empty(綠框)

新增一個空的GameObject 然後取個名(我取Ground)

13

平台製作

  • 新增平台GameObject

把剛剛新增的GameObject加上SpriteRenderer

(善用搜尋會快很多)

14

平台製作

  • 新增平台GameObject

15

在SpriteRenderer的Sprite框裡

按下右邊的圓形(綠框)

搜尋打上Sprite Shape Fill後便會出現一個矩形

點下去

(若只有None可以按右上角的眼睛(綠框))

平台製作

  • 新增平台GameObject

你就得到一個可以自行調整長寬、位置跟顏色的平台了!

也把他加上Collider 2D就完成啦~也可以多複製幾個擺出來

16

本堂課內容

本堂課內容

移動&跳躍

  • 移動腳本
  • 跳躍功能目標

RigidBody(鋼體)

1

這是什麼?

Rigidbody,也就是鋼體

是將GameObject跟物理引擎結合的核心Component

能夠讓GameObject在物理的控制下做出行動

如力、重力、扭力矩等

2

另外

兩個Collider要發生碰撞也需要其中一個GameObject有Rigidbody才行

屬性與設定

3

屬性與設定

  • 加入Rigidbody

在遊戲角色上加一個Rigidbody 2D

(善用搜尋會快很多)

4

屬性與設定

5

  • 屬性說明

Rigidbody類型

  • Dynamic:會碰撞,可受推拉力、重力影響,有質量
  • Kinematic:可在腳本控制下移動,不受推拉力、重力、碰撞所影響,所以沒有質量但有速度,消耗效能低
  • Static:完全不會動,目前沒有用過,可以想想看會用在哪

物理材質

鋼體(也就是GameObject)的質量

移動阻力(無限大則不能開始移動、可以立刻停止移動)

轉動阻力(無限大不能開始轉動、不能停止轉動)

重力倍數(0則不受其影響)

碰撞偵測模式

是否模擬

  • Discrete:離散模式,僅偵測移動後新位置的碰撞
  • Continuous:連續模式,更密集地偵測所有碰撞,可以避免穿模,但很吃效能

凍結X/Y軸(不可移動)

凍結Z軸(不可旋轉)

屬性與設定

6

  • 常用成員

屬性與設定

7

  • 常用成員
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RigidbodySample : MonoBehaviour
{
    Rigidbody2D rb2D; //宣告Rigidbody2D物件
    void Start()
    {
        rb2D = GetComponent<Rigidbody2D>(); //獲取鋼體參考
        rb2D.MovePosition(new Vector2(10,31)); //直接移動到指定座標,如果GameObject需要物理互動就用這個,不要改Transform!!
        rb2D.SetRotation(135); //直接旋轉到指定度數
        rb2D.velocity = new Vector2(3,4); //鋼體的速度,是一個向量,有方向性
        rb2D.AddForce(new Vector2(5,-7.5f), ForceMode2D.Force); //施力於鋼體之上,一樣是個向量,預設模式為ForceMode2D.Force
        rb2D.AddForce(new Vector2(5,-7.5f), ForceMode2D.Impulse); //比Force模式的施力時間更短,多用於跳躍
        rb2D.AddTorque(95); //施力矩於鋼體之上,正數為逆時針(正向)
    }
}

屬性與設定

8

新增了Rigidbody 2D之後

你的遊戲角色就會掉到平台上了!

但他還沒辦法移動...

  • 新增之後...

自訂材質

Unity裡面也可以自訂碰撞器表面的材質

用來做出不同的碰撞/接觸效果

9

當在Unity裡新增Collider和Rigidbody之後

他們的預設材質會是None 因為你還沒有設定

自訂材質

10

由於摩擦力可能造成角色卡牆等不必要的問題

我不想讓遊戲角色有磨擦力

按下左上的Assets --> Create --> 2D --> Physics Material 2D

取個名字之後看到Inspector

  • 新增材質

摩擦係數

恢復係數(0~1,0不會反彈,1則為彈性碰撞,沒有能量損耗)(以上建立在Physical-Base的移動上,等等會說)

自訂材質

11

之後把這個拖進Collider和Rigidbody的Material框框裡

  • 設定材質

本堂課內容

本堂課內容

移動&跳躍

  • 跳躍功能目標

1

  • 移動腳本

上次的因為沒有考慮物理互動

所以這次要用另一個移動的方式(但輸入方式一樣兩種都行)

舊版:

移動&跳躍

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

public class PhysicalOldPlayerMoveSample : MonoBehaviour
{
    float horizontal; //水平軸值,-1~1
    float vx; //水平方向移動速度
    
    [SerializeField] float moveSpeed = 200f; //移動速度

    Rigidbody2D rb2D; //鋼體

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界
    
    void Start() {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f; 
        xBound = yBound/9f*16f; //16:9
    }

    void Facing(){ //前面講過的翻轉,以軸值來判斷角色移動方向,讓角色能左右轉向(之後可以把功能像這樣包裝成函式會很好管理)
        if(horizontal <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontal >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontal * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){ //邊界限制
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontal >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontal <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    void Update(){ //輸入一律放在Update裡
        horizontal = Input.GetAxis("Horizontal"); //回傳平滑處理過的水平軸值(-1~1)

        Facing();
    }

    void FixedUpdate() { //移動程式放在FixedUpdate裡
        Move();
    }
}

2

  • 移動腳本

新版:

移動&跳躍

using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem; //這要自己加!!沒加用不了Input System
using UnityEngine;
using System;

public class PhysicalPlayerInputActionSample : MonoBehaviour
{
    [SerializeField] InputAction movement; //可於編輯器修改的InputAction class實例
    float vx; //一次水平方向移動距離

    public Vector2 input;
    float horizontalInput; //水平軸值

    Rigidbody2D rb2D;

    [SerializeField] float moveSpeed = 10f; //移動速度

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界

    void Start()
    {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f; 
        xBound = yBound/9f*16f;
    }
    
    private void OnEnable() { //啟用物件時同時啟用InputAction
        movement.Enable();
    }

    private void OnDisable() { //停用物件時同時停用InputAction
        movement.Disable();    
    }

    void Facing(){ //翻轉方向
        if(horizontalInput <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontalInput >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontalInput * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontalInput >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontalInput <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    void Update() {
        input = movement.ReadValue<Vector2>(); //讀取前面Input System的Vector2 Value
        horizontalInput = input.x; //水平軸值

        Facing();
    }

    void FixedUpdate() {
        Move();
    }
}

3

  • 移動腳本

寫好之後記得拖進角色

之後改好速度

按AD應該就可以移動啦~

移動&跳躍

4

  • 跳躍腳本

當你在做的是一個上帝視角遊戲

Z軸會是上方

所以不會有「跳躍」這回事

直接用上次的上下左右移動方式就好

移動&跳躍

5

  • 跳躍腳本

但如果你做的是平台遊戲或其他

則會需要有跳躍的功能

移動&跳躍

6

要做到跳躍

我們要達成幾個目標

  1. 跳躍輸入偵測
  1.  
  2. 跳躍動作
  1.  
  2.  
  3. 偵測角色是否在地面上,不在地面上不可以起跳
  • 跳躍腳本---跳躍輸入偵測

這很簡單

我們用Input.GetKeyDown()就能輕鬆偵測指定按鍵被按下

移動&跳躍

7

增加判斷按鍵按下的Code

要用到前面的Rigidbody.AddForce函式

移動&跳躍

8

增加宣告

  • 跳躍腳本---跳躍動作

增加施力程式

舊版:

移動&跳躍

9

  • 跳躍腳本---跳躍動作
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PhysicalOldPlayerMoveSample : MonoBehaviour
{
    float horizontal; //水平軸值,-1~1
    float vx; //水平方向移動速度
    
    [SerializeField] float moveSpeed = 200f; //移動速度

    [SerializeField] float jumpForce = 10f; //跳躍力道

    Rigidbody2D rb2D; //鋼體

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界
    
    void Start() {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f; 
        xBound = yBound/9f*16f; //16:9
    }

    void Facing(){ //前面講過的翻轉,以軸值來判斷角色移動方向,讓角色能左右轉向(之後可以把功能像這樣包裝成函式會很好管理)
        if(horizontal <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontal >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontal * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){ //邊界限制
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontal >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontal <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    void Jump(){
        if(Input.GetKeyDown(KeyCode.Space)){ //Input.GetKeyDown()在按鍵按下的一瞬間會回傳True
            rb2D.AddForce(new Vector2(0,jumpForce),ForceMode2D.Impulse); //施一個大小為(0,jumpForce的脈衝力於鋼體上)
        }
    }

    void Update(){ //輸入一律放在Update裡
        horizontal = Input.GetAxis("Horizontal"); //回傳平滑處理過的水平軸值(-1~1)

        Facing();
    }

    void FixedUpdate() { //移動程式放在FixedUpdate裡
        Move();
        Jump();
    }
}

新版:

移動&跳躍

10

  • 跳躍腳本---跳躍動作
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem; //這要自己加!!沒加用不了Input System
using UnityEngine;
using System;

public class PhysicalPlayerInputActionSample : MonoBehaviour
{
    [SerializeField] InputAction movement; //可於編輯器修改的InputAction class實例
    float vx; //一次水平方向移動距離

    public Vector2 input;
    float horizontalInput; //水平軸值

    Rigidbody2D rb2D;

    [SerializeField] float moveSpeed = 200f; //移動速度

    [SerializeField] float jumpForce = 10f; //跳躍力道

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界

    void Start()
    {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f;
        xBound = yBound/9f*16f; //16:9
    }
    
    private void OnEnable() { //啟用物件時同時啟用InputAction
        movement.Enable();
    }

    private void OnDisable() { //停用物件時同時停用InputAction
        movement.Disable();    
    }

    void Facing(){ //翻轉方向
        if(horizontalInput <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontalInput >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontalInput * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontalInput >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontalInput <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    void Jump(){
        if(Input.GetKeyDown(KeyCode.Space)){ //Input.GetKeyDown()在按鍵按下的一瞬間會回傳True
            rb2D.AddForce(new Vector2(0,jumpForce),ForceMode2D.Impulse); //施一個大小為(0,jumpForce的脈衝力於鋼體上)
        }
    }

    void Update() {
        input = movement.ReadValue<Vector2>(); //讀取前面Input System的Vector2 Value
        horizontalInput = input.x; //水平軸值

        Facing();
    }

    void FixedUpdate() {
        Move();
        Jump();
    }
}

完成之後就可以跳躍了!

也可以調整重力參數來改變下落速度喔

移動&跳躍

11

這很正常

因為現在還沒做是否在地上的判斷

所以一直按空白鍵可以一直施力在角色上

所以他會飛走

但是你可能會說

:學長我的角色飛走了

  • 跳躍腳本---跳躍動作

移動&跳躍

12

  • 跳躍腳本---地面偵測

有兩個地面偵測的方法

  1. Collider(較差)
  1.  
  2. Raycast(較好)

我希望能都講但不知道會不會不夠時間

如果超過時間的話

還請各位多包涵😅

移動&跳躍

13

前面有說Collider可以偵測兩個GameObject之間的碰撞

以下三個函式就可以做到:

  • 跳躍腳本---Collider地面偵測

移動&跳躍

14

  • 跳躍腳本---Collider地面偵測

再提到另一個前面有稍微講到的概念:Tag

可以理解成標籤

有將遊戲角色分類的能力

移動&跳躍

15

  • 跳躍腳本---Collider地面偵測

先按下Inspector裡上方的Tag --> Add Tag...

我有這麼多是因為有另一個遊戲在背後開發

移動&跳躍

16

  • 跳躍腳本---Collider地面偵測

之後會看到這個畫面

按右下角的「+」  -->  輸入名字  -->  按save

移動&跳躍

17

  • 跳躍腳本---Collider地面偵測

把平台全選加入tag

移動&跳躍

18

  • 跳躍腳本---Collider地面偵測

接著加入狀態機宣告

碰撞偵測函式

移動&跳躍

19

  • 跳躍腳本---Collider地面偵測

以及判斷式

判斷是否在空中才進行施力

大功告成!

移動&跳躍

20

  • 跳躍腳本---Collider地面偵測

舊版:

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

public class PhysicalOldPlayerMoveSample : MonoBehaviour
{
    float horizontal; //水平軸值,-1~1
    float vx; //水平方向移動速度
    
    [SerializeField] float moveSpeed = 200f; //移動速度

    [SerializeField] float jumpForce = 10f; //跳躍力道

    Rigidbody2D rb2D; //鋼體

    bool isJumping = true; //跳躍狀態機,true表示正在跳躍,無法再跳一次;false則表示可以跳躍

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界
    
    void Start() {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f; 
        xBound = yBound/9f*16f; //16:9
    }

    void Facing(){ //前面講過的翻轉,以軸值來判斷角色移動方向,讓角色能左右轉向(之後可以把功能像這樣包裝成函式會很好管理)
        if(horizontal <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontal >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontal * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){ //邊界限制
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontal >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontal <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    void OnCollisionEnter2D(Collision2D collision) { //當碰撞發生(碰撞某物件)
        if(collision.gameObject.tag == "platform"){ //用tag判斷碰撞物件是否為地板
            isJumping = false; //狀態機改動
        }
    }

    void OnCollisionExit2D(Collision2D collision) { //當碰撞停止(脫離某物件)
        if(collision.gameObject.tag == "platform"){ //用tag判斷脫離物件是否為地板
            isJumping = true; //狀態機改動
        }
    }

    void Jump(){
        if(Input.GetKeyDown(KeyCode.Space)){ //Input.GetKeyDown()在按鍵按下的一瞬間會回傳True
            if(!isJumping){
                isJumping = true; //因為OnCollisionExit可能有時間誤差,有時會施兩次力產生暴跳,所以要加一個狀態機改動在這
                rb2D.AddForce(new Vector2(0,jumpForce),ForceMode2D.Impulse); //施一個大小為(0,jumpForce的脈衝力於鋼體上
            }
        }
    }

    void Update(){ //輸入一律放在Update裡
        horizontal = Input.GetAxis("Horizontal"); //回傳平滑處理過的水平軸值(-1~1)

        Facing();
    }

    void FixedUpdate() { //移動程式放在FixedUpdate裡
        Move();
        Jump();
    }
}

移動&跳躍

21

  • 跳躍腳本---Collider地面偵測

新版:

using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem; //這要自己加!!沒加用不了Input System
using UnityEngine;
using System;

public class PhysicalPlayerInputActionSample : MonoBehaviour
{
    [SerializeField] InputAction movement; //可於編輯器修改的InputAction class實例
    float vx; //一次水平方向移動距離

    public Vector2 input;
    float horizontalInput; //水平軸值

    Rigidbody2D rb2D;

    [SerializeField] float moveSpeed = 200f; //移動速度

    [SerializeField] float jumpForce = 10f; //跳躍力道

    bool isJumping = true; //跳躍狀態機,true表示正在跳躍,無法再跳一次;false則表示可以跳躍

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界

    void Start()
    {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f;
        xBound = yBound/9f*16f;
    }
    
    private void OnEnable() { //啟用物件時同時啟用InputAction
        movement.Enable();
    }

    private void OnDisable() { //停用物件時同時停用InputAction
        movement.Disable();    
    }

    void Facing(){ //翻轉方向
        if(horizontalInput <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontalInput >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontalInput * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontalInput >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontalInput <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    void OnCollisionEnter2D(Collision2D collision) { //當碰撞發生(碰撞某物件)
        if(collision.gameObject.tag == "platform"){ //用tag判斷碰撞物件是否為地板
            isJumping = false; //狀態機改動
        }
    }

    void OnCollisionExit2D(Collision2D collision) { //當碰撞停止(脫離某物件)
        if(collision.gameObject.tag == "platform"){ //用tag判斷脫離物件是否為地板
            isJumping = true; //狀態機改動
        }
    }

    void Jump(){
        if(Input.GetKeyDown(KeyCode.Space)){ //Input.GetKeyDown()在按鍵按下的一瞬間會回傳True
            if(!isJumping){
                isJumping = true; //因為OnCollisionExit可能有時間誤差,有時會施兩次力產生暴跳,所以要加一個狀態機改動在這
                rb2D.AddForce(new Vector2(0,jumpForce),ForceMode2D.Impulse); //施一個大小為(0,jumpForce的脈衝力於鋼體上
            }
        }
    }

    void Update() {
        input = movement.ReadValue<Vector2>(); //讀取前面Input System的Vector2 Value
        horizontalInput = input.x; //水平軸值

        Facing();
    }

    void FixedUpdate() {
        Move();
        Jump();
    }
}

移動&跳躍

22

  • 跳躍腳本---Raycast地面偵測

剛剛我們使用Collider來做地面偵測

缺點是如果角色在掉落前再次碰到平台

那麼玩家就可以再跳一次

而且因Collider的特性,有些精細的碰撞會有縫隙以致無法偵測

如果我們想解決這個問題呢?

移動&跳躍

23

  • 跳躍腳本---Raycast地面偵測

這東西有點難理解

但不錯用

所以我還是介紹了

Raycast!

可以理解成從一個指定的中心點做一條有寬度的射線

經旋轉後指向某個方向

射線範圍內與Collider的碰撞會被以「RaycastHit2D」的資料型態回傳

包含碰到的Collider資訊(GameObject等)、在哪個位置碰撞到之類的

移動&跳躍

24

  • 跳躍腳本---Raycast地面偵測

首先

Raycast自己不是單獨的Component

他是包含在Physics2D class底下的方法

分成Box(矩形)/Capsule(膠囊形)/Circle(圓形)三種

這次我選擇BoxCast示範(也可以用其他的)

移動&跳躍

25

  • 跳躍腳本---Raycast地面偵測

點進API的BoxCast說明可以看到一坨參數

指定的中心點

矩形大小(射線寬度)

旋轉角度

方向向量

射線長度(矩形長度)

偵測指定Layer中的GameObject

Z軸最低

Z軸最高

Layer是啥??

移動&跳躍

26

  • 跳躍腳本---Raycast地面偵測

一個類似圖層的東西

前面講到的Sorting Layer就是他

除了可以幫很大量的東西初步分類

也可以將同種的GameObject放在同個Layer裡

然後篩選要偵測的

 

(Collider2D裡也有個Layer Overrides可以改

等等可以用之前的遊戲示範效果給你們看)

Layer

移動&跳躍

27

  • 跳躍腳本---Raycast地面偵測

這裡我們要用Layer來區分玩家與平台

按下Inspector右上方的Layer選單 --> Add Layer...

移動&跳躍

28

  • 跳躍腳本---Raycast地面偵測

隨便選一個空格填入名字

移動&跳躍

29

  • 跳躍腳本---Raycast地面偵測

之後全選平台

按下Inspector右上方的Layer選單 --> 你的Layer名稱

Layer就設定好了

移動&跳躍

30

  • 跳躍腳本---Raycast地面偵測

再來開始寫程式

(可以複製一份出來改)

加入宣告

取得參考

移動&跳躍

31

  • 跳躍腳本---Raycast地面偵測

接著是主程式

移動&跳躍

32

  • 跳躍腳本---Raycast地面偵測

最後是判斷

就大功告成啦!

還有除錯(寫在Update裡)

移動&跳躍

33

  • 跳躍腳本---Raycast地面偵測

舊版

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

public class PhysicalOldPlayerMoveSampleRC : MonoBehaviour
{
    float horizontal; //水平軸值,-1~1
    float vx; //水平方向移動速度
    
    [SerializeField] float moveSpeed = 200f; //移動速度

    [SerializeField] float jumpForce = 10f; //跳躍力道

    Rigidbody2D rb2D; //鋼體

    [SerializeField] float raycastLength = 2f; //Raycast長度

    [SerializeField] LayerMask platformLayer; //可在Editor裡修改的圖層

    PolygonCollider2D polygonCollider; //需要collider參考中心點

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界
    
    void Start() {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        polygonCollider = GetComponent<PolygonCollider2D>();
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f; 
        xBound = yBound/9f*16f; //16:9
    }

    void Facing(){ //前面講過的翻轉,以軸值來判斷角色移動方向,讓角色能左右轉向(之後可以把功能像這樣包裝成函式會很好管理)
        if(horizontal <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontal >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontal * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){ //邊界限制
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontal >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontal <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    bool IsOnGround(){
        RaycastHit2D rh2D = Physics2D.BoxCast(polygonCollider.bounds.center,spriteRBound/4,0f,Vector2.down,raycastLength,platformLayer);
        /*
        參數由左向右:
            中心點:polygonCollider.bounds.center,也就是collider的中心點
            大小:sprite的一半
            旋轉角度:0度
            方向向量:朝下
            長度:0.1
            碰撞圖層:platformLayer
        */

        if(rh2D.collider != null){ //因為已經隔離platform所以可以直接這樣寫
            return true;
        }
        else{
            return false;
        }
    }

    void Jump(){
        if(Input.GetKeyDown(KeyCode.Space)){ //Input.GetKeyDown()在按鍵按下的一瞬間會回傳True
            Debug.Log("Jump");
            if(IsOnGround()){
                rb2D.AddForce(new Vector2(0,jumpForce),ForceMode2D.Impulse); //施一個大小為(0,jumpForce的脈衝力於鋼體上
            }
        }
    }

    void Update(){ //輸入一律放在Update裡
        horizontal = Input.GetAxis("Horizontal"); //回傳平滑處理過的水平軸值(-1~1)

        Facing();
        Debug.DrawLine(polygonCollider.bounds.center,polygonCollider.bounds.center+Vector3.down*raycastLength); //繪製一條跟Raycast長度一樣的線來除錯
    }

    void FixedUpdate() { //移動程式放在FixedUpdate裡
        Move();
        Jump();
    }
}

移動&跳躍

34

  • 跳躍腳本---Raycast地面偵測

新版

using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem; //這要自己加!!沒加用不了Input System
using UnityEngine;
using System;

public class PhysicalPlayerInputActionSampleRC : MonoBehaviour
{
    [SerializeField] InputAction movement; //可於編輯器修改的InputAction class實例
    float vx; //一次水平方向移動距離

    public Vector2 input;
    float horizontalInput; //水平軸值

    Rigidbody2D rb2D;

    [SerializeField] float moveSpeed = 200f; //移動速度

    [SerializeField] float jumpForce = 10f; //跳躍力道

    [SerializeField] float raycastLength = 2f; //Raycast長度

    [SerializeField] LayerMask platformLayer; //可在Editor裡修改的圖層

    PolygonCollider2D polygonCollider; //圖像渲染器(獲取Sprite邊界)

    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    Vector3 spriteRBound; //Sprite邊界

    void Start()
    {
        rb2D = GetComponent<Rigidbody2D>(); //取得鋼體Component參考
        polygonCollider = GetComponent<PolygonCollider2D>();
        spriteR = GetComponent<SpriteRenderer>(); //取得渲染器Component參考
        spriteRBound = spriteR.bounds.size/2; //Vector直接除2可以得到半長寬

        yBound = 5f;
        xBound = yBound/9f*16f;
    }
    
    private void OnEnable() { //啟用物件時同時啟用InputAction
        movement.Enable();
    }

    private void OnDisable() { //停用物件時同時停用InputAction
        movement.Disable();    
    }

    void Facing(){ //翻轉方向
        if(horizontalInput <= -0.01f && transform.localScale.x > 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
        else if(horizontalInput >= 0.01f && transform.localScale.x < 0){
            transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z);
        }
    }

    void Move(){ //負責移動的函式
        vx = horizontalInput * Time.deltaTime * moveSpeed; //計算移動速度

        if(transform.localPosition.x + spriteRBound.x < xBound && transform.localPosition.x - spriteRBound.x > -xBound){
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
        else if((transform.localPosition.x + spriteRBound.x >= xBound && horizontalInput >= 0) || (transform.localPosition.x - spriteRBound.x <= -xBound && horizontalInput <= 0)){
            rb2D.velocity = new Vector2(0,rb2D.velocity.y); //不移動(速度為0)
        }
        else{
            rb2D.velocity = new Vector2(vx,rb2D.velocity.y); //更改鋼體速度
        }
    }

    bool IsOnGround(){
        RaycastHit2D rh2D = Physics2D.BoxCast(polygonCollider.bounds.center,spriteRBound/4,0f,Vector2.down,raycastLength,platformLayer);
        /*
        參數由左向右:
            中心點:polygonCollider.bounds.center,也就是collider的中心點
            大小:sprite的一半
            旋轉角度:0度
            方向向量:朝下
            長度:0.1,太短要改長不然可能會偵測不到
            碰撞圖層:platformLayer
        */

        if(rh2D.collider != null){ //因為已經隔離platform所以可以直接這樣寫
            return true;
        }
        else{
            return false;
        }
    }

    void Jump(){
        if(Input.GetKeyDown(KeyCode.Space)){ //Input.GetKeyDown()在按鍵按下的一瞬間會回傳True
            if(IsOnGround()){
                rb2D.AddForce(new Vector2(0,jumpForce),ForceMode2D.Impulse); //施一個大小為(0,jumpForce的脈衝力於鋼體上
            }
        }
    }

    void Update() {
        input = movement.ReadValue<Vector2>(); //讀取前面Input System的Vector2 Value
        horizontalInput = input.x; //水平軸值

        Facing();
        Debug.DrawLine(polygonCollider.bounds.center,polygonCollider.bounds.center+Vector3.down*raycastLength); //繪製一條跟Raycast長度一樣的線來除錯
    }

    void FixedUpdate() {
        Move();
        Jump();
    }
}

移動&跳躍

35

  • 跳躍腳本---Raycast地面偵測

腳本丟進Inspector裡之後記得要設定Layer再執行喔

本堂課內容

本堂課內容

陷阱&鏡頭跟追

1

  • 碰撞偵測腳本

陷阱&鏡頭跟追

2

先用前面建立平台的方式建立幾個紅色的方塊當陷阱

記得加入Collider之後複製幾個

(切記 這個做法不正確 之後會教怎麼使用Prefab 會了之後就不要用複製的了喔)

  • 碰撞偵測腳本

陷阱&鏡頭跟追

3

再來就是寫腳本

開一個新的

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

public class TrapSample : MonoBehaviour
{
    GameObject cat; //宣告遊戲物件
    [SerializeField] Vector3 startpos;

    void Start(){
        cat = GameObject.FindGameObjectWithTag("Player"); //用FindGameObjectWithTag()參考Tag為Player的角色
    }

    void OnCollisionEnter2D(Collision2D collision){
        if(collision.gameObject == cat){ //判斷碰撞對象是否為貓
            cat.GetComponent<Rigidbody2D>().position = startpos; /*用GameObject的GetComponent<>()
                                                                   取得貓的鋼體之後移動回起點*/
        }
    }
}
  • 碰撞偵測腳本

陷阱&鏡頭跟追

4

把腳本拖進陷阱的Inspector之後自訂起點就完成啦~

現在貓碰到陷阱之後應該就會回到起點了

陷阱&鏡頭跟追

現在我想讓鏡頭的中心一直擺在角色的中心

直接來寫程式吧

  • 鏡頭跟追腳本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraTrackingSample : MonoBehaviour
{   
    Transform catTrans; //宣告Transform物件
    void Start()
    {
        catTrans = GameObject.FindGameObjectWithTag("cat").transform; //用FindGameObjectWithTag()參考Tag為cat的角色並參考其Transform
    }

    void Update()
    {
        transform.position = new Vector3(catTrans.position.x,catTrans.position.y,-10); //不斷改動鏡頭Transform的x和y
    }
}

陷阱&鏡頭跟追

之後把腳本拖進Main Camera的Inspector

鏡頭應該就會跟著角色移動啦~

  • 鏡頭跟追腳本

陷阱&鏡頭跟追

7

  • 番外:滑鼠+碰撞器

如果你的遊戲有需要拖動、點擊的元件,可以透過Collider輕鬆達成

但UI介面有Unity內建的系統可以用,所以按鈕等功能不需要用Collider來實現,之後會教

陷阱&鏡頭跟追

8

  • 滑鼠+碰撞器:地圖自訂Pt.1

第一版程式(開新腳本)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem; //要讀取鼠標位置需使用InputSystem,記得using 

public class CustomizableMapFirst : MonoBehaviour
{
    void OnMouseDrag() { //當滑鼠在角色上拖動時
        //用Mouse.current.position.y.value讀取鼠標的位置
        /*由於初始坐標系為(0,0)~(1920,1080),所以我們要先 -(960,540) 之後讓它變成(-960,-540)~(960,540)後
          再 /(1920,1080)*(16,9) 才能轉換成現在的坐標系(-8.3333,-5)~(8.3333,5)*/
        //將角色Position的x和y設為換算後滑鼠Position的x和y,z不變
        transform.position = new Vector3((Mouse.current.position.x.value-960)/1920*16,(Mouse.current.position.y.value-540)/1080*9,transform.position.z);
    }
}

好簡單喔!

陷阱&鏡頭跟追

9

  • 滑鼠+碰撞器:地圖自訂Pt.1

把腳本拖進陷阱的Inspector之後

試著拖曳看看

你會發現陷阱雖然有跟著鼠標在相對移動

但位置不是在鼠標上

為什麼?

陷阱&鏡頭跟追

10

  • 滑鼠+碰撞器:地圖自訂Pt.1

經過實驗之後

我發現鼠標的座標是相對於畫面的

當鏡頭移動(x,y)的時候

鼠標的位置不會隨鏡頭位置而變

換算後依然是(-8.3333,-5)~(8.3333,5)

但實際上座標系統中心已經不是(0,0)

這時候再用鼠標位置定位

就會相對於鼠標產生(-x,-y)的偏移

陷阱&鏡頭跟追

11

  • 滑鼠+碰撞器:地圖自訂Pt.1

實際演示!

陷阱&鏡頭跟追

12

  • 滑鼠+碰撞器:地圖自訂Pt.1

所以我們需要將這個偏移量矯正回來

怎麼做?

把換算出的鼠標x,y座標加上相機x,y即可

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem; //要讀取鼠標位置需使用InputSystem,記得using 

public class CustomizableMap : MonoBehaviour
{
    Transform mainCameraTrans; //宣告相機Transform物件

    void Start()
    {
        mainCameraTrans = GameObject.FindGameObjectWithTag("MainCamera").transform; //用FindGameObjectWithTag()參考Tag為MainCamera的角色並參考其Transform
    }

    void OnMouseDrag() { //當滑鼠在角色上拖動時
        //用Mouse.current.position.y.value讀取鼠標的位置
        /*由於初始坐標系為(0,0)~(1920,1080),所以我們要先 -(960,540) 之後讓它變成(-960,-540)~(960,540)後
          再 /(1920,1080)*(16,9) 才能轉換成現在的坐標系(-8.3333,-5)~(8.3333,5)*/
        //由於鼠標座標是相對於畫面,因此需將換算出的座標加上相機座標才可矯正偏移
        //將角色Position的x和y設為換算後滑鼠Position的x和y,z不變
        transform.position = new Vector3((Mouse.current.position.x.value-960)/1920*16+mainCameraTrans.position.x,(Mouse.current.position.y.value-540)/1080*9+mainCameraTrans.position.y,transform.position.z);
    }
}

陷阱&鏡頭跟追

13

  • 滑鼠+碰撞器:地圖自訂Pt.1

之後將腳本拖到所有的平台和陷阱的Inspector裡

你應該就可以獲得一張可以單次自訂的地圖了

不錯吧~

大家也可以試著做一些優化

比如拖曳的時候讓角色不要受重力影響

才不會修改的時候一直往下掉之類的

做出來可以跟我分享

期待大家的創意!

陷阱&鏡頭跟追

14

  • 滑鼠+碰撞器:地圖自訂Pt.1

:但我想儲存地圖還有新增地圖物件欸

那就要用到之後會教的Prefab和UI系統啦

但我們這學期不會再見面了(好像吧

所以下學期記得再來上課~

(如果我肝沒爆的話應該還是我上課la

希望大家喜歡這學期的課!也謝謝大家的捧場~

(這堂課好像是最多人來上的o(≧∀≦)o  我好快樂

下學期我們不見不散~

Q&A&DO

建北電資Unity遊戲開發小社第3、4堂

By MH Yang

建北電資Unity遊戲開發小社第3、4堂

  • 83