第五节: 考虑屏幕左侧一台坦克,向水平方向发射一枚炮弹,穿越屏幕。
很自然地,这场景中有坦克和炮弹两个对象,各自有各自坐标,坦克坐标是固定的,而炮弹坐标是变化的。因此有两个结构体类型:Tank和Bullet
在Win_Learn工作区中构建新工程WinStep2,同样选择Win32Application,仍然选择典型的Windows程序。
建好工程以后,在WinStep2.cpp文件开始的地方创建两个结构体类型,以及全局变量。
程序片段13 数据结构和全局变量
以上代码中,包含了将坦克和子弹绘出的函数的声明。
程序片段14 绘出坦克和子弹(版本1)
在绘图的消息中添加对坦克和子弹的绘图调用:
程序片段15 在绘图消息调用对象的绘图
仔细录入程序并运行——嗯?看到子弹的轨迹了,这个不太正常。而且子弹速度太快了,根本没有看到子弹穿越过程。所以第一程序需要一个“擦除上次子弹”的代码,第二程序在连续画子弹时需要有个延迟时间。
代码修订如下:
程序片段16 擦除子弹轨迹残影
程序片段17 加入了擦除轨迹和延时的显示代码
注意程序若要正确编译,还需要声明绘制坦克和绘制子弹的地方加入擦除子弹函数的声明。
运行程序——可以看到子弹正确向右发射了,就是似乎慢了点,这个可以通过调整子弹速度和休眠时间来实现。不过有个严重问题——在绘图过程中,整个程序像死机一样。回忆拓展一曾经提到过,任何消息处理的过程必须是快速的,否则程序将停止响应。在上面的例子里,将整个子弹移动过程都放到了绘制消息,若是子弹飞行1分钟,那么这1分钟内程序什么事都做不了。因此需要通过某种方式,将这一分钟的绘图打散为分解为很多时间碎片,每个时间碎片中绘制子弹,然后下一时间碎片中先擦除子弹再在新的位置中画出子弹。这样就需要用到Windows提供的定时器消息。定时器允许最短每50毫秒间隔触发向WndProc发出WM_TIMER消息,在Win7中这个时间间隔被减少到10毫秒,这样可以提供更佳的时间精确度。此外,子弹的射出最好还是由用户控制,比如按下空格键发射子弹。这样程序需要响应WM_KEY,如果是空格键则发出一枚子弹。程序调整如下:(因为代码较长,可以直接贴到VC++)
// WinStep2.cpp : Defines the entry point for the application.
#include "stdafx.h"
#include "resource.h"
#include <math.h>
#define MAX_LOADSTRING 100struct Tank
{int x,y,v;COLORREF MarkColor;char Mark[10];float faceangle;
};
struct Bullet
{int x,y;int v;float direction;BOOL exist;
};void DrawTank(HDC hdc,struct Tank *pt);
void DrawBullet(HDC hdc,struct Bullet *pb);
void EraseBullet(HDC hdc,struct Bullet *pb);
RECT rtWindow; //整个窗口大小,一开始就设置好,省得每次画图都查一遍。
HPEN TankFrame,TankCannon;
HBRUSH TankBody;
HFONT TankMark;
HPEN BulletFrame;
HBRUSH BulletBody;
struct Tank Tank1;
struct Bullet b[10];// Global Variables:
HINSTANCE hInst; // current instance
HWND hWnd; //主窗口
TCHAR szTitle[MAX_LOADSTRING];
TCHAR szWindowClass[MAX_LOADSTRING]; // Foward declarations of functions included in this code module:
ATOM MyRegisterClass(HINSTANCE hInstance);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)
{int i=0;MSG msg;HACCEL hAccelTable;LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);LoadString(hInstance, IDC_WINSTEP2, szWindowClass, MAX_LOADSTRING);MyRegisterClass(hInstance);hInst = hInstance;hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);hAccelTable = LoadAccelerators(hInstance, (LPCTSTR)IDC_WINSTEP2);GetClientRect(hWnd,&rtWindow);TankFrame=(HPEN)GetStockObject(NULL_PEN);TankCannon=CreatePen(PS_SOLID,5,RGB(0,0,0));TankBody=CreateSolidBrush(RGB(0,255,0));TankMark=CreateFont(18,0,0,0,FW_BOLD,TRUE,FALSE,FALSE,DEFAULT_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH,"新宋体");BulletFrame=(HPEN)GetStockObject(NULL_PEN);BulletBody=(HBRUSH)GetStockObject(BLACK_BRUSH);Tank1.x=rtWindow.left+20;Tank1.y=(rtWindow.bottom+rtWindow.top)/2;Tank1.faceangle=0;Tank1.v=3;strcpy(Tank1.Mark,"008");Tank1.MarkColor=RGB(227,221,73);for(i=0;i<10;i++)b[i].exist=FALSE;ShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);SetTimer(hWnd,1,50,NULL); //设置每50毫秒触发一次WM_TIMER.while (GetMessage(&msg, NULL, 0, 0)) {if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {TranslateMessage(&msg);DispatchMessage(&msg);}}return msg.wParam;
}
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_WINSTEP2);wcex.hCursor = LoadCursor(NULL, IDC_ARROW);wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);wcex.lpszMenuName = (LPCSTR)IDC_WINSTEP2;wcex.lpszClassName = szWindowClass;wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);return RegisterClassEx(&wcex);
}LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{PAINTSTRUCT ps;HDC hdc;int i;switch (message) {case WM_PAINT:hdc = BeginPaint(hWnd, &ps);DrawTank(hdc,&Tank1);for(i=0;i<10;i++)if(b[i].exist)DrawBullet(hdc,&b[i]);EndPaint(hWnd, &ps);break;case WM_KEYDOWN: //当用户按下键if(wParam==' ')//如果是空格键{for(i=0;i<10;i++) //寻找一个可用的子弹if(b[i].exist==FALSE){b[i].exist=TRUE;b[i].direction=Tank1.faceangle;b[i].v=5;b[i].x=Tank1.x+30*cos(b[i].direction);b[i].y=Tank1.y+30*sin(b[i].direction);break;}if(i>=10) //屏幕子弹过多Beep(500,50); //发出滴滴声警告}return 0;case WM_TIMER: //定时器hdc=GetDC(hWnd);for(i=0;i<10;i++)if(b[i].exist){EraseBullet(hdc,&b[i]);b[i].x+=b[i].v*cos(b[i].direction);b[i].y+=b[i].v*sin(b[i].direction);if(b[i].x<rtWindow.left || b[i].x>rtWindow.right || b[i].y<rtWindow.top || b[i].y>rtWindow.bottom)b[i].exist=FALSE;elseDrawBullet(hdc,&b[i]);}ReleaseDC(hWnd,hdc);return 0;case WM_DESTROY:PostQuitMessage(0);break;default:return DefWindowProc(hWnd, message, wParam, lParam);}return 0;
}
void DrawTank(HDC hdc,struct Tank* pt)
{RECT TankArea;POINT CannonEnd;HGDIOBJ OldPen=SelectObject(hdc,TankFrame);HGDIOBJ OldBrush=SelectObject(hdc,TankBody);HGDIOBJ OldFont=SelectObject(hdc,TankMark);COLORREF OldFontColor=SetTextColor(hdc,pt->MarkColor);TankArea.left=pt->x-18,TankArea.right=pt->x+18;TankArea.top=pt->y-18,TankArea.bottom=pt->y+18;RoundRect(hdc,pt->x-20,pt->y-20,pt->x+20,pt->y+20,4,4); DrawText(hdc,pt->Mark,-1,&TankArea,DT_CENTER | DT_SINGLELINE | DT_VCENTER);SelectObject(hdc,TankCannon);MoveToEx(hdc,pt->x,pt->y,NULL);CannonEnd.x=pt->x+30*cos(pt->faceangle);CannonEnd.y=pt->y+30*sin(pt->faceangle);LineTo(hdc,CannonEnd.x,CannonEnd.y);SetTextColor(hdc,OldFontColor);SelectObject(hdc,OldFont);SelectObject(hdc,OldBrush);SelectObject(hdc,OldPen);
}void DrawBullet(HDC hdc,struct Bullet* pb)
{HGDIOBJ OldBrush=SelectObject(hdc,BulletBody);HGDIOBJ OldPen=SelectObject(hdc,BulletFrame);Ellipse(hdc,pb->x-2,pb->y-2,pb->x+2,pb->y+2);SelectObject(hdc,OldBrush);SelectObject(hdc,OldPen);
}void EraseBullet(HDC hdc,struct Bullet* pb)
{HGDIOBJ oldBrush=SelectObject(hdc,GetStockObject(WHITE_BRUSH));HGDIOBJ oldPen=SelectObject(hdc,GetStockObject(WHITE_PEN));Rectangle(hdc,pb->x-2,pb->y-2,pb->x+2,pb->y+2);SelectObject(hdc,oldPen);SelectObject(hdc,oldBrush);
}
程序片段18 坦克发射子弹代码
以上程序人机控制较少。还可通过鼠标键来移动坦克的炮口,使用光标键来控制坦克自身的移动。为实现这个功能,需要消息函数中响应鼠标移动事件和响应新的按键事件;另外使用空格键发射子弹不如使用鼠标左键发射子弹来的合理,所以前述程序的WndProc过程可以修改如下:
程序片段19 修改一:增加EraseTank声明
程序片段20 修改二:增加按键响应。这里给出上箭头,其他按键自行补上
程序片段21 修改三:增加鼠标移动消息响应
程序片段22 修改四:增加左键按下响应
程序片段23 修改五:增加擦除坦克代码
订正:
更正为:
思考:若是在子弹移动过程中碰到物体,比如敌方坦克,或者打到墙上,如何处理?这类问题叫做碰撞检测。需要对系统中每个对象保留exist(是否有效)状态,或者life(生命值)。举例而言,若考虑一个经典的坦克大战游戏,包括:砖墙,敌方坦克,己方坦克,草丛,双方子弹,则需要扩充前文的struct结构体,新增加Brick数组,Grass数组,Bullet数组。然后在WM_TIME中依次对这些可变对象做一对一检测。另外坦克是不能穿越砖墙的,所以,在WM_KEYDOWN中也需要对坦克移动作出限制。只需要简单地堆砌代码即可将程序变得更生动有趣。