

Realtime : 실시간 조명(씬에 직접 광을 적용하여 매 프레임마다 조명 연산)으로 연산 부하가 크다. 직접 광원만 적용하고 간접 광원과는 관계가 없다. 반사광 효과도 없다.
Mixed : Mixed Lighting 섹션에 있는 Lighting Mode 속성에 따라 조명 처리 방법이 달라진다.
Lighting Mode
Baked : 런타임 시 조명 연산 처리를 하지 않는 조명, 정적인 물체에 전역 조명과 그림자를 생성한다. 동적인 물체의 조명 효과는 라이트 프로브를 통해 적용한다. Baked 모드의 조명은 스페큘러 조명 효과도 표현할 수 없기에 리플렉션 프로브를 사용해 구현해야 한다.
씬에 배치된 모든 3D 모델에 영향을 미치는 직접 조명, 간접 조명 및 그림자의 효과를 텍스처로 미리 만드는 과정을 말한다. 라이트맵은 런타임 시 실시간 렌더링 화면과 오버레이돼 믹싱된다. 런타임 시 전역 조명에 대한 연산 처리를 하지 않고도 높은 품질의 조명 효과를 구현할 수 있다.
Generate Lighmap UVs 옵션
ipmort한 모델의 속성을 바꿔주어야 한다. 인스펙터에서 Generate Lightmap UVs 속성을 체크해야 라이트매핑 효과가 적용된다.

Contribute GI 플래그
라이트매핑을 하려면 라이트매퍼에 그 대상을 알려줘야 한다. 따라서 프리팹들을 선택하고 인스펙터 뷰의 Contribute GI를 체크해준다.

Scene 탭 속성 :

Environment 탭 속성 :

Progressive 라이트매퍼는 경로 추적(Path tracing) 기반의 라이트매퍼로서 유니티 에디터에서 라이트맵과 라이트 프로브를 점진적으로 베이크하는 기능을 제공한다. 또한 Progressive GPU 라이트매퍼는 GPU의 그래픽 가속 기능을 이용해 좀 더 빠르게 라이트 매핑이 가능하다.
라이트맵 이미지는 exr 파일 포맷으로 그래픽 에디터로 이미지의 색상을 보전하는 후보정 작업이 가능하다. 라이트맵이 베이크된 후에는 Point Light가 실시간 조명으로서의 영향을 미치지 않는다. 조명의 위치를 바꾸거나 추가했을 때, 또는 정적인 물체를 추가했을 때, 또는 정적인 물체를 추가로 배치했을 때는 다시 베이크해야 라이트매 텍스처에 반영된다.

플레이어같은 동적 오브젝트는 여전히 검은색으로 보인다. 플레이어가 라이트매핑의 대상이 아니기 때문이다. 실시간 조명의 Culling Mask를 사용해 주인공에만 조명 효과를 부여할 수 있지만 부자연스러운 장면이 연출된다. ( 간접 광원 또는 그림자 영역에 들어갔을 때도 밝은 조명이 비치는 )
이를 개선하기 위해 유니티에서는 라이트 프로브라는 기능을 제공한다. 스테이지의 조명이 있는 곳 주변에 라이트 프로브를 배치하고 주변부의 광원 데이터를 미리 저장해둔다. 근처를 지나는 동적 객체에 광원 데이터를 전달해 해당 객체의 색상과 보간시켜 마치 실시간 조명과 같은 효과를 내는 방식이다.



Light Probe Group 컴포넌트의 기능은 라이트 프로브를 생성하고 배치하는 것이다. Light Probe Group의 컴포넌트는 4개의 버튼을 제공한다.

Select All과 Duplicate Selected를 활용해서 아래와 같이 맵 전체를 덮을 수 있다.


이제 Generate Lighting을 하면 라이트맵과 라이트 프로브를 동시에 베이크한다. 라이트맵 베이크가 완료되면 플레이어도 주변의 조명 값을 받아 밝아진다. (몬스터도 영향을 받아 밝아진다.)
라이트 프로브는 주인공의 가장 근접한 4개의 라이트 프로브에서 베이크된 조명 값을 전달한다. Anchor Override 속성을 이용해 보간되는 위치를 설정할 수 있다.
머리 관절인 head를 Anchor Override 속성에 연결하면 주변 라이트 프로브가 조명 값을 전달하는 위치가 머리쪽으로 변경된다.

_STAGES를 삭제하고 플레이어와 UI만 관리하는 로직을 관리하는 Scene이다.

_STAGES만 남기고 다 지운다.

기존 UI 버튼 매니저에 씬 이동을 넣어준다.
void OnStartClick() {
SceneManager.LoadScene("Level_01");
SceneManager.LoadScene("Play", LoadSceneMode.Additive);
}
SceneManager : 동적으로 씬을 생성, 해제하거나 기존에 만들어진 씬을 호출하는 여러 메소드를 제공한다.
중요한 점은 Scene을 Build 해줘야한다. File > Build Settings에서 빌드를 해준다.

이제 플레이를 누르면 Scene이 병합되어 실행된다. 이제 Level만 업데이트 해주면 플레이어는 그대로 병합시켜 실행시킬 수 있다는 뜻이다.

Level_01 씬을 하이러키 뷰로 드래그하면 두 개의 씬을 한 번에 로드할 수 있다.

오클루전 컬링(Occlusion:한 물체가 다른 물체를 가리는 것, Culling:도태시킨다)은 렌더링 부하를 줄여주는 기법 중 하나로 3D 게임 및 콘텐츠 개발에 필수적인 요소이다. 카메라에 보이지 않는 객체는 렌더링 하지 않는 기법, 이 기법은 불필요한 요소를 렌더링에서 제외해 부하를 줄이고 속도를 향상 시킨다. 컬링 방식은 여러 가지가 있는데 그중 몇 가지를 알아보자.

메인 카메라를 선택하면 프러스텀 영역이 표시된다. 또한 렌더링 범위를 줄여주는 CLipping Planes의 Near, Far 속성을 조절해서 전방으로 얼마나 렌더링 할 것인지를 정할 수 있다.

프러스텀 컬링은 프러스텀 영역에 들어온 모든 물체를 렌더링한다. 하지만 렌더링 영역에 들어온 모든 물체를 식별하기 어려울 경우에는 LOD를 이용해 일정 거리 밖의 물체는 컬링 시킨다.

LOD Group 컴포넌트를 보면 Culled 10%로 표시된 부분이 있는데, 이 Culled 구간이 거리에 비례해 컬링시키는 기능이다.
카메라 시야에서 다른 물체에 가려 보이지 않는 물체를 렌더링 하지 않는 기법

오클루전 컬링 기능을 구현하려면 먼저 Occluder Static과 Occludee Static을 설정해야한다.

Plane과 앞의 큐브에는 Occluder Static으로 지정

벽 뒤에 있는 녹색 큐브는 Occludee Static으로 지정

오클루전 컬링 베이킹
Window > Rendering > Occlusion Culling을 선택해 오클루전 컬링 창을 열어서 Bake를 하면 된다.
?
Bake하고 난 후 Occlusion Culling 툴 박스에서 뷰 모드를 Visualize로 변경하면 녹색 큐브가 사라진다. 벽에 의해 카메라 시야에서 보이지 않는 물체를 컬링시킨 것이다.

카메라가 옆으로 이동하면 큐브가 렌더링 된다.

오클루전 컬링 기능은 1인칭 시점이고 건물이나 장애물이 많은 스테이지일 경우에는 필수적이다. 다만 컬링할 대상이 없는 3인칭 시점 또는 톱다운 시점의 경우에는 오클루전 컬링의 효과가 없을 뿐더러 오히려 부하를 가중시킬 수 있기 때문에 적합한 장르에만 적용해야 한다.
기존의 Input Manager은 확정성과 편의성이 부족하지만 2019.1 이상의 버전부터 제공하는 InputSystem을 이용하면 굉장히 편리하다.
Action과 Binding으로 구성되어 있는데, Action은 게임 내의 행동, 동작을 의미한다. Binding은 Action을 실제 물리적인 입력장치와 매핑 시키는 것으로 추가적인 입력을 Binding 시켜주면된다.
예를 들어 우클릭으로 점프하고 싶으면 Jump Action에 마우스와 관련된 Binding을 하나 더 추가해주면 된다.
Action Maps, Actions, Properties의 관계도
먼저 패키지 매니저에서 Input System을 import 해준 다음에 재시작한 다음 Edit > Project Settings에서 input System이 적용됐는지 확인할 수 있다.

Create > Input Action 에셋을 생성해줄 수 있다.

플랫폼 별로 공유할 입력 설정과 플랫폼에 특화된 입력 설정이 있을 수 있다. 아래와 같이 PC, Mobile 두 개의 Scheme을 생성해준다.


이제 아래와 같이 Actions와 Type, Binding을 설정해준다.

액션의 Path를 지정해줄때 [T] 버튼을 사용해 직접 문자열을 입력해 바인딩 정보를 설정해줄 수 있다.

왜 Move가 Vector2? : 캐릭터는 전후좌우 이동하기 때문에 키 조합이 Vector2(x, y) 형태의 값을 갖고 있다.
Player Input 컴포넌트를 추가해준 다음에 Main Actions 에셋을 넣어준다.

그런 다음 Behavior 속성을 설정해준다.

SendMassage 함수를 이용해 호출한다. 호출하는 함수이름은 아래와 같다.
void On{액션명}()
#pragma warning disable IDE0051
using UnityEngine;
using UnityEngine.InputSystem;
public class WarriorCtrl : MonoBehaviour
{
private Animator anim;
private new Transform transform;
private Vector3 moveDir;
void Start() {
anim = GetComponent<Animator>();
transform = GetComponent<Transform>();
}
void Update() {
if (moveDir != Vector3.zero) {
transform.rotation = Quaternion.LookRotation(moveDir);
transform.Translate(Vector3.forward * Time.deltaTime * 4.0f);
}
}
#region SEND_MESSAGE
void OnMove(InputValue value) {
Vector2 dir = value.Get<Vector2>();
moveDir = new Vector3 (dir.x, 0, dir.y);
anim.SetFloat("Movement", dir.magnitude);
Debug.Log($"Move = ({dir.x}) {dir.y}");
}
void OnAttack() {
Debug.Log("Attack");
anim.SetTrigger("Attack");
}
#endregion SEND_MESSAGE
}

Unity Event 타입의 속성으로 표시된다. 일번적인 UI Button 이벤트를 연결하는 방식과 동일하게 연결할 수 있다.

아래와 같은 새로운 함수를 정의해야 한다.
void On{액션명}(InputAction.CallbackContext context)
#region UNITY_EVENTS
public void OnMove(InputAction.CallbackContext ctx) {
Vector2 dir = ctx.ReadValue<Vector2>();
moveDir = new Vector3 (dir.x, 0, dir.y);
anim.SetFloat("Movement", dir.magnitude);
Debug.Log($"Move = ({dir.x}) {dir.y}");
}
public void OnAttack(InputAction.CallbackContext ctx) {
Debug.Log($"ctx.phase={ctx.phase}");
if (ctx.performed) {
Debug.Log("Attack");
anim.SetTrigger("Attack");
}
}
#endregion UNITY_EVENTS
왜 Attack 에서 ctx.performed?
Input Actins에 정의한 액션은 시작, 실행, 취소으 ㅣ콜백 함수를 각각 한 번씩 호출한다. 따라서 Performed 처리를 해주지 않으면 공격 애니메이션이 3번 발생한다.
#region ~ #endregion
region 전처리기는 코드의 영역을 정의하는 것으로 특정 영역을 Collapse 할 수 있다. 특정 로직을 그룹활 할 때 편리한 방법으로 영역을 분류할 수 있다.

C# 스크립트에서 직접 이벤트를 연결해 사용하는 방식으로 Player Input 컴포넌트를 사용할 수도 있고 Input Actions 에셋과 Player Input 컴포넌트 없이 다 스크립트로 처리할 수도 있다.
private PlayerInput playerInput;
private InputActionMap mainActionMap;
private InputAction moveAction;
private InputAction attackAction;
void Start() {
anim = GetComponent<Animator>();
transform = GetComponent<Transform>();
playerInput = GetComponent<PlayerInput>();
mainActionMap = playerInput.actions.FindActionMap("PlayerActions");
moveAction = mainActionMap.FindAction("Move");
moveAction = attackActionMap.FindAction("Attack");
moveAction.performed += ctx => {
Vector2 dir = ctx.ReadValue<Vector2>();
moveDir = new Vector3(dir.x, 0, dir.y);
anim.SetFloat("Movement", dir.magnitude);
};
moveAction.canceled += ctx => {
moveDir = Vector3.zero;
anim.SetFloat("Movement", 0.0f);
};
attackAction.performed += ctx => {
Debug.Log("Attack by c# event");
anim.SetTrigger("Attack");
};
}
PlayerInput 컴포넌트 없이 스크립트에서 InputAction을 생성하고 액션을 정의하는 방식이 있다.
public class PlayerCtrlByEvent : MonoBehaviour
{
private InputAction moveAction;
private InputAction attackAction;
private Animator anim;
private Vector3 moveDir;
void Start()
{
anim = GetComponent<Animator>();
moveAction = new InputAction("Move", InputActionType.Value);
moveAction.AddCompositeBinding("2DVector")
.With("Up","<Keyboard>/w")
.With("Down","<Keyboard>/s")
.With("Left","<Keyboard>/a")
.With("Right","<Keyboard>/d");
moveAction.performed += ctx => {
Vector2 dir = ctx.ReadValue<Vector2>();
moveDir = new Vector3(dir.x, 0, dir.y);
anim.SetFloat("Movement", dir.magnitude);
};
moveAction.canceled += ctx => {
moveDir = Vector3.zero;
anim.SetFloat("Movement", 0.0f);
};
moveAction.Enable();
attackAction = new InputAction("Attack", InputActionType.Button, "<Keyboard>/space");
attackAction.performed += ctx => {
Debug.Log("Attack by c# event");
anim.SetTrigger("Attack");
};
attackAction.Enable();
}
void Update() {
if (moveDir != Vector3.zero) {
transform.rotation = Quaternion.LookRotation(moveDir);
transform.Translate(Vector3.forward * Time.deltaTime * 4.0f);
}
}
}
Input Debug
Input Debug는 입력 장치로부터 전달되는 값을 모니터링할 수 있는 기능으로, 에디트 모드에서 다양한 외부 입력 장치의 정보를 확인할 수 있다.


void Start() {
action = () => OnStartClick();
startButton.onClick.AddListener(action);
optionButton.onClick.AddListener(delegate {OnButtonClick(optionButton.name);});
shopButton.onClick.AddListener(() => OnButtonClick(shopButton.name));
}
void OnStartClick() {
SceneManager.LoadScene("Level_01");
SceneManager.LoadScene("Play", LoadSceneMode.Additive);
}
아래는 테스트 용으로 작성한 코드입니다. 실제 프로젝트에는 영향을 주진 않습니다. (참고용으로만 사용하세요)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerCtrlByEvent : MonoBehaviour
{
private InputAction moveAction;
private InputAction attackAction;
private Animator anim;
private Vector3 moveDir;
void Start()
{
anim = GetComponent<Animator>();
moveAction = new InputAction("Move", InputActionType.Value);
moveAction.AddCompositeBinding("2DVector")
.With("Up","<Keyboard>/w")
.With("Down","<Keyboard>/s")
.With("Left","<Keyboard>/a")
.With("Right","<Keyboard>/d");
moveAction.performed += ctx => {
Vector2 dir = ctx.ReadValue<Vector2>();
moveDir = new Vector3(dir.x, 0, dir.y);
anim.SetFloat("Movement", dir.magnitude);
};
moveAction.canceled += ctx => {
moveDir = Vector3.zero;
anim.SetFloat("Movement", 0.0f);
};
moveAction.Enable();
attackAction = new InputAction("Attack", InputActionType.Button, "<Keyboard>/space");
attackAction.performed += ctx => {
Debug.Log("Attack by c# event");
anim.SetTrigger("Attack");
};
attackAction.Enable();
}
void Update() {
if (moveDir != Vector3.zero) {
transform.rotation = Quaternion.LookRotation(moveDir);
transform.Translate(Vector3.forward * Time.deltaTime * 4.0f);
}
}
}