프로젝트/OTT 게임

온라인 서비스 맞고/오목 프로젝트 (23.09.08 ~ 23.12.01)

SniKuz 2023. 10. 8. 20:04

* 해당 글은 외주 쪽 허락으로 작성하며 추후에 내려갈 수 있습니다.

[계기]

학사일정과 스타트업 활동을 병행하고 있는 와중 스타트업에 외주가 들어왔다.
종합 유선 방송사인 CMB에서 레인보우TV라는 OTT를 준비하며 맞고, 오목, 고스톱 등 게임을 티비 버전으로 만드는 외주를 받았는데 맞고는 타회사가 만들고 있고, 그걸 바탕으로 다른 게임들을 만드는 것이었다. 이미 완성본이 있는 상태로 다른 개발을 진행하니 서버는 어떻게 돌아가는지 지식을 배우고 싶었기에  공부하면서 하기 좋을 것 같아서 하고 싶다고 말씀 드렸다.

그러던 중 대표님이 맞고를 만들던 쪽에서 상대방에 피를 뺐는 부분에서 버그가 생겨 진행중이던 맞고가 우리쪽으로 넘어왔는데 조금 빨리 시작해야겠다는 얘기를 하셨다. 서버에 경우 스타트업 내 개발 책임자분이 개발하시고, 나 포함 클라 둘이서 클라 부분 UI 등을 만드는 작업이었다.

그렇게 어지러운 개발이 시작됐다...

[과정]

기존 계획과 달리 서버 쪽과 클라이언트를 아예 새로 개발을 하게 되었고, 병행 개발을 시작하게 되었다.

과정에서 가장 큰 어려움은 방금 시작한 교정도 빠듯한 일정도 아닌 서버에 대한 무지였는데, 네트워크라고는 3학년 1학기 네트워크 강의에서 http 서버 만들어보기 기초과제와 강의만 들은 나에게, 서버와 클라 개발을 동시에 진행하며 서버를 구조를 중간에 여러번 갈아 엎은 프로세스는 너무 어렵게 다가왔다...

게임 서버는 Colyseus라는 웹소켓 기반 소켓통신 프레임워크를 쓰고, 로그인 부분은 외주를 맡긴 쪽에서 파이어베이스 JWT토큰으로 넘겨주시고, 서버에서는 엔티티를 생성해 넘기며 이벤트들은 RPC(Remote Procedure Call)를 쓰면 된다 하셨다. 진짜 나는 RPC를 태어나서 처음 들었다... 마감일이 급박해 RPC를 제대로 알지도 못하고 그냥 사용하고 이미 있던 것들을 끼워 넣는... 옷 만드는데 천이 부족해서 마구 기운듯한 느낌으로 개발을 진행했다...

[문제 상황]

1. 데이터와 맞고 애니메이션 순서

서버에서는 당연하게도 데이터를 계속 보내줬다. 게임 시작 > 선공 후공 > 덱 생성 > 패 제공 > 패 사용 > ...등등
여기서 생긴 문제점은 서버는 아주 빠르게 줘도 클라에서는 그걸 순차적으로 앞뒤 간격을 두고 애니메이션을 순서에 맞게 실행해줘야 한다는 점이었고 그거에 대한 고려를 미처 하지 못했던 부분이 문제였다...

해당 부분을 어떻게 할까하다가 나온 결론은 전달 받은 애니메이션(카드 이동, 고/스톱/폭탄 등 애니메이션 이펙트 등)은 아무리 빠르게 와도 결국 순차적으로 올거고 이를 내가 타이밍에 맞춰  꺼내서 애니메이션을 실행시켜주기만하면 되니 이벤트큐같은 형태를 쓰면 해결되지 않을까? 생각했다.

초기에는 UniTask를 사용해 이전 작업에 완료를 체크하고 다음 task로 넘어가는 방식으로 할려고 하다가 최종적으로는 코루틴으로 작성하게 되었고, 작성을 하다가 원하던 기능이었던 아래 솔루션을 찾아서 아래 내용을 바탕으로 수정해서 사용했다.

더보기
// source : https://www.jacksondunstan.com/articles/3241#comments

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
/// <summary>
/// Imposes a limit on the maximum number of coroutines that can be running at any given time. Runs
/// coroutines until the limit is reached and then begins queueing coroutines instead. When
/// coroutines finish, queued coroutines are run.
/// </summary>
/// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3241</author>
public class CoroutineQueue
{
	/// <summary>
	/// Maximum number of coroutines to run at once
	/// </summary>
	private readonly uint maxActive;
 
	/// <summary>
	/// Delegate to start coroutines with
	/// </summary>
	private readonly Func<IEnumerator,Coroutine> coroutineStarter;
 
	/// <summary>
	/// Queue of coroutines waiting to start
	/// </summary>
	private readonly Queue<IEnumerator> queue;
 
	/// <summary>
	/// Number of currently active coroutines
	/// </summary>
	private uint numActive;
 
	/// <summary>
	/// Create the queue, initially with no coroutines
	/// </summary>
	/// <param name="maxActive">
	/// Maximum number of coroutines to run at once. This must be at least one.
	/// </param>
	/// <param name="coroutineStarter">
	/// Delegate to start coroutines with. Normally you'd pass
	/// <see cref="MonoBehaviour.StartCoroutine"/> for this.
	/// </param>
	/// <exception cref="ArgumentException">
	/// If maxActive is zero.
	/// </exception>
	public CoroutineQueue(uint maxActive, Func<IEnumerator,Coroutine> coroutineStarter)
	{
		if (maxActive == 0)
		{
			throw new ArgumentException("Must be at least one", "maxActive");
		}
		this.maxActive = maxActive;
		this.coroutineStarter = coroutineStarter;
		queue = new Queue<IEnumerator>();
	}
 
	/// <summary>
	/// If the number of active coroutines is under the limit specified in the constructor, run the
	/// given coroutine. Otherwise, queue it to be run when other coroutines finish.
	/// </summary>
	/// <param name="coroutine">Coroutine to run or queue</param>
	public void Run(IEnumerator coroutine)
	{
		if (numActive < maxActive)
		{
			var runner = CoroutineRunner(coroutine);
			coroutineStarter(runner);
		}
		else
		{
			queue.Enqueue(coroutine);
		}
	}
 
	/// <summary>
	/// Runs a coroutine then runs the next queued coroutine (via <see cref="Run"/>) if available.
	/// Increments <see cref="numActive"/> before running the coroutine and decrements it after.
	/// </summary>
	/// <returns>Values yielded by the given coroutine</returns>
	/// <param name="coroutine">Coroutine to run</param>
	private IEnumerator CoroutineRunner(IEnumerator coroutine)
	{
		numActive++;
		while (coroutine.MoveNext())
		{
			yield return coroutine.Current;
		}
		numActive--;
		if (queue.Count > 0)
		{
			var next = queue.Dequeue();
			Run(next);
		}
	}
}

서버와 싱크 등 다른 문제도 많이 남았지만 위 작업은 결과 자체는 꽤 성공적이었다.

 

2. 서버 구조에 대한 이해도 부족

버그 중에 폭탄 더미카드(폭탄을 쓰고 받는 더미카드)가 써졌다가 안써졌다가 하는 문제가 있었다. 확인해보니 각 작업, 카드 등 모두 고유한 엔티티ID를 가지고 있고, ID를 바탕으로 서버에서 작업을 하는데 손패에 있는 카드 사용 함수가 카드 클래스를 인풋으로 받아 사용을 했었는데, 폭탄 카드를 제외한 나머지 카드들은 게임마다 고유하니 카드 단위로 써도 크게 문제가 생기지 않아 생각하지 못했던 부분이었다. 

여러 카드 중 오직 폭탄카드만 카드가 여러장일 수 있어 카드의 태그가 동일해 엔티티ID 단위로 체크를 해줘야 했는데 모든 카드를 처음부터 엔티티 단위로 바꾸기 힘들고 비효율적일 것 같아 예외처리로 해결했다.

3. 게임 세부 요구사항에 대한 내용

맞고 게임을 간간히 플레이했던 입장에서 몇몇 요구사항은 타 게임에서는 별로 없고 납득이 안되는 부분이 있었다. 특히 일반적으로 카드를 내는 시간을 엄청 눈에띄게 UI 설정을 하지 않는 경우가 대다수인데 요구사항은 매 턴마다 무조건 시간초 표시가 나오거나, 선잡이에서 표시 방법등 의문점이 꽤 있었는데 몇몇 의문점은 의견을 넣어 어느정도 수정이 되었고, 대부분은 지속적인 질문으로 해결 되었다.

4. 스마트TV 하드웨어 문제

제일 큰 문제로 남은 문제는 스마트TV에 하드웨어 문제였는데... 이 문제로 일부 티비에서는 기본제공 가상키보드가 깨져서 가상키보드를 에셋으로 따로 접목하거나 기기 성능이 너무 안좋아 아예 게임 진행이 어려운 경우가 있었다.

대부분 스마트 티비 OS가 자체 OS (삼성은 Tizen)를 사용했고 기기 자체도 스토리지가 4GB인데 필수 sw가 이미 2.2GB를 채워져 있고 RAM이 대체 몇인지... 업체쪽에서 쓰는 환경을 우리가 볼 수 가 없어서 매우 답답한 상태였다.

아이패드에서는 무리없이 잘 돌아갔는데 스마트 티비 성능이 이렇게 안좋을줄은 다들 몰랐다... 또 스마트 티비에서 apk로 받는게 아니라 webGL로 하는게 요구사항이었는데, 유니티에서 webGL 성능이 좋은지?... 모르겠다.

[마무리]

맞고는 교정이슈로 발치를 하게되어 거즈물고 작업하면서는 대체 이게 뭐하는건가 싶었지만, 크런치를 하면서 실제로 회사 내 서버로 테스트를 할 때는 너무 재미있었고 신기했다. 과제로 집에서 공유기로 로컬로 http서버 돌아가는걸 해본거나 군대에서 주무관분을 따라다니며 스위치 작업도 했었지만 직접 짠 코드가 서버를 통해 양측 클라에서 잘 돌아가는게 너무 재밌었다.

완성된 게임을 가지고 서버를 천천히 공부하면서 시작할줄 알았지만, 그러기에는 시간과 내 기본지식이 너무 부족했고... RPC에 대한 이해, 서버와 클라에 대한 역할 이해 부족 등에 문제로 많은 것을 느낄 수 있었고, 기존에는 클라도 당연히 서버를 알아야 한다고 생각했지만 이번 경험으로 내가 생각하는 "알아야 한다"와 "진짜 안다"가 많이 차이나는 것을 느꼈다...

이후에 부랴부랴 오목도 참여를 하다가 학업 이슈로 남은 게임들은 참여하지 않게되었는데, 이 프로젝트 이후 학업에 신경쓰지 않고 프로젝트에 전념하고 싶은 생각이 정말 많이 들어 겨울 계절학기 까지는 학업에 전념해 졸업요건을 다 채우고 프로젝트를 진행할 계획이다.