用C/C++制作一个简单的俄罗斯方块小游戏

news2024/11/15 11:37:01

用C/C++制作一个简单的俄罗斯方块小游戏

  • 用C/C++制作一个简单的俄罗斯方块小游戏
    • 0 准备
    • 1 游戏界面设计
      • 1.1 界面布局
      • 1.2 用 EasyX 显示界面
      • 1.3 音乐播放
    • 2 方块设计
      • 2.1 方块显示
      • 2.2 随机生成一个方块
      • 2.3 方块记录
    • 3 方块移动和旋转
      • 3.1 方块的移动
      • 3.2 方块的旋转
      • 3.3 方块的碰撞和消除
        • 3.3.1 碰撞
        • 3.3.2 消除
        • 3.3.3 分数和下落速度
        • 3.3.4 game over
    • 4 制作 exe 文件
    • 5 总结

在这里插入图片描述

0 准备

  • 开发环境:Windows
  • 编程语言:C/C++
  • 开发软件:Visual Studio 2022 (软件安装可参考:链接: Visual Studio 2022 免费版最新版本下载安装教程
  • 图形插件:EasyX

EasyX 的安装非常简单,百度搜索以下即可,但是在安装前一定要先安装 Visual Studio。下面的章节给出如何使用它。

1 游戏界面设计

1.1 界面布局

首先,我们要选择一章图片作为游戏的背景,我们可以在图片网站上下载合适的背景图片。

其次,在背景图片上划分出,游戏区和显示区,一般游戏区在正中间,两边为显示区。游戏区用于控制方块的移动、消除和旋转等;显示区用于速度和分数的展示。下面是我设计的游戏界面:

在这里插入图片描述

用画图工具打开图片,可以看到背景大小为800*600像素,同时在图片中间设置游戏区(虚线框,大小自定义),添加速度和分数的文本框。

1.2 用 EasyX 显示界面

此时,我相信你应该会建立 VS 工程了,并且安装了 EasyX。

建立一个文件夹 imp ,把1.1小节中的图片放在里面。imp 文件夹和 main.cpp 文件同一目录。
显示图形需要调用头文件 graphics.h。我们需要知道显示的图形的大小,这边是800*600,显示的位置为(0,0)

#include <graphics.h>

// 绘图窗口初始化
	initgraph(imp_width, imp_heght);
	loadimage(&background, _T("img/background.png"));
	putimage(0, 0, &background);

显示如下:

背景显示

这是一个简单的显示案例,后续方块的显示也是用这种方式。

1.3 音乐播放

先用 酷狗 下载一首好听的音乐,然后将音乐放在和 main.cpp 同一目录下。

我这边用了两首音乐,每次打开都是随机播放

程序:

#include "Windows.h"
#include <time.h>

#pragma comment (lib, "winmm.lib")

void Music::palyMusic()
{
	int chFlag;
	srand((unsigned)time(NULL));
	chFlag = rand() % 2;
	
	if (chFlag == 0)
	{
		//mciSendString("close 1.mp3", NULL, 0, NULL);
		mciSendString("open 2.mp3", NULL, 0, NULL);
		mciSendString("play 2.mp3 repeat", NULL, 0, NULL);
		mciSendString("setaudio 2.mp3 volume to 100", 0, 0, 0);
	}
	else
	{
		//mciSendString("close 2.mp3", NULL, 0, NULL);
		mciSendString("open 1.mp3", NULL, 0, NULL);
		mciSendString("play 1.mp3 repeat", NULL, 0, NULL);
		mciSendString("setaudio 1.mp3 volume to 100", 0, 0, 0);
	}
}

2 方块设计

2.1 方块显示

设计7种方块类型:

const int blocks[7][4] = {
		1,3,5,7, // I
		2,4,5,7, // Z 1型
		3,5,4,6, // Z 2型
		3,5,4,7, // T
		2,3,5,7, // L
		3,5,7,6, // J
		2,3,4,5, // 田
	};

方块的表示如下图所示:

方块显示

上面两张图表示在游戏中方块是如何表示的,方块可以由界面的横纵坐标表示,我们可以将下落的初始位置作为横坐标,方块的左边界作为纵坐标。其实就是游戏区的左上角作为坐标。

那么显示的原理知道了,就是操作数组,方块的图形呢?

小方块显示

小方块的显示可以根据这张图,从上图不难看出,小方块的大小为:20*20像素。如果我们要显示第一个小方块,我们可以加载这张图片然后从坐标(0,0)开始,显示长宽为20像素的图片。同意,如果要显示第三个绿色方块,就是从坐标(40,0)开始。

下面是实现的部分程序:

//计算小方块位置
blockType = rand() % 7;
for (int i = 0; i < 4; i++)
{
	smallBlock[i][0] = blocks[blockType][i] / 2;
	smallBlock[i][1] = blocks[blockType][i] % 2 + 1;//离左边界一格显示,方便旋转
}

//小方块显示函数
void Graph::block()
{
	IMAGE imgTmp;
	loadimage(&imgTmp, _T("img/small.png"));
	SetWorkingImage(&imgTmp);
	//putimage(0, 0, &imgTmp);
	for (int i = 0; i < 7; i++) {
		this->imgs[i] = new IMAGE;
		getimage(this->imgs[i], i * blocks_size, 0, blocks_size, blocks_size);
	}
	SetWorkingImage();
}

初始位置

2.2 随机生成一个方块

随机生成方块的原理就是将记录正在下落方块的数组清空初始化

void Graph::random()
{
	blockType = rand() % 7;

	for (int i = 0; i < 4; i++)
	{
		smallBlock[i][0] = blocks[blockType][i] / 2;
		smallBlock[i][1] = blocks[blockType][i] % 2 + 1;//离左边界一格显示,方便变形
	}
	colBasis = 0;
	rowBasis = 0;
}

2.3 方块记录

我们不仅需要对当前正在操作的方块进行记录,还需要对已经下落还未被消除的方块进行记录。可以将游戏区看作一个二维数组,开辟一个 29*14 的二维数组进行记录。

	//背景图像大小
	const unsigned int imp_width = 800;
	const unsigned int imp_heght = 600;
	//小方块大小
	const unsigned int blocks_size = 20;

	//游戏区边界
	const unsigned int left_margin = 240;
	const unsigned int right_margin = 485;
	const unsigned int down_margin = 570;
	const unsigned int up_margin = 10;
	const int rows = 29;
	const int cols = 14;
	
	//记录方块的数组
	vector<vector<int>> allBlock;

这里有个小技巧,因为方块要显示不同的颜色,因此我们可以用二维数组allBlock的值当作颜色的值

当allBlock[i][j]的值为0时,表示该位置没有方块;
当allBlock[i][j]的值大于0时,表示该位置有方块,显示的颜色用allBlock[i][j]的之表示

//已静止方块显示
	for (int i = rows-1; i > 3; --i)
	{
		for (int j = 0; j < cols; ++j)
		{
			if(allBlock[i][j]!=0)
				putimage(left_margin + j * blocks_size, up_margin + i * blocks_size, imgs[allBlock[i][j]-1]);
		}
	}

3 方块移动和旋转

3.1 方块的移动

方块的移动就是一个核心:方块的移动 = 对数组的操作

方块的下落 = 行坐标+1
方块的左移 = 列坐标-1
方块的右移 = 列坐标+1

前提是需要判断是否出界或者移动的下一个位置是否有方块

程序如下:

void Graph::moveLeft()
{
	for (int i = 0; i < 4; i++)
	{
		if (smallBlock[i][1] <= 0 || allBlock[smallBlock[i][0]][smallBlock[i][1] - 1] >= 1)
			return;
	}
	for (int i = 0; i < 4; i++)
	{
		--smallBlock[i][1];
	}
	--colBasis;
}

void Graph::moveDown()
{
	for (int i = 0; i < 4; i++)
	{
		if (smallBlock[i][0] >= rows)
			return;
	}
	for (int i = 0; i < 4; i++)
	{
		++smallBlock[i][0];
	}
	++rowBasis;
}

void Graph::moveRight()
{
	for (int i = 0; i < 4; i++)
	{
		if (smallBlock[i][1] >= cols - 1 || allBlock[smallBlock[i][0]][smallBlock[i][1] + 1] >= 1)
			return;
	}
	for (int i = 0; i < 4; i++)
	{
		++smallBlock[i][1];
	}
	++colBasis;
}

当然移动的前提说需要用户按键输入的,所以需要有判断按键输入的函数和读取按键值的函数,我这边使用函数 _kbhit() 来判断是否有按键输入,用函数 _getch() 读取按键值

//控制方块移动
	if (_kbhit() && graph.startFlag)//如果键盘有输入
	{
		graph.keyPlay();
	}
void Graph::keyPlay()
{
	int ch = 0;
	ch = _getch();
	switch (ch)
	{
		//WASD键(小写)
		case 119: changeBlock();//上键
			break;
		case 97: moveLeft();//左键
			break;
		case 115: moveDown();//下键
			break;
		case 100: moveRight();//右键
			break; 
		//上下左右键
		case 72: changeBlock();//上键
			break;
		case 75: moveLeft();//左键
			break;
		case 80: moveDown();//下键
			break;
		case 77: moveRight();//右键
			break;
	}
}

3.2 方块的旋转

方块旋转

如上图所示,这样可以用几行代码实现了方块的旋转,但是仍然需要注意下面的几个问题:

  • 以什么为中心旋转?
  • 方块是不断下落的,行和列是一直在变化的
  • 在边界处有部分方块是不能旋转的

针对第一个问题,如果想让方块的旋转看起来不那么别捏,以4*4方格的中心旋转是最合适的,即图中的2,3,4,5作为旋转的核心。

针对第二个问题,可以将方块的行列切换至初始位置,再进行上图的公式,然后再切回来,这边可以设置两个变量确定方块离初始位置的距离。

针对第三个问题,将方块的行列号暂存,进行变换,然后再进行检测是否有方块在边界外面,如果有,旋转这步算作废。

旋转的时候初始位置的确定也是非常关键的,因为在边界处有些旋转是做不了的

初始位置

程序实现:

void Graph::changeBlock()
{
	int temp[4][2] = { 0 };

	for (int i = 0; i < 4; i++)
	{
		//配合偏置,进行方块的旋转
		temp[i][0] = smallBlock[i][1] - colBasis;
		temp[i][1] = 3 - (smallBlock[i][0] - rowBasis);

		temp[i][0] += rowBasis;
		temp[i][1] += colBasis;

		//检查合法性
		if (temp[i][1] == 0 || temp[i][1] == cols - 1)
			return;
	}
	for (int i = 0; i < 4; i++)//若合法,实行
	{
		smallBlock[i][0] = temp[i][0];
		smallBlock[i][1] = temp[i][1];
	}
}

3.3 方块的碰撞和消除

方块的消除需要考虑下面几个问题

  • 碰撞检测
  • 一行的消除算法

3.3.1 碰撞

碰撞检测很容易实现,由于左右移动我已经设置了边界检测,这边只需要对四个方块进行判断,也就是是说判断它们下面是否有方块就行。如果有,就返回 1

int Graph::check()
{
	int row, col;

	for (int i = 0; i < 4; i++)//若合法,实行
	{
		row = smallBlock[i][0]+1;
		col = smallBlock[i][1];

		if (row >= this->rows || allBlock[row][col] >= 1)
		{
			if (rowBasis == 0)
				return 2;
			else
				return 1;
		}
	}
	return 0;
}

3.3.2 消除

对一行的消除,采用一个二维数组对所有的位置进行记录,如果在(i,j)处有方块,则 allBlock[i][j]=1;在碰撞检测完毕之后,对整个数组进行遍历,对每一行移动的行数进行记录,尽量减少时间复杂度。

int clearRowNum[30] = { 0 };
int num=0;
//unordered_map<int, int>map;
if (check()==1)
{
	for (int i = 0; i < 4; i++) 
	{
		int row = smallBlock[i][0];
		int col = smallBlock[i][1];
		allBlock[row][col] = blockType+1;
	}
	//消除一行
	for (int i = rows-1; i > 3; --i)
	{
		for (int j = 0; j < cols; ++j)
		{
			if (allBlock[i][j] == 0)
			{
				clearRowNum[i] = num;
				break;
			}
			else if (j == cols-1)//该行需要消除
			{
				++num;
				clearRowNum[i] = 0;
			}
		}
	}

	for (int i = rows - 2; i > 3; --i)
	{
		if (clearRowNum[i] != 0)
		{
			for (int j = 0; j < cols; ++j)
			{
				allBlock[i + clearRowNum[i]][j] = allBlock[i][j];
			}
		}
	}
}

3.3.3 分数和下落速度

同时,在消除函数中可以添加分数计算,速度计算。大致的逻辑是每消除一行,分数变多;分数越高,下落速度越快;

//设置速度,得分越多,速度越快
score += num * cols;

speed = 100+score/10;

3.3.4 game over

当小方块处于初始位置时,它的下方有方块时就可以判断 game over了。

if (row >= this->rows || allBlock[row][col] >= 1)
{
	if (rowBasis == 0)
		return 2;
	else
		return 1;
}

game over 之后,界面会一直显示game over,直到输入 回车键

//游戏结束
	if (!graph.startFlag)
	{
		settextcolor(WHITE);
		settextstyle(40, 0, "黑体");
		setbkmode(TRANSPARENT);
		char s[10] = "Game Over";
		outtextxy(300, 280, s);

		if (_kbhit() && _getch() == 13)//如果键盘有输入
		{
			graph.init();
		}
	}

game over

4 制作 exe 文件

如何用 Visual Studio打包项目程序可以参考:

Visual Studio 怎么将项目程序打包成软件

5 总结

最后我想说的是,对方块的移动和旋转,其根本就是在对数组进行操作。

至于后续的一些最高分数记录,下一个方块提示等功能,都是锦上添花的功能,感兴趣的小伙伴可以尝试添加一下。

程序下载:
俄罗斯方块小游戏程序下载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391602.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于 WebSocket、Spring Boot 教你实现“QQ聊天功能”的底层简易demo

目录 前言 一、分析 1.1、qq聊天功能分析 1.2、WebSocket介绍 1.2.1、什么是消息推送呢&#xff1f; 1.2.2、原理解析 1.2.3、报文格式 二、简易demo 2.1、后端实现 2.1.1、引入依赖 2.1.2、继承TextWebSocketHandler 2.1.3、实现 WebSocketConfigurer 接口 2.2、…

LeetCode096不同的二叉搜索树(相关话题:卡特兰数)

目录 题目描述 解题思路 代码实现 进出栈序列理解卡特兰数分析策略 相关知识 参考文章 题目描述 给你一个整数 n &#xff0c;求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种&#xff1f;返回满足题意的二叉搜索树的种数。 示例 1&#xff1a; …

《程序员面试金典(第6版)》面试题 02.07. 链表相交

题目描述 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果…

socket本地多进程通信基本使用方法和示例

目录 前言&#xff1a; socket是什么 socket基本原理框图 socket基本函数 1 socket() 函数 2 bind()函数 3 connect()函数 4 listen() 函数 5 accept() 函数 6 read() write() send() recv()函数 7 close()函数 8 字节序转换&#xff08;hton&#xff09; 示例代码 …

使用 Pulumi 打造自己的多云管理平台

前言在公有云技术与产品飞速发展的时代&#xff0c;业务对于其自身的可用性提出了越来越高的要求&#xff0c;当跨区域容灾已经无法满足业务需求的情况下&#xff0c;我们通常会考虑多云部署我们的业务平台&#xff0c;以规避更大规模的风险。但在多云平台部署的架构下&#xf…

埃安自研版图扩至夸克电驱,动力研发团队已超1000人

埃安的三电自研版图正在扩大。3月3日&#xff0c;广汽集团旗下埃安发布了一项名为“夸克电驱”的技术产品&#xff0c;相比主流电驱体积减少一倍&#xff0c;同时电机功率密度比主流电驱增加了一倍。此前&#xff0c;比亚迪刚刚发布易四方动力系统&#xff0c;特斯拉也在投资者…

HTML常见标签

文章目录一、HTML基础标签注释标签标题标签段落标签换行标签格式化标签图片、音频、视频标签超链接标签列表标签表格标签布局标签表单标签表单标签概述form标签属性表单项标签综合案例一、HTML基础标签 基础标签就是和文字相关的标签 标签描述<h1> ~ <h6>定义标题…

【项目管理】晋升为领导后,如何开展工作?

兵随将转&#xff0c;作为管理者&#xff0c;你可以不知道下属的短处&#xff0c;却不能不知道下属的长处。晋升为领导后&#xff0c;如何开展工作呢&#xff1f; 金九银十&#xff0c;此期间换工作的人不在少数。有几位朋友最近都换了公司&#xff0c;职位得到晋升&#xff0c…

前端——1.相关概念

这篇文章主要介绍前端入门的相关概念 1.网页 1.1什么是网页&#xff1f; 网站&#xff1a;是指在因特网上根据一定的规则&#xff0c;使用HTML等制作的用于展示特定内容相关的网页集合 网页&#xff1a;是网站中的一“页”&#xff0c;通常是HTML格式的文件&#xff0c;它要…

JAVA后端部署项目三步走

1. JAVA部署项目三步走 1.1 查看 运行的端口 lsof -i:8804 &#xff08;8804 为端口&#xff09; 发现端口25111被监听 1.2 杀死进程,终止程序 pid 为进程号 kill -9 pid 1.3 后台运行jar包 nohup java -jar -Xms128M -Xmx256M -XX:MetaspaceSize128M -XX:MaxM…

C++笔记之lambda表达式

引言 Lambda表达式是从C 11版本引入的特性&#xff0c;利用它可以很方便的定义匿名函数对象&#xff0c;通常作为回调函数来使用。大家会经常拿它和函数指针&#xff0c;函数符放在一起比较&#xff0c;很多场合下&#xff0c;它们三者都可以替换着用。 语法 [ captures ] (…

javaScript基础面试题 ---宏任务微任务

宏任务微任务一、为什么JS是单线程语言&#xff1f;二、JS是单线程&#xff0c;怎样执行异步代码&#xff1f;1、JS是单线程语言 2、JS代码执行流程&#xff0c;同步执行完&#xff0c;再进行事件循环&#xff08;微任务、宏任务&#xff09; 3、清空所有的微任务&#xff0c;再…

机器学习100天(四十):040 线性支持向量机-公式推导

《机器学习100天》完整目录:目录 机器学习 100 天,今天讲的是:线性支持向量机-公式推导! 首先来看这样一个问题,在二维平面上需要找到一条直线划分正类和负类。 我们找到了 A、B、C 三条直线。这三条直线都能正确分类所有训练样本。但是,哪条直线最好呢?直观上来看,我…

代码随想录算法训练营第六天|242.有效的字母异位词 、349. 两个数组的交集 、 202. 快乐数、1. 两数之和

当我们遇到了要快速判断一个元素是否出现集合里的时候&#xff0c;就要考虑哈希法。哈希法是牺牲了空间换取了时间&#xff0c;要使用额外的数组&#xff0c;set或者是map来存放数据&#xff0c;才能实现快速的查找。当我们要使用集合来解决哈希问题的时候&#xff0c;优先使用…

【SpringCloud】SpringCloud教程之Nacos实战(1)

目录Nacos是什么&#xff1f;一.Nacos下载二.安装Nacos三.Nacos原理四.Nacos快速入门五.Nacos服务多级存储模式六.Nacos根据集群设置负载均衡1.根据同集群优先访问2.根据权重配置负载均衡七.Nacos的环境隔离八.Nacos和Eureka的区别前提&#xff1a;以订单服务和用户服务为例&am…

【C++基础入门】数组、函数

一&#xff1a;数组 1.1 概述 所谓数组&#xff0c;就是一个集合&#xff0c;里面存放了相同类型的数据元素 特点1&#xff1a; 数组中的每个数据元素都是相同的数据类型 特点2&#xff1a; 数组是由连续的内存位置组成的 1.2 一维数组 1.2.1 一维数组定义方式 一维数组…

世界那么大,你哪都别去了,来我带你了解CSS3 (二)

文章目录‍❤️‍&#x1f525;CSS文档流‍❤️‍&#x1f525;CSS浮动‍❤️‍&#x1f525;CSS定位‍❤️‍&#x1f525;CSS媒体查询‍❤️‍&#x1f525;CSS文档流 文档流是文档中可显示对象在排列时所占用的位置/空间。 例如&#xff1a;块元素自上而下摆放&#xff0c;内…

第一章---Pytorch快速入门---第一节---张量及运算

目录 1.1张量的数据类型 1.2 张量的生成 1.3 张量操作 1.4 张量的计算 一、张量 在高等数学中&#xff0c;单独的一个数是标量&#xff0c;而有序排列的一组数字是一个向量&#xff08;例如一个数组&#xff09;&#xff0c;向量组可以构成矩阵。向量是一维的&#xff0c;而…

Java——腐烂的橘子

题目链接 leetcode在线oj题——腐烂的橘子 题目描述 在给定的 m x n 网格 grid 中&#xff0c;每个单元格可以有以下三个值之一&#xff1a; 值 0 代表空单元格&#xff1b;值 1 代表新鲜橘子&#xff1b;值 2 代表腐烂的橘子。 每分钟&#xff0c;腐烂的橘子 周围 4 个方…

Python数据分析案例19——上市银行财务指标对比

我代码栏目都是针对基础的python数据分析人群&#xff0c;比如想写个本科毕业论文&#xff0c;课程论文&#xff0c;做个简单的案例分析等。过去写的案例可能使用了过多的机器学习和深度学习方法&#xff0c;文科的同学看不懂&#xff0c;可能他们仅仅只想用python做个回归或者…