종스크롤 2D 슈팅 게임 만들기(2)
이제 Enemy를 만든다. Enemy의 이미지와 애니메이션은 Player와 같이 만들어준다. 그리고 Enemy 스크립트를 만든다. Enemy 스크립트에는 플레이어 방해, 이동, 데미지를 받는 기능, 리워드 기능이 들어간다. 먼저 Enemy는 움직이고 데미지를 받기 때문에 IMovement와 IDamaged 인터페이스를 상속받는다. 그리고 프러퍼티로 bool 값을 반환하는 IsDead 함수를 선언한다. 이 함수는 Enemy의 현재 체력 curHP가 0보다 작거나 같을때 true를 반환해서 Enemy 캐릭터가 사망했음을 알 수 있다. 그리고 IDamaged의 TakeDamaged 함수를 구현한다.
public void TakeDamage(GameObject attacker, int damage)
{
if (!IsDead)
{
curHP -= damage;
if (curHP < 1)
{
OnDie();
}
else
{
OnDamaged();
}
}
}
IsDead 함수에서 bool 값을 받아서 Enemy 캐릭터가 살아있다면 데미지를 받고 현재 HP가 1보다 작아서 죽었다면 사망을 처리하는 OnDie 함수를 그렇지 않다면 몬스터의 피격을 처리하는 OnDamaged를 호출한다. 사망 시 처리에는 아이템을 떨어뜨리거나 점수, Enemy 캐릭터 오브젝트 삭제 등이 있고 데미지를 받았을 때 처리에는 깜빡거리는 이펙트 등이 있다. Enemy의 생성은 EnemySpawnManager 스크립트로 다룬다. EnemySpawnManager에서는 먼저 생성해야할 Enemy prefab들을 지정한다. 그리고 Enemy가 생성될 위치를 지정받는다. 스폰 포인트는 EnemySpawnManager 오브젝트의 자식 오브젝트로 설정한다.

위 사진은 Enemy 생성 위치 spawntrans와 생성될 Enemy의 prefab을 지정한 Inspector 창이다. 이번 예제에서는 레벨에 따라서 생성되는 Enemy를 다르게 한다. 그래서 3개의 Enemy prefab을 지정해준다. 그리고 SpawnManager를 초기화해주는 InitSpawnManager 함수, Enemy를 생성하는 코루틴을 멈추는 StopSpawnManager 함수, 그리고 다음 레벨로 넘어가는 NextLevel 함수를 구현한다. 여기서 델리게이트(delegate)가 사용된다. 델리게이트는 메소드들이 참조할 수 있는 하나의 타입이다. 예를 들어 몬스터가 죽었을때 여러 클래스에서 메소드들이 처리를 다르게 해야할 때 사용할 수 있다. 몬스터가 죽었을때 아이템을 드랍하게 하거나 점수를 올리거나 할때 몬스터가 죽었을 때를 아이템 드랍 메소드와 점수를 올리는 메소드가 그 상황을 참조해서 그에 맞는 메소드를 호출하는 것이다. 델리게이트를 사용하면 프로젝트의 확장성을 보장할 수 있다. Enemy 스크립트에서 델리게이트로 Enemy가 죽었을 때의 상황을 델리게이트로 선언해준다.
// delegate의 타입 선언
public delegate void MonsterDiedEvent(Enemy enemyInfo);
// delegate의 체인 선언
public static event MonsterDiedEvent OnMonsterDied;
위와 같은 코드들로 OnMonsterDied라는 MosterDiedEvent 타입의 델리게이트가 선언되었다. 그리고 OnDie 함수에서 OnMosterDied?.Invoke(this)로 델리게이트를 활성화한다. 그리고 += 연산자로 다른 함수에서 이 이벤트에 대해서 처리한다(-=로 해제한다). 그리고 점수를 담당하는ScoreManager 스크립트를 만들어서 OnMonsterDied 델리게이트를 참조한다. 그리고 스크립트가 활성화 될때마다 호출되는 유니티 함수 OnEnable에서 Enemy.OnMonsterDied += HandleMonsterDied(델리게이트를 참조하는 함수명)으로 함수가 델리게이트를 참조하도록 해준다.
델리게이트 이름 += 함수 이름;
해제 할때는 OnDisable 함수에서 -= 연산자를 써서 참조를 해제한다. 그렇지 않으면 가비지 컬렉션이 발생한다.
private void OnEnable()
{
Enemy.OnMonsterDied += HandleMonsterDied;
}
private void OnDisable()
{
Enemy.OnMonsterDied -= HandleMonsterDied;
}
private void HandleMonsterDied(Enemy enemyInfo)
{
... // 몬스터가 사망했을 때 처리
}
이번에는 직선으로 떨어지는 방해물인 Meteorite를 구현한다. 메테오는 화면 밖 범위 내에 랜덤한 x 좌표에서 시작해서 떨어지는데 그 전에 경고하는 빨간 줄을 출력하고 그 빨간 줄이 생성된 위치에서 메테오를 생성해서 떨어뜨린다. 먼저 AlertLine이라는 경고 라인 오브젝트를 생성하고 색상을 바꾸는 애니메이션을 사용해서 깜빡거리는 이펙트를 만든다. 애니메이션을 만들 때 sprite : color를 선택해서 애니메이션에서 이미지의 색상을 바꿀 수 있다. 그래서 빨간색, 흰색으로 색을 번갈아 가면서 적용해서 깜빡거리는 것처럼 보이게 한다.


그리고 메테오 오브젝트에 적용될 Meteorite 스크립트를 작성한다. 먼저 [RequireComponent(typeof(CircleCollider2D))], [RequireComponent(typeof(Rigidbody2D))]를 선언해서 RigidBody와 Collider가 필수적으로 들어가도록 설정해준다. 그리고 Awake에서 RigidBody2D 컴포넌트와 CircleCollider2D의 세부 사항을 설정해준다. 그리고 OnTriggerEnter2D를 통해서 Player 캐릭터와 충돌을 평가하고 충돌했을때 IDamaged의 TakeDamage를 이용해서 충돌한 오브젝트가 "Player" 태그를 갖고있고 IDamaged 컴포넌트를 갖고있다면 1의 데미지를 주는 것으로 설정한다.

그리고 경고 라인과 경고 라인의 위치에서 메테오를 생성하는 AlertLine 스크립트를 작성한다. 깜빡거리는 애니메이션이 끝나면 메테오가 생성되도록 한다. SpawnLine 함수에서 애니메이션을 출력하고 Invoke 함수를 이용해서 메테오를 생성하는 SpawnMeteo 함수를 2.0초 있다가 호출한다. 여기서 2초는 애니메이션이 재생되는 시간이다. Invoke함수는 MonoBehaviour의 함수로 함수를 특정 시간만큼 지연시켜서 호출하는 함수이다. Invoke함수의 인자로는 함수명과 시간이 들어가는다 함수명은 무조건 string이어야 한다.

그리고 경고 라인을 생성(메테오를 생성)을 다루는 MeteoManager 스크립트를 작성한다. AlertLine prefab을 지정해주고 코루틴 함수를 통해서 메테오를 생성한다. SpawnMeteo함수를 보면 while(true)로 게임을 진행하는 동안 계속해서 메테오를 생성해서 떨어뜨린다. 그리고 AlertLine 컴포넌트가 있다면 SpawnLine 함수를 호출해서 경고 라인 애니메이션을 출력하고 메테오를 떨어뜨린다. 그리고 메테오의 갯수를 저장해서 메테오의 갯수가 10개씩 늘어날때마다 메테오가 생성되는 시간을 줄인다. 그리고 yield return을 통해서 spawnTime만큼 기다렸다가 게임이 끝날 때까지 계속해서 while문을 반복해서 메테오를 생성한다. 그리고 MeteoManager도 GameManager에서 초기화해서 메테오가 생성되는 것을 제어한다. 다음은 Enemy가 죽었을때 Reward를 주는 작업이다. Enemy는 보석(점수), PowerUP, Bomb, Heart(체력 회복)등 아이템을 드랍한다. 각각의 아이템은 해야될 일이 다 다르기 때문에 IPicked 인터페이스를 설정해서 습득한 Player에 대해 이벤트를 발동하도록 한다.
public interface IPickuped
{
void OnPickup(GameObject picker);
}
먼저 점수를 나타내는 보석 아이템에 대해서 스크립트를 작성한다.

스크립트 상단에서 똑같이 필요한 RigidBody와 Collider 컴포넌트에 대해서 설정해준다. 그리고 이 스크립트는 IPicked 인터페이스를 상속받는다. 그리고 Awake에서 RigidBody와 Collider 컴포넌트에 대해서 세부 사항을 설정해주는데 여기서 AddForce 함수로 보석이 생성될때(활성화될때) 잠깐 튀어오르게 한다. AddForce함수는 두 가지를 인자로 받는데 속도(힘의 방향과 크기)와 힘의 종류이다. 그래서 velocity.x는 [-0.5, 0.5] 범위에서 velocity.y는 [3.0, 4.0] 범위에서 랜덤으로 정해진다.


그래서 위 그림에서 본것처럼 y축이 증가하는 방향으로 힘을 받아서 좌우로 흩어지면서 위로 튀어올랐다가 중력을 받아서 내려오게 된다. 그리고 Update 함수 내에서는 Player 캐릭터의 습득 범위 안으로 들어오면 Player 캐릭터를 아이템을 습득하는 target으로 설정해서 2.0에 걸처서 캐릭터 방향으로 이동하도록 한다.


그래서 OnPickup 함수에서는 target이 정해졌을때 RigidBody에서 중력을 없애고 속도를 0으로 바꾼다. 그리고 아이템을 습득했을때의 처리를 위해서 OnPickupGem이라는 델리케이트를 선언해서 Player 캐릭터가 아이템을 먹었을때 Invoke를 통해서 델리케이트를 호출한다. 그리고 Player가 아이템을 습득하는 것으로 설정하기 위해 Player의 자식 오브젝트로 ItemCollector를 만들어주고 PlayerItemCollector 스크립트를 작성해서 넣어준다.

위 스크립트에서 Player의 습득범위 내에 오브젝트가 들어오면 그 오브젝트의 정보를 collision으로 받아와 Tag가 "PickupItem"인지 확인하고 IPickuped 인터페이스가 있으면 OnPickup 함수로 아이템의 타겟을 root.gameObject를 통해 ItemCollector 오브젝트의 제일 위에 있는 오브젝트인 Player 오브젝트를 타겟으로 설정해준다. Enemy가 죽었을때 아이템을 드랍하도록 하는 ItemDropManager 스크립트를 작성한다. 여기서는 Gem prefab과 그 밖의 아이템 flyItem를 리스트로 지정하도록 한다.


그리고 OnEnalbe에서 Enemy에서 정의했던 Enemy 캐릭터가 죽었을때 호출되는 델리게이트를 참조해서 Enemy 캐릭터가 죽었을 때 HandleEnemyDied라는 함수를 호출한다. 그리고 HandleEnemyDied함수는 enemyInfo를 인자로 받아서 Enemy가 죽었을때 위치에서 Gem과 다른 아이템들을 생성하도록 했다. 그리고 다른 PowerUp, Heart, Bomb 아이템은 각각 1 퍼센트의 확률로 생성되도록 했다. 다음은 PowerUP, Heart, Bomb 아이템에 대한 처리를 다룬다. 이 세 아이템은 중력을 따라서 떨어지는 Gem과 달리 랜덤한 방향으로 날아다닌다. 그래서 FlyItem이라고 이름을 붙인다. 세 개의 아이템은 습득했을 때 각각의 효과가 있기때문에 먼저 FlyItemBase라는 추상 클래스를 스크립트로 만들어 FlyItem의 기본적인 효과들을 정의하고 FlyItem_PowerUP, FlyItem_Health, FlyItem_Bomb 스크립트가 FlyItemBase 클래스를 상속받도록 해서 각각의 스크립트에서 효과에 대해서 더 자세하게 구현되도록 한다.

이 스크립트도 FlyItem이 움직이므로 IMovement 인터페이스를 상속받고 아이템이기 때문에 IPickuped 인터페이스도 상속받는다. 아이템들의 공통 기능으로 몬스터에서 드랍이 되서 생성되면 일정 시간동안 랜덤한 방향으로 이동했다가 3에서 4초 후에 이동방향을 바꾸는 기능과 플레이어어 충돌하게 되면 플레이어에게 습득처리 되는 것을 구현한다. 추상 메서드로 ApplyEffect를 선언해서 파생 클래스에서 습득시 효과를 구현하도록 선언만한다. 그리고 Move 함수를 구현하고 SetEnable에서 랜덤한 방향으로 방향을 바꾸고 4초동안 기다리는 코루틴 함수 ChangeFlyDirection를 StartCoroutine으로 호출한다. 그리고 이 SetEnable 함수를 Awake로 호출해서 아이템이 활성화 되었을때부터 랜덤한 방향을 정하고 그 방향으로 이동하도록 한다. 그리고 이제 유저가 게임을 플레이하면서 생긴 정보들을 UI로 표시해야하는데 그것을 ScoreManager라는 스크립트에 정보를 저장해서 UI와 연결해서 출력을 해준다. 그리고 ScoreManager에서 FlyItem에 대한 적용 효과도 설정해주기 때문에 FlyItemBase에서 ScoreManager를 참조한다.