형식이나 멤버, 연산을 구체적으로 결정하는 과정을 말한다. 변수를 예로 들면, 변수를 구성하는 식별자, 자료형 속성, 하나 이상의 주소, 자료 값에 구체적인 값으로 확정하는 것을 말한다. 이런 바인딩을 하는 시점에 따라 동적 바인딩, 정적 바인딩으로 나뉜다.
동적 바인딩
실행 시점에 진행되는 바인딩이다. 동적 바인딩은 컴파일 시점에서 어떤 함수나 멤버, 연산이 존재한다는 점을 프로그래머 자신을 알고 있지만 컴파일러는 알지 못하는 경우에 유용하다.
dynamic 키워드를 이용해 선언한다. 프로그래머는 d의 Quack()라는 함수가 있다고 가정하고 코드를 작성한다. 이렇게 하면 적으로 d와 Quack()은 바인딩된다.
정적 바인딩
먼저 컴파일러는 p의 GetObj() 이름의 매개변수 없는 메서드를 찾는다. 그런 메서드가 없으면 컴파일러는 선택적 매개변수를 받는 메서드, 그다음에는 Progoram의 기반 클래스에 있는 메서드 여기에도 없으면 확장 메서드까지 찾는다. 없으면 컴파일 오류를 뱉는다. 정적 바인딩은 컴파일러가 바인딩을 과정을 수행하며 이미 선언되어 컴파일러가 확실하게 파악된 형식만 가능하게 되는 것이다.
컴파일러는 T를 가벼운 불변이 구조체인 System.Nullable<T>로 바꾸어 컴파일한다. 이 구조체에는 두가지 필드가 존재한다. Value와 HashValue라는 두 필드만 있다.
아시다시피 C#에서 정수,구조체 등은 Value타입들은 null을 가질 수 없다. 하지만 이런 값타입도 값이 할당되지 않은 상태를 표현하고자 하려할때 Nullable구조체를 사용한다.
컴파일러는 위쪽 코드를 아래와 같이 바꾸어서 컴파일한다.
HashValue가 거짓인 널 가능 객체의 value를 조회하려 하면 InvalidOperationException이 발생한다. GetValueOrDefault()메서드 HashValue가 참이 면 value를 돌려주고 그렇지 않으면 new T()또는 지정된 커스텀 기본값을 돌려준다.
1. 인터페이스가 메서드를 하나만 정의할 때 & 3. 여러 종류의 메서드를 인터페이스를 여러 번 구현해야 한다
위 예시처럼 ITransformer인터페이스는 메서드를 하나만 가지고 있다. 이럴 경우 왜 델리게이트 더 좋냐면, 인터페이스 같은 경우 하나의 클래스에서는 하나의 메서드만 넘길 수 있는 반면에 델리게이트로 구현하게 되면 다른 메서드(int로 반환하는 메서드) 면 어떠한 메서드도 넘길 수 있기 때문이다.
델리게이트로 하면 Square 1도 되고~ Square2되지만
인터페이스로는
각각 클래스에 인터페이스를 상속받아넘겨야 한다.
2. 다중 캐스팅 능력이 필요하다.
하나의 대리자 인스턴스가 여러 개의 메서드를 지칭할 수 있는 델리게이트 기능이 필요하다면. 예를 들어
여러 형식들에서 재사용할 수 있는 코드를 작성하기 위한 메커니즘이 두 가지 있는, 하나는 상속이고 또 하나는 제네릭이다. 이 둘은 개별적인 메커니즘으로 상속은 기반 형식을 이용해서 재사용성을 표현하는 반면, 제네릭은 '자리표'에 해당하는 형식들은 담은 '템플릿'을 통해서 재사용성을 표현한다. 상속과 비교할 때, 제네릭을 사용하면 형식 안전성이 증가하고 캐스팅과 박싱이 줄어든다.
제네릭 형식
형식 매개변수(type parameter)들을 선언한다. 형식 매개변수는 제네릭 형식이 실제로 쓰일 때 해당 코드가 제공한 실제 형식들이 대신할 자리를 표시하는 '자리표에'해당한다. 말이 어렵지 어떻게 사용하는지 보면 쉽게 이해할 수 있다.
간단하게 스택을 제네릭 형식으로 만든 예제이다. 이렇게 만드면 타입을 상관없이 스택에 담을 수 있게 된다.
적용방법
이렇게 타입별로 클래스를 인스턴싱한 후 원하는 타입으로 사용할 수 있다. 이렇게 하면 위에서 나온 재사용이 가능해진 것이다.
Object형식으로 만들면?
모든 타입을 포함하는 상위 클래스인 Object형식으로 스택을 만들면 가능하지만 이 제네릭과는 조금 다르다. object로 담을 수 있기 때문에
이런 식으로 여러 타입의 값들을 같은 배열에 담을 수 있다.(이렇게 사용하지는 않을 것 같음) 하지만 이것 자체가 박싱이며, 값들을 꺼낼 때도 하향 캐스팅이 필요하다. 박싱과 하향 캐스팅 둘 다 컴파일 시점에서 형식 점검이 일어나지 않기 때문에 실수할 가능성이 높다.
지금은 아무 에러가 없지만 프로그램을 실행하면 컴파일 에러가 난다.
제네릭 스택은 인스턴스를 만들때 한번 설정한 타입으로 컴파일 시점에서 점검이 이루어지기 때문에 안전하게 사용이 가능한 것이다.
Object형식은 모든 형식의 궁극적인 기반 클래스이다. 그 어떤 형식도 Object로 상향 캐스팅할 수 있다. 범용적으로 쓰이는 Stack구조를 살펴보자
class Stack
{
public int Position;
object[] array = new object[10];
public void Push(object o)
{
array[Position++] = o;
}
public object Pop()
{
return array[Position--];
}
}
static void Main(string[] args)
{
Stack stack = new Stack();
stack.Push("Apple");
string answer = (string)stack.Pop(); //하향 캐스팅
}
그 어떤 형식의 인스턴스라도 push와 pop이 가능하다. 하지만 Object형식이기 때문에 Pop을 할 때는 명시적 캐스팅이 필요하다. 또, object는 하나의 클래스이며, 따라서 참조 형식이다. 그렇긴 하지만 int 같은 값 형식과 object사이의 캐스팅도 가능하다. c#의 이러한 특징을 형식 통합이라고 부른다.
Object(참조) 형식도 int(값) 형식으로 변환이 가능하다.
박싱과 언박싱
위에서 살짝 언급한 내용으로 값 형식 인스턴스를 참조 형식 인스턴스로 변환하는 것을 박싱이다. 그럼 그 반대는? 언박싱이겠지라고 생각하는데 맞기는 하는데 한 가지 조건이 있다. 바로, 박싱 과정을 거친 객체를 되돌리는 것이 언박싱이다.
간단한 예제로 살펴보자
1.박싱
int x = 10;
object obj = x; //int 박싱
1.언박싱
int x = 10;
object obj = x; //int 박싱
int y = (int)obj; //int 언박싱
박싱은 그냥 하면 되고, 언박싱은 명시적 캐스팅이 필요하다. 이렇게 쉽게 참조 타입과 값 타입 사이에서 형식 변환을 쉽게 할 수 있다는 것은 매우 편리하다. 모든 함수와 자료구조 타입을 objct형식으로 만들어놓으면 캐스팅만 잘 사용한다면 코드 중복 없이 짧고 쉽게 설계가 가능할 것 같아 보인다. (이 세상에서 분명 공짜는 없다. 박싱과 언박싱에서는 단점이 있다. 다음에 설명 )
하지만, 기억해야 할 것은 objct는 참조 형식이고, int는 값 형식이다.
박싱과 언박싱의 복사 의미론
박싱(값 -> 참조)은 값 형식 인스턴스를 새 객체로 복사하고, 언박싱(참조 -> 값)은 객체의 내용을 다시 값 형식 인스턴스로 복사한다. 말로 풀었을 때 무슨 소리인지 모르겠다. 이 과정은 어떤 과정인지 한 가지 예를 보고 살펴보자.
int i = 5;
object boxed = i;
i = 3;
Console.WriteLine(boxed);
Console.WriteLine(i);
Console.WriteLine(boxed);
먼저 출력 결과를 먼저 알려주면 위에서부터 5,3,5이다. 답을 맞혔다면 저 말을 이해가 된 것이다. boxed와 i는 서로 다른 인스턴스 즉, "i는 boxed라는 새로운 인스턴스를 만들고 자신의 값을 복사했다."라는 말이다. 그래서 i의 값을 바꿔도 boxed의 값은 변경되지 않는다.
자식 클래스에서 부모클래스로 접근은 가능하지만 자식클래스에서 부모클래스의 생성자는 자동으로 상속되지 않는다.
class Parent
{
public int X;
public Parent() { }
public Parent(int X)
{
this.X = X;
}
}
class Child : Parent
{
}
class Program
{
static void Main(string[] args)
{
Child child = new Child(123); //컴파일에러
Console.WriteLine(child.X);
}
}
자식클래스는 자신이 노출하고자 하는 생성자들을 반드시 '다시 정의' 해야한다.
class Parent
{
public int X;
public Parent() { }
public Parent(int X)
{
this.X = X;
}
}
class Child : Parent
{
public Child(int X) : base(X) { } //base 키워드를 사용하여 상속
}
class Program
{
static void Main(string[] args)
{
Child child = new Child(123);
Console.WriteLine(child.X);
}
}
만약 base키워드를 쓰지 않고 매개변수가 없는 생성자를 호출하면 부모클래스의 매개변수가 없는 생성자가 실행된다.
class Parent
{
public int X;
public Parent() { }
public Parent(int X)
{
this.X = X;
}
}
class Child : Parent
{
public int y;
}
class Program
{
static void Main(string[] args)
{
Child child = new Child();
Console.WriteLine(child.X);
}
}
부모클래스에 접근 가능한 기본생성자가 하나도 없으면 자식클래스는 생성자에서 반드시 base클래스를 사용해야한다.
public class Asset
{
}
public class Stock : Asset
{
}
class Program
{
static void Main(string[] args)
{
Stock stock = new Stock();
Asset asset = stock;
}
}
상향 캐스팅 연산은 파생 클래스 참조로부터 기반 클래스 참조를 생성한다.
stock 와 asset가 가리키는 객체는 같다. (stock == asset는 true)
public class Foo
{
public static Foo Instance = new Foo();
public static int X = 3;
Foo() { Console.WriteLine(X); }
}
public class Asset
{
public int nInt;
}
public class Stock : Asset
{
public float fFloat;
}
class Program
{
static void Main(string[] args)
{
Stock stock = new Stock();
Asset asset = stock; //상향 캐스팅
Console.WriteLine(asset.fFloat); //컴파일 오류
}
}
stock형식의 객체를 가리키긴 하지만 변수 자체의 형식은 Asset이기 때문에 fFloat변수접근이 불가능하다.
하향캐스팅
하향캐스팅 역시 변하는 것은 참조 뿐이다. 하향은 실행시점에서 실패 할 수도 있기 때문에 명시적으로 캐스팅이 필요하다.
public class Foo
{
public static Foo Instance = new Foo();
public static int X = 3;
Foo() { Console.WriteLine(X); }
}
public class Asset
{
public int nInt;
}
public class Stock : Asset
{
public float fFloat;
}
class Program
{
static void Main(string[] args)
{
Stock stock = new Stock();
Asset asset = stock; //상향 캐스팅
Stock a = (Stock)asset; //하향 캐스팅
Console.WriteLine(a.fFloat);
Console.WriteLine(a.nInt);
Console.WriteLine(asset == stock); // true
Console.WriteLine(asset == a); // true
Console.WriteLine(a == stock); // true
}
}
as 연산자
as 연산자도 하향 캐스팅을 수행한다. 위와 다른점은 실패할 경우 null로 평가된다.
class Program
{
static void Main(string[] args)
{
Asset a = new Asset();
Stock s1 = (Stock)a;
Stock s2 = a as Stock; // 예외를 발생하지 않음
}
}
마이크로 소프트 이니셔티브에서 제공하는 가상 머신의 구성요소이다. 프로그램 코드를 위한 실행 환경을 정의하는 마이크로소프트의 공통 언어 기반(CLI)표준의 기능이다. C#이나 VB닷넷과 같은 언어로 프로그래밍하며, 해당 언어의 컴파일러가 소스 코드를 공통 중간 언어(IL) 코드로 변환한다.
CLR은 관리되는 코드(managed code)를 실행하기 위한 런타임이다. C#은 여러 관리되는 언어중 하나인데, 관리되는 언어로 작성한 소스코드를 컴파일 하면 관리되는 코드가 생성된다. 관리되는 코드를 실행 파일또는 라이브러리(.dll)형태로 만들고 그것을 형식 정보, 즉 메타자료와 함께 하나의 패키지로 묶은 것을 어셈블리라고 한다. 어셈블리를 적재 할 때 CLR은 IL코드를 해당 컴퓨터(x86 등) 고유의 기계어 코드로 변환한다. 이러한 변환을 담당하는 것이 CLR의 JIT(Just - in - time)컴파일러이다. 어셈블리는 원래의 원본 언어의 구성을 거의 그대로 유지하기 때문에, 코드를 조사하기 쉽고 심지어 동적으로 생성하기도 쉽다.
1.이니셔티브 : 우선권, 주도권, 스스로 상황 판단을하고, 남들이 움직이기 전에 먼저 움직일 수 있는 능력
2.CLI : Command-Line user Interface또는 Character User Interface
무한한 메모리를 가지고 있는 기기는 없기 때문이다. 그렇기에 텍스처 압축 기술을 통해 텍스처 역시 디스크나 메모리의 크기를 절약해야 한다.
메모리 이슈
텍스처는 수많은 비트 데이터들도 이루어진 이미지 데이터이다. 그렇기에 GPU가 이 데이터를 이용하려면 대역폭이 필요하고 성능이슈가 발생하게 된다. 우리가 원하는 것은 적은 메모리로 합리적인 품질을 내야한다. 그렇기에 텍스처 데이터 사이즈를 관리해야한다.
하지만 텍스처 사이즈는 대역폭만의 문제만이 아니라 메모리 문제도 있다. 복잡한 씬을 렌더링하거나 데이터 사이즈가 큰 텍스처들을 한꺼번에 많이 올려두는 경우들이 있다. 이런 문제는 PC보다는 적은 메모리에 모바일 디바이스에서 더 큰 문제로 다가 올 수 있다.
패키지용량 이슈(모바일)
모바일 앱마켓에서 게임을 받을 때 WIFI에서만 다운로드가 가능하거나 권장하는 문구를 본적이 있다. 이렇게 큰 용량의 앱들을 받게 될때 거부감이 들때가 있다. 특히 WIFI가 없는 곳에서는 더더욱! 그렇기 때문에 초기에 모두 받지 않고 설치 이후 패치등으로 데이터드를 다운로드 받게 하는데 이 역시도 너무 큰 용량이라면 거부감이 들 수있다.
압축방식
이미지 압축은 크게 손실압축과 비손실 압축으로 두 종류가 존재한다. 비손실 압축은 원본 이미지를 그대로 유지하면서 데이터만 압축하는 방식이며, 비손실 압축은 원본 이미지의 퀄리티를 어느정도는 훼손하는 대신에 데이터 압축 비율을 높여서 데이터 크기 절약에 집중하는 방식이다. JPG와PNG는 GPU가 바로 읽어 들일 수 없다. 그렇기 때문에 텍스처를 위한 별도의 압축 방식을 이용해야한다. 유니티는 어떤 이미지를 Import하면 원본 이미지 포맷이 PNG인지 JPEG인지 등은 상관없이 특정 포맷의 이미지로 변환하여 사용한다. 텍스처 압축은 플랫폼별 특성이 다르므로 플랫폼마다 적절한 압축 포맷을 설정해주는게 좋다.
빛이 지나가는 경로에 불투명한 물체가 존재하여 빛이 통과하지 못해 생기는 어두운 부분이다. 게임에서 그림자는 모든 물체를 입체감을 더해주는 요소이다. 그림자가 있으면 사물의 공간산 위치를 인지하기 쉽게 만들어주기 때문이다. 유니티는 그림자 또한 쉽게 표현하도록 기능이 구현되어있다.
2. 그림자의 원리
유니티는 쉐도우맵기법을 사용하고 있다. 쉐도우 맵은 깊이 텍스처 값을 이용한 기법이다. 이 기법은 모든 굴곡에서 대응하고 셀프-쉐도우가 처리되는 등 높은 퀄리티를 보여준다.
쉐도우 맵의 원리? (포워드 렌더링)
먼저 뎁스 텍스처를 생성한다. 카메라를 통해서 현재의 씬이 렌더링 되는 정보를 렌더링 하는 과정이다. 렌더링 될 픽셀의 컬러 대신 카메라로부터 픽셀의 위치까지의 거리를 렌더링 한다. 카메라의 가까이 있는 픽셀일 수로 록 검은색, 멀 수록 흰색이다(그림자에 영향을 받는 모든 오브젝트들을 렌더링). 그 후 그림자 처리를 위한 별도의 버퍼가 필요하다. 이 버퍼에는 광원에서 바라보는 오브젝트의 픽셀의 깊이를 저장한다. 이 버퍼가 그림자를 위한 정보를 담아서 쉐도우 맵이다.
그림자의 깊이를 저장하는 버퍼를 만든 후 픽셀 쉐이더에서 깊이를 비교하는 과정을 진행한다. 또한 넓은 영역을 커버하기 위해 여러 구역으로 나누기도 하고 계단 현상을 없애기 위해 여러 번 샘플링하여 필터링 처리를 하는 부가 기능들을 추가한다. 그러다 보니 그림자가 렌더링 비용 중 많은 부분을 차지하게 된다.
뎁스 텍스처와 쉐도우 맵이 완성되면 뎁스 텍스처에 있는 정보와 쉐도우 맵에 있는 정보를 비교하여 그림자 영역을 계산한다. 뎁스 텍처에 있는 픽셀들을 순회하면서 해당 픽셀을 광원으로부터의 거리로 변환하여 비교한다.
2. 어떤 그림자를 이용해야 할까
물론 실시간으로 그림자를 활성화시키고 유니티의 그림자 구현능력을 믿어보면 간단하고 편하다. 하지만 그림자를 렌더링 하는 것은 높은 성능을 요구하기 때문에 이렇게 그냥 맡기는 것은 무책임하다. 그렇기에 만들려는 게임이 무엇인지, 어떤 디바이스를 타깃으로 만들 건지 생각하고 그에 맞는 세팅이 필요하다.
아래는 유니티가 제공하는 컴포넌트에서 쉽게 설정할 수 있는 셋팅방법이다.
Mesh Renderer는 메쉬 필터의 지오메트리를 사용하여 오브젝트의 Transform컴포넌트에서 정의된 위치에서 렌더링 한다. 프로퍼티를 간단하게 살펴보면
Cast Shadows : 그림자 라이트가 비춰질 때 메쉬가 그림자를 만든다.
ReceiveShadows : 오브젝트에 다른 오브젝트의 그림자를 받는 설정이다. 실시간으로 그림자가 적용된다.
1. 그림자를 필요로 하지 않는 오브젝트인 경우는 CastShadow를 꺼놓아야 드로우콜을 절약할 수 있다.
2. ShadowsOnly로 선택하면 최종 화면에는 렌더링 되지 않지만 쉐도우 맵에는 반영되어 그림자를 만드는 오브젝트가 된다. 즉 화면에는 보이지 않는 특성이 있어 그림자 처리만을 위한 3D 모델에 활용할 수 있다.
씬뷰에는 메시만 보이고 게임 뷰에서는 그림자만 보인다.
3. Light -> Shadow Type
Soft Shadows 가 좋은 만큼 성능도 많이 차지한다. 픽셀 쉐이더의 부담이 된다. 모바일이라면 되도록 HardShadows를 사용한다.