Win32 游戏开发:贪吃蛇 下篇

因为个人原因更新来晚了,向各位关注的读者道个歉...

前排提示: 文章非常的长!!!(如有不懂的请在文章下方评论)

6)开始码代码啦

经过上面的前期工作,我们应该更清楚我们要做怎么做了(至少有一个方向了吧~)

一、首先是一个基础的Win32的框架(不懂可以看下我的这篇文章)

二、简单介绍贪吃蛇中游戏对象的类

PS:有看过之前一个游戏的会发现,我这个游戏也有Util这个类,以后会不断将常用的通用的加入到这个类中。

三、在讲游戏对象的类前先来看一下main中对游戏的一些处理

① 程序主循环

MSG msg = { 0 };
DWORD dwLastTick = ::GetTickCount(), dwCurrentTick;
while (msg.message != WM_QUIT)
{
	// 当前时间戳
	dwCurrentTick = ::GetTickCount();
	if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {......}

	// 游戏更新
	SnakeGame_Update(dwCurrentTick - dwLastTick);
	// 游戏渲染
	SnakeGame_Render(hWnd);
	dwLastTick = dwCurrentTick;
	// Sleep 休息10毫秒,避免主循环消耗过多的CPU
	Sleep(10);
}

在主循环中进行游戏的消息处理游戏更新游戏渲染

② 窗口消息处理

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
        // 游戏中对窗口的消息进行处理
	if (g_game != NULL)
		g_game->WndProc(hWnd, message, wParam, lParam);

	switch (message)
	{
	case WM_PAINT:
		// 游戏渲染
		SnakeGame_Render(hWnd);
		break;
	case WM_KEYDOWN:
		// 游戏按键处理
		SnakeGame_KeyDown(wParam);
		break;
	case WM_DESTROY:
		// 窗口销毁,释放资源
		......
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}

	return ((LRESULT)0);
}

在窗口消息处理中,对游戏需要的信息进行传递到游戏中(例如:按键消息)。

③ 游戏更新

VOID SnakeGame_Update(DWORD dwDeltaTime)
{
	/* 游戏更新 */
	if (g_game == NULL)
		return;

	g_game->Update(dwDeltaTime);
}

调用游戏的更新,并传入与上次更新的时间差(毫秒数)。

④ 游戏渲染

VOID SnakeGame_Render(HWND hWnd)
{
	// 游戏渲染
	if (g_mdc == NULL || g_game == NULL)
		return;

	HDC hdc = ::GetDC(hWnd);
	RECT rect;
	::GetClientRect(hWnd, &rect);
	int width = rect.right - rect.left;
	int height = rect.bottom - rect.top;

	// 游戏渲染
	g_game->Render(g_mdc);

	// 将兼容DC绘制到设备DC上 
	::BitBlt(hdc, 0, 0, width, height, g_mdc, 0, 0, SRCCOPY);
	::ReleaseDC(hWnd, hdc);
}

调用游戏的渲染,并传入兼容DC,最后将兼容DC的内容复制到设备DC上。

⑤ 游戏按键处理

VOID SnakeGame_KeyDown(WPARAM wParam)
{
	if (g_game == NULL)
		return;

	Keys key = Keys::KEY_W;
	if (wParam == 'W')
		key = Keys::KEY_W;
	else if (wParam == 'S')
		key = Keys::KEY_S;
	else if (wParam == 'A')
		key = Keys::KEY_A;
	else if (wParam == 'D')
		key = Keys::KEY_D;

	else if (wParam == VK_UP)
		key = Keys::KEY_UP;
	else if (wParam == VK_DOWN)
		key = Keys::KEY_DOWN;
	else if (wParam == VK_LEFT)
		key = Keys::KEY_LEFT;
	else if (wParam == VK_RIGHT)
		key = Keys::KEY_RIGHT;

	/* 游戏按键处理 */
	g_game->OnKeyDown(key);
}

调用了游戏的按键处理,并传入自定义的按键枚举值。

四、下面将列举这些游戏对象的类(列举顺序与上面介绍的相同)

Game (游戏类)

头文件:

//++++++++++++++++++++++++++++++++++
// 游戏类
//----------------------------------

#pragma once

#ifndef __GAME_H__
#define __GAME_H__

// 声明场景类
class Scene;
// 声明按键枚举
enum Keys;

// 枚举游戏场景
enum EnumGameScene {
	SCENE_MAINMENU, SCENE_GAME
};

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

public:
	// 初始化游戏
	void Init(HWND hWnd);
	// 渲染游戏
	void Render(HDC hdc);
	// 更新游戏
	void Update(DWORD dwDeltaTime);
	// 游戏按键处理
	void OnKeyDown(Keys key);
	// 窗口消息的处理
	void WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
	//切换到主菜单场景
	void SwitchSceneToMainMenu();
	// 切换到游戏场景
	void SwitchSceneToGame();
	// 退出游戏
	void ExitGame();
	// 设置切换到主菜单的标志(在下一个更新逻辑中进行切换场景)
	void SetSwitchToMainMenuFlag();
	// 设置切换到游戏场景的标志(在下一个更新逻辑中进行切换场景)
	void SetSwitchToGameFlag();

public:
	// 提供静态的方法,方便获取游戏的窗口句柄
	static const HWND GetGameHwnd() { return m_hWnd; }

private:
	static HWND m_hWnd;             // 游戏窗口句柄
	Scene *m_scene;                 // 当前的游戏场景
	EnumGameScene m_scene_flag;     // 游戏场景的标志
	bool m_is_switch_scene;         // 是否需要在下一个更新逻辑中切换场景
};

#endif // !__GAME_H__

源文件:


#include <windows.h>            // WIN32开发中最重要的头文件(不用详解了吧...)

#include "GameType.h"           // 游戏中使用到的一些枚举、别名定义
#include "Scene.h"              // 场景抽象类
#include "MainMenuScene.h"      // 主菜单场景类
#include "GameScene.h"          // 游戏场景类
#include "Game.h"               // 游戏类
#include "util.h"               // 常用工具类

// 引入std命名空间中的bind
// std::bind 将函数(普通函数、函数对象、成员函数)绑定,同时将可调用对象保存起来(在需要的时候调用)
// 绑定部分形参(或占位符, 例如std::placeholders::_1等)
using std::bind;				
//using namespace std::placeholders; // 占位符, 配合着std::bind使用

// 初始化游戏类的静态成员变量, 窗口句柄
// 为了在外部能通过调用Game::GetGameHwnd()进行获得游戏的窗口句柄
HWND Game::m_hWnd = NULL;

Game::Game() 
	: m_scene_flag(EnumGameScene::SCENE_MAINMENU), // 默认场景为主菜单
	m_is_switch_scene(false)                       // 初始化不切换场景
{
}

Game::~Game()
{
}

void Game::Init(HWND hWnd)
{
	// 保存窗口句柄
	m_hWnd = hWnd;
	// 切换到主菜单
	SwitchSceneToMainMenu();
}

void Game::Render(HDC hdc)
{
	RECT rect;
	::GetClientRect(m_hWnd, &rect);
	HBRUSH hBrush = (HBRUSH)::GetStockObject(WHITE_BRUSH);
	/* 填充背景颜色 */
	::FillRect(hdc, &rect, hBrush);

	/* 绘制场景 */
	m_scene->Render(hdc);
}

void Game::Update(DWORD dwDeltaTime)
{
	// 是否需要切换场景
	if (m_is_switch_scene)
	{
		// 根据切换的场景进行切换相应的场景
		switch (m_scene_flag)
		{
		case SCENE_MAINMENU:
			// 切换到主菜单场景
			SwitchSceneToMainMenu();
			break;
		case SCENE_GAME:
			// 切换到游戏场景
			SwitchSceneToGame();
			break;
		default:
			break;
		}
	}

	// 场景更新
	m_scene->Update(dwDeltaTime);
}

void Game::OnKeyDown(Keys key)
{
	// 场景的按键处理
	m_scene->OnKeyDown(key);
}

void Game::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 场景的消息处理
	m_scene->WndProc(hWnd, message, wParam, lParam);
}

void Game::SwitchSceneToMainMenu()
{
	// 是否存在场景,如果存在则释放当前场景的资源
	if (m_scene != NULL)
		SafeDeleteObject(m_scene);

	// 主菜单场景
	MainMenuScene *scene;
	// 创建一个主菜单场景
	m_scene = scene = new MainMenuScene();
	// 绑定游戏类(Game)的切换到游戏场景方法到主菜单场景(MainMenuScene)的成员函数(function)对象中
	scene->fnSwitchToGame = bind(&Game::SetSwitchToGameFlag, this);
	// 绑定游戏类(Game)的退出游戏方法到主菜单场景(MainMenuScene)的成员函数(function)对象中
	scene->fnExitGame = bind(&Game::ExitGame, this);
	// 调用主菜单场景的初始化
	m_scene->Init(m_hWnd);
	// 设置在Update中进行切换场景完成
	m_is_switch_scene = false;
}

void Game::SwitchSceneToGame()
{
	// 是否存在场景,如果存在则释放当前场景的资源
	if (m_scene != NULL)
		SafeDeleteObject(m_scene);
	
	// 游戏场景
	GameScene *scene;
	// 创建一个游戏场景,并设定为当前的场景
	m_scene = scene = new GameScene();
	// 绑定游戏类(Game)的切换到主界面场景方法到游戏场景(GameScene)的成员函数(function)对象中
	scene->fnSwitchToMainMenu = bind(&Game::SetSwitchToMainMenuFlag, this);
	// 绑定游戏类(Game)的退出游戏方法到游戏场景(GameScene)的成员函数(function)对象中
	scene->fnExitGame = bind(&Game::ExitGame, this);
	// 调用游戏场景的初始化
	m_scene->Init(m_hWnd);
	// 设置在Update中进行切换场景完成
	m_is_switch_scene = false;
}

void Game::ExitGame()
{
	// SendMessage发送WM_CLOSE(窗口关闭消息),使得窗口正常关闭
	::SendMessage(Game::GetGameHwnd(), WM_CLOSE, 0, 0);
}

void Game::SetSwitchToMainMenuFlag()
{
	// 设置切换场景,在下一个Update逻辑更新中进行切换场景
	m_is_switch_scene = true;
	// 设置切换的场景
	m_scene_flag = EnumGameScene::SCENE_MAINMENU;
}

void Game::SetSwitchToGameFlag()
{
	// 设置切换场景,在下一个Update逻辑更新中进行切换场景
	m_is_switch_scene = true;
	// 设置切换的场景
	m_scene_flag = EnumGameScene::SCENE_GAME;
}

下面进行讲解上面的程序:


Game::Init(HWND hWnd)

void Game::Init(HWND hWnd)
{
	// 保存窗口句柄
	m_hWnd = hWnd;
	// 切换到主菜单
	SwitchSceneToMainMenu();
}

1. 保存窗口句柄

2.切换到主菜单场景


Game::Render(HDC hdc)

void Game::Render(HDC hdc)
{
	RECT rect;
	::GetClientRect(m_hWnd, &rect);
	HBRUSH hBrush = (HBRUSH)::GetStockObject(WHITE_BRUSH);
	/* 填充背景颜色 */
	::FillRect(hdc, &rect, hBrush);

	/* 绘制场景 */
	m_scene->Render(hdc);
}

1. 将窗口背景填充为白色(将上一帧绘制的内容覆盖掉)

2. 调用当前场景的渲染


Game::Update(DWORD dwDeltaTime)

void Game::Update(DWORD dwDeltaTime)
{
	// 是否需要切换场景
	if (m_is_switch_scene)
	{
		// 根据切换的场景进行切换相应的场景
		switch (m_scene_flag)
		{
		case SCENE_MAINMENU:
			// 切换到主菜单场景
			SwitchSceneToMainMenu();
			break;
		case SCENE_GAME:
			// 切换到游戏场景
			SwitchSceneToGame();
			break;
		default:
			break;
		}
	}

	// 场景更新
	m_scene->Update(dwDeltaTime);
}

1. dwDeltaTime为距离上一次更新的时间差(毫秒数)

2. 首先处理场景的切换,根据切换的场景标志进行切换不同的场景

3. 最后调用场景的Update(逻辑更新)


Game::OnKeyDown(Keys key)

void Game::OnKeyDown(Keys key)
{
	// 场景的按键处理
	m_scene->OnKeyDown(key);
}

1. 调用场景的按键处理


Game::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

void Game::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 场景的消息处理
	m_scene->WndProc(hWnd, message, wParam, lParam);
}

1. 调用场景的窗口消息处理


Game::SwitchSceneToMainMenu()

void Game::SwitchSceneToMainMenu()
{
	// 是否存在场景,如果存在则释放当前场景的资源
	if (m_scene != NULL) 
		SafeDeleteObject(m_scene);

	// 主菜单场景
	MainMenuScene *scene;
	// 创建一个主菜单场景
	m_scene = scene = new MainMenuScene();
	// 绑定游戏类(Game)的切换到游戏场景方法到主菜单场景(MainMenuScene)的成员函数(function)对象中
	scene->fnSwitchToGame = bind(&Game::SetSwitchToGameFlag, this);
	// 绑定游戏类(Game)的退出游戏方法到主菜单场景(MainMenuScene)的成员函数(function)对象中
	scene->fnExitGame = bind(&Game::ExitGame, this);
	// 调用主菜单场景的初始化
	m_scene->Init(m_hWnd);
	// 设置在Update中进行切换场景完成
	m_is_switch_scene = false;
}

1. 释放当前场景

2. 创建一个新的主菜单场景

3. 设置此新的主菜单场景的按钮点击的函数对象(进入游戏和退出游戏)

4. 初始化新的主菜单场景

5. 设置切换场景完毕


Game::SwitchSceneToGame()

void Game::SwitchSceneToGame()
{
	// 是否存在场景,如果存在则释放当前场景的资源
	if (m_scene != NULL)
		SafeDeleteObject(m_scene);
	
	// 游戏场景
	GameScene *scene;
	// 创建一个游戏场景,并设定为当前的场景
	m_scene = scene = new GameScene();
	// 绑定游戏类(Game)的切换到主界面场景方法到游戏场景(GameScene)的成员函数(function)对象中
	scene->fnSwitchToMainMenu = bind(&Game::SetSwitchToMainMenuFlag, this);
	// 绑定游戏类(Game)的退出游戏方法到游戏场景(GameScene)的成员函数(function)对象中
	scene->fnExitGame = bind(&Game::ExitGame, this);
	// 调用游戏场景的初始化
	m_scene->Init(m_hWnd);
	// 设置在Update中进行切换场景完成
	m_is_switch_scene = false;
}

1. 释放当前场景

2. 创建一个新的游戏场景

3. 设置此新的游戏场景的按钮点击的函数对象(切换到主菜单和退出游戏)

4. 初始化新的游戏场景

5. 设置切换场景完毕


Game::ExitGame()

void Game::ExitGame()
{
	// SendMessage发送WM_CLOSE(窗口关闭消息),使得窗口正常关闭
	::SendMessage(Game::GetGameHwnd(), WM_CLOSE, 0, 0);
}

1. 调用SendMessage将WM_CLOSE(窗口关闭消息)发送到游戏窗口

PS:

1. WM_CLOSE就相当于你按下了窗口的关闭按钮(相当于正常的关闭窗口)

Q&A:

Q: 为什么使用SendMessage而不是PostMessage?

A: (摘抄自百度百科)

SendMessage函数为指定的窗口调用窗口程序,直到窗口程序处理完消息再返回。

PostMessage函数是将一个消息寄送到一个线程的消息队列后就立即返回。

博主:

SendMessage发送消息到窗口,直到处理完毕这个消息后才返回。(同步,会阻塞当前线程)

PostMessage将一个消息发送到消息队列即马上返回。(异步,不会阻塞当前线程)

当我们按下退出游戏按钮想的就是尽快的关闭游戏窗口,所以使用SendMessage进行马上处理WM_CLOSE(窗口关闭)的消息,以尽快的关闭游戏。


Game::SetSwitchToMainMenuFlag()

void Game::SetSwitchToMainMenuFlag()
{
	// 设置切换场景,在下一个Update逻辑更新中进行切换场景
	m_is_switch_scene = true;
	// 设置切换的场景
	m_scene_flag = EnumGameScene::SCENE_MAINMENU;
}

1. 设置在下一个Update(逻辑更新)中进行切换场景

2. 设置切换的场景标志

Q&A:

Q: 为什么不直接调用SwitchSceneToMainMenu进行切换到主菜单场景?

A:

假设这个按钮点击的委托函数为(SwitchSceneToMainMenu),

那么当这个按钮被点击后,调用此函数假设为(SwitchSceneToMainMenu)。

当前场景为游戏场景,这个切换到主菜单的按钮在游戏场景中,

而SwitchSceneToMainMenu是会先释放当前的游戏场景再进行创建新的场景,

那这个按钮即为在自己的点击方法中进行销毁自己 (相当于delete this) ,

不过这样进行自我销毁是可以的,但是在销毁之后就不能再使用this,

但在GameButton中点击之后还要使用this,所以在这个时候销毁了自己,

再使用this的时候,答案很明显会出错。

PS: 同理Game::SetSwitchToGameFlag()


 

Scene (游戏场景抽象类)

头文件:

//++++++++++++++++++++++++++++++++++
// 游戏场景抽象类
//----------------------------------

#pragma once

#ifndef __SCENE_H__
#define __SCENE_H__

class Scene
{
public:
	Scene();
	virtual ~Scene();

public:
	// 初始化场景
	virtual void Init(HWND hWnd) = 0;
	// 渲染场景
	virtual void Render(HDC hdc) = 0;
	// 更新场景
	virtual void Update(DWORD dwDeltaTime) = 0;
	// 场景按键处理
	virtual void OnKeyDown(Keys key) = 0;
	// 窗口消息的处理
	virtual void WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) = 0;
};

#endif // !__SCENE_H__

源文件:


#include <windows.h>	// WIN32开发中最重要的头文件(不用详解了吧...)

#include "GameType.h"	// 游戏中使用到的一些枚举、别名定义
#include "Scene.h"      // 游戏场景抽象类

Scene::Scene()
{
}

Scene::~Scene()
{
}

下面进行讲解上面的程序:

1. 这是一个游戏场景的抽象类,定义了一些场景共通的虚函数。(用以实现场景的多态)


 

MainMenuScene (主菜单场景类)

头文件:

//++++++++++++++++++++++++++++++++++
// 游戏主菜单场景类
//----------------------------------

#pragma once

#ifndef __MAIN_MENU_SCENE_H__
#define __MAIN_MENU_SCENE_H__

class Game;         // 声明游戏类
class GameButton;   // 声明游戏按钮类
class GameLabel;    // 声明游戏标签类

// 继承于游戏场景类
class MainMenuScene : public Scene
{
public:
	MainMenuScene();
	virtual ~MainMenuScene() override;

public:
	// 初始化场景
	virtual void Init(HWND hWnd) override;						
	// 渲染场景
	virtual void Render(HDC hdc) override;						
	// 更新场景
	virtual void Update(DWORD dwDeltaTime) override;			
	// 场景按键处理
	virtual void OnKeyDown(Keys key) override;					
	// 窗口消息的处理
	virtual void WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) override;	

	// 游戏开始按钮点击事件
	void OnStartGameButtonClicked(int x, int y);				
	// 退出游戏按钮点击事件
	void OnExitGameButtonClicked(int x, int y);					

public:
	GameDelegateFunction fnExitGame;        // 退出游戏的函数(function)对象
	GameDelegateFunction fnSwitchToGame;    // 切换到游戏场景的函数(function)对象

private:
	GameButton *m_start_game_button;        // 开始游戏按钮
	GameButton *m_exit_game_button;         // 退出游戏按钮
	GameLabel *m_title_first_word_label;    // 贪吃蛇中的"贪"字标签
	GameLabel *m_title_second_word_label;   // 贪吃蛇中的"吃"字标签
	GameLabel *m_title_third_word_label;    // 贪吃蛇中的"蛇"字标签
	RECT m_rect_scene;                      // 场景的矩形
};

#endif // !__MAIN_MENU_SCENE_H__

源文件:


#include <windows.h>            // WIN32开发中最重要的头文件(不用详解了吧...)
#include <tchar.h>              // 通用字符集头文件,对于字符集之间的兼容比如ASCII编码和Unicode编码

#include "GameType.h"           // 游戏中使用到的一些枚举、别名定义
#include "Scene.h"              // 游戏场景抽象类
#include "Game.h"               // 游戏类
#include "GameButton.h"         // 游戏按钮类
#include "GameLabel.h"          // 游戏标签类
#include "util.h"               // 常用工具类
#include "MainMenuScene.h"      // 主菜单场景类

MainMenuScene::MainMenuScene()
{
	// 创建开始游戏按钮等.......(看名字哈)
	m_start_game_button = new GameButton(0, 0, _T("开始游戏"));
	m_exit_game_button = new GameButton(0, 0, _T("退出游戏"));
	// 这里这里, 用了三个Label进行表示 "贪"、"吃"、"蛇" 只是为了设置三个字不同的颜色效果。(。♥ᴗ♥。) 
	m_title_first_word_label = new GameLabel(0, 0, _T("贪"));
	m_title_second_word_label = new GameLabel(0, 0, _T("吃"));
	m_title_third_word_label = new GameLabel(0, 0, _T("蛇"));
}

MainMenuScene::~MainMenuScene()
{
	// 释放主界面场景的资源
	SafeDeleteObject(m_start_game_button);
	SafeDeleteObject(m_exit_game_button);
	SafeDeleteObject(m_title_first_word_label);
	SafeDeleteObject(m_title_second_word_label);
	SafeDeleteObject(m_title_third_word_label);
}

void MainMenuScene::Init(HWND hWnd)
{
	// 获取窗口的矩形
	::GetClientRect(hWnd, &m_rect_scene);

	// 获取场景的宽度和高度
	int scene_width = m_rect_scene.right - m_rect_scene.left;
	int scene_height = m_rect_scene.bottom - m_rect_scene.top;
	// 获取场景的中心坐标
	float x_center = m_rect_scene.left + scene_width * .5f;
	float y_center = m_rect_scene.top + scene_height * .5f;

	// 设置开始游戏按钮的字体大小,位置
	m_start_game_button->SetFontSize(50);
	m_start_game_button->SetX((int)(x_center - m_start_game_button->GetWidth() * .5f));
	m_start_game_button->SetY((int)(y_center - m_start_game_button->GetHeight() * .5f - scene_height * .1f));
	
	// 设置退出游戏按钮的字体大小,位置
	m_exit_game_button->SetFontSize(50);
	m_exit_game_button->SetX((int)(x_center - m_exit_game_button->GetWidth() * .5f));
	m_exit_game_button->SetY((int)(y_center - m_exit_game_button->GetHeight() * .5f + scene_height * .1f));
	
	// 设置贪吃蛇的"贪"标签的字体大小
	m_title_first_word_label->SetFontSize(125);
	// 设置贪吃蛇的"吃"标签的字体大小
	m_title_second_word_label->SetFontSize(125);
	// 设置贪吃蛇的"蛇"标签的字体大小
	m_title_third_word_label->SetFontSize(125);

	// 设置贪吃蛇的"贪"标签的前景色
	m_title_first_word_label->SetForeground(RGB(238, 130, 238));
	// 设置贪吃蛇的"吃"标签的前景色
	m_title_second_word_label->SetForeground(RGB(218, 112, 214));
	// 设置贪吃蛇的"蛇"标签的前景色
	m_title_third_word_label->SetForeground(RGB(255, 105, 180));

	// 设置贪吃蛇的"吃"标签的x坐标位置, 先设定中间的 "吃" 字为中心, "贪"和"蛇"则根据他的位置设定为其左右
	m_title_second_word_label->SetX((int)(x_center - m_title_second_word_label->GetWidth() * .5f));
	// 设置贪吃蛇的"贪"标签的x坐标位置, 位置为"吃"的左边
	m_title_first_word_label->SetX(m_title_second_word_label->GetX() - m_title_first_word_label->GetWidth());
	// 设置贪吃蛇的"蛇"标签的x坐标位置, 位置为"吃"的右边
	m_title_third_word_label->SetX(m_title_second_word_label->GetX() + m_title_third_word_label->GetWidth());

	// 设置贪吃蛇的"贪"标签的y坐标位置
	m_title_first_word_label->SetY((int)(scene_height * .05f));
	// 设置贪吃蛇的"吃"标签的y坐标位置
	m_title_second_word_label->SetY(m_title_first_word_label->GetY());
	// 设置贪吃蛇的"蛇"标签的y坐标位置
	m_title_third_word_label->SetY(m_title_first_word_label->GetY());

	// 设置开始游戏按钮点击时候的委托
	m_start_game_button->SetClickDelegate(
		std::bind(&MainMenuScene::OnStartGameButtonClicked, this, 
			std::placeholders::_1, std::placeholders::_2));
	// 设置退出游戏按钮点击时候的委托
	m_exit_game_button->SetClickDelegate(
		std::bind(&MainMenuScene::OnExitGameButtonClicked, this, 
			std::placeholders::_1, std::placeholders::_2));
}

void MainMenuScene::Render(HDC hdc)
{
	// 填充场景背景
	::FillRect(hdc, &m_rect_scene, ::CreateSolidBrush(RGB(134, 71, 63)));
	//渲染开始游戏按钮
	m_start_game_button->Render(hdc);
	// 渲染退出游戏按钮
	m_exit_game_button->Render(hdc);
	// 渲染贪吃蛇的"贪"标签
	m_title_first_word_label->Render(hdc);
	// 渲染贪吃蛇的"吃"标签
	m_title_second_word_label->Render(hdc);
	// 渲染贪吃蛇的"蛇"标签
	m_title_third_word_label->Render(hdc);
}

void MainMenuScene::Update(DWORD dwDeltaTime)
{
	// 更新主菜单场景
}

void MainMenuScene::OnKeyDown(Keys key)
{
	// 主菜单场景的按键处理
}

void MainMenuScene::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 主菜单场景对窗口消息的处理

	// 开始游戏按钮对窗口消息的处理
	m_start_game_button->WndProc(hWnd, message, wParam, lParam);
	// 退出游戏按钮对窗口消息的处理
	m_exit_game_button->WndProc(hWnd, message, wParam, lParam);
}

void MainMenuScene::OnStartGameButtonClicked(int x, int y)
{
	// 调用切换到游戏场景的委托函数
	fnSwitchToGame();
}

void MainMenuScene::OnExitGameButtonClicked(int x, int y)
{
	// 调用退出游戏的委托函数
	fnExitGame();
}

下面进行讲解上面的程序:


MainMenuScene::MainMenuScene()

MainMenuScene::MainMenuScene()
{
	// 创建开始游戏按钮等.......(看名字哈)
	m_start_game_button = new GameButton(0, 0, _T("开始游戏"));
	m_exit_game_button = new GameButton(0, 0, _T("退出游戏"));
	// 这里这里, 用了三个Label进行表示 "贪"、"吃"、"蛇" 只是为了设置三个字不同的颜色效果。(。♥ᴗ♥。) 
	m_title_first_word_label = new GameLabel(0, 0, _T("贪"));
	m_title_second_word_label = new GameLabel(0, 0, _T("吃"));
	m_title_third_word_label = new GameLabel(0, 0, _T("蛇"));
}

1. 创建按钮(开始游戏和退出游戏)

2. 创建标签(...偷个懒了)


MainMenuScene::~MainMenuScene()

MainMenuScene::~MainMenuScene()
{
	// 释放主界面场景的资源
	SafeDeleteObject(m_start_game_button);
	SafeDeleteObject(m_exit_game_button);
	SafeDeleteObject(m_title_first_word_label);
	SafeDeleteObject(m_title_second_word_label);
	SafeDeleteObject(m_title_third_word_label);
}

1. 释放在场景中创建的对象。(有始有终)


MainMenuScene::Init(HWND hWnd)

void MainMenuScene::Init(HWND hWnd)
{
	// 设置按钮和标签的字体大小、位置、前景色等
	....

	// 设置开始游戏按钮点击时候的委托
	m_start_game_button->SetClickDelegate(
		std::bind(&MainMenuScene::OnStartGameButtonClicked, this, 
			std::placeholders::_1, std::placeholders::_2));
	// 设置退出游戏按钮点击时候的委托
	m_exit_game_button->SetClickDelegate(
		std::bind(&MainMenuScene::OnExitGameButtonClicked, this, 
			std::placeholders::_1, std::placeholders::_2));
}

1. 设置按钮和标签等的字体大小、位置、 前景色等。

(这里不明白请在评论下留言吧,我再细说)

2. 设置开始游戏按钮的点击函数对象(MainMenuScene::OnStartGameButtonClicked)

3. 设置退出游戏按钮的点击函数对象(MainMenuScene::OnExitGameButtonClicked)

4. _1以及_2的即为占位符


MainMenuScene::Render(HDC hdc)

void MainMenuScene::Render(HDC hdc)
{
	// 填充场景背景
	::FillRect(hdc, &m_rect_scene, ::CreateSolidBrush(RGB(134, 71, 63)));
	//渲染开始游戏按钮
	m_start_game_button->Render(hdc);
	// 渲染退出游戏按钮
	m_exit_game_button->Render(hdc);
	// 渲染贪吃蛇的"贪"标签
	m_title_first_word_label->Render(hdc);
	// 渲染贪吃蛇的"吃"标签
	m_title_second_word_label->Render(hdc);
	// 渲染贪吃蛇的"蛇"标签
	m_title_third_word_label->Render(hdc);
}

1. 填充场景的背景

2. 调用按钮和标签的渲染


MainMenuScene::WndProc(HWND , UINT , WPARAM , LPARAM )

void MainMenuScene::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 主菜单场景对窗口消息的处理

	// 开始游戏按钮对窗口消息的处理
	m_start_game_button->WndProc(hWnd, message, wParam, lParam);
	// 退出游戏按钮对窗口消息的处理
	m_exit_game_button->WndProc(hWnd, message, wParam, lParam);
}

1. 调用按钮的窗口消息处理


MainMenuScene::OnStartGameButtonClicked(int x, int y)

void MainMenuScene::OnStartGameButtonClicked(int x, int y)
{
	// 调用切换到游戏场景的委托函数
	fnSwitchToGame();
}

1. 开始游戏按钮被点击所调用的方法

2. 调用Game的切换到游戏场景的函数对象


MainMenuScene::OnExitGameButtonClicked(int x, int y)

void MainMenuScene::OnExitGameButtonClicked(int x, int y)
{
	// 调用退出游戏的委托函数
	fnExitGame();
}

1. 退出游戏按钮被点击所调用的方法

2. 调用Game的退出游戏的函数对象


 

GameScene (游戏场景类)

头文件:

//++++++++++++++++++++++++++++++++++
// 游戏场景类
//----------------------------------

#pragma once

#ifndef __GAME_SCENE_H__
#define __GAME_SCENE_H__

class GameButton;       // 声明游戏按钮类
class GameBattleArea;   // 声明游戏游戏对战场类
class GameLabel;        // 声明游戏标签类

// 继承于游戏场景类
class GameScene : public Scene
{
public:
	GameScene();
	virtual ~GameScene() override;

public:
	// 初始化场景
	virtual void Init(HWND hWnd) override;						
	// 渲染场景
	virtual void Render(HDC hdc) override;						
	// 更新场景
	virtual void Update(DWORD dwDeltaTime) override;			
	// 场景按键处理
	virtual void OnKeyDown(Keys key) override;					
	// 窗口消息的处理
	virtual void WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) override;

	// 游戏暂停按钮点击事件
	void OnPuaseGameButtonClicked(int x, int y);	
	// 退出游戏按钮点击事件
	void OnExitGameButtonClicked(int x, int y);	
	// 重新开始游戏按钮点击事件
	void OnNewGameButtonClicked(int x, int y);	
	// 切换到主菜单按钮点击事件
	void OnSwitchMainMenuButtonClicked(int x, int y);	
	// 游戏结束事件
	void OnGameOver();				
	// 分数增加事件
	void OnGameScoreAdd(int score);	

private:
	// 绘制游戏结束
	void RenderGameOver(HDC hdc);	

public:
	GameDelegateFunction fnExitGame;            // 退出游戏的函数(function)对象
	GameDelegateFunction fnSwitchToMainMenu;    // 切换到主菜单的函数(function)对象

private:
	HWND m_hWnd;                                // 游戏窗口句柄
	GameBattleArea *m_battle_area;              // 游戏游戏对战场
	GameButton *m_pause_button;                 // 游戏暂停按钮
	GameButton *m_new_game_button;              // 重新开始按钮
	GameButton *m_switch_to_main_menu_button;   // 切换到主菜单按钮
	GameButton *m_exit_button;                  // 退出游戏按钮
	GameLabel *m_score_tips_title;              // 分数标题
	GameLabel *m_score_tips;                    // 分数
	GameLabel *m_score_add_tips;                // 分数增加提示
	GameLabel *m_game_time_tips_title;          // 游戏时间标题
	GameLabel *m_game_time_tips;                // 游戏时间
	GameLabel *m_game_over_title_tips;          // 游戏结束标题
	GameLabel *m_game_over_reason_tips;         // 游戏结束原因
	LPTSTR m_score_string;                      // 分数的字符串缓冲区
	LPTSTR m_score_add_string;                  // 分数增加提示的字符串缓冲区
	LPTSTR m_game_time_string;                  // 游戏时间的字符串缓冲区
	DWORD m_game_tick;                          // 游戏时间(毫秒数)
	RECT m_rect;                                // 场景区域矩形
	RECT m_game_over_panel_rect;                // 游戏结束面板的矩形
	const LONG m_score_add_tips_visible_time;   // 分数增加可见时间
	LONG m_score_add_tips_time;                 // 分数增加距离隐藏的时间
	int m_scene_width;                          // 场景宽度
	int m_scene_height;                         // 场景高度
	int m_game_over_panel_width;                // 游戏结束面板的宽度
	int m_game_over_panel_height;               // 游戏结束面板的高度
	int m_game_over_x_origin;                   // 游戏结束面板的中心点x坐标
	int m_game_over_y_origin;                   // 游戏结束面板的中心点y左边
	int m_score;                                // 游戏分数
};

#endif // !__GAME_SCENE_H__
 

源文件:


#include <windows.h>            // WIN32开发中最重要的头文件(不用详解了吧...)
#include <tchar.h>              // 通用字符集头文件,对于字符集之间的兼容比如ASCII编码和Unicode编码
#include <vector>               // vector容器所在头文件

#include "GameType.h"           // 游戏中使用到的一些枚举、别名定义
#include "Scene.h"              // 场景抽象类
#include "GameButton.h"         // 游戏按钮类
#include "GameLabel.h"          // 游戏标签类
#include "GameBattleArea.h"     // 游戏对战场类
#include "Util.h"               // 常用工具类
#include "GameScene.h"          // 游戏场景类

// Game.cpp 中已经介绍了,此处就不多说了.
using std::bind;
using namespace std::placeholders; // C++11新特征-占位符, 配合着std::bind使用

GameScene::GameScene() 
	: m_score_add_tips_visible_time(500L),                  // 设置分数增提示的可见时间
	m_score_add_tips_time(m_score_add_tips_visible_time)    // 初始化分数增加提示当前已显示的时间
{
	// 创建按钮等.......(看名字哈)
	m_pause_button = new GameButton(0, 0, _T("暂停游戏"));
	m_new_game_button = new GameButton(0, 0, _T("重新开始"));
	m_switch_to_main_menu_button = new GameButton(0, 0, _T("主菜单"));
	m_exit_button = new GameButton(0, 0, _T("退出游戏"));

	// 创建标签等.......(看名字哈)
	m_game_over_title_tips = new GameLabel(0, 0, _T("游戏结束"));
	m_game_over_reason_tips = new GameLabel();
	m_score_tips = new GameLabel(0, 0, _T("0"));
	m_score_tips_title = new GameLabel(0, 0, _T("分数: "));
	m_score_add_tips = new GameLabel();
	m_game_time_tips = new GameLabel(0, 0, _T("00:00:00"));
	m_game_time_tips_title = new GameLabel(0, 0, _T("游戏时间: "));

	// 设置增加分数不可见
	m_score_add_tips->SetVisible(false);

	// 设置游戏结束提示等元素不可见
	m_game_over_title_tips->SetVisible(false);
	m_game_over_reason_tips->SetVisible(false);

	m_new_game_button->SetVisible(false);
	m_switch_to_main_menu_button->SetVisible(false);
	m_exit_button->SetVisible(false);

	// 为分数和游戏时间的字符串缓冲区分配内存
	m_score_string = new TCHAR[32];
	m_game_time_string = new TCHAR[16];
	m_score_add_string = new TCHAR[8];
}

GameScene::~GameScene()
{
	// 释放游戏场景的资源
	SafeDeleteObject(m_pause_button);
	SafeDeleteObject(m_new_game_button);
	SafeDeleteObject(m_switch_to_main_menu_button);
	SafeDeleteObject(m_exit_button);
	SafeDeleteObject(m_game_over_title_tips);
	SafeDeleteObject(m_game_over_reason_tips);
	SafeDeleteObject(m_score_tips);
	SafeDeleteObject(m_score_tips_title);
	SafeDeleteObject(m_game_time_tips);
	SafeDeleteObject(m_game_time_tips_title);
	SafeDeleteObject(m_battle_area);
	
	SafeDeleteArrayObject(m_score_string);
	SafeDeleteArrayObject(m_game_time_string);
	SafeDeleteArrayObject(m_score_add_string);
}

void GameScene::Init(HWND hWnd)
{
	/* 保存窗口句柄 */
	m_hWnd = hWnd;

	/* 设置场景区域大小 */
	const int margin_x = 10;
	const int margin_y = 10;
	::GetClientRect(m_hWnd, &m_rect);

	// 计算场景的宽度和高度
	m_scene_width = m_rect.right - m_rect.left;
	m_scene_height = m_rect.bottom - m_rect.top;

	// 设置暂停按钮的字体大小和位置
	m_pause_button->SetFontSize(20);
	m_pause_button->SetX(m_rect.left + m_scene_width - m_pause_button->GetWidth());
	m_pause_button->SetY(0);

	// 设置重新开始按钮的字体大小
	m_new_game_button->SetFontSize(30);
	// 设置主菜单按钮的字体大小
	m_switch_to_main_menu_button->SetFontSize(30);
	// 设置退出游戏按钮的字体大小
	m_exit_button->SetFontSize(30);

	// 设置游戏结束标题的字体大小
	m_game_over_title_tips->SetFontSize(35);
	// 设置游戏结束原因的字体大小
	m_game_over_reason_tips->SetFontSize(20);

	// 设置分数增加提示的文本前景色
	m_score_add_tips->SetForeground(RGB(251, 216, 96));
	// 设置游戏结束原因的文本前景色
	m_game_over_reason_tips->SetForeground(RGB(0, 0, 0));
	// 设置分数标题的文本前景色
	m_score_tips_title->SetForeground(RGB(0, 0, 0));
	// 设置游戏时间标题的文本前景色
	m_game_time_tips_title->SetForeground(RGB(0, 0, 0));

	// 设置暂停游戏按钮的点击委托
	m_pause_button->SetClickDelegate(bind(&GameScene::OnPuaseGameButtonClicked, this, _1, _2));
	// 设置重新开始按钮的点击委托
	m_new_game_button->SetClickDelegate(bind(&GameScene::OnNewGameButtonClicked, this, _1, _2));
	// 设置主菜单按钮的点击委托
	m_switch_to_main_menu_button->SetClickDelegate(bind(&GameScene::OnSwitchMainMenuButtonClicked, this, _1, _2));
	// 设置退出游戏按钮的点击委托
	m_exit_button->SetClickDelegate(bind(&GameScene::OnExitGameButtonClicked, this, _1, _2));

	// 游戏对战场的矩形
	RECT rcBattle;
	// 游戏对战场的格子大小
	const int nBattleHLatticeCount = 20, nBattleVLatticeCount = 20;
	memcpy((void*)&rcBattle, (void*)&m_rect, sizeof(m_rect));
	// 计算游戏对战场的矩形
	rcBattle.left += margin_x + (int)((m_scene_width % nBattleHLatticeCount) * .5f);
	rcBattle.right -= margin_x + (int)((m_scene_width % nBattleHLatticeCount) * .5f);
	rcBattle.top += margin_y + (int)((m_scene_height % nBattleVLatticeCount) * .5f);
	rcBattle.bottom	-= margin_y + (int)((m_scene_height % nBattleVLatticeCount) * .5f);
	rcBattle.top += 15;
	rcBattle.bottom += 15;

	// 创建游戏对战场
	m_battle_area = new GameBattleArea(rcBattle, nBattleHLatticeCount, nBattleVLatticeCount);
	// 初始化游戏对战场
	m_battle_area->Init(hWnd);
	// 绑定游戏场景类(GameScene)的游戏结束方法到游戏游戏对战场(GameBattleArea)的成员函数(function)对象中
	m_battle_area->fnGameOver = bind(&GameScene::OnGameOver, this);
	// 绑定游戏场景类(GameScene)的分数增加方法到游戏游戏对战场(GameBattleArea)的成员函数(function)对象中
	m_battle_area->fnBattleScoreAdd = bind(&GameScene::OnGameScoreAdd, this, _1);

	// 设置游戏结束面板的宽度和高度
	m_game_over_panel_width = 300;
	m_game_over_panel_height = 250;

	// 初始化游戏结束面板的中心点
	m_game_over_x_origin = m_rect.left + (int)(m_scene_width * .5f);
	m_game_over_y_origin = m_rect.top + (int)(m_scene_height * .5f);

	// 初始化游戏结束面板的矩形
	m_game_over_panel_rect.left = m_game_over_x_origin - (int)(m_game_over_panel_width * .5f) - 3;
	m_game_over_panel_rect.right = m_game_over_x_origin + (int)(m_game_over_panel_width * .5f) + 3;
	m_game_over_panel_rect.top = m_game_over_y_origin - (int)(m_game_over_panel_height * .5f) - 3;
	m_game_over_panel_rect.bottom = m_game_over_y_origin + (int)(m_game_over_panel_height * .5f) + 3;

	// 设置游戏结束标题"游戏结束"在头顶,大概位置吧(没有固定的,只要不超过游戏结束面板的范围即可).
	m_game_over_title_tips->SetX(m_game_over_x_origin - (int)(m_game_over_title_tips->GetWidth() * .5f));
	m_game_over_title_tips->SetY(m_game_over_y_origin - (int)(m_game_over_panel_height * .5f));
	
	// 设置游戏结束的原因的y坐标位置,也是大概位置吧.
	m_game_over_reason_tips->SetY(m_game_over_y_origin - (int)(m_game_over_panel_height * .5f) + m_game_over_title_tips->GetHeight() + 15);

	// 设置重新开始按钮的位置,也是大概位置吧.
	m_new_game_button->SetX(m_game_over_x_origin - (int)(m_new_game_button->GetWidth() * .5f));
	m_new_game_button->SetY(m_game_over_reason_tips->GetY() + (int)(m_game_over_reason_tips->GetHeight() * .5f) + 35);

	// 设置主菜单按钮的位置,也是大概位置吧.
	m_switch_to_main_menu_button->SetX(m_game_over_x_origin - (int)(m_switch_to_main_menu_button->GetWidth() * .5f));
	m_switch_to_main_menu_button->SetY(m_new_game_button->GetY() + (int)(m_new_game_button->GetHeight() * .5f) + 35);

	// 设置退出游戏按钮的位置,也是大概位置吧.
	m_exit_button->SetX(m_game_over_x_origin - (int)(m_exit_button->GetWidth() * .5f));
	m_exit_button->SetY(m_switch_to_main_menu_button->GetY() + (int)(m_switch_to_main_menu_button->GetHeight() * .5f) + 35);


	// 初始化分数和游戏时间
	m_score = 0;
	m_game_tick = 0;

	// 设置分数的字体大小
	m_score_tips->SetFontSize(20);
	// 设置分数增加提示的字体大小
	m_score_add_tips->SetFontSize(18);
	// 设置分数标题的字体大小
	m_score_tips_title->SetFontSize(20);
	// 设置游戏时间的字体大小
	m_game_time_tips->SetFontSize(20);
	// 设置游戏时间标题的字体大小
	m_game_time_tips_title->SetFontSize(20);

	// 设置分数的x坐标位置
	m_score_tips->SetX(m_score_tips_title->GetX() + m_score_tips_title->GetWidth());
	// 设置分数增加提示的y坐标位置
	m_score_add_tips->SetY(m_score_tips->GetY() + (int)((m_score_tips->GetHeight() - m_score_add_tips->GetHeight()) * .5f));
	// 设置游戏时间标题的x坐标位置
	m_game_time_tips_title->SetX((int)(m_rect.left + m_scene_width * .5f - m_game_time_tips_title->GetWidth() * .5f));
	// 设置游戏时间的x坐标位置
	m_game_time_tips->SetX(m_game_time_tips_title->GetX() + m_game_time_tips_title->GetWidth());
}

void GameScene::Render(HDC hdc)
{
	HBRUSH hGameBackgroundBrush = ::CreateSolidBrush(RGB(134, 71, 63));
	// 渲染游戏场景的背景
	::FillRect(hdc, &m_rect, hGameBackgroundBrush);

	// 渲染游戏对战场
	m_battle_area->Render(hdc);
	// 渲染暂停按钮
	m_pause_button->Render(hdc);

	// 渲染分数
	m_score_tips->Render(hdc);
	// 渲染分数标题
	m_score_tips_title->Render(hdc);
	// 渲染游戏时间
	m_game_time_tips->Render(hdc);
	// 渲染游戏时间标题
	m_game_time_tips_title->Render(hdc);

	// 渲染分数增加
	m_score_add_tips->Render(hdc);

	// 对战是否结束
	if (m_battle_area->IsGameOver())
	{
		// 渲染游戏结束
		RenderGameOver(hdc);
	}

	SafeDeleteGDIObject(hGameBackgroundBrush);
}

void GameScene::Update(DWORD dwDeltaTime)
{
	// 如果分数增加提示可见,则减少可见的时间,直到最后消失
	if (m_score_add_tips->IsVisible())
	{
		m_score_add_tips_time -= dwDeltaTime;
		if (m_score_add_tips_time <= 0)
		{
			m_score_add_tips->SetVisible(false);
		}
	}

	// 游戏对战场的更新
	m_battle_area->Update(dwDeltaTime);

	// 对战是否结束
	if (m_battle_area->IsGameOver())
		return;

	if (!m_battle_area->IsPause())
	{
		// 累加游戏时间(毫秒数)
		m_game_tick += dwDeltaTime;

		// 毫秒数 转 时:分:秒
		int hour = m_game_tick / 3600000;
		int min = m_game_tick % 3600000 / 60000;
		int second = m_game_tick % 3600000 % 60000 / 1000;

		// 输出到游戏时间的字符串缓冲区
		_stprintf_s(m_game_time_string, 16, _T("%02d:%02d:%02d"), hour, min, second);
		// 更新游戏时间
		m_game_time_tips->SetText(m_game_time_string);
	}
}

void GameScene::OnKeyDown(Keys key)
{
	// 游戏对战场的按键处理
	m_battle_area->OnKeyDown(key);
}

void GameScene::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 按钮对窗口消息的处理
	m_pause_button->WndProc(hWnd, message, wParam, lParam);
	m_exit_button->WndProc(hWnd, message, wParam, lParam);

	m_new_game_button->WndProc(hWnd, message, wParam, lParam);
	m_switch_to_main_menu_button->WndProc(hWnd, message, wParam, lParam);
}

void GameScene::OnPuaseGameButtonClicked(int x, int y)
{
	// 暂停对战
	if (!m_battle_area->IsPause())
		m_battle_area->Pause();
	else
		m_battle_area->Continue();

	m_pause_button->SetText(m_battle_area->IsPause() ? _T("继续游戏") : _T("暂停游戏"));
}

void GameScene::OnExitGameButtonClicked(int x, int y)
{
	// 调用退出游戏的委托函数
	fnExitGame();
}

void GameScene::OnNewGameButtonClicked(int x, int y)
{
	// 游戏对战场的重新开始
	m_battle_area->Restart(m_hWnd);

	// 设置游戏结束元素为不可见
	m_game_over_title_tips->SetVisible(false);
	m_game_over_reason_tips->SetVisible(false);

	m_new_game_button->SetVisible(false);
	m_switch_to_main_menu_button->SetVisible(false);
	m_exit_button->SetVisible(false);

	// 启用暂停游戏按钮
	m_pause_button->SetEnable(true);

	// 初始化游戏时间和分数
	m_game_tick = 0;
	m_score = 0;

	// 将分数输出到分数的字符串缓冲区
	swprintf_s(m_score_string, 32, _T("%d"), m_score);
	// 更新分数
	m_score_tips->SetText(m_score_string);
}

void GameScene::OnSwitchMainMenuButtonClicked(int x, int y)
{
	// 调用切换到主菜单的委托函数
	fnSwitchToMainMenu();
}

void GameScene::OnGameOver()
{
	// 对游戏结束原因进行设置不同的游戏结束的原因
	switch (m_battle_area->GetGameOverType())
	{
	case GOT_SELF: // 咬到自己导致游戏结束.
		m_game_over_reason_tips->SetText(_T("你咬到自己啦!"));
		break;
	case GOT_TRAP: // 撞到障壁导致游戏结束.
		m_game_over_reason_tips->SetText(_T("你撞到障壁了!"));
		break;
	case GOT_WIN: // 蛇身达到最长,游戏胜利.
		m_game_over_reason_tips->SetText(_T("好棒,你的蛇蛇达到最长,你赢了!"));
		break;
	default:
		break;
	}

	// 设置游戏结束的原因,也是大概位置吧.
	m_game_over_reason_tips->SetX(m_game_over_x_origin - (int)(m_game_over_reason_tips->GetWidth() * .5f));

	// 设置"游戏结束"标题的可见性
	m_game_over_title_tips->SetVisible(true);
	// 设置游戏结束原因的可见性
	m_game_over_reason_tips->SetVisible(true);

	// 设置重新开始和主菜单以及退出游戏按钮的可见性
	m_new_game_button->SetVisible(true);
	m_switch_to_main_menu_button->SetVisible(true);
	m_exit_button->SetVisible(true);

	// 停止启用暂停按钮
	m_pause_button->SetEnable(false);
}

void GameScene::OnGameScoreAdd(int score)
{
	// 累加分数
	m_score += score;

	// 将分数输出到分数的字符串缓冲区
	_stprintf_s(m_score_string, 32, _T("%d"), m_score);
	// 更新分数
	m_score_tips->SetText(m_score_string);

	// 将分数增加输出到分数的字符串缓冲区
	_stprintf_s(m_score_add_string, 8, _T("+%d"), score);
	// 更新分数增加
	m_score_add_tips->SetText(m_score_add_string);
	// 设置分数增加的位置
	m_score_add_tips->SetX(m_score_tips->GetX() + m_score_tips->GetWidth() + 5);
	// 设置分数增加提示的可见性
	m_score_add_tips->SetVisible(true);
	// 设置分数的可见时间
	m_score_add_tips_time = m_score_add_tips_visible_time;
}

void GameScene::RenderGameOver(HDC hdc)
{
	// 调用Util的AlphaBlend 透明混合
	Util::AlphaBlend(hdc, m_rect.left, m_rect.top, m_scene_width, m_scene_height, RGB(0, 0, 0), 168);

	HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(23, 23, 23));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
	HBRUSH hBrush = (HBRUSH)::CreateSolidBrush(RGB(142, 53, 74));
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制矩形(带边框的)
	::Rectangle(hdc, m_game_over_panel_rect.left, m_game_over_panel_rect.top, m_game_over_panel_rect.right, m_game_over_panel_rect.bottom);
	
	// 渲染游戏结束标题
	m_game_over_title_tips->Render(hdc);
	// 渲染游戏结束原因
	m_game_over_reason_tips->Render(hdc);
	// 渲染重新开始按钮
	m_new_game_button->Render(hdc);
	// 渲染主菜单按钮
	m_switch_to_main_menu_button->Render(hdc);
	// 渲染退出游戏按钮
	m_exit_button->Render(hdc);

	::SelectObject(hdc, hOldBrush);
	::SelectObject(hdc, hOldPen);
	SafeDeleteGDIObject(hBrush);
	SafeDeleteGDIObject(hPen);
}

下面进行讲解上面的程序:


GameScene::GameScene()

GameScene::GameScene() 
	: m_score_add_tips_visible_time(500L),					// 设置分数增提示的可见时间
	m_score_add_tips_time(m_score_add_tips_visible_time)	// 初始化分数增加提示当前已显示的时间
{
	// 创建按钮等.......(看名字哈)
	......

	// 创建标签等.......(看名字哈)
	......

	// 设置增加分数不可见
	m_score_add_tips->SetVisible(false);

	// 设置游戏结束提示等元素不可见
	m_game_over_title_tips->SetVisible(false);
	m_game_over_reason_tips->SetVisible(false);

	m_new_game_button->SetVisible(false);
	m_switch_to_main_menu_button->SetVisible(false);
	m_exit_button->SetVisible(false);

	// 为分数和游戏时间的字符串缓冲区分配内存
	m_score_string = new TCHAR[32];
	m_game_time_string = new TCHAR[16];
	m_score_add_string = new TCHAR[8];
}

1. 创建按钮和标签

2. 设置分数增加提示和游戏结束的按钮、标签等不可见。(等游戏结束后在进行显示)

3. 为字符串的内存缓冲区分配内存空间


GameScene::~GameScene()

GameScene::~GameScene()
{
	// 释放游戏场景的资源
	SafeDeleteObject(m_pause_button);
	SafeDeleteObject(m_new_game_button);
	SafeDeleteObject(m_switch_to_main_menu_button);
	SafeDeleteObject(m_exit_button);
	SafeDeleteObject(m_game_over_title_tips);
	SafeDeleteObject(m_game_over_reason_tips);
	SafeDeleteObject(m_score_tips);
	SafeDeleteObject(m_score_tips_title);
	SafeDeleteObject(m_game_time_tips);
	SafeDeleteObject(m_game_time_tips_title);
	SafeDeleteObject(m_battle_area);
	
	SafeDeleteArrayObject(m_score_string);
	SafeDeleteArrayObject(m_game_time_string);
	SafeDeleteArrayObject(m_score_add_string);
}

1. 同样的我们在析构函数中也要进行释放场景中占用的资源


GameScene::Init(HWND hWnd)

void GameScene::Init(HWND hWnd)
{
	/* 保存窗口句柄 */
	m_hWnd = hWnd;

	/* 设置场景区域大小 */
	const int margin_x = 10;
	const int margin_y = 10;
	::GetClientRect(m_hWnd, &m_rect);

	// 计算场景的宽度和高度
	m_scene_width = m_rect.right - m_rect.left;
	m_scene_height = m_rect.bottom - m_rect.top;

	// 设置按钮、标签等的字体大小、位置、前景色等
	......
    
	// 设置暂停游戏按钮的点击委托
	m_pause_button->SetClickDelegate(bind(&GameScene::OnPuaseGameButtonClicked, this, _1, _2));
	// 设置重新开始按钮的点击委托
	m_new_game_button->SetClickDelegate(bind(&GameScene::OnNewGameButtonClicked, this, _1, _2));
	// 设置主菜单按钮的点击委托
	m_switch_to_main_menu_button->SetClickDelegate(bind(&GameScene::OnSwitchMainMenuButtonClicked, this, _1, _2));
	// 设置退出游戏按钮的点击委托
	m_exit_button->SetClickDelegate(bind(&GameScene::OnExitGameButtonClicked, this, _1, _2));

	// 游戏对战场的矩形
	RECT rcBattle;
	// 游戏对战场的格子大小
	const int nBattleHLatticeCount = 20, nBattleVLatticeCount = 20;
	memcpy((void*)&rcBattle, (void*)&m_rect, sizeof(m_rect));
	// 计算游戏对战场的矩形
	rcBattle.left += margin_x + (int)((m_scene_width % nBattleHLatticeCount) * .5f);
	rcBattle.right -= margin_x + (int)((m_scene_width % nBattleHLatticeCount) * .5f);
	rcBattle.top += margin_y + (int)((m_scene_height % nBattleVLatticeCount) * .5f);
	rcBattle.bottom	-= margin_y + (int)((m_scene_height % nBattleVLatticeCount) * .5f);
	rcBattle.top += 15;
	rcBattle.bottom += 15;

	// 创建游戏对战场
	m_battle_area = new GameBattleArea(rcBattle, nBattleHLatticeCount, nBattleVLatticeCount);
	// 初始化游戏对战场
	m_battle_area->Init(hWnd);
	// 绑定游戏场景类(GameScene)的游戏结束方法到游戏游戏对战场(GameBattleArea)的成员函数(function)对象中
	m_battle_area->fnGameOver = bind(&GameScene::OnGameOver, this);
	// 绑定游戏场景类(GameScene)的分数增加方法到游戏游戏对战场(GameBattleArea)的成员函数(function)对象中
	m_battle_area->fnBattleScoreAdd = bind(&GameScene::OnGameScoreAdd, this, _1);

	// 设置游戏结束面板的宽度和高度
	m_game_over_panel_width = 300;
	m_game_over_panel_height = 250;
	// 设置游戏结束面板的中心点
	m_game_over_x_origin = m_rect.left + (int)(m_scene_width * .5f);
	m_game_over_y_origin = m_rect.top + (int)(m_scene_height * .5f);

	// 初始化分数和游戏时间
	m_score = 0;
	m_game_tick = 0;
}

1. 保存游戏窗口句柄

2. 初始化场景的宽度和高度

3. 初始化场景按钮、标签等的字体、位置等

4. 设置按钮点击的函数对象

5. 创建游戏对战场(GameBattleArea),传入对战场的矩形区域以及横纵格子数

6. 初始化游戏对战场

7. 设置游戏对战场的游戏结束和分数增加的函数对象


GameScene::Render(HDC hdc)

void GameScene::Render(HDC hdc)
{
	HBRUSH hGameBackgroundBrush = ::CreateSolidBrush(RGB(134, 71, 63));
	// 渲染游戏场景的背景
	::FillRect(hdc, &m_rect, hGameBackgroundBrush);

	// 渲染游戏对战场
	m_battle_area->Render(hdc);
	// 渲染暂停按钮
	m_pause_button->Render(hdc);

	// 渲染分数
	m_score_tips->Render(hdc);
	// 渲染分数标题
	m_score_tips_title->Render(hdc);
	// 渲染游戏时间
	m_game_time_tips->Render(hdc);
	// 渲染游戏时间标题
	m_game_time_tips_title->Render(hdc);

	// 渲染分数增加
	m_score_add_tips->Render(hdc);

	// 对战是否结束
	if (m_battle_area->IsGameOver())
	{
		// 渲染游戏结束
		RenderGameOver(hdc);
	}

	SafeDeleteGDIObject(hGameBackgroundBrush);
}

1. 填充游戏场景的背景区域

2. 渲染游戏对战场

3. 渲染按钮、标签等

4. 如果游戏结束,则渲染游戏结束的元素


GameScene::Update(DWORD dwDeltaTime)

void GameScene::Update(DWORD dwDeltaTime)
{
	// 如果分数增加提示可见,则减少可见的时间,直到最后消失
	if (m_score_add_tips->IsVisible())
	{
		m_score_add_tips_time -= dwDeltaTime;
		if (m_score_add_tips_time <= 0)
		{
			m_score_add_tips->SetVisible(false);
		}
	}

	// 游戏对战场的更新
	m_battle_area->Update(dwDeltaTime);

	// 对战是否结束
	if (m_battle_area->IsGameOver())
		return;

	if (!m_battle_area->IsPause())
	{
		// 累加游戏时间(毫秒数)
		m_game_tick += dwDeltaTime;

		// 毫秒数 转 时:分:秒
		int hour = m_game_tick / 3600000;
		int min = m_game_tick % 3600000 / 60000;
		int second = m_game_tick % 3600000 % 60000 / 1000;

		// 输出到游戏时间的字符串缓冲区
		_stprintf_s(m_game_time_string, 16, _T("%02d:%02d:%02d"), hour, min, second);
		// 更新游戏时间
		m_game_time_tips->SetText(m_game_time_string);
	}
}

1. 如果分数增加的标签存在则逐渐减少分数增加标签的可见时间(毫秒数),直到消失

2. 调用游戏对战场的Update(逻辑更新)

3. 如果游戏未结束并且未暂停,则累加游戏时间,同时更新游戏时间标签


GameScene::OnKeyDown(Keys key)

void GameScene::OnKeyDown(Keys key)
{
	// 游戏对战场的按键处理
	m_battle_area->OnKeyDown(key);
}

1. 调用游戏对战场的按键处理


GameScene::WndProc(HWND, UINT, WPARAM, LPARAM)

void GameScene::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 按钮对窗口消息的处理
	m_pause_button->WndProc(hWnd, message, wParam, lParam);
	m_exit_button->WndProc(hWnd, message, wParam, lParam);

	m_new_game_button->WndProc(hWnd, message, wParam, lParam);
	m_switch_to_main_menu_button->WndProc(hWnd, message, wParam, lParam);
}

1. 调用按钮的窗口消息处理


GameScene::OnPuaseGameButtonClicked(int x, int y)

void GameScene::OnPuaseGameButtonClicked(int x, int y)
{
	// 暂停对战
	if (!m_battle_area->IsPause())
		m_battle_area->Pause();
	else
		m_battle_area->Continue();

	m_pause_button->SetText(m_battle_area->IsPause() ? _T("继续游戏") : _T("暂停游戏"));
}

1. 暂停游戏或继续游戏按钮被点击,调用游戏对战场的暂停游戏或继续游戏的方法

2. 更新按钮显示的文字


GameScene::OnExitGameButtonClicked(int x, int y)

void GameScene::OnExitGameButtonClicked(int x, int y)
{
	// 调用退出游戏的委托函数
	fnExitGame();
}

1. 退出游戏按钮被点击,调用Game的退出游戏的函数对象


GameScene::OnNewGameButtonClicked(int x, int y)

void GameScene::OnNewGameButtonClicked(int x, int y)
{
	// 游戏对战场的重新开始
	m_battle_area->Restart(m_hWnd);

	// 设置游戏结束元素为不可见
	m_game_over_title_tips->SetVisible(false);
	m_game_over_reason_tips->SetVisible(false);

	m_new_game_button->SetVisible(false);
	m_switch_to_main_menu_button->SetVisible(false);
	m_exit_button->SetVisible(false);

	// 启用暂停游戏按钮
	m_pause_button->SetEnable(true);

	// 初始化游戏时间和分数
	m_game_tick = 0;
	m_score = 0;

	// 将分数输出到分数的字符串缓冲区
	swprintf_s(m_score_string, 32, _T("%d"), m_score);
	// 更新分数
	m_score_tips->SetText(m_score_string);
}

1. 调用游戏对战场的重新开始方法

2. 设置游戏结束元素的不可见

3. 启用暂停游戏按钮

4. 初始化分数和游戏时间,同时更新标签的文字


GameScene::OnSwitchMainMenuButtonClicked(int x, int y)

void GameScene::OnSwitchMainMenuButtonClicked(int x, int y)
{
	// 调用切换到主菜单的委托函数
	fnSwitchToMainMenu();
}

 1. 切换到主菜单按钮被点击,调用Game的切换到主菜单场景的函数对象


GameScene::OnGameOver()

void GameScene::OnGameOver()
{
	// 对游戏结束原因进行设置不同的游戏结束的原因
	switch (m_battle_area->GetGameOverType())
	{
	case GOT_SELF: // 咬到自己导致游戏结束.
		m_game_over_reason_tips->SetText(_T("你咬到自己啦!"));
		break;
	case GOT_TRAP: // 撞到障壁导致游戏结束.
		m_game_over_reason_tips->SetText(_T("你撞到障壁了!"));
		break;
	case GOT_WIN: // 蛇身达到最长,游戏胜利.
		m_game_over_reason_tips->SetText(_T("好棒,你的蛇蛇达到最长,你赢了!"));
		break;
	default:
		break;
	}

	// 设置游戏结束标题"游戏结束"在头顶,大概位置吧(没有固定的,只要不超过游戏结束面板的范围即可).
	m_game_over_title_tips->SetX(m_game_over_x_origin - (int)(m_game_over_title_tips->GetWidth() * .5f));
	// 设置游戏结束的原因,也是大概位置吧.
	m_game_over_reason_tips->SetX(m_game_over_x_origin - (int)(m_game_over_reason_tips->GetWidth() * .5f));

	// 设置"游戏结束"标题的可见性
	m_game_over_title_tips->SetVisible(true);
	// 设置游戏结束原因的可见性
	m_game_over_reason_tips->SetVisible(true);

	// 设置重新开始和主菜单以及退出游戏按钮的可见性
	m_new_game_button->SetVisible(true);
	m_switch_to_main_menu_button->SetVisible(true);
	m_exit_button->SetVisible(true);

	// 停止启用暂停按钮
	m_pause_button->SetEnable(false);
}

1. 根据游戏结束的类型,进行设置不同的游戏结束原因

2. 设置游戏结束原因标签的x坐标位置(因为不同的原因显示不同的文本,要重新计算位置)

3. 设置游戏结束元素的可见

4. 禁用暂停游戏按钮


GameScene::OnGameScoreAdd(int score)

void GameScene::OnGameScoreAdd(int score)
{
	// 累加分数
	m_score += score;

	// 将分数输出到分数的字符串缓冲区
	_stprintf_s(m_score_string, 32, _T("%d"), m_score);
	// 更新分数
	m_score_tips->SetText(m_score_string);

	// 将分数增加输出到分数的字符串缓冲区
	_stprintf_s(m_score_add_string, 8, _T("+%d"), score);
	// 更新分数增加
	m_score_add_tips->SetText(m_score_add_string);
	// 设置分数增加的位置
	m_score_add_tips->SetX(m_score_tips->GetX() + m_score_tips->GetWidth() + 5);
	// 设置分数增加提示的可见性
	m_score_add_tips->SetVisible(true);
	// 设置分数的可见时间
	m_score_add_tips_time = m_score_add_tips_visible_time;
}

1. 累加分数

2. 重新设置分数标签

3. 设置分数增加标签和标签的可见时间


GameScene::RenderGameOver(HDC hdc)

void GameScene::RenderGameOver(HDC hdc)
{
	// 调用Util的AlphaBlend 透明混合
	Util::AlphaBlend(hdc, m_rect.left, m_rect.top, m_scene_width, m_scene_height, RGB(0, 0, 0), 168);

	HPEN hPen = ::CreatePen(PS_SOLID, 3, RGB(23, 23, 23));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
	HBRUSH hBrush = (HBRUSH)::CreateSolidBrush(RGB(142, 53, 74));
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制矩形(带边框的)
	::Rectangle(hdc, m_game_over_panel_rect.left, m_game_over_panel_rect.top, m_game_over_panel_rect.right, m_game_over_panel_rect.bottom);
	
	// 渲染游戏结束标题
	m_game_over_title_tips->Render(hdc);
	// 渲染游戏结束原因
	m_game_over_reason_tips->Render(hdc);
	// 渲染重新开始按钮
	m_new_game_button->Render(hdc);
	// 渲染主菜单按钮
	m_switch_to_main_menu_button->Render(hdc);
	// 渲染退出游戏按钮
	m_exit_button->Render(hdc);

	::SelectObject(hdc, hOldBrush);
	::SelectObject(hdc, hOldPen);
	SafeDeleteGDIObject(hBrush);
	SafeDeleteGDIObject(hPen);
}

1. 使用透明混合绘制矩形,将场景变得暗淡(就像一般的游戏输掉背景都会变暗)

2. 绘制游戏结束的面板,矩形加边框

3. 渲染游戏结束面板上的按钮、标签等


GameBattleArea (游戏对战场)

头文件:

//++++++++++++++++++++++++++++++++++
// 游戏游戏对战场类
//----------------------------------

#pragma once

#ifndef __GAME_BATTLE_AREA_H__
#define __GAME_BATTLE_AREA_H__

class Snake;                    // 声明贪吃蛇类
class Fruit;                    // 声明食物类
class Trap;                     // 声明障壁类
enum SnakeHeadCollisionType;    // 声明蛇头碰撞枚举

class GameBattleArea
{
public:
	GameBattleArea(RECT rcBattle, const int nHLatticeCount, const int nVLatticeCount);
	~GameBattleArea();

public:
	// 初始化场景
	void Init(HWND hWnd);					
	// 渲染场景
	void Render(HDC hdc);					
	// 更新场景
	void Update(DWORD dwDeltaTime);			
	// 场景按键处理
	void OnKeyDown(Keys key);				

	// 检测蛇头的碰撞
	void GetSnakeHeadCollision(SnakeHeadCollisionType &collisionType, DWORD dwDeltaTime);	
	// 对战暂停
	void Pause();							
	// 对战继续
	void Continue();						
	// 游戏游戏对战场重新开始
	void Restart(HWND hWnd);				
	// 获取游戏结束的类型
	GameOverType GetGameOverType();			
	// 游戏游戏对战场是否游戏结束
	bool IsGameOver();						
	// 是否暂停游戏
	bool IsPause();							

private:
	// 生成食物
	void RespawnFruit();					
	// 生成障壁
	void SpawnTraps();						
	// 渲染失败提示
	void RenderDefeatTips(HDC hdc);			

public:
	GameDelegateFunction fnGameOver;                    // 游戏结束的函数(function)对象
	GameBattleScoreDelegateFunction fnBattleScoreAdd;   // 对战分数增加的函数(function)对象

private:
	std::vector<Trap> m_traps;      // 区域中的障壁
	Snake *m_snake;                 // 贪吃蛇
	Fruit *m_fruit;                 // 区域中的食物
	RECT m_battle_rect;             // 区域矩形
	GameOverType m_game_over_type;  // 游戏结束类型
	const int m_h_lattice_count;    // 区域中的横向格子数
	const int m_v_lattice_count;    // 区域中的纵向格子数
	int m_lattice_width;            // 区域中的格子宽度
	int m_lattice_height;           // 区域中的格子高度
	bool m_is_pause;                // 是否暂停对战
	bool m_is_game_over;            // 是否对战结束
	bool m_is_scale_up;             // 放大或者缩写失败提示的"×"
	float m_defeat_tips_scale_rate; // 失败提示的"×"缩放比例
};

#endif // !__GAME_BATTLE_AREA_H__

源文件:


#include <windows.h>            // WIN32开发中最重要的头文件(不用详解了吧...)
#include <tchar.h>              // 通用字符集头文件,对于字符集之间的兼容比如ASCII编码和Unicode编码
#include <time.h>               // 使用与时间相关的函数(例如这里的time())
#include <vector>               // vector容器所在头文件
#include <algorithm>            // 提供了大量基于迭代器的非成员模板函数

#include "GameType.h"           // 游戏中使用到的一些枚举、别名定义
#include "Snake.h"              // 贪吃蛇类
#include "Trap.h"               // 障壁类
#include "Fruit.h"              // 食物类
#include "util.h"               // 常用工具类
#include "GameBattleArea.h"     // 游戏对战场类

GameBattleArea::GameBattleArea(RECT rcBattle, const int nHLatticeCount, const int nVLatticeCount) 
	: m_battle_rect(rcBattle),              // 初始化对战的矩形
	m_h_lattice_count(nHLatticeCount),      // 初始化对战的横向格子数
	m_v_lattice_count(nVLatticeCount)       // 初始化对战的纵向格子数
{
	// 创建贪吃蛇
	m_snake = new Snake(m_v_lattice_count * m_h_lattice_count);
	// 创建食物
	m_fruit = new Fruit(1);
	// 初始化格子宽度和高度
	m_lattice_width = (m_battle_rect.right - m_battle_rect.left) / m_h_lattice_count;
	m_lattice_height = (m_battle_rect.bottom - m_battle_rect.top) / m_v_lattice_count;
	// 初始化失败的 "×" 的缩放率
	m_defeat_tips_scale_rate = .3f;
	// 初始化放大缩放
	m_is_scale_up = true;
}

GameBattleArea::~GameBattleArea()
{
	// 释放游戏游戏对战场的资源
	SafeDeleteObject(m_snake);
	SafeDeleteObject(m_fruit);

	// 清空障壁
	m_traps.clear();
}

void GameBattleArea::Init(HWND hWnd)
{
	// 清空障壁
	m_traps.clear();

	// 初始化蛇的坐标位置
	int center_x, center_y;
	center_x = m_battle_rect.left + (int)(m_h_lattice_count / 2 * m_lattice_width + m_lattice_width * .5f);
	center_y = m_battle_rect.top + (int)(m_v_lattice_count / 2 * m_lattice_height + m_lattice_height * .5f);

	// 初始化蛇
	m_snake->Init(m_lattice_width, m_lattice_height, center_x, center_y);

	// 生成水果
	RespawnFruit();

	// 生成障壁
	SpawnTraps();
}

void GameBattleArea::Render(HDC hdc)
{
	//渲染游戏游戏对战场

	HBRUSH hBattleBackgroundBrush = ::CreateSolidBrush(RGB(144, 81, 73));
	// 填充游戏对战场的背景
	::FillRect(hdc, &m_battle_rect, hBattleBackgroundBrush);

	// 渲染蛇
	m_snake->Render(hdc);

	if (m_fruit != NULL) {
		// 渲染食物
		m_fruit->Render(hdc);
	}

	// 遍历渲染障壁
	for (int i = 0, count = m_traps.size(); i < count; ++i)
		m_traps[i].Render(hdc);

	// 渲染失败提示的 "×"
	if (m_is_game_over)
		RenderDefeatTips(hdc);

	SafeDeleteGDIObject(hBattleBackgroundBrush);
}

void GameBattleArea::Update(DWORD dwDeltaTime)
{
	// 游戏对战场的更新

	// 判断是否暂停或游戏结束
	if (!m_is_pause && !m_is_game_over)
	{
		// 蛇头碰撞的类型
		SnakeHeadCollisionType collisionType = SnakeHeadCollisionType::SHCT_AIR;
		// 获取蛇头碰撞的类型
		GetSnakeHeadCollision(collisionType, dwDeltaTime);

		switch (collisionType)
		{
		case SHCT_SELF: // 咬到自己
			m_is_game_over = true;
			m_game_over_type = GameOverType::GOT_SELF;
			fnGameOver();
			break;
		case SHCT_TRAP: // 碰到障壁
			m_is_game_over = true;
			m_game_over_type = GameOverType::GOT_TRAP;
			fnGameOver();
			break;
		case SHCT_FRUIT: // 吃到食物
			// 蛇身自增
			m_snake->Increase();
			// 更新贪吃蛇
			m_snake->Update(dwDeltaTime);
			// 判断贪吃蛇是否达到最大长度
			if (!m_snake->IsMaxLength())
			{
				// 增加分数
				fnBattleScoreAdd(m_fruit->GetScore());
				// 生成食物
				RespawnFruit();
			}
			else 
			{
				// 贪吃蛇赢得了比赛
				m_is_game_over = true;
				m_game_over_type = GameOverType::GOT_WIN;
				fnGameOver();
			}
			break;
		default:
			// 更新贪吃蛇
			m_snake->Update(dwDeltaTime);
			break;
		}
	}

	// 判断是否游戏结束,进行缩放失败的 "×"
	if (m_is_game_over)
	{
		// 判断是否放大缩放
		if (m_is_scale_up)
		{
			// 按照一定比率缩放
			m_defeat_tips_scale_rate += dwDeltaTime * .001f;
			// 达到最大缩放,进行缩小
			if (m_defeat_tips_scale_rate >= 1.0f)
			{
				m_defeat_tips_scale_rate = 1.0f;
				m_is_scale_up = false;
			}
		}
		else 
		{
			// 按照一定比率缩放
			m_defeat_tips_scale_rate -= dwDeltaTime * .001f;
			// 达到最小缩放,进行放大
			if (m_defeat_tips_scale_rate <= .3f)
			{
				m_defeat_tips_scale_rate = .3f;
				m_is_scale_up = true;
			}
		}
	}
}

void GameBattleArea::OnKeyDown(Keys key)
{
	// 判断是否暂停 或 游戏结束
	if (m_is_pause || m_is_game_over)
		return;

	// 贪吃蛇按键处理
	m_snake->OnKeyDown(key);
}

void GameBattleArea::RespawnFruit()
{
	// 获取蛇身节点数,以用来确定保存蛇身的数组
	int array_count = m_snake->GetNodeCount();
	// 根据蛇身数,进行创建数组
	int *pNodesPositionArray = new int[array_count * 2];
	// 将蛇身的所有节点的位置保存到数组
	// 保存的格式为: array[0]为x坐标, array[1]为y坐标
	m_snake->GetNodesPosition(pNodesPositionArray);

	// 将位置坐标进行重新计算为格子坐标
	std::vector<int> nodes;
	int temp1 = 0, temp2 = 0;
	for (int i = 0, count = array_count * 2; i < count; i += 2)
	{
		// 将蛇的节点x坐标位置转换为格子的x坐标
		temp1 = (pNodesPositionArray[i] - m_battle_rect.left) / m_lattice_width - 1;
		// 将蛇的节点y坐标位置转换为格子的y坐标
		temp2 = (pNodesPositionArray[i + 1] - m_battle_rect.top) / m_lattice_height - 1;
		// 将格子坐标(x, y)以数值保存
		// 数值格式: x + y * (横向格子数 - 2(这个2为左右障壁)),
		// 这样做是为了方便使用索引进行删除非空格子
		// 因为empties保存的顺序就是与这个数值相关的
		nodes.push_back(temp1 + temp2 * (m_h_lattice_count-2));
	}

	// 将除了障壁的格子的坐标都保存到vector容器中
	std::vector<POINT> empties;
	for (int row = 1, row_count = m_v_lattice_count - 1; row < row_count; ++row)
	{
		for (int col = 1, col_count = m_h_lattice_count - 1; col < col_count; ++col)
		{
			// 将格子坐标以POINT结构体进行保存
			POINT pt; pt.x = col; pt.y = row;
			// 添加到empties中
			empties.push_back(pt);
		}
	}

	temp1 = temp2 = 0;
	// 将蛇的节点坐标数值以降序的方式进行排序
	std::sort(nodes.begin(), nodes.end());
	// 通过坐标的数字使用索引进行删除非空节点
	for (int i = 0, count = nodes.size(); i < count; ++i)
	{
		// temp1为empties中非空格子的索引
		// items[i]为蛇的节点所在的节点坐标(以数值的格式)
		// temp2为已在empties中删除的格子数
		temp1 = nodes[i] - temp2;
		// 使用erase进行删除指定的非空格子
		empties.erase(empties.begin() + temp1);
		// 累加删除的格子数
		++temp2;
	}

	// 如果存在空格子
	if (empties.size() > 0)
	{
		// 随机种子
		srand((unsigned)time(NULL));
		// 随机数
		int rnd = rand() % empties.size();
		// 食物的横纵坐标
		int x_fruit = empties[rnd].x;
		int y_fruit = empties[rnd].y;
		// 计算食物的位置坐标
		x_fruit = m_battle_rect.left + x_fruit * m_lattice_width + m_lattice_width / 2;
		y_fruit = m_battle_rect.top + y_fruit * m_lattice_height + m_lattice_height / 2;
		// 重新设置新的食物的位置坐标
		int score = rand() % 3;
		m_fruit->Reset(score == 0 ? 1 : score, x_fruit, y_fruit, m_lattice_width, m_lattice_height);
	}

	// 释放占用的资源
	empties.clear();
	nodes.clear();
	SafeDeleteArrayObject(pNodesPositionArray);
}

void GameBattleArea::GetSnakeHeadCollision(SnakeHeadCollisionType &collisionType, DWORD dwDeltaTime)
{
	// 蛇头碰撞类型
	collisionType = SnakeHeadCollisionType::SHCT_AIR;
	// 横纵坐标偏移量
	const int x_offset = m_battle_rect.left + (int)(m_lattice_width * .5f);
	const int y_offset = m_battle_rect.top + (int)(m_lattice_height * .5f);
	// 蛇头位置坐标
	int x_snake_head, y_snake_head;
	m_snake->GetSnakeHeadNextMovePosition(&x_snake_head, &y_snake_head, dwDeltaTime);
	// 重计算蛇头位置为格子坐标
	int x_head_lattice = (x_snake_head - x_offset) / m_lattice_width;
	int y_head_lattice = (y_snake_head - y_offset) / m_lattice_height;

	// 蛇头位置在障壁的区域内
	if (x_head_lattice == 0 || x_head_lattice == m_h_lattice_count-1 ||
		y_head_lattice == 0 || y_head_lattice == m_v_lattice_count-1)
	{
		collisionType = SnakeHeadCollisionType::SHCT_TRAP;
		return;
	}

	int x_fruit_lattice = (m_fruit->GetX() - x_offset) / m_lattice_width;
	int y_fruit_lattice = (m_fruit->GetY() - y_offset) / m_lattice_height;
	// 蛇头位置在食物的位置中
	if (x_head_lattice == x_fruit_lattice && y_head_lattice == y_fruit_lattice)
	{
		collisionType = SnakeHeadCollisionType::SHCT_FRUIT;
		return;
	}

	// 蛇头的位置在蛇身中
	if (m_snake->IsEatSelf(dwDeltaTime))
	{
		collisionType = SnakeHeadCollisionType::SHCT_SELF;
		return;
	}
}

void GameBattleArea::Pause()
{
	// 暂停对战
	m_is_pause = true;
	// 暂停贪吃蛇的移动
	m_snake->Pause();
}

void GameBattleArea::Continue()
{
	// 继续对战
	m_is_pause = false;
	// 继续贪吃蛇的移动
	m_snake->Continue();
}

void GameBattleArea::SpawnTraps()
{
	// 初始化默认障壁格式
	EnumTrapStyle trapStyle = EnumTrapStyle::TS_DOWNTIP;
	// 遍历格子
	for (int row = 0; row < m_v_lattice_count; ++row)
	{
		for (int col = 0; col < m_h_lattice_count; ++col)
		{
			// 如果格子为第一列 或者 最后一列
			if (row == 0 || row == m_v_lattice_count - 1)
			{
				// 如果为第一行 或者 最后一行 (即为四个角)
				// 第一列为x   第一行为y   即(0, 0)
				// 最后一列为x 第一行为y   即(h, 0)
				// 第一列为x   最后一行为y 即(0, v)
				// 第一列为x   最后一行为y 即(h, v)
				if (col == 0 || col == m_h_lattice_count - 1)
				{
					// 设置障壁样式为块
					trapStyle = EnumTrapStyle::TS_BLOCK;
				}
				else
				{
					// 中间部分, 根据行数
					// 行数为第一行,则为向下的刺
					// 行数为最后一行,则为向上的刺
					trapStyle = row == 0 ? EnumTrapStyle::TS_UPTIP : EnumTrapStyle::TS_DOWNTIP;
				}
			}
			// 如果为第一列
			else if (col == 0)
			{
				// 则为左边的刺
				trapStyle = EnumTrapStyle::TS_LEFTTIP;
			}
			// 如果为最后一列
			else if (col == m_h_lattice_count - 1)
			{
				// 则为右边的刺
				trapStyle = EnumTrapStyle::TS_RIGHTTIP;
			}
			else
				// 如果不是四边的格子,则跳过
				continue;

			// 创建障壁
			Trap trap(trapStyle, (int)(m_battle_rect.left + col * m_lattice_width + m_lattice_width * .5f),
				(int)(m_battle_rect.top + row * m_lattice_height + m_lattice_height * .5f), m_lattice_width, m_lattice_height);
			// 添加到保存障壁的容器中
			m_traps.push_back(trap);
		}
	}
}

bool GameBattleArea::IsGameOver()
{
	// 返回是否对战结束
	return m_is_game_over;
}

bool GameBattleArea::IsPause()
{
	return m_is_pause;
}

void GameBattleArea::Restart(HWND hWnd)
{
	// 重新开始对战
	m_is_game_over = false;
	m_is_pause = false;
	// 重新初始化对战
	Init(hWnd);
}

GameOverType GameBattleArea::GetGameOverType()
{
	// 游戏结束的类型
	return m_game_over_type;
}

void GameBattleArea::RenderDefeatTips(HDC hdc)
{
	// 判断是否胜利,则返回
	if (m_game_over_type == GameOverType::GOT_WIN)
		return;

	HPEN hPen = ::CreatePen(PS_SOLID, 6, RGB(255, 0, 0));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

	// 获取蛇头位置坐标
	int x_center = m_snake->GetHeadX();
	int y_center = m_snake->GetHeadY();

	// 根据蛇头位置,进行加上对应的偏移量,则为碰撞到的位置
#if false
	// 凑合着看吧...o((⊙﹏⊙))o
	"
	 * 
	 *       \   / <----- 失败的提示
	 *       *****  
	 *      **/ \**   
	 *     **     **  
	 *    **       ** 
	 *   *************
	 *   *************
	 *   *************
	 *
	 "
#endif 
	switch (m_snake->GetHeadDir())
	{
	case SND_UP:
		y_center -= (int)(m_lattice_height * .5f);
		break;
	case SND_DOWN:
		y_center += (int)(m_lattice_height * .5f);
		break;
	case SND_LEFT:
		x_center -= (int)(m_lattice_width * .5f);
		break;
	case SND_RIGHT:
		x_center += (int)(m_lattice_width * .5f);
		break;
	default:
		break;
	}

	/* 绘制 "×" */
	float len = m_lattice_width * .5f * m_defeat_tips_scale_rate;
	float angles[] = {
		45, 135, 225, 315
	};

	int points[2][4];

	float rad = 3.1415926f / 180.0f;
	/* 第一条 */
	int x_lt = (int)(x_center + len * cos(angles[0] * rad));
	int y_lt = (int)(y_center + len * sin(angles[0] * rad));
	int x_rd = (int)(x_center + len * cos(angles[2] * rad));
	int y_rd = (int)(y_center + len * sin(angles[2] * rad));

	/* 第二条 */
	int x_rt = (int)(x_center + len * cos(angles[1] * rad));
	int y_rt = (int)(y_center + len * sin(angles[1] * rad));
	int x_ld = (int)(x_center + len * cos(angles[3] * rad));
	int y_ld = (int)(y_center + len * sin(angles[3] * rad));

	points[0][0] = x_lt;
	points[0][1] = y_lt;
	points[0][2] = x_rd;
	points[0][3] = y_rd;

	points[1][0] = x_rt;
	points[1][1] = y_rt;
	points[1][2] = x_ld;
	points[1][3] = y_ld;

	/* 绘制 */
	Util::DrawLine(hdc, points[0]);
	Util::DrawLine(hdc, points[1]);

	::SelectObject(hdc, hOldPen);
	SafeDeleteGDIObject(hPen);
}

下面进行讲解上面的程序:


GameBattleArea::GameBattleArea(RECT, const int, const int)

GameBattleArea::GameBattleArea(RECT rcBattle, const int nHLatticeCount, const int nVLatticeCount) 
	: m_battle_rect(rcBattle),              // 初始化对战的矩形
	m_h_lattice_count(nHLatticeCount),      // 初始化对战的横向格子数
	m_v_lattice_count(nVLatticeCount)       // 初始化对战的纵向格子数
{
	// 创建贪吃蛇
	m_snake = new Snake(m_v_lattice_count * m_h_lattice_count);
	// 创建食物
	m_fruit = new Fruit(1);
	// 初始化格子宽度和高度
	m_lattice_width = (m_battle_rect.right - m_battle_rect.left) / m_h_lattice_count;
	m_lattice_height = (m_battle_rect.bottom - m_battle_rect.top) / m_v_lattice_count;
	// 初始化失败的 "×" 的缩放率
	m_defeat_tips_scale_rate = .3f;
	// 初始化放大缩放
	m_is_scale_up = true;
}

1. 初始化游戏对战场的矩形区域和横纵格子数

2. 创建贪吃蛇和食物

3. 计算格子宽度和高度

4. 初始化贪吃蛇碰撞后的“×”的缩放率和是否放大缩放


GameBattleArea::~GameBattleArea()

GameBattleArea::~GameBattleArea()
{
	// 释放游戏游戏对战场的资源
	SafeDeleteObject(m_snake);
	SafeDeleteObject(m_fruit);

	// 清空障壁
	m_traps.clear();
}

1. 释放游戏对战场的资源


GameBattleArea::Init(HWND hWnd)

void GameBattleArea::Init(HWND hWnd)
{
	// 清空障壁
	m_traps.clear();

	// 初始化蛇的坐标位置
	int center_x, center_y;
	center_x = m_battle_rect.left + (int)(m_h_lattice_count / 2 * m_lattice_width + m_lattice_width * .5f);
	center_y = m_battle_rect.top + (int)(m_v_lattice_count / 2 * m_lattice_height + m_lattice_height * .5f);

	// 初始化蛇
	m_snake->Init(m_lattice_width, m_lattice_height, center_x, center_y);

	// 生成水果
	RespawnFruit();

	// 生成障壁
	SpawnTraps();
}

1. 清空障壁容器

2. 计算游戏对战场矩形的中心坐标

3. 初始化贪吃蛇

4. 生成食物

5. 生成障壁


GameBattleArea::Render(HDC hdc)

void GameBattleArea::Render(HDC hdc)
{
	//渲染游戏游戏对战场

	HBRUSH hBattleBackgroundBrush = ::CreateSolidBrush(RGB(144, 81, 73));
	// 填充游戏对战场的背景
	::FillRect(hdc, &m_battle_rect, hBattleBackgroundBrush);

	// 渲染蛇
	m_snake->Render(hdc);

	if (m_fruit != NULL) {
		// 渲染食物
		m_fruit->Render(hdc);
	}

	// 遍历渲染障壁
	for (int i = 0, count = m_traps.size(); i < count; ++i)
		m_traps[i].Render(hdc);

	// 渲染失败提示的 "×"
	if (m_is_game_over)
		RenderDefeatTips(hdc);

	SafeDeleteGDIObject(hBattleBackgroundBrush);
}

1. 填充游戏对战场的背景

2. 渲染贪吃蛇

3. 渲染食物

4. 渲染障壁

5. 当游戏结束渲染游戏结束的提示(也就是绘制蛇头碰撞的"×",赢了则不绘制)


GameBattleArea::Update(DWORD dwDeltaTime)

void GameBattleArea::Update(DWORD dwDeltaTime)
{
	// 游戏对战场的更新

	// 判断是否暂停或游戏结束
	if (!m_is_pause && !m_is_game_over)
	{
		// 蛇头碰撞的类型
		SnakeHeadCollisionType collisionType = SnakeHeadCollisionType::SHCT_AIR;
		// 获取蛇头碰撞的类型
		GetSnakeHeadCollision(collisionType, dwDeltaTime);

		switch (collisionType)
		{
		case SHCT_SELF: // 咬到自己
			m_is_game_over = true;
			m_game_over_type = GameOverType::GOT_SELF;
			fnGameOver();
			break;
		case SHCT_TRAP: // 碰到障壁
			m_is_game_over = true;
			m_game_over_type = GameOverType::GOT_TRAP;
			fnGameOver();
			break;
		case SHCT_FRUIT: // 吃到食物
			// 蛇身自增
			m_snake->Increase();
			// 更新贪吃蛇
			m_snake->Update(dwDeltaTime);
			// 判断贪吃蛇是否达到最大长度
			if (!m_snake->IsMaxLength())
			{
				// 增加分数
				fnBattleScoreAdd(m_fruit->GetScore());
				// 生成食物
				RespawnFruit();
			}
			else 
			{
				// 贪吃蛇赢得了比赛
				m_is_game_over = true;
				m_game_over_type = GameOverType::GOT_WIN;
				fnGameOver();
			}
			break;
		default:
			// 更新贪吃蛇
			m_snake->Update(dwDeltaTime);
			break;
		}
	}

	// 判断是否游戏结束,进行缩放失败的 "×"
	if (m_is_game_over)
	{
		// 判断是否放大缩放
		if (m_is_scale_up)
		{
			// 按照一定比率缩放
			m_defeat_tips_scale_rate += dwDeltaTime * .001f;
			// 达到最大缩放,进行缩小
			if (m_defeat_tips_scale_rate >= 1.0f)
			{
				m_defeat_tips_scale_rate = 1.0f;
				m_is_scale_up = false;
			}
		}
		else 
		{
			// 按照一定比率缩放
			m_defeat_tips_scale_rate -= dwDeltaTime * .001f;
			// 达到最小缩放,进行放大
			if (m_defeat_tips_scale_rate <= .3f)
			{
				m_defeat_tips_scale_rate = .3f;
				m_is_scale_up = true;
			}
		}
	}
}

1. 当游戏为结束或未暂停,判断蛇头的碰撞以及蛇身是否达到最大长度

2. 游戏结束调用Game的游戏结束的函数对象

3. 当吃到食物则增长蛇身,重新生成食物,以及调用Game的分数增加的函数对象

4. 调用贪吃蛇的Update(逻辑更新)

5. 当游戏结束则,进行放大或缩小"×"的百分比


GameBattleArea::OnKeyDown(Keys key)

void GameBattleArea::OnKeyDown(Keys key)
{
	// 判断是否暂停 或 游戏结束
	if (m_is_pause || m_is_game_over)
		return;

	// 贪吃蛇按键处理
	m_snake->OnKeyDown(key);
}

1. 当游戏未结束并且未暂停,调用贪吃蛇的按键处理


GameBattleArea::RespawnFruit()

void GameBattleArea::RespawnFruit()
{
	// 获取蛇身节点数,以用来确定保存蛇身的数组
	int array_count = m_snake->GetNodeCount();
	// 根据蛇身数,进行创建数组
	int *pNodesPositionArray = new int[array_count * 2];
	// 将蛇身的所有节点的位置保存到数组
	// 保存的格式为: array[0]为x坐标, array[1]为y坐标
	m_snake->GetNodesPosition(pNodesPositionArray);

	// 将位置坐标进行重新计算为格子坐标
	std::vector<int> nodes;
	int temp1 = 0, temp2 = 0;
	for (int i = 0, count = array_count * 2; i < count; i += 2)
	{
		// 将蛇的节点x坐标位置转换为格子的x坐标
		temp1 = (pNodesPositionArray[i] - m_battle_rect.left) / m_lattice_width - 1;
		// 将蛇的节点y坐标位置转换为格子的y坐标
		temp2 = (pNodesPositionArray[i + 1] - m_battle_rect.top) / m_lattice_height - 1;
		// 将格子坐标(x, y)以数值保存
		// 数值格式: x + y * (横向格子数 - 2(这个2为左右障壁)),
		// 这样做是为了方便使用索引进行删除非空格子
		// 因为empties保存的顺序就是与这个数值相关的
		nodes.push_back(temp1 + temp2 * (m_h_lattice_count-2));
	}

	// 将除了障壁的格子的坐标都保存到vector容器中
	std::vector<POINT> empties;
	for (int row = 1, row_count = m_v_lattice_count - 1; row < row_count; ++row)
	{
		for (int col = 1, col_count = m_h_lattice_count - 1; col < col_count; ++col)
		{
			// 将格子坐标以POINT结构体进行保存
			POINT pt; pt.x = col; pt.y = row;
			// 添加到empties中
			empties.push_back(pt);
		}
	}

	temp1 = temp2 = 0;
	// 将蛇的节点坐标数值以降序的方式进行排序
	std::sort(nodes.begin(), nodes.end());
	// 通过坐标的数字使用索引进行删除非空节点
	for (int i = 0, count = nodes.size(); i < count; ++i)
	{
		// temp1为empties中非空格子的索引
		// items[i]为蛇的节点所在的节点坐标(以数值的格式)
		// temp2为已在empties中删除的格子数
		temp1 = nodes[i] - temp2;
		// 使用erase进行删除指定的非空格子
		empties.erase(empties.begin() + temp1);
		// 累加删除的格子数
		++temp2;
	}

	// 如果存在空格子
	if (empties.size() > 0)
	{
		// 随机种子
		srand((unsigned)time(NULL));
		// 随机数
		int rnd = rand() % empties.size();
		// 食物的横纵坐标
		int x_fruit = empties[rnd].x;
		int y_fruit = empties[rnd].y;
		// 计算食物的位置坐标
		x_fruit = m_battle_rect.left + x_fruit * m_lattice_width + m_lattice_width / 2;
		y_fruit = m_battle_rect.top + y_fruit * m_lattice_height + m_lattice_height / 2;
		// 重新设置新的食物的位置坐标
		int score = rand() % 3;
		m_fruit->Reset(score == 0 ? 1 : score, x_fruit, y_fruit, m_lattice_width, m_lattice_height);
	}

	// 释放占用的资源
	empties.clear();
	nodes.clear();
	SafeDeleteArrayObject(pNodesPositionArray);
}

1. 获取蛇的节点位置坐标(坐标是基于窗口的位置坐标)

2. 将这些节点坐标转换为格子坐标,并以数值方式保存到nodes容器中

3. 将所有格子以POINT方式保存到empties容器中

4. 将nodes中的数值进行降序排列

5. 在empties中删除nodes中的格子(使用了索引的方式删除)

6. 使用随机函数(rand)进行随机0-empties的大小获取一个随机值(empties容器的索引了)

7. 通过这个随机值获取empties中对应的格子empties[随机值](POINT结构体)

8. 重新设置食物的位置坐标、大小以及分数

9. 释放资源


GameBattleArea::GetSnakeHeadCollision(SnakeHeadCollisionType &, DWORD)

void GameBattleArea::GetSnakeHeadCollision(SnakeHeadCollisionType &collisionType, DWORD dwDeltaTime)
{
	// 蛇头碰撞类型
	collisionType = SnakeHeadCollisionType::SHCT_AIR;
	// 横纵坐标偏移量
	const int x_offset = m_battle_rect.left + (int)(m_lattice_width * .5f);
	const int y_offset = m_battle_rect.top + (int)(m_lattice_height * .5f);
	// 蛇头位置坐标
	int x_snake_head, y_snake_head;
	m_snake->GetSnakeHeadNextMovePosition(&x_snake_head, &y_snake_head, dwDeltaTime);
	// 重计算蛇头位置为格子坐标
	int x_head_lattice = (x_snake_head - x_offset) / m_lattice_width;
	int y_head_lattice = (y_snake_head - y_offset) / m_lattice_height;

	// 蛇头位置在障壁的区域内
	if (x_head_lattice == 0 || x_head_lattice == m_h_lattice_count-1 ||
		y_head_lattice == 0 || y_head_lattice == m_v_lattice_count-1)
	{
		collisionType = SnakeHeadCollisionType::SHCT_TRAP;
		return;
	}

	int x_fruit_lattice = (m_fruit->GetX() - x_offset) / m_lattice_width;
	int y_fruit_lattice = (m_fruit->GetY() - y_offset) / m_lattice_height;
	// 蛇头位置在食物的位置中
	if (x_head_lattice == x_fruit_lattice && y_head_lattice == y_fruit_lattice)
	{
		collisionType = SnakeHeadCollisionType::SHCT_FRUIT;
		return;
	}

	// 蛇头的位置在蛇身中
	if (m_snake->IsEatSelf(dwDeltaTime))
	{
		collisionType = SnakeHeadCollisionType::SHCT_SELF;
		return;
	}
}

1. 获取蛇头下一次移动的位置

2. 将蛇头位置转换为格子坐标位置

3. 判断是否碰到障壁、食物、自身,相应的设置碰撞类型


GameBattleArea::Pause()

void GameBattleArea::Pause()
{
	// 暂停对战
	m_is_pause = true;
	// 暂停贪吃蛇的移动
	m_snake->Pause();
}

1. 设置暂停对战

2. 暂停贪吃蛇的移动


GameBattleArea::Continue()

void GameBattleArea::Continue()
{
	// 继续对战
	m_is_pause = false;
	// 继续贪吃蛇的移动
	m_snake->Continue();
}

1. 设置暂停对战

2. 继续贪吃蛇的移动


GameBattleArea::SpawnTraps()

void GameBattleArea::SpawnTraps()
{
	// 初始化默认障壁格式
	EnumTrapStyle trapStyle = EnumTrapStyle::TS_DOWNTIP;
	// 遍历格子
	for (int row = 0; row < m_v_lattice_count; ++row)
	{
		for (int col = 0; col < m_h_lattice_count; ++col)
		{
			// 如果格子为第一列 或者 最后一列
			if (row == 0 || row == m_v_lattice_count - 1)
			{
				// 如果为第一行 或者 最后一行 (即为四个角)
				// 第一列为x   第一行为y   即(0, 0)
				// 最后一列为x 第一行为y   即(h, 0)
				// 第一列为x   最后一行为y 即(0, v)
				// 第一列为x   最后一行为y 即(h, v)
				if (col == 0 || col == m_h_lattice_count - 1)
				{
					// 设置障壁样式为块
					trapStyle = EnumTrapStyle::TS_BLOCK;
				}
				else
				{
					// 中间部分, 根据行数
					// 行数为第一行,则为向下的刺
					// 行数为最后一行,则为向上的刺
					trapStyle = row == 0 ? EnumTrapStyle::TS_UPTIP : EnumTrapStyle::TS_DOWNTIP;
				}
			}
			// 如果为第一列
			else if (col == 0)
			{
				// 则为左边的刺
				trapStyle = EnumTrapStyle::TS_LEFTTIP;
			}
			// 如果为最后一列
			else if (col == m_h_lattice_count - 1)
			{
				// 则为右边的刺
				trapStyle = EnumTrapStyle::TS_RIGHTTIP;
			}
			else
				// 如果不是四边的格子,则跳过
				continue;

			// 创建障壁
			Trap trap(trapStyle, (int)(m_battle_rect.left + col * m_lattice_width + m_lattice_width * .5f),
				(int)(m_battle_rect.top + row * m_lattice_height + m_lattice_height * .5f), m_lattice_width, m_lattice_height);
			// 添加到保存障壁的容器中
			m_traps.push_back(trap);
		}
	}
}

1. 遍历对战场的四周进行创建障壁。(第一行、最后一行、第一列、最后一列)


GameBattleArea::Restart(HWND hWnd)

void GameBattleArea::Restart(HWND hWnd)
{
	// 重新开始对战
	m_is_game_over = false;
	m_is_pause = false;
	// 重新初始化对战
	Init(hWnd);
}

1. 初始化游戏结束和暂停游戏的标志

2. 重新初始化对战场


GameBattleArea::RenderDefeatTips(HDC hdc)

void GameBattleArea::RenderDefeatTips(HDC hdc)
{
	// 判断是否胜利,则返回
	if (m_game_over_type == GameOverType::GOT_WIN)
		return;

	HPEN hPen = ::CreatePen(PS_SOLID, 6, RGB(255, 0, 0));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);

	// 获取蛇头位置坐标
	int x_center = m_snake->GetHeadX();
	int y_center = m_snake->GetHeadY();

	// 根据蛇头位置,进行加上对应的偏移量,则为碰撞到的位置
#if false
	// 凑合着看吧...o((⊙﹏⊙))o
	"
	 * 
	 *       \   / <----- 失败的提示
	 *       *****  
	 *      **/ \**   
	 *     **     **  
	 *    **       ** 
	 *   *************
	 *   *************
	 *   *************
	 *
	 "
#endif 
	switch (m_snake->GetHeadDir())
	{
	case SND_UP:
		y_center -= (int)(m_lattice_height * .5f);
		break;
	case SND_DOWN:
		y_center += (int)(m_lattice_height * .5f);
		break;
	case SND_LEFT:
		x_center -= (int)(m_lattice_width * .5f);
		break;
	case SND_RIGHT:
		x_center += (int)(m_lattice_width * .5f);
		break;
	default:
		break;
	}

	/* 绘制 "×" */
	float len = m_lattice_width * .5f * m_defeat_tips_scale_rate;
	float angles[] = {
		45, 135, 225, 315
	};

	int points[2][4];

	float rad = 3.1415926f / 180.0f;
	/* 第一条 */
	int x_lt = (int)(x_center + len * cos(angles[0] * rad));
	int y_lt = (int)(y_center + len * sin(angles[0] * rad));
	int x_rd = (int)(x_center + len * cos(angles[2] * rad));
	int y_rd = (int)(y_center + len * sin(angles[2] * rad));

	/* 第二条 */
	int x_rt = (int)(x_center + len * cos(angles[1] * rad));
	int y_rt = (int)(y_center + len * sin(angles[1] * rad));
	int x_ld = (int)(x_center + len * cos(angles[3] * rad));
	int y_ld = (int)(y_center + len * sin(angles[3] * rad));

	points[0][0] = x_lt;
	points[0][1] = y_lt;
	points[0][2] = x_rd;
	points[0][3] = y_rd;

	points[1][0] = x_rt;
	points[1][1] = y_rt;
	points[1][2] = x_ld;
	points[1][3] = y_ld;

	/* 绘制 */
	Util::DrawLine(hdc, points[0]);
	Util::DrawLine(hdc, points[1]);

	::SelectObject(hdc, hOldPen);
	SafeDeleteGDIObject(hPen);
}

1. 判断游戏是否结束,并且结束的类型不是胜利。(贪吃蛇的蛇身达到最大长度)

2. 获取蛇头位置,加上相应的偏移,在碰撞到的位置进行绘制"×"


Snake (贪吃蛇类)

头文件:

//++++++++++++++++++++++++++++++++++
// 贪吃蛇类
//----------------------------------

#pragma once

#ifndef __SNAKE_H__
#define __SNAKE_H__

class SnakeNode;     // 声明贪吃蛇的节点类
enum Keys;           // 声明按键的枚举

class Snake
{
public:
	Snake(int nMaxLenght);
	~Snake();

public:
	// 初始化贪吃蛇
	void Init(int latticeWidth, int latticeHeight, int xCenter, int yCenter);	
	// 渲染贪吃蛇
	void Render(HDC hdc);									
	// 更新贪吃蛇
	void Update(DWORD dwDeltaTime);							
	// 按键处理
	void OnKeyDown(Keys key);								

	// 移动贪吃蛇
	void Move(DWORD dwDeltaTime);							
	// 贪吃蛇自增
	void Increase();										
	// 暂停移动贪吃蛇
	void Pause();											
	// 继续移动贪吃蛇
	void Continue();										
	// 获取贪吃蛇的蛇头下次移动到的位置
	void GetSnakeHeadNextMovePosition(int *pX, int *pY, DWORD dwDeltaTime);	
	// 获取贪吃蛇的节点数
	int GetNodeCount();										
	// 获取蛇头的x坐标位置
	int GetHeadX();											
	// 获取蛇头的y坐标位置
	int GetHeadY();											
	// 获取蛇头方向
	SnakeNodeDir GetHeadDir();								
	// 获取贪吃蛇的位置
	bool GetNodesPosition(int *&pPositionArray);			
	// 是否咬到自身
	bool IsEatSelf(DWORD dwDeltaTime);										
	// 贪吃蛇的身体是否达到最大长度
	bool IsMaxLength();										

private:
	// 移动贪吃蛇的蛇头
	void MoveHead(SnakeNode &head);							

private:
	std::vector<SnakeNode> m_components;    // 蛇的节点
	DWORD m_move_delay;                     // 蛇每次移动的时间差(毫秒数)
	LONG m_update_delay;                    // 蛇最后一次更新的时间戳
	float m_change_speed_percent;           // 修改移动速度的比率
	int m_change_speed_count;               // 设定蛇身达到一定程度后,修改移动速度
	int m_h_move_distance;                  // 蛇头每次移动的横向距离
	int m_v_move_distance;                  // 蛇头每次移动的纵向距离
	int m_component_width;                  // 蛇的节点宽度
	int m_component_height;                 // 蛇的节点高度
	int m_max_length;                       // 蛇身的最大长度
	bool m_is_pause;                        // 是否暂停蛇的移动
	bool m_can_change_dir;                  // 此次按键是否可以改变蛇头的方向
};

#endif // !__SNAKE_H__

源文件:


#include <windows.h>        // WIN32开发中最重要的头文件(不用详解了吧...)
#include <vector>           // vector容器所在头文件

#include "GameType.h"       // 游戏中使用到的一些枚举、别名定义
#include "SnakeNode.h"      // 蛇身节点类
#include "Snake.h"          // 贪吃蛇类

Snake::Snake(int nMaxLenght) 
	: m_max_length(nMaxLenght),     // 设定蛇身的最大长度
	m_change_speed_percent(.01f),   // 设定速度改变的百分比
	m_change_speed_count(6),        // 设置速度改变根据的身长
	m_update_delay(m_move_delay)    // 初始化距离移动的时间差(毫秒)
{
}

Snake::~Snake()
{

}

void Snake::Init(int latticeWidth, int latticeHeight, int xCenter, int yCenter)
{
	// 清空蛇的节点
	m_components.clear();

	// 初始化节点宽度和高度
	m_component_width = latticeWidth;
	m_component_height = latticeHeight;
	// 设置横向和纵向每次移动的距离
	m_h_move_distance = m_component_width;
	m_v_move_distance = m_component_height;
	// 设置移动延迟
	m_move_delay = 130L;
	// 初始化距离移动的时间差
	m_update_delay = m_move_delay;

	/* *
	 * 初始化蛇的组件, 生成三个组件
	 * 分别是: 蛇头、蛇身、蛇尾
	 * */

	int pos_arr[][2] = {
		{ xCenter, yCenter },
		{ xCenter, yCenter + m_component_height },
		{ xCenter, yCenter + 2 * m_component_height }
	};

	// 获取蛇头的坐标(x, y) 并创建蛇头 
	int x_head, y_head;
	x_head = pos_arr[0][0];
	y_head = pos_arr[0][1];
	SnakeNode head(x_head, y_head, m_component_width, m_component_height, SnakeNodeType::SNT_HEAD);
	head.SetDir(SnakeNodeDir::SND_UP);
	m_components.push_back(head);

	const int c_body = ARRAYSIZE(pos_arr)-2;
	int i_body = 0;
	// 创建蛇身 
	int x_body, y_body;
	while (true)
	{
		if (i_body >= c_body)
			break;

		x_body = pos_arr[i_body + 1][0];
		y_body = pos_arr[i_body + 1][1];
		SnakeNode body(x_body, y_body, m_component_width, m_component_height, SnakeNodeType::SNT_BODY);
		body.SetDir(SnakeNodeDir::SND_UP);
		m_components.push_back(body);
		++i_body;
	}

	// 创建蛇尾 
	int x_tail, y_tail;
	x_tail = pos_arr[i_body + 1][0];
	y_tail = pos_arr[i_body + 1][1];
	SnakeNode tail(x_tail, y_tail, m_component_width, m_component_height, SnakeNodeType::SNT_TAIL);
	tail.SetDir(SnakeNodeDir::SND_UP);
	m_components.push_back(tail);
}

void Snake::Render(HDC hdc)
{
	// 绘制蛇的组件,通过迭代器
	std::vector<SnakeNode>::iterator iter_component;
	iter_component = m_components.begin();
	// 遍历节点
	while (true)
	{
		// 当遍历到最后一个节点
		if (iter_component == m_components.end())
			break;
		// 渲染节点
		iter_component->Render(hdc);
		++iter_component;
	}
}

void Snake::Update(DWORD dwDeltaTime)
{
	// 判断是否暂停
	if (!m_is_pause)
	{
		// 移动蛇
		Move(dwDeltaTime);
	}
}

void Snake::OnKeyDown(Keys key)
{
	// 每一次Update只能改变一次蛇头方向
	if (m_can_change_dir)
	{
		// 根据按键改变蛇头方向
		SnakeNode &head = *m_components.begin();
		if ((key == Keys::KEY_W || key == Keys::KEY_UP) && head.GetDir() != SnakeNodeDir::SND_DOWN)
			head.SetDir(SnakeNodeDir::SND_UP);
		else if ((key == Keys::KEY_S || key == Keys::KEY_DOWN) && head.GetDir() != SnakeNodeDir::SND_UP)
			head.SetDir(SnakeNodeDir::SND_DOWN);
		else if ((key == Keys::KEY_A || key == Keys::KEY_LEFT) && head.GetDir() != SnakeNodeDir::SND_RIGHT)
			head.SetDir(SnakeNodeDir::SND_LEFT);
		else if ((key == Keys::KEY_D || key == Keys::KEY_RIGHT) && head.GetDir() != SnakeNodeDir::SND_LEFT)
			head.SetDir(SnakeNodeDir::SND_RIGHT);
		else
			return;

		// 设置此次Update更新方向完毕
		m_can_change_dir = false;
	}
}

int Snake::GetNodeCount()
{
	// 获取节点数
	return m_components.size();
}

bool Snake::GetNodesPosition(int *&pPositionArray)
{
	if (NULL == pPositionArray)
		return false;

	// 遍历节点,将节点的x y放入数组中
	size_t i;
	for (i = 0; i < m_components.size() * 2; i += 2)
	{
		pPositionArray[i] = m_components[i/2].GetX();
		pPositionArray[i + 1] = m_components[i/2].GetY();
	}
	return (i == m_components.size() * 2);
}

int Snake::GetHeadX()
{
	// 获取蛇头x坐标
	return m_components.begin()->GetX();
}

int Snake::GetHeadY()
{
	// 获取蛇头y坐标
	return m_components.begin()->GetY();
}

SnakeNodeDir Snake::GetHeadDir()
{
	// 获取蛇头方向
	return m_components.begin()->GetDir();
}

void Snake::Move(DWORD dwDeltaTime)
{
	m_update_delay -= dwDeltaTime;

	// 与最后一次移动蛇身时间戳做比较,如果达到移动间隔时间,则移动
	if (m_update_delay > 0)
		return;

	// 设定可改变蛇头方向
	m_can_change_dir = true;

	// 移动蛇,通过反向迭代器
	std::vector<SnakeNode>::reverse_iterator rev_iter_component;
	rev_iter_component = m_components.rbegin();
	int x_body, y_body;
	while (true)
	{
		// 如果到达最后一个,则退出
		if (rev_iter_component == m_components.rend())
			break;
		// 如果为倒数第二个,则为蛇头
		if ((rev_iter_component + 1) == m_components.rend())
		{
			// 移动蛇头
			MoveHead(*rev_iter_component);
			break;
		}

		// 移动蛇的身体和尾巴,通过移动到前一个节点的位置即可完成移动
		x_body = (rev_iter_component + 1)->GetX();
		y_body = (rev_iter_component + 1)->GetY();
		rev_iter_component->MoveTo(x_body, y_body);
		rev_iter_component->SetDir((rev_iter_component + 1)->GetDir());
		++rev_iter_component;
	}

	// 保存当前更新的时间戳
	m_update_delay = m_move_delay;
}

void Snake::MoveHead(SnakeNode &head)
{
	// 移动蛇头
	int x_head, y_head;
	GetSnakeHeadNextMovePosition(&x_head, &y_head, 0);
	head.MoveTo(x_head, y_head);
}

bool Snake::IsMaxLength()
{
	// 贪吃蛇的身体是否达到最大长度
	return m_components.size() >= m_max_length;
}

void Snake::Pause()
{
	// 暂停贪吃蛇的移动
	m_is_pause = true;
}

void Snake::Continue()
{
	// 继续贪吃蛇的移动
	m_is_pause = false;
}

bool Snake::IsEatSelf(DWORD dwDeltaTime)
{
	// 判断是否咬到自身
	int x_head, y_head;
	// 获取当前蛇头移动到的位置(模拟移动)
	GetSnakeHeadNextMovePosition(&x_head, &y_head, dwDeltaTime);
	// 如果未达到移动时间, 蛇头并未发生移动, 不需要做计算, 直接返回
	if (x_head == GetHeadX() && y_head == GetHeadY())
		return false;

	// 遍历蛇的节点(从蛇头开始,因为移动后蛇头就改变了,第二个节点会移动到当前蛇头的位置,而现在蛇头位置并未改变)
	for (int i = 0; i < m_components.size()-1; ++i)
	{
		// 通过判断坐标判断蛇头与蛇身的节点是否重叠,如重叠,则咬到了
		if (m_components[i].GetX() == x_head && m_components[i].GetY() == y_head)
			return true;
	}
	return false;
}

void Snake::GetSnakeHeadNextMovePosition(int *pX, int *pY, DWORD dwDeltaTime)
{
	// 获取蛇头
	SnakeNode head = *m_components.begin();
	// 通过计算当前时间戳与最后一次移动的时间戳比较, 是否达到移动时间间隔
	if (m_update_delay - (LONG)dwDeltaTime > 0)
	{
		// 未到达,返回当前蛇头位置
		*pX = head.GetX();
		*pY = head.GetY();
		return;
	}
	// 到达,则返回移动后的位置(但蛇头并未移动)

	// 前进的距离(x, y)
	int x_forward = 0;
	int y_forward = 0;

	// x
	if (head.GetDir() == SnakeNodeDir::SND_LEFT)
		x_forward = -m_h_move_distance;
	else if (head.GetDir() == SnakeNodeDir::SND_RIGHT)
		x_forward = m_h_move_distance;

	// y
	if (head.GetDir() == SnakeNodeDir::SND_UP)
		y_forward = -m_v_move_distance;
	else if (head.GetDir() == SnakeNodeDir::SND_DOWN)
		y_forward = m_v_move_distance;

	*pX = head.GetX() + x_forward;
	*pY = head.GetY() + y_forward;
}

void Snake::Increase()
{
	// 检测是否达到蛇身的最大长度
	if (IsMaxLength())
		return;

	// 获取插入的蛇身节点(蛇尾节点)
	std::vector<SnakeNode>::iterator iter_insert = m_components.begin() + (m_components.size() - 1);
	// 创建蛇身节点
	SnakeNode body(iter_insert->GetX(), iter_insert->GetY(), m_component_width, m_component_height, SnakeNodeType::SNT_BODY);
	body.SetDir(iter_insert->GetDir());
	// 将新的蛇身节点插入到尾巴节点的前面
	m_components.insert(iter_insert, body);

	// 检测蛇身的长度是否达到改变速度的数量
	if (GetNodeCount() % m_change_speed_count == 0)
	{
		m_move_delay -= m_move_delay * m_change_speed_percent;
	}
}

下面进行讲解上面的程序:


Snake::Snake(int nMaxLenght) 

Snake::Snake(int nMaxLenght) 
	: m_max_length(nMaxLenght),		// 设定蛇身的最大长度
	m_change_speed_percent(.01f),	// 设定速度改变的百分比
	m_change_speed_count(6)		// 设置速度改变根据的身长
{
}

1. 设置蛇的最大长度

2. 初始化速度的改变百分比

3. 设置每当达到多少个节点进行一次速度的改变

4. 初始化蛇的移动延迟


Snake::Init(int latticeWidth, int latticeHeight, int xCenter, int yCenter)

void Snake::Init(int latticeWidth, int latticeHeight, int xCenter, int yCenter)
{
	// 清空蛇的节点
	m_components.clear();

	// 初始化节点宽度和高度
	m_component_width = latticeWidth;
	m_component_height = latticeHeight;
	// 设置横向和纵向每次移动的距离
	m_h_move_distance = m_component_width;
	m_v_move_distance = m_component_height;
	// 设置移动延迟
	m_move_delay = 130L;
	// 初始化距离移动的时间差
	m_update_delay = m_move_delay;
        // 设定可改变蛇头方向
	m_can_change_dir = true;

	/* *
	 * 初始化蛇的组件, 生成三个组件
	 * 分别是: 蛇头、蛇身、蛇尾
	 * */

	int pos_arr[][2] = {
		{ xCenter, yCenter },
		{ xCenter, yCenter + m_component_height },
		{ xCenter, yCenter + 2 * m_component_height }
	};

	// 获取蛇头的坐标(x, y) 并创建蛇头 
	int x_head, y_head;
	x_head = pos_arr[0][0];
	y_head = pos_arr[0][1];
	SnakeNode head(x_head, y_head, m_component_width, m_component_height, SnakeNodeType::SNT_HEAD);
	head.SetDir(SnakeNodeDir::SND_UP);
	m_components.push_back(head);

	// 创建蛇身 
	// - 2 是因为不把尾巴当蛇身进行创建
	const int c_body = ARRAYSIZE(pos_arr) - 2;
	int i_body = 0;
	int x_body, y_body;
	while (true)
	{
		if (i_body >= c_body)
			break;

		x_body = pos_arr[i_body + 1][0];
		y_body = pos_arr[i_body + 1][1];
		SnakeNode body(x_body, y_body, m_component_width, m_component_height, SnakeNodeType::SNT_BODY);
		body.SetDir(SnakeNodeDir::SND_UP);
		m_components.push_back(body);
		++i_body;
	}

	// 创建蛇尾 
	int x_tail, y_tail;
	x_tail = pos_arr[i_body + 1][0];
	y_tail = pos_arr[i_body + 1][1];
	SnakeNode tail(x_tail, y_tail, m_component_width, m_component_height, SnakeNodeType::SNT_TAIL);
	tail.SetDir(SnakeNodeDir::SND_UP);
	m_components.push_back(tail);
}

1. 清空蛇的节点容器

2. 初始化格子宽度和高度以及每一次移动的横纵距离

3. 初始化移动的延迟和延迟的计时

4. 初始化可以通过按键改变蛇头方向。(距离上次移动只能进行一次改变)

4. 创建蛇的节点,通过固定的位置


Snake::Render(HDC hdc)

void Snake::Render(HDC hdc)
{
	// 绘制蛇的组件,通过迭代器
	std::vector<SnakeNode>::iterator iter_component;
	iter_component = m_components.begin();
	// 遍历节点
	while (true)
	{
		// 当遍历到最后一个节点
		if (iter_component == m_components.end())
			break;
		// 渲染节点
		iter_component->Render(hdc);
		++iter_component;
	}
}

1. 通过迭代蛇的节点容器进行渲染贪吃蛇


Snake::Update(DWORD dwDeltaTime)

void Snake::Update(DWORD dwDeltaTime)
{
	// 判断是否暂停
	if (!m_is_pause)
	{
		// 移动蛇
		Move(dwDeltaTime);
	}
}

1. 如果未停止贪吃蛇的移动则进行移动贪吃蛇


Snake::Move(DWORD dwDeltaTime)

void Snake::Move(DWORD dwDeltaTime)
{
	m_update_delay -= dwDeltaTime;

	// 与最后一次移动蛇身时间戳做比较,如果达到移动间隔时间,则移动
	if (m_update_delay > 0)
		return;

	// 设定可改变蛇头方向
	m_can_change_dir = true;

	// 移动蛇,通过反向迭代器
	std::vector<SnakeNode>::reverse_iterator rev_iter_component;
	rev_iter_component = m_components.rbegin();
	int x_body, y_body;
	while (true)
	{
		// 如果到达最后一个,则退出
		if (rev_iter_component == m_components.rend())
			break;
		// 如果为倒数第二个,则为蛇头
		if ((rev_iter_component + 1) == m_components.rend())
		{
			// 移动蛇头
			MoveHead(*rev_iter_component);
			break;
		}

		// 移动蛇的身体和尾巴,通过移动到前一个节点的位置即可完成移动
		x_body = (rev_iter_component + 1)->GetX();
		y_body = (rev_iter_component + 1)->GetY();
		rev_iter_component->MoveTo(x_body, y_body);
		rev_iter_component->SetDir((rev_iter_component + 1)->GetDir());
		++rev_iter_component;
	}

	// 保存当前更新的时间戳
	m_update_delay = m_move_delay;
}

1. 更新移动延迟的计数,当小于零进行移动贪吃蛇

2.  设置可以通过按键改变蛇头方向。(距离上次移动只能进行一次改变)

3. 反向迭代节点容器,每个节点往前一个节点的位置移动,蛇头则根据方向进行相应的移动

4. 重设下一次移动的延迟计时


Snake::OnKeyDown(Keys key)

void Snake::OnKeyDown(Keys key)
{
	// 距离上一次移动只能改变一次蛇头方向
	if (m_can_change_dir)
	{
		// 根据按键改变蛇头方向
		SnakeNode &head = *m_components.begin();
		if ((key == Keys::KEY_W || key == Keys::KEY_UP) && head.GetDir() != SnakeNodeDir::SND_DOWN)
			head.SetDir(SnakeNodeDir::SND_UP);
		else if ((key == Keys::KEY_S || key == Keys::KEY_DOWN) && head.GetDir() != SnakeNodeDir::SND_UP)
			head.SetDir(SnakeNodeDir::SND_DOWN);
		else if ((key == Keys::KEY_A || key == Keys::KEY_LEFT) && head.GetDir() != SnakeNodeDir::SND_RIGHT)
			head.SetDir(SnakeNodeDir::SND_LEFT);
		else if ((key == Keys::KEY_D || key == Keys::KEY_RIGHT) && head.GetDir() != SnakeNodeDir::SND_LEFT)
			head.SetDir(SnakeNodeDir::SND_RIGHT);
		else
			return;

		// 设置更新方向完毕
		m_can_change_dir = false;
	}
}

1. 根据不同的按键进行设置蛇头的方向

2. 设置更新方向完成。(距离上次移动只能进行一次改变)


Snake::GetNodesPosition(int *&pPositionArray)

bool Snake::GetNodesPosition(int *&pPositionArray)
{
	if (NULL == pPositionArray)
		return false;

	// 遍历节点,将节点的x y放入数组中
	size_t i;
	for (i = 0; i < m_components.size() * 2; i += 2)
	{
		pPositionArray[i] = m_components[i/2].GetX();
		pPositionArray[i + 1] = m_components[i/2].GetY();
	}
	return (i == m_components.size() * 2);
}

1. 将节点的位置放入传入的数组中,按照一定的索引进行放入


Snake::MoveHead(SnakeNode &head)

void Snake::MoveHead(SnakeNode &head)
{
	// 移动蛇头
	int x_head, y_head;
	GetSnakeHeadNextMovePosition(&x_head, &y_head, 0);
	head.MoveTo(x_head, y_head);
}

1. 获取蛇头下一次移动的位置

2. 移动蛇头


Snake::IsEatSelf(DWORD dwDeltaTime)

bool Snake::IsEatSelf(DWORD dwDeltaTime)
{
	// 判断是否咬到自身
	int x_head, y_head;
	// 获取当前蛇头移动到的位置(模拟移动)
	GetSnakeHeadNextMovePosition(&x_head, &y_head, dwDeltaTime);
	// 如果未达到移动时间, 蛇头并未发生移动, 不需要做计算, 直接返回
	if (x_head == GetHeadX() && y_head == GetHeadY())
		return false;

	// 遍历蛇的节点(从蛇头开始,因为移动后蛇头就改变了,第二个节点会移动到当前蛇头的位置,而现在蛇头位置并未改变)
	for (size_t i = 0; i < m_components.size() - 1; ++i)
	{
		// 通过判断坐标判断蛇头与蛇身的节点是否重叠,如重叠,则咬到了
		if (m_components[i].GetX() == x_head && m_components[i].GetY() == y_head)
			return true;
	}
	return false;
}

1. 获取蛇头下一次移动的位置

2. 判断蛇头的位置是否发生改变,如果发生改变,则进行判断蛇是否咬到自己

3. 遍历蛇的节点容器如果移动后的蛇头位置与节点的位置相同,则咬到自己了

PS: GetSnakeHeadNextMovePosition只有达到移动的延迟后才会返回移动后的蛇头位置。


Snake::GetSnakeHeadNextMovePosition(int *pX, int *pY, DWORD dwDeltaTime)

void Snake::GetSnakeHeadNextMovePosition(int *pX, int *pY, DWORD dwDeltaTime)
{
	// 获取蛇头
	SnakeNode head = *m_components.begin();
	// 通过计算当前时间戳与最后一次移动的时间戳比较, 是否达到移动时间间隔
	if (m_update_delay - (LONG)dwDeltaTime > 0)
	{
		// 未到达,返回当前蛇头位置
		*pX = head.GetX();
		*pY = head.GetY();
		return;
	}
	// 到达,则返回移动后的位置(但蛇头并未移动)

	// 前进的距离(x, y)
	int x_forward = 0;
	int y_forward = 0;

	// x
	if (head.GetDir() == SnakeNodeDir::SND_LEFT)
		x_forward = -m_h_move_distance;
	else if (head.GetDir() == SnakeNodeDir::SND_RIGHT)
		x_forward = m_h_move_distance;

	// y
	if (head.GetDir() == SnakeNodeDir::SND_UP)
		y_forward = -m_v_move_distance;
	else if (head.GetDir() == SnakeNodeDir::SND_DOWN)
		y_forward = m_v_move_distance;

	*pX = head.GetX() + x_forward;
	*pY = head.GetY() + y_forward;
}

1. 获取蛇头节点

2. 判断移动延迟是否为0。(也就是到达移动的时间)

3. 未到达移动的时间,则返回当前蛇头的位置

4. 到达移动的时间,则返回移动后的蛇头的位置。(但蛇头节点并未移动)


Snake::Increase()

void Snake::Increase()
{
	// 检测是否达到蛇身的最大长度
	if (IsMaxLength())
		return;

	// 获取插入的蛇身节点(蛇尾节点)
	std::vector<SnakeNode>::iterator iter_insert = m_components.begin() + (m_components.size() - 1);
	// 创建蛇身节点
	SnakeNode body(iter_insert->GetX(), iter_insert->GetY(), m_component_width, m_component_height, SnakeNodeType::SNT_BODY);
	body.SetDir(iter_insert->GetDir());
	// 将新的蛇身节点插入到尾巴节点的前面
	m_components.insert(iter_insert, body);

	// 检测蛇身的长度是否达到改变速度的数量
	if (GetNodeCount() % m_change_speed_count == 0)
	{
		m_move_delay -= (DWORD)(m_move_delay * m_change_speed_percent);
	}
}

1. 检测是否达到最大长度,达到了则不再增长蛇身

2. 获取蛇尾节点

3. 创建新的蛇身节点,并将新的蛇身节点位置设定为蛇尾节点的位置以及节点的方向

4. 将新的蛇身节点插入到节点容器中

5. 检测蛇的节点数是否达到更改移动速度的数量,达到则进行更改移动速度


SnakeNode (蛇的节点类)

头文件:

//++++++++++++++++++++++++++++++++++
// 蛇身的节点类
//----------------------------------

#pragma once

#ifndef __SNAKE_NODE_H__
#define __SNAKE_NODE_H__

#define RENDER_BORDER false	// Debug 是否渲染贪吃蛇节点的边框,true即为渲染,false即为不渲染

class SnakeNode
{
public:
	SnakeNode(int x, int y, int width, int height, SnakeNodeType type);
	~SnakeNode();

public:
	// 渲染贪吃蛇的节点
	void Render(HDC hdc);			
	// 移动贪吃蛇的节点
	void MoveTo(int x, int y);		
	// 设置贪吃蛇节点的方向
	void SetDir(SnakeNodeDir dir);	
	// 获取贪吃蛇节点的方向
	SnakeNodeDir GetDir();			
	// 获取贪吃蛇节点的x坐标位置
	int GetX();						
	// 获取贪吃蛇节点的y坐标位置
	int GetY();						

private:
	// 渲染贪吃蛇的蛇头节点
	void RenderHead(HDC hdc);		
	// 渲染贪吃蛇的蛇尾节点
	void RenderTail(HDC hdc);		
	// 渲染贪吃蛇的蛇身节点
	void RenderBody(HDC hdc);		

private:
	SnakeNodeType m_type;    // 贪吃蛇节点的类型
	SnakeNodeDir m_dir;      // 贪吃蛇节点的方向
	int m_x;                 // 贪吃蛇节点的x坐标位置
	int m_y;                 // 贪吃蛇节点的y坐标位置
	int m_w;                 // 贪吃蛇节点的宽度
	int m_h;                 // 贪吃蛇节点的高度
};

#endif // !__SNAKE_NODE_H__

源文件:


#include <windows.h>         // WIN32开发中最重要的头文件(不用详解了吧...)
#include <cmath>             // 数学库头文件,包含了许多常用的数学函数

#include "GameType.h"        // 游戏中使用到的一些枚举、别名定义
#include "util.h"            // 常用工具类
#include "SnakeNode.h"       // 蛇的节点类

SnakeNode::SnakeNode(int x, int y, int width, int height, SnakeNodeType type) 
	: m_x(x),       // 初始化蛇的节点x坐标位置
	m_y(y),         // 初始化蛇的节点y坐标位置
	m_w(width),     // 初始化蛇的节点宽度
	m_h(height),    // 初始化蛇的节点高度
	m_type(type)    // 初始化蛇的节点类型
{
}


SnakeNode::~SnakeNode()
{
}

void SnakeNode::Render(HDC hdc)
{
	/* *
	 * 根据不同部位进行不同的渲染
	 * */
	
	if (m_type == SnakeNodeType::SNT_HEAD)
	{
		// 渲染蛇头节点
		RenderHead(hdc);
	}
	else if (m_type == SnakeNodeType::SNT_TAIL)
	{
		// 渲染蛇尾节点
		RenderTail(hdc);
	}
	else
	{
		// 渲染蛇身节点
		RenderBody(hdc);
	}

#pragma region 渲染边框
#if RENDER_BORDER
	HPEN hPen = ::CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
	HBRUSH hBrush = (HBRUSH)::GetStockObject(NULL_BRUSH);
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	::Rectangle(hdc, m_x - m_w / 2, m_y - m_h / 2, m_x + m_w / 2, m_y + m_h / 2);

	::SelectObject(hdc, hOldPen);
	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hPen);
#endif 
#pragma endregion
}

void SnakeNode::MoveTo(int x, int y)
{
	// 移动到左边位置
	m_x = x;
	m_y = y;
}

void SnakeNode::SetDir(SnakeNodeDir dir)
{
	// 设置节点方向
	m_dir = dir;
}

SnakeNodeDir SnakeNode::GetDir()
{
	// 返回节点方向
	return m_dir;
}

int SnakeNode::GetX()
{
	// 返回x坐标
	return m_x;
}

int SnakeNode::GetY()
{
	// 返回y坐标
	return m_y;
}

void SnakeNode::RenderHead(HDC hdc)
{
	// 绘制头部
	// 计算r,用以绘制半圆
	static const int r = (int)(m_w * .5f);
	// 节点的中心点坐标位置
	const int x_up_center = m_x;
	const int y_up_center = m_y;

	// 下半部分的矩形
	RECT rc_down;
	// 上半部分的半圆起点角度
	float start_angle = .0f;
	// 根据四个方向进行计算(都为固定的啦,用笔和纸画一下就能知道了)
	if (m_dir == SnakeNodeDir::SND_UP)
	{
		rc_down.left = m_x - (int)round(m_w * .5f);
		rc_down.top = m_y;
		rc_down.right = m_x + (int)round(m_w * .5f);
		rc_down.bottom = m_y + (int)round(m_h * .5f);
	}
	else if (m_dir == SnakeNodeDir::SND_DOWN)
	{
		rc_down.left = m_x - (int)round(m_w * .5f);
		rc_down.top = m_y - (int)round(m_h * .5f);
		rc_down.right = m_x + (int)round(m_w * .5f);
		rc_down.bottom = m_y;
		start_angle = 180.0f;
	}
	else if (m_dir == SnakeNodeDir::SND_LEFT)
	{
		rc_down.left = m_x;
		rc_down.top = m_y - (int)round(m_h * .5f);
		rc_down.right = m_x + (int)round(m_w * .5f);
		rc_down.bottom = m_y + (int)round(m_h * .5f);
		start_angle = 90.0f;
	}
	else if (m_dir == SnakeNodeDir::SND_RIGHT)
	{
		rc_down.left = m_x - (int)round(m_w * .5f);
		rc_down.top = m_y - (int)round(m_h * .5f);
		rc_down.right = m_x;
		rc_down.bottom = m_y + (int)round(m_h * .5f);
		start_angle = 270.0f;
	}

	// 进行绘制
	HBRUSH hBrush = ::CreateSolidBrush(RGB(255, 105, 180));
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制上半部分的路径
	::BeginPath(hdc);
	::MoveToEx(hdc, x_up_center, y_up_center, NULL);
	::AngleArc(hdc, x_up_center, y_up_center, r, start_angle, 180.0f);
	::EndPath(hdc);

	// 填充上半部分
	::FillPath(hdc);
	// 绘制下半部分
	::FillRect(hdc, &rc_down, hBrush);

	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hBrush);
}

void SnakeNode::RenderTail(HDC hdc)
{
	// 尾巴绘制三角形
	const int x_center = m_x;
	const int y_center = m_y;
	static const int r = (int)round(Util::GetDistanceBetweenTwoPoint(x_center - m_w * .5f, y_center - m_h * .5f, x_center, y_center));
	static const int r2 = (int)round(m_w * .5f);

	/* *
	 * 三角形的三个点
	 * 四个方向,分别为四个角度
	 * */
	static const int angles[4][3] = {
		{45,  270, 135},/* up    */
		{315, 90,  225},/* down  */
		{45,  180, 315},/* left  */
		{135, 0,   225} /* rigth */
	};

	// 根据当前角度选择上面定义的角度数组
	int angle_index;
	if (m_dir == SnakeNodeDir::SND_UP)
		angle_index = 0;
	else if (m_dir == SnakeNodeDir::SND_DOWN)
		angle_index = 1;
	else if (m_dir == SnakeNodeDir::SND_LEFT)
		angle_index = 2;
	else if (m_dir == SnakeNodeDir::SND_RIGHT)
		angle_index = 3;

	// 根据当前尾巴方向进行绘制 尾巴"△"
	POINT points[3];
	points[0].x = x_center - (int)round((cos(angles[angle_index][0] * Util::RAD)) * r);
	points[0].y = y_center - (int)round((sin(angles[angle_index][0] * Util::RAD)) * r);

	points[1].x = x_center - (int)round((cos(angles[angle_index][1] * Util::RAD)) * r2);
	points[1].y = y_center - (int)round((sin(angles[angle_index][1] * Util::RAD)) * r2);

	points[2].x = x_center - (int)round((cos(angles[angle_index][2] * Util::RAD)) * r);
	points[2].y = y_center - (int)round((sin(angles[angle_index][2] * Util::RAD)) * r);

	// 调用Util的多边形填充
	Util::PolyFill(hdc, RGB(255, 192, 203), points, 3);
}

void SnakeNode::RenderBody(HDC hdc)
{
	// 蛇身矩形
	RECT rc_body;
	rc_body.left	= m_x - (int)round(m_w * .5f);
	rc_body.top		= m_y - (int)round(m_h * .5f);
	rc_body.right	= m_x + (int)round(m_w * .5f);
	rc_body.bottom	= m_y + (int)round(m_h * .5f);

	HBRUSH hBrush = ::CreateSolidBrush(RGB(255, 155, 190));
	// 绘制蛇身
	::FillRect(hdc, &rc_body, hBrush);
	SafeDeleteGDIObject(hBrush);
}

下面进行讲解上面的程序:


SnakeNode::SnakeNode(int x, int y, int width, int height, SnakeNodeType type) 

SnakeNode::SnakeNode(int x, int y, int width, int height, SnakeNodeType type) 
	: m_x(x),       // 初始化蛇的节点x坐标位置
	m_y(y),         // 初始化蛇的节点y坐标位置
	m_w(width),     // 初始化蛇的节点宽度
	m_h(height),    // 初始化蛇的节点高度
	m_type(type)    // 初始化蛇的节点类型
{
}

1. 初始化节点的位置、宽度和高度、类型


SnakeNode::Render(HDC hdc)

void SnakeNode::Render(HDC hdc)
{
	/* *
	 * 根据不同部位进行不同的渲染
	 * */
	
	if (m_type == SnakeNodeType::SNT_HEAD)
	{
		// 渲染蛇头节点
		RenderHead(hdc);
	}
	else if (m_type == SnakeNodeType::SNT_TAIL)
	{
		// 渲染蛇尾节点
		RenderTail(hdc);
	}
	else
	{
		// 渲染蛇身节点
		RenderBody(hdc);
	}

#pragma region 渲染边框
#if RENDER_BORDER
	HPEN hPen = ::CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
	HPEN hOldPen = (HPEN)::SelectObject(hdc, hPen);
	HBRUSH hBrush = (HBRUSH)::GetStockObject(NULL_BRUSH);
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	::Rectangle(hdc, m_x - m_w / 2, m_y - m_h / 2, m_x + m_w / 2, m_y + m_h / 2);

	::SelectObject(hdc, hOldPen);
	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hPen);
#endif 
#pragma endregion
}

1. 根据类型进行不同的渲染

2. 渲染节点的格子边框。(根据宏RENDER_BORDER来确定是否渲染)


SnakeNode::RenderHead(HDC hdc)

void SnakeNode::RenderHead(HDC hdc)
{
	// 绘制头部
	// 计算r,用以绘制半圆
	static const int r = (int)(m_w * .5f);
	// 节点的中心点坐标位置
	const int x_up_center = m_x;
	const int y_up_center = m_y;

	// 下半部分的矩形
	RECT rc_down;
	// 上半部分的半圆起点角度
	float start_angle = .0f;
	// 根据四个方向进行计算(都为固定的啦,用笔和纸画一下就能知道了)
	if (m_dir == SnakeNodeDir::SND_UP)
	{
		rc_down.left = m_x - (int)round(m_w * .5f);
		rc_down.top = m_y;
		rc_down.right = m_x + (int)round(m_w * .5f);
		rc_down.bottom = m_y + (int)round(m_h * .5f);
	}
	else if (m_dir == SnakeNodeDir::SND_DOWN)
	{
		rc_down.left = m_x - (int)round(m_w * .5f);
		rc_down.top = m_y - (int)round(m_h * .5f);
		rc_down.right = m_x + (int)round(m_w * .5f);
		rc_down.bottom = m_y;
		start_angle = 180.0f;
	}
	else if (m_dir == SnakeNodeDir::SND_LEFT)
	{
		rc_down.left = m_x;
		rc_down.top = m_y - (int)round(m_h * .5f);
		rc_down.right = m_x + (int)round(m_w * .5f);
		rc_down.bottom = m_y + (int)round(m_h * .5f);
		start_angle = 90.0f;
	}
	else if (m_dir == SnakeNodeDir::SND_RIGHT)
	{
		rc_down.left = m_x - (int)round(m_w * .5f);
		rc_down.top = m_y - (int)round(m_h * .5f);
		rc_down.right = m_x;
		rc_down.bottom = m_y + (int)round(m_h * .5f);
		start_angle = 270.0f;
	}

	// 进行绘制
	HBRUSH hBrush = ::CreateSolidBrush(RGB(255, 105, 180));
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制上半部分的路径
	::BeginPath(hdc);
	::MoveToEx(hdc, x_up_center, y_up_center, NULL);
	::AngleArc(hdc, x_up_center, y_up_center, r, start_angle, 180.0f);
	::EndPath(hdc);

	// 填充上半部分
	::FillPath(hdc);
	// 绘制下半部分
	::FillRect(hdc, &rc_down, hBrush);

	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hBrush);
}

1. 根据方向设置蛇头上半部分的半圆以及下半部分的矩形

2. 填充半圆以及矩形,则完成蛇头的绘制


SnakeNode::RenderBody(HDC hdc)

void SnakeNode::RenderBody(HDC hdc)
{
	// 蛇身矩形
	RECT rc_body;
	rc_body.left	= m_x - (int)round(m_w * .5f);
	rc_body.top		= m_y - (int)round(m_h * .5f);
	rc_body.right	= m_x + (int)round(m_w * .5f);
	rc_body.bottom	= m_y + (int)round(m_h * .5f);

	HBRUSH hBrush = ::CreateSolidBrush(RGB(255, 155, 190));
	// 绘制蛇身
	::FillRect(hdc, &rc_body, hBrush);
	SafeDeleteGDIObject(hBrush);
}

1. 设置蛇身的矩形,也就是一个格子的大小的矩形


SnakeNode::RenderTail(HDC hdc)

void SnakeNode::RenderTail(HDC hdc)
{
	// 尾巴绘制三角形
	const int x_center = m_x;
	const int y_center = m_y;
	static const int r = (int)round(Util::GetDistanceBetweenTwoPoint(x_center - m_w * .5f, y_center - m_h * .5f, x_center, y_center));
	static const int r2 = (int)round(m_w * .5f);

	/* *
	 * 三角形的三个点
	 * 四个方向,分别为四个角度
	 * */
	static const int angles[4][3] = {
		{45,  270, 135},/* up    */
		{315, 90,  225},/* down  */
		{45,  180, 315},/* left  */
		{135, 0,   225} /* rigth */
	};

	// 根据当前角度选择上面定义的角度数组
	int angle_index;
	if (m_dir == SnakeNodeDir::SND_UP)
		angle_index = 0;
	else if (m_dir == SnakeNodeDir::SND_DOWN)
		angle_index = 1;
	else if (m_dir == SnakeNodeDir::SND_LEFT)
		angle_index = 2;
	else if (m_dir == SnakeNodeDir::SND_RIGHT)
		angle_index = 3;

	// 根据当前尾巴方向进行绘制 尾巴"△"
	POINT points[3];
	points[0].x = x_center - (int)round((cos(angles[angle_index][0] * Util::RAD)) * r);
	points[0].y = y_center - (int)round((sin(angles[angle_index][0] * Util::RAD)) * r);

	points[1].x = x_center - (int)round((cos(angles[angle_index][1] * Util::RAD)) * r2);
	points[1].y = y_center - (int)round((sin(angles[angle_index][1] * Util::RAD)) * r2);

	points[2].x = x_center - (int)round((cos(angles[angle_index][2] * Util::RAD)) * r);
	points[2].y = y_center - (int)round((sin(angles[angle_index][2] * Util::RAD)) * r);

	// 调用Util的多边形填充
	Util::PolyFill(hdc, RGB(255, 192, 203), points, 3);
}

1. 绘制蛇尾,也就是一个三角形。(会在文章的最后会进行详细解释)


Trap (障壁类)

头文件:

//++++++++++++++++++++++++++++++++++
// 障壁类
//----------------------------------

#pragma once

#ifndef __TRAP_H__
#define __TRAP_H__

enum EnumTrapStyle;	// 声明障壁样式枚举

class Trap 
{
public:
	Trap(EnumTrapStyle style, int x, int y, int w, int h);
	~Trap();

public:
	// 食物的渲染
	void Render(HDC hdc);			

	// 返回食物的x坐标位置
	int GetX();						
	// 返回食物的y坐标位置
	int GetY();						

private:
	EnumTrapStyle m_style;  // 障壁的样式
	int m_x;                // 食物的X坐标位置
	int m_y;                // 食物的Y坐标位置
	int m_w;                // 食物的宽度
	int m_h;                // 食物的高度
};

#endif // !__TRAP_H__

源文件:


#include <windows.h>      // WIN32开发中最重要的头文件(不用详解了吧...)

#include "GameType.h"     // 游戏中使用到的一些枚举、别名定义
#include "util.h"         // 常用工具类
#include "Trap.h"         // 游戏障壁类

Trap::Trap(EnumTrapStyle style, int x, int y, int width, int height) 
	: m_style(style), // 初始化障壁的样式
	m_x(x),           // 初始化障壁的x坐标位置
	m_y(y),           // 初始化障壁的y坐标位置
	m_w(width),       // 初始化障壁的宽度
	m_h(height)       // 初始化障壁的高度
{
}

Trap::~Trap()
{
}

void Trap::Render(HDC hdc)
{
	// 根据障壁的样式进行不同的渲染
	if (m_style == EnumTrapStyle::TS_BLOCK)
	{
		// 块状障壁(四个角落的)
		// 障壁矩形
		RECT rc;
		rc.left = m_x - m_w * .5f;
		rc.top = m_y - m_h * .5f;
		rc.right = m_x + m_w * .5f;
		rc.bottom = m_y + m_h * .5f;

		HBRUSH hBrush = ::CreateSolidBrush(RGB(115, 67, 56));
		// 绘制障壁
		::FillRect(hdc, &rc, hBrush);
		::DeleteObject(hBrush); hBrush = NULL;
	}
	else 
	{
		// 绘制三角形障壁
		// 障壁的中心点坐标位置
		const int x_center = m_x;
		const int y_center = m_y;
		static const int r = round(Util::GetDistanceBetweenTwoPoint(x_center - m_w * .5f, y_center - m_h * .5f, x_center, y_center));
		static const int r2 = m_w * .5f;

		/* *
		 * 三角形的三个点
		 * 四个方向,分别为四个角度
		 * */
		static const int angles[4][3] = {
			{45,  270, 135},/* up    */
			{315, 90,  225},/* down  */
			{45,  180, 315},/* left  */
			{135, 0,   225} /* rigth */
		};

		// 根据当前角度选择上面定义的角度数组
		int angle_index;
		if (m_style == EnumTrapStyle::TS_UPTIP)
			angle_index = 0;
		else if (m_style == EnumTrapStyle::TS_DOWNTIP)
			angle_index = 1;
		else if (m_style == EnumTrapStyle::TS_LEFTTIP)
			angle_index = 2;
		else if (m_style == EnumTrapStyle::TS_RIGHTTIP)
			angle_index = 3;

		// 根据当前障壁的样式进行绘制 "◁△▽▷"
		POINT points[3];
		points[0].x = x_center - round((cos(angles[angle_index][0] * Util::RAD)) * r);
		points[0].y = y_center - round((sin(angles[angle_index][0] * Util::RAD)) * r);

		points[1].x = x_center - round((cos(angles[angle_index][1] * Util::RAD)) * r2);
		points[1].y = y_center - round((sin(angles[angle_index][1] * Util::RAD)) * r2);

		points[2].x = x_center - round((cos(angles[angle_index][2] * Util::RAD)) * r);
		points[2].y = y_center - round((sin(angles[angle_index][2] * Util::RAD)) * r);

		// 调用Util的多边形填充
		Util::PolyFill(hdc, RGB(115, 67, 56), points, 3);
	}
}

int Trap::GetX()
{
	// 获取障壁的x坐标位置
	return m_x;
}

int Trap::GetY()
{
	// 获取障壁的y坐标位置
	return m_y;
}

下面进行讲解上面的程序:


Trap::Trap(EnumTrapStyle style, int x, int y, int width, int height) 

Trap::Trap(EnumTrapStyle style, int x, int y, int width, int height) 
	: m_style(style),   // 初始化障壁的样式
	m_x(x),             // 初始化障壁的x坐标位置
	m_y(y),             // 初始化障壁的y坐标位置
	m_w(width),         // 初始化障壁的宽度
	m_h(height)         // 初始化障壁的高度
{
}

1. 初始化障壁的样式、位置、宽度和高度


Trap::Render(HDC hdc)

void Trap::Render(HDC hdc)
{
	// 根据障壁的样式进行不同的渲染
	if (m_style == EnumTrapStyle::TS_BLOCK)
	{
		// 块状障壁(四个角落的)
		// 障壁矩形
		RECT rc;
		rc.left = m_x - (int)(m_w * .5f);
		rc.top = m_y - (int)(m_h * .5f);
		rc.right = m_x + (int)(m_w * .5f);
		rc.bottom = m_y + (int)(m_h * .5f);

		HBRUSH hBrush = ::CreateSolidBrush(RGB(115, 67, 56));
		// 绘制障壁
		::FillRect(hdc, &rc, hBrush);
		::DeleteObject(hBrush); hBrush = NULL;
	}
	else 
	{
		// 绘制三角形障壁
		// 障壁的中心点坐标位置
		const int x_center = m_x;
		const int y_center = m_y;
		static const int r = (int)round(Util::GetDistanceBetweenTwoPoint(x_center - m_w * .5f, y_center - m_h * .5f, x_center, y_center));
		static const int r2 = (int)(m_w * .5f);

		/* *
		 * 三角形的三个点
		 * 四个方向,分别为四个角度
		 * */
		static const int angles[4][3] = {
			{45,  270, 135},/* up    */
			{315, 90,  225},/* down  */
			{45,  180, 315},/* left  */
			{135, 0,   225} /* rigth */
		};

		// 根据当前角度选择上面定义的角度数组
		int angle_index;
		if (m_style == EnumTrapStyle::TS_UPTIP)
			angle_index = 0;
		else if (m_style == EnumTrapStyle::TS_DOWNTIP)
			angle_index = 1;
		else if (m_style == EnumTrapStyle::TS_LEFTTIP)
			angle_index = 2;
		else if (m_style == EnumTrapStyle::TS_RIGHTTIP)
			angle_index = 3;

		// 根据当前障壁的样式进行绘制 "◁△▽▷"
		POINT points[3];
		points[0].x = x_center - (int)round((cos(angles[angle_index][0] * Util::RAD)) * r);
		points[0].y = y_center - (int)round((sin(angles[angle_index][0] * Util::RAD)) * r);

		points[1].x = x_center - (int)round((cos(angles[angle_index][1] * Util::RAD)) * r2);
		points[1].y = y_center - (int)round((sin(angles[angle_index][1] * Util::RAD)) * r2);

		points[2].x = x_center - (int)round((cos(angles[angle_index][2] * Util::RAD)) * r);
		points[2].y = y_center - (int)round((sin(angles[angle_index][2] * Util::RAD)) * r);

		// 调用Util的多边形填充
		Util::PolyFill(hdc, RGB(115, 67, 56), points, 3);
	}
}

1. 如果样式为块,则绘制一个格子的矩形

2. 如果为四边中间的部分,则绘制相应方向的三角形。(与蛇尾相似,看文章最后部分吧)


Fruit (食物类)

头文件:

//++++++++++++++++++++++++++++++++++
// 食物类
//----------------------------------

#pragma once

#ifndef __FRUIT_H__
#define __FRUIT_H__

class Fruit
{
public:
	Fruit(int score) : m_score(score), m_x(0), m_y(0), m_w(0), m_h(0) { }
	Fruit(int score, int x, int y, int w, int h);
	~Fruit();

public:
	// 食物的渲染
	void Render(HDC hdc);					
	// 重新设置食物的位置和宽度
	void Reset(int score, int x, int y, int w, int h);	
	// 返回食物的x坐标位置
	int GetX();								
	// 返回食物的y坐标位置
	int GetY();								
	// 获取食物的分数
	int GetScore();							

private:
	int m_score;     // 食物的分数
	int m_x;         // 食物的x坐标位置
	int m_y;         // 食物的y坐标位置
	int m_w;         // 食物的宽度
	int m_h;         // 食物的高度
};

#endif // !__FRUIT_H__

源文件:


#include <windows.h>     // WIN32开发中最重要的头文件(不用详解了吧...)

#include "util.h"        // 常用工具类
#include "Fruit.h"       // 食物类

Fruit::Fruit(int score, int x, int y, int width, int height) 
	: m_score(score),   // 初始化食物的分数
	m_x(x),             // 初始化食物的x坐标位置
	m_y(y),             // 初始化食物的y坐标位置
	m_w(width),         // 初始化食物的宽度
	m_h(height)         // 初始化食物的高度
{
}

Fruit::~Fruit()
{
}

void Fruit::Render(HDC hdc)
{
	// 绘制食物

	// 创建空画笔
	HPEN hOldPen = (HPEN)::SelectObject(hdc, ::GetStockObject(NULL_PEN));
	// 创建画刷,根据分数创建不同的颜色
	HBRUSH hBrush = ::CreateSolidBrush(GetScore() > 1 ? RGB(255, 0, 0) : RGB(0, 255, 255));
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制食物 "○"
	::Ellipse(hdc, m_x - m_w * .5f, m_y - m_h * .5f, m_x + m_w * .5f, m_y + m_h * .5f);

	::SelectObject(hdc, hOldPen);
	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hBrush);
}

int Fruit::GetX()
{
	// 获取食物的x坐标位置
	return m_x;
}

int Fruit::GetY()
{
	// 获取食物的y坐标位置
	return m_y;
}

void Fruit::Reset(int score, int x, int y, int w, int h)
{
	// 重设食物的位置宽度以及分数
	m_score = score;
	m_x = x;
	m_y = y;
	m_w = w;
	m_h = h;
}

int Fruit::GetScore()
{
	// 获取食物的分数
	return m_score;
}

下面进行讲解上面的程序:


Fruit::Fruit(int score, int x, int y, int width, int height) 

Fruit::Fruit(int score, int x, int y, int width, int height) 
	: m_score(score),   // 初始化食物的分数
	m_x(x),             // 初始化食物的x坐标位置
	m_y(y),             // 初始化食物的y坐标位置
	m_w(width),         // 初始化食物的宽度
	m_h(height)         // 初始化食物的高度
{
}

1. 设置食物的分数、位置、宽度和高度


Fruit::Render(HDC hdc)

void Fruit::Render(HDC hdc)
{
	// 绘制食物

	// 创建空画笔
	HPEN hOldPen = (HPEN)::SelectObject(hdc, ::GetStockObject(NULL_PEN));
	// 创建画刷,根据分数创建不同的颜色
	HBRUSH hBrush = ::CreateSolidBrush(GetScore() > 1 ? RGB(255, 0, 0) : RGB(0, 255, 255));
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制食物 "○"
	::Ellipse(hdc, (int)(m_x - m_w * .5f), (int)(m_y - m_h * .5f), (int)(m_x + m_w * .5f), (int)(m_y + m_h * .5f));

	::SelectObject(hdc, hOldPen);
	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hBrush);
}

1. 绘制一个圆,根据分数设置不同的颜色画刷


Fruit::Reset(int score, int x, int y, int w, int h)

void Fruit::Reset(int score, int x, int y, int w, int h)
{
	// 重设食物的位置宽度以及分数
	m_score = score;
	m_x = x;
	m_y = y;
	m_w = w;
	m_h = h;
}

1. 重设食物的分数、位置、宽度和高度


GameLabel (游戏标签类)

头文件:

//++++++++++++++++++++++++++++++++++
// 游戏标签类
//----------------------------------

#pragma once

#ifndef __GAME_LABEL_H__
#define __GAME_LABEL_H__

class GameLabel
{
public:
	GameLabel(int x = 0, int y = 0, LPCTSTR text = NULL);
	~GameLabel();

public:
	// 渲染标签
	void Render(HDC hdc);								
	// 设置标签文本
	void SetText(LPCTSTR text);							
	// 设置标签字体尺寸
	void SetFontSize(int nHeight);						
	// 设置标签x坐标位置
	void SetX(int x);									
	// 设置标签y坐标位置
	void SetY(int y);									
	// 设置标签背景颜色
	void SetBackground(COLORREF background);			
	// 设置标签前景颜色
	void SetForeground(COLORREF foreground);			
	// 设置标签背景是否透明
	void SetBackgroundTransparent(bool bTransparent);	
	// 设置标签是否启用
	void SetEnable(bool bEnable);						
	// 设置标签是否可见
	void SetVisible(bool bVisible);						
	// 获取标签文本
	LPCTSTR GetText();									
	// 获取标签x坐标位置
	int GetX();											
	// 获取标签y坐标位置
	int GetY();											
	// 获取标签宽度
	int GetWidth();										
	// 获取标签宽度
	int GetHeight();									
	// 是否可见
	bool IsVisible();									
	// 是否启用
	bool IsEnable();									

protected:
	int m_x;                         // 标签x坐标位置
	int m_y;                         // 标签y坐标位置	
	int m_w;                         // 标签宽度
	int m_h;                         // 标签高度
	bool m_background_transparent;   // 标签背景是否透明
	bool m_enable;                   // 标签是否启用
	bool m_visible;                  // 标签是否可见
	COLORREF m_background;           // 标签的背景颜色
	COLORREF m_foreground;           // 标签的前景颜色
	LPCTSTR m_text;                  // 标签的文本
};

#endif // !__GAME_LABEL_H__

源文件:


#include <windows.h>      // WIN32开发中最重要的头文件(不用详解了吧...)
#include <tchar.h>        // 通用字符集头文件,对于字符集之间的兼容比如ASCII编码和Unicode编码

#include "Game.h"         // 游戏类
#include "util.h"         // 常用工具类
#include "GameLabel.h"    // 游戏标签类

GameLabel::GameLabel(int x, int y, LPCTSTR text) 
	: m_x(x),         // 初始化标签的x位置坐标
	m_y(y),           // 初始化标签的y坐标位置
	m_visible(true),  // 初始化标签的可见性
	m_enable(true)    // 初始化标签的启用
{
	// 设置默认字体大小
	SetFontSize(100);
	// 设置文本
	SetText(text == NULL ? _T("GameLabel") : text);
	// 设置背景和前景色,默认为透明背景
	SetBackground(RGB(0, 0, 0));
	SetForeground(RGB(255, 255, 255));
	SetBackgroundTransparent(true);
}

GameLabel::~GameLabel()
{
}

void GameLabel::Render(HDC hdc)
{
	// 判断是否可见
	if (!m_visible)
		return;

	HFONT hFont, hOldFont;
	Util::CreateLogFont(hFont, m_h);
	hOldFont = (HFONT)::SelectObject(hdc, hFont);

	int nOldBkMode;
	COLORREF oldBkColor, oldTextColor;
	if (m_background_transparent)
	{
		// 设置背景透明
		nOldBkMode = ::SetBkMode(hdc, TRANSPARENT);
	}
	else 
	{
		// 设置背景不透明
		nOldBkMode = ::SetBkMode(hdc, OPAQUE);
		// 设置背景色
		oldBkColor = ::SetBkColor(hdc, m_background);
	}

	// 设置前景色
	oldTextColor = ::SetTextColor(hdc, m_foreground);
	// 渲染文本
	::TextOut(hdc, m_x, m_y, m_text, wcslen(m_text));

	::SetTextColor(hdc, oldTextColor);

	if (!m_background_transparent)
	{
		::SetBkColor(hdc, oldBkColor);
	}
	::SetBkMode(hdc, nOldBkMode);
	::SelectObject(hdc, hOldFont);
	SafeDeleteGDIObject(hFont);
}

void GameLabel::SetText(LPCTSTR text)
{
	if (text == NULL)
	{
		text = _T("");
	}

	m_text = text;

	int nWidth = 0;
	HDC hdc = ::GetDC(Game::GetGameHwnd());
	HFONT hFont;
	Util::CreateLogFont(hFont, m_h);
	// 计算文本宽度
	Util::GetStringWidth(hdc, hFont, m_text, &m_w);
	::ReleaseDC(Game::GetGameHwnd(), hdc);
	SafeDeleteGDIObject(hFont);
}

void GameLabel::SetFontSize(int nHeight)
{
	// 设置字体大小
	m_h = nHeight;
	// 重新计算文本宽度
	SetText(GetText());
}

void GameLabel::SetX(int x)
{
	m_x = x;
}

void GameLabel::SetY(int y)
{
	m_y = y;
}

void GameLabel::SetBackground(COLORREF background)
{
	m_background = background;
}

void GameLabel::SetForeground(COLORREF foreground)
{
	m_foreground = foreground;
}

void GameLabel::SetBackgroundTransparent(bool bTransparent)
{
	m_background_transparent = bTransparent;
}

void GameLabel::SetEnable(bool bEnable)
{
	m_enable = bEnable;
}

void GameLabel::SetVisible(bool bVisible)
{
	m_visible = bVisible;
}

LPCTSTR GameLabel::GetText()
{
	return m_text;
}

int GameLabel::GetWidth()
{
	return m_w;
}

int GameLabel::GetHeight()
{
	return m_h;
}

bool GameLabel::IsVisible()
{
	return m_visible;
}

bool GameLabel::IsEnable()
{
	return m_enable;
}

int GameLabel::GetX()
{
	return m_x;
}

int GameLabel::GetY()
{
	return m_y;
}

下面进行讲解上面的程序:


GameLabel::GameLabel(int x, int y, LPCTSTR text) 

GameLabel::GameLabel(int x, int y, LPCTSTR text) 
	: m_x(x),           // 初始化标签的x位置坐标
	m_y(y),             // 初始化标签的y坐标位置
	m_visible(true),    // 初始化标签的可见性
	m_enable(true)      // 初始化标签的启用
{
	// 设置默认字体大小
	SetFontSize(100);
	// 设置文本
	SetText(text == NULL ? _T("GameLabel") : text);
	// 设置背景和前景色,默认为透明背景
	SetBackground(RGB(0, 0, 0));
	SetForeground(RGB(255, 255, 255));
	SetBackgroundTransparent(true);
}

1. 初始化标签的位置、可见、启用

2. 设置标签文本的字体大小、文本、颜色等


GameLabel::Render(HDC hdc)

void GameLabel::Render(HDC hdc)
{
	// 判断是否可见
	if (!m_visible)
		return;

	HFONT hFont, hOldFont;
	Util::CreateLogFont(hFont, m_h);
	hOldFont = (HFONT)::SelectObject(hdc, hFont);

	int nOldBkMode;
	COLORREF oldBkColor, oldTextColor;
	if (m_background_transparent)
	{
		// 设置背景透明
		nOldBkMode = ::SetBkMode(hdc, TRANSPARENT);
	}
	else 
	{
		// 设置背景不透明
		nOldBkMode = ::SetBkMode(hdc, OPAQUE);
		// 设置背景色
		oldBkColor = ::SetBkColor(hdc, m_background);
	}

	// 设置前景色
	oldTextColor = ::SetTextColor(hdc, m_foreground);
	// 渲染文本
	::TextOut(hdc, m_x, m_y, m_text, wcslen(m_text));

	::SetTextColor(hdc, oldTextColor);

	if (!m_background_transparent)
	{
		::SetBkColor(hdc, oldBkColor);
	}
	::SetBkMode(hdc, nOldBkMode);
	::SelectObject(hdc, hOldFont);
	SafeDeleteGDIObject(hFont);
}

1. 若果不可见,则不进行渲染

2. 设置字体背景是否透明以及颜色

3. 设置文本颜色

4. 绘制文本


GameLabel::SetText(LPCTSTR text)

void GameLabel::SetText(LPCTSTR text)
{
	if (text == NULL)
	{
		text = _T("");
	}

	m_text = text;

	int nWidth = 0;
	HDC hdc = ::GetDC(Game::GetGameHwnd());
	HFONT hFont;
	Util::CreateLogFont(hFont, m_h);
	// 计算文本宽度
	Util::GetStringWidth(hdc, hFont, m_text, &m_w);
	::ReleaseDC(Game::GetGameHwnd(), hdc);
	SafeDeleteGDIObject(hFont);
}

1. 设置文本

2. 计算文本的宽度


GameLabel::SetFontSize(int nHeight)

void GameLabel::SetFontSize(int nHeight)
{
	// 设置字体大小
	m_h = nHeight;
	// 重新计算文本宽度
	SetText(GetText());
}

1. 重设文本高度

2. 重新计算文本宽度


GameButton (游戏按钮类)

头文件:

//++++++++++++++++++++++++++++++++++
// 游戏按钮类
//----------------------------------

#pragma once

#ifndef __GAME_BUTTON_H__
#define __GAME_BUTTON_H__

// 枚举按钮鼠标状态
enum ButtonMouseState {
	MOVE, LBUTTONDOWN, LBUTTONUP
};

class GameButton
{
public:
	GameButton(int x = 0, int y = 0, LPCTSTR text = NULL);
	~GameButton();

public:
	// 渲染按钮
	virtual void Render(HDC hdc);										
	// 鼠标的点击事件
	virtual void OnClick(int x, int y);									
	// 窗口消息处理
	virtual void WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); 
	// 鼠标的进入按钮事件
	virtual void OnMouseEnter(int x, int y);							
	// 鼠标的停留事件
	virtual void OnMouseHover(int x, int y);							
	// 鼠标的离开按钮事件
	virtual void OnMouseLeave();										

	// 设置按钮的背景颜色
	void SetBackground(COLORREF backgroundColor);						
	// 设置按钮的前景颜色
	void SetForeground(COLORREF foregroundColor);						
	// 设置按钮鼠标停留的背景颜色
	void SetHoverBackground(COLORREF hoverBackgroundColor);				
	// 设置按钮鼠标停留的前景颜色
	void SetHoverForeground(COLORREF hoverForegroundColor);				
	// 设置按钮鼠标点击的背景颜色
	void SetMouseDownBackground(COLORREF mouseDownBackgroundColor);		
	// 设置按钮鼠标点击的前景颜色
	void SetMouseDownForeground(COLORREF mouseDownForegroundColor);		
	// 设置按钮的文本
	void SetText(LPCTSTR text);											
	// 设置按钮的文字尺寸
	void SetFontSize(int nHeight);										
	// 设置按钮的x坐标位置(左上角)
	void SetX(int x);													
	// 设置按钮的y坐标位置(左上角)
	void SetY(int y);													
	// 设置按钮是否启用
	void SetEnable(bool bEnable);										
	// 设置按钮是否可见
	void SetVisible(bool bVisible);										
	// 设置按钮的点击函数(function)对象
	void SetClickDelegate(GameButtonClickDelegateFunction fnClick);		
	// 获取按钮的文本
	LPCTSTR GetText();													
	// 获取按钮的宽度
	int GetWidth();														
	// 获取按钮的高度
	int GetHeight();													
	// 获取按钮的x坐标位置(左上角)
	int GetX();															
	// 获取按钮的y坐标位置(左上角)
	int GetY();															
	// 是否可见
	bool IsVisible();													
	// 是否启用
	bool IsEnable();													

protected:
	GameButtonClickDelegateFunction m_fn_click; // 按钮点击的函数(function)对象
	LPCTSTR m_text;                             // 按钮的文本
	COLORREF m_background;                      // 按钮的背景颜色
	COLORREF m_foreground;                      // 按钮的前景颜色
	COLORREF m_hover_background;                // 鼠标停留时按钮的背景颜色
	COLORREF m_hover_foreground;                // 鼠标停留时按钮的前景颜色
	COLORREF m_mouse_down_background;           // 鼠标点击时的背景颜色
	COLORREF m_mouse_down_foreground;           // 鼠标点击时的前景颜色
	int m_x;                                    // 按钮的x坐标位置
	int m_y;                                    // 按钮的y坐标位置
	int m_text_w;                               // 按钮中文本的宽度
	int m_text_h;                               // 按钮中文件的高度
	int m_w;                                    // 按钮的宽度
	int m_h;                                    // 按钮的高度
	bool m_mouse_enter;                         // 鼠标是否进入按钮
	bool m_mouse_down;                          // 鼠标是否点击按钮
	bool m_enable;                              // 按钮是否启用
	bool m_visible;                             // 按钮是否可见
};

#endif // !__GAME_BUTTON_H__

源文件:


#include <windows.h>         // WIN32开发中最重要的头文件(不用详解了吧...)
#include <windowsx.h>        // 包含的大量有用的API宏等(这里用到了GET_X_LPARAM和GET_Y_LPARAM)
#include <tchar.h>           // 通用字符集头文件,对于字符集之间的兼容比如ASCII编码和Unicode编码

#include "Util.h"            // 常用工具类
#include "Game.h"            // 游戏类
#include "GameType.h"        // 游戏中使用到的一些枚举、别名定义
#include "GameButton.h"      // 游戏按钮类

GameButton::GameButton(int x, int y, LPCTSTR text) 
	: m_x(x),            // 初始化按钮的x坐标位置
	m_y(y),              // 初始化按钮的y坐标位置
	m_visible(true),     // 初始化按钮的可见性
	m_enable(true)       // 初始化按钮的启用
{
	// 初始化背景色和前景色
	SetBackground(RGB(255, 24, 84));
	SetForeground(RGB(255, 255, 255));
	SetHoverBackground(RGB(255, 36, 96));
	SetHoverForeground(RGB(255, 255, 255));
	SetMouseDownBackground(RGB(255, 48, 108));
	SetMouseDownForeground(RGB(255, 255, 255));
	// 初始化默认字体大小
	SetFontSize(100);
	// 初始化默认文本
	SetText(text == NULL ? _T("GameButton") : text);
}

GameButton::~GameButton()
{
}

void GameButton::Render(HDC hdc)
{
	// 渲染按钮

	// 判断是否可见,则返回
	if (!m_visible)
		return;

	// 背景颜色
	COLORREF background = m_background, foreground = m_foreground;
	if (m_mouse_down)
	{
		// 点击的背景和前景颜色
		foreground = m_mouse_down_foreground;
		background = m_mouse_down_background;
	}
	else if (m_mouse_enter)
	{
		// 鼠标在按钮中的背景和前景颜色
		foreground = m_hover_foreground;
		background = m_hover_background;
	}

	HBRUSH hBrush = ::CreateSolidBrush(background);
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制按钮背景

	// 路径开始
	::BeginPath(hdc);
	// 绘制一个圆角矩形
	::RoundRect(hdc, m_x, m_y, m_x + m_w, m_y + m_h, 25, 50);
	// 路径结束
	::EndPath(hdc);
	// 填充路径.
	// 注意: FillPath结束后选择的路径会自动清除
	::FillPath(hdc);

	HFONT hFont, hOldFont;
	Util::CreateLogFont(hFont, m_text_h);
	hOldFont = (HFONT)::SelectObject(hdc, hFont);

	// 设置字体背景模式(透明或不透明)
	int nOldBkMode = ::SetBkMode(hdc, TRANSPARENT);
	// 设置字体背景颜色
	COLORREF hOldTextColor = ::SetTextColor(hdc, foreground);

	// 绘制按钮文字
	::TextOut(hdc, m_x + 5, m_y + 5, m_text, wcslen(m_text));

	::SetTextColor(hdc, hOldTextColor);
	::SetBkMode(hdc, nOldBkMode);
	::SelectObject(hdc, hOldFont);
	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hBrush);
	SafeDeleteGDIObject(hFont);
}

void GameButton::OnClick(int x, int y)
{
	// 调用点击委托函数
	m_fn_click(x, y);
}

void GameButton::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 按钮的窗口消息处理

	// 如果不可见和不启用,则返回
	if (!m_enable || !m_visible)
		return;

	int x, y;
	// 鼠标在按钮的状态
	ButtonMouseState mouseState = ButtonMouseState::MOVE;
	switch (message)
	{
	case WM_MOUSEMOVE:
		break;
	case WM_LBUTTONDOWN:
		// 左键按下
		mouseState = ButtonMouseState::LBUTTONDOWN;
		break;
	case WM_LBUTTONUP:
		// 左键松开
		mouseState = ButtonMouseState::LBUTTONUP;
		break;
	default:
		return;
	}

	// 获取鼠标位置
	x = GET_X_LPARAM(lParam);
	y = GET_Y_LPARAM(lParam);

	// 检测鼠标坐标是否在按钮内
	if (Util::CheckRectangleContainsPoint(m_x, m_y, m_x + m_w, m_y + m_h, x, y))
	{
		// 鼠标移动
		if (mouseState == ButtonMouseState::MOVE)
		{
			// 判断鼠标是否已经进入按钮
			if (m_mouse_enter)
			{
				// 调用鼠标在按钮停留的事件
				this->OnMouseHover(x, y);
			}
			else
			{
				// 设置鼠标进入
				this->m_mouse_enter = true;
				// 调用鼠标进入按钮的事件
				this->OnMouseEnter(x, y);
			}
		}
		else if (mouseState == ButtonMouseState::LBUTTONDOWN) // 鼠标左键点下
		{
			// 设置鼠标左键点下
			this->m_mouse_down = true;
		}
		else  // 鼠标松开
		{
			// 设置鼠标左键点击取消
			if (this->m_mouse_down)
				this->OnClick(x, y); // 调用点击事件
		}
	}
	else 
	{
		// 鼠标移动离开按钮
		if (m_mouse_enter)
		{
			// 设置鼠标离开
			this->m_mouse_enter = false;
			// 调用鼠标离开按钮事件
			this->OnMouseLeave();
		}
	}

	// 鼠标松开
	if (mouseState == ButtonMouseState::LBUTTONUP)
	{
		this->m_mouse_down = false;
	}
}

void GameButton::OnMouseEnter(int x, int y)
{
}

void GameButton::OnMouseHover(int x, int y)
{
}

void GameButton::OnMouseLeave()
{
}

void GameButton::SetBackground(COLORREF backgroundColor)
{
	m_background = backgroundColor;
}

void GameButton::SetForeground(COLORREF foregroundColor)
{
	m_foreground = foregroundColor;
}

void GameButton::SetHoverBackground(COLORREF hoverBackgroundColor)
{
	m_hover_background = hoverBackgroundColor;
}

void GameButton::SetHoverForeground(COLORREF hoverForegroundColor)
{
	m_hover_foreground = hoverForegroundColor;
}

void GameButton::SetMouseDownBackground(COLORREF mouseDownBackgroundColor)
{
	m_mouse_down_background = mouseDownBackgroundColor;
}

void GameButton::SetMouseDownForeground(COLORREF mouseDownForegroundColor)
{
	m_mouse_down_foreground = mouseDownForegroundColor;
}

void GameButton::SetText(LPCTSTR text)
{
	// 设置按钮文字
	if (text == NULL)
	{
		text = _T("");
	}
	m_text = text;
	// 获取窗口DC
	HDC hdc = ::GetDC(Game::GetGameHwnd());
	HFONT hFont;
	Util::CreateLogFont(hFont, m_text_h);
	// 获取文本宽度
	Util::GetStringWidth(hdc, hFont, m_text, &m_text_w);
	SafeDeleteGDIObject(hFont);
	m_w = m_text_w + 2 * 5;
}

void GameButton::SetFontSize(int nHeight)
{
	// 设置文本高度
	m_text_h = nHeight;
	// 设置按钮高度
	m_h = m_text_h + 2 * 5;
	// 重设置文本,计算宽度
	SetText(GetText());
}

void GameButton::SetClickDelegate(GameButtonClickDelegateFunction fnClick)
{
	// 设置按钮点击函数(function)对象
	m_fn_click = fnClick;
}

LPCTSTR GameButton::GetText()
{
	// 获取文本
	return m_text;
}

int GameButton::GetWidth()
{
	// 获取宽度
	return m_w;
}

int GameButton::GetHeight()
{
	// 获取高度
	return m_h;
}

int GameButton::GetX()
{
	// 获取按钮x坐标位置
	return m_x;
}

int GameButton::GetY()
{
	// 获取按钮y坐标位置
	return m_y;
}

bool GameButton::IsVisible()
{
	return m_visible;
}

bool GameButton::IsEnable()
{
	return m_enable;
}

void GameButton::SetX(int x)
{
	// 设置按钮x坐标位置
	m_x = x;
}

void GameButton::SetY(int y)
{
	// 设置按钮y坐标位置
	m_y = y;
}

void GameButton::SetEnable(bool bEnable)
{
	// 设置按钮释放启用
	m_enable = bEnable;
}

void GameButton::SetVisible(bool bVisible)
{
	// 设置按钮是否可见
	m_visible = bVisible;
}

下面进行讲解上面的程序:


GameButton::GameButton(int x, int y, LPCTSTR text) 

GameButton::GameButton(int x, int y, LPCTSTR text) 
	: m_x(x),           // 初始化按钮的x坐标位置
	m_y(y),             // 初始化按钮的y坐标位置
	m_visible(true),    // 初始化按钮的可见性
	m_enable(true)      // 初始化按钮的启用
{
	// 初始化背景色和前景色
	SetBackground(RGB(255, 24, 84));
	SetForeground(RGB(255, 255, 255));
	SetHoverBackground(RGB(255, 36, 96));
	SetHoverForeground(RGB(255, 255, 255));
	SetMouseDownBackground(RGB(255, 48, 108));
	SetMouseDownForeground(RGB(255, 255, 255));
	// 初始化默认字体大小
	SetFontSize(100);
	// 初始化默认文本
	SetText(text == NULL ? _T("GameButton") : text);
}

1. 初始化按钮的位置、可见、启用

2. 设置前景色等


GameButton::Render(HDC hdc)

void GameButton::Render(HDC hdc)
{
	// 渲染按钮

	// 判断是否可见,则返回
	if (!m_visible)
		return;

	// 背景颜色
	COLORREF background = m_background, foreground = m_foreground;
	if (m_mouse_down)
	{
		// 点击的背景和前景颜色
		foreground = m_mouse_down_foreground;
		background = m_mouse_down_background;
	}
	else if (m_mouse_enter)
	{
		// 鼠标在按钮中的背景和前景颜色
		foreground = m_hover_foreground;
		background = m_hover_background;
	}

	HBRUSH hBrush = ::CreateSolidBrush(background);
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

	// 绘制按钮背景

	// 路径开始
	::BeginPath(hdc);
	// 绘制一个圆角矩形
	::RoundRect(hdc, m_x, m_y, m_x + m_w, m_y + m_h, 25, 50);
	// 路径结束
	::EndPath(hdc);
	// 填充路径.
	// 注意: FillPath结束后选择的路径会自动清除
	::FillPath(hdc);

	HFONT hFont, hOldFont;
	Util::CreateLogFont(hFont, m_text_h);
	hOldFont = (HFONT)::SelectObject(hdc, hFont);

	// 设置字体背景模式(透明或不透明)
	int nOldBkMode = ::SetBkMode(hdc, TRANSPARENT);
	// 设置字体背景颜色
	COLORREF hOldTextColor = ::SetTextColor(hdc, foreground);

	// 绘制按钮文字
	::TextOut(hdc, m_x + 5, m_y + 5, m_text, wcslen(m_text));

	::SetTextColor(hdc, hOldTextColor);
	::SetBkMode(hdc, nOldBkMode);
	::SelectObject(hdc, hOldFont);
	::SelectObject(hdc, hOldBrush);
	SafeDeleteGDIObject(hBrush);
	SafeDeleteGDIObject(hFont);
}

1. 如果不可见,则不进行渲染

2. 根据鼠标在按钮的状态,进行设置不同的按钮背景色以及文本的前景色

3. 绘制带圆弧的矩形、文本


GameButton::OnClick(int x, int y)

void GameButton::OnClick(int x, int y)
{
	// 调用点击委托函数
	m_fn_click(x, y);
}

1. 调用点击的函数对象

PS: 如果函数对象未设置,会出错哦


GameButton::WndProc(HWND, UINT, WPARAM, LPARAM)

void GameButton::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	// 按钮的窗口消息处理

	// 如果不可见和不启用,则返回
	if (!m_enable || !m_visible)
		return;

	int x, y;
	// 鼠标在按钮的状态
	ButtonMouseState mouseState = ButtonMouseState::MOVE;
	switch (message)
	{
	case WM_MOUSEMOVE:
		break;
	case WM_LBUTTONDOWN:
		// 左键按下
		mouseState = ButtonMouseState::LBUTTONDOWN;
		break;
	case WM_LBUTTONUP:
		// 左键松开
		mouseState = ButtonMouseState::LBUTTONUP;
		break;
	default:
		return;
	}

	// 获取鼠标位置
	x = GET_X_LPARAM(lParam);
	y = GET_Y_LPARAM(lParam);

	// 检测鼠标坐标是否在按钮内
	if (Util::CheckRectangleContainsPoint(m_x, m_y, m_x + m_w, m_y + m_h, x, y))
	{
		// 鼠标移动
		if (mouseState == ButtonMouseState::MOVE)
		{
			// 判断鼠标是否已经进入按钮
			if (m_mouse_enter)
			{
				// 调用鼠标在按钮停留的事件
				this->OnMouseHover(x, y);
			}
			else
			{
				// 设置鼠标进入
				this->m_mouse_enter = true;
				// 调用鼠标进入按钮的事件
				this->OnMouseEnter(x, y);
			}
		}
		else if (mouseState == ButtonMouseState::LBUTTONDOWN) // 鼠标左键点下
		{
			// 设置鼠标左键点下
			this->m_mouse_down = true;
		}
		else  // 鼠标松开
		{
			// 设置鼠标左键点击取消
			if (this->m_mouse_down)
				this->OnClick(x, y); // 调用点击事件
		}
	}
	else 
	{
		// 鼠标移动离开按钮
		if (m_mouse_enter)
		{
			// 设置鼠标离开
			this->m_mouse_enter = false;
			// 调用鼠标离开按钮事件
			this->OnMouseLeave();
		}
	}

	// 鼠标松开
	if (mouseState == ButtonMouseState::LBUTTONUP)
	{
		this->m_mouse_down = false;
	}
}

1. 如果不启用和不可见,则不进行处理窗口的消息

2. 处理窗口的鼠标消息

3. 获取鼠标的位置

4. 检测鼠标的位置是否在按钮的矩形中

5. 如果为第一次进入则触发OnMouseEnter,如果已经进入则触发OnMouseHover

6. 如果为点击则触发OnClick

7. 检测鼠标位置不在按钮中,而之前已经进入过,则判定为鼠标离开触发OnMouseLeave


GameButton::SetText(LPCTSTR text)

void GameButton::SetText(LPCTSTR text)
{
	// 设置按钮文字
	if (text == NULL)
	{
		text = _T("");
	}
	m_text = text;
	// 获取窗口DC
	HDC hdc = ::GetDC(Game::GetGameHwnd());
	HFONT hFont;
	Util::CreateLogFont(hFont, m_text_h);
	// 获取文本宽度
	Util::GetStringWidth(hdc, hFont, m_text, &m_text_w);
	SafeDeleteGDIObject(hFont);
	m_w = m_text_w + 2 * 5;
}

1. 设置按钮文本

2. 计算文本宽度,并设置文本的位置为居中,重设按钮高度


GameButton::SetFontSize(int nHeight)

void GameButton::SetFontSize(int nHeight)
{
	// 设置文本高度
	m_text_h = nHeight;
	// 设置按钮高度
	m_h = m_text_h + 2 * 5;
	// 重设置文本,计算宽度
	SetText(GetText());
}

1. 设置文本高度

2. 设置按钮高度

3. 重设文本宽度


Util (常用工具类)

头文件:

//++++++++++++++++++++++++++++++++++
// 常用工具类
//----------------------------------

#pragma once

#ifndef __UTIL_H__
#define __UTIL_H__

// 链接 msimg32.lib库, 该库包含了图像处理的函数接口,比如我们当前用到的AlphaBlend
#pragma comment (lib, "msimg32.lib")

// 安全释放对象
#define SafeDeleteObject(object) if (object != NULL) { delete object; object = NULL; }
// 安全释放数组对象
#define SafeDeleteArrayObject(object) if (object != NULL) { delete[] object; object = NULL; }
// 安全释放GDI对象
#define SafeDeleteGDIObject(object) if (object != NULL) { ::DeleteObject(object); object = NULL; }

class Util
{
public:
#pragma region TicTacToe
	// TicTacToe
	// 绘制一条直线
	static void DrawLine(HDC hdc, int points[4]);
	// 创建双缓冲
	static void CreateDoubleBuffer(HWND hWnd, HDC &hdc, HBITMAP &hBitmap);
	// 创建逻辑字体
	static void CreateLogFont(HFONT &hFont, int nFontHeight);
#pragma endregion

#pragma region Snake 新增
	// Snake 新增
	// 多边形填充
	static void PolyFill(HDC hdc, COLORREF color, LPPOINT points, const int c);
	// 计算两点间距离
	static double GetDistanceBetweenTwoPoint(double x1, double y1, double x2, double y2);
	// 获取字符串的宽度
	static BOOL GetStringWidth(HDC hdc, HFONT hFont, LPCTSTR lpString, int *pWidth);
	// 透明混合
	static BOOL AlphaBlend(HDC hdc, int x, int y, int w, int h, COLORREF blendColor, int nBlendAlpha);
	// 判断点是否在矩形内
	static bool CheckRectangleContainsPoint(int left, int top, int right, int bottom, int px, int py);
#pragma endregion
	
public:
	static const double PI;		// 圆周率
	static const double RAD;	// 弧度
};

#endif // !__UTIL_H__

源文件:


#include <windows.h>
#include <cmath>

#include "util.h"

const double Util::PI = 3.1415926;
const double Util::RAD = 3.1415926 / 180.0;

void Util::DrawLine(HDC hdc, int points[4])
{
	/* *
	 * int[4] 表示两个点的 (x, y) 
	 * 第一个点为 (points[0], points[1])
	 * 第二个点为 (points[2], points[3])
	 * */

	::MoveToEx(hdc, points[0], points[1], NULL);
	::LineTo(hdc, points[2], points[3]);
}

void Util::CreateDoubleBuffer(HWND hWnd, HDC &mdc, HBITMAP &bitmap)
{
	/* *
	 * 创建双缓冲
	 * 也就是: 兼容DC和兼容位图
	 * */

	HDC hdc = ::GetDC(hWnd);
	RECT clientRect;
	::GetClientRect(hWnd, &clientRect);
	mdc = ::CreateCompatibleDC(hdc);
	bitmap = ::CreateCompatibleBitmap(hdc, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);

	::ReleaseDC(hWnd, hdc);
}

void Util::CreateLogFont(HFONT &hFont, int nFontHeight)
{
	/* * 
	 * 创建逻辑字体
	 * */

	LOGFONT logfont;
	ZeroMemory(&logfont, sizeof(LOGFONT));
	logfont.lfCharSet = GB2312_CHARSET;
	logfont.lfHeight = nFontHeight;
	hFont = CreateFontIndirect(&logfont);
}

void Util::PolyFill(HDC hdc, COLORREF color, LPPOINT points, const int c)
{
	/* *
	 * 通过一组点进行相连,进行填充
	 * 每两个元素为一个坐标(x, y)
	 * */
	
	/* 设置接收定点类型 */
	BYTE *type_arr = new BYTE[c];
	::memset((void*)type_arr, 0, sizeof(BYTE) * c);

	int type_index = 1;
	type_arr[0] = PT_MOVETO;
	while (true)
	{
		if (type_index >= c)
			break;
		type_arr[type_index] = PT_LINETO;
		++type_index;
	}
	type_arr[c-1] |= PT_CLOSEFIGURE;
	
	/* 进行绘制路径 */
	::BeginPath(hdc);
	::PolyDraw(hdc, points, type_arr, c);
	::EndPath(hdc);

	/* 填充路径 */
	HBRUSH hBrush = ::CreateSolidBrush(color);
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);
	::FillPath(hdc);
	::SelectObject(hdc, hOldBrush);
	::DeleteObject(hBrush); hBrush = NULL;
	SafeDeleteArrayObject(type_arr);
}

double Util::GetDistanceBetweenTwoPoint(double x1, double y1, double x2, double y2)
{
	// 根据勾股定理计算两点距离
	double a = x1 - x2;
	double b = y1 - y2;
	return sqrt(a * a + b * b);
}

bool Util::CheckRectangleContainsPoint(int left, int top, int right, int bottom, int px, int py)
{
	// 通过计算点与矩形左上角和右下角比较是否在矩形中
	return (px >= left && px <= right && py >= top && py <= bottom);
}

BOOL Util::GetStringWidth(HDC hdc, HFONT hFont, LPCTSTR lpString, int *pWidth)
{
	BOOL bRet = TRUE;

	*pWidth = 0;
	int nWidth = 0;

	// 选择字体
	HFONT hOldFont;
	hOldFont = (HFONT)::SelectObject(hdc, hFont);

	// 遍历字符串
	for (int i = 0, count = wcslen(lpString); i < count; ++i)
	{
		// 获取单个字符的宽度
		bRet = GetCharWidth(hdc, lpString[i], lpString[i], &nWidth);
		if (!bRet)
			return bRet;
		// 累加宽度
		*pWidth += nWidth;
	}

	::SelectObject(hdc, hOldFont);
	return bRet;
}

BOOL Util::AlphaBlend(HDC hdc, int x, int y, int w, int h, COLORREF blendColor, int nBlendAlpha)
{
	// 透明混合

	BOOL bRet = TRUE;
	// 创建兼容DC
	HDC mdc = ::CreateCompatibleDC(hdc);
	// 创建兼容位图
	HBITMAP hBitmap = ::CreateCompatibleBitmap(mdc, w, h);
	// 选择兼容位置
	::SelectObject(mdc, hBitmap);

	// 混合区域
	RECT rcBlend;
	rcBlend.left = x;
	rcBlend.top = y;
	rcBlend.right = x + w;
	rcBlend.bottom = y + h;

	// 填充混合颜色
	::FillRect(mdc, &rcBlend, ::CreateSolidBrush(blendColor));

	// 设置混合操作的结构体
	BLENDFUNCTION bf;
	// 设置混合操作
	bf.BlendOp = AC_SRC_OVER;
	// 设置混合标志(必须为0)
	bf.BlendFlags = 0;
	// 设置透明度(0-255)
	bf.SourceConstantAlpha = nBlendAlpha;
	// 设置混合方式
	bf.AlphaFormat = 0;

	// 进行混合
	bRet = ::AlphaBlend(hdc, 0, 0, w, h, mdc, 0, 0, w, h, bf);
	
	// 释放DC
	::DeleteDC(mdc); mdc = NULL;
	// 释放位图
	SafeDeleteGDIObject(hBitmap);
	return bRet;
}

下面进行讲解上面的程序:(Snake新增的部分)


Util::PolyFill(HDC hdc, COLORREF color, LPPOINT points, const int c)

void Util::PolyFill(HDC hdc, COLORREF color, LPPOINT points, const int c)
{
	/* *
	 * 通过一组点进行相连,进行填充
	 * 每两个元素为一个坐标(x, y)
	 * */
	
	/* 设置接收定点类型 */
	BYTE *type_arr = new BYTE[c];
	::memset((void*)type_arr, 0, sizeof(BYTE) * c);

	int type_index = 1;
	type_arr[0] = PT_MOVETO;
	while (true)
	{
		if (type_index >= c)
			break;
		type_arr[type_index] = PT_LINETO;
		++type_index;
	}
	type_arr[c-1] |= PT_CLOSEFIGURE;
	
	/* 进行绘制路径 */
	::BeginPath(hdc);
	::PolyDraw(hdc, points, type_arr, c);
	::EndPath(hdc);

	/* 填充路径 */
	HBRUSH hBrush = ::CreateSolidBrush(color);
	HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);
	::FillPath(hdc);
	::SelectObject(hdc, hOldBrush);
	::DeleteObject(hBrush); hBrush = NULL;
	SafeDeleteArrayObject(type_arr);
}

1. 设置坐标的操作

2. 绘制路径

3. 填充路径

PS: 详见(Microsoft的PolyDraw)

第一点为移动到当前点

第二点为从上一点连接到当前点

最后一点为完成连接后指示对应点是图中的最后一个点并且图形已关闭


Util::GetStringWidth(HDC hdc, HFONT hFont, LPCTSTR lpString, int *pWidth)

BOOL Util::GetStringWidth(HDC hdc, HFONT hFont, LPCTSTR lpString, int *pWidth)
{
	BOOL bRet = TRUE;

	*pWidth = 0;
	int nWidth = 0;

	// 选择字体
	HFONT hOldFont;
	hOldFont = (HFONT)::SelectObject(hdc, hFont);

	// 遍历字符串
	for (int i = 0, count = wcslen(lpString); i < count; ++i)
	{
		// 获取单个字符的宽度
		bRet = GetCharWidth(hdc, lpString[i], lpString[i], &nWidth);
		if (!bRet)
			return bRet;
		// 累加宽度
		*pWidth += nWidth;
	}

	::SelectObject(hdc, hOldFont);
	return bRet;
}

1. 根据高度创建逻辑字体,并设置到HDC中

2. 通过遍历字符串,调用GetCharWidth获取单个字符的宽度,并累加


Util::AlphaBlend(HDC, int, int, int, int, COLORREF, int)

BOOL Util::AlphaBlend(HDC hdc, int x, int y, int w, int h, COLORREF blendColor, int nBlendAlpha)
{
	// 透明混合

	BOOL bRet = TRUE;
	// 创建兼容DC
	HDC mdc = ::CreateCompatibleDC(hdc);
	// 创建兼容位图
	HBITMAP hBitmap = ::CreateCompatibleBitmap(mdc, w, h);
	// 选择兼容位置
	::SelectObject(mdc, hBitmap);

	// 混合区域
	RECT rcBlend;
	rcBlend.left = x;
	rcBlend.top = y;
	rcBlend.right = x + w;
	rcBlend.bottom = y + h;

	// 填充混合颜色
	::FillRect(mdc, &rcBlend, ::CreateSolidBrush(blendColor));

	// 设置混合操作的结构体
	BLENDFUNCTION bf;
	// 设置混合操作
	bf.BlendOp = AC_SRC_OVER;
	// 设置混合标志(必须为0)
	bf.BlendFlags = 0;
	// 设置透明度(0-255)
	bf.SourceConstantAlpha = nBlendAlpha;
	// 设置混合方式
	bf.AlphaFormat = 0;

	// 进行混合
	bRet = ::AlphaBlend(hdc, 0, 0, w, h, mdc, 0, 0, w, h, bf);
	
	// 释放DC
	::DeleteDC(mdc); mdc = NULL;
	// 释放位图
	SafeDeleteGDIObject(hBitmap);
	return bRet;
}

1. 创建兼容DC和兼容位图,并将兼容位图设置到兼容DC中

2. 设置混合区域,并填充到兼容DC中

3. 设置混合操作(BLENDFUNCTION)结构体。(详见Microsoft的BLENDFUNCTION结构体)

4. 通过AlphaBlend进行混合操作,将兼容DC的填充部分混合到传入的DC中

5. 释放兼容DC和兼容位图 


Util::GetDistanceBetweenTwoPoint(double x1, double y1, double x2, double y2)

double Util::GetDistanceBetweenTwoPoint(double x1, double y1, double x2, double y2)
{
	// 根据勾股定理计算两点距离
	double a = x1 - x2;
	double b = y1 - y2;
	return sqrt(a * a + b * b);
}

1. 通过勾股定理计算两点之间的距离


Util::CheckRectangleContainsPoint(int left, int top, int right, int bottom, int px, int py)

bool Util::CheckRectangleContainsPoint(int left, int top, int right, int bottom, int px, int py)
{
	// 通过计算点与矩形左上角和右下角比较是否在矩形中
	return (px >= left && px <= right && py >= top && py <= bottom);
}

1. 如果点超过左上角或者右下角的点,则不在矩形内


五、文章最后的一些赘述

① 蛇尾以及障壁的三角形绘制


1. 向左方向的三角形绘制


2. 向上方向的三角形绘制


3. 向右方向的三角形绘制


4. 向下方向的三角形绘制

下面贴一贴游戏图:

主菜单:

游戏中:

游戏结束:

源代码:

避免有些没有C币的小伙伴(就像我),所以将源代码放到Github了啦~

至此本章(贪吃蛇)的讲解已经结束了,也感谢认真阅读到此的各位读者。


一些总结:

可是回顾贪吃蛇的源码,是否会发现太多的重复Render和Update以及KeyDown等的重复调用,以至于以后我们的游戏都需要重复的写XXX->Render等的,所以在下一个游戏中将优化掉这一部分的重复调用。

下章预告: Win32 游戏开发: 推箱子

发布了23 篇原创文章 · 获赞 88 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/qq_31243065/article/details/97837421