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

可以看出,选择关卡场景由以下元素构成:
- 返回按钮
- 场景标题
- 前后翻页按钮
- 每页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();
}
}
