Unity용 Endless Runner 튜토리얼

비디오 게임에서는 세상이 아무리 크더라도 항상 끝이 있습니다. 그러나 일부 게임은 무한한 세계를 모방하려고 시도하며 이러한 게임은 Endless Runner라는 범주에 속합니다.

Endless Runner는 플레이어가 포인트를 수집하고 장애물을 피하면서 끊임없이 전진하는 게임 유형입니다. 주요 목표는 장애물에 떨어지거나 장애물과 충돌하지 않고 레벨 끝까지 도달하는 것이지만, 플레이어가 장애물과 충돌할 때까지 레벨이 무한히 반복되어 점차 난이도가 높아지는 경우가 많습니다.

지하철 서퍼스 게임플레이

현대의 컴퓨터/게임 장치도 처리 능력이 제한되어 있다는 점을 고려하면 진정으로 무한한 세계를 만드는 것은 불가능합니다.

그렇다면 일부 게임은 어떻게 무한한 세계의 환상을 만들어낼 수 있을까요? 대답은 빌딩 블록을 재사용하는 것입니다(객체 풀링이라고도 함). 즉, 블록이 카메라 보기 뒤나 바깥으로 이동하자마자 앞으로 이동합니다.

Unity에서 끝없는 달리기 게임을 만들려면 장애물과 플레이어 컨트롤러가 있는 플랫폼을 만들어야 합니다.

1단계: 플랫폼 생성

나중에 Prefab에 저장될 타일 플랫폼을 만드는 것부터 시작합니다.

  • 새로운 GameObject를 생성하고 호출합니다. "TilePrefab"
  • 새 큐브 만들기(GameObject -> 3D 개체 -> 큐브)
  • 큐브를 "TilePrefab" 개체 내부로 이동하고 위치를 (0, 0, 0)으로 변경하고 크기를 (8, 0.4, 20)으로 변경합니다.

  • 선택적으로 다음과 같이 추가 큐브를 생성하여 측면에 레일을 추가할 수 있습니다.

장애물에는 3가지 장애물 변형이 있지만 필요한 만큼 많이 만들 수 있습니다.

  • "TilePrefab" 개체 내에 3개의 게임 개체를 만들고 이름을 "Obstacle1", "Obstacle2"로 지정하고 "Obstacle3"
  • 첫 번째 장애물의 경우 새 큐브를 만들고 "Obstacle1" 개체 내부로 이동합니다.
  • 새 큐브의 크기를 플랫폼과 거의 같은 너비로 조정하고 높이를 낮추십시오(플레이어는 이 장애물을 피하기 위해 점프해야 합니다).
  • 새 머티리얼을 생성하고 이름을 "RedMaterial"로 지정하고 색상을 빨간색으로 변경한 다음 큐브에 할당합니다(이는 장애물이 메인 플랫폼과 구별되기 위한 것입니다).

  • "Obstacle2"의 경우 두 개의 큐브를 만들어 삼각형 모양으로 배치하고 아래쪽에 열린 공간을 하나 남겨둡니다(플레이어는 이 장애물을 피하기 위해 몸을 숙여야 합니다).

  • 마지막으로 "Obstacle3"은 "Obstacle1"과 "Obstacle2"의 복사본이 되어 함께 결합됩니다.

  • 이제 장애물 내의 모든 개체를 선택하고 해당 태그를 "Finish"로 변경합니다. 이는 나중에 플레이어와 장애물 간의 충돌을 감지하는 데 필요합니다.

무한 플랫폼을 생성하려면 개체 풀링 및 장애물 활성화를 처리하는 몇 가지 스크립트가 필요합니다.

  • 새 스크립트를 만들고 이름을 "SC_PlatformTile"로 지정한 후 그 안에 아래 코드를 붙여넣습니다.

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);
        }
    }
}
  • 새 스크립트를 만들고 이름을 "SC_GroundGenerator"으로 지정하고 그 안에 아래 코드를 붙여넣습니다.

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));
    }
}

시작점과 끝점의 경우 플랫폼의 시작과 끝 부분에 각각 배치해야 하는 2개의 GameObject를 생성해야 합니다.

  • SC_PlatformTile에 시작점 및 끝점 변수 할당

  • "TilePrefab" 객체를 Prefab에 저장하고 장면에서 제거합니다.
  • 새로운 GameObject를 생성하고 호출합니다. "_GroundGenerator"
  • SC_GroundGenerator 스크립트를 "_GroundGenerator" 개체에 연결합니다.
  • 기본 카메라 위치를 (10, 1, -9)로 변경하고 회전을 (0, -55, 0)으로 변경합니다.
  • 새 GameObject를 생성하고 이름을 "StartPoint"으로 지정하고 위치를 (0, -2, -15)로 변경합니다.
  • "_GroundGenerator" 객체를 선택하고 SC_GroundGenerator에서 Main Camera, Start Point 및 Tile Prefab 변수를 할당합니다.

이제 Play를 누르고 플랫폼이 어떻게 움직이는지 관찰하세요. 플랫폼 타일이 카메라 뷰에서 벗어나자마자 무작위 장애물이 활성화되면서 끝으로 다시 이동하여 무한 레벨의 환상을 만듭니다(0:11로 건너뛰기).

카메라는 비디오와 유사하게 배치되어야 플랫폼이 카메라를 향하고 그 뒤에 있도록 됩니다. 그렇지 않으면 플랫폼이 반복되지 않습니다.

Sharp Coder 비디오 플레이어

2단계: 플레이어 생성

플레이어 인스턴스는 점프하고 앉을 수 있는 기능을 갖춘 컨트롤러를 사용하는 간단한 구입니다.

  • 새 Sphere(GameObject -> 3D Object -> Sphere)를 생성하고 해당 Sphere Collider 구성 요소를 제거합니다.
  • 이전에 생성된 "RedMaterial"를 여기에 할당합니다.
  • 새로운 GameObject를 생성하고 호출합니다. "Player"
  • "Player" 개체 내부에서 Sphere를 이동하고 위치를 (0, 0, 0)으로 변경합니다.
  • 새 스크립트를 만들고 이름을 "SC_IRPlayer"으로 지정하고 그 안에 아래 코드를 붙여넣습니다.

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;
        }
    }
}
  • SC_IRPlayer 스크립트를 "Player" 개체에 연결합니다(Rigidbody라는 또 다른 구성 요소가 추가된 것을 볼 수 있습니다)
  • "Player" 개체에 BoxCollider 구성 요소를 추가합니다.

  • "Player" 개체를 "StartPoint" 개체 바로 위에 카메라 바로 앞에 배치합니다.

Play를 누르고 W 키를 사용하여 점프하고 S 키를 사용하여 앉으세요. 목표는 빨간색 장애물을 피하는 것입니다.

Sharp Coder 비디오 플레이어

Horizon Bending Shader를 확인하세요.

원천
📁EndlessRunner.unitypackage26.68 KB
추천 기사
Unity의 매치-3 퍼즐 게임 튜토리얼
Unity에서 2D 벽돌깨기 게임 만들기
Unity에서 슬라이딩 퍼즐 게임 만들기
Unity에서 Flappy Bird에서 영감을 받은 게임을 만드는 방법
Unity의 미니 게임 | 큐브피하다
농장 좀비 | Unity로 2D 플랫폼 게임 만들기
Unity의 미니 게임 | 플래피 큐브