【C++】 制作游戏壳

news2024/11/23 15:18:21

目录

前言

GameFrame游戏壳

搭建游戏壳

游戏初始化

游戏重绘

游戏运行

用回调函数实现游戏运行

关闭窗口,退出程序

测试

增加子类继承游戏壳子

继承

多态

优化

测试

总结

使用方法

常见错误

完整代码

GameFrame.h

main.cpp


前言

为了方便以后制作小游戏,我们将游戏中通用的方法以及属性抽离出来,制作成一个游戏壳,是制作游戏的效率更高!


GameFrame游戏壳

之前写过一个超级玛丽移动的小练习,但是我们每次写游戏的时候都要从0开始太过复杂,我们怎么能将游戏公共的东西抽离出来,在写游戏的时候直接使用呢,于是我们就将公共的东西封装成一个游戏壳。

搭建游戏壳

我们大致抽出来四种方法:初始化、重绘、运行、关闭

在头文件中创建类

 #pragma once
 ​
 class GameFrame {
 ​
 };

游戏初始化

第一次编写

在类中先创建一个游戏初始化的方法,作用是创建指定大小的窗口,移动窗口到指定位置(可放大或缩小),设置标题,设定窗口背景色和窗口刷新,并给出具体游戏初始化的接口。

其中创建窗口时我们学了一个新的变量窗口变量HWND,也就是窗口句柄,他主要是起到标识区分的作用,让我们在操作窗口时直到我们是在对哪个窗口进行操作。

我们要对这个函数传入的参数有:窗口的宽和高,窗口的横纵坐标,窗口的标题

为了能在其他类成员函数中也可使用此窗口句柄,我们将窗口句柄提升为类成员属性,有了类成员属性,那么构造析构函数也要写出来了。

此时的头文件:

 #pragma once
 #include<easyx.h>
 ​
 class CGameFrame {
 ​
     HWND m_hWnd;
 public:
     CGameFrame(): m_hWnd(nullptr){}
     ~CGameFrame(){}
     //1.初始化
     void InitGame(int width, int height, int posx, int posy, LPCWSTR pTitle) {
         //创建指定大小的窗口
         m_hWnd = ::initgraph(width, height);
 ​
         //移动窗口
         ::MoveWindow(m_hWnd,posx,posy/*移动窗口到某个位置*/, width, height/*窗口大小(可放大、缩小)*/, TRUE/*是否重绘*/);  //窗口句柄:窗口变量 用以标识区分
 ​
         //设置标题
         ::SetWindowText(m_hWnd, pTitle);
         //设定窗口的背景色,为白色
         ::setbkcolor(RGB(255, 255, 255));
         ::cleardevice();//使窗口背景色立即刷新
 ​
         On_Init();
     }
 ​
     //-----------具体游戏相关的函数-------------------------------
 ​
     void On_Init() {
 ​
     }
 ​
 };

为窗口指定回调函数

用函数SetWindowLong()去指定,参数分别为窗口句柄,设置什么属性(设置回调函数),回调函数的地址

         ::SetWindowLong(m_hWnd, GWL_WNDPROC/*指定回调函数*/, (LONG) & ::RunGame);

回调函数我们要在头文件中声明,源文件的全局中定义。

 //头文件类外
 LRESULT CALLBACK RunGame(_In_  HWND hwnd, _In_  UINT uMsg, _In_  WPARAM wParam, _In_  LPARAM lParam);

游戏重绘

在重绘中首先的是要批量绘图,然后清除上一次绘图,最后结束绘图。同样,在结束绘图之前要留出具体游戏重绘的接口

     //2.重绘
     void PaintGame() {
         ::BeginBatchDraw(); //批量绘图
         ::cleardevice();//清除上一次绘图痕迹
         //------具体游戏的重绘----------------
         On_Paint();
         //---------------------------------------
         ::EndBatchDraw();
 ​
     }
 ​
     void On_Paint() {
 ​
     }

游戏运行

第一次编写

这个函数较复杂,先写出名字,后面再补上具体代码

     //3.游戏运行
     void RunGame() {
 ​
     }

第二次编写

对窗口做的任何操作都是通过获取消息实现的,所以我们的程序要不断的获取消息,然后根据消息调用对应的函数

获取消息的函数:getmessage(),其中参数为指定要获取的消息范围(filter),默认为-1获取所有类别的消息,可以用以下值或值的组合获取指定类别的消息:

返回值为保存消息的ExMessage结构体

所以我们要定义一个ExMessage结构体变量,用以装获取到的消息

在接收到消息后根据不同的消息做对应处理,这里就拿键盘按下和鼠标左键抬起为例,获取到的消息为刚才定义的结构体中的message变量,而对应的键盘按下和鼠标左键抬起为easyx中给定的宏WM_KEYDOWN和WM_LBUTTONDOWN,然后在处理函数位置给出具体游戏处理函数的接口,键盘按下处理函数的参数为msg.vkcode,鼠标抬起函数的参数为坐标x和y

此时的函数代码:

     //3.游戏运行
     void RunGame() {
 ​
         ExMessage msg;   //装获取到的消息
         while (1) {
             //不断的获取消息
             msg = ::getmessage();
 ​
             //根据不同的消息,做对应的处理
             if (msg.message == WM_KEYDOWN) {
                 On_WM_KEYDOWN(msg.vkcode);  //调用处理函数
             }
             if (msg.message == WM_LBUTTONUP) {
                 On_WM_LBUTTONUP(msg.x, msg.y);
             }
 ​
 ​
         }
     }
     
     void On_WM_KEYDOWN(BYTE vkcode) {
         int a = 0;
     }
 ​
     void On_WM_LBUTTONUP(int x, int y) {
 ​
     }

第三次编写

由于我们写的是死循环所以要加一个退出循环的判断,仍然是用msg.message判断是否等于系统给出的WM_CLOSE,然后调用关闭窗口函数。

但是通过测试,我们发现无法实现,我们手动退出程序时message并没有接收到退出的消息,也调用不了关闭窗口函数。

通过查看帮助文档,我们发现easyx的message接收不到窗口类别的关闭消息

在创造窗口之前有设计窗口,可以为窗口指定一个函数,称之为回调函数,之后对窗口做的所有操作都由回调函数去接收

系统会产生一个消息队列,对窗口操作的消息会放入到消息队列中,然后按顺序取出,取出后经过翻译转发到回调函数中

所以我们要为窗口句柄指定一个回调函数,这个回调函数要在游戏初始化中去指定

那么至此我们就要去用回调函数去实现游戏运行了以上代码可以注释掉了

用回调函数实现游戏运行

第一次编写

窗口的回调函数

hwnd: 窗口句柄 uMsg: 消息ID == msg.message (接收到的消息) wParam,lParam: 消息携带的参数

 LRESULT CALLBACK RunGame(_In_  HWND hwnd, _In_  UINT uMsg, _In_  WPARAM wParam, _In_  LPARAM lParam) {
 ​
     return ::DefWindowProc(hwnd, uMsg, wParam, lParam);//window 默认的消息处理
 }

将之前写的消息判断拿到回调函数中,然后用uMsg替换掉msg.message

然后主函数中就不用对象去调用运行函数了

因为我们的回调函数是在源文件的全局处定义的,所以他里面的类成员函数需要用对象去调用,所以我们将在主函数中定义的对象提到全局区域

那么这里有个问题,我们将对象放到全局区并且赋空,那么回调函数中的使用的对象会不会是空?我们要明白一个顺序,我们是在主函数中先让对象指向空间,然后在用对象去调用初始化函数,并且我们为窗口指定回调函数是在初始化函数中实现的,所以不用担心全局区的回调函数中的对象指向的是空

处理函数中的参数也要替换,其中wParam就相当于之前的vkcode,lParam可以获取到xy坐标,通过高字节和低字节来获取,我们需要加上一些特定的方法函数,并且需要加上头文件(Windowsx.h)

此时的回调函数代码及相关操作:

 CGameFrame* pGameFrame = nullptr;
 LRESULT CALLBACK RunGame(_In_  HWND hwnd, _In_  UINT uMsg, _In_  WPARAM wParam, _In_  LPARAM lParam) {
     //根据不同的消息,做对应的处理
     if (uMsg == WM_KEYDOWN) {
         
         pGameFrame->On_WM_KEYDOWN(wParam);  //调用处理函数
     
     }
     if (uMsg == WM_LBUTTONUP) {
 ​
         //xPos = GET_X_LPARAM(lParam);
         //yPos = GET_Y_LPARAM(lParam);
 ​
         pGameFrame->On_WM_LBUTTONUP(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
     }
     if (uMsg == WM_CLOSE) {
         //pGameFrame->CloseGame();
     }
 ​
     return ::DefWindowProc(hwnd, uMsg, wParam, lParam);//window 默认的消息处理
 }

新增头文件:

 #include <windowsx.h>

在主函数中放置一个死循环来一直接收消息

     while (1) {
 ​
 ​
         Sleep(1000);
     }

第二次编写

随后我们也将CloseGame函数取消注释,但是在之前的游戏运行中我们是在循环中关闭,但是现在循环在在主函数中,所以我们需要定义一个标记用以关闭循环

在类中定义一个bool类型的标识,如果是true就退出,是false就不退出,并在构造函数中进行初始化

     bool m_isQuit;//标识程序是否退出,true:退出 false:不退出
         CGameFrame(): m_hWnd(nullptr), m_isQuit(false){}

由于类中的成员属性我们设置的是私有的,所以要设置一个接口才能在类外去使用

     bool GetQuit() {return m_isQuit;}

那么主函数中循环的判断条件就是这个函数了

     while (!pGameFrame->GetQuit()) {
 ​
 ​
         Sleep(1000);
     }

当我们按下关闭窗口时,回调函数会调用关闭游戏的函数,所以我们在关闭游戏函数中去将标识(m_isQuit)改为true

第三次编写

增加消息

目前增加消息的想法就是,增加一个if判断,然后增加对应的处理函数,想要删除消息就是要将代码注释掉

但是这么增加消息要增加到什么时候为止呢?我们要写的是一个大部分情况下都可用的游戏壳子,要把常见的消息都罗列出来

所以我要做一个优化,目前情况下变数太多,我们想要弄一个通用的增加或删除消息的不变的代码结构

做到代码量不随着消息量的增加而增加,将所有的消息做出分类,灵感来自于easyx的文档

 

一共就四类

这里一个类型对应多个消息ID,然后每个消息ID对应一个处理函数

我们采用map来处理,因为map只有一个key跟value,但是我们有三层需要区分,所以我们可以做一个嵌套,先用类型当作key然后找到消息ID,然后再拿消息ID作为key,然后找到处理函数。但是这种方法需要去遍历表,我们不想遍历,我们可以将ID作为一个key值,然后将类型和处理函数做一个融合,那么怎么融合呢,这里采用间接调用,就像函数指针可以指向两个整数的加减乘除的函数一样,我们用函数指针指向参数类型一样的处理函数,然后类别就可以和指针融合了。

map,key值是ID,value值是类别和函数指针的融合,融合考虑用结构体封装

代码实现:

在头文件类中创建一个map,key值是消息ID也就是UINT类型,value是我们要创建的结构体

结构体中首先是消息类别,我们发现消息类别的本质是整型,所以用整型定义,然后是指向类别的函数指针,这里先写一个鼠标类别的函数指针,因为函数是类成员函数,所以我们typedef一个指向鼠标类别函数的类成员函数指针

     typedef void (CGameFrame::* P_FUN_EX_MOUSE)(int x, int y);

所以结构体为:

     struct MsgTypeFun {
         int msgType;    //消息类别
         P_FUN_EX_MOUSE p_fun_EX_MOUSE;    //指向鼠标类别的函数指针
 ​
 ​
     };

map为:

     map < UINT, MsgTypeFun > m_msgMap;

这个以映射表就为消息映射表

并且别忘了加上map的头文件

 #include<map>

第四次编写

在类中加入一个新的函数,用来初始化消息映射表

在其中通过消息ID找到消息类别然后赋值,在根据消息ID找到处理函数指针,然后指向处理函数

     void InitMsgMap() {
         m_msgMap[WM_LBUTTONUP].msgType = EX_MOUSE;
         m_msgMap[WM_LBUTTONUP].p_fun_EX_MOUSE = &CGameFrame::On_WM_LBUTTONUP;
 ​
     }

这个初始化也要在游戏初始化中写出,并且要写在指定回调函数之前

 

此时我们就可以用数据结构改造回调函数了

比如改造这个判断

改造前:

改造后:

先判断消息是否存在

然后根据消息类别调用对应的类成员函数指针

     if (pGameFrame->m_msgMap.count(uMsg)) {  //消息存在,能够处理
 ​
         switch (pGameFrame->m_msgMap[uMsg].msgType)   //判断类别
         {
         case EX_MOUSE:
         {
             (pGameFrame->*pGameFrame->m_msgMap[uMsg].p_fun_EX_MOUSE)(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); //类成员函数指针
                 //类成员函数指针的使用方法:
             //(对象->*p_fun)(参数列表);
         }
         break;
         case EX_KEY:
         {
 ​
         }
         break;
         case EX_CHAR:
         {
 ​
         }
         break;
         case EX_WINDOW:
         {
 ​
         }
         break;
 ​
         }
 ​
     }

第五次编写

要增加一个对消息的操作应该增加什么,那么下面就增加一个键盘按下的消息和对应操作

根据分析,我们应该增加一个消息类别,和函数指针,因为之前没有添加过对键盘消息接收的操作,然后还要在初始化消息映射表中添加具体消息对应的类别和函数指针指向的函数

分别是:

重命名一个键盘类别的类成员函数指针类型

  

然后在结构体中添加一个该类型的指针

  

在初始化消息映射表中添加键盘按下消息ID的类别和处理函数

  

最后在回调函数中通过判断消息类别和消息映射表的key值来通过函数指针间接调用函数

优化

解决结构体空间浪费问题

在结构体部分,我们一共要做四个类别的函数指针,但是每次操作单个消息我们只用一个指针,那么这样就会造成空间浪费,所以我们要在结构体中嵌套一个共用体,将四个函数指针放在里面,这样这四个指针就可以联合共享空间了

  

用宏来使增加消息操作更简单

在初始化消息映射表中,我们每增加一个对消息的操作时,就会增加两行代码,那么这两行代码有很多部分都是相同的,所以我们选择用一个宏来优化。

此处将LBUTTONUP改为LBUTTONDOWN

优化之前:

     m_msgMap[WM_KEYDOWN].msgType = EX_KEY;
     m_msgMap[WM_KEYDOWN].p_fun_EX_KEY = &CGameFrame::On_WM_KEYDOWN;

优化之后:

宏:

 #define INIT_MSGMAP(MSGID,MSGTYPE)\
     m_msgMap[MSGID].msgType = MSGTYPE;\
     m_msgMap[MSGID].p_fun_##MSGTYPE = &CGameFrame::On_##MSGID;
 
        INIT_MSGMAP(WM_KEYDOWN,EX_KEY)

第六次编写

接下来再把EX_CHAR类别和EX_WINDOW类别做一个补充

EX_CHAR类别中有一个WM_CHAR的消息,参数可以和键盘类别相同

WM_CHAR和WM_KEYDOWN的区别:KEYDOWN是对应到键盘所有的键,CHAR只能对应到能转为字符的键

首先增加一个对消息的操作函数

     void On_WM_CHAR(BYTE vkcode) {
         int a = 0;
     }

然后增加一个消息类别的函数指针类型

     typedef void (CGameFrame::* P_FUN_EX_CHAR)(BYTE);

在结构体中的联合体中增加函数指针

     P_FUN_EX_CHAR p_fun_EX_CHAR;

在初始化消息映射表中添加对该消息ID的操作(根据消息ID使消息类别等于对应类别,根据消息ID使函数指针指向对应的处理函数)

     INIT_MSGMAP(WM_CHAR, EX_CHAR)

在回调函数中判断消息ID调用处理函数

         case EX_CHAR:
         {
             (pGameFrame->*pGameFrame->m_msgMap[uMsg].p_fun_EX_CHAR)(wParam);
         }
         break;

添加EX_WINDOW类别

close就是一个该类别的消息,但是我们根据之前写的代码发现,我们根据消息ID调用的处理函数的名字样式应该为On_消息ID,但是在最开始我们划分功能时直接将关闭窗口函数命名为CloseGame了,所以我们要将该函数名进行修改(因为我们在宏中调用的函数名称就是该格式的)

  

窗口消息类别的参数是WPARAM和LPARAM

 void On_WM_CLOSE(WPARAM,LPARAM)

然后后续的添加操作也是一样

     typedef void (CGameFrame::* P_FUN_EX_WINDOW)(WPARAM,LPARAM);
     P_FUN_EX_WINDOW p_fun_EX_WINDOW;
     INIT_MSGMAP(WM_CLOSE, EX_WINDOW)
         case EX_WINDOW:
         {
             (pGameFrame->*pGameFrame->m_msgMap[uMsg].p_fun_EX_WINDOW)(wParam,lParam);
         }
         break;

第七次编写

现在已经达到了之前的目标:回调函数中的代码不会随着消息的增加而增加了,并且四种消息类别也在结构体中创建完毕,再增加消息我们只需要在初始化消息映射表中写出对应的宏和参数,并且增加相应的处理函数即可。

我们发现此时重绘没有被调用过,它目前是游离在体系之外的,那么它应该在哪里调用呢

第一种思路:在回调函数中,我们在处理完消息后,调用重回函数及时刷新

另一种思路是我们通过消息去驱动

我们选择第二种重绘方式

首先我们将重绘函数的函数名改为我们定义处理函数规则样式的函数名(On_消息ID)

并且这种消息ID属于窗口类别

 

在初始化消息映射表中添加对应的宏

     INIT_MSGMAP(WM_PAINT, EX_WINDOW)

通过测试我们发现我们可以接收到重绘的消息,但并不能进入到处理函数中,所以我们创建一个无效区将窗口变为无效的

我们用InvalidateRect这个函数,他的参数分别为窗口句柄,窗口的哪个部分,是否擦除

然后我们要创建一个RECT用以函数的第二个参数

         RECT rect{ 0,0,400,300 };   //矩形区域,同窗口大小
         ::InvalidateRect(hwnd,&rect,FALSE)

再次测试,我们发现在窗口接收到消息后可以进入重绘函数了

优化

解决消息映射表私有问题

之前我们的消息映射表在类外去用了,所以我们就将他变为公有了,但是如果这个就是一个很隐私的东西,我们不想让外人去用,就想让他成为私有的,那类外就不能直接使用了,所以我们要怎么解决呢

我们有一个已知的方法,就是在类成员函数中加一个接口,但是现在要介绍一个新的办法,叫友元

关键字friend,将回调函数变成类里的友元,那么他就可以使用类里的私有或保护的成员了

     //friend:定义友元的关键字,在朋友中可以使用自己的私有成员(共有,保护更可以)
     friend LRESULT CALLBACK RunGame(_In_  HWND hwnd, _In_  UINT uMsg, _In_  WPARAM wParam, _In_  LPARAM lParam);

实现窗口的大小位置标题可以根据具体游戏而改变

让这个窗口随着游戏的变化而变化,并不是在游戏壳主函数中写死的

那么我们解决这个问题的办法和让父类指针指向子类对象的方法相似

就是让变量在游戏壳子中声明,变量声明时不要忘了在前面加上extern

 extern int wnd_width;
 extern int wnd_height;
 extern int wnd_posx;
 extern int wnd_posy;
 extern LPCWSTR pTitle;

然后将回调函数中我们定义的矩形区域中的宽度和高度改成这个变量

 将主函数中游戏初始化中的参数也改为这些变量

使用时就是将这些变量在具体游戏的源文件中去定义

测试:

 

但是我们还需要继续优化,因为我们每次写一个游戏都需要将这些代码写一遍

我们还是选择用宏去优化,顺带将返回父类指针类型的对象那个函数一同优化

在游戏壳的头文件中创建一个宏,将游戏中定义的函数粘贴过来,并将返回的位置改为可变的参数

 #define CREATE_OBJECT(CLASS)\
     CGameFrame* CreateObject() {\
         return new CLASS;\
     }

那么使用的时候只需要在游戏中写这个宏就可以了

 CREATE_OBJECT(AA)

继续优化那些变量

 #define WND_PARAM(WIDTH,HEIGHT,X,Y,TITLE)\
 int wnd_width = WIDTH;\
 int wnd_height = HEIGHT;\
 int wnd_posx = X;\
 int wnd_posy = Y;\
 LPCWSTR pTitle = TITLE;

使用

 WND_PARAM(800,400,600,100,L"AA")

将窗口句柄变为保护的

因为在游戏壳的子类中可能还会用到这个窗口句柄,比如说子类想在某一时刻移动一下窗口

那么我们就将类中所有私有的成员都变为保护的即可

初始化消息映射表优化

在我们编写这里时,我们为了做测试一共写了五个消息对应的操作,其中重绘和关闭是游戏中一定会用到的,但是另外三个有的也用不到,并且只有这几个也不一定够

所以我们想让这个位置按照具体游戏的需要去增减消息操作

那么我们就抽离出来一个增加消息映射表的接口(增加一个虚函数)

     virtual void AddMsgMap() = 0; 

将这个虚函数放在子类中去声明定义

在游戏壳子类中的初始化消息映射表中只留下关闭和重绘的消息操作,并且添加这个函数

 

剩余消息操作就可以在子类中根据需要去添加了

 

我们现在接收调消息并处理的过程是:接收消息然后根据虚函数找到对增加这个消息的操作,然后再找到游戏壳中的对消息的处理函数,最终通过虚函数找到子类中真正的处理函数

但是我们既然都把这个消息操作放在子类了,并且真正的处理函数也在子类,所以我们直接在子类中调用不就行了吗

之前我们初始化消息映射表宏中绑定的是父类中的处理函数,那么我们通过分析发现,如果能跟子类的绑定那会更方便

我们回到初始化消息映射表的宏中,增加一个参数作为类,然后将处理函数的作用域改为此类

那么父类中使用这个宏,参数就加一个父类,调用的处理函数就是父类中的了

  

子类也是如此,那么既然子类中使用这个宏绑定的是子类中的处理函数,那父类中对应的处理函数就不再需要了

 

子类中的处理函数也就不用变成虚函数了

但是我们发现子类现在在使用这个宏的时候会报错,原因是我们在创建函数指针的时候创建的是父类的函数指针,而在子类中使用时这个指针就从父类降到子类了

  

那么我们还是对这个宏进行操作,将处理函数前面再加一个强转,转成父类的函数指针类型

 

这样就可以了

关闭窗口,退出程序

第一次编写

在关闭程序窗口之前给出具体游戏关闭的接口

     //4.关闭窗口,退出程序
     void CloseGame() {
         On_Close();
         //关闭窗口
         ::closegraph();
     }
 ​
     void On_Close() {
 ​
     }

加入关闭循环的标识

注意在这里我们要将标识放在关闭窗口的后面,因为要先关闭窗口,然后在去退出循环关闭程序

     void CloseGame() {
         On_Close();
 ​
         //关闭窗口
         ::closegraph();
 ​
         m_isQuit = true;  //告诉主函数,该退出了
     }

测试

第一次测试

写出源文件主函数,在其中创建游戏壳对象,并调用初始化和运行两个函数。

主函数:

 #include<iostream>
 #include"GameFrame.h"
 using namespace std;
 ​
 ​
 int main() {
     CGameFrame* pGameFrame = new CGameFrame;
     pGameFrame->InitGame(400, 300, 500, 50, L"测试");
 ​
     pGameFrame->RunGame();
 ​
     delete pGameFrame;
     pGameFrame = nullptr;
     return 0;
 }

编译正确,窗口也正常显示:

在接口函数处下断点

点击窗口可以看出鼠标消息能够被接收到

 

键盘消息也能够正常接收

 

第二次测试

测试回调函数能否正常运行

鼠标左键抬起:

  

键盘按下:

  

最重要的是看能不能在关闭程序的时候接收到对应消息,因为在这之前我们没用回调函数实现接收关闭程序消息是接收不到的,所以采用了回调函数

  

我们可以看到接收到了!

第三次测试

本次要测试的是消息能否通过回调函数调用处理函数

鼠标点击:

  

键盘按下:

 

第四次测试

在加完关闭循环标识后,我们来测试一下程序能不能按合理的顺序去退出

当我们按下叉后,程序首先会接收到退出的消息

  

然后会进入到他调用的关闭函数

 

执行完这个函数,窗口会关闭,标识会变为true,但是程序仍在运行

随后会跳出主函数中的循环,然后回收掉资源,最终退出程序

  

这种退出才是一个合理的有序的退出

第五次测试

测试用消息映射表判断类别和调用函数

鼠标点击:

  

成功进入函数体

流程:先判断消息是否存在,然后判断消息类别,根据类别调用函数

测试一下用消息映射表和回调函数调用键盘按下的处理函数

  

可以进入函数体

测试WM_CHAR消息

  

可以进入处理函数

测试关闭窗口

 

关闭时可以接收到消息并且进入到处理函数

测试重绘

 

测试后我们发现,虽然回调函数能够接收到重绘的消息,但是不会调用重绘函数

在加入无效区后再次测试

 

可以调用重绘

增加子类继承游戏壳子

继承

增加一个子类继承游戏壳类,将游戏壳中的具体游戏相关函数变为虚函数

 #pragma once
 #include "GameFrame.h"
 class AA :public CGameFrame
 {
 ​
 };

多态

游戏相关的函数中初始化重绘关闭是游戏中一定会用到重写的,所以要改为纯虚函数

     virtual void On_Init() = 0;
     virtual void On_Paint() = 0;
     virtual void On_Close() = 0;
 ​
     virtual void On_WM_KEYDOWN(BYTE vkcode) {}
     virtual void On_WM_LBUTTONDOWN(int x, int y) {}
     virtual void On_WM_CHAR(BYTE vkcode) {}

子类中声明虚函数

 class AA :public CGameFrame
 {
 public:
     AA();
     ~AA();
     virtual void On_Init();
     virtual void On_Paint();
     virtual void On_Close();
 ​
     virtual void On_WM_KEYDOWN(BYTE vkcode);
     virtual void On_WM_LBUTTONDOWN(int x, int y);
     virtual void On_WM_CHAR(BYTE vkcode);
 ​
 };

源文件中定义:

 
#include "AA.h"
 ​
 ​
 AA::AA(){
 ​
 }
 ​
 AA::~AA(){
 ​
 }
 ​
 void AA::On_Init() {
 ​
 }
 void AA::On_Paint() {
 ​
 }
 void AA::On_Close() {
 ​
 }
 ​
 void AA::On_WM_KEYDOWN(BYTE vkcode) {
 ​
 }
 void AA::On_WM_LBUTTONDOWN(int x, int y) {
 ​
 }
 void AA::On_WM_CHAR(BYTE vkcode) {
 ​
 }

我们要实现多态,那么父类指针要指向子类对象

所以要在游戏壳子的主函数中将对象改为AA,但是这样并不好,游戏壳要有通用性,不能每次做游戏都要去游戏壳的主函数中改变父类指针指向的对象

所以我们在主函数外做一个创建对象的函数,返回值类型为父类指针

 CGameFrame* CreateObject() {
     return new AA;
 }
     pGameFrame = CreateObject();

优化

但是这样函数中的返回位置也要变化,所以我们将函数的定义和声明拆开

声明放在游戏壳的源文件中,定义放在子类的源文件中

 

测试

通过测试我们发现子类中的所有继承到的函数都可以使用,唯独析构函数不会 被调用

  

原因是我们需要将父类的析构函数变为虚析构

  

这样就没有问题了

总结

使用方法

游戏壳包含两个文件:GameFrame.h main.cpp ,需要定义一个具体游戏类,继承游戏壳类(CGameFrame),重写四个虚函数 virtual void On_Init(); //初始化 virtual void On_Paint(); //重绘 virtual void On_Close(); //关闭 virtual void AddMsgMap(); //添加消息映射表,根据实际用到的消息添加 如何添加: INIT_MSGMAP(消息ID, 所属类别,具体游戏类类名) ,在具体游戏类中添加消息对应的处理函数 void On_消息ID (对应参数); 使用宏:CREATE_OBJECT(具体游戏类名) WND_PARAM(窗口参数)

常见错误

错误样式

在运行程序时,我总是会出现一个LNK1168样式的错误

LNK1168 无法打开 xxx.exe 进行写入 xxx

原因

这种错误的原因是:运行程序, 退出不是按正常流退出,是按窗口右上角的“X”来关闭程序,但是后台的xxx.exe控制台程序还在运行。修改程序的代码后再运行,就会报LNK1168的错误。

解决方法

打开任务管理器,点击详细信息,关闭对应exe进程即可

完整代码

GameFrame.h

#pragma once
#include<easyx.h>
#include<map>
using namespace std;

LRESULT CALLBACK RunGame(_In_  HWND hwnd, _In_  UINT uMsg, _In_  WPARAM wParam, _In_  LPARAM lParam);

#define INIT_MSGMAP(MSGID,MSGTYPE,CLASS)\
	m_msgMap[MSGID].msgType = MSGTYPE;\
	m_msgMap[MSGID].p_fun_##MSGTYPE = (P_FUN_##MSGTYPE)&CLASS::On_##MSGID;
//两个#是连接

class CGameFrame {
	//friend:定义友元的关键字,在朋友中可以使用自己的私有成员(共有,保护更可以)
	friend LRESULT CALLBACK RunGame(_In_  HWND hwnd, _In_  UINT uMsg, _In_  WPARAM wParam, _In_  LPARAM lParam);
protected:
	HWND m_hWnd;
	bool m_isQuit;//标识程序是否退出,true:退出 false:不退出

	typedef void (CGameFrame::* P_FUN_EX_MOUSE)(int, int);
	typedef void (CGameFrame::* P_FUN_EX_KEY)(BYTE);
	typedef void (CGameFrame::* P_FUN_EX_CHAR)(BYTE);
	typedef void (CGameFrame::* P_FUN_EX_WINDOW)(WPARAM,LPARAM);

	struct MsgTypeFun {
		int msgType;    //消息类别
		union {  //联合 共享空间
			P_FUN_EX_MOUSE p_fun_EX_MOUSE;    //指向鼠标类别的函数指针
			P_FUN_EX_KEY p_fun_EX_KEY;        //指向键盘类别的函数指针
			P_FUN_EX_CHAR p_fun_EX_CHAR;
			P_FUN_EX_WINDOW p_fun_EX_WINDOW;

		};

	};
protected:
	map < UINT, MsgTypeFun > m_msgMap;   //消息映射表

public:
	bool GetQuit() {return m_isQuit;}

	void InitMsgMap() {
		INIT_MSGMAP(WM_CLOSE, EX_WINDOW,CGameFrame)
		INIT_MSGMAP(WM_PAINT, EX_WINDOW, CGameFrame)

		AddMsgMap();
	}


	CGameFrame(): m_hWnd(nullptr), m_isQuit(false){}
	virtual ~CGameFrame(){}
	//1.初始化
	void InitGame(int width, int height, int posx, int posy, LPCWSTR pTitle) {
		//创建指定大小的窗口
		m_hWnd = ::initgraph(width, height);

		//移动窗口
		::MoveWindow(m_hWnd,posx,posy/*移动窗口到某个位置*/, width, height/*窗口大小(可放大、缩小)*/, TRUE/*是否重绘*/);  //窗口句柄:窗口变量 用以标识区分

		//设置标题
		::SetWindowText(m_hWnd, pTitle);
		//设定窗口的背景色,为白色
		::setbkcolor(RGB(255, 255, 255));
		::cleardevice();//使窗口背景色立即刷新

		InitMsgMap();  //初始化消息映射表

		//为窗口指定回调函数,窗口句柄,设置什么属性(设置回调函数),回调函数的地址
		::SetWindowLong(m_hWnd, GWL_WNDPROC/*32位下的宏*//*指定回调函数*/, (LONG) & ::RunGame);

		//具体游戏初始化
		On_Init();
	}
	//2.重绘
	//void PaintGame() {
	void On_WM_PAINT(WPARAM,LPARAM) {
		::BeginBatchDraw(); //批量绘图
		::cleardevice();//清除上一次绘图痕迹
		//------具体游戏的重绘----------------
		On_Paint();
		//---------------------------------------
		::EndBatchDraw();

	}

	//4.关闭窗口,退出程序
	void On_WM_CLOSE(WPARAM,LPARAM) {
		On_Close();

		//关闭窗口
		::closegraph();

		m_isQuit = true;  //告诉主函数,该退出了
	}

	//-----------具体游戏相关的函数-------------------------------

	virtual void On_Init() = 0;
	virtual void On_Paint() = 0;
	virtual void On_Close() = 0;

	virtual void AddMsgMap() = 0;    //提供给具体游戏的添加消息映射表的接口

};


#define CREATE_OBJECT(CLASS)\
	CGameFrame* CreateObject() {\
		return new CLASS;\
	}

#define WND_PARAM(WIDTH,HEIGHT,X,Y,TITLE)\
int wnd_width = WIDTH;\
int wnd_height = HEIGHT;\
int wnd_posx = X;\
int wnd_posy = Y;\
LPCWSTR pTitle = TITLE;

/*
游戏壳包含两个文件:GameFrame.h main.cpp  ,需要定义一个具体游戏类,继承游戏壳类(CGameFrame),重写四个虚函数
	virtual void On_Init();   //初始化
	virtual void On_Paint();   //重绘
	virtual void On_Close();   //关闭
	virtual void AddMsgMap();  //添加消息映射表,根据实际用到的消息添加
	如何添加:		INIT_MSGMAP(消息ID, 所属类别,具体游戏类类名)  ,在具体游戏类中添加消息对应的处理函数  void On_消息ID (对应参数);
	使用宏:CREATE_OBJECT(具体游戏类名)  WND_PARAM(窗口参数)


*/

main.cpp

#include<iostream>
#include"GameFrame.h"
#include <windowsx.h>
using namespace std;

//窗口的回调函数
/*
hwnd: 窗口句柄
uMsg: 消息ID == msg.message
wParam,lParam: 消息携带的参数

*/

CGameFrame* pGameFrame = nullptr;
CGameFrame* CreateObject();

extern int wnd_width;
extern int wnd_height;
extern int wnd_posx;
extern int wnd_posy;
extern LPCWSTR pTitle;

LRESULT CALLBACK RunGame(_In_  HWND hwnd, _In_  UINT uMsg, _In_  WPARAM wParam, _In_  LPARAM lParam) {

	//-------------------------------------------
	if (pGameFrame->m_msgMap.count(uMsg)) {  //消息存在,能够处理

		switch (pGameFrame->m_msgMap[uMsg].msgType)   //判断类别
		{
		case EX_MOUSE:
		{
			(pGameFrame->*pGameFrame->m_msgMap[uMsg].p_fun_EX_MOUSE)(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); //类成员函数指针
				//类成员函数指针的使用方法:
			//(对象->*p_fun)(参数列表);
		}
		break;
		case EX_KEY:
		{
			(pGameFrame->*pGameFrame->m_msgMap[uMsg].p_fun_EX_KEY)(wParam);
		}
		break;
		case EX_CHAR:
		{
			(pGameFrame->*pGameFrame->m_msgMap[uMsg].p_fun_EX_CHAR)(wParam);
		}
		break;
		case EX_WINDOW:
		{
			(pGameFrame->*pGameFrame->m_msgMap[uMsg].p_fun_EX_WINDOW)(wParam,lParam);
		}
		break;

		}

		//处理完消息后,及时刷新  -> 重绘

		//WM_PAINT;
		RECT rect{ 0,0,wnd_width,wnd_height };   //矩形区域,同窗口大小
		::InvalidateRect(hwnd, &rect, FALSE);

	}

	return ::DefWindowProc(hwnd, uMsg, wParam, lParam);//window 默认的消息处理
}



int main() {
	pGameFrame = CreateObject();
	pGameFrame->InitGame(wnd_width, wnd_height, wnd_posx, wnd_posy, pTitle);

	while (!pGameFrame->GetQuit()) {

		Sleep(1000);
	}

	delete pGameFrame;
	pGameFrame = nullptr;
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/539925.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据存储梳理记录

目录 1、FMDB-第三方SQLite数据库框架1.1 现状1.2 线程安全问题1.2.1 FMDatabase1.2.2 FMDatabaseQueue1.2.3 FMDatabasePool 2、进程间通信2.1 URL Scheme2.2 keyChain2.3 UIPasteboard2.4 UIDocumentInteractionController2.5 local socket2.6 AirDrop2.7 UIActivityViewCont…

Blender 建模小飞机(基础着色、Cycles渲染引擎)

目录 1. 飞机建模1.1 机身1.2 机身表面细分1.3 机翼1.4 尾翼1.5 尾翼镜像1.6 涡轮1.7 添加经纬球1.8 螺旋桨1.9 螺旋桨调整1.10 柱子1.11 柱子镜像1.12 起落架1.13 轮胎1.14 管1.15 镜像1.16 调整飞机角度 2. 着色 渲染2.1 添加地面2.2 飞机着色2.3 其他材质着色2.4 环境纹理2.…

JSDoc 拥抱 Javascript

JSDoc 在 vs code 已经内置了. 可以在 js 文件的开头添加 // ts-check 即可. 在注释中标注来实现一些 ts 的功能. JSDoc 支持以下注解. Types typeparam (or arg or argument)returns (or return)typedefcallbacktemplate Classes Property Modifiers public, private, p…

Go1.21 速览:Go 终于打算进一步支持 WebAssembly 了。。。

大家好&#xff0c;我是煎鱼。 之前写过一篇关于 Go WebAssembly 的相关文章 《一分钟搞明白&#xff01;快速掌握 Go WebAssembly》&#xff0c;今天带来一则新消息。 想着 Go 过去了那么多年了&#xff0c;只在 Go1.11 支持了 WebAssembly1.0 的部分功能&#xff08;js/sysca…

一个实际音视频开发问题!

前言&#xff1a; 大家好&#xff0c;今天给大家分享的内容是关于平时在做音频编解码会遇到的一些问题&#xff0c;比如说&#xff1a; 解码播放的时候&#xff0c;播不出来解码播放的时候&#xff0c;画面有条纹编码的时候&#xff0c;修改分辨率大小&#xff0c;没有反应 这三…

深圳先进院李骁健团队:植入式脑机接口技术向医疗器械转化的问题与挑战

近几年植入式脑机接口技术取得了非常显著的进步&#xff0c;从工程实现能力和服务功能场景来说&#xff0c;脑机接口技术已经达到了临床应用的临界点&#xff0c;在实验室科研成果向临床医疗器械转化过程中将会面临新的挑战。本文章由此出发&#xff0c;首先介绍了脑机接口技术…

工程监测无线中继采集仪的常用功能与设置

工程监测无线中继采集仪的常用功能与设置 LoRA 频道与中心频率 无线中继采集发送仪使用频道来设置不同的射频中心频率。 中心频率 MHz 基频 (频道) &#xff0c;无线中继采集发送仪 的 LoRA 基频已设置为 420 或者 854MHz&#xff0c;可以使用$STRFxxx 重新设置基频。 例如&a…

RT-Thread 1. GD32移植RT-Thread Nano

1. RT-Thread Nano 下载 RT-Thread Nano 是一个极简版的硬实时内核&#xff0c;它是由 C 语言开发&#xff0c;采用面向对象的编程思维&#xff0c;具有良好的代码风格&#xff0c;是一款可裁剪的、抢占式实时多任务的 RTOS。其内存资源占用极小&#xff0c;功能包括任务处理…

Ansys Zemax | 设计抬头显示器时要使用哪些工具 – 第三部分

本文为使用OpticStudio工具设计优化HUD抬头显示器系统的第三部分&#xff0c;主要包含演示了如何使用OpticStudio非序列模式工具正向分析HUD系统的性能以及后续可能的扩展分析。 上两篇文章中(第一部分点此查看&#xff0c;第二部分点此查看)&#xff0c;我们主要介绍了如何以逆…

js 定时去重复显示和隐藏的思路

文章目录 思路一 每秒执行思路二 直接用定时器&#xff0c;但是不每秒思路三 es9 异步迭代异步迭代例子1 直接使用例子2 async await例子3 * yield异步遍历器 --》要想用for await of 必须遍历这个简化 for await of解决1秒出现第一个结果&#xff0c;再过2秒出现第二个&#x…

电脑录屏软件哪个好用?3款屏幕录制大师分享!

案例&#xff1a;如何快速录制电脑屏幕&#xff1f; 【每次录制电脑屏幕都要花费我很多时间&#xff0c;十分影响我工作的效率。有没有什么方法可以帮助我实现电脑快速录屏&#xff1f;蹲一款好用的电脑录屏软件!】 电脑录屏是我们在工作或学习中常常需要使用到的功能&#x…

Swagger介绍及Springboot整合Swagger避坑

Swagger简介 前后端分离 vueSpringboot 后端时代&#xff1a;前端之管理静态页面&#xff0c;html>后端。模板引擎 JSP>后端是主力 前后端分离时代&#xff1a; 后端&#xff1a;后端控制层、服务层、数据访问层【后端团队】前端&#xff1a;前端控制层、视图层【前端…

网易云音乐开发--recommendSong搭建

recommendSong页面头部静态搭建 先新建一个 recommendSong 修改上面的nav部分 写上面的样式。我们现在写在这个框里面放个日期 就是让这个文字基于父元素垂直水平居中 样式搞定 recommendSong日期动态显示 之前我们将recommendSong静态头部做好了&#xff0c;现在我们需…

Springboot idea 中 maven配置问题,找不到依赖:Could not find artifact xxxx

现象&#xff1a;当我们从代码仓拉取新项目时&#xff0c;从该项目的开发同事拿到其maven的settings文件&#xff0c;作为项目的maven配置&#xff0c;为了是能找到工程中所依赖的包&#xff0c;能从远程仓下载下来。 然后本地仓的包&#xff0c;也从同事那边拷贝一份过来&…

二叉搜索树详解及代码实现

目录 一、什么是二叉搜索树 二、二叉搜索树的有关操作 2.1 查找&#xff1a; 2.2插入&#xff1a; 2.3 删除&#xff1a; 2.4 打印 三、二叉搜索树的应用 3.1 K模型&#xff1a; 3.2 KV模型&#xff1a; 四、整体代码&#xff1a; K模型&#xff1a; KV模型&#xff…

物联网应用普及正在改变我们的生活

物联网&#xff08;Internet of Things&#xff0c;IoT&#xff09;指的是通过互联网连接各种物品、设备和传感器&#xff0c;实现物品之间的互联互通&#xff0c;形成智能化、自动化的数据交互和服务体系。简单来说&#xff0c;就是将各类物品通过互联网连接&#xff0c;实现互…

小心!YouTube官方邮件恐是网络钓鱼

在最近的一条推文中&#xff0c;YouTube披露了有关新的网络钓鱼诈骗的详细信息&#xff0c;并呼吁用户小心“no-replyyoutube.com”发送的信件。该骗局一直在使用真实的no-replyyoutube.com电子邮件地址来引诱用户泄露他们的登录凭据。那么这种新的网络钓鱼是如何运作的呢&…

原神QQ机器人BOT搭建教程Ubuntu系统

原神QQ机器人BOT搭建教程Ubuntu系统 大家好我是艾西&#xff0c;今天跟大家分享的是YUAN神qi鹅群机器人bot搭建方式以及详细的操作步骤。跟上艾西的节奏准备发车啦&#xff01; 前言&#xff1a;&#xff08;xxxx即为xxxx&#xff09;&#xff08;zzz即为zzz&#xff09; qi…

【池化技术】基于Apache组件--对象池的介绍与使用

文章目录 一、背景二、对象池有什么特征&#xff1f;三、池的大小选择四、运行原理五、对象管理5.1添加对象5.2借用对象5.3归还对象5.4对象状态 六、对象池的使用6.1 接入6.2 实现线程池工厂6.3 初始化 七、优缺点八、应用场景8.1Redis应用8.2 Web服务器例子8.3 游戏开发种的例…

论文中文翻译——Double-Fetch情况如何演变为Double-Fetch漏洞:Linux内核中的双重获取研究

本论文相关内容 论文下载地址——Web Of Science论文中文翻译——How Double-Fetch Situations turn into Double-Fetch Vulnerabilities A Study of Double Fetches in the Linux Kernel 文章目录 本论文相关内容前言Double-Fetch情况如何演变为Double-Fetch漏洞&#xff1a;…