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 {} --

  1. public float Range; // 可触及的距离
  2. private GameObject SelectedBlockGraphics; // 选中状态的block
  3.  
  4. public void Awake() {
  5. if (Range <= ) {
  6. Debug.LogWarning("Range must be greater than 0");
  7. Range = 5.0f;
  8. }
  9. SelectedBlockGraphics = GameObject.Find("selected block graphics");
  10. }
  11.  
  12. public void Update() {
  13. // 判断使用哪一种事件,鼠标或是十字准心
  14. if (Engine.SendCameraLookEvents) { CameraLookEvents(); }
  15. if (Engine.SendCursorEvents) { MouseCursorEvents(); }
  16. }
  17.  
  18. private void CameraLookEvents() {
  19. // first person camera
  20. VoxelInfo raycast = Engine.VoxelRaycast
  21. (Camera.main.transform.position,
  22. Camera.main.transform.forward, Range, false);
  23. // 从camera处向视角正前方发出的射线,长度为range
  24. // 最后一个false为IgnoreTransparent,是否忽略透明的block -- 不忽略
  25. // 返回的VoxelInfo对象,为当前视野正前方的小方块的属性
  26.  
  27. // draw the ray -- 在Scene模式可以把射线看得更清楚
  28. Debug.DrawLine(Camera.main.transform.position,
  29. Camera.main.transform.position +
  30. Camera.main.transform.forward * Range, Color.red);
  31.  
  32. if (raycast != null) { // 视野范围range内接触到方块
  33. // create a local copy of the hit voxel so we can call functions on it
  34. GameObject voxelObject = Instantiate(
  35. Engine.GetVoxelGameObject(raycast.GetVoxel())) as GameObject;
  36.  
  37. // only execute this if the voxel actually has any voxel events
  38. if (voxelObject.GetComponent<VoxelEvents>() != null) {
  39. voxelObject.GetComponent<VoxelEvents>().OnLook(raycast);
  40.  
  41. // for all mouse buttons, send events
  42. for (int i = ; i < ; i++) {
  43. if (Input.GetMouseButtonDown(i)) {
  44. voxelObject.GetComp<VoxelEvents>().OnMouseDown(i, raycast);
  45. }
  46. if (Input.GetMouseButtonUp(i)) {
  47. voxelObject.GetComp<VoxelEvents>().OnMouseUp(i, raycast);
  48. }
  49. if (Input.GetMouseButton(i)) {
  50. voxelObject.GetComp<VoxelEvents>().OnMouseHold(i, raycast);
  51. }
  52. }
  53. }
  54. Destroy(voxelObject);
  55. } else {
  56. // disable selected block ui when no block is hit
  57. if (SelectedBlockGraphics != null) {
  58. SelectedBlockGraphics.GetComponent<Renderer>().enabled = false;
  59. }}}
  60.  
  61. private void MouseCursorEvents() { // cursor position
  62. //Vector3 pos=new Vector3(Input.mousePosition.x,Input.mousePos.y,10.0f);
  63. VoxelInfo raycast = Engine.VoxelRaycast(Camera.main.ScreenPointToRay
  64. (Input.mousePosition), Range, false);
  65.  
  66. if (raycast != null) {
  67. // create a local copy of the hit voxel so we can call functions on it
  68. // ...实例化
  69.  
  70. // only execute this if the voxel actually has any events
  71. // ...
  72. Destroy(voxelObject);
  73. } else {
  74. // disable selected block ui when no block is hit...
  75. }
  76. }

任务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个数,而不是距离

  1. public Index GetAdjacentIndex ( Direction direction ) {
  2. if (direction == Direction.down) return new Index(x,y-,z);
  3. else if (direction == Direction.up) return new Index(x,y+,z);
  4. else if (direction == Direction.left) return new Index(x-,y,z);
  5. else if (direction == Direction.right) return new Index(x+,y,z);
  6. else if (direction == Direction.back) return new Index(x,y,z-);
  7. else if (direction == Direction.forward) return new Index(x,y,z+);
  8. else return null;
  9. }

-- 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中

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

-- 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

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

右键门的时候,如果门的状态为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总结:

  1. public class VoxelEvents : MonoBehaviour {
  2.  
  3. public virtual void OnMouseDown/ Up/ Hold(int mouseButton, VoxelInfo voxelInfo) {
  4. // 鼠标左右键的按键事件
  5. // 左键进行DestroyBlock
  6. // 右键触发PlaceBlock
  7. }
  8.  
  9. public virtual void OnLook(VoxelInfo voxelInfo) {
  10. // selectedBlock在相应位置的显示
  11. }
  12.  
  13. public virtual void OnBlockPlace(VoxelInfo voxelInfo) {
  14. // if the block below is grass, change it to dirt
  15. }
  16.  
  17. public virtual void OnBlockDestroy(VoxelInfo voxelInfo) {
  18. // if the block above is tall grass, destroy it as well
  19. }
  20.  
  21. public virtual void OnBlockChange(VoxelInfo voxelInfo) {
  22. }
  23.  
  24. public virtual void OnBlockEnter(GameObject enteringObject, VoxelInfo voxelInfo) {
  25. }
  26. public virtual void OnBlockStay(GameObject stayingObject, VoxelInfo voxelInfo) {
  27. }
  28. }

评价:这种事件的触发比较耗费性能,因为每次触发都需要实例化一个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);

  1. public class MapManager : MonoBehaviour {
  2. // private bool hasGenerated = false;
  3. // private Vector3 lastPlayerPos;
  4. private Transform playerTrans;
  5. private Index lastChunkIndex = new Index(, , );
  6. private Index currChunkIndex;
  7.  
  8. void Start() {
  9. playerTrans = GameObject.FindWithTag("Player").transform;
  10. InvokeRepeating("InitMap", , );
  11. }
  12.  
  13. private void InitMap() {
  14. // 安全判断Engine和ChunkManager是否初始化完成
  15. if (!Engine.Initialized || !ChunkManager.Initialized) {
  16. return; // 等待加载完成
  17. }
  18.  
  19. /*
  20. // 每当角色位置更新,就进行SpawnChunks
  21. if (lastPlayerPos != playerTrans.position) {
  22. ChunkManager.SpawnChunks(playerTrans.position);
  23. lastPlayerPos = playerTrans.position;
  24. // hasGenerated = true;
  25. }
  26. */
  27.  
  28. // 当Player进入另外的Chunk时,进行SpawnChunks
  29. currChunkIndex = Engine.PositionToChunkIndex(playerTrans.position);
  30. if (lastChunkIndex.x != currChunkIndex.x
  31. || lastChunkIndex.y != currChunkIndex.y
  32. || lastChunkIndex.z != currChunkIndex.z) {
  33. ChunkManager.SpawnChunks(playerTrans.position);
  34. lastChunkIndex = currChunkIndex;
  35. }}}

任务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);
}

  1. public class BlockManager : MonoBehaviour {
  2. private int range = ;
  3. private ushort currBlockId = ;
  4. private Transform selectedBlockEffect;
  5. private void Start() {
  6. selectedBlockEffect = GameObject.Find("selected block graphics").transform;
  7. selectedBlockEffect.gameObject.SetActive(false);
  8. }
  9. private void SelectBlock() {
  10. for(ushort i = ; i<; i++) {
  11. if(Input.GetKeyDown(i.ToString())) { currBlockId = i;
  12. }}}
  13. void Update () {
  14. // 得到十字准心对准的体素
  15. VoxelInfo voxelInfo = Engine.VoxelRaycast(Camera.main.transform.position,
  16. Camera.main.transform.forward, range, false);
  17.  
  18. SelectBlock();
  19.  
  20. // 对voxelInfo的操作
  21. if (voxelInfo != null) {
  22. // 显示十字准心对准的效果
  23. selectedBlockEffect.position = voxelInfo.chunk.VoxelIndexToPosition(voxelInfo.index);
  24. selectedBlockEffect.gameObject.SetActive(true);
  25.  
  26. if(Input.GetMouseButtonDown()) {
  27. // 鼠标左键,删除
  28. Voxel.DestroyBlock(voxelInfo);
  29. } else if (Input.GetMouseButtonDown()) {
  30. // 鼠标右键,摆放
  31. VoxelInfo adjacentVoxelInfo = new VoxelInfo
  32. (voxelInfo.adjacentIndex, voxelInfo.chunk);
  33. Voxel.PlaceBlock(adjacentVoxelInfo, currBlockId);
  34. }} else {
  35. selectedBlockEffect.gameObject.SetActive(false);
  36. }}}

添加水资源(Unity内置):

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

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

任务28:结束语

数据的保存和加载

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

保存:-- Engine.SaveWorld

Siki_Unity_7-4_高自由度沙盘游戏地图生成_MineCraft_Uniblocks插件(可拓展)的更多相关文章

  1. 如何在高并发分布式系统中生成全局唯一Id

    月整理出来,有兴趣的园友可以关注下我的博客. 分享原由,最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案.我和我的小伙伴们也讨论了这个主题,我受益匪浅啊…… 博文示例: 1.     ...

  2. 如何在高并发分布式系统中生成全局唯一Id(转)

    http://www.cnblogs.com/heyuquan/p/global-guid-identity-maxId.html 又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文, ...

  3. 使用Aspose.Cell控件实现Excel高难度报表的生成(三)

    在之前几篇文章中,介绍了关于Apsose.cell这个强大的Excel操作控件的使用,相关文章如下: 使用Aspose.Cell控件实现Excel高难度报表的生成(一) 使用Aspose.Cell控件 ...

  4. 使用Aspose.Cell控件实现Excel高难度报表的生成(二)

    继续在上篇<使用Aspose.Cell控件实现Excel高难度报表的生成(一)>随笔基础上,研究探讨基于模板的Aspose.cell报表实现,其中提到了下面两种报表的界面,如下所示: 或者 ...

  5. (转)如何在高并发分布式系统中生成全局唯一Id

    又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文,后续再奉上.最近还写了一个发邮件的组件以及性能测试请看 <NET开发邮件发送功能的全面教程(含邮件组件源码)> ,还弄了 ...

  6. 高并发分布式系统中生成全局唯一(订单号)Id js返回上一页并刷新、返回上一页、自动刷新页面 父页面操作嵌套iframe子页面的HTML标签元素 .net判断System.Data.DataRow中是否包含某列 .Net使用system.Security.Cryptography.RNGCryptoServiceProvider类与System.Random类生成随机数

    高并发分布式系统中生成全局唯一(订单号)Id   1.GUID数据因毫无规律可言造成索引效率低下,影响了系统的性能,那么通过组合的方式,保留GUID的10个字节,用另6个字节表示GUID生成的时间(D ...

  7. 使用Aspose.Cell控件实现Excel高难度报表的生成

    1.使用Aspose.Cell控件实现Excel高难度报表的生成(一) http://www.cnblogs.com/wuhuacong/archive/2011/02/23/1962147.html ...

  8. 基于eclipse的mybatis映射代码自动生成的插件

    基于eclipse的mybatis映射代码自动生成的插件 分类: JAVA 数据库 工具相关2012-04-29 00:15 2157人阅读 评论(9) 收藏 举报 eclipsegeneratori ...

  9. 基于eclipse的mybatis映射代码自动生成的插件http://blog.csdn.net/fu9958/article/details/7521681

    基于eclipse的mybatis映射代码自动生成的插件 分类: JAVA 数据库 工具相关2012-04-29 00:15 2157人阅读 评论(9) 收藏 举报 eclipsegeneratori ...

随机推荐

  1. 关于移动端APP开发-字体样式变大问题

    前两天在写App项目的时候发现一个问题,就是明明css写的样式是14px,刚开始在页面显示时并未出现问题,可是内容一多,字体突然变大了. what?,不明所以,在各大网站上找了好久才知道是浏览器的字体 ...

  2. JDBC使用

    在工作中碰到要向另一个数据库进行操作的需求,例如数据源为mysql的工程某个方法内需要向oracle数据库进行某些查询操作 接口类 package com.y.erp.pur.util; import ...

  3. Android 高级UI设计笔记24:Android 夜间模式之 WebView 实现白天 / 夜间阅读模式 (使用JavaScript)

    1. 问题引入: 前面我们是使用方法 降低屏幕亮度(不常用) 和 替换theme,两者都是针对Activity的背景进行白天.夜间模式的交换,但是如果我们显示的是Html的内容,这个时候改怎么办? 分 ...

  4. ios的图片解压

    YYKit SDWebImage FLAnimatedImage YYKit YYCGImageCreateDecodedCopy YYImageCoder 1 2 3 4 5 6 7 8 9 10 ...

  5. [HAOI2015]按位或

    题目 好神的题啊 我们发现我们求这个东西如果常规\(dp\)的话可以建出一张拓扑图来,但是边的级别高达\(3^n\),转移的时候还要解方程显然不能通过本题 我们考虑神仙的\(min-max\)容斥 设 ...

  6. C++ pair(对组)的简单了解

    类模板:template<class T1,class T2> struct pair 参数:T1是第一个值得数据类型,T2是第二个值的数据类型. 功能:pair将一对值组合成一个值, 这 ...

  7. 细数用anaconda安装mayavi时出现的各种问题

    这段时间需要利用mayavi做科学数据的处理,因此需要利用到mayavi库,但是官网上面的指示说:如果安装了anaconda,其中自带各种科学库,但是实践中,并没有发现mayavi. 官方网站导航:m ...

  8. P1586 四方定理

    题目描述 四方定理是众所周知的:任意一个正整数nn ,可以分解为不超过四个整数的平方和.例如:25=1^{2}+2^{2}+2^{2}+4^{2}25=12+22+22+42 ,当然还有其他的分解方案 ...

  9. 关闭生产订单时报错“订单&的未处理将来更改记录组织删除标记/完成”,消息号CO688

    消息号 CO688 诊断 仍存在未来的更改记录,或从订单的确认过程的确认中要处理的错误记录.可能的确认过程是: 自动收货 反冲 实际成本的计算 数据传输至 HR 系统响应 未打算对订单设置删除标记/‘ ...

  10. mysql/mariadb学习记录——查询2

    Alias——使用一个列名别名AS 关键字: mysql> select sno as studentId,sname as studentName from student; +------- ...