小时候,大家都应玩过或听说过《俄罗斯方块》,它是红白机,掌机等一些电子设备中最常见的一款游戏。而随着时代的发展,信息的进步,游戏画面从简单的黑白方块到彩色方块,游戏的玩法机制从最简单的消方块到现在的多人pk等,无一不是在体现它的火爆。在这里,通过这篇文章向大家分享一下自己在制作俄罗斯方块的经验和心得,以及文章最后的源码和pc程序。
首先,看标题都知道这篇文章中所用到的游戏引擎是:unity3d,版本不限,但最好是5.4.3以上的,原因是因为作者自己没有用过5.4.3以下的版本。
准备工具都有:unity3d + Visual Studio 2015
素材准备有(密码:1vp5):字体方正粗圆_GBK,以及一些图集
项目分析:
当一切准备就绪后,就可以开始创建我们的俄罗斯方块工程(2d)的。
一、游戏框架的搭建和方块预设物的制作
1.双击unity快捷方式,打开unity界面点击New新建工程,Template类型选择2d,工程名 Tetris,点击创建后稍等片刻进入编辑器界面
2.在Assets文件夹下创建几个常用文件夹用来归类:
创建文件夹方式,在Project面板下右键
Scenes(场景文件夹),Resources(资源文件夹),Script(脚本文件夹),Texture(图集文件夹),Prefabs(预设物文件夹)
从图中可以看到将预设物文件夹放到了资源文件夹下,这样做的好处是可以在代码中直接通过unity3d提供的Resources类进行访问。当然其他文件或文件夹也可以放进去,但是了解过Unity3d的你,应该知道Resource下的文件在打包的时候会占用很大资源,具体的提自行百度。
3.导入图片等资源到对应的文件夹中(也可以自行创建);保存当前场景,场景名MainGame;切割图片资源生成我们想要的图片集。
4.在层级面板中右键创建UI面板Canvas,有关UI的对象都放置到UI面板中。设置相机背景颜色设置为纯白色,Canvas属性UIScaleMode设置为屏幕分辨率
界面搭建过程中在Canvas下可创建空物体用来做对应界面的父对象,也可以直接创建Panel。这里采用了创建空物体
4-1.开始界面的搭建:
创建空物体F2修改名字为StartPanel,设置StartPanel大小,点击stretch,按住Alt键,之后点击右下角,StartPanel的大小和位置就会被拉伸到Canvas尺寸
在StartPanel下创建UI→Text,将其锚点固定到上方中心位置也放到上方,文本信息设置为:俄罗斯方块
创建一个空物体重命名:ButtonGroup,用来整合开始界面中的按钮,将空物体放置到界面下方,在ButtonGroup下创建3个UI→Button,然后删除Button下的Text子物体,添加子物体Image,并分别重命名:btn_Start,btn_Set,btn_Rank。在ButtonGroup上添加布局组件可以将子物体进行一些布局调整
(做好一个按钮后可以将其拖拽成预设物,方便下次使用)
在预设物文件夹下创建三个文件夹用来分类预设物:Panel,Square,Other,将做好的StartPanel拖放到Panel文件夹下,btn_Start移动到Other文件夹下。
4-2. 游戏运行界面搭建:创建四个Text其中两个用来文本提示,另外两个用来显示分数。将之前做好的按钮预设物拽上来,修改名字用来做暂停按钮,最后将做好的界面放到Panel文件夹中。
4-3.设置界面排行界面暂停界面以及游戏结束界面,可分别搭建成如下图所示的样子。
4-4:界面预设物打包下载:https://pan.baidu.com/s/1XP8LkIVQEZfWYreHnKrYdg 提取码:ad1o
5.制作方块预设物:玩过俄罗斯方块的都知道,俄罗斯方块下落的方块类型有7种:
这7种方块类型,我提供了下载包,可直接下载使用:https://pan.baidu.com/s/1d-40cuiD_ioH69Pc_Hv9ng 提取码:8tj7
6.制作方块背景地图Map:Map最直接最简单的作用是为了显示方块可下落的位置,为此我们在做Map时,第一块物体的位置很重要,在这里,我们将其位置重置为0,然后Ctrl+D复制创建,然后选中复制出来的按住键盘Ctrl键进行向右拖拽。最后做到一行有10个方块,之后创建一个空物体将其坐标重置,将这一行方块整体放倒空物体后重命名空物体:Row,重复Ctrl+D复制创建并且按住键盘Ctrl键进行向上拖拽,做到场景中有12个Row后创建一个空物体将其坐标重置重命名为Map,将12行Row放到Map下面,最终生成10*12一块Map,将做好的Map拖拽到Other文件夹下生成预设物。提取码:q0o8
注意:在制作Map时,可以将第一块方块拖拽成预设物,这样子可以方便修改Map中所有方块的属性。
至此,俄罗斯方块界面框架和预设物算是完成了。
二、游戏代码逻辑编写
在unity中提供两种常见的代码方式:C#和JavaScript,在这里采用C#去编写。游戏代码的编写风格以及框架有很多种,可以根据自己的习惯去书写自己满意的代码风格,但为了他人在阅读浏览自己代码是有可能会造成的一些问题,我们应该规范自己的代码风格和框架。在本教程中采用的是简单的MVC框架。
在Script文件夹下创建三个文件夹:Ctrl,View,Data,分别用来存放控制脚本,视图脚本和数据脚本。
在View文件夹下分别创建对应UIPanel的C#脚本:StartView,RunView,SetView,RankView,PasueView,OverView。
using UnityEngine; using UnityEngine.UI; using System.Collections; public class StartView : MonoBehaviour { public Button Btn_Start { get; set; } public Button Btn_ReStart { get; set; } public Button Btn_Set { get; set; } public Button Btn_Rank { get; set; } public Text Txt_Title { get; set; } // Use this for initialization void Awake() { Btn_Start = transform.Find(\"ButtonGroup/btn_Start\").GetComponent<Button>(); Btn_Set = transform.Find(\"ButtonGroup/btn_Set\").GetComponent<Button>(); Btn_Rank = transform.Find(\"ButtonGroup/btn_Rank\").GetComponent<Button>(); Txt_Title = transform.Find(\"txt_Title\").GetComponent<Text>(); } // Update is called once per frame void Update () { } }
StartView脚本
using UnityEngine; using UnityEngine.UI; using System.Collections; public class RunView : MonoBehaviour { public Button Btn_Pause { get; set; } public Text Txt_Curr { get; set; } public Text Txt_Max { get; set; } // Use this for initialization void Awake() { Btn_Pause = transform.Find(\"TopGroup/btn_Pasue\").GetComponent<Button>(); Txt_Curr = transform.Find(\"TopGroup/txt_Current/Text\").GetComponent<Text>(); Txt_Max = transform.Find(\"TopGroup/txt_Max/Text\").GetComponent<Text>(); } public void SetScoreText(int curr,int max) { Txt_Curr.text = curr.ToString(); Txt_Max.text = max.ToString(); } public void SetScoreText(string curr, string max) { Txt_Curr.text = curr; Txt_Max.text = max; } }
RunView脚本
using UnityEngine; using UnityEngine.UI; using System.Collections; public class SetView : MonoBehaviour { public Button Btn_Forum { get; set; } public Button Btn_WebSite { get; set; } public Button Btn_Collect { get; set; } public Button Btn_Sound { get; set; } public Button Btn_Return { get; set; } public GameObject mask { get; set; } // Use this for initialization void Awake() { Btn_Forum = transform.Find(\"Img_Bg/Btn_Forum\").GetComponent<Button>(); Btn_WebSite = transform.Find(\"Img_Bg/Btn_WebSite\").GetComponent<Button>(); Btn_Collect = transform.Find(\"Img_Bg/Btn_Collect\").GetComponent<Button>(); Btn_Sound = transform.Find(\"Img_Bg/Btn_Sound\").GetComponent<Button>(); mask = Btn_Sound.transform.Find(\"Mask\").gameObject; Btn_Return = GetComponent<Button>(); } public void SetSoundMask(bool isActive) { mask.SetActive(isActive); } public void SetSoundMask() { mask.SetActive(!mask.activeSelf); GameManager.instance.Sound = mask.activeSelf; AudioManager.instance.SetAudioSource(!mask.activeSelf); } }
SetView脚本
using UnityEngine; using UnityEngine.UI; using System.Collections; public class RankView : MonoBehaviour { public Button Btn_Clear { get; set; } public Button Btn_Return { get; set; } public Text Txt_Curr { get; set; } public Text Txt_Max { get; set; } public Text Txt_Count { get; set; } // Use this for initialization void Awake() { Btn_Clear = transform.Find(\"Img_Bg/Btn_Clear\").GetComponent<Button>(); Txt_Curr = transform.Find(\"Img_Bg/Txt_Name\").GetComponent<Text>(); Txt_Max = transform.Find(\"Img_Bg/Txt_Max/Text\").GetComponent<Text>(); Txt_Count = transform.Find(\"Img_Bg/Txt_Count/Text\").GetComponent<Text>(); Btn_Return = GetComponent<Button>(); } void Start() { SetScoreText(); } public void TextClearData() { Txt_Max.text = 0.ToString(); Txt_Count.text = 0.ToString(); GameManager.instance.Score = 0; GameManager.instance.highScore = 0; GameManager.instance.numbersGame = 0; } public void SetScoreText() { Txt_Max.text = GameManager.instance.highScore.ToString(); Txt_Count.text = GameManager.instance.numbersGame.ToString(); } }
RankView脚本
using UnityEngine; using System.Collections; using UnityEngine.UI; public class PasueView : MonoBehaviour { public Button Btn_Home { get; set; } public Button Btn_Start { get; set; } public Text Txt_Curr { get; set; } // Use this for initialization void Awake() { Btn_Home = transform.Find(\"Img_Bg/Btn_Home\").GetComponent<Button>(); Btn_Start = transform.Find(\"Img_Bg/Btn_Start\").GetComponent<Button>(); Txt_Curr = transform.Find(\"Img_Bg/Txt_Count\").GetComponent<Text>(); } }
PasueView脚本
using UnityEngine; using UnityEngine.UI; using System.Collections; public class OverView : MonoBehaviour { public Button Btn_Home { get; set; } public Button Btn_ReStart { get; set; } public Text Txt_Curr { get; set; } // Use this for initialization void Awake () { Btn_Home = transform.Find(\"Img_Bg/Btn_Home\").GetComponent<Button>(); Btn_ReStart = transform.Find(\"Img_Bg/Btn_ReStart\").GetComponent<Button>(); Txt_Curr = transform.Find(\"Img_Bg/Txt_Count\").GetComponent<Text>(); } public void SetScoreText(int score) { Txt_Curr.text = score.ToString(); } }
OverView脚本
在Ctrl文件夹下分别创建对应UIPanel的C#脚本:StartCtrl,RunCtrl,SetCtrl,RankCtrl,PasueCtrl,OverCtrl。
using UnityEngine; using System.Collections; public class StartCtrl : MonoBehaviour { public StartView view { get; set; } Camera mainCamera { get; set; } void Awake() { mainCamera = Camera.main; } void Start () { view = gameObject.AddComponent<StartView>(); //开始按钮事件 view.Btn_Start.onClick.AddListener(delegate() { Destroy(gameObject); GameManager.instance.CreatePanel(PanelType.RunPanel); AudioManager.instance.PlayCursor(); }); //设置按钮事件 view.Btn_Set.onClick.AddListener(delegate () { GameManager.instance.CreatePanel(PanelType.SetPanel); AudioManager.instance.PlayCursor(); }); //排行榜按钮事件 view.Btn_Rank.onClick.AddListener(delegate () { GameManager.instance.CreatePanel(PanelType.RankPanel); AudioManager.instance.PlayCursor(); }); StartCoroutine(doZoomOut()); } //缩小 IEnumerator doZoomOut() { bool isdo = false; yield return null; while (!isdo) { if (mainCamera.orthographicSize > 12.5f) { isdo = true; } mainCamera.orthographicSize += 0.1f; yield return new WaitForSeconds(0.02f); } } }
StartCtrl脚本
using UnityEngine; using System.Collections; public class RunCtrl : MonoBehaviour { public bool isPause { get; set; } public RunView view { get; set; } Camera mainCamera { get; set; } private void Awake() { view = gameObject.AddComponent<RunView>(); GameManager.instance.isOver = false; isPause = false; mainCamera = GameObject.Find(\"Main Camera\").GetComponent<Camera>(); } // Use this for initialization void Start () { view.SetScoreText(0, GameManager.instance.highScore); StartCoroutine(doZoomIn()); view.Btn_Pause.onClick.AddListener(delegate () { isPause = true; GameManager.instance.currentShape.isPause = true; GameManager.instance.CreatePanel(PanelType.PasuePanel); AudioManager.instance.PlayCursor(); }); } /// <summary> /// 放大 /// </summary> /// <returns></returns> IEnumerator doZoomIn() { bool isdo = false; yield return null; while (!isdo) { if (Camera.main.orthographicSize < 10f) { isdo = true; } Camera.main.orthographicSize -= 0.1f; yield return new WaitForSeconds(0.02f); } } void Update() { if (isPause) return; while (GameManager.instance.currentShape == null) { SquareType st = (SquareType)Random.Range(0, 7); //测试生成 GameManager.instance.CreateSquare(st); } } }
RunCtrl脚本
using UnityEngine; using System.Collections; public class SetCtrl : MonoBehaviour { public SetView view { get; set; } // Use this for initialization void Start () { view = gameObject.AddComponent<SetView>(); view.SetSoundMask(GameManager.instance.Sound); //声音按钮事件 view.Btn_Sound.onClick.AddListener(delegate () { view.SetSoundMask(); DataModel.SaveData(); AudioManager.instance.PlayControl(); }); view.Btn_Collect.onClick.AddListener(delegate () { }); view.Btn_Forum.onClick.AddListener(delegate () { }); view.Btn_WebSite.onClick.AddListener(delegate () { }); //返回按钮事件 view.Btn_Return.onClick.AddListener(delegate () { Destroy(gameObject); }); } // Update is called once per frame void Update () { } }
SetCtrl脚本
using UnityEngine; using System.Collections; public class RankCtrl : MonoBehaviour { public RankView view { get; set; } // Use this for initialization void Start () { view = gameObject.AddComponent<RankView>(); view.Btn_Clear.onClick.AddListener(delegate () { view.TextClearData(); DataModel.SaveData(); }); //返回按钮事件 view.Btn_Return.onClick.AddListener(delegate () { Destroy(gameObject); }); } // Update is called once per frame void Update () { } }
RankCtrl脚本
using UnityEngine; using System.Collections; public class PasueCtrl : MonoBehaviour { public PasueView view { get; set; } // Use this for initialization void Start () { view = gameObject.AddComponent<PasueView>(); view.Txt_Curr.text = GameManager.instance.Score.ToString(); //继续游戏按钮事件 view.Btn_Start.onClick.AddListener(delegate () { GameManager.instance.isOver = false; GameManager.instance.ctrl_run.isPause = false; GameManager.instance.currentShape.isPause = false; Destroy(gameObject); AudioManager.instance.PlayCursor(); }); view.Btn_Home.onClick.AddListener(delegate () { DestroyAll(); GameManager.instance.isOver = true; AudioManager.instance.PlayCursor(); GameManager.instance.CreatePanel(PanelType.StartPanel); }); } void DestroyAll() { for (int i = 1; i < GameManager.instance.transform.childCount; i++) { Destroy(GameManager.instance.transform.GetChild(i).gameObject); } for (int i = 0; i < GameManager.instance.CreatePoint.childCount; i++) { Destroy(GameManager.instance.CreatePoint.GetChild(i).gameObject); } } }
PasueCtrl脚本
using UnityEngine; using System.Collections; public class OverCtrl : MonoBehaviour { public OverView view { get; set; } // Use this for initialization void Start () { view = gameObject.AddComponent<OverView>(); view.SetScoreText(GameManager.instance.Score); AudioManager.instance.PlayGameOver(); //重新开始 view.Btn_ReStart.onClick.AddListener(delegate() { ClearSquare(); GameManager.instance.isOver = false; //取消暂停 GameManager.instance.ctrl_run.isPause = false; }); //返回主页 view.Btn_Home.onClick.AddListener(delegate() { ClearSquare(); GameManager.instance.isOver = true; Destroy(GameManager.instance.ctrl_run.gameObject); GameManager.instance.CreatePanel(PanelType.StartPanel); }); } // Update is called once per frame void ClearSquare () { //清除已生成的方块 for (int i = 0; i < GameManager.instance.CreatePoint.childCount; i++) { Destroy(GameManager.instance.CreatePoint.GetChild(i).gameObject); } //清除当前分数 GameManager.instance.Score = 0; GameManager.instance.ctrl_run.view.Txt_Curr.text = 0.ToString(); //销毁自身 Destroy(gameObject); } }
OverCtrl脚本
在Data文件夹下分别创建方块和游戏涉及到的数据脚本:DataModel,CtrlInput,SquareControl。
using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; /// <summary> /// 面板类型(PanelType) /// </summary> public enum PanelType { StartPanel, RunPanel, SetPanel, RankPanel, PasuePanel, GameOverPanel } /// <summary> /// 方块类型 /// </summary> public enum SquareType { Square_1, Square_2, Square_3, Square_4, Square_5, Square_6, Square_7 } /// <summary> /// 方向键控制 /// </summary> public enum DirectionType { None, Left, Right, Down, Up } public class DataModel { /// <summary> /// 获取数据储存文件完整路径 /// </summary> public static readonly string dataPath = Application.persistentDataPath + @\"\\data.bin\"; /// <summary> /// 保存数据 /// </summary> public static void SaveData() { string content = GameManager.instance.highScore + \"\\n\" + GameManager.instance.numbersGame + \"\\n\" + GameManager.instance.Sound; //写出文件 File.WriteAllText(dataPath, content); } /// <summary> /// 读取数据 /// </summary> public static void LoadData() { //判断文件是否存在 if (File.Exists(dataPath)) { string content = File.ReadAllText(dataPath); GameManager.instance.highScore = int.Parse(content.Split(\'\\n\')[0]); GameManager.instance.numbersGame = int.Parse(content.Split(\'\\n\')[1]); GameManager.instance.Sound = content.Split(\'\\n\')[2] == \"true\" ? true : false; } else { GameManager.instance.highScore = 0; GameManager.instance.numbersGame = 0; GameManager.instance.Sound = false; } } }
DataModel脚本
using UnityEngine; using System.Collections; using System; //委托定义好了 public delegate void InputEventHandler(DirectionType res = DirectionType.None); /// <summary> /// 触点滑动操作类 /// </summary> /// 1、可用于移动端触点移动 /// 2、可用于PC端鼠标移动 public class CtrlInput { /// <summary> /// 委托方法事件 /// </summary> public InputEventHandler handler; /// <summary> /// 触摸起点 /// </summary> Vector3 m_ptStart = Vector3.zero; /// <summary> /// 开始时间 /// </summary> float m_TimeStart; /// <summary> /// 有效滑动长度 /// </summary> float m_Len = 30; /// <summary> /// 操作标志:用于控制操作识别完成后,不松手再滑动,操作无效。 /// </summary> bool m_Flag = false; /// <summary> /// 起始位置 /// </summary> /// <param name=\"pt\"></param> public void Start(Vector3 pt) { //记录原点位置 m_ptStart = pt; m_TimeStart = Time.fixedTime; m_Flag = true; } /// <summary> /// 滑动后位置 /// </summary> /// <param name=\"pt\"></param> public void Check(Vector3 pt) { //操作无效 if (!m_Flag) return; //滑动限时3秒 if (Time.fixedTime - m_TimeStart > 3) return; //获取两点间的距离 Vector3 v = pt - m_ptStart; float len = v.magnitude; //长度不够 if (len < m_Len) return; //Mathf.Rad2Deg 弧度到度转换常数(只读)。 //Mathf.Atan2 反正切 以弧度为单位计算并返回 y/x 的反正切值。 //返回值表示相对直角三角形对角的角,其中 x 是临边边长,而 y 是对边边长。 //返回值是在x轴和一个二维向量开始于0个结束在(x, y)处之间的角。 float degree = Mathf.Rad2Deg * Mathf.Atan2(v.x, v.y); if (-45 >= degree && degree >= -135) { // 左 m_Flag = false; handler(DirectionType.Left); } else if (45 <= degree && degree <= 135) { // 右 m_Flag = false; handler(DirectionType.Right); } else if (-45 <= degree && degree <= 45) { // 上 m_Flag = false; handler(DirectionType.Up); } else if (135 <= degree || degree <= 135) { // 下 m_Flag = false; handler(DirectionType.Down); } } #region 示例 /*使用方法1:实例化 CtrlInput 类, * 实例化对象名:如 ctrlInput * 用新实例化出来的对象,调用方法 * ctrlInput.TouchControl(); * ctrlInput.MouseControl(); * * 使用方法2:实例化 CtrlInput 类, * 将方法拷贝或剪切到 控制类中 * 修改示例中的 Start(touch.position),Check(touch.position) 方法调用为 * ctrlInput.Start(touch.position); * ctrlInput.Check(touch.position); */ #region 1、移动端触点移动示例 /// <summary> /// 触点输入控制 /// </summary> public void TouchControl() { //如果触点数量为0返回 if (Input.touchCount == 0) return; Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { Start(touch.position); } else if (touch.phase == TouchPhase.Moved) { Check(touch.position); } } #endregion #region 2、PC端鼠标输入移动示例 bool m_MouseMove = false; /// <summary> /// 鼠标输入控制 /// </summary> void MouseControl() { //鼠标按下时记录当前位置 if (Input.GetMouseButtonDown(0)) { m_MouseMove = true; Start(Input.mousePosition); } else if (Input.GetMouseButtonUp(0)) { m_MouseMove = false; } //滑动位置 if (m_MouseMove) { Check(Input.mousePosition); } } #endregion #endregion }
CtrlInput脚本
using UnityEngine; using System.Collections; using System; public class SquareControl : MonoBehaviour { float timer;//计时 float stepTime = 0.8f; int multiple = 15; // 标示鼠标是否是按下的移动 bool m_MouseMove = false; // 处理输入 CtrlInput m_CtrlInput = new CtrlInput(); public bool isPause { get; set; } public DirectionType dirType { get; set; } private void Awake() { dirType = DirectionType.None; m_CtrlInput.handler += onMove; } void onMove(DirectionType res) { dirType = res; } // Update is called once per frame void Update () { if (isPause) return; timer += Time.deltaTime; if (timer > stepTime) { timer = 0; WhereAbouts(); } InputControl(); MouseControl(); TouchControl(); } //IEnumerator doWhereabouts() //{ // while (true) // { // while (!isPause) // { // Vector3 pos = transform.position; // pos.y -= 1; // transform.position = pos; // if (!GameManager.instance.IsValidMapPosition(transform)) // { // AudioManager.instance.PlayDrop(); // pos.y += 1; // transform.position = pos; // isPause = true; // bool isLineclear = GameManager.instance.PlaceShape(transform); // if (isLineclear) // { // AudioManager.instance.PlayLineClear(); // } // else // { // AudioManager.instance.PlayControl(); // } // GameManager.instance.FallDown(); // Destroy(this); // } // //调整速度 // if (!isSpeedup) // { // yield return new WaitForSeconds(1); // } // else // { // yield return new WaitForSeconds(0.2f); // } // } // yield return null; // } // //while (isSpeedup) // //{ // // transform.position = new Vector2(transform.position.x, transform.position.y - 1); // // yield return new WaitForSeconds(0.5f); // //} //} void WhereAbouts() { Vector3 pos = transform.position; pos.y -= 1; transform.position = pos; if (!GameManager.instance.IsValidMapPosition(transform)) { AudioManager.instance.PlayDrop(); pos.y += 1; transform.position = pos; isPause = true; bool isLineclear = GameManager.instance.PlaceShape(transform); if (isLineclear) { AudioManager.instance.PlayLineClear(); } else { AudioManager.instance.PlayControl(); } GameManager.instance.FallDown(); Destroy(this); } } /// <summary> /// 键盘输入控制 /// </summary> void InputControl() { float h = 0; if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow) || dirType == DirectionType.Left) { h = -1; dirType = DirectionType.None; } else if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow) || dirType == DirectionType.Right) { h = 1; dirType = DirectionType.None; } else if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow) || dirType == DirectionType.Down) { stepTime /= multiple; dirType = DirectionType.None; } if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow) || dirType == DirectionType.Up) { dirType = DirectionType.None; transform.RotateAround(transform.Find(\"Pivot\").transform.position, Vector3.forward, 90); if (!GameManager.instance.IsValidMapPosition(transform)) { transform.RotateAround(transform.Find(\"Pivot\").transform.position, Vector3.forward, -90); } AudioManager.instance.PlayDrop(); } if (h != 0) { dirType = DirectionType.None; Vector3 pos = transform.position; pos.x += h; transform.position = pos; if (!GameManager.instance.IsValidMapPosition(transform)) { pos.x -= h; transform.position = pos; } AudioManager.instance.PlayDrop(); } } /// <summary> /// 鼠标输入控制 /// </summary> void MouseControl() { //鼠标按下时记录当前位置 if (Input.GetMouseButtonDown(0)) { m_MouseMove = true; m_CtrlInput.Start(Input.mousePosition); } else if(Input.GetMouseButtonUp(0)) { m_MouseMove = false; } //滑动位置 if (m_MouseMove) { m_CtrlInput.Check(Input.mousePosition); } } /// <summary> /// 触屏控制 /// </summary> void TouchControl() { if (Input.touchCount == 0) return; Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { m_CtrlInput.Start(touch.position); }else if(touch.phase == TouchPhase.Moved) { m_CtrlInput.Check(touch.position); } } public void SetColor(Color color) { foreach (Transform t in transform) { if (t.tag == \"Block\") { t.GetComponent<SpriteRenderer>().color = color; } } } }
SquareControl脚本
在Script文件夹下创建一个名为GameManager的C#脚本,用来管理游戏的整体逻辑,并且该脚本只在场景中挂在一遍。打开GameManager脚本将其做成单例模式:
1 public static GameManager instance { get; set; } 2 void Awake () 3 { 4 instance = this; 5 }
单例模式
在游戏打开的时候,要实现Map地图,开始界面等初始化,所以在GameManager类中写一个初始化方法和创建界面生成的方法
/// <summary> /// 初始化窗口 /// </summary> public void Init() { GameObject tranMap = Resources.Load<GameObject>(\"Prefabs/Other/Map\"); Transform map = Instantiate(tranMap).transform; map.name = \"Map\"; CreatePoint = map.Find(\"CreatePoint\"); } /// <summary> /// 创建面板 /// </summary> /// <param name=\"type\"></param> /// <returns></returns> public GameObject CreatePanel(PanelType type) { GameObject prefab = Resources.Load<GameObject>(\"Prefabs/Panel/\" + type); if (prefab != null) { GameObject clone = Instantiate(prefab, transform, false) as GameObject; clone.name = type.ToString(); switch (type) { case PanelType.StartPanel: ctrl_start = clone.AddComponent<StartCtrl>(); break; case PanelType.RunPanel: ctrl_run = clone.AddComponent<RunCtrl>(); break; case PanelType.SetPanel: ctrl_set = clone.AddComponent<SetCtrl>(); break; case PanelType.RankPanel: ctrl_rank = clone.AddComponent<RankCtrl>(); break; case PanelType.GameOverPanel: ctrl_over = clone.AddComponent<OverCtrl>(); break; case PanelType.PasuePanel: ctrl_pasue = clone.AddComponent<PasueCtrl>(); break; } return clone; } return null; }
初始化
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { public StartCtrl ctrl_start { get; set; } public RunCtrl ctrl_run { get; set; } public SetCtrl ctrl_set { get; set; } public RankCtrl ctrl_rank { get; set; } public OverCtrl ctrl_over { get; set; } public PasueCtrl ctrl_pasue { get; set; } public int Score { get; set; } public bool Sound { get; set; } public int highScore { get; set; } public int numbersGame { get; set; } public bool isOver { get; set; } public SquareControl currentShape { get; set; } public Transform CreatePoint { get; set; } public const int NORMAL_ROWS = 12; public const int MAX_ROWS = 15; public const int MAX_COLUMNS = 10; Transform[,] map = new Transform[MAX_COLUMNS, MAX_ROWS]; bool isUpData; public static GameManager instance { get; set; } void Awake () { instance = this; //初始化 Init(); isOver = false; //添加音频管理脚本 gameObject.AddComponent<AudioManager>(); //判断是否是PC端 if (Application.platform == RuntimePlatform.WindowsPlayer) { //只有在windows系统的平台上才会执行 //设置窗口大小 Screen.SetResolution(540, 960, false); } } void Start() { if (ctrl_start == null) { CreatePanel(PanelType.StartPanel); } DataModel.LoadData(); AudioManager.instance.SetAudioSource(!Sound); } void Update () { if (!isOver) { if (IsGameOver()) { //游戏结束,暂停 ctrl_run.isPause = true; isOver = true; } } if (Input.GetKeyDown(KeyCode.Escape) && isOver) { Application.Quit(); } } /// <summary> /// 初始化窗口 /// </summary> public void Init() { GameObject tranMap = Resources.Load<GameObject>(\"Prefabs/Other/Map\"); Transform map = Instantiate(tranMap).transform; map.name = \"Map\"; CreatePoint = map.Find(\"CreatePoint\"); } /// <summary> /// 创建面板 /// </summary> /// <param name=\"type\"></param> /// <returns></returns> public GameObject CreatePanel(PanelType type) { GameObject prefab = Resources.Load<GameObject>(\"Prefabs/Panel/\" + type); if (prefab != null) { GameObject clone = Instantiate(prefab, transform, false) as GameObject; clone.name = type.ToString(); switch (type) { case PanelType.StartPanel: ctrl_start = clone.AddComponent<StartCtrl>(); break; case PanelType.RunPanel: ctrl_run = clone.AddComponent<RunCtrl>(); break; case PanelType.SetPanel: ctrl_set = clone.AddComponent<SetCtrl>(); break; case PanelType.RankPanel: ctrl_rank = clone.AddComponent<RankCtrl>(); break; case PanelType.GameOverPanel: ctrl_over = clone.AddComponent<OverCtrl>(); break; case PanelType.PasuePanel: ctrl_pasue = clone.AddComponent<PasueCtrl>(); break; } return clone; } return null; } /// <summary> /// 生成方块 /// </summary> /// <param name=\"type\"></param> /// <returns></returns> public GameObject CreateSquare(SquareType type) { GameObject prefab = Resources.Load<GameObject>(\"Prefabs/Square/\" + type); if (prefab != null) { GameObject clone = Instantiate(prefab, CreatePoint) as GameObject; clone.name = type.ToString(); clone.transform.position = CreatePoint.position; currentShape = clone.AddComponent<SquareControl>(); return clone; } return null; } /// <summary> /// 方块位置是否有效 /// </summary> /// <param name=\"t\">方块坐标</param> /// <returns></returns> public bool IsValidMapPosition(Transform t) { //遍历方块子物体 foreach (Transform child in t) { //跳过标签不为 Block 的子物体 if (child.tag != \"Block\") continue; Vector2 pos = Round(child.position); //判断是否在地图中 if (IsInsideMap(pos) == false) return false; //判断是否在数组中 if (map[(int)pos.x, (int)pos.y] != null) return false; } return true; } /// <summary> /// 是否在地图中 /// </summary> /// <param name=\"pos\"></param> /// <returns></returns> private bool IsInsideMap(Vector2 pos) { return pos.x >= 0 && pos.x < MAX_COLUMNS && pos.y >= 0; } /// <summary> /// 放置方块 /// </summary> /// <param name=\"t\"></param> /// <returns></returns> public bool PlaceShape(Transform t) { foreach (Transform child in t) { if (child.tag != \"Block\") continue; Vector2 pos = Round(child.position); map[(int)pos.x, (int)pos.y] = child; } return CheckMap(); } /// <summary> /// 检查地图是否不要消除行 /// </summary> /// <returns></returns> private bool CheckMap() { int count = 0; for (int i = 0; i < MAX_ROWS; i++) { bool isFull = CheckIsRowFull(i); if (isFull) { count++; DeleteRow(i); MoveDownRowsAbove(i + 1); i--; } } Debug.Log(count); if (count > 0) { Score += (count * 100); if (Score > highScore) { highScore = Score; } isUpData = true; return true; } else return false; } /// <summary> /// 是否满行 /// </summary> /// <param name=\"row\"></param> /// <returns></returns> bool CheckIsRowFull(int row) { for (int i = 0; i < MAX_COLUMNS; i++) { if (map[i, row] == null) return false; } return true; } /// <summary> /// 删除指定行 /// </summary> /// <param name=\"row\"></param> void DeleteRow(int row) { for (int i = 0; i < MAX_COLUMNS; i++) { Destroy(map[i, row].gameObject); map[i, row] = null; } } /// <summary> /// 向下移动多行 /// </summary> /// <param name=\"row\"></param> void MoveDownRowsAbove(int row) { for (int i = row; i < MAX_ROWS; i++) { MoveDownRow(i); } } /// <summary> /// 向下移动行 /// </summary> /// <param name=\"row\"></param> void MoveDownRow(int row) { for (int i = 0; i < MAX_COLUMNS; i++) { if (map[i, row] != null) { map[i, row - 1] = map[i, row]; map[i, row] = null; map[i, row - 1].position += new Vector3(0, -1, 0); } } } /// <summary> /// 方块落下 /// </summary> public void FallDown() { currentShape = null; if (isUpData) { ctrl_run.view.SetScoreText(Score, highScore); } foreach (Transform t in CreatePoint) { if (t.childCount <= 1) { Destroy(t.gameObject); } } } /// <summary> /// V3转V2 /// </summary> /// <param name=\"v\"></param> /// <returns></returns> public static Vector2 Round(Vector3 v) { //RoundToInt:返回四舍五入到最接近的整数。 int x = Mathf.RoundToInt(v.x); int y = Mathf.RoundToInt(v.y); return new Vector2(x, y); } /// <summary> /// 游戏是否结束 /// </summary> /// <returns></returns> public bool IsGameOver() { for (int i = NORMAL_ROWS; i < MAX_ROWS; i++) { for (int j = 0; j < MAX_COLUMNS; j++) { if (map[j, i] != null) { isOver = true; ctrl_run.isPause = true; //加载结束面板 CreatePanel(PanelType.GameOverPanel); ctrl_run.view.SetScoreText(0, highScore); numbersGame++; DataModel.SaveData(); return true; } } } return false; } }
GameManager脚本完整版
留到最后的话,一款游戏如果缺了音乐总会觉得少些动感,所以在程序的最后说一下本工程中的音频管理。
本工程中音频AudioSource有两个分别用来控制有可能冲突的音效,两个AudioSource分别添加到MainCamera和Canvas上。并且将音频文件所在的文件夹放置到Resource文件夹下,方便调用。在脚本文件夹中创建一个AudioManager脚本用来控制程序的音频。
using UnityEngine; using System.Collections; public class AudioManager : MonoBehaviour { /// <summary> /// 点击音效 /// </summary> public AudioClip cursor; /// <summary> /// 放置音效 /// </summary> public AudioClip balloon; /// <summary> /// 下落音效 /// </summary> public AudioClip drop; /// <summary> /// 消除音效 /// </summary> public AudioClip lineclear; /// <summary> /// 结束音效 /// </summary> public AudioClip gameover; AudioSource audioSource1 { get; set; } AudioSource audioSource2 { get; set; } /// <summary> /// 单例模式 /// </summary> public static AudioManager instance; private void Awake() { instance = this; audioSource1 = GameObject.Find(\"Main Camera\").GetComponent<AudioSource>(); audioSource2 = GameObject.Find(\"Canvas\").GetComponent<AudioSource>(); cursor = Resources.Load<AudioClip>(\"Audio/Cursor\"); balloon = Resources.Load<AudioClip>(\"Audio/Balloon\"); drop = Resources.Load<AudioClip>(\"Audio/Drop\"); lineclear = Resources.Load<AudioClip>(\"Audio/Lineclear\"); gameover = Resources.Load<AudioClip>(\"Audio/Gameover\"); } public void SetAudioSource(bool iscan = true) { audioSource1.enabled = audioSource2.enabled = iscan; } /// <summary> /// 点击音效 /// </summary> public void PlayCursor() { PlayAudio2(cursor); } /// <summary> /// 下落音效 /// </summary> public void PlayDrop() { PlayAudio1(drop); } /// <summary> /// 放置音效 /// </summary> public void PlayControl() { PlayAudio2(balloon); } /// <summary> /// 消除音效 /// </summary> public void PlayLineClear() { PlayAudio2(lineclear); } /// <summary> /// 结束音效 /// </summary> public void PlayGameOver() { PlayAudio2(gameover); } /// <summary> /// 播放音效 /// </summary> /// <param name=\"clip\">音效源</param> void PlayAudio1(AudioClip clip) { audioSource1.clip = clip; audioSource1.Play(); //audioSource1.Pause(); } void PlayAudio2(AudioClip clip) { audioSource2.clip = clip; audioSource2.Play(); } }
AudioSource
至此,俄罗斯方块的代码也完成了,代码偏多,阅读不便。以下是程序的工程源码和打包好的可执行程序
unity3d俄罗斯方块源码5.4.3版(有动画版)
https://download.csdn.net/download/u012433546/11037870
unity3d俄罗斯方块源码2018.2.14版(无动画):
链接:https://pan.baidu.com/s/1-2UhD7_A-4IQf8qMMBm_Wg
提取码:w8rr
unity3d俄罗斯方块PC端程序:
https://download.csdn.net/download/u012433546/11038002
链接:https://pan.baidu.com/s/16vdhu6rRC5nx1Rz7W2qGsg
提取码:ypa2