작성 이유
이번 프로젝트에서 여러 담당을 맡았던 도중에 용광로 핸들러를 마무리 작업을 하던 도중에 문제가 생겼다.
문제는 내가 설계한 대로 하면 클라랑 서버랑 계속 시간을 비교해서 동기화를 서로 해줘야하는 작업이 있어야한다.
다시 말하자면 현재 값을 계속 클라한테 보내줘야 하는데 멀티 게임 특성상, 클라 끼리 계산을 해줄 수 있는걸 보내줬어야 한다.
만약에 서버랑 클라랑 주기적으로 동일한 시간을 계산 할 수 있는 걸 받을 수 있다면 문제 없는 코드이지만 우리는 현재 클라에게 부담을 주지 않게 모든걸 계산해서 다 줘야한다.
설계
요약 설계 흐름
Furnace Class: 용광로의 상태와 진행도를 관리.
Team Class: 각 팀마다 Furnace를 포함하여 용광로 상태를 관리.
FurnaceSyncManager: 팀의 용광로 상태를 주기적으로 클라이언트와 동기화.
클라이언트 업데이트: 동기화된 데이터를 바탕으로 UI 및 로직 업데이트.
1. Furnace Class
용광로의 상태(State)와 진행도(Progress)를 관리하는 클래스입니다.
2. Team Class with Furnace
팀 클래스에 용광로(Furnace)를 추가하여 각 팀이 자신의 용광로 상태를 관리할 수 있게 설계합니다.
플레이어 상태 및 상호작용:
빈손 & 용광로 상태 PRODUCING 이상 → 플레이어 아이템에 Iron 또는 Garbage 추가.
아이템이 Ironstone & 용광로 상태 WAITING → 플레이어 아이템 비움.
아이템이 Ironstone & 용광로 상태 PRODUCING 또는 OVERFLOW → 진행률(Progress)을 0으로 초기화, 1초마다 10씩 증가.
용광로 진행률 관리:
IntervalManager로 진행률 증가 작업 반복.
상태 동기화 패킷(S2CSyncFurnaceStateNoti)으로 팀 전체에 알림.
결과 코드 처리:
용광로 상태가 RUNNING → FurnaceResultCode.RUNNING_STATE 전송.
상태가 WAITING & Ironstone 없음 → 실패(FAIL) 코드 전송.
추가 고려사항:
순환 구조 방지.
작업 취소 및 재시작 가능하도록 IntervalManager 활용.
부족한 점 보완
IntervalManager 활용: Furnace 클래스에 직접 통합하여 진행률 관리.
비동기 이벤트 관리: 플레이어와 용광로의 상호작용을 명확히 분리.
결과 처리 코드 정리: 재사용 가능하도록 공통 로직 분리.
코드 순서
1. 관리할 곳 Team
public class Team
{
// key - userToken
public readonly Dictionary<string, Player> Players = new();
public Furnace TeamFurnace { get; private set; } = new Furnace();
...
}
2. Team Class 만듦
using Cysharp.Threading.Tasks;
using ResourceWar.Server.Lib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ResourceWar.Server
{
public class Furnace
{
public SyncFurnaceStateCode State { get; private set; } = SyncFurnaceStateCode.WAITING;
public float Progress { get; private set; } = 0.0f; // 진행도 (0~100%)
private CancellationTokenSource progressToken;
public void StartProgress()
{
if (progressToken != null)
{
progressToken.Cancel();
}
progressToken= new CancellationTokenSource();
IntervalManager.Instance.AddTask(
$"Furnace_{GetHashCode()}",
async token =>
{
Progress += 10;
if (Progress >= 100 && Progress < 150)
{
State = SyncFurnaceStateCode.PRODUCING;
}
else if (Progress >= 150)
{
State = SyncFurnaceStateCode.OVERFLOW;
}
await UniTask.CompletedTask;
},
1.0f);
}
public void ResetProgress()
{
Progress = 0;
State = SyncFurnaceStateCode.WAITING;
IntervalManager.Instance.CancelTask(progressToken.Token);
}
public void UpdateState(SyncFurnaceStateCode newState)
{
State = newState;
}
public void UpdateProgress(float newProgress)
{
Progress = Math.Clamp(newProgress, 0.0f, 150.0f);
UpdateStateBasedOnProgress();
}
private void UpdateStateBasedOnProgress()
{
if (Progress < 1.0f)
State = SyncFurnaceStateCode.WAITING;
else if (Progress < 100.0f)
State = SyncFurnaceStateCode.RUNNING;
else if (Progress < 150.0f)
State = SyncFurnaceStateCode.PRODUCING;
else
State = SyncFurnaceStateCode.OVERFLOW;
}
}
}
3. GameManager에 생성
#region 용광로
public async UniTask FurnaceResponseHandler(ReceivedPacket receivedPacket)
{
var clientId = receivedPacket.ClientId;
var token = receivedPacket.Token;
if (!TryGetTeamIndex(clientId, out int teamIndex, out var team))
{
Logger.LogError($"Team not found for clientId {clientId}");
return;
}
var furnace = team.TeamFurnace;
if (!TryGetPlayer(clientId, out var player))
{
Logger.LogError($"Player not found for clientId {clientId}");
return;
}
FurnaceResultCode resultCode = FurnaceResultCode.SUCCESS;
// 용광로 상태 처리
switch (furnace.State)
{
case SyncFurnaceStateCode.WAITING:
if (player.EquippedItem == (int)ItemTypes.Ironstone)
{
player.EquippedItem = 0;
furnace.StartProgress();
}
else
{
resultCode = FurnaceResultCode.INVALID_ITEM;
}
break;
case SyncFurnaceStateCode.PRODUCING:
case SyncFurnaceStateCode.OVERFLOW:
{
player.EquippedItem = furnace.State == SyncFurnaceStateCode.PRODUCING ? (int)ItemTypes.Iron : (int)ItemTypes.Garbage;
furnace.ResetProgress();
furnace.StartProgress();
}
break;
case SyncFurnaceStateCode.RUNNING:
{
resultCode = FurnaceResultCode.RUNNING_STATE;
}
break;
default:
resultCode = FurnaceResultCode.FAIL;
break;
}
var responsePacket = new Packet
{
PacketType = PacketType.FURNACE_RESPONSE,
Token = token,
Payload = new S2CFurnaceRes
{
FurnaceResultCode = (uint)resultCode,
}
};
await SendPacketForUser(responsePacket);
// 용광로 상태 동기화
if (resultCode == FurnaceResultCode.SUCCESS)
{
await SyncFurnaceState(token, teamIndex, furnace);
}
}
private async UniTask SyncFurnaceState(string clientToken, int teamIndex, Furnace furnace)
{
var syncPacket = new Packet
{
PacketType = PacketType.SYNC_FURNACE_STATE_NOTIFICATION,
Token = clientToken,
Payload = new S2CSyncFurnaceStateNoti
{
TeamIndex = (uint)teamIndex,
FurnaceStateCode = (uint)furnace.State,
Progress = furnace.Progress
}
};
await SendPacketForTeam(syncPacket);
}
#endregion
결론
현재 구조에서는 처음부터 서버에서 모든 연산과 시뮬레이션을 돌려서 결과 값만 클라에 보내가지고 클라가 그걸 가지고 랜더링을 해줘야하는 과정을 거쳐야 하는 데 클라에게 부담을 주는 작업을 해서 다시 설계하기로 하였다.
따라서, 서버에서 1초마다 용광로 상태를 계속 보내주기로 했다.
2탄에서 설명하도록하겠습니다.
'TIL' 카테고리의 다른 글
Node.js 서버 GetSkill.Handler 작성 - 최종프로젝트 1 (0) | 2024.12.17 |
---|---|
자원 전쟁 (서버) 프로젝트 - 용광로 만들기 2탄 (0) | 2024.12.05 |
C# .csv파일 변환 방법과 예시 (0) | 2024.11.26 |
Class와 Structure의 차이점 및 사용 목적 (0) | 2024.11.26 |
자원 전쟁 프로젝트 ProtoBuf 구분 기준 (1) | 2024.11.15 |