이제 만들어놓은 룸들을 룸 노드 그래프에 맞게 배치해서 던전 룸을 실제로 만들어주는 DungeonBuilder를 제작해보자. 던전 빌더는 앞서 원리는 다음과 같다.
일단 던전 빌더는 레벨에 맞는 프리팹을 맞게 룸을 가져와야 된다.
그렇기에 던전 레벨 Scriptable Object로 던전 레벨과 관련된 정보를 관리한다.
던전 빌더의 알고리즘 순서도는 아래와 같다.
RoomNodeSO 정보로 랜덤한 RoomTemplateSO를 던전 빌더가 선택해서 가져오고 그 룸을 실제 Room 오브젝트로 만들어서 던전을 구현한다.
던전 빌더를 구현하기 전에 유효성 검사를 하나 추가해준다.
public static bool ValidateCheckEnumerableValues(Object thisObject, string fieldName, IEnumerable enumerableObjectToCheck)
{
bool error = false;
int count = 0;
if (enumerableObjectToCheck == null)
{
Debug.Log(fieldName + " is null in object " + thisObject.name.ToString());
return true;
}
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;
}
enumerableObjectToCheck가 null인지를 먼저 확인하고나서 enumerableObjectToCheck의 item들을 확인한다.
이제 위에서 기술한 Dungeon Level을 Scriptable Object로 만들어주기 위해 DungeonLevelSO.cs를 작성한다.
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Unity.VisualScripting;
using UnityEngine;
[CreateAssetMenu(fileName = "DungeonLevel_", menuName = "Scriptable Objects/Dungeon/Dungeon Level")]
public class DungeonLevelSO : ScriptableObject
{
#region Header BASIC LEVEL DETAILS
[Space(10)]
[Header("BASIC LEVEL DETAILS")]
#endregion Header BASIC LEVEL DETAILS
#region Tooltip
[Tooltip("The name for the level")]
#endregion Tooltip
public string levelName;
#region Header ROOM TEMPLATES FOR LEVEL
[Space(10)]
[Header("ROOM TEMPLATES FOR LEVEL")]
#endregion Header ROOM TEMPLATES FOR LEVEL
#region Tooltip
[Tooltip("Populate the list with the room templates that you want to be part of the level. You need to ensure that room templates are included for all room node types that are specified in the Room Node Graphs for the level.")]
#endregion Tooltip
public List<RoomTemplateSO> roomTemplateList;
#region Header ROOM NODE GRAPHS FOR LEVEL
[Space(10)]
[Header("ROOM NODE GRAPHS FOR LEVEL")]
#endregion Header ROOM NODE GRAPHS FOR LEVEL
#region Tooltip
[Tooltip("Populate this list with the room node graphs which should be randomly selected from for the level.")]
#endregion Tooltip
public List<RoomNodeGraphSO> roomNodeGraphList;
#region Validation
#if UNITY_EDITOR
// Validate scriptable object details eneterd
private void OnValidate()
{
HelperUtilities.ValidateCheckEmptyString(this, nameof(levelName), levelName);
if (HelperUtilities.ValidateCheckEnumerableValues(this, nameof(roomTemplateList), roomTemplateList))
return;
if (HelperUtilities.ValidateCheckEnumerableValues(this, nameof(roomNodeGraphList), roomNodeGraphList))
return;
// Check to make sure that room templates are specified for all the node types in the
// specified node graphs
// First check that nortrth/south corridor, east/west corridor and entrance types have been specified
bool isEWCorridor = false;
bool isNSCorridor = false;
bool isEntrance = false;
// Loop through all room templates to check that this node type has been specified
foreach (RoomTemplateSO roomTemplateSO in roomTemplateList)
{
if (roomTemplateSO == null)
return;
if (roomTemplateSO.roomNodeType.isCorridorEW)
isEWCorridor = true;
if (roomTemplateSO.roomNodeType.isCorridorNS)
isNSCorridor = true;
if (roomTemplateSO.roomNodeType.isEntrance)
isEntrance = true;
}
if (isEWCorridor == false)
{
Debug.Log("In " + this.name.ToString() + " : No E/W Corridor Room Type Specified");
}
if (isNSCorridor == false)
{
Debug.Log("In " + this.name.ToString() + " : No N/S Corridor Room Type Specified");
}
if (isEntrance == false)
{
Debug.Log("In " + this.name.ToString() + " : No Entrance Room Type Specified");
}
// Loop through all node graphs
foreach (RoomNodeGraphSO roomNodeGraph in roomNodeGraphList)
{
if (roomNodeGraph == null)
return;
// Loop throu all nodes in node graph
foreach (RoomNodeSO roomNodeSO in roomNodeGraph.roomNodeList)
{
if (roomNodeSO == null)
continue;
// Check that a room template has been specified for each roomNode type
// Corridors and entrance already checked
if (roomNodeSO.roomNodeType.isEntrance || roomNodeSO.roomNodeType.isCorridorEW || roomNodeSO.roomNodeType.isCorridorNS ||
roomNodeSO.roomNodeType.isCorridor || roomNodeSO.roomNodeType.isNone)
continue;
bool isRoomNodeTypeFound = false;
// Loop though all room templates to check that this node type has been specified
foreach (RoomTemplateSO roomTemplateSO in roomTemplateList)
{
if (roomTemplateSO == null)
continue;
if (roomTemplateSO.roomNodeType == roomNodeSO.roomNodeType)
{
isRoomNodeTypeFound = true;
break;
}
}
if (!isRoomNodeTypeFound)
Debug.Log("In " + this.name.ToString() + " : No room template " + roomNodeSO.roomNodeType.name.ToString() + " found for node graph " + roomNodeGraph.name.ToString());
}
}
}
#endif
#endregion Validation
}
이제 위에서 작성한 코드를 유니티로 돌아와 컴파일 해주고 Dungeon 폴더안에 Levels 폴더를 만들고 DungeonLevelSO를 만들어보자.
Level1 DungeonLevelSO는 아래 사진과 같이 설정한다. 이때 원하는 룸 Template과 원하는 노드 그래프 리스트들을 직접 생성해서 넣어줄 수 있다.
동일한 방식으로 Level2 ~ Level6까지 생성해준다.
BossRoom을 지우면 보스룸이 없다고 오류가 검사되는 것을 통해 유효성 검사가 잘 작동하고 있는 것을 확인할 수 있다