레트로의 유니티 게임 프로그래밍 에센스 - 7.5

Cosmos·2023년 4월 7일
0

학습 매체 : 책

책이름 : 레트로의 유니티 게임 프로그래밍 에센스

저자 : 이제민


본 내용은 해당 강의 내용을 공부하면서 정리한 글입니다.


7.5 탄알 생성기 스크립트 만들기


  • 탄알을 생성할 수 있게 하는 BulletSpawner 스크립트를 만들자


7.5.1 BulletSpawner의 변수

  • 스크립트에는 다음 역할을 수행할 변수들이 필요하다.
  • 생성할 탄알의 원본

  • 탄알을 발사하여 맞출 대상

  • 탄알을 생성하는 시간 간격

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletSpawner : MonoBehaviour
{
    public GameObject bulletPrefab;
    public float spawnRateMin = 0.5f;
    public float spawnRateMax = 3f;

    private Transform target;
    private float spawnRate;
    private float timeAfterSpawn;

    void Start()
    {
        
    }

    void Update()
    {
        
    }
}

public 변수

  • bulletPrefab : 탄알을 생성하는 데 사용할 원본 프리팹
  • spawnRateMin : 새 탄알을 생성하는 데 걸리는 시간의 최솟값
  • spawnRateMax : 새 탄알을 생성하는 데 걸리는 시간의 최댓값

private 변수

  • target : 조준할 대상 게임 오브젝트의 트랜스폼 컴포넌트
  • spawnRate : 다음 탄알을 생성할 때까지 기다릴 시간. spawnRateMin과 spawnRateMax 사이의 랜덤값으로 설정됨
  • timeAfterSpawn : 마지막 탄알 생성 시점부터 흐른 시간을 표시하는 '타이머'

7.5.2 Start( ) 메서드

  • Start( ) 메서드는 시간에 관한 변수를 초기화한다.

  • 탄알을 발사할 목표가 될 게임 오브젝트의 트랜스폼 컴포넌트를 찾아서 가져온다.

    void Start()
    {
        timeAfterSpawn = 0f;
        spawnRate = Random.Range(spawnRateMin, spawnRateMax);
        target = FindObjectOfType<PlayerController>().transform;
    }
  • 마지막으로 탄알을 생성한지 몇 초 지났는지 기록하는 timeAfterSpawn을 0으로 초기화

  • 탄알의 생성 간격인 spawnRate의 초깃값으로 spawnRateMin와 spawnRateMax 사이의 랜덤값을 할당한다.

spawnRate = Random.Range(spawnRateMin, spawnRateMax);
  • 여기서 사용된 Random.Range( ) 메서드는 입력으로 최댓값최솟값을 순서대로 받고, 그 사이의 랜덤한 숫자를 출력하는 유니티 내장 메서드이다.

입력 타입에 따른 Random.Range( ) 메서드의 동작 차이

  • Random.Range( ) 메서드는 float 입력받을 때와 int를 입력받을 때의 동작이 다르다.
  • Random.Range(0, 3) : 0, 1, 2 중 하나가 int 값으로 출력
  • Random.Range(0f, 3.0f) : 0f부터 3f 사이의 float 값이 출력 (예 : 0.5f)
  • 마지막으로 씬에서 PlayerController 타입의 컴포넌트를 가진 게임 오브젝트를 찾고, 해당 게임 오브젝트의 트랜스폼 컴포넌트를 찾아서 target에 할당한다.
target = FindObjectOfType<PlayerController>().transform;

FindObjectOfType( ) 메서드

  • target에는 탄알이 날아갈 대상, 즉 Player 게임 오브젝트의 트랜스폼 컴포넌트가 할당되어야 한다. Player 게임 오브젝트의 트랜스폼으로 Player 게임 오브젝트의 위치를 파악할 수 있다.

  • target에 Player 게임 오브젝트의 트랜스폼 컴포넌트를 할당하는 방법 중에는 target을 public으로 선언하고 인스펙터 창에서 target 필드에 Player 게임 오브젝트를 직접 드래그&드롭하는 방법이 있다.

  • 이 경우 탄알 생성기가 여러 개 존재하면 일일이 Player 게임 오브젝트를 여러 탄알 생서기의 target 변수로 드래그&드롭해야 한다.

  • 여기서는 일일이 드래그&드롭하는 대신 코드 상에서 Player 게임 오브젝트의 트랜스폼 컴포넌트를 찾아서 가져올 것이다. 우리는 Player 게임 오브젝트에 PlayerController 컴포넌트가 추가되어 있는 것을 안다.

  • FindObjectOfType( ) 메서드는 꺾쇠 <>에 어떤 타입을 명시하면 씬에 있는 모든 오브젝트를 검색해서 해당 타입의 오브젝트를 가져온다.

  • 우리는 FindObjectOfType<PlayerController>()를 사용하여 씬에서 PlayerController 컴포넌트를 찾았다.

  • 해당 컴포넌트를 가진 게임 오브젝트의 트랜스폼 컴포넌트를 transform으로 접근하여 target에 할당했다.

  • 위 코드는 아래의 두 줄 코드를 한 줄로 축약한 것이다.

PlayerController playerController = FindObjectOfType<PlayerController>();
target = playerController.transform;

FindObjectOfType( ) 메서드의 처리 비용

  • FindObjectOfType( ) 메서드는 씬에 존재하는 모든 오브젝트를 검색하여 원하는 타입의 오브젝트를 찾아낸다.
  • FindObjectOfType( ) 메서드는 처리 비용이 크기 때문에 Start( ) 메서드처럼 초기에 한두 번 실행되는 메서드에서만 사용해야 한다.
  • 만약 Update( ) 메서드에서 FindObjectOfType( )을 사용하면 프로그램이 심각하게 느려질 수 있다.

FindObjectOfType( )와 FindObjectsOfType( )

  • FindObjectOfType( )와 비슷한 이름을 가진 FindObjectsOfType( )도 있다. 전자는 해당 타입의 오브젝트를 하나만 찾는다. 후자는 해당 타입의 오브젝트를 모두 찾아 배열로 반환한다. 혼동하지 말자!

7.5.3 일정 주기로 실행 반복하기

  • Update( ) 메서드에서 탄알을 생성할 것이다. 그런데 Update( ) 메서드는 1초에 수십 번씩 실행된다. 무작정 Update( ) 메서드에 탄알 생성 코드를 넣으면 탄알이 1초에 수십 개씩 쉴 새 없이 생성된다.

  • 따라서 탄알을 생성하기 전에 마지막으로 탄알을 생성한 시점에서 누적된 시간을 저장하는 변수 timeAfterSpawn을 체크한다.

  • 위 그림처럼 timeAfterSpawn 값은 시간의 흐름에 맞춰 계속 증가한다.

  • 우리는 주기적으로 timeAfterSpawn을 체크해서 timeAfterSpawn이 탄알 생성 주기보다 커진 순간 탄알을 생성하고 timeAfterSpawn을 0으로 리셋한다.

  • 그러면 timeAfterSpawn 값이 0부터 다시 시작되어 증가한다. 이후에 다시 timeAfterSpawn 값이 탄알 생성 주기보다 커진 순간 탄알을 생성하고 timeAfterSpawn의 값을 0으로 리셋한다.

  • 이런 방식으로 탄알 생성을 일정 주기로 반복할 수 있다.

  • 이때 필요한 것이 Update( ) 메서드의 실행 시간 간격이다.


Update( ) 메서드의 실행 시간 간격

  • 마지막 Update( )가 실행된 시점과 현재 Update( )가 실행된 시점 사이의 시간 간격이 프레임이 새로 그려지는 데 걸리는 시간이다.

  • 만약 1초에 60프레임으로 화면이 갱신되면 직전의 Update( )가 실행되고 현재 Update( )가 실행되기까지의 시간 간격은 1/60초이다.

  • 이 실행 '시간 간격'을 Update( )가 실행될 때마다 어떤 변수에 누적하면 특정 시점에서 시간이 얼마만큼 흘렀는지 표현할 수 있다.

  • 예를 들어 게임에서 Update( )가 1초에 60번 실행된다고 하자. Update( )가 실행될 때마다 timeAfterSpawn에 1/60을 더한다고 하자.

  • 게임이 시작된 이후 0.5초가 지나면 Update( )는 총 30번 실행된다. 이때 timeAfterSpawn 값은 1/60을 30회 누적해서 더한 1/60 * 30 = 0.5가 된다.

  • 이런 식으로 특정 시점에서 시간이 얼마만큼 흘렀는지 알 수 있다.


7.5.4 Time.dletaTime

  • 직전 Update( ) 실행과 현재 Update( ) 실행 사이의 간격은 고정되어 있지 않다.

  • Update( ) 실행 사이의 시간 간격을 알기 위해 내장 변수 Time.deltaTime을 사용한다. Time.deltaTime에는 이전 프레임과 현재 프레임 사이의 시간 간격이 자동으로 할당된다.

  • 즉 1초에 60프레임의 속도로 화면을 갱신하는 컴퓨터에서 Time.deltaTime의 값은 1/60이다.

  • 따라서 Update( ) 메서드에서 어떤 변수에 Time.deltaTime 값을 계속 누적하면 특정 시점으로부터 시간이 얼마나 흘렀는지 표현할 수 있다.


7.5.5 Instantiate( ) 메서드

  • 탄알을 복제 생성하는데 Instantiate( ) 메서드를 사용할 것이다.

  • 유니티는 게임 도중에 실시간으로 오브젝트를 생성할 때 Instantiate( ) 메서드를 사용한다.

Instantiate(원본);
  • Instantiate( ) 메서드는 원본 오브젝트를 주면 해당 오브젝트를 복제한 오브젝트를 생성한다.

  • Instantiate는 '인스턴스화'로 번역한다.

  • 우리는 생성할 탄알의 원본이 될 Bullet 프리팹을 미리 만들어두었다. 그리고 나중에 Bullet 프리팹을 bulletPrefab 변수에 할당할 것이다.

  • 따라서 Instantiate( ) 메서드에 bulletPrefab을 입력하고, 실행하면 실시간으로 Bullet 프리팹을 복제한 새로운 Bullet 게임 오브젝트가 생성된다.

Instantiate(bulletPrefab);
  • 하지만 이런 식으로 Instantiate( ) 메서드를 사용하면 복제 생성된 게임 오브젝트의 위치와 회전이 임의로 결정된다.

  • 다행이 Instantiate( ) 메서드에 복제본을 생성할 위치와 회전을 지정할 수 있다.

Instantiate(원본, 위치, 회전);
  • Instantiate( ) 메서드에 (탄알의 원본, 탄알 생성기의 위치, 탄알 생성기의 회전)을 입력한다.

  • 탄알 생성기 자신의 위치와 회전은 다음 변수로 알 수 있다.

  • transform.position : 자신의 위치

  • transform.rotation : 자신의 회전

Instantiate(bulletPrefab, transform.position, transform.rotation);

7.5.6 Update( ) 메서드

  • 얼마만큼 시간이 흘렀는지 아는 방법, 주기적으로 처리를 반복하는 방법, 오브젝트의 복제본을 생성하는 방법을 알아보았다.

  • 위 방법들을 활용하여 Update( ) 메서드에서 다음과 같은 처리를 하여 주기적으로 탄알을 생성하는 처리를 구현한다.

  • 탄알을 생성한 마지막 시점에서 지금까지 시간이 얼마나 지났는지 측정
  • 탄알 생성 주기 이상의 시간이 흘렀다면 측정 시간을 리셋하고 탄알 생성
void Update()
    {
        timeAfterSpawn += Time.deltaTime;

        if (timeAfterSpawn >= spawnRate) 
        {
            timeAfterSpawn = 0f;

            GameObject bullet = Instantiate(bulletPrefab, transform.position, transform.rotation);

            bullet.transform.LookAt(target);

            spawnRate = Random.Range(spawnRateMin, spawnRateMax);
        }
    }
  • 현재 Update( ) 메서드의 동작
  • timeAfterSpawn에 Time.deltaTime을 계속 누적해서 더하기
  • timeAfterSpawn >= spawnRate면 timeAfterSpawn을 0으로 리셋하고 탄알 생성
  • 생성된 탄알이 target을 바라보도록 회전

주기적으로 반복하기

  • 게임이 시작되면 Start( ) 메서드가 timeAfterSpawn을 0으로 초기화 한다. 이후 매프레임마다 Update( ) 메서드가 실행되고 timeAfterSpawn에 Time.deltaTime이 누적된다.

  • timeAfterSpawn 값이 증가하다가 탄알 생성 주기 spawnRate 값 이상이 되면 if 문의 조건을 만족하고 if 문 안쪽의 코드가 실행된다.

  • if 문 안쪽에서 timeAfterSpawn은 다시 0으로 초기화되고 탄알이 생성된다.

  • 탄알이 생성될 때마다 매번 timeAfterSpawn을 0으로 리셋하기 때문에 timeAfterSpawn의 값은 정확하게 '마지막 탄알 생성 시점에서 지난 시간'이 된다.

  • timeAfterSpawn이 spawnRate 값보다 크거나 같을 때마다 탄알을 생성하므로 탄알 생성은 spawnRate마다 반복된다.


탄알 복제 생성

GameObject bullet = Instantiate(bulletPrefab, transform.position, transform.rotation);
  • Instantiate( ) 메서드는 복제본을 생성하고, 동시에 생성된 복제본을 메서드 출력으로 반환한다. 따라서 Instantiate( )로 복제 생성된 게임 오브젝트를 = 로 받아올 수 있다.

  • 위 코드는 bulletPrefab의 복제본을 transform.position 위치와 transform.rotation 회전에 생성하고, 생성된 복제본을 코드 상에서 수정할 수 있도록 bullet이라는 변수로 받아 챙긴 거다.

bullet.transform.LookAt(target);
  • bullet의 트랜스폼 컴포넌트의 LookAt( ) 메서드를 사용하여 복제 생성된 탄알이 target을 바라보도록 회전시켰다.

  • 트랜스폼의 LookAt( ) 메서드는 입력으로 다른 게임 오브젝트의 트랜스폼을 받는다. LookAt( ) 메서드는 입력받은 트랜스폼의 게임 오브젝트를 바라보도록 자신의 트랜스폼 회전을 변경한다.


다음 생성 시점 변경하기

spawnRate = Random.Range(spawnRateMin, spawnRateMax);
  • if 문 마지막에는 다음번 탄알 생성 시점을 랜덤하게 변경하는 처리를 구현했다.

  • 탄알이 생성될 때마다 위 코드에 의해 spawnRate 값이 spawnRateMin과 spawnRateMax 사이의 새로운 랜덤값으로 변경된다.

  • 탄알이 생성될 때마다 다음번 탄알 생성까지의 시간 간격이 랜덤하게 바뀌기 때문에 탄알이 다소 불규칙한 간격으로 생성된다.


7.5.7 완성된 탄알 생성기 스크립트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletSpawner : MonoBehaviour
{
    public GameObject bulletPrefab;
    public float spawnRateMin = 0.5f;
    public float spawnRateMax = 3f;

    private Transform target;
    private float spawnRate;
    private float timeAfterSpawn;

    void Start()
    {
        timeAfterSpawn = 0f;
        spawnRate = Random.Range(spawnRateMin, spawnRateMax);
        target = FindObjectOfType<PlayerController>().transform;
    }

    void Update()
    {
        timeAfterSpawn += Time.deltaTime;

        if (timeAfterSpawn >= spawnRate) 
        {
            timeAfterSpawn = 0f;

            GameObject bullet = Instantiate(bulletPrefab, transform.position, transform.rotation);

            bullet.transform.LookAt(target);

            spawnRate = Random.Range(spawnRateMin, spawnRateMax);
        }
    }
}
  • 코드를 제대로 작성했는지 확인하고 스크립트를 저장하고 유니티 에디터로 돌아가자.

7.5.8

  • BulletSpawner 스크립트를 Bullet Spawner 게임 오브젝트로 드래그&드롭

  • Bullet 프리팹을 인스펙터 창의 Bullet Prefab 필드로 드래그&드롭

  • 이제 플레이 버튼을 눌러 테스트를 해보자.


7.5.9 탄알 생성기 배치하기

  • 이제 완성된 탄알 생성기를 프리팹으로 만들고, 씬에 탄알 생성기를 여러 개 배치하자.

  • Bullet Spawner 프리팹을 하이어라키 창으로 세 번 드래그&드롭

  • 탄알 생성기들 위치 변경하기

  • 이제 네 개의 탄알 생성기를 모두 Level 게임 오브젝트의 자식으로 넣어 하이어라키 창을 깔끔하게 정리하자.

  • 플레이 버튼을 눌러 테스트


정리하기

  • 탄알과 탄알 생성기를 만들었다.

  • 유니티에서 충돌을 감지하는 방법을 배우고 탄알과 플레이어 사이의 충돌 감지를 구현했다.

  • Time.deltaTime으로 시간을 측정하는 방법, 프리팹을 만드는 방법, 게임 오브젝트의 복제본을 실시간으로 생성하는 방법을 배웠다.

  • 이들을 활용하여 탄알을 주기적으로 실시간으로 생성하는 처리를 구현했다.


다음 강의에서 계속~

profile
게임 개발을 목적으로 공부하고 있는 대학생입니다.

0개의 댓글