Unity를 위한 Endless Runner 튜토리얼
비디오 게임에서는 세상이 아무리 크더라도 항상 끝이 있습니다. 하지만 어떤 게임은 무한한 세상을 모방하려 하는데, 이런 게임은 Endless Runner라는 카테고리에 속합니다.
Endless Runner는 플레이어가 포인트를 모으고 장애물을 피하면서 끊임없이 전진하는 게임 유형입니다. 주요 목표는 장애물에 빠지거나 충돌하지 않고 레벨의 끝에 도달하는 것이지만, 종종 레벨이 무한히 반복되어 플레이어가 장애물과 충돌할 때까지 점차 난이도가 높아집니다.
현대의 컴퓨터/게임 장비조차 처리 능력이 제한되어 있다는 점을 고려하면, 진정으로 무한한 세계를 만드는 것은 불가능합니다.
그렇다면 어떤 게임은 어떻게 무한한 세계의 환상을 만들어낼까요? 답은 빌딩 블록을 재사용하는 것입니다(즉, 객체 풀링). 즉, 블록이 카메라 뷰 뒤나 밖으로 나가자마자 앞으로 이동합니다.
Unity에서 무한 달리기 게임을 만들려면 장애물과 플레이어 컨트롤러가 있는 플랫폼을 만들어야 합니다.
1단계: 플랫폼 만들기
먼저 Prefab에 나중에 저장될 타일 플랫폼을 만듭니다.
- 새로운 GameObject를 생성하고 호출합니다. "TilePrefab"
- 새로운 Cube를 생성합니다(GameObject -> 3D Object -> Cube)
- "TilePrefab" 객체 내부로 Cube를 이동하고 위치를 (0, 0, 0)으로 변경하고 크기를 (8, 0.4, 20)으로 변경합니다.
- 선택적으로 다음과 같이 추가 큐브를 만들어 측면에 레일을 추가할 수 있습니다.
장애물의 경우, 3가지 장애물 변형이 있지만, 필요한 만큼 만들어도 됩니다.
- "TilePrefab" 객체 내부에 3개의 GameObject를 생성하고 이를 "Obstacle1", "Obstacle2", "Obstacle3"로 명명합니다. "Obstacle3"
- 첫 번째 장애물의 경우 새 Cube를 생성하여 "Obstacle1" 객체 내부로 이동합니다.
- 새로운 큐브의 너비를 플랫폼과 거의 같게 조정하고 높이를 줄입니다(플레이어는 이 장애물을 피하기 위해 점프해야 합니다)
- 새로운 Material을 만들고 이름을 "RedMaterial"로 지정한 다음 색상을 빨간색으로 변경한 다음 Cube에 할당합니다(이것은 장애물이 주 플랫폼과 구별되도록 하기 위한 것입니다)
- "Obstacle2"의 경우 큐브 몇 개를 만들어 삼각형 모양으로 배치하고 맨 아래에 빈 공간을 하나 남겨둡니다(플레이어는 이 장애물을 피하기 위해 웅크리고 있어야 합니다)
- 마지막으로, "Obstacle3"은 "Obstacle1"과 "Obstacle2"을 합친 것의 복제본이 됩니다.
- 이제 장애물 안에 있는 모든 물체를 선택하고 태그를 "Finish"로 변경합니다. 이는 나중에 플레이어와 장애물 사이의 충돌을 감지하는 데 필요합니다.
무한 플랫폼을 생성하려면 객체 풀링과 장애물 활성화를 처리하는 몇 가지 스크립트가 필요합니다.
- 새 스크립트를 로 만들고 "SC_PlatformTile"이라고 이름붙인 후 아래 코드를 붙여넣습니다.
SC_플랫폼타일.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));
}
}
- SC_PlatformTile 스크립트를 "TilePrefab" 객체에 첨부합니다.
- "Obstacle1", "Obstacle2" 및 "Obstacle3" 객체를 Obstacles 배열에 할당합니다.
시작점과 종료점의 경우 각각 플랫폼의 시작점과 끝에 배치해야 하는 2개의 GameObject를 만들어야 합니다.
- SC_PlatformTile에서 시작점 및 종료점 변수 지정
- "TilePrefab" 객체를 Prefab에 저장하고 Scene에서 제거합니다.
- 새로운 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 변수를 지정합니다.
이제 재생을 눌러 플랫폼이 어떻게 움직이는지 관찰하세요. 플랫폼 타일이 카메라 뷰에서 나가자마자 무작위 장애물이 활성화되어 끝으로 다시 이동하여 무한 레벨의 환상을 만듭니다(0:11로 건너뛰기).
카메라는 비디오와 비슷하게 배치해야 합니다. 즉, 플랫폼이 카메라를 향하고 뒤로 가야 합니다. 그렇지 않으면 플랫폼이 반복되지 않습니다.
2단계: 플레이어 만들기
플레이어 인스턴스는 점프하고 웅크리는 기능이 있는 컨트롤러를 사용하는 간단한 구체입니다.
- 새로운 Sphere(GameObject -> 3D Object -> Sphere)를 생성하고 Sphere Collider 구성 요소를 제거합니다.
- 이전에 생성한 "RedMaterial"를 여기에 할당하세요
- 새로운 GameObject를 생성하고 호출합니다. "Player"
- 구를 "Player" 객체 내부로 이동하고 위치를 (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 스크립트를 객체에 첨부합니다(Rigidbody라는 또 다른 구성 요소가 추가되었음을 알 수 있습니다)
- "Player" 객체에 BoxCollider 구성 요소를 추가합니다.
- "Player" 객체를 "StartPoint" 객체 바로 위, 카메라 바로 앞에 배치합니다.
Play를 누르고 W 키를 사용하여 점프하고 S 키를 사용하여 웅크리고 앉습니다. 목표는 빨간색 장애물을 피하는 것입니다.
이 Horizon Bending Shader를 확인해 보세요.