Mvc开发俄罗斯方块
1. 前言
这个嘛,主要是跟着学习一下mvc的框架,业余跟着做一下,以求更规范的开发游戏,希望各位在看完本博客后也能有所收获
2..初步搭建我们的mvc框架
首先,我们将我们的脚本进行分层
1. model层,存放有关于我们模型的数据处理
2. view层,控制我们想要显示的视图
3. ctrl层,作为model层和view层的中介
我们采用这样的框架来进行我们俄罗斯方块的开发,分层的结构能帮助我们更好地管理我们的数据和信息,model层和view层会实现完全的分离,那么首先让我来展示一下我们的ctrl层对于其他两层的引用
[HideInInspector]
public Modelmodel;
[HideInInspector]
public Viewview;
// Use this for initialization
private void Awake()
{
//我们在controller层完成对于model层和视图层的引用
model = GameObject.FindGameObjectWithTag("Model").GetComponent
view = GameObject.FindGameObjectWithTag("View").GetComponent
}
4. 创建我们的菜单ui
嗯,就照着这个样子拼就可以了
5. emmm,因为都是一些可视化界面的,界面制作步骤我就不介绍了,注意就是给各个界面的rayCast需要取消
接下来,我们制作一下俄罗斯方块的各个形状
这里我也是直接拼好的,讲一下注意要素1.所有小方块都添加tag:Block,方便进行管理2.所有小方块都是1x1的类型,便于二维数组存储3.每个类型都要设置好对应的旋转中心点
6引入有限状态机来管理游戏状态
这里关于有限状态机各位自行去研究,这里的话我使用有限状态机管理游戏状态,首先我们创建四种状态去继承有限状态机,然后回到我们的ctrl层来将状态添加到状态机里并且设置初始状态
private void AddSFsm()
{
fsm = newFSMSystem();
FSMState[] states = GetComponentsInChildren
foreach(FSMStatestate in states)
{
//遍历子物体的状态并且添加到状态机中
fsm.AddState(state);
}
MenuState menuState = GetComponentInChildren
//设置默认状态为菜单状态
fsm.SetCurrentState(menuState);
}
6. 设置各个状态的id,并且在添加状态的时候设置我们的fsmSystem
这里的话也就是在setState的时候把我们的System也设置过去,办法是提供一个set方法即可
protectedFSMSystem fsm;
publicFSMSystem FSM { set { fsm =value; } }
然后我们给每个状态添加上它们的状态id即可,例如
private void Awake()
{
stateID = StateID.Pause;
}
7. 显示菜单栏完成状态的切换
这里的话我们的菜单切换放在ctrl层来执行,而视图的变化是在view层,这里我们要做的就是在我们用有限状态机将各个菜单状态获得到的同时,持有ctrl层,并且通过ctrl层完成动画的播放
(1) 我们在view层创建播放的动画
public void ShowMenu()
{
logoTransform.gameObject.SetActive(true);
logoTransform.DOAnchorPosY(-78.5f, 0.5f);
menuTransform.gameObject.SetActive(true);
menuTransform.DOAnchorPosY(-2f, 0.5f);
}
(2) 修改有限状态机完成对于ctrl层的持有
public void AddState(FSMStates,Ctrl ctrl)
{
// Check for Null reference beforedeleting
if (s == null)
{
Debug.LogError("FSM ERROR: Null reference is not allowed");
}
s.FSM = this;
s.CTRL = ctrl;
3.在ctrl层添加持有
private void AddSFsm()
{
fsm = newFSMSystem();
FSMState[] states = GetComponentsInChildren
foreach(FSMStatestate in states)
{
//遍历子物体的状态并且添加到状态机中
fsm.AddState(state,this);
}
MenuState menuState = GetComponentInChildren
//设置默认状态为菜单状态
fsm.SetCurrentState(menuState);
}
4我们通过menuState来管理我们菜单切换的状态
class MenuState:FSMState
{
private void Awake()
{
stateID = StateID.Menu;
}
public override void DoBeforeEntering()
{
ctrl.view.ShowMenu();
}
}
8. 控制相机的变化
这里也是和上面一样,用doTween控制相机size,用ctrl获得相机,再在menu层调用相机变换
public class CameraManager:MonoBehaviour
{
private Cameracamera1;
private void Awake()
{
camera1 = Camera.main;
}
public void ZoomIn()
{
camera1.DOOrthoSize(50.4f, 0.5f);
}
}
public override void DoBeforeEntering()
{
ctrl.view.ShowMenu();
ctrl.cameraManager.ZoomIn();
}
9. 点击开始按钮切换当前状态
这里第一个是点击开始按钮通过动画隐藏menu,比较简单
public void HideMenu()
{
//在这里进行界面的隐藏
logoTransform.DOAnchorPosY(78.5f, 0.5f).OnComplete(delegate
{
logoTransform.gameObject.SetActive(false);
});
menuTransform.DOAnchorPosY(-66.3f, 0.5f).OnComplete(delegate
{
menuTransform.gameObject.SetActive(false);
});
}
然后是一个我们切换状态,这个需要先将StartButtonClick这个Transition添加进去,然后我们在Awake里添加这个Transition切换当前状态并且隐藏menu
10. 切换到GameUI状态
做法嘛,和直接的一样,基本照搬就是了,这里贴下代码
class PlayState:FSMState
{
private void Awake()
{
stateID = StateID.Play;
AddTransition(Transition.BackButtonClick,StateID.Menu);
}
public override void DoBeforeEntering()
{
ctrl.view.ShowGameUI();
ctrl.cameraManager.ZoomOut();
}
public override void DoBeforeLeaving()
{
ctrl.view.HideGameUI();
ctrl.view.ShowRestartButton();
}
public void OnBackButtonClick()
{
fsm.PerformTransition(Transition.BackButtonClick);
}
}
11. 创建俄罗斯方块生成的脚本
生成嘛,比较简单,我们用GameManager管理一下即可,这里贴一下代码,还有一个是shape脚本遍历方块改颜色的脚本
public class GameManager:MonoBehaviour
{
private bool isPause = true;
public Color[]color;
public Shape[]shapes;
private ShapecurrentShape = null;
private void Update()
{
if (isPause) return;
if (currentShape == null)
{
SpawnerShape();
}
}
public void StartGame()
{
isPause = false;
}
public void QuitGame()
{
isPause = true;
}
private void SpawnerShape()
{
//随机生成一个类型的方块
int index = Random.Range(0, shapes.Length);
int colorIndex = Random.Range(0, color.Length);
currentShape=GameObject.Instantiate(shapes[index]);
currentShape.Init(color[colorIndex]);
}
}
public void Init(Color color)
{
foreach (Transform t in transform)
{
if (t.tag == "block")
{
t.GetComponent
}
}
}
12. 方块下落的脚本
13. using System;
14. usingSystem.Collections.Generic;
15. using System.Linq;
16. using System.Text;
17. using UnityEngine;
18.
19. namespace Assets.Scripts.Ctrl
20. {
21. public class Shape : MonoBehaviour
22. {
23. private bool isPause = false;
24. private float timmer = 0;
25. private float stepTime = 0.8f;
26. //这里面控制的是每个方块的颜色
27. private void Update()
28. {
29. if (isPause) return;
30. timmer+=Time.deltaTime;
31.
32. if (timmer >=stepTime)
33. {
34. Fall();
35. timmer = 0;
36.
37. }
38.
39. }
40. public void Init(Color color)
41. {
42. foreach (Transform t in transform)
43. {
44. if (t.tag == "block")
45. {
46. t.GetComponent
47. }
48. }
49. }
50. private void Fall()
51. {
52. Vector3pos = transform.position;
53. pos.y =pos.y - 3;
54. transform.position = pos;
55. }
56. }
57. }
13.判断方块位置是否可用
下面的话就是判断方块位置是否可用了,每次方块存储位置的时候都要检测一下这个是否可以放置,如果可以,才会存储到二维数组中,其他的看注释,这里的话还有一个扩展工具类,也就是取整我们的二维数组
usingSystem.Collections;
usingSystem.Collections.Generic;
usingUnityEngine;
usingAssets.Scripts.Tool;
public class Model : MonoBehaviour {
//首先我们需要新建一个二维数组来存放我们的地图坐标
public const int MAX_COLUMNS = 10;
public const int MAX_ROWS = 20;
privateTransform[,] mapTransform = new Transform[MAX_COLUMNS, MAX_ROWS];
public boolIsValidMapPosition(Transform t)
{
//这个方法是判断我们的方块是否可以被放置到这个位置,要确定的有两点,1.该位置是否已经有方块2.是否超出边界
foreach(Transform child in t)
{
//首先判断该位置是否是方块,如果是方块,就不合法,如果不是,就继续判断
if (child.tag != "block") continue;
//这里判断的是原位置是否已经被占了
Vector2 pos =child.position.Round();
if (mapTransform[(int)pos.x,(int) pos.y] != null) return false;
if (!isInsideMap(pos)) return false;
}
return true;
}
public bool isInsideMap(Vector2pos)
{
return pos.x>= 0 && pos.x <= MAX_COLUMNS && pos.y >= 0;
}
}
14.控制俄罗斯方块存放和叠加
在下落的时候调用之前写的代码进行判断,如果可以的话,就停止运动然后将当前方块坐标赋值给二维数组
private void Fall()
{
Vector3 pos = transform.position;
pos.y -= 1;
transform.position = pos;
//这里要检测下落的位置是否合理
if(ctrl.model.IsValidMapPosition(this.transform)==false)
{
pos.y += 1;
transform.position = pos;
isPause = true;
gameManager.FallDown();
ctrl.model.PlaceMap(this.transform);
}
}
15.添加音量控制
namespaceAssets.Scripts.Ctrl
{
public class AudioManager:MonoBehaviour
{
//在这里我们希望实现什么功能?控制音量
publicAudioClip[] clips;
privateAudioSource audioSource;
private bool isMute = false;
private void Awake()
{
audioSource =GetComponent
}
public void PlayDropClip()
{
PlayAudio(clips[0]);
}
private void PlayAudio(AudioClipaudioClip)
{
if (isMute == true) return;
audioSource.clip = audioClip;
audioSource.Play();
}
}
}
16.控制方块的左右移动
private void ControlShape()
{
int h = 0;
if(Input.GetKeyDown(KeyCode.LeftArrow))
{
h = -1;
}
else if(Input.GetKeyDown(KeyCode.RightArrow))
{
h = 1;
}
if (h !=0)
{
Vector3 pos = transform.position;
pos.x += h;
transform.position = pos;
if (ctrl.model.IsValidMapPosition(this.transform) == false)
{
pos.x -= h;
transform.position = pos;
}
}
}
17.控制方块旋转
if(Input.GetKeyDown(KeyCode.UpArrow))
{
transform.RotateAround(pivot.position,transform.forward, 90);
if (ctrl.model.IsValidMapPosition(this.transform) == false)
{
transform.RotateAround(pivot.position, transform.forward, -90);
}
}
18.控制俄罗斯方块的消除
这里比较简单,看看注释就行了,按照步骤依次消除,清除二维数组方块位置并且移动上一行等操作
public bool PlaceMap(Transformt)
{
//这个用来将方块存放到地图内
foreach(Transform child in t)
{
if (child.tag != "block") continue;
Vector2 pos = child.position.Round();
mapTransform[(int)pos.x, (int)pos.y] = child;
}
return CheckMap();
}
private void MoveDownAllAbove(int row)
{
for (int i = row; i
{
MoveDownBlocks(i);
}
}
//当我们把物体放入地图的时候,就要检测是否已经满行了
private bool CheckMap()
{
int count =0;
//这里我们首先要遍历我们所有的行
for (int i = 0; i
{
//然后我们要进行的操作,1.检测是否有行被填满
if (IsRowFull(i))
{
count++;
DestoryRows(i);
MoveDownAllAbove(i + 1);
i--;
}
}
if (count> 0)
{
return true;
}
else
{
return false;
}
}
private bool IsRowFull(int row)
{
//这里我们会在指定那一行遍历每个格子,如果全部满的话,我们会调用销毁方法
for (int i = 0; i
{
if (mapTransform[i, row] == null)
{
return false; }
}
return true;
}
private void DestoryRows(int row)
{
//我们在这里执行销毁操作,具体销毁方法还是遍历该行每个格子,然后置空
for (int i = 0; i
{
//ctrl.audioManager.PlayFullClip();
Destroy(mapTransform[i,row].gameObject);
mapTransform[i, row] = null;
// Debug.Log(ctrl.audioManager);
}
}
private void MoveDownBlocks(int row)
{
//在我们销毁之后,我们需要将上一层的俄罗斯方块放下来
for (int i = 0; i
{
if (mapTransform[i, row ] != null)
{
mapTransform[i, row - 1] =mapTransform[i, row];
mapTransform[i, row] = null;
mapTransform[i, row -1].position += newVector3(0, -1, 0);
}
}
}
19.控制分数的更新
这里就讲一下思路了,方块落下,我们先更新的是model层里的分数,当行数被清除时,会更新它的数据,同时我们将isUpdate设置为true,通知其他方法分数增加
然后我们在view层更新我们显示分数的方法,初始化游戏时将分数清空,后续分数从本地读取,更新分数读取的是model层的分数
最后我们在ctrl层下的GameManager里管理分数的变化即可
20.显示游戏结束ui,存储分数
这两个我们直接用PlayerPrefs进行保存即可,很方便,存了以后在PlayState显示即可,存档则在游戏结束时存档
显示UI和之前的gameUi类似,这里就不再赘述了
private void SaveData( int highScore,int numbersGame)
{
PlayerPrefs.SetInt("highScore", highScore);
PlayerPrefs.SetInt("numbersGame", numbersGame);
}
private void LoadData()
{
highScore= PlayerPrefs.GetInt("highScore",0);
numbersGame = PlayerPrefs.GetInt("numbersGame", 0);
}
20.游戏结束时的重开和返回首页
这个是重开的操作
public void ReStartGame()
{
for (int i = 0; i
{
for (int j = 0; j < MAX_ROWS; j++)
{
if (mapTransform[i, j] != null)
{
Destroy(mapTransform[i,j].gameObject);
mapTransform[i, j] = null;
}
}
}
}
public voidOnReStartButtonClick()
{
//在这里我们要重新开始游戏
ctrl.model.ReStartGame();
ctrl.view.HideGamOverUI();
ctrl.gameManager.StartGame();
ctrl.view.ShowGameUI(0,ctrl.model.HighScore);
}
返回首页就很简单了,重新加载本场景即可
public void OnBackHomeClick()
{
HideGameUI();
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
21.垃圾回收,清空多余的shape
之前的删除行并不会清楚已经没有方块的shape,这里我们加一个清理机制清楚它
foreach(Transform item inshapeControllerTransform)
{
if (item.childCount <= 1)
{
Destroy(item.gameObject);
}
}
22.结束语
嗯,其实还有些功能没开发,比如什么清理数据之类的,不过绝大部分的事是做完了的,接下里无论是各个状态的切换,还是更新UI界面,都是重复性的工作,从学习的角度讲已经没有过多意义,这次俄罗斯方块的制作本质并不是制作俄罗斯方块,而是通过这个案例初步地了解到mvc的开发模式,通过将将各种数据分层,便于我们更好地管理和扩展,我们会把模型方面修改放在model,把ui放在view层,把控制放在controller层,并且model层和view层的分离可以降低项目的冗余度,便于扩展,了解到这个,也为了我们的游戏开发和扩展实现了更多的助力,那么各位,我们下个博客见,对了,这个游戏的工程我也会发在csdn,有兴趣的朋友可以下下来看看