배틀블루를 만들면서 짬짬히 슈팅게임을 만들어 보았습니다.
이번 프로그램은 '상속'이라는 개념과 사용법을 잡는 데 주안점을 두었습니다.
동영상을 보시면 아시겠지만, 이번 게임은 SoundManager를 통하여, 사운드를 제어해보기도 했습니다.
배틀블루에 비하면 비교적 볼륨이 적은 프로그램이기에, 비교적 이른 시간에 게임을 완성할 수 있었습니다.

사진이 조금 크지만, 위 사진은 제 핸드폰에서 구동한 게임의 로비 화면입니다.
보시다시피 게임은 Portarit환경에서 올바르게 작동되는 게임입니다.
바로 구현파트로 넘어가보겠습니다.
로딩씬의 기본적인 구조는 지금까지 구현했던 코드와 비슷합니다.
기본적으로 페이크 로딩이 존재하고, progress에 따라 로딩바가 진행됩니다.

다만, 로딩 퍼센트를 나타내는 텍스트는 제거되고, Loadding 진행상태를 Bar 형태가 아닌 Circle형태로 나타내었습니다.

덕분에 코드가 비교적 간결해졌네요.

로비씬의 에디터 화면입니다.
화면 전체를 감싸는 버튼 오브젝트가 존재하여, 사용자의 클릭을 입력받아, 게임씬으로 넘어가도록하는 역할을 합니다.
하단의 설명용 텍스트는 코루틴 함수를 이용하여,1초 주기로 문자열이 깜빡이게 만들었습니다.

보시다시피, 매니저 클래스 또한 간결합니다.
MonoBeHaviour의 Start메서드에서 코루틴함수를 호출하고, 코루틴 함수는 문자열 제어를 무한 반복합니다.
그리고 버튼 클릭 이벤트 함수로 사용자의 입력을 인식하여 메인 게임으로 넘어갑니다.

실질적인 게임의 구동을 담당하는 메인게임씬입니다.
기본적인 화면은 위와 같습니다.
제가 개발을 했던 흐름에 맞춰 문서를 작성해보도록 하겠습니다.
슈팅게임을 하시다보면, 백그라운드 이미지가 스스로 흐르고 있는 것을 확인하실 수 있을 겁니다.
이는 사용자의 몰입감을 증가시켜주기도 하고, 전투기가 계속 앞으로 나아가고 있다는 착각이 주기도 합니다.
사실 전투기는 한정된 공간에서 XY축을 이동할 뿐인데말이죠.
그리고 이런 배경화면의 제어는 GameManager에서 이뤄지고 있습니다.
#region 배경 슬라이드
IEnumerator BackGroundMove()
{
float moveSpeed = 10f; // 배경 이미지의 이동 속도
LinkedListNode<Sprite> currentNode = Bgr_images.First;
while (!isGameEnd)
{
yield return new WaitForSeconds(0.05f);
// 배경 이미지를 아래로 이동시킴
img_BgrField.transform.position -= new Vector3(0f, moveSpeed * Time.deltaTime, 0f);
// 배경 이미지가 일정 위치 아래로 이동하면 위치를 초기화함
if (img_BgrField.transform.position.y < 0f)
{
// 현재 스프라이트를 다음 스프라이트로 바꾸고, 위치를 초기화함
currentNode = currentNode.Next ?? Bgr_images.First;
img_BgrField.sprite = currentNode.Value;
img_BgrField.transform.position = bgr_OriginPos.position;
}
}
}
void SetBgrSizeAndLink()
{
foreach (Sprite sprite in img_BgrSprites)
{
Bgr_images.AddLast(sprite);
}
}
#endregion
위 코드를 이용하여, 배경화면의 흐름을 제어합니다.
BackGround를 직렬화된 이미지 변수로 받아옵니다.
기본적으로 하나의 배경이미지가 존재하며, 수시로 스프라이트를 변환하여 배경 화면을 전환시킵니다.
이와 동시에 어느 일정 지점 이하로 이미지가 이동하면 기존의 위치로 이동하죠.
스프라이트는 배열의 형태로 저장하며, 저장된 배열을 LinkedList의 자료구조로 변환시킵니다.
이때 사용하는 함수가 SetBgrSizeAndLink()입니다.
배열에 저장된 백그라운드 이미지를 링크드 리스트에 Add하죠.
0.05sec 주기로 이미지는 position이 y축의 -방향으로 이동합니다.
즉, 이미지가 아래방향으로 내려간다는 뜻이죠.
img_BgrField.transform.position -= new Vector3(0f, moveSpeed * Time.deltaTime, 0f);
그리고 일정 위치에 도달하면 링크드 리스트 내의 스프라이트 이미지를 전환하도록 합니다.
currentNode.Next가 null이 아니면 currentNode.Next의 값을 반환하고, null이면 Bgr_images.First의 값을 반환합니다. 이것은 리스트에서 현재 노드의 다음 노드가 존재하면 그것을 선택하고, 그렇지 않으면 리스트의 첫 번째 노드를 선택하는 것을 의미합니다.
그리고 최종적으로 스프라이트가 변경된 이미지를, 기존의 상단 위치로 옮겨줍니다.
모바일 환경은 PC 환경과 다릅니다.
PC에서는 마우스와 키보드가 존재하기에 Input.GetAxis와 Input.GetKey를 이용하여 간단하게 코드를 짤 수 있지만, 모바일 환경은 오로지 터치만으로 모든 것을 해결해야 합니다.
조이스틱은 모바일 환경에서 플레이어 기체를 움직이게 하는 방법 중 하나입니다.
많은 모바일 게임에서 이러한 기법을 활용하고 있죠.


OnPointerDown 함수는 조이스틱 배경 이미지를 클릭할 때 호출되며, OnDrag 함수를 호출하여 조이스틱 이미지를 이동시킵니다.
OnDrag 함수에서는 클릭한 위치의 좌표를 받아서, 조이스틱 배경의 크기에 맞게 정규화하고, 입력 벡터 값을 계산합니다. 이후, 조이스틱 이미지의 위치를 입력 벡터 값에 따라 이동시킵니다.
OnPointerUp 함수는 조이스틱 배경 이미지에서 손을 뗄 때 호출되며, 입력 벡터 값을 초기화하고, 조이스틱 이미지를 원래 위치로 되돌립니다.
마지막으로, GetHorizontalValue와 GetVerticalValue 함수는 입력 벡터 값을 반환합니다.
즉, 하얀색 원을 바깥쪽으로 드래그하면, 테두리가 경계까지 움직이고, GetHorizontalValue와 GetVerticalValue 함수를 호출하여 입력 벡터 값을 반환합니다.
최종적으로 이렇게 반환한 inputVector 변수를 이용하여 플레이어 기체의 움직임을 구현합니다.
다음으로 구현한 부분은 캐릭터 무빙 파트였습니다.
상단의 컨트롤러 클래스에서 구현한 inputVector 변수를 가져와서 캐릭터의 좌표에 반영하여 이동시킵니다.

기체가 카메라 바깥으로 벗어나면 안 되기 때문에 스크린 좌표를 계산하고 Clamp함수를 이용하여 값을 보정해줍니다.
그리고 보정된 좌표를 기준으로 ViewPort좌표를 월드 좌표로 조정하고, 플레이어 기체의 z축을 대입하여, 최종적으로 객체의 위치를 이동시킵니다.
플라이트 클래스는, 이 게임의 핵심 코드 중에 하나입니다.
Enemy와 Player, 그리고 Boss까지 모두 Flgiht 클래스를 상속받아서 데미지 계산과 파괴 연산 및 변수 등을 제어합니다.

데미지 계산부분입니다.
"MyMissle"은 플레이어가 발사하는 Bullet오브젝트에 부착된 태그로써, Enemy와 boss 객체는 모두, 위 클래스로 데미지 계산이 들어갑니다.
다만, 플레이어 객체의 경우에는 적 미사일 태그를 조건문으로 받아서 데미지를 계산해야 하기 때문에 해당 Virtual 클래스를 Override하여 새롭게 재정의하여 데미지 계산을 합니다.
다만, 큰 틀에서의 로직은 위와 다르지 않습니다.
Collision으로 충돌을 감지하고 충돌한 객체의 태그를 확인한 후에 hp를 깎습니다.
그리고 데미지 만큼 스코어를 올려주고 이펙트 효과가 있는 파티클 시스템을 Instantiate로 생성 후 파괴합니다.

위 함수는 Attack을 담당하는 함수입니다.
bool 변수를 파라미터로 입력받아, Up/Down 방식을 분기로 나누어 탄환을 발사합니다.
미사일은 기본적으로 배열의 형태로 저장받으며, nFireLevel 변수로 원하는 미사일을 발사하도록 합니다.
Enemy객체의 경우에는 하나의 미사일을 발사하지만, 플레이어의 경우 무기 PowerUp에 따라서 다른 미사일을 발사해야하기 때문에 배열로 관리하고 있습니다.
그리고 매개변수로 받은 부울의 참거짓에 따라 AddForece로 탄환을 발사합니다.
마지막으로 Interval의 값만큼 코루틴에 Wait를 줍니다.

Update역시도 모든 Flight객체가 필요로하는 필수 로직을 입력한 후에 가상함수로 정의하여 필요에 따라 재정의할 수 있게 하였습니다.
게임 종료시에 객체 파괴, 그리고 hp가 0에 도달할 경우 객체 파괴.
또한, 카메라 시야에서 일정거리 이상 벗어날 경우 객체 파괴.
모두 Destroy와 관련된 함수입니다.
이외에도 플라이트 클래스는 각종 변수들의 Setter,Getter를 가지고 있습니다.
hp,공격속도,스피드가 그 예입니다.

플레이어 기체를 제어하는 FlgihtManager 클래스는 OnTriggerEnter2D를 재정의합니다.
앞서 말씀드렸다시피, 적기체가 발사하는 Bullet에 부착된 Missile 태그를 확인하여 데미지 계산을 합니다.
또한, 기체가 파괴될 경우에는 GameEnd를 True로 바꾸어, 게임프로세르를 관리하는 게임매니저 클래스에 송신합니다.
위에서 설명드린 Moving함수 역시도 플라이트 객체의 Update함수를 오버라이딩하여, Moving함수를 추가로 동작시키고 있습니다.

폭탄과 회복 관련.
슈팅게임에는 아이템이 존재합니다.
대표적으로 플레이어를 구사일생해주는 폭탄과 체력을 회복시켜주는 아이템이죠.
위 함수가 이를 구현한 로직에 해당합니다.
BoomUseClick은 BoomCount의 조건을 돌려 참일 경우, 값을 감산하고 폭격기를 생성시킵니다.
폭탄을 사용할 수 있는 버튼은 Boomber는

해당 화면 전체를 덮는 버튼 오브젝트입니다.

메인게임씬은 두 개의 캔버스로 분리하여 UI를 관리하고 있습니다. BackGround캔버스는 배경화면과 폭탄 버튼을 가지고 있는 캔버스이며, ConsolePortCanvas는 하단의 컨트롤 박스와 게임종료, 처음화면으로 돌아갈 수 있는 버튼이 포함된 EndBox 관리하는 UI입니다.
콘솔포트 캔버스는 SortingLayer를 5레벨을 주어, 다른 캔버스나 오브젝트에 비하여 우선하여 나타낼 수 있도록 하였습니다.
위에서 보셨다 시피, 폭탄버튼을 클릭하면 Boomber를 생성합니다.
폭격기는 플레이어 기체와 마찬가지로 TriggerEnter를 상속받아 Missile태그를 가진 오브젝트에 피해를 입도록 만들었는데, 이는 플레이어를 대신하여 총탄을 맞을 수 있도록 하기 위함입니다.
또한 Update를 상속받아, 위쪽으로 이동하도록 만들었습니다.

Shoot()함수는 원형으로 탄막을 뿌리는 함수입니다.
먼저, 변수 angle을 0으로 초기화합니다. 이 변수는 원주 위의 각도를 저장하기 위한 용도로 사용됩니다.
for 반복문을 사용하여, bulletCount 변수에 저장된 값 만큼 총알을 발사합니다.
각도 계산에는 수학적인 원리가 사용되는데, 삼각함수에 의거하였습니다.
angle 변수는 360 / bulletCount 만큼씩 증가하면서, 다음 총알이 발사될 방향을 계산합니다.
(불릿 카운트는 총 발사하는 총알의 갯수입니다, 갯수가 많을 수록 빽빡하게 발사됩니다.)
이 때, Mathf.Cos()와 Mathf.Sin() 함수를 사용하여 원주 위의 각도에 해당하는 삼각함수 값을 계산합니다. Mathf.Deg2Rad 상수는 각도를 라디안 값으로 변환하기 위한 상수입니다.
- Mathf.Cos()와 Mathf.Sin() 함수는 삼각함수 중 코사인과 사인 값을 계산하는 함수입니다. 이 함수는 라디안 값을 인자로 받기 때문에, 각도 값을 라디안 값으로 변환해 주어야 합니다. 예를 들어, 90도 각도에 해당하는 라디안 값은 Mathf.PI / 2 입니다.
- 삼각함수는 직각삼각형에서의 각도와 변의 길이를 연결하는 함수입니다. 따라서 삼각형의 각도와 한 변의 길이가 주어지면, 나머지 두 변의 길이나 해당 삼각형 내부의 다른 값들을 계산할 수 있습니다.
- 2차원 평면 상의 벡터를 생각해보면, 벡터는 크기와 방향으로 구성됩니다. 벡터의 방향은 2차원 평면 상에서 각도로 표현될 수 있으며, 이 때 각도와 삼각함수를 이용하여 벡터를 구할 수 있습니다. 예를 들어, x축과 이루는 각도가 a인 벡터의 x 성분은 cos(a)이고, y 성분은 sin(a)입니다.
따라서, 삼각함수를 사용하여 각도와 벡터를 연결하고, 각도에 따라 변화하는 방향성을 나타낼 수 있습니다.
Mathf.Deg2Rad 상수를 사용하여 각도를 라디안 값으로 변환합니다. 이 상수는 180도를 파이(π)의 라디안 값으로 변환하는 상수입니다. 따라서, 1도는 Mathf.Deg2Rad의 값에 1을 곱한 값과 같습니다.
angle 변수에 360도를 탄환 개수로 나눈 값을 대입합니다.
Mathf.Cos()와 Mathf.Sin() 함수를 사용하여 해당 각도에 해당하는 x, y 좌표 값을 계산할 수 있습니다.
이렇게 계산된 x, y 좌표 값은 탄환의 방향을 나타내는 벡터가 됩니다.
총알은 Instantiate() 함수를 사용하여 생성하고, 생성된 총알의 Rigidbody2D 컴포넌트에 계산된 방향과 속도를 설정하여 발사합니다.
적 기체에 부착된 클래스입니다.
부모 클래스인 Flight의 메서드를 대부분 활용하기에 특별한 점은 없습니다.

객체의 이동과 파괴 시에 일정 확률로 아이템을 생성하여 드랍시킵니다.
게임매니저의 역할은 게임의 흐름을 관리하는 것입니다.
열거형 함수를 이용하여 State와 Stage에 따라서 분기하여, 필요한 함수를 호출합니다.

eState가 Ing면, 게임 플레이를 관리하는 StageProcess 함수를 호출합니다.
end라면 스테이지의 클리어 여부에 따라 victory함수와 fail함수를 호출합니다.
이때 조건문으로 있는 Flag 변수는, 코루틴함수의 반복에 따른 함수 중복 호출을 막기 위함입니다.

스테이지 프로세스는 eStage에 따라서 스테이지를 호출하는 함수입니다.
사실 코드의 기능만 놓고보면, StageStart()함수를 호출하는 것 뿐이니, 굳이 스위치 문으로 나눌 필요는 없다고 생각하지만, 코드의 가독성을 위해서 Switch문으로 나누었습니다.
게임매니저가 호출한 StageStart()는 스테이지의 진행을 관리하는 매니저 함수입니다.

크게 복잡한 건 없습니다.
스테이지마다 알맞는 브금을 재생하고, 코루틴함수를 호출합니다.

스테이지 코루틴 함수는 기본적으로 위와 같습니다.
이는 스테이지1,2,3 모두 비슷합니다.
다만, 특정 적 객체를 추가로 투입하는 등의 차이점이 있을 뿐이죠.
라운드의 클리어는 킬카운트를 기준으로 셈합니다.
특정 킬수 이상에 도달한다면, 잡몹구간이 종료되고 보스몹 구간으로 전환되는 방식이죠.
Enemy객체는 스폰포인트로부터 -2f,2f 사이의 랜덤한 지점에 적 객체를 생성하는 방식입니다.
그리고, EnemyFlight 클래스 생성자에 필요한 데이터를 입력한 객체를 만들어,
obj객체가 가지고 있는 EnemyFlight 컴포넌트를 가져와 Copyg합니다.
보스 구간에서는 isBoss를 True로 바꾸고, yield break하여 코루틴함수를 탈출시킵니다.
아이템 클래스는 플레이어의 데이터를 직접적으로 수정하는 함수입니다.
아이템은 열거형 변수로 타입을 관리하고, enemy객체에서 설정한 열거형 타입에 따라 지정되어있는 효과를 일으킵니다.
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.gameObject.tag == "Player")
{
switch(eItemType)
{
case eITEM_TYPE.HP:
HP_ItemFunc(collision.gameObject);
break;
case eITEM_TYPE.Power:
Power_ItemFunc(collision.gameObject);
break;
case eITEM_TYPE.Boom:
Boom_ItemFunc(collision.gameObject);
break;
case eITEM_TYPE.Interval:
Interval_ItemFunc(collision.gameObject);
break;
default: break;
}
GameManager.Instance.PlusScore(2000);
Destroy(this.gameObject);
}
}
public void setType(eITEM_TYPE type)
{
this.eItemType = type;
}
void HP_ItemFunc(GameObject obj)
{
FlightManager manager = obj.GetComponent<FlightManager>();
int maxHp = manager.getMaxHp();
if(manager.getHp()+10 >= maxHp)
{
manager.setHp(maxHp);
manager.HpBarView();
}
else
{
manager.RecoveryHp(10);
}
}
void Power_ItemFunc(GameObject obj)
{
FlightManager manager = obj.GetComponent<FlightManager>();
int len = manager.missiles.Length;
if (manager.getFireLevel() >= len-1)
return;
else
{
int num = manager.getFireLevel();
manager.setFireLevel(num+1);
}
}
void Boom_ItemFunc(GameObject obj)
{
FlightManager manager = obj.GetComponent<FlightManager>();
manager.setHaveBoomCount();
}
void Interval_ItemFunc(GameObject obj)
{
FlightManager manager = obj.GetComponent<FlightManager>();
float interval = manager.getFireInterval();
interval = interval - (interval * 0.1f);
manager.setFireInterval(interval);
}
switch문을 이용하여, 객체가 보유한 eItemType에 따라서 충돌체(플레이어)를 매개변수로 넘기고 함수를 호출합니다.
그리고 각 함수는, HP, PowerLevel, Interval, Boom 숫자를 조정합니다.
hp와 인터벌, powerlevel은 Flight 클래스가 가지고 있는 Setter를 호출하여 값을 보냅니다.
Boom은 플레이어만이 가지고 있는 고유한 값이기에, FlightManager 클래스에 따로 구현해야 합니다.
스테이지 1 보스의 특징은 플레이어를 향해 포탑이 방향이 돌아가고, 미사일을 발사한다는 것입니다.
바로 코드로 확인하겠습니다.

포탄은 3초 단위로 발사됩니다.
하지만, 포탑이 3초 단위로 움직인다면 부자연스럽기 때문에 MainCannonRotate는 따로 코루틴함수를 호출하여 관리합니다.
그렇기에 미사일 발사와 방향과 관련된 함수를 살펴보겠습니다.
direction 벡터는 target.transform.position - mainCannon.position로 계산됩니다.
(두 포지션을 '-'하였을 때, 특정 방향을 구할 수 있게 되는데, , 각 위치 벡터는 3차원 좌표로 나타낼 수 있습니다. 예를 들어, A의 좌표를 (Ax, Ay, Az)로 표현하고 B의 좌표를 (Bx, By, Bz)로 표현한다면, B - A는 (Bx - Ax, By - Ay, Bz - Az)로 계산됩니다.)
따라서 target.transform.position - mainCannon.position의 결과는 목표 지점과 현재 위치 벡터 사이의 차이 벡터를 나타내게 됩니다. 이 차이 벡터는 원점을 기준으로 목표 지점 방향을 가리키는 벡터입니다.
이는 target 위치에서 mainCannon 위치를 뺀 벡터로서, mainCannon에서 target을 향하는 방향을 나타냅니다.
다음으로, Mathf.Atan2(direction.y, direction.x)는 direction 벡터의 y 성분과 x 성분을 이용하여 방향 각도를 계산합니다. Mathf.Atan2 함수는 라디안 단위로 각도를 반환하며, Mathf.Rad2Deg를 곱하여 각도를 도 단위로 변환합니다.
계산된 각도에 -90f를 빼는 이유는 회전값을 조정하기 위해서입니다. 각도를 Quaternion으로 표현하기 위해서는 회전 축과 회전각을 사용해야 합니다. Quaternion.AngleAxis(angle + 180f, Vector3.forward)는 angle + 180f도 만큼 회전하는 Quaternion을 생성합니다. 여기서 Vector3.forward는 z축을 나타내며, 객체를 2D 평면에 정렬하기 위해 사용됩니다.
덧붙여, 180도를 추가한 이유는, 미사일 프리팹의 각도가 180도 틀어져 있기 때문입니다.
마지막으로, obj.transform.rotation = rotation은 회전값을 객체의 회전으로 설정합니다. 이를 통해 객체는 target을 향하는 방향으로 회전하게 됩니다.

포탑의 회전은 Frame단위로 이뤄집니다.
Vector3 dir = target.transform.position - transform.position;는 현재 위치로부터 타겟 위치 까지의 방향을 나타내는 Vector3 변수입니다.
다음 줄인 angle은 dir 벡터의 y 성분과 x 성분을 이용하여 아크탄젠트 함수 Mathf.Atan2()를 사용하여 방향 각도 angle을 계산하는 코드입니다.
이 각도는 라디안 값이므로, Mathf.Rad2Deg를 곱하여 각도 단위로 변환하고, -90f를 빼는 것은 회전 기준을 조정하기 위한 보정값입니다.
셋째줄은 q는 계산된 방향 각도 angle을 이용하여 회전을 나타내는 쿼터니온 q를 생성합니다.
(유니티에서는 xyz 3원수가 아닌 Quaternion에 의거한 4원수로 회전을 계산합니다.)
Quaternion.AngleAxis() 함수는 주어진 각도와 축을 기반으로 회전을 나타내는 쿼터니온을 생성합니다. 이 코드에서는 angle을 회전 각도로 사용하고, Vector3.forward를 축으로 사용합니다.
(2D환경에서 좌우로 돌아가는 모습을 구현하기 위해서는 z축을 사용해야합니다.)
MainCannonPrefab.transform.rotation = Quaternion.RotateTowards(MainCannonPrefab.transform.rotation, q, 180f);: 현재 객체의 회전을 MainCannonPrefab의 현재 회전값에서 q로 점진적으로 변경합니다. Quaternion.RotateTowards() 함수는 현재 회전값에서 목표 회전값으로의 회전을 부드럽게 처리하여, 최대 회전 각도를 180도로 제한합니다. 이를 통해 부드러운 회전 동작을 구현할 수 있습니다.
스테이지 2 보스는, Boomber의 서클 Shoot와 더불어, 플레이어 방향을 향해 10발의 탄막을 호를 그리며 발사하는 특징을 가지고 있습니다.

radius는 미사일이 발사될 원의 반지름을 정의합니다.
angleStep = 360f / numShots는 원주를 numShots로 나눈 값으로, 미사일 간의 발사 각도 간격을 계산합니다.
player는 함수 바깥에서 선언된 변수로, 플레이어 태그를 지닌 객체의 GameObject 값을 저장하는 변수입니다.
missile.transform.position과 player.transform.position을 감산하여 방향을 찾습니다.
그리고 해당 방향을 향해 탄막을 발사시킵니다.
(ForceMode2D.Impulse는 순간적인 힘을 가속시키는 메서드입니다.)
angle 값에 수식을 통하여, 180도 + 미사일 번째* 발사 각도 간격을 조절하여 대입합니다.
mainCannon.transform.position.x는 발사 지점의 x 좌표이며, 발사 각도 angle을 이용하여 원의 반지름 radius에 대한 x 좌표를 계산합니다. Mathf.Cos 함수는 주어진 각도의 삼각함수인 코사인 값을 반환합니다.
mainCannon.transform.position.y는 발사 지점의 y 좌표이며, 발사 각도 angle을 이용하여 원의 반지름 radius에 대한 y 좌표를 계산합니다. 추가로 y 좌표에 2를 더하여 높이를 조정합니다. Mathf.Sin 함수는 주어진 각도의 삼각함수인 사인 값을 반환합니다.
(높이를 조정한 이유는 원의 둘레를 따라 탄막을 배치할 때, 원의 중심에 위치하는 것보다 약간 위에 위치시키는 것이 시각적으로 더 자연스럽고 예쁘기 때문입니다.)
missile의 포지션 위치를 계산된 좌표 (x, y, 0f)로 이동합니다.
missile.transform.rotation에 미사일의 회전을 설정합니다. 오일러 각 (0f, 0f, angle)을 이용하여 미사일을 z 축을 중심으로 주어진 각도 angle만큼 회전시킵니다.
스테이지 3 보스는, 4가지 공격패턴을 가지고 있습니다.
첫 번째는, 플레이어 추적 포탑패턴,
두 번째는, 8방향 탄막,
세 번째는, 플레이어 캐릭터와 수직을 이루며 발사되는 네 개의 탄막,
네 번째는, 써클 탄막.
첫 번째와 네 번째는 앞서 설명했기에 바로 두 번째와 세 번째로 넘어가겠습니다.

먼저 8방위로 발사되는 탄막입니다.
플레이어의 방향은 Directrion을 기준으로 up,down,left,right(절대값)을 더하여 정규화를 합니다.
이렇게 구한 방향값으로 탄환을 발사하면, 8방위로 탄을 발사할 수 있습니다.

플레이어에 수직을 이루며 날아가는 탄환입니다.
left 벡터: left 벡터의 x-좌표는 -direction.y로 설정되고, y-좌표는 direction.x로 설정됩니다. 이는 direction 벡터의 y-좌표를 음수로 취하고, x-좌표를 그대로 사용합니다.
right 벡터: right 벡터의 x-좌표는 direction.y로 설정되고, y-좌표는 -direction.x로 설정됩니다. 이는 direction 벡터의 y-좌표를 그대로 사용하고, x-좌표를 음수로 사용합니다.

그림으로 표현하면 이렇습니다.
이렇게 구한 right와 left를 direction과 조합하여 정규화합니다.
dir1 : direction 벡터에서 시계 방향으로 90도 회전한 방향
dir2 : direction 벡터에서 반시계 방향으로 90도 회전한 방향
dir3 : direction 벡터에서 시계 방향으로 90도 회전한 방향
dir4 : direction 벡터에서 반시계 방향으로 90도 회전한 방향을 가리킵니다.

최종적으로 이런 벡터값을 가지며, 해당 방향으로 탄환을 발사합니다.
Vector3 pos1 = new Vector3(0f, 0f, 0);
Vector3 pos2 = new Vector3(0.1f, 0f, 0);
Vector3 pos3 = new Vector3(0f, 0.1f, 0);
Vector3 pos4 = new Vector3(0.1f, 0.1f, 0);
위 코드는 탄환의 초기 생성 위치값을 조정하는 코드들입니다.
사운드의 생성과 파괴를 담당하는 클래스입니다.

싱글턴을 이용하여 사운드 매니저 내의 해당 함수를 호출하면, AudioSource를 제어하여, AudioClip들을 재생하고 파괴합니다.
이펙트의 경우에는, 주기적으로 파괴를 해주어야하기 때문에 Update를 이용하여, 리스트 내의 오디오 오브젝트를 파괴하게 만들었습니다.
학원에서 선생님들과 함께 게임을 만들어본 것을 제외하고,
저 스스로 하나의 게임을 완성하는 건 이번이 두 번째였습니다.
본래 세 번째로 시작한 프로젝트인 스타슈트가 먼저 완성된 것은
예상외로 배틀블루의 구현이 오래걸린 탓도 있겠네요.
(현재 캐릭터 스킬을 만들고, 전투 프로세스를 구현중입니다.)
프로젝트 빌드 과정에서 저를 당황하게 만들었던 실수도 있었는데,
코드를 입력하는 과정에서 뜻하지 않게 필요 없는 라이브러리가 추가되어, 빌드 도중 에러가 발생하는 문제가 발생했었습니다.
이걸 해결하겠다고, 개발자 커뮤니티 사이트도 뒤져보고, 열심히 삽질했던 기억이 나네요.
결국은, 에러 메시지를 차분히 읽어보고 스크립트 내에 문제가 있는 것을 확인하고 불필요한 라이브러리 파일을 모두 제거하였더니, 빌드가 원활하게 되더군요.
게임을 만들고 플레이하는 건 정말 재미있는 경험이라고 생각합니다.
제가 게임 클라이언트 개발자를 지망하는 이유도 바로 이때문이지요.
원래도 게임을 좋아하기도 하지만요.
그이상으로 게임을 개발한다는 행위 자체가 재밌고 흥미롭게 느껴졌습니다.
다음번에는 더 훌륭하고 짜임새있는 게임을 만들어보도록 하겠습니다.
감사합니다.