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)中用于控制类成员(属性、方法)访问权限的修饰符,核心规则如下:可以被当前类本身访问(和
private、public 一致);可以被当前类的子类(无论子类和父类是否在同一个包 / 命名空间)访问;
接下来实现“开始游戏”和“结束游戏”这两个按钮类
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,就可以直接在这里评论。