Royal-run #1

John Jean·2025년 1월 24일

Unity

목록 보기
10/19

Level Generation Using For Loops

지형이 될 청크 생성, x와z의 scale을 10으로 조정(스크립트상에서 편집하기 용이하게 딱 떨어지는 숫자가 좋다.) 그리고 프리팹으로 만든다.

LevelGenerator.cs 생성, 청크의 생성을 담당할 코드.

[SerializeField] GameObject chunkPrefab; // 생성될 청크(지형)
[SerializeField] int startingChunkAmount = 12; // 생성될 개수
[SerializeField] Transform chunkParent; // 청크가 생성될 위치(보기 편하게 정리할 용도)

[SerializeField] float chunkLength = 10f; // 청크의 한 면의 길이

private void Start()
{
    for (int i = 0; i < startingChunkAmount; i++)
    {
        float spawnPositionZ;

        if(i == 0)
        {
            spawnPositionZ = transform.position.z;
        }
        else
        {
            spawnPositionZ = transform.position.z + (i * chunkLength);
        }

        Vector3 chunkSpawnPos = new Vector3(transform.position.x, transform.position.y, spawnPositionZ);
        Instantiate(chunkPrefab, chunkSpawnPos, Quaternion.identity, chunkParent); // 기본적인 생성인자 3개 + 생성될 위치
        
    }
}

반복문으로 지형을 일자로 생성되도록 한다.

Chunk Movement

생성된 지형이 플레이어 쪽으로 이동해야 한다.

즉, 플레이어가 움직이는것이 아니라, 맵이 움직이는것.

Endless Runner 장르에서 자주 사용되는 기법이라니까 Cliff Diverz에서 적용해봐야겠다.

일단 지형 프리팹이 밋밋하니 꾸며준다.

// levelGenerator.cs

[SerializeField] float moveSpeed = 8f;
GameObject[] chunks = new GameObject[12];


private void Update()
 {
     MoveChunks();
 }

void MoveChunks()
{
    for(int i = 0; i < startingChunkAmount;i++)
    {
        chunks[i].transform.Translate(-transform.forward * (moveSpeed * Time.deltaTime)); // 유니티에서는 벡터값과 float의 연산이 비교적 느리므로 float끼리 일단 연산하고 벡터와 연산을 하자.
    }
}

청크를 저장할 배열을 만들어주고, Start()에서 모든 청크를 배열에 넣어줌.

반복문을 사용해 생성된 모든 청크를 플레이어 쪽으로 이동시킴.

Create & Destroy Chunks

무한 지형을 구현.

카메라와 멀어지는 지형은 파괴, 파괴되면 맨 앞에 새로운 지형 생성.

카메라와 거리를 계산,

// levelGenerator.cs

void MoveChunks()
{
    for(int i = 0; i < chunks.Count;i++)
    {
        GameObject chunk = chunks[i];
        chunks[i].transform.Translate(-transform.forward * (moveSpeed * Time.deltaTime)); // 유니티에서는 벡터값과 float의 연산이 비교적 느리므로 float끼리 일단 연산하고 벡터와 연산을 하자.

        if (chunk.transform.position.z <= Camera.main.transform.position.z - chunkLength)
        {
            chunks.Remove(chunk);
            Destroy(chunk);
            SpawnChunk();
        }
    }
}

멀어지면 삭제, 삭제되면 새로 생성.

Player Movement

새로운 액션맵 생성.

액션타입은 PassThrough, 타입은 Vector2.

Player 컴포넌트에 Player Actions추가, 생성한 액션맵 할당, Behavior -> Invoke Unity Events 설정.

PlayerMovement.cs 생성후 Player 컴포넌트에 추가.

// PlayerMovement.cs

Vector2 movement;

public void Move(InputAction.CallbackContext context)
{
    movement = context.ReadValue<Vector2>(); // 런타임에서 인풋으로 받아온 값.
    Debug.Log(movement);
}

리스트에 추가해주고, 방금 넣어준 컴포넌트 할당한뒤, 만들어준 메서드 할당.

private void FixedUpdate()
{
    HandleMovement();
}

private void HandleMovement()
{
    Vector3 currentPosition = rigidBody.position;
    Vector3 moveDirection = new Vector3(movement.x, 0f, movement.y);
    Vector3 newPosition = currentPosition + moveDirection * (moveSpeed * Time.deltaTime);
    rigidBody.MovePosition(newPosition);
}

현재 위치, 이동 방향, 새 위치를 계산해 rigidBody.MovePosition 을 사용해 이동.

FixedUpdate()를 사용했더니 이동이 부드럽지 않습니다.

유니티의 물리 엔진(PhysX)은 FixedUpdate에서 고정된 간격(0.02초 간격)으로 실행되지만, 프레임 속도는 가변적이므로 Update에서 실행되는 렌더링과 차이가 날 수 있다.

따라서 Interpolate 옵션으로 보간을 해줘야 함.

Restrain Player Using Clamp

플레이어의 이동반경을 제한해야 한다.

저번 강의 프로젝트 때 해봤던 메서드를 사용할것.

Mathf.Clamp(float value, float min, float max)

valueThe floating point value to restrict inside the range defined by the minimum and maximum values.
minThe minimum floating point value to compare against.
maxThe maximum floating point value to compare against.
// PlayerControler.cs

...
[SerializeField] float xClamp = 3f;
[SerializeField] float zClamp = 3f;
...

private void HandleMovement()
{
    Vector3 currentPosition = rigidBody.position;
    Vector3 moveDirection = new Vector3(movement.x, 0f, movement.y);
    Vector3 newPosition = currentPosition + moveDirection * (moveSpeed * Time.deltaTime);

    newPosition.x = Mathf.Clamp(newPosition.x, -xClamp, xClamp);
    newPosition.z = Mathf.Clamp(newPosition.z, -zClamp, zClamp);

    rigidBody.MovePosition(newPosition);
}

간단하게 말해서 newPosition.x = Mathf.Clamp(newPosition.x, -xClamp, xClamp);, newPosition.z = Mathf.Clamp(newPosition.z, -zClamp, zClamp); 이 두 줄이 이동 범위를 제한한다고 보면된다.

Obstacles Using While Loops

While문과 코루틴을 사용해 장애물이 하나씩 굴러오는 기능을 구현해보자.

유니티에선 중력을 마음대로 조종할 수 있는데, y좌표 뿐만 아니라 z좌표에도 중력을 추가해 장애물이 플레이어를 향해 날아오도록 수정, 프로젝트 설정 - physics - gravity의 z값에 y값과 동일한 값 추가.

장애물 생성기라는 빈 오브젝트 생성후 다음의 스크립트를 붙이기.

// obstacleSpawner.cs

[SerializeField] GameObject obstaclePrefab;
[SerializeField] float obstacleSpawnTime = 1f;

int obstacleSpawned = 0;

// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
    StartCoroutine(SpawnObstacleRoutine());
}

IEnumerator SpawnObstacleRoutine()
{
    while (obstacleSpawned < 5)
    {
        yield return new WaitForSeconds(obstacleSpawnTime);
        Instantiate(obstaclePrefab, transform.position, Quaternion.identity);
        obstacleSpawned++;
    }
}

간단한 코루틴 메서드로 시간차를 두고 장애물이 스폰되도록 설정.

profile
크래프톤 6기 정글러

0개의 댓글