说起来真是很巧,写这个俄罗斯方块前一天晚上突然在B站看到一条视频,说是33行代码实现俄罗斯方块。然后脑海里想起了EasyX村长的话,大概是说一直写到能写出俄罗斯方块才能算勉强合格。
于是晚上睡之前心血来潮,心想,我也要写出来一个俄罗斯方块!
界面部分 | 大小/位置 |
---|---|
整体区域大小 | 600 × 540 600 \times 540 600×540(单位:像素) |
游戏区域大小 | 360 × 540 360 \times 540 360×540(单位:像素)、 10 × 15 10 \times 15 10×15(单位:方块) |
侧边区域大小 | 240 × 540 240 \times 540 240×540(单位:像素) |
侧边预览框大小 | 144 × 144 144 \times 144 144×144(单位:像素)、 4 × 4 4 \times 4 4×4(单位:方块) |
侧边预览框位置 | ( 360 + 48 , 36 ) (360 + 48, 36) (360+48,36) |
按键提示文字大小 | 30 30 30(单位:像素) |
按键提示文字位置 | ( 360 + 48 , 300 + ( i − 1 ) ∗ 30 ) , i = 1 , 2 , 3 , 4 (360 + 48, 300 + (i - 1) * 30), i=1,2,3,4 (360+48,300+(i−1)∗30),i=1,2,3,4 |
得分显示文字大小 | 30 30 30(单位:像素) |
得分显示文字位置 | ( 360 + 48 , 200 ) (360 + 48, 200) (360+48,200) |
方块元素大小 | 36 × 36 36 \times 36 36×36(单位:像素) |
注:绘图的位置都是指绘制时左上角的坐标,字体都是选用了“华文楷体”,颜色没什么就不用多说了
设计 | 大小(单位:像素) |
---|---|
占据部分 | 36 × 36 36 \times 36 36×36 |
内边距 | 2 2 2 |
边框圆角 | 6 6 6 |
实体大小 | 32 × 32 32 \times 32 32×32 |
用一个颜色(COLORREF
)数组存放所有的颜色,比如我这里就是:红橙黄绿青蓝紫
COLORREF colors[7] = { RGB(255, 0, 0), RGB(255, 165, 0), RGB(255, 255, 0), RGB(0, 128, 0), RGB(0, 255, 255), RGB(0, 0, 255), RGB(128, 0, 128) };
方块的颜色的绘制只需要用EasyX提供的floodfill
函数,以其边框线颜色(白色)为边界,进行填充即可。
单个方块的绘制函数代码如下:
void drawItem(int x, int y, COLORREF c)
{
// 方块设计
// 实际尺寸:32 * 32(单位:像素)
// 边框颜色:白色
// 圆角半径:4(单位:像素)
// 内部间距:2(单位:像素)
tmp_fill_color = getfillcolor();
const int r = 6;
const int p = 2;
int up_l_x = x + p + r;
int up_r_x = x + 36 - p - r;
int up___y = y + p;
int down_l_x = x + p + r;
int down_r_x = x + 36 - p - r;
int down___y = y + 36 - p;
int left_u_y = y + p + r;
int left_d_y = y + 36 - p - r;
int left___x = x + p;
int right_u_y = y + p + r;
int right_d_y = y + 36 - p - r;
int right___x = x + 36 - p;
line(up_l_x, up___y, up_r_x, up___y);
line(down_l_x, down___y, down_r_x, down___y);
line(left___x, left_u_y, left___x, left_d_y);
line(right___x, right_u_y, right___x, right_d_y);
arc(x + p, y + p, x + p + 2 * r, y + p + 2 * r, pi / 2, pi);
arc(x + 36 - p, y + p, x + 36 - p - 2 * r, y + p + 2 * r, 0, pi / 2);
arc(x + p, y + 36 - p, x + p + 2 * r, y + 36 - p - 2 * r, -pi, -pi / 2);
arc(x + 36 - p, y + 36 - p, x + 36 - p - 2 * r, y + 36 - p - 2 * r, -pi / 2, 0);
setfillcolor(c);
floodfill(x + p + r + 1, y + p + r + 1, WHITE);
setfillcolor(tmp_fill_color);
}
俄罗斯方块类型大致分为7类,如下表所列举:
俄罗斯方块类型 | 实际形状个数 |
---|---|
山型 | 4 4 4 |
L型 | 4 4 4 |
反L型 | 4 4 4 |
Z型 | 2 2 2 |
反Z型 | 2 2 2 |
I型 | 2 2 2 |
田型 | 1 1 1 |
由此可见,一个俄罗斯方块应该具有特征【形状】,一个俄罗斯方块最多有4个形状,每个形状都需要描述4个点,每个点需要用2个坐标值来描述坐标,因此我们需要一个 4 × 4 × 2 4 \times 4 \times 2 4×4×2的数组来储存俄罗斯方块的形状,得到俄罗斯方块的结构体如下:
struct Square
{
int dir[4][4][2];
};
结构体定义好了,我们就需要根据7种俄罗斯方块的结构来实例化7个Square
,我的实例化方式如下:
Square squares[7] = {
{ 0, -2, 0, -1, 0, 0, 1, 0, 0, -1, 1, -1, 2, -1, 0, 0, 0, -2, 1, -2, 1, -1, 1, 0, 0, 0, 1, 0, 2, 0, 2, -1 }, // L 型
{ 0, 0, 1, 0, 1, -1, 1, -2, 0, -1, 0, 0, 1, 0, 2, 0, 0, -2, 0, -1, 0, 0, 1, -2, 0, -1, 1, -1, 2, -1, 2, 0 }, // L 型(反)
{ 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0 }, // 田 型
{ 0, 0, 1, -1, 1, 0, 2, 0, 1, -2, 1, -1, 1, 0, 2, -1, 0, -1, 1, -1, 1, 0, 2, -1, 0, -1, 1, -2, 1, -1, 1, 0 }, // 山 型
{ 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3, 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3 }, // | 型
{ 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1, 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1 }, // Z 型
{ 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0, 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0 } // Z 型(反)
};
Square
结构体里面,坐标(地图中)的定义自然都需要相对于同一个地图点而言。
这里我单独拉出来一个图形来说,就比如squares[0].dir[0]
的四个坐标 ( 0 , − 2 ) (0, -2) (0,−2)、 ( 0 , − 1 ) (0, -1) (0,−1)、 ( 0 , 0 ) (0, 0) (0,0)、 ( 1 , 0 ) (1, 0) (1,0),这些坐标的值代表了相对于当前位置的偏移量。
假设当前位置的地图点坐标为 ( 0 , 0 ) (0, 0) (0,0),那么我们根据上述偏移量可以绘制出以下图形:
类似地,我们根据上面定义的7种形状squares
,同样假设当前位置的地图点坐标为 ( 0 , 0 ) (0, 0) (0,0),整理出来就可以得到如下图所示的所有图形(省略了重复的):
到此,俄罗斯方块的所有形状就设计完成了。
在如上所述的设计中,一个俄罗斯方块的全部属性仅仅由以下三者共同唯一决定:
s_idx
d_idx
c_idx
以上三者确定后,可以确定俄罗斯方块的四个关键绘制点在squares[s_idx].dir[d_idx]
中,其颜色为colors[c_idx]
.
因此,如果要随机生成一个方块,只需要令其 { s _ i d x = r a n d ( ) % 7 d _ i d x = r a n d ( ) % 4 c _ i d x = r a n d ( ) % 7 \begin{cases} s\_idx = rand() \% 7 \\ d\_idx = rand() \%4 \\ c\_idx = rand() \% 7 \end{cases} ⎩⎪⎨⎪⎧s_idx=rand()%7d_idx=rand()%4c_idx=rand()%7
要确定当前俄罗斯方块和下一个俄罗斯方块,我们需要下面6个变量 { n o w _ s _ i d x 、 n o w _ d _ i d x 、 n o w _ c _ i d x n e x t _ s _ i d x 、 n e x t _ d _ i d x 、 n e x t _ c _ i d x \begin{cases} now\_s\_idx、now\_d\_idx、now\_c\_idx \\ next\_s\_idx、next\_d\_idx、next\_c\_idx \end{cases} {now_s_idx、now_d_idx、now_c_idxnext_s_idx、next_d_idx、next_c_idx
当需要切换到下一个俄罗斯方块时,将下一个俄罗斯方块的全部索引赋值给当前俄罗斯方块,然后重新随机生成下一个俄罗斯方块。
局部代码如下:
now_c_idx = next_c_idx;
now_s_idx = next_s_idx;
now_d_idx = next_d_idx;
next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;
由于迭代过程中,当前俄罗斯方块是直接从把下一个俄罗斯方块拿来用,因此初始时,只需要初始化下一个俄罗斯方块即可。
局部代码如下:
next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;
当前俄罗斯方块的地图位置需要设置两个变量now_mp_x
、now_mp_y
来确认,然后只要根据当前俄罗斯方块的dir
属性,就可以知道当前俄罗斯方块的4个方块的地图位置,从而绘制出来。
(在上方超出地图外的部分有时候需要稍微处理一下,因为可能会有下标越界的可能;绘制的时候可以不用管,因为绘制超出了窗口范围也不会报错)
用一个二维的 10 × 15 10 \times 15 10×15的int
数组mp
来存储地图中的俄罗斯方块、COLORREF
的数组mp_c
来存储地图中俄罗斯方块的颜色。
由于当前俄罗斯方块需要频繁地移动,因此不宜把我们看到的场面都存入mp
,只有当确定俄罗斯方块已经“沉底”、不能移动了,我们才将它的数据存入地图mp
中。因此,正在移动的当前俄罗斯方块是不会被记入地图mp
中的
用了enum
枚举类型枚举了几种控制指令,如下所示:
enum Cmd
{
Cmd_rotate, // 方块旋转
Cmd_left, // 方块左移
Cmd_right, // 方块右移
Cmd_down // 方块下移
};
通过一个函数接收按键消息,实现获取指令的功能。
为了不让俄罗斯因为输入了移动指令而一直卡在某一层,我们需要增加一个计时器,每隔一段时间就自动执行下降指令。
接受指令的函数如下:
Cmd getCmd()
{
while (true)
{
// 指令超时
DWORD time_tmp = GetTickCount();
if (time_tmp - time_now >= 1000)
{
time_now = time_tmp;
return Cmd_down;
}
// 接受指令
if (_kbhit())
{
switch (_getch())
{
case 72:
return Cmd_rotate;
case 75:
return Cmd_left;
case 77:
return Cmd_right;
case 80:
return Cmd_down;
}
}
// 降低CPU占用
Sleep(20);
}
}
当触发了旋转指令以后,我们需要依次尝试该俄罗斯方块的其他三种形状,如果出现了以下情况之一,就能说明这样旋转后的放置是不合理的
mp
上已经存在方块据此,我们写出了以下校检函数,用于检查当前俄罗斯方块根据属性dir
的形状索引dir_idx
在地图坐标 ( m p _ x , m p _ y ) (mp\_x, mp\_y) (mp_x,mp_y)位置是否可以被成功放置:
bool checkPut(int mp_x, int mp_y, int dir_idx)
{
int sq_x[4];
int sq_y[4];
for (int i = 0; i < 4; ++i)
{
sq_x[i] = mp_x + squares[now_s_idx].dir[dir_idx][i][0];
sq_y[i] = mp_y + squares[now_s_idx].dir[dir_idx][i][1];
}
// 【左右越界、下方越界、重复占格】
for (int i = 0; i < 4; ++i)
{
if (sq_x[i] < 0 || sq_x[i] > 9 || sq_y[i] > 14)
return false;
if (sq_y[i] < 0) // 检查坐标合法性
continue;
if (mp[sq_x[i]][sq_y[i]])
return false;
}
return true;
}
如果某一种旋转放置不合理,那么我们尝试下一种旋转。
如果存在某种旋转放置合理,那么我们更新当前俄罗斯方块的形状索引now_d_idx
;如果所有旋转放置都不合理,那么说明不能旋转,什么都不用去改变。
(需要注意的地方是,由于每一种俄罗斯方块的形状索引只能为 0 ∼ 3 0 \sim 3 0∼3,为了避免下标越界这种尴尬的情况出现,一定要将形状索引对4取余)
旋转指令执行函数代码如下:
// 尝试剩余所有形状,可以旋转即调整旋转状态
for (int i = 1; i <= 3; ++i)
if (checkPut(now_mp_x, now_mp_y, (now_d_idx + i) % 4))
{
now_d_idx = (now_d_idx + i) % 4;
break;
}
和旋转指令一样,只要尝试看看左/右移后的方块能否被成功放置,然后再决定是否执行即可。
左/右移指令执行函数代码如下:
void moveLeft()
{
// 尝试能否左移
if (checkPut(now_mp_x - 1, now_mp_y, now_d_idx))
--now_mp_x;
}
void moveRight()
{
// 尝试能否右移
if (checkPut(now_mp_x + 1, now_mp_y, now_d_idx))
++now_mp_x;
}
和上面稍微有点不同,因为不能下移说明方块“沉底”了,因此也就有了下面几种需求:
mp
中提示可以开始下一个方块我们可以用一个变量flag_next
来标识。当前俄罗斯方块还能移动时,保持值为0(也是每一小轮的初始值);当前俄罗斯方块不能再移动了,设置值为1,等待开启下一小轮。
将当前俄罗斯方块记录也很简单,只要根据当前位置的地图坐标 ( n o w _ m p _ x , n o w _ m p _ y ) (now\_mp\_x, now\_mp\_y) (now_mp_x,now_mp_y)以及当前俄罗斯方块的三个索引,将mp
中对应位置标记为1、mp_c
中对应位置颜色标记为对应颜色即可。
代码如下:
void recordSquareNow()
{
int sq_x[4];
int sq_y[4];
for (int i = 0; i < 4; ++i)
{
sq_x[i] = now_mp_x + squares[now_s_idx].dir[now_d_idx][i][0];
sq_y[i] = now_mp_y + squares[now_s_idx].dir[now_d_idx][i][1];
}
for (int i = 0; i < 4; ++i)
if (sq_y[i] >= 0)
{
mp[sq_x[i]][sq_y[i]] = 1;
mp_c[sq_x[i]][sq_y[i]] = colors[now_c_idx];
}
}
进行消行也不难实现,只要将地图mp
中 x x x方向上值全部为1的某一层删除即可。我觉得反正都要遍历,不如遍历的时候把符合条件的复制到临时数组里,然后再从临时数组拷贝过来就行了。
代码如下:
void execClear()
{
memset(mp_tmp, 0, sizeof(mp_tmp));
memset(mp_c_tmp, 0, sizeof(mp_c_tmp));
int cnt_j = 14;
for (int j = 14; j >= 0; --j)
{
int cnt = 0;
for (int i = 0; i < 10; ++i)
if (mp[i][j])
++cnt;
if (cnt != 10)
{
for (int i = 0; i < 10; ++i)
{
mp_tmp[i][cnt_j] = mp[i][j];
mp_c_tmp[i][cnt_j] = mp_c[i][j];
}
--cnt_j;
}
else
++score;
}
for (int j = 0; j < 15; ++j)
for (int i = 0; i < 10; ++i)
{
mp[i][j] = mp_tmp[i][j];
mp_c[i][j] = mp_c_tmp[i][j];
}
}
然后就是判断游戏是否结束,这个就很简单了,只要看看地图mp
中的最上层是否存在值为1的某一块即可。我们也需要设置一个变量flag_over
来标识游戏是否结束。游戏没有结束时,保持值为0(也是每一大局的初始值);游戏已经结束时,设置值为1,询问是否再来亿局。
代码如下:
bool checkOver()
{
for (int i = 0; i < 10; ++i)
if (mp[i][0])
return true;
return false;
}
当上述函数实现后,我们就可以完整地执行下移了
下移移指令执行函数代码如下:
void moveDown()
{
// 尝试能否下移
if (checkPut(now_mp_x, now_mp_y + 1, now_d_idx))
{
++now_mp_y;
return;
}
// 不能下移则说明这块方块“触礁”了,执行以下操作
// 1、提示可以开始下一个方块
// 2、将方块记录到map地图中
// 3、判断是否可以消行
// 4、判断消行后游戏是否结束
flag_next = 1;
recordSquareNow();
execClear();
if (checkOver())
flag_over = 1;
}
因为我不是很喜欢很单调的颜色,所以用了线性渐变的方法,不停画线,做出了线性渐变的背景样式。
局部代码:
for (int i = 0; i < 541; ++i)
{
setlinecolor(RGB(135, 206, 250 - i / 5));
line(0, 540 - i, 360, 540 - i);
}
for (int i = 0; i < 541; ++i)
{
setlinecolor(RGB(224, 178, 220 - i / 15));
line(361, i, 600, i);
}
就是一个简单的线性渐变
void drawGameBG()
{
// 划分区域(游戏区域、计分区域)
// 方块尺寸——36 * 36(单位:像素)
// 游戏尺寸——10 * 15(单位:方块)
// 下一个方块显示区域——4 * 4(单位:方块)
COLORREF tmp = getlinecolor();
for (int i = 0; i < 541; ++i)
{
setlinecolor(RGB(135, 206, 250 - i / 5));
line(0, 540 - i, 360, 540 - i);
}
setlinecolor(tmp);
}
然后需要根据地图数组mp
绘制已经“尘埃落定”的方块:
void drawMap()
{
for (int i = 0; i < 10; ++i)
for (int j = 0; j < 15; ++j)
if (mp[i][j])
drawItem(i * 36, j * 36, mp_c[i][j]);
}
void drawSquareNow()
{
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][0][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][0][1]) * 36, colors[now_c_idx]);
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][1][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][1][1]) * 36, colors[now_c_idx]);
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][2][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][2][1]) * 36, colors[now_c_idx]);
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][3][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][3][1]) * 36, colors[now_c_idx]);
}
主要是还需要将预览效果弄上去,这里封装了一个drawSquareNext
函数来实现,在drawSide
函数里被调用。
void drawSquareNext()
{
int tmp_x = 360 + 48;
int tmp_y = 36 + 108;
COLORREF c = colors[next_c_idx];
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][0][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][0][1] * 36, c);
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][1][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][1][1] * 36, c);
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][2][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][2][1] * 36, c);
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][3][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][3][1] * 36, c);
}
void drawSide()
{
tmp_line_color = getlinecolor();
getlinestyle(&tmp_line_style);
tmp_text_color = getlinecolor();
gettextstyle(&tmp_text_style);
for (int i = 0; i < 541; ++i)
{
setlinecolor(RGB(224, 178, 220 - i / 15));
line(361, i, 600, i);
}
settextcolor(RGB(65, 105, 225));
settextstyle(30, 0, L"华文楷体");
outtextxy(360 + 48, 300, L"左移:←");
outtextxy(360 + 48, 330, L"右移:→");
outtextxy(360 + 48, 360, L"变形:↑");
outtextxy(360 + 48, 390, L"下落:↓");
setlinecolor(WHITE);
rectangle(360 + 48, 36, 600 - 48, 36 + 144);
drawSquareNext();
setlinecolor(0x7FFFAA);
rectangle(360 + 48, 36, 600 - 48, 36 + 144);
swprintf(score_tips, 29, L"得分:%d", score * 100);
outtextxy(360 + 48, 200, score_tips);
setlinecolor(tmp_line_color);
setlinestyle(&tmp_line_style);
settextcolor(tmp_text_color);
settextstyle(&tmp_text_style);
}
主要包括
void initEnvironment()
{
// 窗口设置
initgraph(600, 540);
HWND hwnd = GetHWnd();
SetWindowText(hwnd, L"俄罗斯方块");
SetWindowPos(hwnd, HWND_TOP, 700, 20, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW);
// 绘图模式设置
setbkmode(TRANSPARENT);
// 随机数种子
srand(time(NULL));
}
主要包括
mp
与其对应颜色mp_c
void initDatasPerRound()
{
memset(mp, 0, sizeof(mp));
memset(mp, 0, sizeof(mp_c));
time_now = GetTickCount();
next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;
flag_over = 0;
score = 0;
}
主要包括
void initDatasPerSquare()
{
now_mp_x = 5;
now_mp_y = -1;
flag_next = 0;
now_c_idx = next_c_idx;
now_s_idx = next_s_idx;
now_d_idx = next_d_idx;
next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;
}
int main()
{
initEnvironment();
// 开始游戏
while (true)
{
initDatasPerRound();
while (!flag_over)
{
initDatasPerSquare();
while (!flag_next)
{
BeginBatchDraw();
drawGameBG();
drawSide();
//drawLines();
drawMap();
drawSquareNow();
EndBatchDraw();
Cmd cmd = getCmd();
execCmd(cmd);
}
}
// 一局结束后的统计
swprintf(over_tips, 39, L"游戏结束\n你的最终得分:%d\n是否再来亿局?", score * 100);
if (MessageBox(GetHWnd(), over_tips, L"再来亿局?", MB_ICONQUESTION | MB_YESNO) == IDNO)
break;
}
return 0;
}
由于绘图循环比较多,一开始频繁地直接绘制导致游戏过程中老是闪屏,后来,在EasyX村长和村民的友情提醒下,总算是成功优化了。
只需要在绘图前使用BeginBatchDraw
这个函数标识一下,在绘图完成时再用EndBatchDraw
这个函数标识一下,就可以将其中的所有绘图内容批量绘制,这样可以有效避免闪屏问题。
整个程序的代码如下(没有任何资源文件):
#include
#include
#include
#include
#include
struct Square
{
int dir[4][4][2];
};
enum Cmd
{
Cmd_rotate, // 方块旋转
Cmd_left, // 方块左移
Cmd_right, // 方块右移
Cmd_down // 方块下移
};
Square squares[7] = {
{ 0, -2, 0, -1, 0, 0, 1, 0, 0, -1, 1, -1, 2, -1, 0, 0, 0, -2, 1, -2, 1, -1, 1, 0, 0, 0, 1, 0, 2, 0, 2, -1 }, // L 型
{ 0, 0, 1, 0, 1, -1, 1, -2, 0, -1, 0, 0, 1, 0, 2, 0, 0, -2, 0, -1, 0, 0, 1, -2, 0, -1, 1, -1, 2, -1, 2, 0 }, // L 型(反)
{ 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0 }, // 田 型
{ 0, 0, 1, -1, 1, 0, 2, 0, 1, -2, 1, -1, 1, 0, 2, -1, 0, -1, 1, -1, 1, 0, 2, -1, 0, -1, 1, -2, 1, -1, 1, 0 }, // 山 型
{ 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3, 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3 }, // | 型
{ 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1, 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1 }, // Z 型
{ 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0, 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0 } // Z 型(反)
};
const double pi = acos(-1);
COLORREF tmp_line_color;
LINESTYLE tmp_line_style;
COLORREF tmp_text_color;
LOGFONT tmp_text_style;
COLORREF tmp_fill_color;
COLORREF colors[7] = { RGB(255, 0, 0), RGB(255, 165, 0), RGB(255, 255, 0), RGB(0, 128, 0), RGB(0, 255, 255), RGB(0, 0, 255), RGB(128, 0, 128) };
int mp[10][15];
int mp_tmp[10][15];
COLORREF mp_c[10][15];
COLORREF mp_c_tmp[10][15];
wchar_t score_tips[30];
wchar_t over_tips[50];
DWORD time_now;
int flag_next;
int flag_over;
int now_c_idx;
int now_s_idx;
int now_d_idx;
int next_c_idx;
int next_s_idx;
int next_d_idx;
int now_mp_x;
int now_mp_y;
int score;
void initEnvironment();
void initDatasPerRound();
void initDatasPerSquare();
void drawGameBG();
void drawSide();
//void drawLines();
void drawItem(int x, int y, COLORREF c);
void drawSquareNow();
void drawSquareNext();
void drawMap();
Cmd getCmd();
bool checkPut(int mp_x, int mp_y, int dir_idx);
void execClear();
bool checkOver();
void execCmd(Cmd cmd);
void moveRotate();
void moveLeft();
void moveRight();
void moveDown();
void recordSquareNow();
int main()
{
initEnvironment();
// 开始游戏
while (true)
{
initDatasPerRound();
while (!flag_over)
{
initDatasPerSquare();
while (!flag_next)
{
BeginBatchDraw();
drawGameBG();
drawSide();
//drawLines();
drawMap();
drawSquareNow();
EndBatchDraw();
Cmd cmd = getCmd();
execCmd(cmd);
}
}
// 一局结束后的统计
swprintf(over_tips, 39, L"游戏结束\n你的最终得分:%d\n是否再来亿局?", score * 100);
if (MessageBox(GetHWnd(), over_tips, L"再来亿局?", MB_ICONQUESTION | MB_YESNO) == IDNO)
break;
}
return 0;
}
void initEnvironment()
{
// 窗口设置
initgraph(600, 540);
HWND hwnd = GetHWnd();
SetWindowText(hwnd, L"俄罗斯方块");
SetWindowPos(hwnd, HWND_TOP, 700, 20, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW);
// 绘图模式设置
setbkmode(TRANSPARENT);
// 随机数种子
srand(time(NULL));
}
void initDatasPerRound()
{
memset(mp, 0, sizeof(mp));
memset(mp, 0, sizeof(mp_c));
time_now = GetTickCount();
next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;
flag_over = 0;
score = 0;
}
void initDatasPerSquare()
{
now_mp_x = 5;
now_mp_y = -1;
flag_next = 0;
now_c_idx = next_c_idx;
now_s_idx = next_s_idx;
now_d_idx = next_d_idx;
next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;
}
void drawGameBG()
{
// 划分区域(游戏区域、计分区域)
// 方块尺寸——36 * 36(单位:像素)
// 游戏尺寸——10 * 15(单位:方块)
// 下一个方块显示区域——4 * 4(单位:方块)
COLORREF tmp = getlinecolor();
for (int i = 0; i < 541; ++i)
{
setlinecolor(RGB(135, 206, 250 - i / 5));
line(0, 540 - i, 360, 540 - i);
}
setlinecolor(tmp);
}
void drawSide()
{
tmp_line_color = getlinecolor();
getlinestyle(&tmp_line_style);
tmp_text_color = getlinecolor();
gettextstyle(&tmp_text_style);
for (int i = 0; i < 541; ++i)
{
setlinecolor(RGB(224, 178, 220 - i / 15));
line(361, i, 600, i);
}
settextcolor(RGB(65, 105, 225));
settextstyle(30, 0, L"华文楷体");
outtextxy(360 + 48, 300, L"左移:←");
outtextxy(360 + 48, 330, L"右移:→");
outtextxy(360 + 48, 360, L"变形:↑");
outtextxy(360 + 48, 390, L"下落:↓");
setlinecolor(WHITE);
rectangle(360 + 48, 36, 600 - 48, 36 + 144);
drawSquareNext();
setlinecolor(0x7FFFAA);
rectangle(360 + 48, 36, 600 - 48, 36 + 144);
swprintf(score_tips, 29, L"得分:%d", score * 100);
outtextxy(360 + 48, 200, score_tips);
setlinecolor(tmp_line_color);
setlinestyle(&tmp_line_style);
settextcolor(tmp_text_color);
settextstyle(&tmp_text_style);
}
//void drawLines()
//{
// for (int i = 0; i <= 10; ++i)
// line(36 * i, 0, 36 * i, 540);
// for (int j = 0; j <= 15; ++j)
// line(0, j * 36, 360, j * 36);
//
// for (int i = 1; i < 4; ++i)
// line(360 + 48 + i * 36, 36, 360 + 48 + i * 36, 36 + 144);
// for (int j = 1; j < 4; ++j)
// line(360 + 48, 36 + j * 36, 600 - 48, 36 + j * 36);
//}
void drawItem(int x, int y, COLORREF c)
{
// 方块设计
// 实际尺寸:32 * 32(单位:像素)
// 边框颜色:白色
// 圆角半径:4(单位:像素)
// 内部间距:2(单位:像素)
tmp_fill_color = getfillcolor();
const int r = 6;
const int p = 2;
int up_l_x = x + p + r;
int up_r_x = x + 36 - p - r;
int up___y = y + p;
int down_l_x = x + p + r;
int down_r_x = x + 36 - p - r;
int down___y = y + 36 - p;
int left_u_y = y + p + r;
int left_d_y = y + 36 - p - r;
int left___x = x + p;
int right_u_y = y + p + r;
int right_d_y = y + 36 - p - r;
int right___x = x + 36 - p;
line(up_l_x, up___y, up_r_x, up___y);
line(down_l_x, down___y, down_r_x, down___y);
line(left___x, left_u_y, left___x, left_d_y);
line(right___x, right_u_y, right___x, right_d_y);
arc(x + p, y + p, x + p + 2 * r, y + p + 2 * r, pi / 2, pi);
arc(x + 36 - p, y + p, x + 36 - p - 2 * r, y + p + 2 * r, 0, pi / 2);
arc(x + p, y + 36 - p, x + p + 2 * r, y + 36 - p - 2 * r, -pi, -pi / 2);
arc(x + 36 - p, y + 36 - p, x + 36 - p - 2 * r, y + 36 - p - 2 * r, -pi / 2, 0);
setfillcolor(c);
floodfill(x + p + r + 1, y + p + r + 1, WHITE);
setfillcolor(tmp_fill_color);
}
void drawSquareNow()
{
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][0][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][0][1]) * 36, colors[now_c_idx]);
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][1][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][1][1]) * 36, colors[now_c_idx]);
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][2][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][2][1]) * 36, colors[now_c_idx]);
drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][3][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][3][1]) * 36, colors[now_c_idx]);
}
void drawSquareNext()
{
int tmp_x = 360 + 48;
int tmp_y = 36 + 108;
COLORREF c = colors[next_c_idx];
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][0][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][0][1] * 36, c);
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][1][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][1][1] * 36, c);
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][2][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][2][1] * 36, c);
drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][3][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][3][1] * 36, c);
}
void drawMap()
{
for (int i = 0; i < 10; ++i)
for (int j = 0; j < 15; ++j)
if (mp[i][j])
drawItem(i * 36, j * 36, mp_c[i][j]);
}
Cmd getCmd()
{
while (true)
{
// 指令超时
DWORD time_tmp = GetTickCount();
if (time_tmp - time_now >= 1000)
{
time_now = time_tmp;
return Cmd_down;
}
// 接受指令
if (_kbhit())
{
switch (_getch())
{
case 72:
return Cmd_rotate;
case 75:
return Cmd_left;
case 77:
return Cmd_right;
case 80:
return Cmd_down;
}
}
// 降低CPU占用
Sleep(20);
}
}
void execCmd(Cmd cmd)
{
switch (cmd)
{
case Cmd_down:
moveDown();
break;
case Cmd_left:
moveLeft();
break;
case Cmd_right:
moveRight();
break;
case Cmd_rotate:
moveRotate();
break;
}
}
bool checkPut(int mp_x, int mp_y, int dir_idx)
{
int sq_x[4];
int sq_y[4];
for (int i = 0; i < 4; ++i)
{
sq_x[i] = mp_x + squares[now_s_idx].dir[dir_idx][i][0];
sq_y[i] = mp_y + squares[now_s_idx].dir[dir_idx][i][1];
}
// 【左右越界、下方越界、重复占格】
for (int i = 0; i < 4; ++i)
{
if (sq_x[i] < 0 || sq_x[i] > 9 || sq_y[i] > 14)
return false;
if (sq_y[i] < 0) // 检查坐标合法性
continue;
if (mp[sq_x[i]][sq_y[i]])
return false;
}
return true;
}
void execClear()
{
memset(mp_tmp, 0, sizeof(mp_tmp));
memset(mp_c_tmp, 0, sizeof(mp_c_tmp));
int cnt_j = 14;
for (int j = 14; j >= 0; --j)
{
int cnt = 0;
for (int i = 0; i < 10; ++i)
if (mp[i][j])
++cnt;
if (cnt != 10)
{
for (int i = 0; i < 10; ++i)
{
mp_tmp[i][cnt_j] = mp[i][j];
mp_c_tmp[i][cnt_j] = mp_c[i][j];
}
--cnt_j;
}
else
++score;
}
for (int j = 0; j < 15; ++j)
for (int i = 0; i < 10; ++i)
{
mp[i][j] = mp_tmp[i][j];
mp_c[i][j] = mp_c_tmp[i][j];
}
}
bool checkOver()
{
for (int i = 0; i < 10; ++i)
if (mp[i][0])
return true;
return false;
}
void moveRotate()
{
// 尝试剩余所有形状,可以旋转即调整旋转状态
for (int i = 1; i <= 3; ++i)
if (checkPut(now_mp_x, now_mp_y, (now_d_idx + i) % 4))
{
now_d_idx = (now_d_idx + i) % 4;
break;
}
}
void moveLeft()
{
// 尝试能否左移
if (checkPut(now_mp_x - 1, now_mp_y, now_d_idx))
--now_mp_x;
}
void moveRight()
{
// 尝试能否右移
if (checkPut(now_mp_x + 1, now_mp_y, now_d_idx))
++now_mp_x;
}
void moveDown()
{
// 尝试能否下移
if (checkPut(now_mp_x, now_mp_y + 1, now_d_idx))
{
++now_mp_y;
return;
}
// 不能下移则说明这块方块“触礁”了,执行以下操作
// 1、提示可以开始下一个方块
// 2、将方块记录到map地图中
// 3、判断是否可以消行
// 4、判断消行后游戏是否结束
flag_next = 1;
recordSquareNow();
execClear();
if (checkOver())
flag_over = 1;
}
void recordSquareNow()
{
int sq_x[4];
int sq_y[4];
for (int i = 0; i < 4; ++i)
{
sq_x[i] = now_mp_x + squares[now_s_idx].dir[now_d_idx][i][0];
sq_y[i] = now_mp_y + squares[now_s_idx].dir[now_d_idx][i][1];
}
for (int i = 0; i < 4; ++i)
if (sq_y[i] >= 0)
{
mp[sq_x[i]][sq_y[i]] = 1;
mp_c[sq_x[i]][sq_y[i]] = colors[now_c_idx];
}
}
做这个的前一天,我一直在想,应该挺简单的吧。一早上一个半小时基本都在考虑绘图方面的内容。
到了下午,真正写逻辑的时候,我开始慌了,问题还是比较多的,比如“7种俄罗斯方块的不同形状要怎么弄啊”、“接受按键命令的同时就算不操作也要执行下移要怎么实现”、“移动命令要怎么实现”……
后来看这看那,现学现卖,一整天花了9个多小时,总算是写完了(如此看来我还是比较菜的吧23333 )
不过也确实让我颇感实践项目经验的重要性。人家熟练的人可以用5分钟、以33行的代码成功实现,而我却花了整整9小时、写了460行代码才实现出来(虽然可视化的效果还是可以比的23333 )。
如此看来,小的项目经验也是十分重要的,只有多写多实践,以后才能得心应手。
PS:本博客的编写总计耗时5小时50分钟,之所以写的这么详细是因为昨天在写这个的时候到CSDN上想找一些点子,但是很无奈找不着一些很细化的思路,因此也花了大把精力整理了这么一篇博客,也算是一次小结吧。