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

課程內容:
  生成地形
  建立新的方塊
  擺放/刪除方塊元素
  地圖數據的保存/加載
  後續開發的擴展安全

任務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 -- Scriptsapp

打開Demo.unity
  ssh

Uniblocks Dude -- 主角
Engine -- 遊戲啓動核心
SimpleSun -- 太陽光
selected block graphics -- 選中的方塊
crosshair -- 十字準心ide

遊戲操做:

空格:跳躍; 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 -- 多人遊戲ui

Voxel.cs -- 每種小塊的共同屬性
VoxelInfo.cs -- 一個大塊下的小塊們的信息(如位置等)
  詳見任務19this

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

 

相關文章
相關標籤/搜索