오클루전 컬링이란 다른 오브젝트에 의해 가려져서 카메라에 보이지 않을 때 (Occlusion)
해당 오브젝트의 렌더링을 비활성화 하는 기능이다.
기본적으로 카메라는절두체(프러스텀) 컬링을 수행하여 카메라의 뷰 절두체에 속하지 않는 모든 렌더러를 제외한다.
하지만 절두체 컬링은 렌더러가 다른 게임 오브젝트에 의해 가려지는지를 확인하지 않으므로,
Unity가 최종 프레임에 표시되지 않는 렌더러에 대한 렌더링 작업에 CPU 및 GPU 시간을 여전히 낭비할 수 있다.
오클루전 컬링은 Unity가 이러한 낭비되는 작업을 수행하지 못하도록 방지한다.
오클루더: 다른 오브젝트를 가리는 역할. 사전 연산을 걸쳐서 데이터를 미리 Bake 해놓는다. 따라서 Static 오브젝트이다.
오클루디: 오클루더에 의해 가려지는 오브젝트.
2. 오클루전 컬링을 사용해야 할 때
오클루전 컬링은 작고 윤곽이 또렷한 영역이 견고한 게임 오브젝트에 의해 서로 명확하게 분리된 씬에서 가장 잘 작동한다. (EX. 복도로 연결된 방)
오클루전 컬링을 사용하여 동적 게임 오브젝트를 가릴 수 있지만, 동적 게임 오브젝트는 다른 게임 오브젝트를 가릴 수 없다. 프로젝트가 런타임 시점에 씬 지오메트리를 생성하는 경우에는 Unity의 빌트인 오클루전 컬링이 프로젝트에 적합하지 않다. (런타임 시 생성되는 장애물은 오클루전 적용이 불가능)
야외 씬 같이 오브젝트끼리 가려지는 경우가 많지 않은 경우에는 오브젝트를 가림으로써 얻는 이득보다 오클루전 컬링 연산에 드는 비용이 더 클 수 있다.
한 마디로, 오클루전 컬링은 좁은 맵에서 움직이지 않는 견고한 장애물이 많을 때 효과적이다.
Delegate는 C++의 함수 포인터와 비슷한 개념으로 함수의 레퍼런스를 리스트에 저장하여 사용한다.
(정확히는 객체의 인스턴스 참조와 메서드의 참조를 동시에 갖고 있다.)
함수 포인터는 CallBack (함수가 함수를 부름)을 하기 위해 주로 사용되었는데,
Delegate도 메서드를 다른 메서드로 전달할 수 있도록 하기 위해 만들어졌다.
Delegate는 메서드의 파라미터로 호출되어 다른 메서드에 접근이 가능하고, 그냥 호출되어 모든 함수를 부를 수도 있다.
또 자료형, 매개변수가 같은 비슷한 역할의 메서드를 서로 연결해서 한 번에 활용할 수도 있다.
Delegate Chain이라고 부르고 연결된 메서드 리스트는 .Net MultiCastDelegate 클래스에서 관리한다.
// 델리게이트 선언. 파라미터로 사용하기 위해서 public 처리해 주었다.
public delegate int DelEx(params int[] a);
// 델리게이트 예시 클래스. 싱글톤을 이용해 만들어보았다.
public class DelegateEx
{
private static DelegateEx _instance;
public static DelegateEx instance
{
get {
if (_instance == null) _instance = new DelegateEx();
return _instance;
}
}
// 델리게이트에 넣어줄 함수 1.
public int Plus(params int[] a) {
int sum = 0;
foreach (var i in a) { sum += i; };
Console.WriteLine("Plus에 들렀습니다 : {0}", sum);
return sum;
}
// 델리게이트에 넣어줄 함수 2.
public int Minus(params int[] a) {
int sum = 0;
foreach (var i in a) { sum -= i; };
Console.WriteLine("Minus에 들렀습니다 : {0}", sum);
return sum;
}
// 델리게이트 사용 함수.
public void DelChain()
{
// 최초 인스턴스 생성 시 반드시 할당 해주어야 한다.
DelEx del = new DelEx(Plus);
// new를 생략하여 사용도 가능.
del = Minus;
// 연산자를 이용하여 델리게이트 내부에 더하거나 빼줄수 있음.
del += Plus;
// 무명 메서드를 활용.
del += delegate { Console.WriteLine("무명 메서드를 활용 : {0}", 777); return 15; };
Console.WriteLine("가장 마지막 함수의 반환값입니다 : {0}", del(5, 5, 5));
DelExample(del);
}
// 메서드의 파라미터로도 사용할 수 있다.
public void DelExample(DelEx ex)
{
// WriteLine은 가장 끝의 인덱스에 있는 리턴 정보를 가져온다. 즉, Minus의 반환값이 아닌 무명 메서드의 반환값만 가져온다.
Console.WriteLine("가장 마지막 함수의 반환값입니다 : {0}", ex(5, 5, 5));
}
}
class Program
{
static void Main(string[] args)
{
DelegateEx.instance.DelChain();
}
}
Minus에 들렀습니다 : -15
Plus에 들렀습니다 : 15
무명 메서드를 활용 : 777
가장 마지막 인덱스의 반환값입니다 : 15
Minus에 들렀습니다 : -15
Plus에 들렀습니다 : 15
무명 메서드를 활용 : 777
가장 마지막 메서드의 반환값입니다 : 15
이 코드를 작성하면서알게 된점인데, 디버깅 시 자주 쓰는 Console.WriteLine 함수에
복수의 레퍼런스를 가진 델리게이트가 들어가면 가장 마지막에 들어간레퍼런스의 메서드 리턴값만호출된다.
2. Event
델리게이트는 보안 수준이 낮으면 클래스 내부에 있어도 다른 멤버처럼 외부에서 접근할 수 있다.
또 할당을(=) 통해 리스트의 멤버를 초기화 할 수도 있는데,
이러한 특징에서 나오는 단점을 보완하기 위하여 사용되는 것이 Event이다.
Event는 클래스 내부에서는 델리게이트와 비슷하게 동작하나
클래스 외부(정확히는 다른필드)에서는 호출과 할당을 할 수 없으며, 오직 연산자를 이용한 멤버 증감만 가능하다.
Event는 이름 그대로 어떤 메서드의 이벤트가 된다.
Event에 참여한 메서드는 Event가 발생 시(호출) 그 쪽을 주목(Notify)하고 행동한다.
디자인 패턴 중 옵저버 패턴이 이와 같은 특징을 가지고 Event를 사용하여 자주 구현된다.
public class EventEx
{
public delegate int ExDel(params int[] a);
// event는 delegate와 보안 수준을 맞춰주어야 한다.
public event ExDel exEvent;
// 이벤트에 넣어줄 함수 1.
public int Plus(params int[] a)
{
int sum = 0;
foreach (var i in a) { sum += i; };
Console.WriteLine("Plus에 들렀습니다 : {0}", sum);
return sum;
}
// 이벤트에 넣어줄 함수 2.
public int Minus(params int[] a)
{
int sum = 0;
foreach (var i in a) { sum -= i; };
Console.WriteLine("Minus에 들렀습니다 : {0}", sum);
return sum;
}
public void UseEvent() {
// 클래스 내부에서는 호출 및 할당 둘 다 가능.
ExCallBack(exEvent);
exEvent = Plus;
}
void ExCallBack(ExDel exDel) {
exDel(5, 5, 5, 5, 5);
}
}
class Program
{
static void Main(string[] args)
{
EventEx eventEx = new EventEx();
// 증감 연산자를 이용한 접근만 가능.
eventEx.exEvent += eventEx.Plus;
eventEx.exEvent += eventEx.Minus;
// 클래스 외부에서 호출 및 할당 불가.
// eventEx.exEvent = eventEx.Plus;
// eventEx.exEvent();
eventEx.UseEvent();
}
}
Plus에 들렀습니다 : 25
Minus에 들렀습니다 : -25
델리게이트는 메서드 간의 연결을 담당할 수 있다는 점에서 굉장히 매력적이지만 메서드와 객체의 레퍼런스를 가지고 있다는 점에서 다루기 예민한 부분도 있다. (물론, 레퍼런스가 아닌 주소를 직접 가진 함수 포인터보다는 안전하다.)
예민한 부분을 다루는 델리게이트를 개발 중 실수를 방지하는 것이 Event의 역할이라고 볼 수 있다.
in, out 키워드는 내부적으로 ref 키워드를 붙였을 때의 동작 방식과 같으며 특정 제약 조건이 추가된 것이다.
1. params (가변 매개 변수)
params 키워드는 매개 변수의 영문형 parameter의 복수형을 키워드로 표현한 것이다.
가변 길이의 매개 변수를 지정할 수 있고 1차원 배열만 매개 변수로 사용할 수 있다.
params을 사용하면 인수 배열로만 사용이 가능해지고 인수가 없어도 해당 함수를 사용할 수 있다.
using System;
using System.Collections;
namespace Intro_Ex1
{
class Example
{
public void UseParams(params int[] list) {
foreach (int param in list) {
Console.WriteLine(param);
}
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
Example example = new Example();
int[] array1 = { 1, 3, 5, 7, 9 };
// 쉼표로 구분된 인수 목록 사용
example.UseParams(array1);
// 지정된 형식의 인수 배열을 사용
example.UseParams(2, 4, 6, 8, 10);
// 가변 길이를 사용 가능하기에 인수가 없어도 사용 가능
example.UseParams();
}
}
}
1
3
5
7
9
2
4
6
8
10
또한, param 키워드는 매개 변수 목록의 끝에 반드시 위치 해야한다.
예를 들면,
void UseParams(int a, int b, params int[] list) 처럼 위치가 맨 뒤면 가능하고
void UseParams(params int[] list, int a, int b) 처럼 위치가 맨 뒤가 아니면 불가능하다.
2. ref
ref키워드는 값이 참조(reference)로 전달됨을 나타낸다. (Call by Reference)
참조로 값을 보낼 시 해당 실제 값에 직접 접근할 수 있으므로
전달받은 매개 변수의 실제 값을 함수 내에서 변경하는 것이 가능해진다.
즉, C#에 존재하지 않는포인터의 기능을 ref가 대신한다고 볼 수 있다.
ref를 사용하면 데이터를 양방향으로 교환이 가능해진다.
또한, ref는 몇 가지 특징을 가진다.
매개 변수는 메서드로 전달되기 전 반드시 초기화 해야한다.
호출 메서드로 돌아가기 전에 초기화 할 필요는 없다.
out 키워드를 사용한 메서드와 오버로딩 불가능하다. (컴파일이 동일하게 처리되기 때문)
using System;
using System.Collections;
namespace Intro_Ex1
{
class Example
{
public void UseParams(params int[] list) {
foreach (int param in list) {
Console.WriteLine(param);
}
Console.WriteLine();
}
public void UseRefs(ref int[] list) {
list = new int[] { 3, 6, 9, 12, 15 };
}
public void NonUseRefs(int[] list) {
list = new int[] { 3, 6, 9, 12, 15 };
}
}
class Program
{
static void Main(string[] args)
{
Example example = new Example();
// 초기화 되어있는 데이터를 사용하여야 한다.
int[] array1 = { 1, 3, 5, 7, 9 };
// 키워드 미사용 시
example.NonUseRefs(array1);
example.UseParams(array1);
// 키워드 사용 시 (꼭 인수 앞에 키워드를 붙여주어야 한다.)
example.UseRefs(ref array1);
example.UseParams(array1);
}
}
}
1
3
5
7
9
3
6
9
12
15
또한, ref는 참조 변수로도 활용이 가능한데 C++의 참조자(&)와도 어느정도 비슷한 규칙이 있다.
// 주소값이 없는 rvalue를 할당하고 있으므로 불가능. 키워드를 붙여도 불가능하다.
ref int a = 2;
ref int b = null;
// 선언 직후에 바로 할당하지 않았으므로 사용 불가능하다.
ref int c;
c = ref a;
// 주소값이 있는 lvalue를 할당하고 있으니 가능. 꼭 ref 키워드를 붙여주어야 한다.
ref int d = ref a;
3. out
out 키워드도 ref와 유사한 기능을 한다.
ref와는 몇 가지 차이점을 가지는데,
매개 변수는 메서드로 전달되기 전에 초기화 할 필요는 없다.
호출 메서드로 돌아가기 전에는 반드시 초기화해야 한다.
ref 키워드를 사용한 메서드와 오버로딩 불가능하다. (컴파일이 동일하게 처리되기 때문)
using System;
using System.Collections;
namespace Intro_Ex1
{
class Example
{
// 함수 내에서 매개 변수를 할당하지 않았으니 에러
public void UseOut1(out int[] list)
{
}
// 반드시 함수 내에서 매개 변수를 할당해주어야 한다.
public void UseOut2(out int[] list)
{
list = new int[] { 4, 8, 12, 16, 20 };
}
}
class Program
{
static void Main(string[] args)
{
Example example = new Example();
// 반드시 초기화 할 필요는 없다.
int[] array2;
example.UseOut1(out array2);
example.UseOut2(out array2);
}
}
}
4. in
in 키워드는 out과 반대이다.
차이점은 호출과 선언 모두 키워드를 붙여주어야 했던 ref, out과는 다르게
호출에는 안 쓰고 선언에만 쓰는 경우가 가능하다.
ref와 같이 반드시 초기화 된 변수만 사용할 수 있으며 읽기 전용으로만 사용이 가능하나 필수는 아니다.
즉, 외부의 값을 변경할 수 없다.
매개 변수는 메서드로 전달되기 전에 초기화 할 필요는 없다.
호출 메서드에서 초기화 할 수 없으며 읽기만 가능하다. (readonly)
ref 키워드를 사용한 메서드와 오버로딩 불가능하다. (컴파일이 동일하게 처리되기 때문)
using System;
using System.Collections;
namespace Intro_Ex1
{
class Example
{
public void UseIn(in int[] list)
{
// 읽기 전용이므로 할당 및 초기화 불가능
list = new int[] { 4, 8, 12, 16, 20 };
// 읽기만 가능하나 반드시 해야할 필요는 없다.
int[] s = list;
}
}
class Program
{
static void Main(string[] args)
{
Example example = new Example();
// 반드시 초기화 되어야 한다.
int[] array2 = { 1, 2, 3, 4, 5 };
// ref, out하고는 다르게 호출 시 키워드 선택 가능
example.UseIn(in array2);
example.UseIn(array2);
}
}
}
다른 클래스에서 공통적으로 많이 접근하는 클래스가 있을 경우 (ex GameManager)
해당 클래스의 인스턴스(실체)를 하나로 유지하여 프로젝트에서 클래스의 데이터를 유지하고 객체를 단일화한다.
2. Unity에서 Singleton을 사용하는 이유
Manager류의 게임을 관리하는 오브젝트를 생성할 때
인스턴스가 하나이기 때문에 데이터를 쉽게 유지할 수 있고,
정적 선언을 하기 때문에 그 유지된 데이터를 다른 클래스에서 쉽게 접근하여 사용이 가능하다.
3. 예제
public class GameManager : MonoBehaviour
{
// static으로 정적 선언하여 프로그램이 끝날 때까지 유지하고 private하여 다른 클래스에서 수정할 수 없게 한다
static GameManager _instance = null;
// public 선언하여 다른 클래스에서 접근할 수 있게 하고 get 속성을 통해 접근만 가능하게 한다
public static GameManager instance {
get {
if (_instance == null) {
_instance = new GameManager();
}
return _instance;
}
}
// MonoBehaviour를 상속받는 클래스가 인스턴스화 되는 과정을 막을 수 없으므로 Awake에서 null여부를 검사하여 예외처리를 한다
void Awake() {
// 인스턴스가 비어있을 경우(막 생성되었을 때) 인스턴스를 this로 지정한다
if (_instance == null) {
_instance = this;
}
// 인스턴스가 비어있지 않을 경우(씬 이동 같은 경우로 인해 MonoBehaviour가 호출되어 인스턴스가 재생성 될 때) 재생성된 오브젝트를 파괴해준다
else if (_instance != null) {
Destroy(this.gameObject);
}
// 씬 로드시 해당 오브젝트를 파괴하지 않아야 다음 씬에도 유지가 된다
DontDestroyOnLoad(this.gameObject);
}
}
4. 이외의 방법
이외에도 MonoBehaviour를 상속받지 않고 사용하는 방법도 있다.
이 경우 하이어라키에 있는 인스턴스를 신경쓰지 않아도 되는 장점이 생긴다.
즉, 씬 이동시 인스턴스가 재생성 되는 과정을 신경쓰지 않아도 되니 Awake에서 다시 검사해주는 과정이 필요가 없다.
하지만, 말 그대로 메모리에만 존재하고 하이어라키에는 보이지 않아 인스펙터로 시각적으로 정보를 확인할 수 없다.
Dependency Injection Pattern 줄여서, DI 패턴은 디펜던시를 따로 참조하지 않아도 주입해주는 기능을 한다.
// Case 1 (의존성이 있지만 스크립트 내에서 참조)
GameObject obj1;
void Awake() {
obj = GameObject.Find("Player");
}
// Case 2 (유니티 인스펙터에서 접근하여 의존성을 주입)
public GameObject obj1;
// Case 3 (유니티 인스펙터에서 접근하여 의존성을 주입)
[Serializefield]
GameObject obj1;
2. 장점
Reduced Dependencies - 종속성이 감소한다. - components의 종속성이 감소하면 변경에 민감하지 않다. More Reusable Code - 재사용성이 증가한다. - 일부 인터페이스의 다른 구현이 필요한 경우, 코드를 변경할 필요없이 해당 구현을 사용하도록 components를 구성할 수 있다. More Testable Code - 더 많은 테스트 코드를 만들 수 있다. - Mock 객체는 실제 구현의 테스트로 사용되는 객체 종속성을 components에 주입할 수 있는 경우 이러한 종속성의 Mock 구현을 주입할 수 있다. 예를 들어, Mock 객체가 올바른 객체를 반환할 때, null을 반환할 때, 예외가 발생할 때 모두 처리한다. More Readable Code - 코드를 읽기 쉬워진다. - components의 종속성을 보다 쉽게 파악할 수 있으므로 코드를 쉽게 읽을 수 있다.