유니티 버전 - 2022.3.59f1
목차
- 기존 코드의 문제점
- 전체적인 구조
- IMethodCommand와 Command
- CommandPriorityComparer
- CommandQueue
- CommandScheduler
- 메서드 등록
- 실행 결과와 주의점
기존 코드의 문제점
기존 코드입니다, 게임에서 하루가 지나고나서 실행되어야할 메서드들이 MorningRoutine이라는 메서드 아래 묶여있습니다, 코드간 결합도가 높고 확장성이 부족할뿐만 아니라 테스트, 가독성등 여러가지 측면에서 문제가 있습니다, MorningRoutine에서 해주어야하는 동작들이기는하지만, 좋지 못한 구조라고 볼 수 있겠습니다
CommandSceheduler의 전체적인 구조
1. CommandSceheduler 에서는 개별 Queue에 접근하고, 명령을 추가하고, 실행할 수 있는 기능을 제공합니다
2. CommandQueue에서는 Interface를 이용하여, 명령들이 모여있는 List에 명령을 추가/실행/정렬하는 기능을 제공합니다
3. Command는 IMethodCommand 를 상속받고, 실제로 실행될 명령(메서드)과 실행 우선순위를 가지고있습니다
4. IMethodCommand는 개별 Command를 캡슐화하여 제어할 수 있도록 합니다
IMethodCommand와 Command
명령(작업)을 캡슐화하는 역할을 담당합니다, 모든 Command를 공통된 구조와 동작 방식을 가지게 하여, 하나의 통일된 방식으로 제어하고 실행할 수 있게 해줍니다
public interface IMethodCommand
{
int Priority { get; }
public void Execute();
}
Command입니다, 실행하고자하는 메서드와 우선순위를 지정해줍니다, <T>을 이용하여 매개변수 또한 받을 수 있도록 해주었습니다, 실제 실행할 작업을 캡슐화하고 스케줄러에 등록하여 실행될 수 있도록합니다
using System;
public class Command : IMethodCommand
{
private readonly Action _action;
public int Priority { get; private set; }
public Command(Action action, int priority)
{
_action = action;
Priority = priority;
}
public void Execute()
{
_action?.Invoke();
}
}
public class Command<T> : IMethodCommand
{
private readonly Action<T> _action;
private readonly T _arg;
public int Priority { get; private set; }
public Command(Action<T> action, T arg, int priority)
{
_action = action;
_arg = arg;
Priority = priority;
}
public void Execute()
{
_action?.Invoke(_arg);
}
}
public class Command<T1, T2> : IMethodCommand
{
private readonly Action<T1, T2> _action;
private readonly T1 _arg1;
private readonly T2 _arg2;
public int Priority { get; private set; }
public Command(Action<T1, T2> action, T1 arg1, T2 arg2, int priority)
{
_action = action;
_arg1 = arg1;
_arg2 = arg2;
Priority = priority;
}
public void Execute()
{
_action(_arg1, _arg2);
}
}
CommandPriorityComparer
IMethodCommand의 Priority를 비교하여 명령 실행 순서를 결정하는데 사용합니다, x의 Priority가 y의 Priority보다 작으면 음수, 같으면 0, 크면 양수를 반환합니다.
using System.Collections.Generic;
public class CommandPriorityComparer : IComparer<IMethodCommand>
{
public int Compare(IMethodCommand x, IMethodCommand y)
{
return x.Priority.CompareTo(y.Priority);
}
}
CommandQueue
특정 동작에대한 명령들을 저장하는 리스트에 명령을 추가하며 Priority에 의한 정렬을 수행합니다, ExecuteAll을 이용하여 등록된 명령들을 수행합니다
using System;
using System.Collections.Generic;
public class CommandQueue
{
private readonly List<IMethodCommand> _commands = new List<IMethodCommand>();
public void AddCommand(Action action, int priority)
{
Enqueue(new Command(action, priority));
}
public void AddCommandWithArg<T>(Action<T> action, T arg, int priority)
{
Enqueue(new Command<T>(action, arg, priority));
}
public void AddCommandWithArgs<T1, T2>(Action<T1, T2> action, T1 arg1, T2 arg2, int priority)
{
Enqueue(new Command<T1, T2>(action, arg1, arg2, priority));
}
private void Enqueue(IMethodCommand command)
{
int index = _commands.BinarySearch(command, new CommandPriorityComparer());
if (index < 0)
{
index = ~index;
}
_commands.Insert(index, command);
}
public void ExecuteAll()
{
foreach (IMethodCommand command in _commands)
{
command.Execute();
}
}
}
CommandScheduler
ScheduledCommandType(Enum)을 이용하여 딕셔너리에서 여러종류의 CommandQueue를 만들고 관리하고 실행합니다
using System.Collections.Generic;
using System;
public enum ScheduledCommandType
{
Sleep,
AfterSleep
}
public static class CommandScheduler
{
private static readonly Dictionary<ScheduledCommandType, CommandQueue> _commandQueues = new Dictionary<ScheduledCommandType, CommandQueue>();
public static void Enqueue(ScheduledCommandType queueName, Action action, int priority)
{
GetQueue(queueName).AddCommand(action, priority);
}
public static void EnqueueWithArg<T>(ScheduledCommandType queueName, Action<T> action, T arg, int priority)
{
GetQueue(queueName).AddCommandWithArg(action, arg, priority);
}
public static void EnqueueWithArgs<T1, T2>(ScheduledCommandType queueName, Action<T1, T2> action, T1 arg1, T2 arg2, int priority)
{
GetQueue(queueName).AddCommandWithArgs(action, arg1, arg2, priority);
}
private static CommandQueue GetQueue(ScheduledCommandType queueName)
{
if (!_commandQueues.ContainsKey(queueName))
{
_commandQueues[queueName] = new CommandQueue();
}
return _commandQueues[queueName];
}
public static void ExecuteAll(ScheduledCommandType queueName)
{
if (_commandQueues.TryGetValue(queueName, out CommandQueue queue))
{
queue.ExecuteAll();
}
}
}
메서드 등록
개별 스크립트에서 실행되어야 하는 명령들을 Scheduler에 등록해줍니다
여기서 UpdateGrowth는 Void 형식이기에 메서드 그룹 방식으로 사용할 수 있습니다
하지만 SetPlayerPosition은 비동기 메서드로 UniTask를 반환합니다, Action 델리게이트는 반환 값이 없는 메서드를 기대하기 때문에, 람다식으로 감싸 실행하고, Forget();을 이용하여 반환 값을 무시하도록 하였습니다
CommandScheduler.Enqueue(ScheduledCommandType.Sleep, UpdateGrowth, 3);
CommandScheduler.Enqueue(ScheduledCommandType.Sleep, () => SetPlayerPosition().Forget(), 1);
// 나머지 메서드 또한 모두 등록
실행 결과와 주의점
게임이 시작되면 메서드들이 등록됩니다
디버그를 이용해 등록 순서와 실행 순서를 비교해보니 의도대로 동작하였습니다
주의할점으로는 Priority를 위와같이 숫자를 이용하여 관리할경우 메서드의 실행 순서가 바뀌어야할 경우가 생겼을때 관리가 어렵고, 아래와같이 숫자가 겹쳤을때 같은 숫자를 사용하는 메서드간 실행순서가 의도와 다르게 나타날 수도 있기에 적절한 관리 방법을 이용하는것이 좋겠습니다
Enum을 이용한 관리법, 순서가 매우 정확해야하지않는다면 중요도를 기준으로 상중하로 나누어 관리하는것이 일반적으로 좋을것 같습니다
public enum CommandPriority
{
High = 1,
Medium = 2,
Low = 3
}
public enum SleepCommand
{
MethodName1 = 1,
MethodName2 = 2,
MethodName3 = 3,
...
}
'코드 및 공부 > 최적화' 카테고리의 다른 글
딕셔너리의 GetValueOrDefault() (0) | 2025.03.27 |
---|---|
ReferenceEquals()를 이용한 null 비교 수행 (0) | 2025.03.25 |
그래픽 옵션(Quality) 변경하기 (0) | 2025.02.04 |
Profiler와 Stopwatch를 이용한 코드 비교 및 최적화 (1) | 2025.01.16 |
스프라이트 아틀라스(Sprite Atlas) (0) | 2024.12.23 |