인터페이스
여러 종류를 같은 코드로 다루는 방법, 그리고 클래스용과 컴포넌트용의 구분.
인터페이스는 "무엇인가"가 아니라 "무엇을 할 수 있는가"에 대한 약속입니다. 약속을 향해 코드를 쓰면, 그 약속을 지키는 모든 것에서 동작합니다.
public interface IShape
{
int Area();
}
public class Square : IShape
{
public int side;
public int Area()
{
return side * side;
}
}
public class Rect : IShape
{
public int width;
public int height;
public int Area()
{
return width * height;
}
}
IShape를 들고 있는 쪽은 그것이 어떤 종류인지 모른 채 넓이를 물을 수 있고, 답은 실제로 거기 있는 클래스에서 나옵니다.
IShape shape = square ? (IShape)new Square() : new Rect();
int area = shape.Area();
이 선택은 월드가 돌아가는 도중에 할 수 있습니다. 이것이 일상적인 쓰임이고, 인터페이스가 값을 하는 자리입니다. 코드 하나로 여러 종류를 다룹니다.
효과가 나는 곳
타입을 나타내는 필드에 대고 긴 if를 쓰게 될 것 같은 모든 곳입니다.
- 인벤토리 아이템. 사용했을 때 하는 일이 저마다 다름
- 퀘스트 단계. 완료 판정이 저마다 다름
- 플레이어에게 걸리는 효과. 규칙이 저마다 다름
- 무기. 발사한다는 것의 의미가 무기마다 다름
인터페이스로 써 두면 새로운 종류의 아이템을 더하는 일은 클래스 하나를 더하는 일입니다. IItem을 쓰는 쪽은 아무것도 바뀌지 않습니다.
컴포넌트는 다릅니다: 베이스 클래스를 쓰세요
인터페이스는 코드에서 직접 만드는 클래스를 위한 것입니다. 컴포넌트는 사정이 다릅니다. GameObject에 끌어다 놓고 Unity가 그 참조를 저장해야 하는데, Unity는 인터페이스 필드를 저장하지 못하기 때문입니다. 인터페이스 타입으로 두면 그 필드는 인스펙터에 아예 나타나지 않습니다.
그래서 공유하려는 대상이 behaviour라면 추상 베이스 클래스를 쓰세요.
public abstract class Door : UdoniteBehaviour
{
public abstract void Open();
}
public class SlidingDoor : Door
{
public override void Open() { /* ... */ }
}
public class SwingingDoor : Door
{
public override void Open() { /* ... */ }
}
이제 스위치는 필드 하나로 두 종류 모두를 다룹니다.
public class Switch : UdoniteBehaviour
{
public Door door; // SlidingDoor든 SwingingDoor든 여기에 끌어다 놓으세요
public override void Interact()
{
door.Open();
}
}
필드는 인스펙터에 나타나고, 두 문 모두 받아들이며, door.Open()은 맞는 쪽을 실행합니다. 베이스 클래스는 공유 상태와 공유 코드도 가질 수 있습니다. 인터페이스는 못 하는 일입니다.
이것은 평범한 Unity에서도 같은 이유로 같은 방식으로 하는 이야기입니다.
무엇을 쓸까
| 하고 싶은 것 | 쓸 것 |
|---|---|
| 코드에서 만드는 여러 클래스가 같은 약속을 갖게 하기 | 인터페이스 |
| GameObject 위의 여러 behaviour가 같은 약속을 갖게 하기 | 추상 베이스 클래스 |
| 상태나 메서드 본문을 공유하기 | 추상 베이스 클래스 |
베이스 클래스가 인터페이스를 구현할 수도 있으므로, 정말 필요할 때는 behaviour가 둘 다 할 수 있습니다.