记得自己小时候曾经玩过这个小游戏,如今长大了,有了一定的知识就可以实现一些小时候未曾想过的事情,或者说梦想去做的事情!虽然这次实现的小游戏比较简单,但是也算游戏吧,比较自己还不是这方面的大神。
如果想要用Win32项目制作一个小游戏,那么首先你要对对C/C++语言熟悉,对Windows编程有一定的了解。其实还有一点就是你在开始制作小游戏之前,你要知道你的游戏的逻辑结构。对于一般的小游戏结构可以用下图的结构表示。
Game_main()是游戏的主体函数,不过它在主事件循环每次处理windows消息之后就会调用一次,要注意每次进入和终止该函数的时候,自动变量是瞬间变化的,如果想要一直使用的数据,建议使用全局变量,或者也可以设置为局部静态变量。
恰好最近正在学Windows编程,想试试Windows是否真的适合制作游戏编程。最后在看了《Windows游戏编程大师技巧》之后,决定将里面的FreakOut游戏实现一下。
由于平时用习惯了C/C++编程可能开始会有些不习惯,但是用多了就会发现其实最重要的就是要理解Windows编程中的消息驱动原理,简单的说其实就是Windows有其自己的基于事件的中枢神经系统。当我们按下一个键,就会有消息从按键事件中创建并且在系统中散播,直到有程序检出这个消息并使用它。当然Windows编程中的要注意的点还是挺多的,字符编码就是其中一个,不过解决起来的很简单(例如著名的“L”字符和TCHAR)。还有一点就是Windows编程中的主函数WinMain(),如果第一眼看到他,觉得很复杂,但是WinMain并不需要是一个又大又复杂的应用代码大杂烩。
相信大家都玩过FreakOut游戏(俗称打砖块游戏),游戏主要是利用小球消灭所有的砖块,所有的砖块大小相等,小球每次弹到砖块上都会发生反弹,按照物理上的说法就是发生弹性碰撞,当然,小球只能在屏幕(其实说窗口更准确)内运动,所以当小球碰到窗口的边界的时候就会发生反弹,假设小球做的是直线运动,根据速度的分解可知可以将小球的速度正交分解,分成一个X方向上的运动和一个Y方向上的运动。再碰到边界或者是砖块或者你跳板之后都会发生反弹,所以只要保证某一个方向上的速度不变,另一个方向上的速度反向即可。(不过我在具体实现的过程中没有完全按照物理学规律来制作,在碰到砖块或者跳板的时候让他们的速度有一点变化,这样有时候可以避免小球一直做同样的运动)。
下面就通过代码来解释:
首先来看下头文件FreakOut.h:
#pragma once #include "resource.h" //define for Windows 窗口大小定义 #define WINDOW_WIDTH 640 #define WINDOW_HEIGHT 480 //state for game loop 游戏的循环逻辑状态 #define GAME_STATE_INIT 0 #define GAME_STATE_START_LEVEL 1 #define GAME_STATE_RUN 2 #define GAME_STATE_SHUTDOWN 3 #define GAME_STATE_EXIT 4 //block defines 有关砖块的定义 #define NUM_BLOCK_ROWS 2 #define NUM_BLOCK_COLUMNS 8 #define BLOCK_WIDTH 64 #define BLOCK_HEIGHT 16 #define BLOCK_ORIGIN_X 8 #define BLOCK_ORIGIN_Y 8 #define BLOCK_X_GAP 80 #define BLOCK_Y_GAP 32 #define BLOCK_COLOR RGB(222,200,125) //ball defines 有关小球的定义 #define BALL_COLOR RGB(255,0,0) #define BALL_START_Y (WINDOW_HEIGHT/2) #define BALL_SIZE 14 //paddle defines 跳板的定义 #define PADDLE_START_X (WINDOW_WIDTH/2 - 16) #define PADDLE_START_Y (WINDOW_HEIGHT - 32 ) #define PADDLE_WIDTH 50 #define PADDLE_HEIGHT 8 #define PADDLE_COLOR RGB(0,0,255) //these read the keyboard asynchronously 键盘的相应按键 #define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code)&0x8000)?1:0) #define KEY_UP(vk_code) ((GetAsyncKeyState(vk_code)&0x8000)?0:1) //set bounds 设置关卡数 #define Game_Count 3 //basic unsigned types typedef unsigned short USHORT; typedef unsigned short WORD; typedef unsigned char UCHAR; typedef unsigned char BYTE;头文件主要是一些游戏中一些对象的初始值的有关定义。可以按照个人喜好设置。
下面来看一下内部的实现逻辑,先贴上我的代码FreakOut.cpp:
// FreakOut.cpp : 定义应用程序的入口点。 // #include "stdafx.h" #include "FreakOut.h" #define MAX_LOADSTRING 100 // 全局变量: HINSTANCE hInst; // 当前实例 TCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本 TCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名 HWND main_window_handle = NULL; //save the window handle HINSTANCE main_instance = NULL; //save the instance int game_state = GAME_STATE_INIT; //starting state int paddle_x = 0, paddle_y = 0; //tracks position of handle int ball_x = 0, ball_y = 0; //tracks position of ball int ball_dx = 0, ball_dy = 0; //velocity of ball int score = 0; //the score int level = 1; //the Current level int block_hit = 0; //tracks number of block hit DWORD start_clock_count = 0, now_clock = 0; //used for timing UCHAR blocks[NUM_BLOCK_ROWS][NUM_BLOCK_COLUMNS];//this contains the game grid data // 此代码模块中包含的函数的前向声明: ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); int Game_Init(void *parms = NULL); int Game_Shutdown(void *parms = NULL); int Game_Main(void *parms = NULL); DWORD Start_Clock(); DWORD Get_Clock(void); DWORD Wait_Clock(DWORD ); void Change_Mode(HWND, DWORD, int); int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // TODO: 在此放置代码。 MSG msg; HACCEL hAccelTable; WNDCLASSEX winclass; HWND hwnd; HDC hdc; PAINTSTRUCT ps; // 初始化全局字符串 LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hInstance, IDC_FREAKOUT, szWindowClass, MAX_LOADSTRING); /*MyRegisterClass(hInstance);*/ //注册窗口类 winclass.cbSize = sizeof(WNDCLASSEX); winclass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; winclass.lpfnWndProc = WndProc; winclass.cbClsExtra = 0; winclass.cbWndExtra = 0; winclass.hInstance = hInstance; winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); winclass.hCursor = LoadCursor(NULL, IDC_ARROW); winclass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); winclass.lpszMenuName = MAKEINTRESOURCE(IDC_FREAKOUT); winclass.lpszClassName = szWindowClass; winclass.hIconSm = LoadIcon(winclass.hInstance, MAKEINTRESOURCE(IDI_SMALL)); if (!RegisterClassEx(&winclass)) //注册窗口类是否成功 { return FALSE; } int cxNonClient = GetSystemMetrics(SM_CXBORDER) * 2 + 10; int cyNonClient = GetSystemMetrics(SM_CYBORDER) + GetSystemMetrics(SM_CYCAPTION) + 10; //Create the window, note the use of WS_POPUP hwnd = CreateWindowEx(NULL,szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, 0, 0, WINDOW_WIDTH + cxNonClient, WINDOW_HEIGHT + cyNonClient, NULL, NULL, hInstance, NULL); // 执行应用程序初始化 if (!hwnd) { return FALSE; } ShowWindow(hwnd, nCmdShow); // WM_SIZE消息是由 ShowWindow函数发出的 UpdateWindow(hwnd); //hide mouse //ShowCursor(FALSE); //save the window handle and instance in a global main_window_handle = hwnd; main_instance = hInstance; hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_FREAKOUT)); //perform all game console specific initialization Game_Init(); // 主消息循环: while (true) { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { //test if this is a quit msg if (msg.message == WM_QUIT) { break; } //translate any accelerator keys TranslateMessage(&msg); //send the message to the window proc DispatchMessage(&msg); } //main game processing goes here Game_Main(); Sleep(10); } return (int) msg.wParam; } /*DRAW Function ********** */ int Draw_Rectangle(int x1, int y1, int x2, int y2, int color) { //this function users Win32 API to draw a filled rectangle HBRUSH hbrush; HDC hdc; RECT rect; SetRect(&rect, x1, y1, x2, y2); hbrush = CreateSolidBrush(color); hdc = GetDC(main_window_handle); //SelectObject(hdc, hbrush); FillRect(hdc, &rect, hbrush); ReleaseDC(main_window_handle, hdc); DeleteObject(hbrush); return 1; } int Draw_Ball(int x1, int y1, int x2, int y2, int color) { HDC hdc; HBRUSH hbrush; hbrush = CreateSolidBrush(color); hdc = GetDC(main_window_handle); SelectObject(hdc, hbrush); Ellipse(hdc, x1, y1, x2, y2); ReleaseDC(main_window_handle, hdc); DeleteObject(hbrush); return 1; } int DrawText_GUI(TCHAR *text, int x, int y, int color) { HDC hdc; hdc = GetDC(main_window_handle); //set color for the text up SetTextColor(hdc, color); //set background mode to transparent so black isn't copied SetBkMode(hdc, TRANSPARENT); //设置为透明的 //draw the text TextOut(hdc, x, y, text, lstrlen(text)); //release the dc ReleaseDC(main_window_handle, hdc); return 1; } /*GAME PROGRAMMING CONSOLE FUNCTIONS *********************************/ void Init_Blocks(void) { //Initialize the block field int row = 0, col = 0; for (row = 0; row < level*NUM_BLOCK_ROWS; row++) { for (col = 0; col < level*NUM_BLOCK_COLUMNS; col++) { blocks[row][col] = 1; //初始化 } } } void Draw_Blocks(void) { //this function draws all the blocks in row major form int x1 = BLOCK_ORIGIN_X; int y1 = BLOCK_ORIGIN_Y; int row = 0, col = 0; //draw all the blocks for ( row = 0; row < level*NUM_BLOCK_ROWS; row++) { x1 = BLOCK_ORIGIN_X; for (col = 0; col < level*NUM_BLOCK_COLUMNS; col++) { if (blocks[row][col] != 0) { Draw_Rectangle(x1, y1, x1 + BLOCK_WIDTH, y1 + BLOCK_HEIGHT, BLOCK_COLOR); } //advance column position x1 += BLOCK_X_GAP; } //advance to next row position y1 += BLOCK_Y_GAP; } } void Process_Ball(void) { //this function tests if the ball has hit a block or the paddle if so, the ball is bounced //and the block is removed from the playfield note:very chessy collision algorithm:) //first test for ball block collisions //the algorith basically test the ball against each black's bounding box this is inefficient. //but easy to implement,later we'll see a better way //current rendering position int x1 = BLOCK_ORIGIN_X; int y1 = BLOCK_ORIGIN_Y; //computer center of ball int ball_cx = ball_x + (BALL_SIZE / 2); int ball_cy = ball_y + (BALL_SIZE / 2); //test the ball has hit the paddle if (ball_y>(WINDOW_HEIGHT/2)&&ball_dy>0) { int x = ball_x + (BALL_SIZE / 2); int y = ball_y + (BALL_SIZE / 2); //test for collision with paddle if ((x <= (paddle_x + PADDLE_WIDTH )) && (x >= paddle_x ) && (y >= paddle_y )&& (y <= paddle_y + PADDLE_HEIGHT/2)) { if ( ((ball_dx)<0 && ((-ball_dx)>ball_dy))|| ((ball_dx>0) && (ball_dx>ball_dy)) ) { //test if there are no blocks, if so send a message to game loop to start another level if (block_hit >= (level*NUM_BLOCK_ROWS)*(level*NUM_BLOCK_COLUMNS)) { level++; if (level>Game_Count) { MessageBox(main_window_handle, L"Congraculation, you pass all customs!", L"FreakOut", MB_OK); game_state = GAME_STATE_INIT; } else { game_state = GAME_STATE_START_LEVEL; } } //make a little noise MessageBeep(MB_OK); return; } else { //relect ball ball_dy = -ball_dy; //puch ball out of paddle since it made contact ball_y = ball_y + ball_dy; //add a little english to ball based on mation of paddle if (KEY_DOWN(VK_RIGHT)) { ball_dx -= rand() % 3; //change speed } else if (KEY_UP(VK_LEFT)) { ball_dx += rand() % 3; } else { ball_dx += (-1 + rand() % 3); } //test if there are no blocks, if so send a message to game loop to start another level if (block_hit >= (level*NUM_BLOCK_ROWS)*(level*NUM_BLOCK_COLUMNS)) { level++; if (level>Game_Count) { MessageBox(main_window_handle, L"Congraculation, you pass all customs!", L"FreakOut", MB_OK); game_state = GAME_STATE_INIT; } else { game_state = GAME_STATE_START_LEVEL; } } //make a little noise MessageBeep(MB_OK); return; } } } //now scan thru all the blocks and see of ball hit blocks int row, col; for (row = 0; row < level*NUM_BLOCK_ROWS; row++) { x1 = BLOCK_ORIGIN_X; for (col = 0; col < level*NUM_BLOCK_COLUMNS; col++) { if (blocks[row][col]) { //test ball against bounding box of block if ((ball_cx >= x1) && (ball_cx <= x1 + BLOCK_WIDTH) && (ball_cy >= y1) && (ball_cy <= y1 + BLOCK_HEIGHT)) { //remove blocks[row][col] = 0; //increment global block counter, so we know when to start another level up block_hit++; //bounce the ball ball_dy = -ball_dy; //add a little english ball_dx += (-1 + rand() % 3); //make a little noise MessageBeep(MB_OK); //add some points score += 5 * (level + (abs)(ball_dx)); return; } } //advance column position x1 += BLOCK_X_GAP; } //advance row position y1 += BLOCK_Y_GAP; } } int Game_Init(void *parms) { //this function is where you do all the initialization for your game return 1; } int Game_Shutdown(void *parms) { //this function is where you shutdown your game and release all resources //that you allocated return 1; } int Game_Main(void *parms) { //this is the workhorse of your game it will be called continuously in real-time //this is like main() in C all the calls for you game ge here! TCHAR buffer[80]; //what state is the game in? if (game_state == GAME_STATE_INIT) { //send the random number generator so game is different each play srand((unsigned)time(NULL)); //send the paddle position here to the middle buttom paddle_x = PADDLE_START_X; paddle_y = PADDLE_START_Y; //set ball position and velocity ball_x = 8 + rand() % (WINDOW_WIDTH - 16); ball_y = BALL_START_Y; ball_dx = -4 + rand() % (8 + 1); ball_dy = 6 + rand() % 2; //transition to start level state game_state = GAME_STATE_START_LEVEL; } else if (game_state == GAME_STATE_START_LEVEL) { //get a new level ready to run //initialize the blocks Init_Blocks(); //reset block counter block_hit = 0; //transition to run state game_state = GAME_STATE_RUN; } else if (game_state == GAME_STATE_RUN) { //start the timing clock Start_Clock(); //clear drawing surface for next frame of animation Draw_Rectangle(0, 0, WINDOW_WIDTH - 1, WINDOW_HEIGHT - 1, RGB(255,255,255)); //move to paddle if (KEY_DOWN(VK_RIGHT)) { //move paddle to right paddle_x += 8; //make sure that the paddle doesn't go off screen if (paddle_x > WINDOW_WIDTH - PADDLE_WIDTH) { paddle_x = WINDOW_WIDTH - PADDLE_WIDTH; } } if (KEY_DOWN(VK_LEFT)) { //move paddle to left paddle_x -= 8; //make sure that the paddle doesn't go off screen if (paddle_x < 0) { paddle_x = 0; } } //draw blocks Draw_Blocks(); //move the ball ball_x += ball_dx; ball_y += ball_dy; //keep ball on screen, if the ball hits the edge of screen then //bounce it by reflecting its velocity if ((ball_x < 0)||(ball_x > WINDOW_WIDTH - BALL_SIZE/2)) { //reflect x-axis velocity ball_dx = -ball_dx; //update position ball_x += ball_dx; } //now y-axis if (ball_y < 0) { //relect y-axis velocity ball_dy = -ball_dy; //update position ball_y += ball_dy; } else if (ball_y >WINDOW_HEIGHT - BALL_SIZE/2) { //reflect y-axis velocity ball_dy = -ball_dy; //update position ball_y += ball_dy; //minus the score score -= 100; } //now watch out for ball velocity getting out of hand if (ball_dx > 8) { ball_dx = 8; } else if (ball_dx < -8) { ball_dx = -8; } //test if ball hit any blocks or the paddle Process_Ball(); //draw the paddle Draw_Rectangle(paddle_x, paddle_y, paddle_x + PADDLE_WIDTH, paddle_y + PADDLE_HEIGHT, PADDLE_COLOR); //draw the ball //Draw_Rectangle(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, BALL_COLOR); Draw_Ball(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, BALL_COLOR); //check if it has reword //now_clock = Get_Clock(); //Change_Mode(main_window_handle, 15000, score); //draw the info wsprintf(buffer, TEXT("Game Scroe %d Level %d"), score, level); DrawText_GUI(buffer, 8, WINDOW_HEIGHT - 50, 127); //sync to 30 fps Wait_Clock(30); //Sleep(30); //check if user is trying to exit if (KEY_DOWN(VK_ESCAPE)) { //send to message to windows to exit PostMessage(main_window_handle, /*WM_QUIT*/WM_DESTROY, 0, 0); //send exit state game_state = GAME_STATE_SHUTDOWN; } } else if (game_state == GAME_STATE_SHUTDOWN) { //in this state shut everything down and release resources //switch to exit state game_state = GAME_STATE_EXIT; } else { ; } return 1; } //CLOCK FUNCTIONS DWORD Get_Clock(void) { //this function returns the current tickcount //return time return GetTickCount(); } DWORD Start_Clock() { //this function starts the block, that is, saves the current count //use in conjunction with Wait_Clock() return (start_clock_count = Get_Clock()); } DWORD Wait_Clock(DWORD count) { //this function is used to wait for a specific number of clicks since //the call to Start_Clock while (Get_Clock() - start_clock_count < count) { ; } return Get_Clock(); } void Change_Mode(HWND hWnd, DWORD Times, int score)// 有问题 { HDC hdc; hdc = GetDC(hWnd); RECT rect, wnd; if (score >= Reword_Score) { while (Get_Clock() - now_clock < Times) { rect = { paddle_x - PADDLE_WIDTH, paddle_y, paddle_x + PADDLE_WIDTH, paddle_y + PADDLE_HEIGHT }; FillRect(hdc, &rect, CreateSolidBrush(RGB(0, 0, 255))); wnd = { 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT - 2 * PADDLE_HEIGHT }; InvalidateRect(hWnd, &wnd, false); } } score -= 50; InvalidateRect(hWnd, NULL, false); ReleaseDC(hWnd, hdc); } // 函数: WndProc(HWND, UINT, WPARAM, LPARAM) // // 目的: 处理主窗口的消息。 // // WM_COMMAND - 处理应用程序菜单 // WM_PAINT - 绘制主窗口 // WM_DESTROY - 发送退出消息并返回 // // LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); // 分析菜单选择: switch (wmId) { case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: 在此添加任意绘图代码... /* //创建内存HDC HDC memHDC = CreateCompatibleDC(hdc); //获取客户区大小 RECT rectClient; GetClientRect(hWnd,&rectClient); //创建位图 HBITMAP bmpBuff = CreateCompatibleBitmap(hdc, RECT_WIDTH(rectClient), RECT_HEIGHT(rectClient)); HBITMAP pOldBMP = (HBITMAP)SelectObject(memHDC, bmpBuff); // draw something DrawBackGround(memHDC); //拷贝内存HDC内容到实际HDC BOOL tt = BitBlt(hdc, rectClient.left, rectClient.top, RECT_WIDTH(rectClient), RECT_HEIGHT(rectClient), memHDC, rectClient.left, rectClient.top, SRCCOPY); //内存回收 SelectObject(memHDC, pOldBMP); DeleteObject(bmpBuff); DeleteDC(memHDC);*/ EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; case WM_ERASEBKGND: //防止清除背景造成的白屏 //什么也不做,返回0使默认窗口回调不再处理这个消息 return 0; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } // “关于”框的消息处理程序。 INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); switch (message) { case WM_INITDIALOG: return (INT_PTR)TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return (INT_PTR)TRUE; } break; } return (INT_PTR)FALSE; }下面是游戏的截图:
初始状态:
游戏设计了好几关,每一关的砖块数量都不同。
一些比较关键的地方都有注释,这个游戏的基本框架可以在《Windows游戏编程大师技巧》之中找到。游戏的美工方面做的挺差的,不过逻辑基本实现了。其实为了更美观你可以添加背景。这样看起来就会更不错一些。