3. TileEntity에서의 Capability 구현 - Tictim/Capabilities-Within-TileEntity GitHub Wiki

대충 Capability의 등록 방법을 알아봤으니, Capability를 이용한 TileEntity를 제작하고 이를 이용해 봅시다. 아주 간단한 요약본은 다음과 같이 정의될 수 있습니다.

public class MyTileEntity extends TileEntity {
    private final IMyAwesomeMagic myAwesomeMagic = /* ... */;

    private final LazyOptional<IMyAwesomeMagic> myAwesomeMagicCap = LazyOptional.of(() -> myAwesomeMagic);

    @Override
    public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side){
        if(cap==ModCapabilities.MY_AWESOME_MAGIC){
            // cast()는 임의 타입의 LazyOptional을 LazyOptional<T>로 강제 캐스팅하는 메소드입니다.
            // 아래 방법과 같이 형변환을 수행할 수도 있습니다만, 이는 컴파일 단계에서 경고를 일으키게 됩니다.
            //   return (LazyOptional<T>)myAwesomeMagicCap;
            // 물론 cast()가 안전하다는 소리는 절대 아닙니다. 만약 엉뚱한 타입의 Capability에 반응하여 cast()를 사용하였다면, LazyOptional이 접근되는 즉시 ClassCastException을 일으킬 것입니다. 이 상황과 같이 정확히 무슨 타입이 목적인지 알고 있을 때에만 사용하세요.
            return myAwesomeMagicCap.cast(); 
        }
        return super.getCapability(cap, side);
    }
}

TileEntity 내부에 Capability가 존재하지 않을 시 super 메소드로 콜을 넘기는 부분에 주목하세요. 왜냐하면 비록 TileEntity 클래스 자체에서는 아무런 Capability를 자체적으로 제공하지는 않지만, 포지는 이벤트를 통해 TileEntity에 원하는 Capability를 부착할 수 있게 하는 기능을 제공합니다. 따라서 super 메소드를 호출해서 이벤트를 통해 추가된 Capability 또한 체크되도록 해야 합니다.

만약 특정 방향에만 반응하고 싶으시다면, 예를 들어 위 또는 아래에서의 접근에만 Capability를 반환하고 싶다면, 간단하게 다음과 같이 메소드를 확장하면 됩니다.

@Override
public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side){
    if(cap==ModCapabilities.MY_AWESOME_MAGIC){
        if(side==null||side==Direction.UP||side==Direction.DOWN) return myAwesomeMagicCap.cast();
        else return LazyOptional.empty(); // "기능 없음"은 null이 아니라 LazyOptinal.empty()입니다!
    }
    return super.getCapability(cap, side);
}

side는 null일 수 있다는 점 기억하세요. null을 switch문 등에 사용하게 되면 게임이 터집니다.

만약 우리가 원하는 기능이 read/write를 요구하지 않고, 다른 어떠한 내부 상호작용 또한 요구하지 않는다면, 우리에게 필요한 코드는 다음으로 충분합니다.

public class MyTileEntity extends TileEntity {
    private final LazyOptional<IMyAwesomeMagic> myAwesomeMagic = LazyOptional.of(MySimpleAwesomeMagic::new);

    @Override
    public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side){
        if(cap==ModCapabilities.MY_AWESOME_MAGIC){
            return myAwesomeMagic.cast();
        }
        return super.getCapability(cap, side);
    }
}

LazyOptional의 생성 시 받은 Supplier는 첫 번째 값 요청 시 단 한 번 호출되며, 이후 LazyOptional은 인스턴스가 지워질 때까지 해당 값을 유지합니다.

이와 여러분의 기능이 NBT로 저장되고 로드되어야 한다면, 당연히 이는 따로 핸들링되어야 합니다.

public class MyTileEntity extends TileEntity {
    private final MyAwesomeMagicWithSerializeAndDeserializeAbility myAwesomeMagic = /* ... */;

    private final LazyOptional<IMyAwesomeMagic> myAwesomeMagicCap = LazyOptional.of(() -> myAwesomeMagic);

    @Override
    public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side){
        if(cap==ModCapabilities.MY_AWESOME_MAGIC){
            return myAwesomeMagicCap.cast();
        }
        return super.getCapability(cap, side);
    }

    @Override
    public void read(BlockState state, CompoundNBT nbt){
        super.read(state, nbt);
        myAwesomeMagic.read(nbt);
    }

    @Override
    public CompoundNBT write(CompoundNBT nbt){
        super.write(nbt);
        myAwesomeMagic.write(nbt);
        return nbt;
    }
}

여기서 가장 중요한 점은 myAwesomeMagic 필드는 final이며, 레퍼런스가 절대 변하지 않는다는 점입니다. 기억하세요! LazyOptional의 값은 단 한 번 evaluate됩니다. LazyOptional의 고급 사용법에서 다루는 절차를 통해 Capability로 제공하는 인스턴스를 변경할 수 있는 옳은 방법이 존재하기는 하지만, 여전히 LazyOptional 인스턴스를 바꾸는 것은 굉장히 귀찮은 절차를 요구하기에 가능한 한 권장되지 않는 구조입니다.

또한 이 말은 아래와 같은 구현 또한 사용해서는 안 된다는 의미입니다.

@Override
public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side){
    if(cap==ModCapabilities.MY_AWESOME_MAGIC){
        return LazyOptional.of(() -> myAwesomeMagic); // 나쁜 코드!
    }
    return super.getCapability(cap, side);
}

위 코드는 getCapability 콜마다 새로운 LazyOptional 인스턴스를 생성하게 되며, 이는 LazyOptional의 고급 사용법에서 설명되는 이상적인 구조와는 거리가 멉니다.