Minecraft刷怪笼顾名思义就是刷怪的笼子,遍布Minecraft各地,除了在水中、浮空和末地不存在外,一般来说刷怪笼都存在于低于地平线的位置,但是也有一些刷怪笼生成于高于地面的山中,有时会出现多个刷怪笼密集生成于一片区域的情况,比如蜘蛛刷怪笼。每种刷怪笼只会刷出一种怪,比如骷髅刷怪笼只会刷出骷髅弓箭手。当周围有刷怪笼时,可以听到独特的嘶嘶声,并且如果感觉某种怪物数量骤增,前赴后继地来攻击你,那周围很可能有刷怪笼存在。另外大部分刷怪笼都处于一个特制的房间中,其特点就是地板和墙壁是苔石铺成的,如果在挖掘时遇到苔石墙就得留意,后面很可能有刷怪笼存在。刷怪笼为了保护旁边箱子里的“宝物”(很可能只是几片面包),在人靠近时会不断刷出怪,当然也存在旁边没有箱子的刷怪笼,比如蜘蛛刷怪笼。
图1. 刷怪笼示意图
还有一种刷怪车的东西,作用基本和刷怪笼一样。
图2. 刷怪车示意图
刷怪笼是作为一种TileEntity存在的
public class TileEntityMobSpawner extends TileEntity implements IUpdatePlayerListBox { private final MobSpawnerBaseLogic spawnerLogic = new MobSpawnerBaseLogic() { public void setBlockEvent(int eventid) { TileEntityMobSpawner.this.worldObj.addBlockEvent(TileEntityMobSpawner.this.pos, Blocks.mob_spawner, eventid, 0); } public World getSpawnerWorld() { return TileEntityMobSpawner.this.worldObj; } public BlockPos getPos() { return TileEntityMobSpawner.this.pos; } public void setRandomEntity(MobSpawnerBaseLogic.WeightedRandomMinecart randomEntity) { super.setRandomEntity(randomEntity); if (this.getSpawnerWorld() != null) { this.getSpawnerWorld().markBlockForUpdate(TileEntityMobSpawner.this.pos); } } }; public void readFromNBT(NBTTagCompound compound) { super.readFromNBT(compound); this.spawnerLogic.readFromNBT(compound); } public void writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); this.spawnerLogic.writeToNBT(compound); } /** * Updates the JList with a new model. */ public void update() { this.spawnerLogic.updateSpawner(); } /** * Allows for a specialized description packet to be created. This is often used to sync tile entity data from the * server to the client easily. For example this is used by signs to synchronise the text to be displayed. */ public Packet getDescriptionPacket() { NBTTagCompound nbttagcompound = new NBTTagCompound(); this.writeToNBT(nbttagcompound); nbttagcompound.removeTag("SpawnPotentials"); return new S35PacketUpdateTileEntity(this.pos, 1, nbttagcompound); } public boolean receiveClientEvent(int id, int type) { return this.spawnerLogic.setDelayToMin(id) ? true : super.receiveClientEvent(id, type); } public MobSpawnerBaseLogic getSpawnerBaseLogic() { return this.spawnerLogic; } }
TileEntityMobSpawner的Update其实就是由MobSpawnerBaseLogic来决定的。
Minecraft Wiki里面对刷怪箱的刷怪逻辑如下
玩家距离刷怪箱16个方块内时,刷怪箱才会工作。当刷怪箱工作时,会以刷怪箱方块为中心的8×3×8(8格长宽,3格高)的有效区域生成生物,这意味着生物可以在一个9×9的区域,或距离刷怪箱3.5格的位置生成。生物可以在此区域符合生物生成要求的任意一处生成,生物更有可能生成在靠近刷怪箱而不是远离刷怪箱的地方。
当生物生成的 X 和 Z 坐标(注:不一定与刷怪箱对齐)是小数时,它们会生成在 Y 坐标是整数的地方。生物可以生成在8×8平面区域内的任意一处,但生成的生物脚的高度会与刷怪箱方块在同一层,或者比它高一层或低一层。
对于一些在生成区域以外生成的生物来说,必须远离不透明方块以确保可以容纳生物的高度和宽度,或由其它规则支配它们的每个生成区域。对于一些需要2格高或以上的空间才能生成的生物(如僵尸、骷髅或在Y轴最上面生成的烈焰人)来说,上面的空间必须只包含空气。
刷怪箱方块会尝试在有效区域内随机选择的位置生成4个生物,每次生成后会等待200-799刻(10-39.95秒)。在等待时,刷怪箱方块里面的生物会越转越快。除了对地面的生成要求,生物的其它生成要求也必须要满足(例如不能生成在固体方块里、亮度范围要正确等),因此刷怪箱常常不能生成4个生物。当刷怪箱生成了生物时,它会发出嘶嘶声,刷怪箱内火焰升腾。如果刷怪箱在有效区域内找不到任何符合要求的位置生成生物,则每一刻都会尝试一次。如果在生成阶段刷怪箱周围17×9×17的空间存在至少6个生物,则刷怪箱内火焰会升腾(表示已经“生成”了新的生物),但实际上生成过程被跳过,进入下一个周期。
当在一个没有有效位置生成生物的刷怪箱附近进行开采时,有时候刷怪箱会在方块被开采后立即生成一只怪物。
这些逻辑都在MobSpawnerBaseLogic.Java中定义
public abstract class MobSpawnerBaseLogic { /** The delay to spawn. */ private int spawnDelay = 20; private String mobID = "Pig"; /** List of minecart to spawn. */ private final List minecartToSpawn = Lists.newArrayList(); private MobSpawnerBaseLogic.WeightedRandomMinecart randomEntity; /** The rotation of the mob inside the mob spawner */ private double mobRotation; /** the previous rotation of the mob inside the mob spawner */ private double prevMobRotation; private int minSpawnDelay = 200; private int maxSpawnDelay = 800; private int spawnCount = 4; /** Cached instance of the entity to render inside the spawner. */ private Entity cachedEntity; private int maxNearbyEntities = 6; /** The distance from which a player activates the spawner. */ private int activatingRangeFromPlayer = 16; /** The range coefficient for spawning entities around. */ private int spawnRange = 4; /** * Gets the entity name that should be spawned. */ private String getEntityNameToSpawn() { if (this.getRandomEntity() == null) { if (this.mobID.equals("Minecart")) { this.mobID = "MinecartRideable"; } return this.mobID; } else { return this.getRandomEntity().entityType; } } public void setEntityName(String entityName) { this.mobID = entityName; } /** * Returns true if there's a player close enough to this mob spawner to activate it. */ private boolean isActivated() { BlockPos blockpos = this.getBlockPos(); return this.getSpawnerWorld().hasPlayerInRange((double)blockpos.getX() + 0.5D, (double)blockpos.getY() + 0.5D, (double)blockpos.getZ() + 0.5D, (double)this.activatingRangeFromPlayer); } public void updateSpawner() { if (this.isActivated()) { BlockPos blockpos = this.getBlockPos(); double posX; if (this.getSpawnerWorld().isRemote) { double d0 = (double)((float)blockpos.getX() + this.getSpawnerWorld().rand.nextFloat()); double d1 = (double)((float)blockpos.getY() + this.getSpawnerWorld().rand.nextFloat()); posX = (double)((float)blockpos.getZ() + this.getSpawnerWorld().rand.nextFloat()); this.getSpawnerWorld().spawnParticle(EnumParticleTypes.SMOKE_NORMAL, d0, d1, posX, 0.0D, 0.0D, 0.0D, new int[0]); this.getSpawnerWorld().spawnParticle(EnumParticleTypes.FLAME, d0, d1, posX, 0.0D, 0.0D, 0.0D, new int[0]); if (this.spawnDelay > 0) { --this.spawnDelay; } this.prevMobRotation = this.mobRotation; this.mobRotation = (this.mobRotation + (double)(1000.0F / ((float)this.spawnDelay + 200.0F))) % 360.0D; } else { if (this.spawnDelay == -1) { this.resetTimer(); } if (this.spawnDelay > 0) { --this.spawnDelay; return; } boolean flag = false; for (int i = 0; i < this.spawnCount; ++i) { Entity entity = EntityList.createEntityByName(this.getEntityNameToSpawn(), this.getSpawnerWorld()); if (entity == null) { return; } int j = this.getSpawnerWorld().getEntitiesWithinAABB(entity.getClass(), (new AxisAlignedBB((double)blockpos.getX(), (double)blockpos.getY(), (double)blockpos.getZ(), (double)(blockpos.getX() + 1), (double)(blockpos.getY() + 1), (double)(blockpos.getZ() + 1))).expand((double)this.spawnRange, (double)this.spawnRange, (double)this.spawnRange)).size(); if (j >= this.maxNearbyEntities) { this.resetTimer(); return; } posX = (double)blockpos.getX() + (this.getSpawnerWorld().rand.nextDouble() - this.getSpawnerWorld().rand.nextDouble()) * (double)this.spawnRange + 0.5D; double posY = (double)(blockpos.getY() + this.getSpawnerWorld().rand.nextInt(3) - 1); double posZ = (double)blockpos.getZ() + (this.getSpawnerWorld().rand.nextDouble() - this.getSpawnerWorld().rand.nextDouble()) * (double)this.spawnRange + 0.5D; EntityLiving entityliving = entity instanceof EntityLiving ? (EntityLiving)entity : null; entity.setLocationAndAngles(posX, posY, posZ, this.getSpawnerWorld().rand.nextFloat() * 360.0F, 0.0F); if (entityliving == null || entityliving.getCanSpawnHere() && entityliving.handleLavaMovement()) { this.InstantiateEntity(entity, true); this.getSpawnerWorld().playAuxSFX(2004, blockpos, 0); if (entityliving != null) { entityliving.spawnExplosionParticle(); } flag = true; } } if (flag) { this.resetTimer(); } } } } private Entity InstantiateEntity(Entity entity, boolean needSpawn) { if (this.getRandomEntity() != null) { NBTTagCompound nbttagcompound = new NBTTagCompound(); entity.writeToNBTOptional(nbttagcompound); Iterator iterator = this.getRandomEntity().nbtCompund.getKeySet().iterator(); while (iterator.hasNext()) { String s = (String)iterator.next(); NBTBase nbtbase = this.getRandomEntity().nbtCompund.getTag(s); nbttagcompound.setTag(s, nbtbase.copy()); } entity.readFromNBT(nbttagcompound); if (entity.worldObj != null && needSpawn) { entity.worldObj.spawnEntityInWorld(entity); } NBTTagCompound nbttagcompounposX; for (Entity entity1 = entity; nbttagcompound.hasKey("Riding", 10); nbttagcompound = nbttagcompounposX) { nbttagcompounposX = nbttagcompound.getCompoundTag("Riding"); Entity entity2 = EntityList.createEntityByName(nbttagcompounposX.getString("id"), entity.worldObj); if (entity2 != null) { NBTTagCompound nbttagcompound1 = new NBTTagCompound(); entity2.writeToNBTOptional(nbttagcompound1); Iterator iterator1 = nbttagcompounposX.getKeySet().iterator(); while (iterator1.hasNext()) { String s1 = (String)iterator1.next(); NBTBase nbtbase1 = nbttagcompounposX.getTag(s1); nbttagcompound1.setTag(s1, nbtbase1.copy()); } entity2.readFromNBT(nbttagcompound1); entity2.setLocationAndAngles(entity1.posX, entity1.posY, entity1.posZ, entity1.rotationYaw, entity1.rotationPitch); if (entity.worldObj != null && needSpawn) { entity.worldObj.spawnEntityInWorld(entity2); } entity1.mountEntity(entity2); } entity1 = entity2; } } else if (entity instanceof EntityLivingBase && entity.worldObj != null && needSpawn) { ((EntityLiving)entity).func_180482_a(entity.worldObj.getDifficultyForLocation(new BlockPos(entity)), (IEntityLivingData)null); entity.worldObj.spawnEntityInWorld(entity); } return entity; } private void resetTimer() { if (this.maxSpawnDelay <= this.minSpawnDelay) { this.spawnDelay = this.minSpawnDelay; } else { int i = this.maxSpawnDelay - this.minSpawnDelay; this.spawnDelay = this.minSpawnDelay + this.getSpawnerWorld().rand.nextInt(i); } if (this.minecartToSpawn.size() > 0) { this.setRandomEntity((MobSpawnerBaseLogic.WeightedRandomMinecart)WeightedRandom.getRandomItem(this.getSpawnerWorld().rand, this.minecartToSpawn)); } this.setBlockEvent(1); } public void readFromNBT(NBTTagCompound nbtCompound) { this.mobID = nbtCompound.getString("EntityId"); this.spawnDelay = nbtCompound.getShort("Delay"); this.minecartToSpawn.clear(); if (nbtCompound.hasKey("SpawnPotentials", 9)) { NBTTagList nbttaglist = nbtCompound.getTagList("SpawnPotentials", 10); for (int i = 0; i < nbttaglist.tagCount(); ++i) { this.minecartToSpawn.add(new MobSpawnerBaseLogic.WeightedRandomMinecart(nbttaglist.getCompoundTagAt(i))); } } if (nbtCompound.hasKey("SpawnData", 10)) { this.setRandomEntity(new MobSpawnerBaseLogic.WeightedRandomMinecart(nbtCompound.getCompoundTag("SpawnData"), this.mobID)); } else { this.setRandomEntity((MobSpawnerBaseLogic.WeightedRandomMinecart)null); } if (nbtCompound.hasKey("MinSpawnDelay", 99)) { this.minSpawnDelay = nbtCompound.getShort("MinSpawnDelay"); this.maxSpawnDelay = nbtCompound.getShort("MaxSpawnDelay"); this.spawnCount = nbtCompound.getShort("SpawnCount"); } if (nbtCompound.hasKey("MaxNearbyEntities", 99)) { this.maxNearbyEntities = nbtCompound.getShort("MaxNearbyEntities"); this.activatingRangeFromPlayer = nbtCompound.getShort("RequiredPlayerRange"); } if (nbtCompound.hasKey("SpawnRange", 99)) { this.spawnRange = nbtCompound.getShort("SpawnRange"); } if (this.getSpawnerWorld() != null) { this.cachedEntity = null; } } public void writeToNBT(NBTTagCompound nbtCompound) { nbtCompound.setString("EntityId", this.getEntityNameToSpawn()); nbtCompound.setShort("Delay", (short)this.spawnDelay); nbtCompound.setShort("MinSpawnDelay", (short)this.minSpawnDelay); nbtCompound.setShort("MaxSpawnDelay", (short)this.maxSpawnDelay); nbtCompound.setShort("SpawnCount", (short)this.spawnCount); nbtCompound.setShort("MaxNearbyEntities", (short)this.maxNearbyEntities); nbtCompound.setShort("RequiredPlayerRange", (short)this.activatingRangeFromPlayer); nbtCompound.setShort("SpawnRange", (short)this.spawnRange); if (this.getRandomEntity() != null) { nbtCompound.setTag("SpawnData", this.getRandomEntity().nbtCompund.copy()); } if (this.getRandomEntity() != null || this.minecartToSpawn.size() > 0) { NBTTagList nbttaglist = new NBTTagList(); if (this.minecartToSpawn.size() > 0) { Iterator iterator = this.minecartToSpawn.iterator(); while (iterator.hasNext()) { MobSpawnerBaseLogic.WeightedRandomMinecart weightedrandomminecart = (MobSpawnerBaseLogic.WeightedRandomMinecart)iterator.next(); nbttaglist.appendTag(weightedrandomminecart.generateNBT()); } } else { nbttaglist.appendTag(this.getRandomEntity().generateNBT()); } nbtCompound.setTag("SpawnPotentials", nbttaglist); } } /** * Sets the delay to minDelay if parameter given is 1, else return false. */ public boolean setDelayToMin(int delay) { if (delay == 1 && this.getSpawnerWorld().isRemote) { this.spawnDelay = this.minSpawnDelay; return true; } else { return false; } } @SideOnly(Side.CLIENT) public Entity cacheEntity(World worldIn) { if (this.cachedEntity == null) { Entity entity = EntityList.createEntityByName(this.getEntityNameToSpawn(), worldIn); if (entity != null) { entity = this.InstantiateEntity(entity, false); this.cachedEntity = entity; } } return this.cachedEntity; } private MobSpawnerBaseLogic.WeightedRandomMinecart getRandomEntity() { return this.randomEntity; } public void setRandomEntity(MobSpawnerBaseLogic.WeightedRandomMinecart _randomEntity) { this.randomEntity = _randomEntity; } public abstract void setBlockEvent(int eventId); public abstract World getSpawnerWorld(); public abstract BlockPos getBlockPos(); @SideOnly(Side.CLIENT) public double getMobRotation() { return this.mobRotation; } @SideOnly(Side.CLIENT) public double getPrevMobRotation() { return this.prevMobRotation; } public class WeightedRandomMinecart extends WeightedRandom.Item { private final NBTTagCompound nbtCompund; private final String entityType; public WeightedRandomMinecart(NBTTagCompound nbtCompound) { this(nbtCompound.getCompoundTag("Properties"), nbtCompound.getString("Type"), nbtCompound.getInteger("Weight")); } public WeightedRandomMinecart(NBTTagCompound nbtCompound, String entityType) { this(nbtCompound, entityType, 1); } private WeightedRandomMinecart(NBTTagCompound nbtCompound, String entityType, int weight) { super(weight); if (entityType.equals("Minecart")) { if (nbtCompound != null) { entityType = EntityMinecart.EnumMinecartType.byNetworkID(nbtCompound.getInteger("Type")).getName(); } else { entityType = "MinecartRideable"; } } this.nbtCompund = nbtCompound; this.entityType = entityType; } public NBTTagCompound generateNBT() { NBTTagCompound nbttagcompound = new NBTTagCompound(); nbttagcompound.setTag("Properties", this.nbtCompund); nbttagcompound.setString("Type", this.entityType); nbttagcompound.setInteger("Weight", this.itemWeight); return nbttagcompound; } } }
public class WeightedRandom { /** * Returns the total weight of all items in a collection. * * @param collection Collection to get the total weight of */ public static int getTotalWeight(Collection collection) { int i = 0; WeightedRandom.Item item; for (Iterator iterator = collection.iterator(); iterator.hasNext(); i += item.itemWeight) { item = (WeightedRandom.Item)iterator.next(); } return i; } /** * Returns a random choice from the input items, with a total weight value. * * @param collection Collection of the input items */ public static WeightedRandom.Item getRandomItem(Random random, Collection collection, int seed) { if (seed <= 0) { throw new IllegalArgumentException(); } else { int j = random.nextInt(seed); return getRandomItem(collection, j); } } public static WeightedRandom.Item getRandomItem(Collection collection, int totalWeight) { Iterator iterator = collection.iterator(); WeightedRandom.Item item; do { if (!iterator.hasNext()) { return null; } item = (WeightedRandom.Item)iterator.next(); totalWeight -= item.itemWeight; } while (totalWeight >= 0); return item; } /** * Returns a random choice from the input items. * * @param collection Collection to get the random item from */ public static WeightedRandom.Item getRandomItem(Random random, Collection collection) { /** * Returns a random choice from the input items, with a total weight value. * * @param collection Collection of the input items */ return getRandomItem(random, collection, getTotalWeight(collection)); } public static class Item { /** The Weight is how often the item is chosen(higher number is higher chance(lower is lower)) */ public int itemWeight; public Item(int itemWeightIn) { this.itemWeight = itemWeightIn; } } }
Minecraft wiki
Minecraft Forge