遊戲開發(三)——WIN32 黑白棋(三)——遊戲畫面的現實

整個遊戲分3部分介紹。

1、棋局的現實

2、AI的現實

3、遊戲畫面的現實

提供一下完整項目下載


這是第三部分:畫面的顯示


這部分其實就比較簡單的,說白了就是api的堆砌。


主要了解下windows的消息機制,以及怎麼畫圖


主要是分別封裝了下對棋盤,棋子,以及當前輪到誰,當前比分是多少,就是遊戲畫面上不同的部分的繪製。

void DrawReversiBoard();

void DrawReversiPieces(EnumReversiPiecesType type, int row_y, int column_x);

void DrawReversiResult(EnumReversiResult res, EnumReversiPiecesType type);

void DrawPiecesCount(BYTE black, BYTE white);

4個函數,分別繪製不同的部分。


然後主要的一個問題,就是遊戲畫面並不是直接繪製到屏幕上的,後臺單獨創建了一個HDC,所有的畫面是先繪製到這個HDC上,然後再從這個HDC上用BitBlt拷貝到屏幕上,BitBlt得速度要比單獨一部分一部分的直接繪製到屏幕,要快很多,通常都是用這樣的內部緩存來解決直接繪製的閃屏的問題。


然後列一下主要用到的API:

畫線的:MoveToEx,LineTo

畫矩形的:Rectangle

畫橢圓的:Ellipse

選擇什麼樣的畫筆(即畫的線,圓、矩形的邊框線是什麼樣子的)GetStockObject(BLACK_PEN),

BLACK_PEN是系統提供的幾種畫筆之一,是黑色的畫筆。你也可以自定義畫筆。

選擇什麼樣的畫刷(即圓,矩形的內部用什麼樣式、顏色填充)GetStockObject(BLACK_BRUSH),

BLACK_BRUSH是系統提供的幾種畫刷之一,是用黑色填充。你也可以自定義畫刷。

輸出文字的:TextOut

設置文字的顏色:SetTextColor

設置文字背景的顏色:SetBkColor


從屏幕DC創建一個繪圖層CreateCompatibleDC,得到一個HDC,以上的圓、矩形、線條都是繪製到這個HDC上。

創建一個位圖CreateCompatibleBitmap,得到一個HBITMAP,最終就是用這個HBITMAP,然後BitBlt到屏幕的DC上。

SelectObject(HDC, HBITMAP);即在這個HDC繪圖層上,應用這個HBITMAP。


然後遊戲窗口上有個菜單這個東西,這個東西怎麼做的呢:

在項目中加一個叫XXX.rc的資源文件,比如叫game.rc。這是WIN32項目的資源文件。

在裏面寫上如下內容

#include "resource.h"
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
//
// Menu
//

REVERSI MENU DISCARDABLE
BEGIN
    POPUP "Begin"
    BEGIN
        MENUITEM "Player First",              IDM_PLAYER_FIRST
        MENUITEM "AI First",                  IDM_AI_FIRST
    END
END
當然你也可以在IDE工具裏面直接做,現在一般都支持可視化的菜單創建,點點鼠標就出來了。

只不過其實質就是類似如上的一段代碼表示的。


其中resource.h

#define IDM_PLAYER_FIRST	101
#define IDM_AI_FIRST		102

就是定義的菜單項對應的鍵值,玩家先手對應的101,AI先手對應的102。

這個鍵值在windows消息機制中會用到,用來區分鼠標點的是哪一個菜單用的。


#include "afxres.h"

這是MFC庫的一個頭文件,VC6.0下面可能會報這個頭文件找不到,首先你看一下VC安裝路徑下\VC98\MFC\INCLUDE這個目錄有沒有include進來,如果加進來了,但是確實沒有這個文件,就另外到網上搜一個下載下來,直接粘貼到VC6.0的安裝目錄裏面去一樣可以使用。VC6.0之後的版本的IDE,基本上都沒問題。


windows消息機制中,本遊戲使用到的消息:

WM_CREATE:窗口的創建。在這個消息中做各種對象的初始化,然後創建一個定時器SetTimer。

WM_TIMER:上面創建的定時器,會定時發出的消息。在這個消息中,刷新遊戲畫面,不管有沒有畫面上變動。比如SetTimer設定爲17毫秒一次,則每隔17毫秒會有這個消息被收到並處理,即每隔17毫秒遊戲畫面刷新一次。17毫秒一次大約就是每秒60幀的頻率(1000 / 60 = 16.667)。

WM_DESTROY:窗口的關閉釋放的消息。在這個消息中做一些必要的回收,比如上面的定時器要KillTimer掉。

WM_KEYDOWN:是鍵盤按鍵消息。這個消息中帶有參數wParam,代表不同的鍵值。這裏判斷一下是不是VK_ESCAPE(ESC鍵),如果玩家按了ESC,等同於點了左上角的X的效果,所以PostMessage(hwnd, WM_DESTROY, 0, 0)手動發出一個WM_DESTROY的消息。然後程序接下來就會收到WM_DESTROY並處理,最終退出。

WM_LBUTTONDOWN,鼠標左鍵的單擊消息。這個消息帶兩個參數,分別代表的是相對窗口左上角(0,0)這個像素點的相對座標位置。在這個消息中,將鼠標點的(x,y)

換算成棋盤的格子座標,然後處理玩家在這個位置落子的邏輯處理,即調用Player對象的Play方法,然後PostMessage(hwnd, WM_TIMER, 0, 0),手動發一個定時器消息,刷新一下游戲畫面。之後判斷是否是AI可以落子,如果輪到AI落子了,則PostMessage(hwnd, WM_DO_AI, 0, 0)手動發一個WM_DO_AI的消息通知AI落子

WM_RBUTTONDOWN,鼠標右鍵的單擊消息。在這個消息中處理悔棋邏輯。

WM_DO_AI,這是一個自定義消息#define WM_DO_AI WM_USER + 0x0001。在這個消息中,AI就執行搜索,找出最優的那一個位置的座標,並落子在這個位置。然後同樣手動發一個定時器消息,刷新一下游戲畫面。之後判斷是否還是AI落子,因爲AI落子之後,玩家可能無子可下,所以判斷下是否下一步還是AI落子,是的話繼續PostMessage(hwnd, WM_DO_AI, 0, 0),讓下一個WM_DO_AI繼續處理AI的落子羅輯。如果AI落子之後輪到玩家落子,則什麼都不做,等玩家落子即可。

WM_COMMAND,這個就是菜單消息了,帶一個參數,表示的菜單的編號,即上面resource.h中定義的菜單編號,如果遊戲過程中玩家點了菜單,那麼表示重新開局了,將各個對象重新初始化一下,棋局重新開始。


最後說到創建窗口了。

首先需要RegisterClass,註冊一個窗口,主要註冊的是窗口的小圖標啦,鼠標樣式了,窗口的名字了,菜單的名字了,窗口的消息處理函數啦,等類似這樣的信息。

然後CreateWindow,主要是窗口的長寬,初始座標位置,窗口的樣式,比如有沒有標題那一欄啦,右上角有沒有最大化,最小化按鈕啦,等等


下面貼一下棋局的畫面繪製的代碼

ReversiScene.h

#ifndef _ReversiScene_h_
#define _ReversiScene_h_

#include <windows.h>

#include "ReversiCommon.h"
#include "ReversiBitBoard.h"

//棋盤一個單元格長寬各60像素
const int REVERSI_GAME_GRID = 60;

//棋子半徑,舉例單元格邊界5像素
const int REVERSI_PIECES_R = REVERSI_GAME_GRID / 2 - 5;

//棋盤左上角在窗口上的座標位置
const int REVERSI_GAME_ROW_Y = 10;
const int REVERSI_GAME_COLUMN_X = 10;

//遊戲場景大小
const int REVERSI_SCENE_WIDTH = 700;
const int REVERSI_SCENE_HEIGHT = 600;

class ReversiScene
{
public:
	ReversiScene();
	~ReversiScene();

	void Init(HDC hDC);

	void Draw(ReversiBitBoard& reversi);

	void DrawToScreen();

private:
	void DrawReversiBoard();

	void DrawReversiPieces(EnumReversiPiecesType type, int row_y, int column_x);

	void DrawReversiResult(EnumReversiResult res, EnumReversiPiecesType type);

	void DrawPiecesCount(BYTE black, BYTE white);

	HDC m_hDC;

	HDC m_hMemDC;
	HBITMAP m_hMemBitmap;

	ReversiBitBoard m_LastMap;
};

#endif


ReversiScene.cpp

#include "ReversiScene.h"

ReversiScene::ReversiScene()
{
	m_hMemDC = NULL;
	m_hMemBitmap = NULL;
}

ReversiScene::~ReversiScene()
{
	DeleteObject(m_hMemDC);
	DeleteObject(m_hMemBitmap);
}

void ReversiScene::Init(HDC hDC)
{
	m_hDC = hDC;

	m_hMemDC = CreateCompatibleDC(m_hDC);
	m_hMemBitmap = CreateCompatibleBitmap(m_hMemDC, REVERSI_SCENE_WIDTH, REVERSI_SCENE_HEIGHT);
	SelectObject(m_hMemDC, m_hMemBitmap);

	DrawReversiBoard();
}

void ReversiScene::Draw(ReversiBitBoard& reversi)
{
	EnumReversiPiecesType type1;
	EnumReversiPiecesType type2;
	for (int i = 0; i < REVERSI_MAX_ROW; i++)
	{
		for (int j = 0; j < REVERSI_MAX_COLUMN; j++)
		{
			type1 = reversi.GetPieces(i, j);
			type2 = m_LastMap.GetPieces(i, j);
			if (type1 != type2)
			{
				DrawReversiPieces(type1, i, j);
				m_LastMap.SetPieces(type1, i, j);
			}
		}
	}

	DrawReversiResult(reversi.IsGameOver(), reversi.GetCurrType());

	int black = reversi.GetCount(enum_ReversiPieces_Black);
	int white = reversi.GetCount(enum_ReversiPieces_White);
	DrawPiecesCount(black, white);
}

void ReversiScene::DrawToScreen()
{
	BitBlt(m_hDC, 0, 0, REVERSI_SCENE_WIDTH, REVERSI_SCENE_HEIGHT, 
		m_hMemDC, 0, 0, SRCCOPY);
}

void ReversiScene::DrawReversiBoard()
{
	int begin_row_y = REVERSI_GAME_ROW_Y;
	int begin_column_x = REVERSI_GAME_COLUMN_X;

	SelectObject(m_hMemDC, GetStockObject(WHITE_PEN));
	SelectObject(m_hMemDC, GetStockObject(WHITE_BRUSH));
	Rectangle(m_hMemDC, 0, 0, REVERSI_SCENE_WIDTH, REVERSI_SCENE_HEIGHT) ;

	SelectObject(m_hMemDC, GetStockObject(BLACK_PEN));
	for (int i = 0; i <= REVERSI_MAX_ROW; i++)
	{
		MoveToEx(m_hMemDC, 
			begin_row_y + i * REVERSI_GAME_GRID, begin_column_x, 
			NULL);
		LineTo(m_hMemDC, 
			begin_row_y + i * REVERSI_GAME_GRID, 
			begin_column_x + REVERSI_MAX_COLUMN * REVERSI_GAME_GRID);
	}
	for (int i = 0; i <= REVERSI_MAX_COLUMN; i++)
	{
		MoveToEx(m_hMemDC, 
			begin_row_y, begin_column_x + i * REVERSI_GAME_GRID, 
			NULL);
		LineTo(m_hMemDC, 
			begin_row_y + REVERSI_MAX_ROW * REVERSI_GAME_GRID, 
			begin_column_x + i * REVERSI_GAME_GRID);
	}
}

void ReversiScene::DrawReversiPieces(EnumReversiPiecesType type, 
	int row_y, int column_x)
{
	int begin_row_y = REVERSI_GAME_ROW_Y + 1 + 
		REVERSI_GAME_GRID / 2 - REVERSI_PIECES_R + 
		row_y * REVERSI_GAME_GRID;
	int begin_column_x = REVERSI_GAME_COLUMN_X + 1 + 
		REVERSI_GAME_GRID / 2 - REVERSI_PIECES_R + 
		column_x * REVERSI_GAME_GRID;
	int end_row_y = REVERSI_GAME_ROW_Y - 
		(REVERSI_GAME_GRID / 2 - REVERSI_PIECES_R) + 
		(row_y + 1) * REVERSI_GAME_GRID;
	int end_column_x = REVERSI_GAME_COLUMN_X - 
		(REVERSI_GAME_GRID / 2 - REVERSI_PIECES_R) + 
		(column_x + 1) * REVERSI_GAME_GRID;

	if (enum_ReversiPieces_Black == type)
	{
		SelectObject(m_hMemDC, GetStockObject(BLACK_PEN));
		SelectObject(m_hMemDC, GetStockObject(BLACK_BRUSH));
	}
	else if (enum_ReversiPieces_White == type)
	{
		SelectObject(m_hMemDC, GetStockObject(BLACK_PEN));
		SelectObject(m_hMemDC, GetStockObject(WHITE_BRUSH));
	}
	else
	{
		SelectObject(m_hMemDC, GetStockObject(WHITE_PEN));
		SelectObject(m_hMemDC, GetStockObject(WHITE_BRUSH));
	}
	Ellipse(m_hMemDC, begin_column_x, begin_row_y, end_column_x, end_row_y);
}

void ReversiScene::DrawReversiResult(EnumReversiResult res, 
	EnumReversiPiecesType type)
{
	WCHAR wsCurrRes[20];

	if (enum_Reversi_Playing == res)
	{
		if (enum_ReversiPieces_Black == type)
		{
			wcscpy_s(wsCurrRes, 20, L"當前輪到:黑子");
		}
		else
		{
			wcscpy_s(wsCurrRes, 20, L"當前輪到:白子");
		}
	}
	else if (enum_Reversi_Win_Black == res)
	{
		wcscpy_s(wsCurrRes, 20, L"黑勝!遊戲結束");
	}
	else if (enum_Reversi_Win_White == res)
	{
		wcscpy_s(wsCurrRes, 20, L"白勝!遊戲結束");
	}
	else
	{
		wcscpy_s(wsCurrRes, 20, L"平局!遊戲結束");
	}

	SetTextColor(m_hMemDC, RGB(0, 0, 0));
	SetBkColor(m_hMemDC, RGB(255, 255, 255));

	TextOut(m_hMemDC, 
		REVERSI_GAME_COLUMN_X + REVERSI_MAX_COLUMN * REVERSI_GAME_GRID + 20, 
		100, 
		wsCurrRes, 
		wcslen(wsCurrRes)) ;
}

void ReversiScene::DrawPiecesCount(BYTE black, BYTE white)
{
	WCHAR wsCurrBlack[20];
	WCHAR wsCurrWhite[20];
	
	wsprintf(wsCurrBlack, L"當前黑子:%02d", black);
	wsprintf(wsCurrWhite, L"當前白子:%02d", white);

	SetTextColor(m_hMemDC, RGB(0, 0, 0));
	SetBkColor(m_hMemDC, RGB(255, 255, 255));

	TextOut(m_hMemDC, 
		REVERSI_GAME_COLUMN_X + REVERSI_MAX_COLUMN * REVERSI_GAME_GRID + 20, 
		130, 
		wsCurrBlack, 
		wcslen(wsCurrBlack)) ;

	TextOut(m_hMemDC, 
		REVERSI_GAME_COLUMN_X + REVERSI_MAX_COLUMN * REVERSI_GAME_GRID + 20, 
		160, 
		wsCurrWhite, 
		wcslen(wsCurrWhite)) ;
}

最後遊戲的主羅輯game.cpp

#define WIN32_LEAN_AND_MEAN 

#include <windows.h>

#include "Resource.h"

#include "ReversiBitBoard.h"
#include "ReversiScene.h"
#include "ReversiPlayer.h"
#include "ReversiAI.h"

ReversiBitBoard g_Reversi;
ReversiPlayer g_Player;
ReversiAI g_AI;
ReversiScene g_Scene;

const int WINDOWS_WIDTH = REVERSI_SCENE_WIDTH + 6;
const int WINDOWS_HEIGHT = REVERSI_SCENE_HEIGHT + 25;

#define WM_DO_AI WM_USER + 0x0001

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
	WCHAR szAppName[] = L"Reversi" ;

	HWND hwnd ;

	MSG msg ;

	WNDCLASS wndclass ;
	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc = WndProc;
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hInstance = hInstance;
	wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION);
	wndclass.hCursor = LoadCursor (NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wndclass.lpszMenuName = szAppName;
	wndclass.lpszClassName = szAppName;

	if (!RegisterClass (&wndclass))
	{
		MessageBox (NULL, 
			L"This program requires Windows NT!", 
			szAppName, 
			MB_ICONERROR);
		return 0;
	}

	hwnd = CreateWindow(szAppName,
		szAppName,
		WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
		//重疊窗口,有標題欄,有系統菜單,有最小化,沒有最大化
		0,
		0,
		WINDOWS_WIDTH,
		WINDOWS_HEIGHT,
		NULL,
		NULL,
		hInstance,
		NULL);
	
	ShowWindow(hwnd, iCmdShow);
	
	UpdateWindow(hwnd);
	
	while(GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return 0;
}


LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_CREATE:
		{
			g_Scene.Init(GetDC(hwnd));
			g_Reversi.Init();
			g_Player.Init(enum_ReversiPieces_Black);
			g_AI.Init(enum_ReversiPieces_White);
			g_Scene.Draw(g_Reversi);
			SetTimer(hwnd, 1, 17, NULL);
		}
		return 0;

	case WM_DESTROY:
		{
			KillTimer(hwnd, 1);
			PostQuitMessage(0);
		}
		return 0;

	case WM_COMMAND:
		{
			switch (LOWORD(wParam))
			{
			case IDM_PLAYER_FIRST:
				{
					g_Reversi.Init();
					g_Player.Init(enum_ReversiPieces_Black);
					g_AI.Init(enum_ReversiPieces_White);
					g_Scene.Draw(g_Reversi);
				}
				return 0;
			case IDM_AI_FIRST:
				{
					g_Reversi.Init();
					g_Player.Init(enum_ReversiPieces_White);
					g_AI.Init(enum_ReversiPieces_Black);
					g_AI.Play(g_Reversi);
					g_Scene.Draw(g_Reversi);
				}
				return 0;
			}
		}
		return 0;

	case WM_KEYDOWN:
		{
			switch (wParam)
			{
			case VK_ESCAPE:
				{
					PostMessage(hwnd, WM_DESTROY, 0, 0);
				}
				return 0;
			}
		}
		return 0;

	case WM_DO_AI:
		{
			if (g_Reversi.GetCurrType() == g_AI.GetPlayerType() && 
				g_Reversi.CanPlay(g_AI.GetPlayerType()))
			{	
				g_AI.Play(g_Reversi);
				g_Scene.Draw(g_Reversi);

				PostMessage(hwnd, WM_TIMER, 0, 0);

				if (g_Reversi.GetCurrType() == g_AI.GetPlayerType())
				{
					PostMessage(hwnd, WM_DO_AI, 0, 0);
				}
			}
		}
		return 0;

	case WM_LBUTTONDOWN:
		{
			int y = HIWORD(lParam);
			int x = LOWORD(lParam);
			int row_y = (y - REVERSI_GAME_ROW_Y) / REVERSI_GAME_GRID;
			int column_x = (x - REVERSI_GAME_COLUMN_X) / REVERSI_GAME_GRID;
			
			if (g_Reversi.GetCurrType() == g_Player.GetPlayerType() &&
				g_Reversi.CanPlay(g_Player.GetPlayerType(), row_y, column_x))
			{
				g_Player.Play(g_Reversi, row_y, column_x);
				g_Scene.Draw(g_Reversi);

				PostMessage(hwnd, WM_TIMER, 0, 0);

				if (g_Reversi.CanPlay(g_AI.GetPlayerType()))
				{
					PostMessage(hwnd, WM_DO_AI, 0, 0);
				}
			}
		}
		return 0;

	case WM_RBUTTONDOWN:
		{
			g_Player.Cancel(g_Reversi);
			g_Scene.Draw(g_Reversi);

			PostMessage(hwnd, WM_TIMER, 0, 0);
		}
		return 0;

	case WM_TIMER: 
		{
			g_Scene.DrawToScreen();
		}
		return 0;
	}

	return DefWindowProc (hwnd, message, wParam, lParam) ;
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章