我的世界Minecraft源码分析(2):Block,Section和Chunk
这个系列通过对我的世界Minecraft源码进行拆分讲解,让大家可以清除的了解一款游戏是怎么一步步被实现出来的,下面就介绍Minecraft源码第二篇关于Block,Section和Chunk的使用。
Block
block是Minecraft中最基本的组成元素,也就是常说的“块”。其类图如下
图1. Block结构
简单说明一下Block基类:
pos:块的位置
lightOpacity:透光系数
lightValue:当前块的光照值
blockHardness:块的坚硬度,和挖掘次数有关
slipperriness:摩擦系数
stepSound:踩在Block上的脚步声
函数主要是一些Get/Set方法,还有一些回掉函数。
有了Block基类之后,其他的Block只要继承它就可以了,然后override掉要自定义的函数。
Section和Chunk
如下图所示,一个Section由16*16*16,一个Chunk由16个Section组成,最下面是0号,最上面是15号。
图2 Chunk和Section示意图
而整个世界就是一个个Chunk组成。
Section在MC中用ExtendedBlockStorage来描述
- public class ExtendedBlockStorage
- {
- /** Contains the bottom-most Y block represented by this ExtendedBlockStorage. Typically a multiple of 16. */
- private int yBase;
- /** A total count of the number of non-air blocks in this block storage's Chunk. */
- private int blockRefCount;
- /**
- * Contains the number of blocks in this block storage's parent chunk that require random ticking. Used to cull the
- * Chunk from random tick updates for performance reasons.
- */
- private int tickRefCount;
- private char[] data;
- /** The NibbleArray containing a block of Block-light data. */
- private NibbleArray blocklightArray;
- /** The NibbleArray containing a block of Sky-light data. */
- private NibbleArray skylightArray;
- public ExtendedBlockStorage(int y, boolean storeSkylight)
- {
- this.yBase = y;
- this.data = new char[4096];
- this.blocklightArray = new NibbleArray();
- if (storeSkylight)
- {
- this.skylightArray = new NibbleArray();
- }
- }
- public IBlockState get(int x, int y, int z)
- {
- IBlockState iblockstate = (IBlockState)Block.BLOCK_STATE_IDS.getByValue(this.data[y << 8 | z << 4 | x]);
- return iblockstate != null ? iblockstate : Blocks.air.getDefaultState();
- }
- public void set(int x, int y, int z, IBlockState state)
- {
- if (state instanceof net.minecraftforge.common.property.IExtendedBlockState)
- state = ((net.minecraftforge.common.property.IExtendedBlockState) state).getClean();
- IBlockState iblockstate1 = this.get(x, y, z);
- Block block = iblockstate1.getBlock();
- Block block1 = state.getBlock();
- if (block != Blocks.air)
- {
- --this.blockRefCount;
- if (block.getTickRandomly())
- {
- --this.tickRefCount;
- }
- }
- if (block1 != Blocks.air)
- {
- this.blockRefCount;
- if (block1.getTickRandomly())
- {
- this.tickRefCount;
- }
- }
- this.data[y << 8 | z << 4 | x] = (char)Block.BLOCK_STATE_IDS.get(state);
- }
- /**
- * Returns the block for a location in a chunk, with the extended ID merged from a byte array and a NibbleArray to
- * form a full 12-bit block ID.
- */
- public Block getBlockByExtId(int x, int y, int z)
- {
- return this.get(x, y, z).getBlock();
- }
- /**
- * Returns the metadata associated with the block at the given coordinates in this ExtendedBlockStorage.
- */
- public int getExtBlockMetadata(int x, int y, int z)
- {
- IBlockState iblockstate = this.get(x, y, z);
- return iblockstate.getBlock().getMetaFromState(iblockstate);
- }
- /**
- * Returns whether or not this block storage's Chunk is fully empty, based on its internal reference count.
- */
- public boolean isEmpty()
- {
- return this.blockRefCount == 0;
- }
- /**
- * Returns whether or not this block storage's Chunk will require random ticking, used to avoid looping through
- * random block ticks when there are no blocks that would randomly tick.
- */
- public boolean getNeedsRandomTick()
- {
- return this.tickRefCount > 0;
- }
- /**
- * Returns the Y location of this ExtendedBlockStorage.
- */
- public int getYLocation()
- {
- return this.yBase;
- }
- /**
- * Sets the saved Sky-light value in the extended block storage structure.
- */
- public void setExtSkylightValue(int x, int y, int z, int value)
- {
- this.skylightArray.set(x, y, z, value);
- }
- /**
- * Gets the saved Sky-light value in the extended block storage structure.
- */
- public int getExtSkylightValue(int x, int y, int z)
- {
- return this.skylightArray.get(x, y, z);
- }
- /**
- * Sets the saved Block-light value in the extended block storage structure.
- */
- public void setExtBlocklightValue(int x, int y, int z, int value)
- {
- this.blocklightArray.set(x, y, z, value);
- }
- /**
- * Gets the saved Block-light value in the extended block storage structure.
- */
- public int getExtBlocklightValue(int x, int y, int z)
- {
- return this.blocklightArray.get(x, y, z);
- }
- public void removeInvalidBlocks()
- {
- this.blockRefCount = 0;
- this.tickRefCount = 0;
- for (int i = 0; i < 16; i)
- {
- for (int j = 0; j < 16; j)
- {
- for (int k = 0; k < 16; k)
- {
- Block block = this.getBlockByExtId(i, j, k);
- if (block != Blocks.air)
- {
- this.blockRefCount;
- if (block.getTickRandomly())
- {
- this.tickRefCount;
- }
- }
- }
- }
- }
- }
- public char[] getData()
- {
- return this.data;
- }
- public void setData(char[] dataArray)
- {
- this.data = dataArray;
- }
- /**
- * Returns the NibbleArray instance containing Block-light data.
- */
- public NibbleArray getBlocklightArray()
- {
- return this.blocklightArray;
- }
- /**
- * Returns the NibbleArray instance containing Sky-light data.
- */
- public NibbleArray getSkylightArray()
- {
- return this.skylightArray;
- }
- /**
- * Sets the NibbleArray instance used for Block-light values in this particular storage block.
- */
- public void setBlocklightArray(NibbleArray newBlocklightArray)
- {
- this.blocklightArray = newBlocklightArray;
- }
- /**
- * Sets the NibbleArray instance used for Sky-light values in this particular storage block.
- */
- public void setSkylightArray(NibbleArray newSkylightArray)
- {
- this.skylightArray = newSkylightArray;
- }
- }
可以看到,每一个Section都用一个int来表示它是chunk中的第几个Section,data是一个4096个char大小的数组,注意,Java由于使用的unicode编码,所以一个char要用两个Byte,每个byte是4个bit。
4096 = 16 * 16 * 16,也就是每个block用一个char来表示Block的类型以及状态。这里的状态用id来表示,范围是0-65535,不仅包含了块的种类,比如石块,土块等,还包含了比如门是打开的还是关闭的。
接下来是用两个NibbleArray来存储Block的光照信息。一个是点光源,一个是skylight。
NibbleArray里面用了一个2048大小的byte数组来进行存储信息。关于这2048byte的数据分布,MC中的光照强度分0-15级,用4bit来表示,所以一共用4 * 4096 bit,也就是2048个byte来存储。
接下来看一下Chunk
Chunk相关的一些类的结构如下
图3 Chunk相关的一些类
可以看到这里采用了用了Provider模式,将Chunk的接口进行封装了一遍,分别实现了服务器端的ChunkProviderServer和客户端的ChunkProviderClient,服务器端的Prover还含有一个ChunkLoader成员用于处理Chunk的加载和卸载。
Chunk的几个主要成员
posX,posZ:chunk坐标
SectionStorageArray:就是Section
blockBiomeArray:Chunk的Biome信息
heighmap:256长度的int数组记录每一个colume的height
isChunkLoaded:记录Chunk是否被加载
tileEntityList:Chunk中的tileEntity,比如宝箱之类的
entityList:Chunk中的mob等
Chunk的函数成员主要是一些Get/Set方法和回调函数,下面具体分析一下其中的几个重要函数。
Chunk的加载和卸载
- /**
- * Called when this Chunk is unloaded by the ChunkProvider
- */
- public void onChunkUnload()
- {
- this.isChunkLoaded = false;
- Iterator iterator = this.chunkTileEntityMap.values().iterator();
- while (iterator.hasNext())
- {
- TileEntity tileentity = (TileEntity)iterator.next();
- this.worldObj.markTileEntityForRemoval(tileentity);
- }
- for (int i = 0; i < this.entityLists.length; i)
- {
- this.worldObj.unloadEntities(this.entityLists[i]);
- }
- MinecraftForge.EVENT_BUS.post(new ChunkEvent.Unload(this));
- }
主要是将Chunk内的TileEntity和普通entity都添加到World中对应的List,world可以对这些entity做出对应的处理。
Chunk的加载调用的是 ChunkProviderServer.loadChunk方法, 主要发生
1)初次生成世界,需要预先加载一部分的块,对应的是Minecraft Server的 copy
- protected void initialWorldChunkLoad()
- {
- boolean flag = true;
- boolean flag1 = true;
- boolean flag2 = true;
- boolean flag3 = true;
- int i = 0;
- this.setUserMessage("menu.generatingTerrain");
- byte b0 = 0;
- logger.info("Preparing start region for level " b0);
- WorldServer worldserver = net.minecraftforge.common.DimensionManager.getWorld(b0);
- BlockPos blockpos = worldserver.getSpawnPoint();
- long j = getCurrentTimeMillis();
- for (int k = -192; k <= 192 && this.isServerRunning(); k = 16)
- {
- for (int l = -192; l <= 192 && this.isServerRunning(); l = 16)
- {
- long i1 = getCurrentTimeMillis();
- if (i1 - j > 1000L)
- {
- this.outputPercentRemaining("Preparing spawn area", i * 100 / 625);
- j = i1;
- }
- i;
- worldserver.theChunkProviderServer.loadChunk(blockpos.getX() k >> 4, blockpos.getZ() l >> 4);
- }
- }
- this.clearCurrentTask();
- }
2)服务器中添加了新的玩家,需要加载玩家所在块。
对应PlayerInstance中的
- public void addPlayer(EntityPlayerMP playerMP)
- {
- if (this.playersWatchingChunk.contains(playerMP))
- {
- PlayerManager.pmLogger.debug("Failed to add player. {} already is in chunk {}, {}", new Object[] {playerMP, Integer.valueOf(this.chunkCoords.chunkXPos), Integer.valueOf(this.chunkCoords.chunkZPos)});
- }
- else
- {
- if (this.playersWatchingChunk.isEmpty())
- {
- this.previousWorldTime = PlayerManager.this.theWorldServer.getTotalWorldTime();
- }
- this.playersWatchingChunk.add(playerMP);
- Runnable playerRunnable = null;
- if (this.loaded)
- {
- playerMP.loadedChunks.add(this.chunkCoords);
- }
- else
- {
- final EntityPlayerMP tmp = playerMP;
- playerRunnable = new Runnable()
- {
- public void run()
- {
- tmp.loadedChunks.add(PlayerInstance.this.chunkCoords);
- }
- };
- PlayerManager.this.getMinecraftServer().theChunkProviderServer.loadChunk(this.chunkCoords.chunkXPos, this.chunkCoords.chunkZPos, playerRunnable);
- }
- this.players.put(playerMP, playerRunnable);
- }
- }
还有玩家重生时候调用ServerConfiguratuinManager.recreateEntity也会Load对应的Chunk
3)当玩家移动的时候,要将玩家视线内的Chunk都加载进来。
Chunk加载对应的回调函数如下
- /**
- * Called when this Chunk is loaded by the ChunkProvider
- */
- public void onChunkLoad()
- {
- this.isChunkLoaded = true;
- this.worldObj.addTileEntities(this.chunkTileEntityMap.values());
- for (int i = 0; i < this.entityLists.length; i)
- {
- Iterator iterator = this.entityLists[i].iterator();
- while (iterator.hasNext())
- {
- Entity entity = (Entity)iterator.next();
- entity.onChunkLoad();
- }
- this.worldObj.loadEntities(com.google.common.collect.ImmutableList.copyOf(this.entityLists[i]));
- }
- MinecraftForge.EVENT_BUS.post(new ChunkEvent.Load(this));
- }
加载块是从之前存储的NBT中读取,然后把Chunk恢复回来。
玩家移动引起的Chunk加载卸载在PlayerManager,updateMountedMovingPlayer()处理.
参考
Minecraft Wiki
Minecraft forge