Посібник Endless Runner для Unity
У відеоіграх, яким би великим не був світ, він завжди має кінець. Але деякі ігри намагаються імітувати нескінченний світ, такі ігри підпадають під категорію Endless Runner.
Endless Runner — це тип гри, де гравець постійно рухається вперед, збираючи очки та уникаючи перешкод. Основна мета — досягти кінця рівня, не потрапляючи на перешкоди чи не стикаючись із ними, але часто рівень повторюється нескінченно, поступово збільшуючи складність, поки гравець не зіткнеться з перешкодою.
Враховуючи, що навіть сучасні комп’ютери/ігрові пристрої мають обмежену обчислювальну потужність, неможливо створити справді нескінченний світ.
Отже, як деякі ігри створюють ілюзію нескінченного світу? Відповідь полягає в повторному використанні будівельних блоків (так відомих як об’єднання об’єктів), іншими словами, щойно блок виходить за або за межі огляду камери, він переміщується на передній план.
Щоб створити нескінченну гру в Unity, нам потрібно зробити платформу з перешкодами та контролером гравця.
Крок 1: Створіть платформу
Ми починаємо зі створення плиткової платформи, яка пізніше буде збережена в Prefab:
- Створіть новий GameObject і назвіть його "TilePrefab"
- Створити новий куб (GameObject -> 3D Object -> Cube)
- Перемістіть куб всередину об’єкта "TilePrefab", змініть його положення на (0, 0, 0) і масштабуйте на (8, 0,4, 20)
- За бажанням ви можете додати рейки до сторін, створивши додаткові куби, наприклад:
Для перешкод у мене буде 3 варіанти перешкод, але ви можете зробити скільки завгодно:
- Створіть 3 GameObjects всередині об’єкта "TilePrefab" і назвіть їх "Obstacle1", "Obstacle2" і "Obstacle3"
- Для першої перешкоди створіть новий куб і перемістіть його всередину об’єкта "Obstacle1"
- Збільште новий куб приблизно до такої ж ширини, як платформа, і зменште його висоту (гравцеві потрібно буде стрибнути, щоб уникнути цієї перешкоди)
- Створіть новий матеріал, назвіть його "RedMaterial" і змініть його колір на червоний, а потім призначте його кубу (це лише для того, щоб перешкода відрізнялася від основної платформи)
- Для "Obstacle2" створіть пару кубиків і розмістіть їх у трикутній формі, залишивши один вільний простір внизу (гравцеві потрібно буде присісти, щоб уникнути цієї перешкоди)
- І нарешті, "Obstacle3" буде дублікатом "Obstacle1" і "Obstacle2", поєднаних разом
- Тепер виберіть усі об’єкти всередині перешкод і змініть їхній тег на "Finish", це знадобиться пізніше, щоб виявити зіткнення між гравцем і перешкодою.
Щоб створити нескінченну платформу, нам знадобиться кілька сценаріїв, які оброблятимуть Object Pooling та активацію Obstacle:
- Створіть новий сценарій, назвіть його "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 об’єкти гри, які потрібно розмістити на початку та в кінці платформи відповідно:
- Призначте змінні Start Point і End Point у 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, щоб присісти. Мета полягає в тому, щоб уникнути червоних перешкод:
Перегляньте цей Horizon Bending Shader.