▶ 개요
이름: Dumb & Zombie
인원: 4명(프로그래머 3명, 그래픽 1명)
작업 기간: 일주일(24.08.05 ~ 24.08.11)
작업 내용: Photon Fusion2를 사용한 캐릭터 로직 및 동기화 작업
플레이 링크: https://zempie.com/game/cw3ut56iif5
▶ 들어가며
저희는 팀 구성이 프로그래머 3명, 그래픽 1명입니다.
프로그래머가 많았지만, 세 명 모두 멀티 게임 제작 경험이 전무했습니다.
이 상황에서 일주일의 짧은 기간 동안 새로운 기술을 배우며 발생한 이슈들을 정리했습니다.
부족한 부분이 많으며 비효율적인 과정이 많습니다.
▶ 무엇을 했는가?
저희 팀은 가산점을 위해 Fusion2를 사용해서 게임을 개발했습니다.
서버, 캐릭터, 오브젝트 세 파트로 업무를 구분했으며,
저는 캐릭터 파트를 맡아 게임을 제작했습니다.
▶ 기술 부채
처음 사용하는 기술인 만큼 기술 부채 관리에 어려움이 있었습니다.
짧은 시간 동안 처음 사용해 보는 기술을 완벽하게 익히기는 불가능하다고 생각했습니다.
그렇기에 먼저 로컬로 게임을 개발하고 서버로 포팅하는 방식을 채택했습니다.
이 과정에서 수없이 많은 문제가 발생하기 시작했습니다.
작업 3일차인 8월 7일, 본격적인 서버 포팅 작업에 들어갔습니다.
포톤에 업로드된 [레이저 매드니스]셈플 프로젝트를 기반으로 로컬로 만든 움직임을 점진적으로 포팅했습니다. 이 과정은 난항의 연속이었습니다.
로컬 상태에서는 정상적으로 움직이던 캐릭터가 순간이동 하는 문제, 서버와 클라이언트의 동기화 이슈, 매칭 이슈, 캐릭터 생성 이슈 등등 수없이 많은 문제들이 터져 나오기 시작했습니다.
게임의 핵심 로직인 로프 기능인 만큼 구현이 필수적인데
익숙하지 않은 환경에 어디서부터 어떻게 접근해야 하는지조차 명확하지 않아 막막했으며,
시간은 부족한 상황이었기에 걱정도 많았습니다.
이런 기술 부채를 해결하기 위해 디스코드의 기술 문의 포럼을 활용하고 기존 샘플 프로젝트를 분석했으며, 기술 문서를 정독하여 Fusion에 대한 이해도를 높였습니다. 위 과정에서 에반젤리스트님의 친절한 답변 덕분에 문제의 갈피를 파악할 수 있었고, 이를 바탕으로 작업을 진행한 결과 이틀에 걸쳐 핵심 로직을 구현할 수 있었습니다.
▶ 핵심 로직, 무엇이 문제였나
문제의 원인은 복합적이었습니다.
동기화 이슈와 로직 논리의 오류로 버그가 생겼고, 잘못된 방향으로 문제를 정의하면서 버그를 발견하는 데 난항을 겪었습니다.
질문을 통해 핵심 로직인 Spring Joint 컴포넌트를 Fusion2에서 사용할 수 있다는 답변을 받은 뒤 계속해서 시도해 봤으나, 문제 해결에 어려움을 겪게 되면서 추가 질문을 드렸습니다.
포톤 에반젤리스트님이 작성한 코드를 보면서 검토 및 답변을 해주셨고
이를 바탕으로 동기화와 당기는 로직에 문제가 있음을 확인하고 수정했습니다.
그 외 문제 해결을 위해 고민한 내용은 이런 것들이 있었습니다.
- 내부 물리 처리로 인한 동기화 이슈 추측
처음에는 Spring Joint 컴포넌트의 경우 엔진에서 내부적으로 Rigidbody2D의 Velocity를 처리하기 때문에 동기화가 되지 않는 것으로 생각했습니다.
그래서 최악의 경우, 플레이어끼리 연결하는 로직을 직접 구현하는 방향도 고민했습니다.
- SpringJoint2D 상속
Velocity 값이 동기화되지 않는 것이 문제라면, SpringJoint2D를 상속받아서 변경하는 값을 직접 동기화하는 방법을 생각했습니다.
접근지정자로 막혀있어도 리플랙션으로 어떻게든 값을 가져올 수 있으니 가능할 것으로 생각했지만,
[sealed] 키워드로 상속이 막혀있어 포기했습니다.
- 기술 문서 튜토리얼 정독
아무리 샘플 프로젝트를 기반으로 작업하더라도 Fusion2를 모르는 상태에서 RPC의 기본 개념만 가지고 개발하기에는 문제가 많았습니다.
중간부터 튜토리얼을 하나씩 읽으면서 막혔던 부분을 파악하고 보완했습니다.
이틀에 걸쳐 문제 해결을 위해 여러가지 노력 끝에 결국 성공했습니다.
[Networked, OnChangedRender(nameof(SetJoint))]
public bool IsJointEnabled { get; set; }
[Networked, OnChangedRender(nameof(SetJoint))]
public float NetworkRopeDistance { get; set; }
void SetJoint()
{
_springJoint2D.enabled = IsJointEnabled;
_springJoint2D.distance = Mathf.Clamp(NetworkRopeDistance, 1, maxRopeDistance);
}
FixedUpdateNetwork에서 Joint에 접근하여 값을 수정하는 방식은 클라마다 동기화가 잘 안되는 것으로 확인했습니다.
Networked를 이용하여 변수의 값을 동기화시킨 후, 위 값이 변경될 때마다 Render 함수를 호출하여 동일한 값이 할당되도록 수정했습니다.
▶ 그 밖의 이슈
- 서버에서만 로직이 실행되도록 제작
Fusion2의 FixedUpdateNetwork에 대한 이해도가 부족한 상태에서 많은 어려움이 있었습니다.
Host에서는 모든 오브젝트의 FixedUpdateNetwork 함수가 한 틱당 1회씩 실행되는 반면, 클라에서는 컨트롤하는 오브젝트의 FixedUpdateNetwork 함수가 한 틱에 여러 번 실행되면서 결과를 가늠하기가 힘들었습니다. (튜토리얼에서는 시뮬레이션을 위해 여러번 실행된다고 나와있습니다.)
if (!Runner.IsServer)
return;
그래서 저는 모든 스크립트가 원활하게 돌아가는 Host 클라이언트를 기준으로 작업하기로 결정했습니다.
효율적인 방법은 아니었지만, 리스크를 고려할 때 최선의 선택이었습니다.
조건 분기를 추가하여 Host에서만 명령을 하달하는 방식으로 조금 지연이 발생하겠지만, 명확하게 로직이 실행되도록 제작했습니다.
▶ 테스팅 환경 세팅 이슈
처음으로 제작하는 멀티 게임이었기에 테스트 환경 구성을 어떻게 해야 하는지 잘 몰랐습니다.
서버와 클라이언트 동기화 확인을 위해 [In-game Debug Console]이라는 디버그 에셋을 사용했습니다.
이번에는 일일이 빌드하면서 테스트했습니다. 그렇기에 매번 빌드를 하면서 대기하는 시간이 많아 불편했습니다. 이런 문제를 해결할 수 있는 [ParrelSync] 오픈 소스를 지인에게 추천받아 다음에는 이를 활용해 개발 환경을 세팅할 계획입니다.
▶ 마치며
일주일간 강행군을 진행하면서 어려움이 많았습니다.
fusion2가 널리 보급된 네트워크 라이브러리가 아니다 보니 정보도 많이 없었고, 알 수 없는 버그가 계속 발생했습니다. 의도치 않은 작업으로 지연이 생겨 원하던 느낌을 내지는 못한 게 아쉬웠습니다.
그래도 처음 써보는 기술이 신기하기도 하고 이겨내고 구현됐을 때의 뿌듯함은 역시나 좋네요
재미있게 즐겼으며 많은 것을 알아가는 시간이었습니다 :D