大家好,我是赛博红兔。欢迎回到《和我一起做3A游戏》系列!在介绍今天的游戏之前,我建议完全没有Pygame基础的朋友们先去看看本系列的前三集基础教学,链接我会放在下方评论区。相信大家对《俄罗斯方块》这个游戏再熟悉不过了。在我小时候只要有机会就会捧着专门玩《俄罗斯方块》的掌机玩到天黑。它的核心玩法简单却极具挑战性:玩家需要旋转和移动7种不同形状的方块,使它们在底部拼接成完整的横排,从而消除行并获得分数。它是由一位苏联的科学家阿列克谢·帕基特诺夫在1984年发明的。由于苏联当时实行计划经济,个人无法拥有知识产权,《俄罗斯方块》的版权最初归苏联政府所有。在1991年苏联解体后,帕基特诺夫移居美国,最终在1996 年正式获得《俄罗斯方块》的版权。如今,《俄罗斯方块》已成为世界上最畅销、最经典的游戏之一,被移植到几乎所有游戏平台,总销量超过 5 亿份。今天我们这期节目也来效仿一下这款经典的游戏,这也是我这个系列编写的游戏里最有挑战的一个!
我已经把这个游戏用到的资源、代码还有EXE的应用程序打包放在百度网盘了,链接会放在下面,代码也可以去我的GitHub或者博客下载。你可以按照上面的安装说明部署项目,都写得比较详细了。如果,只想玩这个游戏的朋友,可以直接运行我打包好的EXE应用文件,在电脑上直接跑不需要安装python。记住,跑这个游戏的电脑需要有一张声卡来运行背景音乐,而且一定要把应用程序和assets也就是游戏资源包放在一起,不然会出错。
游戏完整代码及资源链接:https://pan.baidu.com/s/1cir610ncEuB0_Jvqpmgg7g 提取码:elsf
游戏GitHub项目:https://github.com/XingshengXu/Tetris-Game
好了,想继续学习这游戏是怎么做的朋友请接着往下看。首先,我在第一集已经介绍了我所有的游戏资源,包括图片、声效、BGM,都是从这两个开源网站下载的,大家也可以共享一下自己了解的游戏资源库。接下来我们看到的是游戏源代码,这里,我主要会重点介绍之前没有见过的pygame功能和游戏特色部分的代码。具体大伙可以按照我的源代码去学习和拓展。
在我们的项目包里第一个asset文件夹里都是游戏的资源文件,包括一些图片和音效。下面是一个主文件和一个配置文件,这游戏常量有不少,比如游戏窗口显示,图片、音乐存放路径,事件代码等等,我们都用英文大写来命名它们并且都放在了这个叫settings的配置文件里,方便主文件的调用。游戏的核心结构划分为三个主要类:Block单个方块类、Tetromino整个方块类、Game游戏类。首先我们引入random模块用于随机选择方块类型和生成随机数。导入了pygame库,sys模块里的exit函数是用来方便退出游戏用的。最后还要导入所有的游戏配置参数文件。
- 功能函数
在初始化pygame库之后,我们先来介绍事先搭建的游戏功能函数。load_sprite_sheet函数是用来加载精灵表然后按指定的宽和高拆分成多个独立的贴图,这里的精灵表是包含多个方块的贴图。我们还使用了render_font函数来渲染文本信息,比如分数、游戏名称等等。
- Block单个方块类
首先我们来看Block单个方块类,它是Tetromino整个方块组的一部分。它是继承pg.sprite.Sprite的一个精灵类。想详细了解精灵类的去看前三集教学篇,总的来说Pygame提供了精灵sprite这个模块来方便管理游戏中的对象,包括它的位置大小、色彩渲染与其他精灵的碰撞等等。我们用__init__函数初始化Block类的实例,参数tetromino用于指定方块的类型,比如 L 形、T 形。coord参数是在Tetromino整个方块组里的初始坐标。block_move方法是用来让每次Block 位置变化,它的rect(也就是矩形位置)保持同步更新。block_rotate方法用来计算方块的旋转,但它不会直接旋转,而是返回旋转后的新坐标。它通过计算 当前坐标相对旋转中心的偏移量,找到当前旋转角度下的索引,最后计算旋转后的新坐标并返回。现在让我们停下来好好看看我们是怎么用Block来制作Tetromino的。比如,一个T形的 Tetromino在自己的局部坐标系里,我们约定这个原点是它的旋转中心。这样的话,给所有Tetromino设置合适的坐标就很简单了。然后,我们可以在设置文件里创建一个字典,键是Tetromino的形状,值是一个包含所有块坐标的列表,其中第一个元素是旋转中心。按照这个方法处理完所有的Tetromino,然后全部添加到这个字典里。俄罗斯方块的旋转主要是围绕某个旋转中心(也就是方块组的中心点)进行的。从数学的角度上来看,我们可以定义一系列的旋转矩阵TETROMINO_ROTATIONS来计算Tetromino旋转后的结果。这里我们看到的就是每个形状的Tetromino旋转不同角度的旋转矩阵。正不愧是一位科学家发明的游戏啊。block_destroy方法是用来处理方块消除。如果Block被消除,就播放动画,然后就彻底删除。check_collision是一个碰撞检测方法。检查一个坐标在下面情况是否发生碰撞,首先坐标在游戏范围内,其次小放款还在出生区或这个位置没有别的方块,则不算碰撞。
另外,我为了炫技,还在这游戏里放上了一个方块消除的特殊视觉效果。当一个方块需要消除的时候,首先会创建每个方块特效图片,它是原来图片的一个副本,并设置透明度为 200,这意味着它会变得半透明,带有一种淡出的效果。接着,为了让消除动画看起来更加自然,每个方块在消除时会像爆米花一样炸开,这个炸开的速度采用随机生成,这样每个方块在消失的过程中不会完全同步,而是有一点细微的错开,让动画更加流畅和真实。sfx_play这个方法就是执行真正的动画逻辑,首先它会把方块的图片 替换为特效图片,让它看起来像正在消失。然后它会稍微向上移动随机的像素点,让方块像是被消除后缓慢飘散,而不是瞬间消失。最后,用pg.transform.rotate让方块以某个角度进行旋转,旋转的角度随着时间的推移而增加。怎么样看起来是不是酷炫不少?
- Tetromino类
Tetromino 代表的是7种俄罗斯方块的形状。分别是I,O,T,L,J,S,Z形。
每个Tetromino由4个Block组成,而 Tetromino 类负责管理这些 Block 的 移动、旋转、碰撞检测等行为。首先初始化里,我们通过random模块随机选择一个 Tetromino 形状。用load_sprite_sheet() 载入所有方块的图片,让不同 Tetromino 选到对应的贴图。最后,返回该 Tetromino 的四个方块坐标。tetromino_rotate方法让Tetromino 旋转 90°,但如果旋转后发生碰撞,就撤销旋转。tetromino_fall方法Tetromino 向下移动。这里我们来着重介绍一下在配置文件中的MOVEMENTS 变量。我们用math数学库里的Vector2向量来简单地定义方块的移动方向。这里的vector向量对应了左移,右移,正常下落和加速下落四个方向。tetromino_move方法让玩家用方向键来控制Tetromino。方向键 左右控制方块左右移动、向上键控制旋转、向下键控制快速下落。不过,我们首先先要看看移动方向有没有碰撞,没有才允许移动。put_tetromino_blocks_in_array方法让Tetromino落地了之后把信息存到一个数组field_array里。而且,这个数组其实有双重功能——它不仅存储了这些方块对象,同时也用于碰撞检测。
- 主游戏
最后我们来看一下主游戏Game这一类。它是俄罗斯方块的核心,负责初始化游戏(包括窗口、音效、字体),控制游戏逻辑(方块下落、碰撞检测、分数计算),管理玩家输入,和游戏主循环main_loop()。首先,我们在初始化里使用load_resources 方法加载游戏所需的资源(像是字体、游戏界面、图像、声音),用setup 方法初始化游戏变量、创建游戏窗口、设置定时器等。
接下来是几个功能性的方法:
- 我们用draw_grid方法在屏幕上绘制网格线。draw_background方法绘制游戏背景,并更新下一个方块预览和 游戏界面。
- get_field_array 方法之前也说了,创建空的游戏2D数组网格,代表游戏区域。
- check_full_lines 方法用来从底部开始,向上遍历所有行。找到并清除方块形成完整的一行。
- update_fall_speed方法用来更新方块下落速度,这个游戏我们随着时间会越来越难,体现在越靠后难度越高,下落的速度就越快。cal_level 方法就是用来计算关卡难度,每消除一定的行数,就提高难度一次。
- cal_score 方法用来计算分数,并且触发方块消除的音效。
- is_game_over 方法作用是检测游戏是否结束。逻辑很简单:如果当前已经落下的Tetromino的第一个方块的 y 坐标等于初始位置,说明方块在出生位置就已经无法下落,游戏结束。
- 每次调用generate_tetromino 方法时,都会返回一个新的 Tetromino。
- spawn_tetromino 方法用来生成下一个Tetromino,并且用draw_next_tetromino 方法显示在 “下一个方块”的区域。
最后,我们来看一下游戏的主循环 main_loop,这个循环不断更新游戏的状态,并在每一帧绘制游戏画面。我们在里面控制帧率、处理游戏中的事件和玩家输入、绘制背景和图片、更新每一个Tetromino甚至是每个Block的状态、检查碰撞情况,最后是更新游戏窗口。游戏的核心逻辑都在这个main_loop主循环里,它确保了游戏的流畅运行。
在游戏主体部分,我们实例化了Game类的一个对象,负责管理游戏窗口、背景音乐、音效以及整个游戏的运行逻辑。最后,通过 game.main_loop(),游戏进入了主循环。这意味着从这一刻开始,游戏正式运行,所有的绘制、事件处理、碰撞检测等都会在这个循环里不断进行。
大功告成!现在我们就一起玩一下新鲜出炉的《俄罗斯方块》。(演示)好了,接下来,就由大家亲自体验这款游戏的魅力吧。你们在边学边玩的时候,遇到了什么问题或者BUG请给我留言。

Leave a comment