张银峰的编程课堂

描述程序对象

设计程序时,选择合理的数据类型也是重要的一环,这里我们以俄罗斯方块游戏为起点,探索数据的表达。

说到俄罗斯方块,我想你脑海中肯定已经冒出了形形色色的形状,甚至低估着:嗯,我讨厌那个N状方块,它经常让我无所适从。回到我们的讨论,现在的首要问题是这些方块在程序中该如何表示?快速的回想一遍所知的形状,你会发现最宽的是横条,它有4个小方块的宽度;最高的是竖条,它有4个小方块的高度。而L形状虽然也由4个小方块构成,但是仅占据了2列宽度。

显然,如果我们选择最宽与最高的维度,构建成一个网格,此时任意一个形状都可以容纳在其中。对于当前的方块而言,所在的小方格是可见的,其它的方格是隐藏的,如果我们用1与0来表示这两种状态,可以得到下面这样的图例。

glimix.com

第一眼看去,对于这样4x4的格子,我们可以使用一个二维数组来表达,将形状所在的区域置位1即可,我们先排除掉浮点类型,因为它们的相等性比较不直接。

/*
    0 ■ 0 0
    0 ■ ■ 0
    0 0 ■ 0
    0 0 0 0
*/
int shape[4][4] =
{
   0, 1, 0, 0,
   0, 1, 1, 0,
   0, 0, 1, 0,
   0, 0, 0, 0
}

二维数组意味着可能需要双层循环才能遍历出,也许你喜欢一维数组的易用性,也可能这样表达。

/*
    ■ ■ ■ ■
    0 0 0 0
    0 0 0 0
    0 0 0 0
*/
int shape[16] = { 1, 1, 1, 1, 0 };

现在从内存花费角度考虑:使用int数组表达一个形状将占用64个字节,而使用char数组仅需16个字节,这一举使我们节省了75%的内存消耗!

#include <stdio.h>

int main()
{
    /*
        ■ 0 0 0
        ■ 0 0 0
        ■ ■ 0 0
        0 0 0 0
    */
    char shape[16] =
    {
       1, 0, 0, 0,
       1, 0, 0, 0,
       1, 1
    };

    printf("\n\n");

    for (char i = 0; i < 16; i++)
    {
        if (i % 4 == 0)
            putchar('\t');

        if (shape[i])
            printf("■");

        if ((i + 1) % 4 == 0)
            putchar('\n');
    }

    return 0;
}

glimix.com

我们还可以在内存消耗方面进行优化,方法是通过bit位来存储方块的信息,这样一个形状仅需2个字节就能表达(我们将这16位每4位看作一行,就形成了4x4网格),此时对于形状L分解如下:

        | BYTE-0      BYTE-1
--------|------------------------
  BIN   | 10001000    11000000
  DEC   | 136         192
  HEX   | 88          C0
---------------------------------

用代码可以这样表示:

char shapeL[2] = { 0x88, 0xC0 };

当你尝试遍历shapeL时,你会发现有点麻烦,因为数据被分散到两个离散量中,也许short是更加合适的选择。

short shapeL = 0x88C0;

目前我们只是站在具体形状的角度描述程序如何表达所需的对象。站在设计的角度看,方块还需要一个颜色的表达,这样程序会更美观;站在策划的角度看,还需要记录每种方块出现的次数,以便在玩家急需出现竖条时,而保证它久久不会出现。如你所见,不同的层次对所需要的表达不尽相同,现在我们则需要一个表达形状的抽象概念,这可以由结构体实现。如:

struct shape
{
    short box;
    COLOR color;
    int   count; // 已出现次数
};

示例程序并没有表达所有可能的属性,仅是附加了色彩表示。在显示一个方块前,先使用system()函数清除上次屏幕输出,然后绘图,接下来等待你按下任意键以输出下一个形状,直到循环结束。

```C
#include <stdio.h>
#include <stdlib.h>

#define MAX_SHAPES  6

// 色彩枚举
typedef enum COLOR
{
    BLUE   = 31,
    GREEN  = 175,
    RED    = 203,
    YELLOW = 224,
    PURPLE = 223,
    CYAN   = 178,
    BROWN  = 100,
} COLOR;

// 方块形状抽象
typedef struct SHAPE
{
    short  box;
    COLOR  color;
} SHAPE, *PSHAPE;

// 形状数组
SHAPE g_shapes[MAX_SHAPES] =
{
    { 0x88C0, RED },
    { 0x8c40, BROWN },
    { 0x4E00, CYAN },
    { 0xF000, YELLOW },
    { 0xCC00, GREEN },
    { 0x8888, PURPLE }
};

// 设置控制台色彩
void set_console_color(int color)
{
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleTextAttribute(hConsole, color);
}

// 绘制方块
void draw_tetris(int x, int y, PSHAPE shape)
{
    // 定位起始行到y处
    for (int y1 = 0; y1 < y; ++y1)
        printf("\n");

    // 绘制方块
    for (int i = 0, mask = 1 << 15; i < 16; i++)
    {
        if (i % 4 == 0)
        {
            if (i > 1)
                printf("\n");

            // 定位起始列到x处
            set_console_color(0);
            for (int x1 = 0; x1 < x; ++x1)
                printf(" ");
        }

        if (mask & shape->box)
        {
            set_console_color(shape->color);
            printf("■");
        }
        else
        {
            set_console_color(0);
            putchar(' ');
        }

        mask >>= 1;
    }
}

int main()
{
    for (int i = 0; i < MAX_SHAPES; i++)
    {
        // 调用system函数清除屏幕显示
        system("cls");

        // 显示当前俄罗斯方块
        draw_tetris(15, 2, &g_shapes[i]);

        // 按下任意键显示下一个方块
        getch();
    }

    return 0;
}

glimix.com