Unity 无尽跑者教程
在电子游戏中,无论世界有多大,它总会有一个尽头。但有些游戏试图模拟无限的世界,这类游戏属于 无尽奔跑者 类别。
无尽奔跑者是一种游戏,玩家在不断前进的同时收集积分并避开障碍物。主要目标是在不陷入或与障碍物相撞的情况下到达关卡终点,但通常情况下,关卡会无限重复,难度逐渐增加,直到玩家与障碍物相撞。
考虑到即使是现代计算机/游戏设备的处理能力也有限,因此不可能创造一个真正无限的世界。
那么有些游戏是如何创造无限世界的幻觉的呢?答案是通过重用构建块(又称对象池),换句话说,一旦块位于相机视图的后面或外面,它就会被移动到前面。
为了在 Unity 中制作一款无尽跑酷游戏,我们需要制作一个带有障碍物和玩家控制器的平台。
步骤 1:创建平台
我们首先创建一个平铺平台,稍后将其存储在 Prefab 中:
- 创建新的 GameObject 并调用它 "TilePrefab"
- 创建新立方体(游戏对象 -> 3D 对象 -> 立方体)
- 将立方体移至 "TilePrefab" 对象内,将其位置更改为 (0, 0, 0),并缩放至 (8, 0.4, 20)
- 您可以选择通过创建额外的立方体将轨道添加到侧面,如下所示:
对于障碍,我将有 3 种障碍变化,但您可以根据需要制作任意数量的障碍:
- 在 "TilePrefab" 对象内创建 3 个游戏对象,并将它们命名为 "Obstacle1"、"Obstacle2" 和 "Obstacle3"
- 对于第一个障碍,创建一个新的立方体并将其移动到 "Obstacle1" 对象内
- 将新的立方体缩放到与平台相同的宽度,并缩小其高度(玩家需要跳跃来避开这个障碍物)
- 创建一个新的材质,将其命名为 "RedMaterial" 并将其颜色更改为红色,然后将其分配给立方体(这只是为了将障碍物与主平台区分开来)
- 对于 "Obstacle2",创建几个立方体并将它们摆成三角形,在底部留出一个开放空间(玩家需要蹲下才能避开这个障碍物)
- 最后,"Obstacle3" 将是 "Obstacle1" 和 "Obstacle2" 的重复项,合并在一起
- 现在选择障碍物内的所有物体并将其标签更改为 "Finish",这将在稍后检测玩家和障碍物之间的碰撞时需要。
为了生成无限平台,我们需要一些处理对象池和障碍物激活的脚本:
- 创建个新脚本,将其命名为"SC_PlatformTile"并将以下代码粘贴到其中:
SC_PlatformTile.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_PlatformTile : MonoBehaviour
{
public Transform startPoint;
public Transform endPoint;
public GameObject[] obstacles; //Objects that contains different obstacle types which will be randomly activated
public void ActivateRandomObstacle()
{
DeactivateAllObstacles();
System.Random random = new System.Random();
int randomNumber = random.Next(0, obstacles.Length);
obstacles[randomNumber].SetActive(true);
}
public void DeactivateAllObstacles()
{
for (int i = 0; i < obstacles.Length; i++)
{
obstacles[i].SetActive(false);
}
}
}
- 创建个新脚本,将其命名为"SC_GroundGenerator"并将以下代码粘贴到其中:
SC_GroundGenerator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SC_GroundGenerator : MonoBehaviour
{
public Camera mainCamera;
public Transform startPoint; //Point from where ground tiles will start
public SC_PlatformTile tilePrefab;
public float movingSpeed = 12;
public int tilesToPreSpawn = 15; //How many tiles should be pre-spawned
public int tilesWithoutObstacles = 3; //How many tiles at the beginning should not have obstacles, good for warm-up
List<SC_PlatformTile> spawnedTiles = new List<SC_PlatformTile>();
int nextTileToActivate = -1;
[HideInInspector]
public bool gameOver = false;
static bool gameStarted = false;
float score = 0;
public static SC_GroundGenerator instance;
// Start is called before the first frame update
void Start()
{
instance = this;
Vector3 spawnPosition = startPoint.position;
int tilesWithNoObstaclesTmp = tilesWithoutObstacles;
for (int i = 0; i < tilesToPreSpawn; i++)
{
spawnPosition -= tilePrefab.startPoint.localPosition;
SC_PlatformTile spawnedTile = Instantiate(tilePrefab, spawnPosition, Quaternion.identity) as SC_PlatformTile;
if(tilesWithNoObstaclesTmp > 0)
{
spawnedTile.DeactivateAllObstacles();
tilesWithNoObstaclesTmp--;
}
else
{
spawnedTile.ActivateRandomObstacle();
}
spawnPosition = spawnedTile.endPoint.position;
spawnedTile.transform.SetParent(transform);
spawnedTiles.Add(spawnedTile);
}
}
// Update is called once per frame
void Update()
{
// Move the object upward in world space x unit/second.
//Increase speed the higher score we get
if (!gameOver && gameStarted)
{
transform.Translate(-spawnedTiles[0].transform.forward * Time.deltaTime * (movingSpeed + (score/500)), Space.World);
score += Time.deltaTime * movingSpeed;
}
if (mainCamera.WorldToViewportPoint(spawnedTiles[0].endPoint.position).z < 0)
{
//Move the tile to the front if it's behind the Camera
SC_PlatformTile tileTmp = spawnedTiles[0];
spawnedTiles.RemoveAt(0);
tileTmp.transform.position = spawnedTiles[spawnedTiles.Count - 1].endPoint.position - tileTmp.startPoint.localPosition;
tileTmp.ActivateRandomObstacle();
spawnedTiles.Add(tileTmp);
}
if (gameOver || !gameStarted)
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (gameOver)
{
//Restart current scene
Scene scene = SceneManager.GetActiveScene();
SceneManager.LoadScene(scene.name);
}
else
{
//Start the game
gameStarted = true;
}
}
}
}
void OnGUI()
{
if (gameOver)
{
GUI.color = Color.red;
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Game Over\nYour score is: " + ((int)score) + "\nPress 'Space' to restart");
}
else
{
if (!gameStarted)
{
GUI.color = Color.red;
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Press 'Space' to start");
}
}
GUI.color = Color.green;
GUI.Label(new Rect(5, 5, 200, 25), "Score: " + ((int)score));
}
}
- 将 SC_PlatformTile 脚本附加到"TilePrefab" 对象
- 将 "Obstacle1"、"Obstacle2" 和 "Obstacle3" 对象分配给障碍物数组
对于起点和终点,我们需要创建 2 个游戏对象,分别放置在平台的起点和终点:
- 在 SC_PlatformTile 中分配起点和终点变量
- 将 "TilePrefab" 对象保存为预制件并将其从场景中移除
- 创建新的 GameObject 并调用它 "_GroundGenerator"
- 将 SC_GroundGenerator 脚本附加到 "_GroundGenerator" 对象
- 将主摄像机位置更改为(10,1,-9),并将其旋转更改为(0,-55,0)
- 创建一个新的 GameObject,将其命名为 "StartPoint" 并将其位置更改为 (0, -2, -15)
- 选择 "_GroundGenerator" 对象并在 SC_GroundGenerator 中分配主摄像头、起点和 Tile Prefab 变量
现在按下播放按钮,观察平台如何移动。平台方块一离开摄像机视野,就会移回终点,并激活随机障碍物,营造出无限关卡的幻觉(跳至 0:11)。
摄像机的放置位置必须与视频类似,因此平台朝向摄像机并位于其后方,否则平台不会重复。
第 2 步:创建玩家
玩家实例将是一个简单的球体,使用具有跳跃和蹲伏能力的控制器。
- 创建一个新的球体(游戏对象 -> 3D 对象 -> 球体)并移除其球体碰撞器组件
- 将之前创建的 "RedMaterial" 分配给它
- 创建新的 GameObject 并调用它 "Player"
- 将球体移动到 "Player" 对象内并将其位置更改为 (0, 0, 0)
- 创建一个新的脚本,将其命名为 "SC_IRPlayer" 并将以下代码粘贴到其中:
SC_IRPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class SC_IRPlayer : MonoBehaviour
{
public float gravity = 20.0f;
public float jumpHeight = 2.5f;
Rigidbody r;
bool grounded = false;
Vector3 defaultScale;
bool crouch = false;
// Start is called before the first frame update
void Start()
{
r = GetComponent<Rigidbody>();
r.constraints = RigidbodyConstraints.FreezePositionX | RigidbodyConstraints.FreezePositionZ;
r.freezeRotation = true;
r.useGravity = false;
defaultScale = transform.localScale;
}
void Update()
{
// Jump
if (Input.GetKeyDown(KeyCode.W) && grounded)
{
r.velocity = new Vector3(r.velocity.x, CalculateJumpVerticalSpeed(), r.velocity.z);
}
//Crouch
crouch = Input.GetKey(KeyCode.S);
if (crouch)
{
transform.localScale = Vector3.Lerp(transform.localScale, new Vector3(defaultScale.x, defaultScale.y * 0.4f, defaultScale.z), Time.deltaTime * 7);
}
else
{
transform.localScale = Vector3.Lerp(transform.localScale, defaultScale, Time.deltaTime * 7);
}
}
// Update is called once per frame
void FixedUpdate()
{
// We apply gravity manually for more tuning control
r.AddForce(new Vector3(0, -gravity * r.mass, 0));
grounded = false;
}
void OnCollisionStay()
{
grounded = true;
}
float CalculateJumpVerticalSpeed()
{
// From the jump height and gravity we deduce the upwards speed
// for the character to reach at the apex.
return Mathf.Sqrt(2 * jumpHeight * gravity);
}
void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.tag == "Finish")
{
//print("GameOver!");
SC_GroundGenerator.instance.gameOver = true;
}
}
}
- 将 SC_IRPlayer 脚本附加到 "Player" 对象(您会注意到它添加了另一个名为 Rigidbody 的组件)
- 将 BoxCollider 组件添加到 "Player" 对象
- 将 "Player" 物体放置在 "StartPoint" 物体的稍上方,就在相机的正前方
按 Play 并使用 W 键跳跃,S 键蹲伏。目标是避开红色障碍物:
检查这个地平线弯曲着色器。