[Unity] Dungeon Gunner (5) - Dungeon Creation Design [3]

suhan0304·2024년 1월 14일
0
post-thumbnail

게임 리소스 리포지토리

게임 리소스를 관리하는 중앙 리포지토리를 생성하여 다양한 클래스가 쉽게 액세스 할 수 있도록 한다. 즉, 일종의 싱글톤처럼 정적 인스턴스 멤버 변수로 구성 요소를 만들어서 어디서든지 리소스를 액세스해서 사용할 수 있도록 한다.

GameResources.cs

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 스크립트에 추가하고 에디터에 나타나게 된다. 이제 우리가 원하는 다른 컴포넌트나 스크립트에 엑세스 할 수 있게 된다.

RoomNodeTypeListSO 생성

이제 룸 노드 타입 리스트 오브젝트를 생성해보자. 스크립터블 오브젝트들을 생성할 ScriptableObjectAssets > Dungeon > DungeonRoomNodeType 폴더를 생성해주고 해당 경로에 미리 사전에 작성해둔 RoomNodeTypeListSO를 생성해준다.

빈 리스트를 생성하면 콘솔에 로그가 다음과 같이 출력되는데 이를 통해 사전에 제작한 유틸리티, 유효성 검사가 제대로 작동하고 있는 것을 알 수 있다. 다시 한 번 언급하지만 이러한 유효성 검사를 최대한 하면서 진행해야 추후에 데이터를 입력하지 않아 발생할 수 있는 오류를 방지할 수 있다.

RoomNodeTypeSO 생성

이제 리스트에 추가해줄 RoomNodeTypeSO를 생성해보자. 동일한 경로에 여러 RoomNodeTypeSO을 추가해주고 각각의 RoomNodeTypeSO의 이름과 속성을 설정해준다.

이 또한 아무런 속성 값을 설정하지 않으면 유효성 검사가 진행되면서 콘솔에 로그가 출력될 것이다.

다음은 BoosRoom을 하나 생성한 후에 속성을 설정한 것이다.

이러한 방식으로 BoosRoom, ChestRoom 등, 등 다양한 RoomNodeTypeSO를 만들어준다.

이제 이 RoomNodeTypeSO를 만들어놓은 TypeListSO에 추가해준다.

이제 콘솔 로그를 Clear해서 지워준 후 다시 다른 부분을 클릭해도 어떤 로그도 출력되지 않으면 모든 SO(스크립터블 오브젝트)가 유효성 검사에 통과했다고 할 수 있다.

이제 아까 만들어준 GameResources 프리팹 오브젝트의 GameResources 스크립트 컴포넌트의 RoomNodeTypeList에 드래그드랍해서 초기화해준다.

룸 노드 그래프 편집기 업데이트

이제 룸 노드 그래프 편집기 스크립트를 수정해서 빈 편집기에서 여러 기능을 수행할 수 있도록 하자.

UnityEditor.Callbacks는 편집기에서 발생하는 특정한 일을 감지하고 콜백을 캡처할 수 있게 해주는 네임스패이스이다.

RoomNodeGraphEditor.cs

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;

    }

}

RoomNodeSO.cs

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
}

DungeonRoomNodeGraph 생성

이제 던전 룸 노드 그래프를 직접 생성해보자. Assets > ScriptableObejectAssets > Dungeon > DungeonRoomNodeGraphs에 RoomNodeGraph를 생성한다.

더블 클릭하면 아래와 같이 빈 편집기 창이 뜬다.

마우스 우클릭을 통해 노드를 하나 새로 생성할 수 있고 생성한 노드의 타입을 수정할 수 있다.

다음과 같이 생성된 노드들이 자연스럽게 RoomNodeGraphSO에 RoomNodeSO가 생성된 것을 확인할 수 있다.

실제로 RoomNode 들을 하나씩 선택해보면 선택한 RoomNodeType으로 RoomNodeTypeSO가 연결 되어있는 것을 확인할 수 있다.

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글