개발 과정을 상세히 기록하기 위해 GIF 파일이 첨부된 경우가 많습니다.
최근에는 이미지를 최대한 압축하여 첨부하고 있으나 GIF 이미지 특성상 MP4 영상보다 용량이 큰 경우가 많다보니,
로딩까지 다소 시간이 걸리더라도 기다리며 읽어주시면 감사하겠습니다.
9월 27일
프로젝트 문 피주빈 님(UI/UX 디자이너)
게임을 플레이하는 유저의 입장에서 플레이하던 게임을 직접 만드는 개발자의 입장으로 변한 케이스. 이 분께 가장 궁금했던 점은 게임의 어떤 부분이 그렇게 좋아서 진로를 바꿀 정도인지 그 요소였다. 말씀하시기로는 다른 게임에서 볼 수 없는 독창적인 세계관에 매료되어 지금까지 오게 되어 일명 '성덕'이 되었다고 하셨다. 그렇다면 좋아하는 것이 '일' 이 되었을 때의 기분은 어떨까? 명확한 답을 듣지는 못했지만 최소한 퇴사 생각이 들지는 않는다고 하셨다. 업무 강도가 굉장히 강한 것에 비해 만족도가 높다는 얘기로 들렸다.
그래서 '프로젝트 문'의 림버스 컴퍼니를 간단히(한 시간 정도) 플레이해봤다. 사실 이런 서브컬쳐 장르는 이전에도 많이 시도해봤으나 게임이 주는 맥락이 와닿지 않아 번번히 실패하곤 했었다. 림버스 컴퍼니도 마찬가지였다. 일러스트와 텍스트를 통한 스토리텔링은 (최소한 내겐) 지루하고 번거로운 일처럼 느껴졌다. 스스로 그 세계관의 깊이를 느껴봐야 두터운 팬층이 형성될 수 있었던 이유를 분명하게 파악할 수 있을텐데, 당장 이 장벽을 깨기는 어려울 것 같다. 물론 UI 디자인은 훌륭했다. 감히 평가할 수 없을 정도다. 단순히 UI가 '화면 위에 얹혀져있다' 는 느낌이 아닌, 인게임 요소와 함께 어우러져 '주체적으로 존재한다' 라는 느낌이 강하게 들었다(실제로 애니메이션을 통해 움직이기도 한다).
한 사람의 진로마저도 바꿀만한 게임. 결국 게임 내에서의 경험이 게임 바깥까지도 이어져야만 가능한 것일텐데. 그정도의 강렬한 여운을 남기기 위해 필요한 요소가 무엇일지 더욱 고민할 필요가 있다.
-
10월 1일
샌디플로어 황성현 님(팀 디렉터)
이 분은 팀의 단순 구성원보다는 이끄는 역할을 맡고 계시다 보니 내가 볼 수 없는 혹은 떠올리지 못했던 그런 부분들에 대해 많이 알고 계셨고, 많이 알려주려 하셨다. 그리고 (내가 정확히 이해한 게 맞다면) 게임을 기획하는 데에 있어 팀 밸류, 오더, 유저 경험(재미) 순으로 우선시 하는 걸로 보였다. 먼저 각 팀 구성원의 특성을 파악하고, 어떤 방향으로 성장할 수 있는지 혹은 하고 싶은지 등에 대해 끊임 없이 소통하며, 팀 전체의 가치를 끌어 올리는 것. 그리고 그와 동시에 자신이 소속된 회사가 원하는 방향과 유저가 원하는 재미를 추구하는 것. 사실 내용 자체만 보면 너무나도 이상적이어서 실천이 가능할까 싶기도 하지만, 본인이 생각한 바와 몸소 겪은 과정 그리고 이에 대한 결과까지 모두 제시해주셔서 납득할 수 밖에 없었다.
그래서 우리 팀에 이를 어떻게 적용할 것이냐? 에 대해 팀장과 이야기를 했다. 일단 디렉터 님이 말씀하신대로 우리가 어떤 부분에서 성장할 수 있고 또 성장하고 싶은지, 그리고 어떤 방식의 스트레스를 받아야 이를 견뎌내고 한 단계 더 나아갈지. 이 부분에 대해서는 각자 생각을 한 뒤에 의견을 나눠보기로 했다. 그 뒤에 팀장인 도현이가 프로세스를 짠 뒤 오더를 내리면, 난 그 부분에 대해 충실히 실행하는 방향으로. 물론 그 과정에서 나오는 결과물이 결국 유저에게 와닿지 못할만한 정도라면 가차 없이 폐기하고 다시 만들어야겠지만.
9. 30 ) 에셋 제작 시작 -> 에셋 제작완료
제작을 하며 어떤 느낌을 내기 위해 이런 리소스들을 사용했는지 조금 알 수 있게되었다. 우리가 만들어가고자 하는 컨텐츠에 따라 얼마나 직관적이면서 많은 것을 담아야하는지 고민을 하게 되는 계기가 되었고 리소스들을 찾는 와중에도 필요하다면 리소스에 맞춰서 원하는 리소스를 제작하는 방향도 생각해 봐야할 것 같다.
10.2 ) 슬레이더 스파이어 분석자료에 버프, 디버프 시트 추가하여 현재 1차 정리 완료
적의 시스템을 버프로 구현하다보니 생각보다 많은 양이 나왔지만 2차 가공으로 적의 시스템을 구별하고 3차로 파워카드들을 정리하면 실제 유동적으로 변하는 버프, 디버프를 유추할 수 있을 것 같다.
그럼에도 불구하고 많은 양의 버프, 디버프가 있었으며 이것들이 어떤 가중치로 변하는지 파악을 빠르게 해야 R&D를 진행하고 이후에 레벨기획에서 더 빠른 결과를 도출하고 많은 피드백 횟수를 얻을 수 있으니 결국 병행하면서 체계적으로 진행해야 할 듯 싶다.
마우스를 올렸을 때 마우스와 상호작용 되는 범위를 특정해보았다. 캐릭터는 총 2 구획, 적은 총 3구획으로 상호작용 구역이 나눠져 있었다.
카드와의 상호작용은 캐릭터에 외접한 사각형 구획에서만 작용되며 상태를 확인하는 구획은 위의 총 구획들과 동일하다.
버프 및 디버프 구획은 체력과 함께 한 구획으로 이루어져 있으며 늘어날 때마다 구획 안에서 추가가 된다.
DOTween을 이용하여 카드 뒷면의 페이드 아웃과 카드 전체의 스케일을 조절해 보았다.
마지막으로 정리할 때 페이드 아웃을 넣지 않았지만 전체적인 구성을 구현해보았다.
페이드 아웃을 위한 자식 오브젝트로의 접근이 생각보다 쉽지않았지만 방법이 보였다.
다음에는 하나의 시퀀스로 만들어보면 될 것 같고 다양한 방법으로 시도하여 팀원과 공유할 때 바로 이해가 가도록 가공을 해야겠다.
가공하기 전 카드를 펼칠 때 높이가 제대로 구현이 안된 것을 발견하였다.
밑의 첫번째 GIF를 보면 확실히 알 수 있을 것이다.
이를 해결하기 위해 높이에 수식을 넣어서 해결하려고 0.2f를 카드 위치 당 부과하였다. 하지만 원했던 모양이 아닌 중앙부만 제대로 곡선을 그렸고 바깥으로 벗어날 수록 오히려 카드 위치가 더 높아지는 느낌이 났다.
그래서 수치 별로 분석하여 정확한 위치의 좌표에서 각 카드의 위치와 중앙 위치의 차이를 수식으로 정리하다보니 피보나치 수열이었다. 이를 이용하여 다시 식을 도입한 결과 정확한 결과가 2번째 GIF에 나타났다.
이후 모든 버튼의 동작을 하나의 시퀀스로 연결해보았다.
모든 시퀀스가 정상 작동했지만 애니메이션 작동시간 안에 버튼을 클릭할 시 코루틴이 겹쳐 버그가 생기는 걸 3번째 GIF, 4번째 GIF에서 확인할 수 있다.
두 GIF의 문제는 DOTween.KillALL();을 사용하여 처리하였더니 문제가 해결되었다.
이를 해결하며 원본과 비슷하게 1차 가공을 진행하였다. 원본과 얼마나 비슷한지 5번째 GIF와 6번째 GIF, 7번째 GIF를 보면 확인할 수 있다.
애니메이션의 작동 시간과 fade 아웃을 조금만 더 가공하면 비슷한 결과가 나올 것 같다.
도현 팀장이 만든 애니메이션을 토대로 다듬어보고 있다.
팀장이 분석한 결과 1, 2번째 카드는 거의 동시에 빠르게 드로우되고, 이후 카드들은 일정한 간격에 따라 드로우된다.
이 부분을 표현하기 위해 First Draw Speed(1 ~ 2번째 카드 사이의 드로우 텀)와 Draw Speed(그 외 카드 드로우 텀)을 슬라이드로 만들어 작동 여부를 확인했고, 이제 홀수가 아닌 짝수의 카드가 핸드로 들어올 때 중앙 축을 기준으로 다시 정렬되는 부분을 구현해야 한다.
이제 홀수가 아닌 짝수의 카드가 핸드로 들어올 때를 구현해보려 한다.
카드가 10장일 때 중앙 축을 기준으로 비교해보면 우측의 카드가 코스트 칸을 기준으로 좌측 카드를 약간 포개며, 이때 포개는 면이 굉장히 예리한 각을 형성한다.
따라서 핸드 내 카드 수가 짝수일 때는 중앙 축 기준 우측 면에 있는 카드들이 좌측 면에 있는 카드들보다 살짝 왼쪽으로 치우치게끔 연출했다.
이제 꽤나 그럴듯해졌으나 핸드의 카드가 방사형으로 펼쳐질 때 양 끝에 다다를수록 펼쳐지는 면적이 작다는 점을 구현해내지 못했다. 그래서 10장의 카드 모두 핸드에 들어왔을 때 화면에서 벗어나게 된다.
핸드의 양끝으로 갈수록 보이는 면적이 더 좁아지도록 수정은 했으나, 핸드가 가득 찼을 때 한눈에 들어오지 않아 좌우 파일(Pile)을 슬레이 더 스파이어처럼 양 코너로 몰아넣었다.
그럼에도 불구하고 더 넓고, 낮게 펼쳐진다는 느낌이 강해 X 값과 Y 값을 대폭 수정했다. 이게 X값을 수정하니 Y값도 민감하게 반응을 해서 알맞는 값을 찾는 게 중요했다.
카드의 크기도 생각보다 컸다. 우리가 만든 카드와 동일한 비율로 겹쳐보았을 때, 우리 카드의 가로 길이가 38 픽셀 정도 더 짧았다(약 6.2%). 그래서 X, Y, 로컬 스케일 모두 세부 조정이 필요했다.
문제는 X 값이 일정하게 변하는 게 아니다 보니 그에 맞는 Y 값을 찾는 게 참 까다로웠다. X 값이 일정할 때는 Y 값도 일정하게 넣어주면 되는데, X 값에 따라 Y 값이 천차만별로 출렁이다보니 규칙을 찾기가 어려웠다.
수작업으로 하면 4번 사진처럼 깔끔하게 맞출 수는 있지만, 핸드가 가득 차지 않았을 때 5번 사진처럼 중구난방으로 핸드에 들어오는 문제가 있다.
일단은 핸드가 가득 찼을 때 양 파일(Pile) 안에 가지런히 놓이게 하는 건 실패했다. 어느덧 새벽 2시 22분이어서 오늘은 여기까지만 하고 내일 다시 이어서 해야겠다. 그래도 카드 비율이라도 맞춰 다행이다.
도현 팀장의 조언을 듣고 실제 슬레이 더 스파이어 인게임 내에서 카드 간의 거리를 계산해보았다.
카드가 10장일 때 기준, 양쪽 두 장씩의 카드가 다른 카드들에 비해 80% 정도의 거리만을 유지하고 있다는 걸 발견했다. 그래서 이 부분을 중점적으로 구현해보려 했다.
문제는 유니티에서의 1 유닛은 100 픽셀, 가장 많이 쓰이는 16:9 비율의 표준인 1080p 해상도의 가로 길이는 1920 픽셀로 딱 맞아 떨어지지 않는다는 것이었다. 이 부분은 포토샵 내 기준선의 픽셀값을 기록해두고, 챗GPT에게 유니티 유닛으로 변환해달라고 요청해 해결했다.
처음에는 이 간격을 어떻게 맞출 수 있을까? 생각하다가 기존 카드 위치에서 보정값을 더해보는 방식을 먼저 시도해봤다. 이 방식의 경우 구현은 간단하지만 카드가 1 ~ 10장일 때 모든 경우의 수에 맞는 보정값을 구해야 해 오히려 비효율적이었다.
그래서 양쪽 두 장씩만 80% 거리를 유지한다는 점에 착안해 그들만 예외 처리를 하기로 했다. 이렇게 하니 원하는 대로 작동을 했고 구현 또한 편했다.
이후에는 기본 핸드 5장 기준으로 슬레이 더 스파이어와 최대한 똑같이 구현하는 것을 목표로 했다.
사실 방법은 단순했다. 아래에 영상 틀어놓고 될 때까지 세부 조정했다. 이제 꽤나 비슷해졌다.
슬레이 더 스파이어에서 플레이어 턴이 활성화 된 경우 카드 바깥에 파란 이펙트가 주어지며, 카드가 겹치는 부분에도 표현된다.
기존의 경우 이펙트는 표현되지만, 애니메이션도 각 오브젝트마다 따로 실행이 되고 카드가 겹치는 부분에도 표현이 되지 않았다.
그래서 애니메이터의 Rebind, enabled를 활용하고 레이어의 순서를 변경해 아래 이미지와 같이 표현하였다.
슬레이 더 스파이어에서는 카드가 드로우될 때 보라색 면에 가려지다가 화면 2/4 지점에서부터 서서히 밝아지며 카드의 모습이 드러난다.
이 부분을 표현하기 위해 먼저 보라색 이미지를 덮어씌운 뒤(1번) DOTween의 DOFade를 사용해 페이드 인 되는 듯한 연출(2번)을 해보았다.
이렇게 연출하면 슬레이 더 스파이어와 같이 카드를 뒤집는 느낌이 나진 않고 서서히 밝아지는 느낌이 나서 제 맛이 안 살아난다.
그래서 DOTween의 Ease.Outquint를 사용해 코루틴이 끝나갈 쯤에 알파값이 확 떨어지게끔 연출했다(3번).
그리고 도현 팀장이 페이드 인 타이밍이 더 빨라야 한다고 피드백을 줘서 마지막 사진과 같이 수정했다.
마지막으로 카드가 드로우 될 때는 애니메이션이 재생되지 않다가, 핸드에 들어온 뒤 펼쳐질 쯔음 다같이 재생되는 그 느낌도 살려봤다(5번). 이것도 마찬가지로 Outquint를 사용했으나 값을 다르게 주었다.
장상욱 선생님의 조언에 따라 게임 속도를 조절할 수 있는 버튼을 만들고, 높이 및 단차 조절까지 마친 뒤 슬레이 더 스파이어의 카드를 넣어보았다.
그리고 10장일 경우의 단차 등을 더 자세히 분석해서 추가 수정까지 마쳤다.
이 정도면 꽤나 비슷해진 것 같다.
선생님과 팀장에게 피드백을 받아 단차를 늘리고 가운데 카드의 높이를 더욱 높였다.
이 정도면 95% 정도의 퀄리티를 갖추었다고 결론 짓고 드로우는 마무리 짓기로 했다.
도현 팀장이 임시로 사용할 카드 템플릿도 다시 제작해주어서 추가했다.
슬레이 더 스파이어에서는 핸드 내 카드 위에 마우스 커서를 대면 해당 카드가 위로 떠오르고 양 옆의 카드들은 반대 방향으로 밀린다.
이를 위해서는 먼저 화면 내 마우스 위치 파악과, 커서 위치에 있는 카드 오브젝트가 무엇인지 확인할 필요가 있었다.
그래서 1단계로 마우스 위치에 따라 카드가 위로 떠오르는지 먼저 확인해보았다(2번).
그런 다음에는 슬레이 더 스파이어와 마찬가지로 애니메이션 없이 즉시 position, rotation, localScale 값이 변하도록 구현해보았다(3번).
마찬가지로 마우스를 뗐을 때 제자리로 돌아오게까지 구현을 했다(4번).
다만, 카드 간에 마우스 커서를 왔다 갔다 이동시키면 제대로 작동하지 않는 등의 문제가 발생했다. 일단 하나의 변수 값에 여러 카드의 데이터가 저장되는 원인을 파악했고, 도현 팀장이 해결법까지 구현했으나 코드가 너무 길어지고 복잡해져서 일단은 지웠다.
마우스 위치의 스크린 포인트를 월드 포인트로 변환한 뒤, 레이를 쏴서 충돌 여부를 판정하는 이 방식이 비효율적이라 생각해 처음부터 다시 만들 생각이다.
광빈 팀원이 연출 구현을 정확하고 빠르게 해준 덕분에 다음 단계로 넘어 갈 수 있게 되었다. 팀원과의 원활한 작업을 위해 조금 더 직관적인 구조로 프로그래밍 할 필요성을 절실히 느껴 추후 스크립트를 재분배하는 과정에서 서로 간의 합의를 통해 좀 더 견고하며 상호 이해 가능한 코드로 가공을 하려고 한다.
본론으로 넘어가겠다. 팀원이 호버 연출을 구현하는 동안 다음 단계로 넘어가기 위해 노드맵 시스템을 구현하려고 하며, 구현을 위해 우선적으로 그래프를 학습할 필요를 느껴 학습을 진행하려한다.
그래프 학습 및 구현에 대한 순서를 간략하게 잡으려 한다. 현재 그래프에 대해 명확하게 이해하지 못하였으므로 두서가 없지만 추후 진행하며 순서를 수정하려 한다.
그래프의 정의
그래프의 자료구조
그래프의 자료구조 및 테스트용 그래프 제작
제작된 그래프로 탐색 알고리즘 구현
절차적 맵 생성 학습
학습한 절차적 맵 구현
위 모든 학습을 토대로 슬레이 더 스파이어 노드 맵 구조 구현
위 순서로 학습을 진행하려 하며 팀원이 구현에 집중할 수 있도록 팀원의 분석 자료 외 보강 분석 자료를 병행해서 준비하려 한다.
장상욱 선생님께서 조언해주신대로 유니티의 Pointer 인터페이스를 사용해 다시 구현해보려 한다.
일단 마우스 커서가 닿았을 때와 뗐을 때 두 경우를 기준으로 구현할 예정이라, IPointerEnterHandler의 OnPointerEnter 함수와 IPointerExitHandler의 OnPointerExit 함수 두 가지를 사용해보려 한다.
일단 함수에 Debug.Log만 넣고 테스트를 해보았으나 작동하지 않아 찾아보았더니 레이캐스트 타겟이 있어야만 작동 가능하다는 것 같다(출처). 그래서 메인 카메라에 Physics 2D Raycaster를 추가해줬더니 로그까지는 제대로 찍히는 걸 확인했다. 아마 해당 인터페이스 사용시 스크립트가 달린 오브젝트에는 별도의 변수 정의 없이 접근 가능한 것 같았다. 그래서 transform.localScale 만으로도 크기 변경이 가능했다.
이전 실패와 분석에서 내린 결론은, 카드의 크기 변환은 이동과는 구별해야 한다는 것이었다. 실제 슬레이 더 스파이어에서도 카드 드로우 중에 마우스 커서를 대면 크기가 커진다. 이뿐 아니라 대부분의 상황에서 그렇다. 그래서 크기를 변경시키는 메서드 2개와, 붕 떠오르는 Hover 메서드 그리고 다시 내려오는 Unhover 메서드 세 가지로 나누어서 만들어보고자 한다.
먼저 SizeUp 메서드와 SizeDown 메서드를 만들어, 마우스 커서 위치에 따라 크기를 조절해보았는데 잘 작동하는 걸 확인했다(1번). 확실히 내장 인터페이스의 함수를 쓰니 굉장히 간편하다는 걸 느꼈다(직접 구현해봐서 그럴지도 모르지만).
가장 큰 차이점은 각 오브젝트마다 할당된 멤버 변수를 사용하기 때문에 이전처럼 하나의 값만 저장되어 생기는 번거로움이 없다는 점이었다. 이전에는 빈 오브젝트에 스크립트를 부착해 카드들의 크기/이동을 제어하려 했고, 실제로 동작시키기는 했지만 굉장히 베베 꼬아서 동작하는 느낌이었다. 이번에는 각 인스턴스화된 프리팹마다 멤버 변수가 따로 지정되어 있어 따로 관리하지 않아도 되어 매우 편리했다.
한번 해봐서인지 인터페이스가 매우 편리하게 작동 가능해서인지는 모르겠지만 카드가 올라가고 내려오는 것까진 아무 불편 없이 구현해냈다(2번). 확실히 '할 줄 아는 것' 도 중요하지만, '쓸 줄 아는 것' 또한 그에 못지 않게 중요하다는 걸 느꼈다.
한 가지 문제가 있다면 카드 드로우 중에 마우스가 닿으면 제대로 동작하지 않는다는 것이었는데, 두 가지 트윈(Tween)이 동시에 작동하기에 생기는 문제였다. 이를 해결하기 위한 방법을 찾던 중 두 트윈을 블렌드하거나 덮어씌우는 등의 방식이 있다는 걸 알게 되었다. 트윈을 효율적으로 제어하기 위해 TweenManager 스크립트를 하나 만들어주고, sequence를 사용해보려 한다. 처음에는 NULL sequence에 트윈을 추가할 수 없다길래 당황했는데, sequence 또한 인스턴스화를 해주어야만 사용이 가능한 것 같았다. 이는 sequence = DOTween.Sequence 한 줄로 해결 가능했다.
이후 여러 시간 동안 많은 시도를 해봤는데 또다시 실패했다. 기능상의 이유는 마우스를 굉장히 빠르게 움직이면 호버가 제대로 작동하지 않아서이다. 그리고 내부적인 다른 이유로는 기존 카드 드로우에서 사용되는 트윈이 관리되고 있지 않아 하나의 시퀀스로 묶어서 제어할 수 없기 때문이다. 체크해보니 5장의 카드를 드로우해서 핸드에 들어오기까지 74개의 트윈이 동시에 재생된다. 트윈을 실행할 당시에 시퀀스에 Append 혹은 Join 하거나 트윈 타입의 변수에 할당하는 등 미리 관리를 했어야 했는데 이 부분에 대해서는 지식이 없어 미리 알 수가 없었다. 이를 해결하기 위해서는 기존 드로우 스크립트를 싹 갈아 엎고 트윈을 재생함과 동시에 관리할 수 있게끔 다시 만드는 것인데, 이렇게 될 경우 기존 슬레이 더 스파이어와 같은 느낌을 위해 맞추게 된 수치가 전부 의미가 없어져 버리기에 시간이 꽤나 많이 소요될 수 있다. 사실상 처음부터 만드는 것이나 다름이 없어진다.
다시 만드는 건 두렵지 않으나, 트윈 관리법을 몰랐듯이 접근 방식에 대해 무지한 걸 수도 있다는 생각에 또 다시 한번 도전해보려고 한다. 트윈끼리 겹쳐서 문제가 생기는 거라면, 단순하게 러프 함수로 구현을 해보면 또 되지 않을까라는 느낌을 받기도 한다.
사실 호버를 구현하는 방법은 이미 오래 전에 찾아놓았다(출처 링크). 지금이라도 저 코드를 분석해서 우리 것에 적용하면 작동할 것 같다. 다만 혼자서도 구현하기 어렵지 않을 거라 생각해서 시작한 거고, 또 두 번 실패했지만 굉장히 다양한 접근 방법으로 시도해보았기 때문에 많은 점을 배울 수 있었다. 일단 가장 크게 느낀 점은 이 분야에 대한 지식이 두텁지 않은 상태에서 만들다 보니, 만드는 행위 자체가 배우는 행위와 동일해져 모르는 부분이 나오면 시간이 많이 지체된다는 것이다. 단순 몰라서 뿐만 아닌, 앞서 만든 구조 자체의 확장성이 떨어지는 등 많은 한계점에 부딪히며 해나간다는 뜻이다. 그러나 우습게도 이 과정이 참 즐겁다. 하루종일 붙잡다가 결국 실패해도, 일련의 과정이 즐거운 건 참 낯선 경험이다. 그만큼 이 일에 대한 열망이 있다는 것이니 다시 한번 의기를 다져야겠다.
확실히 푹 자고 나서 다시 붙잡으니 전과는 다른 방식으로 접근할 수 있게 되었다. 어제 실패했을 때의 가장 큰 문제점은 한 번에 많은 작업을 처리하려고 했던 부분이 아닌가 싶었다. 그래서 다시 처음부터 차근차근 시작해보았다. 먼저 카드를 올리고 내리는 부분은 같으니 기존과 동일하게 트윈 시퀀스를 활용해서 구현해보았다. 어제와 동일하게 작동은 잘 되지만 마우스를 빠르게 움직일 때 문제가 발생했다(1번).
웬만하면 Update 함수는 사용하지 않으려 했는데, OnPointerEnter와 OnPointerExit이 잘 작동하지 않는 건지. 일단 트윈들은 시퀀스를 통해 관리하고 있고, 마우스 위치에 따라 Kill 처리를 해주는 중이라 이 문제는 아닌 거라 판단했다. 그래서 Update 함수 내에서 카드 위치에 따른 예외 처리를 해서 원래 위치로 돌아오도록 시도해보았다(2번).
이렇게 했더니 마우스가 빠르게 움직일 때는 잘 동작하지만, 두 장의 카드 사이를 좁은 간격으로 빠르게 왔다 갔다 하면 오류가 생긴다는 걸 파악했다(3번). 기존 예상과는 다르게 OnPointnerEnter/OnPointerExit은 정상적으로 작동 중이라는 걸 확인했으니 다른 원인 파악이 필요했다. 그래서 확실한 문제점 파악을 위해 작동 중인 트윈을 모두 체크하도록 각 카드별 인디케이터를 탑재했다(4번).
4번 사진을 보면 알 수 있듯, 가운데 카드의 트윈이 커서가 카드 위에서 이동하지 않을 때에도 계속 재생/멈춤을 반복하고 있다는 걸 파악했다. 이로 인해 트윈의 최대 개수가 초과하는 등의 자잘한 오류도 발생하고 있었다. 이는 2번에서 넣은 Update 함수 문제인 것으로 확인되어 해당 부분을 없애니 해결되었다(5번). 그리고 트윈의 개수를 확인하기 위한 트윈 카운터도 하나 마련해두었다. 그래도 여전히 기능적인 문제는 해결되지 않았다.
도현 팀장의 조언을 받아, OnPointerEnter와 OnPointerExit에 마우스 포인터 위치에 따른 조건을 달아보았다. 그러나 여전히 문제는 해결되지 않아 게임 배속을 0.1 배속으로 맞추고 다시 테스트 해보았다(6번). 확인해보니 카드가 내려오는 트윈 동작 중에 마우스가 올라가 있으면 카드가 올라가는 트윈이 1 프레임 작동 되었다가 바로 멈춘다는 것을 확인했다. 이 때문에 계속 튀어 오르는 현상이 발생한 것이다. 다만 이게 굉장히 짧은 순간이다보니 영상 촬영에 잡히지 않아 직접 아이폰 카메라로 촬영해서 재확인했다(7번).
트윈 매니저 설계 문제일까? 이번에는 이전 경험을 바탕 삼아 굉장히 깔끔하게 작동하도록 설계했다고 생각했는데... 도현 팀장은 콜라이더에서 문제가 발생하는 것일 수도 있으니 이 부분을 확실하게 확인하고 넘어가자고 했다. 그래서 박스 콜라이더를 그리는 기즈모를 추가했다(8번). 일단 CursurOn 인디케이터로 봤을 땐 마우스 위치에 따라 카드가 잘 선택되고 있는 것 같긴 하지만, 원인을 더 파악해보아야겠다. 이 부분만 해결되면 호버는 마무리 단계인데 참 아쉽다. 다만 요 며칠 너무 무리한 경향이 있어 조금 쉬었다가 다시 시도해보려 한다.
여러 방법을 시도해봤지만 확실한 원인을 찾지는 못했고, 추측되는 원인은 실행되는 트윈의 개수가 굉장히 많을 경우 이들간의 빠른 전환이 잘 되지 않는다는 것이었다. 그래서 단순히 애니메이션 속도를 높이는 것으로 일단은 해결해두었다(1번).
사실 더 큰 문제는 드로우 애니메이션이 진행 중일 때 호버 애니메이션이 진행되면 둘의 트윈이 독자적으로 작용해 의도치 않은 위치, 회전, 스케일 값을 갖게 된다는 것이었다. 이번에 만든 호버 애니메이션의 경우 카드 트윈 매니저를 따로 만들고 시퀀스를 활용해 관리가 가능한 상태이지만, 이전에 만든 드로우 애니메이션의 경우 트윈들이 별도의 트윈 혹은 시퀀스 타입의 변수에 할당되어 있지 않아 개별 혹은 일괄 관리가 불가능에 가까웠다.
이를 해결하기 위해 도현 팀장이 기존 드로우 애니메이션 스크립트를 싹 수정해 모든 트윈이 각 행동별 시퀀스에 할당될 수 있도록 수정을 해주었다. 덕분에 각 애니메이션 간의 전환을 관리하기가 용이해졌다. 다만 문제는 우리 팀 모두가 트윈에 대한 지식이나 경험이 그리 깊지 않다는 것이었는데, 이로 인해 발생한 문제는 다음과 같다.
필드에 멤버 변수로 선언 및 초기화까지 할 경우 시퀀스 값이 NULL이 되어 트윈을 할당할 수 없다는 것
메서드 내에 모두 초기화할 경우 기존 방식대로 작동하지 않는다는 것
시퀀스들을 한 번에 관리할 방법이 없다는 것
일단 첫 번째 문제와 두 번째 문제는 각 시퀀스가 진행되기 직전에 초기화 시키는 방식으로 해결했다. 세 번째 문제의 경우 단순하게 Sequence[] 배열로 선언도 해보고, Sequence<List> 리스트화도 해보았지만 이렇게 할 경우 단순 추가만 되지 그 안에 있는 독자적인 시퀀스들의 관리나 판단이 어려웠다.
그래서 찾은 방식이 또 다른 시퀀스를 다시 만들고, 그 시퀀스에 다른 시퀀스들을 모두 추가하는 방법이다. 예를 들면 mainSequence에 mainSequence.Join(subSequence1); mainSequence.Join(subSequence2); ... 이런 식으로 개별 시퀀스들을 추가해놓고, mainSequence.OnComplete(()=>{}); 등의 메서드를 사용하여 일괄 관리하는 것이다. 이 방식은 사용하기도 쉽고 가독성도 좋아 앞으로 트윈들을 관리할 때 자주 쓸 것 같다.
위 방식으로 handSequence를 만들고 handSequence.OnComplete 될 때 bool 타입의 변수를 받아 드로우 중인지 판별할 수 있게 해두었다. 그리고 드로우 중에는 호버 애니메이션이 작동하지 않도록 구현을 마쳤다(2번).
사실 애니메이션 속도를 높여서 문제를 해결한 것이 바람직하지도 않고, 추후 다른 문제가 또 발생할 것으로 예상되지만(카드를 사용하여 핸드의 수가 줄었을 경우 등) 그때 되어서는 또 다른 문제가 복합적으로 발생할 것이기에 일단은 이대로 진행하려고 한다.
앞서 구현한 드로우, 호버와 함께 작동할 때도 문제 없도록 호환시키는 게 가장 중요했다.
카드가 올라가고, 내려오고, 양 옆 카드가 밀리고, 다시 돌아오는 이 모든 과정들이 어떤 상황에서도 어우러질 수 있도록 최대한 노력했다.
이때 각 트윈 시퀀스들이 언제 어떻게 작동하는지 등을 면밀하게 체크하는 게 굉장히 중요했는데, 이 부분이 아직 미숙해서 시간이 꽤 걸렸다.
특히 시퀀스의 경우 멤버 변수에 할당한 뒤 메서드 내에서 인스턴스화를 시키더라도, 해당 시퀀스를 Kill 시키면 그 멤버 변수는 더 이상 사용할 수 없게 된다.
마찬가지로 트윈을 트윈 타입의 변수에 할당한 뒤 시퀀스에 추가하면, 시퀀스 내에서는 해당 트윈 변수에 접근할 수 없는 등의 제한 사항이 있었다.
이를 해결하기 위해 사이즈 업/다운과 같은 예외적인 상황에만 단독 트윈으로 작동하게끔 구현하였고, 이 경우에는 Kill 한 뒤 새로 인스턴스화 해도 문제 없이 작동하여 단순 기능만을 별도 구현하기에 용이했다.
이후는 아래와 같은 디테일 구현에 중점을 두었다.
양 옆 카드가 밀릴 때 각도가 기울어지지 않고 x 좌표 값만 변한다는 점
밀린 카드를 다시 당겨올 때(마우스 커서를 올릴 때) y 좌표 값과 스케일 값은 딜레이 없이 바로 변하지만 x은 서서히 돌아오는 점
그 외 세부 좌표/회전 값 등
슬레이 더 스파이어의 경우 모든 카드의 위치 값을 모두 고정 값으로 설정해두었다.
우리는 만들어둔 수식이 처음 동작 완료한 이후의 값을 저장하는 방식으로, 완전 동일하게 맞추기는 어려웠지만 비율을 계산해 최대한 유사하게 맞춰두었다.
트윈 코드도 정리하여 상시 구동 트윈 수가 30개에서 5개로 줄었다.
프로토타입의 그림을 그려보기 위해 슬레이 더 스파이어의 에셋을 동일하게 배치해보았다.
슬레이 더 스파이어에서는 카드를 선택(클릭)하면 커서 위치를 따라 이동한다.
이때의 이동 속도는 커서의 속도와 같거나 느리며, 절대 커서 속도보다 빨라지지 않는다.
따라서 커서의 속도가 빠를 경우, 카드 범위 밖으로 벗어날 수도 있다.
범위 밖으로 벗어나도 카드는 커서를 뒤따라 이동한다.
카드가 커서를 따라 이동할 때에는, 커서와 핸드 내의 다른 카드들과는 상호 작용하지 않는다.
글을 작성하면서 놓친 부분을 확인했다. 선택된 카드의 레이어가 가장 상단에 위치하여 다른 카드들보다 위에서 보인다.
이 부분은 카드를 드로우할 당시 지정해주는 SpriteRenderer.SortingOrder 값을 별도의 변수에 할당하니 구현이 편리했다(3번).
(레이어 오더 수정 전 촬영한 영상이라 그 부분은 반영이 안 되어 있으나 실제로는 수정 완료)
다양한 경우에서도 의도한대로 작동하는지 여러 차례의 테스트를 거쳤다.
원래는 글을 작성하면서 작업을 진행하기에 자세한 내용을 적을 수 있었지만, 이번엔 조금 무리하느라(벌써 새벽 4시 26분이다) 그렇게까지 하지 못한 게 아쉽다.
그래도 깨달은 거 하난 꼭 적고 가야겠다. DOTween의 트윈이나 시퀀스를 변수에 할당한 뒤 Kill() 메서드를 사용하면 해당 변수의 재사용이 불가능한 문제가 있었다. 여러 방법으로 시도해보다 알아낸 사실은 Kill() 메서드 사용 이후 null 값으로 다시 초기화해주면 재사용이 가능하다는 것이다. 이전에는 다시 인스턴스화하는 등의 방법만 시도해봤는데 생각보다 훨씬 간단한 방법으로 해결이 가능한 부분이어서 당황스러웠다.
오늘은 개발 일지에 많은 내용을 적지 못해 아쉽다. 요즘 X(트위터), 레딧 등을 통해 해외 개발자, 원화가, 게이머들의 다양한 작품과 의견들을 보고 있는데 그동안 느끼지 못했던 것들을 많이 경험하고 있다. 구체적으로 설명하기는 어렵지만 이전에는 게임을 플레이할 때 나라는 개인의 흥미만을 우선시했다면, 지금은 이 게임이 의도한 바가 무엇인지, 사람들은 어떤 걸 느끼고 있는 지 등에 대해 더욱 신경 쓰고 있다.
이로 인해 같은 게임을 플레이 해도 이전에는 느끼지 못했던 바들을 다시금 깨닫기도 하고, 게임을 개발하는 것 못지 않게 플레이하는 것 또한 더욱 가치 있게 느껴져 이 일에 대한 애착이 더욱 더 생겼다. 다만 요즘 하루의 모든 시간을 개발, 공부 및 자료 조사 등에 쓰다 보니 게임은 많이 하지 못하고 있다. 사실 게임을 못하는 것보다 걱정인 건 건강이다. 아직까진 큰 이상은 없지만 앞으로도 이런 생활을 오래 유지하고 싶다.
뭔가 최근에 플레이한 게임에 대한 소감이나 스크린샷 같은 것도 공유하고 싶은데... 올릴 게 없으니 FC 25로 대체한다.
오늘 개발 외로 여러가지 경험을 하게되어 개발 전 리프레시를 하기 위해 재밌는 것을 해보기로 하였다.
부채꼴의 범위 중 자신의 범위에서 최소 범위와 최대 범위를 검출하여 그 안에서 랜덤하게 좌표를 찍는 것을 구현해보았다.
최소 범위 부채꼴과 최대 범위 부채꼴을 구현하고 두 부채꼴을 빼는 것이 기본적이지만 유니티를 더 익숙하게 다루기 위해 직관적으로 최소 범위 끝점과 최대범위 끝점만 가지고 구현을 하게되었다.
부채꼴이 카메라 방향의 중심으로 오기 위해 임의의 부채꼴의 각도를 절반으로 나누어 음수와 양수로 부여하여 게임에서의 범위를 표현하였다. 부채꼴의 반지름을 임의로 지정하고 반지름과 각도를 이용하여 부채꼴의 빗변 2개를 구하고 빗변의 끝점 2개를 기준으로 사이에 있는 범위에 부채꼴의 반지름 크기의 세그먼트 갯수를 임의로 주되 많은 양을 주어 추후 그려질 호를 더 부드럽게 만들었다.
위 식을 기준으로 최소와 최대 범위의 반지름을 부여하여 2범위를 만들었다. 만들어진 두 범위에서 원점으로부터 그어져있는 각 끝점까지의 변을 각 점끼리의 선을 그어 호를 포함하여 최소 최대범위가 포함된 범위를 구하였다.
최대 범위 끝점들과 최소범위 끝점들 사이의 좌표를 전부 리스트에 넣어 관리하였고, 임의의 점 갯수를 부여하여 랜덤하게 뽑힌 점들을 리스트에서 검출하고 시각화해보았다.
슬레이 더 스파이어에서 카드를 사용하길 원한다면, 마우스 왼쪽 버튼 클릭 후 다시 한 번 눌러주면 된다.
정중앙에 눈에 보이지 않는 존(Zone)이 있고 카드가 그곳에 닿으면 멈춘다.
따라서 카드를 사용한 마우스 위치 쪽으로 약간 치우쳐 있다.
중앙까지 최종 스케일은 기본 카드 스케일(호버로 커지기 이전)이다.
일단 이 부분 먼저 간단하게 구현해보았다. 기존에 구현한 선택 해제는 레퍼런스 게임과 마찬가지로 우클릭에 배정해두었다.
카드를 선택하고 사용하기 이전 약간의 딜레이가 발생하는 것을 확인했다.
Tween Count와 인디케이터의 Pulling X 부분을 보면 클릭시 이전에 작동하던 애니메이션이 꺼지기까지의 딜레이가 생긴다는 걸 알 수 있다.
DOTween.KillAll(); 사용시 증상이 사라지는 것을 확인했고, 트윈 문제인 것이 확실했기에 Pulling X가 작동하는 메서드를 확인해보았다.
여지까지 생각나던대로 구현하다보니 겹치는 코드들이 군데군데 있었고, 여지껏 작성한 스크립트(3개)를 용도에 맞게 첨삭 및 수정한 뒤 인디케이터만 다루는 스크립트를 하나 추가해주었다.
이후 2번 사진을 보면 알 수 있듯, 마우스를 빠르게 움직이며 마구 클릭해도 카드 사용이 원활하게 동작했다.
다만 3번 사진과 같이 특정한 경우 카드가 2개 선택될 때가 생긴다.
버그가 발생하는 경우를 찾아내 재현해보았다.
마우스 왼쪽 버튼으로 카드를 선택한다.
선택한 채로 다른 카드 위에서 우클릭으로 선택 해제한다.
선택 해제하면 카드는 제자리로 돌아가나 스케일은 그대로이며, isMouseIn 이 꺼지지 않는다.
이 상태에서 다른 카드를 선택하면 기존 선택 카드와 두 번째 카드 모두 선택되며 동시에 사용도 된다.
이 상태에서 선택을 해제하고 또 다른 카드를 선택한다고 해서 3개가 선택되지는 않는다.
일단 밤이 늦었으니 여기서 마무리 하려 한다. 오늘 작업의 결과로 세운 단기 목표는 아래와 같다.
버그 수정
드로우 & 어레인지(핸드 내 카드 위치 정렬) 중 다른 트윈 애니메이션 작동할 수 있도록 수정
2번까지 완료하고 도현 팀장 제작 중인 카드 순환 시스템 <드로우 - 핸드 - 디스카드 - 셔플> 과 결합해보기
확실히 몸에 무리가 가고 체력이 떨어지니 집중력과 작업 효율이 많이 낮아진다. 욕심을 조금 덜어낼 필요가 있을 것 같다.
프로토 제작이 마무리 되어가며 잠시 시간을 내서 2차 가공을 진행해보려한다. 어제 제작했던 범위 인식을 범위 내 랜덤 생성으로 바꿔보았다. 원하는 갯수만큼 출력해 보았고 앞 뒤 변경 후 각 범위가 정확하게 반전되도록 설정하였다.
또한 커스텀 인스펙터를 사용하여 팀원과의 개발에 도움이 될만한 부분을 찾아 개선시키는 방향으로 만들고 있고 아마 다음에도 한번 더 가공을 통해 추후에 다른 프로젝트나 작업에 쓸 수 있게 가공을 해보려한다.
현재까지 개발한 커스텀 인스펙터이며 밑에 사진과 같은 결과로 나온다. 마음에 들진 않지만 기능은 원하는대로 구현이 됬지만 다른 방식으로 한번 더 만들어보며 새로운 아이디어에 대한 돌파구가 되지 않을까하는 생각도 든다.
3차 가공으로 각종 데이터를 인스펙터에 시각화하며 사용 또는 추출에 용이하게 바꿔볼 예정이며 이를 통해 추후 진행될 프로젝트의 다음 단계 시작 전 다양한 검증 도구를 만들어 프로토를 만들며 발생했던 문제에 대해 재발 방지하며 쾌적한 개발환경으로 바꿔 팀원의 열의를 반감시키지 않도록 불편함을 제거해 나갈 생각이다.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(ArcDrawing))]
public class ArcDrawingEditor : Editor
{
private bool showProgressBar = false;
private int currentPointIndex = 0;
private float pointGenerationDelay = 0.5f;
private float lastUpdateTime;
public override void OnInspectorGUI()
{
ArcDrawing arcDrawing = (ArcDrawing)target;
arcDrawing.arcColor = EditorGUILayout.ColorField("Arc Color", arcDrawing.arcColor);
arcDrawing.Angle = EditorGUILayout.Slider("Angle", arcDrawing.Angle, 0, 360);
arcDrawing.LongRadius = EditorGUILayout.Slider("Long Radius", arcDrawing.LongRadius, arcDrawing.ShortRadius, 100);
arcDrawing.ShortRadius = EditorGUILayout.Slider("Short Radius", arcDrawing.ShortRadius, 0, arcDrawing.LongRadius);
arcDrawing.segments = EditorGUILayout.Slider("Segments", arcDrawing.segments, 0, 100);
arcDrawing.MyDirection = (ArcDrawing.Direction)EditorGUILayout.EnumPopup("Direction", arcDrawing.MyDirection);
if (GUILayout.Button("Check One Random Point"))
{
showProgressBar = false;
arcDrawing.RandomPointCount = 1;
arcDrawing.RandomPoints = arcDrawing.GenerateRandomPointsInSector((int)arcDrawing.RandomPointCount);
Debug.Log(arcDrawing.RandomPoints[0]);
}
arcDrawing.RandomPointCount = EditorGUILayout.IntField("Random Counts", (int)arcDrawing.RandomPointCount);
if (GUILayout.Button("Check Several Random Points"))
{
showProgressBar = true;
arcDrawing.RandomPoints.Clear();
currentPointIndex = 0;
lastUpdateTime = (float)EditorApplication.timeSinceStartup;
EditorApplication.update += GeneratePointsWithDelay;
}
if (GUILayout.Button("Stop Checking"))
{
showProgressBar = false;
EditorApplication.update -= GeneratePointsWithDelay;
ShowResultsPopup(arcDrawing.RandomPoints.Count, (int)arcDrawing.RandomPointCount);
Debug.Log($"범위 안에서 {arcDrawing.RandomPoints.Count}개를 검출하였습니다.");
}
if (showProgressBar)
{
float progress = (arcDrawing.RandomPoints.Count > 0 && arcDrawing.RandomPointCount > 0)
? (float)arcDrawing.RandomPoints.Count / arcDrawing.RandomPointCount
: 0;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Random Points Progress:");
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, progress, $"{arcDrawing.RandomPoints.Count} / {arcDrawing.RandomPointCount}");
}
EditorGUILayout.HelpBox("This is a warning! Adjust the parameters carefully.", MessageType.Warning);
if (GUI.changed)
{
EditorUtility.SetDirty(target);
}
}
private void GeneratePointsWithDelay()
{
ArcDrawing arcDrawing = (ArcDrawing)target;
if (currentPointIndex < arcDrawing.RandomPointCount && (float)EditorApplication.timeSinceStartup - lastUpdateTime > pointGenerationDelay)
{
Vector3 newPoint = arcDrawing.GenerateRandomPointsInSector(1)[0];
arcDrawing.RandomPoints.Add(newPoint);
Debug.Log(newPoint);
currentPointIndex++;
lastUpdateTime = (float)EditorApplication.timeSinceStartup;
Repaint();
}
else if (currentPointIndex >= arcDrawing.RandomPointCount)
{
EditorApplication.update -= GeneratePointsWithDelay;
ShowResultsPopup(arcDrawing.RandomPoints.Count, (int)arcDrawing.RandomPointCount);
}
}
private void ShowResultsPopup(int generatedCount, int totalCount)
{
EditorUtility.DisplayDialog("Result",
$"총 {totalCount}개 중 {generatedCount}개가 범위안에 생성되었습니다. \n미검출된 포인트는 {totalCount-generatedCount}개입니다.",
"확인");
}
}
우선 이전에 발생한 버그 먼저 해결했다. 버그가 발생한 경우를 똑같이 재현하고, 디버깅 툴을 통해 그 과정 중 어떤 트윈이 작동하고 안 하는지를 확인해보니 원인을 쉽게 찾을 수 있었다. 우클릭 시(카드 선택 취소) 마우스 커서를 체크하는 isMouseIn 타입의 변수에 강제로 false 값을 할당하고, 이 카드가 밀려 있는지 체크하는 메서드를 넣어 어떤 상황에서도 2개가 선택되지 않도록 해두었다. 버그 수정은 마쳤으니 다음 단계로 넘어갈 차례다. 이전 단계에서 아래와 같은 단기 목표를 세웠었다.
버그 수정(완료)
드로우 & 어레인지(핸드 내 카드 위치 정렬) 중 다른 트윈 애니메이션 작동할 수 있도록 수정
2번까지 완료하고 도현 팀장 제작 중인 카드 순환 시스템 <드로우 - 핸드 - 디스카드 - 셔플> 과 결합해보기
2번과 3번 작업을 완료하기 이전에, 사용한 카드가 디스카드 파일(버린 카드 덱)에 들어가는 기능 먼저 구현을 해둬야 진행이 가능할 것 같다고 생각했다. 이전 단계에서는 카드 사용시 중앙에서 Destroy 되도록 구현을 해두었는데, 이제 파괴가 아닌 디스카드 파일에 추가해야 하니 이 부분을 먼저 없앴다. 그리고 카드를 사용하고 중앙까지 이동 및 원래의 스케일로 돌아오는 것까지는 구현했으니 그 다음부터 이어나가면 됐다.
현재는 팀장이 제작한 시스템과의 결합을 먼저 해두기 위해 베지어 곡선 연출은 제외했다. 이 부분은 공부가 필요해 시간이 더 소모될 것으로 예상된다.
슬레이 더 스파이어에서 카드를 사용하면, 이전 단계에서 구현한 것과 같이 (카드 사용했을 당시 마우스 커서의 위치로 살짝 치우친) 중앙으로 카드가 이동하고, 호버가 해제되며 원래 카드의 크기로 돌아온다. 이 동작이 끝나고 나면 디스카드 파일로 들어가는 애니메이션이 시작된다. 이때부터 카드를 뽑을 때와는 반대로 페이드 아웃이 실행되며, 카드를 뽑았을 때의 크기로 돌아가고, 디스카드 파일을 향해 반시계 방향(좌측)으로 회전한다.
일단은 2D 에서의 각도를 구하는 것에 익숙치 않아 이 부분을 먼저 공부해보았다. 다만 내가 원하는 '특정 오브젝트를 향해 z값만 반시계 방향으로 회전' 시키는 글은 찾을 수 없었다. 그래도 구글링 해보니 '특정 오브젝트를 향하는 것' 과 '각도를 구하는 법' 에 대한 정보는 꽤 있었기에 이 자료들을 토대로 스스로 만들어 낼 필요가 있었다. 조사한 방법들은 아래와 같다.
DOTween의 DOLookAt
Mathf의 Atan2와 Rad2Deg 활용(출처 Unity 2D 환경에서 특정 오브젝트 방향으로 회전 (tistory.com))
일단 직접 사용해보니 1번과 2번은 내가 원하는 z값 각도 '만을' 활용하기 번거로움이 있어 3번 방법을 택했다.
// pos
Vector3 pos = trans.position;
Vector3 discardPilePos = new Vector2(8.2f, -4.3f);
// rot
Vector3 dir = (pos - discardPilePos).normalized;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
Quaternion rot = Quaternion.AngleAxis(angle + 90, Vector3.forward);
이제 각도를 구했으니 반시계 방향으로 돌리는 게 문제였다. DOTween의 DORotate는 가장 가까운 방향으로 회전을 하는 특징이 있다. 그래서 카드가 디스카드 파일로 향하는 각도로는 무조건 시계 방향으로만 회전한다. 이 부분을 어떻게 구현할까 여러 테스트를 진행하다 원하는 값이 나오지 않아 조사를 더 진행했다. 그러다 이런 글을 발견하게 되었다.
DORotate RotateMode.FastBeyond360 rotating backwards · Issue #595 · Demigiant/dotween (github.com)
작성자는 DORotate 의 FastBeyond360 모드를 사용하니 회전이 반대로 진행되는 게 문제라고 여겼던 것 같다. 그러나 그에게는 문제일 수 있어도 나에게는 그토록 찾던 방법이 아닌가! 그래서 DOTween 문서를 살펴보니 RotateMode라는 게 따로 있었다. 이 중 FastBeyond360 이라는 모드는 오브젝트가 360도 이상 회전할 수 있도록 하는 기능이었다(FastBeyond360: The rotation will go beyond 360°). 테스트 해보니 정말 반시계 방향으로 회전하는 게 아닌가! 씬(Scene) 윈도우에서 오브젝트를 직접 회전시켜보니 z에 360이 넘는 값을 넣어도 정상적으로 동작하는 것을 확인했다. 다만 rot.eulerAngles 의 z 값은 약 220 정도가 나오는데, FastBeyond360 을 사용하면 -140 정도의 값이 나와서 이 부분은 좀 의아하다. 어째서 음수가 나오는 건지 이 부분에 대해선 공부가 더 필요할 것 같다.
trans.DORotate(rot.eulerAngles, speed, RotateMode.FastBeyond360)
사실 이 과정은 드로우 때 사용했던 방식을 역순으로 재생시키면 되는 거라 어려운 건 없었다.
다만 핸드 리스트에서 디스카드 리스트로 들어갈 때 2개씩 추가되는 문제가 있었고, 이는 카드를 사용할 때와 턴을 종료할 때의 버려지는 애니메이션을 분리해서 해결했다. 분리하지 않고 같이 두어서 중복 작동한 것이 원인이었다.
사실 이 부분은 의외로 까다로운 점이 많았다. 일단 기존에 만들어둔 애니메이션들이 '턴' 이라는 요소를 고려하지 않고 만들었다보니 생긴 문제들이 대부분이었다. 일단 카드를 버리는 것까진 된다고 해도, 버리는 것(discard)와 뽑는 것(draw)이 동시에 실행돼 5장의 카드를 버려도 새로 뽑아야 할 5장까지 버려지는 현상이 생겼다. 이를 해결하기 위해 async(비동기) 방식을 사용하려 했으나, 기존에 사용되는 여러 메서드들을 모두 수정해야 해서 추후 어떤 문제가 발생할 지 가늠이 되질 않았다. 그래서 버튼을 누르면 핸드 속 카드 장수를 받아놓고, 카드를 버릴 때 장수를 세는 count 변수를 따로 만들었다. 그런 뒤 모든 카드가 버려졌을 때 비로소 카드를 다시 뽑도록 작동시켰다. 이 방법이 가장 단순하고 기존 애니메이션들과 충돌하지 않아서 이대로 사용하기로 했다.
여기까지 해결하고 나니 도현 팀장이 의도했던 대로 <드로우 파일(pile) - 핸드 - 디스카드 파일> 의 구조 내에서 카드 오브젝트를 순환시킬 수 있었다.
+
End Turn 버튼을 빠르게 누를 때 카드가 복사되는 버그가 발견되었다. 이 또한 드로우 판정이 끝나기 전에 턴이 끝나버려서 발생한 문제로 확인되어, 드로우할 때도 count를 세는 방식으로 해결하였다.
그리고 셔플된 이후에 드로우 시 페이드가 제대로 작동되지 않아 이 부분 또한 해결하였다. 첫 게임 시작 때 덱을 만드는데, 이 때만 레이어 오더를 설정해주고 이후 드로우 시에는 설정해주지 않아서 발생한 문제였다.
이전 과정까지는 편의를 위해 카드를 뽑는 애니메이션과 정렬하는 애니메이션이 작동하는 중에 카드를 선택하거나 다른 애니메이션을 작동시킬 수 없게 막아두었다. 이제 카드 내에서의 애니메이션은 베지어 곡선을 제외하면 모두 만들었으니, 슬레이 더 스파이어처럼 카드를 연속적으로 사용할 수 있도록 해보겠다.
버그 수정(완료)
드로우 & 어레인지(핸드 내 카드 위치 정렬) 중 다른 트윈 애니메이션 작동할 수 있도록 수정
2번까지 완료하고 도현 팀장 제작 중인 카드 순환 시스템 <드로우 - 핸드 - 디스카드 - 셔플> 과 결합해보기(완료)
이 파트에서 가장 중요한 것은 트윈의 제어다. 기존 애니메이션이 작동하는 중에 또 다른 애니메이션이 정상적으로 작동되어야 하기 때문에, 각 트윈들을 적재적소에 배치해두고 원할 때 켜고 끌 수가 있어야 했다. 사실 여기까지 오면서 트윈에 대해 굉장히 많은 실험을 해보았지만 결론적으로 DOTween의 Kill, Complete 등은 필연적인 딜레이를 발생시키기 때문에 이 부분을 자체적으로 개선할 필요가 있었다. 예를 들어 카드를 당기는 트윈이 실행 중일 때 이 트윈을 단순히 Kill 이나 Complete 를 한다고 해서 곧바로 다음 트윈으로 이어지지는 않는다. 이는 OnKill, OnComplete 에 작성해도 마찬가지이다. 0.05 배속으로 했을 때 최소 1프레임이라도 딜레이가 생길 수 밖에 없다.
처음에는 트윈 제어가 잘 되고 있지 않아서 그런 줄 알았지만, 상시 구동 트윈 수를 0으로 줄이고 동시에 작동하는 트윈도 최대한으로 줄였는데도 불구하고 똑같았다. 계속해서 개선해가며 작업을 해왔기 때문에 현재는 마우스를 움직이지 않으면 카드가 뽑히고 있지 않는 이상 작동하는 트윈은 무조건 0개다. 그래서 OnComplete 나 OnKill 에서는 딜레이가 관계 없는 값의 변경 등을 처리할 때만 사용하는 것이 바람직하다. 딜레이 없이 다른 트윈과 부드럽게 연결시키고 싶다면 차라리 OnUpdate 중에 조건문을 달아 Kill 한 뒤 다른 트윈을 동작시키는 게 낫다. 물론 이건 개인적인 경험에 의한 바이기 때문에 정확하지 않을 수 있다. 다만 최소한 이 프로젝트 내에서는 분명한 사실이다.
각설하고 슬레이 더 스파이어에서는 드로우 중이나 카드를 사용하여 디스카드 파일에 들어가는 중에도 다른 카드를 선택할 수 있고, 이 때 스케일을 조정하는 애니메이션이 작동한다. 이 때는 호버와는 다르게 위치 값은 변하지 않고 스케일만 커진다는 점에 착안하여 스케일을 관리하는 부분을 모두 삭제하고, 스케일 만을 담당하는 별도의 시퀀스를 만들어두었다.
추가적으로 기존에는 드로우를 마치고 나면 그 위치값을 각 카드 오브젝트에 저장해두었었는데, 예상치 못한 상황에 위치가 튀는 것을 방지하기 위해 카드를 뽑고 이동하기 직전에 위치값을 미리 저장해두고 이동하는 방식으로 변경시켰다. 이 두 과정을 위해 기존에 있던 정렬(ArrangeHand) 코드를 모두 손보았고, 시간은 오래 걸렸지만 의도했던대로 위치가 튀는 일은 생기지 않았다.
어쨌든 이제 처음 계획했던대로 Draw, Discard, Use, Arrange, Hover, Push, Pull 등 모든 카드 애니메이션을 구현해냈다. 이제 필요하다면 세부 수치를 조정하고, Discard 시에 라인 렌더러로 베지어 곡선만 그려주면 된다.
사실 이번 작업을 통해 굉장히 많은 것들을 깨달았지만, 제때 작성해두질 못해서 까먹은 것들이 더 많다. 다만 몸으로 습득했기에 프로그래밍 실력이 늘었다는 건 믿어 의심치 않는다. 그럼 무얼 깨달았나? 했을 때 떠오르는 것들을 적어보겠다.
1. 디버깅의 중요성
이건 직접 부딪혀보지 않고 깨닫기는 어려웠을 거다. 수없이 터지는 버그 속에서 언제, 어떻게, 무엇 때문에 작동하는지를 정확하게 파악하려면 디버깅 툴이 절실히 필요하다. 다행히 장상욱 선생님께서 미리 귀뜸을 해주셔서 각 변수 값을 띄우는 인디케이터와 속도를 느리게 재생할 수 있는 버튼 등을 미리 만들어두어 비교적 수월하게 작업할 수 있었다. 그리고 진행 중에도 확인이 어려운 부분이 있다면 이 툴을 개선시켜 더 빠른 원인 파악을 할 수 있도록 만들었다. 실제로 장상욱 선생님께서 만든 게임인 던전 추적자(Dungeon Tracer)에 숨겨진 테스팅 툴이 있는데, 이에 착안하여 드로우 파일 카운트 부분에 디버깅 툴을 켜고 끌 수 있는 버튼을 만들어두었다.
선생님께서 또 말씀하신 게, 해당 버그를 똑같이 재현하지 못하면 고치기가 매우 힘들다고 하셨다. 이건 정말 뼈저리게 느껴서 버그가 발생하면 어디서 문제가 발생하는지 찾고, 버그가 생긴 상황을 정확하게 만들어내는 것에 중점을 뒀다. 이것도 하다보니 늘어서 버그 잡는 속도가 점점 빨라졌다.
-
2. Unity 사용의 미숙함
유니티 프로그램과 함수들의 사용에 보다 능숙해졌다. 아직 걸음마는 커녕 아둥바둥하는 단계이지만 이것저것 할 줄 아는 게 늘었다. 그 중 기억나는 걸 적어보려 한다.
아무래도 카드의 개수가 유동적이기에 게임 오브젝트의 관리가 필수적이다보니 이에 관련된 것들이 많이 기억에 남는다. 예를 들어 인스펙터에서 자식 오브젝트를 먼저 비활성화한 뒤 부모 오브젝트를 비활성화하면, 스크립트를 통해 부모 오브젝트를 활성화시킬 때 먼저 비활성화된 자식 오브젝트는 활성화되지 않는다. 이와 반대로 인스펙터에서 따로 비활성화하지 않은 자식 오브젝트는 부모 오브젝트의 활성화 여부에 따라간다.
그리고 비활성화된 오브젝트를 찾아내기 위해 사용할 수 있던 FindObjectOfTypeAll(비활성화된 오브젝트까지 찾아주는 함수)은 더 이상 사용 불가하다고 판단했다. 관련 글들이 모두 오래되었고 Resources.FindObjectOfTypeAll(typeof(타입명)) 등으로 사용해도 작동하지 않았다. 문서도 더이상 업데이트되고 있지 않는 걸 보니 이제 사용하지 않는 함수인 것 같다. 이를 대체하기 위해 FindObjectsOfType<타입명>(true)를 사용했다. 이는 배열에만 할당이 가능하고 (true)는 매개 변수 (bool includeInactive), 즉 비활성화 오브젝트 검색 여부를 의미한다.
현재 제작하고 있는 프로토타입의 목적은 레퍼런스 게임의 핵심 요소를 직접 만들어보기 위함이지, 그대로 따라해내기 위해서는 아니다. 그럼에도 불구하고 슬레이 더 스파이어는 수많은 디테일들이 살아있는 게임이기에 계속해서 조사해나갈 필요가 있다.
프로토타입을 만들면서 느낀 점은 똑같은 에셋을 사용해도 똑같은 분위기를 자아내지 못했다는 것이다. 카드 애니메이션에 대해서는 그래도 8~90% 정도 동일하게 만들었다고 자부하지만, 그 외에 부분은 아직 많이 다르다. 그 이유에 대해 현재까지 파악하기로는 아래와 같다.
배경 이미지와 별개로 독자적으로 동작하는 FX
둥둥 떠다니는 아이콘
정적 이미지에 생동감을 부여하는 애니메이션(정적 이미지에 본을 입혀 애니메이팅 한 것으로 추측하나 조사 필요)
카드 파일(pile)과 에너지에 부여되는 파티클 혹은 애니메이션
슬레이 더 스파이어에서 곡선을 그리는 경우는 아래와 같다(1, 2번 이미지 참고).
카드를 사용할 때
카드를 버릴 때
카드를 섞을 때
저 곡선을 그리기 위해 라인 렌더러를 활용할 건데, 베지어 곡선을 사용하고자 한다. 일단 베지어 곡선에 대한 정의와 작동 원리에 대해 숙지하고 갈 필요가 있었다. 이를 위해 Sebastian Lague 의 영상을 참고하였는데 설명이 아주 잘 되어 있어 기본적인 원리를 이해할 수 있었다. 그럼 이를 어떻게 구현할 것인가? 저 영상과 마찬가지로 lerp를 사용할 수도 있겠지만, 기왕 DOTween 을 사용하기로 한 거 DOTween 의 DOPath를 사용해 시험해볼 예정이다.
DOPath(Vector3[] waypoints, float duration, PathType pathType = Linear, PathMode pathMode = Full3D, int resolution = 10, Color gizmoColor = null)
Tweens a Rigidbody's position through the given path waypoints, using the chosen path algorithm.
Additional options are available via SetOptions and SetLookAt.
waypoints The waypoints to go through.
duration The duration of the tween.
pathType The type of path: Linear (straight path), CatmullRom (curved CatmullRom path) or CubicBezier (curved path with 2 control points per each waypoint).
pathMode The path mode, used to determine correct LookAt options: Ignore (ignores any lookAt option passed), 3D, side-scroller 2D, top-down 2D.
resolution The resolution of the path (useless in case of Linear paths): higher resolutions make for more detailed curved paths but are more expensive. Defaults to 10, but a value of 5 is usually enough if you don't have dramatic long curves between waypoints.
gizmoColor The color of the path (shown when gizmos are active in the Play panel and the tween is running).
CUBIC BEZIER PATHS
CubicBezier path waypoints must be in multiple of threes, where each group-of-three represents: 1) path waypoint, 2) IN control point (the control point on the previous waypoint), 3) OUT control point (the control point on the new waypoint). Remember that the first waypoint is always auto-added and determined by the target's current position (and has no control points).
3번 이미지 출처: Trajectory Generation - 베지에 곡선 ( Bezier Curves ) : 네이버 블로그 (naver.com)
오늘부터 이틀동안 데이터 정리에 앞서 이펙트와 데이터의 상관관계 등 데이터에 들어갈 요소들을 정리하여 데이터 테이블을 작성하고, 작성한 자료를 토대로 시스템에 연동하기 위해 스크립트를 작성 중에 있다.
1. 카드의 이펙트
--- 카드의 이펙트는 크게 2가지로 나눌 수 있다.
1) 공격타입 : 공격 타입은 찌르기, 베기, 때리기 3가지를 기본으로 한다. 카드 이미지 등의 정보에 맞게 찌르기 ,베기 ,때리기의 이펙트가 추가가 된다.
2) 상태타입 : 상태 타입은 버프, 디버프 2가지를 기본으로 한다. 각 버프와 디버프 내에도 다양한 효과에 따라 이펙트가 달라진다.
먼저 각 캐릭터 마다 왜 기본 공격이 굳이 있었는 가를 생각하던 중 한번 사용 시를 체크해보자는 방향으로 생각이 바뀌어 동작 영상을 준비했다.
각 캐릭터 마다 같은 카드 임에도 불구하고 다른 이펙트가 보이는 것을 확인 할 수 있다. 왜 그렇게 되는지 생각을 해보자.
첫번째 아이언 클래드는 왜 우측 위에서 좌측 베어내리는 듯한 이펙트가 나오는가? 기본 공격 카드와 아이언 클래드의 무장인 장검을 생각해보자. 장검을 들고 기본 공격 카드와 같은 이미지의 공격을 한다고 했을 때 동작은 오른쪽 위에서 좌측 아래로 허리를 돌리며 최대의 힘으로 베어내는 장면이 떠오를 것이다. 그렇다 이펙트는 카드의 효과에 따라 부과가 되지만 실제 플레이어는 캐릭터의 무장을 보고 사용 장면을 연상시켜 느끼는 것이다.
그렇다면 사일런트와 디펙트는 어떨까? 사일런트의 무장은 단검이며 사일런트의 기본 공격카드는 찌르기를 하고 있다. 그렇다면 사일런트의 동작을 연상해보면 현재 몸의 높이가 낮고 숙인 상태이며 이 상태로 공격카드의 이미지와 같은 공격을 한다고 연상해보자 앞으로 나아가며 같은 높이로 팔이 뻗어져 나가는 것이다. 이것을 이펙트와 비교해보면 동일한 높이에 아이언 클래드의 공격과 다르게 처음과 끝이 동일한 그림으로 되어있다.
마지막으로 디펙트의 기본 공격을 알아보자 디펙트는 아이언클래드나 사일런트와 다르게 무장을 가지고 있지 않다. 그렇다 디펙트가 무기 없이 주먹을 쥐어 휘두르는 모션이 기본 공격 이미지에 담겨있다. 이것을 이펙트와 함께 보면 이해할 수 있을 것이다.
즉 기본 공격카드가 나뉜 이유는 각 캐릭의 공격모션을 카드로 구현하기 위해서이다. 이러한 디테일 하나하나가 모였기 때문에 슬레이 더 스파이어가 현재까지 팬층을 유지할 수 있던 것아닐까 하는 생각을 해본다.
2. 데이터 정리
위와 같은 정보를 바탕으로 현재 카드에 들어갈 정보들을 파악해보았다.
카드는 코스트, 이름, 아이콘사진, 카드타입, 데미지타입, 공격과 방어에 쓰일 수치, 버프, 버프수치, 디버프, 디버프 수치, 설명
이렇게 데이터 테이블을 나눌 수 있다. 버프와 디버프의 수치를 합치지 않은 이유는 버프와 디버프를 같이 받을 경우도 있기 때문에 방지하기 위해서이다.
1차 가공으로 이렇게 작성되었지만 버프와 디버프를 합쳐 환경변수로 명명하고 제일 앞에 버프인지 디버프인지를 선택할 환경type으로 주는 것도 고려하고있다.
이것을 기반으로 1차 데이터테이블을 제작중이며 아래 사진과 같이 작업이 되고있다. 추후 프로토타입을 통해 입증을 하고 불편한 점을 개선할 예정이다.
이전 단계에서 베지어 곡선을 따라 카드가 이동하게끔 수치를 지정해두었으니 라인 렌더러를 통해 그려주기만 하면 됐다.
처음 시작은 라인 렌더러가 제대로 그려지는지를 먼저 확인해보기 위해 첫 시작점과 카드의 위치를 그리도록 하였다.
그 다음에는 카드 위치값을 저장하는 배열을 따로 만들어 카드의 위치에 따라 이동하는 선을 그려보았다.
그려지는 걸 확인했으니 그 길이를 더 늘리고, 카드가 사라진 뒤에도 라인 렌더러가 남게 하여 잔상 같은 효과를 주려 했다.
이 파트에서 어려웠던 점은 다음과 같다.
단순한 직선이나 곡선이 아니다.
4개의 컨트롤 포인트와 2개의 웨이 포인트를 가진 큐빅 베지어
웨이 포인트까지의 곡선을 완만하게 그리기 위해 컨트롤 포인트를 미세하게 조정할 필요가 있음
카드가 매우 빠르게, 부드러운 곡선을 그리며 움직인다.
베지어 곡선의 완만한 굴곡을 표현하기 위해 라인 렌더러의 position count가 커야했음
위치 값을 저장하는 배열의 길이가 너무 작으면 선의 길이가 짧아짐
라인 렌더러의 position 을 set 하는 동안 카드가 빠르게 움직이면 곡선이 거칠어짐
화면 중앙이 아닌 핸드에서부터 선이 그려지는 경우
=> 카드 사용 후 중앙으로 이동하는 애니메이션과 다른 애니메이션이 분리되지 않아 짧은 프레임 레이트 동안 카드가 실제 움직였던 것
=> 카드가 사용되면 다른 애니메이션들이 간섭하지 못하도록 수정하여 해결
라인 렌더러가 멈춘 채로 남아 있는 경우
=> 카드가 <드로우 - 핸드 - 디스카드> 되는 과정에서 프리팹을 통해 새로 생성하는 것이 아닌 활성/비활성하는 방식의 오브젝트 풀링 사용 중
=> 베지어 곡선을 모두 그리고 라인 렌더러가 따라 가기 이전에 트윈이 종료된 채로 라인 렌더러만 활성화되어 생기는 문제
=> 트윈의 종료 시점을 시작 후 2초로 두어 해결
슬레이 더 스파이어에서는 플레이어 캐릭터와 적 모두 살아 숨쉬듯 움직이는 동작을 취한다. 이를 보니 앞으로 우리가 만들 게임에서도 정적인 이미지를 다방면으로 활용하여 연출할 수 있겠다 싶어 공부해보기로 했다. 스프라이트를 사용한 애니메이팅 방식은 매 프레임마다 직접 그려야 하기에 시간이 많이 소모된다. 이미지를 분해하여 유니티 엔진 내에서 뼈대(Bone)를 넣어주는 리깅 방식은 다소 번거로울 수는 있으나 하나의 이미지만으로도 구현이 가능하다.
실제 레퍼런스 게임에서도 이런 연출을 했을까 궁금해서 찾아보니 예상대로였다. 우리가 보는 캐릭터의 모습은 모든 파트가 합쳐진 모습이지만, 실제로는 스프라이트 아틀라스 한 장에 부위별로 분리되어 있었다. 이 이미지를 활용하기 위해 여러 영상과 자료를 찾아보았으나, 가장 적합한 글은 이 글이었다. 먼저 포토샵을 통해 각 부위별로 레이어를 나눈 뒤 하나의 이미지로 합친다. 그런 다음 PSB 파일로 저장하며 유니티 프로젝트에 넣어주면 사전 준비는 끝이다.
PSB 파일을 확인해보면 다른 Prefab과 마찬가지로 프로젝트에서 바로 사용할 수 있도록 되어있다. 이를 스프라이트 에디터의 스키닝(Skinning Editor)를 통해 뼈대를 입힌 뒤 지오메트리(Geometry)까지 설정해주면 마지막 이미지와 같은 결과물을 만들 수 있다.
리깅을 통해 애니메이션을 구현하는 중 느낀 점은 유니티의 녹화 기능이 너무나도 편리하다는 것이다. 그래서 이를 활용해서 카드 사용시 연출되는 이펙트 또한 만들어보고자 한다. 마찬가지로 정적인 이미지를 활용한 연출이 대부분이기 때문에 이전 단계에서 한 것과 크게 다르지는 않았다. 오히려 뼈대를 입히는 과정이 없어 더 쉬웠다.
테스트 하다 보니 선택한 카드의 이미지가 UI 보다 뒤에 있어 거슬린다는 느낌을 받았다. 이를 해결하기 위해 기존에 하나였던 카메라를 세 개로 늘렸다.
백 카메라
UI 카메라
메인 카메라(카드)
백 카메라에서는 카드와 UI 외 인게임 장면들을 보여주게끔 해두었고, 기존의 메인 카메라는 카드만을 비추게 하였다. 이로써 코드 수정 없이 UI 문제를 해결할 수 있었다. 참고로 Canvas의 렌더 모드는 Screen Space - Camera 로 두고 렌더 카메라는 UI Camera 를 선택해주었다. 이렇게 하지 않으면 정상적으로 보이지않는다.
레퍼런스 게임에서의 턴 종료 버튼은 말 그대로 턴을 종료시키는 기능을 한다. 마우스를 올리면 글로우 효과와 함께 버튼 색이 밝아지며, 글씨의 색 또한 빨갛게 바뀐다. 이때 텍스트는 아웃라인과 그림자를 가지고 있으며 턴을 종료할 수 없는 상황일 때는 버튼 전체가 어두워진다.
처음에는 단순히 상황에 맞는 이미지가 모두 준비되어 있을 것이라 생각했다. 그런데 실제로 뜯어보니 버튼 이미지와 글로우 이미지만 있고 글자와 색상이 다른 파일들로 나뉘어 있지는 않았다. 그래서 이 버튼을 동일하게 만들기 위해서는 아래 기능들을 구현해야만 했다.
TextMesh Pro - 아웃라인, 그림자
마우스 위치 및 턴 상황에 따른 밝기 변화
1번의 경우 아웃라인 등이 쉐이더에 부착되어 있는 기능이라, 하나의 텍스트 오브젝트에서 아웃라인 처리를 하면 동일한 폰트를 가진 모든 텍스트에 적용이 되었다. 그래서 폰트 에셋의 복사본을 만들고 상황에 맞는 폰트를 넣어둘 필요가 있었다.
마찬가지로 2번 또한 스프라이트가 기본 쉐이더를 통해 연출되고 있어 밝기를 1 이상 올릴 수 없었다.
Image image = GetComponent<Image>();
Color color = image.color;
image.color = (color.r * 2f, color.g * 2f, color.b * 2f, color.a);
위와 같은 방식으로 처리를 해도 기본 밝기에서 더 높아질 순 없다는 걸 확인하고는 포토샵으로 밝은 이미지를 하나 더 만들어두었다. 그래서 결과적으로 버튼 하나에 5개의 자식 오브젝트가 들어갔고, 텍스트 오브젝트도 3개의 자식 오브젝트를 갖게 되었다. 이럴 바엔 이미지를 여러 장 만들어두는 게 나을텐데, 아마 원작자는 이런 방식을 통해 구현한 것은 아닌 것 같다. 알파 단계에 진입하며 우리 게임을 만들 때에는 더 깊은 고민을 갖고 접근할 필요가 있을 것이다.
기존 애니메이션을 다듬고 눈이 붉어지는 부분도 구현했다. 더불어 적의 애니메이션도 추가해주었다.
체력 바와 방어 카드의 사용 또한 구현했다.
사실 더 디테일하게 구현하려면 했겠지만 카드 드로우가 아닌 UI와 연출 파트는 연습에 가깝고 정해진 게 없기에 비슷하게만 만들어두었다.
레퍼런스 게임인 슬레이 더 스파이어에서는 실드 카드를 사용하면 방어 스택이 쌓이고 체력 바의 모습이 변하는데, 이 부분까지 구현해두었다.
체력 바의 색을 변경하기 위해 Fill Area 의 Image.color에 접근했는데, 컬러 피커에서 찾아둔 값을 입력해도 3번 사진과 같이 다른 색이 나와 당황했다.
이 글에 의하면 쉐이더와 마찬가지로 색상의 최대값이 1이기에 255가 아닌 1을 기준으로 색상을 주어야 했다.
마지막으로 턴이 끝날 때 방어가 깨지는 효과까지 넣어주고 마쳤다. 애니메이션은 이전과 마찬가지로 유니티의 녹화 기능을 활용했다.
이번에는 카드의 타겟을 지정할 때 생기는 애로우를 구현해보고자 한다. 이 애로우는 20개의 세그먼트(Segment)를 갖고 있으며, 2번째와 3번째 지점 사이를 기준으로 10개씩 나뉘어 작동한다. 카드의 효과를 적용시킬 상대를 선택시키기 위한 용도이므로 타겟에 닿으면 색이 빨갛게 변한다.
이번에는 이전과 다르게 오로지 러프만을 사용해 베지어 곡선을 그린 뒤 애로우를 만들어볼까 했다. 그런데 20개의 세그먼트(라인 렌더러의 포지션)을 만들어 작동시킨다 해도, 세그먼트마다 관절처럼 분리하여 이동하는 부분을 구현해내기가 번거로웠다. 그 이유는 다음과 같다.
이미지의 알파 처리가 필요하다
각 세그먼트 머리 부분의 방향을 정하기 위해서는 별도의 쉐이더 코딩이 필요하다
러프 처리를 할 경우 베지어 곡선이 계속해서 그려진다
사실 1, 2번의 경우 단순 번거로워서 그렇다 치더라도 3번은 계속해서 러프가 작동한다는 점도 거슬리고 이전에 만들었던 베지어 곡선과는 다르게 마우스 커서 위치에 따라 움직이지 않고 고정되기 때문에 다른 방식으로 만들 필요가 있다고 생각했다. 특히 모든 처리를 마치고 라인 렌더러의 선 굵기 등을 디테일하게 조정한다 해도 원하는 모양새가 나오지 않을 것이다.
라인 렌더러가 아닌 방식으로 애로우를 구현하기 위해선 어떻게 해야 할까? 일단 슬레이 더 스파이어가 그러하듯 수치를 하나하나 정해놓고 작동시키는 것도 단순하지만 정교한 방법이라 생각했다. 다만 여지껏 그랬듯 수식에 의해 '작동한다' 는 느낌으로 만들고 싶었기에 머릿 속으로 그림을 계속 그려봤다. 배열을 만들어 놓고 각 배열 요소의 위치값마다 세그먼트를 부착해놓으면 원하는 그림이 나오지 않을까하는 생각에 정보를 찾아보았고, 그러다 내 상상과 100% 일치하는 영상을 찾게 되었다.
Game Dev Tutorial: How to recreate the targeting arrow in Slay the Spires
정말 군더더기 없이 완벽한 영상이다. 베지어 곡선을 만드는 수식을 그대로 코드로 옮겨 담은 뒤, 슬레이 더 스파이어에서 작동하는 위치 값을 대략적으로 계산하여 그대로 구현해놓은 과정을 담고 있다. 말그대로 이 영상 그대로 따라하면 완성이 될 정도이다. 그렇다고 해서 똑같이 따라했다는 소리는 아니다. 일단 해당 영상에서는 UI 상의 이미지를 Rect Transform을 활용해 제작하였고, 우리가 만드는 프로토타입에서는 이미 < 게임 - UI - 카드 > 세 가지 레이어에 따른 카메라 또한 분리해 놓았기에 저 방식대로 사용해도 쓸 수가 없다.
그래서 위 영상의 내용을 바탕으로 우리 것에 적용할 수 있도록 새로 만들어보았다. 먼저 마우스 위치에 따라 잘 따라오는지 확인하기 위해 (0, 0) 값을 기준으로 테스트를 마쳤고, 이후 카드 중앙 값으로 위치를 변경한 뒤 곡선을 그려보았다. 물론 애로우를 테스트하기 전 카드 연출(공격 카드 선택시 카드가 중앙으로 작아지며 들어오는 부분) 먼저 구현을 해두고 작업을 시작했다. 그런 다음에는 타겟 위에 마우스가 올라갈 때 색상이 붉어지는 부분까지 구현해두고 마쳤다.
최광빈 팀원이 다양한 방법으로 베지어 곡선을 테스트하는 것을 보고 Lerp와 DOTween으로 구현을 하는 것을 확인했다. 마지막으로 남은 수식을 가지고 제작하는 데 있어 조금의 도움을 주고자 제작해보았다.
먼저 B(t) = P0(1 − t)3 + 3P1t(1 - t)2 + 3P2t2(1 - t) + P3t3, t∈[0,1] 식을 이용하여 코드를 작성해보았다.
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
public class Bezier : MonoBehaviour
{
[Range(0, 1)]
[SerializeField]
private float t;
[Range(0, 100)]
[SerializeField]
private float Segments;
여기까지가 동적으로 변할 변수들을 선언한 부분이다.
t는 기준 점과 다음 점에 대해 어느 쪽에 가까운지 나타내는 매개변수이다.
Segments는 시작부터 끝점까지의 거리를 나누는 비율로 쓰일 매개변수이다.
public GameObject p2Go;
public GameObject p3Go;
private Vector3 P1;
private Vector3 P2;
private Vector3 P3;
private Vector3 P4;
P1은 카드의 위치
P2는 카드의 위치 동일한 Y축에서 수직인 점
P3는 P4의 Y축과 동일한 높이에 있는 점
P4는 마우스의 위치
이렇게 4점을 지정하여 실험하였다.
private Vector3 point;
이 변수는 베지어 곡선 식을 통해 나오는 곡선의 점을 담아둘 변수이다.
void Start()
{
t = 0.5f;
Segments = 20f;
P1 = new Vector3(0, -3.5f, 0);
P2 = p2Go.transform.position;
P3 = p3Go.transform.position;
}
각각의 변수들에 초기값들을 지정해주었다.
void Update()
{
P4 = Camera.main.ScreenToWorldPoint(Input.mousePosition);
CalculateBezierPoint(t, P1, P2, P3, P4);
}
마우스의 위치에 따라 즉각적으로 정보를 얻게 끔 Update로 진행하였다.
public Vector3 CalculateBezierPoint(float t, Vector3 P1, Vector3 P2, Vector3 P3, Vector3 P4)
{
Vector3 point = Mathf.Pow((1-t),3) * P1
+ 3 * Mathf.Pow((1-t),2) * t * P2
+ 3 * (1-t) * Mathf.Pow(t,2) * P3
+ Mathf.Pow(t,3) * P4;
Debug.Log($"Point의 위치는 : {point}");
return point;
}
수식을 통해 베지어 곡선의 점을 리턴하는 메서드를 정의하였다.
void OnDrawGizmos()
{
Gizmos.color = Color.red;
Vector3 previousPoint = P1;
for (int i = 1; i <= Segments; i++)
{
Debug.Log("체크포인트3");
float t = i / Segments;
Vector3 point = CalculateBezierPoint(t, P1, P2, P3, P4);
Gizmos.DrawLine(previousPoint, point);
previousPoint = point;
}
}
}
Segments의 비율로 각 점들을 찾아 선으로 그어보았다.
아쉽게도 P2, P3를 동적으로 이동시키며 테스트하는 것을 지금 단계에서는 넣지 않아
1. 마우스의 위치까지 베지어 곡선이 잘 그어지는지?
2. Segments에 따라 베지어 곡선이 잘 분배되는지?
3. t값에 따라 잘 작동하는지?
이 세 가지에 초점을 맞추고 1차 제작을 완료하였다.
밑의 영상을 통해 3가지 다 잘 작동하는 것을 알 수 있다.
이후 시간이 되면 나머지 기능들을 넣어 테스터를 제작완료할 예정이다.
공격 카드 사용시 발동하는 이펙트를 구현해보았다. 원작 게임엔 더 디테일한 묘사가 있지만 현재 단계에서의 목적은 이펙트와 애니메이션까지 동일하게 구현하는 것이 아니기에 다음 단계로 넘어가려 한다.
마찬가지로 간단한 테스트를 통해 느낌 정도만 맞추고 넘어가겠다. 유니티 애니메이터, 애니메이션 기능 위주로 활용하여 코드 쓸 것도 별로 없었다.
참고로 매 과정을 끝낼 때마다 사운드도 넣고 있는데 들려줄 수 없어 아쉽다.
타겟에 커서를 대면 보이는 조준선 또한 만들었다. 기존에는 IPointerEnterHandler의 OnPointerEnter 함수를 활용해 마우스 커서가 적 콜라이더 위에 올라가면 동작하도록 했었는데, 이렇게 할 경우 마우스가 이미 올라가 있는 상태에서 키보드 버튼으로 카드를 선택하면 작동하지 않는 문제가 생겼다. 그래서 이 부분을 뜯어 고쳐 OnPointnerEnter 함수가 아닌 RaycastHit2D를 활용했다. 원리는 단순하다. 레이를 쏴서 마우스 위치와 카드 선택 여부에 따라 작동하도록 수정하였다.
데미지 인디케이터의 경우 베지어 곡선을 다시 사용할까 하다가, 데미지 크기(int)를 받기 위해 TextMesh Pro를 사용할 필요가 있어 애니메이션으로 대체했다. 이때 2D 프로젝트에서 TMP의 3D Object를 만들시 Rect Transform으로만 사용이 가능하여 다른 효과들과 위치값 통일이 어려울 것 같아 아예 UI로 만들어두었다. 다른 UI들보다 위에서 보여질 수 있도록 Effect 캔버스를 따로 만들어두었다.
사실 슬레이 더 스파이어에서는 데미지 인디케이터가 수직으로 떨어질 때 아웃라인이 진한 녹색으로 변하지만, 이 부분은 TMP 폰트 에셋의 쉐이더 값에 따라가 애니메이션 내에 자체적으로 넣기는 힘들었다. 그래서 두 개의 텍스트를 겹쳐두고 떨어질 때 알파 값을 서로 교환해 자연스럽게 아웃라인 색이 변하는 연출을 해볼까 한다.
프로토타입에서는 딱 3장의 카드 기능만 구현해보기로 했다. 그 중 마지막인 강타 카드의 구현을 마쳤으니 프로토타입 제작은 끝난 셈이다.
이로써 카드의 순환, 사용에서 오는 재미 검증을 위한 프로토타입 제작을 마쳤다. 사실 처음 생각했던 것보다 더 많은 기능들을 구현하게 되었으나 애초 목표 일정이 10월 31일까지라 시간이 남아서 한 것들이 많았다. 특히 애니메이션 관련된 부분은 고려조차 하지 않았던 부분인데 기대만큼 결과물도 잘 나오고 바로 보여지는 부분이 있다보니 시간이 아깝지 않았다.
이제 마무리 검토 후에 자체 테스트를 해볼 생각이다. 테스트의 주 목적은 카드를 뽑는 것과 쓰는 것이 재미있느냐가 될 것이다.
마우스를 흔들어 적을 베어내는 게임. 아이디어도 흥미롭고 보여지는 모습도 게임 시스템이 주는 맥락과 일치하여 재밌어 보인다.
실제로 X에 게시된 포스트를 보면 반응도 나쁘지 않아 보이는데 5,600원이라는 가격에 심리적 저항이 느껴지는 건 왜일까.
게임 플레이가 단조로울 것 같다는 생각이 들어서일까? 애니메이션이 화려하고 끊임 없이 조작이 가해진다고 해서 단조로움을 벗어날 수 있는 건 아니다.
물론 해보지도 않은 게임에 대해 지레짐작하는 것일 뿐이지만...
나중에 우리가 만든 게임에도 비슷한 느낌을 받는 사람이 분명 있을텐데 이런 부분을 미리 방지할 수 있도록 고민해봐야겠다.
R&D 프로토타입이 제작 완료되어 기능적인 구현을 정해진 기간 내에 수행할 수 있음을 확인하였고 팀 2명 모두 작업을 이어나가는데 있어 문제가 없다고 판단하여 베타를 목표로 1차 프로토타입 제작을 시작하였다.
먼저 제작에 앞서 스토리를 견고히 할 필요성을 느껴 기존 기획단계의 스토리를 더 풍부하게 하기위해 작업하고있다. 생각외로 쉽지 않은 작업이지만 문학적으로 경험 많이 했으면 조금 더 나은 결과를 얻지 않았을까하는 생각을 하며 다시 쓰기를 반복하고 있다. 쓰면서 머릿속에 있는 이미지를 시각화 하기위해 스토리에 따른 시스템들을 적어가며 바로 제작할 수 있도록 구조를 만들어두고 있다. 물론 다시 쓰게 된다면 사용이 안될 시스템들이지만 이러한 작은 경험 하나하나 쌓여 추후 다른 장르의 작업을 하게 될 때 밑거름이 될 것이라고 판단하여 폐기하지 않고 시스템 창고라는 폴더를 만들어 구조들을 정리한 메모장을 모아 저장하고 있다.
현재 아서 C 클라크의 유년기의 꿈을 읽고 있으며, 헤르만 헤세의 데미안 또한 읽어볼 예정이며 각각의 책들을 읽으면서 안목을 높여갈 예정이다.
내가 만족을 할 정도는 되어야 부족한 느낌을 들더라도 제작하는데 있어 문제는 들지 않을 것이다.
또한 PC게임들 뿐만 아니라 보드게임들도 플레이 해보며 시스템들을 바라보는데 있어 한 시점에 국한되지 않도록 하고 있다.
여러가지 경험과 추억들을 담아내며 그것을 공유하도록 공감을 만든다. 라는 것이 쉽다고는 생각하지 않았지만 더 어려운 일이었고 부족함이 보이고 있지만 이러한 부족한 점이 명확하게 보인다는 것이 다행이다. 이를 통해 더 성장할 수 있다는 것이기 때문이다.
말이 길어졌지만 기능적으로 돌아가는 것을 넘어 우리의 게임으로 만들기 위한 과정의 한 걸음에 대한 포부를 적어보았다.
이 첫 걸음이 두 걸음이 되고 결승점까지 걸어가는데 있어 손을 잡고 있는 모두가 흔들리지 않도록 서로를 바라보며 발 맞춰 나아가보려고 한다.
플랫폼: Windows 11, Steam
플레이 타임: 5시간 이상
목적: 일명 '카드깡' 의 재미와 스팀 내 출시된 인디 게임의 성공 요인 확인
-
이 게임을 구매한 이유
스팀 스토어를 둘러보던 중 '최근 인기 있는 게임' 이라며 슬며시 추천된 게임이 하나 있다. 바로 TCG Card Shop Simulator다. 썸네일과 트레일러를 보고 '이런 게임을 추천한다고?' 라는 생각이 들었고, 15,000 개 이상 쌓인 긍정적인 리뷰들을 보며 '대체 얼마나 재밌길래' 라는 호기심이 생겼다. 그 리뷰 중 대다수는 '카드깡' 을 재미 요소로 꼽았으나 그로 인한 재미를 느껴본 적이 없어서인지 쉽사리 공감하지는 못했다. 백문불여일견. 백 번 듣는 것보다 한 번 보는 게 낫다고 하지들 않던가. 그걸 넘어 백 번 보는 것보다 한 번 하는 게 나을 거 같아 게임을 구매했다.
-
카드깡, 이게 그렇게 재밌나?
다들 카드깡, 카드깡 노래를 부르니 이걸 먼저 확인해보지 않을 수 없었다. 본디 카드깡이라 함은 카드 팩을 뜯어서 원하는 카드를 얻는 행위가 아니겠는가? 그런데 이 게임에서는 '원하는 카드' 라는 게 있을 수 없다. 왜냐면 카드를 얻어봤자 쓸모가 전혀 없기 때문이다. 카드마다 포켓몬을 닮은 듯한 테트라몬의 일러스트와 각종 효과가 나열되어 있기는 하나, 플레이어는 그저 카드를 판매하는 사람일 뿐 이를 어떤 게임에 사용하는 '플레이어' 가 될 수는 없다. 심지어 매장에 찾아오는 고객들의 모습을 지켜봐도 카드 게임을 진짜 플레이하는 건지 그런 시늉만 하는 건지 분간이 안 될 정도이다. 게다가 이 '테트라몬' 들은 카드 안에 그저 그려져 있을 뿐, 그 외 다른 곳에서의 모습은 전혀 지켜볼 수 없기에 특정 테트라몬에게 정을 붙이기에는 아무런 맥락이나 이야기가 주어지지 않는다. 적어도 내게는 이 카드가 그저 돈의 단위를 나타내는 '화폐' 일 뿐, 다른 방식으로 작용하지는 않았다. 즉 카드깡은 게임 내 재화를 벌기 위한 하나의 수단일 뿐 핵심 재미요소 로 다가오지는 않았다는 뜻이다.
-
단순 노동에 재미를
그렇다면 무엇이 이 게임을 '재밌다' 고 느끼게 하는가. 바로 끊임 없이 쏟아지는 단순 과제들이다. 당신은 가게를 열자마자 카드를 주문하고 상자를 들여온 뒤, 매대에 진열하고 매장을 정리하며, 물건을 계산까지 해주어야 한다. 그리고 이 일련의 과정은 개장부터 폐장까지 무한히 반복된다. 그렇다면 이게 전부인가? 그것도 아니다. 당신은 이 과정 속에서 매장의 크기를 확장하고 물건의 종류를 다양화하며 직원을 고용하는 등 그 굴레의 크기를 키우는 행위까지 동반시켜야 한다. 그러다보면 실제 매장에서 일을 한 듯 피로감 또한 느낄 수 있지만 그렇다고 재미가 없는 건 아니다. 이 게임엔 재미가 있다. 그래서 피곤한 듯 느껴져도 계속 진행할 원동력이 생긴다. 처음 팰월드(Palworld)를 접했을 때와 비슷한 인상을 주는 게임이다. 게임 내에서 제시하는 소과제를 하나하나 뜯어보면 단순 노동에 불과한데, 서로 다른 단순 노동이 합쳐져 큰 틀을 구성하는 그런 느낌. 불미스럽게도 두 게임 모두 포켓몬 프랜차이즈를 카피(?)했다는 공통점이 있기도 하다. 개인적으로 팰월드에서의 팰은 포켓몬의 개성을 담아낸 다른 무언가로 느껴졌다면, 이 게임에서의 테트라몬은 정말 포켓몬 카피로 느껴진다. 아래 첨부한 카드 북 이미지를 보면 대번에 느낄 수 있을 것이다.
-
결론
TCG Card Shop Simulator는 재밌는 게임이다. 으레 '시뮬레이터' 를 표방하는 게임들이 그렇듯 이 게임 또한 영혼 없는 눈을 가진 NPC들이 가득하지만, 이토록 정신 없이 집중해서(말이 안 되지만 다른 적절한 표현이 없다) 플레이한 게임은 정말 오랜만이기에 높이 평가하고 싶다. 특히 매 상호 작용의 애니메이션이나 사운드 등 의외의 세심함을 보여준 구석도 있어 여러모로 놀라운 인상을 준다. 특히 카드 팩을 뜯을 때와 계산대 앞에 설 때 등 실제 세상에서 행동했을 때 보일 법한 시야(그리고 그렇게 들릴 법한 사운드)를 구현하려 했다는 게 느껴졌다. 이 게임이 선불이 아닌 후불이었다 하더라도 14,500 원을 마땅히 지불했을 것이다. 다만 우리가 만들 게임에 적용할 만한 요소를 찾지 못한 것은 아쉽다. 아무리 생각해도 이런 방식의 카드깡은 우리가 만들 게임이 카드 게임일지라도 적용해내기는 어려울 것으로 예상된다.
스토리를 러프하게 작성하여 살을 붙여보며 팀원과 공유하여 내용을 점검하고 수정을 거쳤지만 너무 밝은 분위기와 그렇지 않은 내용의 갭으로 인해 1차로 문제가 생겼고 2차로 실제 게임에 대입하여 보았을 때 너무 난잡해지는 이미지가 그려저 전부 폐기하고 새로 작성하기로 결정을 하였다.
기존 스토리에서 차용할 부분을 차용하고 분위기를 좀 더 무겁게 잡고 작성을 해보았지만 스토리의 구멍이 너무 많아 한번 더 폐기하기로 결정하였다. 하지만 성과로는 꼭 가져가야할 부분들이 스토리를 폐기할 때마다 생겨나 스토리를 재작성 할때마다 좀 더 견고해지는 것이 느껴지고있다.
좀 더 속도를 내야하며 속도를 내는 와중에도 견고하게 하여 팀원과 내가 동의할 수 있는 흥미로운 내용으로 구상을 하려고 한다.