先别急着看源码,首先要解决的问题是程序框架。
VC只是一个开发环境,支持的是C++。Windows下的程序,当然是用Windows API(Application Programming Interface)写成的。 VC提供了一个类库MFC,如同课本中介绍的一样,“点几下鼠标即可生成一个程序”。控件靠拖动即可生成。很方便,但也是最大的缺陷。隐藏了太多东西。我的电脑中VC的一个链接库有点问题,无法运行MFC程序,API是可以运行的。我用API来做。
既然是API,默认生成的结构是:主函数WinMain,注册类MyRegisterClass,初始化InitInstance,消息处理WndProc。
在InitInstance中处理变量的初始设置。
在WndProc中实现游戏的流程控制。
以前做《俄罗斯方块》,包括博客中贴的《扫雷》,都是用一个变量表示当前的状态。这是一种状态机的转换,很简单,一个状态迁移到另一个状态。不同状态下,有不同的消息处理。我用全局变量iGameState来记录。
LPARAM lParam)
{
switch (message)
{
case 消息ID:
switch (当前状态)
{
case 游戏开始菜单:
break ;
case 游戏进行中:
break ;
}
break ;
case 消息ID:
break ;
}
}
这个游戏用了多少种状态?
#define GAME_STATE_MENU 0
#define GAME_STATE_MAIN 1
#define GAME_STATE_END 2
#define GAME_STATE_END_NEXT 3
先看游戏的第一个状态GAME_STATE_MENU:用户点击“开始”,进入游戏。
很简单,建一个按钮,处理它的单击消息,修改当前游戏状态。
创建按钮可以用CreateWindow,我没这样做。
画一个图片(我所有的图片资源都是BMP位图),包含两幅图像,一个是按钮正常情况下显示,一个是鼠标移动到上面后显示,并列排放。
怎样显示图片呢?每个图片都由一个位图句柄表示。先把图片文件加载到工程资源resources中,在加载到一个位图句柄。屏幕作为一个显示设备,需要获取他的Device Contexts的句柄。MFC中的CDC就是对设备句柄HDC的封装。Device Contexts,有时候翻译成“设备上下文”,中英文的意思还是有差别,直接叫DC。显示的时候,先画到一个临时的DC,再画到屏幕上。
这个初始化、显示的功能,放到一个类中。这样的小游戏,如果不用类来组织,也能实现功能,但是有大缺点,代码不易维护。一个基本的思想就是:尽量模块化。
void MYBITMAP::Init(HINSTANCE hInstance,int iResource,int row,int col)
{
BITMAP bm;
inum=row;
jnum=col;
hBm=LoadBitmap(hInstance,MAKEINTRESOURCE(iResource));
GetObject(hBm,sizeof(BITMAP),&bm);
width=bm.bmWidth/inum;
height=bm.bmHeight/jnum;
}
简单说明一下:hInstance程序实例句柄,iResource位图资源ID,row,col,把图像分割成几行几列。函数记录了这张图像的图像句柄,有几行几列,分割后单幅图的宽高。
这是图像的信息记录完成,以下要设置“屏幕DC”,“临时DC”。
void MYBITMAP::SetDC(HDC hdest,HDC hsrc)
{
hdcdest=hdest;
hdcsrc=hsrc;
}
从临时DC显示到屏幕上,参数为坐标、第几幅图像:
void MYBITMAP::Show(int x,int y,int iFrame)
{
xpos=x;
ypos=y;
SelectObject(hdcsrc,hBm);
BitBlt(hdcdest,xpos,ypos,width,height,hdcsrc,iFrame*width,0,SRCCOPY);
}
说明:显示的时候记录了它的坐标,SelectObject将句柄装入临时DC,再BitBlt到屏幕上。
图像的显示问题解决了。下面要判断鼠标坐标,当鼠标移到其上时,改变图像,下面这两个函数入参为鼠标坐标,判断是否在图片范围内,设置framenow。然后,重绘屏幕,根据framenow显示指定图像即可。
int MYBITMAP::MouseOut(int x,int y)
{
if(x<xpos || x>(xpos+width) || y<ypos || y>(ypos+height))
framenow=0;
return 1;
}
int MYBITMAP::MouseOver(int x,int y)
{
if(x<xpos || x>(xpos+width) || y<ypos || y>(ypos+height))
return 0;
framenow=1;
return 1;
}
看一下这个状态下的消息处理:重绘消息,单击消息,鼠标移动消息
重绘消息:画一个背景,画一个“开始”按钮。
hdcwindow = BeginPaint(hWnd, & ps);
switch (iGameState)
{
case GAME_STATE_MENU:
SelectObject(hdcscreen,hback);
bBack.ShowCenter( 0 );
bBegin.ShowCenter( 200 );
break ;
单击消息:如果鼠标单击了开始按钮,迁移状态。
switch (iGameState)
{
case GAME_STATE_MENU:
if (bBegin.DetectMouseUp(LOWORD(lParam),HIWORD(lParam)))
{
iGameState = GAME_STATE_MAIN;
// 加载地图
gmap.LoadFirstMap();
gamecatch.Init();
myclock.Begin(hWnd, 100 ,TIME_MATCH);
// 清除当前金钱数量
gmap.ClearMoney();
}
break ;
鼠标移动消息:检测鼠标坐标,改变图片当前帧(第几幅):
switch (iGameState)
{
case GAME_STATE_MENU:
bBegin.DetectMouseMove(LOWORD(lParam),HIWORD(lParam));
break ;
这样,游戏的第一个状态就完成了。且听下回分解。
附:游戏主文件源码
//
#include " stdafx.h "
#include " resource.h "
#include " bitmaptool.h "
#include " filereport.h "
#include " gamemap.h "
#include " catch.h "
#include " myclock.h "
#include " mybutton.h "
#include " myequip.h "
#define MAX_LOADSTRING 100
#define GAME_STATE_MAIN 1
#define GAME_STATE_MENU 0
#define GAME_STATE_END 2
#define GAME_STATE_END_NEXT 3
// FILEREPORT f1("main.txt");
// Global Variables:
HINSTANCE hInst; // current instance
TCHAR szTitle[MAX_LOADSTRING]; // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // The title bar text
MYBITMAP bBegin(GAMEWIDTH,GAMEHEIGHT);
MYBITMAP bMap(GAMEWIDTH,GAMEHEIGHT);
MYBITMAP bEarth(GAMEWIDTH,GAMEHEIGHT);
MYBITMAP bObject(GAMEWIDTH,GAMEHEIGHT);
MYBITMAP bBack(GAMEWIDTH,GAMEHEIGHT);
MYBITMAP bFire(GAMEWIDTH,GAMEHEIGHT);
MYBITMAP bBombAni(GAMEWIDTH,GAMEHEIGHT);
MYEQUIP bEquip;
struct EQUIP
{
int iPrice;
char info[ 100 ];
};
struct EQUIP equip[] = {
{ 100 , " 炮:能炸掉抓到的物品 100元 " },
{ 200 , " 体力:加快拉动物品的速度 200元 " }};
MYBUTTON btNext;
HDC hdcwindow,hdcscreen,hdcmem;
HBITMAP hback;
HFONT hf[ 5 ];
// 游戏状态
int iGameState = GAME_STATE_MENU;
GAMEMAP gmap;
GAMECATCH gamecatch;
MYCLOCK myclock;
//
void InitFont()
{
char temp[ 10 ] = { 0 };
int i;
for (i = 0 ;i <= sizeof (hf) / sizeof (HFONT);i ++ )
{
sprintf(temp, " black%d " ,i);
hf[i] = CreateFont( 10 * (i + 2 ), 0 , 0 , 0 ,FW_HEAVY, 0 , 0 , 0 ,GB2312_CHARSET,OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH | FF_DONTCARE,temp);
}
}
// Foward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int );
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM);
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// TODO: Place code here.
MSG msg;
HACCEL hAccelTable;
// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_GAMEGOLD, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// Perform application initialization:
if ( ! InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_GAMEGOLD);
// Main message loop:
while (GetMessage( & msg, NULL, 0 , 0 ))
{
if ( ! TranslateAccelerator(msg.hwnd, hAccelTable, & msg))
{
TranslateMessage( & msg);
DispatchMessage( & msg);
}
}
return msg.wParam;
}
//
// FUNCTION: MyRegisterClass()
//
// PURPOSE: Registers the window class.
//
// COMMENTS:
//
// This function and its usage is only necessary if you want this code
// to be compatible with Win32 systems prior to the 'RegisterClassEx'
// function that was added to Windows 95. It is important to call this function
// so that the application will get 'well formed' small icons associated
// with it.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof (WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0 ;
wcex.cbWndExtra = 0 ;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_GAMEGOLD);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1 );
// wcex.lpszMenuName = (LPCSTR)IDC_GAMEGOLD;
wcex.lpszMenuName = NULL;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
return RegisterClassEx( & wcex);
}
//
// FUNCTION: InitInstance(HANDLE, int)
//
// PURPOSE: Saves instance handle and creates main window
//
// COMMENTS:
//
// In this function, we save the instance handle in a global variable and
// create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
int x,y;
hInst = hInstance; // Store instance handle in our global variable
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0 , CW_USEDEFAULT, 0 , NULL, NULL, hInstance, NULL);
if ( ! hWnd)
{
return FALSE;
}
// 窗口居中
x = GetSystemMetrics(SM_CXSCREEN);
y = GetSystemMetrics(SM_CYSCREEN);
MoveWindow(hWnd,(x - GAMEWIDTH) / 2 ,(y - GAMEHEIGHT) / 2 ,GAMEWIDTH,GAMEHEIGHT, false );
hdcwindow = GetDC(hWnd);
hdcmem = CreateCompatibleDC(hdcwindow);
hdcscreen = CreateCompatibleDC(hdcwindow);
gmap.hdc = hdcscreen;
myclock.SetDC(hdcscreen);
ReleaseDC(hWnd,hdcwindow);
// 设置字体
InitFont();
gmap.SetFont(hf, sizeof (hf));
// 加载图像
hback = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP_NULL));
bBack.Init(hInst,IDB_BITMAP_NULL, 1 , 1 );
bBack.SetDC(hdcscreen,hdcmem);
bBegin.Init(hInst,IDB_BITMAP_BEGIN, 2 , 1 );
bBegin.SetDC(hdcscreen,hdcmem);
bMap.Init(hInst,IDB_BITMAP_GROUND, 1 , 1 );
bMap.SetDC(hdcscreen,hdcmem);
bEarth.Init(hInst,IDB_BITMAP_EARTH, 3 , 1 );
bEarth.SetDC(hdcscreen,hdcmem);
bObject.Init(hInst,IDB_BITMAP_OBJECT, 5 , 1 );
bObject.SetDC(hdcscreen,hdcmem);
bEquip.Init(hInst,IDB_BITMAP_EQUIP, 3 , 1 );
bEquip.SetDC(hdcscreen,hdcmem);
bFire.Init(hInst,IDB_BITMAP_FIRE, 1 , 1 );
bFire.SetDC(hdcscreen,hdcmem);
bBombAni.Init(hInst,IDB_BITMAP_BOMB, 4 , 1 );
bBombAni.SetDC(hdcscreen,hdcmem);
// 按钮
btNext.Init( " 下一关 " ,RGB( 0 , 0 , 255 ),RGB( 0 , 128 , 255 ),
RGB( 255 , 255 , 0 ),RGB( 255 , 255 , 128 ),hf[ 3 ]);
btNext.SetDC(hdcscreen,hdcmem);
// 得到总关数
gmap.GetMatchNum();
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// FUNCTION: WndProc(HWND, unsigned, WORD, LONG)
//
// PURPOSE: Processes messages for the main window.
//
// WM_COMMAND - process the application menu
// WM_PAINT - Paint the main window
// WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
int i;
PAINTSTRUCT ps;
TCHAR szHello[MAX_LOADSTRING];
LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);
switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
break ;
case IDM_EXIT:
DestroyWindow(hWnd);
break ;
default :
return DefWindowProc(hWnd, message, wParam, lParam);
}
break ;
case WM_PAINT:
hdcwindow = BeginPaint(hWnd, & ps);
switch (iGameState)
{
case GAME_STATE_MENU:
SelectObject(hdcscreen,hback);
bBack.ShowCenter( 0 );
bBegin.ShowCenter( 200 );
break ;
case GAME_STATE_MAIN:
bMap.ShowCenter( 0 );
bEarth.ShowLoop( 0 , 80 ,GAMEWIDTH,GAMEHEIGHT,gmap.GetMatchNow());
for (i = 0 ;i < gmap.iObjectNum;i ++ )
{
if (gmap.objectarray[i].id <= 0 )
{
continue ;
}
bObject.ShowNoBack(gmap.objectarray[i].x,
gmap.objectarray[i].y,
gmap.objectarray[i].id - 1 );
}
gamecatch.DrawAngle(hdcscreen);
gmap.ShowMoneyNow();
bFire.ShowNoBackLoop( 389 , 46 , 0 ,gmap.iFireNum);
bBombAni.ShowAni();
myclock.Show();
break ;
case GAME_STATE_END:
gmap.GameEnd();
break ;
case GAME_STATE_END_NEXT:
gmap.GameNext();
bEquip.ShowFramesNoBack( 80 , 150 , 2 );
bEquip.ShowFrameLast();
if (bEquip.gridnow >= 0 && bEquip.gridnow <= 2 )
{
TextOut(hdcscreen, 80 , 220 ,equip[bEquip.gridnow].info,
strlen(equip[bEquip.gridnow].info));
}
btNext.Show( 350 , 50 );
break ;
}
BitBlt(hdcwindow, 0 , 0 ,GAMEWIDTH,GAMEHEIGHT,hdcscreen, 0 , 0 ,SRCCOPY);
EndPaint(hWnd, & ps);
break ;
case WM_MOUSEMOVE:
switch (iGameState)
{
case GAME_STATE_MENU:
bBegin.DetectMouseMove(LOWORD(lParam),HIWORD(lParam));
break ;
case GAME_STATE_END_NEXT:
btNext.DetectMouseMove(LOWORD(lParam),HIWORD(lParam));
bEquip.DetectMouseOverGrid(LOWORD(lParam),HIWORD(lParam));
break ;
}
InvalidateRect(hWnd,NULL, false );
break ;
case WM_LBUTTONUP:
switch (iGameState)
{
case GAME_STATE_MENU:
if (bBegin.DetectMouseUp(LOWORD(lParam),HIWORD(lParam)))
{
iGameState = GAME_STATE_MAIN;
// 加载地图
gmap.LoadFirstMap();
gamecatch.Init();
myclock.Begin(hWnd, 100 ,TIME_MATCH);
// 清除当前金钱数量
gmap.ClearMoney();
}
break ;
case GAME_STATE_END_NEXT:
if (btNext.DetectMouseUp(LOWORD(lParam),HIWORD(lParam)))
{
iGameState = GAME_STATE_MAIN;
gmap.LoadMap();
gamecatch.Init();
myclock.Begin(hWnd, 100 ,TIME_MATCH);
}
if (bEquip.DetectMouseOverGrid(LOWORD(lParam),HIWORD(lParam)))
{
bEquip.UseEquip();
// 使用了道具
gmap.UseEquip(equip[bEquip.gridnow].iPrice,bEquip.gridnow,gamecatch);
}
break ;
}
InvalidateRect(hWnd,NULL, false );
break ;
case WM_KEYDOWN:
switch (iGameState)
{
case GAME_STATE_MAIN:
gamecatch.KeyProc(wParam);
gmap.KeyProc(wParam);
InvalidateRect(hWnd,NULL, false );
break ;
}
break ;
case WM_TIMER:
switch (iGameState)
{
case GAME_STATE_MAIN:
if (myclock.Dec())
{
// 时间到
iGameState = GAME_STATE_END;
myclock.Begin(hWnd, 100 , 3 );
InvalidateRect(hWnd,NULL, false );
break ;
}
gamecatch.TurnAngle();
if ( 1 == gamecatch.iState)
{
// 在叉子伸出的过程中检测是否抓到物品
if (gmap.DetectCatch(gamecatch.xpos,gamecatch.ypos,gamecatch.iAngle))
{
// 如果抓到物品
gamecatch.SetState( 2 ); // 开始收回
gmap.SetSpeedBack(gamecatch);
}
}
// 移动物品
gmap.MoveObject(gamecatch.xpos,gamecatch.ypos,gamecatch.iAngle);
InvalidateRect(hWnd,NULL, false );
break ;
case GAME_STATE_END:
if (myclock.Dec())
{
// 动画结束
gmap.GameEndAniOver();
// 点击进入下一关
if (gmap.CanNextMatch())
{
bEquip.ClearUseFlag();
iGameState = GAME_STATE_END_NEXT;
}
else
{
iGameState = GAME_STATE_MENU;
}
}
InvalidateRect(hWnd,NULL, false );
break ;
}
break ;
case WM_DESTROY:
DeleteDC(hdcmem);
DeleteDC(hdcscreen);
PostQuitMessage( 0 );
break ;
default :
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0 ;
}
// Mesage handler for about box.
LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_INITDIALOG:
return TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return TRUE;
}
break ;
}
return FALSE;
}