학습 매체 : 책
책이름 : 레트로의 유니티 게임 프로그래밍 에센스
저자 : 이제민
본 내용은 해당 강의 내용을 공부하면서 정리한 글입니다.
생성할 탄알의 원본
탄알을 발사하여 맞출 대상
탄알을 생성하는 시간 간격
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 : 마지막 탄알 생성 시점부터 흐른 시간을 표시하는 '타이머'
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( ) 메서드는 float 입력받을 때와 int를 입력받을 때의 동작이 다르다.
- Random.Range(0, 3) : 0, 1, 2 중 하나가 int 값으로 출력
- Random.Range(0f, 3.0f) : 0f부터 3f 사이의 float 값이 출력 (예 : 0.5f)
target = FindObjectOfType<PlayerController>().transform;
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( )도 있다. 전자는 해당 타입의 오브젝트를 하나만 찾는다. 후자는 해당 타입의 오브젝트를 모두 찾아 배열로 반환한다. 혼동하지 말자!
Update( ) 메서드에서 탄알을 생성할 것이다. 그런데 Update( ) 메서드는 1초에 수십 번씩 실행된다. 무작정 Update( ) 메서드에 탄알 생성 코드를 넣으면 탄알이 1초에 수십 개씩 쉴 새 없이 생성된다.
따라서 탄알을 생성하기 전에 마지막으로 탄알을 생성한 시점에서 누적된 시간을 저장하는 변수 timeAfterSpawn을 체크한다.
위 그림처럼 timeAfterSpawn 값은 시간의 흐름에 맞춰 계속 증가한다.
우리는 주기적으로 timeAfterSpawn을 체크해서 timeAfterSpawn이 탄알 생성 주기보다 커진 순간 탄알을 생성하고 timeAfterSpawn을 0으로 리셋한다.
그러면 timeAfterSpawn 값이 0부터 다시 시작되어 증가한다. 이후에 다시 timeAfterSpawn 값이 탄알 생성 주기보다 커진 순간 탄알을 생성하고 timeAfterSpawn의 값을 0으로 리셋한다.
이런 방식으로 탄알 생성을 일정 주기로 반복할 수 있다.
이때 필요한 것이 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가 된다.
이런 식으로 특정 시점에서 시간이 얼마만큼 흘렀는지 알 수 있다.
직전 Update( ) 실행과 현재 Update( ) 실행 사이의 간격은 고정되어 있지 않다.
Update( ) 실행 사이의 시간 간격을 알기 위해 내장 변수 Time.deltaTime을 사용한다. Time.deltaTime에는 이전 프레임과 현재 프레임 사이의 시간 간격이 자동으로 할당된다.
즉 1초에 60프레임의 속도로 화면을 갱신하는 컴퓨터에서 Time.deltaTime의 값은 1/60이다.
따라서 Update( ) 메서드에서 어떤 변수에 Time.deltaTime 값을 계속 누적하면 특정 시점으로부터 시간이 얼마나 흘렀는지 표현할 수 있다.
탄알을 복제 생성하는데 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);
얼마만큼 시간이 흘렀는지 아는 방법, 주기적으로 처리를 반복하는 방법, 오브젝트의 복제본을 생성하는 방법을 알아보았다.
위 방법들을 활용하여 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);
}
}
- 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 사이의 새로운 랜덤값으로 변경된다.
탄알이 생성될 때마다 다음번 탄알 생성까지의 시간 간격이 랜덤하게 바뀌기 때문에 탄알이 다소 불규칙한 간격으로 생성된다.
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);
}
}
}
Bullet 프리팹을 인스펙터 창의 Bullet Prefab 필드로 드래그&드롭
이제 플레이 버튼을 눌러 테스트를 해보자.
탄알 생성기들 위치 변경하기
이제 네 개의 탄알 생성기를 모두 Level 게임 오브젝트의 자식으로 넣어 하이어라키 창을 깔끔하게 정리하자.
탄알과 탄알 생성기를 만들었다.
유니티에서 충돌을 감지하는 방법을 배우고 탄알과 플레이어 사이의 충돌 감지를 구현했다.
Time.deltaTime으로 시간을 측정하는 방법, 프리팹을 만드는 방법, 게임 오브젝트의 복제본을 실시간으로 생성하는 방법을 배웠다.
이들을 활용하여 탄알을 주기적으로 실시간으로 생성하는 처리를 구현했다.
다음 강의에서 계속~