Unity2d速通

Lachrymosa發表於2024-08-11

1 概述

關於Unity正文前,先大致整理一些預備內容和框架。

  • Unity是一個平臺/引擎,有很多庫檔案,開發者需要做兩件事:利用Unity自帶的圖形化介面(Inspector皮膚)掛載調整;利用自己寫的C#(Script指令碼)呼叫庫檔案實現邏輯。

  • 一個Unity工程有多個場景(Scene),每個場景下有多個物件(GameObject,也稱物件),一個物件就是一個容器(Container),容器中有很多元件(Component),一個元件有很多屬性(Attribute)。

  • 通常一個Unity2d專案的物件包括:基礎(Camera、EventSystem、Light等)、環境(Background、Midground、Foreground等)、遊戲(Player、Enemy等)、使用者介面(Canvas、UI Elements等)。

  • Unity的xyz座標系是左手系。對於在3D場景中z座標更小(朝外)但圖層更低的物件,在2D中會被z座標更大(朝裡)的物件遮住。

  • 做好的複用物件模版儲存在預製體(Prefab)中。整體Assets資料夾通常包括Art、Animation、Prefab、Scene、Script等。

  • 如果需要共享工程,只需要打包Assets、Packages、ProjectSettings三個資料夾。

2 C#

好了,讓我們學習程式碼基礎。

2.1 讓變數顯示在Inspector皮膚

兩種方式:SerializeField(序列化欄位)或Public(公有)。

    [SerializeField] private float walkSpeed = 1;
    public float jumpForce = 45f;

區別在於SerializeField仍保持封裝不被外部指令碼修改(推薦),Public的變數則是完全公開修改的。

通常為了易讀性,這樣組織變數定義:

    [Header("Ground Check Settings:")]
    [SerializeField] private Transform groundCheckPoint; //point at which ground check happens
    [SerializeField] private float groundCheckY = 0.2f; //how far down from ground chekc point is Grounded() checked
    [SerializeField] private float groundCheckX = 0.5f; //how far horizontally from ground chekc point to the edge of the player is
    [SerializeField] private LayerMask whatIsGround; //sets the ground layer
    [Space(5)]

可以在定義時賦值或不賦值。

2.2 生命週期函式

  • Awake():初始化,最先呼叫

  • OnEnable():物件被啟用時呼叫

  • Start():在Awake()和OnEnable()之後呼叫

  • FixedUpdate():按固定間隔,多幀呼叫

  • Update():每幀呼叫

  • LateUpdate():在Update()之後,每幀呼叫

  • OnDisable():物件被禁用時呼叫

  • OnDestroy():物件被銷燬時呼叫

單例模式(Singleton)的類在同一時刻只存在一個例項(比如玩家角色就是單例),並且提供全域性訪問點,是典型的Awake()函式實現:

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            Instance = this;
        }
        DontDestroyOnLoad(gameObject);
    }

Start()通常用於獲取元件和屬性,為了避免後面每一幀Update()頻繁呼叫,例如:

    void Start()
    {
        pState = GetComponent<PlayerStateList>();

        rb = GetComponent<Rigidbody2D>();
        sr = GetComponent<SpriteRenderer>();

        anim = GetComponent<Animator>();

        gravity = rb.gravityScale;

        Health = maxHealth;
        Mana = mana;
        manaStorage.fillAmount = Mana;
    }

Update()處理每一幀動畫,以下是一個完整的2D角色單幀處理流程:

    void Update()
    {
        if (pState.cutscene) return;

        GetInputs();
        UpdateJumpVariables();

        if (pState.dashing) return;
        RestoreTimeScale();
        FlashWhileInvincible();
        Move();
        Heal();
        CastSpell();
        if (pState.healing) return;
        Flip();
        Jump();
        StartDash();
        Attack();
    }

FixedUpdate()用於處理多幀動作:

    private void FixedUpdate()
    {
        if (pState.dashing || pState.healing || pState.cutscene) return;
        Recoil();
    }

2.3 獲取Input輸入

這些可以在Input Manager調整,程式碼如:

    void GetInputs()
    {
        xAxis = Input.GetAxisRaw("Horizontal");
        yAxis = Input.GetAxisRaw("Vertical");
        attack = Input.GetButtonDown("Attack");
    }

2.4 地面檢測

這個功能通常需要完成兩個步驟:將地面設定為專用圖層,然後給物件新增地面檢查點。

圖層設定可以在Inspector中完成(指派Ground圖層為whatIsGround),地面檢查點作為子物件附加到父物件(groundCheck寬度小於父物件的Box Collider)。

程式碼可以這樣實現:

    public bool Grounded()
    {
        if (Physics2D.Raycast(groundCheckPoint.position, Vector2.down, groundCheckY, whatIsGround) 
            || Physics2D.Raycast(groundCheckPoint.position + new Vector3(groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround) 
            || Physics2D.Raycast(groundCheckPoint.position + new Vector3(-groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

這裡Raycast(射線檢測)指的是從起點沿著指定的方向發射一條射線,檢測是否與任何物體發生碰撞。Vector2和Vector3分別代表二維向量與三維向量,Vector2.down是正下方,groundCheckX和groundCheckY是檢測範圍長度。

2.5 左右翻轉

這可以透過一個狀態bool變數實現。

    public bool lookingRight;

然後編寫程式碼:

    void Flip()
    {
        if (xAxis < 0)
        {
            transform.localScale = new Vector2(-Mathf.Abs(transform.localScale.x), transform.localScale.y);
            pState.lookingRight = false;
        }
        else if (xAxis > 0)
        {
            transform.localScale = new Vector2(Mathf.Abs(transform.localScale.x), transform.localScale.y);
            pState.lookingRight = true;
        }
    }

Transform是少數不需要手動GetComponent()的元件,其三個屬性分別是position、rotation、localscale。

另外移動的程式碼如下:

    private void Move()
    {
        if (pState.healing) rb.velocity = new Vector2(0, 0);
        rb.velocity = new Vector2(walkSpeed * xAxis, rb.velocity.y);
        anim.SetBool("Walking", rb.velocity.x != 0 && Grounded());
    }

2.6 幀間時間deltatime

為了避免幀率對時間造成影響,採用deltatime(兩幀的時間間隔)如:

    void Attack()
    {
        timeSinceAttack += Time.deltaTime;
        if (attack && timeSinceAttack >= timeBetweenAttack)
        {
            timeSinceAttack = 0;
            anim.SetTrigger("Attacking");

            if (yAxis == 0 || yAxis < 0 && Grounded())
            {
                Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, recoilXSpeed);
                Instantiate(slashEffect, SideAttackTransform);
            }
            else if (yAxis > 0)
            {
                Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, recoilYSpeed);
                SlashEffectAtAngle(slashEffect, 80, UpAttackTransform);
            }
            else if (yAxis < 0 && !Grounded())
            {
                Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, recoilYSpeed);
                SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
            }
        }

這裡也用到了自定義的計時器,用於控制時間間隔。

2.7 視覺化除錯OnDrawGizmos

這種方法只在編輯模式和遊戲模式下的場景檢視中起作用,不會在實際遊戲執行時的螢幕上顯示。

    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
        Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
        Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
    }

2.8 協程Coroutine

協程用於處理需要一定時間完成的動作。以衝刺為例:

    IEnumerator Dash()
    {
        canDash = false;
        pState.dashing = true;
        anim.SetTrigger("Dashing");
        rb.gravityScale = 0;
        int _dir = pState.lookingRight ? 1 : -1;
        rb.velocity = new Vector2(_dir * dashSpeed, 0);
        if (Grounded()) Instantiate(dashEffect, transform);
        yield return new WaitForSeconds(dashTime);
        rb.gravityScale = gravity;
        pState.dashing = false;
        yield return new WaitForSeconds(dashCooldown);
        canDash = true;
    }

3 皮膚

3.1 影像

PPU(Pixels Per Unit)越大,匯入圖片越小,畫面越清晰。

Max Size是匯入的圖片在縮放下不超過的最大尺寸,如果過小會使得畫面模糊。

Filter Mode是過濾模式,通常多線性的紋理效果優於點線性。

Wrap Mode是環繞模式,Repeat會在超出範圍時重複顯示,Clamp是鉗制。

3.2 剛體

Rigidbody處理物體的物理運動,包括重力、速度、力。

注意如果子物件不是剛體,那麼父物件的Collider是自己和子物件的Collider之和。此時子物件可以觸發父物件的OnTriggerEnter2D()。

3.3 SortingLayer

Unity有兩種自帶的圖層:Layer和Sorting Layer。前者如檢測碰撞用於圖層邏輯(邏輯分層),後者用於顯示渲染覆蓋(排序分層)。

Sorting Layer在製作環境、背景時非常有用,可以製作景深、霧氣、特殊的前後遮擋效果等等。

3.4 URP Lighting

2D Light在新版Unity中被放進URP(Universal Render Pipeline)Package中。

為了新增Lighting,需要把所有被Light的物件材質換成URP材質。

選擇需要的Sorting Layer新增2D Light,適當調整顏色和強度以匹配環境。

4 動畫

4.1 K幀

K幀即關鍵幀動畫,透過在關鍵幀之間插值實現平滑過渡。

Unity的K幀動畫在Animation視窗,樸素的方法是把預製的關鍵幀Sprite新增進去儲存,會自動在Animator視窗生成狀態機,然後可以在程式碼中呼叫如:

        anim.SetBool("Walking", rb.velocity.x != 0 && Grounded());

這樣可以控制Animator的標記變數實現條件狀態轉移。

4.2 骨骼動畫

樸素的K幀消耗大量時間,這時可以用Unity的skinning editor處理分層的單張圖片(推薦psb/psd,也可以是png;Unity可以自動切割圖片,注意背景透明)。

通常先繫結(Rigging)骨骼(如要細緻動畫可在頭髮或衣服上新增分叉的骨骼),並在Visualization分配骨骼排序。然後使用Auto Geometry生成權重,應用後在需要動畫的物件新增Sprite Skin元件建立骨骼。

如果需要為同一個物件準備多個Sprite,那麼都對其作繫結,後續動畫採用Sprite Resolver或Sprite Swap等元件設定同一個物件不同Sprite的可見性。

武器裝備之類應當和角色骨骼層級相同,不應低於角色。

4.3 IK反向動力學

IK Manager可以實現子骨骼連續帶動父骨骼轉動的效果。

這通常是把手、腳等邊緣骨骼掛載到IK Manager的List中,然後建立Target,並對翻轉、約束作調整。

4.4 粒子系統ParticleSystem

粒子系統可以製作動畫特效。

首先需要粒子的Material,然後在層次皮膚建立粒子系統,在Inspector中配置。

相關文章