说起俄罗斯方块,记忆回到了童年时代,几个小伙伴在排队等待玩同学的俄罗斯方块游戏机,后来再也没玩过这个游戏了,本项目根据从前对俄罗斯方块的认知,复原出了一些俄罗斯方块应有的功能。
图1基于M5StickC plus 制作的罗斯方块游戏设计流程图
1、游戏基本原理
M5Core系列已有俄罗斯方块的游戏原型,但是功能简陋,没有计分、重新开始游戏、背景音乐、加速游戏等功能,但还是可以用来理解俄罗斯方块游戏的基本运行机理。
俄罗斯方块的基本元素有不同颜色4个小立方块组成的7种类型的游戏方块,个别类型方块只有1种形态,例如田字形方块,无论如何旋转都是一样的,其他方块最多有4种形态(如图2所示),这些信息是对俄罗斯方块最基本和必要的理解,如何在游戏中描述展示这些方块需要一个比较大的脑洞。在Arduino版本游戏中,由一群日本人移植的游戏原型,代码极其精简,没有注释,几乎以炫技形式呈现,极为张狂。它采用了一个结构体来描述了这7种类型的方块,通过位置偏移量类呈现方块的各种形态。比如,图3 所示的‘Z’形态方块的表示方式,其结构体数据为 {{-1,1},{0,1}, {0,0},{1, 0}} ,其他的方块也按照这种方式和规律进行排布,在游戏中我们通过外设对当前活动的方块进行控制,左右移动和翻转到需求的状态,另外,在即使玩家不操作的情况下方块也是会自主的下落。
图2俄罗斯方块7种类型方块
图3 所示的‘Z’形态方块的表示方式
看似简单的游戏其内部逻辑细节的也不是那么容易理解掌握,一个方块在没有任何阻挡的时候可以顺利到达底部,但一个方块的旅程途中可能遇到一些特殊的情况,在游戏中是怎么解决的。在理解游戏代码的过程发现该游戏的设计相当巧妙,环环相扣,有效地应对各种突发情况。限于篇幅不好把“一个方块的冒险旅程”的故事充分展开,有兴趣的朋友可以一起交流讨论,或者日后有机会再详细叙述。在本项目的实施过程第一步是将这个游戏适配到stickCplus上来,因为游戏原型的分辨率和stickCplus的不一样。我们需要修改游戏空间的尺寸,方块单元的尺寸,利用Arduino图形库TFT_eSPI的作图工具对游戏进行移植,该项目其实是深化图形库TFT_eSPI深入学习和应用,你对TFT_eSPI掌握程度,决定了游戏精彩程度。在活动期间我断断续续的对游戏20多版本的更新迭代,不断地完善游戏的功能,最终完成了自己记忆中俄罗斯应有的功能。在此只呈现游戏的最终版,其中的苦乐留给本人慢慢消化。
2、体感控制
按照项目要求需要用体感来控制游戏,stickCplus上有颗加速度传感器通过算法可以感知姿态。通过读取这颗加速度传感器的x,y,z变化数值,找到了左右倾斜的临界数值,并作为游戏左右移动的控制量。 由于采用加速度数值开控制会比较敏感,初期的游戏感受不那么好,很容易就过度触发,控制的精度比较难把握,除了加入了适当的延时,还在界面顶部画个小实心红圆作为操作提示。
//===============================================================
bool KeyPadLoop() {
if (accX > 0.4) {
ClearKeys();
but_LEFT = true;
M5.Lcd.fillCircle(20, 10, 9, TFT_RED);
delay(50);
return true;
}
if (accX < -0.4) {
ClearKeys();
but_RIGHT = true;
M5.Lcd.fillCircle(100, 10, 9, TFT_RED);
delay(50);
return true;
}
if ((accX > -0.4) & (accX < 0.3)) {
M5.Lcd.fillCircle(20, 10, 9, TFT_BLACK);
M5.Lcd.fillCircle(100, 10, 9, TFT_BLACK);
}
if (accY < -0.1) { //加速下降
gameSpeed = 1;
delay(50);
}
//===============================================================
3、方块预告功能
游戏原型是没有方块的提示功能的,跟我的记忆有悖。因此本游戏最大的亮点之一就是有下一方块的提示功能。在方块生成之际同时生成下一方块的状态,下一个方块出现时就承接上一次生成的状态,然后继续生成下一方块的参数。通过图层功能(sprite)在游戏区域左上角开辟了一个区域用来显示下一方块的图形,该区域凌驾于游戏区域。图层的好处是整体输出可以独立控制,用在这个环节上最好不过了。
//========================================================================
void NewBlockPos() {
gameDelay = 20;
gameSpeed = 10;
pos.X = 8;
pos.Y = 1;
block = nextblock;
rot = nextRotation;
nextblock = blocks[random(7)];
nextRotation = random(nextblock.Rotation);
drawSpr();
}
//========================================================================
void drawSpr() {
int bLength = 8;
spr.fillSprite(TFT_BLACK);
for (int i = 0; i < 4; ++i) {
int x = 1 + nextblock.square[nextRotation][i].X;
int y = 1 + nextblock.square[nextRotation][i].Y;
spr.fillRect(x * bLength, y * bLength, bLength, bLength,
colors[nextblock.color]);
spr.drawFastVLine(x * bLength, y * bLength, bLength, TFT_BLACK);
spr.drawFastHLine(x * bLength, y * bLength, bLength, TFT_BLACK);
}
spr.pushSprite(5, 24);
}
//========================================================================
4、计分和重启游戏功能
在消行函数里加入了一个成绩变量用来记录消行的成绩,并在屏幕顶部居中位置显示取得的成就。原来设置有随着消行的次数增多然后加快方块下降的时间间隔来加快游戏,想想我们的生活已经很卷了,还了愉快地玩耍比较重要,就取消了这个功能。重启游戏是在游戏大循环中加了对游戏是否开始的判断,进行游戏的重新初始化,这样就不像游戏原型那么憋足,游戏结束后需要重启机器才能再来一次,增加游戏的便利性。
/========================================================================
void DelLine() {
for (int j = 0; j < Height; ++j) {
bool Delete = true;
for (int i = 0; i < Width; ++i)
if (screen[i][j] == 0) Delete = false;
if (Delete) {
score++;
M5.Lcd.drawString("SCORE:" + String(score), 40, 8, 1);
for (int k = j; k >= 1; --k)
for (int i = 0; i < Width; ++i)
screen[i][k] = screen[i][k - 1]; // the upper block will
// drop down one step.
}
}
}
//========================================================================
if (gameover) {
if (M5.BtnA.wasPressed()) {
gameStart();
}
return;
}
void gameStart() {
for (int j = 0; j < Height; ++j)
for (int i = 0; i < Width; ++i) screen[i][j] = 0;
gameover = false;
score = 0;
gameDelay = 20;
level = 1;
NewBlockPos(); // Start Position
FlashBlockPlace();
M5.Lcd.drawString("SCORE:" + String(score), 40, 8, 1);
Draw();
}
//========================================================================
5、播放背景音乐
一个静默的游戏再精彩也好像缺少了些什么,因此加入背景音乐是游戏移植难度最高的一个环节。我们知道esp32是个双核的mcu可以用FreeRTos 实现多任务管理和分配工作核心。但是所谓“三心二意”办不好事,多任务处理不得当系统很容易崩溃,我知道你知道我为什么知道的。这里的工作的经验要点是把背景音乐相关的初始化工作都放在该核心上进行初始化。
xTaskCreatePinnedToCore(tetris_Music, "bg music", 1024 * 2, NULL, 1, NULL, 0);
//========================================================================
void tetris_Music(void * pvParameters)
{
const int music_pin = 32;
int freq = 50;
int ledChannel = 7;
int resolution = 10;
ledcSetup(ledChannel, freq, resolution);
ledcAttachPin(music_pin, ledChannel);
ledcWrite(ledChannel, 256); // 0°
int notes = sizeof(melody) / sizeof(melody[0]) / 2;
int wholenote = (60000 * 4) / tempo;
int divider = 0, noteDuration = 0;
while (1) {
for (int thisNote = 0; thisNote < notes * 2; thisNote = thisNote + 2)
{
divider = melody[thisNote + 1];
if (divider > 0)
{
noteDuration = (wholenote) / divider;
}
else if (divider < 0)
{
noteDuration = (wholenote) / abs(divider);
noteDuration *= 1;
}
ledcWriteTone(ledChannel, melody[thisNote]);
vTaskDelay(noteDuration);
}
}
}
//========================================================================
最后
本游戏基于对俄罗斯方块游戏运行方式的理解,掌握Arduino绘图库的高级绘图功能,应用esp32的FreeRTos实时操作系统完成了一项较为复杂的工程,是一个不可多得的Arduino应用锻炼项目。自然,学习永无止境,随着学习的不断深入,在将来可能获得更加优美的解决方案。
至此,该游戏的重大更新修改已完成,另外的一些游戏细节不便一一展开了,等待有缘人去慢慢去发现吧。