Учебник Endless Runner для Unity
В видеоиграх, каким бы большим ни был мир, у него всегда есть конец. Но некоторые игры пытаются эмулировать бесконечный мир, такие игры попадают в категорию под названием Бесконечный бегун.
Endless Runner — это тип игры, в которой игрок постоянно движется вперед, собирая очки и избегая препятствий. Основная цель — дойти до конца уровня, не упав и не столкнувшись с препятствиями, но зачастую уровень повторяется бесконечно, постепенно увеличивая сложность, пока игрок не столкнется с препятствием.
Учитывая, что даже современные компьютеры/игровые устройства имеют ограниченную вычислительную мощность, создать по-настоящему бесконечный мир невозможно.
Так как же некоторые игры создают иллюзию бесконечного мира? Ответ заключается в повторном использовании строительных блоков (также известных как пул объектов), другими словами, как только блок оказывается позади или за пределами поля зрения камеры, он перемещается вперед.
Чтобы создать бесконечную игру-раннер в Unity, нам понадобится создать платформу с препятствиями и контроллер игрока.
Шаг 1: Создание платформы
Начнем с создания плиточной платформы, которая позже будет сохранена в Prefab:
- Создайте новый GameObject и назовите его "TilePrefab"
- Создать новый куб (GameObject -> 3D Object -> Куб)
- Переместите куб внутрь объекта "TilePrefab", измените его положение на (0, 0, 0) и масштабируйте до (8, 0.4, 20).
- При желании вы можете добавить рельсы по бокам, создав дополнительные кубы, например, так:
Что касается препятствий, у меня будет 3 варианта препятствий, но вы можете сделать их столько, сколько нужно:
- Создайте 3 игровых объекта внутри объекта "TilePrefab" и назовите их "Obstacle1", "Obstacle2" и "Obstacle3"
- Для первого препятствия создайте новый куб и поместите его внутрь объекта "Obstacle1".
- Масштабируйте новый куб примерно до той же ширины, что и платформа, и уменьшите его высоту (игроку придется прыгать, чтобы избежать этого препятствия).
- Создайте новый материал, назовите его "RedMaterial" и измените его цвет на красный, затем назначьте его кубу (это нужно для того, чтобы препятствие отличалось от основной платформы).
- Для "Obstacle2" создайте пару кубов и разместите их в форме треугольника, оставив одно открытое пространство внизу (игроку придется приседать, чтобы избежать этого препятствия)
- И наконец, "Obstacle3" будет дубликатом "Obstacle1" и "Obstacle2", объединенных вместе.
- Теперь выберите все объекты внутри препятствий и измените их тег на "Finish", это понадобится позже для обнаружения столкновения между игроком и препятствием.
Для создания бесконечной платформы нам понадобится пара скриптов, которые будут обрабатывать Object Pooling и Obstacle activation:
- Создайте новый скрипт, назовите его "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" массиву Obstacles
Для начальной и конечной точек нам необходимо создать 2 игровых объекта, которые следует разместить в начале и конце платформы соответственно:
- Назначьте переменные Начальная точка и Конечная точка в SC_PlatformTile
- Сохраните объект "TilePrefab" в Prefab и удалите его из сцены.
- Создайте новый GameObject и назовите его "_GroundGenerator"
- Присоедините скрипт SC_GroundGenerator к объекту "_GroundGenerator"
- Измените положение основной камеры на (10, 1, -9) и измените ее поворот на (0, -55, 0).
- Создайте новый GameObject, назовите его "StartPoint" и измените его положение на (0, -2, -15)
- Выберите объект "_GroundGenerator" и в SC_GroundGenerator назначьте переменные Main Camera, Start Point и Tile Prefab.
Теперь нажмите Play и наблюдайте, как движется платформа. Как только плитка платформы выходит из поля зрения камеры, она возвращается в конец, и активируется случайное препятствие, создавая иллюзию бесконечного уровня (перейти к 0:11).
Камера должна быть размещена аналогично видео, так чтобы платформы шли по направлению к камере и за нее, иначе платформы не будут повторяться.
Шаг 2: Создание игрока
Экземпляр игрока будет представлять собой простую сферу, использующую контроллер с возможностью прыгать и приседать.
- Создайте новую сферу (GameObject -> 3D Object -> Sphere) и удалите ее компонент Sphere Collider.
- Назначьте ему ранее созданный "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 для приседания. Цель состоит в том, чтобы избегать красных препятствий:
Проверьте этот шейдер изгиба горизонта.