使用插件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
課程內容:
生成地形
建立新的方塊
擺放/刪除方塊元素
地圖數據的保存/加載
後續開發的擴展安全
將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:行走性能
對方塊的管理方式:
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
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; 關閉抗鋸齒便可
建立文件夾Scenes,建立簡單的場景Simple
1. 在scene中建立Uniblocks->UniblocksObjects->_DROPTHISINTHESCENE->Engine
2. 建立空物體,添加腳本ChunkLoader.cs
ChunkLoader的做用爲 啓用Engine(至關於開始發動的駕駛員)
地形是圍繞ChunkLoader的位置生成的(與y座標無關)
不少時候ChunkLoader遊戲物體爲角色自己,由於須要將圍繞角色生成地形
地形的高度不會超過48,所以能夠將角色的高度調至48以上,表示生成時角色在地圖上方
在菜單欄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端獲得數據
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
基事件類: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
在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... } }
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;
...
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;
}
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的腳本,再觸發事件
新建場景 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; }}}
建立十字準心
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
數據的保存和加載
加載會自動完成,只要勾選了Engine.Save/Load Voxel Data即會在開始場景時自動讀取地圖數據
保存:-- Engine.SaveWorld