2탄을 만든 이유:
1. 동기화 기준의 불명확성
- 서버와 클라이언트 간 용광로 상태를 동기화하는 명확한 기준이 부재
- 클라이언트간 독립적으로 계산할 수 있는 데이터를 제공하지 않고, 서버와 클라간에 시간을 주기적으로 서버에서 받아야만 정상 동작으로 설계
2. SRP 위반
- GameManager에 용광로 관련 연산 로직을 포함, 단일 책임 원칙을 지키지 못함.
- GameManager의 역할이 비대해지며 코드 가독성과 유지보수성에 부정적인 영향을 미침.
개선 방법:
1. 동기화 방식 개선
- 멀티플레이 환경에서 모든 클라이언트가 동일한 상태를 유지할 수 있도록 설계 변경.
- 서버 시간과 클라이언트 시간을 계산해 동기화하는 대신, 용광로 상태가 변경될 때마다 서버에서 시뮬레이션을 수행하여 연산 결과를 클라이언트에 전송.
- 클라이언트는 수신된 데이터를 기반으로 UI 및 로직을 업데이트하여 정확한 상태 동기화를 유지.
2. SRP 준수
- DataDispatcher를 사용해 데이터 참조 방식을 개선
- 기존에 직접 참조 없이 데이터를 가져오던 구조를 정리하여 책임 분리를 명확히 함.
- GameManager의 역할을 줄이고, 데이터를 관리하는 전용 클래스에서만 용광로 연산 로직을 처리.
GameManager 수정 방안
Furnace Class 수정
Furnace Class 전체 코드
using Cysharp.Threading.Tasks;
using Protocol;
using ResourceWar.Server.Lib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static ResourceWar.Server.GameManager;
namespace ResourceWar.Server
{
public class Furnace
{
public enum Event
{
FurnaceRequest
}
public SyncFurnaceStateCode State { get; private set; } = SyncFurnaceStateCode.WAITING;
public float Progress { get; private set; } = 0.0f; // 진행도 (0~100%)
private CancellationTokenSource progressToken;
public Furnace()
{
// 독립적인 EventDispatcher이고
// Event가 Key, ReceivedPacket이 value
// GameManager에 있는 EventDispatcher와 다르게 작동
EventDispatcher<Event,ReceivedPacket>
.Instance
.Subscribe(Event.FurnaceRequest, FurnaceResponseHandler);
}
public async UniTask FurnaceResponseHandler(ReceivedPacket receivedPacket)
{
var clientId = receivedPacket.ClientId;
var token = receivedPacket.Token;
var player = await DataDispatcher<int, Player>
.Instance.RequestDataAsync(clientId);
var (teamIndex, team) = await DataDispatcher<int, (int teamIndex, Team team)>
.Instance.RequestDataAsync(clientId);
FurnaceResultCode resultCode = this.FurnaceStateProcess(player, teamIndex, token);
var packet = new Packet
{
PacketType = PacketType.FURNACE_RESPONSE,
Token = token,
Payload = new S2CFurnaceRes
{
FurnaceResultCode = (uint)resultCode,
}
};
await EventDispatcher<GameManager.GameManagerEvent, Packet>
.Instance.NotifyAsync(GameManagerEvent.SendPacketForUser, packet);
}
public FurnaceResultCode FurnaceStateProcess(Player player, int teamIndex, string clientToken)
{
FurnaceResultCode resultCode = FurnaceResultCode.SUCCESS;
// 용광로 상태 처리
switch (State)
{
case SyncFurnaceStateCode.WAITING:
if (player.EquippedItem == PlayerEquippedItem.IRONSTONE)
{
player.EquippedItem = PlayerEquippedItem.NONE;
StartProgress(teamIndex, clientToken);
}
else
{
resultCode = FurnaceResultCode.INVALID_ITEM;
}
break;
case SyncFurnaceStateCode.PRODUCING:
case SyncFurnaceStateCode.OVERFLOW:
{
player.EquippedItem = State == SyncFurnaceStateCode.PRODUCING
? PlayerEquippedItem.IRON
: PlayerEquippedItem.GARBAGE;
ResetProgress();
StartProgress(teamIndex, clientToken);
}
break;
case SyncFurnaceStateCode.RUNNING:
{
resultCode = FurnaceResultCode.RUNNING_STATE;
}
break;
default:
resultCode = FurnaceResultCode.FAIL;
break;
}
return resultCode;
}
public void StartProgress(int teamIndex, string clientToken)
{
StopProgress();
progressToken= new CancellationTokenSource();
IntervalManager.Instance.AddTask(
$"Furnace_{GetHashCode()}",
async token =>
{
Progress += 10;
UpdateStateBasedOnProgress();
// 상태 업데이트 콜백 실행
OnFurnaceStateUpdate(State, teamIndex, Progress, clientToken);
await UniTask.CompletedTask;
},
1.0f);
}
private async void OnFurnaceStateUpdate(SyncFurnaceStateCode state, int teamIndex, float progress, string token)
{
if (state == SyncFurnaceStateCode.WAITING)
{
return; // WAITING 상태는 동기화 하지 않음
}
var syncPacket = new Packet
{
PacketType = PacketType.SYNC_FURNACE_STATE_NOTIFICATION,
Token = token,
Payload = new S2CSyncFurnaceStateNoti
{
TeamIndex = (uint)teamIndex,
FurnaceStateCode = (uint)state,
Progress = progress
}
};
await EventDispatcher<GameManager.GameManagerEvent, Packet>
.Instance.NotifyAsync(GameManager.GameManagerEvent.SendPacketForTeam, syncPacket);
}
public void StopProgress()
{
if (progressToken != null)
{
IntervalManager.Instance.CancelTask(progressToken.Token);
progressToken.Dispose();
progressToken = null;
}
}
public void ResetProgress()
{
StopProgress();
Progress = 0;
State = SyncFurnaceStateCode.WAITING;
}
public void Reset()
{
StopProgress();
Progress = 0;
State = SyncFurnaceStateCode.WAITING;
}
private void UpdateStateBasedOnProgress()
{
if (Progress <= 0.001f)
State = SyncFurnaceStateCode.WAITING;
else if (Progress < 100.0f)
State = SyncFurnaceStateCode.RUNNING;
else if (Progress < 150.0f)
State = SyncFurnaceStateCode.PRODUCING;
else
State = SyncFurnaceStateCode.OVERFLOW;
}
}
}
'TIL' 카테고리의 다른 글
Node.js 서버 user, dungeon 관리 작성 - 최종프로젝트 2 (0) | 2024.12.17 |
---|---|
Node.js 서버 GetSkill.Handler 작성 - 최종프로젝트 1 (0) | 2024.12.17 |
자원 전쟁 (서버) 프로젝트 - 용광로 만들기 1탄 (0) | 2024.12.05 |
C# .csv파일 변환 방법과 예시 (0) | 2024.11.26 |
Class와 Structure의 차이점 및 사용 목적 (0) | 2024.11.26 |