Entity种类
生物大致划分为四种:攻击型,被动型,水生型(也就是鱿鱼)和环境型(也就是蝙蝠)。攻击型生物有一个每游戏刻(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; } }
怪物容量(可理解为人口)与适合生成的区块总数直接成比例。要计算容量的话,生成区域在每一方向上均扩展一个区块,所以有17*17区块的大小,然后总的区块数被代入到下式中:
容量=常量*区块数/289
每一种生物均具有自己的容量计算和公式中不同的常量值:
攻击型 = 70
被动型 = 10
环境型(蝙蝠) = 15
水生型 = 5
服务器架构
图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); } } } } } } }
怪物的生命周期
待续。
特殊刷怪机制
刷怪箱逻辑
待续。
参考
Minecraft Wiki
Minecraft Forge