1. ITickable 与 TileEntity

TileEntity 的刷新功能由 ITickable 接口提供。TileEntity 默认是没有刷新功能的,你必须实现 ITickable 接口方能获得1秒20次的刷新能力。

1.1. Tick

Tick 是很多游戏中的最小时间单位的名字,但具体定义是取决于实际情况的。 想象一下时间的流逝:你在读这些文本时大概没什么别的事发生,但时间依旧在走。Tick 的概念也是一样:你在游戏中可能什么都不做,但游戏内的时间依旧在走。时间流逝这个动作通常被称为 Ticking(没错,tick 突然从名词变成了动词),和“刷新”意思相近。
通常,实现这个效果的方式是一个可以跳出的无限循环。

// 在我们点退出游戏,或者服务器输入 /stop 命令时,
// 这个 keepGameOn 就会变成 false。
while (keepGameOn) {
    // 这个 doGameLogic 里包含了各种各样的东西:
    // 实体刷新、方块随机刷新、TileEntity 刷新、
    // 天气更新、向各个客户端发包、……
    // 我们根据当前游戏的状态来决定游戏接下来会
    // 变成什么样子(另见:时序电路)。
    doGameLogic();
}

Minecraft 中 1 秒最多刷新 20 次。它大约是这样实现的:

while (keepGameOn) {
    for (int i = 0; i < 20; i++) {
        // 记录这个 tick 开始的时间
        long start = System.currentTimeMillis();
        doGameLogic();
        // 根据结束时间,计算这个 tick 的实际耗时
        long timeElapsed = System.currentTimeMillis() - start;
        // 若这个 tick 耗时低于 50 毫秒,则通过 Thread.sleep 补足至 50 毫秒
        if (timeElapsed < 50) {
            Thread.sleep(50 - timeElapsed);
        }
    }
}

有鉴于此,Minecraft 的游戏刻 (Gametick) 的精确定义为“最理想状态下,1 秒 = 20 tick”。
与此同时出现的还有 TPS(Tick Per Second,每秒刻数)的概念。相应的,在 Minecraft 的世界中,TPS 达到最大值 20 时才是正常的。TPS 低于 20 时,1 秒也将不再是 20 tick,游戏逻辑的执行也会慢下来(以耗时长的方式呈现)。

public final class MyLavaFurnaceEntity extends TileEntity implements ITickable {

    private int progress;
    private int fuel;

    @Override
    public void update() {
        // 所有的逻辑全部在这里发生…… 至于这里会发生什么完全要看
        // 想象力。
        // 这也是 Minecraft 的魅力之一——由原版熔炉所产生的“操纵
        // 物品”的概念,产生了无数机器,为科技主题 Mod 提供了
        // 理论可能。

        // 如上文所述,只要这个 TileEntity 还存在于这个世界上,
        // 这个 TileEntity 的 update 方法每一个 tick 就都会
        // 被调用一次。我们不应也没有必要在这里写死循环。
        if (!world.isRemote && fuel > 0 && ++progress > 200) {
            for (EntityItem entity : this.world.getEntitiesWithinAABB(EntityItem.class, new AxisAlignedBB(this.pos.up()))) {
                final ItemStack result = FurnaceRecipes.instance().getSmeltingResult(entity.getItem());
                if (!result.isEmpty()) {
                    fuel--;
                    entity.setItem(result.copy());
                    if (fuel <= 0) {
                        break;
                    }
                }
            }
        }
    }

    public ItemStack tryAcceptFuel(ItemStack fuel) {
        if (fuel.getItem() == Items.LAVA_BUCKET) {
            this.fuel += 1000;
            return new ItemStack(Items.BUCKET);
        } else {
            return fuel;
        }
    }

    public int getFuel() {
        return this.fuel;
    }

    @Override
    public void readFromNBT(NBTTagCompound tag) {
        super.readFromNBT(tag);
        this.progress = tag.getInteger("Progress");
        this.fuel = tag.getInteger("Fuel");
    }

    @Override
    public NBTTagCompound writeToNBT(NBTTagCompound tag) {
        tag.setInteger("Progress", this.progress);
        tag.setInteger("Fuel", this.fuel);
        return super.writeToNBT(tag);
    }
}

1.2. ITickable 并非必须

只是为了存储数据的话,不需要实现 ITickable 接口,这样可以节约资源。