![[Side Project] Project KS 개발일지](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduCdqv%2FbtsJxCAbgLR%2FGNxAttDVXsOolElkfpn580%2Fimg.png)
전에 만들었던 Project RM을 만들래에서 주최한 10분콘에 냈었는데, 사실 또 하나 더 냈다..
이 프로젝트가 10분콘을 노리고 만든 게임이고, 일주일만에 엄청난 성과를 이뤄낸 플젝이기도 했다.
다른 프로젝트를 같이하고있는 선배와 동기들로 구성된 4명 팀이었고, 여기에서 나는 기획, 서브 개발, 아트, 사운드의 역할로써 참여했다. (또 다함) (그 선배는 바로 건호형)
게임 기획
또 같이 프로젝트를 하는 선배에게는 꿈이 하나 있었다... 바로 네크로맨서가 주인공인 게임을 만드는 것이다.
그래서 초기 기획을 할 때 여러 컨셉들과 함께 네크로맨서가 주인공인 기획을 하나 만들어두었다.
그리고 기획을 선배에게 주자마자 빛의 속도로 네크로맨서 기획이 선정되었다. ㅋㅋㅋㅋ
이제 그 기획을 간단히 설명하자면, 네크로맨싱 마법을 배운 주인공 마법사가 자신의 죽은 동료 기사를 되살려서 둘이 힘을 합쳐 성을 지켜내는 3D 뱀서라이크 디펜스 게임이다.
아래의 더보기는 기획의 이해를 돕고 게임의 컨셉을 확정짓기위해 썼던 게임의 프리퀄 스토리라인이다.
절친한 친구 관계였던 왕국의 유능한 마법사 누크와 왕실 기사단장 베세린. 그들은 어릴 적부터 친한 친구였고, 둘 다 왕실에 들어온 이후로는 훈련도 함께 할 정도로 깊은 우정을 가지고 있었다. 그러던 중 때는 1273년, 이종족 적국의 침략으로 두 국가간의 전쟁이 발발했다. 누크는 후방에서 기사들을 지원하며, 베세린은 전방에서 기사들을 지휘하며 누구보다 열심히 전장에 임했다.
하지만 적군의 힘은 거셌고, 병사들을 해치워도 어디선가 끝도 없이 몰려왔다. 결국 베세린이 지키던 최전방의 전선이 무너졌고, 베세린을 포함한 최전방의 기사들이 모두 목숨을 잃었다.
전쟁통에 소식을 들은 누크는 하늘이 무너지는 것 같았다. 전사자의 시체들이 왕국으로 들어왔을 때, 그 소식은 현실이 되었다. 그 때, 기사단에 가로막혀 관직에 오르지 못한 왕국의 부패한 세력들이 이것을 기회로 삼아 기사단장의 책임을 묻기 시작했다. 그들의 주된 주장은 해이해진 훈련의 강도였다. 베세린이 친구 누크와 놀듯이 훈련한 탓에 적들의 공격에 충분한 대비가 이뤄지지 않았다는 것이다. 하지만 누크는 두 사람이 누구보다 열심히 훈련했다는 것을 알고있었기에, 이에 거세게 반발했다. 하지만 누크는 한 사람이었고, 그들은 여러명이었다. 결국 왕국의 여론은 그들의 편으로 돌아서기 시작했고, 삽시간에 베세린과 누크는 왕국의 반역자가 되었다.
누크는 이 상황을 버틸 수가 없었다. 친구가 열심히 왕국을 위해 싸우다 전사했는데 반역자가 되었고, 덩달아 자신까지 왕국에 반기를 든 취급을 받고있다는 생각에 정신적으로 무너져갔다. 결국 누크는 돌이킬 수 없는 길을 걷기 시작했다. 바로 왕국에서 오랫동안 봉인되어 금기로 여겨지던 ‘네크로맨스’를 이용해 친구를 살리고, 반역자로 몰아갔던 세력을 모두 죽인 후에 왕국을 떠난다는 결심을 했다.
누크는 바로 실행에 옮겼다. 베세린의 시체를 등에 업고 네크로맨스 서적이 있는 지하 최하층의 도서관을 지키던 병사들을 분노에 찬 마법으로 죽기 직전까지 만든 후, 서적을 찾아 옆에 베세린의 시체를 눕히고 적혀있는 주문을 외기 시작했다. 주문은 성공적이었다. 베세린은 전쟁에서 흉하고 심하게 다친 모습 그대로 다시 살아났다. 하지만, 네크로맨스 주문은 사용자도 잠식당할 위험이 있어 봉인당했기에 누크 역시 이를 피해갈 수는 없었다. 이름모를 소름끼치는 기운이 누크의 몸과 정신을 감싸는 것이 느껴졌지만, 누크는 확실한 목표가 있었다. 바로 이 상황까지 오게 한 그들을 죽이는 것, 그 집념 하나로 잠식당하는 와중에 희미한 의식은 남게 되었다.
누크는 베세린을 데리고 그들이 있는 장소로 갔다. 그들은 자신들이 관직에 오를거라는 희망을 가지고 술과 음식을 한가득 먹고있었다. 누크는 그들이 알아챌 전조도 주지 않고 바로 베세린을 앞세워 그들을 덮쳤다. 눈 깜짝할 새에 술과 음식에는 피가 섞였고, 바닥과 벽은 피로 물들었다. 그 순간, 저 멀리에서 적군을 살피던 병사가 이 상황을 보게되었고, 바로 나팔을 불어 경보를 울렸다. 이를 들은 누크는 베세린을 데리고 왕국을 빠져나가기 시작했다. 그들을 가로막는 것은 성문이든 병사든 모두 부서져버렸다. 한 사람은 이성을 잃었고, 한 사람은 목숨을 잃었기에 그들에게 더 이상 잃을 것은 없었다. 그렇게 그들은 피칠갑이 된 왕국을 뒤로하고 근처의 숲 속에 임시 거처를 삼았다.
그들이 없는 왕국은 힘이 전혀 없었다. 결국 적들이 다시 몰려왔을 때 남은 세력들이 막아봤지만 힘없이 무너졌다. 그리고 적군이 왕국을 점령했지만, 이내 네크로맨스 주문의 봉인이 풀려있다는 사실을 알고 저주받은 왕국으로 취급해 떠났다. 적군도 떠난 부서진 왕국에는 적막만이 감돌 뿐이었다.
왕국에 걸려있는 적국의 깃발을 보고 누크와 베세린은 다시 왕국 앞으로 조심스레 가 보았다. 하지만 아무도 없었고, 부서진 왕국을 자신들의 새로운 거처로 삼기 시작했다. 그 때 정찰을 나온 적국의 병사에게 두 사람이 발각되었고, 적국에는 죽은 기사단장이 돌아왔다는 소식과 함께 바로 왕국과의 전쟁태세를 알리는 신호탄이 되었다. 왕국은 이미 부서졌고 두 사람밖에 없었지만 누크도 이를 어느정도 눈치 채고있었고, 다시 베세린을 잃고싶지 않아 쳐들어오는 적군을 베세린과 함께 막기로 결심했다.
초승달이 서슬푸른 밤, 몰려오는 적군과 함께, 누크와 베세린, 죽은 자들의 전쟁이 시작된다.
이런 스토리라인을 가짐으로써 생기는 게임 시스템이 있는데, 바로 죽은 사람을 살릴 수 있다는 것이다. 네크로맨서니까 당연하기도 하지만 게임에 녹여내는게 의외로 쉽지는 않았다.
그래도 잘 녹여내었고, 한 가지의 메인 조종 시스템과 세 가지의 기물 조작 시스템을 기획하였다.
🧟♂️ 빙의 : 죽은 베세린을 되살려 직접 조종한다.
💀 조복 : 죽은 적군을 되살려서 아군으로 삼는다.
💀 러쉬 : 30초동안 정신을 집중해 그 시간동안 죽는 적군을 바로 되살려 아군으로 만든다.
💀 시체폭발 : 죽었다가 되살아난 아군들에게 주문을 걸어 폭파시키고 주변에 큰 광역 데미지를 준다.
조복, 러쉬, 시체폭발의 세 가지는 모두 마나를 소모하고, 빙의는 따로 마나의 소모 없이 쿨타임만 있다.
이후에는 적들을 구성했다. 뱀파이어 서바이벌의 웨이브 시스템을 참고하여 일정 시간마다 새로운 웨이브가 몰려오게 하고, 한 분기(스테이지)가 끝나면 중간보스가 등장한다. 그리고 그 중간보스를 물리치면 다음 분기(스테이지) 로 넘어가도록 기획하였다.
이렇게 하니 게임 하나가 뚝딱 기획되었다. 진짜로 거짓말 안하고 몇시간 안에 다 기획했다.
프로젝트 여러개를 기획하다보니 그새 기획이 늘었나...? 싶다.
그리고 이 기획을 하면서 로고도 바로바로 떠올라서 뚝딱 만들었다.

Kindred라는 영어단어에 '마음이 맞는 사람' 이라는 뜻이 있었다. 나도 로고를 만들면서 알게 되었다.
적 웨이브 구현
기획을 끝낸 시점에 바로 개발에 들어갔다. 코드를 짜기 전에 먼저 구상을 했다. 게임 흐름 코루틴을 하나 만들어 그 안에서 while문을 돌리면서 시간에 따라 나오는 적을 달리하는 방식의 웨이브 시스템을 생각했다.
그리고 그것을 바로 실행에 옮겼다.
IEnumerator GameFlow()
{
bool wave1boss = false;
bool wave2boss = false;
bool wave3boss = false;
intervalTime = 0f;
List<int> tosummon = new List<int>() { 6 }; // 초록 망치든애
IntervalSummon(tosummon, greenEnemies);
while (true)
{
if (time >= 0f && time < 30f)
{
if (intervalTime >= summonInterval)
{
intervalTime = 0f;
List<int> toSummon = new List<int>() { 6 }; // 초록 망치든애
IntervalSummon(toSummon, greenEnemies);
}
yield return null;
}
else if (time >= 30f && time < 60f)
{
if (intervalTime >= summonInterval)
{
intervalTime = 0f;
List<int> toSummon = new List<int>() { 6, 1 }; // 초록 망치든애, 초록 기사
IntervalSummon(toSummon, greenEnemies);
}
yield return null;
}
else if (time >= 60f && time < 90f)
{
if (intervalTime >= summonInterval)
{
intervalTime = 0f;
List<int> toSummon = new List<int>() { 6, 1, 8, 9 }; // 초록 망치든애, 초록 기사, 초록 창술사, 초록 정예기사
IntervalSummon(toSummon, greenEnemies);
}
yield return null;
}
코드가 너무 길어서 일부만 가져왔다. 간단히 요약하자면, while문 안에서 시간 변수에 계속 deltaTime을 더해주며 시간을 체크하고, 그 시간대에 맞는 적들을 소환하는 방식이다.
그리고 이것을 짜면서 새로 알게 된 사실이 있다. 바로 코루틴 안에서 yield return null의 역할이다.
전에는 그냥 '이걸 왜 쓰지? null 줄거면?' 하는 생각으로 그냥 남들이 다 쓰니까 썼었는데, 이걸 개발하면서 언제 한 번 저 yield return null을 빼먹은 적이 있었다.
그렇게... 유니티는 새하얗게 질려버렸다.
다행히 저장은 했었지만, 도대체 왜 멈춰버리는지를 몰랐다. 알고보니 yield return null은 코루틴 안에서 한 프레임 대기하도록 해주는 역할을 하고있었다. 한 프레임 대기를 하지 않고 무한 반복을 돌려버리니 유니티가 차라리 죽여달라고 할 수 밖에 없던 것이었다...
아무튼 저런 시행착오 끝에 적 웨이브를 완성했다.
네크로맨서 시스템 구현
위의 기획에서 설명했듯이, 빙의, 조복, 러쉬, 시체폭발의 총 네 가지 시스템이 있다. 이중에서 빙의는 선배가 하기로 해서, 조복, 러쉬 , 시체폭발 세개를 구현했다.
조복
IEnumerator Rise()
{
if (spirits.Count == 0)
{
StopCoroutine(UIManager.instance.manaIndian());
StartCoroutine(UIManager.instance.manaIndian());
yield break;
}
if (mana < 20 * spirits.Count)
isSkillDoing = true;
ManageMana(-20f * spirits.Count);
StartCoroutine(UIManager.instance.SkillInitiated("Make Them Immortal.", 1.0f, 20 * spirits.Count));
// yield return waitHalfSec;
audioSource.PlayOneShot(RiseSound);
int count = spirits.Count;
for (int i = 0; i < count; i++) {
StartCoroutine(Revive(spirits[0], 0));
yield return null;
}
isSkillDoing = false;
yield return null;
}
간단하게 설명하자면, 죽은 적군들의 정보를 담고 있는 리스트에 접근해 그 위치에 같은 종류의 아군을 소환하는 로직이다. 그리고 UIManager에 있는 스킬 시전 UI 코루틴을 가져와 스킬을 쓰는 것을 눈으로 확인할 수 있도록 했다.
UIManager도 올리고싶지만, 어김없이 두트윈 떡이라서 굳이 올리지 않았다.
러쉬
러쉬도 조복이랑 비슷해서 금방 짰던것 같다.
그런데 초반의 모든 버그는 여기에서 터져서 고치는데 엄청 애먹었다...
IEnumerator Rush()
{
if (mana < 300f)
{
StopCoroutine(UIManager.instance.manaIndian());
StartCoroutine(UIManager.instance.manaIndian());
yield break;
}
isSkillDoing = true;
isRushing = true;
rushBGM.volume = 1f;
bgm.DOFade(0f, 2f).SetEase(Ease.Linear).OnComplete(() => rushBGM.Play());
StartCoroutine(UIManager.instance.SkillInitiated("Feeling Spirits Of Death...", 3f, 300));
ManageMana(-300f);
yield return waitThreeSec;
StartCoroutine(UIManager.instance.SkillInitiated("Rushing In To The Deep Death.", 30f, 0));
float time = 0f;
while (time <= 30f)
{
yield return null;
time += Time.deltaTime;
while (spirits.Count != 0)
{
StartCoroutine(Revive(spirits[0], 1f));
yield return null;
}
}
isSkillDoing = false;
isRushing = false;
rushBGM.DOFade(0f, 1.5f).SetEase(Ease.Linear).OnComplete(() => rushBGM.Stop());
bgm.DOFade(1f, 2f).SetEase(Ease.Linear);
yield return null;
}
여기에서 버그가 엄청 터졌던 이유는 내가 바보같이 죽은 적군이 없어도 살리려고 했기 때문이다...
그것도 모르고... 다뜯어고친 나 자신 칭찬해...
시체 폭발
말이 좀 무섭긴 한데, 그냥 아군 처치하고 그 주위로 데미지 입히는 로직이다.이 기능의 특징은, 버그의 굴레가 있다는 것이었다...현재는 다 고쳤다.
IEnumerator Explode()
{
if (allies.Count == 0)
{
print("Not Enough Bodies!");
yield break;
}
if (mana < 500f)
{
StopCoroutine(UIManager.instance.manaIndian());
StartCoroutine(UIManager.instance.manaIndian());
yield break;
}
isSkillDoing = true;
StartCoroutine(UIManager.instance.SkillInitiated("Time To Sleep Again.", 1.5f, 500));
ManageMana(-500f);
audioSource.PlayOneShot(explosionSound);
int count = allies.Count;
for (int i = 0; i < count; i++)
{
if (allies[i] != null)
{
GameObject explosionvfx = Instantiate(explosion, allies[i].transform.position, Quaternion.identity);
Destroy(explosionvfx, 3.0f);
allies[i].GetComponent<UnitController>().Stop();
}
yield return null;
}
yield return new WaitForSeconds(1.6f);
count = allies.Count;
for (int i =0; i<count; i++)
{
if (allies[i] != null)
{
GameObject boom = Instantiate(boomer, allies[i].transform.position, Quaternion.identity);
boomList.Add(boom);
allies[i].GetComponent<UnitController>().Die();
}
yield return null;
}
allies.Clear();
isSkillDoing = false;
while (boomList.Count != 0)
{
StartCoroutine(DestroyBoom(boomList[0]));
yield return null;
}
boomList.Clear();
UIManager.instance.AllyTextChange(allies.Count);
yield return null;
}
간단하게 설명하면, 현재 아군 자리를 받아와 그 위치에 콜라이더를 소환하고 닿은 적들에게 데미지를 입히는 것이다.
처음에는 잘 작동하는가 싶다가, QA 과정에서 버그가 발생했다. 그래서 그걸 고치고, 다시 해보면 또 다른 버그가 나오고, 또 고치면 또또 다른 버그가...
이 작업을... 이틀간 했다. 정말 머리털 다 빠지는줄 알았지만 머리털은 생각보다 단단해서 다행이었다.
그리고 프로젝트가 끝나기 몇 시간 전에 버그를 다 잡아내서 정말 인간 승리였다.
이렇게 세 개의 기능을 짠 후에 UIManager와 연결해서 실제로 플레이가 가능한 게임이 나오게 되었다.
UI 제작
네크로맨서 컨셉에 맵 배경이 어두워서, UI도 어두운 쪽으로 가는 것이 좋을 것 같았다. 이리저리 설명하는 것 보다는 사진이 나을 것 같아서 사진으로 올리겠다.
모두 어도비 일러스트레이터로 제작했다.

어디에서 레퍼런스를 얻지 않고 손수 구상 및 제작까지 했다.
확실히 UI를 직접 만들고 직접 구현하니까 코드도 편하고 아트작업도 편했다.
BGM 제작
BGM도 제작했다. 선배님이 전쟁시대 느낌이 나는 배경음악을 원하셔서 최대한 그런 느낌이 나도록 해봤다.
그리고 러쉬 할 때 조금 더 특색을 주기 위해 러쉬 전용 BGM 도 만들었다. 메인 테마를 비슷하게 가져가면서 일렉기타를 강조했다.
이걸 직접 플레이 해볼 수 있다고?
Project RM 처럼 만들래의 10분콘에 출품하여 플레이 해볼수 있습니다!
Kindred Spirits
액션, 전략, 디펜스 | Unity
mandlemandle.com
플레이 해보시고 리뷰 남겨주세요!
이번 프로젝트는 정말 건강을 싹 갈아넣어서 한 것 같다. 솔직히 처음에는 일주일도 안돼서 끝날 프로젝트 같았는데 하다보니 일주일을 꽉꽉 채워서 해버렸다.
그래도 이렇게 단기간에 이정도 퀄리티의 게임을 뽑아낼 수 있는 정도면 그래도 좀 실력이 는 것 같다.
프로젝트를 할 때 마다 기획하고 개발하고 아트하고 사운드를 다 하고있는데, 이러다가 1인개발쪽으로 진짜 빠져버릴 것 같다... 오히려 좋을지도
일주일간 고생한 우리 팀원들 다 박수쳐주고싶다. 물론 나도. ㅎ
모두들 Project KS 완성하느라 수고했다!!

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