Minecraft源码分析(2) - Block,Section和Chunk

Block

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


Minecraft源码分析(2) - Block,Section和Chunk_第1张图片

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


Minecraft源码分析(2) - Block,Section和Chunk_第2张图片

图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相关的一些类的结构如下

Minecraft源码分析(2) - Block,Section和Chunk_第3张图片

图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的加载和卸载


卸载对应的函数是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方法, 主要发生

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


你可能感兴趣的:(源码,算法,游戏开发,三维)