张银峰的编程课堂

选择关卡

选择关卡同游戏界面类似都具有可交互性;不同的是,游戏界面直接浮动于游戏场景之上,而关卡选择界面则是一个独立的场景。在介绍实现之前,我们先看看本阶段最终实现的效果,以便更加清晰的理解设计思路。

glimix.com

可以看出,选择关卡场景由以下元素构成:

  • 返回按钮
  • 场景标题
  • 前后翻页按钮
  • 每页25个关卡按钮

对于选择关卡界面,我们并没有遵循前面游戏界面的设计策略,抽象出Widget的概念,而是采用直接绘制的方式。整体思路就是先设计好版式,如左上角放置返回按钮,后面跟标题;然后窗口中部左右两边放置翻页按钮,最后将关卡按钮,通过循环的方式排列起来即可,而这只需要简单的数学计算即可。

好了,我们先从接口文件说起。

LevelUI.h

#define SELECT_NONE -1
#define SELECT_BACK  0

// 关卡选择界面初始化
bool Level_UI_Init(int width, int height);

// 关卡选择界面渲染
void Level_UI_Render();

// 关卡选择界面命中测试
int Level_UI_HitTest(E2DMouseEvent me, E2DMouseButton button, int x, int y);

在选择关卡的交互过程中,用户有3种选择的可能性,对应了Level_UI_HitTest的返回值。

  • 未选中任意交互元素,这使用SELECT_NONE表达。
  • 选择了返回按钮,由SELECT_BACK表达。
  • 选择了具体关卡,由关卡号表达。

而对于初始化函数,我们需要知道游戏场景的范围信息,这是我们通过整齐布局关卡按钮的必须条件。

LevelUI.c

现在我们看看实现部分,对于初始化函数,我们采用了延迟初始化策略,即只有当用户点击选择关卡按钮后,我们才创建关卡资源。

bool Level_UI_Init(int width, int height)
{
    if (!g_inited)
    {
        g_scene_width = width;
        g_scene_height = height;

        //...
        // 载入资源

        g_inited = 1;
    }

    return g_inited;
}

选择关卡的渲染是核心部分,这里我们使用一些简单的计算定位,来直接输出交互元素。

void Level_UI_Render()
{
    // 每页显示5x5个关卡按钮
    const int rows = 5;
    const int columns = 5;

    const int margin = 15;          // 场景边距
    const int tex_page_size = 32;   // 翻页按钮大小
    const int tex_panel_size = 64;  // 关卡按钮大小

    // 背景
    E2D_RenderTexture(g_tex_bakcground, 0, 0, E2D_FLIP_NONE);

    // 绘制返回按钮
    int x = margin;
    int y = margin;
    E2D_RenderTexture(g_tex_back, x, y, E2D_FLIP_NONE);

    // 绘制窗口标题
    E2DColor clr = { 255, 255, 0, 255 };
    E2D_TextOut(g_level_font, "SELECT LEVEL", x + tex_panel_size, y, clr, NULL);

    // 绘制向前翻页按钮
    x = margin;
    y = (g_scene_height - tex_page_size) / 2;    
    if (g_cur_page > 1)
    {
        E2D_RenderTexture(g_tex_previous_page, x, y, E2D_FLIP_NONE);
    }

    // 绘制向后翻页按钮
    if (g_cur_page < 4)
    {
        x = (g_scene_width - margin) - tex_page_size;        
        E2D_RenderTexture(g_tex_next_page,x, y, E2D_FLIP_NONE);
    }

    int sx = margin + tex_page_size + margin;
    int ex = g_scene_width - sx;
    int sy = tex_panel_size + tex_panel_size / 2;
    int ox = (ex - sx - tex_panel_size * columns) / (columns - 1);

    int level = (g_cur_page - 1) * (rows * columns) + 1;
    E2DColor text_color = {255, 255, 255, 255};

    x = sx;
    y = sy;

    for (int r = 0; r < rows; r++)
    {
        for (int c = 0; c < columns; c++)
        {
            E2D_RenderTexture(g_tex_level_panel, x, y, E2D_FLIP_NONE);

            char level_str[32];
            sprintf_s(level_str, 32, "%d", level);
            E2DRect r = {x, y, tex_panel_size, tex_panel_size};
            E2D_DrawText(g_level_font, level_str, r, E2D_ALIGN_CENTER, text_color, NULL);

            x += tex_panel_size + ox;
            level += 1;
        }

        x = sx;
        y += tex_panel_size + margin;
    }
}

由于没有像游戏界面那样缓存按钮信息,因此交互部分就是渲染部分的复刻,只是将渲染代码换成命中测试实现即可。

状态

我们一开始提过,关卡选择是一个独立的场景,这就是说,我们需要在游戏场景与选择关卡场景间维持一个状态切换。程序启动后进入游戏场景,点击选择关卡按钮后,进入该场景;选择结束,返回游戏场景。因此我们在主框架中定义了Stage枚举,来维护这种交互关系。

// 场景阶段
enum STAGE
{
    STAGE_PLAY,
    STAGE_SELECT_LEVEL
};

int g_stage = STAGE_PLAY;

然后在鼠标事件中,切换场景状态。

void Mouse(E2DMouseEvent me, E2DMouseButton button, int x, int y, int lbs, int mbs, int rbs)
{
    if (g_stage == STAGE_PLAY)
    {
        int hit = Game_UI_HitTest(me, button, x, y);

        switch (hit)
        {
        case BUTTON_SELECT_LEVEL:
            Level_UI_Init(MAP_WIDTH, MAP_HEIGHT);
            g_stage = STAGE_SELECT_LEVEL;
            break;

        case BUTTON_NEXT:
            Game_Load_Level(Game_Get_Current_Level() + 1);
            break;

        case BUTTON_RETRY:
            Game_Load_Level(Game_Get_Current_Level());
            break;
        }
    }
    else if (g_stage == STAGE_SELECT_LEVEL)
    {
        int hit = Level_UI_HitTest(me, button, x, y);

        if (hit != SELECT_NONE)
        {
            g_stage = STAGE_PLAY;

            if (hit != SELECT_BACK)
                Game_Load_Level(hit);
        }
    }
}

渲染则是根据场景状态选择执行。

void Render()
{
    if (g_stage == STAGE_PLAY)
    {
        Game_Render();
        Game_UI_Render(Game_Get_Current_Level());
    }
    else if (g_stage == STAGE_SELECT_LEVEL)
    {
        Level_UI_Render();
    }
}

glimix.com