Opplæring for Endless Runner Unity

I videospill, uansett hvor stor verden er, har den alltid en slutt. Men noen spill prøver å etterligne den uendelige verden, slike spill faller inn under kategorien som heter Endless Runner.

Endless Runner er en type spill hvor spilleren hele tiden beveger seg fremover mens han samler poeng og unngår hindringer. Hovedmålet er å nå slutten av nivået uten å falle inn i eller kollidere med hindringene, men ofte gjentar nivået seg i det uendelige, og øker gradvis vanskelighetsgraden, helt til spilleren kolliderer med hindringen.

Subway Surfers Gameplay

Med tanke på at selv moderne datamaskiner/spillenheter har begrenset prosessorkraft, er det umulig å lage en virkelig uendelig verden.

Så hvordan skaper noen spill en illusjon av en uendelig verden? Svaret er ved å gjenbruke byggeklossene (a.k.a. objektsammenslåing), med andre ord, så snart blokken går bak eller utenfor kameravisningen, flyttes den til fronten.

For å lage et uendelig løperspill i Unity, må vi lage en plattform med hindringer og en spillerkontroller.

Trinn 1: Lag plattformen

Vi begynner med å lage en flislagt plattform som senere vil bli lagret i Prefab:

  • Lag et nytt GameObject og kall det "TilePrefab"
  • Opprett ny kube (GameObject -> 3D Object -> Cube)
  • Flytt kuben inne i "TilePrefab"-objektet, endre posisjonen til (0, 0, 0) og skala til (8, 0,4, 20)

  • Eventuelt kan du legge til skinner på sidene ved å lage flere kuber, slik:

For hindringene vil jeg ha 3 hindervarianter, men du kan lage så mange du trenger:

  • Lag 3 GameObjects inne i "TilePrefab"-objektet og navngi dem "Obstacle1", "Obstacle2" og "Obstacle3"
  • For den første hindringen, lag en ny kube og flytt den inn i "Obstacle1"-objektet
  • Skaler den nye kuben til omtrent samme bredde som plattformen og skaler høyden ned (spilleren må hoppe for å unngå denne hindringen)
  • Lag et nytt materiale, navngi det "RedMaterial" og endre fargen til rødt, og tilordne det deretter til kuben (dette er bare slik at hindringen kan skilles fra hovedplattformen)

  • For "Obstacle2", lag et par terninger og plasser dem i en trekantet form, og la det være en åpen plass nederst (spilleren må huke seg for å unngå denne hindringen)

  • Og til slutt, "Obstacle3" kommer til å være et duplikat av "Obstacle1" og "Obstacle2", kombinert sammen

  • Velg nå alle objektene innenfor hindringer og endre taggen deres til "Finish", dette vil være nødvendig senere for å oppdage kollisjonen mellom spiller og hindring.

For å generere en uendelig plattform trenger vi et par skript som vil håndtere objektpooling og hindringsaktivering:

  • Lag et nytt skript, kall det "SC_PlatformTile" og lim inn koden nedenfor 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);
        }
    }
}
  • Lag et nytt skript, kall det "SC_GroundGenerator" og lim inn koden nedenfor 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));
    }
}
  • Fest SC_PlatformTile-skriptet til "TilePrefab"-objektet
  • Tilordne "Obstacle1", "Obstacle2" og "Obstacle3" objekt til hindringer array

For startpunktet og sluttpunktet må vi lage 2 GameObjects som skal plasseres på henholdsvis starten og slutten av plattformen:

  • Tilordne startpunkt- og sluttpunktvariabler i SC_PlatformTile

  • Lagre "TilePrefab"-objektet til Prefab og fjern det fra scenen
  • Lag et nytt GameObject og kall det "_GroundGenerator"
  • Fest SC_GroundGenerator-skriptet til "_GroundGenerator"-objektet
  • Endre hovedkameraposisjonen til (10, 1, -9) og endre rotasjonen til (0, -55, 0)
  • Lag et nytt GameObject, kall det "StartPoint" og endre dets posisjon til (0, -2, -15)
  • Velg "_GroundGenerator"-objektet og i SC_GroundGenerator tilordne hovedkamera-, startpunkt- og tile-prefabrikerte variabler

Trykk nå på Play og se hvordan plattformen beveger seg. Så snart plattformbrikken går ut av kameravisningen, flyttes den tilbake til slutten med en tilfeldig hindring som aktiveres, noe som skaper en illusjon av et uendelig nivå (Hopp til 0:11).

Kameraet må plasseres på samme måte som videoen, så plattformene går mot kameraet og bak det, ellers vil ikke plattformene gjenta seg.

Sharp Coder Videospiller

Trinn 2: Lag spilleren

Spilleren Instance vil være en enkel Sphere som bruker en kontroller med muligheten til å hoppe og huke.

  • Opprett en ny Sphere (GameObject -> 3D Object -> Sphere) og fjern dens Sphere Collider-komponent
  • Tilordne tidligere opprettede "RedMaterial" til den
  • Lag et nytt GameObject og kall det "Player"
  • Flytt sfæren inne i "Player"-objektet og endre dens posisjon til (0, 0, 0)
  • Lag et nytt skript, kall det "SC_IRPlayer" og lim inn koden nedenfor 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;
        }
    }
}
  • Fest SC_IRPlayer-skriptet til "Player"-objektet (du vil legge merke til at det la til en annen komponent kalt Rigidbody)
  • Legg til BoxCollider-komponenten til "Player"-objektet

  • Plasser "Player"-objektet litt over "StartPoint"-objektet, rett foran kameraet

Trykk Play og bruk W-tasten for å hoppe og S-tasten til å huke. Målet er å unngå røde hindringer:

Sharp Coder Videospiller

Sjekk denne Horizon Bending Shader.

Foreslåtte artikler
Opplæring for Match-3-puslespill i Unity
Opprette et 2D Brick Breaker-spill i Unity
Lag et glidende puslespill i Unity
Hvordan lage et Flappy Bird-inspirert spill i Unity
Minispill i Unity | CUBEunngå
Farm zombier | Laging av 2D-plattformspill i Unity
Minispill i Unity | Flappy Cube