SaveData
AdventureTutorial은 씬마다 SaveData가 존재한다.
씬 전환 시 해당 씬의 변경사항을 잃지 않고 저장해보자.
(PlayerSaveData는 영속적인 씬에서 계속 존재한다. 씬 로드 시 플레이어 위치를 초기화하기 위해 값이 저장된다. 값은 Reaction 중 SaveScene이 가지고 있으며 Scene전환 시 저장한다.)
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
|
using System;
using UnityEngine;
using System.Collections.Generic;
[CreateAssetMenu]
public class SaveData : ResettableScriptableObject
{
// 리스트 2개로 딕셔너리를 구현했다.
// 딕셔너리는 Serializable을 지원하지 않기 때문이다.
// Inspector뷰에 노출시키기 위해 리스트로 대신함.
[Serializable]
public class KeyValuePairLists<T>
{
public List<string> keys = new List<string>();
public List<T> values = new List<T>();
public void Clear ()
{
keys.Clear ();
values.Clear ();
}
public void TrySetValue (string key, T value)
{
int index = keys.FindIndex(x => x == key);
if (index > -1)
{
values[index] = value;
}
else
{
keys.Add (key);
values.Add (value);
}
}
public bool TryGetValue (string key, ref T value)
{
int index = keys.FindIndex(x => x == key);
if (index > -1)
{
value = values[index];
return true;
}
return false;
}
}
public KeyValuePairLists<bool> boolKeyValuePairLists = new KeyValuePairLists<bool> ();
public KeyValuePairLists<int> intKeyValuePairLists = new KeyValuePairLists<int>();
public KeyValuePairLists<string> stringKeyValuePairLists = new KeyValuePairLists<string>();
public KeyValuePairLists<Vector3> vector3KeyValuePairLists = new KeyValuePairLists<Vector3>();
public KeyValuePairLists<Quaternion> quaternionKeyValuePairLists = new KeyValuePairLists<Quaternion>();
public override void Reset()
{
// Reset이 필요한 이유는
// Editor상에서 플레이 시 .asset파일에 저장되기 때문이다.
// 빌드해서 배포하면 로드한 .asset파일 내용이 저장되지 않기 때문에 reset이 필요없다.
boolKeyValuePairLists.Clear();
intKeyValuePairLists.Clear();
stringKeyValuePairLists.Clear();
vector3KeyValuePairLists.Clear();
quaternionKeyValuePairLists.Clear();
}
private void Save<T>(KeyValuePairLists<T> lists, string key, T value)
{
lists.TrySetValue(key, value);
}
private bool Load<T>(KeyValuePairLists<T> lists, string key, ref T value)
{
return lists.TryGetValue(key, ref value);
}
public void Save (string key, bool value)
{
Save(boolKeyValuePairLists, key, value);
}
/* Save 오버로딩 함수 중략 */
public void Save (string key, Quaternion value)
{
Save(quaternionKeyValuePairLists, key, value);
}
public bool Load (string key, ref bool value)
{
return Load(boolKeyValuePairLists, key, ref value);
}
/* Load 오버로딩 함수중략 */
public bool Load (string key, ref Quaternion value)
{
return Load (quaternionKeyValuePairLists, key, ref value);
}
}
|
SaveData.cs. ResettableScriptableObject을 상속한다. reset 호출이 가능한 ScriptableObject인 것이다.
Reset이 필요한 이유
1. Editor
SaveData는 .asset파일이기 때문에 Editor에서 발생한 변경사항이 그대로 디스크에 저장된다. 따라서 고정값(Constant)에 대해서는 Reset이 필요하다.
"이전 게시글에 Constant 병행하는 법에 대해 언급했다. 콜백 (2), 오해와 진실 (5)"
2. Runtime
빌드된 앱의 런타임에서는 Editor와 다르게 변경사항이 저장되지 않는다. 왜냐하면 .asset파일을 로드하면 메모리 상에서 값이 변경되는데 그 것을 다시 .asset파일로 저장하지 않기 때문이다. "Load만 수행"
하지만 런타임 시 메모리 값의 Reset이 필요한 경우가 있을 수 있어서 ResettableScriptableObject는 활용도가 높다.
각 SaveData의 값은 Saver에 의해 변경된다.
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
|
using System;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(SaveData))]
public class SaveDataEditor : Editor
{
private SaveData saveData;
private Action<bool> boolSpecificGUI;
private Action<int> intSpecificGUI;
private Action<string> stringSpecificGUI;
private Action<Vector3> vector3SpecificGUI;
private Action<Quaternion> quaternionSpecificGUI;
private void OnEnable ()
{
saveData = (SaveData)target;
/* 델리게이트 초기화
<인자> => { 함수 정의 부 }
value는 Saver로부터 받는 값이며
GUI가 반환하는 값을 value에 대입할 수 없으므로
아래 GUI들은 출력용(readonly)이다.
*/
// 토글 버튼
boolSpecificGUI = value => { EditorGUILayout.Toggle(value); };
intSpecificGUI = value => { EditorGUILayout.LabelField(value.ToString()); };
stringSpecificGUI = value => { EditorGUILayout.LabelField (value); };
vector3SpecificGUI = value => { EditorGUILayout.Vector3Field (GUIContent.none, value); };
quaternionSpecificGUI = value => { EditorGUILayout.Vector3Field (GUIContent.none, value.eulerAngles); };
}
public override void OnInspectorGUI ()
{
// target = SaveData
KeyValuePairListsGUI ("Bools", saveData.boolKeyValuePairLists, boolSpecificGUI);
KeyValuePairListsGUI ("Integers", saveData.intKeyValuePairLists, intSpecificGUI);
KeyValuePairListsGUI ("Strings", saveData.stringKeyValuePairLists, stringSpecificGUI);
KeyValuePairListsGUI ("Vector3s", saveData.vector3KeyValuePairLists, vector3SpecificGUI);
KeyValuePairListsGUI ("Quaternions", saveData.quaternionKeyValuePairLists, quaternionSpecificGUI);
}
private void KeyValuePairListsGUI<T> (string label, SaveData.KeyValuePairLists<T> keyvaluePairList, Action<T> specificGUI)
{
EditorGUILayout.BeginVertical(GUI.skin.box);
EditorGUI.indentLevel++;
EditorGUILayout.LabelField (label);
if (keyvaluePairList.keys.Count > 0)
{
for (int i = 0; i < keyvaluePairList.keys.Count; i++)
{
EditorGUILayout.BeginHorizontal ();
EditorGUILayout.LabelField (keyvaluePairList.keys[i]);
specificGUI (keyvaluePairList.values[i]);
EditorGUILayout.EndHorizontal ();
}
}
EditorGUI.indentLevel--;
EditorGUILayout.EndVertical();
}
}
|
SaveDataEditor.cs
토글과 벡터필드는 값을 넣어도 그 값을 value에 저장할 수 없기 때문에 Saver의 값을 출력만 한다.
Saver
Saver는 각 씬마다 하나씩 존재한다. Saver는 추상클래스며 다양한 Saver 파생 클래스가 존재한다.
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;
public abstract class Saver : MonoBehaviour
{
public string uniqueIdentifier;
public SaveData saveData;
// SaveData의 리스트로부터 value를 추출하기 위한 key
protected string key;
private SceneController sceneController;
private void Awake()
{
// 에셋을 경유하지 않고 바로 접근한다.
// 에셋을 사용하지 않으며 Inspector뷰에 드러나지 않으므로 지양해야할 부분이다.
sceneController = FindObjectOfType<SceneController>();
if(!sceneController)
throw new UnityException("Scene Controller could not be found, ensure that it exists in the Persistent scene.");
key = SetKey ();
}
private void OnEnable()
{
// 이벤트 에셋 대신 직접 구독한다. "지양점"
sceneController.BeforeSceneUnload += Save;
sceneController.AfterSceneLoad += Load;
}
private void OnDisable()
{
// 이벤트 에셋 대신 직접 구독해제한다. "지양점"
sceneController.BeforeSceneUnload -= Save;
sceneController.AfterSceneLoad -= Load;
}
protected abstract string SetKey ();
protected abstract void Save ();
protected abstract void Load ();
}
|
cs |
Saver.cs. 기존 싱글톤 매니저와 같은 구조다. 지양해야할 부분이다.
하지만 AdventureTutorial의 게임 장르 상 영속적 씬에 의해 SceneController의 초기화가 보장됐고, 관계가 직관적이기 때문에 이벤트-리스너 관계를 코드 속에 숨겨놓아도 한계가 크게 발생하진 않는다.
SceneController
사실상 관계는 이렇게 되어있다. Saver와 SceneController는 이벤트-리스너 관계이며 이 관계는 코드 속에 숨어있다.
Saver의 Save가 언제 호출되는지 파악하기 위해서 코드 상에서 Saver의 참조와 SceneController의 이벤트 참조를 같이 들여댜 봐야만 한다.
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
|
using System;
using UnityEngine;
using System.Collections;
using UnityEngine.SceneManagement;
public class SceneController : MonoBehaviour
{
// 이벤트의 리스너들을 확인하려면 '모든참조찾기'로 찾아봐야한다.
public event Action BeforeSceneUnload;
public event Action AfterSceneLoad;
public CanvasGroup faderCanvasGroup;
public float fadeDuration = 1f;
public string startingSceneName = "SecurityRoom";
public string initialStartingPositionName = "DoorToMarket";
public SaveData playerSaveData;
private bool isFading;
private IEnumerator Start ()
{
faderCanvasGroup.alpha = 1f;
playerSaveData.Save (PlayerMovement.startingPositionKey, initialStartingPositionName);
yield return StartCoroutine (LoadSceneAndSetActive (startingSceneName));
StartCoroutine (Fade (0f));
}
public void FadeAndLoadScene (SceneReaction sceneReaction)
{
if (!isFading)
{
StartCoroutine (FadeAndSwitchScenes (sceneReaction.sceneName));
}
}
private IEnumerator FadeAndSwitchScenes (string sceneName)
{
// Fade함수는 코루틴이므로 함수가 완료될 때까지 지연된다.
// Fade함수의 'yield return null'구문에도 지연을 유지한다.
yield return StartCoroutine (Fade (1f));
if (BeforeSceneUnload != null)
BeforeSceneUnload ();
yield return SceneManager.UnloadSceneAsync (SceneManager.GetActiveScene ().buildIndex);
yield return StartCoroutine (LoadSceneAndSetActive (sceneName));
if (AfterSceneLoad != null)
AfterSceneLoad ();
yield return StartCoroutine (Fade (0f));
}
private IEnumerator LoadSceneAndSetActive (string sceneName)
{
// 비동기 호출이므로 yield를 반환할 수 있다.
// 비동기 호출이므로 씬이 호출 될 동안 다른 작업이 수행된다.
yield return SceneManager.LoadSceneAsync (sceneName, LoadSceneMode.Additive);
// 씬이 로드 되면 해당 씬을 활성화 한다.
Scene newlyLoadedScene = SceneManager.GetSceneAt (SceneManager.sceneCount - 1);
SceneManager.SetActiveScene (newlyLoadedScene);
}
private IEnumerator Fade (float finalAlpha)
{
isFading = true;
faderCanvasGroup.blocksRaycasts = true;
float fadeSpeed = Mathf.Abs (faderCanvasGroup.alpha - finalAlpha) / fadeDuration;
while (!Mathf.Approximately (faderCanvasGroup.alpha, finalAlpha))
{
faderCanvasGroup.alpha = Mathf.MoveTowards (faderCanvasGroup.alpha, finalAlpha,
fadeSpeed * Time.deltaTime);
yield return null;
}
isFading = false;
faderCanvasGroup.blocksRaycasts = false;
}
}
|
SceneController.cs
코루틴의 yield return 구문은 해당 함수가 완료될 때까지 코드 진행을 지연시킨다.
'Unity > 스크립터블오브젝트' 카테고리의 다른 글
Scriptable Object - 응용: 씬 직렬화의 특징과 활용 (14) (0) | 2020.06.11 |
---|---|
Scriptable Object - 응용: ReactionCollection, 팝업과 드래그박스, 폴드아웃 (13) (0) | 2020.06.11 |
Scriptable Object - 응용: ConditionEditor, AllConditionsEditor, 프로젝트뷰 정리 (12) (0) | 2020.06.09 |
Scriptable Object - 응용: EditorWithSubEditors, 확장 메서드 (11) (2) | 2020.06.08 |
Scriptable Object - 응용: Interactable, 커스텀 에디터 (10) (0) | 2020.06.08 |
댓글