Posts 유니티 - 전장의 안개(Fog of War)
Post
Cancel

유니티 - 전장의 안개(Fog of War)

목차



개념


  • 맵과 크기가 동일한 RGBA(0, 0, 0, a)의 텍스쳐를 이용해 지정한 유닛들의 주변시야를 표현한다.
  • 지정한 유닛이 현재 위치한 원형 범위 내 영역들은 a = 0,
  • 지정한 유닛이 한 번이라도 위치했던 영역들은 a = 0.5~0.8,
  • 지정한 유닛이 한 번도 방문하지 않은 영역은 a = 1로 표현한다.

[스타크래프트2 게임 플레이 화면]


구현 방법


[1] 카메라와 지상 사이에 검정색 Plane 사용

  • 시야의 역할을 해줄 Plane을 카메라와 지상 사이에 위치시킨다.

  • 맵 전체를 좌표 형태의 2차원 배열로 관리하여, 유닛들이 현재 위치한 영역, 방문했던 영역, 한 번도 방문하지 않은 영역의 정보를 실시간으로 저장한다.

  • 카메라와 해당 유닛들 사이에서 Plane 위의 시야 중심 좌표를 구하고, 로컬 좌표로 변환하여 해당 좌표를 기준으로 원형 범위 내의 정점 색상들을 변경시킨다.

  • Plane 위의 시야 중심 좌표를 구하는 방법 : 레이캐스트 또는 비례식 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
* 비례식 이용하여 좌표 구하기

<정의>
- 카메라의 좌표 : (cX, cY, cZ)
- 유닛의 좌표 : (uX, uY, uZ)
- 구해야 할 plane 위의 정점 좌표 : (pX, pY, pZ)

pX - cX : uX - cX = cY - pY : cY 이고,

cY(pX - cX) = (uX - cX)(cY - pY),

pX - cX = (uX - cX)(cY - pY)/cY,

pX = (uX - cX)(cY - pY)/cY + cX 이다.

pX, pZ를 제외한 모든 값을 사전에 알고 있으며

동일한 방법으로 pZ도 구할 수 있다.


[2] 타일맵 기반 구현


타일맵을 이용한 구현


타일맵

  • 정사각형 타일 하나의 가로, 세로 너비와 Plane의 가로, 세로 너비를 결정한다.

    (예 : 타일 0.5x0.5, Plane 20x20 -> 타일 개수 : (20/0.5 * 20/0.5) = 1600개 )

  • 게임 시작 시 각각의 타일마다 지형의 높이(position.y)를 계산해 2차원 배열로 저장한다.

    (-Y 방향 레이캐스트 이용, 배열의 크기는 타일의 가로 개수x세로 개수)


유닛

  • 시야를 밝힐 대상 유닛들은 리스트를 통해 실시간으로 관리된다.


Visit 배열

  • 배열의 크기는 타일의 개수와 같다.

  • Visit.current 배열은 현재 유닛들의 시야가 유지되는 타일들에 대해 true 값을 가지며,

    매 주기마다 시야 계산을 하기 전에 전체를 false로 초기화한다.

  • Visit.ever 배열은 한 번이라도 시야가 확보됐던 타일들에 대해 true값을 가지며,

    한 번 true가 된 타일은 항상 그 값을 true로 유지한다.

  • 주기적인 시야 계산을 통해 Visit 배열을 갱신한다.


시야 계산

  • 주기적으로(0.2~0.5초) 각 유닛들이 위치한 타일 기준으로 주변의 시야를 계산한다.

    • [1] 유닛의 시야 만큼의 원형 범위 내 모든 타일들을 가져와 검사한다.
    • [2] 현재 유닛 타일보다 높은 곳에 위치한 타일들은 배제한다.
    • [3] 장애물 검사 알고리즘을 통해, 유닛과 해당 타일 사이에 장애물이 위치하는 경우 해당 타일을 배제한다.
    • [4] 결과로 얻은 타일들에 대해 Visit.current 및 Visit.ever 배열의 해당 값들을 true로 초기화한다.


장애물 검사 알고리즘

  • 시야 계산을 위해, 목표 지점의 타일과 유닛이 위치한 타일 사이에

    유닛보다 더 높은 위치에 있는 타일(장애물)이 존재하는지 검사하는 알고리즘


1. 내적을 이용한 알고리즘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<정의>
 - U : 유닛이 위치한 타일
 - C : 현재 검사 중인 타일
 - O : 장애물 타일(U보다 더 높은 위치에 있는 타일)
 - AB : XY평면에서 A에서 B를 향하는 직선
 - dist(AB) : XY평면에서 A와 B 사이의 직선거리
 - nDot(AB, AC) : XY 평면에서 AB, AC를 각각 정규화한 값을 서로 내적한 값
 - T : 시야를 밝힐 수 있는 타일
 - F : 시야를 밝힐 수 없는 타일

 [1] U 기준으로 원형 범위 내에 있는 모든 타일들을 리스트(InRangeList)로 가져온다.

 [2] 그 중, O에 해당하는 타일들은
    InRangeList에 넣지 않고 장애물 리스트(ObstacleList)에 넣는다.

 [2-1] ObstacleList의 크기가 0인 경우,
       InRangeList 내 모든 타일들을 T로 설정하고 [5]로 이동한다.

 [3] ObstacleList를 유닛과 장애물 타일 사이의 XY평면상 직선거리로 오름차순 정렬한다.

 [4] InRangeList 내 모든 타일들에 대해 각각 순회하며 검사한다.

   [4-1] UC가 UO의 최솟값보다 작은 경우 C <- T

   [4-2] dist(UC) > dist(UO)이면서 nDot(UC, UO) > 0.9인 O가 존재하지 않는 경우 C <- T

   [4-3] [4-1], [4-2]에 모두 해당하지 않는 경우 C <- F

 [5] 모든 T에 대해 시야를 밝힌다.


2. 타일 전진 알고리즘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<정의>
 - U : 유닛이 위치한 타일
 - C : 현재 검사 중인 목표 타일
 - O : 장애물 타일(유닛보다 더 높은 위치에 있는 타일)
 - G :U와 C를 지나는 직선의 기울기
 - aG : G의 절댓값
 - T : 시야를 밝힐 수 있는 타일
 - F : 시야를 밝힐 수 없는 타일

U와 C의 관계를 4가지로 분류하여 진행한다.

[1] 동일한 X좌표를 가진 경우
 - U에서 C까지 Y좌표를 한 칸씩 전진시킨다.
 - C에 도달할 때까지 O를 하나라도 발견하면 C <- F
 - O를 발견하지 못하면 C <- T

[2] 동일한 Y좌표를 가진 경우
 - U에서 C까지 X좌표를 한 칸씩 전진시키며, [1]과 같다.

[3] aG >= 1인 경우
 - U로부터 X축으로 전진한 거리의 절댓값을 aX,
           Y축으로 전진한 거리의 절댓값을 aY라고 정의한다.
 - C에 도달할 때까지 aX / (aY+1) < aG일 때 X축으로 1칸 전진하며,
   aX / (aY+1) >= aG일 때는 Y축으로 1칸 전진한다.
 - O를 하나라도 발견한 경우 C <- F, 발견하지 못한 경우 C <- T

[4] aG < 1인 경우
 - aX, aY의 정의는 [3]과 같다.
 - C에 도달할 때까지 aY / (aX+1) < aG일 때 Y축으로 1칸 전진하며,
   aY / (aX+1) >= aG일 때는 X축으로 1칸 전진한다.
 - O를 하나라도 발견한 경우 C <- F, 발견하지 못한 경우 C <- T

결과로 얻은 모든 T에 대해 시야를 밝힌다.
  • 그림으로 표현하면 다음과 같다.

    (초록색 : 유닛 위치, 파란색 : 시야 탐색 대상, 갈색 : 장애물)

  • 장애물을 발견하지 못한 경우(C <- T) :

  • 장애물을 발견한 경우(C <- F) :

  • 그림 내에 보이는 모든 타일에 대한 T/F 판정 :

    (T : 초록색, F : 빨간색)


Fog 텍스쳐

  • 텍스쳐의 색상 저장을 위한 Color 배열이 필요하며, 크기는 타일의 개수와 같다.

  • Fog 텍스쳐의 넓이는 타일의 가로 개수 * 세로 개수와 같다.

  • 시야 계산이 끝날 때마다 Visit.current, Visit.ever 값에 따라 Color 배열의 알파값을 지정한다. (current -> 0, ever -> 0.5 ~ 0.8, 미방문 타일 1)

  • Color 배열을 텍스쳐에 적용한다.


Fog 쉐이더

  • Fog 쉐이더는 ZTest Off로 설정하여 항상 다른 오브젝트들 위에 보이게 한다.


블러, 보간 효과

  • 시야 계산이 끝날 때마다 쉐이더를 통해 가우시안 블러를 적용한다.

  • 부자연스러운 픽셀이 나타나는 것을 방지하기 위해 렌더 텍스쳐를 여러 장 거쳐 가우시안 블러를 적용한다.

  • 매 프레임마다 이전 프레임의 시야 텍스쳐를 현재 프레임의 텍스쳐에 부드럽게 보간하여 적용한다.


구현 결과


[1] Gizmo를 통해 유닛의 가시 영역 확인

[2] 실제 게임 뷰에서 시야 변화 확인


프로파일링, 최적화


Job + Burst Compiler 적용

  • 장애물 검사 알고리즘은 타일 전진 알고리즘을 사용했으며, 프로파일링 결과 주요 병목이라고 생각하여 Job System을 적용해보았다.

테스트 1

  • Fog Width : 40x40
  • Tile Size : 1
  • Update Cycle : 0.2
  • Number of Units : 105
  • 상단 : 미적용 / 하단 : 적용

테스트 2

  • Fog Width : 40x40
  • Tile Size : 0.5
  • Update Cycle : 0.5
  • Number of Units : 105
  • 상단 : 미적용 / 하단 : 적용

잡 시스템 적용 결과

  • 별 차이가 없다. 그래서 프로파일링을 구체화해서 다시 확인해보았다.


병목 재확인 및 최적화

image

  • 예상 외의 결과였다.
  • ComputeFog_1 부분은 시야 범위 타일 얻어오기, 2는 위에서 잡으로 돌린 장애물 계산, 3이 시야 정보를 업데이트하고 Blit하는 부분이다.

  • 그래서 GPU 병목이겠거니 하고 더 세분화 했는데,

image

  • 저부분은..

image

  • 단순히 방문 정보를 이용해 컬러 배열의 알파값을 초기화하는 부분이었다.

  • 그래서 방문 정보를 저장하는 방식을 바꿨다.
  • 기존에는
1
2
3
4
5
6
7
8
9
10
public struct Visit
{
    /// <summary> 현재 위치함 </summary>
    public bool current;

    /// <summary> 과거에 방문한 적 있음 </summary>
    public bool ever;
}

private Visit[] visit;
  • 두 개의 bool 변수를 이용해 방문 여부, 과거 방문, 미방문으로 구분했지만
  • 변경 이후
1
private float[] visit;
  • 이렇게 단순히 float 배열을 통해 알파값을 직접 배열에 넣고,

  • 컬러 버퍼에는 그 알파값을 직접 넣도록 변경했다.
  • 그 결과,

  • 이 부분의 병목을 많이 줄일 수 있었다.


가비지 최적화

  • ComputeFog(), GetVisibleTilesInRange() 메소드 내에서 List를 지역적으로 할당하여 사용하는 부분이 세 군데 있었다.

  • 예를 들어,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<FowTile> tilesInSight = new List<FowTile>();
for (int i = -sightRangeInt; i <= sightRangeInt; i++)
{
    for (int j = -sightRangeInt; j <= sightRangeInt; j++)
    {
        if (i * i + j * j <= rangeSquare)
        {
            var tile = GetTile(pos.x + i, pos.y + j);
            if (tile != null)
            {
                tilesInSight.Add(tile);
            }
        }
    }
}
  • 이런 부분들에서 사용하는 List를 모두 필드로 옮겨주고
1
2
tilesInSight.Clear();
// ... Same Codes
  • 이렇게 Clear()하여 재사용하는 방식으로 바꾸어 주었더니

  • GC 호출을 크게 줄일 수 있었다.


  • 프로파일링 결과 :

image

  • Fog Width : 40x40
  • Tile Size : 1
  • Update Cycle : 0.2
  • Number of Units : 105

image


Reference



Source Code



Download


This post is licensed under CC BY 4.0 by the author.