这个系列通过对我的世界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号。
而整个世界就是一个个Chunk组成。
Section在我的世界中用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相关的一些类的结构如下
可以看到这里采用了用了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的加载和卸载
卸载对应的函数是Chunk.onChunkUnload
/**
* 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卸载的时候,需要将chunk信息全部存储到NBT格式的文件中,包括光照信息,entity信息,block信息等。具体处理对应的类是AnvilChunkLoader。
Chunk的加载调用的是 ChunkProviderServer.loadChunk方法, 主要发生
❶初次生成世界,需要预先加载一部分的块,对应的是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();
}
❷服务器中添加了新的玩家,需要加载玩家所在块。
对应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
❸当玩家移动的时候,要将玩家视线内的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()处理.