DeskVegas

3D Las Vegas multiplayer board game

Release v1.3.1
DeskVegas
Timeline

Overview

DeskVegas는 단 4일간의 개발로 완성한 3D 라스베가스 멀티플레이 보드게임입니다. Unity 엔진과 Mirror 프레임워크를 기반으로 구축했으며, Steamworks를 이용해 Steam 플랫폼과 멀티플레이 환경을 통합했습니다.

친구들과 보드게임장에서 게임을 하다가 너무 재미있어서, 집에서도 같이 할 수 있게 직접 만들어 보면 어떨까 하는 생각에서 시작했습니다.

개발 일정

2025.06.16 | 네트워크 기반 핵심 코어 구현

Note (2025.06.16 개발일지)
  • 게임 로비, 방 참가, 가입, 씬 전환 완성
  • 게임 매니저 추가
  • 게임 플레이어 추가
  • 메인 메뉴 UI 완성
  • 인게임 UI 완성

Steamworks를 이용한 게임 로비 시스템

Assets/Scripts/P2P/Steam/SteamLobbyService.cs
public class SteamLobbyService : DontDestroySingleton<SteamLobbyService>
{
...
// Steam 콜백
private Callback<LobbyCreated_t> _cbLobbyCreated;
private Callback<LobbyMatchList_t> _cbMatchList;
private Callback<LobbyEnter_t> _lobbyEnterCallback;
private Callback<LobbyDataUpdate_t> _cbLobbyDataUpdated;
...
private void CreateLobby(ELobbyType type) { }
private void JoinLobby(CSteamID lobbyID) { }
public void LeaveLobby() { }
...
}

InGame UI 시스템

InGame UI


2025.06.18 | 게임 로직 구현

Note (2025.06.18 개발일지)
  • 주사위 던지는 기능 추가
  • 보드판에 돈 분배 기능 추가
  • 보드판 시스템 추가
  • 주사위 보드판에 배치 추가
  • 정산 시스템 추가
  • 턴 규칙 수정
  • Sync 오류 수정
  • 기타 안정화 작업

물리 기반 주사위 시스템

Assets/Scripts/Dice/VegasDice.cs
[Server]
private void RollPhysically()
{
if (!_rb || _rb.isKinematic)
{
CustomLogger.LogWarning(LogCategory.System, $"주사위 {diceID} 물리 굴리기 실패 - Rigidbody가 null이거나 kinematic 상태", this);
return;
}
// 물리 초기화
ResetPhysics();
Vector3 force = (Vector3.down + Random.onUnitSphere * 0.5f).normalized * 3f;
_rb.AddForce(force, ForceMode.Impulse);
// 랜덤 회전력
Vector3 torque = new Vector3(
Random.Range(MIN_TORQUE, MAX_TORQUE),
Random.Range(MIN_TORQUE, MAX_TORQUE),
Random.Range(MIN_TORQUE, MAX_TORQUE)
);
_rb.AddTorque(torque, ForceMode.Impulse);
}

보드판에 지폐 분배 시스템

Assets/Scripts/Board/VegasCasinoBoardManager.cs
[Server]
private void SetupCasinoBills(int casinoIndex)
{
var casinoBills = new List<BillCard>();
int totalMoney = 0;
while (totalMoney < minCasinoMoney && _billCardPool.Count > 0)
{
var card = _billCardPool[0];
_billCardPool.RemoveAt(0);
casinoBills.Add(card);
totalMoney += card.GetActualAmount();
}
casino.SetBillCards(casinoBills);
}

2025.06.20 ~ 2025.06.23 | 안정화 및 릴리즈

Note (2025.06.19 Release v1.1.0)
  • isReady 확인 코드 추가
  • MainMenu Lobby의 PlayerList에서 한글 이름이 깨지는 오류 해결
  • InGame UI의 MaxRound가 4로 고정된 오류 해결
  • 게임 종료 크래시 오류 해결
  • 게임 자동 종료 시간 변경 (15초 → 10초)
  • 게임 자동 종료 카운트다운 추가
  • 앣타 망가짐님의 메인 메뉴 일러스트 추가
  • MainMenu UI 전면 변경
  • 턴 제한 시간 변경 (120초 → 60초)

MainMenu UI

Note (2025.06.23 Release v1.2.0)
  • 이모지 시스템 추가
  • 배경음악 시스템 추가
  • 주사위 배치 시스템을 2줄에서 3줄로 수정
Note (2025.06.23 Release v1.3.0)
  • 핑크색 주사위 하이라이트 오류 수정
  • 디스코드 매니저 추가

핵심 기술 문제 해결

Mirror의 씬 전환 문제

Warning (문제 상황)

다른 씬으로 넘어갈 때 호스트가 클라이언트의 준비를 기다리지 않고 게임을 시작하는 문제가 발생했습니다. 예를 들어 메인 메뉴 씬에서 인게임 씬으로 넘어갈 때 재현되었습니다.

Tip (클라이언트가 호스트에게 준비 상태 전달)
Assets/Scripts/P2P/SceneReadyManager.cs
[Command(requiresAuthority = false)]
private void NotifyServerSceneReady(NetworkConnectionToClient sender = null)
{
if (_isSceneCheckComplete)
{
CustomLogger.LogVerbose(LogCategory.Network, $"씬 체크가 이미 완료됨 - 클라이언트 {sender.connectionId} 늦은 응답", this);
TargetConfirmNotificationReceived(sender);
return;
}
if (!_readyClients.Add(sender))
{
CustomLogger.LogVerbose(LogCategory.Network, $"클라이언트 {sender.connectionId}는 이미 Ready 상태로 등록됨", this);
TargetConfirmNotificationReceived(sender);
return;
}
CustomLogger.LogInfo(LogCategory.Network, $"클라이언트 {sender.connectionId} 씬 준비 완료 등록! ({_readyClients.Count}/{_expectedClientCount} 준비됨)", this);
TargetConfirmNotificationReceived(sender);
CheckAllClientsReady();
}

인게임 씬으로 이동하면 호스트는 일정 시간 동안 NetworkServer.connections.Count - 1만큼, 즉 호스트를 제외한 클라이언트들의 응답을 기다립니다. 모든 응답을 받으면 GameEvents.Network.OnSceneLoaded?.Invoke();를 호출하여 게임을 시작하도록 수정했습니다.

Mirror의 Sync 값 동기화 순서 문제

개발 중 가장 복잡했던 문제는 ClientRpc와 SyncVar 간의 동기화 타이밍 불일치였습니다.

Warning (문제 상황)

ClientRpc 메시지가 SyncVar 업데이트보다 먼저 클라이언트에 도착하여 클라이언트가 이전 상태 값을 참조하는 문제가 발생했습니다.

예를 들어 턴이 Player 1에서 Player 2로 넘어갈 때 다음 순서로 문제가 재현되었습니다.

  1. 서버에서 currentPlayerIndex SyncVar를 2로 업데이트
  2. 서버에서 RpcOnCurrentPlayerChanged() ClientRpc 호출
  3. 클라이언트에서 ClientRpc가 먼저 실행되어 여전히 currentPlayerIndex = 1을 읽음
Tip (Sync 값 대신 변경 값을 직접 전달)
Assets/Scripts/Core/VegasGameManager.cs
[ClientRpc]
private void RpcOnCurrentPlayerChanged()
{
// currentPlayerIndex SyncVar가 아직 업데이트되지 않을 수 있음
GameEvents.VegasGame.OnCurrentPlayerChanged?.Invoke(currentPlayerIndex);
}
[ClientRpc]
private void RpcOnCurrentPlayerChanged(int newPlayerIndex)
{
currentPlayerIndex = newPlayerIndex;
// 서버에서 직접 전달받은 값 사용
GameEvents.VegasGame.OnCurrentPlayerChanged?.Invoke(newPlayerIndex);
}

Credits

  • hy — Developer
  • 앣타 망가짐 — MainMenu Illustrator