티스토리 뷰
0. 코드 전문
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class Command { public float delay = 0; public Action command; public float interval = 0; public Command(float delay, Action action, float interval) { this.delay = Mathf.Max(0, delay); // 음수 방지 command = action; this.interval = Mathf.Max(0, interval); } } public class Commander : MonoBehaviour { private List<Command> commands = new List<Command>(); private Coroutine coroutine = null; private bool autoClear = true; private bool autoKill = true; private int loopCount = 1; #region Add //액션 추가 public Commander Add(Action action) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(0, action, 0)); return this; } /// <summary> /// 대기 시간 추가 /// </summary> public Commander Add(float interval) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(0, null, interval)); return this; } /// <summary> /// 실행 지연 시간, 액션 추가 /// </summary> public Commander Add(float delay, Action action) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(delay, action, 0)); return this; } /// <summary> /// 실행 지연 시간, 액션, 다음 액션 호출 지연 /// </summary> public Commander Add(float delay, Action action, float interval) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(delay, action, interval)); return this; } /// <summary> /// 액션, 다음 액션 호출 지연 /// </summary> public Commander Add(Action action, float interval) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(0, action, interval)); return this; } #endregion /// <summary> /// 0 보다 작은 수를 넣으면 무한히 반복합니다. /// </summary> public Commander SetLoop(int loopCount = -1) { if (loopCount == 0) loopCount = 1; this.loopCount = loopCount; return this; } public Commander AutoClear(bool autoClear) { this.autoClear = autoClear; return this; } public Commander AutoKill(bool autoKill) { this.autoKill = autoKill; return this; } public Commander SetClear() { commands.Clear(); return this; } public bool IsPlaying => coroutine != null; public int GetCount => commands.Count; /// <summary> /// 저장한 커맨드를 모두 실행합니다. /// </summary> public void Play(bool autoKill = true) { if (commands.Count == 0) { Debug.Log("저장된 커맨드가 없습니다."); return; } if (IsPlaying) { Debug.Log("커맨더가 실행 중입니다."); return; } this.autoKill = autoKill; coroutine = StartCoroutine(Playing_co()); } /// <summary> /// 실행을 중단 합니다. /// </summary> public void Stop() { if (IsPlaying) { StopCoroutine(coroutine); coroutine = null; } } private IEnumerator Playing_co() { int l = loopCount; while (l != 0) { for (int i = 0; i < commands.Count; i++) { yield return new WaitForSeconds(commands[i].delay); commands[i].command?.Invoke(); OnComplete?.Invoke(); yield return new WaitForSeconds(commands[i].interval); } OnCompleteAll?.Invoke(); l--; } if (autoKill) { Destroy(this); } else { coroutine = null; if (autoClear) commands.Clear(); } } public event Action OnComplete; public event Action OnCompleteAll; } | cs |
1. 매개변수 Action
Action을 매개 변수로 사용할 수 있다는 것을 알게 되었을 때 떠올린 것은
"DOTween의 OnComplete() 함수에 사용할 익명메서드를 Action으로 전달해볼까?" 였다.
public void In(float duration = 1, Action onFadeIn = null)
{
StartFade(Color.black, 0, duration, onFadeIn);
}
StartFade는 내부적으로 이미지의 색을 검은색에서 투명한 색으로 바꿔주는 Tween이 구현되어있다.
그래서 선택적으로 마지막 인자값에 Action을 추가할 수 있게 만들었는데,
UIMaster.Fade.In(1.5f, () =>
{
UIMaster.LineMessage.PopUp("게임 시작", 3f, () =>
{
OnGameStartEvent?.Invoke();
});
});
이것을 실제로 사용해보니 가독성이 좋지 않았다.
어떻게하면 가독성을 높일 수 있을지 고민해보니
DOTween처럼 메서드 체인을 사용하면 좋을 것 같았다.
2. 메서드 체인
public class Deck
{
public Deck Shuffle()
{
//셔플 알고리즘을 실행
return this;
}
}
메서드 체인이란, 메서드를 void 형식으로 선언하는게 아니라,
객체 자신을 반환하는 함수로 만들어서
객체의 함수를 계속 사용할 수 있게 만드는 것이다.
함수 내부에서 실행하고 싶은 메서드를 실행한 후
결과적으로 다시 객체를 반환하므로
다시 객체 내부의 메서드를 불러올 수 있게 되는 것이다.
DOTween이 그런 형식인 것이다. (ex. SetEase, SetDelay...)
3. List<Action>
이제 이것들을 바탕으로 Action을 List로 관리하고
for문을 사용하여 리스트의 Action을 순차적으로 실행하는 class를 만들기로 하였다.
제작 의도는, Action을 순서대로 실행하되,
지연시킬 수도 있어야 했다.
n초 후에 Action1을 실행하거나
Action1을 실행하고 m초 후에 Action2를 실행할 수 있도록 말이다.
이를 위해 실행시킬 Action과 실행을 지연시키고 싶은 만큼의 시간을 저장하고 있는 객체를 만들어야 했다.
public class Command
{
public float delay = 0;
public Action command;
public float interval = 0;
public Command(float delay, Action action, float interval)
{
this.delay = Mathf.Max(0, delay); // 음수 방지
command = action;
this.interval = Mathf.Max(0, interval);
}
}
new 생성자로 command 객체의 지연시간과 실행할 Action을 지정하도록 하였다.
public class Commander : MonoBehaviour
{
private List<Command> commands = new List<Command>();
}
이 Command를 리스트로 관리하는 Commander class를 만들어 필요한 메서드를 구현하였다.
private IEnumerator Playing_co()
{
for (int i = 0; i < commands.Count; i++)
{
yield return new WaitForSeconds(commands[i].delay);
commands[i].command?.Invoke();
yield return new WaitForSeconds(commands[i].interval);
}
Destroy(this);
}
이것을 for문을 사용하여 코루틴으로 실행하면 Action들을 원하는 간격으로 순서대로 실행할 수 있다.
4. Add
리스트에 Command를 추가하는 기능이 필요하다.
public Commander Add(float delay, Action action, float interval)
{
commands.Add(new Command(delay, action, interval));
return this;
}
Command를 추가하는데에 필요한 것은 실행 지연 시간, 실행할 Action, 다음 Action 실행 대기 시간 세 가지다.
private Coroutine coroutine = null;
public void Play(bool autoKill = true)
{
if (commands.Count == 0)
{
Debug.Log("저장된 커맨드가 없습니다.");
return;
}
if (IsPlaying)
{
Debug.Log("커맨더가 실행 중입니다.");
return;
}
this.autoKill = autoKill;
coroutine = StartCoroutine(Playing_co());
}
public bool IsPlaying
{
get
{
return coroutine != null;
}
}
public Commander Add(float delay, Action action, float interval)
{
if (IsPlaying)
{
Debug.Log("커맨드 실행 중");
return this;
}
commands.Add(new Command(delay, action, interval));
return this;
}
다만 코루틴을 실행하는 중에 갑자기 리스트가 늘어나면 예상치 못한 문제가 생길 수 있다.
때문에 코루틴이 실행하는 중간엔 Command를 추가할 수 없게 예외처리를 해야한다.
이를 위해 실행 중인 코루틴을 캐싱할 Coroutine 변수를 만들고
코루틴이 실행 중이 아닐 때는 coroutine을 null로 관리하는 것이다.
coroutine이 null이라면 커맨더는 실행 중이 아닌 것이고,
coroutine이 null이 아니라면 커맨더는 실행 중인 것이다.
5. 결과물
사용 방법...
커맨더 자체는 new 생성이 아니라 AddComponent로 추가해야한다.
위가 Commander를 사용한 것이고
아래가 매개변수로 Action을 사용할 뿐인 DOTween 실행 메서드이다.
가독성이 많이 좋아졌다!
추가적으로 Action 리스트를 재사용 할 수 있게
실행을 마치면 Commander를 Destroy 하지 않도록 하는 bool 값을 만들었고,
캐싱을 유지하기 위해 List<Command>만 Clear() 할 수 있는 bool 값 또한 만들었다.
각각의 Command가 실행되고 난 후의 콜백 액션과
모든 Command가 실행되고 난 후의 콜백 액션도 준비하였다.
private IEnumerator Playing_co()
{
int l = loopCount;
while (l != 0)
{
for (int i = 0; i < commands.Count; i++)
{
yield return new WaitForSeconds(commands[i].delay);
commands[i].command?.Invoke();
OnComplete?.Invoke();
yield return new WaitForSeconds(commands[i].interval);
}
OnCompleteAll?.Invoke();
l--;
}
if (autoKill)
{
Destroy(this);
}
else
{
coroutine = null;
if (autoClear)
commands.Clear();
}
}
커맨더의 핵심인 코루틴 전문이다.
코드를 완성하고 나서보니 이것이 커맨드 패턴과 유사한 형태라는 것을 알게 되었다.
(커맨드 패턴 자체도 무엇인지 이번에 알게 되었다...)
특별히 대단한 아니고, 명령을 캡슐화하여 관리한다는 정도?
UniTask를 사용하여 개선한 버전을 추가합니다.
커맨더는 Action과 대기시간을 캡슐화한 커맨드를 List로 저장하여
입력한 순서대로 간격을 두고 실행합니다.
Action을 실행하고 설정한 시간만큼 다음 작업을 대기하거나
특정 조건이 될 때까지(Until) 대기할 수도 있고
특정 조건 동안(While) 대기할 수 있습니다.
커맨더는 작업을 시작하면 자체적으로 Update()를 가집니다.
커맨더는 특정한 조건일 때 하던 작업을 중단할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | using System; using System.Threading; using Cysharp.Threading.Tasks; using System.Collections.Generic; using UnityEngine; public class Command { public Action command { get; private set; } //실행 후... public float interval { get; private set; } //~초 동안 대기 public Func<bool> until { get; private set; } //~까지 실행 대기 public Func<bool> @while { get; private set; } //~동안 실행 대기 public Command(Action command) { interval = 0; until = null; @while = null; this.command = command; } public Command(Func<bool> until) { interval = 0; @while = null; this.until = until; command = null; } public Command(Func<bool> @while, int interval = 0) { this.interval = interval; this.@while = @while; until = null; command = null; } public Command(float interval) { this.interval = interval; command = null; @while = null; until = null; } public Command(Action command, Func<bool> until) { interval = 0; this.until = until; @while = null; this.command = command; } public Command(Action command, Func<bool> @while, int interval = 0) { until = null; this.interval = interval; this.@while = @while; this.command = command; } public Command(Action command, float interval) { this.interval = interval; this.command = null; @while = null; until = null; } } /// <summary> /// Action과 익명 메서드를 리스트에 저장합니다. /// <para>Add()를 통해 명령과 명령 실행 후 대기할 시간을 설정할 수 있습니다.</para> /// <para>Play()하여 실행할 수 있습니다.</para> /// </summary> public class Commander { private List<Command> commands = new List<Command>(); private int loopCount = 1; bool autoClear = true; public bool IsPlaying => cancel != null && !cancel.IsCancellationRequested; public Commander() { loopCount = 1; autoClear = true; } public Commander(bool autoClear) { this.autoClear = autoClear; } public Commander(int loopCount) { SetLoop(loopCount); } public Commander(bool autoClear, int loopCount) { this.autoClear = autoClear; SetLoop(loopCount); } #region Add public Commander Add(Action action) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(action)); return this; } public Commander WaitSeconds(float interval) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(interval)); return this; } public Commander WaitUntil(Func<bool> until) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(until)); return this; } public Commander WaitWhile(Func<bool> @while) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(@while, 0)); return this; } public Commander Add(Action action, float interval) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(action, interval)); return this; } public Commander Add_Until(Action action, Func<bool> until) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(action, until)); return this; } public Commander Add_While(Action action, Func<bool> @while) { if (IsPlaying) { Debug.Log("커맨드 실행 중"); return this; } commands.Add(new Command(action, @while, 0)); return this; } #endregion /// <summary> /// 0 보다 작은 수를 넣으면 무한히 반복합니다. /// </summary> public Commander SetLoop(int loopCount = -1) { if (loopCount == 0) loopCount = 1; this.loopCount = loopCount; return this; } /// <summary> /// 작업을 마치면 입력받은 커맨드를 전부 삭제합니다. /// </summary> public Commander SetAutoClear(bool autoClear = true) { this.autoClear = autoClear; return this; } public void Clear() { commands.Clear(); } public int CommandCount => commands.Count; int taskCount = 0; public float TaskProgress { get { if (CommandCount == 0) { return 0; } else { return Mathf.Clamp((float)taskCount / CommandCount, 0, 1); } } } /// <summary> /// 저장한 커맨드를 모두 실행합니다. /// </summary> public void Play(bool autoClear = true) { this.autoClear = autoClear; if (commands.Count == 0) { Debug.Log("저장된 커맨드가 없습니다."); return; } if (IsPlaying) { Debug.Log("커맨더가 실행 중입니다."); return; } cancel?.Cancel(); cancel = new CancellationTokenSource(); Task(cancel.Token).Forget(); Update(cancel.Token).Forget(); } /// <summary> /// 실행을 중단 합니다. /// </summary> public void Cancel() { if (IsPlaying && cancel != null) { cancel.Cancel(); // 작업 취소 } } private async UniTask Task(CancellationToken token) { int l = loopCount; taskCount = 0; while (l != 0 && !token.IsCancellationRequested) { foreach (Command cmd in commands) { cmd.command?.Invoke(); //~초 동안 대기 if (cmd.interval > 0) await UniTask.Delay(TimeSpan.FromSeconds(cmd.interval), cancellationToken: token); //~까지 대기 if (cmd.until != null) await UniTask.WaitUntil(cmd.until, cancellationToken: token); //~까지 대기 if (cmd.@while != null) await UniTask.WaitWhile(cmd.@while, cancellationToken: token); // 취소되었는지 확인 if (token.IsCancellationRequested) break; onComplete?.Invoke(); taskCount = Mathf.Clamp(taskCount + 1, 0, CommandCount); } onCompleteAll?.Invoke(); l--; } if (autoClear) { onComplete = null; onCompleteAll = null; onUpdate = null; ClearCancelTrigger(); } if (!token.IsCancellationRequested) { cancel.Cancel(); } } private CancellationTokenSource cancel; Action onComplete; public Commander OnComplete(Action onComplete) { this.onComplete = onComplete; return this; } Action onCompleteAll; public Commander OnCompleteAll(Action onCompleteAll) { this.onCompleteAll = onCompleteAll; return this; } Action onUpdate; public Commander OnUpdate(Action onUpdate) { this.onUpdate = onUpdate; return this; } async UniTask Update(CancellationToken token) { while (!token.IsCancellationRequested) { onUpdate?.Invoke(); if (cancelTrigger()) { cancel.Cancel(); } await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken: token); } } Func<bool> cancelTrigger = () => { return false; }; public Commander CancelTrigger(Func<bool> cancelTrigger) { this.cancelTrigger = cancelTrigger; return this; } public Commander ClearCancelTrigger() { cancelTrigger = () => { return false; }; return this; } } | cs |
'유니티 > C# Code' 카테고리의 다른 글
[DOTween] Sequence의 상태에 대하여 (0) | 2024.09.14 |
---|---|
[Object Pooling] Image Pop Up (0) | 2024.09.13 |
Mesh Bake를 배웠습니다. (0) | 2024.08.31 |
RaycastAll과 Array.Sort (0) | 2024.08.31 |
GPT >> Application의 path 종류에 대해 설명해줘 (0) | 2024.06.19 |