IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    qp120291570发表于 2016-05-03 23:27:28
    love 0

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


    卸载对应的函数是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




沪ICP备19023445号-2号
友情链接