주말동안 보스 스킬 구현과 각종 작업들을 마쳐서 1번 게임을 완성했다.
private IEnumerator DoBossAttack()
{
while(true)
{
for (int i = 0; i < 5; i++)
{
// 죽었을 때는 스킬이 시전되지 않고, 적 소환하지 않음 (최소 1초 대기)
while (!shootingGameManager.GetIsAlivePlane())
{
yield return new WaitForSeconds(1);
}
if (i < 4)
{
StartCoroutine(shootingGameManager.UseBossSkill(i));
}
else
{
StartCoroutine(shootingGameManager.NextPhase());
}
yield return new WaitForSeconds(4);
}
}
}
EnemyContorller.cs에서 DoBossAttack() 함수를 반복하여 보스의 패턴을 만들었다.
아군이 죽어있을 때는 최소 1초간 정지하도록 하고, 이외에는 4초마다 패턴을 반복하도록 했다.
미도리->->유즈->아리스 순으로 스킬을 사용하고, 적을 소환하도록 했다. 이 5개를 한 사이클로 계속 반복하도록 하였다.
적은 1/2/3/4번 적이 반복하여 나오도록 했다.
public IEnumerator UseBossSkill(int skillNum)
{
if (skillNum == 0)
{
_audioManager.PlaySFX("MidoriSkill");
_bossMidoriHalo.GetComponent<Image>().DOFade(1, 1);
yield return new WaitForSeconds(1);
_bossMidoriHalo.GetComponent<Image>().DOFade(0, 1);
for(int i=0; i<10; ++i)
{
ShootEnemyBulletToMidoriPlane(_bossEnemy);
yield return new WaitForSeconds(0.2f);
}
}
처음 구현한 것은 미도리 스킬이었다. 적의 공격이 아군의 비행기를 향해 발사하도록 만들었기 때문에 공격을 10번 하는 것으로 쉽게 구현했다.
else if (skillNum == 1)
{
_audioManager.PlaySFX("MidoriSkill");
_bossMomoiHalo.GetComponent<Image>().DOFade(1, 1);
yield return new WaitForSeconds(1f);
_audioManager.PlaySFX("MomoiSkill");
_bossMomoiHalo.GetComponent<Image>().DOFade(0, 1);
for(int i = 0; i < 11; i += 2)
{
StartCoroutine(ShootEnemyBulletToMidoriPlaneCoroutine(_bossEnemy, new Vector2(-400 + (80 * i), YDownEnd)));
}
yield return new WaitForSeconds(0.5f);
for (int i = 1; i < 11; i += 2)
{
StartCoroutine(ShootEnemyBulletToMidoriPlaneCoroutine(_bossEnemy, new Vector2(-400 + (80 * i), YDownEnd)));
}
yield return new WaitForSeconds(0.5f);
for (int i = 0; i < 11; i += 2)
{
StartCoroutine(ShootEnemyBulletToMidoriPlaneCoroutine(_bossEnemy, new Vector2(-400 + (80 * i), YDownEnd)));
}
}
모모이의 스킬은 기존 함수를 조금 수정해야했다. 원래 유도로 공격하는 기능이었기 때문에 따로 방향을 받지 않았는데, 방향을 받는 것으로 수정했다. -400 ~ 400까지 80단위로 나누어 0/2/4/6/8/10 방향, 1/3/5/7/9 방향으로 번갈아 6발 / 5발 / 6발씩 발사하도록 구현했다.
else if (skillNum == 2)
{
_audioManager.PlaySFX("MidoriSkill");
_bossYuzuHalo.transform.SetParent(_midoriPlane.transform);
_bossYuzuHalo.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
_bossYuzuHalo.GetComponent<Image>().DOFade(1, 1);
yield return new WaitForSeconds(1f);
_bossYuzuHalo.transform.SetParent(_haloParent.transform);
yield return new WaitForSeconds(1f);
StartCoroutine(ShootEnemyBulletToMidoriPlaneCoroutine(_bossEnemy, _bossYuzuHalo.GetComponent<RectTransform>().anchoredPosition, "YuzuGrenade"));
yield return new WaitForSeconds(_yuzuBulletDuration);
_bossYuzuHalo.GetComponent<Image>().DOFade(0, 1);
// 플레이어가 날아오는 도중에 총알을 맞았을 경우 작동하지 않도록 함
if(_yuzuGrenade.GetComponent<Image>().color != Color.clear)
{
_audioManager.PlaySFX("8bitBomb");
_yuzuGrenade.SetActive(false);
for (int i = -2; i < 3; ++i)
{
for (int j = -2; j < 3; ++j)
{
if ((i >= -1 && i <= 1) && (j >= -1 && j <= 1)) continue;
// 각도를 균일하게 하기 위해 값을 크게 함
Vector2 targetPosition = new Vector2(_bossYuzuHalo.GetComponent<RectTransform>().anchoredPosition.x + (-10000 * i),
_bossYuzuHalo.GetComponent<RectTransform>().anchoredPosition.y + (-10000 * j));
StartCoroutine(ShootEnemyBulletToMidoriPlaneCoroutine(_bossYuzuHalo, targetPosition, "YuzuBullet"));
}
}
}
}
유즈 스킬을 구현할 때 조금 애로사항이 있었다. 게임 상에서는 유탄을 날려서 원형 범위에 폭발하는 스킬을 사용하는데, 곡선으로 날아가는 것도 문제고 피하기가 너무 쉬웠다. 친구가 날아간 자리에서 폭발해서 사방으로 총알이 나아가면 어떻겠느냐는 의견을 내주어서 해당 의견을 채용했다.
// 적의 총알 객체를 발사하는 함수
public IEnumerator ShootEnemyBulletToMidoriPlaneCoroutine(GameObject enemy, Vector2 direction, string bulletName = "Normal")
{
GameObject bullet = GetPrefab("EnemyBullet");
bullet.GetComponent<RectTransform>().localScale = new Vector3(1, 1, 1);
bullet.SetActive(true);
bullet.GetComponent<Image>().color = Color.white;
bullet.GetComponent<RectTransform>().anchoredPosition = new Vector2(enemy.GetComponent<RectTransform>().anchoredPosition.x,
enemy.GetComponent<RectTransform>().anchoredPosition.y - 50);
// 적들의 통상 공격
if (bulletName == "Normal")
{
direction = ExtendBulletDirection(bullet.GetComponent<RectTransform>().anchoredPosition, direction);
}
// 유즈 유탄 공격
else if (bulletName == "YuzuGrenade")
{
bullet.GetComponent<RectTransform>().localScale = new Vector3(6f, 6f, 6f);
_yuzuGrenade = bullet;
// 유탄이 헤일로 위치까지 날아가는 시간을 저장
_yuzuBulletDuration = Vector2.Distance(bullet.GetComponent<RectTransform>().anchoredPosition, _bossYuzuHalo.GetComponent<RectTransform>().anchoredPosition) / 600 * EnemyBulletSpeed;
// 유탄의 경로를 연장, UseBossSkill 부분에서 _yuzuBulletDuration이 경과한 이후 체크하기 위함
direction = ExtendBulletDirection(bullet.GetComponent<RectTransform>().anchoredPosition, direction);
}
else if (bulletName == "YuzuBullet")
{
// 유즈 헤일로의 중심에서 총알이 나가는 것이기 때문에 y값을 제자리로 돌려줌
bullet.GetComponent<RectTransform>().anchoredPosition = new Vector2(enemy.GetComponent<RectTransform>().anchoredPosition.x,
enemy.GetComponent<RectTransform>().anchoredPosition.y);
direction = ExtendBulletDirection(bullet.GetComponent<RectTransform>().anchoredPosition, direction);
}
LookRotation2D(bullet, direction);
float bulletDuration = Vector2.Distance(bullet.GetComponent<RectTransform>().anchoredPosition, direction) / 600 * EnemyBulletSpeed;
bullet.GetComponent<RectTransform>().DOLocalMove(direction, bulletDuration).SetEase(Ease.Linear);
yield return new WaitForSeconds(bulletDuration);
bullet.GetComponent<Image>().color = Color.clear;
bullet.SetActive(false);
_enemyBulletQueue.Enqueue(bullet);
}
이를 위해서 함수를 좀 수정했다. 유탄이 날아가는 시간을 따로 저장하거나, 총알이 중앙 기준 y값 -50에서 나가기 때문에 해당 부분도 맞춰주는 등의 작업이 필요했다.
else if (skillNum == 3)
{
_audioManager.PlaySFX("MidoriSkill");
_bossArisHalo.transform.SetParent(_midoriPlane.transform);
_bossArisHalo.GetComponent<RectTransform>().anchoredPosition = Vector2.zero;
_bossArisLazer.SetActive(true);
StartCoroutine(_bossArisLazer.GetComponent<LazerController>().ChasePlane());
_bossArisHalo.GetComponent<Image>().DOFade(1, 1);
yield return new WaitForSeconds(1f);
_bossArisHalo.transform.SetParent(_haloParent.transform);
_bossArisHalo.GetComponent<Image>().DOFade(0, 1);
_bossArisLazer.GetComponent<LazerController>().SetCanMoveLazer(false);
yield return new WaitForSeconds(0.5f);
_audioManager.PlaySFX("ArisSkill");
StartCoroutine(_bossArisLazer.GetComponent<LazerController>().ShootLazer());
yield return new WaitForSeconds(1f);
_bossArisLazer.SetActive(false);
}
아리스 스킬을 구현하는 것이 가장 어려웠다. 처음에는 보스와 플레이어를 레이저로 잇고, 잠시 후 레이저의 위치가 고정되며 그 뒤 레이저가 넓어지며 공격하는 스킬을 구상했다.
처음에는 선 모양 스프라이트를 넣어서 각도나 길이를 조정할까 생각했는데 아무리 생각해도 어려울 것 같아 다른 방법을 찾아보았다. 그러다가 LineRenderer를 발견했다. 처음 써보는 것이라 조금 헤맸다. 먼저 나는 Canvas를 사용하고 있기 때문에 Use World Space 옵션을 꺼야 했다. 또, 화면에 보이지 않아 Position.z를 -1로 설정해줘야 보였다.
public IEnumerator ChasePlane()
{
_canMoveLazer = true;
_lineRenderer.startWidth = 0.01f;
_lineRenderer.endWidth = 0.01f;
_lineRenderer.startColor = Color.red;
_lineRenderer.endColor = Color.red;
while (_canMoveLazer)
{
// 보스의 눈에서 레이저가 나가도록 위치 설정
_startPosition = new Vector2(_shootingGameManager._bossEnemy.GetComponent<RectTransform>().anchoredPosition.x + 28,
_shootingGameManager._bossEnemy.GetComponent<RectTransform>().anchoredPosition.y - 2);
_targetPosition = ExtendDirection(_startPosition,
_shootingGameManager._midoriPlane.GetComponent<RectTransform>().anchoredPosition);
_lineRenderer.SetPosition(0, _startPosition);
_lineRenderer.SetPosition(1, _targetPosition);
yield return null;
}
while (!_canMoveLazer)
{
_startPosition = new Vector2(_shootingGameManager._bossEnemy.GetComponent<RectTransform>().anchoredPosition.x + 28,
_shootingGameManager._bossEnemy.GetComponent<RectTransform>().anchoredPosition.y - 2);
_lineRenderer.SetPosition(0, _startPosition);
SetColliderPosition();
yield return null;
}
}
그리고 처음에는 보스와 플레이어 사이를 잇는 레이저였기 때문에 보스의 Child로 두었는데, 이렇게 하니 레이저가 정상적으로 그어지지 않아서 LineRenderer가 포함된 GameObject를 바깥에 두고, 보스의 GameObject와 플레이어의 비행기 GameObject를 직접 연결해줘야 제대로 그려졌다.
처음에는 보스와 플레이어를 따라오고, 시간이 지나서 _canMoveLazer 변수가 false가 되면 보스쪽 좌표만 움직인다.
public IEnumerator ShootLazer()
{
Color lazerColor = Color.white;
// ColorUtility.TryParseHtmlString("#D1B2FF", out lazerColor);
_lineRenderer.startColor = lazerColor;
_lineRenderer.endColor = lazerColor;
_shootingGameManager.SetIsShootingLazer(true);
while (_lineRenderer.startWidth < 0.3f)
{
_lineRenderer.startWidth += 0.01f;
_lineRenderer.endWidth += 0.01f;
yield return new WaitForSeconds(0.01f);
}
yield return new WaitForSeconds(0.5f);
while (_lineRenderer.startWidth > 0f)
{
_lineRenderer.startWidth -= 0.01f;
_lineRenderer.endWidth -= 0.01f;
yield return new WaitForSeconds(0.01f);
}
_lineRenderer.startColor = Color.red;
_lineRenderer.endColor = Color.red;
_shootingGameManager.SetIsShootingLazer(false);
}
이후에는 레이저를 두껍게, 얇게 하여 발사하는 모션을 만들었다.
public void SetColliderPosition()
{
Vector2 position;
_linePositionList.Clear();
for (int i = 0; i < _lineRenderer.positionCount; i++)
{
position = _lineRenderer.GetPosition(i);
_linePositionList.Add(position);
}
_edgeCollider.SetPoints(_linePositionList);
}
레이저에 닿았을 때 죽게 하기 위해 Collider 작업도 필요했는데, 인터넷에서 찾아보니 MeshCollider를 활용하라고 되어있었다. LineRenderer가 BakeMesh라고 하는 함수를 제공하여 선에 알맞은 Mesh를 만들어주는 것 같았는데, 2D라 그런지 제대로 Mesh가 만들어지지 않았다. 이를 해결하기 위해서 여러 방법을 찾아보았으나, 애초에 MeshCollider는 3D에서 사용하기 때문에 RigidBody2D를 사용하는 나에게는 알맞지 않은 방법이었다.
때문에 EdgeCollider를 사용하기로 했다. LineRenderer의 시작점과 끝점을 이어주기만 하면 간단하게 선 모양 Collider가 완성된다. 두께를 반영하지 못하는 점이 아쉬웠는데, 때문에 레이저를 조금 얇게 만들고, 대신 레이저가 금방 나오도록 했다.
// 적이 스폰되는 번호에 따라 위치와 동작 부여
private void SpawnBezierCurveEnemy(string enemyName, int spawnNum, float speed)
{
GameObject enemy;
if (spawnNum == 0)
{
enemy = SpawnEnemy(enemyName, new Vector2(-300, 600));
StartCoroutine(enemy.GetComponent<EnemyController>().DoBezierCurves2(enemy.GetComponent<RectTransform>().anchoredPosition,
new Vector2(-400, 0), new Vector2(500, 600), new Vector2(300, -600), speed));
}
else if (spawnNum == 1)
{
enemy = SpawnEnemy(enemyName, new Vector2(-150, 600));
StartCoroutine(enemy.GetComponent<EnemyController>().DoBezierCurves2(enemy.GetComponent<RectTransform>().anchoredPosition,
new Vector2(800, 0), new Vector2(-800, 0), new Vector2(150, -600), speed));
}
else if (spawnNum == 2)
{
enemy = SpawnEnemy(enemyName, new Vector2(150, 600));
StartCoroutine(enemy.GetComponent<EnemyController>().DoBezierCurves2(enemy.GetComponent<RectTransform>().anchoredPosition,
new Vector2(-800, 0), new Vector2(800, 0), new Vector2(-150, -600), speed));
}
else if (spawnNum == 3)
{
enemy = SpawnEnemy(enemyName, new Vector2(300, 600));
StartCoroutine(enemy.GetComponent<EnemyController>().DoBezierCurves2(enemy.GetComponent<RectTransform>().anchoredPosition,
new Vector2(400, 0), new Vector2(-500, 600), new Vector2(-300, -600), speed));
}
}
public IEnumerator NextPhase()
{
yield return new WaitForSeconds(3);
if (_phaseNum == 0)
{
for (int i = 0; i < 5; ++i)
{
++_leftEnemy;
SpawnStraightEnemy("PinkEnemy", i);
}
}
else if (_phaseNum == 1)
{
for (int i = 0; i < 5; ++i)
{
++_leftEnemy;
SpawnStraightEnemy("GreenEnemy", i);
}
}
else if (_phaseNum == 2)
{
for(int i=0; i<4; ++i)
{
++_leftEnemy;
SpawnBezierCurveEnemy("YellowEnemy", i, YellowEnemySpeed);
}
}
else if (_phaseNum == 3)
{
for (int i = 0; i < 4; ++i)
{
++_leftEnemy;
SpawnBezierCurveEnemy("PurpleEnemy", i, PurpleEnemySpeed);
}
}
else if (_phaseNum == 4)
{
_leftEnemy = 9;
for (int i = 0; i < 5; ++i)
{
SpawnStraightEnemy("GreenEnemy", i);
}
yield return new WaitForSeconds(3);
for (int i = 0; i < 4; ++i)
{
SpawnBezierCurveEnemy("PurpleEnemy", i, PurpleEnemySpeed);
}
}
else if (_phaseNum == 5)
{
_leftEnemy = 9;
for (int i = 0; i < 5; ++i)
{
SpawnStraightEnemy("GreenEnemy", i);
}
yield return new WaitForSeconds(3);
for (int i = 0; i < 4; ++i)
{
SpawnBezierCurveEnemy("YellowEnemy", i, YellowEnemySpeed);
}
}
else if (_phaseNum == 6)
{
_leftEnemy = 9;
for (int i = 0; i < 5; ++i)
{
SpawnStraightEnemy("PinkEnemy", i);
}
yield return new WaitForSeconds(3);
for (int i = 0; i < 4; ++i)
{
SpawnBezierCurveEnemy("PurpleEnemy", i, PurpleEnemySpeed);
}
}
else if (_phaseNum == 7)
{
_leftEnemy = 9;
for (int i = 0; i < 5; ++i)
{
SpawnStraightEnemy("PinkEnemy", i);
}
yield return new WaitForSeconds(3);
for (int i = 0; i < 4; ++i)
{
SpawnBezierCurveEnemy("YellowEnemy", i, YellowEnemySpeed);
}
}
else if (_phaseNum == 8)
{
// 보스 등장
StartCoroutine(_audioManager.FadeOutMusic());
_isSpawnedBoss = true;
_bossEnemy.transform.SetParent(_aliveEnemyParent.transform);
_bossEnemy.SetActive(true);
yield return new WaitForSeconds(1f);
_audioManager.PlayBGM("FinalFancyBattle");
_bossHPBar.transform.GetChild(0).GetComponent<Image>().DOFade(1, 1);
_bossHPBar.transform.GetChild(1).GetChild(0).GetComponent<Image>().DOFade(1, 1);
}
else
{
// 보스 등장 이후 반복
if (_phaseNum % 4 == 1)
{
for (int i = 0; i < 5; ++i)
{
++_leftEnemy;
SpawnStraightEnemy("PinkEnemy", i);
}
}
else if (_phaseNum % 4 == 2)
{
for (int i = 0; i < 5; ++i)
{
++_leftEnemy;
SpawnStraightEnemy("GreenEnemy", i);
}
}
else if (_phaseNum % 4 == 3)
{
for (int i = 0; i < 4; ++i)
{
++_leftEnemy;
SpawnBezierCurveEnemy("YellowEnemy", i, YellowEnemySpeed);
}
}
else if (_phaseNum % 4 == 0)
{
for (int i = 0; i < 4; ++i)
{
++_leftEnemy;
SpawnBezierCurveEnemy("PurpleEnemy", i, PurpleEnemySpeed);
}
}
}
_phaseNum++;
}
스테이지 구성은 [핑크색 적 -> 초록색 적 -> 노란색 적 -> 보라색 적 -> 초록색 + 보라색 적 -> 초록색 + 노란색 적 -> 핑크색 + 노란색 적 -> 핑크색 + 보라색 적] 순으로 총 8스테이지가 진행되고, 보스가 나온 뒤에 [핑크색 적 -> 초록색 적 -> 노란색 적 -> 보라색 적] 순으로 등장한다.
최고 점수는 보스가 쫄 소환을 총 4번 했을 때 다 잡은 점수인 49,000점으로 설정했다. 해당 적들을 전부 잡고 핑크색 적을 1번이라도 때리면 최고 점수를 달성할 수 있다.
난이도는 잘 못하는 사람도 맞출 수 있도록 적당하게 설정했다.
이외에도 Slider를 활용하여 보스의 체력바를 추가하고, 공격/스킬 부분에 조작키인 Z와 X를 표시했다.
보스가 나올 때 배경음악은 이 음악을 사용했다.
오늘까지 완성된 부분은 다음과 같다.
앞에 있는 쫄 파트는 이전 영상과 같아서 보스 부분만 녹화했다.
이렇게 1번 게임을 드디어 완성했다. 아직 게임 오버 부분이 빠져있긴 한데, 스토리와 게임을 연동하면서 추가하게 될 것 같다. 아마 내일은 스토리 부분을 이어서 작성하고, 게임 시작 부분과 연결할 것 같다. 아마 재시작이 필요할테니 게임 재시작을 위한 작업도 할 것 같다. 이후에는 2장 스토리를 작성하고, 2장 게임인 리듬게임의 기획과 개발도 해야할 것 같다. 이번에는 슈팅 게임처럼 개발하면서 기획하는 주먹구구식이 아니라 기획을 마쳐놓고, 어떤 작업들이 필요할지 생각하고 계획을 세워서 개발하면 더 좋을 것 같다.
주말에 가족들과 교보문고에 갔다 왔는데, 이것저것 둘러보다가 게임 개발 관련 코너에 머무르면서 책을 봤다. 그러던 와중에 지금 나에게 도움이 될만한 두 개의 책을 발견했다. 유니티로는 보통 모바일 게임을 많이 만들기 때문에 최적화 관련해서 문제가 생기는 경우가 많다. 그래서 최적화에 관심이 많기 때문에 최적화 관련 책을 구입했다. 또, 디자인 패턴에 대한 지식도 부족하여 관련 책도 구입했다. 디자인 패턴 이외에도 게임을 개발할 때 도움이 될만한 지식들이 많이 담겨있는 것 같다. 사실 이런 정보나 지식들은 인터넷에서 찾아보면 있긴 하겠지만, 내가 원하는 정보를 찾는 것도 힘들고, 올바른 정보를 얻는 것도 어렵다. 거기다가 전후 흐름에 맞추어 상세히 기술되어 있으니 인터넷의 정보는 아직까지 책을 따라오기 어려운 부분이 없지 않은 것 같다. 해당 책에서 지식을 얻고, 게임에 적용하면서 내 것으로 익혀가면 좋을 것 같다. 우선은 각종 매니저에 싱글톤을 적용해서 코드를 약간 깔끔하게 정리하면 좋을 것 같다.
'개발 > 개발일지' 카테고리의 다른 글
Tales Saga Chronicle Blast 개발일지 23 - 일본어 로컬라이징 (0) | 2023.04.02 |
---|---|
Tales Saga Chronicle Blast 개발일지 22 - 2장 완성, 중간 정리 (0) | 2023.03.31 |
Tales Saga Chronicle Blast 개발일지 20 - 베지에 곡선/기즈모 활용하여 적 이동 구현, 스테이지 구성, 일부 기능 추가 등 (0) | 2023.03.24 |
Tales Saga Chronicle Blast 개발일지 19 - 부활, 적 공격, 점수, 거리 연장 함수 등 구현 (0) | 2023.03.23 |
Tales Saga Chronicle Blast 개발일지 18 - 스킬 오류 수정, 슈팅게임 UI추가, 아군 피격 기능 추가 등 (0) | 2023.03.22 |