![[2024 SWF] Project RM 개발일지 #4](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb98rgm%2FbtsJopa6RbU%2F5X79k0plUbfpCZbr4cbo81%2Fimg.png)
이번에는... 아주 큰 발전을 해냄과 동시에 아주 크게 건강을 해쳤다.
'만들래' 라는 인디게임 플랫폼에서 주최하는 10분 게임 콘테스트에 Project RM의 데모를 출품하기로 하여 일주일간 엄청난 개발 러쉬에 들어갔다.
로고 제작
게임의 성격을 결정하는 로고를 제작했다.
게임이 어둡고, 망한 도시를 배경으로 하고 있어서 Dystopia라는 영어단어를 가져왔고, 그 안에서 사는 사람이라서 -ian을 붙여 Dystopian이라는 게임 이름을 정하게 되었다.
그리고 도트 게임이기에 도트로 제작했고, 리듬 게임이기에 악보같은 디자인도 추가하였다.
그렇게 탄생한 로고는...
이런 로고가 탄생하게 되었다!
느낌 가는대로 손 닿는대로 만든 로고였는데 팀원들에게 호평을 받아서 다행이었다.
튜토리얼 씬 완성
저번에 하고있던 튜토리얼을 개발을 다듬어 완성했다. 추가된 것은 다음 스테이지로 넘어가는 것, 테스트 몬스터의 단계, UI 간소화, 그리고 대망의...노트 연결을 했다.
노트 연결은 내가 개발한 부분이 아니기도 하고 내용이 조금 많아서 아래에서 따로 조금 설명할 것이다.
public IEnumerator TutorialFlow()
{
Pause.isOKToPause = false;
yield return waitOneSec;
yellerChat.EnableChat();
yield return StartCoroutine(yellerChat.Chat(1.65f, "다음!"));
yield return waitOneSec;
yellerChat.DisableChat();
yield return StartCoroutine(PlayerMoveX(10f, 2f));
yield return new WaitForSeconds(0.2f);
chatting.EnableChat();
yield return StartCoroutine(chatting.Chat(3.95f, "자네가 다음 지원자인가?"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.4f, "이렇게 건강한 사람들만 와서야 원..."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.7f,"아, 자네 앞에 많은 지원자가 있었다네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(3.15f, "다 어떻게 됐냐고?"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(6.1f, "자네 순서까지 왔으면 알 법도 하지 않나?"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(3.9f, "잡소리는 여기까지 하고,"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.55f, "이왕 왔으니 내 친절히 알려드리리다."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(6.7f,"자네는 지금부터 '부패한 성' 으로 들어간다네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.9f, "그 안에는 썩은 고위 전사들과 마법사들,"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(4.3f, "그리고 괴생명체들이 있다네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(6.55f, "자네 임무는 부패한 왕을 찾아 없애는 것일세."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(2.95f, "어떻게 없애냐고?"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.1f, "아무래도 왕의 숨통을 끊어야겠지."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(3.05f, "맨몸으로 가냐고?"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(2.78f, "말 한번 잘했네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(6.8f, "장비는 줄걸세. 그리고 사용법도 친히 알려주지."));
yield return StartCoroutine(WaitForUser());
//카메라 무빙
yield return StartCoroutine(CameraMoveX(10f, 1f, "flex"));
yield return StartCoroutine(chatting.Chat(6.4f, "저 앞에 총이 보이는가? 가서 한번 잡아보게."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(MovieEnd());
//카메라 무빙 해제
yield return StartCoroutine(CameraMoveX(-10f, 1f, "flex"));
//yield return StartCoroutine(CameraMoveX(-20f, 0.5f));
CameraReturns();
playerPlayerController.enabled = true;
StartCoroutine(DisableWithDelay(chatting));
ArrowAppear();
yield return StartCoroutine(WaitForGun());
ArrowDisappear();
StartCoroutine(MovieStart());
playerPlayerController.enabled = false;
yield return StartCoroutine(PlayerMoveX(9.5f, 4f));
yield return StartCoroutine(PlayerMoveX(10f, 4f));
chatting.EnableChat();
blocker.enabled = false;
yield return StartCoroutine(chatting.Chat(3.6f, "그 총은 '리듬 건' 일세."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.2f, "음악이 흘러나오는 신기한 무기지."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(6.4f, "그 음악의 박자에 맞춰야 총탄을 내뱉는다네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(4.05f, "한번 시험삼아 써 보겠나?"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(2.25f, "그렇다면..."));
yield return waitHalfSec;
yield return StartCoroutine(CameraMoveX(20f, 1f, "flex"));
StartCoroutine(TestRoomAppear());
yield return StartCoroutine(CameraShake(5f, 0.1f, 40, false));
yield return StartCoroutine(CameraShake(1f, 0.1f, 40, true));
yield return waitHalfSec;
yield return StartCoroutine(CameraMoveX(-20f, 1f, "flex"));
CameraReturns();
yield return StartCoroutine(chatting.Chat(7.2f, "저 앞에 있는 건물 안에 시험용 벌레들이 있다네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(7.55f, "무기를 얕봤다간 큰코 다치니 작은 것부터 해보자고."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(6.7f, "그럼 건물 뒤로 가 있을테니 다 처치하고 오게."));
yield return StartCoroutine(WaitForUser());
chatting.DisableChat();
yield return StartCoroutine(NPCMoves(40f));
yield return StartCoroutine(MovieEnd());
//Pause.isOKToPause = true;
keysInfo.GetComponent<RectTransform>().DOAnchorPosY(-50f, 0.5f).SetEase(Ease.OutSine);
playerPlayerController.enabled = true;
yield return StartCoroutine(WaitForDoorOpen());
playerPlayerController.enabled = false;
readyText.enabled = true;
EnableNote();
DOTween.To(() => readyTextCharacterSpace, x => readyTextCharacterSpace = x, 300f, 2f).SetEase(Ease.OutSine).OnUpdate(() => readyText.characterSpacing = readyTextCharacterSpace).OnComplete(() => readyText.enabled = false);
readyText.DOFade(0f, 2f).SetEase(Ease.OutQuart);
yield return new WaitForSeconds(2f);
//EnemyNoteManager.isMusicStart = true;
countText.enabled = true;
countText.text = "3";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
countText.text = "2";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
LoadBMS.Instance.play_song("tutorialLevel1");
countText.text = "1";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
countText.text = "GO!";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
countText.enabled = false;
playerPlayerController.enabled = true;
yield return StartCoroutine(WaitForElemenations(levelOneEnemies.transform.childCount, 1));
keysInfo.GetComponent<RectTransform>().DOAnchorPosY(150f, 0.5f).SetEase(Ease.OutSine);
playerPlayerController.enabled = false;
StartCoroutine(MovieStart());
CenterFrame.MusicFadeOut();
//Pause.isOKToPause = false;
//EnemyNoteManager.isMusicStart = false;
DisableNote();
readyText.enabled = false;
countText.enabled = false;
isDoorOpened = false;
yield return waitHalfSec;
yield return PlayerMoveX(38f, 3f);
chatting.EnableChat();
InfoTextDisappear();
//TestRoomDoor.Reset();
yield return StartCoroutine(chatting.Chat(3.85f, "잘 했네. 재능이 있구만."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(4.5f, "그럼 바로 다음 단계로 가지."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(CameraMoveX(-5f, 0.5f, "flex"));
yield return waitOneSec;
yield return StartCoroutine(WakeUpLevelTwoEnemies());
yield return waitOneSec;
yield return StartCoroutine(CameraMoveX(5f, 0.5f, "flex"));
CameraReturns();
yield return StartCoroutine(chatting.Chat(8.25f, "저 무시무시한 애들은 '케이지' 라고 불리는 괴생명체라네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(3.9f, "이번에는 쉽지 않을걸세."));
yield return StartCoroutine(WaitForUser());
StartCoroutine(DisableWithDelay(chatting));
yield return StartCoroutine(MovieEnd());
InfoTextAppear();
InfoTextChange("모든 케이지를 처치하세요.");
//Pause.isOKToPause = true;
playerPlayerController.enabled = false;
EnableNote();
readyText.enabled = true;
readyText.DOFade(1f, 0.00001f);
readyTextCharacterSpace = 50f;
readyText.characterSpacing = readyTextCharacterSpace;
DOTween.To(() => readyTextCharacterSpace, x => readyTextCharacterSpace = x, 300f, 2f).SetEase(Ease.OutSine).OnUpdate(() => readyText.characterSpacing = readyTextCharacterSpace).OnComplete(() => readyText.enabled = false);
readyText.DOFade(0f, 2f).SetEase(Ease.OutQuart);
yield return new WaitForSeconds(2f);
countText.enabled = true;
countText.text = "3";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
countText.text = "2";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
LoadBMS.Instance.play_song("tutorialLevel2");
LoadBMS.currentTime = 0d;
countText.text = "1";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
countText.text = "GO!";
yield return new WaitForSeconds(60f / bpmManager.instance.bpm);
countText.enabled = false;
readyText.enabled = false;
GiveCagesEnemyController();
playerPlayerController.enabled = true;
yield return WaitForElemenations(levelTwoEnemies.transform.childCount, 2);
//Pause.isOKToPause = false;
CenterFrame.MusicFadeOut();
DisableNote();
playerPlayerController.enabled = false;
StartCoroutine(MovieStart());
InfoTextDisappear();
yield return waitHalfSec;
yield return PlayerMoveX(38f, 3f);
chatting.EnableChat();
yield return StartCoroutine(chatting.Chat(3.1f, "벌써 해치웠는가?"));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.9f, "이정도면 거기에 들어가서도 잘 하겠군."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(5.6f, "그럼 그 총을 가지고 안으로 들어가게."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(4.9f, "자네라면 꼭 성공할거라고 믿네."));
yield return StartCoroutine(WaitForUser());
yield return StartCoroutine(chatting.Chat(2.4f, "행운을 빌지."));
yield return StartCoroutine(WaitForUser());
StartCoroutine(MovieEnd());
StartCoroutine(DisableWithDelay(chatting));
playerPlayerController.enabled = true;
Pause.isOKToPause = true;
pauseButton.gameObject.SetActive(true);
PlayerPrefs.SetInt("tutorialCleared", 1);
blocker2.enabled = false;
}
중간중간 보이는 blocker에 대한 코드는 튜토리얼을 할 때 플레이어가 자유롭게 움직이는 부분이 있는데, 이 때 튜토리얼을 스킵하고 바로 스테이지로 넘어가는 것을 방지하기 위해 넘을 수 없는 콜라이더로 길을 두 번 막았다. 그리고 특정 단계를 수행하면 이를 꺼서 열리게 했다.
그리고 WaitForSeconds 함수를 미리 변수로 선언해두고 리턴하는 방식을 썼는데, 이러면 최적화가 조금 된다고 한다. 이 코드를 반절정도 짜기 전까지는 몰랐는데, 다른 프로젝트에서 같이 개발하는 포톤 고수 오잉선배가 쓰는 것을 보고 어깨너머로 배웠다.
그리고 이 부분에서 연출적인 부분도 많이 신경을 썼다. DisableWithDelay() 함수를 이용해 채팅을 약간의 딜레이를 준 후
Pause를 조절하는 bool 값을 변경하여 일시정지가 가능할 시점을 정해 UX를 개선한다거나 하는 시도를 해봤다.
이런 직접적인 연출은 처음이었는데, 보는 것 만큼 간단해서 그렇게 어려움은 없었다.
대사 스크립트를 그냥 생으로 짰는데, 나중에 json 파일로 이를 저장해서 간단하게 해볼 계획이다.
튜토리얼 스크립트가 생각보다 길지만 코루틴으로 꽉차서 그리 어렵지 않아보일수는 있는데, 진짜 머리 터지는줄 알았다...
코드 자체는 어렵지 않아도 튜토리얼의 흐름을 생각해서 코루틴들을 적절히 배치하는게 쉽지 않았다.
튜토리얼이 길어지면 길어질수록 신경쓸게 많아져서... 그래도 정신 붙잡고 완성해서 뿌듯하다.
아래는 플레이 영상이다.
BMS 연동
노트 개발을 담당하는 팀원이 bms를 유니티로 해석해오는 스크립트를 짜서 노트를 외부 프로그램에서 찍은 후 이를 가져오기만 하면 되는 편한 작업이 이루어지게 되었다.
그리고 이것을 노트 스크립트에 연결하고, 또 적 공격 메소드도 추가하여 입력된 노트에 맞춰 적이 공격하는 방식을 구현했다.
아래는 플레이 영상이다.
영상에서 확인할 수 있듯이 적들이 노트에 맞춰 공격한다.
이 기능을 구현했을때에는 정말 게임같아서 행복하기도 하고 성취감이 들기도 했다.
스테이지는 튜토리얼에서 썼던 코루틴과 비슷한 방식으로 구현했다.
그리고 여기 나오는 음악도 작곡했다..ㅎ
보스전 구현
던전의 꽃은 보스전이다. 원래 개발 전에도 보스전을 기획하고있었는데 아직 뭔가가 진행되지 않은 상태라서 후딱 구현했다.
튜토리얼에서 썼던 대사 코루틴 흐름과, 적과 연동한 공격 스크립트를 적절히 섞어서 완성해냈다.
아래는 플레이 영상이다.
보스는 다른 적들과 다르게 패턴도 다양하고, 조금 더 강력한 공격을 하게 했다.
에셋이 좋아서 그런지 기능을 조금만 구현해도 보스 태가 나서 다행이었다.
10분 게임 콘테스트 출품
이렇게 만든 디스토피안의 데모를 아래 링크에서 플레이해볼 수 있다!
(유니티 빌드 오류가 나서 부득이하게 빌드 파일을 스테이지별로 쪼개 올린점 양해 부탁드립니다...)
플레이 하고 리뷰 남겨주시면 감사하겠습니다...ㅎ
이 대회를 위해 달린 일주일은 진짜 시간이 삭제된 것 같았다...
방학이 2주정도 남은 시점에서 일주일이 그냥 사라지니 조금 허망하긴 했지만 그래도 결과물을 보면 뿌듯하다.
앞으로도 디스토피안은 소프트웨어 페스티벌에 내는 날까지 꾸준히 개발하며 퀄리티를 높일 계획이다.
이번 대회를 위해 개발하면서 전체적인 틀이 다 잡혀서 앞으로는 이 틀에 맞춰 문제없이 개발할 수 있을 것 같다.
그리고 개강한 지금... 방학이 너무 빨리 지나갔다 방학 돌려줘ㅓㅓㅓㅓㅓㅓ

'팀 프로젝트 > Project RM' 카테고리의 다른 글
[2024 SWF] Project RM 개발일지 #3 (0) | 2024.08.17 |
---|---|
[2024 SWF] Project RM 개발일지 #2 (0) | 2024.08.10 |
[2024 SWF] Project RM 개발일지 #1 (0) | 2024.07.25 |
안녕하세요! 코드 짜는 농부입니다! 경희대학교 소프트웨어융합학과 23학번 재학중입니다. 문의 : dsblue_jun@khu.ac.kr
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!