Tutorial Endless Runner untuk Unity

Dalam gim video, tidak peduli seberapa besar dunia, dunia itu akan selalu memiliki akhir. Namun, beberapa gim mencoba meniru dunia yang tak terbatas, gim semacam itu termasuk dalam kategori yang disebut Endless Runner.

Endless Runner adalah jenis permainan di mana pemain terus bergerak maju sambil mengumpulkan poin dan menghindari rintangan. Tujuan utamanya adalah mencapai akhir level tanpa jatuh atau bertabrakan dengan rintangan, tetapi sering kali, level tersebut berulang tanpa henti, secara bertahap meningkatkan kesulitan, hingga pemain bertabrakan dengan rintangan.

Permainan Subway Surfers

Mengingat bahwa komputer/perangkat permainan modern sekalipun memiliki daya pemrosesan yang terbatas, mustahil untuk membuat dunia yang benar-benar tak terbatas.

Jadi, bagaimana beberapa permainan menciptakan ilusi dunia yang tak terbatas? Jawabannya adalah dengan menggunakan kembali blok-blok penyusun (alias penggabungan objek), dengan kata lain, begitu blok berada di belakang atau di luar tampilan Kamera, blok tersebut akan dipindahkan ke depan.

Untuk membuat game endless-runner di Unity, kita perlu membuat platform dengan rintangan dan pengontrol pemain.

Langkah 1: Buat Platform

Kita mulai dengan membuat platform berubin yang nantinya akan disimpan di Prefab:

  • Buat GameObject baru dan beri nama "TilePrefab"
  • Buat Kubus baru (GameObject -> Objek 3D -> Kubus)
  • Pindahkan Kubus ke dalam objek "TilePrefab", ubah posisinya menjadi (0, 0, 0), dan skalakan ke (8, 0.4, 20)

  • Secara opsional Anda dapat menambahkan Rel ke sisi dengan membuat Kubus tambahan, seperti ini:

Untuk rintangannya, saya akan membuat 3 variasi rintangan, tetapi Anda dapat membuatnya sebanyak yang dibutuhkan:

  • Buat 3 GameObjects di dalam objek "TilePrefab" dan beri nama "Obstacle1", "Obstacle2" dan "Obstacle3"
  • Untuk rintangan pertama, buatlah sebuah Kubus baru dan pindahkan ke dalam objek "Obstacle1"
  • Skalakan Kubus baru dengan lebar yang hampir sama dengan platform dan turunkan tingginya (pemain harus melompat untuk menghindari rintangan ini)
  • Buat Material baru, beri nama "RedMaterial" dan ubah warnanya menjadi Merah, lalu tetapkan ke Kubus (ini hanya agar rintangan dibedakan dari platform utama)

  • Untuk "Obstacle2" buat beberapa kubus dan letakkan dalam bentuk segitiga, sisakan satu ruang terbuka di bagian bawah (pemain harus berjongkok untuk menghindari rintangan ini)

  • Dan terakhir, "Obstacle3" akan menjadi duplikat dari "Obstacle1" dan "Obstacle2", digabungkan bersama

  • Sekarang pilih semua Objek di dalam Rintangan dan ubah tag mereka menjadi "Finish", ini akan dibutuhkan nanti untuk mendeteksi tabrakan antara Pemain dan Rintangan.

Untuk menghasilkan platform tak terbatas, kita memerlukan beberapa skrip yang akan menangani Penggabungan Objek dan aktivasi Kendala:

  • Buat skrip baru, beri nama "SC_PlatformTile" dan tempel kode di bawah ini di dalamnya:

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);
        }
    }
}
  • Buat skrip baru, beri nama "SC_GroundGenerator" dan tempel kode di bawah ini di dalamnya:

SC_GeneratorTanah.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));
    }
}
  • Lampirkan skrip SC_PlatformTile ke objek "TilePrefab"
  • Tetapkan objek "Obstacle1", "Obstacle2" dan "Obstacle3" ke array Hambatan

Untuk Titik Awal dan Titik Akhir, kita perlu membuat 2 GameObject yang masing-masing harus ditempatkan di awal dan akhir platform:

  • Tetapkan variabel Titik Awal dan Titik Akhir di SC_PlatformTile

  • Simpan objek "TilePrefab" ke Prefab dan hapus dari Scene
  • Buat GameObject baru dan beri nama "_GroundGenerator"
  • Lampirkan skrip SC_GroundGenerator ke objek "_GroundGenerator"
  • Ubah posisi Kamera Utama ke (10, 1, -9) dan ubah rotasinya ke (0, -55, 0)
  • Buat GameObject baru, beri nama "StartPoint" dan ubah posisinya menjadi (0, -2, -15)
  • Pilih objek "_GroundGenerator" dan di SC_GroundGenerator tetapkan variabel Kamera Utama, Titik Awal, dan Tile Prefab

Sekarang tekan Play dan amati bagaimana platform bergerak. Begitu ubin platform keluar dari tampilan kamera, ubin tersebut akan dipindahkan kembali ke ujung dengan rintangan acak yang diaktifkan, menciptakan ilusi level tak terbatas (Langsung ke 0:11).

Kamera harus ditempatkan serupa dengan video, sehingga platform mengarah ke Kamera dan di belakangnya, jika tidak, platform tidak akan berulang.

Sharp Coder Pemutar video

Langkah 2: Buat Pemain

Instansi pemain akan berupa Sphere sederhana yang menggunakan pengontrol dengan kemampuan untuk melompat dan berjongkok.

  • Buat Sphere baru (GameObject -> Objek 3D -> Sphere) dan hapus komponen Sphere Collider-nya
  • Tetapkan "RedMaterial" yang telah dibuat sebelumnya padanya
  • Buat GameObject baru dan beri nama "Player"
  • Pindahkan Sphere ke dalam objek "Player" dan ubah posisinya menjadi (0, 0, 0)
  • Buat skrip baru, beri nama "SC_IRPlayer" dan tempel kode di bawah ini di dalamnya:

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;
        }
    }
}
  • Lampirkan skrip SC_IRPlayer ke objek "Player" (Anda akan melihat bahwa ia menambahkan komponen lain yang disebut Rigidbody)
  • Tambahkan komponen BoxCollider ke objek "Player"

  • Tempatkan objek "Player" sedikit di atas objek "StartPoint", tepat di depan Kamera

Tekan Play dan gunakan tombol W untuk melompat dan tombol S untuk berjongkok. Tujuannya adalah menghindari rintangan merah:

Sharp Coder Pemutar video

Periksa Horizon Bending Shader ini.