Hướng dẫn Endless Runner cho Unity

Trong trò chơi điện tử, dù thế giới có lớn đến đâu thì nó cũng luôn có hồi kết. Nhưng một số trò chơi cố gắng mô phỏng thế giới vô hạn, những trò chơi như vậy thuộc thể loại Endless Runner.

Endless Runner là một loại trò chơi mà người chơi liên tục di chuyển về phía trước trong khi thu thập điểm và tránh chướng ngại vật. Mục tiêu chính là đến cuối cấp độ mà không rơi vào hoặc va chạm với chướng ngại vật, nhưng thường thì cấp độ sẽ lặp lại vô tận, tăng dần độ khó, cho đến khi người chơi va chạm với chướng ngại vật.

Trò chơi Subway Surfers

Nếu xét đến việc ngay cả máy tính/thiết bị chơi game hiện đại cũng có sức mạnh xử lý hạn chế thì việc tạo ra một thế giới thực sự vô hạn là điều không thể.

Vậy một số trò chơi tạo ra ảo giác về một thế giới vô hạn như thế nào? Câu trả lời là bằng cách tái sử dụng các khối xây dựng (hay còn gọi là nhóm đối tượng), nói cách khác, ngay khi khối đó nằm sau hoặc nằm ngoài chế độ xem Camera, nó sẽ được di chuyển ra phía trước.

Để tạo ra trò chơi chạy vô tận trong Unity, chúng ta sẽ cần tạo một nền tảng có chướng ngại vật và bộ điều khiển cho người chơi.

Bước 1: Tạo nền tảng

Chúng ta bắt đầu bằng cách tạo một nền tảng lát gạch sẽ được lưu trữ sau trong Prefab:

  • Tạo một GameObject mới và gọi nó "TilePrefab"
  • Tạo Cube mới (GameObject -> 3D Object -> Cube)
  • Di chuyển khối lập phương bên trong đối tượng "TilePrefab", thay đổi vị trí của nó thành (0, 0, 0) và chia tỷ lệ thành (8, 0,4, 20)

  • Bạn có thể tùy chọn thêm Thanh ray vào các mặt bên bằng cách tạo thêm các Khối lập phương, như thế này:

Đối với các chướng ngại vật, tôi sẽ có 3 biến thể chướng ngại vật, nhưng bạn có thể tạo nhiều tùy theo nhu cầu:

  • Tạo 3 GameObject bên trong đối tượng "TilePrefab" và đặt tên cho chúng là "Obstacle1", "Obstacle2" và "Obstacle3"
  • Đối với chướng ngại vật đầu tiên, hãy tạo một khối lập phương mới và di chuyển nó vào bên trong đối tượng "Obstacle1"
  • Thu nhỏ khối lập phương mới sao cho có cùng chiều rộng với nền tảng và thu nhỏ chiều cao của nó xuống (người chơi sẽ cần phải nhảy để tránh chướng ngại vật này)
  • Tạo một Vật liệu mới, đặt tên là "RedMaterial" và đổi màu thành Đỏ, sau đó gán nó vào Khối lập phương (điều này chỉ để phân biệt chướng ngại vật với nền tảng chính)

  • Đối với "Obstacle2", hãy tạo một vài khối lập phương và đặt chúng theo hình tam giác, chừa một khoảng trống ở phía dưới (người chơi sẽ cần phải khom người để tránh chướng ngại vật này)

  • Và cuối cùng, "Obstacle3" sẽ là bản sao của "Obstacle1" và "Obstacle2", được kết hợp lại với nhau

  • Bây giờ hãy chọn tất cả các Đối tượng bên trong Chướng ngại vật và đổi thẻ của chúng thành "Finish", điều này sẽ cần thiết sau này để phát hiện va chạm giữa Người chơi và Chướng ngại vật.

Để tạo ra một nền tảng vô hạn, chúng ta sẽ cần một vài tập lệnh xử lý Object Pooling và kích hoạt chướng ngại vật:

  • Tạo một tập lệnh mới, đặt tên là "SC_PlatformTile" và dán đoạn mã bên dưới vào bên trong:

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);
        }
    }
}
  • Tạo một tập lệnh mới, đặt tên là "SC_GroundGenerator" và dán đoạn mã bên dưới vào bên trong:

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));
    }
}
  • Đính kèm tập lệnh SC_PlatformTile vào đối tượng "TilePrefab"
  • Gán đối tượng "Obstacle1", "Obstacle2" và "Obstacle3" vào mảng Obstacles

Đối với Điểm bắt đầu và Điểm kết thúc, chúng ta cần tạo 2 GameObject được đặt ở điểm bắt đầu và điểm kết thúc của nền tảng:

  • Gán các biến Điểm bắt đầu và Điểm kết thúc trong SC_PlatformTile

  • Lưu đối tượng "TilePrefab" vào Prefab và xóa nó khỏi Scene
  • Tạo một GameObject mới và gọi nó "_GroundGenerator"
  • Đính kèm tập lệnh SC_GroundGenerator vào đối tượng "_GroundGenerator"
  • Thay đổi vị trí Camera chính thành (10, 1, -9) và thay đổi góc quay của nó thành (0, -55, 0)
  • Tạo một GameObject mới, gọi nó là "StartPoint" và thay đổi vị trí của nó thành (0, -2, -15)
  • Chọn đối tượng "_GroundGenerator" và trong SC_GroundGenerator chỉ định các biến Main Camera, Start Point và Tile Prefab

Bây giờ hãy nhấn Play và quan sát cách nền tảng di chuyển. Ngay khi ô nền tảng ra khỏi chế độ xem camera, nó sẽ di chuyển trở lại cuối cùng với một chướng ngại vật ngẫu nhiên được kích hoạt, tạo ra ảo giác về một cấp độ vô hạn (Chuyển đến 0:11).

Camera phải được đặt tương tự như video, do đó các nền tảng hướng về phía Camera và phía sau Camera, nếu không các nền tảng sẽ không lặp lại.

Sharp Coder Trình phát video

Bước 2: Tạo Trình phát

Instance của người chơi sẽ là một quả cầu đơn giản sử dụng bộ điều khiển có khả năng nhảy và cúi người.

  • Tạo một Sphere mới (GameObject -> 3D Object -> Sphere) và xóa thành phần Sphere Collider của nó
  • Gán "RedMaterial" đã tạo trước đó cho nó
  • Tạo một GameObject mới và gọi nó "Player"
  • Di chuyển hình cầu vào bên trong đối tượng "Player" và thay đổi vị trí của nó thành (0, 0, 0)
  • Tạo một tập lệnh mới, đặt tên là "SC_IRPlayer" và dán đoạn mã bên dưới vào bên trong:

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;
        }
    }
}
  • Đính kèm tập lệnh SC_IRPlayer vào đối tượng "Player" (bạn sẽ nhận thấy rằng nó đã thêm một thành phần khác có tên là Rigidbody)
  • Thêm thành phần BoxCollider vào đối tượng "Player"

  • Đặt vật thể "Player" cao hơn một chút so với vật thể "StartPoint", ngay phía trước Camera

Nhấn Play và sử dụng phím W để nhảy và phím S để cúi người. Mục tiêu là tránh các chướng ngại vật màu đỏ:

Sharp Coder Trình phát video

Kiểm tra Horizon Bending Shader.