实现交互界面
现在我们要为游戏窗口增加一些标签与按钮,来实现反馈与交互,这些功能统一交由GameUI模块处理,当然它也遵循模块的设计原则:
- GameUI.h: 提供游戏界面与主框架的接口
- GameUI.c: 实现游戏界所有逻辑
游戏界面中的元素,我们可以称为Widget(部件),一个Widget描述一个按钮或标签,定义如下:
typedef struct tagWIDGET
{
int id;
int tex;
E2DRect rect;
} WIDGET;
其中定义如下:
- id: 部件的标识;我们通过此id来判断当前操作发生在那个部件上,如果部件不会参考交互,可设置为通用ID。
- tex: 部件渲染时使用的纹理;我们的部件都是通过纹理来呈现外观。
- rect: 部件的渲染区域;通过此区域可以测试部件与鼠标的交互。
GameUI.h
在GameUI.h中,定义了与外部交互的接口等描述信息。
- WIDGET_NONE: 用于交互信息,表示鼠标没有命中任何部件
- WIDGET_COMMON_ID: 用于为不需要参与交互的部件指定标识。
#pragma once
#include <stdbool.h>
#include "Easy2D.h"
// 界面控件通用ID定义
#define WIDGET_NONE 0
#define WIDGET_COMMON_ID 1
// 界面按钮ID定义
#define BUTTON_SELECT_LEVEL 2
#define BUTTON_NEXT 3
#define BUTTON_RETRY 4
#define BUTTON_UNDO 5
// 初始化界面
bool Game_UI_Init();
// 渲染界面
void Game_UI_Render(int level);
// 界面点击测试
int Game_UI_HitTest(E2DMouseEvent me, E2DMouseButton button, int x, int y);
四个BUTTON_开头的定义,指出了参与交互的四个按钮,分别是:
- BUTTON_SELECT_LEVE: 选择关卡
- BUTTON_NEXT: 跳转到下一关卡
- BUTTON_RETRY: 重玩当前关卡
- BUTTON_UNDO: 悔步(撤消)
在现阶段,我们仅渲染前三个按钮,功能上只实现跳转到下一关卡、重玩当前关卡,因为现有的游戏实现,已经具备了这些功能。只需要根据游戏界面的交互结果,进行简单的接入即可。
GameUI.c
首先我们看一下与实现相关的变量定义:
// 当前UI中组件数量
#define WGCNT 4
// 组件数组
static WIDGET g_widgets[WGCNT];
// 绘制标题使用的字体
static int g_title_font = E2D_INVALID_RESID;
// 标题纹理
static int g_title_tex = E2D_INVALID_RESID;
- 我们的界面定了4个交互按钮(但BUTTON_UNDO现在不处理),实际实现上还包括一个当前关卡信息的标签部件,由于它不需要参考交互,因此未在头文件中指明,这些部件由
g_widgets表达; - 为了使关卡文本信息有点卡通趣味,我们载入了
Mario字体,这由g_title_font保存。 g_title_tex则是用于优化文本的显示。
接下来看看初始化函数。
bool Game_UI_Init()
{
int sx = 15;
int sy = 510;
const int padding = 10;
const int button_width = 48;
WIDGET tmp[WGCNT] =
{
{
WIDGET_COMMON_ID,
E2D_CreateTexture("./res/images/ui/bar.png"),
sx, sy, 190, 49
},
{
BUTTON_SELECT_LEVEL,
E2D_CreateTexture("./res/images/ui/menu.png"),
sx += (190 + padding), sy, 64, 64
},
{
BUTTON_NEXT,
E2D_CreateTexture("./res/images/ui/next.png"),
sx += (button_width + padding), sy, 64, 64
},
{
BUTTON_RETRY,
E2D_CreateTexture("./res/images/ui/start.png"),
sx += (button_width + padding), sy, 64, 64
},
};
for (int i = 0; i < WGCNT; i++)
{
g_widgets[i] = tmp[i];
}
g_title_font = E2D_LoadFont("./res/font/Mario.ttf", 32);
return true;
}
初始化时,我们填充了所有部件的定义。这里简化了一些实现,比如显示的指定了当前图像的大小;没有进行资源有效性检测等。从实现中可以看出,这些部件是水平排列在窗口底部的。
准备工作完成了,现在就是将它们呈现出来的时候了,渲染实现如下:
//=======================================================================================
void Game_UI_Render(int level)
{
// 绘制所有窗口部件
for (int i = 0; i < WGCNT; i++)
{
const WIDGET* const w = &g_widgets[i];
E2D_RenderTexture(w->tex, w->rect.x, w->rect.y, E2D_FLIP_NONE);
}
// 绘制关卡信息
char text[32];
sprintf_s(text, 32, "Level %d", level);
const WIDGET* const w = &g_widgets[0];
const E2DColor clr = { 255, 255, 0, 255 };
E2DRect rect = w->rect;
rect.x += 20;
E2D_DrawText(g_title_font, text, rect, E2D_ALIGN_LEFT | E2D_ALIGN_VCENTER, clr, NULL);
}
最后,我们可以看看交互部分,主要就是判断鼠标点击了哪个按钮,这一部分相当简单。
int Game_UI_HitTest(E2DMouseEvent me, E2DMouseButton button, int x, int y)
{
if (me == E2D_MOUSEBUTTONUP && button == E2D_MB_LEFT)
{
for (int i = 0; i < WGCNT; i++)
{
const WIDGET *w = &g_widgets[i];
if (E2D_PointInRect(x, y, w->rect))
return w->id;
}
}
return WIDGET_NONE;
}
游戏界面的交互是与主框架模块交互,后者又根据前者的反馈与游戏模块交互,调用相应的关卡函数完成功能。
void Mouse(E2DMouseEvent me, E2DMouseButton button, int x, int y, int lbs, int mbs, int rbs)
{
int hit = Game_UI_HitTest(me, button, x, y);
switch (hit)
{
case BUTTON_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;
}
}
