5. LazyOptional의 고급 사용법 - Tictim/Capabilities-Within-TileEntity GitHub Wiki
LazyOptional
은 개념적으로 복잡하지 않지만, 여러 상황에서 그다지 직관적이지 않은 부분을 가지고 있습니다. 이것들을 모르고 지나친다면 잘못된 코드를 짤 가능성이 높습니다.
첫 번째로 중요한 LazyOptional
의 특성 중 하나는 재사용 가능하다는 점입니다. LazyOptional
은 다른 TileEntity와 같은 객체의 내부에 캐시될 수 있으며, 캐시된 LazyOptional
은 특수한 인풋 이전까지는 Capability에 대한 유효한 값을 제공해야 합니다. 다음 예제를 봅시다.
public class AnotherTileEntity extends TileEntity implements ITickableTileEntity{
private LazyOptional<IMyAwesomeMagic> adjacentMagic = LazyOptional.empty();
@Override
public void tick() {
if(world.isRemote) return;
IMyAwesomeMagic magic = adjacentMagic.orElse(null);
if(magic!=null) {
// Do the thing
}
}
public void refreshAdjacentMagic(){
TileEntity te = world.getTileEntity(pos.offset(Direction.UP));
if(te!=null) {
adjacentMagic = te.getCapability(ModCapabilities.MY_AWESOME_MAGIC);
}else adjacentMagic = LazyOptional.empty();
}
}
Capability를 사용하는 모습이 보이는군요... 하지만 이 코드는 뭔가 다릅니다. 이 TileEntity는 Capability를 제공하지 않습니다. 대신, 이 TileEntity는 자신의 바로 위 블럭에 있는 Capability를 사용합니다. 그리고 이 과정에서 전달받은 LazyOptional
을 내부에 저장하는 모습을 볼 수 있습니다. 이는 매 틱마다, 또는 매 접근마다 TileEntity에 접근하여 Capability를 뜯어오는 번거로운 과정을 생략할 수 있도록 합니다.
하지만 이 구조가 완벽한 것은 아닙니다. 만약 받아온 LazyOptional
이 공급원 TileEntity가 월드에서 사라지거나 하는 이유로 무효화되었다면, adjacentMagic
필드는 잘못된 값으로의 추가적인 접근을 막기 위해 LazyOptional.empty()
로 초기화되어야 합니다. 어떻게 해야 할까요? 한정된 상황에서라면 단순히 블럭의 변화를 체크할 수도 있겠습니다만, 만약 공급원 TileEntity의 내부에서 Capability로 반환한 LazyOptional
을 초기화하고 새 값을 반환해야만 한다면? ICapabilityProvider
는 TileEntity뿐만이 아니며, Capability의 사용처 또한 TileEntity에 한정된 것이 아닙니다. LazyOptional
의 수명을 관리하고 이를 통보해야 하는 상황은 충분히 존재합니다.
다행히도, LazyOptional
은 자체적으로 이러한 상황을 처리할 수 있도록 invalidate
메소드를 제공합니다.
myLazyOptional.invalidate();
invalidate
가 호출된 LazyOptional
은 그 기능을 영구적으로 잃습니다. LazyOptional의 값은 더 이상 접근될 수 없으며, isPresent
체크는 false
를 반환합니다.
또한 invalidate
가 호출될 때 LazyOptional
은 이벤트를 생성합니다. 아뇨, 포지 이벤트 말고요! 여기에서 이벤트는 LazyOptional
내부에서 작동하는 콜백을 이야기합니다.
myLazyOptional.addListener(lazyOptional -> {
// 이 코드는 `myLazyOptional.invalidate()` 메소드 내부에서 호출됩니다.
});
LazyOptional의 제공
그렇다면, 이 이벤트를 코드에 적용시켜 봅시다! 먼저 Capability를 제공하는 클래스부터 살펴봅시다.
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){
return myAwesomeMagicCap.cast();
}
return super.getCapability(cap, side);
}
@Override
public void remove(){
super.remove();
myAwesomeMagicCap.invalidate();
}
기존과 똑같은 코드 베이스에 추가적인 메소드 한 개를 오버라이드하여 invalidate
를 처리하는 것을 보실 수 있습니다. remove
는 타일엔티티가 블럭의 전환이나 언로딩 등으로 인해 월드에서 사라질 때 호출되며, TileEntity
의 내부에서 만들어진 모든 Capability
들이 여기에서 invalidate
되어야 합니다.
만약 여러분이 LazyOptional
을 통해 제공하던 인스턴스를 바꾸어야 한다면(매우 권장되지 않습니다만!), 이전 LazyOptional
을 버리기 이전에 invalidate
를 반드시 호출해야 합니다.
public class MyTileEntity extends TileEntity {
private IMyAwesomeMagic myAwesomeMagic = /* ... */;
// myAwesomeMagic 인스턴스가 교체될 때 호출해야 합니다!
private void setMyAwesomeMagic(IMyAwesomeMagic myAwesomeMagic){
this.myAwesomeMagic = myAwesomeMagic;
if(myAwesomeMagicCap!=null){
myAwesomeMagicCap.invalidate();
myAwesomeMagicCap = null;
}
}
// LazyOptional 인스턴스는 Capability의 요청에 맞추어 만들어도 충분합니다.
@Nullable
private LazyOptional<IMyAwesomeMagic> myAwesomeMagicCap;
@Override
public <T> LazyOptional<T> getCapability(Capability<T> cap, @Nullable Direction side){
if(cap==ModCapabilities.MY_AWESOME_MAGIC){
if(myAwesomeMagicCap==null) myAwesomeMagicCap = LazyOptional.of(() -> myAwesomeMagic);
return myAwesomeMagicCap.cast();
}
return super.getCapability(cap, side);
}
@Override
public void remove(){
super.remove();
if(myAwesomeMagicCap==null) myAwesomeMagicCap.invalidate();
}
이제 우리는 왜 이전에 보았던 코드가 나쁜 예제인지 알 수 있습니다. 이 코드 기억나시나요?
@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);
}
이 코드가 좋은 코드가 아닌 이유는, 바로 생성한 LazyOptional
들을 무효화할 방법이 존재하지 않기 때문입니다.
LazyOptional의 사용
그리고 이렇게 제공되는 LazyOptional은 아래와 같이 사용될 수 있습니다. 그다지 많은 설명이 필요하지는 않으니, 코드를 알아서 분석해 보시기 바랍니다.
public class AnotherTileEntity extends TileEntity implements ITickableTileEntity{
private LazyOptional<IMyAwesomeMagic> adjacentMagic = LazyOptional.empty();
// 타일엔티티의 첫 tick()에 TileEntity의 Capability를 가져오기 위해서 초기값을 true로 설정합니다.
private boolean refreshAdjacentMagic = true;
// refreshAdjacentMagic를 외부에서 접근하기 위한 메소드.
public void markRefreshAdjacentMagic(){
this.refreshAdjacentMagic = true;
}
@Override
public void tick() {
if(world.isRemote) return;
if(this.refreshAdjacentMagic){
this.refreshAdjacentMagic = false;
TileEntity te = world.getTileEntity(pos.offset(Direction.UP));
if(te!=null) {
adjacentMagic = te.getCapability(ModCapabilities.MY_AWESOME_MAGIC);
if(adjacentMagic.isPresent()) adjacentMagic.addListener(o -> markRefreshAdjacentMagic());
}else adjacentMagic = LazyOptional.empty();
}
IMyAwesomeMagic magic = adjacentMagic.orElse(null);
if(magic!=null) {
// Do the thing
}
}
}
물론 이 코드로는 TileEntity 주변 블럭의 변화를 감지할 수 없으니, 이는 블럭에서 따로 처리를 해 주어야 합니다.
// AnotherBlock.java
@Override
public void neighborChanged(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean isMoving){
if(pos.offset(Direction.UP).equals(fromPos)){
TileEntity te = world.getTileEntity(fromPos);
if(te instanceof AnotherTileEntity) ((AnotherTileEntity)te).markRefreshAdjacentMagic();
}
}
// ...
마지막으로, 위처럼 LazyOptional
을 캐시하는 방법을 모든 Capability의 사용처에 적용시킬 수는 없습니다. 비 주기적인 Capability의 접근, 혹은 Capability의 캐시가 불가능한 경우에는 예전에 배운 것처럼 LazyOptional
에서 값을 그대로 꺼내오는 것으로 충분합니다. Capability의 사용에 어떠한 메소드를 사용할 것인가는 여러분의 판단에 맡기겠습니다.