block是Minecraft中最基本的组成元素,也就是常说的“块”。其类图如下
图1. Block结构
简单说明一下Block基类:
pos:块的位置
lightOpacity:透光系数
lightValue:当前块的光照值
blockHardness:块的坚硬度,和挖掘次数有关
slipperriness:摩擦系数
stepSound:踩在Block上的脚步声
函数主要是一些Get/Set方法,还有一些回掉函数。
有了Block基类之后,其他的Block只要继承它就可以了,然后override掉要自定义的函数。
如下图所示,一个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方法和回调函数,下面具体分析一下其中的几个重要函数。
/** * 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的加载调用的是 ChunkProviderServer.loadChunk方法, 主要发生
1)初次生成世界,需要预先加载一部分的块,对应的是Minecraft Server的
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