Endless Runner Tutorial för Unity

I videospel, oavsett hur stor världen är, har den alltid ett slut. Men vissa spel försöker efterlikna den oändliga världen, sådana spel faller under kategorin som kallas Endless Runner.

Endless Runner är en typ av spel där spelaren hela tiden rör sig framåt samtidigt som han samlar poäng och undviker hinder. Huvudsyftet är att nå slutet av nivån utan att hamna i eller kollidera med hindren, men ofta upprepar nivån sig oändligt, gradvis ökar svårigheten, tills spelaren kolliderar med hindret.

Subway Surfers Gameplay

Med tanke på att även moderna datorer/spelenheter har begränsad processorkraft är det omöjligt att skapa en verkligt oändlig värld.

Så hur skapar vissa spel en illusion av en oändlig värld? Svaret är genom att återanvända byggstenarna (alias objektpoolning), med andra ord, så fort blocket går bakom eller utanför kameravyn flyttas det till framsidan.

För att skapa ett oändligt löparspel i Unity måste vi skapa en plattform med hinder och en spelarkontroll.

Steg 1: Skapa plattformen

Vi börjar med att skapa en kaklad plattform som senare kommer att lagras i Prefab:

  • Skapa ett nytt GameObject och anropa det "TilePrefab"
  • Skapa ny kub (GameObject -> 3D Object -> Cube)
  • Flytta kuben inuti "TilePrefab"-objektet, ändra dess position till (0, 0, 0) och skala till (8, 0,4, 20)

  • Alternativt kan du lägga till Rails på sidorna genom att skapa ytterligare kuber, så här:

För hindren kommer jag att ha 3 hindervarianter, men du kan göra så många som behövs:

  • Skapa 3 GameObjects inuti "TilePrefab"-objektet och döp dem till "Obstacle1", "Obstacle2" och "Obstacle3"
  • För det första hindret, skapa en ny kub och flytta den inuti "Obstacle1"-objektet
  • Skala den nya kuben till ungefär samma bredd som plattformen och skala ner dess höjd (spelaren måste hoppa för att undvika detta hinder)
  • Skapa ett nytt material, namnge det "RedMaterial" och ändra dess färg till Röd, och tilldela det sedan till kuben (detta är bara så att hindret kan skiljas från huvudplattformen)

  • För "Obstacle2" skapa ett par kuber och placera dem i en triangulär form, lämna ett öppet utrymme längst ner (spelaren måste huka sig för att undvika detta hinder)

  • Och slutligen, "Obstacle3" kommer att vara en dubblett av "Obstacle1" och "Obstacle2", kombinerade tillsammans

  • Välj nu alla objekt inuti hinder och ändra deras tagg till "Finish", detta kommer att behövas senare för att upptäcka kollisionen mellan spelare och hinder.

För att skapa en oändlig plattform behöver vi ett par skript som kommer att hantera objektpoolning och hinderaktivering:

  • Skapa ett nytt skript, kalla det "SC_PlatformTile" och klistra in koden nedan i det:

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);
        }
    }
}
  • Skapa ett nytt skript, kalla det "SC_GroundGenerator" och klistra in koden nedan i det:

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));
    }
}
  • Bifoga SC_PlatformTile-skriptet till "TilePrefab"-objektet
  • Tilldela "Obstacle1", "Obstacle2" och "Obstacle3" objekt till obstacles array

För startpunkten och slutpunkten måste vi skapa 2 GameObjects som ska placeras i början respektive slutet av plattformen:

  • Tilldela startpunkts- och slutpunktvariabler i SC_PlatformTile

  • Spara "TilePrefab"-objektet i Prefab och ta bort det från scenen
  • Skapa ett nytt GameObject och anropa det "_GroundGenerator"
  • Bifoga SC_GroundGenerator-skriptet till "_GroundGenerator"-objektet
  • Ändra huvudkamerans position till (10, 1, -9) och ändra dess rotation till (0, -55, 0)
  • Skapa ett nytt GameObject, kalla det "StartPoint" och ändra dess position till (0, -2, -15)
  • Välj "_GroundGenerator"-objektet och i SC_GroundGenerator tilldela Main Camera, Start Point och Tile Prefab variabler

Tryck nu på Play och observera hur plattformen rör sig. Så snart plattformsbrickan försvinner från kameravyn flyttas den tillbaka till slutet med ett slumpmässigt hinder som aktiveras, vilket skapar en illusion av en oändlig nivå (Hoppa över till 0:11).

Kameran måste placeras på samma sätt som videon, så plattformarna går mot kameran och bakom den, annars upprepas inte plattformarna.

Sharp Coder Videospelare

Steg 2: Skapa spelaren

Spelarens instans kommer att vara en enkel sfär som använder en kontroller med förmågan att hoppa och huka sig.

  • Skapa en ny Sphere (GameObject -> 3D Object -> Sphere) och ta bort dess Sphere Collider-komponent
  • Tilldela tidigare skapade "RedMaterial" till den
  • Skapa ett nytt GameObject och anropa det "Player"
  • Flytta sfären inuti "Player"-objektet och ändra dess position till (0, 0, 0)
  • Skapa ett nytt skript, kalla det "SC_IRPlayer" och klistra in koden nedan i det:

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;
        }
    }
}
  • Bifoga SC_IRPlayer-skriptet till "Player"-objektet (du kommer att märka att det lade till en annan komponent som heter Rigidbody)
  • Lägg till BoxCollider-komponenten till "Player"-objektet

  • Placera "Player"-objektet något ovanför "StartPoint"-objektet, precis framför kameran

Tryck på Play och använd W-tangenten för att hoppa och S-tangenten för att huka sig. Målet är att undvika röda hinder:

Sharp Coder Videospelare

Kolla denna Horizon Bending Shader.