TIL

자원 전쟁 (서버) 프로젝트 - 용광로 만들기 2탄

추운날_너를_기다리며 2024. 12. 5. 22:16

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;
        }
    }
}