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/
课程内容:
生成地形
创建新的方块
摆放/删除方块元素
地图数据的保存/加载
后续开发的扩展
任务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插件(可拓展)的更多相关文章
- 如何在高并发分布式系统中生成全局唯一Id
月整理出来,有兴趣的园友可以关注下我的博客. 分享原由,最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案.我和我的小伙伴们也讨论了这个主题,我受益匪浅啊…… 博文示例: 1. ...
- 如何在高并发分布式系统中生成全局唯一Id(转)
http://www.cnblogs.com/heyuquan/p/global-guid-identity-maxId.html 又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文, ...
- 使用Aspose.Cell控件实现Excel高难度报表的生成(三)
在之前几篇文章中,介绍了关于Apsose.cell这个强大的Excel操作控件的使用,相关文章如下: 使用Aspose.Cell控件实现Excel高难度报表的生成(一) 使用Aspose.Cell控件 ...
- 使用Aspose.Cell控件实现Excel高难度报表的生成(二)
继续在上篇<使用Aspose.Cell控件实现Excel高难度报表的生成(一)>随笔基础上,研究探讨基于模板的Aspose.cell报表实现,其中提到了下面两种报表的界面,如下所示: 或者 ...
- (转)如何在高并发分布式系统中生成全局唯一Id
又一个多月没冒泡了,其实最近学了些东西,但是没有安排时间整理成博文,后续再奉上.最近还写了一个发邮件的组件以及性能测试请看 <NET开发邮件发送功能的全面教程(含邮件组件源码)> ,还弄了 ...
- 高并发分布式系统中生成全局唯一(订单号)Id js返回上一页并刷新、返回上一页、自动刷新页面 父页面操作嵌套iframe子页面的HTML标签元素 .net判断System.Data.DataRow中是否包含某列 .Net使用system.Security.Cryptography.RNGCryptoServiceProvider类与System.Random类生成随机数
高并发分布式系统中生成全局唯一(订单号)Id 1.GUID数据因毫无规律可言造成索引效率低下,影响了系统的性能,那么通过组合的方式,保留GUID的10个字节,用另6个字节表示GUID生成的时间(D ...
- 使用Aspose.Cell控件实现Excel高难度报表的生成
1.使用Aspose.Cell控件实现Excel高难度报表的生成(一) http://www.cnblogs.com/wuhuacong/archive/2011/02/23/1962147.html ...
- 基于eclipse的mybatis映射代码自动生成的插件
基于eclipse的mybatis映射代码自动生成的插件 分类: JAVA 数据库 工具相关2012-04-29 00:15 2157人阅读 评论(9) 收藏 举报 eclipsegeneratori ...
- 基于eclipse的mybatis映射代码自动生成的插件http://blog.csdn.net/fu9958/article/details/7521681
基于eclipse的mybatis映射代码自动生成的插件 分类: JAVA 数据库 工具相关2012-04-29 00:15 2157人阅读 评论(9) 收藏 举报 eclipsegeneratori ...
随机推荐
- vue-cli项目打包优化(webpack3.0)
1.修改source-map配置:此配置能大大减少打包后文件体积. a.首先修改 /config/index.js 文件: // /config/index.js dev环境:devtool: 'ev ...
- 数据库学习之中的一个: 在 Oracle sql developer上执行SQL必知必会脚本
1 首先在開始菜单中打开sql developer: 2. 创建数据库连接 点击左上角的加号 在弹出的对话框中填写username和password 測试假设成功则点击连接,记得角色要写SYSDBA ...
- JavaScript中烧脑的&&和||
在js中经常能看到以下的写法: var obj1 = a || b || c; var obj2 = a && b && c; 刚看到时,很容易认为返回的两个变量都是 ...
- BZOJ3514:GERALD07加强版(LCT,主席树)
Description N个点M条边的无向图,询问保留图中编号在[l,r]的边的时候图中的联通块个数. Input 第一行四个整数N.M.K.type,代表点数.边数.询问数以及询问是否加密. 接下来 ...
- Hive学习之路 (十四)Hive分析窗口函数(二) NTILE,ROW_NUMBER,RANK,DENSE_RANK
概述 本文中介绍前几个序列函数,NTILE,ROW_NUMBER,RANK,DENSE_RANK,下面会一一解释各自的用途. 注意: 序列函数不支持WINDOW子句.(ROWS BETWEEN) 数据 ...
- 使用nginx替换Ingress
总感觉k8s Ingress 不可控, 所以使用nginx 替换Ingress,还是比较简单的. apiVersion: extensions/v1beta1 kind: DaemonSet meta ...
- [图解tensorflow源码] Session::Run() 分布式版本
- 火狐下不能使用非行间样式currentStyle用getComputedStyle获取
用js的style属性可以获得html标签的样式,但是不能获取非行间样式.那么怎么用js获取css的非行间样式呢?在IE下可以用currentStyle,而在火狐下面我们需要用到getComputed ...
- Linux基础-6.系统的启动过程
Linux启动时我们会看到许多启动信息 Linux系统的启动过程并不是大家想象中的那么复杂,其过程可以分为5个阶段: 内核的引导 运行init 系统初始化 建立终端 用户登录系统 init程序的类型: ...
- STM32中EXTI和NVIC的关系
(1)NVIC(嵌套向量中断):NVIC是Cortex-M3核心的一部分,关于它的资料不在<STM32的技术参考手册>中,应查阅ARM公司的<Cortex-M3技术参考手册>C ...