这个系列通过对我的世界Minecraft源码进行拆分讲解,让大家可以清除的了解一款游戏是怎么一步步被实现出来的,下面就介绍Minecraft源码第二篇关于Block,Section和Chunk的使用。

Block 

·block是Minecraft中最基本的组成元素,也就是常说的“块”。其类图如下

我的世界源码java 我的世界源码方块原理_数据结构


图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号。

我的世界源码java 我的世界源码方块原理_ide_02


  

而整个世界就是一个个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相关的一些类的结构如下

我的世界源码java 我的世界源码方块原理_ide_03

 

可以看到这里采用了用了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()处理.