Unity Sunny Land開發流程(二)

csu_xiji發表於2020-11-25

0.開發流程一

  詳見https://blog.csdn.net/xiji333/article/details/109621328

1.Animation Events

  首先來修改之前程式碼中的一個問題,你可能已經發現了,就是當人物從高處落下(不是跳躍後落下)時,動畫並沒有切換到 f a l l i n g falling falling,依然是 i d l e idle idle狀態,而且此時落到敵人頭上並不會消滅敵人,因為動畫不是 f a l l i n g falling falling狀態,現在來解決這個問題吧:

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

public class PlayerController : MonoBehaviour
{
    void SwitchAnimation()
    {
        //animator.SetBool("idle", false);
        if (rb2d.velocity.y < 0 && !boxCollider2D.IsTouchingLayers(ground)) //y軸速度<0且沒有接觸到地面時
        {
            animator.SetBool("falling", true);
        }
    }
}

  記得也要修改 p l a y e r player player的狀態機。
  接下來製作敵人的動畫:
在這裡插入圖片描述
  設定狀態機:
在這裡插入圖片描述
  在修改程式碼之前,我們先來捋一下思路。我們要實現的是青蛙的移動,但是青蛙不能一直在跳躍呀,他需要適時的回到 i d l e idle idle狀態,能不能找到一種方法,讓青蛙在 i d l e idle idle動畫播放完畢後自動跳躍一次呢?可以,通過 A n i m a t i o n   E v e n t s Animation\ Events Animation Events我們可以在某個動畫的某一幀設定一個事件,讓他呼叫某個函式。那麼我們可以在 i d l e idle idle結束的時候設定一個 e v e n t s events events,呼叫函式使青蛙進入跳躍狀態,同時我們還需要在 u p d a t e update update內修改播放的動畫,而且青蛙移動的邏輯也需要修改了,如果青蛙在跳躍過程中轉向的話肯定會非常奇怪吧?所以我,我們需要提前判斷落點位置是否在地面上,如果不在的話就需要提前進行轉向啦。

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

public class Enemy_Frog : MonoBehaviour
{
    private Rigidbody2D rb2d;
    private Animator animator;
    private CircleCollider2D circleCollider2D;

    public LayerMask ground;
    //為了增加遊戲的多樣性 我們可以設定minSpeed和maxSpeed 隨機選擇中間的某個值作為青蛙的移動速度
    public float minSpeed = 2.5f;
    public float maxSpeed = 4f;
    public float jumpForce = 4.5f;
    [SerializeField]
    private bool faceLeft = true;
    //記錄初始時的y座標
    private float initPositionY;
    // Start is called before the first frame update
    void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        circleCollider2D = GetComponent<CircleCollider2D>();
        initPositionY = transform.position.y;
    }

    // Update is called once per frame
    void Update()
    {
        SwitchAnimation();
    }

    //這個函式將通過animation event呼叫!
    void Movement()
    {
        Vector2 frontPosition = transform.position;
        frontPosition.y = initPositionY;
        float speed = Random.Range(minSpeed, maxSpeed);
        //預測落地 這裡並沒有精確計算 不過和你的jumpForce有關係
        if (faceLeft)
            frontPosition += Vector2.left * speed;
        else
            frontPosition += Vector2.right * speed;
        if (!Physics2D.Linecast(frontPosition + Vector2.down, frontPosition, ground) || Physics2D.Linecast(frontPosition, frontPosition + Vector2.up * jumpForce, ground)) //沒有檢測到地面 或者頭上有障礙物 
        {
            faceLeft = !faceLeft;
            transform.localScale = new Vector3(faceLeft ? 1 : -1, 1, 1); //角色反向
            //注意此時應該結束這個函式 不然frog反向後依然會跳出去 然而你並不能確定這次跳躍的有效性
            animator.Play("Frog_idle", 0, 0f);
            return;
        }
        rb2d.velocity = new Vector2(faceLeft ? -speed : speed, jumpForce);
        animator.SetBool("jumping", true);
    }

    void SwitchAnimation()
    {
        if (animator.GetBool("jumping")) //跳躍狀態
        {
            if (rb2d.velocity.y < 0)
            {
                animator.SetBool("jumping", false);
                animator.SetBool("falling", true);
            }
        }
        else if (animator.GetBool("falling")) //下落狀態
        {
            if (circleCollider2D.IsTouchingLayers(ground))
            {
                animator.SetBool("falling", false);
            }
        }
    }

    private void OnDrawGizmosSelected()
    {
        Vector2 frontPosition = new Vector2(transform.position.x, initPositionY) + Vector2.left * minSpeed * (faceLeft ? 1 : -1);
        Gizmos.DrawLine(frontPosition + Vector2.down, frontPosition + Vector2.up * jumpForce);
        frontPosition = new Vector2(transform.position.x, initPositionY) + Vector2.left * maxSpeed * (faceLeft ? 1 : -1);
        Gizmos.DrawLine(frontPosition + Vector2.down, frontPosition + Vector2.up * jumpForce);
    }

}

在這裡插入圖片描述
  先扯一下 d e b u g debug debug的辛酸歷程,首先是青蛙的移動問題,程式碼中只判斷了落點的有效性,然而這並不能證明當前位置和落點之間都是地面,所以青蛙還是有機率會跳出地圖外,這取決於你地圖的搭建以及 m i n S p e e d 、 m a x S p e e d 、 j u m p F o r c e minSpeed、maxSpeed、jumpForce minSpeedmaxSpeedjumpForce這三個數的值,不過對於我的地圖而言,上面的這種做法夠用了;其次是如何拿到落點的有效位置,我這裡只能做近似估計,而且要記錄青蛙初始的 y y y座標,否則基於當前座標再判斷落點的話是比較麻煩的;最後是一種比較尷尬的情況,因為 s p e e d speed speed是隨機的,那麼有可能出現青蛙往左跳往右跳都不行的情況,我的做法是讓青蛙原地不動,再次播放 i d l e idle idle動畫,順便提一下,我的動畫的 l o o p loop loop選項被關閉了,所以通過程式碼再次播放,如果你的動畫是重複播放的,那麼就無需通過程式碼控制。綜上,這種做法並不完美,但是我目前並不打算修改orz。

2.Class呼叫&死亡動畫&繼承多型

  首先來製作老鷹:
在這裡插入圖片描述
  然後製作老鷹的動畫:
在這裡插入圖片描述
  接下來開始寫控制老鷹移動的程式碼,這裡用的邏輯和青蛙不一樣哦,我打算指定一個上下邊界,讓老鷹在這個邊界內上下移動:

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

public class Enemy_Eagle : MonoBehaviour
{
    private Rigidbody2D rb2d;

    private float minPositionY, maxPositionY;
    [SerializeField]
    private bool isUp = false;

    public float moveLength = 2.5f;
    public float speed = 3.5f;
    // Start is called before the first frame update
    void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
        minPositionY = transform.position.y - moveLength;
        maxPositionY = transform.position.y + moveLength;
    }

    // Update is called once per frame
    void Update()
    {
        Movement();
    }

    void Movement()
    {
        if (transform.position.y > maxPositionY)
            isUp = false;
        else if (transform.position.y < minPositionY)
            isUp = true;
        rb2d.velocity = new Vector2(0, isUp ? speed : -speed);
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.DrawLine(new Vector2(transform.position.x, minPositionY), new Vector2(transform.position.x, maxPositionY));
    }
}

  然後製作消滅青蛙的動畫:
在這裡插入圖片描述
在這裡插入圖片描述

  現在來捋一下思路,我們之前是怎麼消滅青蛙的?是在人物的 O n C o l l i s i o n E n t e r 2 D OnCollisionEnter2D OnCollisionEnter2D裡面直接銷燬了對應的遊戲物件。當遊戲比較簡單時這麼做無可厚非,但是現在青蛙死亡要播放一段動畫,如果把這段邏輯也交給人物來控制的話,是不是不太合理?我們希望青蛙自己控制這段邏輯,並向外提供一個介面供人呼叫即可。 o k ok ok,那麼還有一個問題,青蛙要先播放完動畫再消滅自己,這個怎麼控制呢?當然是利用我們上一節剛學過的 e v e n t s events events辣:
E n e m y F r o g . c s : Enemy_Frog.cs: EnemyFrog.cs

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

public class Enemy_Frog : MonoBehaviour
{
    private void Death()
    {
        Destroy(gameObject);
    }

    //供player呼叫
    public void JumpOn()
    {
        animator.SetTrigger("death");
    }
}

在這裡插入圖片描述
P l a y e r C o n t r o l l e r . c s : PlayerController.cs: PlayerController.cs

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

public class PlayerController : MonoBehaviour
{
	……
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Enemy")
        {
            if (animator.GetBool("falling"))    //掉落狀態
            {
                collision.gameObject.GetComponent<Enemy_Frog>().JumpOn();
				……
            }
            ……
        }
    }
}

  至此青蛙的死亡動畫已經做好了,但是先別激動,你還有老鷹的沒做23333。不過在動手之前,我想請你認真思考一下,如果再從頭實現老鷹的死亡動畫的話,是不是大部分程式碼都是重複的?如果以後還要增加新的敵人,那豈不是每個都要重寫,而且在人物的程式碼中還要具體區分每一個敵人。想想就頭痛,那有沒有更好的辦法呢?當然有,我們可以使用物件導向的思維,給所有的敵人寫一個基類,讓敵人繼承這個基類。這樣不僅可以提高程式碼的複用,還可以讓程式更加靈活。接下來將涉及到繼承多型等知識,大家可以先學習一波。
E n e m y . c s : Enemy.cs: Enemy.cs

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

public class Enemy : MonoBehaviour
{
    // Start is called before the first frame update
    protected Animator animator;
    protected virtual void Start()
    {
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    private void Death()
    {
        Destroy(gameObject);
    }

    //供player呼叫
    public void JumpOn()
    {
        animator.SetTrigger("death");
    }
}

  老鷹和青蛙需要繼承這個基類,同時它們不需要自己定義 a n i m a t o r animator animator變數了, D e a t h Death Death J u m p O n JumpOn JumpOn兩個函式也可以省去。

E n e m y F r o g . c s : Enemy_Frog.cs: EnemyFrog.cs

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

public class Enemy_Frog : Enemy
{

    protected override void Start()
    {
        //呼叫基類的start
        base.Start();
    }

P l a y e r C o n t r o l l e r . c s : PlayerController.cs: PlayerController.cs

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

public class PlayerController : MonoBehaviour
{
    ……
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Enemy")
        {
            if (animator.GetBool("falling"))    //掉落狀態
            {
                //注意這裡得到的元件是 所有敵人的基類 Enemy
                collision.gameObject.GetComponent<Enemy>().JumpOn();
                ……
            }
            ……
        }
    }
}

3.音效Audio

在這裡插入圖片描述
  給 P l a y e r Player Player新增背景音樂:
在這裡插入圖片描述
  給兩個敵人新增爆炸的音效並修改程式碼:

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

public class Enemy : MonoBehaviour
{
    // Start is called before the first frame update
    protected Animator animator;
    protected AudioSource audioSource;
    protected virtual void Start()
    {
        animator = GetComponent<Animator>();
        audioSource = GetComponent<AudioSource>();
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    private void Death()
    {
        Destroy(gameObject);
    }

    //供player呼叫
    public void JumpOn()
    {
        animator.SetTrigger("death");
        audioSource.Play();
    }
}

  人物新增跳躍、受傷、拾取櫻桃的音效並通過程式碼控制:
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

4.對話方塊Dialog&淡入動畫效果

  這一節我們將建立一個對話方塊,用來在恰當的時機提示使用者如何進入下一個關卡,那麼就要用到 U I UI UI了:
在這裡插入圖片描述
  接下來設定它的位置,注意要修改錨點:
在這裡插入圖片描述
  接下來為它新增一個子物體 T e x t Text Text,並設定相關的屬性:
在這裡插入圖片描述
  我想把房子的門當作下一關的入口,所以我需要一個碰撞體,並把它當作觸發器使用,同時建立一個指令碼用來控制對話方塊的顯示與關閉。如果你需要多個這樣的入口,那麼你可以自己實現一個預製體——帶有控制指令碼和碰撞體的空遊戲物件,這樣可以不用重新寫程式碼,只需要把它放置在合適的位置即可。
在這裡插入圖片描述

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

public class EnterDialog : MonoBehaviour
{
    // Start is called before the first frame update
    public GameObject dialog;
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag == "Player")
        {
            dialog.SetActive(true);
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.tag == "Player")
        {
            dialog.SetActive(false);
        }
    }
}

  接下來給我們的對話方塊製作一段簡單的動畫效果吧~注意這次的動畫和之前的不太一樣,之前是通過多張圖片來製作的,這次要錄製動畫。點選下圖所示的紅色圓圈,就可以開始錄製了。
在這裡插入圖片描述
  這個時候你可以修改對應物體(動畫所對應的物體)的顏色、透明度、位置、旋轉等等屬性,這些修改會以關鍵幀的形式表現出來:
在這裡插入圖片描述
  以我製作的為例,第一幀對話方塊的透明度為 0 0 0,第二幀背景的透明度回到之前設定的值,文字部分保持不變,第三幀文字部分的透明度回到之前設定的值。這時候點選播放就可以看到淡入效果啦。
  在開始下一章的內容之前,你需要自己搭建第二關的地形:
在這裡插入圖片描述

5.場景控制SceneManager&Invoke&BuildIndex

  首先給我們的場景底部加一條 D e a d   L i n e Dead\ Line Dead Line,當玩家掉落到這條線以下時認為遊戲失敗,並重新載入第一個場景,我們希望這個載入可以滯後兩秒執行( I n v o k e Invoke Invoke),同時停止播放背景音樂:
在這裡插入圖片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag == "Collection")
        {
            pickCherryAudio.Play();
            Destroy(collision.gameObject);
            ++cherry;
            cherryText.text = cherry.ToString();
        }
        else if (collision.tag == "DeadLine")
        {
            //注意這個只會停止播放第一個
            GetComponent<AudioSource>().Stop();
            //延遲2s後執行
            Invoke(nameof(Restart), 2f);
        }
    }

    void Restart()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }

  接下來製作第一關到第二關的跳轉。還記得我們之前製作的 d i a l o g dialog dialog嗎,它是用來提示玩家進入下一關卡的,那麼我們可以寫一個指令碼並將其掛在 d i a l o g dialog dialog上,這樣只有當 d i a l o g dialog dialog為活躍狀態時才能響應玩家的按鍵:

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

public class EnterHouse : MonoBehaviour
{
    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.E))
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
        }
    }
}

  上面函式的引數是場景的 b u i l d I n d e x buildIndex buildIndex,在哪可以看到呢?
在這裡插入圖片描述
在這裡插入圖片描述

6.趴下效果Crouch&Input設定

  在開始之前,我們需要設定一下趴下的按鍵:
在這裡插入圖片描述
在這裡插入圖片描述
  然後製作趴下的動畫並設定狀態機:
在這裡插入圖片描述
  控制動畫的程式碼很好寫,不多贅述。但是當人物下蹲時,我們也應該修改它的碰撞體的位置和大小,或者使用一個新的碰撞體,更進一步,我們不能無條件的信任玩家,如果他們下蹲進入了一個障礙物,然後站立起來,此時我們依然認為人物回到了 i d l e idle idle狀態的話就會有問題,比如卡在障礙物裡面、播放錯誤的動畫等等。所以我們需要判斷玩家的頭上有沒有障礙物。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
    private Rigidbody2D rb2d;
    private Animator animator;
    private BoxCollider2D boxCollider2D;
    [SerializeField]
    private int cherry = 0;
    [SerializeField]
    private bool isHurt = false;
    private Vector2 initBoxCollider2DOffset;
    private Vector2 initBoxCollider2DSize;
    private Vector2 crouchBoxCollider2DOffset = new Vector2(0, -0.57f);
    private Vector2 crouchBoxCollider2DSize = new Vector2(0.92f, 0.85f);


    public Text cherryText;
    public float speed = 7f;
    public float jumpForce = 7f;
    public LayerMask ground;
    public AudioSource jumpAudio;
    public AudioSource hurtAudio;
    public AudioSource pickCherryAudio;
    // Start is called before the first frame update
    void Start()
    {
        rb2d = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        boxCollider2D = GetComponent<BoxCollider2D>();
        initBoxCollider2DOffset = boxCollider2D.offset;
        initBoxCollider2DSize = boxCollider2D.size;
    }

    // Update is called once per frame
    void Update()
    {
        if (!isHurt)//非受傷狀態
        {
            Movement();
        }
        SwitchAnimation();
    }
  
    void Movement()
    {
        float horizontalMove = Input.GetAxis("Horizontal");
        int faceDirection = (int)Input.GetAxisRaw("Horizontal");
        rb2d.velocity = new Vector2(horizontalMove * speed, rb2d.velocity.y);
        animator.SetFloat("running", Mathf.Abs(horizontalMove));
        if (faceDirection != 0)
        {
            transform.localScale = new Vector3(faceDirection, 1, 1);
        }
        if (Input.GetButtonDown("Jump") && boxCollider2D.IsTouchingLayers(ground))
        {
            rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);
            animator.SetBool("jumping", true);
            jumpAudio.Play();
        }
        Crouch();
    }

    void Crouch()
    {
        //頭頂上沒有障礙物
        if (!Physics2D.OverlapCircle(transform.position, 0.4f, ground))
        {
            //持續按下
            if (Input.GetButton("Crouch"))
            {
                animator.SetBool("crouching", true);
                boxCollider2D.offset = crouchBoxCollider2DOffset;
                boxCollider2D.size = crouchBoxCollider2DSize;
            }
            else
            {
                animator.SetBool("crouching", false);
                boxCollider2D.offset = initBoxCollider2DOffset;
                boxCollider2D.size = initBoxCollider2DSize;
            }
        }
    }

    void SwitchAnimation()
    {
        //animator.SetBool("idle", false);
        if (rb2d.velocity.y < 0 && !boxCollider2D.IsTouchingLayers(ground)) //y軸速度<0且沒有接觸到地面時
        {
            animator.SetBool("falling", true);
        }
        if (animator.GetBool("jumping")) //跳躍狀態
        {
            if (rb2d.velocity.y < 0)
            {
                animator.SetBool("jumping", false);
                animator.SetBool("falling", true);
            }
        }
        else if (animator.GetBool("falling")) //下落狀態
        {
            if (boxCollider2D.IsTouchingLayers(ground))
            {
                animator.SetBool("falling", false);
                animator.SetBool("idle", true);
            }
        }
        else if (isHurt) //受傷狀態
        {
            animator.SetBool("hurt", true);
            int sign = rb2d.velocity.x < 0 ? -1 : 1;
            rb2d.velocity += new Vector2(speed * Time.deltaTime, 0f) * -sign;
            if (Mathf.Abs(rb2d.velocity.x) < 0.1f)
            {
                isHurt = false;
                animator.SetBool("hurt", false);
            }
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag == "Collection")
        {
            pickCherryAudio.Play();
            Destroy(collision.gameObject);
            ++cherry;
            cherryText.text = cherry.ToString();
        }
        else if (collision.tag == "DeadLine")
        {
            //注意這個只會停止播放第一個
            GetComponent<AudioSource>().Stop();
            //延遲2s後執行
            Invoke(nameof(Restart), 2f);
        }
    }

    void Restart()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Enemy")
        {
            if (animator.GetBool("falling"))    //掉落狀態
            {
                //注意這裡得到的元件是 所有敵人的基類 Enemy
                collision.gameObject.GetComponent<Enemy>().JumpOn();
                rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);    //小跳效果
                animator.SetBool("jumping", true);
            }
            else if (transform.position.x < collision.gameObject.transform.position.x)//左側
            {
                hurtAudio.Play();
                isHurt = true;
                rb2d.velocity = new Vector2(-7f, rb2d.velocity.y);
            }
            else if(transform.position.x > collision.gameObject.transform.position.x)//右側
            {
                hurtAudio.Play();
                isHurt = true;
                rb2d.velocity = new Vector2(7f, rb2d.velocity.y);
            }
        }
    }

}

7.2D光效

  這一節我們來做一些簡單的 2 D 2D 2D光效。我希望第二個場景總體是暗的,只有壁火和人物身上有光源。首先修改 t i l e m a p tilemap tilemap的渲染材質:
在這裡插入圖片描述
  自己建立一個 m a t e r i a l material material,把它新增到人物和門上面:
在這裡插入圖片描述
  然後在合適的位置新增點光源吧!記得修改光源的 z z z軸:
在這裡插入圖片描述

8.優化程式碼Fix code

  這一節用來優化之前的程式碼或者實現。首先可以清除 A n i m a t o r Animator Animator中一些沒用的條件,比如人物中的那個 i d l e idle idle變數。視訊中提到的其他問題我暫時沒有遇到(因為我的人物只有 1 1 1個碰撞體),所以不打算修改了,至於移動和跳躍的手感問題我打算放到最後說。

相關文章