Sådan laver du Endless Runner-spil i Unity

I videospil, uanset hvor stor verden er, har den altid en ende. Men nogle spil forsøger at efterligne den uendelige verden, sådanne spil falder ind under kategorien Endless Runner.

Endless Runner er en type spil, hvor spilleren konstant bevæger sig fremad, mens han samler point og undgår forhindringer. Hovedformålet er at nå slutningen af ​​niveauet uden at falde ind i eller kollidere med forhindringerne, men ofte gentager niveauet sig selv uendeligt og gradvist øger sværhedsgraden, indtil spilleren kolliderer med forhindringen.

Subway Surfers Gameplay

I betragtning af at selv moderne computere/spilenheder har begrænset processorkraft, er det umuligt at skabe en virkelig uendelig verden.

Så hvordan skaber nogle spil en illusion om en uendelig verden? Svaret er ved at genbruge byggeklodserne (alias objektpooling), med andre ord, så snart blokken går bagved eller uden for kameravisningen, flyttes den til fronten.

For at lave et spil med endeløse løbere i Unity, skal vi lave en platform med forhindringer og en spillercontroller.

Trin 1: Opret platformen

Vi begynder med at skabe en flisebelagt platform, som senere vil blive gemt i Prefab:

  • Opret et nyt GameObject og kald det "TilePrefab"
  • Opret ny terning (GameObject -> 3D Object -> Cube)
  • Flyt terningen inde i "TilePrefab"-objektet, skift dens position til (0, 0, 0) og skaler til (8, 0,4, 20)

  • Eventuelt kan du tilføje skinner til siderne ved at oprette yderligere terninger, som dette:

Til forhindringerne vil jeg have 3 forhindringsvarianter, men du kan lave så mange, du har brug for:

  • Opret 3 GameObjects inde i "TilePrefab" objektet og navngiv dem "Obstacle1", "Obstacle2" og "Obstacle3"
  • For den første forhindring skal du oprette en ny terning og flytte den ind i "Obstacle1"-objektet
  • Skaler den nye terning til omkring samme bredde som platformen og skaler dens højde ned (spilleren bliver nødt til at hoppe for at undgå denne forhindring)
  • Opret et nyt materiale, navngiv det "RedMaterial" og skift dets farve til Rød, og tildel det derefter til terningen (dette er bare for at skelne forhindringen fra hovedplatformen)

  • Til "Obstacle2" skal du oprette et par terninger og placere dem i en trekantet form, så der efterlades en åben plads i bunden (spilleren bliver nødt til at sidde på hug for at undgå denne forhindring)

  • Og endelig vil "Obstacle3" være en duplikat af "Obstacle1" og "Obstacle2", kombineret sammen

  • Vælg nu alle objekterne inde i forhindringer og skift deres tag til "Finish", dette vil være nødvendigt senere for at opdage kollisionen mellem spiller og forhindring.

For at generere en uendelig platform har vi brug for et par scripts, der vil håndtere objektpooling og forhindringsaktivering:

  • Opret et nyt script, kald det "SC_PlatformTile" og indsæt 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);
        }
    }
}
  • Opret et nyt script, kald det "SC_GroundGenerator" og indsæt 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));
    }
}
  • Vedhæft SC_PlatformTile-scriptet til "TilePrefab"-objektet
  • Tildel "Obstacle1", "Obstacle2" og "Obstacle3" objekt til forhindringer array

For startpunktet og slutpunktet skal vi oprette 2 GameObjects, der skal placeres ved henholdsvis starten og slutningen af ​​platformen:

  • Tildel startpunkt- og slutpunktvariabler i SC_PlatformTile

  • Gem "TilePrefab" objektet i Prefab og fjern det fra scenen
  • Opret et nyt GameObject og kald det "_GroundGenerator"
  • Vedhæft SC_GroundGenerator-scriptet til "_GroundGenerator"-objektet
  • Skift hovedkameraets position til (10, 1, -9) og skift dets rotation til (0, -55, 0)
  • Opret et nyt GameObject, kald det "StartPoint" og skift dets position til (0, -2, -15)
  • Vælg "_GroundGenerator"-objektet, og i SC_GroundGenerator tildel hovedkamera-, startpunkt- og tile-præfabrikerede variabler

Tryk nu på Play og observer, hvordan platformen bevæger sig. Så snart platformflisen forsvinder fra kameravisningen, flyttes den tilbage til enden med en tilfældig forhindring, der aktiveres, hvilket skaber en illusion om et uendeligt niveau (Spring til 0:11).

Kameraet skal placeres på samme måde som videoen, så platformene går mod kameraet og bagved det, ellers gentager platformene sig ikke.

Sharp Coder Videoafspiller

Trin 2: Opret afspilleren

Spilleren Instance vil være en simpel sfære ved hjælp af en controller med evnen til at hoppe og huke.

  • Opret en ny Sphere (GameObject -> 3D Object -> Sphere) og fjern dens Sphere Collider-komponent
  • Tildel tidligere oprettede "RedMaterial" til det
  • Opret et nyt GameObject og kald det "Player"
  • Flyt sfæren inde i "Player"-objektet og skift dens position til (0, 0, 0)
  • Opret et nyt script, kald det "SC_IRPlayer" og indsæt 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;
        }
    }
}
  • Vedhæft SC_IRPlayer-scriptet til "Player"-objektet (du vil bemærke, at det tilføjede en anden komponent kaldet Rigidbody)
  • Tilføj BoxCollider-komponenten til "Player"-objektet

  • Placer "Player"-objektet lidt over "StartPoint"-objektet lige foran kameraet

Tryk på Play og brug W-tasten til at hoppe og S-tasten til at lægge på hug. Målet er at undgå røde forhindringer:

Sharp Coder Videoafspiller

Tjek denne Horizon Bending Shader.