• Siki_Unity_7-4_高自由度沙盘游戏地图生成_MineCraft_Uniblocks插件(可拓展)


    Unity 7-4 高自由度沙盘游戏地图生成 MineCraft
    (插件Uniblocks)

    任务1&2&3&4 素材 && 课程演示 && 课程简介

    使用插件Uniblocks Voxel Terrain v1.4.1 -- 专用于生成方块地图 (该插件目前在AssetStore中不可用)
      讲解博客:https://blog.csdn.net/qq_37125419/article/details/78339771
      官方地址:
        https://forum.unity.com/threads/uniblocks-cube-based-infinite-voxel-terrain-engine.226014/

    课程内容:
      生成地形
      创建新的方块
      摆放/删除方块元素
      地图数据的保存/加载
      后续开发的扩展

    任务5:创建工程 导入插件

    将Uniblocks Voxel Terrain v1.4.1.unitypackage导入新建工程MineCraftMapGenerator


    删除多余文件(高亮)
    Standard Assets -- 角色控制
    Uniblocks -- 地形生成
      UniblocksAssets -- DemoScene, Models, Meshes, Textures, Materials等
      UniblocksDocumentation -- 文档
      UniblocksObjects -- Prefabs: 比如blocks,Engine,Chunks等
      UniblocksScripts -- Scripts

    打开Demo.unity
      

    Uniblocks Dude -- 主角
    Engine -- 游戏启动核心
    SimpleSun -- 太阳光
    selected block graphics -- 选中的方块
    crosshair -- 十字准心

    游戏操作:

    空格:跳跃; WASD:行走

    任务6:Uniblock中对方块生成的组织管理方式

    对方块的管理方式:
      Engine引擎-->ChunkManager大块管理器  Chunk大块  VoxelInfo小块
      每一个Block是一个小块,多个小块构成一个Chunk大块
        小块不是单个游戏物体,Chunk才是
          -- 如果每个小块都是单独的游戏物体,都需要进行渲染:耗费性能
          https://blog.csdn.net/qq_30109815/article/details/53769393

    Engine物体中有三个脚本
      Engine.cs -- 配置生成地图所需要的信息
      ChunkManager.cs -- 存放一个集合,管理所有的Chunk,Chunk负责各自内部的VoxelInfo小块
      ConnectionInitializer.cs -- 多人游戏

    Voxel.cs -- 每种小块的共同属性
    VoxelInfo.cs -- 一个大块下的小块们的信息(如位置等)
      详见任务19

    任务7:Block、Voxel和VoxelInfo的区别与联系、禁用抗锯齿

    Block是小块,没有对应类,但是有对应的Prefab(block0~block9)

    每一个Block的prefab中都被添加了一个Voxel.cs的脚本 -- 确定了方块的种类
      Voxel:体素,体素是体积元素(Volume Pixel)的简称

    当一个Block在Scene中被创建出来的时候,就多了一个VoxelInfo.cs的脚本,用于表示在Chunk中的位置

    抗锯齿:
      Console中的警告:
      Uniblocks: Anti-aliasing is enabled. This may cause seam lines to appear between blocks. If you see lines between blocks, try disabling anti-aliasing, switching to deferred rendering path, or adding some texture padding in the engine settings.

    我们会发现,在有的地方,方块之间会出现一条白线,这是渲染造成的问题
    解决方法:Edit->Project Settings->Quality->Anti-Aliasing = Disabled; 关闭抗锯齿即可

    任务8:最简单的地图生成方式

    创建文件夹Scenes,创建简单的场景Simple

    1. 在scene中创建Uniblocks->UniblocksObjects->_DROPTHISINTHESCENE->Engine

    2. 创建空物体,添加脚本ChunkLoader.cs
      ChunkLoader的作用为 启用Engine(相当于开始发动的驾驶员)
      地形是围绕ChunkLoader的位置生成的(与y坐标无关)
      很多时候ChunkLoader游戏物体为角色本身,因为需要将围绕角色生成地形
        地形的高度不会超过48,因此可以将角色的高度调至48以上,表示生成时角色在地图上方

    任务9:地图生成大小的设置 + Chunk的产生和销毁
    &10:Chunk生成的变化高度和大小
    &11:大块贴图、网格、碰撞器的设置
    &12:数据保存和加载

    在菜单栏Window中会发现两个新增的选项:
      UniBlocks-BlockEditor
      UniBlocks-EngineSettings

    这两个选项分别对应UniblocksScripts->Editor中的两个脚本BlockEditor.cs和EngineSettings.cs

    WorldName:与自动创建的Worlds文件夹下的TextWorld文件夹对应,
      每一个WorldName会对应一个文件夹,里面保存着世界的数据

    Chunk相关:
    ChunkSpawnDistance=8:地图大小,以ChunkLoader为中心分别朝四周扩展8个Chunk
      当ChunlLoader移动时,会保证四周都有8个Chunk(新生成chunk补上)
    ChunkDespawnDistance=3:销毁已生成的远距离(超过该距离8+3=11)的Chunk--基于性能考虑

    ChunkHeightRange=3:整个地图中最高和最低不超过3个chunk的高度 (每个高度为ChunkSizeRange)
      因此高度为48~-48之间变化
    ChunkSideLength=16:每个Chunk管理16*16*16m的一个区域(高度不一定为16,但肯定不超过16)

    贴图相关:
    Uniblocks->UniblocksObjects->ChunkObjects->Chunk -- Prefab
      生成Chunk的时候,根据这个prefab来生成Chunk,Chunk中的小块blocks再通过Mesh进行渲染
      Mesh中有很多小格,需要给每个小格贴图--Chunk中的MeshRenderer.material=texture sheet指定贴图
      每个小格的贴图都是从texture sheet中取得的
          -- 贴图可以以其他方式显示,如正方形
        贴图的整体如图:
          
          贴图分成了 8*8 个小格,将所有小格的贴图存放在同一个贴图中 -- 性能优化(贴图越少性能越好)
          增加小贴图,直接修改psd文件在空白处增加即可

    TextureUnit=0.125:由于贴图分成8*8个小格,0.125即1/8
    TexturePadding=0:小格与小格之间的空隙,比如空隙为1个pixel,值就是1/512
      没有空隙的坏处是:由于美工裁剪的不精确,有可能出现把其他小格的部分也包括了,变成细缝

    注意:Chunk是可以添加多个材质MeshRenderer.materials的,要求是这些材质的大小必须相同(便于裁剪成小格)

    其他设置:

    Generate Meshes: 是否生成Mesh网格
    Generate Colliders: 是否生成碰撞体
    Show Border Faces: 略,默认为false

    事件有关:

    Send Camera Look Events: 聚焦在Camera视野的中心 (十字准心 CrossHair)
    Send Cursor Events: 聚焦在鼠标的位置

    数据的保存和加载:

    Save/ Load Voxel Data: 取消勾选时,重新加载场景的时候,会重新生成地图
      如果勾选,在加载场景的时候,则会先判断本地是否有地图数据,如果有则加载。
      -- 保存: 需要手动调用Engine中保存的方法;
      -- 加载: 如果勾选时,会自动在开始场景的时候进行加载

    在DemoScene中会有自动保存功能
      

    Multiplayer设置:

    与地图同步有关,地图在Server端同步,Client从Server端得到数据

    任务13:Block的有关设置(创建、修改、复制、删除)
    &14&15&16:Block的Mesh、贴图、透明度和碰撞器设置

    Window -> Uniblocks -> Block Editor

    BlocksPath: 存储blocks的prefab的路径
      之前说Chunk是生成单位,每个block并不会生成对应的游戏物体 -- 性能优化
      那么这里的blocks的prefab是用来干什么的呢?
        用来保存每一种blocks的属性,并不是用来实例化的

    empty:每一个chunk由长*宽*高个blocks组成,那些空的部分就是由empty blocks填充的
      比如一个chunk,除了表面显示的那部分blocks,下面的是dirt或其他blocks,上面的就是empty blocks了

    创建:点击New block,修改属性即可
    删除:直接删除在Project中的对应prefab即可
    复制:直接修改需要复制的block的id值,按Apply,就能得到一个新的id的block,原来的block不变

    block的属性:

    id -- 每一种block的id是不同的,是identity

    Mesh相关:
    Custom mesh -- 默认的mesh是立方体,比如door和tall grass就是自定义的
    Mesh -- 勾选了Custom Mesh后,需要指定自定义的mesh
    Mesh Rotation -- 勾选了Custom Mesh后,可以选择Mesh Rotation,表示mesh的旋转 (None/ Back/ Right/ Left)
      比如door:如果mesh rotation=back,则门是创建在格子的另一边

    贴图相关:
    当勾选了CustomMesh后,贴图就会使用默认的贴图 -- 在创建模型时就处理好贴图;
    若没有勾选CustomMesh,则可以在这里选择Texture属性
      Texture: 上面对texture sheet进行了讲解,它是一个 8*8的贴图,从左下角开始为 (0, 0)
        之前在EngineSettings中设定了TextureUnit=0.125,
        这里以坐标的方式指定贴图 (x, y)(横向为x轴,纵向为y轴),即可获取对应格子位置的贴图
      Define Side Texture: 每个立方体有六个面,如果六个面的Texture不同,则需要勾选
        比如grass:
          grass的四周是半dirt半grass的显示,上方为grass,下方为dirt
          所以 -- Top: (0,2); Bottom: (0,0); Right/ Left/ Forward/ Back: (0,1)

    Material Index: 如果Chunk的MeshRenderer.material中有多个材质,则可以指定当前为第index个材质

    透明度设置:

    Transparency: Solid 不透明/ Semi Transparent 半透明/ Transparent 全透明
      leave/ grass/ door为半透明
      半透明和全透明的区别:
        全透明会使中间部分没有显示,而半透明会显示中间部分,如:
        
          左图为全透明,右图为半透明,很明显,右图显示的更密集,因为把中间部分的叶子也显示出来了
          上图为Scene视图,Game视图更加明显,也可以观察影子对比。

      对于Solid的方块而言,若六面都有其他方块包裹,则Chunk会将其mesh删除,不再渲染 -- 性能优化

    碰撞器设置:

    Collider: 可以选择Cube/ Mesh/ None
      一般为Cube,door为Mesh,tall grass为None

    id=70的door open是后期添加的block,用来和id=7的door配对,开门以后door block就会转换为door open block了

    Blocks宏观:
      每个block的prefab上挂载一个Voxel.cs脚本,用于上述定义该block的属性,比如mesh/ 透明度等 -- 根据这个来渲染
        渲染之后 (在Chunk中)生成脚本VoxelInfo,用于保存该block在该chunk中的位置信息
      每个prefab上也有其他脚本比如DefaultVoxelEvents.cs,用于实现其他事件操作,比如当人走到该block中时需要怎样
      在生成prefab

    任务17:Block事件类的继承关系

    基事件类:VoxelEvents.cs
      里面是一些virtual的虚方法:-- 需要我们自定义去触发
        Virtual详解:https://blog.csdn.net/songsz123/article/details/7369913
        Virtual与Abstract -- https://www.cnblogs.com/zyj649261718/p/6256327.html
      public virtual void OnMouseDown/Up/Hold (int mouseButton, VoxelInfo voxelInfo) {} // 当鼠标操作时

      public virtual void OnLook (VoxelInfo voxelInfo) {} // 十字准心对准的block,会触发OnLook事件
        -- 将selectedBlock的ui放置在十字准心对准的block的位置

      public virtual void OnBlockPlace/Destroy/Change (VoxelInfo voxelInfo) {} // 放置/销毁/转换一个Block时触发
      -- OnBlockPlace/Destroy/Change()都有对应的Multiplayer版本的方法
        因为这些方法对环境造成了影响,需要做相应的Server端的同步

      public virtual void OnBlockEnter/Stay (GameObject entering/stayingObject, VoxelInfo voxelInfo) {}
        // 当player进入或停留在block上的时候会触发

    事件脚本的调用是在一个临时的对象里面,所以不能在事件脚本里存储数据

    其他事件类:
      DefaultVoxelEvents
      VoxelGrass
      DoorOpenClose

      其中,DefaultVoxelEvents继承自VoxelEvents类,为它的实现类。
        DefaultVoxelEvents被挂载在普通没有特殊功能的block上
        VoxelGrass和DoorOpenClose均继承自DefaultVoxelEvents
          被分别挂载在Grass和Door上

      DefaultVoxelEvents实现了
        OnMouseDown()
        OnLook()
        OnBlockPlace/ Destroy()
        OnBlockEnter()

      VoxelGrass只override了一个方法:
        OnBlockPlace()
          -- switch to dirt if the block above is not id=0
          -- if the block below is grass, change it to dirt

      DoorOpenClose只override了一个方法:
        OnMouseDown()
          -- destroy with left click
          -- for right click, if open door, set to closed; if closed door, set to open

    任务18&19&20&21:事件的触发
    任务18:相机正前方瞄准事件的触发

    在Uniblocks Dude的prefab上,添加了许多脚本
      MouseLook.cs
      CharacterMotor.cs
      FPSInputController.cs
      Debugger.cs
      ExampleInventory.cs
      ChunkLoader.cs -- 任务8中详述,这样就以主角为中心,进行chunk的生成和删除
      CameraEventsSender.cs -- 根据相机的方向进行事件的检测(位于UniblocksScripts->PlayerInteraction)
      ColliderEventsSender.cs
      FrameRateDisplay.cs
      MovementSwitch.cs

    CameraEventsSender.cs
      -- 触发事件
        OnMouseDown/ Up/ Hold()
        OnLook()

    成员变量:
    public float Range; // 可触及的距离
    private GameObject SelectedBlockGraphics; // 处于选中状态的block

    方法:
    Awake() {
      // 初始化Range和SelectedBlockGraphics的值
    }

    Update() {
      // 判断使用哪一种事件:鼠标或是十字准心
      if (Engine.SendCameraLookEvents或SendCursorEvents) { CameraLookEvents()或MouseCursorEvents(); }
    }

    private void CameraLookEvents() {
      // 需要得到当前视野前方的体素
      // 从camera处向视角正前方发出射线,长度为Range
      // 最后一个false为IgnoreTransparent,是否忽略透明的block -- 不忽略
      // 返回的是VoxelInfo对象,表示当前视野正前方的小方块的属性
      VoxelInfo raycast = Engine.VoxelRaycast(Camera.main.transform.position,
        Camera.main.transform.forward, Range, false);

      // draw the ray -- 在Scene模式可以把射线看得更清楚
      Debug.DrawLine(Camera.main.transform.position, Camera.main.transform.position +
        Camera.main.transform.forward * Range, Color.red);

      // 当视野范围range内可以接触到方块时
      if(raycast!=null) ...

      // create a local copy of the hit voxel so we can call functions on it
      GameObject voxelObject = Instantiate(Engine.GetVoxelGameObject(raycast.GetVoxel())) as GameObject;
      // raycast为VoxelInfo对象,VoxelInfo.GetVoxel()返回的是Chunk.GetVoxel(index);
      // 解释:每一个体素都属于一个Chunk,在VoxelInfo中会保留一个Chunk的引用,表示属于该chunk
      // ...
      // raycast.GetVoxel() 返回的是十字准心对准的block的id -- 方块类型

      // 通过Engine.GetVoxelGameObject(id) 得到该类型block的prefab

      // 通过Instantiate(prefab) as GameObject 得到实例voxelObject
      -- 得到了实例化的block,现在就能够进行事件的触发了
      -- 这种事件触发方式效率比较低,因为需要先实例化block,才能进行事件的触发

      // 开始事件处理
      // 如果该block有挂载VoxelEvents,则调用VoxelEvents.OnLook(raycast)事件
      // 并将当前正在看的体素传递过去
      if(voxelObject.GetComponent<VoxelEvents>() != null) {
        voxelObject.GetComponent<VoxelEvents>().OnLook(raycast);

    // 检测鼠标按键事件
    if(int i = 0~2) { // 分别表示三个鼠标按键
      if(Input.GetMouseButton/Down/Up(i)) {
        voxelObject.GetComponent<VoxelEvents>().OnMouseDown/Up/Hold(i, raycast);
        // 传递了十字准心瞄准的block,和按的哪个键
    }}

    }
    // 销毁生成的实例化block
    Destroy(voxelObject);

    } else {
      // 在视野前方没有范围内的block
      // 需要disable selectedBlock
      if(SelectedBlockGraphics != null) {
        SelectedBlockGraphics.GetComponent<Renderer>().enable = false;

    }}}

    代码 -- public class CameraEventsSender : MonoBehaviour {} -- 

    public float Range; // 可触及的距离
    private GameObject SelectedBlockGraphics; // 选中状态的block
    
    public void Awake() {
        if (Range <= 0) {
            Debug.LogWarning("Range must be greater than 0");
            Range = 5.0f;
        }
        SelectedBlockGraphics = GameObject.Find("selected block graphics");
    }
    
    public void Update() {
        // 判断使用哪一种事件,鼠标或是十字准心
        if (Engine.SendCameraLookEvents) { CameraLookEvents(); }
        if (Engine.SendCursorEvents) { MouseCursorEvents(); }
    }
    
    private void CameraLookEvents() {
        // first person camera
        VoxelInfo raycast = Engine.VoxelRaycast
            (Camera.main.transform.position,
            Camera.main.transform.forward, Range, false);
        // 从camera处向视角正前方发出的射线,长度为range
        // 最后一个false为IgnoreTransparent,是否忽略透明的block -- 不忽略
        // 返回的VoxelInfo对象,为当前视野正前方的小方块的属性
    
        // draw the ray -- 在Scene模式可以把射线看得更清楚
        Debug.DrawLine(Camera.main.transform.position,
            Camera.main.transform.position +
            Camera.main.transform.forward * Range, Color.red);
    
        if (raycast != null) { // 视野范围range内接触到方块
            // create a local copy of the hit voxel so we can call functions on it
            GameObject voxelObject = Instantiate(
                Engine.GetVoxelGameObject(raycast.GetVoxel())) as GameObject;
    
            // only execute this if the voxel actually has any voxel events
            if (voxelObject.GetComponent<VoxelEvents>() != null) {
                voxelObject.GetComponent<VoxelEvents>().OnLook(raycast);
    
                // for all mouse buttons, send events
                for (int i = 0; i < 3; i++) {
                    if (Input.GetMouseButtonDown(i)) {
                        voxelObject.GetComp<VoxelEvents>().OnMouseDown(i, raycast);
                    }
                    if (Input.GetMouseButtonUp(i)) {
                        voxelObject.GetComp<VoxelEvents>().OnMouseUp(i, raycast);
                    }
                    if (Input.GetMouseButton(i)) {
                        voxelObject.GetComp<VoxelEvents>().OnMouseHold(i, raycast);
                    }
                }
            }
            Destroy(voxelObject);
        } else {
            // disable selected block ui when no block is hit
            if (SelectedBlockGraphics != null) {
                SelectedBlockGraphics.GetComponent<Renderer>().enabled = false;
    }}}
            
    
    private void MouseCursorEvents() { // cursor position
        //Vector3 pos=new Vector3(Input.mousePosition.x,Input.mousePos.y,10.0f);
        VoxelInfo raycast = Engine.VoxelRaycast(Camera.main.ScreenPointToRay
            (Input.mousePosition), Range, false);
    
        if (raycast != null) {
            // create a local copy of the hit voxel so we can call functions on it
            // ...实例化
    
            // only execute this if the voxel actually has any events
            // ...
            Destroy(voxelObject);
        } else {
            // disable selected block ui when no block is hit...
        }
    }

    任务19:VoxelInfo和Chunk类的API介绍(之间的关系)

    VoxelInfo类:表示当一个block在一个Chunk中存在时,block的属性

    成员变量:
    public Index index; -- 表示该方块存在chunk中的位置
    public Index adjacentIndex;
    public Chunk chunk;  -- 该方块属于的chunk的引用

    -- Index类
      有x, y, z三个成员变量
      如何表示在chunk中的位置呢?
        x轴正方向 == Direction.right
        y轴正方向 == Direction.up
        z轴正方向 == Direction.forward
      计量单位为block个数,而不是距离

    public Index GetAdjacentIndex ( Direction direction ) {
        if (direction == Direction.down)    return new Index(x,y-1,z);
        else if (direction == Direction.up)    return new Index(x,y+1,z);
        else if (direction == Direction.left)    return new Index(x-1,y,z);
        else if (direction == Direction.right)    return new Index(x+1,y,z);
        else if (direction == Direction.back)    return new Index(x,y,z-1);
        else if (direction == Direction.forward)    return new Index(x,y,z+1);
        else return null;
    }

     -- Chunk类

    成员变量:
    public ushort[] VoxelData; // new ushort[SideLength * SideLength * SideLength]; 即16*16*16
      // 存储的为block的id -- 表示每个位置分别为什么类型的block
      // 通过GetVoxel(index)的方法,在任务18中,返回视野指向的block的id
    public Index chunkIndex;
    public Chunk[] NeighborChunks;
    public bool Empty;
    ...

    任务20:OnBlockEnter()和OnBlockStay()的触发

    Uniblocks Dude的脚本ColliderEventSender.cs
      触发事件OnBlockEnter/ Stay()

    成员变量:
    private Index LastIndex;
    private Chunk LastChunk;

    Update() {
      // 得到当前角色所在Chunk
      GameObject chunkObject = Engine.PositionToChunk(transform.position);
      // 因为ColliderEventSender挂载在角色物体,将角色位置transform.position传入Engine.PositionToChunk()
      // 得到该位置对应的chunk

      // 当返回的chunk为空时,如角色在空中时,就不检测碰撞了
      if(chunk == null) return;

      // 得到当前位置的voxelIndex
      Chunk chunk = chunkObject.GetComponent<Chunk>();
      Index voxelIndoex = chunk.PositionToVoxelIndex(transform.position);
      // 通过传递当前位置给chunk.PositionToVoxelIndex()
        -- Chunk.PositionToVoxelIndex(position)
          Vector3 point = transform.InverseTransformPoint(position);
          // 将世界坐标变换为局部坐标
          ...通过Mathf.RoundToInt()给返回值Index赋值 -- 求得角色当前所在体素的index,而不是脚下的体素

      // 通过voxelIndex得到当前voxelInfo -- 因为是角色当前所在的体素,所以id一直为0
      // Bug ...
      // ---- 怎么改bug呢?
      // 可以从当前位置向下发射射线,将碰撞到的collider的位置转换为Index
      // 或可以直接通过Index.y - 1的方法
      VoxelInfo voxelInfo = new VoxelInfo(voxelIndex, chunk);
      // 并实例化该voxel
      GameObject voxelObject = Instantiate(Engine.GetVoxelGameObject(voxelInfo.GetVoxel())) as GameObject;
      VoxelEvents voxelEvents = voxelObject.GetComponent<VoxelEvents>();

      // 得到事件后,触发OnBlockEnter/Stay()事件
      if(events != null) {
        // 因为上面得到的block的id恒为0,而0号block并没有挂载任何VoxelEvents脚本,因此不会进行事件检测
        // OnBlockEnter -- 当当前chunk变动,或voxelIndex变动
        if(chunk != LastChunk || voxelIndex.IsEqual(LastIndex) == false ) {
          voxelEvents.OnBlockEnter(this.gameObject, voxelInfo);
        } else { // OnBlockStay
          voxelEvents.OnBlockStay(this.gameObject, voxelInfo);
      }}

      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        这个时候我反应过来。源代码是没有错的,老师讲解的角度错了。
        我想到的OnBlockStay/ Enter() 是作用在比如压力板、草地、水之类的block上的
        而普通的草地之类的是不需要触发类似事件的
        有因为草地、水这些可以近似看作没有占据物理空间,player是可以进入该体素的
        因此player所在的voxelIndex就是草地、压力板所在的voxelIndex
      !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

      // 销毁刚才实例化的block,并更新当前chunk和voxelIndex
      Destroy(voxelObject);
      LastChunk = chunk;
      LastIndex = voxelIndex;
    }

    任务21:voxel其他事件的触发

    OnBlockPlace()
    OnBlockDestroy()
    OnBlockChange()

    在DefaultVoxelEvents.cs中

    public override void OnMouseDown ( int mouseButton, VoxelInfo voxelInfo ) {
        if ( mouseButton == 0 ) { // destroy a block with LMB
            Voxel.DestroyBlock (voxelInfo); 
        } else if ( mouseButton == 1 ) { // place a block with RMB
            if ( voxelInfo.GetVoxel() == 8 ) { 
                // if we're looking at a tall grass block, replace it with the held block
                Voxel.PlaceBlock (voxelInfo, ExampleInventory.HeldBlock);
            }
            else { // else put the block next to the one we're looking at
                VoxelInfo newInfo=new VoxelInfo (voxelInfo.adjacentIndex, voxelInfo.chunk); 
                // use adjacentIndex to place the block
                Voxel.PlaceBlock (newInfo, ExampleInventory.HeldBlock);
    }}}

    -- Voxel.DestroyBlock(voxelInfo)
      // 实例化当前体素
      GameObject voxelObject = Instantiate(Engine.GetVoxelGameObject(voxelInfo.GetVoxel())) as GameObject;
      // 得到体素的events,并触发事件OnBlockDestroy()
      if(voxelObject.GetComponent<VoxelEvents>() != null) {
        voxelObject.GetComponent<VoxelEvents>().OnBlockDestroy(voxelInfo);
      }
      voxelInfo.chunk.SetVoxel(voxelInfo.index, 0, true);
      Destroy(voxelObject);

    -- OnBlockDestroy(voxelInfo) 中
      // if the block above is tall grass, destroy it as well
      Index indexAbove = ...
      if(voxelInfo.chunk.GetVoxel(indexAbove) == 8) {
        voxelInfo.chunk.SetVoxel(indexAbove, 0, true);
        // 在indexAbove位置,设置为0号block,并update mesh
      }

    -- Voxel.PlaceBlock(voxelInfo) 中
      // 两种情况:1. voxelIndex处为tall grass,2. 不为tall grass
      if(voxelInfo.GetVoxel() == 8) {
        // 直接在当前voxelInfo处PlaceBlock()
        Voxel.PlaceBlock(voxelInfo, ExampleInventory.HeldBlock);
      } else {
        // 在邻接处的voxelIndex处PlaceBlock()
        VoxelInfo adjacentVoxelInfo = new VoxelInfo(voxelInfo.adjacentIndex, voxelInfo.chunk);
        Voxel.PlaceBlock(adjacentVoxelInfo, ExampleInventory.HeldBlock);
      }

    -- Voxel.PlaceBlock(voxelInfo, data)
      // 更新当前voxel
      voxelInfo.chunk.SetVoxel(voxelInfo, data, true);
      // 实例化,并得到events脚本
      GameObject voxelObject = Instantiate(Engine.GetVoxelGameObject(data)) as GameObject;
      if(... != null) {
        voxelObject.GetComponent<VoxelEvents>().OnBlockPlace(voxelInfo);
      }
      Destroy(voxelObject);

    -- OnBlockPlace(voxelInfo)中
      -- 如果放置物的下方是grass且当前物体是solid(不是草或其他门等),则将其自动转换为dirt
      Index indexBelow = ...;
      if(voxelInfo.GetVoxelType().VTransparency == Transparency.solid
        && voxelInfo.chunk.GetVoxel(indexBelow) == 2) {
        voxelInfo.chunk.SetVoxel(indexBelow, 1, true);
      }

    VoxelDoorOpenClose.cs中 -- 门的开关需要触发事件OnBlockChange

    public override void OnMouseDown(int mouseButton, VoxelInfo voxelInfo) {
        if (mouseButton == 0) {
            Voxel.DestroyBlock(voxelInfo);  // destroy with left click
        } else if (mouseButton == 1) { // open/close with right click
            if (voxelInfo.GetVoxel() == 70) { // if open door
                Voxel.ChangeBlock(voxelInfo, 7); // set to closed
            } else if (voxelInfo.GetVoxel() == 7) { // if closed door
                Voxel.ChangeBlock(voxelInfo, 70); // set to open
    }}}

    右键门的时候,如果门的状态为70,则Voxel.ChangeBlock(voxelInfo, 7);
           如果门的状态为7,则Voxel.ChangeBlock(voxelInfo, 70);

    -- Voxel.ChangeBlock(voxelInfo, id) 
      // 更新当前voxel
      voxelInfo.chunk.SetVoxel(voxelInfo.index, data, true);
      // 实例化,并得到VoxelEvents脚本
      GameObject voxelObject = Instantiate(Engine.GetVoxelGameObject(data))) as GameObject;
      if( ... != null) {
        voxelObject.GetComponent<VoxelEvents>().OnBlockChange(voxelInfo);
      }
      Destroy(voxelObject);

    -- 未实现OnBlockChange(voxelInfo)

    事件VoxelEvents总结:

    public class VoxelEvents : MonoBehaviour {
    
        public virtual void OnMouseDown/ Up/ Hold(int mouseButton, VoxelInfo voxelInfo) {
            // 鼠标左右键的按键事件
            // 左键进行DestroyBlock
            // 右键触发PlaceBlock
        }
    
        public virtual void OnLook(VoxelInfo voxelInfo) {
            // selectedBlock在相应位置的显示
        }
    
        public virtual void OnBlockPlace(VoxelInfo voxelInfo) {
            // if the block below is grass, change it to dirt
        }
    
        public virtual void OnBlockDestroy(VoxelInfo voxelInfo) {
            // if the block above is tall grass, destroy it as well
        }
    
        public virtual void OnBlockChange(VoxelInfo voxelInfo) {
        }
    
        public virtual void OnBlockEnter(GameObject enteringObject, VoxelInfo voxelInfo) {
        }
        public virtual void OnBlockStay(GameObject stayingObject, VoxelInfo voxelInfo) {
        }
    }

    评价:这种事件的触发比较耗费性能,因为每次触发都需要实例化一个block的prefab,得到events的脚本,再触发事件

    任务22&23&24:代码实现地图的生成 && Player移动的改进、地图的更新

    新建场景 MineCraft

    导入角色 -- 自定义一个角色,不使用插件中的Uniblocks Dude
      Project -> Import Packages -> Characters -- 从Standard Assets中导入

    将Characters->FirstPersonCharacter->Prefabs->FPSController拖入场景
      这个prefab自带一个Camera,Audio Listener和Flare Layer
      将场景自带的Camera删除

    将Uniblocks中的Engine拖入场景

    创建空物体,命名Manager
      添加脚本MapManager.cs

    如何创建地图呢?
      ChunkManager.SpawnChunks();
      参数可以为Index或Vector3 pos -- Index既可以表示体素在chunk中的位置,也可以表示chunk在地图中的位置

    MapManager.cs中:

    因为需要等待Engine中的Engine.cs和ChunkManager.cs初始化完,才可以开始进行其他地图生成操作
    // 安全判断
    Update() {
      if(Engine.Initialized == false || ChunkManager.Initialized == false) return;
      // 如果每帧都调用,会耗费性能,因此定义成员变量
      -- private bool hasGenerated = false;
      并把上述判断增加一个条件 || hasGenerated)

      // 进行地图的生成
      // 因为要围绕player进行生成
      -- private Transform playerTrans = GameObject.FindWithTag("Player").transform;
      ChunkManager.SpawnChunks(playerTrans.position);
      hasGenerated = true;

    }

    自此,地图在场景开始会进行创建,并且player的移动控制也都实现了

    1. Player控制移动的改进 -- 行走时有晃动的模拟,这里把它取消掉
      取消勾选FirstPersonController.cs中的Use Fov Kick和Use Head Bob

    2. 场景加载刚开始的时候会卡住十几秒 -- 老师的电脑,我自己的不会
      原因:刚开始就进行资源消耗很大的地图生成代码ChunkManager.SpawnChunks()
      解决方案:不要一开始就调用,等一段时间再调用
        将生成地图的代码写入方法 private void InitMap() { ... }
        再将该方法在Start中调用
          InvokeRepeating("InitMap", 1, 0.02f);
          // 一秒钟后开始调用,调用时间间隔为0.02f (即每帧时间间隔,也可写为Time.deltaTime吧)

    3. 在2中为什么要使用InvokeRepeating()重复调用InitMap
      因为我们希望地图的生成会随着Player的位置改变而相应变化
      但是因为hasGenerated的condition,导致InitMap中的生成地图代码的调用只会出现一次

    解决方法:
      当角色的位置发生改变时,就进行InitMap中的生成地图代码
      private Vector3 lastPlayerPos;
      当lastPlayerPos与当前位置不同时
      if(lastPlayerPos != playerTrans.position) {
        ChunkManager.SpawnChunks(playerTrans.position);
        lastPlayerPos = playerTrans.position;
      }

    这么进行地图更新 -- 性能较低
      因为一旦player进行的移动,就会进行地图更新
      而事实上并不需要这么频繁地更新
    解决方法:
      当Player进入另外的chunk时,进行更新即可

      currChunkIndex = Engine.PositionToChunkIndex(playerTrans.position);
      // 注意在开始的时候需要初始化lastChunkIndex的值
      if(lastChunkIndex.x!=currChunkIndex.x || ...y || ...z) {
        ChunkManager.SpawnChunks(playerTrans.position);
        lastChunkIndex = currChunkIndex;
      }

    4. 在3的基础上,进一步进行优化
      调用InitMap()的频率可以低一些,因为player的移动速度是有限的
      InvokeRepeating("InitMap", 1, 1);

    public class MapManager : MonoBehaviour {
        // private bool hasGenerated = false;
        // private Vector3 lastPlayerPos;
        private Transform playerTrans;
        private Index lastChunkIndex = new Index(0, 0, 0);
        private Index currChunkIndex;
    
        void Start() {
            playerTrans = GameObject.FindWithTag("Player").transform;
            InvokeRepeating("InitMap", 1, 1);
        }
    
        private void InitMap() {
            // 安全判断Engine和ChunkManager是否初始化完成
            if (!Engine.Initialized || !ChunkManager.Initialized) {
                return;  // 等待加载完成
            }
    
            /*
            // 每当角色位置更新,就进行SpawnChunks
            if (lastPlayerPos != playerTrans.position) {
                ChunkManager.SpawnChunks(playerTrans.position);
                lastPlayerPos = playerTrans.position;
                // hasGenerated = true;
            }
            */
    
            // 当Player进入另外的Chunk时,进行SpawnChunks
            currChunkIndex = Engine.PositionToChunkIndex(playerTrans.position);
            if (lastChunkIndex.x != currChunkIndex.x
                || lastChunkIndex.y != currChunkIndex.y
                || lastChunkIndex.z != currChunkIndex.z) {
                ChunkManager.SpawnChunks(playerTrans.position);
                lastChunkIndex = currChunkIndex;
    }}}

    任务25&26&27:创建十字准心和获得瞄准的VoxelInfo && Block的放置功能
    && 显示十字准心瞄准的效果、添加简单水资源

    创建十字准心
      UI->Image,位于正中心,SourceImage: None,黑色,调节宽高成一个横条
      创建子物体Image,调节宽高成一个竖条,即可
      
      不需要进行事件监测:
        取消勾选raycast target
        删除EventSystem
        删除Canvas->Graphic Raycaster(Scripte)

      发现,在游戏视野中,可以看到Canvas的边框 -- 白线
      如何消除:http://tieba.baidu.com/p/5138227264
        直接重新打开一个Game窗口即可
        Unity的坑

    实现摆放、生成、删除block功能

    Manager添加脚本BlockManager.cs

    获得十字准心瞄准的体素
    -- Engine.VoxelRaycast(ray, range, ignoreTransparent)

    在Update中

    Engine.VoxelRaycast(Camera.main.transform.position, camera.main.trasnform.forward, range, false);
    // 通过Camera.main获得的相机需要tag="MainCamera"
    // 起点,方向,可触及距离,是否忽略透明物体
    // 返回值为VoxelInfo类型,赋值给VoxelInfo targetVoxelInfo

    // 判断鼠标按键的按下事件
    if(voxelInfo != null) {

    显示十字准心瞄准的位置:
      -- UniblocksObject->Other->selected block graphics
      这是一个prefab,正好比体素大一点,可以作为一个外框显示出来

    // 得到该组件
    -- private Transform selectedBlockEffect;

    // 初始化
    -- selectedBlockEffect = GameObject.Find("selected block graphics").transform;
    -- selectedBlockEffect.gameObject.SetActive(false);

    // 显示该边框
    selectedBlockEffect.position = voxelInfo.chunk.VoxelIndexToPosition(voxelInfo.index);
    selectedBlockEffect.gameObject.SetActive(true);

      if(Input.GetMouseButtonDown(0) {
        // 鼠标左键按下,删除Block功能
        Voxel.DestroyBlock(voxelInfo);
        VoxelInfo.chunk.SetVoxel(
        // ---------运行发现,当player很靠近block的时候,无法销毁
        // 这是因为player自身的collider影响了射线的检测
        // 解决方法:将Player的Layer设置到IgnoreRaycast中即可

    } else if (Input.GetMouseButtonDown(1) {
      // 鼠标右键按下,摆放Block功能

      // 需要知道当前要摆放的是哪一种block
      -- private ushort currBlockId = 0;
      private void BlockSelect() {
        if(ushort i = 0; i < 10; i++) {
          if(Input.GetKeyDown(i.ToString())) {
            currBlockId = i;
      }}}
      -- 在Update开始,调用SelectBlock() 进行block的选定检测

      Voxel.PlaceBlock(voxelInfo, currBlockId);
      // 这么写的结果是什么呢?
        -- 直接替换了视野前方的block,而不是在邻接处增加一个block

      // 邻接处:voxelInfo.adjacentIndex
      VoxelInfo adjacentVoxelInfo = new VoxelInfo(voxelInfo.adjacentIndex, voxelInfo.chunk);
      Voxel.PlaceBlock(adjacentVoxelInfo, currBlockId);
    }

    } else {  // voxelInfo == null
      selectedBlockEffect.gameObject.SetActive(false);
    }

    public class BlockManager : MonoBehaviour {
        private int range = 5;
        private ushort currBlockId = 0;
        private Transform selectedBlockEffect;
        private void Start() {
            selectedBlockEffect = GameObject.Find("selected block graphics").transform;
            selectedBlockEffect.gameObject.SetActive(false);
        }
        private void SelectBlock() {
            for(ushort i = 0; i<10; i++) {
                if(Input.GetKeyDown(i.ToString())) {  currBlockId = i;
        }}}
        void Update () {
            // 得到十字准心对准的体素
            VoxelInfo voxelInfo = Engine.VoxelRaycast(Camera.main.transform.position, 
                Camera.main.transform.forward, range, false);
    
            SelectBlock();
    
            // 对voxelInfo的操作
            if (voxelInfo != null) {
                // 显示十字准心对准的效果
                selectedBlockEffect.position = voxelInfo.chunk.VoxelIndexToPosition(voxelInfo.index);
                selectedBlockEffect.gameObject.SetActive(true);
    
                if(Input.GetMouseButtonDown(0)) {
                    // 鼠标左键,删除
                    Voxel.DestroyBlock(voxelInfo);
                } else if (Input.GetMouseButtonDown(1)) {
                    // 鼠标右键,摆放
                    VoxelInfo adjacentVoxelInfo = new VoxelInfo
                        (voxelInfo.adjacentIndex, voxelInfo.chunk);
                    Voxel.PlaceBlock(adjacentVoxelInfo, currBlockId);
            }} else {
                selectedBlockEffect.gameObject.SetActive(false);
    }}}

    添加水资源(Unity内置):

    Project->Import Package->Environment->Water和Water(Basic)

    这里我选择了Water->Prefabs->WaterProDayTime

    任务28:结束语

    数据的保存和加载

    加载会自动完成,只要勾选了Engine.Save/Load Voxel Data即会在开始场景时自动读取地图数据

    保存:-- Engine.SaveWorld

     

     

     

  • 相关阅读:
    [LeetCode 1029] Two City Scheduling
    POJ 2342 Anniversary party (树形DP入门)
    Nowcoder 106 C.Professional Manager(统计并查集的个数)
    2018 GDCPC 省赛总结
    CF 977 F. Consecutive Subsequence
    Uva 12325 Zombie's Treasure Chest (贪心,分类讨论)
    Poj 2337 Catenyms(有向图DFS求欧拉通路)
    POJ 1236 Network of Schools (强连通分量缩点求度数)
    POJ 1144 Network (求割点)
    POJ 3310 Caterpillar(图的度的判定)
  • 原文地址:https://www.cnblogs.com/FudgeBear/p/8855345.html
Copyright © 2020-2023  润新知