生物大致划分为四种:攻击型,被动型,水生型(也就是鱿鱼)和环境型(也就是蝙蝠)。攻击型生物有一个每游戏刻(1/20秒)一次的生成周期。被动型和水生型生物只有每400刻(20秒)一次的生成周期。因为这一点,攻击型生物可以在任何时候生成,而动物生成则非常少。另外,大部分动物在建立世界时产生的区块中生成。源码中对应的类是
EnumCreatureType.java
public enum EnumCreatureType { MONSTER(IMob.class, 70, Material.air, false, false), CREATURE(EntityAnimal.class, 10, Material.air, true, true), AMBIENT(EntityAmbientCreature.class, 15, Material.air, true, false), WATER_CREATURE(EntityWaterMob.class, 5, Material.water, true, false); /** * The root class of creatures associated with this EnumCreatureType (IMobs for aggressive creatures, EntityAnimals * for friendly ones) */ private final Class creatureClass; private final int maxNumberOfCreature; private final Material creatureMaterial; /** A flag indicating whether this creature type is peaceful. */ private final boolean isPeacefulCreature; /** Whether this creature type is an animal. */ private final boolean isAnimal; private EnumCreatureType(Class class, int _maxNumberOfCreature, Material _creatureMaterial, boolean _isPeacefulCreature, boolean _isAnimal) { this.creatureClass = class; this.maxNumberOfCreature = _maxNumberOfCreature; this.creatureMaterial = _creatureMaterial; this.isPeacefulCreature = _isPeacefulCreature; this.isAnimal = _isAnimal; } public Class getCreatureClass() { return this.creatureClass; } public int getMaxNumberOfCreature() { return this.maxNumberOfCreature; } /** * Gets whether or not this creature type is peaceful. */ public boolean getPeacefulCreature() { return this.isPeacefulCreature; } /** * Return whether this creature type is an animal. */ public boolean getAnimal() { return this.isAnimal; } }
图1. MC服务器架构
MinecraftServer作为服务器,主要负责服务端的更新,里面可以包含多个WorldServer,WorldClent作为服务端,当玩家加入一个服务器的时候,就会创建一个在本地。SpawnerAnimals作为刷怪的工具类,主要用来处理刷怪逻辑。
首先来看WorldServer的Tick
注:如无特别说明,所有tick都是一秒20次。
图2. WorldServer Tick 流程
很清晰的逻辑,这里主要看一下findChunksForSpawning的实现。
在单人游戏模式下,区块计数总为17*17=289,那么各种生物的容量也就是上面列出的数值。在多人游戏中,在多个玩家范围内的区块只被计算一次,所以玩家越分散,更多地区块会被覆盖且会有更高的生物容量。
在每次生成周期的开始都会检查一次容量。如果存活的生物数量超过它的容量,整个生成周期就会被跳过。
在每一生成周期中,会在每一个合适的区块中进行一次生成一组生物的尝试。该区块内选择一个随机地点作为这组生物的中心点。为生成这组生物,中心方块对水生生物而言必须是水方块,对所有其它生物来说则必须是空气方块。注意在后面的情形中,它一定得是空气方块。任何其它方块,哪怕是一个透明方块都会阻止整组生物的生成。
图3. 陆地怪物的生成条件
如果该组位置合适,会在以中心方块为原点41*1*41的范围(就是41*41格大小的方型,有1格高的区域)内进行12次尝试以生成多至4个的生物(狼是8个,恶魂是1个)。生物将会在这一区域生成其身体的最下部分。在每次生成尝试中,会在这一区域中随机选择一个方块的地点。尽管生成区域能扩展到中心21格之外,但是随机选出的地点强烈地向该组的中心集中。大约有85%的生成将会在该组中心的5格以内,99%会落在10格以内
组内所有的生物都是相同的种类。在该组第一次生成尝试时从该地区所适合生成的种类中随机挑选一种以决定整组的种类。
具体的种类可以参考要Minecraft的Wiki。
findChunksForSpawning函数实现的就是上面描述的逻辑。看一下SpawnerAnimals.java这个类。
public final class SpawnerAnimals { private static final int MOB_COUNT_DIV = (int)Math.pow(17.0D, 2.0D); /** The 17x17 area around the player where mobs can spawn */ private final Set eligibleChunksForSpawning = Sets.newHashSet(); private static final String __OBFID = "CL_00000152"; /** * adds all chunks within the spawn radius of the players to eligibleChunksForSpawning. pars: the world, * hostileCreatures, passiveCreatures. returns number of eligible chunks. */ public int findChunksForSpawning(WorldServer server, boolean spawnHostileMobs, boolean spawnPeacefulMobs, boolean isSpecialSpawnTick) { if (!spawnHostileMobs && !spawnPeacefulMobs) { return 0; } else { this.eligibleChunksForSpawning.clear(); int chunkCount = 0; Iterator iterator = server.playerEntities.iterator(); int k; int creatureCount; while (iterator.hasNext()) { EntityPlayer entityplayer = (EntityPlayer)iterator.next(); if (!entityplayer.isSpectator()) { int j = MathHelper.floor_double(entityplayer.posX / 16.0D); k = MathHelper.floor_double(entityplayer.posZ / 16.0D); byte b0 = 8; for (int l = -b0; l <= b0; ++l) { for (creatureCount = -b0; creatureCount <= b0; ++creatureCount) { boolean flag3 = l == -b0 || l == b0 || creatureCount == -b0 || creatureCount == b0; ChunkCoordIntPair chunkcoordintpair = new ChunkCoordIntPair(l + j, creatureCount + k); if (!this.eligibleChunksForSpawning.contains(chunkcoordintpair)) { ++chunkCount; if (!flag3 && server.getWorldBorder().contains(chunkcoordintpair)) { this.eligibleChunksForSpawning.add(chunkcoordintpair); } } } } } } int totalEntityCount = 0; BlockPos blockpos2 = server.getSpawnPoint(); EnumCreatureType[] aenumcreaturetype = EnumCreatureType.values(); k = aenumcreaturetype.length; for (int i = 0; i < k; ++i) { EnumCreatureType enumcreaturetype = aenumcreaturetype[i]; if ((!enumcreaturetype.getPeacefulCreature() || spawnPeacefulMobs) && (enumcreaturetype.getPeacefulCreature() || spawnHostileMobs) && (!enumcreaturetype.getAnimal() || isSpecialSpawnTick)) { creatureCount = server.countEntities(enumcreaturetype, true); int maxCreatureCount = enumcreaturetype.getMaxNumberOfCreature() * chunkCount / MOB_COUNT_DIV; if (creatureCount <= maxCreatureCount) { Iterator iterator1 = this.eligibleChunksForSpawning.iterator(); ArrayList<ChunkCoordIntPair> tmp = new ArrayList(eligibleChunksForSpawning); Collections.shuffle(tmp); iterator1 = tmp.iterator(); label115: while (iterator1.hasNext()) { ChunkCoordIntPair chunkcoordintpair1 = (ChunkCoordIntPair)iterator1.next(); BlockPos blockpos = getRandomChunkPosition(server, chunkcoordintpair1.chunkXPos, chunkcoordintpair1.chunkZPos); int j1 = blockpos.getX(); int k1 = blockpos.getY(); int l1 = blockpos.getZ(); Block block = server.getBlockState(blockpos).getBlock(); if (!block.isNormalCube()) { int entityCountOnChunk = 0; int j2 = 0; while (j2 < 3) { int k2 = j1; int l2 = k1; int i3 = l1; byte b1 = 6; BiomeGenBase.SpawnListEntry spawnlistentry = null; IEntityLivingData ientitylivingdata = null; int j3 = 0; while (true) { if (j3 < 4) { label108: { k2 += server.rand.nextInt(b1) - server.rand.nextInt(b1); l2 += server.rand.nextInt(1) - server.rand.nextInt(1); i3 += server.rand.nextInt(b1) - server.rand.nextInt(b1); BlockPos blockpos1 = new BlockPos(k2, l2, i3); float f = (float)k2 + 0.5F; float f1 = (float)i3 + 0.5F; //Check must be away from Player by 24 block, and away from player spawn point. 576 = 24 * 24 if (!server.CheckCanSpawnHere((double)f, (double)l2, (double)f1, 24.0D) && blockpos2.distanceSq((double)f, (double)l2, (double)f1) >= 576.0D) { if (spawnlistentry == null) { spawnlistentry = server.GetSpawnListEntry(enumcreaturetype, blockpos1); if (spawnlistentry == null) { break label108; } } if (server.CheckChunkHasSpawnEntry(enumcreaturetype, spawnlistentry, blockpos1) && canCreatureTypeSpawnAtLocation(EntitySpawnPlacementRegistry.GetSpawnPointType(spawnlistentry.entityClass), server, blockpos1)) { EntityLiving entityliving; try { entityliving = (EntityLiving)spawnlistentry.entityClass.getConstructor(new Class[] {World.class}).newInstance(new Object[] {server}); } catch (Exception exception) { exception.printStackTrace(); return totalEntityCount; } entityliving.setLocationAndAngles((double)f, (double)l2, (double)f1, server.rand.nextFloat() * 360.0F, 0.0F); Result canSpawn = ForgeEventFactory.canEntitySpawn(entityliving, server, f, l2, f1); if (canSpawn == Result.ALLOW || (canSpawn == Result.DEFAULT && (entityliving.getCanSpawnHere() && entityliving.handleLavaMovement()))) { if (!ForgeEventFactory.doSpecialSpawn(entityliving, server, f1, l2, f1)) ientitylivingdata = entityliving.getEntityData(server.getDifficultyForLocation(new BlockPos(entityliving)), ientitylivingdata); if (entityliving.handleLavaMovement()) { ++entityCountOnChunk; server.spawnEntityInWorld(entityliving); } if (entityCountOnChunk >= ForgeEventFactory.getMaxSpawnPackSize(entityliving)) { continue label115; } } totalEntityCount += entityCountOnChunk; } } ++j3; continue; } } ++j2; break; } } } } } } } return totalEntityCount; } } protected static BlockPos getRandomChunkPosition(World worldIn, int x, int z) { Chunk chunk = worldIn.getChunkFromChunkCoords(x, z); int k = x * 16 + worldIn.rand.nextInt(16); int l = z * 16 + worldIn.rand.nextInt(16); int creatureCount = MathHelper.Ceiling(chunk.getHeight(new BlockPos(k, 0, l)) + 1, 16); int j1 = worldIn.rand.nextInt(creatureCount > 0 ? creatureCount : chunk.getTopFilledSegment() + 16 - 1); return new BlockPos(k, j1, l); } public static boolean canCreatureTypeSpawnAtLocation(EntityLiving.SpawnPlacementType placeType, World worldIn, BlockPos pos) { if (!worldIn.getWorldBorder().contains(pos)) { return false; } else { Block block = worldIn.getBlockState(pos).getBlock(); if (placeType == EntityLiving.SpawnPlacementType.IN_WATER) { return block.getMaterial().isLiquid() && worldIn.getBlockState(pos.down()).getBlock().getMaterial().isLiquid() && !worldIn.getBlockState(pos.up()).getBlock().isNormalCube(); } else { BlockPos blockpos1 = pos.down(); if (!worldIn.getBlockState(blockpos1).getBlock().canCreatureSpawn(worldIn, blockpos1, placeType)) { return false; } else { Block block1 = worldIn.getBlockState(blockpos1).getBlock(); boolean flag = block1 != Blocks.bedrock && block1 != Blocks.barrier; return flag && !block.isNormalCube() && !block.getMaterial().isLiquid() && !worldIn.getBlockState(pos.up()).getBlock().isNormalCube(); } } } } /** * Called during chunk generation to spawn initial creatures. */ public static void performWorldGenSpawning(World worldIn, BiomeGenBase biomeGenBase, int chunkCenterX, int chunkCenterY, int rangeX, int rangeY, Random rand) { List list = biomeGenBase.getSpawnableList(EnumCreatureType.CREATURE); if (!list.isEmpty()) { while (rand.nextFloat() < biomeGenBase.getSpawningChance()) { BiomeGenBase.SpawnListEntry spawnlistentry = (BiomeGenBase.SpawnListEntry)WeightedRandom.getRandomItem(worldIn.rand, list); int creatureCount = spawnlistentry.minGroupCount + rand.nextInt(1 + spawnlistentry.maxGroupCount - spawnlistentry.minGroupCount); IEntityLivingData ientitylivingdata = null; int j1 = chunkCenterX + rand.nextInt(rangeX); int k1 = chunkCenterY + rand.nextInt(rangeY); int l1 = j1; int entityCountOnChunk = k1; for (int j2 = 0; j2 < creatureCount; ++j2) { boolean flag = false; for (int k2 = 0; !flag && k2 < 4; ++k2) { BlockPos blockpos = worldIn.getTopSolidOrLiquidBlock(new BlockPos(j1, 0, k1)); if (canCreatureTypeSpawnAtLocation(EntityLiving.SpawnPlacementType.ON_GROUND, worldIn, blockpos)) { EntityLiving entityliving; try { entityliving = (EntityLiving)spawnlistentry.entityClass.getConstructor(new Class[] {World.class}).newInstance(new Object[] {worldIn}); } catch (Exception exception) { exception.printStackTrace(); continue; } entityliving.setLocationAndAngles((double)((float)j1 + 0.5F), (double)blockpos.getY(), (double)((float)k1 + 0.5F), rand.nextFloat() * 360.0F, 0.0F); worldIn.spawnEntityInWorld(entityliving); ientitylivingdata = entityliving.func_180482_a(worldIn.getDifficultyForLocation(new BlockPos(entityliving)), ientitylivingdata); flag = true; } j1 += rand.nextInt(5) - rand.nextInt(5); for (k1 += rand.nextInt(5) - rand.nextInt(5); j1 < chunkCenterX || j1 >= chunkCenterX + rangeX || k1 < chunkCenterY || k1 >= chunkCenterY + rangeX; k1 = entityCountOnChunk + rand.nextInt(5) - rand.nextInt(5)) { j1 = l1 + rand.nextInt(5) - rand.nextInt(5); } } } } } } }
在维基百科中,生物群系(Biome)在气候学和地理学上被定义为具有类似气候条件的地方,比如植物、动物和土壤生物组成的群落,它经常被称作生态系统。是Minecraft里有不同的地域特色,植物,高度,温度,湿度评级的地区。 在Minecraft中, 从万圣节更新开始,它意味着具有不同高度、温度、湿度、叶子颜色的区域。
图4. 高寒地区Biome
图5. 雪地Biome
当地图被创建时会具有雪地或草地主题。但在这个更新之后,一个世界中就可以具有所有的主题,它们的分布由生物群系图决定。
Anvil文件格式中,世界数据直接存储在生物群系中,这不同于先前的国家或地区的文件格式的格式,其中的生物群系,从种子中动态计算。
Chunk中的生物也是根据Biome来生成,这里的限制主要体现在当SpawnAnimals在Chunk中生成生物的时候,总会调用WorldServer的方法进行Check,看对应的生物种类能否生成。
public boolean CheckCreatureCanSpawn(EnumCreatureType creatureType, BiomeGenBase.SpawnListEntry spawnListEntry, BlockPos blockPos) { List list = this.getChunkProvider().getSpawnableList(creatureType, blockPos); return list != null && !list.isEmpty() ? list.contains(spawnListEntry) : false; }
public List getSpawnableList(EnumCreatureType creatureType, BlockPos pos) { BiomeGenBase biomegenbase = this.worldObj.getBiomeGenForCoords(pos); if (this.mapFeaturesEnabled) { if (creatureType == EnumCreatureType.MONSTER && this.scatteredFeatureGenerator.func_175798_a(pos)) { return this.scatteredFeatureGenerator.getScatteredFeatureSpawnList(); } if (creatureType == EnumCreatureType.MONSTER && this.settings.useMonuments && this.oceanMonumentGenerator.func_175796_a(this.worldObj, pos)) { return this.oceanMonumentGenerator.func_175799_b(); } } return biomegenbase.getSpawnableList(creatureType); }
而所有的Biome和生物的对应关系都在BiomeGenBase这个类中定义。
在EntityLiving中定义了一个getCanSpawnHere函数,用于查询是否可以在某个位置生成
/** * Checks if the entity's current position is a valid location to spawn this entity. */ public boolean getCanSpawnHere() { return true; }
/** * Checks if the entity's current position is a valid location to spawn this entity. */ public boolean getCanSpawnHere() { int i = MathHelper.floor_double(this.posX); int j = MathHelper.floor_double(this.getEntityBoundingBox().minY); int k = MathHelper.floor_double(this.posZ); BlockPos blockpos = new BlockPos(i, j, k); return this.worldObj.getBlockState(blockpos.down()).getBlock() == this.spawnBlock && this.worldObj.getLight(blockpos) > 8 && super.getCanSpawnHere(); }
/** * Checks if the entity's current position is a valid location to spawn this entity. */ public boolean getCanSpawnHere() { Chunk chunk = this.worldObj.getChunkFromBlockCoords(new BlockPos(MathHelper.floor_double(this.posX), 0, MathHelper.floor_double(this.posZ))); if (this.worldObj.getWorldInfo().getTerrainType().handleSlimeSpawnReduction(rand, worldObj)) { return false; } else { if (this.worldObj.getDifficulty() != EnumDifficulty.PEACEFUL) { BiomeGenBase biomegenbase = this.worldObj.getBiomeGenForCoords(new BlockPos(MathHelper.floor_double(this.posX), 0, MathHelper.floor_double(this.posZ))); if (biomegenbase == BiomeGenBase.swampland && this.posY > 50.0D && this.posY < 70.0D && this.rand.nextFloat() < 0.5F && this.rand.nextFloat() < this.worldObj.getCurrentMoonPhaseFactor() && this.worldObj.getLightFromNeighbors(new BlockPos(this)) <= this.rand.nextInt(8)) { return super.getCanSpawnHere(); } if (this.rand.nextInt(10) == 0 && chunk.getRandomWithSeed(987234911L).nextInt(10) == 0 && this.posY < 40.0D) { return super.getCanSpawnHere(); } } return false; } }
Minecraft Wiki
Minecraft Forge