Stack Game

Intro

写这个是因为自己和同学尝试用unity实现这个游戏。 stack 核心的算法就在与动态的生成方块,并且在点击时即时的断裂。

实现方法

我借鉴了Procedural Generation中的地图的mesh随机产生的方法,使用mesh filter即时的根据长宽来计算mesh。

顶点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Node{
public Vector3 position;
public int vertexindex = -1;

public Node(Vector3 _pos){
position = _pos;
}
}

public class controlNode:Node{
public Node bottom;

public controlNode(Vector3 _pos,float size):base(_pos){
bottom = new Node(position - Vector3.up * size/4f);
}
}

这2个结构体,第一个是基本的顶点,然而即时生成的cube拥有8个顶点,我们并不想使用8个node去计算,因此使用了controlNode来记录cube的上表面(y轴正方向),其余的顶点使用bottom来记录。

1
2
3
4
5
6
7
8
9
10
11
public class Square{//面包含4个顶点
public Node topLeft,topRight,bottomLeft,bottomRight;

public Square(Node _topLeft, Node _topRight, Node _bottomRight, Node _bottomLeft)
{
topLeft = _topLeft;
topRight = _topRight;
bottomRight = _bottomRight;
bottomLeft = _bottomLeft;
}
}

按照顺时针的顺序来将每4个顶点构成一个面。

Triangles

1
2
3
4
5
6
7
8
9
10
11
struct Triangle{
int vertexA;
int vertexB;
int vertexC;

public Triangle(int a,int b,int c){
vertexA = a;
vertexB = b;
vertexC = c;
}
}

三角形面在通过meshfilter生成具体的模型的时候使用,将Square按照一定的顺序来生成。由于直接用于filter的triangles,故其内部的点的类型并非Node而是int,类似一种索引。

ControlNode

1
2
3
4
5
6
7
public class controlNode:Node{
public Node bottom;

public controlNode(Vector3 _pos,float size):base(_pos){
bottom = new Node(position - Vector3.up * size/4f);
}
}

继承自node类,多出一个bottom用来记录下方的点。此子类主要用于游戏的实时生成断开的cube的时候记录位置使用。

具体的算法

生成Cube

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void createCube(float length, float width){
Vector3 posleft = new Vector3 (-length/2f, 1/4f, width/2f);
Vector3 posright = new Vector3 (length/2f, 1/4f, width/2f);
Vector3 posbleft = new Vector3 (-length/2f, 1/4f, -width/2f);
Vector3 posbright = new Vector3 (length/2f, 1/4f, -width/2f);

controlNode topleft = new controlNode (posleft,1f);
controlNode topright = new controlNode (posright,1f);
controlNode bottomleft = new controlNode (posbleft,1f);
controlNode bottomright = new controlNode (posbright,1f);//控制节点,对应着上顶,侧面和底面使用controlnode的bottom来构造Square,在断裂的时候将传入新的position来分块

Square top = new Square (topleft, topright, bottomright, bottomleft);
Square bottom = new Square (topright.bottom, topleft.bottom, bottomleft.bottom, bottomright.bottom);
Square left = new Square (topleft, bottomleft, bottomleft.bottom, topleft.bottom);
Square right = new Square (bottomright, topright, topright.bottom, bottomright.bottom);
Square forward = new Square (topright, topleft, topleft.bottom, topright.bottom);
Square back = new Square (bottomleft, bottomright, bottomright.bottom, bottomleft.bottom);
//每个面初始化的4个顶点,按照tl,tr,bl,br的顺序构造

squares.Add (top);
squares.Add (left);
squares.Add (right);
squares.Add (forward);
squares.Add (back);
squares.Add (bottom);//将每个面加入队列,在Generate中遍历队列,逐个面计算顶点和三角形
}

squares是一个定义好的储存Square的List。

Triangles的生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void CreateTriangles(Node a,Node b,Node c){
triangles.Add (a.vertexindex);
triangles.Add (b.vertexindex);
triangles.Add (c.vertexindex);

//Triangle triangle = new Triangle (a.vertexindex, b.vertexindex, c.vertexindex);
//Add2TriangleDictionary (a.vertexindex, triangle);
//Add2TriangleDictionary (b.vertexindex, triangle);
//Add2TriangleDictionary (c.vertexindex, triangle);
}

void MeshFromPoints(params Node[] points){
for (int x = 0; x < points.Length; x++) {
//if (points [x].vertexindex == -1) {去掉的原因是保证vertices和uv的count相等,实际只有8个顶点,但是包含重复的顶点一共24个(?如何只保留8个顶点?)
points [x].vertexindex = vertices.Count;
//Debug.Log (points [x].vertexindex);
vertices.Add (points [x].position);
//}
}

if (points.Length >= 3)
CreateTriangles (points [0], points [1], points [2]);
if (points.Length >= 4)
CreateTriangles (points [0], points [2], points [3]);//因为是矩形所以不存在一个面5和6个顶点,故只有2个triangles
//if (points.Length >= 5)
// CreateTriangles (points [0], points [3], points [4]);
//if (points.Length >= 6)
// CreateTriangles (points [0], points [4], points [5]);
}

void TriangluateSquare(Square square){
MeshFromPoints (square.topLeft, square.topRight, square.bottomRight, square.bottomLeft);//计算每个面的三角形
}

分为3个部分

  1. 直接将node类型作为参数传入,将Node类中的vertexindex加入triangles的List。
  2. 通过判定传入的Node参数数量,按照一定的顺序将Square转化为triangles,这里写了如果不是正方形面之后的顺序,顺序可以自定,按照一定的方向不要有反向和重复即可。可以看出triangles和vertices的关系,其在list中的顺序即是点和其所在面的关系。
  3. 直接在Generate中调用的方法,传入Square类型即可。

UV的生成

1
2
3
4
5
6
void caculateUV(Square square){
uv.Add (new Vector3 (square.topLeft.position.x, square.topLeft.position.z));
uv.Add (new Vector3 (square.topRight.position.x, square.topRight.position.z));
uv.Add (new Vector3 (square.bottomLeft.position.x, square.bottomLeft.position.z));
uv.Add (new Vector3 (square.bottomRight.position.x, square.bottomRight.position.z));
}

uv的生成就非常的简单了,直接按照自定的顺序将一个面的4个点的坐标加入list即可。

GenerateCube

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void GenerateCube(float length,float width){//x dir -> length,z dir -> width
if (length >= width) {
movespeed = 6f / length >= 3.8f ? 3.8f : 6f / length;
} else {
movespeed = 6f / width >= 3.8f ? 3.8f : 6f / width;
}


vertices = new List<Vector3>();
triangles = new List<int>();
uv = new List<Vector2> ();

createCube (length, width);

for (int x = 0; x < squares.Count; x++) {
//Debug.Log (squares [x].topLeft.position);
caculateUV(squares[x]);
TriangluateSquare (squares [x]);
}

Mesh mesh = new Mesh();
GetComponent<MeshFilter>().mesh = mesh;
GetComponent<MeshCollider> ().sharedMesh = mesh;

mesh.vertices = vertices.ToArray();
mesh.triangles = triangles.ToArray();
mesh.uv = uv.ToArray();
mesh.RecalculateNormals ();

this.gameObject.AddComponent<Rigidbody> ();
GetComponent<Rigidbody> ().isKinematic = true;//在计算mesh之后加入rigidbody,否则rigidbody读取不到mesh,没有碰撞体积

//Debug.Log (vertices.Count);
//Debug.Log (uv.Count); 2者count必须一样,检错
}

最初的判断忽略,后期为了提高难度加的一个变速,加上之后速度受长宽的影响,只是简单的测试,并不一定难度合理。 Generate的顺序也是非常明显,即先生成Square之后再按照list中的顺序存在vertices和triangles里,最后生成uv即可。调用meshfilter生成,并将rigid body 加上。

游戏逻辑

  1. 主要就是一些方向的判断,具体的生成直接调用cubemeshGenerator的方法即可。其次是一些游戏效果的实现,相机正交这个肯定不用多说,其次是一个combo的记录和背景颜色和cube颜色的渐变。这里使用rgba效果不好,故采用HSV来实现,在unity,ps里都能通过调色板来看hsv的特点。
  2. 其次一点是cube的断裂效果,我采用的方式是记录上次cube的controlNode的位置来实现的,去判定接受到点击指令时的当前的cube的controlNode和记录的cube的controlNode,通过cube的移动方向来生成新的8个点(这里我采用的记录长宽,即是坐标的差值),再次调用GenerateCube并确定生成的位置(y轴不变,x、z由分开的8个点计算)。stack1-w316如图所示,红色和蓝色的点进行计算即可。
  3. 对于cube位置的矫正,如果完全重合才能触发combo,游戏难度过于高,因此通过在断开cube的时候来判定2个cube在运动方向上的距离是否小于设置的临界值(superposition)即可,如果小于即算是触发了combo,在显示效果即是不断开即可。

Source Code