게임 리소스를 관리하는 중앙 리포지토리를 생성하여 다양한 클래스가 쉽게 액세스 할 수 있도록 한다. 즉, 일종의 싱글톤처럼 정적 인스턴스 멤버 변수로 구성 요소를 만들어서 어디서든지 리소스를 액세스해서 사용할 수 있도록 한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameResources : MonoBehaviour
{
private static GameResources instance;
public static GameResources Instance
{
get
{
if(instance == null)
{
instance = Resources.Load<GameResources>("GameResources");
}
return instance;
}
}
#region Header DUNGEON
[Space(10)]
[Header("DUNGEON")]
#endregion
#region Tooltip
[Tooltip("Populate with the dungeon RoomNodeTypeListSO")]
#endregion
public RoomNodeTypeListSO roomNodeTypeList;
}
해당 게임 리소스에서 룸 노드 타입 리스트(스크립터블 오브젝트)를 액세스 할 수 있도록 작성한다.
Resources.Load는 유니티에서 특수한 유형의 폴더, 리소스 폴더 내에 배치한 리소스를 로드할 수 있는 방법으로 Resources라는 이름으로 폴더를 만들면 해당 폴더에서 리소스를 쉽고 빠르게 로드해올 수 있다.
더 자세한 내용은 유니티 공식 문서를 참고하자
빈 오브젝트를 만들고 위에서 작성한 GameResources 스크립트를 컴포넌트에 추가해준 후에 Resources 폴더 안에 프리팹화 해준다.
이제 프리팹화 시킨 GameResources 오브젝트를 리소스 리포지토리라고 할 수 있다. 여러 컴포넌트가 접근하고자 하는 리소스를 GameResources 스크립트에 추가하고 에디터에 나타나게 된다. 이제 우리가 원하는 다른 컴포넌트나 스크립트에 엑세스 할 수 있게 된다.
이제 룸 노드 타입 리스트 오브젝트를 생성해보자. 스크립터블 오브젝트들을 생성할 ScriptableObjectAssets > Dungeon > DungeonRoomNodeType 폴더를 생성해주고 해당 경로에 미리 사전에 작성해둔 RoomNodeTypeListSO를 생성해준다.
빈 리스트를 생성하면 콘솔에 로그가 다음과 같이 출력되는데 이를 통해 사전에 제작한 유틸리티, 유효성 검사가 제대로 작동하고 있는 것을 알 수 있다. 다시 한 번 언급하지만 이러한 유효성 검사를 최대한 하면서 진행해야 추후에 데이터를 입력하지 않아 발생할 수 있는 오류를 방지할 수 있다.
이제 리스트에 추가해줄 RoomNodeTypeSO를 생성해보자. 동일한 경로에 여러 RoomNodeTypeSO을 추가해주고 각각의 RoomNodeTypeSO의 이름과 속성을 설정해준다.
이 또한 아무런 속성 값을 설정하지 않으면 유효성 검사가 진행되면서 콘솔에 로그가 출력될 것이다.
다음은 BoosRoom을 하나 생성한 후에 속성을 설정한 것이다.
이러한 방식으로 BoosRoom, ChestRoom 등, 등 다양한 RoomNodeTypeSO를 만들어준다.
이제 이 RoomNodeTypeSO를 만들어놓은 TypeListSO에 추가해준다.
이제 콘솔 로그를 Clear해서 지워준 후 다시 다른 부분을 클릭해도 어떤 로그도 출력되지 않으면 모든 SO(스크립터블 오브젝트)가 유효성 검사에 통과했다고 할 수 있다.
이제 아까 만들어준 GameResources 프리팹 오브젝트의 GameResources 스크립트 컴포넌트의 RoomNodeTypeList에 드래그드랍해서 초기화해준다.
이제 룸 노드 그래프 편집기 스크립트를 수정해서 빈 편집기에서 여러 기능을 수행할 수 있도록 하자.
UnityEditor.Callbacks는 편집기에서 발생하는 특정한 일을 감지하고 콜백을 캡처할 수 있게 해주는 네임스패이스이다.
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using Unity.VisualScripting;
using UnityEditor.MPE;
using System;
public class RoomNodeGraphEditor : EditorWindow //편집기
{
private GUIStyle roomNodeStyle;
private static RoomNodeGraphSO currentRoomNodeGraph;
private RoomNodeTypeListSO roomNodeTypeList;
//Node layout Values
private const float nodeWidth = 160f;
private const float nodeHeight = 75f;
private const int nodePadding = 25;
private const int nodeBorder = 12;
[MenuItem("Room Node Graph Editor", menuItem = "Window/Dungeon Editor/Room Node Graph Editor")]
private static void OpenWindow()
{
GetWindow<RoomNodeGraphEditor>("Room Node Graph Editor");
}
private void OnEnable()
{
// Define node layout style
roomNodeStyle = new GUIStyle();
roomNodeStyle.normal.background = EditorGUIUtility.Load("node1") as Texture2D;
roomNodeStyle.normal.textColor = Color.white;
roomNodeStyle.padding = new RectOffset(nodePadding, nodePadding, nodePadding, nodePadding);
roomNodeStyle.border = new RectOffset(nodeBorder, nodeBorder, nodeBorder, nodeBorder);
// Load Room Node types
roomNodeTypeList = GameResources.Instance.roomNodeTypeList;
}
/// <summary>
/// Open the room node graph editor window if a room node graph scriptable object asset is double clicked in the inspector
/// </summary>
[OnOpenAsset(0)] // Need the namespace UnityEditor.Callbacks
public static bool OnDoubleClickAsset(int instanceID, int line)
{
RoomNodeGraphSO roomNodeGraph = EditorUtility.InstanceIDToObject(instanceID) as RoomNodeGraphSO;
if (roomNodeGraph != null)
{
OpenWindow();
currentRoomNodeGraph = roomNodeGraph;
return true;
}
return false;
}
/// Draw Editor GUI
private void OnGUI()
{
// If a scriptable object of type RoomNodeGraphSo has been selected then process
if (currentRoomNodeGraph != null)
{
// Process Events
ProcessEvent(Event.current);
// Draw Room Nodes
DrawRoomNodes();
}
if (GUI.changed)
Repaint();
}
private void ProcessEvent(Event currentEvent)
{
ProcessRoomNodeGraphEvents(currentEvent);
}
/// <summary>
/// Process Room Node Graph Events
/// </summary>
private void ProcessRoomNodeGraphEvents(Event currentEvent)
{
switch(currentEvent.type)
{
// Process Mouse Down Events
case EventType.MouseDown:
ProcessMouseDownEvent(currentEvent);
break;
default:
break;
}
}
/// <summary>
/// Process mouse down events on the room node graph (not over a node)
/// </summary>
private void ProcessMouseDownEvent(Event currentEvent)
{
// Process right click mouse down on graph event (show context menu)
if(currentEvent.button == 1)
{
ShowContextMenu(currentEvent.mousePosition);
}
}
/// <summary>
/// Show the context menu
/// </summary>
private void ShowContextMenu(Vector2 mousePosition)
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("Create Room Node "), false, CreateRoomNode, mousePosition);
menu.ShowAsContext();
}
/// <summary>
/// Create a room node at the mouse position
/// </summary>
private void CreateRoomNode(object mousePositionObject)
{
CreateRoomNode(mousePositionObject, roomNodeTypeList.list.Find(x => x.isNone));
}
/// <summary>
/// Create a room node at the mouse position - overloaded to also pass in RoomNodeType
/// </summary>
private void CreateRoomNode(object mousePositionObject, RoomNodeTypeSO roomNodeType)
{
Vector2 mousePosition = (Vector2)mousePositionObject;
// create room node scriptable object asset
RoomNodeSO roomNode = ScriptableObject.CreateInstance<RoomNodeSO>();
// add room node to current room node graph room node list
currentRoomNodeGraph.roomNodeList.Add(roomNode);
// set room node values
roomNode.Initialise(new Rect(mousePosition, new Vector2(nodeWidth, nodeHeight)), currentRoomNodeGraph, roomNodeType);
// add room node to room node graph scriptable object assets database
AssetDatabase.AddObjectToAsset(roomNode, currentRoomNodeGraph);
AssetDatabase.SaveAssets();
}
/// <summary>
/// Draw room nodes in the graph window
/// </summary>
private void DrawRoomNodes()
{
// Loop through all room nodes and draw them
foreach (RoomNodeSO roomNode in currentRoomNodeGraph.roomNodeList)
{
roomNode.Draw(roomNodeStyle);
}
GUI.changed = true;
}
}
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices.WindowsRuntime;
using UnityEditor;
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;
#region Editor Code
// the following code should only be run in the Unity Editor
#if UNITY_EDITOR
[HideInInspector] public Rect rect;
/// <summary>
/// Initialise node
/// </summary>
public void Initialise(Rect rect, RoomNodeGraphSO nodeGraph, RoomNodeTypeSO roomNodeType)
{
this.rect = rect;
this.id = Guid.NewGuid().ToString();
this.name = "RoomNode";
this.roomNodeGraph = nodeGraph;
this.roomNodeType = roomNodeType;
// Load room node type list
roomNodeTypeList = GameResources.Instance.roomNodeTypeList;
}
public void Draw(GUIStyle nodeStyle)
{
// Draw Node Box Using Begin Area
GUILayout.BeginArea(rect, nodeStyle);
// Start Region To Detect Popup Selection Changes
EditorGUI.BeginChangeCheck();
// Display a popup using the RoomNodeType name values that can be selected from (default to the currently set roomNodeType)
int selected = roomNodeTypeList.list.FindIndex(x => x == roomNodeType);
int selection = EditorGUILayout.Popup("", selected, GetRoomNodeTypeToDisplay());
roomNodeType = roomNodeTypeList.list[selection];
if (EditorGUI.EndChangeCheck())
EditorUtility.SetDirty(this);
GUILayout.EndArea();
}
/// <summary>
/// Populate a string array with the room node types to display that can be selected
/// </summary>
public string[] GetRoomNodeTypeToDisplay()
{
string[] roomArray = new string[roomNodeTypeList.list.Count];
for(int i = 0; i < roomNodeTypeList.list.Count; i++)
{
if (roomNodeTypeList.list[i].displayInNodeGraphEditor)
{
roomArray[i] = roomNodeTypeList.list[i].roomNodeTypeName;
}
}
return roomArray;
}
#endif
#endregion Editor COde
}
이제 던전 룸 노드 그래프를 직접 생성해보자. Assets > ScriptableObejectAssets > Dungeon > DungeonRoomNodeGraphs에 RoomNodeGraph를 생성한다.
더블 클릭하면 아래와 같이 빈 편집기 창이 뜬다.
마우스 우클릭을 통해 노드를 하나 새로 생성할 수 있고 생성한 노드의 타입을 수정할 수 있다.
다음과 같이 생성된 노드들이 자연스럽게 RoomNodeGraphSO에 RoomNodeSO가 생성된 것을 확인할 수 있다.
실제로 RoomNode 들을 하나씩 선택해보면 선택한 RoomNodeType으로 RoomNodeTypeSO가 연결 되어있는 것을 확인할 수 있다.