라이트매핑은 조명효과를 텍스처로 미리 만드는 과정을 말한다. 이 텍스처를 라이트맵이라고 하고 이과정을 Bake한다고 한다. 라이트맵은 실시간 렌더링화면과 오버레이돼 믹싱된다.

 

 Generate Lightmap UVs 옵션

외부에서 임포트된 모델은 Models  탭에서 Generate Lightmap UVs속성을 체크해야 라이트매핑 효과가 적용된다.

05.Models/Stair폴더의 Stair모델을 선택하고 인스펙터뷰에서 Model탭을 누르고 아래 Generate Lightmap UVs를 체크하고 Apply한다.

 

 

Contribute GI 플러그

라이트매핑을 하려면 라이트매퍼에 그 대상을 알려줘야 한다. 따라서 Barrel, Floor, Wall, Stair를 선택하고 인스펙터뷰의 Static 플래그 중 Contribute GI를 체크해야한다. 프로젝트뷰 프리팹폴더에서 처리한다.

Barrel은 동적장애물 처리를 했기때문에 Contrubute GI만 선택하고 나머지 Floor, Wall,Stair등은 Navigation Static도 선택한다. Contribute GI를 선택하지 않으면 라이트매핑 대상에서 제외된다.

라이팅뷰

메뉴에서 Window>Rendering>Lighting을 선택해 라이팅 뷰를 열고 Scene탭을 클릭한다. 라이팅뷰는 4개의 영역으로 분리돼 있으며 라이팅매핑을 위한 다양한 속성을 제공한다. 먼저 Scene탭의 속성은 다음과 같다.

한번도 라이트맵을 베이킹하지 않았다면 모든 섹션이 비활성화 되어 있습니다 .[New Lighting Settings]버튼을 클릭해 새로운 라이팅 세팅을 생성한다. 생성한 라이팅 섹션의 이름을 씬이름과 동일하게 Play라고 지정한다.

아래 Auto Generate속성을 체크하면 라이트맵을 백그라운드로 베이크한다. 씬뷰에 새로운 모델 또는 조명을 변경하면 라이트매퍼가 다시 베이크작업을 시작한다. 이때 오른쪽 아래 있는 상태바에 베이크 과정이 표시된다. 라이트맵은 캐시에 저장되는데 캐시는 실행파일에 포함되지 않으므로 실행파일과 함께 배포하려면 반드시 Auto Generate 옵션을 끄고 수동으로 베이크한다.

Progressive 라이트매퍼

Progressive 라이트매퍼는 경로추적기반의 라이트매퍼로서 라이트맵과 라이트프로브를 점진적으로 베이크하는 기능을 제공한다. 또한,  Progressive GPU라이트매퍼는 GPU의 그래픽 가속 기능을 이용해 좀 더 빠르게 라이트 매핑을 할 수 있다. 기존 Enlighten 라이트매퍼는 베이크가 완료전까지는 결과물을 볼수 없으나 Progressive 라이트 매퍼는 대략적인 결과물을 바로 보여준다. 

 

라이팅뷰의 Environment

라이트맵을 수동으로 베이크해보자. 씬뷰에 있는 Directional Light는 Realtime조명으로 씬을 전체적으로 비췄던 조명이다. 라이트맵의 결과물을 확인하기 쉽게 이걸 잠시 비활성화한다.

조명을 비활성화해도 희미하게 보이는데 Skybox의 하늘빛이 환경 광원으로 작용하고 있다. 환경광원은 Ambient Light라고도 한다. 바닥에도 반사광이 표시된다. 

라이트뷰를 열어 Environment탭을 선택해 보면 Source가 Skybox로 되어 있어서 그렇고  Intensity Multiplier를 2로 변경하면 좀더 밝아진다.

어둡게 하기위해 Soruce를 color로 바꾸고 검정색을 선택한다.

Environment Reflection속성은 3D 모델 표면에 반사 광원의 효과를 설정하는 옵션이다. 기본값인 Skybox의 빛을 Floor에 비추고 있다. Source를 Custom으로 변경하거나 반사율을 나타내는 Intensity Multiplier속성을 0으로 설정하면 바닥이 어둡게 된다.

Baked 라이트매핑

Baked 라이트매핑을 위해 먼저 라이팅뷰의 [Scene]탭으로 이동한 다음 Realtime Lighting 섹션에서 Realtime Global Illumination 속성을 언체크한다. 이 기능은 전역조명을 실시간 계산해서 반영하는것으로 게임용 PC에서 적합하다.

Mixed Lighting - Baked Global Illumination을 체크하고 Lighting Mode는 Subtractive로 설정해 라이트맵과 라이트프로브, 그림자를 모두 하나의 라이트맵에 베이크한다.

조명을 여러개 배치하기 위해 하이라키뷰의 _STAGE를 선택하고  Point Light를 차일드로 추가한다. Point Light속성은 intensity는 10으로 다음 표과 같이 설정한다. 조명의높이는 주인공의 키 높이 정도로 Y:2정도로 올려서 스테이지에 배치한다.  10개정도  카피해서 배치한후. 색은 R G B 원색으로 각각 바꿔준다.

AutoGenerate 기능이 켜져있기 대문에 조명을 추가하면 바로바로 라이트맵을 베이크하기 시작하고 시간이 지나면서 품질이 좋와진다. 대신 AutoGenerate 기능이 켜져있어 조명을 추가할때마다 엄청 부하가 먹는다 

 

라이트맵 베이크

라이트맵을 캐시가 아닌 텍스처파일로 베이크하려면 백그라운드기능을 꺼야한다 .라이트뷰 Auto Generate 옵션을 언체크한다.

Generate Lighting을 클릭하면 열심히 렌더링한다 제대로 조명이 안나온다면 오브젝트의 Static에 GI가 체크되어 있는지 확인해본다

Lighting 뷰의 [Baked Lightmaps]탭을 누르면 라이트맵을 확인할 수 있다. 포토샵에서 편집이 가능하다.

L

Area Light

_Stage 오브젝트의 자식으로 Area Light를 하나 만들고 벽쪽으로 배치하고 다음과 같이 길게 만들어 본다. Area Light는 유일하게 실시간 라이트가 아니다. 베이크후 효과를 볼 수 있다.

 

Realtime Lighting

이 섹션에는 인라이튼 실시간 전역 조명 시스템과 관련된 설정이 포함되어 있습니다.

프로퍼티기능

Realtime Global Illumination 이 체크박스를 선택하면 Unity는 이 조명 설정 에셋을 사용하는 씬에서 인라이튼 실시간 전역 조명 시스템을 활성화합니다.
Realtime Environment Lighting 인라이튼 실시간 전역 조명 시스템을 사용하여 주변광을 실시간으로 계산하고 업데이트하려면 이 프로퍼티를 활성화합니다.

이 프로퍼티는 씬에서 인라이튼 실시간 전역 조명  베이크된 전역 조명이 모두 활성화된 경우에만 사용할 수 있습니다.

 

Mixed Lighting

이 섹션에는 조명 설정 에셋을 사용하는 씬 내 베이크된 광원  혼합 광원의 동작에 영향을 주는 설정이 나와 있습니다.

프로퍼티:기능:

Baked Global Illumination 이 설정을 활성화하면 Unity는 이 조명 설정 에셋을 사용하는 씬에 대해 베이크된 전역 조명 시스템을 활성화합니다. 이 설정을 비활성화하면 Unity는 이 조명 설정 에셋을 사용하는 씬에 대해 베이크된 전역 조명 시스템을 비활성화합니다.

베이크된 전역 조명 시스템을 활성화하면 Unity는 라이트매핑의 경우에만 씬에서 베이크된 광원을 사용하고, 혼합 광원은 Lighting Mode 설정에 따라 동작합니다. 베이크된 전역 조명 시스템을 비활성화하면 Unity는 씬의 모든 베이크된 광원과 혼합 광원이 실시간 광원처럼 동작하도록 강제로 지정합니다.
Lighting Mode Unity가 이 조명 설정 에셋을 사용하는 씬 내 모든 혼합 광원에 사용할 조명 모드를 지정합니다.

조명 모드를 변경하면 이 조명 설정 에셋을 사용하는 씬의 조명 데이터를 다시 베이크해야 합니다.
  Baked Indirect 이 조명 설정 에셋을 사용하는 씬 내 모든 혼합 광원에 Baked Indirect 조명 모드를 사용합니다.
  Subtractive 이 조명 설정 애셋을 사용하는 씬의 모든 혼합 조명에 대해 서브트랙티브 조명 모드를 사용합니다.

  Shadowmask 이 조명 설정 애셋을 사용하는 씬의 모든 혼합 조명에 대해 섀도우 마스크 조명 모드를 사용합니다.

 

Lightmapping Settings

프로퍼티기능

Lightmap Resolution 라이트맵에 사용할 단위당 텍셀 수를 지정합니다. 이 값을 높게 설정하면 라이트맵 품질이 개선되지만 베이크 시간도 늘어납니다. 이 값을 두 배로 늘리면 라이트맵의 높이와 너비가 모두 2배가 되기 때문에 텍셀 수는 4배로 증가합니다. 조명 창의 통계 영역에서 Occupied texels 수를 확인할 수 있습니다.
Lightmap Padding 베이크된 라이트맵의 개별 셰이프 간 간격을 텍셀 단위로 결정합니다. 기본값은 2입니다.
Max Lightmap Size 개별로 포함된 게임 오브젝트 영역을 통합하는 전체 라이트맵 텍스처의 크기(픽셀 단위)를 지정합니다. 기본값은 1024입니다.
Lightmap compression 에디터가 라이트맵에 사용하는 압축 수준입니다.
- None: 라이트맵을 압축하지 않습니다.
- Low Quality: 보통 품질보다 메모리와 저장 공간을 덜 사용하지만 시각적 결함이 발생할 수도 있습니다.
- Normal Quality: 메모리 사용량과 화질 사이의 균형을 유지합니다.
- High Quality: 보통 품질보다 더 많은 메모리와 저장 공간이 필요하지만 더 나은 시각적 결과를 제공합니다.
Ambient Occlusion 베이크된 앰비언트 오클루전에서 상대 표면 밝기를 제어할 수 있습니다. 이것은 조명을 베이크하는데 사용하는 라이트매퍼에 의해 계산된 간접 조명에만 적용됩니다. 기본적으로 활성화되어 있습니다. 앰비언트 오클루전이 활성화되면 최대 거리, 간접 조정, 직접 조정의 세 가지 설정이 노출됩니다. 세 가지 설정 모두 값이 높을수록 오클루전된 영역과 조명을 완전히 받은 영역 간의 콘트라스트가 더욱 증가합니다.
Max Distance 오브젝트에 오클루전을 적용할지 결정하기 위해 조명 시스템에서 광선을 투사하는 거리를 지정합니다. 값이 높을수록 더 긴 광선이 생성되고 라이트맵에 그림자가 더 많이 추가되는 한편, 값이 낮을수록 오브젝트가 서로 매우 가까이에 있는 경우에만 그림자를 추가하는 더 짧은 광선이 생성됩니다. 값을 0으로 설정하면 최대 거리가 없는 무한하게 긴 광선이 투사됩니다. 기본값은 1입니다.
Indirect Contribution 최종 라이트맵의 오브젝트에서 바운스되고 방출되는 간접 주변광의 밝기를 스케일합니다. 0과 10 사이의 값으로 기본값은 1입니다. 값이 1보다 작으면 강도가 감소하고 값이 1보다 크면 증가합니다.
Direct Contribution 직접광의 밝기를 스케일합니다. 0과 10사이의 값으로 기본값은 0입니다. 값이 높을수록 에디터가 직접 조명에 적용하는 콘트라스트가 증가합니다.
Directional Mode 오브젝트 표면의 각 포인트에서 가장 우세하게 비추는 광원의 특성에 관한 정보를 저장할 수 있는 라이트맵을 활성화합니다. 자세한 내용은 방향성 라이트매핑을 참조하십시오. 기본 모드는 Directional입니다.
Directional Directional 모드에서 Unity는 광원에서 비추는 우세한 방향을 저장하는 두 번째 라이트맵을 생성합니다. 이를 통해 디퓨즈 노멀 매핑된 머티리얼을 전역 조명 시스템과 연동할 수 있습니다. 셰이더는 렌더링하는 동안 두 개의 라이트맵 텍스처를 샘플링합니다. 결과적으로 방향성 모드는 추가 라이트맵 데이터를 위해 비방향성 모드보다 약 2배의 비디오 메모리를 필요로 합니다. 추가 방향성 텍스처를 생성하면 베이킹 성능에 영향을 줍니다. 방향 라이트맵은 SM2.0 하드웨어에서 또는 GLES2.0을 사용할 때 디코드할 수 없습니다.
Non-directional Non-directional 모드 라이트맵에는하나의 텍스처만 포함됩니다. 결과적으로 방향 라이트맵보다 비디오 메모리와 저장 공간이 적게 필요하며 셰이더에서 더 빠르게 디코드할 수 있습니다. 이러한 최적화는 화질을 저하합니다.
Indirect Intensity 실시간 및 베이크된 라이트맵에 저장되는 간접광의 밝기를 결정합니다. 0과 5 사이의 값입니다. 값을 1보다 크게 설정하면 간접광의 강도가 증가하고, 값을 1보다 작게 설정하면 간접광의 강도가 감소합니다. 기본값은 1입니다.
Albedo Boost Unity가 표면 사이에서 바운스하는 광원의 양을 지정합니다. 이 값은 1에서 10 사이입니다. 이 값을 높게 설정할수록 간접광 계산을 위해 알베도 값을 흰색으로 끌어옵니다. 기본값은 1이며 물리적으로 정확합니다.
Lightmap Parameters 라이트맵 파라미터 에셋은 베이크된 전역 조명과 관련된 설정 값을 저장합니다. 에디터는 선택할 수 있는 몇 가지 기본 라이트맵 파라미터 에셋을 제공하지만 Create New 옵션을 사용하여 자체 라이트맵 파라미터 파일을 만들 수도 있습니다. 자세한 내용은 라이트맵 파라미터를 참조하십시오. 기본값은 Default-Medium입니다. 옵션은 Default-Medium, Default-HighResolution, Default-LowResolution 및 Default-VeryLowResolution입니다.

'유니티게임강좌 > 라이트매핑' 카테고리의 다른 글

[라이트매핑] 라이트 프로브  (2) 2023.03.20
[라이트매핑] 광원 타입  (0) 2023.03.19

광원 타입

이 페이지는 Type 프로퍼티가 Light 컴포넌트에 미치는 영향에 대해 설명합니다.

Type 프로퍼티를 사용하여 조명의 동작 방식을 선택할 수 있습니다. 사용할 수 있는 값은 다음과 같습니다.

  • 점 광원, 씬의 한 점에 위치하여 모든 방향으로 균등하게 빛을 발산하는 광원
  • 스폿 광원, 씬의 한 점에 위치하여 원뿔 모양으로 빛을 발산하는 광원
  • 방향 광원, 무한히 멀리 위치하여 한 방향으로만 빛을 발산하는 광원
  • 면 광원, 씬에서 사각형 또는 디스크로 정의하며 표면 영역 전체에 걸쳐 균등하게 모든 방향으로 빛을 방출하지만 사각형 또는 디스크의 한쪽 면에서만 빛을 방출하는 광원

점 광원

점 광원은 공간의 한 점에 있고, 광원을 모든 방향으로 균등하게 내보냅니다. 표면에 닿는 광원의 방향은 광원이 닿은 지점에서 라이트 오브젝트의 중심으로 다시 이어지는 선입니다. 광도는 광원에서 거리가 멀어질수록 약해지고, 지정된 거리에서 0이 됩니다. 이를 ‘역제곱 법칙’이라고 하며, 광원이 실제 세계에서 작용하는 방법과 유사합니다.

점 광원은 씬에 있는 램프와 기타 로컬 광원을 시뮬레이션하는 데 유용합니다. 스파크 또는 폭발에서 발생하는 광원이 주변을 실감나게 비추게 하는 데도 사용할 수 있습니다.

씬의 점 광원 효과

스폿 광원

스폿 광원은 점 광원과 마찬가지로 광원이 떨어지는 지정된 위치와 범위가 있습니다. 하지만 스폿 광원은 특정 각도로 제약되므로 조명이 비추는 원뿔 모양의 영역이 발생합니다. 원뿔의 중앙은 라이트 오브젝트의 전방(Z) 방향을 가리킵니다. 또한 광원은 스폿 광원 원뿔의 가장자리로 갈수록 약해집니다. 각도를 넓히면 원뿔의 너비가 증가하고 ‘페넘브라’라고 하는 이 페이드(광원이 약해지는 영역)의 크기도 함께 커집니다.

스폿 광원은 일반적으로 플래시라이트, 자동차 헤드라이트 및 서치라이트 같은 인공 광원으로 사용됩니다. 스크립트나 애니메이션에서 방향을 제어하는 움직이는 스폿 광원은 씬의 작은 영역에만 광원을 비추고 극적인 조명 효과를 만들어 냅니다.

씬의 스폿 광원 효과

방향 광원

방향 광원은 씬에서 햇빛 같은 효과를 만드는 데 유용합니다. 여러 면에서 태양처럼 동작하는 방향 광원은 무한한 거리에 멀리 떨어져 있는 광원이라고 생각할 수 있습니다. 방향 광원은 확인할 수 있는 광원 포지션이 없으므로, 라이트 오브젝트를 씬의 아무 위치에나 배치할 수 있습니다. 씬의 모든 오브젝트에는 광원이 항상 같은 방향에서 오는 것처럼 비춥니다. 광원과 타겟 오브젝트의 거리는 정의되어 있지 않으므로 광원이 약해지지 않습니다.

방향 광원은 게임 세계의 범위 밖에 있는 위치에서 오는 크고 먼 광원을 표현합니다. 사실적인 씬에서는 방향 광원을 사용하여 해나 달을 시뮬레이션할 수 있습니다. 추상적인 게임 세계에서는 빛이 정확하게 어디서 오는지 지정하지 않고 오브젝트에 사실적인 셰이딩을 추가하는 데 유용한 방법이 될 수 있습니다.

씬 내 방향 광원 효과

기본적으로 새 Unity 씬에는 모두 방향 광원이 있습니다. 여기에서 방향 광원은 조명 패널의 환경 조명 섹션(Lighting>Scene>Skybox)에서 정의되는 절차적 스카이 시스템에 연결됩니다. 기본 방향 광원을 삭제하고 새 광원을 만들거나 간단히 ‘Sun’ 파라미터(Lighting>Scene>Sun)에서 다른 게임 오브젝트를 지정하여 이 동작을 변경할 수 있습니다.

기본 방향 광원(즉 ‘Sun’)을 회전하면 ‘스카이박스’가 업데이트됩니다. 빛이 지면과 평행하게 측면에 비스듬하게 비치도록 하면 일몰 효과를 얻을 수 있습니다. 또한 빛이 위쪽을 향하게 하면 밤이 된 것처럼 하늘이 검게 변합니다. 빛을 위에서 비스듬하게 비추면 하늘이 주간과 유사해집니다.

스카이박스를 앰비언트 소스로 선택하면 이런 컬러에 따라 주변광이 변합니다.

면 광원

면 광원(Area lights)은 공간에 있는 사각형 또는 디스크 중 한 가지 모양으로 정의할 수 있습니다. 면 광원은 해당 모양의 한 쪽 면에서 빛을 방출합니다. 자체 광원은 해당 모양의 표면적 전체에 걸쳐 모든 방향으로 균일하게 퍼집니다. Range 프로퍼티는 해당 모양의 크기를 결정합니다. 면 광원이 제공하는 조명의 강도는 광원으로부터 거리의 역제곱에 의해 결정되는 비율로 감소합니다(역제곱 법칙 참조). 이 조명 연산에는 프로세서 성능이 꽤 많이 사용되기 때문에 면 광원은 런타임 시점에 사용할 수 없고 라이트맵으로만 베이크할 수 있습니다.

면 광원은 오브젝트를 여러 다양한 방향에서 동시에 비추기 때문에 다른 광원 타입에 비해 셰이딩이 더 부드럽고 섬세합니다. 면 광원을 사용하여 사실적인 가로등이나 플레이어 가까이에 길게 늘어선 불빛을 만들 수 있습니다. 작은 면 광원을 사용하면 실내 조명처럼 더 작은 광원을 점 광원보다 더 사실적으로 시뮬레이션할 수 있습니다.

면 광원의 전체 표면에서 발산되어 부드러운 그림자를 생성하는 산란광을 만듭니다.

'유니티게임강좌 > 라이트매핑' 카테고리의 다른 글

[라이트매핑] 라이트 프로브  (2) 2023.03.20
[라이트매핑] 라이트매핑  (0) 2023.03.20

네비게이션 기능중 특정 경로에 가중치를 적용해 NavMeshAgent에서 최단 거리를 계산할 때 경로의 가중치를 줄수 있다. 예를 들어 평지와 진흙길에 가중치를 줘 쉬운길로 갈 수 있게 한다.

 

경로의 가중치

내비게이션뷰의 Ares탭을 클릭하면 Area Mask와 Cost를 설정할 수 있다. 여기에 다음과 같이 3개의 Area Mask를 지정하고 각각 Cost를 입력한다.

스테이지에 다음과 같이 세 갈래 길을 구현하고 각각 Area Mak를 Road, Mud, Water,Stair로 지정한다.

교재에서는 패스를 3개 만들라고 했는데 그냥 하이라키뷰에서 Stair를 선택하고 내비게이션 뷰의 Object탭에서 Area Mask를 선택할 수 있다. 

 

교재에 의하면 Bake후 가중치에 따라 색깔도 바뀐다고 한다.

몬스터의 이동은 NavMeshAgent에 의해 관리되고 있다. 이동하면서 회전하기 때문에 회전이 부자연스럽다. 좀더 빠르게 회전하도록 직접 회전로직을 구현해본다.

MonsterCtrl 스크립트의 Awake함수를 수정하고 Update함수를 다음과 같이 추가한다.

updaterotation=false로 자동회전기능을 비활성화 시켰다. updatePosition=false로 자동 추적기능을 끌수도 있다.

void Awake() {
	//중략
    agent = GetComponent<NavMeshAgent>(); //NavMeshAgent 할당
    agent.updateRotation= false; // 자동회전기능 비활성화
	//중략   
}

agent.remaingDistance는 타겟까지 남은 거리이다. agent.desiredVelocity는 장애물을 고려한 이동방향이다.

private void Update() {
    //목적지 까지 남은 거리로 회전여부 체크
    if(agent.remainingDistance >= 2.0f) {
        Vector3 direction = agent.desiredVelocity;
        Quaternion rot = Quaternion.LookRotation(direction);
        monsterTr.rotation = Quaternion.Slerp(monsterTr.rotation, rot, Time.deltaTime * 10.0f);
    }
}

이외 velocity, pathPending, isPathStale 등의 속성이 있다.

 

자동 생성된 Off Mesh Link는 Drop Height 높이 이내면 무조건 생성되어 특정지점만 만들고 싶을 경우 적합하지 않다. 이제부터 계단 상단에서 바닥으로 연결하는 하나의 사용자정의 Off Mesh Link를 만들어 보자.

Stair선택후 네비게이션뷰의 [Object]탭에서 Generate OffMeshLinks 속성을 언체크하고 [Bake]탭을 눌러 다시 베이크하면 앞서 생성되었던 Off Mesh Link들이 모두 사라진다.

 

Stair 하위에 빈 게임오브젝트를 2개 만들어 StartPos,EndPos로 이름을 바꾸고 StartPos는 계단 맨위, 착지할  EndPos는 그 아래 바닥에 겹치게 배치한다.  관찰하기 좋게 MyGizmo 스크립트를 추가한다.

교재에서는 Off Mesh Link가 자동으로 연결된다는데 실수로 지웠는지 Stair 인스펙터뷰에 Off Mesh Link컴포넌트를 추가해주고 Start,End가 자동으로 연결안되어 있으면 연결해 줬다.

Bidirectional 은 양방향으로 점프할수 있다는 뜻이고 Activated는 Run Time시 Off Mesh Link를 활성화 하는 것이다. Auto Update Positions는 RunTime시 링크위치가 동적으로 바뀌는 것에 대응한다.

 

 

내비게이션은 3D메시의 정보를 기반으로 네비메시를 생성한다. 따라서 분리된 메시는 추적할 수 없다. 이때 Off Mesh Link를 통해 서로 분리된 메시를 연결할 수 있다. 즉, 추적이 가능하게 연결고리를 생성할 수 있다.

다운로드한 Resources/Models 폴더에 있는 Stair패키지를 설치한다.

프로젝트뷰에 임포트된  Stair폴더는 05.Models로 옮긴다.

Stair 폴더 하위에 있는 Stair모델을 씬뷰로 드래그해서 적절한 위치에 배치한다. 아래부분이 바닥에 묻히게 조절한다.

추가한 Stair는 _STAGE하위로 차일드화 한다. Stair를 선택후 인스펙터뷰에서 Navigation Static과 Off Mesh Link를 선택한다. 인스펙터뷰 Navigation탭에서도 선택할 수 있다.

Navigation Bake TAB을 클릭후 Generated Off Mesh Links-Drop Height을 5로 설정한다. 이는 Off Mesh Link가 생성되는 최대 높이를 지정하는 것이다.

 Bake를 눌러 내비메시를 생성하면 다음과 같이 연결선이 보인다.

계단에 Collider를 추가해 올라갈수 있게 해준다. 연산부하를 줄이기 위해 Mesh Collider컴포넌트의 Convex속성을 체크해 폴리곤수를 낮춘다.

Navigation뷰의 Bake탭의 Agent Radius를 0.2로 변경후 Bake하면 좀더 촘촘하게 Off Mesh Link가 생성된다.

이제 실행해서 계단을 올라가면 몬스터들이 따라온다.

처음에 Player가 계단을 못올라 가서 헤맸는데 다음과 같이  Player collider의 Trigger를 꺼주고 해결하였다. 그리고 교재와 Rigidbody 속성이 달라 수정하였다

드럼통 폭발이후에도 적 몬스터는 그 지점을 장애물로 인식하는 문제점이 있다. 이는 내비게이션 메시가 정적으로 Bake되어 졌기 때문이다. 이 문제는 NavMeshObstacle 컴포넌트로 해결할 수 있다.

Barrel Prefab을 선택후 이름 우측 Static을 Off한후 children에 반영시킨다.

메뉴>window>AI>Navigation을 선택해 뷰에서 Bake탭을 선택후 아래 베이크 버튼을 클릭한다.

씬뷰에서 보면 Floor바닥 전체가 내비메시로 베이크된다.

배럴 프리팹 원본을 선택후 인스펙터뷰 이름옆 Open을 눌러 에디트 모드로 전환된다. NavMeshObstacle을 추가한다.

 NavMeshObstacle 속성을 다음과 같이 변경한다.  Auto save기능이 있기 때문에 하이라키뷰"<"나 씬뷰[Scenes]을 클릭해 돌아가도 자동으로 저장된다.

Carve옵션을 체크하면 실시간으로 내비메시가 변경되지만 부하를 줄이기 위해 최적의 옵션값을 설정해야한다. 

Move Threshold : 갱신할 최소거리

Time To Stationary : 갱신한 최소 정지시간

Carve Only Stationary : 정지 상태에서만 내비메시를 갱신한다.

 

 

지금까지 구현한 총알 발사 로직은 실제 Bullet모델이 날아가서 몬스터와 충돌을 일으키는 Projectile방식으로 돼 있다. 대부분의 FPS게임에서는 실제 총알이 날라가지 않고 발사와 동시에 적에 명중해서 혈흔  효과와 같은 이벡트를 연출하고 적이 사망한다. 유니티에서 이러한 방식을 구현할수 있는  Raycast를 제공한다.

눈에 보이지 않는 광선을 발사해서 광선에 맞은 물체를 판단해서 후처리를 하는 방식이다. 다음과 같이 광선의 발사원점과 발사각, 거리등의 인자로 광선을 발사 할 수 있다.

이 기능은 게임스테이지에서 마우스 포인트 위치로 레이캐스트해서 3차원좌푯값을 읽어온후 해당 좌표로 이동시킬때도 사용한다.

 

DrawRay

레이캐스트는 씬 뷰에서 시각적으로 표시되지 않기 때문에 개발할 때는 DrawRay 함수를 이용해 시각적으로 표시하면서 개발을 해야한다. FireCtrl 스크립트의 Update()에서 Debug.DrawRay()를 추가한다.

void Update()
{  //마우스 왼쪽 버튼을 클릭했을때  Fire 함수 호출
    Debug.DrawRay(firePos.position, firePos.forward * 10.0f, Color.green);
    if(Input.GetMouseButtonDown(0)) {
        Fire();  //발사처리
    }
}

Play후 씬뷰탭을 눌러 보면 Ray가 잘 그려진다. 사실 좀 불편하다.

LineRenderer

게임뷰에서도 보일수 있게 하이라키뷰에서 Player를 선택하고 LineRendere 컴포넌트를 추가한다.

FireCtrl 스크립트 전역변수에 lr을 선언하고

 private LineRenderer lr;

Start()에서 LineRenderer 컴포넌트를 연결해주고 선의 두께, 색을 지정해준다.

    private void Start() {
		//중략
        lr =  GetComponent<LineRenderer>();
        lr.startWidth = 0.01f;   //라인 시작 두께
        lr.endWidth = 0.1f;  //라인 시작 두께
        lr.startColor = Color.red;  //라인 시작 색깔
        lr.endColor = Color.blue;  //라인 종료 색깔
    }

이제 Update()에서 시작점과 종료점을 지정해주면 실시간으로 선을 그려준다. 

    void Update()
    {  //마우스 왼쪽 버튼을 클릭했을때  Fire 함수 호출
        lr.SetPosition(0, firePos.position);
        lr.SetPosition(1,firePos.position + firePos.forward * 20f - firePos.up);

Raycast, RaycastHit

레이캐스트가 객체를 검출하기 위해서는 그 객체는 Collider 컴포넌트를 갖고 있어야 한다. 특정 레이어만 감지하게 할 수도 있다. 몬스터는 6번째 MONSTER_BODY 레이어로 지정되어 있어 LayerMask(1<<6)를 사용 필터링 할 수 있다.

이제 물리적 총알은 시각적이고 Ray를 맞았을때 데미지를 입도록 수정해보자.

총알생성로직은 FireCtrl 스크립를 수정한다.

전역변수로 hit를 추가한다. Raycast발사후 충돌객체의 정보를 돌려준다.

private RaycastHit hit;  // Raycast 결과값을 저장하기 위한 구조체

Variables

barycentricCoordinate 충돌한 triangle의 무게중심 좌표
collider 충돌한 collider
distance Ray의 origin으로부터 충돌 지점까지의 거리
lightmapCoord 충돌 지점의 uv lightmap 좌표
normal Ray가 충돌한 surface의 normal
point Ray가 충돌한 Collider의 충돌 지점 (world 좌표 사용)
rigidbody 충돌한 collider의 rigidbody (rigidbody가 없는 경우 null 반환)
textureCoord 충돌 위치에서의 uv texture 좌표
textureCoord2 충돌 위치에서의 2차 uv texture 좌표
transform 충돌한 Transform의 Rigidbody 또는 Collider
triangleIndex 충돌한 triangle의 index

이제 Physics.Raycast()를 이용해 눈에 보이지 않는 광선을 쏴보자 사용법은 여러가지가 있으나 다음 방법을 사용했다

Physics.Raycast(발사원점, 발사방향벡터, 결과값, 발사거리, 레이어마스크)

총구의 위치가 약간 위를 바라보고 있어 방향 layDir 을 약간 아래로 수정했다. 1<<6은 몬스터 레이어가 6번째이무로 시프트연산자를 사용해 오른쪽에서 6번째 비트를 켠것이다. 비트연산자를 사용해 복수의 레이어를 선택할 수도 있다.

void Update()
{  //마우스 왼쪽 버튼을 클릭했을때  Fire 함수 호출
	//중략
   Vector3 layDir = firePos.forward * 20.0f - firePos.up;
   if (Physics.Raycast(firePos.position, layDir, out hit, 20.0f, 1 << 6)) {
}

교재에서는 Instantiate(bullet)를 마크했으나 필요에 따라 사용할수 있게 매개변수bool shot로 끄고 켤수 있게 했다. Update()안에서 Fire(false)로 잠시 총알 발사를 못하게 할 수 있다.

void Fire(bool shot) {
        if(shot) {
            GameObject b = Instantiate(bullet, firePos.position, firePos.rotation); //Bullet프리팹을 동적으로 발생
            b.transform.Rotate(Vector3.right * 3f);  //총탄의 각도가 높아서 낮추었음
        }
        audio.PlayOneShot(fireStx, 1.0f);  //총소리발생\
        StartCoroutine(ShowMuzzleFlash()); //총구화염효과 코루틴함수 호출
    }

Fire(false)로 이제 총알이 나가지 않는다 Raycast()는 OnCollisionEnter나 OnTriggerEnter Event를 발생시키지 않는다. 따라서 다음과 같이 MonsterCtrl의 OncollisionEnter()의 피격효과및 HP관리를 지우고 OnDamage함수로 처리를 옮겨준다.

 private void OnCollisionEnter(Collision coll) {
        if (coll.collider.CompareTag("BULLET")) {
            Destroy(coll.gameObject);             //충돌한 총알을 삭제
        }
    }
    public void OnDamage(Vector3 pos, Vector3 normal) {
        anim.SetTrigger(hashHit);  //피격 리액셔  애니메이션 실행
        Quaternion rot = Quaternion.LookRotation(normal); //충돌지점 법선벡트
        ShowBloodEffect(pos, rot); //혈흔효과를 생성하는 함수호출
        hp -= 20;
        Debug.Log(hp);
        if (hp <= 0) {
            state = State.DIE;
            GameManager.instance.DisplayScore(50);
        }
    }

OnDamage를 유니티의 표준 콜백함수가 아니므로 FireCtrl 스크립트내 Update()안의 RaycastHit hit변수를 보고 충돌이 있었을때 충돌체의 MonsterCtrl스크립트 컴포넌트내의 OnDamage()함수를 호출한다.

if (Input.GetMouseButtonDown(0)) {
    Fire(false);  //true=총알발사처리, false=총소리만
    Vector3 layDir = firePos.forward * 20.0f - firePos.up;
       if (Physics.Raycast(firePos.position, layDir, out hit, 20.0f, 1 << 6)) {
        //Debug.Log($"Hit={hit.transform.name}");
        hit.transform.GetComponent<MonsterCtrl>()?.OnDamage(hit.point, hit.normal);
    } 
}

이제 물리적 총알 발사없이 타격효과및 Hp 관리가 문제없이 된다면 다시 위 코드에서 Fire(true)로 바꿔 물리적 총알 발사를 활성화한다.

if (Input.GetMouseButtonDown(0)) {
    Fire(true);  //true=총알발사처리, false=총소리만

#Debug.DrawRay #LineRenderer #RayCast #RayCastHit 3가지를 배우느라 좀 힘들수 있다. 실제 이 3가지를 정리하느라 몇칠 걸린듯 하니 천천히 이해해주시기 바랍니다.

모바일플랫폼에서 게임오브젝트및 프리팹을 동적으로 생성하는 방식은 부하를 증가시킨다. 따라서 미리 만들어 놓고 필요할때 가져다 쓰는 방식을 오브젝트 풀링이라 한다.

동적으로 만들던 몬스터를 게임시작시 미리 만들어 놓고 비활성화해서 하나씩 불러다 쓰고 사망시 Destroy하지 않고 오브젝트풀에서 재사용할 수 있게 변경한다. 다음과 같이 GameManager.cs 를 수정한다.

몬스터 풀로 사용할 List<>와 최대생성수를 정의한다.

    public List<GameObject> monsterPool = new List<GameObject>(); // 몬스트풀 리스트
    public int maxMonsters = 10; //몬스터 최대 생성 수

CreateMonsterPool()을 호출 몬스터프리팹을이용 Instantiate()로 몬스터를 생성하고 이름을 Monster_00방식으로 변경하고 비활성화후 풀 List에 add해준다.

    void CreateMonsterPool() {  //몬스트풀 생성 함수
        for(int i=0; i < maxMonsters; i++) {
            var _monster = Instantiate<GameObject>(monster); //몬스터 생성
            _monster.name = $"Monster_{i:00}"; //몬스터이름 생성
            _monster.SetActive(false);  //몬스터 비활성화
            monsterPool.Add(_monster); //생성된 몬스트를 풀에 저장
        }
    }

풀생성후 InvokeRepeating()함수를 통해 몬스터가 주기적으로 자동생성된다.

    void Start() {
        CreateMonsterPool();  // 몬스터풀 생성 함수
        InvokeRepeating("CreateMonster", 2.0f, createTime);  //2초후 반복적으로 몬스터를 만든다
    }

몬스터 생성은 Instantiate대신 풀에서 비활성환된 몬스터를 골라 리턴 GetMonsterInPool()을 호출 

    void CreateMonster() {  //동적 몬스터 생성
        int idx = Random.Range(0, points.Count);
        //Instantiate(monster, points[idx].position, points[idx].rotation); //몬스트프리팹 동적 생성
        GameObject _monster = GetMonsterInPool();
        _monster?.transform.SetPositionAndRotation(points[idx].position, points[idx].rotation);
        _monster?.SetActive(true);
    }

풀을 순환하며 비활성환된 몬스터를 골라 리턴해주는 GetMonsterInPool() 

    public GameObject GetMonsterInPool() {
        foreach(var _monster in monsterPool) {
            if(_monster.activeSelf == false) {  
                return _monster;  }
        }
        return null;
    }

이제 플레이해보면 10개까지 잘 생성되는데 몬스터를 죽이면 다시 생성되지 않는다 왜냐하면 몬스터가 Destroy되지만 active한 상태가 되기 때문에 풀에서 비활성 몬스터가 없어지기 때문에 Destroy대신 active를 false로 해준다

몬스터의 Hp관리는 MonsterCtrl에서 한다. Start()함수를 Awake()로 이름을 바꾸고

    void Awake()
    {
        monsterTr= GetComponent<Transform>();
        playerTr = GameObject.FindWithTag("PLAYER").GetComponent<Transform>();  
        agent = GetComponent<NavMeshAgent>();
        anim = GetComponent<Animator>();
        bloodEffect = Resources.Load<GameObject>("GoopSpray"); //혈흔효과      
    }

StartCourotine()2개를 OnEnable함수로 옮겨 연결되지 않는 컴포넌트의 참조하는 오류를 막는다

    private void OnEnable() {  //스크립트가 활성활 될때 콜백되는 함수
        PlayerCtrl.OnPlayerDie += this.OnPlayerDie; //교재는 PlayerCtrl.
        StartCoroutine(CheckMonsterState()); //몬스트 상태를 체크하는 코루틴
        StartCoroutine(MonsterAction());  //상태에 따라 행동을 수행하는 코루틴
    }

MonsterAction()의 case State.DIE부분을 수정한다. 책 코드대로 실행해본 결과 죽은 몬스터가 부활할때 다시 죽는 경우가 발생했다. 따라서 state=State.IDLE을 추가해서 해결하였다.(DIE상태로 부활하면 다시 DIE상태로 무한 반복한다)

IEnumerator MonsterAction() {
        while (!isDie) {
            transform.LookAt(playerTr);
            switch (state) {
            //중략
                case State.DIE: //사망상태
                    isDie= true;  
                    agent.isStopped = true;  //추적 정지
                    anim.SetTrigger(hashDie); // 애니메이트 hasDie 트리거발생
                    GetComponent<CapsuleCollider>().enabled = false;  //충돌컴포넌트 비활성화
                    //일정시간 경과후 오브젝트풀링으로 환원
                    yield return new WaitForSeconds(0.3f); //일정시간경과후
                    hp = 10;  //hp를 초기화하고
                    isDie= false; // 상태를 살리고
                    GetComponent<CapsuleCollider>().enabled = true;  //콜라이더를 다시 true
                    this.gameObject.SetActive(false); //몬스터를 비활성화 한다.
                    state = State.IDLE;  //Idel로 안하면 태어나자 마자 죽는다
                    break;
            }
            yield return new WaitForSeconds(0.3f);
        }
    }

GameManager.cs
0.00MB
MonsterCtrl.cs
0.01MB

 

싱글턴 패턴은 메모리상에 오직 하나의 인스턴스만 생성하고 그 인스턴스에 전역적인 접근을 제공하는 방식이다. 

스크립트에서 제일먼저 실행되는 Awake()함수에서 GameManager를 static키워드로 정적 메모리영역에 올려두고 다른 스크립트에서 바로 접근할 수 있게 구현하는 방식이다.

그리고 자신이 아니라면 지우고 다른씬으로 넘어가도 삭제되지 않게 DontDestroyOnLoad()명령을 실행해 놓는다.

public static GameManager instance = null;
private void Awake() {
    if (instance == null) {
        instance = this; //싱글턴지정
    } else if (instance != this){  //싱글턴검사
        Destroy(this.gameObject);
    }
    DontDestroyOnLoad(this.gameObject);  //다른씬으로 넘어가도 삭제하지 않고 유지함
}

이제  PlayerCtrl에서 검색이나 연결없이 GameManager 컴포넌트의 프라퍼티로 바로 사용가능하다.

    void PlayerDie() {
        //MONSTER 태그를 가진 모든 게임오브젝트를 찾아옴
        /*
        GameObject[] monsters = GameObject.FindGameObjectsWithTag("MONSTER");
        foreach(GameObject monster in monsters) {  //모든 오브젝트를 순차적으로 불러옴
            monster.SendMessage("OnPlayerDie", SendMessageOptions.DontRequireReceiver);
        }
        */
        OnPlayerDie();  //주인공 사망 이벤트 호출(발생)
        //GameObject.Find("GameMgr").GetComponent<GameManager>().IsGameOver = true;  //적 생산 멈춤
        GameManager.instance.IsGameOver= true; // IsGameOver 프라퍼티로 바로 사용가능
    }

 

+ Recent posts