跳转至

04-界面实现与设计模式

1 享元模式

1.1 介绍

享元模式是一种代码的设计模式,“享元”的含义是“共享元素”。
比如,在三维世界中的树元素,一般的结构体定义如下:

struct Tree
{
    Model model;    // 树的模型
    Texture texture; // 树的贴图
    int x,y,z;      // 树的位置
}

采用享元模式则应该这样写
// 树的资产结构体
struct TreeAsset
{
    Model model;    // 树的模型
    Texture texture; //树的贴图
}

// 树结构体
struct Tree
{
    TreeAsset* asset; //资产指针
    int x, y, z; // 树的位置
}

1.2 代码实现

回顾我们之前写的代码,每个Animation类都有自己的Animation列表,每个Enemy类都有两个自己的Animation对象。
这就意味着程序中每产生一个敌人,都会从磁盘上加载两套动画的图片到内存中。

所以我们定义Atlas类来表示动画所使用的图集

class Atlas
{
public:
    Atlas(LPCTSTR path, int num)
    {
        TCHAR path_file[256];
        for (size_t i = 0; i < num; i++) {
            _stprintf_s(path_file, path, i);

            IMAGE *frame = new IMAGE();
            loadimage(frame, path_file);
            frame_list.push_back(frame);
        }
    }

    ~Atlas()
    {
        for (size_t i = 0; i < frame_list.size(); i++) {
            delete frame_list[i];
        }
    }
public:
    std::vector<IMAGE *> frame_list;
};

在animation类中,将原本的frame_list换为Atlas对象anim_atlas,
Atlas *anim_atlas;

对应的构造函数和析构函数也应该修改
Animation(Atlas* atlas, int interval) {
    anim_atlas = atlas;
    interval_ms = interval;
}

~Animation() = default;

在全局定义用于存储相关资源的指针变量
Atlas *atlas_player_left;
Atlas *atlas_player_right;
Atlas *atlas_enemy_left;
Atlas *atlas_enemy_right;

IMAGE *global_img_player_shadow;
IMAGE *global_img_enemy_shadow;

修改Enemy类的构造函数Enemy(),以及Player类的构造函数Player()
Enemy()
{
    img_shadow = global_img_enemy_shadow;
    //6张序列帧,帧间隔为45毫秒
    anim_left = new Animation(atlas_enemy_left, 45);
    anim_right = new Animation(atlas_enemy_right, 45);
    ......
}

 Player()
{

    img_shadow = global_img_player_shadow;
    //6张序列帧,帧间隔为45毫秒
    anim_left = new Animation(atlas_player_left, 45);
    anim_right = new Animation(atlas_player_right, 45);
}

另外,析构函数也要修改,原本析构函数负责资源的回收,delete掉这个对象初始化时加载的函数。但是在享元模式下,每个对象本身不负责资源的加载和回收。所以删掉原本的delete语句

在main函数的开头加载资源,注意加载资源要在Player初始化之前,因为Player初始化时要用到这些资源的指针。

global_img_enemy_shadow = new IMAGE();
global_img_player_shadow = new IMAGE();
loadimage(global_img_player_shadow, _T("..\\img\\shadow_player.png"));
loadimage(global_img_enemy_shadow, _T("..\\img\\shadow_enemy.png"));

atlas_player_left = new Atlas(_T("..\\img\\player_left_%d.png"), 6);
atlas_player_right = new Atlas(_T("..\\img\\player_right_%d.png"), 6);
atlas_enemy_left = new Atlas(_T("..\\img\\enemy_left_%d.png"), 6);
atlas_enemy_right = new Atlas(_T("..\\img\\enemy_right_%d.png"), 6);

结尾回收资源
delete global_img_enemy_shadow;
delete global_img_player_shadow;

delete atlas_enemy_left;
delete atlas_enemy_right;
delete atlas_player_left;
delete atlas_player_right;

2 用户界面

EasyX与GUI库有着本质区别,我们可以方便地调用函数绘制点线面各种图形,但是想要在窗口创建一个带有交互效果的按钮,就需要自己动手了。
像Qt这些定位明确的GUI框架,一般也不会直接参与到游戏本身的制作中。在游戏这种实时渲染的框架中,ImGUI是比较合适的。
所以我们自己实现一个Button类

class Button
{
public:
    Button(RECT rect, LPCTSTR path_img_idle, LPCTSTR path_img_hovered, LPCTSTR path_img_pushed)
    {
        region = rect;

        loadimage(&img_idle, path_img_idle);
        loadimage(&img_hovered, path_img_hovered);
        loadimage(&img_pushed, path_img_pushed);
    }
private:
    enum class Status
    {
        Idle = 0,
        Hovered,
        Pushed
    };

private:
    RECT region;
    IMAGE img_idle;
    IMAGE img_hovered;
    IMAGE img_pushed;
    Status status = Status::Idle;
}

之后编写检测鼠标位置是否位于按钮上的函数
private:
//检测鼠标点击
bool CheckCursorHit(int x, int y)
{
    return x >= region.left && x <= region.right && y >= region.top && y <= region.bottom;
}

事件处理和绘制按钮的方法
void ProcessEvent(const ExMessage& msg)
{
    switch (msg.message)
    {
    case WM_MOUSEMOVE:
        if (status == Status::Idle && CheckCursorHit(msg.x, msg.y))
            status = Status::Hovered;
        else if (status==Status::Hovered && !CheckCursorHit(msg.x, msg.y))
            status = Status::Idle;
        break;
    case WM_LBUTTONDOWN:
        if (CheckCursorHit(msg.x, msg.y))
            status = Status::Pushed;
        break;
    case WM_LBUTTONUP:
        if (status == Status::Pushed)
            OnClick();
        break;
    default:
        break;
    }
}

void Draw()
{
    switch (status)
    {
    case Status::Idle:
        putimage(region.left, region.top, &img_idle);
        break;
    case Status::Hovered:
        putimage(region.left, region.top, &img_hovered);
        break;
    case Status::Pushed:
        putimage(region.left, region.top, &img_pushed);
        break;
    }
}

事件处理函数会调用OnClick()函数来执行点击按钮后的逻辑。这个函数是一个虚函数,这意味着以Button的子类都要自己实现一个特定的OnClick()
protected:
    virtual void OnClick() = 0;

protected(受保护的)是面向对象编程(OOP)中用于控制类成员(属性、方法)访问权限的修饰符,核心规则如下:
可以被当前类本身访问(和 privatepublic 一致);可以被当前类的子类(无论子类和父类是否在同一个包 / 命名空间)访问;

接下来实现“开始游戏”和“结束游戏”这两个按钮类

class StartGameButton : public Button
{
public:
    StartGameButton(RECT rect, LPCTSTR path_img_idle, LPCTSTR path_img_hovered, LPCTSTR path_img_pushed) 
        : Button(rect, path_img_idle, path_img_hovered, path_img_pushed) {}
    ~StartGameButton() = default;
protected:
    void OnClick()
    {
        is_game_started = true;
    }
};

class QuitGameButton : public Button
{
public:
    QuitGameButton(RECT rect, LPCTSTR path_img_idle, LPCTSTR path_img_hovered, LPCTSTR path_img_pushed) 
        : Button(rect, path_img_idle, path_img_hovered, path_img_pushed) {}
    ~QuitGameButton() = default;
protected:
    void OnClick()
    {
        running = false;
    }
};

定义表示游戏运行状态的两个全局变量

bool running = true;
bool is_game_started = false;

在main函数中定义前面提到的两种按钮对象

RECT region_btn_start_game, region_btn_quit_game;

region_btn_start_game.left = (WINDOW_WIDTH - BUTTON_WIDTH) / 2;
region_btn_start_game.right = region_btn_start_game.left + BUTTON_WIDTH;
region_btn_start_game.top = 430;
region_btn_start_game.bottom = region_btn_start_game.top + BUTTON_HEIGHT;

region_btn_quit_game.left = (WINDOW_WIDTH - BUTTON_WIDTH) / 2;
region_btn_quit_game.right = region_btn_quit_game.left + BUTTON_WIDTH;
region_btn_quit_game.top = 550;
region_btn_quit_game.bottom = region_btn_quit_game.top + BUTTON_HEIGHT;

StartGameButton btn_start_game = StartGameButton(region_btn_start_game,
     _T("..\\img\\ui_start_idle.png"), _T("..\\img\\ui_start_hovered.png"), _T("..\\img\\ui_start_pushed.png"));
QuitGameButton btn_quit_game = QuitGameButton(region_btn_quit_game,
     _T("..\\img\\ui_quit_idle.png"), _T("..\\img\\ui_quit_hovered.png"), _T("..\\img\\ui_quit_pushed.png"));

在事件处理部分使用is_game_started作分支
while (peekmessage(&msg)) {
    if(is_game_started) {
        player.ProcessEvent(msg);
    } else {
        btn_start_game.ProcessEvent(msg);
        btn_quit_game.ProcessEvent(msg);
    }
}

游戏开始后的相关逻辑也需要套上if语句

绘制画面也要根据is_game_started绘制不同的画面

if (is_game_started) {
    putimage(0, 0, &img_background);
    //这里固定帧率为144Hz
    player.Draw(1000 / 144);

    for(Enemy* enemy : enemy_list) {
        enemy->Draw(1000 / 144);
    }
    for(const Bullet& bullet : bullet_list) {
        bullet.Draw();
    }
    DrawPlayerScore(score);
} else {
    putimage(0, 0, &img_menu);
    btn_start_game.Draw();
    btn_quit_game.Draw();
}

评论

如果你已登录 GitHub,就可以直接在这里评论。