张银峰的编程课堂

实现交互界面

现在我们要为游戏窗口增加一些标签与按钮,来实现反馈与交互,这些功能统一交由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;
    }
}

glimix.com