스크립터블 개체 클래스를 사용하여 룸 노드 데이터를 저장한다. 룸 노드 형식을 저장하기 위해 스크립터블 클래스를 생성하고 이러한 룸 노드 형식의 목록을 저장할 클래스도 생성한다.
또한 생성한 스크립터블 개체와 같이 직렬화된 데이터가 올바르게 채워졌는지 확인하는 유효성 검사 또한 수행할 수 있도록 한다.
데이터를 저장하기 위한 스크립터블 클래스의 개체 Asset을 추가할수록 소실된 데이터를 개발자에게 알려주는 유효성 검사가 중요해진다. 이러한 유효성 검사가 없으면 오류가 날 확률이 높다.
룸 노드 그래프의 데이터 구조가 어떤 형식으로 구성될 것인지를 확인해보자. 일단 먼저 RoomNodeGraphSO로 완전한 노드 그래프를 나타낸다. 이 룸 노드 그래프는 각 개별 방 노드를 나타내는 방 노드 스크립터블 오브젝트로 구성된다. 이때 각기 방 노드 즉, RoomNodeSO는 RoomNodeTypeSO로 방 유형을 정의한다.
SO는 Scriptable Object의 약자이다.
룸 노드가 어떤 형식으로 계층 구조로 표현되는지 확인해보자. 각 노드 별로 부모 노드 ID 목록과 자녀 노드 ID 목록이 있다. 이를 통해 노드 그래프에서 노드의 레이아웃을 나타낼 수 있다. 이를 통해 1번 RoomNodeSO가 2, 3번 RoomNodeSo와 링크되어있고 그 아래의 자식 룸 노드들로 링크되는것을 확인할 수 있다.
이제 코드로 위 내용들을 구현해보자!
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "RoomNodeGraph", menuName = "Scriptable Objects/Dungeon/Room Node Graph")]
public class RoomNodeGraphSO : ScriptableObject
{
[HideInInspector] public RoomNodeTypeListSO roomNodeTypeList;
[HideInInspector] public List<RoomNodeGraphSO> roomNodeList = new List<RoomNodeSO>();
[HideInInspector] public Dictionary<string, RoomNodeGraphSO> roomNodeDictionary = new Dictionary<string, RoomNodeSO>();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoomNodeSO : ScriptableObject
{
[HideInInspector] public string id;
[HideInInspector] public List<string> parentRoomNodeIDList = new List<string>();
[HideInInspector] public List<string> childRoomNodeIDList = new List<string>();
[HideInInspector] public RoomNodeGraphSO roomNodeGraph;
public RoomNodeTypeSO roomNodeType;
[HideInInspector] public RoomNodeTypeListSO roomNodeTypeList;
}
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
[CreateAssetMenu(fileName = "RoomNodeType", menuName = "Scriptable Objects/Dungeon/Room Node Type")]
public class RoomNodeTypeSO : ScriptableObject
{
public string roomNodeTypeName;
#region Header
[Header("Only flag the RoomNodeTypes that should be visible in the editor")]
#endregion Header
public bool displayInNodeGraphEditor = true;
#region Header
[Header("One Type Should Be A Corridor")]
#endregion Header
public bool isCorridor = true;
#region Header
[Header("One Type Should Be A CorridorNS")]
#endregion Header
public bool isCorridorNS = true;
#region Header
[Header("One Type Should Be A CorridorEW")]
#endregion Header
public bool isCorridorEW = true;
#region Header
[Header("One Type Should Be A Entrance")]
#endregion Header
public bool isEntrance = true;
#region Header
[Header("One Type Should Be A Boss Room")]
#endregion Header
public bool isBossRoom = true;
#region Header
[Header("One Type Should Be None (Unassigned)")]
#endregion Header
public bool isNone = true;
#region Validation
#if UNITY_EDITOR
private void OnValidate()
{
HelperUtilities.ValidateCheckEmptyString(this, nameof(roomNodeTypeName), roomNodeTypeName);
}
#endif
#endregion
}
HelperUtilities는 아래 HelperUtilities.cs를 참고한다.
#if UNITY_EDITOR ~ #endif 사이의 내용은 유니티 에디터 환경에서만 작동한다. 즉, 실제 게임 플레이 상에서는 실행되지않는 코드이고 에디터, 즉 개발 환경에서만 유효성 검사가 실행되는 것이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "RoomNodeTypeListSO", menuName = "Scriptable Objects/Dungeon/Room Node Type List")]
public class RoomNodeTypeListSO : ScriptableObject
{
#region Header ROOM NODE TYPE LIST
[Space(10)]
[Header("ROOM NODE TYPE LIST")]
#endregion
#region Tooltip
[Tooltip("This list should be populated with all the RoomNodeTypeSO for the game - it is used instead of an enum")]
#endregion
public List<RoomNodeTypeSO> list;
#region Validation
#if UNITY_EDITOR
private void OnValidate()
{
HelperUtilities.ValidateCheckEnumerableValues(this, nameof(list), list);
}
#endif
#endregion
}
아래는 유효성 검사를 도와주는 HelperUtilities 스크립트이다.
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class HelperUtilities
{
/// <summary>
/// Empty String Debug check
/// 문자열(stringToCheck)이 공백인지 확인하는 유효성 검사
/// </summary>
/// <param name="thisObject"></param>
/// <param name="fieldName"></param>
/// <param name="stringToCheck"></param>
/// <returns></returns>
public static bool ValidateCheckEmptyString(Object thisObject, string fieldName, string stringToCheck)
{
if(stringToCheck == "")
{
Debug.Log(fieldName + " is empty and must contain a value in object " + thisObject.name.ToString());
return true;
}
return false;
}
/// <summary>
/// list empty or contains null value check - returns true if there is an error
/// 리스트가 비어있거나 null value가 있는지 확인하는 유효성 검사
/// </summary>
/// <param name="thisObject"></param>
/// <param name="fieldName"></param>
/// <param name="enumerableObjectToCheck"></param>
/// <returns></returns>
public static bool ValidateCheckEnumerableValues(Object thisObject, string fieldName, IEnumerable enumerableObjectToCheck)
{
bool error = false;
int count = 0;
foreach(var item in enumerableObjectToCheck)
{
if (item == null)
{
Debug.Log(fieldName + " has null values in object " + thisObject.name.ToString());
error = true;
}
else
{
count++;
}
}
if(count == 0)
{
Debug.Log(fieldName + " has no values in object " + thisObject.name.ToString());
error = true;
}
return error;
}
}
위와 같이 스크립트를 작성해서 기본적인 스크립터블 오브젝트 구조를 모두 구현해낼 수 있다. 이제 이러한 스크립터블 오브젝트를 이용해서 룸 노드 그래프를 생성할 수 있는 에디터를 완성 시키고 에디터를 이용해 만든 룸 노드 그래프를 기반으로 던전의 룸 노드들이 배치되도록 한다.