DOTween 라이브러리의 효율적인 사용 방법
DOTween 라이브러리를 이용하여 다양한 애니메이션을 효율적으로 만드는 방법에 대해서 고민해봤습니다.
타워 오브 가디언즈를 개발하면서 겪었던 문제 상황과 그에 대한 해결 방법이다.
카드를 사용하는 장르의 게임에선 카드가 이리저리 이동하는 상황이 많다.
예를 들면, 덱을 드로우한다거나 버리는 경우다.
이런 장르에서 Unity에서 제공하는 애니메이션만으로 다양한 이동을 구현하기엔 비효율적이다.
그래서 DOTween 애니메이션 라이브러리를 채용했다.
사용 방법은 Cocos2d-x의 Action과 매우 유사해서 다루는 데 어려움은 크게 없었다.
문제는 특정 카드마다 다른 애니메이션 연출을 사용해야 한다는 점이었다.
주먹구구식 DOTween을 활용한 메서드 정의
DOTween을 채용했을 당시에 상황으로 카드 연출 애니메이션이 필요한 상황이 아니었다.
그래서 단순히 카드를 관리하는 UI에서 DOTween 라이브러리를 이용하여 카드 연출을 임시로 구현해뒀다.
나는 카드가 범용적으로 사용되길 원치 않았기 때문에 각 카드는 각자의 책임만 가져야 한다고 생각한다.
그래서 모든 카드를 사용처에 맞게 구분해뒀다.
- 손에 들고 있는 카드
- 필드에 낸 카드
- 교체 후보 카드
- 임시 카드(이 카드가 애니메이션이 적용되는 대상이다.)
즉, 각 카드를 관리하는 UI에서 모두 DOTween을 이용하여 카드 연출을 구현했다.
이땐 뭐 정확히 이렇다 저렇다 할 기획이 나온 상황이 아니었기 때문에 크게 문제성을 느끼지 못했다.
임시 카드 팩토리와 DOTween 애니메이터 및 컨트롤러
위의 방법은 카드 연출 애니메이션이 필요한 곳에서 다음과 같은 파이프라인을 거쳐야만 했다.
Object Pool에서 임시 카드를 빌려온다.- 임시 카드에 매번
DOTween애니메이션을 적용한다.
뭐 여러 문제가 있다. 몇 가지 문제는 문맥 상 뒤로 넘겨야만 한다.
MVP아키텍처로 구현한 내 UI 코드에서Singleton객체인Object Pool을 참조해야 한다.DOTween시퀀스 코드를 이용하여 연출 애니메이션을 매번 정의해줘야만 한다.
Object Pool 직접 참조 문제
우선 Presenter가 존재하는 목적은 Model과 View의 중재자 역할이다.
따라서 Presenter는 프레임워크로부터 최대한 약하게 결합되어야 하고 순수 로직으로 구성을 이루어야 한다.
Presenter가 Object Pool를 직접 참조하고 있기 때문에 Object Pool에 문제가 발생하면 연쇄적으로 Presenter에 문제가 발생한다.
그래서 위의 문제를 해결하기 위해 임시 카드 팩토리를 만들기로 결정했다.
임시 카드 팩토리가 Object Pool을 통해 임시 카드를 빌려와 반환하면 된다.
임시 카드의 레이어 정렬도 걱정할 필요가 없다.
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
using UnityEngine;
public class TemporaryCardFactory : MonoBehaviour
{
[Header("캔버스")]
[SerializeField] private Canvas m_canvas;
[Space(30f), Header("의존성 목록")]
[Header("임시 카드 프리펩")]
[SerializeField] private GameObject m_temp_card_prefab;
public GameObject InstantiateCard(BattleCardData card_data)
{
var m_temp_card_obj = ObjectPoolManager.Instance.Get(m_temp_card_prefab);
m_temp_card_obj.transform.SetParent(m_canvas.transform);
var temp_card_view = m_temp_card_obj.GetComponent<TemporaryCardView>();
_ = new TemporaryCardPresenter(temp_card_view, card_data);
return m_temp_card_obj;
}
public void ReturnCard(GameObject temp_card_obj)
=> ObjectPoolManager.Instance.Return(temp_card_obj);
}
매번 DOTween 시퀀스 코드를 작성해야 하는 문제
모든 UI에서 DOTween 애니메이션을 원하는 연출에 따라 시퀀스를 늘렸다 줄였다, 변경했다를 반복해야 한다.
기획자분 입맛이 어떨지 모르기 때문에 반복하면서 피드백을 받는다..
그래서 내가 이 문제를 해결하려고 생각했던 것은 다음과 같다.
일련의 애니메이션 코드를 만들어두고, 소소한 디테일 값 변경은 사용하는 곳에서 맡는다.
물론 위에서 만든 팩토리를 활용하는 것도 잊지 않았다.
다음과 같은 임시 카드 및 애니메이션 생성 구조를 생각했다.
1
2
3
4
5
6
7
8
Temporary Card Factory: 오브젝트 풀로부터 임시 카드를 빌려온다.
▼▼▼
Temporary Card Animator: 일련의 애니메이션을 미리 정의해둔다.
▼▼▼
Temporary Card Controller: 클라이언트에게 API를 노출하고, 전달받은 값을 통해 애니메이션을 호출한다.
▲
▲
client: 트랜스폼(위치, 회전, 크기) 지정, 애니메이션 시간 지정, 인터벌 지정 등등
TemporaryCardAnimator는 다음과 같이 일련의 애니메이션을 미리 정의해둔다.
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
using System;
using UnityEngine;
using DG.Tweening;
public class TemporaryCardAnimator : MonoBehaviour
{
[Header("임시 카드 팩토리")]
[SerializeField] private TemporaryCardFactory m_factory;
public event Action<BattleCardData> OnAnimationEnd;
public void Animate(BattleCardData card_data,
Vector3 start_position,
Vector3 end_position,
float scale,
float arc_power,
float duration)
{
// 팩토리를 이용하여 카드를 생성한다.
var temp_card = m_factory.InstantiateCard(card_data);
temp_card.transform.position = start_position;
// 애니메이션 시퀀스 코드를 생성한다.
var sequence_animator = DOTween.Sequence();
sequence_animator.Append(temp_card.transform.DOScale(scale, duration).SetEase(Ease.OutBack));
sequence_animator.Join(temp_card.transform.DOJump(end_position, arc_power, 1, duration).SetEase(Ease.InQuad));
sequence_animator.OnComplete(() => { OnAnimationEnd?.Invoke(card_data);
ReturnCard(temp_card); });
}
private void ReturnCard(GameObject temp_card)
{
ObjectPoolManager.Instance.Return(temp_card);
}
}
그리고 TemporaryCardController는 TemporaryCardAnimator를 이용하여 여러 API를 지원한다.
너무 길어서 몇 가지만 압축하도록 하겠다.
using System;
using System.Collections;
using UnityEngine;
public class TemporaryCardController : MonoBehaviour
{
[Header("임시 카드 애니메이터")]
[SerializeField] private TemporaryCardAnimator m_animator;
public event Action<BattleCardData> OnAnimationEnd;
private void Awake()
=> m_animator.OnAnimationEnd += AnimationEndHandler;
// 여러 카드에 애니메이션을 적용시키는 경우
public void PlayAnime(BattleCardData[] card_datas,
Vector3 start_position,
Vector3 end_position,
float scale,
float arc_power,
float duration,
float interval)
{
StartCoroutine(Co_Anime(card_datas,
start_position,
end_position,
scale,
arc_power,
duration,
interval));
}
// 한 카드에 애니메이션을 적용시키는 경우
public void PlayAnime(BattleCardData card_data,
Vector3 start_position,
Vector3 end_position,
float scale,
float arc_power,
float duration)
{
m_animator.Animate(card_data,
start_position,
end_position,
scale,
arc_power,
duration);
}
// 인터벌을 두어 각각의 카드에 대해 애니메이터를 호출한다.
private IEnumerator Co_Anime(BattleCardData[] card_datas,
Vector3 start_position,
Vector3 end_position,
float scale,
float arc_power,
float duration,
float interval)
{
for(int i = 0; i < card_datas.Length; i++)
{
yield return new WaitForSeconds(interval);
m_animator.Animate(card_datas[i],
start_position,
end_position,
scale,
arc_power,
duration);
}
}
private void AnimationEndHandler(BattleCardData card_data)
{
OnAnimationEnd?.Invoke(card_data);
}
}
그러면 클라이언트는 단순히 TemporaryCardController를 이용하여 속성만 채워주면 된다.
아래는 한 클라이언트에서 TemporaryCardController를 사용하는 예시다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Collections.Generic;
using UnityEngine;
public class ThrowAnimeController : MonoBehaviour
{
...
public void PlayRemove(IThrowCardView card_view, BattleCardData card_data)
{
var target_card = card_view as ThrowCardView;
// 단순히 각 속성에 해당하는 값만 넣어주면 된다.
m_temp_card_controller.PlayAnime(card_data,
target_card.transform.position,
m_hand_transform.position,
1f,
0f,
0f);
ObjectPoolManager.Instance.Return(target_card.gameObject);
}
}
결과적으로 생각해보면 PlayRemove()에서는 다음과 같은 작업이 압축된 것과 다름이 없다.
1
2
3
<전> <후>
1. 오브젝트 풀로부터 임시 카드 빌려오기 ===> 1. 애니메이션 속성 넣기
2. 애니메이션 시퀀스 작성하기
여전히 많은 문제들은 남아있다. 매개변수가 너무 많아서 코드 정리하기가 어렵다.
또한 의미 없는 매직 넘버들의 나열이 너무 복잡하고 어느 값이 어느 속성을 나타내는지 알기 어렵다.
그리고 가장 큰 문제라고도 할 수 있다. 모든 애니메이션은 저렇게 단순하고 규칙적이지 않다.
다른 애니메이션이 미리 정의해둔 메서드에 어울리지 않으면 매번 새로운 메서드가 늘어나야 한다.
뭐든 됐으니 일단 이쯤에서 임시 카드 애니메이션에 대해서는 끝냈다.
이 부분까지 왔음에도 아직 카드 애니메이션에 대한 기획이 나오지 않았기 때문이다.
다양한 애니메이션에 대응할 수 있는 방법
드디어 기획이 나왔다. 역시나 내가 생각한대로 단순하고 규칙적이진 않았다.
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
1. 카드를 드로우할 때:
- 드로우 덱에서 한 장씩 손까지 이동시킨다.
- 이동 중인 카드는 점점 커진다.
- 카드의 이동 경로는 선형이다.
2. 손에 있는 카드를 교체할 때:
- 손에 있는 카드를 한 장씩 교체 덱으로 이동시킨다.
- 카드의 이동 경로는 포물선이다.
- 이동 중인 카드는 현재 손에 있는 카드를 가려서는 안된다.
- 이동 중인 카드는 시계 방향으로 180도 회전하면서 이동한다.
- 이동 중인 카드는 점점 작아지면서 빨려들어가는 느낌이 있어야 한다.
- 이동 중인 카드는 점점 투명해진다.
3. 필드에 있는 카드를 교체할 때:
- 손에 있는 카드를 교체할 때와 동일하다.
- 공격 필드 그리고 방어 필드 순서로 교체 덱으로 이동시킨다.
4. 교체 카드가 손으로 들어올 때:
- 교체 필드의 카드를 한 번에 손으로 이동시킨다.
- 이동 중인 카드의 크기는 변함 없다.
- 카드의 이동 경로는 선형이다.
5. 교체 카드를 확정할 때:
- 교체 필드의 카드를 한 장씩 교체 덱으로 이동시킨다.
- 나머지는 손에 있는 카드를 교체할 때와 동일하다.
앞서 만든 TemporaryCardController는 2번 경우만 생각해도 메서드가 늘어난다.
그리고 지원하지 않는 회전이라던가 투명도 기능이 존재한다.
실제로 이를 대응하기 위해 메서드를 늘려봤지만 너무 비슷한 코드들의 중복이 증가헸다.
메서드 이름을 짓기에도 너무나 불편함이 많았다.
또, 카드 애니메이션이 시작할 때의 이벤트, 끝나고서의 이벤트, 전체가 끝나고서의 이벤트도 필요했다.
현재로서는 지원하지 않는 기능이다.
여기에서 다시 코드를 리팩터링해야겠다는 생각이 들었다.
리팩터링의 목적은 “메서드 단 하나로 모든 애니메이션을 지원하겠다”라는 데 두었다.
다양한 속성에 대한 처리
속성에 따라서 애니메이션이 달라진다. 이걸 먼저 생각해보고 처리하는게 우선이었다.
그래서 속성을 기입하는 객체를 하나 만들고 매개변수로 이 객체를 전달하는 방법을 생각했다.
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
using UnityEngine;
using System;
using DG.Tweening;
[Serializable]
public class TemporaryCardSettings
{
// 애니메이션 실행 시간에 대한 속성
public float Duration = 0.5f;
// 이동 및 점프에 대한 속성
public bool UseJump = false;
public float JumpPower = 0f;
public Ease MoveEase = Ease.InQuad;
// 크기에 대한 속성
public bool UseScale = false;
public Vector3 Scale = Vector3.zero;
public Ease ScaleEase = Ease.OutBack;
// 회전에 대한 속성
public bool UseRotation = false;
public Vector3 TargetEuler;
public RotateMode RotateMode = RotateMode.FastBeyond360;
public Ease RotateEase = Ease.OutQuad;
// 투명도에 대한 속성
public bool UseOpacity = false;
public float Opacity = 1f;
public Ease OpacityEase = Ease.InQuad;
// 시작 크기 지정에 대한 속성
public bool ForceStartScale = false;
public Vector3 StartScale = Vector3.one;
// 시작 회전 지정에 대한 속성
public bool ForceStartRotation = false;
// 시작 투명도 지정에 대한 속성
public bool ForceStartOpacity = false;
public float StartOpacity;
}
그리고 한 카드에 날려보낼 데이터는 따로 정의한다.
여기서 TemporaryCardSettings을 멤버로 가진다.
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
using UnityEngine;
using System;
public class TemporaryCardAnimeRequest
{
// 카드의 렌더링 순서를 결정하기 위한 트랜스폼
public Transform TargetRoot;
// 한 장이던 여러 장이던 배열로 카드 데이터를 입력한다.
// 따라서 단일 카드용, 여러 카드용 메서드를 구분할 필요가 사라진다.
public BattleCardData[] CardDatas;
// 시점 및 종점에 대한 정보
// 시점은 각 카드일 수도 있고, 특정 한 위치일 수도 있다.
public Vector3[] StartPositions;
public Vector3 StartPosition;
public Vector3 EndPosition;
// 회전에 대한 정보
// 각 카드가 다른 회전을 가질 수도 있고 아닐 수도 있다.
public Vector3[] StartRotations;
public Vector3 StartRotation;
// 인터벌에 대한 정보
public float Interval = 0.05f;
// DOTween 속성에 대한 정보를 따로 전달하지 않으면 기본 값을 사용한다.
public TemporaryCardSettings Settings = new();
// 속성을 오버라이딩할 때 사용한다.
public Func<int, TemporaryCardSettings, TemporaryCardSettings> PerCardOverride;
public Vector3 GetStartPosition(int index)
=> (StartPositions != null && index < StartPositions.Length) ? StartPositions[index] : StartPosition;
public Vector3 GetStartRotation(int index)
=> (StartRotations != null && index < StartRotations.Length) ? StartRotations[index] : StartRotation;
public TemporaryCardSettings GetSettings(int index)
=> PerCardOverride != null ? PerCardOverride(index, Settings) : Settings;
}
임시 카드 팩토리의 개선
앞서 정의했던 TemporaryCardFactory는 단순히 캔버스 하위에 임시 카드를 생성했다.
이 기능만으로는 손에 있는 카드를 버릴 때 카드가 가려지는 것을 방지할 수 없다.
따라서 현재 기능을 두고 InstantiateCard()를 확장하여 매개변수로 Transform을 받기로 했다.
Transform이 null이라면 캔버스 하위에 생성하는 방법이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Unity.VisualScripting;
using UnityEngine;
public class TemporaryCardFactory : MonoBehaviour
{
[Header("캔버스")]
[SerializeField] private Canvas m_canvas;
[Space(30f), Header("의존성 목록")]
[Header("임시 카드 프리펩")]
[SerializeField] private GameObject m_temp_card_prefab;
public GameObject InstantiateCard(BattleCardData card_data, Transform transform)
{
var m_temp_card_obj = ObjectPoolManager.Instance.Get(m_temp_card_prefab);
m_temp_card_obj.transform.SetParent(transform == null ? m_canvas.transform : transform);
...
}
...
}
임시 카드 애니메이터의 개선
앞서 TemporaryCardAnimator는 내가 사전에 정의해둔 동작만을 토대로 속성 값에 따라 작동했다.
즉, 여기에 벗어나는 애니메이션이 있다면 새롭게 정의를 해야만 한다.
새로운 메서드가 늘어나면 TemporaryCardController도 연쇄적으로 메서드가 늘어나게 된다.
이는 OCP 원칙에 위배된다. 즉, 애초에 TemporaryCardAnimator에서 모든 경우를 처리하면 된다.
아래의 개선된 TemporaryCardAnimator은 기획자가 의도했던 모든 애니메이션 연출을 처리할 수 있다.
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
using System;
using UnityEngine;
using DG.Tweening;
public class TemporaryCardAnimator : MonoBehaviour
{
[Header("임시 카드 팩토리")]
[SerializeField] private TemporaryCardFactory m_factory;
public Tween AnimateOne(
Transform target_root,
BattleCardData data,
Vector3 start_position,
Vector3 end_position,
Vector3 start_rotation,
TemporaryCardSettings s,
Action<BattleCardData> on_start = null,
Action<BattleCardData> on_complete = null)
{
// 카드 애니메이션이 실행되는 시점에 등록된 이벤트가 있다면 발행한다.
on_start?.Invoke(data);
var card_object = m_factory.InstantiateCard(data, target_root);
var t = card_object.transform;
t.position = start_position;
// 애니메이션 시작 전에, 초기값 설정 여부에 따라 카드 트랜스폼을 조정한다.
if (s.ForceStartScale) t.localScale = s.StartScale;
if (s.ForceStartRotation) t.eulerAngles = start_rotation;
// 시퀀스는 분기문을 이용하여 모든 경우에 대응한다.
var seq = DOTween.Sequence();
var canvas_group = t.GetComponent<CanvasGroup>();
canvas_group.alpha = s.ForceStartOpacity ? s.StartOpacity : 1f;
if (s.UseOpacity)
seq.Join(canvas_group.DOFade(s.Opacity, s.Duration)).SetEase(s.OpacityEase);
if (s.UseJump)
seq.Join(t.DOJump(end_position, s.JumpPower, 1, s.Duration).SetEase(s.MoveEase));
else
seq.Join(t.DOMove(end_position, s.Duration).SetEase(s.MoveEase));
if (s.UseScale)
seq.Join(t.DOScale(s.Scale, s.Duration).SetEase(s.ScaleEase));
if (s.UseRotation)
seq.Join(t.DORotate(s.TargetEuler, s.Duration, s.RotateMode).SetEase(s.RotateEase));
seq.OnComplete(() =>
{
// 카드 애니메이션이 끝나는 시점에 등록된 이벤트가 있다면 발행한다.
on_complete?.Invoke(data);
m_factory.ReturnCard(card_object);
});
return seq;
}
}
임시 카드 컨트롤러의 개선
TemporaryCardController는 카드의 개수에 따라서 처리하는 API를 여러 개 지원했다.
이것도 TemporaryCardAnimator가 개선되면서 2개까지가 최대다.
단일 카드 전용, 다수 카드 전용으로 메서드를 2개 정의해도 된다고는 생각한다.
하지만 단일 카드도 다수 카드처럼 처리하면 하나로 줄일 수 있다. 그래서 그 방법을 택했다.
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
using System;
using System.Collections;
using UnityEngine;
public class TemporaryCardController : MonoBehaviour
{
[SerializeField] private TemporaryCardAnimator m_animator;
public event Action<BattleCardData> OnCardAnimationBegin;
public event Action<BattleCardData> OnCardAnimationEnd;
public event Action OnFinalAnimationEnd;
private Coroutine m_running;
// 이젠 단순히 Play만 호출하면 애니메이션을 실행한다.
// 복잡하던 매개변수들은 Request와 Setting으로 전부 다 빠져나갔다.
public void Play(TemporaryCardAnimeRequest req)
{
if (m_running != null)
StopCoroutine(m_running);
m_running = StartCoroutine(Co_Play(req));
}
// 단일 카드던 복수 카드던 모두 처리할 수 있는 코루틴이다.
private IEnumerator Co_Play(TemporaryCardAnimeRequest req)
{
for (int i = 0; i < req.CardDatas.Length; i++)
{
if (req.Interval > 0f)
yield return new WaitForSeconds(req.Interval);
var s = req.GetSettings(i);
m_animator.AnimateOne(
req.TargetRoot,
req.CardDatas[i],
req.GetStartPosition(i),
req.EndPosition,
req.GetStartRotation(i),
s,
d => OnCardAnimationBegin?.Invoke(d),
d => OnCardAnimationEnd?.Invoke(d)
);
}
OnFinalAnimationEnd?.Invoke();
m_running = null;
}
}
클라이언트의 개선
클라이언트 역시, 불필요한 코드 중복이 있다.
동일한 TemporaryCardController를 사용함에 있어 모든 이벤트에 대한 처리를 반복한다.
이를 해결하기 위해 추상 클래스로 CardEffector를 정의했다.
이 추상 클래스의 목적은 이벤트 핸들링 방법을 통일 및 강제하는 데 있다.
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
using UnityEngine;
[RequireComponent(typeof(TemporaryCardController))]
public abstract class CardEffector : MonoBehaviour
{
[Header("시작점")]
[SerializeField] protected Transform m_start_transform;
[Header("도착점")]
[SerializeField] protected Transform m_end_transform;
protected TemporaryCardController m_temp_card_controller;
protected TemporaryCardSettings m_temp_card_settings;
protected TemporaryCardAnimeRequest m_temp_card_anime_request;
private void Awake()
{
m_temp_card_controller = GetComponent<TemporaryCardController>();
m_temp_card_settings = new();
m_temp_card_anime_request = new();
m_temp_card_controller.OnCardAnimationBegin += OnTempCardAnimeStart;
m_temp_card_controller.OnCardAnimationEnd += OnTempCardAnimeEnd;
m_temp_card_controller.OnFinalAnimationEnd += OnFinalAnimeEnd;
}
private void OnDestroy()
{
if(m_temp_card_controller != null)
{
m_temp_card_controller.OnCardAnimationBegin -= OnTempCardAnimeStart;
m_temp_card_controller.OnCardAnimationEnd -= OnTempCardAnimeEnd;
m_temp_card_controller.OnFinalAnimationEnd -= OnFinalAnimeEnd;
}
}
public virtual void Execute()
{
m_temp_card_controller.Play(m_temp_card_anime_request);
}
protected virtual void OnTempCardAnimeStart(BattleCardData card_data) {}
protected virtual void OnTempCardAnimeEnd(BattleCardData card_data) {}
protected virtual void OnFinalAnimeEnd() {}
}
세부적인 이벤트 함수들과 Execute()만을 virtual로 선언한다.
이벤트 함수를 재정의할지 말지 선택할 권한을 주고자했다.
이를 구체화한 클래스의 예시를 보면 다음과 같다.
사전에 확정되어 모든 카드가 영향을 받는 속성과 옵션의 경우에는 의존성 주입 시점에 결정한다. 하지만 매번 다른 경우(카드 개수나 데이터)는 실행 시점에 결정한다.
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
using UnityEngine;
using DG.Tweening;
using System.Collections.Generic;
public class HandCardToThrowEffector : CardEffector
{
[Header("카드 부모 트랜스폼")]
[SerializeField] private Transform m_card_root;
private HandPresenter m_hand_presenter;
public void Inject(HandPresenter hand_presenter)
{
m_hand_presenter = hand_presenter;
m_temp_card_settings = new()
{
Duration = 0.5f,
UseJump = true,
JumpPower = 50f,
MoveEase = Ease.Unset,
UseScale = true,
Scale = Vector3.one * 0.11f,
ScaleEase = Ease.InQuad,
UseRotation = true,
TargetEuler = new Vector3(0f, 0f, -180f),
RotateMode = RotateMode.LocalAxisAdd,
RotateEase = Ease.Unset,
UseOpacity = true,
Opacity = 0.5f,
OpacityEase = Ease.Unset,
ForceStartScale = true,
StartScale = Vector3.one * 0.66f,
ForceStartRotation = true,
ForceStartOpacity = false,
};
m_temp_card_anime_request = new()
{
TargetRoot = m_card_root,
EndPosition = m_end_transform == null ? Vector3.zero : m_end_transform.position,
Interval = 0.1f,
Settings = m_temp_card_settings,
};
}
public override void Execute()
{
m_temp_card_anime_request.CardDatas = m_hand_presenter.GetCardDatas();
List<Vector3> hand_card_positions = new();
foreach(IHandCardView card_view in m_hand_presenter.GetCardViews())
hand_card_positions.Add((card_view as HandCardView).transform.position);
List<Vector3> hand_card_rotations = new();
foreach(IHandCardView card_view in m_hand_presenter.GetCardViews())
hand_card_rotations.Add((card_view as HandCardView).transform.eulerAngles);
m_temp_card_anime_request.StartPositions = hand_card_positions.ToArray();
m_temp_card_anime_request.StartRotations = hand_card_rotations.ToArray();
base.Execute();
}
protected override void OnTempCardAnimeStart(BattleCardData card_data)
=> m_hand_presenter.RemoveCard(m_hand_presenter.GetCardView(card_data), false);
protected override void OnTempCardAnimeEnd(BattleCardData card_data)
{
GameData.Instance.handDeck.Remove(card_data.data.id);
GameData.Instance.UseCard(card_data.data.id);
GameData.Instance.InvokeDeckCountChange(DeckType.Throw);
}
}
