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

 

 

 

猜你喜欢

转载自www.cnblogs.com/FudgeBear/p/8855345.html