09
02
728x90

이번에는 하늘에서 총알이 떨어지는데, 랜덤한 위치에서 총알이 떨어지도록 만들 것이다.

만들어야 할 것은 총알 오브젝트 한 개, 총알 스크립트 한 개, 총알 풀 오브젝트 한 개, 게임매니저 오브젝트 한 개, 바닥 오브젝트 한 개이다.

 

총알 스크립트는 총알 오브젝트가 활성화 되었을 때의 세팅, 아래로 떨어지기, 플레이어와 바닥 충돌 감지를 할 것이고,

게임매니저 스크립트는 총알 오브젝트를 생성하고 스폰하는 역할을 하게 될 것이다.


필요한 오브젝트 생성

먼저 하이어라키에서 동그라미 스프라이트 1개와 빈 오브젝트 3개를 만들어 준다.

동그라미는 Bullet1이라는 이름을 주고, 빈 오브젝트는 GameManager, Floor, Bullet Pool이름을 준다.

 

다 만들면 하이어라키는 이렇게 된다.

 

스크립트에 Bullet도 만들어 준다.

게임매니저에 들어갈 스크립트는 전에 만들어둔 스크립트를 사용할 것이다.

 

위는 Player의 Insprctor인데, Add Component를 눌러서 Circle Collider 2D와 Rigidbody 2D를 만들어 준다.

 

스크립트에서 충돌(Collision)을 감지하고 이에 따라서 플레이어의 체력을 까고, 총알을 사라지게 만드는 기능을 해야하기에 두 컴포넌트를 넣었다.

 

Circle Collider 2D는 만들면 오브젝트의 크기대로 맞춰져서 자동으로 생길 것이고, Is Trigger만 체크 해 준다.

Is Trigger를 체크하지 않으면 진짜 충돌이 일어나는데, 우리는 벽을 통과 못하게 하는 충돌효과같은 것을 원하는 것이 아닌, 닿았을 때의 이벤트만 체크하면 되므로 체크를 해 주는 것이다.

 

Rigidbody 2D는 물리적 효과를 담당하는데, 여기서는 물리효과 보다는 충돌의 처리를 하기 때문에 Body Type을 Dynamic이 아닌 Kinematic으로 설정 해 준다.

Dynamic을 주면 중력값이 있어서 게임을 실행하면 아래로 쭉 떨어져 버린다.

 

충돌 효과를 체크하기 위해서는 충돌하는 물체와 충돌 당하는 물체 둘 중 하나는 Rigidbody를 가지고 있어야 한다.

 

위는 Bullet의 Inspector로, Transform의 Scale은 0.5, 0.5, 1로 둔 다음 Circle Collider 2D를 만들어 Is Trigger를 체크 해 준다.

 

위는 Floor의 인스펙터로, 먼저 태그를 Floor로 주고, Transform의 Position은 0, -5.5, 0

Rigidbody 2D는 Kinematic

Box Bollider 2D는 IsTrigger 체크하고 Size는 6, 1로 한다.

 


스크립트

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

public class Bullet : MonoBehaviour
{
    public float moveSpeed = 2.0f;

    // 오브젝트 활성화 직후 해당 함수 호출
    void OnEnable() {
        float randomX = Random.Range(-2.8f, 2.8f);
        transform.position = new Vector3(randomX, 5.5f, 0.0f);
    }

    // Update is called once per frame
    void Update()
    {
        transform.position += Vector3.down * moveSpeed * Time.deltaTime;
    }

    private void OnTriggerEnter2D(Collider2D collision) {
        if(collision.tag == "Player") {
            Debug.Log("Hit");
            gameObject.SetActive(false);        // 맞았으니 끄기
        }
        else if(collision.tag == "Floor") {
        	gameObject.SetActive(false);
        }
    }
}

위는 Bullet.cs의 코드이다.

OnEnable()은 오브젝트가 활성화 되었을 때 호출되는 함수로, 쉽게 말하면

인스펙터에서 위 체크박스를 해제하면 비활성화, 체크하면 활성화 인 것이다.

 

그럼 어차피 처음 시작할 때 호출되는 Start()를 사용하면 되는거 아닌가요? 할 수 있는데,

해당 방식을 사용한 이유는 GameManager까지 모두 만든 뒤에 설명하겠다.

OnEnable()함수에서는 -2.8 ~ 2.8 사이의 랜덤 값을 받아 총알의 위치를 설정해 주는 역할을 한다.

 

다음으로 Update()는 단순히 현재 위치에 Vector3.down(y값이 -1)과 스피드값으로 아래로 떨어지는 역할을 하게 된다.

 

그 아래가 처음 보는건데, OnTriggerEnter2D(Collider2D collision)은 위에서 설명했던 충돌과 관련된 것이다.

해당 스크립트를 가진 오브젝트가 다른 어떤 오브젝트와 충돌을 하게 되면 해당 함수가 실행된다.

매개변수로는 충돌된 오브젝트의 Collider2D타입이 들어오게 되고, 해당 타입을 사용해 tag나 name같은 것을 참조해서 어떤 물체와 충돌했는지 알아낼 수 있다.

해당 코드는 바로 이전 강의에서 만들었던 Player태그를 사용해서 체크를 한다.

즉, 이 함수는 어떤 오브젝트와 충돌을 하면 호출되고, 그 오브젝트의 태그가 Player라면 콘솔창에 "Hit"을 뿌린 다음 자기 자신을 비활성화 한다.

Floor에 닿아도 비활성화 한다.

 

해당 코드를 모두 작성한 다음 Bullet1에 집어넣는다.

 

다음으로는 프리팹이라는 것을 만들 것이다.

Assets 폴더 안에 Prefabs 폴더를 만들고, 아까 만들었던 Bullet1을 하이어라키 창에 드래그 앤 드랍으로 집어넣으면 위 사진처럼 파란 박스 아이콘으로 Bullet1이 복사된다.

 

이 파란 박스가 Prefab이라는 것이다.

총알이 단 한개만 떨어지는게 아니라 반복해서 여러개를 떨어뜨릴건데, 그 오브젝트를 하나하나 만들어서 떨구느니, 저렇게 미리 만든 프리팹을 사용해서 그 프리팹을 코드로 생성해 설정하는게 낫기에 미리 만들어두는 것이다. 단순히 복붙으로 오브젝트를 만드는 것보다 효율적이라는 것이다.

또한 여러 씬에서 해당 오브젝트를 재사용하고 싶을 때도 이렇게 만들어두면 세팅 값이 그대로 가기 때문에 유용하다.

 

프리팹을 만들었으면 하이어라키상의 Bullet1은 지워버려도 된다.

 

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    public GameObject bullet1Prefab;        // 프리팹
    public int bullet1Amount = 10;          // 만들 총알1 갯수

    private GameObject []bullet1;           // 총알1 담을 GameObject
    private int index_Bullet1 = 0;          // 총알1 인덱스
    private float spawnDelayTime = 2.0f;    // 총알 스폰 딜레이
    private bool isSpawnDelay = false;      // 딜레이 체킹

    // Start is called before the first frame update
    void Start()
    {
        CreateBullet();
    }

    // Update is called once per frame
    void Update()
    {
        SpawnBullet();
    }

    void CreateBullet() {
        bullet1 = new GameObject[bullet1Amount];    // 총알1 담을 GameObject

        // 미리 만듦
        for(int i = 0; i < bullet1Amount; i++) {
            bullet1[i] = Instantiate(bullet1Prefab);    // 프리팹 생성 후
            bullet1[i].transform.parent = GameObject.Find("Bullet Pool").transform; // 부모 옮기고
            bullet1[i].SetActive(false);    // 끄기
        }
    }

    void SpawnBullet() {
        if(!isSpawnDelay) {
            isSpawnDelay = true;
            bullet1[index_Bullet1].SetActive(true); // 스폰이니 켜고
            index_Bullet1++;    // 인덱스 올리고

            index_Bullet1 %= bullet1Amount; // 최대 인덱스 벗어나지 않게 조절
            
            StartCoroutine(Coroutine_SpawnBullet());
        }
    }

    IEnumerator Coroutine_SpawnBullet() {
        yield return new WaitForSeconds(spawnDelayTime);
        isSpawnDelay = false;
    }


    public void onBtnClicked()
    {
        SceneManager.LoadScene("SampleScene");
    }
}

위 코드는 전에 씬 전환 챕터에서 만들었던 GameManager.cs고, 총알 생성과 스폰코드를 추가했다.

 

코드를 간단히 살펴보면

public타입으로 Bullet1 프리팹을 받아올 GameObject타입 클래스 하나와, Bullet1을 몇 개 만들지에 대한 int타입 변수 하나를 만들어준다. 이는 오브젝트 풀링 기법을 위한 것이다.

 

오브젝트 풀링?, Bullet.cs에서 OnEnable()을 사용하지 않은 이유

더보기

게임을 하다보면 씬을 넘어갈 때, 맵을 넘어갈 때 로딩창을 본 적이 있을텐데, 이 때는 말 그대로 로딩을 하는 과정에 대기시간을 표시한 것으로, 플레이어가 보거나 사용할 오브젝트나 배경등을 미리 로드해 놓는 것이다.

이렇게 하는 이유는, 필요할 때마다 하나하나 만들어서 사용하는 것보다, 미리 필요할 것 같은걸 만들어놓고 필요한 순간에 활성화시키는 것이 훨씬 플레이에 수월하기 때문이다. 오브젝트를 생성(Instantiate)하고 파괴(Destroy)하는 것은 메모리를 새로 할당하는 등의 일이 수반되기 때문에 성능저하, 프레임 드랍이 생길 수 있다.

 

우리도 총알을 시간이 되면 하나씩 만들고 위치 설정하고 떨구고 플레이어나 바닥에 닿으면 총알 없애고 다시 만들고를 반복하느니 미리 여러 개를 만들고 비활성화 시킨 다음 필요할 때마다 활성화시켜 떨구고 없어질 때는 비활성화만 하면 오브젝트를 만들 때 필요한 시간을 줄일 수 있다.

 

Bullet.cs에서 Start()를 사용하지 않고 OnEnable()을 사용한 이유도 그것이다.

오브젝트를 계속해서 만들게 아니라 미리 여러개 만든 다음 필요할 때 활성화, 비활성화만 할 것이기 때문이다.

위처럼 미리 여러개 만들어서 보관하는 공간을 Pool이라고 하며, 이렇게 풀을 사용하는 기법을 오브젝트 풀링(Object Pooling) 기법이라고 한다.

원래는 미리 풀에 만든 다음 필요시 꺼내쓰고 다 쓰면 다시 집어넣는 과정을 반복하는 것이지만 꺼내고 넣는것을 이번에는 하지 않겠다.

 

Start()에서는 CreateBullet()을 호출한다.

말 그대로 총알을 생성하는 함수다.

bullet1배열을 메모리에 할당하고, bullet1Amount 갯수만큼 총알을 만들고 Bullet Pool이라는 오브젝트로 만든 총알을 옮긴 다음 비활성화 시킨다.

 

SpawnBullet()에서는 코루틴이라는 것이 등장한다.

코루틴은 생명주기상의 Update()처럼 반복해서 호출되는 함수로가 생각하면 되지만 Update처럼 프레임 단위 호출이 아닌 spawnDelayTime이 돌 때마다 호출된다.

쉽게 생각하면 쿨타임 같은거라고 보면 된다.

 

먼저 isSpawnDelay로 아직 딜레이타임인지 스폰해도 되는지 체크한다.

bullet1배열에서 알맞은 인덱스의 총알을 활성화 하고(인덱스로 체크하지 않고 Pool 내의 비활성화 된 오브젝트를 활성화시키는 것이 낫다. 여기서는 임시로 이렇게 구현한다.) 인덱스를 올리고 계산한다.

이후 코루틴을 호출한다.

 

그 아래 IEnumerator Coroutine_SpawnBullet()이 정의되어 있는데, 이름은 아무거나 해도 된다.

spawnDelayTime만큼 기다린 후에 isSpawnDelay를 설정해 다시 총알을 스폰(활성화)하도록 한다.

 


실행

실행시켜보면 BulletPool에 프리팹으로 10개가 생성되고 활성화, 비활성화를 반복한다.

활성화된 총알이 하늘에서 떨어지고, 캐릭터에 맞거나 바닥에 닿으면 비활성화 되며 캐릭터에 맞으면 Hit이 출력된다.

이제 Hit을 출력하는 대신 캐릭터의 생명이 깎이는 것으로 만들면 된다.

728x90
COMMENT