티스토리 뷰

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(0null, 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> @whileint 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> @whileint 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(@while0));
        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, @while0));
        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, 01);
            }
        }
    }
 
 
    /// <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 + 10, 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
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함