zoukankan      html  css  js  c++  java
  • 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

     

     

     

  • 相关阅读:
    *args和**kwargs
    事件驱动模型
    同步异步和阻塞非阻塞
    多进程和多线程
    认识tornado(五)
    认识tornado(四)
    认识tornado(三)
    [GO]使用select实现超时
    [GO]使用select实现斐波那契
    [GO]ticker的使用
  • 原文地址:https://www.cnblogs.com/FudgeBear/p/8855345.html
Copyright © 2011-2022 走看看