2. Capability의 기본 구조 - Tictim/Capabilities-Within-TileEntity GitHub Wiki
알고 보니 포지에서 기본으로 제공하는 Capability
시스템이 우리가 원하던 바로 그것이었습니다! 물론 완전 그대로는 아니며, 여러 케이스에서 가장 적절하게 사용될 수 있도록 여러 부분이 확장되고 조정되어 있습니다.
우리의 시스템에서 IContentProvider
가 담당했던 역할을 처리하는 인터페이스는 ICapabilityProvider
입니다. ICapabilityProvider는 타일엔티티를 제외한 여러 인게임 인스턴스에서 공통적으로 사용되며, 이를 상속하는 오브젝트들에는 어떤 Capability라도 원하는 대로 붙여서 똑같은 방법으로 사용할 수 있습니다. ICapabilityProvider
를 기본적으로 구현하는 타입들은 다음과 같습니다.
- World
- TileEntity
- ItemStack
- Entity
- Chunk
보시다시피 TileEntity는 ICapabilityProvider
를 기본으로 구현하기에, 사용 시 ICapabilityProvider
로 형변환하지 않아도 됩니다.
// ICapabilityProvider로의 형변환 없음!
IMyAwesomeMagic myAwesomeMagic = te.getCapability(MY_AWESOME_MAGIC).orElse(null);
ICapabilityProvider
는 한 개의 추상 메소드와 한 개의 default 메소드를 포함합니다.
public interface ICapabilityProvider{
<T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side);
default <T> LazyOptional<T> getCapability(Capability<T> cap) {
return getCapability(cap, null);
}
} // 가독성을 위해 @Nonnull과 final은 제거되었습니다.
우리 시스템에서의 기능 요청은 Class
자체를 키 값으로 주어 핸들링했다면, Capability는 Capability
인스턴스를 키 값으로 사용합니다. 또한 ICapabilityProvider는 추가로 Direction
을 받는데, 이는 블럭의 면 중 어떤 부분에 해당하는 Capability가 요청되었는지를 나타냅니다. 특정 방향에서의 요청에만 응답하는 것으로 특정 면에서만 접근 가능한 인벤토리를 만들거나, 특정 면에서만 전기를 빼낼 수 있게 하는 등의 섬세한 작업이 가능합니다. 이 방향은 null
값으로 비워 둘 수 있으며, 방향이 없는 Capability 요청은 내부 혹은 자신에서의 요청을 나타냅니다.
또한 반환되는 인스턴스가 바로 지급되는 것이 아니라, LazyOptional
이라는 인스턴스를 통해 반환됩니다. LazyOptional
은 이름에서 알 수 있다시피 실제 값의 요청 시 인스턴스가 생성되는(Lazy) 존재할 수도 있고 없을 수도 있는 값(Optional)입니다. 하지만 이 클래스는 간단한 이름과는 다르게 상당히 비직관적인 구조와 쓰임새를 가지며, 추후에 더 자세히 설명됩니다.
Capability
는 특정 타입의 기능에 대한 키 값입니다. Capability는 다음과 같이 선언될 수 있습니다.
@CapabilityInject(IMyAwesomeMagic.class)
public static Capability<IMyAwesomeMagic> MY_AWESOME_MAGIC;
...잠깐, 값을 지정하지 않았으니 null
이잖아요? 저 이상한 어노테이션은 뭔가요? 기다려 보세요. 차근차근 설명드릴게요.
Capability는 필드로 선언되더라도 실제 인스턴스의 등록 없이는 null
의 상태를 유지합니다. Capability 인스턴스의 등록은 FMLCommonSetupEvent
에서 진행할 수 있습니다.
@SubscribeEvent
public static void setup(FMLCommonSetupEvent event){
CapabilityManager.INSTANCE.register(IMyAwesomeMagic.class, new Capability.IStorage<IMyAwesomeMagic>(){
@Nullable @Override public INBT writeNBT(Capability<IMyAwesomeMagic> capability, HotThing instance, Direction side){
return null;
}
@Override public void readNBT(Capability<IMyAwesomeMagic> capability, IMyAwesomeMagic instance, Direction side, INBT nbt){}
}, MySimpleAwesomeMagic::new);
}
CapabilityManager.INSTANCE.register
메소드는 3개의 인자를 받습니다.
-
첫 번째는 Capability가 가지는 클래스 타입입니다. Capability가 등록될 때, 포지는 전달된 클래스 타입의
CapabilityInject
를 가진 모든 선언된 Capability 필드에 인스턴스를 주입해 줍니다. 즉 위 예제에서는CapabilityManager.INSTANCE.register
가 실행되는 즉시 앞에서 선언한MY_AWESOME_MAGIC
필드는 자동으로 Capabillity 인스턴스를 전달받게 됩니다. 이는 선언된 필드를 가진 클래스의 존재 여부와 상관없이 작동됩니다. 어떻게요? 마법*으로!이 클래스는 어떤 것이든 넣을 수 있습니다. 여러분의 클래스가 아니어도 됩니다. 여러분이 원한다면
Integer
타입의 Capability를 등록할 수도 있습니다... 추천드리지는 않습니다만. 하지만 이미 Capability가 등록된 클래스의 Capability를 다시 등록하지는 마세요!이 말은 즉, Capability의 생성에 사용된 Class의 상속 관계는 Capability를 구분하는 것 외엔 별 역할을 하지 않는다는 뜻도 됩니다.
List
타입의 Capability와ArrayList
타입의 두 Capability는 사용된 클래스가 서로 상속 관계에 있지만, Capability는 서로 같지 않습니다. 따라서List
에 해당하는 기능을 제공하는ICapabilityProvider
는ArrayList
타입의 Capability로는 접근될 수 없을 수도 있습니다... 제공하는 기능의 타입이ArrayList
일지라도요. -
두 번째는 Capability의 NBT에서 읽어오는/NBT로 쓰는 기본 로직을 제공하는
Capability.IStorage
입니다. 이 로직은 지정하지 않을 수 있으며, 간단하게writeNBT
에서는null
을 반환할 수 있습니다. -
마지막은 Capability에 해당하는 기능 중 가장 보편적인 것들을 제공하는 기본 구현 인스턴스를 제공하는 Factory입니다.
저는 개인적으로 Capability의 이 부분은 사용해 본 적이 없습니다. 여러분의 기능을 하나의 ""보편적 기본 구현""에 얽매이게 만드는 것 자체부터 의문점을 가져 볼 만 한 부분입니다. 그래서 저는 팩토리의 호출에 다음과 같이 오류를 일으키도록 만듭니다.
() -> { throw new UnsupportedOperationException(); }
구현하지 않아도 된다는 특별한 언급은 없지만, 어차피 이 녀석을 사용하는 사람은 아무도 없기 때문에 이런 식으로 구현해도 별 탈 없습니다.[citation needed]
사실 저도 두 번째와 세 번째 패러미터가 존재하는 이유는 잘 모릅니다. 제가 알기로는 아무도 이 기능을 사용하지 않으니, 특별한 이유가 없으면 사용하지 않아도 될 것입니다. 여러분에게 중요한 부분은 Capability는 선언 이후에도 등록이 필요하며, 이는 CapabilityManager.INSTANCE.register
를 통해 이루어진다는 점입니다.
그리고 잊어버리기 쉬운 점 중에서 등록이 이루어지기 전에는 선언된 Capability는 null
이라는 점입니다. 만약 등록이 이루어지지 않았다면, 선언된 Capability는 그대로 null인 채로 존재합니다. 이는 나중에 보실 모드 간 상호작용에서 중요한 역할을 수행합니다.