이번 포스팅에서는 적(Enemy) 캐릭터를 제거할 수 있는 무기를 Player에게 주도록 하겠습니다.
무기를 발사할 때 사용되는 총알을 오브젝트 풀링 기법을 사용하겠습니다.
포스팅은 유니티 2D 게임 개발(게임 개발 프로그래밍)에 나온 예제로 진행합니다.
오브젝트 풀링(Object Pooling)
오브젝트 풀링이란 오브젝트를 재사용하여 실행 중에 메모리의 할당, 해제 때문에 일어나는 성능 저하를 최소화하는 기법입니다.
씬에서 사용이 끝난 오브젝트를 제거하지 않고, 나중에 씬에서 다시 사용할 수 있게 비활성화 한뒤에 오브젝트 풀로 돌려보내고 사용을 해야 할 때 활성화를 시키는 것입니다.
1. 필요한 시점 전에 오브젝트 풀을 미리 인스턴스화 하고 비활성화 합니다.
2. 게임에 오브젝트가 필요할 때 비활성화했던 오브젝트를 인스턴스화 하지 않고 활성화합니다.
3. 사용이 끝난 오브젝트를 비활성화하여 풀로 돌려보냅니다.
오브젝트 풀링 기법을 사용하기 전에 오브젝트 풀링 기법을 적용할 오브젝트를 생성하겠습니다.
Assets > Sprite > Objects 폴더에 총알로 사용할 스프라이트를 임포트 하였습니다.
Inspector 창 > Ammo Import Settings은 사진과 같습니다.
Sprite Mode : Single
Pixels Per Unit : 32
Filter Mode : Point(no filter)
Compression : None
Apply 눌러 적용합니다. Sprite Editor을 열지 않아도 괜찮습니다.
1. Hierarchy 창 > Create Empty > AmmoObject를 생성합니다.
2. AmmoObject의 Ammo레이어를 만들고 추가합니다.
3. Sprite Renderer 컴포넌트를 추가하고 Sprite에 방금 임포트 한 Ammo를 설정합니다.
4. Sprite Renderer의 Sorting Layer를 Characters로 설정합니다.
5. Circle Collider 2D 컴포넌트를 추가하고 Edit Collider로 콜라이더의 범위를 설정합니다.
6. Circle Collider 2D 컴포넌트의 Is Tirgger 속성을 체크합니다.
Ammo의 콜라이더가 Enemy 콜라이더와 상호작용하고 다른 콜라이더와 상호작용하지 않게 만들기 위하여 레이어 충돌 매트릭스를 수정하겠습니다.
1. Edit > Project Settings > Physic 2D를 선택합니다.
2. 사진과 같이 Ammo와 Enemies레이어만 체크합니다.
Ammo 스크립트(C#)를 만들어서 AmmoObject에 적용하겠습니다.
Assets > Scripts > MonoBehaviour 폴더에 Ammo 스크립트(C#)를 생성하였습니다.
Ammo스크립트(C#)의 전체 코드입니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Ammo : MonoBehaviour
{
public int damageInflicted;
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision is BoxCollider2D)
{
Enemy enemy = collision.gameObject.GetComponent<Enemy>();
StartCoroutine(enemy.DamageCharacter(damageInflicted, 0.0f));
gameObject.SetActive(false);
}
}
}
코드를 확인하고 저장합니다.
public int damageInflicted;
int형식으로 damageInflicted는 적에게 줄 피해량을 나타냅니다.
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision is BoxCollider2D)
{
Enemy enemy = collision.gameObject.GetComponent<Enemy>();
StartCoroutine(enemy.DamageCharacter(damageInflicted, 0.0f));
gameObject.SetActive(false);
}
}
OnTriggerEnter2D()메서드의 Collider 2D는 써클콜라이더를 말합니다.
if문을 확인하겠습니다.
써클 콜라이더가 적 오브젝트의 BoxCollider2D와 충돌했는지 확인합니다.
enemy에 Enemy 스크립트 컴포넌트의 참조를 얻습니다.
StartCoroutine() 메서드로 enemy의 DamageCharacter() 메서드를 시작합니다
이때 damageInflicted를 첫 번째 인수로 전달하고 0.0f 간격을 두 번째 인수로 전달합니다.
AmmoObject가 적과 부딪쳤으므로 gameObject를 삭제하지 않고 비활성화합니다.
삭제하지 않고 비활성화하는 이유는 오브젝트 풀링 기법을 사용하려 하기 때문입니다.
방금 만든 Ammo스크립트(C#) 컴포넌트를 AmmoObject에 추가합니다.
Ammo 컴포넌트의 Damage Inflicted 속성을 1로 설정합니다.
AmmoObject를 프리 팹으로 만들고 Hierarchy창에서 삭제합니다.
AmmoObject를 실제로 움직이는 역할을 하는 Arc 스크립트를 만들겠습니다.
Assets > Scripts > MonoBehaviour 폴더에 Arc 스크립트(C#)를 생성하고 저장합니다.
Arc스크립트(C#)의 전체 코드입니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Arc : MonoBehaviour
{
public IEnumerator TravelArc(Vector3 destination, float duration)
{
var startPosition = transform.position;
var percentComplete = 0.0f;
while (percentComplete < 1.0f)
{
percentComplete += Time.deltaTime / duration;
transform.position = Vector3.Lerp(startPosition, destination, percentComplete);
yield return null;
}
gameObject.SetActive(false);
}
}
코드를 확인하고 저장합니다.
TravleArc() 메서드는 게임 오브젝트를 움직이는 메서드입니다.
여러 프레임에 걸쳐 실행해야 하므로 코루틴으로 만들 것입니다.
destination은 최종 위치, duration은 Arc스크립트를 추가한 게임 오브젝트 즉 Ammo오브젝트를 시작 위치에서 최종 위치로 옮길 때까지의 시간입니다.
"var"키워드를 사용했습니다. "var"키워드는 int, float처럼 명시적 키워드가 아닌 암시적 키워드이고, 메서드 안에서만 사용할 수 있습니다.
현재 게임 오브젝트의 transform.position을 startPosition에 대입합니다.
percentComplete는 Lerp 계산할 때 사용할 변수입니다.
while문을 보도록 하겠습니다.
percentComplete가 1.0보다 작은지 확인합니다. 1.0은 100%에 해당합니다.
AmmoObject가 매끄럽게 움직이는 것을 표현하려고 합니다.
프레임마다 총알을 이동할 거리는 총알을 움직이려는 시간과 이미 지난 시간에 따라 달라집니다.
percentComplete += Time.deltaTime / duration에서 Time.deltaTime은 지난 프레임을 그린 이후에 흐른 시간입니다.
지난 프레임 이후로 흐른 시간을 총알을 움직이려는 시간으로 나누면 현재 프레임에서의 진행률을 구할 수 있습니다.
지난 프레임 이후로 흐른 시간 : Time.deltaTime
총알을 움직이려는 시간 : duratiron
percentComplete는 이전까지의 진행률에 현재 프레임에서의 진행률을 더한 현재까지의 총 진행률입니다.
AmmoObject가 매끄럽게 움직이는 효과를 내려면 Lerp를 사용해야 합니다.
Lerp(선형보간)는 시작위치(startPosition), 종료위치(destination), 0~1사이의 백분률(percentComplete)가 필요합니다.
Lerp()메서드는 총 진행률을 시작 위치와 종료 위치 사이의 지점을 반환하고 그것을 transform.position에 대입합니다.
yield return null로 다음 프레임까지 코루틴의 실행을 일시 정지하고 목적지에 도달하면 게임 오브젝트를 비활성화합니다.
Arc스크립트(C#) 컴포넌트를 AmmoObject에 추가합니다.
AmmoObject가 날아가는 궤도를 포물선으로 만들고 싶다면 아래의 수정한 스크립트를 사용하면 됩니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Arc : MonoBehaviour
{
public IEnumerator TravelArc(Vector3 destination, float duration)
{
var startPosition = transform.position;
var percentComplete = 0.0f;
while (percentComplete < 1.0f)
{
percentComplete += Time.deltaTime / duration;
var currentHeight = Mathf.Sin(Mathf.PI * percentComplete);
transform.position = Vector3.Lerp(startPosition, destination, percentComplete) + Vector3.up * currentHeight;
// transform.position = Vector3.Lerp(startPosition, destination, percentComplete);
yield return null;
}
gameObject.SetActive(false);
}
}
다음은 Weapon 클래스를 만들어 안에 AmmoObject의 오브젝트 풀을 만들어서 저장하겠습니다.
Weapon 클래스는 무기의 역할을 담당하고 애니메이션을 제어하겠습니다.
Assets > Scritpts > MonoBehaiour 폴더에 Weapon 스크립트(C#)를 생성합니다.
PlayerObject 프리팹에 Weapon 스크립트(C#) 컴포넌트를 추가합니다.
Weapon 스크립트(C#)의 전체 코드입니다.
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
public GameObject ammoPrefab;
static List<GameObject> ammoPool;
public int poolSize;
public float weaponVelocity;
void Awake()
{
if (ammoPool == null)
{
ammoPool = new List<GameObject>();
}
for (int i = 0; i < poolSize; i++)
{
GameObject ammoObject = Instantiate(ammoPrefab);
ammoObject.SetActive(false);
ammoPool.Add(ammoObject);
}
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
FireAmmo();
}
}
GameObject SpawnAmmo(Vector3 location)
{
foreach (GameObject ammo in ammoPool)
{
if (ammo.activeSelf == false)
{
ammo.SetActive(true);
ammo.transform.position = location;
return ammo;
}
}
return null;
}
void OnDestroy()
{
ammoPool = null;
}
void FireAmmo()
{
Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
GameObject ammo = SpawnAmmo(transform.position);
if (ammo != null)
{
Arc arcScript = ammo.GetComponent<Arc>();
float travelDuration = 1.0f / weaponVelocity;
StartCoroutine(arcScript.TravelArc(mousePosition, travelDuration));
}
}
}
코드를 확인하고 저장합니다.
public GameObject ammoPrefab;
static List<GameObject> ammoPool;
public int poolSize;
public float weaponVelocity;
변수 부분을 확인해보도록 하겠습니다.
ammoPrefab 속성은 에디터에서 AmmoObject 프리팹의 복사본을 인스턴스화 할 때 사용합니다.
List 형식인 ammoPool 속성을 사용하여 오브젝트 풀을 구현하였습니다.
ammoPool을 static 키워드를 사용하여 메모리에 단 하나의 복사본만 존재하도록 만들었습니다.
List에 저장할 오브젝트 형식은 GameObject로 지정하여 GameObject형식만 저장할 것입니다.
poolSize는 public으로 지정하였고 오브젝트 풀에 넣을 수 있는 오브젝트 수를 에디터에서 설정하겠습니다.
weaponVelocity는 총알의 속력을 정할 때 사용하겠습니다.
Awake() 메서드입니다.
void Awake()
{
if (ammoPool == null)
{
ammoPool = new List<GameObject>();
}
for (int i = 0; i < poolSize; i++)
{
GameObject ammoObject = Instantiate(ammoPrefab);
ammoObject.SetActive(false);
ammoPool.Add(ammoObject);
}
}
Awake() 메서드는 스크립트를 로드할 때 딱 한번 불립니다.
if문을 확인하겠습니다.
ammoPool이 null인지 확인합니다. null이면 게임 오브젝트에 저장할 리스트 형식을 새롭게 만듭니다.
for문을 확인하겠습니다.
poolSize를 최대로 하는 루프입니다. 루프를 반복할 때 ammoPrefab의 복사본을 인스턴스 화해서 ammoObject에 대입하고 ammoObject를 비활성화시킨 후에 ammoPool에 추가합니다.
SpawnAmmo() 메서드입니다.
GameObject SpawnAmmo(Vector3 location)
{
foreach (GameObject ammo in ammoPool)
{
if (ammo.activeSelf == false)
{
ammo.SetActive(true);
ammo.transform.position = location;
return ammo;
}
}
return null;
}
SpawnAmmo() 메서드는 오브젝트 풀의 루프를 돌면서 비활성화한 AmmoObject를 찾고 활성화를 시키고 transform.position을 설정하여 반환합니다.
foreach문은 ammoPool의 범위에서 GameObject인 ammo를 루프 합니다.
if문을 확인해보겠습니다.
AmmoObject가 비활성화 상태인지 확인합니다.
비활성화 상태라면 활성화 상태로 변하고 location을 AmmoObject의 transform.position으로 설정하고 AmmoObject를 반환합니다.
ammoPool 범위 안에 오브젝트가 사용 중이라 비활성 오브젝트를 찾지 못했다면 null을 반환합니다.
OnDestroy() 메서드입니다.
void OnDestroy()
{
ammoPool = null;
}
OnDestroy() 메서드는 해당 스크립트를 추가한 게임 오브젝트를 제거할 때 불립니다.
ammoPool에 null을 설정하여 오브젝트 풀을 제거하고 메모리에서 해제합니다.
FireAmmo() 메서드입니다.
void FireAmmo()
{
Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
GameObject ammo = SpawnAmmo(transform.position);
if (ammo != null)
{
Arc arcScript = ammo.GetComponent<Arc>();
float travelDuration = 1.0f / weaponVelocity;
StartCoroutine(arcScript.TravelArc(mousePosition, travelDuration));
}
}
FireAmmo() 메서드를 확인하기 전에 화면 공간과 월드 공간에 대하여 이야기하겠습니다
화면공간 : 실제로 눈에 보이는 화면상의 공간을 말합니다. 예제 게임은 1280x720 픽셀입니다.
월드 공간 : 크기에 제한이 없는 실제 게임 세계를 말합니다.
Camera.main.ScreenToWorldPoint() 메서드는 화면 공간을 월드 공간으로 변환할 수 있는 메서드입니다.
마우스는 화면 공간을 사용합니다. 화면 공간의 마우스 위치를 월드 공간 위치로 변환합니다.
SpawnAmmo() 메서드를 통해 오브젝트 풀에서 활성화 한 AmmoObject를 얻고 이 Weapon 스크립트를 사용하는 오브젝트의 현재 transform.position을 AmmoObject의 시작 위치로 전달합니다.
if문을 확인해보겠습니다.
ammo != null 이 아닌지 확인합니다. SpawnAmmo() 메서드가 GameObject인 AmmoObject를 반환했는지 확인하는 것입니다.
acrScript에 AmmoObject의 Arc 컴포넌트의 참조를 얻어서 저장합니다.
1.0을 weaponVelocity의 값으로 나누면 AmmoObject의 총 이동 시간으로 사용할 값이 나옵니다.
이 식에 따르면 목적지가 멀수록 AmmoObject의 속도는 빨라집니다.
Arc 스크립트에서 만들었던 TravelArc 메서드를 호출합니다.
PlayerObject 프리팹을 설정하도록 하겠습니다.
Weapon 스크립트(C#) 컴포넌트를 사진과 같이 설정합니다.
Ammo Prefab : AmmoObject 프리팹
Pool Size : 7
Weapon Velocity : 2
설정합니다.
재생 버튼을 눌러 게임을 진행합니다. 적을 향하여 마우스 클릭하면 AmmoObject가 그 방향으로 이동하는지 확인하고 AmmoObject를 몇 대 맞은 후에 EnemyObject가 사라지는지 확인합니다.
마지막으로 Ctrl + S를 눌러 Scene을 저장합니다!
다음 포스팅에서는 AmmoObject를 발사할 때마다 Player의 애니메이션을 변경하고, 플레이어, 적이 공격받았을 때 깜박이는 효과를 추가하겠습니다.
감사합니다! :)
'유니티2D' 카테고리의 다른 글
#22 유니티 마지막이야기와 빌드 (4) | 2020.12.25 |
---|---|
#20 유니티 인공지는 적(Enemy) 만들기 (9) | 2020.12.23 |
#19 유니티 OnCollisionEnter2D , OnCollisionExit2D (3) | 2020.12.22 |
#18 유니티 코루틴(Coroutine), Character 보강 (7) | 2020.12.21 |
#17 유니티 카메라 매니저(플레이어 추적) (7) | 2020.12.19 |