본문 바로가기

UNITY

UNITY_20231002[팀 과제-로그라이크 RPG - 벽, 방, 복도 만들기]

어제 작성한 글에서는 하나의 공간(방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# 내장 키워드라면)사용 방법을 확인할 수 있다.