어제 작성한 글에서는 하나의 공간(방Room)만들 만들었다. 레퍼런스가 된 게임의 맵은 하나의 방만으로 이루어진 맵이 아니기에, 방을 여러개 만들고, 각각의 방의 이어줄 연결 맵(복도Corridor)를 생성한다.
또한, 맵의 가장자리에는 캐릭터가 더이상 진행할 수 없도록 경계(벽Wall)을 생성한다.
아래는 위 작업을 수행한 스크립트다. 추가, 수정된 변수와 메서드가 많기에 이전 스크립트와 대응하지 않는다.
이전 글과 같이, 중요 코드에는 주석을 추기했다.
TilemapVisualizer : 생성된 좌표를 바탕으로 타일맵을 생성하는 메서드(이전 글에도 있지만, 벽 타일 메서드가 새로 생겼기에 수정본을 게재한다.)
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class TilemapVisualizer : MonoBehaviour
{
[SerializeField]
private Tilemap floorTilemap, wallTilemap; // Tilemap게임오브젝트, 씬 뷰에서 바둑판 격자 그거
[SerializeField]
private TileBase floorTile, wallTile; // Tile로 생성(색칠)할 TilePallete의 파일.
public void PaintFloorTile(IEnumerable<Vector2Int> floorPositions)
{
PaintTiles(floorPositions, floorTilemap, floorTile);
}
public void PaintSingleBasicWall(Vector2Int position) // 벽 타일 생성 메서드
{
PaintSingleTile(wallTilemap, wallTile, position);
}
private void PaintTiles(IEnumerable<Vector2Int> positions, Tilemap tilemap, TileBase tile)
{
foreach (var position in positions)
{
PaintSingleTile(tilemap, tile, position);
}
}
private void PaintSingleTile(Tilemap tilemap, TileBase tile, Vector2Int position)
{
var tilePosition = tilemap.WorldToCell((Vector3Int)position);
tilemap.SetTile(tilePosition, tile);
}
public void Clear()
{
floorTilemap.ClearAllTiles(); // floorTilemap에 색칠된 모든 타일 삭제
wallTilemap.ClearAllTiles();
}
}
CorridorFirstDungeonGenerator : 복도 좌표 생성 메서드
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class CorridorFirstDungeonGenerator : SimpleWalkDungeonGenerator
{
[SerializeField]
private int corriderLength = 14, corridorCount = 5; // 생성될 복도 길이 / 복도 생성 횟수
[SerializeField]
[Range(0.1f, 1f)]
private float roomPercent; // 생성할 방의 비율
[SerializeField]
public SimpleRandomWalkSO roomGenerationParameters; // SimpleRandomWalkSO의 방 생성 변수들
protected override void RunProceduralGeneration()
{
// base.RunProceduralGeneration(); => RunProcedualGeneration메서드를 그냥 그대로 쓰겠다는 말. 물론 이 클래스에서는 그대로 안쓴다.
CorridorFirstGeneration();
}
private void CorridorFirstGeneration() // 복도 좌표 생성 최초메서드
{
HashSet<Vector2Int> floorPositions = new HashSet<Vector2Int>(); // 생성된 모든 좌표의 집합
HashSet<Vector2Int> potentialRoomrPositions = new HashSet<Vector2Int>(); // 좌표 중 방이 생성될 가능성이 있는 좌표의 집합
CreateCorridors(floorPositions, potentialRoomrPositions);
HashSet<Vector2Int> roomPositions = createRooms(potentialRoomrPositions);
floorPositions.UnionWith(roomPositions); // 위 메서드로 생성된 방 좌표도 복도 좌표와 함께 바닥 타일을 생성할 좌표다.
tilemapVisualizer.PaintFloorTile(floorPositions);
WallGenerator.CreateWalls(floorPositions, tilemapVisualizer);
}
private HashSet<Vector2Int> createRooms(HashSet<Vector2Int> potentialRoomPositions)
{
HashSet<Vector2Int> roomPositions = new HashSet<Vector2Int>(); // 생성될 방의 위치 집합
int roomToCreateCount = Mathf.RoundToInt(potentialRoomPositions.Count * roomPercent);
// 생성될 방의 갯수. 모든 potentialRoomPositions에 생성하지 않고, roomPercent에 따라 적절히 감소시킨다. Mathf.RoundToInt() :정수로 반올림
List<Vector2Int> roomsToCreate = potentialRoomPositions.OrderBy(x => Guid.NewGuid()).Take(roomToCreateCount).ToList();
// GUID : 전역 고유 식별자. Guid.NewGuid() : 고유한 키 생성. Take(정수값) : 배열 중 처음 인덱스부터 정수값 갯수까지만 할당한다.
foreach (var roomPosition in roomsToCreate)
{
var roomFloor = RunRandomWalk(randomWalkParameters, roomPosition); // 각 방 좌표마다 방 생성 변수에 따라 방 좌표 집합 생성
roomPositions.UnionWith(roomFloor);
}
return roomPositions;
}
private void CreateCorridors(HashSet<Vector2Int> floorPositions, HashSet<Vector2Int> potentialRoomrPositions)
{
var currentPosition = startPosition; // 0,0부터 시작
potentialRoomrPositions.Add(currentPosition); // 물론, 시작좌표에도 방이 만들어질 수 있다.
for (int i = 0; i < corridorCount; i++)
{
var corridor = ProceduralGenerationAlgorithms.RandomWalkCorridor(currentPosition, corriderLength); // corridor 좌표 집합 생성 명령
floorPositions.UnionWith(corridor); // 좌표집합 합치기
currentPosition = corridor[corridor.Count - 1]; // 다음 corridor 좌표 생성의 시작점은 현재까지 만들어진 corrider의 마지막 좌표
potentialRoomrPositions.Add(currentPosition);
}
}
}
ProceduralGenerationAlgorithm : 바닥 맵의 좌표 생성 알고리즘 클래스. 이전 작성글의 스크립트에 복도 좌표 집합 생성 메서드를 추가했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class ProceduralGenerationAlgorithms
{
public static HashSet<Vector2Int> SimpleRandomWalk(Vector2Int startPosition, int walkLength)
{
HashSet<Vector2Int> path = new HashSet<Vector2Int>(); // HashSet<T> : 인덱스가 없는 제레릭 배열. 인덱스 없이 Hash로 관리된다.
// 여기서 만드는 path가 SimpleWalkDungeonGenerator의 floorPositions, 즉 타일이 생성될 좌표의 집합이 된다.
path.Add(startPosition); // 첫 시작은 startPosition부터. 물론 SimpleWalkDungeonGenerator.startRandomlyEachIteration = true일 경우에는 이마저도 0,0이 아닌 생성된 타일 좌표 중에서 랜덤으로 정해진다.
var previousposition = startPosition;
for (int i = 0; i < walkLength; i++)
{
var newPosition = previousposition + Direction2D.GetRandomCardinalDirection(); // newPosition : 기존의 좌표에 상하좌우 택1 한칸이동 값을 더한다.
path.Add(newPosition); // 새로 만들어진 좌표를 path에 추가
previousposition = newPosition; // 새로 만들어진 좌표는 다시 기존의 좌표가 된다.
}
return path; // path => floorPositions
}
public static List<Vector2Int> RandomWalkCorridor(Vector2Int startPosition, int corridorLength) // 실행되면 corridorLength만큼 좌표 집합 한줄이 상하좌우택1 쭉 깔린다.
{
List<Vector2Int> corridor = new List<Vector2Int>();
var direction = Direction2D.GetRandomCardinalDirection();
var currentPosition = startPosition;
for (int i = 0; i < corridorLength; i++)
{
currentPosition += direction;
corridor.Add(currentPosition);
}
return corridor;
}
}
public static class Direction2D
{
public static List<Vector2Int> cardinalDirectionsList = new List<Vector2Int>() // cardinal : 기본적인, 가장 중요한
{
new Vector2Int(0,1), // UP
new Vector2Int(0,-1), // DOWN
new Vector2Int(1,0), // RIGHT
new Vector2Int(-1,0), // LEFT
};
public static Vector2Int GetRandomCardinalDirection()
{
return cardinalDirectionsList[Random.Range(0, cardinalDirectionsList.Count)]; // cardinalDirectionList리스트의 랜덤 인덱스에 따른 값
}
}
사실, 상단의 글 "방을 여러개 만들고, 각각의 방의 이어줄 연결 맵(복도Corridor)를 생성"은 위 코드에 따르면 잘못된 표현이다.
먼저 복도의 좌표 집합을 만들고, 이 과정에서 복도의 교점(혹은 가장자리)에 방이 생성될 좌표 집합도 같이 만든 다음, 방의 좌표 집합에 (생성 비율에 맞게)각각 방으로 만들어질 좌표 집합을 만들어 넣는다. 이 과정의 집합은 모두 하나의 좌표 집합으로 통합돼, 마지막에 이 집합에 따라 타일맵이 생성된다.
쉽게 말해, 복도 위치 -> 방 위치 -> 방이 차지하는 공간 -> 바닥 타일 순으로 진행한다.
WallGenerator : 벽 좌표 집합 생성 클래스
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class WallGenerator
{
public static void CreateWalls(HashSet<Vector2Int> floorPositions, TilemapVisualizer tilemapVisualizer)
{ // floorPositions : 바닥에 깔린 맵 좌표 집합
var basicWallPositions = FindWallsInDirections(floorPositions, Direction2D.cardinalDirectionsList);
foreach (var position in basicWallPositions)
{
tilemapVisualizer.PaintSingleBasicWall(position);
}
}
private static HashSet<Vector2Int> FindWallsInDirections(HashSet<Vector2Int> floorPositions, List<Vector2Int> directionsList)
{ // 굉장한 코드다.
HashSet<Vector2Int> wallPositions = new HashSet<Vector2Int>();
foreach (var position in floorPositions) // 바닥 맵 좌표마다
{
foreach (var direction in directionsList) // 상하좌우 4방향 전부 체크하면서
{
var neighbourPosition = position + direction; // 바닥맵 좌표에 인접한 좌표가 -
if (!floorPositions.Contains(neighbourPosition)) // 바닥맵 좌표에 포함되어 있지 않다면 => 비어있는 좌표라면
{
wallPositions.Add(neighbourPosition); // 그 좌표는 벽을 만들 좌표다.
}
}
}
return wallPositions;
}
}
C# Tips
모든 키워드에 마우스를 갖다 올리면 나오는 팝업창의 키워드 상세는 거의 전부 클릭해서 해당 키워드의 사용처, 위치, (C# 내장 키워드라면)사용 방법을 확인할 수 있다.
'UNITY' 카테고리의 다른 글
| UNITY_20231005[유니티 3D 강의 시작, 알고리즘 문제 풀이] (0) | 2023.10.05 |
|---|---|
| UNITY_20231004[팀 과제-로그라이크 RPG - 완료] (0) | 2023.10.04 |
| UNITY_20230926[팀 과제-로그라이크 RPG - RandomWalk 알고리즘] (0) | 2023.09.26 |
| UNITY_20230925[팀 과제-로그라이크 RPG, 디자인 패턴-전략 패턴] (1) | 2023.09.25 |
| UNITY_20230921[개인과제] (0) | 2023.09.22 |