小时候,大家都应玩过或听说过《俄罗斯方块》,它是红白机,掌机等一些电子设备中最常见的一款游戏。而随着时代的发展,信息的进步,游戏画面从简单的黑白方块到彩色方块,游戏的玩法机制从最简单的消方块到现在的多人pk等,无一不是在体现它的火爆。在这里,通过这篇文章向大家分享一下自己在制作俄罗斯方块的经验和心得,以及文章最后的源码和pc程序。
首先,看标题都知道这篇文章中所用到的游戏引擎是:unity3d,版本不限,但最好是5.4.3以上的,原因是因为作者自己没有用过5.4.3以下的版本。
准备工具都有:unity3d + Visual Studio 2015
素材准备有(密码:m6gz):字体方正粗圆_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 () {
}
}


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;
}
}


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);
}
}


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();
}
}


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>();
}
}


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();
}
}
在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);
}
}
}


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);
}
}
}


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 () {
}
}


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 () {
}
}


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);
}
}
}


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);
}
}
在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;
}
}
}
在游戏打开的时候,要实现Map地图,开始界面等初始化,所以在GameManager类中写一个初始化方法和创建界面生成的方法
留到最后的话,一款游戏如果缺了音乐总会觉得少些动感,所以在程序的最后说一下本工程中的音频管理。
本工程中音频AudioSource有两个分别用来控制有可能冲突的音效,两个AudioSource分别添加到MainCamera和Canvas上。并且将音频文件所在的文件夹放置到Resource文件夹下,方便调用。在脚本文件夹中创建一个AudioManager脚本用来控制程序的音频。
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
原文出处:https://www.cnblogs.com/CLXiao-1029/p/10562062.html
来源:oschina
链接:https://my.oschina.net/u/4258878/blog/3267781