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 <= ) {
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 = ; i < ; 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-,z);
else if (direction == Direction.up) return new Index(x,y+,z);
else if (direction == Direction.left) return new Index(x-,y,z);
else if (direction == Direction.right) return new Index(x+,y,z);
else if (direction == Direction.back) return new Index(x,y,z-);
else if (direction == Direction.forward) return new Index(x,y,z+);
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 == ) { // destroy a block with LMB
Voxel.DestroyBlock (voxelInfo);
} else if ( mouseButton == ) { // place a block with RMB
    if ( voxelInfo.GetVoxel() == ) {
            // 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 == ) {
Voxel.DestroyBlock(voxelInfo); // destroy with left click
} else if (mouseButton == ) { // open/close with right click
if (voxelInfo.GetVoxel() == ) { // if open door
Voxel.ChangeBlock(voxelInfo, ); // set to closed
} else if (voxelInfo.GetVoxel() == ) { // if closed door
Voxel.ChangeBlock(voxelInfo, ); // 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(, , );
private Index currChunkIndex; void Start() {
playerTrans = GameObject.FindWithTag("Player").transform;
InvokeRepeating("InitMap", , );
} 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 = ;
private ushort currBlockId = ;
private Transform selectedBlockEffect;
private void Start() {
selectedBlockEffect = GameObject.Find("selected block graphics").transform;
selectedBlockEffect.gameObject.SetActive(false);
}
private void SelectBlock() {
for(ushort i = ; i<; 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()) {
// 鼠标左键,删除
Voxel.DestroyBlock(voxelInfo);
} else if (Input.GetMouseButtonDown()) {
// 鼠标右键,摆放
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

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. vue-cli项目打包优化(webpack3.0)

    1.修改source-map配置:此配置能大大减少打包后文件体积. a.首先修改 /config/index.js 文件: // /config/index.js dev环境:devtool: 'ev ...

  2. 数据库学习之中的一个: 在 Oracle sql developer上执行SQL必知必会脚本

    1 首先在開始菜单中打开sql developer: 2. 创建数据库连接 点击左上角的加号 在弹出的对话框中填写username和password 測试假设成功则点击连接,记得角色要写SYSDBA ...

  3. JavaScript中烧脑的&&和||

    在js中经常能看到以下的写法: var obj1 = a || b || c; var obj2 = a && b && c; 刚看到时,很容易认为返回的两个变量都是 ...

  4. BZOJ3514:GERALD07加强版(LCT,主席树)

    Description N个点M条边的无向图,询问保留图中编号在[l,r]的边的时候图中的联通块个数. Input 第一行四个整数N.M.K.type,代表点数.边数.询问数以及询问是否加密. 接下来 ...

  5. Hive学习之路 (十四)Hive分析窗口函数(二) NTILE,ROW_NUMBER,RANK,DENSE_RANK

    概述 本文中介绍前几个序列函数,NTILE,ROW_NUMBER,RANK,DENSE_RANK,下面会一一解释各自的用途. 注意: 序列函数不支持WINDOW子句.(ROWS BETWEEN) 数据 ...

  6. 使用nginx替换Ingress

    总感觉k8s Ingress 不可控, 所以使用nginx 替换Ingress,还是比较简单的. apiVersion: extensions/v1beta1 kind: DaemonSet meta ...

  7. [图解tensorflow源码] Session::Run() 分布式版本

  8. 火狐下不能使用非行间样式currentStyle用getComputedStyle获取

    用js的style属性可以获得html标签的样式,但是不能获取非行间样式.那么怎么用js获取css的非行间样式呢?在IE下可以用currentStyle,而在火狐下面我们需要用到getComputed ...

  9. Linux基础-6.系统的启动过程

    Linux启动时我们会看到许多启动信息 Linux系统的启动过程并不是大家想象中的那么复杂,其过程可以分为5个阶段: 内核的引导 运行init 系统初始化 建立终端 用户登录系统 init程序的类型: ...

  10. STM32中EXTI和NVIC的关系

    (1)NVIC(嵌套向量中断):NVIC是Cortex-M3核心的一部分,关于它的资料不在<STM32的技术参考手册>中,应查阅ARM公司的<Cortex-M3技术参考手册>C ...