ETH官方钱包

前往
大廳
主題

Unity 入門教學 - (01) 製作一個小遊戲

Jerry | 2022-11-24 12:32:16 | 巴幣 1000 | 人氣 3317

前言-
我打算在這邊寫一些關於Unity遊戲製作的內(nèi)容,雖然說標題寫的是入門,這篇以後其實是計劃寫ML-Agents(神經(jīng)網(wǎng)路學習)的相關內(nèi)容,同樣都是非常基礎的教學。更新頻率不會太高,以我自己的行程為主。
這篇會講解如何製作一個簡單的遊戲,玩家可以在場景中操控一個方塊,藉由蒐集場上的目標來得分。

下一篇文章內(nèi)容預告:

另外,我並不打算全部從最基礎的內(nèi)容講起,而且會比較著重在實作的部分,因此理論也只會稍稍帶過,程式?jīng)]註解,但對有基礎的人應該不會太難。對於遊戲製作、程式等等完全沒有基礎的話,讀起來會比較吃力,不懂的地方都歡迎留言詢問。

先備知識-
至少要知道如何在Unity新增、修改元件的內(nèi)容,對Unity的系統(tǒng)有些微了解,以及有基礎的C#程式設計能力,這樣大致上就夠了,其他的部分會在提到時講解。
補充一點,英文能力雖然不是必須,但是對於學習遊戲製作、程式設計等方面還是有一定的幫助。

環(huán)境-
Unity 2020.3.15f1 LTS
Visual Studio 2022
建議使用相同或接近的Unity版本

進入正題-
完整示範影片
  • 新增一個3D專案。
  • 首先,在場景中加入兩個Cube,作為我們的玩家和平臺,將兩者命名為Player 和Platform,並調(diào)整好適當?shù)拇笮『臀恢?,也調(diào)整好Main Camera的位置和角度。接著替玩家新增一個Rigidbody元件。
  • 這邊先讓玩家可以移動。新增一個C# Script給Player,命名為PlayerMove,程式內(nèi)容如下:(建議將程式碼複製到IDE比較好讀)

PlayerMove.cs
using System.Collections;
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    public float moveSpeed = 3f;
    public float rotateSpeed = 50f;
    public float jumpForce = 300f;

    private Rigidbody playerRigidbody;
    private bool jumpButtonDown;

    private void Start()
    {
        playerRigidbody = GetComponent<Rigidbody>();
        jumpButtonDown = false;
    }

    private void Update()
    {
        if (Input.GetButtonDown("Jump"))
        {
            jumpButtonDown = true;
        }
    }

    private void FixedUpdate()
    {
        float moveZ = Input.GetAxisRaw("Vertical");
        Vector3 playerMove = transform.forward * moveZ * moveSpeed * Time.deltaTime;
        transform.position += playerMove;

        float rotateY = Input.GetAxisRaw("Horizontal");
        Vector3 playerRotate = new Vector3(0f, rotateY * rotateSpeed * Time.deltaTime, 0f);
        transform.Rotate(playerRotate);

        if (jumpButtonDown)
        {
            jumpButtonDown = false;
            Vector3 playerJump = transform.up * jumpForce;
            playerRigidbody.AddForce(playerJump);
        }
    }
}

  • 這邊透過修改位置,在預設輸入中,按下w, s或上下鍵,讓玩家能夠在自己的z軸方向前後移動。並讓玩家在按下a, d或左右鍵時,以每秒50度往左右旋轉(zhuǎn)。並在按下空白鍵時,向上施加加速度讓玩家跳躍。這邊將是否按下空白鍵的判定放在Update,因為GetButtonDown只會在按下空白鍵那一幀回傳true,而FixedUpdate的更新頻率與實際幀率不同,容易發(fā)生按下空白鍵卻沒反應的情況。
  • 回到場景中,輸入玩家移動和旋轉(zhuǎn)的速度、跳躍力道,按下Play後,現(xiàn)在應該能透過鍵盤來操控玩家了。
  • 這邊會發(fā)現(xiàn),只要不停按下空白鍵,我們的玩家就會無限上升,這大概不是我們想要的,但是現(xiàn)在先忽略。
  • 現(xiàn)在來製作我們的目標。在場景中新增另一個Cube,命名為Target,並將其Coiilder中的Is Trigger 打勾。其實我們的目標不需要任何程式,但這邊可以加入一點效果,讓遊戲看起來比較豐富。

  • 新增一個C# Script ,命名為RotatingCube。

RotatingCube.cs

using UnityEngine;

public class RotatingCube : MonoBehaviour
{
    void FixedUpdate()
    {
        Vector3 rotateAngle = new Vector3(15f, 30f, 45f);
        transform.Rotate(rotateAngle * Time.deltaTime);
    }
}

  • 這邊讓目標以每秒15, 30, 45度,在x, y, z軸旋轉(zhuǎn)。
  • 按下Play後,我們已經(jīng)可以移動玩家,並看到我們的目標在原地旋轉(zhuǎn)了。我們可以修改目標的Rotation 為(15, 30, 45),讓旋轉(zhuǎn)看起來比較正常,並把Scale縮小一點,才不會讓玩家看起來太小。

  • 這時候場景中一片白,很難分辨物體,我們可以加入Material,幫物件簡單的上色。
  • 現(xiàn)在讓玩家可以真正吃掉我們的目標,先新增一個Tag叫做Target,並將目標的Tag改為Target。

  • 在PlayerMove.cs中加入OnTriggerEnter函式:

PlayerMove.cs
using System.Collections;
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    public float moveSpeed = 3f;
    public float rotateSpeed = 50f;
    public float jumpForce = 300f;

    private Rigidbody playerRigidbody;
    private bool jumpButtonDown;

    private void Start()
    {
        playerRigidbody = GetComponent<Rigidbody>();
        jumpButtonDown = false;
    }

    private void Update()
    {
        if (Input.GetButtonDown("Jump"))
        {
            jumpButtonDown = true;
        }
    }

    private void FixedUpdate()
    {
        float moveZ = Input.GetAxisRaw("Vertical");
        Vector3 playerMove = transform.forward * moveZ * moveSpeed * Time.deltaTime;
        transform.position += playerMove;

        float rotateY = Input.GetAxisRaw("Horizontal");
        Vector3 playerRotate = new Vector3(0f, rotateY * rotateSpeed * Time.deltaTime, 0f);
        transform.Rotate(playerRotate);

        if (jumpButtonDown)
        {
            jumpButtonDown = false;
            Vector3 playerJump = transform.up * jumpForce;
            playerRigidbody.AddForce(playerJump);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag("Target"))
        {
            other.gameObject.SetActive(false);
        }
    }
}

  • 這邊在玩家碰到為Trigger的Collider,並且該物件的Tag為Target時,將碰到的物件活動狀態(tài)設為false,即物件從場上消失。
  • 回到Unity,按下Play,我們已經(jīng)可以操控玩家,吃掉目標了。我們可以將目標拖進Assets,讓我們之後可以很方便地新增和修改目標,並將場景也轉(zhuǎn)為Prefab。

  • 至此,我們可以在場景中加入圍牆,讓玩家不會掉出邊界。(請注意要在Prefab中更改或新增物件,否則變更不會反映到其他相同的Prefab上)
  • 接下來,我們可以將目標放到較高的平臺上,並試著操控玩家跳躍並吃掉目標。
  • 這邊可以發(fā)現(xiàn),我們的玩家能夠在空中跳躍,但是這通常並不是我們想要的,因此可以對PlayerMove.cs做一些修改。
  • 將玩家可以跳躍的平臺都加上Platform的Tag


PlayerMove.cs

using System.Collections;
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    public float moveSpeed = 3f;
    public float rotateSpeed = 50f;
    public float jumpForce = 300f;

    private Rigidbody playerRigidbody;
    private bool jumpButtonDown;
    private bool isOnPlatform;

    private void Start()
    {
        playerRigidbody = GetComponent<Rigidbody>();
        isOnPlatform = false;
        jumpButtonDown = false;
    }

    private void Update()
    {
        if (Input.GetButtonDown("Jump") && isOnPlatform)
        {
            jumpButtonDown = true;
        }
    }

    private void FixedUpdate()
    {
        float moveZ = Input.GetAxisRaw("Vertical");
        Vector3 playerMove = transform.forward * moveZ * moveSpeed * Time.deltaTime;
        transform.position += playerMove;

        float rotateY = Input.GetAxisRaw("Horizontal");
        Vector3 playerRotate = new Vector3(0f, rotateY * rotateSpeed * Time.deltaTime, 0f);
        transform.Rotate(playerRotate);

        if (jumpButtonDown)
        {
            jumpButtonDown = false;
            Vector3 playerJump = transform.up * jumpForce;
            playerRigidbody.AddForce(playerJump);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag("Target"))
        {
            other.gameObject.SetActive(false);
        }
    }

    private void OnCollisionStay(Collision collision)
    {
        if (collision.gameObject.CompareTag("Platform"))
        {
            isOnPlatform = true;
        }
    }

    private void OnCollisionExit(Collision collision)
    {
        if (collision.gameObject.CompareTag("Platform"))
        {
            isOnPlatform = false;
        }
    }
}

  • 這邊新增一個bool,透過OnCollisionStay與OnCollisionExit判斷,玩家待在平臺上時為true,反之為false,並以此為依據(jù)來決定是否讓玩家跳躍。


延伸-
至此,我們已經(jīng)完成基本的目標了,接下來可以加入如計分、時間限制等元素,讓遊戲更有挑戰(zhàn)性。
這邊我直接設計一個遊戲機制,假設玩家是有時間限制的,當這個數(shù)值小於0後,玩家即死亡,並將剩餘時間顯示在玩家上方。玩家死亡,或是場上的目標都被吃掉後,遊戲結(jié)束,在倒數(shù)後重新開始。
程式碼和必要的設置附在底下,,需要註解,或是有任何需要說明或是遺漏的地方都歡迎提出~
  • 完整程式碼

PlayerMove.cs

using System.Collections;
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    public float moveSpeed = 3f;
    public float rotateSpeed = 50f;
    public float jumpForce = 300f;
    public GameObject[] targets;
    public TextMesh playerState;

    private Rigidbody playerRigidbody;
    private bool isOnPlatform;
    private bool canPlay;
    private int targetNum;
    private float startHealth = 5f;
    private float playerHealth;
    private bool jumpButtonDown;

    private void Start()
    {
        ResetScene();
        playerRigidbody = GetComponent<Rigidbody>();
        isOnPlatform = false;
        jumpButtonDown = false;
        targetNum = targets.Length;
        playerHealth = startHealth;
        playerState.text = playerHealth.ToString("0.0");
    }

    private void Update()
    {
        if (canPlay)
        {
            playerHealth -= Time.deltaTime;
            playerState.text = playerHealth.ToString("0.0");
            if (playerHealth < 0)
            {
                StartCoroutine("ResetCountdownLose");
            }

            if (Input.GetButtonDown("Jump") && isOnPlatform)
            {
                jumpButtonDown = true;
            }
        }
    }

    private void FixedUpdate()
    {
        if (canPlay)
        {
            float moveZ = Input.GetAxisRaw("Vertical");
            Vector3 playerMove = transform.forward * moveZ * moveSpeed * Time.deltaTime;
            transform.position += playerMove;

            float rotateY = Input.GetAxisRaw("Horizontal");
            Vector3 playerRotate = new Vector3(0f, rotateY * rotateSpeed * Time.deltaTime, 0f);
            transform.Rotate(playerRotate);

            if (jumpButtonDown)
            {
                jumpButtonDown = false;
                Vector3 playerJump = transform.up * jumpForce;
                playerRigidbody.AddForce(playerJump);
            }
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag("Target"))
        {
            targetNum--;
            playerHealth += 5f;
            other.gameObject.SetActive(false);
            if (targetNum == 0)
            {
                StartCoroutine("ResetCountdownWin");
            }
        }
    }

    private void OnCollisionStay(Collision collision)
    {
        if (collision.gameObject.CompareTag("Platform"))
        {
            isOnPlatform = true;
        }
    }

    private void OnCollisionExit(Collision collision)
    {
        if (collision.gameObject.CompareTag("Platform"))
        {
            isOnPlatform = false;
        }
    }

    private void ResetScene()
    {
        transform.position = new Vector3(0f, 1.5f, -5f);
        transform.rotation = Quaternion.identity;
        foreach (GameObject target in targets)
        {
            target.SetActive(true);
        }
        playerHealth = startHealth;
        targetNum = targets.Length;
        canPlay = true;
    }

    private IEnumerator ResetCountdownWin()
    {
        canPlay = false;
        float countdown = 3.5f;
        while (countdown > 0f)
        {
            countdown -= Time.deltaTime;
            playerState.text = "Winn" + countdown.ToString("0");
            yield return null;
        }
        ResetScene();
    }

    private IEnumerator ResetCountdownLose()
    {
        canPlay = false;
        float countdown = 3.5f;
        while (countdown > 0f)
        {
            countdown -= Time.deltaTime;
            playerState.text = "Losen" + countdown.ToString("0");
            yield return null;
        }
        ResetScene();
    }
}


RotatingCube.cs

using UnityEngine;

public class RotatingCube1 : MonoBehaviour
{
    void FixedUpdate()
    {
        Vector3 rotateAngle = new Vector3(15f, 30f, 45f);
        transform.Rotate(rotateAngle * Time.deltaTime);
    }
}


stateMove.cs (讓物件跟隨玩家,並隨著鏡頭角度旋轉(zhuǎn))

using UnityEngine;

public class stateMove : MonoBehaviour
{
    public GameObject player;

    private Vector3 offset;
    // Start is called before the first frame update
    void Start()
    {
        offset = transform.position - player.transform.position;
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        transform.position = player.transform.position + offset;
        transform.rotation = Quaternion.Euler(0f, player.transform.rotation.eulerAngles.y, 0f);
    }
}

  • 場景中的元件
Player

PlayerState (3D Text)

  • 我們還可以將Camera拖入Player底下,使其做為Player的child component,不用程式碼實現(xiàn)Camera隨著Player視角移動的效果。


這篇教學到這裡告一段落,會開始寫這篇單純是一時興起,如果有任何希望詳細解釋,或是改進的地方,都歡迎提出。
下一篇文章開始應該就會著重在ML-Agents 的部分,內(nèi)容也會相當基礎,原本預計再一個月左右發(fā)布,但是實在找不到時間,如果有人願意催我一下的話,可能比較有機會盡快發(fā)佈w
歡迎到時候有興趣的人也歡迎看一下。希望看到這裡,還沒有打開或甚至安裝Unity環(huán)境的新手,可以實際動手嘗試,踏出你在遊戲製作的第一步。
感謝閱讀至此的各位~

轉(zhuǎn)載或引用請註明出處。

創(chuàng)作回應

Jerry
原本預計這篇發(fā)布的一個月後要發(fā)下一篇,現(xiàn)在看來實在是來不及了...
應該會先發(fā)個原本沒有要寫的,ML-Agents的設置教學
2022-12-21 01:48:02
追蹤 創(chuàng)作集

作者相關創(chuàng)作

相關創(chuàng)作

更多創(chuàng)作