使用插件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