[게임만들기 ⑥] 1인 게임 개발에 한 번 도전해보았습니다 - 적 만들기 심화편

기획기사 | 윤서호 기자 | 댓글: 9개 |



대부분 게임에서 초반에는 적들이 비교적 단순한 패턴을 보여주지만, 후반으로 가면 갈수록 다양한 패턴으로 무장한 적들이 등장합니다. 특히나 중간중간 등장하는 보스 캐릭터들은 각기 다양한 패턴으로 유저들의 앞길을 가로막고는 합니다. 그렇게 닥쳐온 어려움을 극복하면서 유저들은 재미를 느끼고는 하죠.

적들이 다양한 패턴을 보여준다는 것은 그만큼 개발자가 손을 대야 할 부분이 많다는 의미이기도 합니다. 패턴이 다양하면 다양할수록, 또 조건이 복잡하면 복잡할수록 여러 각도에서 신경쓸 부분도 많아지죠. 물론 기본 자체는 다르지 않습니다. 애셋을 만들고, 알고리즘을 설계하고, 애니메이션과 스크립트를 만들면서 차근차근 설계에 맞춰 구축한다는 개념은 동일하기 때문이죠.

그렇지만 실제로 그렇게 움직이게 하기 위해서는, 여러 난관을 겪어야만 합니다. 이미 일반 쫄몹을 만드는 과정에서 제가 어떤 난관을 겪고 있는지는 이미 설명했습니다. 보스를 만들려면 더 복잡한 로직과 알고리즘, 그리고 코드를 더해야 하는 만큼 더 큰 난관이 기다리고 있는 셈이죠. 특히나 저처럼 프로그래밍을 기초부터 차근차근 배우지 않아서 직접 코드를 짜지 못하는 경우라면 더욱 그렇습니다.

결론부터 말하자면 지금 보스를 만들지 못하고 있습니다. 비유하자면 보스 트라이를 앞두고 잡몹들의 패턴에 농락당해서 계속 당하고 있다고 해야 할까요? 게임을 할 때와 다른 점이 있다면, 이미 만들어진 패턴에 당하는 것이 아니라 패턴을 만드는 과정을 공략하지 못하고 있다는 점이겠죠.

아마 또 다시 제한된 부분은 어떻게든 편법으로 넘어갈지 모르겠습니다. 다만 지금 부딪힌 문제는 장르의 기본적인 문법에서부터 비롯된 것이기 때문에, 어떻게든 짚고 해결할 필요가 있었죠. 아주 기초적인 부분에서 막히고 있기 때문에 "이런 것 때문에 막힌다고?"라고 생각하실지 모르겠습니다. 그만큼이나 기본기나 기초적인 지식이 중요하다는 것을 다시 한 번 말씀드리면서 지금 겪고 있는 문제와 작업 과정에 대해서 말씀드리도록 하겠습니다.




■ 당면한 과제 1 - 패트롤, 그리고 이를 구현하는 방법

플랫포머뿐만 아니라, 게임을 하다보면 일정 구간을 왔다갔다하는 적들을 흔히 볼 수 있습니다. 이렇게 적들이 특정 구간을 왔다갔다 하는 것을 일반적으로 '패트롤'이라고 하죠. 이런 기본적인 것을 구현할 때에도 난관이 닥쳐오곤 했습니다. 특히나 제가 잘못 이해하고 있던 게 있었던 탓에, 이 문제는 정말 풀리지 않았던 것이죠.


■ NavMeshAgent는 3D용이다 - 대안은 iTween



▲ 결국 기본기를 점검하러 유니티에 다시 찾아갔습니다

"NavMeshAgent는 3D에 주로 쓰이는 방식입니다."

유니티에 방문해서 오지현 에반젤리스트에게 솔루션을 받을 때, 가장 먼저 들었던 말이었습니다. 예시로 나왔던 패트롤 자료들만 보고 NavMeshAgent를 활용해서 패트롤을 구현하려고 했던 저였기 때문에, 이는 상당히 큰 충격이 아닐 수 없었죠.

사실 NavMeshAgent를 배우기도 했고, 실습을 하기도 했지만 그때 3D로 된 물체만 적용했다는 건 까맣게 잊어버렸던 겁니다. 실제로 NavMeshAgent의 용례를 살펴봐도 3D 위주로 되어있었죠. NavMeshAgent를 살펴보면, 3D 물체의 표면에 네비게이션 메시를 깔고, 이를 따라가도록 지시하는 방식입니다. 이를 어떻게 잘 살려보면 2D에도 적용할 수 있지 않을까, 싶었지만 2D와 3D는 방식이 다르기 때문에 이를 활용하기가 어려웠던 것이죠.



▲ 실제로 NavMeshAgent의 용례를 보면, 다수가 3D입니다.
바닥에 깔아둔 Mesh를 따라 물체가 이동하도록 하는 방식이죠.

이를 대신하는 것이 iTween이었습니다. iTween은 애셋 스토어에 올라온 2D 게임 내에서 많이 사용하는 함수들을 모아둔 일종의 라이브러리로, 좀 더 간단하게 짠 코드만으로도 반복적으로 활용되는 동작들을 쉽게 구현할 수 있도록 했죠.

다만 iTween은 라이브러리에서 저장된 것을 불러오기 위해서 또 다른 공식을 이해해야 하는 만큼, 이를 바로 적용하기는 어려웠습니다. 시험삼아서 몇 번 iTween을 활용했지만, 생각한 것만큼 매끄럽게 문제가 해결되지 않았거든요. 물론 iTween에는 제가 필요로 하는 기능이 있다보니, 조금 더 연구를 거쳐서 적용할 필요는 있었습니다.



▲ 2D 게임을 위한 에디터 라이브러리, iTween



▲ 동작 원리는 사전에 함수들과 수식을 만들어두고, 이를 호출해오는 방식입니다



▲ 다만 언어의 압박이 조금 있는 편입니다.



■ iTween이 능숙하지 않아 선택한 임시방편, 그리고 또 발발한 문제

일단 iTween을 활용한 패트롤은 뒤로 제껴둔 채, 지금 단계에서 구현한 방식은 기본 중의 기본인 '충돌'을 활용한 방법입니다. 적에게 웨이포인트라고 태그가 걸린 물체가 닿으면, 반대편으로 이동하도록 하는 방식이죠. 코드로 짜면 대략 이렇습니다.

public class BkleinMovement : MonoBehaviour {

public float MoveSpeed = 10;

void FixedUpdate()
{
transform.Translate(new Vector3(MoveSpeed, 0, 0) * Time.deltaTime);
}

void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag("Waypoint"))
{
MoveSpeed *= -1;
}
}
}

간단하게 말하면, Waypoint라고 태그를 걸어둔 물체에 닿으면 이동 방향이 반대로 바뀌도록 짠 것입니다. 오브젝트의 위치, 회전, 그리고 스케일을 저장하기 위해서는 '트랜스폼(Transform)'이라는 것을 가지게 되죠. 트랜슬레이트(Translate)는 이 트랜스폼이 있는 오브젝트를 이동시킨다는 의미입니다. 이를 벡터3로 지정해서 어느 축에서, 어떤 방향으로 얼마나 이동할지 new Vector3(x, y, z)로 표시해주는 것이죠. Time.deltaTime은 프레임이 완료되는 시간을 나타내는 것으로 매 프레임마다 어떤 값을 더하거나 빼는 계산을 하는 경우에 이를 곱해서 사용하게 됩니다. 즉 매 프레임마다 계속 제가 지정한 MoveSpeed의 값만큼, 물체가 좌표를 이동하도록 한 것이죠.



▲ 웨이포인트를 설치하고, 그 웨이포인트 사이를 왔다갔다하도록 한 것이죠.

앞서 본 것처럼 게임 엔진 내에서 물체의 위치나 움직임은 함수를 활용해서 계산합니다. 함수 그래프를 잠깐 생각해보면 x축 기준으로 양수는 오른쪽, 음수는 왼쪽에 위치하죠. 한 번 충돌하면 MoveSpeed의 음, 양의 부호가 반대가 되기 때문에 물체가 반대로 이동하는 식으로 구현한 것입니다.

여기까지는 간단한 과정이었지만, 눈썰미가 좋은 분이라면 여기서 한 가지 더 지적을 하실 겁니다. "그러면 이미지는 어떻게 되나요?"라는 질문 말이죠. 예, 여기서 문제가 발생했습니다. 이동방향을 바꾸는 것은 성공했지만, 물체가 보는 방향에 대해서는 언급을 하지 않았기 때문에 방향은 그냥 처음에 설정한 그대로 보게 되는 것이죠.



▲ 물체가 보는 방향에 대해서는 언급을 안 했기 때문에 움직임과 따로 놀게 됐습니다

이 문제를 해결하기 위해서 흔히 드는 예제는, Flip()이라는 함수를 구축하는 방식입니다. void Flip()이라는 함수에 스케일을 변경하는 식을 작성해둔 뒤, 특정 조건이 발생하면 이를 호출하는 식이죠. 물체의 스케일은 음수냐 양수냐 따라서 방향이 바뀌기 때문입니다. 이를 적용하기 위해서는 일반적으로 사전에 facingRight에 대한 정의를 추가한 뒤, 흔히 이와 같은 식을 예로 듭니다.

void Flip()
{
facingRight = !facingRight;
Vector3 theScale = transform.localScale;
theScale.x *= -1;
transform.localScale = theScale;
}

그리고 이 함수를, 특정 조건에 따라서 적용하도록 배치하는 것이죠. 여기까지는 문제가 없지만, 또 한 가지 문제가 발생합니다. 제가 설계한 함수에서는 어디에 배치해도 Flip() 함수를 한 번 호출한 뒤, 이 값이 유지가 되지 않는다는 점이죠. 왜냐하면 매 프레임마다 호출되는 정보는 FixedUpdate()에서 처리하고 있는데, 여기에 스케일이 바뀌었다는 정보는 뜨지 않기 때문에 충돌이 일어난 프레임을 제외하면 그대로 유지가 되는 것이죠.



▲ 충돌할 때는 "CRUSH", 플립이 일어날 때는 "Reverse"가 뜨는데



▲ 플립 이후에 그 데이터가 보존이 되지 않아서



▲ 이런 문제가 발생하고 있습니다

NavMeshAgent를 오용한 사건 이후 코드나 함수를 따올 때는 활용한 예제까지도 살펴보게 됐는데, Flip()의 경우는 대체로 플레이어 캐릭터의 방향을 전환할 때 많이 사용됩니다. 이런 점 때문에 이번에도 Flip()이 아닌 다른 대안을 써야 하는 것 같지만, 이 논리를 어떻게 기계가 이해할 수 있는 방향으로 짤지는 현재까지는 뚜렷한 답을 못 찾은 상태입니다.



▲ 정말 말도 안 되는 짓이지만, FixedUpdate()에서 호출해버리게끔 짜기도 했습니다

이 때문에 찾은 임시 방편은, 앞뒤 모습이 똑같은 캐릭터를 추가하는 방식이죠. 그렇게 해서 뫼비우스퀘어 등, 간단한 몹을 즉석에서 만들어서 추가했습니다. 물론 이건 어디까지나 임시 방편이고, 이 문제는 확실하게 해결할 필요는 있습니다. 단순히 비클라인이라는 개체에 한정해서 코드를 만들긴 했지만, 이 이동 방식은 대체로 모든 적들에게 공통으로 적용되기 때문이죠.



▲ 임시방편으로 더 간단한 모양의 적을 만들었지만, 결국 이 문제는 한 번 짚고 넘어가야 합니다



■ 당면한 과제 2 - 파편이 튀는 것, 그리고 오브젝트 발사 문제



▲ 폭발한 뒤, 파편으로 타격을 입히는 적 유형을 묘사하려면 파편 처리도 필요합니다

또 다른 난관은, 특정 패턴을 구현할 때 특수 효과를 어떤 방식으로 나타내야 할까 하는 문제였습니다. 결국 패턴은 어떤 동작만으로 구성되는 것이 아니라, 그에 맞는 특수 효과가 뒷받침이 되어야 비로소 완성이 되기 때문입니다. 예를 들자면 어떤 대상이 폭발할 때 사방으로 튀는 파편이나 불꽃이 없으면 어딘가 섭섭한 것처럼 말이죠.

폭발물이 터졌을 때, 파편이 튀는 것을 하나하나 표현하기 어렵습니다. 특히나 그 파편이 하나하나 물리값을 가진다면, 더욱 더 그렇죠. 그 패턴을 일일이 만드는 것도 일이고, 그 자잘한 파편이 하나하나 움직이는 것을 애니메이션을 주는 것도 일입니다.

이에 대한 편법으로 '파티클 시스템을 활용하면 어떨까?'라는 생각으로 접근했습니다. 더군다나 Collision이라는 문구를 보자마자 바로 혹했죠.


■ Particle System은 시각적 효과 위주, 폭발할 때 파편에 충돌을 주려면 수작업이 더 간편하다.




물론 예상하셨겠지만, 파티클 시스템은 그런 식으로 활용되지는 않았습니다. 주로 특수 효과에서 나오는 시각적인 효과에 치중해있었죠. 여기서 말하는 Collision은 데미지를 입히는 충돌이라기보다는, 어떤 오브젝트에 부딪혔을 때 튀는 효과를 표현하기 위한 것입니다. 불꽃을 예로 들자면, 불꽃이 벽에 부딪혔을 때 그냥 떨어지지 않고 반작용으로 일정 거리 튀어나온 뒤에 떨어지는 것을 표현하는 것이죠.

뿐만 아니라 이 Collision에는 이벤트를 호출하거나 하는 것이 굉장히 까다로웠습니다. 애니메이터를 별도로 사용하는 것이 아니다보니, 이벤트를 끼워놓고 호출하는 기본 공식을 활용하기가 어려웠거든요. 그렇기 때문에 폭발해서 파편이 튀고, 이 파편으로 적에게 데미지를 입힌다는 공식은 다른 방법으로 구현해야 했습니다.



▲ 일반적인 Collider, Trigger 세팅과는 좀 다르고, 용법도 다릅니다

가장 단순한 방법은 파편이 터지는 애니메이션을 수작업으로 구현하는 방법입니다. 파편이 터질 때 나타나는 위치와, 최종적으로 날아갈 위치를 정해서 키값을 주고 재생하는 것이죠. 자잘한 파편 여러 가지를 한꺼번에 구현하기에는 번거롭지만, 가장 확실하게 원하는 대로 파편이 튀는 궤도나 속도를 조절할 수 있다는 장점이 있죠. 뿐만 아니라 이를 까다로운 공식 없이도 단순하게 키값을 주는 것만으로 할 수 있다는 것도 장점입니다. 조금 더 활용할 줄 알면, 키값 중간중간에 이벤트 함수를 호출해서 다양한 효과를 줄 수 있죠.



▲ 가장 단순한 방법은 파편 하나하나에 키값을 주고



▲ 특정 이벤트에서 재생하도록 하는 방법입니다

다만 파편이 많아질수록, 이 방법은 사용하기가 어렵습니다. 하나하나 키값을 다 일일이 주는 것이 번거롭기 때문이죠. 그리고 파편의 애셋을 하나하나 적용하는 것도 문제가 되죠. 이런 상황에서 오지현 에반젤리스트가 제안한 해법은 파편 애니메이션이나 애셋은 애셋스토어에서 구하고 파편 각각에 Collider를 넣는 것이 아니라 특정 범위에 Collider를 두고 IsTrigger를 설정하는 방식이었습니다.

일일이 표현하기 어려울 정도로 많은 파편이 튀는 상황이라면, 물리적으로 회피하기가 어려운 상황이기 때문에 아예 그 범위 내에 있는 모두에게 데미지를 준다는 식으로 해석을 하는 것이었죠. 애니메이션도 파편 하나하나를 키 애니메이션으로 녹화하는 것이 아니라, 폭발하면서 방사형으로 파편들이 튀는 그 모습 자체를 스프라이트 애니메이션으로 구현하는 식입니다.



▲ 폭발범위를 정해두고, 폭발할 때 해당 범위에 있는 타겟에게 피해를 입히는 코드를 불러오는 방식입니다

파티클 시스템으로 무언가를 만들어내기는 어렵지만, 그래도 시각적인 효과를 주기 위해서 활용할 여지는 있었습니다. 공상어가 헤엄을 칠 때 그냥 헤엄치는 모션으로는 허전하기 때문에 여기에 파티클을 주면서 이펙트를 주거나 할 수 있겠죠. 다만 2D 스프라이트, 그것도 지극히 간단한 그래픽으로 구현한 만큼 세팅을 어떻게 해야 좀 더 자연스러울지는 여러 가지 시도를 거칠 필요가 있었습니다.


■ 총알이나 투사체, 언제 어느 때 어떻게 발사할까요? - 함수, 법칙, 그리고 응용




"함수 안의 식이 지금의 상황과는 좀 안 맞는 거 같은데요. 살짝 바꿔야 될 거 같아요"

코드를 긁어올 때 으레 발생하는 일 중 하나입니다. 원전에서는 잘 돌아가는 코드가, 자신이 만든 것에 적용할 때는 상황이 다르기 때문에 기능을 하지 않는 것이죠.

예전에 예시를 든 포탑, KleinHowitzerB는 원전인 GucioDev의 포탑과는 조금 다르게 설정을 했습니다. 공격 범위가 한쪽에만 한정이 되어있고, 애니메이션 구현 방식이나 이벤트 호출 방식도 살짝 달랐죠. 그랬기 때문에 타겟을 포착하고, 그 방향으로 탄을 날리는 수식도 조금 달라져야 했습니다.

또 한 가지 문제는, 이 코드는 플레이어의 위치에 따라서 탄이 그 방향으로 발사되도록 한 코드인데 그냥 적용했다는 점이었죠. 이런 코드도 필요하지만, 단순히 직선으로 포탄이 발사되는 코드도 작성할 필요가 있었습니다. 뿐만 아니라 자신의 위치가 고정되지 않은 상태에서 적을 포착하면 움직임을 잠시 멈추고 탄을 쏘거나, 혹은 이동하면서 탄을 쏘는 코드도 필요했죠. 플랫포머에 나오는 적들의 유형을 생각하면 포탄 외에도 비행하는 적들은 종종 그런 패턴을 보여주곤 하니까요.



▲ 일단은 방향을 산출하는 방식이 기존과 차이가 생겨버려서 이를 반영했지만



▲ 총알이 생성되는 지점, 각도, 궤도, 발사 패턴 등의 문제가 남아있습니다

이를 구현하기 위해서는 타겟의 위치를 어떻게 산출해내는지, 공식을 좀 더 살펴볼 필요가 있었습니다. target이나 position을 활용하고, 수학 공식을 도입해서 만든다는 것까지는 대강 알지만 이것만으로는 부족했던 것이죠. 정확하게 표현하면 x, y, z축상에서 움직이는 물체의 좌표값을 구하는 함수인데, 이 함수에 사용되는 수식이나 함수를 구현하는 방법을 좀 더 세밀하게 알아야 원하는 대로 구현할 수가 있었습니다. 물론 이것을 제가 단시간에 구현하는 것이 어려웠기 때문에, 다른 사람들이 짠 코드를 참고해서 수정하는 것이 현 단계에서는 최선이었죠.

이런 조건이 없이 단순히 탄을 발사한다고 하면, 그 수식 자체는 그리 어렵지는 않았습니다. 탄환을 계속 생성하는 코드와, 이 탄환이 특정 방향으로 가도록 힘을 가하는 것만 구현하면 되기 때문이죠. 물론 탄의 궤도나, 포탄의 움직임까지 고려하면 이것만으로는 부족합니다. 여기에 다양한 것들이 더 추가가 되어야 하죠. 조건이 쌓이면 쌓일수록, 다른 조건과 충돌할 여지가 있기 때문에 이를 사전에 정리해서, 충돌하지 않도록 논리를 구성하는 능력도 필요했습니다. 그 논리나 알고리즘, 수식을 짜기 위해서 학창시절 이후 오랜만에 종이 노트에다가 적어가면서 공부하고는 있지만, 그 결과가 어떻게 나올지는 아직까지는 미지수입니다.









▲ 사실 애초에 기획서를 깔끔하게 썼으면 이 과정이 좀 더 순탄했을 겁니다



■ 당면한 과제 3 - 본 애니메이션에서 스프라이트 애니메이션 적용하기



▲ 위로 베기 위해서는 그립을 반대로 쥐어야 하는데, 그러려면 스프라이트가 바뀌어야 합니다

처음에 본 애니메이션의 문제점을 발견했던 건 주인공 채림의 액션을 구현하던 중이었습니다. 특수 점프 공격인 '상승참'의 모션을 구현하는데, 손의 스프라이트를 바꿔야 하는데 스프라이트 메시로 본에 바인딩된 상태에서는 불가능했던 것이죠.

이런 제한은 2D 스프라이트 기법을 활용하는 게임을 개발할 때는 아주 큰 문제가 됩니다. 캐릭터의 상태 변화 등을 모두 스프라이트로 구현해야 하기 때문이죠. 특히나 캐릭터의 상태가 자주 변하는 보스나 중간 보스라면 이 문제가 더 체감이 될 수밖에 없습니다.



▲ "얘 나중에 스프라이트 막 바꿔야 하는데, 그거 안 되면 곤란해요"

다행히도 이 문제는 어느 정도 해결 방안은 있었습니다. 스프라이트를 불러오는 함수를 집어넣고 호출하는 방식이 있었기 때문이죠. 물론 이 스프라이트를 어떤 상황에서 불러올지는, 함수를 어떤 식으로 호출하느냐에 따라서 달라질 수밖에 없었습니다.

제가 원하는 방식은 특정 동작을 재생할 때에만 스프라이트가 변하는 방식이었습니다. 그러려면 스크립트에 해당 함수를 구현한 뒤에, 애니메이션 키에다가 '이벤트'를 추가하는 것이죠. 이것만으로 끝이 아니라, 다른 애니메이션과 트랜지션이 일어날 때 리드 타임과 그 때문에 동작이 매끄럽게 이어지지 않는 것도 다듬을 필요가 있었죠.

▲ 본 애니메이션에서 스프라이트 스왑에 대한 해설(출처:Sergio's Tech)

또 한 가지 문제는, 스프라이트가 달라지면서 Collider의 크기도 변해야 한다는 문제입니다. 단순히 보스몹이 스케일만 커지는 것이라면 문제가 없지만, 아예 스프라이트의 모양이 변하면서 크기가 변하면 그 모양에 맞춰서 Collider도 변할 필요가 있죠. 이 역시도 이벤트를 추가하고, 함수를 불러오는 방식으로 해결할 수는 있습니다. 빈 오브젝트를 만들고, 그 오브젝트에 충돌체를 부여한 뒤에 평소에는 비활성화된 상태로 있다가 특정 상황에서만 활성화시키는 함수를 적용하면 되니까요. 물론 그 비활성화와 활성화 여부를 어떤 식으로 그 스크립트에 전달하느냐, 하는 문제가 남긴 합니다.



▲ 이랬던 녀석을



▲ 이렇게 패턴과 애니메이션을 만들어 가기 위해선 갈 길이 아직 남았습니다



■ 보스를 만들기 전까지 아직 남아있는, 다양한 과제들

여러 가지 설명을 드리긴 했지만, 간단하게 요약하자면 "아직 준비가 안 됐다"라고 할 수 있겠습니다. 네, 보스 잡으러 갔는데 그 앞에 놓인 잡몹들에게 두드려맞고 골골거리는 셈이죠. 그것도 다크소울에 나오는 잡몹 정도가 아니라, 슈퍼마리오 1-1 스테이지의 중간도 못 가서 게임오버가 됐다고 비유하면 될 거 같습니다.

걷지도 못하는데 뛰려고 발악하는 셈이 되어버렸지만, 어쨌든 여전히 편법과 임시방편을 활용해서 오기는 했습니다. 그렇지만 보스의 패턴을 생각하면 생각할수록, 여러 가지로 제가 넘어야 할 산들이 많다는 것을 실감하고 있습니다.






▲ 발판한테도 농락당하는 클래스인데, 무리한 것 아닌가 싶어지기도 합니다...

가장 기본적으로 구현해야 할 공상어의 패턴은 여러 가지입니다. 디폴트인 헤엄치기에서부터 위로 솟구치는 동작, 그때에 부가적으로 생겨나는 물결과 파도, 입을 크게 벌리는 모션과, 화면 밖에서 갑자기 화면을 집어삼킬 정도로 입을 크게 벌리는 묘사까지 다양한 것을 소화해내야 하죠.






▲ 대강 구상한 것만 구현하려고 해도 지금 단계에서는 상당히 난이도가 높습니다

이 중에서 당장에 수식을 작성할 수 있는 것은 몇 개 안 되고, 나머지는 다른 함수나 수식들을 따와서 수정하고, 편집을 해야 하죠. 이뿐만 아니라, 그 함수를 호출할 수 있는 조건을 작성하고 알고리즘을 구축해야 합니다. 더 문제는 알고리즘을 만들었어도, 이 알고리즘을 기계가 이해하도록 짠 것인지 확신이 들지 않는다는 점이죠. 그래서 테스트를 하고, 로그를 계속 보면서 분석하고 어떻게 만들어가면 좋을지 다시 궁리하는 것을 반복해나가고 있습니다.

마음 같아서는 바로 보스를 어떤 식으로 만들었나 소개하고 싶지만, 이번에도 여전히 제 역량의 부족함만 소개한 것 같아 조금 심란합니다. 처음 시작할 때 "플랫포머는 그래도 좀 쉽지 않을까"라고 만만히 봤던 과거의 제 자신에게 한 마디 해주고 싶은 심정입니다. 마치 데드풀처럼 말이죠. 그게 불가능한 만큼, 현 단계에서 이제 시급하게 해야 할 부분이 무엇인지 파악해나가야 하죠.

아마도 한동안은 스프라이트 작업을 다시 들어가고, 알고리즘을 다시 짠 뒤에 이를 적용하면서 테스트하는 과정을 거칠 예정입니다. 이 과정은 단순 반복 작업인 만큼, 그 다음 단계에 들어갈 때에 또 다시 찾아뵙도록 하겠습니다. 그 다음 단계의 후보는 보스 패턴의 완성, 혹은 스토리 컷씬 작업, 사운드 기획서 등을 예상하고 있지만, 그대로 흘러갈지는 조금 더 지켜봐야 될 것 같습니다.



▲ 앞으로도 갈 길은 멀고도 험합니다

※그래서 요약하자면?

1) 적의 패턴이 다양하면 다양할수록, 미리 구축해야 할 것들이 많다. 그리고 그 조건들을 딱딱 맞게 배치해야 한다.

2) 그 복잡한 패턴을 가진 보스로 넘어가기 전에, 잡몹들의 일반적인 패턴을 만드는 것에서부터 막혀버렸다. 기본적인 것이 아닌, 심화 과정에서 막혔다면 어떻게든 편법을 활용할 수 있지만 그것을 적용하기 어려운 단계에서부터 난관에 봉착했다.

3) 첫 번째 문제는 패트롤 구현이었다. 3D에 주로 활용하는 NavMeshAgent를 이용해서 패트롤을 구현하려고 했기 때문에 실패로 돌아갈 수밖에 없었다. 이를 2D에 적용하는 방법이 있지만, 그보다는 iTween이라는 일종의 라이브러리를 활용하는 방식을 활용하는 것이 좋다는 조언을 받았다.

4) iTween의 기능에는 유용한 것이 많지만, 지금 당장 사용하기엔 이해도가 떨어져서 이를 활용하지 않고 간단한 수식을 활용해서 패트롤을 하는 코드를 짰다. 해당 코드는 다음과 같다.

public class BkleinMovement : MonoBehaviour {

public float MoveSpeed = 10;

void FixedUpdate()
{
transform.Translate(new Vector3(MoveSpeed, 0, 0) * Time.deltaTime);
}

void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag("Waypoint"))
{
MoveSpeed *= -1;
}
}
}

요는 웨이포인트라는 태그가 걸린 물체에 닿으면, 이동방향이 반대로 가도록 짰다는 것이다. 즉 양쪽에 웨이포인트를 설치하고, 그 사이를 계속 왔다갔다 하도록 한 것이다.

5) 다만 이 코드에는 오브젝트가 이동방향에 따라서 몸을 틀도록 하는 파트가 없다. 주로 나오는 예제는 Flip()이라는 함수를 만들어두고, 이 함수에다가

void Flip()
{
facingRight = !facingRight;
Vector3 theScale = transform.localScale;
theScale.x *= -1;
transform.localScale = theScale;
}

이 코드를 작성한다. 해당 코드는 오브젝트의 스케일 x축의 음양 부호가 바뀌면, 오브젝트의 방향이 바뀐다는 것을 이용한 것이다.

6) 이 코드를 어떻게 적용해야 한 번 오브젝트의 스케일이 바뀐 이후, 이 데이터를 계속 유지하는지가 문제였다. 이 방법을 찾고 있는 중이다.

7) 그 방법을 찾기 전에 위의 수식들을 활용할 수 있는 간단한 잡몹을 추가했다. 그래서 나온 것이 뫼비우스퀘어다. 그리고 움직이는 블록에는, 이 수식을 적용했다.

8) 파티클 시스템을 활용해서 파편을 표현하려고 했지만, 파티클 시스템은 시각적인 효과를 표현하는 데에 좀 더 집중했기 때문에 적에게 피해를 주는 파편을 표현하기엔 적합하지 않았다.

9) 타격을 주는 파편을 구현하는 방법은 크게 두 가지다. 하나는 직접 하나하나 움직여서 키값을 주고, 이벤트 때 호출하는 방식이다. 이 방법은 번거롭긴 하지만, 가장 간단하고 확실하게 자신이 원하는 대로 파편의 움직임이나 데미지를 컨트롤할 수 있다.

10) 또 다른 방법은 파편들 각각에 Collider를 주는 것이 아니라, 폭발이 일어나면 특정 범위 전체에 데미지가 가도록 짜는 방식이다. 오브젝트를 중심으로 빈 오브젝트를 주고, Circle Collider를 넣은 다음에 IsTrigger를 주고 이 트리거가 발동하는 조건이 되면 플레이어에게 데미지를 주게끔 함수를 짜는 것이다. 주로 파편이 너무 자잘하고 많아서, 물리적으로 피하기 어려운 상황이라고 가정했을 때 활용한다.

11) 파티클 시스템은 시각적인 효과를 이용할 때 유용하지만, 2D 스프라이트, 그것도 간단한 그래픽으로 구현한 현 작품에 맞는지는 다시 확인해야 한다.

12) 탄환을 발사하게 하기 위해서는, 탄환을 생성하는 코드와 그렇게 만들어진 탄환이 특정한 방향으로 이동하도록 하는 코드를 구현해야 한다. 그 외에 탄환이 생성되는 조건이나, 날아가는 방향을 정하려면 다른 수식들이 필요하다. 예제로 든 코드는 적이 있는 방향을 체크하고, 그쪽으로 탄환이 날아가도록 한 것이었는데 적 방향을 계산하는 방식이 달라졌던 터라 작동하지 않았었다.

13) 공식과 수식 자체를 몰라도 이런 걸 만들 수 있긴 하지만, 정확하게 의도한대로 만들기 위해서는 이를 좀 알 필요가 있었다. 현 단계에서는 남이 만든 것을 수정하면서, 최대한 의도에 맞게끔 수정하는 것이 최선이었다.

14) 본 애니메이션에서 스프라이트 교체를 하려면, 스크립트를 활용해야 한다. 애니메이터에 이벤트를 부여하고, 그 이벤트가 호출될 때 애니메이터상에서 스프라이트가 교체되도록 하는 방식이다. 다만 이 방식만으로는 완벽하게 모든 패턴을 구현할 수 없기 때문에, 다른 방법도 추가로 고안해야 했다.

15) 구상해둔 공상어의 패턴은 다음과 같으며, 이를 구현하기 위해서 알고리즘을 짜고 있지만 그 알고리즘이 과연 기계가 이해할 수 있는 방식인지는 확신이 들지 않는다.

댓글

새로고침
새로고침

기사 목록

1 2 3 4 5