전체 페이지뷰

2017년 7월 11일 화요일

Unity Tutorial: Space Shooter, part 5


Explosions




소행성을 볼트로 맞출 수 있게 되었습니다만 심심하기 짝이 없이 그냥 사라져 버릴 뿐입니다. 폭발 효과를 적용해 보겠습니다.

DestroyByContact 스크립트에서 소행성 폭발 오브젝트를 위한 레퍼런스를 첨가하고, Destroy() 메소드 앞에서 인스턴스화해 줍니다..

public class DestroyByContact : MonoBehaviour
{
    public GameObject explosion;
    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Boundary")
            return;
        Instantiate(explosion, transform.position, transform.rotation);
        Destroy(other.gameObject);
        Destroy(gameObject);
    }
}
cs

저장하고 유니티로 돌아갑니다.

Prefabs>VFX>Explosions 폴더 내에 이미 폭발 효과 오브젝트가 들어 있습니다. 드래그 하여 Explosion 프로퍼티로 끌어 놓습니다.


이제 플레이 해보면 볼트가 맞았을 때 터지는 모습을 볼 수 있습니다.



총이 아닌 비행선으로 직접 부딪혀도 같은 효과가 나타나면 재미가 떨어집니다. 플레이어가 부딪혔을 때 터지는 효과를 주기 위해 또 다른 레퍼런스를 스크립트에 도입합니다.

public GameObject playerExplosion;
cs

그리고, other의 태그가 Player일 때, 인스턴스화합니다.

public class DestroyByContact : MonoBehaviour
{
    public GameObject explosion;
    public GameObject playerExplosion;
    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Boundary")
            return;
        Instantiate(explosion, transform.position, transform.rotation);
        if (other.tag == "Player")
        {
            Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
        }
        Destroy(other.gameObject);
        Destroy(gameObject);
    }
}
cs

스크립트를 저장하고 유니티로 돌아가서, Player 오브젝트의 태그를 전과 마찬가지로 Player로 지정해 줍니다. 그리고, Asteroid를 다시 선택한 상태에서  VFX 폴더의 explosion_player를 드래그 해서 Player Explosion 프로퍼티에 연결해 줍니다.


씬을 저장하고 플레이해서 비행선을 부딪혀보면 대형 폭발이 일어나는 것을 볼 수 있습니다.

다음으로 할 일은 소행성이 플레이어 쪽으로 이동하는 것을 구현하는 것입니다. Asteroid를 선택하고, Scripts폴더에 있는 Mover 스크립트를 드래그 해서 인스펙터에 갖다 놓습니다.


이 스크립트는 볼트를 이동 시킬때 만들어진 스크립트이지만 속도 설정을 달리해주면 Asteroid에서도 사용 가능합니다. Speed 프로퍼티를 -5로 설정해 줍니다.

씬을 저장하고 플레이 해보면 소행성이 플레이어 쪽으로 돌진하는 것이 보입니다.

움직임까지 다 구현했으니 Asteroid가 완성되었습니다. Prefabs 폴더로 드래그하여 프리팹으로 만들고 Hierarchy에서는 지워줍시다.



Game Controller


비행선과 무기, 장애물이 구비되었습니다. 이제는 이것들을 한데 묶어 게임의 형태로 만들어야 하는데, 그러기 위해서는 게임을 시작하고, 소행성을 생성하고, 점수를 계산하고, 플레이어가 파괴되었을 때 게임을 종료하는 역할을 수행하는 게임 콘트롤러가 필요합니다.

우리의 로직을 담아줄 빈 게임 오브젝트를 Game Controller라는 이름으로 생성하고 Reset합니다. 이 오브젝트는 게임상에 나타나는 것이 아니라서 콜라이더나 렌더링도 필요치 않으므로 Reset하는 것이 필수적인 과정은 아닙니다.

Tag 부분을 드랍다운 해보면 이미 GameController라는 태그가 있습니다. 그것을 선택해서 태그를 지정합니다.

그리고 Add Component>New Script하여 우리의 로직을 담을 스크립트를 GameController라는 이름으로 생성합니다. 늘 하던 대로 루트 폴더에 생성된 파일을 Scripts 폴더로 옮겨두고 에디터를 엽니다.

GameController는 몇 가지 역할이 있습니다. 그 중 가장 주된 것은 장애물 소행성을 스폰하는 것입니다. 따라서 hazard라는 이름으로 레퍼런스를 선언합니다. 그리고 이 해저드가 연속해서 스포닝 되어 플레이어 쪽으로 밀려가야 하므로 그것을 담당할 메소드를 SpawnWaves라는 이름으로 작성합니다. 이 메소드는 게임 시작과 더불어 호출되어야 하므로 Start()내에서 호출합니다.

지금까지의 코드 얼개는 아래와 같습니다.
public class GameController : MonoBehaviour
{
    public GameObject hazard;
    private void Start()
    {
        SpawnWaves();
    }
    void SpawnWaves()
    {
    }
}
cs

그럼 이제 SpawnWaveshazard를 인스턴스화하면 됩니다.
Instantiate는 이미 살펴보았듯이 세개의 인수를 가집니다. Object와 Vector3 position, Quaternion rotation입니다.

public class GameController : MonoBehaviour
{
    public GameObject hazard;
    private void Start()
    {
        SpawnWaves();
    }
    void SpawnWaves()
    {
        Vector3 spawnPosition = new Vector3();
        Quaternion spawnRotation = new Quaternion();
        Instantiate(hazard, spawnPosition, spawnRotation);
    }
}
cs

이것이 지금까지 작성한 기본 구조입니다. 일단 여기까지 작성하고 스크립트를 저장한 뒤 유니티로 돌아갑니다.

Hierarchy 창에서 Game Controller를 선택한 채로 인스펙터 창을 보면 Hazard라는 프로퍼티가 생성되어 있습니다. 여기에 프리팹의 Asteroid를 연결해 줍니다.



spawnPosition은 어떻게 지정해야 할까요? 다시 스크립트로 돌아갑니다. 스크립트 상에서 spawnValues라는 Vector3타입의 변수를 하나 더 선언합니다. 바로 spawnPosition을 선언하는 것이 아니라 spawnValues를 선언하는 이유는 나중에 설명하겠습니다.

유니티로 돌아가 봅시다. Spawn Values라는 프로퍼티를 설정해야 합니다. 시각적 이해를 위해 임시로 씬에 Asteroid 프리팹을 하나 추가해서 보겠습니다.


소행성은 비행선과 같은 레벨에서 움직이므로 Y축은 변화가 없이 XZ 평면에서만 움직입니다. 그러므로 Spawn Values의 Y는 0으로 둡니다. 또 소행성은 최상단에서 생성되어야 하므로 움직여보면 16정도에서 화면 상단으로 사라집니다. 따라서 Spawn Values의 Z는 16으로 설정합니다. 문제는 X입니다. X는 -6에서 6 사이의 값에서 새로 생성될 때마다 랜덤값으로 설정되어야 합니다. 이것이 바로 spawnPosition을 선언하지 않고 spawnValues로 우회해 선언한 이유입니다.

스크립트로 돌아가서 SpawnWaves() 내에 선언된 spawnPosition을 수정합니다.

public class GameController : MonoBehaviour
{
    public GameObject hazard;
    public Vector3 spawnValues;
    private void Start()
    {
        SpawnWaves();
    }
    void SpawnWaves()
    {
        Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x,spawnValues.x),
            spawnValues.y,spawnValues.z);
        Quaternion spawnRotation = new Quaternion();
        Instantiate(hazard, spawnPosition, spawnRotation);
    }
}
cs

Y,Z 값은 그대로 사용하고 X는 Random클래스의 Range() 메소드를 사용해서 임의로 생성되도록 하였습니다. X=6 이라고 두면 -6~6사이의 랜덤값이 생성됩니다.

저장하고 유니티로 돌아가서 Spawn Values의 X를 6으로 설정합니다.

자, 그럼 마지막으로 spawnRotation은 어떻게 할까요? 이것은 Quaternion 타입입니다.
좀 복잡하긴 하지만 API를 찾아보면 회전을 전혀 없이 생성시키려면 Identity라는 프로퍼티를 사용하면 됩니다.

public class GameController : MonoBehaviour
{
    public GameObject hazard;
    public Vector3 spawnValues;
    private void Start()
    {
        SpawnWaves();
    }
    void SpawnWaves()
    {
        Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x,spawnValues.x),
            spawnValues.y,spawnValues.z);
        Quaternion spawnRotation = Quaternion.identity;
        Instantiate(hazard, spawnPosition, spawnRotation);
    }
}
cs

저장 후 유니티로 돌아갑니다. 임시로 씬에 올려두었던 Asteroid 인스턴스는 제거하고 실행해보면 임의의 위치에서 소행성이 하나 떨어지는 것을 볼 수 있습니다.


Spawning waves


소행성이 하나 떨어지도록 했습니다. 이제는 연달아서 떨어지도록 해야합니다. 소행성 개수를 카운트할 hazardCount라는 int 타입 변수를 선언하고, SpawnWaves()for 루프를 도입합니다.

public class GameController : MonoBehaviour
{
    public GameObject hazard;
    public Vector3 spawnValues;
    public int hazardCount;
    private void Start()
    {
        SpawnWaves();
    }
    void SpawnWaves()
    {
        for (int count = 0; count < hazardCount; count++)
        {
            Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x, spawnValues.x),
            spawnValues.y, spawnValues.z);
            Quaternion spawnRotation = Quaternion.identity;
            Instantiate(hazard, spawnPosition, spawnRotation);
        }        
    }
}
cs

저장하고 다시 유니티로 돌아가 봅시다. 생성된 Hazard Count 프로퍼티를 10으로 설정하고 실행해보면, 거의 동시에 소행성 여러개가 떨어집니다. 하나씩 떨어지도록 하려면 시간 간격을 두어야 할 것 같습니다.

스크립트로 돌아가서 시간 간격을 위한 float 타입 spawnWait 변수를 선언합니다. 그리고 Instantiate() 메소드 뒤에 WaitForSeconds(spawnWait)을 사용해야 하는데, 코드 전체가 기다리게 하지 않으려면 코루틴을 사용해야 합니다.

코루틴은 좀 복잡합니다. 일단 void형으로 되어 있는 SpawnWaves()IEnumerator 타입으로 바꿔야합니다. 코루틴인 SpawnWaves()는 바로 시작할 수가 없어서 StartCoroutine()으로 시작해야 합니다.

하는 김에 게임 시작 전 준비할 시간을 갖도록 startWait라는 시작 대기 시간도 선언하고 for 루틴 앞에서 기다리게 합니다.
public class GameController : MonoBehaviour
{
    public GameObject hazard;
    public Vector3 spawnValues;
    public int hazardCount;
    public float spawnWait;
    public float startWait;
 
    private void Start()
    {
        StartCoroutine(SpawnWaves());
    }
 
    IEnumerator SpawnWaves()
    {
        yield return new WaitForSeconds(startWait);
        for (int count = 0; count < hazardCount; count++)
        {
            Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x, spawnValues.x),
            spawnValues.y, spawnValues.z);
            Quaternion spawnRotation = Quaternion.identity;
            Instantiate(hazard, spawnPosition, spawnRotation);
            yield return new WaitForSeconds(spawnWait);
        }        
    }
}
cs

스크립트를 저장하고 유니티로 돌아가 프로퍼티를 설정합니다. 1초에 두개 떨어지도록 Spawn Wait는 0.5, Start Wait는 1로 설정합니다.

씬을 저장하고 플레이 해보면,

소행성들이 시간차를 두고 떨어져 옵니다. 그런데 10개의 소행성이 나오고 나면 플레이어가 할 것이  없습니다. 물론 소행성 수를 50,100,100 이런 식으로 크게 잡으면 되겠습니다만 좀 단조로울 수 있습니다.

스크립트로 돌아갑시다.

소행성이 연속해서 몰려오게 하기 위해 for 루프를 while 루프로 다시 래핑하겠습니다. 조건으로 true를 주어 무한 루프를 만들고, 소행성의 웨이브와 웨이브 사이에 시간차를 두기 위해 waveWait라는 변수를 또 하나 도입합니다.

public class GameController : MonoBehaviour
{
    public GameObject hazard;
    public Vector3 spawnValues;
    public int hazardCount;
    public float spawnWait;
    public float startWait;
    public float waveWait;
 
    private void Start()
    {
        StartCoroutine(SpawnWaves());
    }
 
    IEnumerator SpawnWaves()
    {
        yield return new WaitForSeconds(startWait);
        while (true)
        {
            for (int count = 0; count < hazardCount; count++)
            {
                Vector3 spawnPosition = new Vector3(Random.Range(-spawnValues.x, spawnValues.x),
                spawnValues.y, spawnValues.z);
                Quaternion spawnRotation = Quaternion.identity;
                Instantiate(hazard, spawnPosition, spawnRotation);
                yield return new WaitForSeconds(spawnWait);
            }
            yield return new WaitForSeconds(waveWait);
        }        
    }
}
cs

스크립트를 저장하고 다시 유니티로 돌아가서, Wave Wait 프로퍼티를 4로 설정합니다. 플레이 해보면 이제서야 진짜 게임처럼 보이기 시작합니다.

그런데 문제가 있습니다. 게임 중에 Hierarchy 창을 보니, explosion 오브젝트가 한 가득 생성된 채로 누적되고 있습니다.


이 객체는 DestroyByContact에 의해 파괴되지 않으므로 다른 방도를 강구해야 합니다. 이번에는 프로젝트 창의 Scripts폴더를 선택하고, Create>C# Script를 선택하여 그 이름을 DestroyByTime이라고 짓습니다.



에디터로 열어서 코드를 작성합니다.
public class DestroyByTime : MonoBehaviour
{
    public float lifetime;
 
    private void Start()
    {
        Destroy(gameObject, lifetime);
    }
}
cs

아주 간단한 코드이고 이해하기 어렵지 않습니다. 저장 후 유니티로 돌아갑니다.

프로젝트 창의 Prefabs>VFX>Explosions>explosion_asteroid를 선택하고, 인스펙터 창에서 Add Component>Scripts를 선택합니다.


그리고 Destroy By Time을 추가하고, Lifetime 프로퍼티는 2초로 설정합니다. 나머지 두 개의 explosion에도 이 스크립트를 추가합니다. 이번엔 두개를 동시에 선택해서 Add Component>Scripts>Destroy By Time해서 추가합니다.

씬을 저장하고 플레이 해보면 explosion 클론 오브젝트들이 생성되었다가 2초 후 사라지는 것을 확인할 수 있습니다.

댓글 없음:

댓글 쓰기