软件工程第二次二人协作项目 3D俄罗斯方块

1 篇文章 0 订阅

开发背景:

完成软件工程任务,提高自己解决问题的实际能力,适应二人协作开发的模式,互助进取。 本次作业将完成一个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:  设置为空间俄罗斯方块,有三个面板,左侧,右侧,和底面,分别从三个面的前方掉落方块,同时对三个面方块进行游戏,会很大程度的增加游戏难度,但会给人带来既那么熟悉,又不乏乐趣的游戏和视觉体验。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值