前言-
我打算在這邊寫一些關於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.csusing 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.csusing 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ù)後重新開始。
程式碼和必要的設置附在底下,,需要註解,或是有任何需要說明或是遺漏的地方都歡迎提出~
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);
}
}
PlayerState (3D Text)
- 我們還可以將Camera拖入Player底下,使其做為Player的child component,不用程式碼實現(xiàn)Camera隨著Player視角移動的效果。
這篇教學到這裡告一段落,會開始寫這篇單純是一時興起,如果有任何希望詳細解釋,或是改進的地方,都歡迎提出。
下一篇文章開始應該就會著重在ML-Agents 的部分,內(nèi)容也會相當基礎,原本預計再一個月左右發(fā)布,但是實在找不到時間,如果有人願意催我一下的話,可能比較有機會盡快發(fā)佈w
歡迎到時候有興趣的人也歡迎看一下。希望看到這裡,還沒有打開或甚至安裝Unity環(huán)境的新手,可以實際動手嘗試,踏出你在遊戲製作的第一步。
感謝閱讀至此的各位~
轉(zhuǎn)載或引用請註明出處。