Endless Runner-zelfstudie voor Unity
In videogames, hoe groot de wereld ook is, heeft het altijd een einde. Maar sommige games proberen de oneindige wereld na te bootsen, zulke games vallen onder de categorie Endless Runner.
Endless Runner is een type spel waarbij de speler constant vooruit beweegt terwijl hij punten verzamelt en obstakels ontwijkt. Het hoofddoel is om het einde van het level te bereiken zonder in de obstakels te vallen of ermee te botsen, maar vaak herhaalt het level zichzelf oneindig, waarbij de moeilijkheidsgraad geleidelijk toeneemt, totdat de speler met het obstakel botst.
Als je bedenkt dat zelfs moderne computers en spelcomputers een beperkte verwerkingskracht hebben, is het onmogelijk om een werkelijk oneindige wereld te creëren.
Dus hoe creëren sommige games een illusie van een oneindige wereld? Het antwoord is door de bouwstenen te hergebruiken (ook wel object pooling genoemd), met andere woorden, zodra het blok achter of buiten het camerabeeld komt, wordt het naar voren verplaatst.
Om een eindeloos rennerspel in Unity te maken, moeten we een platform met obstakels en een spelercontroller maken.
Stap 1: Creëer het platform
We beginnen met het maken van een betegeld platform dat later wordt opgeslagen in de Prefab:
- Maak een nieuw GameObject en noem het "TilePrefab"
- Nieuwe kubus maken (GameObject -> 3D Object -> Kubus)
- Verplaats de kubus binnen het "TilePrefab"-object, verander de positie naar (0, 0, 0) en schaal naar (8, 0,4, 20)
- Optioneel kunt u Rails aan de zijkanten toevoegen door extra Cubes te maken, zoals deze:
Voor de obstakels heb ik 3 obstakelvariaties, maar je kunt er zoveel maken als je wilt:
- Maak 3 GameObjects binnen het "TilePrefab"-object en noem ze "Obstacle1", "Obstacle2" en "Obstacle3"
- Voor het eerste obstakel maak je een nieuwe kubus en verplaats je deze naar binnen in het "Obstacle1"-object
- Schaal de nieuwe kubus tot ongeveer dezelfde breedte als het platform en schaal de hoogte ervan naar beneden (de speler zal moeten springen om dit obstakel te vermijden)
- Maak een nieuw materiaal, noem het "RedMaterial" en verander de kleur naar Rood, wijs het vervolgens toe aan de Kubus (dit is alleen om het obstakel te onderscheiden van het hoofdplatform)
- Voor de "Obstacle2" maak je een paar kubussen en plaats je ze in een driehoekige vorm, waarbij je een open ruimte aan de onderkant overlaat (de speler moet hurken om dit obstakel te vermijden)
- En als laatste zal "Obstacle3" een duplicaat zijn van "Obstacle1" en "Obstacle2", gecombineerd
- Selecteer nu alle objecten binnen de obstakels en verander hun tag naar "Finish". Dit is later nodig om de botsing tussen de speler en het obstakel te detecteren.
Om een oneindig platform te genereren, hebben we een aantal scripts nodig die Object Pooling en Obstacle-activering afhandelen:
- Maak een nieuw script, noem het "SC_PlatformTile" en plak de onderstaande code erin:
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);
}
}
}
- Maak een nieuw script, noem het "SC_GroundGenerator" en plak de onderstaande code erin:
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));
}
}
- Koppel het SC_PlatformTile-script aan het "TilePrefab"-object
- Wijs "Obstacle1", "Obstacle2" en "Obstacle3" objecten toe aan de Obstacles array
Voor het startpunt en het eindpunt moeten we 2 GameObjects maken die respectievelijk aan het begin en het einde van het platform moeten worden geplaatst:
- Startpunt- en eindpuntvariabelen toewijzen in SC_PlatformTile
- Sla het "TilePrefab"-object op in Prefab en verwijder het uit de scène
- Maak een nieuw GameObject en noem het "_GroundGenerator"
- Koppel het SC_GroundGenerator-script aan het "_GroundGenerator"-object
- Verander de positie van de hoofdcamera naar (10, 1, -9) en verander de rotatie naar (0, -55, 0)
- Maak een nieuw GameObject, noem het "StartPoint" en verander de positie naar (0, -2, -15)
- Selecteer het "_GroundGenerator"-object en wijs in SC_GroundGenerator de variabelen Hoofdcamera, Startpunt en Tegelprefab toe
Druk nu op Play en kijk hoe het platform beweegt. Zodra de platformtegel uit het camerabeeld verdwijnt, wordt deze terug naar het einde bewogen met een willekeurig obstakel dat wordt geactiveerd, waardoor de illusie van een oneindig level ontstaat (Ga naar 0:11).
De camera moet op dezelfde manier worden geplaatst als de video, zodat de platformen naar de camera toe en erachter staan. Anders worden de platformen niet herhaald.
Stap 2: De speler maken
De speler-instance is een eenvoudige bol met een controller waarmee je kunt springen en hurken.
- Maak een nieuwe bol (GameObject -> 3D Object -> Bol) en verwijder het Bol Collider-component
- Wijs eerder gemaakte "RedMaterial" eraan toe
- Maak een nieuw GameObject en noem het "Player"
- Verplaats de bol binnen het "Player"-object en verander de positie naar (0, 0, 0)
- Maak een nieuw script, noem het "SC_IRPlayer" en plak de onderstaande code erin:
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;
}
}
}
- Voeg het SC_IRPlayer-script toe aan het "Player"-object (je zult merken dat er een ander onderdeel is toegevoegd, genaamd Rigidbody)
- Voeg het BoxCollider-component toe aan het "Player"-object
- Plaats het "Player"-object iets boven het "StartPoint"-object, precies voor de camera
Druk op Play en gebruik de W-toets om te springen en de S-toets om te hurken. Het doel is om rode obstakels te vermijden:
Bekijk deze Horizon Bending Shader.