开发背景:
完成软件工程任务,提高自己解决问题的实际能力,适应二人协作开发的模式,互助进取。 本次作业将完成一个3D版本的俄罗斯方块的小游戏开发。
开发组员:
张羿 2012211841 宋浩达 2012211896
Unity3D,3D开发软件 IDE工具:选用Unity3D软件自带的 MonoDevelop工具
采用的脚本语言是C#,操作系统 win8.1
任务分析:
俄罗斯方块是从小就接触的一个游戏,曾经我也学着做过一个java版本的平面俄罗斯方块游戏:
所以游戏算法逻辑还是很了解的,我还记得当时在JAVA平面中开发俄罗斯方块的时候,需要用图形绘制的类绘制出一些方块图形,用一个二维的逻辑矩阵来表示游戏的逻辑,所谓的游戏逻辑就是用二维的数组矩阵(0或1)来表示此位置是否有方块(在后面我会用图形解释),将方块和游戏逻辑分开,就可以对我们的游戏方块的位置进行抽象,方块的移动靠的是java中对图形的重新绘制,间隔一定的时间对图形进行重新绘制,对于边界判断,障碍物的碰撞,就是用逻辑的矩阵来实现。
在3D中,并没有Java那样需要事件处理机制,线程,等相关技术,所以也没有需要那么多个类,代码相对简单。主要的技术难点也是在于图像的边界判断,和java不同的是,3D中图形的变化不是重新绘制图形,而是通过Unity中的一个重要的Transform 类 通过对 Position, Rantation Scale
三个属性的 操作来实现游戏组件的动作。在3D中,任何一个游戏组件都有这个组件,不管是用户视角的摄像机,还是 一块地板,还是一个方块。
项目中, 我们设定了三个类:
Block用于表示一个方块的各种属性和操作,Manager 用于游戏逻辑控制和运行,GUIController 用于2D界面的按钮控制。
具体实现:
要实现一个3D俄罗斯方块需要做到以下几点:
在出,3D中,我的游戏思路是: 在空间当中构造出一个,类似二维的立体的平面,摄像机正对平面,构造出一个类似槽的东西,在空间中约束方块的活动返回。
它由三个面板构成:
底面:Ground leftWall rightWall
这是只是在空间当中的建模,然而在程序当中需要用数据结构来表示面板,面板具有表示方块是否在位的能力,方块的变换是需要基于面板的,所以在程序中用了一个二维数组来表示面板:
<span style="font-family:Microsoft YaHei;font-size:12px;"> for (int i = 0;i < fieldHeight;i++){
for (int j =0 ;j < maxBlockSize;j++){
fields[j, i] = true;
fields[fieldWidth -1 - j, i] = true;
}
}
for (int i=0;i<fieldWidth;i++){
fields[i, 0] = true;
}</span>
通过此算法 得出的面板数据结构及值为下图表示:
我开始自己的思路是边上只用宽度为1来判断,参考了网上此算法后,才觉得用size为4比较合理。 其中 10为宽度 with 14为高度 方块所能运行的空间就是中间值为0的白色区域。底面是用于逻辑判断的。
2,方块的表示:
俄罗斯方块一共有7类图
由于 在游戏中 图形需要变换,第一个图 和第二图 以及 第三个图和第四个图是不一样的。所以 在程序设计中,俄罗斯方块其实一共有7种图形。不是我们认为的5种。
每种图形有四个小正方形构成,曾经我在java平面开发当中,用的是java中的图形绘制的一个类来绘制方块,是用一个4*4的二维数组来表示一个图形的样子。
在Unity3d中采用了另外一种解决方案:
如
00
00
表示第7个图形。
011
010
010
为一个3*3的矩阵 就可以表示第一个图形。
0000
1111
0000
0000
同样为一个4*4的正方形矩阵,表示的是第6个图形。
因此用一个正方的矩阵来表示一个图形是很恰当的。然而在3D中,表示一个图形,不可能是简单的填充矩形。在Unity 我们操作的就是一个实际的立体的方块(Cube):这是我随意建立的一个方块模型,由这样四个小方块将组成一个图形:
它被称作一个游戏物体 GameObject 我们可以对这个游戏物体的各种信息进行修改,位置,移动,纹理等。但在游戏当中,我们需要通过程序控制,实现对游戏物体的操控。控制一个方块的旋转,下移,加速下移,停止,消去等等。
表示图形的代码:
<span style="font-family:Microsoft YaHei;font-size:12px;">//固定halfSsize 和 childSize大小
halfSize = (size + 1) * 0.5f;//halfsize = 2
childSize = (size - 1) * 0.5f; //childsize=1
halfSizeFloat = size * .5f; //定位屏幕位置.
//将字符串数组矩阵抓换成bool型的矩阵 1 表示有方块.
blockMatrix = new bool[size, size];
for(int y=0;y<size;y++){
for(int x=0;x<size;x++){
if (block[y][x] == '1'){//如果为1 就生成小方块
blockMatrix[y, x] = true;
//实例化一个方块出来. 克隆原始物体并返回克隆物体 Vector3 位置 Quatenion.identity 旋转
var cube = (Transform)Instantiate(Manager.manager.cube, new Vector3(x - childSize, childSize - y, 0), Quaternion.identity); //将矩阵信息改变成位置信息
cube.parent = transform;
}
}
}</span>
2方块的变形和移动:
首先是方块的下落:
Unity中物体的移动,是很方便的,只需要改变游戏物体的Transform ,加上程序控制,就可以让我们的小方块动起来了。
在Unity的C#中,游戏的下落是可以通过Update()方法来实现,也可以通过IEnumerator方案来实现,参考3D开发前辈们的实现方式,采用IEumerator方案实现:在while(true)循环里面使用yield,满足特定条件时跳出循环,设置一个跳出的特定条件,而且是程序会最终执行到那一步,不然,程序会陷入死循环。
算法代码:
<span style="font-family:Microsoft YaHei;font-size:12px;">IEnumerator Fall(){
while(true){
yPosition--;
if (Manager.manager.CheckBlock(blockMatrix, xPosition, yPosition)){
Manager.manager.SetBlock(blockMatrix, xPosition, yPosition + 1);
Destroy(gameObject);
break;
}
for (float i = yPosition + 1;i > yPosition;i -= Time.deltaTime * fallSpeed){
transform.position = new Vector3(transform.position.x, i - childSize, transform.position.z);
yield return null; //迭代器中取得数据立即返回 提高了遍历效率
}
}
}</span>
图形的旋转:图形的旋转,也是需要修改逻辑矩阵中01的值,通过检查方块周围是否有障碍物来判断是否可以旋转,也就是通过 01 true false 逻辑判断的矩阵来判断是否可以旋转。
主要代码如下:
<span style="font-family:Microsoft YaHei;font-size:12px;">void RotateBlock(){
//修改逻辑矩阵中的值
var tempMatrix = new bool[size, size];
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
tempMatrix[y, x] = blockMatrix[x, (size-1)-y];
}
}
//根据CheckBlock 旋转90度 如果周围没有方块则可以旋转
if (!Manager.manager.CheckBlock(tempMatrix, xPosition, yPosition)){
System.Array.Copy(tempMatrix, blockMatrix, size * size);
transform.Rotate(0, 0, 90);//旋转90度
}
}</span>
图形的移动:
图形的移动主要是针对图形的左右移动,和向下加速:
<span style="font-family:Microsoft YaHei;font-size:12px;">IEnumerator CheckInput(){
while(true){
var input = Input.GetAxisRaw("Horizontal");
if (input < 0){
yield return StartCoroutine(MoveHorizontal(-1));
}
if (input > 0){
yield return StartCoroutine(MoveHorizontal(1));
}
if (Input.GetKeyDown(KeyCode.UpArrow)){
RotateBlock();
}
if (Input.GetKeyDown(KeyCode.DownArrow)){
fallSpeed = Manager.manager.blockDropSpeed;
drop = true;
}
if (Input.GetKeyUp("space")){
fallSpeed = Manager.manager.blockNormalFallSpeed;
drop = false;
}
yield return null;
}
}</span>
3边界的判断和消除:
检查方块的算法:
<span style="font-family:Microsoft YaHei;font-size:12px;">public bool CheckBlock(bool [,] blockMatrix, int xPos, int yPos){
var size = blockMatrix.GetLength(0);
for (int y = 0;y < size;y++){
for (int x = 0;x < size;x++){
if (blockMatrix[y, x] && fields[xPos + x, yPos - y]){
return true;
}
}
}
return false;
}</span>
满行的判断和消除<span style="font-family:Microsoft YaHei;font-size:12px;">//判断每行
IEnumerator CheckRows(int yStart, int size){
yield return null;
if (yStart < 1)yStart = 1;
int count = 1;
for (int y = yStart;y < yStart + size;y++){
int x;
for (x = maxBlockSize;x < fieldWidth - maxBlockSize;x++){
if (!fields[x, y]){
break;
}
}
if (x == fieldWidth - maxBlockSize){
yield return StartCoroutine(SetRows(y));
Score += 10 * count;
y--;
count++;
}
}
CreateBlock(blockRandom);
}
IEnumerator SetRows(int yStart){
for (int y = yStart;y < fieldHeight - 1;y++){
for (int x = maxBlockSize;x < fieldWidth - maxBlockSize;x++){
fields[x, y] = fields[x, y + 1];
}
}
for (int x = maxBlockSize;x < fieldWidth - maxBlockSize;x++){
fields[x, fieldHeight - 1] = false;
}
var cubes = GameObject.FindGameObjectsWithTag("Cube");
int cubeToMove = 0;
for (int i = 0;i < cubes.Length;i++){
GameObject cube = cubes[i];
if (cube.transform.position.y > yStart){
cubeYposition[cubeToMove] = (int)(cube.transform.position.y);
cubeTransforms[cubeToMove++] = cube.transform;
}
else if (cube.transform.position.y == yStart){
Destroy(cube);
}
}
float t = 0;
while (t <= 1f){
t += Time.deltaTime * 5f;
for(int i = 0;i < cubeToMove;i++){
cubeTransforms[i].position = new Vector3(cubeTransforms[i].position.x, Mathf.Lerp(cubeYposition[i], cubeYposition[i] - 1, t),
cubeTransforms[i].position.z);
}
yield return null;
}
if (++clearTimes == TimeToAddSpeed){
blockNormalFallSpeed += addSpeed;
clearTimes = 0;
}
}</span>
信息提示:
OnGUI函数,相当于摄像机是一个平面:
<span style="font-family:Microsoft YaHei;font-size:12px;">void OnGUI(){
GUI.BeginGroup (new Rect(10,Screen.height/8*0.5f,100,100));//采用相对布局 适应屏幕的变化
GUI.Label (new Rect(0,10,100,40),".组员:张羿,宋浩达");
GUI.Label(new Rect(0, 30, 80, 40),"分数:");
GUI.Label(new Rect(80, 30, 100, 40),Score.ToString());
GUI.Label(new Rect(0, 50, 80, 40),"最高分:");
GUI.Label(new Rect(80, 50, 80, 40),Highest.ToString());
GUI.EndGroup ();
//设置颜色大小
for (int y = 0;y < nextSize;y++){
for (int x = 0;x < nextSize;x++){
if (nextblock[y][x] == '1'){
GUI.Button(new Rect(180 + 30 * x, 100 + 30 * y, 30, 30), cubeTexture);
}
}
}
}
</span>
如图:分数的记录和最高分的记录:
manager类中:
<span style="font-family:Microsoft YaHei;font-size:14px;">void Start () {
if (manager == null){
manager = this;
}
//游戏存档.
if (PlayerPrefs.HasKey("最高分")){
Highest = PlayerPrefs.GetInt("最高分");
}
else{
PlayerPrefs.SetInt("最高分", 0);
} </span>
程序运行时,Start()函数就会加载,此时就从Unity的游戏存档中读取最高分,如果没有最高分则初始化最高分为0。
背景的添加:
图中的三维坐标系的原点就是摄像机的位置,蓝色方向就是摄像机的方向,也就是我们的视野,图中右下角就是我们眼中的游戏界面,添加游戏背景有两种方法:
1,在游戏槽的后面,也就是摄像机的正前方添加一块Plan(一块挡板),将一张2D的图片,当做纹理附着在面板上。让图片充满整个摄像机的视野。
2,第二种方法就是 我采用的方法,在摄像机游戏物体上添加一个天空盒子的组件:
可以按照我们的需要添加不同的天空
如此以来 游戏变得有背景了。
之后为游戏添加背景音乐:
最后在OnGUIController中添加游戏控制:
<span style="font-family:Microsoft YaHei;font-size:12px;">void OnGUI(){
<span style="white-space: pre;"> </span>//定位屏幕中的位置
GUI.BeginGroup (new Rect(Screen.width/2+Screen.width/4,Screen.height/4-Screen.height/8,120,300));
if (GUI.Button (new Rect (0, 20, 100, 30), "重新开始游戏"))
Application.LoadLevel(0);
if (GUI.Button (new Rect (0, 60, 100, 30), "暂停游戏"))
PauseGame ();
if (GUI.Button (new Rect (0, 100, 100, 30), "继续游戏"))
StartGame ();
if (GUI.Button (new Rect (0, 140, 100, 30), "播放背景音乐"))
audio.Play ();
if (GUI.Button (new Rect (0, 180, 100, 30), "暂停背景音乐"))
audio.Pause ();
if (GUI.Button (new Rect (0, 220, 100, 30), "停止背景音乐"))
audio.Stop ();
GUI.EndGroup();
}
void StartGame()
{
IsGamePaused = false;
Time.timeScale = 1;
//Debug.Log("Start Game" + Time.fixedTime);
}
void PauseGame()
{
IsGamePaused = true;
Time.timeScale = 0;
//Debug.Log("Pause Game");
}</span>
最后添加面板的纹理,使槽看起来不像最开始的那么苍白:
最后打包生成PC windows平台游戏:
遇到的问题:
在开发过程中,遇到的问题很多,主要分为两个方面:实现方式,和实现过程。
在实现方式上,方块的边界判断方式,和方块的消行方式。是我们解决的重点,也是靠做过Unity开发的前辈代码的指点。
而在实现过程上,主要是坐标的定位,会容易出错,因为是三维坐标,坐标定位的准确,和坐标的变化是难点,这是学习了别人的算法明白的,比如:
在代码中:
<span style="font-family:Microsoft YaHei;"><span style="font-size:14px;"> </span><span style="font-size:12px;"> //固定halfSsize 和 childSize大小
halfSize = (size + 1) * 0.5f;//halfsize = 2
childSize = (size - 1) * 0.5f; //childsize=1
halfSizeFloat = size * .5f; //定位屏幕位置.
//将字符串数组矩阵抓换成bool型的矩阵 1 表示有方块.
blockMatrix = new bool[size, size];
for(int y=0;y<size;y++){
for(int x=0;x<size;x++){
//这是哪里赋值
if (block[y][x] == '1'){
blockMatrix[y, x] = true;
//实例化一个方块出来. 克隆原始物体并返回克隆物体 Vector3 位置 Quatenion.identity 旋转
var cube = (Transform)Instantiate(Manager.manager.cube, new Vector3(x - childSize, childSize - y, 0), Quaternion.identity); //将矩阵信息改变成位置信息
cube.parent = transform; //设置 cube 的父级为transfrom 相对于transform来变换
}
}
}
</span></span>
在上断代码中:
如何将Bool形的矩阵定位到屏幕中的位置 通过了两个步骤:
1,halfSize,和ChildSize的取值
<span style="white-space:pre"> </span>halfSize = (size + 1) * 0.5f;//halfsize = 2
childSize = (size - 1) * 0.5f; //childsize=1
2,vector3的定位
<span style="white-space:pre"> </span>var cube = (Transform)Instantiate(Manager.manager.cube, new Vector3(x - childSize, childSize - y, 0), Quaternion.identity); //将矩阵信息改变成位置信息
比如:Size= 3 也就是:
边界长度为3 我们可以看出 为 1 的点 在矩阵中的坐标为: (0,1) (1,1) (1,2) ( 2,2 ) 而在空间当中 要以中间的方块为原点生成方块,所以 矩阵的坐标和空间的坐标的映射关系就为:
<span style="font-family:Microsoft YaHei;font-size:18px;"><span style="white-space:pre"> </span>x - childSize, childSize - y, 0</span>
映射结果为 0,1,0) (0, 0, 0) (1,0,1) (1,-1 ,0) 其实就是以中心点为三维的原点坐标。分别取得每个小方块左前下角的世界坐标。
游戏扩展思路:
方案1,将游戏改为多层的俄罗斯方块,方块不仅可以左右移动还可以前后移动,图像的变形方式 将改为 树立和颠倒和旋转,也就是图形可以360度旋转。
方案2: 设置为空间俄罗斯方块,有三个面板,左侧,右侧,和底面,分别从三个面的前方掉落方块,同时对三个面方块进行游戏,会很大程度的增加游戏难度,但会给人带来既那么熟悉,又不乏乐趣的游戏和视觉体验。