这个系列通过对我的世界Minecraft源码进行拆分讲解,让大家可以清除的了解一款游戏是怎么一步步被实现出来的,下面就介绍Minecraft源码第一篇内容,关于刷怪逻辑。
生成循环
生物大致划分为四种:攻击型,被动型,水生型(也就是鱿鱼)和环境型(也就是蝙蝠)。攻击型生物每一个Tick(1/20秒)有一次的生成周期。被动型和水生型生物只有每400刻(20秒)一次的生成周期。因为这一点,攻击型生物可以在任何时候生成,而动物生成则非常少。另外,大部分动物在建立世界时产生的Block中生成。
生物的刷新范围通常是以玩家为中心的 15 * 15的Chunk,当有多个玩家的时候,所有玩家的附近都会刷新。当敌对怪物离玩家超过128个block的时候,就会立即把它刷掉。下图表述刷新逻辑和玩家距离的关系。
Entity种类
源码中对应的类是
EnumCreatureType.java
publicenumEnumCreatureType
{
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)
*/
privatefinalClass creatureClass;
privatefinalintmaxNumberOfCreature;
privatefinalMaterial creatureMaterial;
/** A flag indicating whether this creature type is peaceful. */
privatefinalbooleanisPeacefulCreature;
/** Whether this creature type is an animal. */
privatefinalbooleanisAnimal;
privateEnumCreatureType(Classclass,int_maxNumberOfCreature, Material _creatureMaterial,boolean_isPeacefulCreature,boolean_isAnimal)
{
this.creatureClass =class;
this.maxNumberOfCreature = _maxNumberOfCreature;
this.creatureMaterial = _creatureMaterial;
this.isPeacefulCreature = _isPeacefulCreature;
this.isAnimal = _isAnimal;
}
publicClass getCreatureClass()
{
returnthis.creatureClass;
}
publicintgetMaxNumberOfCreature()
{
returnthis.maxNumberOfCreature;
}
/**
* Gets whether or not this creature type is peaceful.
*/
publicbooleangetPeacefulCreature()
{
returnthis.isPeacefulCreature;
}
/**
* Return whether this creature type is an animal.
*/
publicbooleangetAnimal()
{
returnthis.isAnimal;
}
}怪物容量(可理解为人口)与适合生成的Chunk总数直接成比例。要计算容量的话,生成区域在每一方向上均扩展一个区块,所以有17*17 BChunk的大小,然后总的ChunkCount被代入到下式中:容量 = 常量* ChunkCount / 289每一种生物均具有自己的容量计算和公式中不同的常量值:攻击型 = 70被动型 = 10环境型(蝙蝠) = 15
水生型 = 5
在单人游戏的时候,ChunkCount 一直是289,但是在多人游戏中,每个chunk只被计算一次,所以玩家分得越开,怪物的容量也就越大。
在 Spawn开始之前,首先进行一次 mob 容量检查,如果生物的数量超过了容量,那么这个种类的生物的生成就会跳过。
服务器架构
图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这个类。
publicfinalclassSpawnerAnimals
{
privatestaticfinalintMOB_COUNT_DIV = (int)Math.pow(17.0D,2.0D);
/** The 17x17 area around the player where mobs can spawn */
privatefinalSet eligibleChunksForSpawning = Sets.newHashSet();
privatestaticfinalString __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.
*/
publicintfindChunksForSpawning(WorldServer server,booleanspawnHostileMobs,booleanspawnPeacefulMobs,booleanisSpecialSpawnTick)
{
if(!spawnHostileMobs && !spawnPeacefulMobs)
{
return0;
}
else
{
this.eligibleChunksForSpawning.clear();
intchunkCount =0;
Iterator iterator = server.playerEntities.iterator();
intk;
intcreatureCount;
while(iterator.hasNext())
{
EntityPlayer entityplayer = (EntityPlayer)iterator.next();
if(!entityplayer.isSpectator())
{
intchunkCoordX = Mathf.FloorToInt(entityplayer.GetPosition().x /16.0f);
intchunkCoordZ = Mathf.FloorToInt(entityplayer.GetPosition().z /16.0f);
bytechunkLength =8;
for(intdirectionX = -chunkLength; directionX <= chunkLength; directionX)
{
for(directionZ = -chunkLength; directionZ <= chunkLength; directionZ)
{
bool isBorderChunk = (directionX == -chunkLength || directionX == chunkLength || directionZ == -chunkLength || directionZ == chunkLength);
IntVector2 chunkcoordintpair = newIntVector2(directionX chunkCoordX, directionZ chunkCoordZ);
if(!this.eligibleChunksForSpawning.Contains(chunkcoordintpair))
{
chunkCount;
if(!isBorderChunk && server.getWorldBorder().contains(chunkcoordintpair))
{
this.eligibleChunksForSpawning.Add(chunkcoordintpair);
}
}
}
}
}
}
inttotalEntityCount =0;
BlockPos blockpos2 = server.getSpawnPoint();
EnumCreatureType[] aenumcreaturetype = EnumCreatureType.values();
k = aenumcreaturetype.length;
for(inti =0; i
{
EnumCreatureType enumcreaturetype = aenumcreaturetype[i];
if((!enumcreaturetype.getPeacefulCreature() || spawnPeacefulMobs) && (enumcreaturetype.getPeacefulCreature() || spawnHostileMobs) && (!enumcreaturetype.getAnimal() || isSpecialSpawnTick))
{
creatureCount = server.countEntities(enumcreaturetype, true);
intmaxCreatureCount = enumcreaturetype.getMaxNumberOfCreature() * chunkCount / MOB_COUNT_DIV;
if(creatureCount <= maxCreatureCount)
{
Iterator iterator1 = this.eligibleChunksForSpawning.iterator();
ArrayListtmp = newArrayList(eligibleChunksForSpawning);
Collections.shuffle(tmp);
iterator1 = tmp.iterator();
label115:
while(iterator1.hasNext())
{
ChunkCoordIntPair chunkcoordintpair1 = (ChunkCoordIntPair)iterator1.next();
BlockPos blockpos = getRandomChunkPosition(server, chunkcoordintpair1.chunkXPos, chunkcoordintpair1.chunkZPos);
intj1 = blockpos.getX();
intk1 = blockpos.getY();
intl1 = blockpos.getZ();
Block block = server.getBlockState(blockpos).getBlock();
if(!block.isNormalCube())
{
intentityCountOnChunk =0;
intj2 =0;
while(j2 <3)
{
intk2 = j1;
intl2 = k1;
inti3 = l1;
byteb1 =6;
BiomeGenBase.SpawnListEntry spawnlistentry = null;
IEntityLivingData ientitylivingdata = null;
intj3 =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 = newBlockPos(k2, l2, i3);
floatf = (float)k20.5F;
floatf1 = (float)i30.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)
{
breaklabel108;
}
}
if(server.CheckChunkHasSpawnEntry(enumcreaturetype, spawnlistentry, blockpos1) && canCreatureTypeSpawnAtLocation(EntitySpawnPlacementRegistry.GetSpawnPointType(spawnlistentry.entityClass), server, blockpos1))
{
EntityLiving entityliving;
try
{
entityliving = (EntityLiving)spawnlistentry.entityClass.getConstructor(newClass[] {World.class}).newInstance(newObject[] {server});
}
catch(Exception exception)
{
exception.printStackTrace();
returntotalEntityCount;
}
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(newBlockPos(entityliving)), ientitylivingdata);
if(entityliving.handleLavaMovement())
{
entityCountOnChunk;
server.spawnEntityInWorld(entityliving);
}
if(entityCountOnChunk >= ForgeEventFactory.getMaxSpawnPackSize(entityliving))
{
continuelabel115;
}
}
totalEntityCount = entityCountOnChunk;
}
}
j3;
continue;
}
}
j2;
break;
}
}
}
}
}
}
}
returntotalEntityCount;
}
}
protectedstaticBlockPos getRandomChunkPosition(World worldIn,intx,intz)
{
Chunk chunk = worldIn.getChunkFromChunkCoords(x, z);
intk = x *16worldIn.rand.nextInt(16);
intl = z *16worldIn.rand.nextInt(16);
intcreatureCount = MathHelper.Ceiling(chunk.getHeight(newBlockPos(k,0, l))1,16);
intj1 = worldIn.rand.nextInt(creatureCount >0? creatureCount : chunk.getTopFilledSegment()16-1);
returnnewBlockPos(k, j1, l);
}
publicstaticbooleancanCreatureTypeSpawnAtLocation(EntityLiving.SpawnPlacementType placeType, World worldIn, BlockPos pos)
{
if(!worldIn.getWorldBorder().contains(pos))
{
returnfalse;
}
else
{
Block block = worldIn.getBlockState(pos).getBlock();
if(placeType == EntityLiving.SpawnPlacementType.IN_WATER)
{
returnblock.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))
{
returnfalse;
}
else
{
Block block1 = worldIn.getBlockState(blockpos1).getBlock();
booleanflag = block1 != Blocks.bedrock && block1 != Blocks.barrier;
returnflag && !block.isNormalCube() && !block.getMaterial().isLiquid() && !worldIn.getBlockState(pos.up()).getBlock().isNormalCube();
}
}
}
}
/**
* Called during chunk generation to spawn initial creatures.
*/
publicstaticvoidperformWorldGenSpawning(World worldIn, BiomeGenBase biomeGenBase,intchunkCenterX,intchunkCenterY,intrangeX,intrangeY, Random rand)
{
List list = biomeGenBase.getSpawnableList(EnumCreatureType.CREATURE);
if(!list.isEmpty())
{
while(rand.nextFloat()
{
BiomeGenBase.SpawnListEntry spawnlistentry = (BiomeGenBase.SpawnListEntry)WeightedRandom.getRandomItem(worldIn.rand, list);
intcreatureCount = spawnlistentry.minGroupCount rand.nextInt(1spawnlistentry.maxGroupCount - spawnlistentry.minGroupCount);
IEntityLivingData ientitylivingdata = null;
intj1 = chunkCenterX rand.nextInt(rangeX);
intk1 = chunkCenterY rand.nextInt(rangeY);
intl1 = j1;
intentityCountOnChunk = k1;
for(intj2 =0; j2
{
booleanflag =false;
for(intk2 =0; !flag && k2 <4; k2)
{
BlockPos blockpos = worldIn.getTopSolidOrLiquidBlock(newBlockPos(j1,0, k1));
if(canCreatureTypeSpawnAtLocation(EntityLiving.SpawnPlacementType.ON_GROUND, worldIn, blockpos))
{
EntityLiving entityliving;
try
{
entityliving = (EntityLiving)spawnlistentry.entityClass.getConstructor(newClass[] {World.class}).newInstance(newObject[] {worldIn});
}
catch(Exception exception)
{
exception.printStackTrace();
continue;
}
entityliving.setLocationAndAngles((double)((float)j10.5F), (double)blockpos.getY(), (double)((float)k10.5F), rand.nextFloat() *360.0F,0.0F);
worldIn.spawnEntityInWorld(entityliving);
ientitylivingdata = entityliving.func_180482_a(worldIn.getDifficultyForLocation(newBlockPos(entityliving)), ientitylivingdata);
flag = true;
}
j1 = rand.nextInt(5) - rand.nextInt(5);
for(k1 = rand.nextInt(5) - rand.nextInt(5); j1 = chunkCenterX rangeX || 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,看对应的生物种类能否生成。
publicbooleanCheckCreatureCanSpawn(EnumCreatureType creatureType, BiomeGenBase.SpawnListEntry spawnListEntry, BlockPos blockPos)
{
List list = this.getChunkProvider().getSpawnableList(creatureType, blockPos);
returnlist !=null&& !list.isEmpty() ? list.contains(spawnListEntry) :false;
}
相关的还有ChunkProviderGenerate.getSpawnableList()
publicList getSpawnableList(EnumCreatureType creatureType, BlockPos pos)
{
BiomeGenBase biomegenbase = this.worldObj.getBiomeGenForCoords(pos);
if(this.mapFeaturesEnabled)
{
if(creatureType == EnumCreatureType.MONSTER &&this.scatteredFeatureGenerator.func_175798_a(pos))
{
returnthis.scatteredFeatureGenerator.getScatteredFeatureSpawnList();
}
if(creatureType == EnumCreatureType.MONSTER &&this.settings.useMonuments &&this.oceanMonumentGenerator.func_175796_a(this.worldObj, pos))
{
returnthis.oceanMonumentGenerator.func_175799_b();
}
}
returnbiomegenbase.getSpawnableList(creatureType);
}
而所有的Biome和生物的对应关系都在BiomeGenBase这个类中定义。
在EntityLiving中定义了一个getCanSpawnHere函数,用于查询是否可以在某个位置生成
/**
* Checks if the entity's current position is a valid location to spawn this entity.
*/
publicbooleangetCanSpawnHere()
{
returntrue;
}继承EntityLiving的类就override这个函数,比如EntityAnimal
/**
* Checks if the entity's current position is a valid location to spawn this entity.
*/
publicbooleangetCanSpawnHere()
{
inti = MathHelper.floor_double(this.posX);
intj = MathHelper.floor_double(this.getEntityBoundingBox().minY);
intk = MathHelper.floor_double(this.posZ);
BlockPos blockpos = newBlockPos(i, j, k);
returnthis.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.
*/
publicbooleangetCanSpawnHere()
{
Chunk chunk = this.worldObj.getChunkFromBlockCoords(newBlockPos(MathHelper.floor_double(this.posX),0, MathHelper.floor_double(this.posZ)));
if(this.worldObj.getWorldInfo().getTerrainType().handleSlimeSpawnReduction(rand, worldObj))
{
returnfalse;
}
else
{
if(this.worldObj.getDifficulty() != EnumDifficulty.PEACEFUL)
{
BiomeGenBase biomegenbase = this.worldObj.getBiomeGenForCoords(newBlockPos(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()
{
returnsuper.getCanSpawnHere();
}
if(this.rand.nextInt(10) ==0&& chunk.getRandomWithSeed(987234911L).nextInt(10) ==0&&this.posY <40.0D)
{
returnsuper.getCanSpawnHere();
}
}
returnfalse;
}
}
参考
Minecraft Wiki
Minecraft Forge
http://blog.csdn.net/silangquan/article/details/51258052