用Unity盖房子(一):《勇者斗恶龙:建造者2》游戏功能的猜想

作者:沈琰 2019-06-04


前言

前段时间一直忙着研究CatLikeCoding的HexMap系列教程,好长时间没有新开坑写点小工程了,这次又有了些新点子,与大家分享一下。

现在轮到本期主角出场了:《勇者斗恶龙:建造者2》(以下简称DQB2)


游戏类型是大家都不陌生的开放世界方块建造类。这类游戏之前也玩过不少,比如《七日杀》、《传送门骑士》,当然还有大名鼎鼎的《Minecraft》,但就个人感觉而言,DQB2在可玩性上要高很多,可以说是此类游戏的集大成之作。并且还融合了一些经营模拟养成,RPG战斗的元素到其中,仅主线任务就让我玩得不亦乐乎。

简而言之就是:我沉迷了。


单就建造而论,DQB2里的工具就设计的非常实用,比如一次性更换多个同种类型方块的刷子,一次获取大量素材的大锤子等,此外还发现了一个十分贴心的功能。

大家都知道建造类游戏有一个问题,就是玩家上下限差距太大。例如《Minecraft》还有一个别称叫"别人的世界"。好不容易自己搭建了一个火柴盒,突然看到视频里大佬搭建的世界奇观,突然就失去玩下去的动力了。即便是想仿照大佬的建筑复制一遍,所需要的工作量也是惊人的,大多数咸鱼(比如我)就直接放弃了。而在DQB2中这个问题得到极大改善,你可以直接联机到大佬的岛上闲逛参观,看见喜欢的建筑样式可以直接把设计图拷贝回来,甚至搭建都不用自己动手,在图纸规划地旁边放上一个装有材料的箱子,NPC就会自动帮忙建造。这简直是建造游戏爱好者的福音,极大的提升了游戏体验,同时也让我对此功能的实现方式产生兴趣。


那么关于安利部分就此打住,进入正题。

下面用Unity来对自动建造功能做一个探索,预计内容分为两篇。第一篇是关于方块建造游戏基础功能在Unity内的实现,第二篇是NPC自动建造系统功能实现方式的猜想。

另外,由于难度直线升高,HexMap系列教程的翻译进度会稍微放缓,但肯定会继续更新下去直到完结,这一点可以放心。

1搭建方块场景

要实现方块场景的搭建编辑功能,最简单粗暴的方法是每一个方块都视为一个单独的GameObject,每次点击时实例化一个方块。简单归简单,但这种方式在效率上肯定会有问题,特别是当在较大的地图上计算物理碰撞而每个方块都有自己的碰撞器时。我不知道好点的电脑运行起来如何,反正我的老爷机肯定就卡逑了。

刚好这个问题可以参考之前六边形地图教程里的思路,把每一次的编辑看做是对一整块mesh里顶点的修改。

(1)获取方块放置坐标

首先要做的是在鼠标指向一个位置时,获取这个位置的方块坐标。即使是粗暴方法这一步也是省不掉的。

为方便起见就设定每个方块的边长是Unity里的标准单位1,那么无论怎么转换,方块坐标都处于方块的中心位置,坐标的小数部分肯定都是是0.5。

  1. public static Vector3 FromWorldPositionToCubePosition(Vector3 position)
  2.     {
  3.         Vector3 resut = Vector3.zero;
  4.         resut.x = position.x > 0 ? (int)position.x * 1f + 0.5f : (int)position.x * 1f - 0.5f;
  5.         resut.y = position.y > 0 ? (int)position.y * 1f + 0.5f : (int)position.y * 1f - 0.5f;
  6.         resut.z = position.z > 0 ? (int)position.z * 1f + 0.5f : (int)position.z * 1f - 0.5f;
  7.         return resut;
  8.     }
复制代码

然后通过屏幕发射射线,换算击中坐标为方块坐标,并用Gizmo验证一下计算是否正确。当然,别忘了新建的测试plane上要有碰撞器,不然射线检测不会起作用。

  1.   bool GetMouseRayPoint(out Vector3 addCubePosition)
  2.     {
  3.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
  4.         RaycastHit hitInfo;
  5.         if (Physics.Raycast(ray, out hitInfo))
  6.         {

  7.             Debug.DrawRay(hitInfo.point, Vector3.up, Color.red);

  8.             addCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point - ray.direction * 0.001f);
  9.            
  10.             return true;
  11.         }
  12.         addCubePosition = Vector3.zero;
  13.         return false;
  14.     }

  15.    private void OnDrawGizmos()
  16.     {
  17.         
  18.         if (GetMouseRayPoint(out Vector3 cubePosition)
  19.         {
  20.             Gizmos.DrawWireCube(cubePosition, Vector3.one);
  21.         }
  22.         
  23.     }
复制代码

红线即鼠标射线击中位置

(2)方块构建

方块的位置正确无误之后,下一步就是添加方块的操作。之前已经说了,要用修改顶点数据的方式来实现这个功能,所以第一步先定义当正方体中心为零点时八个顶点的相对坐标。

  1. public static Vector3[] cubeVertex =
  2.    {
  3.         //上面四个顶点
  4.         //左上
  5.         new Vector3(-0.5f,0.5f,0.5f),
  6.         //右上
  7.         new Vector3(0.5f,0.5f,0.5f),
  8.         //右下
  9.         new Vector3 (0.5f,0.5f,-0.5f),
  10.         //左下
  11.         new Vector3(-0.5f,0.5f,-0.5f),
  12.         //下面四个顶点
  13.         //左上
  14.         new Vector3(-0.5f,-0.5f,0.5f),
  15.         //右上
  16.         new Vector3(0.5f,-0.5f,0.5f),
  17.         //右下
  18.         new Vector3(0.5f,-0.5f,-0.5f),
  19.         //左下
  20.         new Vector3(-0.5f,-0.5f,-0.5f)
  21.     };
复制代码

然后为整个mesh新建一个类,用来处理方块的形状问题。

  1. [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
  2. public class CubeMesh : MonoBehaviour
  3. {
  4.     Mesh cubeMesh;
  5.     MeshCollider meshCollider;


  6.     List<Vector3> vertices;
  7.     List<int> triangles;

  8. private void Awake()
  9. {
  10.     {
  11.         GetComponent<MeshFilter>().mesh = cubeMesh = new Mesh();
  12.         meshCollider = gameObject.AddComponent<MeshCollider>();      
  13.         vertices = new List<Vector3>();
  14.         triangles = new List<int>();   
  15.     }
  16. }
复制代码

由于是正方体,它的三角剖分非常简单且有规律,所以可以写一个较为通用的方法来三角化。这样能使代码更易读,且更方便后续功能的添加。

  1. public void TriaggulateCube(Vector3 p)
  2.     {
  3.        Vector3 v1 = p + CubeMetrics.cubeVertex[0];
  4.        Vector3 v2 = p + CubeMetrics.cubeVertex[1];
  5.        Vector3 v3 = p + CubeMetrics.cubeVertex[2];
  6.        Vector3 v4 = p + CubeMetrics.cubeVertex[3];
  7.        Vector3 v5 = p + CubeMetrics.cubeVertex[4];
  8.        Vector3 v6 = p + CubeMetrics.cubeVertex[5];
  9.        Vector3 v7 = p + CubeMetrics.cubeVertex[6];
  10.        Vector3 v8 = p + CubeMetrics.cubeVertex[7];

  11.         for (int i = 0; i < 6; i++)
  12.         {         
  13.           AddCubeSurface(v1, v2, v3, v4,v5, v6, v7, v8,(CubeSurface)i);      
  14.         }
  15.     }

  16. void AddCubeSurface(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
  17.                      Vector3 v5, Vector3 v6, Vector3 v7, Vector3 v8)
  18.     {
  19.         switch (suface)
  20.         {
  21.             case CubeSurface.up:         
  22.                 AddSurfaceQuad(v1, v2, v3, v4);
  23.                 break;
  24.             case CubeSurface.down:
  25.                 AddSurfaceQuad(v6, v5, v8, v7);
  26.                 break;
  27.             case CubeSurface.left:
  28.                 AddSurfaceQuad(v1, v4, v8, v5);
  29.                 break;
  30.             case CubeSurface.right:
  31.                 AddSurfaceQuad(v3, v2, v6, v7);
  32.                 break;
  33.             case CubeSurface.front:
  34.                 AddSurfaceQuad(v2, v1, v5, v6);
  35.                 break;
  36.             case CubeSurface.back:
  37.                 AddSurfaceQuad(v4, v3, v7, v8);
  38.                 break;
  39.         }
  40.     }

  41. void AddSurfaceQuad(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4)
  42.     {
  43.         int vertexIndex = vertices.Count;
  44.         vertices.Add(v1); vertices.Add(v2); vertices.Add(v3); vertices.Add(v4);
  45.         triangles.Add(vertexIndex); triangles.Add(vertexIndex + 1); triangles.Add(vertexIndex + 2);
  46.         triangles.Add(vertexIndex); triangles.Add(vertexIndex + 2); triangles.Add(vertexIndex + 3);
  47.     }

  48. public enum CubeSurface
  49. {
  50.     front, right, back, left, up, down
  51. }
复制代码

顶点和三角形数据填充进去后再刷新mesh。

  1. public void Apply()
  2.     {
  3.         cubeMesh.SetVertices(vertices);
  4.         cubeMesh.SetTriangles(triangles, 0);
  5.         cubeMesh.RecalculateNormals();
  6.         meshCollider.sharedMesh = cubeMesh;
  7.   
  8.         
  9.     }

  10.     public void Clear()
  11.     {
  12.         vertices.Clear();
  13.         triangles.Clear();
  14.         cubeMesh.Clear();
  15.     }
复制代码



可以看到方块虽然是一个一个添加的,但数据是表现在一个mesh上的。

(3)删除方块

能添加自然就应该能删除,所以下一步是实现删除的功能,后续还能扩展成DQB2里的创造师手套搬运功能。

不知道有没有同学注意到,之前在写射线坐标转换成方块坐标时,代码里给了一个射线反方向的微小偏移,这是为了防止方块坐标在某些角度计算到了错误的位置。由于现在所有方块共用的一个碰撞器,所以没办法通过碰撞信息来识别点击的是哪个方块。那么反过来考虑这个问题,直接通过给射线正方向的偏移,就能让换算坐标变为当前鼠标指着的方块坐标。

  1. bool GetMouseRayPoint(out Vector3 addCubePosition, out Vector3 removeCubePosition)
  2.     {
  3.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
  4.         RaycastHit hitInfo;
  5.         if (Physics.Raycast(ray, out hitInfo))
  6.         {

  7.             Debug.DrawRay(hitInfo.point, Vector3.up, Color.red);

  8.             addCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point - ray.direction * 0.001f);
  9.             removeCubePosition = CubeMetrics.FromWorldPositionToCubePosition(hitInfo.point + ray.direction * 0.001f);
  10.             return true;
  11.         }
  12.         addCubePosition = Vector3.zero;
  13.         removeCubePosition = Vector3.zero;
  14.         return false;
  15.     }
复制代码


坐标计算是没问题,但是该如何告诉mesh删除这些指定的顶点和三角形呢?

办法当然是有,射线的RaycastHit结构体里是可以获取击中位置的三角形下标和uv坐标的,凭借这些信息已经足够计算出要删除的顶点和三角形下标了。


但即使能算出来,用脚指头想也知道会很复杂,咱们不是来做数学题的,所以换个思路。

我们可以这么去思考这个问题:用空的GameObject当做信息载体,在添加方块时添加这些GameObject到mesh脚本新建的容器里,然后遍历这个容器来完成三角化。同理,删除方块时也根据坐标从容器中找到这个GameObject,然后删除它并更新mesh。

  1. public class CubeInfo : MonoBehaviour
  2. {
  3.     public string cubeName;   

  4.     public Vector3 Position
  5.     {
  6.         get
  7.         {
  8.             return transform.localPosition;
  9.         }
  10.     }
  11. }
复制代码

新建上面的脚本并挂在一个空的GameObject上并拖成预制体,然后在添加方块的时候实例化这个预制体并加到列表中。

  1. public void AddCube(Vector3 position)
  2.     {
  3.         CubeInfo cube = Instantiate(CubePrefab, position, Quaternion.identity, transform);
  4.         Debug.Log("传入坐标" + position + "||cube本地坐标" + cube.transform.localPosition+"type:"+(int)type);
  5.       
  6.         Allcubes.Add(cube);
  7.         TriangulateAllCubes();
  8.     }

  9. void TriangulateAllCubes()
  10.     {
  11.         Clear();
  12.         foreach (var c in Allcubes)
  13.         {
  14.             TriaggulateCube(c);
  15.         }
  16.         Apply();
  17.     }
复制代码

这样一来删除方块的方法也容易写了。

  1. public void RemoveCube(Vector3 positon)
  2.     {
  3.         CubeInfo cube;
  4.         if (GetCubeByPosition(positon, out cube))
  5.         {
  6.             Allcubes.Remove(cube);
  7.             Destroy(cube.gameObject);
  8.             TriangulateAllCubes();
  9.         }
  10.     }

  11. bool GetCubeByPosition(Vector3 position, out CubeInfo resutCube)
  12.     {
  13.         for (int i = 0; i < Allcubes.Count; i++)
  14.         {
  15.             if (Allcubes[i].Position == position)
  16.             {
  17.                 resutCube = Allcubes[i];
  18.                 return true;
  19.             }
  20.         }
  21.         resutCube = null;
  22.         return false;
  23.     }
复制代码


2设置相邻方块与顶点优化

到目前为止添加和删除方块都实现了,来考虑一下在两个方块相邻时隐藏接触面来优化顶点的方法。这一步并不是很必要,优化顶点后并不能带来明显的提升,就保持现在这样也没问题。但考虑到在后面还要给NPC做寻路功能,获取方块之间的相邻关系是必须的。以此为前提条件的基础下,那么优化顶点其实就是个顺带的事情。

(1)获取方块之间相邻关系

首先自然就是获取相邻关系,在CubeInfo脚本里新建一个数组去存放相邻方块的引用关系。基于导航的需要,水平面的每个朝向上还要额外存储斜上斜下两个方块,因此最大相邻方块的个数就是3X4+2=14个。把把数组的长度设为14,同时把方向用枚举保存。

  1. public enum CubeNeighborDirection
  2. {
  3.     front,
  4.     frontUp,
  5.     frontDown,

  6.     back,
  7.     backUp,
  8.     backDown,

  9.     left,
  10.     leftUp,
  11.     leftDown,

  12.     right,
  13.     rightUp,
  14.     rightDown,

  15.     up,
  16.     dowm,
  17. }
复制代码

下一步是写一个方法,根据当前方块的坐标和指定方向来推算出这个方向上的方块坐标。

  1. public static Vector3 GetCubePosByDirection(Vector3 pos,CubeNeighborDirection direction)
  2.     {  
  3.         switch (direction)
  4.         {
  5.             case CubeNeighborDirection.front:
  6.                 pos += Vector3.forward;
  7.                 break;
  8.             case CubeNeighborDirection.frontUp:
  9.                 pos += Vector3.forward + Vector3.up;
  10.                 break;
  11.             case CubeNeighborDirection.frontDown:
  12.                 pos += Vector3.forward + Vector3.down;
  13.                 break;
  14.             case CubeNeighborDirection.back:
  15.                 pos += Vector3.back;
  16.                 break;
  17.             case CubeNeighborDirection.backUp:
  18.                 pos += Vector3.back + Vector3.up;
  19.                 break;
  20.             case CubeNeighborDirection.backDown:
  21.                 pos += Vector3.back + Vector3.down;
  22.                 break;
  23.             case CubeNeighborDirection.left:
  24.                 pos += Vector3.left;
  25.                 break;
  26.             case CubeNeighborDirection.leftUp:
  27.                 pos += Vector3.left + Vector3.up;
  28.                 break;
  29.             case CubeNeighborDirection.leftDown:
  30.                 pos += Vector3.left + Vector3.down;
  31.                 break;
  32.             case CubeNeighborDirection.right:
  33.                 pos += Vector3.right;
  34.                 break;
  35.             case CubeNeighborDirection.rightUp:
  36.                 pos += Vector3.right + Vector3.up;
  37.                 break;
  38.             case CubeNeighborDirection.rightDown:
  39.                 pos += Vector3.right + Vector3.down;
  40.                 break;
  41.             case CubeNeighborDirection.up:
  42.                 pos += Vector3.up;
  43.                 break;
  44.             case CubeNeighborDirection.dowm:
  45.                 pos += Vector3.down;
  46.                 break;               
  47.         }
  48.         return pos;
  49.     }
复制代码

下一步就是根据这个坐标,在之前保存的所有CubeInfo的容器中找到与之对应的方块。

  1. bool GetCubeByDirection(Vector3 position, CubeNeighborDirection direction, out CubeInfo resutCube)
  2.     {
  3.         CubeInfo cube;
  4.         if (GetCubeByPosition(CubeMetrics.GetCubePosByDirection(position, direction), out cube))
  5.         {
  6.             resutCube = cube;
  7.             return true;
  8.         }
  9.         resutCube = cube;
  10.         return false;
  11.     }

  12.     bool GetCubeByPosition(Vector3 position, out CubeInfo resutCube)
  13.     {
  14.         for (int i = 0; i < Allcubes.Count; i++)
  15.         {
  16.             if (Allcubes[i].Position == position)
  17.             {
  18.                 resutCube = Allcubes[i];
  19.                 return true;
  20.             }

  21.         }
  22.         resutCube = null;
  23.         return false;
  24.     }
复制代码

然后就可以设置相邻方块了,由于方块的添加有先后,可以在为一个方块设置其相邻方块时在相邻方块的相反方向上设置自己为相邻方块。但是方向的数量并不对称,方向的枚举转换成int不好找到规律,所以就用笨办法。

  1.    public void SetNeighbor(CubeNeighborDirection direction,CubeInfo cube)
  2.     {
  3.         neighbors[(int)direction] = cube;
  4.         cube.neighbors[(int)CubeMetrics.GetOppositeDirection(direction)] = this;
  5.     }
  6.   public static CubeNeighborDirection GetOppositeDirection(CubeNeighborDirection direction)
  7.     {
  8.         switch(direction)
  9.         {
  10.             case CubeNeighborDirection.front:
  11.                 return CubeNeighborDirection.back;
  12.             case CubeNeighborDirection.frontUp:
  13.                 return CubeNeighborDirection.backDown;
  14.             case CubeNeighborDirection.frontDown:
  15.                 return CubeNeighborDirection.backUp;

  16.             case CubeNeighborDirection.back:
  17.                 return CubeNeighborDirection.front;
  18.             case CubeNeighborDirection.backUp:
  19.                 return CubeNeighborDirection.frontDown;
  20.             case CubeNeighborDirection.backDown:
  21.                 return CubeNeighborDirection.frontUp;

  22.             case CubeNeighborDirection.left:
  23.                 return CubeNeighborDirection.right;
  24.             case CubeNeighborDirection.leftUp:
  25.                 return CubeNeighborDirection.rightDown;
  26.             case CubeNeighborDirection.leftDown:
  27.                 return CubeNeighborDirection.rightUp;

  28.             case CubeNeighborDirection.right:
  29.                 return CubeNeighborDirection.left;
  30.             case CubeNeighborDirection.rightUp:
  31.                 return CubeNeighborDirection.leftDown;
  32.             case CubeNeighborDirection.rightDown:
  33.                 return CubeNeighborDirection.leftUp;

  34.             case CubeNeighborDirection.up:
  35.                 return CubeNeighborDirection.dowm;
  36.             case CubeNeighborDirection.dowm:
  37.                 return CubeNeighborDirection.up;

  38.             default:
  39.                 return direction;
  40.         }
  41.     }
复制代码

当然也别忘了在删除方块时把相邻关系也更新一下。

  1. public void RemoveNeighbor(CubeNeighborDirection direction)
  2.     {
  3.         neighbors[(int)direction] = null;
  4.     }
复制代码



现在就能在添加和删除时设置正确的相邻关系了,下一步就是优化顶点了。

(2)顶点优化

现在能知道方块之间的相邻关系,那在相邻方块的方向上隐藏当前面就是一句话的事情了。根据之前表示相邻方向的枚举可知,下标为0,3,6,9,12,13的相邻方块分别对应前,后,左,右,上,下,接下来就是根据当前三角化的面的朝向来检测相邻方块是否为空。

  1. public void TriaggulateCube(Vector3 p)
  2.     {
  3.        Vector3 v1 = p + CubeMetrics.cubeVertex[0];
  4.        Vector3 v2 = p + CubeMetrics.cubeVertex[1];
  5.        Vector3 v3 = p + CubeMetrics.cubeVertex[2];
  6.        Vector3 v4 = p + CubeMetrics.cubeVertex[3];
  7.        Vector3 v5 = p + CubeMetrics.cubeVertex[4];
  8.        Vector3 v6 = p + CubeMetrics.cubeVertex[5];
  9.        Vector3 v7 = p + CubeMetrics.cubeVertex[6];
  10.        Vector3 v8 = p + CubeMetrics.cubeVertex[7];

  11.         for (int i = 0; i < 6; i++)
  12.         {   
  13.             if (i == 0 && cube.neighbors[0]) { continue; }
  14.             else if (i == 1 && cube.neighbors[3]) { continue; }
  15.             else if (i == 2 && cube.neighbors[6]) { continue; }
  16.             else if (i == 3 && cube.neighbors[9]) { continue; }
  17.             else if (i == 4 && cube.neighbors[12]) { continue; }
  18.             else if (i == 5 && cube.neighbors[13]) { continue; }      
  19.           AddCubeSurface(v1, v2, v3, v4,v5, v6, v7, v8,(CubeSurface)i);      
  20.         }
  21.     }
复制代码

减肥前

减肥成功后

虽然看不出来变化,但表现在数据上还是蛮明显的。

3方块的类型UV设置

由于所有方块都用一个Mesh表示,所以直接改其材质球的颜色是无法区分方块类型的。那么办法就是把所有的方块纹理集合在一张纹理图上,而根据方块的类型不同传入不同的UV坐标。所以首先在CubeInfo里定义方块类型的枚举字段。

  1. ublic class CubeInfo : MonoBehaviour
  2. {
  3.     public string cubeName;
  4.     public CubeInfo[] neighbors;

  5.     public M_CubeType type;
  6. }
  7. public enum M_CubeType
  8. {
  9.     test1,
  10.     test2,
  11.     test3,
  12.     test4,
  13.     test5,
  14.     test6
  15. }
复制代码

先暂且用test占位,后面再来考虑具体的类型。至于为什么是6个类型,因为正方形有六个面,设置为六种类型刚好纹理图就是正方形。

然后就找也好,自己画也好,搞到一张6乘6正方形的纹理图,大概就像这样:

随手画的,不好看轻喷..

把纹理图导入Unity中,由于这是像素图,所以记得修改图片的Filter Mode为Point,然后把图片类型改为Sprite。


接下来在添加顶点信息时同时把UV信息也添加进去。这里用了一个易于扩展的写法,之后扩展方块类型也可以直接修改TypeCount的值,很方便。

  1. void AddCubeSurface(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4,
  2.                         Vector3 v5, Vector3 v6, Vector3 v7, Vector3 v8,
  3.                         CubeSurface suface, M_CubeType type,int TypeCount)
  4.     {
  5.         //正方体为六个面,若使UV图为正方形,则暂设正方体的类型为n种
  6.         //v坐标基点:0~5/n

  7.         float uCoordinate = ((int)suface * 1.0f) / 6.0f;
  8.         float vCoordinate=((int)type*1.0f)/TypeCount*1.0f;
  9.      
  10.         Vector2 uvBasePoint=new Vector2(uCoordinate,vCoordinate);

  11.         switch (suface)
  12.         {
  13.             case CubeSurface.up:         
  14.                 AddSurfaceQuad(v1, v2, v3, v4,uvBasePoint,TypeCount);
  15.                 break;
  16.             case CubeSurface.down:
  17.                 AddSurfaceQuad(v6, v5, v8, v7,uvBasePoint, TypeCount);
  18.                 break;
  19.             case CubeSurface.left:
  20.                 AddSurfaceQuad(v1, v4, v8, v5,uvBasePoint, TypeCount);
  21.                 break;
  22.             case CubeSurface.right:
  23.                 AddSurfaceQuad(v3, v2, v6, v7,uvBasePoint, TypeCount);
  24.                 break;
  25.             case CubeSurface.front:
  26.                 AddSurfaceQuad(v2, v1, v5, v6,uvBasePoint, TypeCount);
  27.                 break;
  28.             case CubeSurface.back:
  29.                 AddSurfaceQuad(v4, v3, v7, v8,uvBasePoint, TypeCount);
  30.                 break;
  31.         }
  32.     }

  33. void AddSurfaceQuad(Vector3 v1, Vector3 v2, Vector3 v3, Vector3 v4, Vector2 uvDp,int uvCount)
  34.     {
  35.         AddQuad(v1, v2, v3, v4);
  36.         AddQuadUV(uvDp,uvCount);
  37.     }

  38. void AddQuadUV(Vector2 uvBasePoint,int TypeCount)
  39.     {
  40.         float deltaU = 1f / 6.0f;
  41.         float deltaV = 1f / TypeCount*1.0f;
  42.         Vector2 uv1 = new Vector2(uvBasePoint.x, uvBasePoint.y + deltaV);
  43.         Vector2 uv2 = new Vector2(uvBasePoint.x + deltaU, uvBasePoint.y + deltaV);
  44.         Vector2 uv3 = new Vector2(uvBasePoint.x + deltaU, uvBasePoint.y);
  45.         Vector2 uv4 = uvBasePoint;
  46.         uvs.Add(uv1); uvs.Add(uv2); uvs.Add(uv3); uvs.Add(uv4);
  47.     }
复制代码

在场景里新建6个toggle作为方块类型选择,并关联至脚本里修改方块类型的枚举。

  1. <p>public void TypeSelect(int type)</p><p>
  2. </p><p>{</p><p>
  3. </p><p>cubeType=(M_CubeType)type;</p><p>
  4. </p><p>}</p>
复制代码


现在就可以根据选中的类型来方便切换方块类型了。


4方块旋转

我们的项目里现在都是方块,而且由于我画的UV图除了最上面都是一个样子,方块能不能旋转无所谓。但原版游戏中不是所有的建造素材都是方块形状,其中可能有阶梯或者别的不对称几何形状,我们后续也可以往这方面扩展,所以我们还是有必要去实现这个方块旋转功能。还是用枚举来定义方块的朝向,为方便起见,我们把旋转的范围限制在水平面上。

  1. bool OrientateControl()
  2.     {
  3.         CubeOrientate temp = Orientate;
  4.         if (Input.GetKeyDown(KeyCode.Q))
  5.         {
  6.             Orientate = (int)Orientate == 0 ? (CubeOrientate)3 : (CubeOrientate)Orientate - 1;
  7.         }
  8.         else if (Input.GetKeyDown(KeyCode.E))
  9.         {
  10.             Orientate = (int)Orientate == 3 ? (CubeOrientate)0 : (CubeOrientate)Orientate + 1;
  11.         }

  12.         if(temp!=Orientate)
  13.         {
  14.             return true;
  15.         }

  16.         return false;
  17.     }

  18.     void Update()
  19.     {
  20.       
  21.         if(OrientateControl())
  22.         {
  23.             preview.UpdateCube(cubeType, Orientate);
  24.         }
  25.       
  26.     }

  27. public enum CubeOrientate
  28. {
  29.     front, right, back, left
  30. }
复制代码

然后在CubeInfo里定义方块的朝向字段,在添加方块时将当前朝向一并传入。

  1.   public void AddCube(Vector3 position, M_CubeType type,CubeOrientate orientate)
  2.     {
  3.         CubeInfo cube = Instantiate(CubePrefab, position, Quaternion.identity, transform);
  4.         cube.type = type;
  5.         cube.Orientate = orientate;
  6.         Debug.Log("传入坐标" + position + "||cube本地坐标" + cube.transform.localPosition+"type:"+(int)type);
  7.       

  8.         SetNeighbors(cube);

  9.         TriangulateAllCubes();
  10.     }
复制代码

使用属性,在修改方块朝向枚举的同时也直接修改实际朝向。

  1. public CubeOrientate Orientate
  2.     {
  3.         get
  4.         {
  5.             return orientate;
  6.         }
  7.         set
  8.         {
  9.             
  10.             switch(value)
  11.             {
  12.                 case CubeOrientate.front:
  13.                     transform.forward = Vector3.forward;
  14.                     break;
  15.                 case CubeOrientate.back:
  16.                     transform.forward = Vector3.back;
  17.                     break;
  18.                 case CubeOrientate.left:
  19.                     transform.forward = Vector3.left;
  20.                     break;
  21.                 case CubeOrientate.right:
  22.                     transform.forward = Vector3.right;
  23.                     break;
  24.             }
  25.             orientate = value;
  26.         }
  27.     }
复制代码

刚才很巧合的画了一个六面不同的UV,刚好用来检测旋转功能是否正确。(怎么可能是巧合,我肯定是故意的)


但是还没完,别忘了我们之前还为mesh做了"减肥",那么现在旋转了方块之后对于需要隐藏面的判定就会出问题,所以这个地方需要修正。干脆直接把这个部分抽成一个函数。

  1. public bool CanHideSurface(CubeSurface surface)
  2.     {
  3.         if((int)surface<4)
  4.         {
  5.             int temp = (int)surface -(int)orientate;
  6.             if(temp<0)
  7.             {
  8.                 temp += 4;
  9.             }
  10.             switch((CubeOrientate)temp)
  11.             {
  12.                 case CubeOrientate.front:
  13.                     return neighbors[0];
  14.                 case CubeOrientate.back:
  15.                     return neighbors[3];
  16.                 case CubeOrientate.left:
  17.                     return neighbors[6];
  18.                 case CubeOrientate.right:
  19.                     return neighbors[9];
  20.                 default:
  21.                     return false;
  22.             }
  23.         }
  24.         else if((int)surface == 4)
  25.         {
  26.             return neighbors[12];
  27.         }
  28.         else
  29.         {
  30.             return neighbors[13];
  31.         }
  32.         
  33.     }
  34. }
  35.     void TriaggulateCube(CubeInfo cube)
  36.     {
  37.         TransformToCubeVertices(cube);

  38.         for (int i = 0; i < 6; i++)
  39.         {
  40.             if (!cube.CanHideSurface((CubeSurface)i))
  41.             {
  42.                 AddCubeSurface(tempCubeVertices[0], tempCubeVertices[1], tempCubeVertices[2], tempCubeVertices[3],
  43.                                tempCubeVertices[4], tempCubeVertices[5], tempCubeVertices[6], tempCubeVertices[7],
  44.                               (CubeSurface)i, cube.type,6);
  45.             }
  46.         }
  47.     }
复制代码

然后再检查一下相邻时是否会出问题。


结束

这期咱们算是把基本的架子搭出来了,可以看到使用简单粗暴但耗性能的方式一旦换了个思路,其实还是有点麻烦,但这也正是写这种小工程有意思的地方。


文章的代码贴地有些乱,有兴趣的同学还是下载工程研究吧,感谢观看至此。

本期工程地址:https://github.com/tank1018702/CubeBuilder

有意参与线下游戏开发学习的童鞋,欢迎访问http://levelpp.com/。

作者:沈琰
专栏地址:https://zhuanlan.zhihu.com/p/61067078

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多