본문 바로가기
Unity

Unity(8) - Map Error Resolution

by Srff5123 2026. 4. 13.
728x90

몬스터를 맵의 끝부분 1 ~ 5칸에서 일정 시간마다 랜덤 스폰을 시켜서 플레이어 방향으로 쫓아와 공격하는 형식으로 만들건데

이거를 해서 테스트를 위해서는 예전 맵을 만들때부터 있었던 가장 큰 치명적인 오류를 먼저 해결해야 한다.

 

캐릭터가 벽에 밀착하면 벽에 끼이는 부분(점프가 원활이 동작되지 않음)

일부 블록을 깨서 나온 바닥을 밟으면 블럭은 렌더링이 되어져 눈에 보이나 피직스가 없는건지 충돌처리가 안되는건지 캐릭터가 바닥을 뚫고 떨어져 버리는 부분

 

이렇게 크게 두가지 정도의 치명적인 오류가 있으며 이것을 먼저 해결하고자 한다.

 

이동하는 부분에서의 오류는 없었으나 

복셀 월드의 충돌메시 생성 방식에서의 오류에 의해 생기는 버그였다

VoxelChunk.AddFace()의 삼각형 인덱스 순서가 면의 정점 배열과 맞지않아 일부 면의 법선 방향이 뒤집힌 상태로 생성되어 충돌용 면의 방향에 대한 문제였다

충돌의 안정성을 위해 Physics.queriesHitBackfaces = true를 start에 설정해두어서 백페이스의 삼각형을 감지해 일부는 충돌이 되었지만 일부에서는 안되는 오류

 

VoxelWorld.cs

월드의 전체관리자로 청크들을 생성하고 각 청크 블록 데이터를 채운다음 마지막에 BuildMesh()를 호출한다.

VoxelChunk.cs

청크 하나의 블록 배열을 들고 있고 각 블록의 6면을 검사해 보이는 면만 AddFace()로 메시를 만든다

그리고 그 렌더용 메시를 그래도 MeshCollider에도 넣는 구조로 AddFace()의 역할이 중점이 되어 렌더링, 충돌을 해당 함수가 맡아서했다.

 

if (dir == FaceDir.NegX || dir == FaceDir.NegY || dir == FaceDir.NegZ)
{
    tris.Add(vCount + 0); tris.Add(vCount + 2); tris.Add(vCount + 1);
    tris.Add(vCount + 0); tris.Add(vCount + 3); tris.Add(vCount + 2);
}
else
{
    tris.Add(vCount + 0); tris.Add(vCount + 1); tris.Add(vCount + 2);
    tris.Add(vCount + 0); tris.Add(vCount + 2); tris.Add(vCount + 3);
}

기존의 코드를 보면 이런식으로 음수 방향의 면만 뒤집으면 된다는 가정으로 짜여져 있었는데

내가 정의한 면의 정점 배열은 해당 부분과 다르게 설정을 했었다

facePosx, y, z, 3개의 정점 배열 순서가 이미 positive face를 기본 순서로 삼각형을 만들때 outward normal이 나오지 않는 배열이었는데

코드는 Pos면은 그대로, Neg 면만 뒤집기로 처리하여서 법선 방향이 뒤집힌 면이 생기게 되었다

private static readonly Vector3[] facePosY =
{
    new(0, 1, 0),
    new(1, 1, 0),
    new(1, 1, 1),
    new(0, 1, 1)
}

 

y값의 윗면 정점 배열은 이런식으로 되어져 normal이 +y를 향해야 하는데 실제로는 아래-y쪽으로 향하게 되어 화면상으로는 윗면이 제대로 렌더링되어 바닥으로 보이지만 물리 판정에서는 뒤집힌 윗면의 상태가 되어 플레이어가 위를 걸을 때

어떤 프레임에는 맞고 어떤 프레임에는 판정이 흔들리게 되어 바닥을 뚫고 내려가는 현상이 생기게 된다

tris.Add(vCount + 0); tris.Add(vCount + 2); tris.Add(vCount + 1);
tris.Add(vCount + 0); tris.Add(vCount + 3); tris.Add(vCount + 2);

그래서 다시 모든 면을 face배열 기준으로 다시 뒤집은 순서로 넣고 아까의 

Physics.queriesHitBackfaces를 false로 수정

 

using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer), typeof(MeshCollider))]
public class VoxelChunk : MonoBehaviour
    // 맵을 한번에 만들기에는 너무 크기 때문에 청크로 쪼개서 관리
{
    public const int SizeX = 16;
    public const int SizeZ = 16;
    public const int SizeY = 64;

    // 아틀라스 설정 가로 12칸 세로 1칸, 
    private const int ATLAS_TILES_X = 12; // 가로 텍스쳐 수
    private const int ATLAS_TILES_Y = 1; 

    // UV_EPS는 bleeding 방지
    // UV 경계에서 살짝 안쪽으로 당겨서 경계 밖으로 나가지 않게 하여
    // 타일 경게에서 옆 타일이 비쳐보이는것을 방지한다
    private const float UV_EPS = 0.0015f;

    public Vector2Int ChunkCoord { get; private set; }
    // 청크 좌표, 해당 청크가 월드에서 몇 번째인지, 청크는 X,Y 평면이라 2D로

    private BlockType[,,] blocks;
    // 청크 내부 블록 데이터 저장 X,Y,Z

    private MeshFilter mf; // 메시 데이터
    private MeshRenderer mr; // 메시 렌더링
    private MeshCollider mc; // 메시 충돌 구현

    private VoxelWorld world; // 현재 해당 청크가 속한 곳

    public PhysicsMaterial slipperyMat; // 미끄러운 느낌 적용



    public void Initialize(VoxelWorld world, Vector2Int coord, Material mat)
    {
        // 청크 생성, 초기화
        this.world = world;
        ChunkCoord = coord;
        // 현재 청크가 어디에 속하는지 , 청크 좌표

        mf = GetComponent<MeshFilter>();
        mr = GetComponent<MeshRenderer>();
        mc = GetComponent<MeshCollider>();

        mr.sharedMaterial = mat; // 만들어진 아틀라스 텍스쳐를 미리 넣어둔 셰이더 머테리얼을
                                 // 청크 렌더러에 연결

        mc.convex = false; // 맵을 볼록한 형태로
        mc.isTrigger = false;

        mc.cookingOptions =
            MeshColliderCookingOptions.CookForFasterSimulation;
            //MeshColliderCookingOptions.EnableMeshCleaning |
            //MeshColliderCookingOptions.WeldColocatedVertices; 
        // 충돌, 시뮬레이션을 빠르게하고
        // 메시 정리를 통해 폴리곤 삼각형 수를 줄여줌
        // 겹치는 정점을 합쳐 안정성을 줌

        blocks = new BlockType[SizeX, SizeY, SizeZ];
        // 청크가 저장할 블록 데이터 공간
    }

    public void SetBlock(int x, int y, int z, BlockType type) => blocks[x, y, z] = type;
    // 청크 내부 좌표에 블록 타입을 기록

    public BlockType GetBlockLocal(int x, int y, int z)
    {
        if (x < 0 || x >= SizeX || y < 0 || y >= SizeY || z < 0 || z >= SizeZ)
            return BlockType.Air;
        return blocks[x, y, z];
        // 청크 로컬 좌표를 조회하고 범위 밖이면 air 반환
    }

    private BlockType GetBlockWithNeighbors(int x, int y, int z)
    {
        if (x >= 0 && x < SizeX && y >= 0 && y < SizeY && z >= 0 && z < SizeZ)
            return blocks[x, y, z];

        int worldX = ChunkCoord.x * SizeX + x;
        int worldZ = ChunkCoord.y * SizeZ + z;
        return world.GetBlockWorld(worldX, y, worldZ);

        // 좌표가 청크 밖으로 나가면 월드를 통해 확인하고 블록 타입을 가져옴
        // 이웃 청크에 블록이 있다면 해당 면은 그리지 않아도 됌
    }

    public void BuildMesh()
    {
        var verts = new List<Vector3>(8192); // 정점 좌표 저장
        var tris = new List<int>(16384); // 삼각형 인덱스 저장
        var cols = new List<Color32>(8192); // 버텍스 컬러,
        var uvs = new List<Vector2>(8192); // 텍스쳐 uv 좌표
        // 8192 는 초기 용량임, 미리미리 크게 잡음 재할당 방지를 위함

        int vCount = 0; // 현재 추가된 버텍스 개수

        // 텍스처 기반이면 VertexColor는 그냥 흰색 고정(곱해도 영향 없게)
        Color32 white = new Color32(255, 255, 255, 255);

        for (int x = 0; x < SizeX; x++)
            for (int y = 0; y < SizeY; y++)
                for (int z = 0; z < SizeZ; z++)
                {
                    // 청크 안의 모든 좌표 순회
                    BlockType t = blocks[x, y, z];
                    if (t == BlockType.Air) continue;
                    // 빈공간은 메시 생성 x

                    Vector3 basePos = new Vector3(x, y, z);
                    // 블록의 원점 

                    if (IsFaceVisible(x + 1, y, z)) AddFace(FaceDir.PosX, basePos, t, ref vCount, verts, tris, cols, uvs, white);
                    if (IsFaceVisible(x - 1, y, z)) AddFace(FaceDir.NegX, basePos, t, ref vCount, verts, tris, cols, uvs, white);
                    if (IsFaceVisible(x, y + 1, z)) AddFace(FaceDir.PosY, basePos, t, ref vCount, verts, tris, cols, uvs, white);
                    if (IsFaceVisible(x, y - 1, z)) AddFace(FaceDir.NegY, basePos, t, ref vCount, verts, tris, cols, uvs, white);
                    if (IsFaceVisible(x, y, z + 1)) AddFace(FaceDir.PosZ, basePos, t, ref vCount, verts, tris, cols, uvs, white);
                    if (IsFaceVisible(x, y, z - 1)) AddFace(FaceDir.NegZ, basePos, t, ref vCount, verts, tris, cols, uvs, white);
                    // 이웃 블록이 air인지 검사, air면 바깥으로 노출된 면으로 렌더링 생성함
                    // 노출되지 않는 면은 생성하지 않도록하여
                    // 렌더링 비용을 감소 시켜준다.
                }

        Mesh mesh = mf.sharedMesh;
        // 현재 메시 가져옴

        if (mesh == null)
        {
            mesh = new Mesh { name = "ChunkMesh" };
        }
        else
        {
            mesh.Clear();
        }
        // 처음 빌드하는 거면 새로운 mesh 생성
        // 리빌드면 데이터 초기화 후 재사용

        mesh.indexFormat = (verts.Count > 65000)
            ? UnityEngine.Rendering.IndexFormat.UInt32
            : UnityEngine.Rendering.IndexFormat.UInt16;
        // 메시 깨짐 방지를 위해 버텍스의 수가 많담녀 uint32로 변경

        mesh.SetVertices(verts);
        mesh.SetTriangles(tris, 0);
        mesh.SetColors(cols);
        mesh.SetUVs(0, uvs);
        // 지금까지 만든 리스트들을 mesh에 넣어줌
        // uv channel 0에 uvs 적용

        mesh.RecalculateNormals(); // 조명 계산 
        mesh.RecalculateBounds(); // 컬링

        mc.sharedMesh = null;
        mf.sharedMesh = mesh; // 새 메시를 적용하여 화면 렌더링 준비

        if (mc != null)
        {
            // if (slipperyMat != null) mc.material = slipperyMat;
            mc.material = null;
            mc.sharedMesh = null;
            mc.sharedMesh = mesh;
        }
        // mc가 있으면 콜라이더에도 같은 메시를 넣어줌
        // 콜라이더 메시가 바뀌었다는 것을 인식시켜줌

        Physics.SyncTransforms();
        // 변경된 사항을 물리 엔진에 즉시 동기화, 물리가 한프레임 늦게 반영될 수 있어서
        // 현재 오류때문에 추가했는데,, 해결이안됌 다른 문제인거같음
    }

    private bool IsFaceVisible(int nx, int ny, int nz)
        => GetBlockWithNeighbors(nx, ny, nz) == BlockType.Air;
    // 이웃이 air면 면을 보여줌, 청크 경계도 확인

    private enum FaceDir { PosX, NegX, PosY, NegY, PosZ, NegZ }
    // 면 방향을 명확하게 표현,면별로 어떤 텍스쳐를 사용할지
   
    private static readonly Vector3[] facePosX = { new(1, 0, 0), new(1, 0, 1), new(1, 1, 1), new(1, 1, 0) };
    private static readonly Vector3[] faceNegX = { new(0, 0, 1), new(0, 0, 0), new(0, 1, 0), new(0, 1, 1) };
    private static readonly Vector3[] facePosY = { new(0, 1, 0), new(1, 1, 0), new(1, 1, 1), new(0, 1, 1) };
    private static readonly Vector3[] faceNegY = { new(0, 0, 1), new(1, 0, 1), new(1, 0, 0), new(0, 0, 0) };
    private static readonly Vector3[] facePosZ = { new(1, 0, 1), new(0, 0, 1), new(0, 1, 1), new(1, 1, 1) };
    private static readonly Vector3[] faceNegZ = { new(0, 0, 0), new(1, 0, 0), new(1, 1, 0), new(0, 1, 0) };
    // 면은 사각형으로 정점 4개를 통해서 그려짐
    // 방향마다 정점 좌표가 다르기 때문에 미리 테이블로 만들어줌

    private static void AddFace(
     FaceDir dir, Vector3 basePos, BlockType type,
     ref int vCount,
     List<Vector3> verts, List<int> tris, List<Color32> cols, List<Vector2> uvs,
     Color32 vertexColor)
    {
        // 방향에 맞는 정점 템플릿 
        Vector3[] f = dir switch
        {
            FaceDir.PosX => facePosX,
            FaceDir.NegX => faceNegX,
            FaceDir.PosY => facePosY,
            FaceDir.NegY => faceNegY,
            FaceDir.PosZ => facePosZ,
            _ => faceNegZ,
        };
        // 정점 4개 추가
        verts.Add(basePos + f[0]);
        verts.Add(basePos + f[1]);
        verts.Add(basePos + f[2]);
        verts.Add(basePos + f[3]);
        // 컬러 4개
        cols.Add(vertexColor);
        cols.Add(vertexColor);
        cols.Add(vertexColor);
        cols.Add(vertexColor);

        // 아틀라스 UV 계산
        int tileX = GetTileIndex(type, dir);
        float tw = 1f / ATLAS_TILES_X;
        float th = 1f / ATLAS_TILES_Y;

        float u0 = tileX * tw;
        float v0 = 0f;
        float u1 = u0 + tw;
        float v1 = v0 + th;

        u0 += UV_EPS; u1 -= UV_EPS;
        v0 += UV_EPS; v1 -= UV_EPS;

        uvs.Add(new Vector2(u0, v0));
        uvs.Add(new Vector2(u1, v0));
        uvs.Add(new Vector2(u1, v1));
        uvs.Add(new Vector2(u0, v1));

        // 현재 face 배열 기준으로는 모든 면을 해당 순서로 수정, 그래야 바깥 방향의 법선이 맞게 나옴
        tris.Add(vCount + 0); tris.Add(vCount + 2); tris.Add(vCount + 1);
        tris.Add(vCount + 0); tris.Add(vCount + 3); tris.Add(vCount + 2);

        // 정점 4개 추가 카운트 증가
        vCount += 4;
    }

   
    // 면 방향에 따라 아틀라스 선택 및 블록 타입에 맞게 설정
    // 0 Dirt, 1 GrassSide, 2 Stone, 3 Wood, 4 Planks, 5 Coal, 6 Iron, 7 Gold, 8 Diamond, 9 CraftingTable, 10 GrassTop, 11 Water
    private static int GetTileIndex(BlockType t, FaceDir dir)
        // 아틀라스 몇 번째 텍스쳐 사용할건지 블록 타입에 따라 리턴
    {
        switch (t)
        {
            case BlockType.Dirt:
                return 0;

            case BlockType.Grass:
                // 윗면만 GrassTop, 아랫면은 Dirt, 옆면은 GrassSide
                if (dir == FaceDir.PosY) return 10; // 위
                if (dir == FaceDir.NegY) return 0; // 아래
                return 1; // 옆
               

            case BlockType.Stone:
                return 2;

            case BlockType.Wood:
                return 3;

            case BlockType.Planks:
                return 4;

            case BlockType.Coal:
                return 5;

            case BlockType.Iron:
                return 6;

            case BlockType.Gold:
                return 7;

            case BlockType.Diamond:
                return 8;

            case BlockType.CraftingTable:
                return 9;

            case BlockType.Water:
                return 11;
            default:
                return 2; // 모르면 Stone
        }
    }
}

그래서 수정된 VoxelWorld.cs의 전체 코드이다

 

다음에도 이런 오류가 생긴다면

정점순서, 삼각형 인덱스 순서, 법선의 방향, backface 설정, colleremesh 이런것들을 확인한 후에

캐릭터 무브에서의 오류를 찾는게 맞는것 같다

현재까지의 플레이 영상은 파일로 올렸다. 티스토리는 이제 동영상 지원을 안해줘서,, ㅠㅠ

0413Play.mp4
10.23MB

 

원래는 몬스터 생성까지 하고자 하였으나 맵 오류를 고치는 부분에 상당한 시간을 쓰게되어 몬스터는 다음 글에서 하겠습니다.

728x90

'Unity' 카테고리의 다른 글

Unity(10) - Take Damage Event & UI  (0) 2026.04.28
Unity(9) - Random Spawn Monster & A*  (0) 2026.04.27
Unity(7) - 3x3 Crafting Table  (0) 2026.03.13
Unity(6) - Atlas Block Map & Crafting Table (2)  (0) 2026.03.10
Unity(5) - Inventory & Crafting  (1) 2026.03.05