Unity Game Development

第1~2話 細說遊戲物件 & 讓角色動起來!

Lecturer:水獺

目錄(可點)

本堂課內容

本堂課內容

遊戲物件 & 屬性

  • Component
    • 調整技巧
    • 啟用停用
    • 下拉選單

細談GameObject、Component

  • GameObject
    • 子物件
    • 啟用停用
    • 多選

細談GameObject、Component

  • Component

將鼠標移到值為數字的屬性上會出現雙箭頭,可以左右拖動直接調整數值

有三角形的區塊是一組,不用的時候可以按一下收起來不占空間

這裡可以控制Component的啟用與否,未打勾是停用(不會有任何效果),打勾是啟用

細談GameObject、Component

  • Component

按這個會連到這個Component的Unity Scripting Reference

回復出廠初始設置

快速移動屬性順序至最前/後

移動屬性順序至前/後

移除Component

複製Component

以新增的方式貼上複製的Component

以覆寫參數值的方式貼上複製的Component

也可以用拖的

在Scene搜尋有參考此Component的GameObject(少用)

將此視窗彈出為浮動視窗(少用)

細談GameObject、Component

  • GameObject

一個GameObject盡量不要加種類相同的兩個Component,避免執行時混亂、出錯

那如果我們想要兩個東西是一個整體,可以一起移動,但同時又保有各自的Component可以做不同的動作或被分開操控,像是角色與他的武器、火柴人的手與腳等等,那該怎麼做?

細談GameObject、Component

  • GameObject

子物件!我們可以:

  1. 選定一個部分當父物件,類似這個群體的頭領
  2. 之後將其他部分選取、拖曳到父物件上,這些部分就會變成父物件的子物件

當父物件被移動,子物件也會跟著移動,如繪圖軟體的「群組」功能,我自己也會拿來做分類

細談GameObject、Component

  • GameObject

Unity內部運作的原理為:

將子物件的Transform數值改為相對於父物件

舉個例,當子物件的LocalPosition(相對於父物件,會顯示於Inspector上)為(-5,9,0),父物件的Position為(6,15,0)

則子物件的實際位置為(1,24,0)

細談GameObject、Component

  • GameObject

GameObject跟Component一樣也有停用與否的勾選框,

停用某個物件代表的便是停用整個物件的Component

結合子物件的概念,當子物件被停用,父物件不受影響

但當父物件被停用,子物件也會同時受影響

  • Sprite是什麼?
  • Sprite名稱來源?
  • Sprite Renderer是什麼?
  • 圖像Assets設置
  • 細談Sprite Renderer
  • 細談Draw Mode

Sprite & SpriteRenderer

翻譯一下:Sprite  n.雪碧精靈

Sprite & SpriteRenderer

簡單來說,Sprite就是一個將多圖層/圖像整合在一個2D平面上的技術

"在電腦圖學中,當一張二維圖像整合進場景中,成為整個顯示圖像的一部分時,這張圖就稱為精靈圖"———維基百科

  • Sprite是什麼

因為這樣的圖像並不是背景的一部份,而是「懸浮」在背景之上,就像幽靈或精靈一樣,所以才以「精靈圖」命名當然你要叫他雪碧圖我也沒意見

  • Sprite名稱來源?

Sprite & SpriteRenderer

鏡頭(Camera)

遊戲物件(GameObjects)

遊戲畫面

再翻譯一下:Sprite  Renderer  n.精靈圖渲染器

Sprite & SpriteRenderer

  • SpriteRenderer是什麼

這大概是在2D遊戲開發中你唯一會用到的渲染器,我們會用這個Component來渲染Sprite到畫面上,實踐「多圖像整合」,有不同的渲染模式/功能,等等介紹

記得弄清楚:Sprite ≠ SpriteRenderer

 Sprite(物件外觀圖):資料夾中的Assets

 SpriteRenderer:綁定在GameObject上的Component,

           需要提供一個Sprite給它。

  1. 開啟上次的專案(沒來上課的就創一個)
  2. 點擊Project視窗中的圖片(綠框處),你就可以看到右方的Inspector視窗出現五花八門的設置選項

Sprite & SpriteRenderer

  • 圖像Assets設置

這個東西是專案管理,

之後會教

Sprite & SpriteRenderer

  • 圖像Assets設置

修改完記得按Apply

圖片壓縮品質,依照官方說法是會影響一部份的效能但我覺得不多,一般不用改,如果圖片畫素很低或者為像素風格則建議改None

圖片過濾器模式,若圖片不夠平滑(像素感太重)可使其平滑,但如果是像素風格圖片則須改Point,否則會模糊

可以改圖片的旋轉中心(Pivot,原為中心(0.5,0.5))、拉伸限制

一般會用Tight以節省效能,有興趣可以來問我,除非要用到其他的繪製模式(Draw Mode)不然別改Full Rect

類似比例尺,初始值為100,改小會放大圖像(如50即邊長變為兩倍),改大則相反。如果圖像不合大小可先改這裡,盡量不要先動GameObject的大小

圖像類型(一般只用Sprite)

Sprite & SpriteRenderer

  • 細談SpriteRenderer

渲染對象Sprite,可以從Project視窗將圖像直接拖進這個框裡來新增/切換Sprite,也可以按右邊的小圈圈雙擊檔案/搜尋檔名

渲染後疊加於Sprite上的顏色,之前我有用過的如:

  1. 一個白色的立體按鈕,疊加後就可以變成不同顏色,不須重畫;
  2. 可以實做移動到按鈕上變色的效果
  3. 也可以利用腳本調整Alpha透明度值實現物件淡入淡出

X、Y軸鏡像翻轉

繪製模式,微複雜,等等說

渲染材質,之後如果有教光線再改,不然不用改

圖片重疊排序順序,適用於較少物件之排序,可直接在同排序階層(Layer)做上下排序,數字越小越下面(類似同圖層)

圖片重疊排序階層,適用於較多物件之排序(類似不同圖層)

記得選取Scene中的遊戲物件,Inspector視窗才會顯示在右側

Sprite & SpriteRenderer

  • 細談Draw Mode

單純渲染,只能依原比例調整大小、位置

可依照自己喜好調整Sprite之長寬,單位為場景中的座標單位長

把圖片以如同磁磚般重複的形式渲染出來

(貓貓大軍!!!!

長寬調整如Sliced模式

Sprite & SpriteRenderer

  • 實作!
  1. 注意Sliced跟Tiled兩個模式都要把Mesh Type改成Full Rect,顯示才不會出問題
  2. 大家可以先玩玩看,也可以想想這個可以用在哪邊~

Transform

  • Transform是什麼?
  • Transform名稱來源?
  • Transform會用到的資料型態?

Transform

一個儲存GameObject在場景座標中的位置、旋轉、尺寸比例的Component,對於每個GameObject來說唯一

  • Transform是什麼

 

 

Scale(尺寸比例):

GameObject在場景座標中的位置,由於Unity最早是純3D引擎,所以這裡使用三維坐標系(x-左右、y-上下、z-前後,上、右、前為正向),2D遊戲中除了Camera的Z軸座標值為 -10 外,其他物件一律是0(否則Camera有機會看不到)

GameObject在三個座標軸上的旋轉角度,2D遊戲只會碰到Z軸上的旋轉。另外,雖然在Inspector中看起來是三維,實際上Unity中儲存角度的是一個叫四元數(Quaternion)的東西,等等會講

GameObject在三個座標軸上的尺寸比例,大於1會放大,在0~1之間會縮小,小於0時效果與以該軸為基準鏡像相同,比如說要讓遊戲角色面相反方向可以將X軸上的Scale改為-1;2D遊戲Z座標上的值不影響

 

Rotation(旋轉):

 

Position(位置):

顧名思義,Transform指的是「變形」,指的就是:

  1. 平移
  2. 旋轉
  3. 縮放
  4. 傾斜(以二維的角度來看,X、Y軸上的旋轉即是傾斜)

如果有用一些繪圖軟體的話,裡面也會有類似的選項

Transform

  • Transform名稱來源?

無論是Position、Rotation、Scale的數值都是用Vector3(三維向量)的資料型態來在腳本中儲存和使用的,等一下會講

Transform

  • Transform會用到的資料型態?

本堂課內容

本堂課內容

腳本(Script)

腳本(Script)

  • C#語言特性?

相信大家多少有碰過C++,不管是建電還是北資的大社課應該都有教

C#和C++的相同處:

  1. 分號很重要!!!一定要加不然會報錯
  2. 資料型態如:int、float、double、bool、string、char
  3. 運算子如:+ - * / = += -= *= /= && || ==
  4. 語法如:for、while、if else、switch case、struct(之後做商店很好用)
  5. 函式的定義方式

腳本(Script)

  • C#語言特性?

需要注意的地方:

  • 變數作用域(全域、區域變數)
  • 使用小數的時候,因為Unity默認讀取double格式,建議都定義成double型態,真的要用float記得在數字後加f,如:

 

  • C#中並沒有類似#define的東西可以省字
  • C#中有指標但不建議用,很麻煩,可用全域變數解決就用
  • 還是有與C++ STL略有不同的資料結構,用之前記得確認
float a = 0.1f

腳本(Script)

  • 基本架構

操作時間!這裡要認真聽喔!

  1. 在左下角的Project視窗點進Assets資料夾
  2. 按右鍵 → Create → Folder新增一個Scripts資料夾存放腳本
  3. 在Scripts資料夾裡再按右鍵 → Create → C# Script新增一個腳本檔案,取名甚麼隨便你,但記得第一個字母要大寫(之後建議用該腳本功能命名較好管理),副檔名會是.cs

注意!不要直接從VS Code裡新增.cs檔案,雖然副檔名對但腳本格式需要自己新增很麻煩

然後記得把腳本拖到角色的Inspector裡面,讓他變成一個Component

很重要

腳本(Script)

  • 基本架構

點兩下用VS code打開檔案後你會發現裡面長醬:

腳本(Script)

  • 基本架構

所以在Unity裡寫程式到底是怎麼一回事?

第一件要認知的事情是,C#語言中的所有東西,包含現在的腳本、GameObject、Component、各種不同的資料格式(Vector、圖像、資料結構等)都是一個由使用者自訂義的「Class(類別)」

Class?

接下來我們來看看這個聽起來很抽象的名詞是個什麼東西

腳本(Script)

  • 基本架構

以下是Class的大致結構

public class Color{ //大寫
    //那我們就讓它隨機好了,跟前面那隻ㄎㄧㄤˉ掉的一樣
}

public class Cat{ //大寫
    public int ID; //在變數前加上public可以讓腳本中的其他Class與其函式存取
    public Color color; //一個Color Class型態的變數
    int age; //沒有public的則為私有,不可被其他Class及其函式存取
    float h;
    float w;
    public float GetBMI(){ /*Class中也可以放函式,第一個字記得大寫,
                             同樣加public可被外部取用*/
        if(h <= 0 || w <= 0){
            return -1;
        }
        else{
            return w / (h * h);
        }
    }
}
public class MyCat{ //大寫
    Cat myCat = new Cat(); /*宣告一個Cat實例,
                             裡面包含Cat class中的所有變數、函式*/
    
    void catInfo(){
        myCat.ID = 951031; //public,可被存取
        myCat.h = 20.5; //無public,不可被存取,會出錯
        float BMI = myCat.GetBMI();
    }
}

這些類別的實例稱為物件,每個物件底下都有成員,這些成員可能是變數(資料型態可以是其他已宣告的Class)或是函式,成員變數的名稱為小寫字母開頭,成員函式(需有括號)的名稱是大寫開頭。

成員如果沒有在宣告時加上public,就無法被其他類別存取及使用,也可以設定成只能讀取不能修改。除非你的腳本中有需要被其他物件使用的函式或變數,否則不要隨便將成員設為public,確保不會被修改會比較好管理。

腳本(Script)

  • 基本架構

當我們在寫Unity腳本時,我們實際上是在創造出一個新的Class

而這個Class的物件會變成GameObject的一個Component

寫腳本時我們可以使用Unity已經寫好的一大堆Class

(即為Unity Scripting API)及其成員來做出我們想完成的功能

以上大概就是Unity Coding的模式

腳本(Script)

  • 基本架構

以Transform舉個例(等等的移動也會用這個Component):

宣告一個Transform物件

宣告一個Test函式

(所有除了宣告外的程式都要寫在裡面,不然會出錯)

(之後一樣盡量照功能命名,方便管理)

腳本(Script)

  • 基本架構

之後在函式中打出你宣告的Transform物件名稱加一個點,

如果你VSC模組有裝對則可以發現有個清單:

紫色方塊代表函式

扳手代表變數

這裡會列出所有這個物件擁有的成員,可以往下捲動

腳本(Script)

  • 基本架構

接著打上position並將鼠標停在字上,

你會發現有個提示框出現:

此成員的資料型態為Vector3,等等會講,簡單來說就是一個三維座標

剛剛講到的存取權:

  • get為可以取得值
  • set為可以改變值

兩個都有則代表此成員為public

成員簡短介紹

這個成員是屬於哪個Class的

腳本(Script)

  • 基本架構

給一個public的反例:static

這種成員型態是透過Class本身存取,即「Class名稱.成員名稱」

比如Vector3.forward,是一個已經定義好的、值為(0,0,1)的向量

這時將鼠標移到上面,會發現存取權處只有「get」而沒有「set」

即可以存取但不能修改

腳本(Script)

  • 基本架構

:好那我這樣寫

把位置設成(0,0,1),然後把角色隨便放到畫面中的一個位置

:執行!接下來呢?角色位置沒變啊?

腳本(Script)

  • 基本架構

讓我們看回現在的腳本

MonoBehaviour中的內建函式

寫在這裡的程式是當腳本開始運作時首先會執行一次的事

MonoBehaviour中的內建函式

當腳本開始運作後於每一幀執行一次的事

(還有一個FixedUpdate之後介紹,常用於角色移動、物理狀態更新)

類似C++的include、Python的import,之後如果會用到文字會在加,現在不用改

這行的意義是你正在撰寫一個叫「Sample」、繼承(等等會講一下) MonoBehaviour 的 Class

腳本(Script)

  • 基本架構

這是一張有關MonoBehaviour內建函式執行順序的圖,有興趣可以自己造訪Unity Documentation看官方說明

(真的很難)

常用的為Awake、Start、FixedUpdate及Update:

  • Awake:最先,遊戲初始化,如讀取資料庫、建構清單等
  • Start:第二,初始化變數
  • FixedUpdate:不斷,會每0.02秒執行一次,相對於Update計算幀數的執行方式更為穩定
  • Update:不斷,每一幀執行一次

腳本(Script)

  • 基本架構

:那我這樣寫就可以了對吧?

顯然否。

注意,現在的catTransform只是一個空的物件,根本還沒對應到任何一個實際的Transform Component

腳本(Script)

  • 基本架構

要讓catTransform對應到Component有兩種方式:

1.使用GetComponent<Component Name>()函式

功能是尋找同一個GameObject下的指定Component並回傳其參考(可以理解成一個與Component連結的東西,讓你能夠存取/修改其內容)

記得寫在Start()之內、Test()之前才有初始化的效果,不然跑到Test()就來不及了

2.於catTransform的宣告前加入[SerializeField],

Inspector中會多一個可互動的框,並手動拖曳Transform Component進框框

當然也可以寫public但不建議(難管理、Debug)

腳本(Script)

  • 基本架構

大功告成!

再次開始遊戲,這次你的角色應該就會回到中間啦!

腳本(Script)

  • 基本架構

補充:Unity Scripting API其實有內建一個名為

「transform」的成員來表示Script所屬的GameObject的Transform Component,所以可以這樣寫就OK

不過也不要覺得剛剛在浪費時間,因為除了Transform之外的Component幾乎都要用剛剛的方法才能存取及修改,只是因為目前只有提到Transform才以其為例

腳本(Script)

之前看到一個不錯的例子

我們可以用「杯子」來理解物件導向的概念

舉個例

一個杯子可能會有好幾種形式

有沒有杯耳、高度高低、杯口大小、材質...等等

這時我們就可以把這些「杯子的特性」

統整歸納成一張藍圖

就會比較好維護及方便設計使用

  • 淺談物件導向

腳本(Script)

那剛剛提到的「繼承」又是什麼意思?

再舉個例

如果現在你發現有些杯子有自己獨特的屬性

比如名貴的茶杯可能會有生產國家、生產年代等特殊資訊

但他同時也會有一般杯子所有擁有的屬性

那我們就可以透過「繼承」一般杯子的藍圖後

再增加一些特別的屬性

也就不用重新、重複地再畫一次一個杯子的藍圖

  • 淺談物件導向

本堂課內容

本堂課內容

讓角色動起來

  • 實作並讓角色移動

讓角色動起來

  • Vector2、Vector3

簡單來說就是存一個二維/三維座標系中的點

讓角色動起來

  • Vector2、Vector3
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class VectorSample : MonoBehaviour
{
    float exampleFloat;

    //宣告
    Vector2 vec2a,vec2b,vec2c;
    Vector3 vec3a,vec3b;
    void Start(){
        //賦值
        vec2a = Vector2.up; //(0,1)
        vec2b = Vector2.right; //(1,0)
        vec3a = Vector3.forward; //(0,0,1)
        vec3b.Set(0,0,0); //直接設定Vector的值
        vec3b = new Vector3(95.0f,10,31); //也可以使用new運算符,之後可能會細講

        //運算
        vec2c = vec2a + vec2b; //(1,1),(x1 + x2 , y1 + y2),減法同規則,Vector3也可用
        vec2c = vec2c * 10; //(10,10),(x * k , y * k),係數積,Vector3也可用
        vec2c *= 10; //同上

        //一些好用的方法/屬性,Vector3也可用,回傳型別為float
        exampleFloat = Vector2.Distance(vec2a,vec2b); //回傳兩個座標間的距離,計算方式就是直線長度公式
        exampleFloat = Vector2.Angle(vec2a,vec2b); //回傳起始座標與結束座標連線與水平方向的夾角
        exampleFloat = Vector2.SqrMagnitude(vec2a); //回傳向量長度的平方,若只是為了長度比較可以用這個代替(花費效能較少)
        exampleFloat = vec2a.sqrMagnitude; //同上
        exampleFloat = vec2a.magnitude; //回傳向量長度(花費效能較多)
        transform.position = Vector2.MoveTowards(transform.position, vec2c , 0.1f); /*計算一次向目標Vector移動後的位置後回傳,
                                                                                      把這行寫在Update或FixedUpdate裡面可以達成不斷向目標Vector移動的效果。
                                                                                      第一個參數為起點Vector,第二個為終點Vector,最後一個是每次執行會移動的距離,
                                                                                      可以當成移動速度來想*/

        transform.position = Vector2.Lerp(transform.position, vec2c , 0.5f); /*計算一次兩個參數Vector的插值後回傳,
                                                                               把這行寫在Update或FixedUpdate裡面可以達成「更加平滑地」不斷向目標Vector移動的效果。
                                                                               第一個參數為起點Vector,第二個為終點Vector,最後一個是插值,可以想成分點公式的兩線段比例,
                                                                               0會回傳起點Vector,1會回傳終點Vector,0.5則會回傳兩個向量的平均值*/

        exampleFloat = vec2a.x; //Vector的x項
        exampleFloat = vec2a.y; //Vector的y項
        exampleFloat = vec3a.z; //Vector的z項(Vector3獨有)
        vec2a.Normalize(); //將此Vector變成一個方向不變、長度為1的Vector
        vec2c = vec2b.normalized; //回傳一個同上的Vector,但不更改原Vector的值
    }
}

讓角色動起來

  • Vector2、Vector3
  • Transform

儲存一個GameObject的位置、旋轉及尺寸比例資訊

讓角色動起來

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

public class TransformSample : MonoBehaviour
{
    void Start(){
        transform.position = new Vector3(95,10,31); //(world) position,(世界中的)座標位置,以一個新的Vector3表示
        float x = transform.position.x; //position資料型態為Vector3,所以可以存取xyz值,但transform的各個變數都是唯獨
        transform.localPosition = Vector3.zero; //相對於父物件的位置,若無父物件則與position相同
        transform.localScale = Vector3.one; //GameObject於三維座標上的尺寸比例,有父物件則此數值為相對於父物件,沒有則同transform.scale
        transform.localScale = new Vector3(-transform.localScale.x,transform.localScale.y,transform.localScale.z); //x軸Scale設成與目前相反可以達成左右旋轉的效果
        transform.rotation = Quaternion.identity; //GameObject的旋轉,以Quaternion(四元數)格式儲存,非常複雜,此處的identity為不旋轉
        transform.rotation = Quaternion.Euler(95,10,31); //若想以較易理解的三維方式(歐拉角)旋轉可使用此函式
        Vector3 angle = transform.eulerAngles; //回傳當時之歐拉角

        Vector3 faceDir = transform.up; //為一指向GameObject初始狀態時上方的向量,會隨rotation而變,可直接修改以改變朝向
        faceDir = -transform.up; //只有transform.up及transform.right,若要左/下則加負號即可

        transform.Translate(new Vector3(95,10,31)); //將Position以指定的Vector3變動
    }
}
  • Transform

讓角色動起來

讓角色動起來

  • 其他比較細的
using System;
using System.Collections.Generic;
using UnityEngine;

public class OtherSample : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        float exampleFloat = 2.5f;
        float exampleFloatAbs = Mathf.Abs(exampleFloat); //abs函式可以回傳數值的絕對值

        SpriteRenderer exampleSpriteRenderer = GetComponent<SpriteRenderer>();

        GameObject exampleObject1 = GameObject.Find("cat"); //用名字找Scene中的遊戲物件並回傳第一個符合的
        GameObject exampleObject2 = GameObject.FindGameObjectWithTag("Cat"); //用Tag找Scene中的遊戲物件並回傳第一個符合的
        GameObject[] exampleObjects = GameObject.FindGameObjectsWithTag("Cat"); //用Tag找Scene中的遊戲物件並以Array回傳擁有此Tag的所有物件
        GameObject.Destroy(this.gameObject); //摧毀以此Script作為Component的遊戲物件

        exampleSpriteRenderer.enabled = true; //exampleSpriteRenderer的啟用狀態,可直接修改(相當於框框打勾與否)

        this.enabled = true; //此Script的啟用狀態,可直接修改(相當於框框打勾與否)

        exampleObject2.SetActive(false); //停用exampleObject2(相當於框框不打勾),啟用則改為true

        float DeltaTime = Time.deltaTime; //在FixedUpdate中,這個值大約為0.02秒,其他地方則為一幀的時間
    }
}

讓角色動起來

  • 其他比較細的

讓角色動起來

  • 鍵鼠輸入讀取—前言

 Unity中提供了兩種讀取使用者輸入的方式

因為是先後出現

所以我以新舊版系統稱呼

因為各有優缺所以我兩種都會說

大家可以依自己的喜好和需求決定要使用哪種

讓角色動起來

  • 鍵鼠輸入讀取—舊版

雖然說是舊版

但我自己比較常使用這種

優點是他的移動開始及停下可以做到非常平滑

不會有突然中斷的感覺(當然要直接停下也可以)

因為是內建的 所以引入使用也很容易

缺點是改綁定鍵比較複雜

而且好像做不到遊戲中改按鍵綁定的功能

讓角色動起來

  • 鍵鼠輸入讀取—舊版設定

按左上方的Edit --> Project Settings --> Input Manager

點一下Axes旁的小鍵頭

在這裡你可以設定想要綁定的按鍵及其他參數

這裡的鍵頭點開之後就可以看到設定選項

讓角色動起來

  • 鍵鼠輸入讀取—舊版設定

在寫程式的時候用來存取這個Axis的名字(第一個字盡量大寫)

第一組用來改變這個Axis的value的正負按鍵

第二組(以上四格留空則不會有該格的效果)

據說打開可以做到類似急停的效果

(正負鍵一起按可以停下)但疑似沒用

輸入類型(目前應該都用鍵鼠)

控制的軸(x、y、z),因為是Horizontal所以為x軸

用搖桿控制的時候可以設定哪支搖桿要對應這個Axis

讓角色動起來

  • 鍵鼠輸入讀取—舊版程式
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class OldPlayerMoveSample : MonoBehaviour
{
    float horizontal; //水平軸值,-1~1
    float vertical; //垂直軸值,-1~1
    float dx; //一次水平方向移動距離
    float dy; //一次垂直方向移動距離
    float boundedx; //被限制過的水平移動位置
    float boundedy; //被限制過的垂直移動位置
    
    [SerializeField] float moveSpeed = 10f; //移動速度
    
    [SerializeField] float xBound; //水平邊界
    [SerializeField] float yBound; //垂直邊界
    SpriteRenderer spriteR; //圖像渲染器(獲取Sprite邊界)
    Vector3 spriteRBound; //Sprite邊界
    
    void Start() {
        spriteR = GetComponent<SpriteRenderer>(); 
        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(){ //負責移動的函式,兩種改位置方法擇一
        //計算一次移動的距離,乘Time.deltaTime是為了讓一秒內移動的距離接近Input * moveSpeed的值
        dx = horizontal * Time.deltaTime * moveSpeed; 
        dy = vertical * Time.deltaTime * moveSpeed;

        transform.Translate(new Vector3(dx, dy, transform.localPosition.z)); /*改位置方法1:transfrom.Translate(Vector3)
                                                                               會把position.x/y/z值分別加上傳入的
                                                                               Vector3.x/y/z(如果要限制邊界要寫if else,邊界內才移動)*/

        //計算限制後的位置,前面的半長寬會用在這,spriteRBound.x為半寬(水平),.y則為半長(垂直)
        // Mathf.Clamp(value, min, max)可以將傳入的value限制在min跟max之間,低於min則等於min,高於max則等於max
        boundedx = Mathf.Clamp(transform.localPosition.x + dx , -xBound + spriteRBound.x, xBound - spriteRBound.x);
        boundedy = Mathf.Clamp(transform.localPosition.y + dy , -yBound + spriteRBound.y , yBound - spriteRBound.y);

        transform.position = new Vector3(boundedx, boundedy, transform.localPosition.z); /*改位置方法2:分別計算最後的x/y值後
                                                                                           直接改localPosition*/
    }

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

        Facing();
    }

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

寫好之後記得拖進Inspector

讓角色動起來

  • 鍵鼠輸入讀取—新版須知:KeyCode

鍵盤上的每一個鍵都有自己對應的KeyCode

用來讓電腦知道你在說的是哪個鍵

讓角色動起來

  • 鍵鼠輸入讀取—新版須知:KeyCode
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class KeyCodeSample : MonoBehaviour
{
    KeyCode wKey = KeyCode.W; //字母
    KeyCode mouseLeftKey = KeyCode.Mouse0; //滑鼠
    KeyCode alpha0Key = KeyCode.Alpha0; //上排數字
    KeyCode f1Key = KeyCode.F1; //F1-12
    KeyCode keypad0 = KeyCode.Keypad0; //右邊數字
    KeyCode upArrowKey = KeyCode.UpArrow; //箭頭

    void Start() { //會在等等講到的變更綁定中用到
        string wKeyName = wKey.ToString(); //可以用.ToString()將KeyCode轉為string,值是該Key的名稱
        bool isContain = "sdfASFaWez".Contains(wKeyName); //string.Contains(string)可判斷一個字串中是否包含某個字串
    }
}

常用的一些例子以及可能會用到的方法

其他KeyCode如果不知道可以去這裡

讓角色動起來

  • 鍵鼠輸入讀取—新版

Unity在2020年4月新出的一個Package

說實話也蠻久了(笑

但之前的學長沒有去研究

這次一起教給你們

優點是調整比較人性化 可以做到遊戲內更改按鍵綁定

缺點是無法做到平滑處理 而且引入比較麻煩

讓角色動起來

  • 鍵鼠輸入讀取—新版設定

按右上方的Window --> Package Manager

左上方的Packages選單往下拉 選Unity Registry

讓角色動起來

  • 鍵鼠輸入讀取—新版設定

在左邊會看到一堆Package的名字

向下捲找到Input System 按下Install

讓角色動起來

  • 鍵鼠輸入讀取—新版設定

按左上方的Edit --> Project Settings --> Player

點一下下面的「Other Settings」

 

讓角色動起來

  • 鍵鼠輸入讀取—新版設定

向下捲找到Active Input Handling,改成Both

如果之後不能用可以來檢查一下這邊有沒有跑掉

讓角色動起來

  • 鍵鼠輸入讀取—新版程式-1
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem; //這要自己加!!沒加用不了Input System
using UnityEngine;
using System;

public class PlayerInputActionSample : MonoBehaviour
{
    [SerializeField] InputAction movement; //可於編輯器修改的InputAction class實例

    void Start() {

    }

    void Update() {

    }
}

讓角色動起來

  • 鍵鼠輸入讀取—新版:設定InputAction

把腳本拖到Inspector裡

把剛剛的舊版腳本先停用

之後你會看到這個畫面

讓角色動起來

  • 鍵鼠輸入讀取—新版:設定InputAction

按下Movement旁邊的齒輪

選Vector2

讓角色動起來

  • 鍵鼠輸入讀取—新版:設定InputAction

按下Movement旁邊的+

點Add Up\Down\Left\Right Composite

三維雙向輸入,Value用Vector3

一維雙向輸入,Value用Axis

單鍵輸入

二維雙向輸入,Value用Vector2

雙鍵組合輸入,如ctrl+v

三鍵組合輸入,如ctrl+alt+del

讓角色動起來

  • 鍵鼠輸入讀取—新版:設定InputAction

之後會看到這樣,等等會細說這些是什麼

可以改名字

讓角色動起來

  • 鍵鼠輸入讀取—新版:設定InputAction

Input System分為三個階層

Action(動作集)

Composite(多個按鍵&行為綁定的組合)

Binding(一個按鍵&行為的綁定)

此處對於Vector:

   Up (按W) --> +Y

   Down (按S) --> -Y

   Left (按A) --> -X

   Right (按D) --> +X

讓角色動起來

  • 鍵鼠輸入讀取—新版:設定InputAction

在其中一個Binding上點兩下

這裡點開可以

手動改按鍵綁定

這裡可以改這個Binding在Composite裡的角色

(比如Up,Down,Left或Right)

讓Editor偵測你的輸入

直接輸入

讓角色動起來

  • 鍵鼠輸入讀取—新版:InputAction常用方法

讓角色動起來

  • 鍵鼠輸入讀取—新版:InputAction常用方法
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine;

public class InputSystemSample : MonoBehaviour
{
    [SerializeField] InputAction movement; //可於編輯器修改的InputAction class實例
    Vector2 input;
    void Start() {
        movement.Enable(); //啟用InputAction(可以輸入)
        movement.Disable(); //停用InputAction(不可輸入)

        movement.ChangeBinding("Up").WithPath("<Keyboard>/k"); /*變更按鍵綁定,其中
                                                                 movement為InputAction實例
                                                                 ChangeBinding(BindingName)指定要變更的綁定名稱
                                                                 WithPath(KeyPath)指定按鍵路徑*/
    }

    void Update() {
        input = movement.ReadValue<Vector2>(); //讀取InputAction被設定的資料型態裡的資料,剛剛設定的是Vector2
    }
}

讓角色動起來

  • 鍵鼠輸入讀取—新版程式
using System.Collections;
using System.Collections.Generic;
using UnityEngine.InputSystem; //這要自己加!!沒加用不了Input System
using UnityEngine;
using System;

public class PlayerInputActionSample : MonoBehaviour
{
    [SerializeField] InputAction movement; //可於編輯器修改的InputAction class實例
    float dx; //一次水平方向移動距離
    float dy; //一次垂直方向移動距離
    float boundedx; //被限制過的水平移動位置
    float boundedy; //被限制過的垂直移動位置

    Vector2 input;
    float horizontalInput; //水平軸值
    float verticalInput; //垂直軸值

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

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

    public static bool changing = false; //等待按鍵變更的狀態機
    public static string BindingName; //要變更的按鍵
    System.Array keycodes = System.Enum.GetValues(typeof(KeyCode)); //把所有KeyCode存成一個Array

    void Start()
    {
        spriteR = GetComponent<SpriteRenderer>();
        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 Change(){
        if(changing){ //當案件正在等待變更(狀態機為true)
            movement.Disable(); //因為Input.GetKey()方法和Input System會衝突,所以先停用

            //以下是一個很爛的「讀取現在按下的鍵」的寫法,但Unity沒有Input.keycode之類的東西就是沒有,超可悲,目前只能有這種跟另一種(比較吃效能)
            foreach(KeyCode keycode in keycodes) { //類似C++的for(int v:vector),反正就是一個遍歷Array值的for迴圈
                if (Input.GetKey(keycode)){ /*Input.GetKey(KeyCode or KeyName)可以判斷輸入的是否為指定的按鍵,
                                              用這個方式判斷目前迴圈跑到的KeyCode跟按下的KeyCode是不是一樣,達成判斷按鍵的功能*/

                    //用來偵測按下按鍵之後變更按鍵綁定的基礎程式
                    if(keycode.ToString().Contains("Mouse")){ //判斷是不是滑鼠,因為滑鼠鍵位在Input System及KeyCode中是不同的名字,需要特判
                        if(keycode == KeyCode.Mouse0){ //左鍵在KeyCode中為Mouse0
                            movement.ChangeBinding(BindingName).WithPath("<Mouse>/leftButton"); 
                            //左鍵在Input System中的Path為<Mouse>/leftButton
                        }
                        else if(keycode == KeyCode.Mouse1){ //右鍵在KeyCode中為Mouse1
                            movement.ChangeBinding(BindingName).WithPath("<Mouse>/rightButton"); 
                            //右鍵在Input System中的Path為<Mouse>/rightButton
                        }
                        else if(keycode == KeyCode.Mouse2){ //中鍵在KeyCode中為Mouse2
                            movement.ChangeBinding(BindingName).WithPath("<Mouse>/middleButton"); 
                            //中鍵在Input System中的Path為<Mouse>/middleButton
                        }
                        changing = false; //狀態機設定回false
                    }
                    else{
                        movement.ChangeBinding(BindingName).WithPath("<Keyboard>/" + keycode.ToString()); 
                        //其他鍵在Input System中的Path為<Keyboard>/Name
                        changing = false; //狀態機設定回false
                    }
                }
            }
        }
        else if(gameObject.activeSelf){ //檢查遊戲物件的停用/啟用狀態
            movement.Enable(); //若物件為啟用則啟用movement
        }
    }

    void Move(){ //負責移動的函式,兩種改位置方法擇一
        //計算一次移動的距離
        dx = horizontalInput * Time.deltaTime * moveSpeed;
        dy = verticalInput * Time.deltaTime * moveSpeed;

        transform.Translate(new Vector3(dx, dy, transform.localPosition.z)); /*改位置方法1:transfrom.Translate(Vector3)
                                                                               會把position.x/y/z值分別加上傳入的
                                                                               Vector3.x/y/z(如果要限制邊界要寫if else,邊界內才移動)*/

        //計算限制後的位置,前面的半長寬會用在這,spriteRBound.x為半寬(水平),.y則為半長(垂直)
        // Mathf.Clamp(value, min, max)可以將傳入的value限制在min跟max之間,低於min則等於min,高於max則等於max
        boundedx = Mathf.Clamp(transform.localPosition.x + dx , -xBound + spriteRBound.x, xBound - spriteRBound.x);
        boundedy = Mathf.Clamp(transform.localPosition.y + dy , -yBound + spriteRBound.y , yBound - spriteRBound.y);

        transform.position = new Vector3(boundedx, boundedy, transform.localPosition.z); /*改位置方法2:分別計算最後的x/y值後
                                                                                           直接改localPosition*/
    }

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

        Facing();
        Change();
    }

    void FixedUpdate() {
        Move();
    }
}

讓角色動起來

  • 鍵鼠輸入讀取—新版程式

另外我有寫一個

用按鈕控制現在是移動還是改按鍵的程式

也把基礎樣板做出來了 但背景、美編那些還沒放

有興趣可以來找我 大概會長這樣

讓角色動起來

  • 鍵鼠輸入讀取—新版

之後,按下開始

在Inspector裡調整你想要的速度之後,就可以移動你的角色了!

如果你想知道他是怎麼變色的可以來問我

讓角色動起來

  • 鍵鼠輸入讀取—新版

但是

退出之後...

:速度怎麼又變回10了?

讓角色動起來

  • 鍵鼠輸入讀取—新版

因為你是在遊戲模式裡面改Component參數

前面有講過 這樣不會儲存

那如果要讓遊戲模式裡改的參數可以被保存要怎麼做?

先複製

再貼上數值就可以了

但是如果有很多 那就截圖之後結束遊戲模式再慢慢打吧

讓角色動起來

  • 番外篇—旋轉程式

前面的移動都會了

那我們來做點其他的

現在我想做一顆會跟著貓移動

而且會隨著貓的移動輸入而旋轉的毛線球(感覺反了?

那程式怎麼寫?

讓角色動起來

  • 番外篇—旋轉程式
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Yarn_ball : MonoBehaviour
{
    [SerializeField] GameObject cat; //宣告一個可以在Editor中存取的GameObject物件

    [SerializeField] float rotatingSpeed = 1000f; //旋轉速度
    [SerializeField] float movingSpeed = 0.075f; //移動速度

    PlayerInputActionSample catInputAction; //一個PlayerInputActionSample物件,用來存取貓的移動輸入
    
    void Start() {
        catInputAction = cat.GetComponent<PlayerInputActionSample>(); //取得Component
        transform.position = new Vector3(0,4,0); //設定初始位置
    }

    void Update() {
        //旋轉,其中x,y角度不變,z角度則參照貓的x,y輸入決定大小,乘Time.deltaTime是為了讓一秒內旋轉的度數接近Input * RotatingSpeed的值
        transform.rotation = Quaternion.Euler(0,0,transform.eulerAngles.z + rotatingSpeed * Time.deltaTime * Mathf.Sqrt(Mathf.Pow(catInputAction.input.x,2) + Mathf.Pow(catInputAction.input.y,2)));
    }

    void FixedUpdate() { //兩種移動方式擇一
        transform.position = Vector3.Lerp(transform.position,cat.transform.position,0.01f); //平滑移動
        transform.position = Vector3.MoveTowards(transform.position,cat.transform.position,movingSpeed); //等速移動
    }
}

讓角色動起來

  • 番外篇—旋轉程式

剛剛應該有看到

腳本中是需要從另一個腳本Component讀取數值的

所以要記得把PlayerInputSystemSample裡的input加public

這樣才能用其他腳本存取

讓角色動起來

  • 番外篇—旋轉程式

新建一個毛線球 把腳本拖進去Inspector之後你會發現

下面多了一個空格

把遊戲角色從Hierarchy中拖進去

讓角色動起來

  • 番外篇—旋轉程式

就大功告成啦!!!

開始遊戲之後毛線球應該就會追著貓跑了

而且在按下WASD的時候毛線球會旋轉

如果要用舊版輸入也可以自己試著改改看

有問題歡迎問我

之後也可以自己試著做出其他的效果和功能!

Q&A&DO

Made with Slides.com