本项目通过使用 windows 窗口应用程序 实现一个简化版的 吃豆子游戏,主要涉及的知识点包含有:面向对象编程思想、windows 消息循环的工作原理、windows 窗口应用程序实现、父类与子类的设计和使用、GDI 函数的简单了解、函数模板与动态分配的使用。

简化版 “吃豆子游戏—pacman”

需求分析:
在游戏中,玩家操作的角色是一张大嘴,游戏的目的就是玩家操作大嘴移动躲避敌人,并在移动过程中吃掉地图上所有的豆子,游戏胜利通关。游戏的地图是一张二维平面地图,并且存在墙壁和障碍物。游戏中敌人运动速度比大嘴稍慢,确保大嘴能够躲避敌人,敌人种类数量多种,分为普通敌人、保卫者、扰乱者。

项目主要包含以下文件:
1、GMap.h:地图类的声明文件
2、GMap.cpp:地图类的实现文件
3、GObject.h:物体类的声明文件
4、GObject.cpp:物体类的实现文件
5、pacman.cpp:创建主窗口,实现游戏运行的客户端

头文件:

(一)、设计一个地图类,其基本属性包含有:障碍物的尺寸、豆子的半径、大小为19 x 19的二维矩阵(逻辑地图点阵)、墙体与障碍物的颜色等,实现接口包含有:初始化敌我双方出现位置没有豆子出现、绘制地图与豆子,声明 Gobject 类和 pacman 类与本类为友元关系。最后在该地图类同个头文件下创建了三个继承 GMap 类的关卡地图类。

GMap.h

#pragma once //为避免头文件被包含多次,确保只被编译一次
#include "stdafx.h" //头文件预编译
#include <list> //导入“标准模板库”中的“链表类”头文件

#define MAPLENTH 19  //定义逻辑地图大小
#define P_ROW 10 //大嘴初始逻辑位置(从0开始数)
#define P_ARRAY 9
#define E_ROW 8  //敌人初始逻辑位置
#define E_ARRAY 9

using std::list; //链表命名空间

//抽象类GMap
class GMap
{
protected:
	static int LD; //障碍物的尺寸
	static int PD; //豆子的半径
	bool mapData[MAPLENTH][MAPLENTH]; //障碍物逻辑地图点阵	
	bool peaMapData[MAPLENTH][MAPLENTH]; //豆子逻辑地图点阵(四个联通口也各有一颗豆子哦)
	COLORREF color;//墙体与障碍物的颜色
    void InitOP(); //初始化敌我双方出现位置没有豆子出现

public:
	void  DrawMap(HDC& hdc); //绘制地图(参数为:绘图设备的句柄HDC)
	void  DrawPeas(HDC& hdc); //绘制豆子
	virtual ~GMap(); //将析构函数声明为虚函数,确保派生类可以被虚构析构
	GMap() //构造函数
	{
	
	}

	friend class GObject; //允许物体类使用直线的起点和终点的信息做碰撞检测
	friend class PacMan; //允许"大嘴"访问豆子地图
};

//定义关卡
//"第一关"
class Stage_1 :public GMap //子类,公有继承
{
private:
	bool static initData[MAPLENTH][MAPLENTH]; //初始化二维矩阵
public:
	Stage_1(); //初始化自身的成员矩阵
};

//"第二关"
class Stage_2 :public GMap
{
private:
	bool static initData[MAPLENTH][MAPLENTH];
public:
	Stage_2();
};

//"第三关"
class Stage_3 :public GMap
{
private:
	bool static initData[MAPLENTH][MAPLENTH];
public:
	Stage_3();
};

(二)、采用面向对象的编程思想分析物体类
面向对象的编程思想:
1、大嘴与敌人的共性:
(1)都会移动
(2)移动方向分为上下左右四个方向
(3)碰到墙壁、障碍物后都会停止
(4)都拥有自己的坐标位置
2、不同之处:
(1)大嘴是需要人为控制移动的,敌人是通过程序设定随机或有方向性的移动的
(2)大嘴能够吃豆子,敌人不能
(3)敌人能够抓住大嘴,而大嘴不能抓住敌人

依据大嘴和敌人的共性和不同之处,设计一个父类,取名为GObject,其基本属性包含有:物体在地图中的实际坐标位置、运动的朝向、帧数,实现接口包含有:判断物体是否到达逻辑坐标位置、碰撞检测、将实际坐标转换为逻辑坐标、判断物体到达逻辑坐标后更新物体在矩阵中的行列坐标等。最后在该物体类同个头文件下创建其他类,包含有: 继承 GObject 父类的大嘴类和敌人类、继承敌人类的普通敌人类、继承普通敌人类的保卫者类和扰乱者类。

GObject.h

#include "stdafx.h"
#include "GMap.h" //地图类的头文件
#include <time.h> //时间库函数头文件

#define PLAYERSPEED 6 //设置玩家速度比敌人速度快,确保大嘴能够躲避敌人
#define ENERMYSPEED 4 //敌人速度
#define LEGCOUNTS 5 //敌人腿的数量
#define DISTANCE 10 //图型范围
#define BLUE_ALERT 8 //蓝色敌人的警戒范围大小
#define D_OFFSET   2 //绘图的误差
#define RD (DISTANCE + D_OFFSET) //绘图范围12 

//建立一个方向枚举,包含上、下、左、右、结束,当玩家的方向变为“结束”时,游戏失败结束
enum TWARDS{ UP, DOWN, LEFT, RIGHT, OVER };

//物体类
class GObject
{
protected: //保护型基本属性
	int mX; //物体的实际坐标位置
	int mY;
	TWARDS twCommand; //方向指令缓存
	POINT point; //物体在方格的中心坐标
	int dRow; //物体的逻辑横坐标
	int dArray; //逻辑纵坐标
	int speed; //速度
	TWARDS tw; //朝向
	int frame; //祯数

	//子程序
	bool Achive(); //判断物体是否到达逻辑坐标位置(即是否到达中方格中心)
	bool Collision(); //逻辑碰撞检测,将物体摆放到合理的位置
	int PtTransform(int k); //将实际坐标转换为逻辑坐标
	virtual void AchiveCtrl(); //物体到达逻辑坐标位置后更新物体在矩阵中的行列坐标位置

public:
	void SetPosition(int Row, int Array);  //设置实际坐标位置(传入逻辑坐标位置设置实际坐标位置)
	void DrawBlank(HDC& hdc); //在物体位置周围绘制大小为24x24的白框
	void virtual Draw(HDC& hdc) = 0;//绘制物体对象,虚函数
	static GMap* pStage; //指向地图类的指针,设置为静态,使所有自类对象都能够使用相同的地图

	GObject(int Row, int Array) //构造函数
	{
		frame = 1; //初始化设置帧数为1,用于刷新大嘴的张合,像是在吃东西一样
		pStage = NULL;
		this->dRow = Row;
		this->dArray = Array;
		this->point.y = dRow * pStage->LD + pStage->LD / 2;
		this->point.x = dArray * pStage->LD + pStage->LD / 2;
		this->mX = point.x;
		this->mY = point.y;
	}

	void virtual action() = 0; //数据变更的表现,这是一个纯虚函数,能够阻止物体类的实例化,敌我双方移动的模板
	int GetRow(); //获取大嘴所在的行、列信息
	int GetArray();
};

//玩家控制的对象:大嘴
class PacMan :public GObject
{
protected:
	virtual void AchiveCtrl(); //重写虚函数,若没有撞墙则吃掉豆子

public:
	POINT GetPos(); //得到自身的位置
	TWARDS GetTw(); //得到自身的朝向
	bool Win(); //赢得胜利
	void Draw(HDC& hdc); //刷新大嘴,动态的哦
	void SetTwCommand(TWARDS command); //设置自身的朝向
	void Over(); //游戏失败结束
	PacMan(int x, int y) :GObject(x, y) //构造函数,初始化速度与朝向
	{
		this->speed = PLAYERSPEED;
		twCommand = tw = LEFT;
	}

	void action(); //碰撞检测
};

//追捕大嘴的敌人
class Enermy :public GObject
{
protected:
	void Catch(); //判断是否抓住大嘴
	void virtual MakeDecision(bool b) = 0; //实现改变方向的函数
	COLORREF color; //敌人的颜色

public:
	static PacMan* player; //声明一个pacman类的指针,设置为静态变量,必须初始化,在类的外部将它初始化为空
	void virtual  Draw(HDC& hdc); //用来画敌人的模板,虚函数
	Enermy(int x, int y) :GObject(x, y) //构造函数,初始化朝向和速度
	{
		this->speed = ENERMYSPEED;
		tw = LEFT;
		twCommand = UP;
	}

	void virtual action(); //数据变更的表现,虚函数
};

class RedOne :public Enermy  //普通敌人,继承Enermy
{
protected:
	void virtual MakeDecision(bool b);

public:
	void Draw(HDC& hdc);
	RedOne(int x, int y) :Enermy(x, y)
	{
		color = RGB(255, 0, 0);
	}
};

class BlueOne :public RedOne //守卫者,继承RedOne
{
protected:
	void virtual MakeDecision(bool b);

public:
	void Draw(HDC& hdc);
	BlueOne(int x, int y) :RedOne(x, y)
	{
		color = RGB(0, 0, 255);
	}

};

class YellowOne :public RedOne //扰乱者,继承RedOne
{
protected:
	void virtual MakeDecision(bool b);

public:
	void Draw(HDC& hdc);
	YellowOne(int x, int y) :RedOne(x, y)
	{
		color = RGB(200, 200, 100);
	}
};

源文件:

(一)、地图类实现文件,设置了障碍物尺寸实际大小为36,豆子半径为3(注意这两个值是实际大小),实现:初始化敌我双方出现位置没有豆子出现,绘制墙壁,绘制豆子,重写析构函数,初始化二维矩阵,初始化自身的成员矩阵。

GMap.cpp

#include "stdafx.h"
#include "GMap.h"

int GMap::LD = 36;
int GMap::PD = 3;

//敌我双方出现位置没有豆子出现
void GMap::InitOP()
{
	peaMapData[E_ROW][E_ARRAY] = false;
	peaMapData[P_ROW][P_ARRAY] = false;
}

void GMap::DrawMap(HDC& memDC)
{
	for (int i = 0; i<MAPLENTH; i++)
	{
		for (int j = 0; j<MAPLENTH; j++)
		{
			//绘制墙壁
			if (!mapData[i][j])
			{
				RECT rect;
				rect.left = j*LD;
				rect.top = i*LD;
				rect.right = (j + 1)*LD;
				rect.bottom = (i + 1)*LD;
				FillRect(memDC, &rect, CreateSolidBrush(color));
			}
		}
	}
}

void GMap::DrawPeas(HDC& hdc)
{
    //绘制豆子
	for (int i = 0; i<MAPLENTH; i++)
	{
		for (int j = 0; j<MAPLENTH; j++)
		{
			if (peaMapData[i][j])
			{
				Ellipse(hdc, (LD / 2 - PD) + j*LD, (LD / 2 - PD) + i*LD, (LD / 2 + PD) + j*LD, (LD / 2 + PD) + i*LD);
			}
		}
	}
}

GMap::~GMap()
{

}

//Stage_1成员定义:
#define A true //定义
#define B false
bool Stage_1::initData[MAPLENTH][MAPLENTH] =
{
	B, B, B, B, B, B, B, B, B, A, B, B, B, B, B, B, B, B, B,//0
	B, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, B,//1
	B, A, A, B, A, A, B, B, B, A, B, B, B, A, A, B, A, A, B,//2
	B, A, B, B, A, A, A, A, A, A, A, A, A, A, A, B, B, A, B,//3
	B, A, B, A, A, A, B, B, B, A, B, B, B, A, A, A, B, A, B,//4
	B, A, B, A, A, A, A, A, A, A, A, A, A, A, A, A, B, A, B,//5
	B, A, A, A, A, A, B, B, A, A, A, B, B, A, A, A, A, A, B,//6
	B, A, B, A, A, A, A, A, A, A, A, A, A, A, A, A, B, A, B,//7
	B, A, B, A, A, A, A, A, B, A, B, A, A, A, A, A, B, A, B,//8
	A, A, A, A, A, A, A, A, B, B, B, A, A, A, A, A, A, A, A,//9
	B, A, B, A, A, A, A, A, A, A, A, A, A, A, A, A, B, A, B,//10
	B, A, B, A, A, B, A, A, A, A, A, A, A, B, A, A, B, A, B,//11
	B, A, B, A, B, B, B, A, A, A, A, A, B, B, B, A, B, A, B,//12
	B, A, A, A, A, B, A, A, A, A, A, A, A, B, A, A, A, A, B,//13
	B, A, B, B, A, A, A, A, A, A, A, A, A, A, A, B, B, A, B,//14
	B, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, B,//15
	B, A, A, A, A, B, B, B, A, B, A, B, B, B, A, A, A, A, B,//16
	B, A, A, A, A, B, A, A, A, A, A, A, A, B, A, A, A, A, B,//17								 
	B, B, B, B, B, B, B, B, B, A, B, B, B, B, B, B, B, B, B,//18
};

#undef A //取消定义
#undef B

Stage_1::Stage_1()
{
	color = RGB(140, 240, 240);
	for (int i = 0; i < MAPLENTH; i++)
	{
		for (int j = 0; j < MAPLENTH; j++)
		{
			this->mapData[i][j] = this->initData[i][j];
			this->peaMapData[i][j] = this->initData[i][j];
		}
	}
	//敌我双方出现位置没有豆子出现
	this->InitOP();
}

//Stage_2成员定义
#define A true
#define B false
bool Stage_2::initData[MAPLENTH][MAPLENTH] =
{
	B, B, B, B, B, B, B, B, B, A, B, B, B, A, B, B, B, B, B,//0
	A, A, A, A, A, A, A, B, A, A, B, A, A, A, B, A, B, A, A,//1
	B, A, A, A, B, A, A, B, A, A, B, A, B, A, B, A, B, A, B,//2
	B, B, B, A, B, A, A, B, B, A, B, A, B, A, B, A, B, B, B,//3
	B, A, A, A, A, A, A, A, A, A, A, A, B, B, B, A, A, A, B,//4
	B, A, A, B, A, A, A, A, A, A, A, A, A, A, A, A, A, A, B,//5
	B, A, A, B, A, A, A, B, B, B, B, B, B, A, A, B, A, A, B,//6
	B, A, A, B, A, B, A, A, A, A, A, A, A, A, A, B, A, A, B,//7
	B, A, A, B, A, B, A, A, B, A, B, A, A, B, A, B, A, A, B,//8
	A, A, A, B, A, B, A, A, B, B, B, A, A, B, A, B, A, A, A,//9
	B, A, A, B, A, B, A, A, A, A, A, A, A, B, A, A, A, A, B,//10
	B, A, A, B, A, A, A, B, B, B, B, B, A, B, A, A, A, A, B,//11
	B, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, B,//12
	B, A, A, A, B, B, B, B, B, B, B, A, A, A, A, A, A, A, B,//13
	B, A, A, A, A, A, A, A, A, A, A, A, A, B, A, A, A, A, B,//14
	B, B, B, B, B, A, A, A, A, B, B, B, A, B, A, A, A, A, B,//15
	B, A, A, A, B, B, B, A, A, A, A, B, A, B, B, B, A, A, B,//16
	A, A, A, A, B, A, A, A, A, A, A, B, A, A, A, B, A, A, A,//17								 
	B, B, B, B, B, B, B, B, B, A, B, B, B, A, B, B, B, B, B,//18
};

#undef A
#undef B

Stage_2::Stage_2()
{
	color = RGB(240, 140, 140);
	for (int i = 0; i<MAPLENTH; i++)
	{
		for (int j = 0; j<MAPLENTH; j++)
		{
			this->mapData[i][j] = this->initData[i][j];
			this->peaMapData[i][j] = this->initData[i][j];
		}
	}
	//敌我双方出现位置没有豆子出现
	this->InitOP();
}

//Stage_3成员定义
#define A true
#define B false
bool Stage_3::initData[MAPLENTH][MAPLENTH] =
{
	B, B, B, B, B, B, B, B, B, A, B, B, B, B, B, B, B, B, B,//0
	A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A,//1
	B, A, A, B, A, A, B, B, B, B, B, B, B, A, A, A, B, A, B,//2
	B, A, B, B, A, A, A, A, A, A, A, A, B, A, A, A, B, A, B,//3
	B, A, B, A, A, A, B, B, B, B, B, B, B, A, A, A, B, A, B,//4
	B, A, B, A, B, B, B, A, A, A, A, A, B, B, B, A, B, A, B,//5
	B, A, A, A, B, A, B, A, A, A, A, A, A, A, A, A, B, A, B,//6
	B, A, B, A, B, A, A, A, A, A, A, A, A, B, A, A, B, A, B,//7
	B, A, B, A, B, B, A, A, B, A, B, A, A, B, A, A, B, A, B,//8
	B, A, A, A, A, B, A, A, B, B, B, A, A, B, A, A, B, A, B,//9
	B, A, B, A, A, B, A, A, A, A, A, A, A, B, A, A, A, A, B,//10
	B, A, B, A, A, B, A, A, A, A, A, A, B, B, B, A, B, A, B,//11
	B, A, B, A, A, B, A, B, B, B, B, B, B, A, B, A, B, A, B,//12
	B, A, B, A, A, B, A, A, A, A, A, A, A, A, B, A, B, A, B,//13
	B, A, B, B, A, B, B, B, B, B, B, A, B, A, B, A, B, A, B,//14
	B, A, A, A, A, B, A, A, A, A, A, A, B, A, B, A, B, A, B,//15
	B, B, B, B, B, B, A, A, B, B, B, A, B, A, B, A, B, A, B,//16
	A, A, A, A, A, A, A, A, B, A, A, A, A, A, B, A, A, A, A,//17								 
	B, B, B, B, B, B, B, B, B, A, B, B, B, B, B, B, B, B, B,//18
};

#undef A
#undef B

Stage_3::Stage_3()
{
	color = RGB(100, 300, 100);
	for (int i = 0; i<MAPLENTH; i++)
	{
		for (int j = 0; j<MAPLENTH; j++)
		{
			this->mapData[i][j] = this->initData[i][j];
			this->peaMapData[i][j] = this->initData[i][j];
		}
	}
	//敌我双方出现位置没有豆子出现
	this->InitOP();
}

(二)、物体类的实现文件,实现接口包含有:初始化地图类指针变量,获取物体行、列位置,判断物体是否到达逻辑坐标位置、碰撞检测、将实际坐标转换为逻辑坐标、判断物体到达逻辑坐标后更新物体在矩阵中的行列坐标等、绘制大嘴,绘制敌人,初始化在Enermy内部实现的pacman类的指针,抓捕函数实现,敌人的运动实现,即信息变更实现,普通敌人、守卫者、扰乱者的不同移动方式等等,blablabla一大堆,在代码中我认为需要注释的地方全部都注释了,相信所有读者了解面向对象思想的都应该看得懂。

#include "stdafx.h"
#include "GObject.h"

//GOject成员定义:
GMap* GObject::pStage = NULL;

int GObject::GetRow()
{
	return dRow;
}

int GObject::GetArray()
{
	return dArray;
}

int GObject::PtTransform(int k)
{
	return (k - (pStage->LD) / 2) / pStage->LD;
}

bool GObject::Achive()
{
	int n = (point.x - pStage->LD / 2) % pStage->LD;
	int k = (point.y - pStage->LD / 2) % pStage->LD;
	bool l = (n == 0 && k == 0);
	return l;
}

void GObject::AchiveCtrl()
{
	if (Achive())
	{
		dArray = PtTransform(point.x);//更新列
		dRow = PtTransform(point.y);//更新行
	}
}

void GObject::DrawBlank(HDC& hdc)
{
	RECT rect;
	rect.top = mY - RD;
	rect.left = mX - RD;
	rect.right = mX + RD;
	rect.bottom = mY + RD;
	FillRect(hdc, &rect, ::CreateSolidBrush(RGB(255, 255, 255)));
}

void GObject::SetPosition(int Row, int Array)
{
	dRow = Row;
	dArray = Array;
	this->point.y = dRow*pStage->LD + pStage->LD / 2;
	this->point.x = dArray*pStage->LD + pStage->LD / 2;
}

bool GObject::Collision()
{
	bool b = false;
	AchiveCtrl();//更新行、列的数据,如果是大嘴,则会执行PacMan重写的AchiveCtrl函数消除豆子
	//判断指令的有效性
	if (dArray < 0 || dRow < 0 || dArray > MAPLENTH - 1 || dRow > MAPLENTH - 1)
	{
		b = true;
	}
	else if (Achive())
	{
		switch (twCommand) //判断物体行进的方向
		{
		case LEFT:
			if (dArray > 0 && !pStage->mapData[dRow][dArray - 1]) //判断下一个格子是否能够通行
			{
				b = true; //指令无效
			}
			break;
			//以下方向的判断原理相同
		case RIGHT:
			if (dArray < MAPLENTH - 1 && !pStage->mapData[dRow][dArray + 1])
			{
				b = true;
			}
			break;
		case UP:
			if (dRow > 0 && !pStage->mapData[dRow - 1][dArray])
			{
				b = true;
			}
			break;
		case DOWN:
			if (dRow < MAPLENTH - 1 && !pStage->mapData[dRow + 1][dArray])
			{
				b = true;
			}
			break;
		}
		
		if (!b)
		{
			tw = twCommand; //没撞墙,指令成功
		}
	}
	
	//依照真实的方向位移
	mX = point.x; //得到实际位置坐标
	mY = point.y;
	int MAX = pStage->LD * MAPLENTH + pStage->LD / 2; //四个通口值
	int MIN = pStage->LD / 2; 四个通口值
	switch (tw) //判断行进的方向
	{
	case LEFT:
		if (dArray > 0 && !pStage->mapData[dRow][dArray - 1]) //判断下一个格子是否能够通行
		{
			b = true;
			break;//"撞墙了"
		}
		point.x -= speed;
		if (point.x<MIN)
		{
			point.x = MAX; //越过屏幕到另一端出现
		}
		break;
		
		//以下方向的判断原理相同
	case RIGHT:
		if (dArray<MAPLENTH - 1 && !pStage->mapData[dRow][dArray + 1])
		{
			b = true;
			break;//"撞墙了"
		}
		point.x += speed;
		if (point.x>MAX)
		{
			point.x = MIN;
		}
		break;
		
	case UP:
		if (dRow>0 && !pStage->mapData[dRow - 1][dArray])
		{
			b = true;
			break;//"撞墙了"
		}
		point.y -= speed;
		if (point.y<MIN)
		{
			point.y = MAX;
		}
		break;
		
	case DOWN:
		if (dRow<MAPLENTH - 1 && !pStage->mapData[dRow + 1][dArray])
		{
			b = true;
			break;//"撞墙了"
		}
		point.y += speed;
		if (point.y>MAX)
		{
			point.y = MIN;
		}
		break;
	}
	return b;
}

//PacMan成员定义:
void PacMan::AchiveCtrl()
{
	GObject::AchiveCtrl();
	if (Achive())
	{
		if (dRow >= 0 && dRow<MAPLENTH&&dArray >= 0 && dArray<MAPLENTH)//防止数组越界
		{
			if (pStage->peaMapData[dRow][dArray])
			{
				pStage->peaMapData[dRow][dArray] = false;
			}
		}
	}
}

void PacMan::action()
{
	Collision();
}

void PacMan::SetTwCommand(TWARDS command)
{
	twCommand = command;
}

bool PacMan::Win()
{
	for (int i = 0; i <= MAPLENTH; i++)
	{
		for (int j = 0; j <= MAPLENTH; j++)
		{
			if (pStage->peaMapData[i][j] == true)
			{
				return false; //存在任意一个豆子,没取得胜利
			}
		}
	}
	return true;//没有豆子,胜利
}

POINT PacMan::GetPos()
{
	return point;
}

void PacMan::Over()
{
	tw = OVER;
}

TWARDS PacMan::GetTw()
{
	return tw;
}

void PacMan::Draw(HDC& memDC) //绘制大嘴
{
	if (tw == OVER) //被抓住,游戏结束
	{

	}
	else if (frame % 2 == 0) //第4祯动画与第2祯动画
	{
		int x1 = 0, x2 = 0, y1 = 0, y2 = 0;
		int offsetX = DISTANCE / 2 + D_OFFSET; //设置弧弦交点:6
		int offsetY = DISTANCE / 2 + D_OFFSET; 
		switch (tw)
		{
		case UP:
			x1 = point.x - offsetX;
			x2 = point.x + offsetX;
			y2 = y1 = point.y - offsetY;
			break;
			
		case DOWN:
			x1 = point.x + offsetX;
			x2 = point.x - offsetX;
			y2 = y1 = point.y + offsetY;
			break;
			
		case LEFT:
			x2 = x1 = point.x - offsetX;
			y1 = point.y + offsetY;
			y2 = point.y - offsetY;
			break;
			
		case RIGHT:
			x2 = x1 = point.x + offsetX;
			y1 = point.y - offsetY;
			y2 = point.y + offsetY;
			break;
		}
		
		Arc(memDC, point.x - DISTANCE, point.y - DISTANCE,
			point.x + DISTANCE, point.y + DISTANCE,
			x1, y1,
			x2, y2); //画圆弧,内切圆,以x1y1,x2y2逆时针画
			
		MoveToEx(memDC, x1, y1, NULL); //将绘图位置移动到点x1y1
		LineTo(memDC, point.x, point.y); //画x1y1到实际位置坐标直线
		LineTo(memDC, x2, y2); //再到x2y2
	}
	else if (frame % 3 == 0) //第3帧动画
	{
		Ellipse(memDC, point.x - DISTANCE, point.y - DISTANCE,
			point.x + DISTANCE, point.y + DISTANCE);
	}
	else {
		int x1 = 0, x2 = 0, y1 = 0, y2 = 0;
		switch (tw)
		{
		case UP:
			x1 = point.x - DISTANCE;
			x2 = point.x + DISTANCE;
			y2 = y1 = point.y;
			break;
			
		case DOWN:
			x1 = point.x + DISTANCE;
			x2 = point.x - DISTANCE;
			y2 = y1 = point.y;
			break;
			
		case LEFT:
			x2 = x1 = point.x;
			y1 = point.y + DISTANCE;
			y2 = point.y - DISTANCE;
			break;
			
		case RIGHT:
			x2 = x1 = point.x;
			y1 = point.y - DISTANCE;
			y2 = point.y + DISTANCE;
			break;
		}

		Arc(memDC, point.x - DISTANCE, point.y - DISTANCE,
			point.x + DISTANCE, point.y + DISTANCE,
			x1, y1,
			x2, y2);
		MoveToEx(memDC, x1, y1, NULL);
		LineTo(memDC, point.x, point.y);
		LineTo(memDC, x2, y2);
	}

	frame++; //绘制下一祯
}

//Enermy成员定义:
PacMan* Enermy::player = NULL; //在Enermy内部实现一个pacman类的指针,由于是静态变量,所以必须初始化

void Enermy::Catch()
{
	int DX = point.x - player->GetPos().x;
	int DY = point.y - player->GetPos().y;
	if ((-RD < DX && DX < RD)  &&  (-RD < DY && DY < RD))
	{
		player->Over(); //被抓住,游戏失败结束
	}
}

void Enermy::Draw(HDC& hdc) //画敌人
{
	HPEN pen = ::CreatePen(0, 0, color); //创建一个画笔实例
	HPEN oldPen = (HPEN)SelectObject(hdc, pen); 
	Arc(hdc, point.x - DISTANCE, point.y - DISTANCE,
		point.x + DISTANCE, point.y + DISTANCE,
		point.x + DISTANCE, point.y,
		point.x - DISTANCE, point.y); //画了半圆型的头
		
	int const LEGLENTH = (DISTANCE) / (LEGCOUNTS);
	
	//根据祯数来绘制身体和“腿部”
	if (frame % 2 == 0)
	{
		MoveToEx(hdc, point.x - DISTANCE, point.y, NULL); //矩形的身子
		LineTo(hdc, point.x - DISTANCE, point.y + DISTANCE - LEGLENTH);
		MoveToEx(hdc, point.x + DISTANCE, point.y, NULL);
		LineTo(hdc, point.x + DISTANCE, point.y + DISTANCE - LEGLENTH);
		
		//从左往右绘制“腿部”
		for (int i = 0; i<LEGCOUNTS; i++) 
		{
			Arc(hdc, point.x - DISTANCE + i * 2 * LEGLENTH, point.y + DISTANCE - 2 * LEGLENTH,
				point.x - DISTANCE + (i + 1) * 2 * LEGLENTH, point.y + DISTANCE,
				point.x - DISTANCE + i * 2 * LEGLENTH, point.y + DISTANCE - LEGLENTH,
				point.x - DISTANCE + (i + 1) * 2 * LEGLENTH, point.y + DISTANCE - LEGLENTH
				);
		}
	}
	else{
		MoveToEx(hdc, point.x - DISTANCE, point.y, NULL); //绘制身体
		LineTo(hdc, point.x - DISTANCE, point.y + DISTANCE);
		MoveToEx(hdc, point.x + DISTANCE, point.y, NULL);
		LineTo(hdc, point.x + DISTANCE, point.y + DISTANCE);
		
		//从左往右绘制“腿部”
		MoveToEx(hdc, point.x - DISTANCE, point.y + DISTANCE, NULL);
		LineTo(hdc, point.x - DISTANCE + LEGLENTH, point.y + DISTANCE - LEGLENTH);
		for (int i = 0; i<LEGCOUNTS - 1; i++)
		{
			Arc(hdc, point.x - DISTANCE + (1 + i * 2)*LEGLENTH, point.y + DISTANCE - 2 * LEGLENTH,
				point.x - DISTANCE + (3 + i * 2)*LEGLENTH, point.y + DISTANCE,
				point.x - DISTANCE + (1 + i * 2)*LEGLENTH, point.y + DISTANCE - LEGLENTH,
				point.x - DISTANCE + (3 + i * 2)*LEGLENTH, point.y + DISTANCE - LEGLENTH
				);
		}
		MoveToEx(hdc, point.x + DISTANCE, point.y + DISTANCE, NULL);
		LineTo(hdc, point.x + DISTANCE - LEGLENTH, point.y + DISTANCE - LEGLENTH);
	}
	
	//根据方向绘制眼睛
	int R = DISTANCE / 5; //眼睛的半径
	switch (tw) //依据行进的朝向画不同朝向的眼睛
	{
	case UP: 
		Ellipse(hdc, point.x - 2 * R, point.y - 2 * R,
			point.x, point.y);
		Ellipse(hdc, point.x, point.y - 2 * R,
			point.x + 2 * R, point.y);
		break;
		
	case DOWN:
		Ellipse(hdc, point.x - 2 * R, point.y, point.x, point.y + 2 * R);
		Ellipse(hdc, point.x, point.y, point.x + 2 * R, point.y + 2 * R);
		break;
		
	case LEFT:
		Ellipse(hdc, point.x - 3 * R, point.y - R,
			point.x - R, point.y + R);
		Ellipse(hdc, point.x - R, point.y - R,
			point.x + R, point.y + R);
		break;
		
	case RIGHT:
		Ellipse(hdc, point.x - R, point.y - R,
			point.x + R, point.y + R);
		Ellipse(hdc, point.x + R, point.y - R,
			point.x + 3 * R, point.y + R);
		break;
	}

	frame++; //准备绘制下一祯
	SelectObject(hdc, oldPen);
	DeleteObject(pen); //释放画笔资源
	return;
}

void Enermy::action() //移动
{
	bool b = Collision(); //碰撞检测
	MakeDecision(b); //有障碍物则改变移动方向
	Catch(); //捕抓检测
}

//RedOne成员,普通敌人
void RedOne::Draw(HDC& hdc)
{
	Enermy::Draw(hdc);
}

void RedOne::MakeDecision(bool b)
{
	int i = rand();
	if (b) //撞到墙壁,改变方向
	{
		//逆时针转向
		if (i % 4 == 0)
		{
			tw == UP ? twCommand = LEFT : twCommand = UP;
		}
		else if (i % 3 == 0)
		{
			tw == DOWN ? twCommand = RIGHT : twCommand = DOWN;
		}
		else if (i % 2 == 0)
		{
			tw == RIGHT ? twCommand = UP : twCommand = RIGHT;
		}
		else
		{
			tw == LEFT ? twCommand = DOWN : twCommand = LEFT;
		}
		return;
	}

	if (i % 4 == 0) //没有碰到墙壁
	{
		twCommand != UP ? tw == DOWN : twCommand == UP;
	}
	else if (i % 3 == 0)
	{
		tw != DOWN ? twCommand = UP : twCommand = DOWN;
	}
	else if (i % 2 == 0)
	{
		tw != RIGHT ? twCommand = LEFT : twCommand = RIGHT;
	}
	else
	{
		tw != LEFT ? twCommand = RIGHT : twCommand = LEFT;
	}
}

//BlueOne成员定义 ,保卫者
void BlueOne::Draw(HDC& hdc)
{
	Enermy::Draw(hdc);
}

void BlueOne::MakeDecision(bool b)
{
	const int DR = this->dRow - player->GetRow(); //获取与大嘴的逻辑距离
	const int DA = this->dArray - player->GetArray();
	
	if (!b && DR == 0)
	{
		if (DA <= BLUE_ALERT && DA > 0) //玩家在左侧边警戒范围s
		{
			twCommand = LEFT;	//向左移动
			return;
		}
		if (DA < 0 && DA >= -BLUE_ALERT) //右侧警戒范围
		{
			twCommand = RIGHT; //向右移动
			return;
		}
	}
	
	if (!b && DA == 0)
	{
		if (DR <= BLUE_ALERT&&DR>0) //下方警戒范围
		{
			twCommand = UP;
			return;
		}
		if (DR < 0 && DR >= -BLUE_ALERT) //上方警戒范围
		{
			twCommand = DOWN;
			return;
		}
	}

	RedOne::MakeDecision(b); //不在追踪模式时RED行为相同
}

//YellowOne成员定义,扰乱者
void YellowOne::Draw(HDC& hdc)
{
	Enermy::Draw(hdc);
}

void YellowOne::MakeDecision(bool b)
{
	const int DR = this->dRow - player->GetRow();
	const int DA = this->dArray - player->GetArray();
	
	if (!b)
	{
		if (DR * DR > DA * DA)
		{
			if (DA>0) //玩家在左侧边警戒范围
			{
				twCommand = LEFT;	//向左移动
				return;
			}
			else if (DA<0) //右侧警戒范围
			{
				twCommand = RIGHT; //向右移动
				return;
			}
		}
		else
		{
			if (DR > 0) //下方警戒范围
			{
				twCommand = UP;
				return;
			}
			if (DR < 0) //上方警戒范围
			{
				twCommand = DOWN;
				return;
			}
		}
	}
	RedOne::MakeDecision(b); //不在追踪模式时RED行为相同
}

(三)、pacman的主程序实现文件,主要难点是 需要对 windows 的消息循环工作原理有一定的了解,讲起来相对较复杂,读者如果看不懂,可以看看创建窗口应用程序并未添加任何文件时,参考相关windows窗口应用程序的讲解看一看一个空白窗口的工作原理,对后面的编程理解有一定的帮助。

// pacman.cpp : 定义应用程序的入口点。

#include "stdafx.h"
#include "pacman.h"
#include "GObject.h"

#define WLENTH 700 //窗口应用程序的大小
#define WHIGHT 740
#define STAGE_COUNT 3 //关卡总的数量

#define MAX_LOADSTRING 100

// 全局变量: 
HINSTANCE hInst;  //当前实例
TCHAR szTitle[MAX_LOADSTRING]; //标题栏文本
TCHAR szWindowClass[MAX_LOADSTRING]; //主窗口类名

//游戏物体
PacMan* p; //大嘴
GObject* e1; //敌人1234
GObject* e2;
GObject* e3;
GObject* e4;

//在程序中使用了动态分配,需要使用堆内存回收,定义一个释放动态内存函数模板,传入指针变量即可实现对它所指向的堆内存回收
template<class T>
void Realese(T t)
{
	if (t != NULL)
		delete t;
}

// 此代码模块中包含的函数的前向声明: 
ATOM MyRegisterClass(HINSTANCE hInstance); //注册是咧
BOOL InitInstance(HINSTANCE, int, HWND&); //这里改动原本函数,增加了hwnd来获得窗口的句柄
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);

void ResetGObjects();

//主程序入口
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPTSTR    lpCmdLine,
	_In_ int       nCmdShow) 
	//HINSTANCE代表的是程序的实例,第一个参数代表本程序的实例,第二个参数代表上一个程序的实例。
	//...它是系统用来管理程序而使用的标识。第三和第四个参数表示的是控制台参数的设置。
{
	UNREFERENCED_PARAMETER(hPrevInstance); //因为hprevInstance和lpCmdLine这两个参数几乎不会被用到,所以这里是使用一个
	UNREFERENCED_PARAMETER(lpCmdLine); //UNREFERENCED_PARA                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    METER的宏,用来忽略来自编译器的“未使用过的参数”警报

	// TODO:  在此放置代码。
	MSG msg; //MSG是一个结构体,它是windows消息的记录形式,而下面的HACCEL是窗口中的热键表。
	HACCEL hAccelTable; //窗口中的热键表

	int s_n = 0; //进行到的关卡数
	p = new PacMan(P_ROW, P_ARRAY);
	e1 = new RedOne(E_ROW, E_ARRAY);
	e2 = new RedOne(E_ROW, E_ARRAY);
	e3 = new BlueOne(E_ROW, E_ARRAY);
	e4 = new YellowOne(E_ROW, E_ARRAY);
	GMap* MapArray[STAGE_COUNT] = { new Stage_1(), new Stage_2(), new Stage_3() };
	GObject::pStage = MapArray[s_n]; //初始化为第一关地图
	Enermy::player = p;

	// 初始化全局字符串
	LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); //LoadString这个函数是将程序一开始定义的两个全局字符串变量szTitle、
	LoadString(hInstance, IDC_PACMAN, szWindowClass, MAX_LOADSTRING); //szWindowsClass,初始化为String Table中对应ID的字符串的值。
	MyRegisterClass(hInstance);//窗口类

	// 执行应用程序初始化:
	HWND hWnd;
	if (!InitInstance(hInstance, nCmdShow, hWnd))
	{
		return FALSE;
	}

	hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PACMAN));
	DWORD t = 0;

	//主消息循环:
	while (p->GetTw() != OVER && s_n < 3)
	{
		if (p->Win()) //通关提示
		{
			HDC hdc = GetDC(hWnd);//获取窗口句柄
			s_n++; //下一关
			ResetGObjects(); //重新注册大嘴和敌人1234
			if (s_n <3)
			{
				MessageBoxA(hWnd, "恭喜您过关", "吃豆子提示", MB_OK);
				GObject::pStage = MapArray[s_n];//下一张地图
				RECT screenRect;//窗口的位置
				screenRect.top = 0;
				screenRect.left = 0;
				screenRect.right = WLENTH;
				screenRect.bottom = WHIGHT;
				::FillRect(hdc, &screenRect, CreateSolidBrush(RGB(255, 255, 255)));
				GObject::pStage->DrawMap(hdc);
			}
			continue;
		}

		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) // 获得消息队列
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		if (GetAsyncKeyState(VK_DOWN) & 0x8000) //获得键盘状态的API
		{
			p->SetTwCommand(DOWN);
		}
		if (GetAsyncKeyState(VK_LEFT) & 0x8000)
		{
			p->SetTwCommand(LEFT);
		}
		if (GetAsyncKeyState(VK_RIGHT) & 0x8000)
		{
			p->SetTwCommand(RIGHT);
		}
		if (GetAsyncKeyState(VK_UP) & 0x8000)
		{
			p->SetTwCommand(UP);
		}
		else
		{//每58毫秒游戏的数据和画面更新一次
			if (GetTickCount() - t > 58) //获取从开机到当前时刻机器与运行的毫秒数,在消息循环外使用一个无符号长整型变量t存储游戏计时
			{
				HDC hdc = GetDC(hWnd);
				e1->action();
				e2->action();
				e3->action();
				e4->action();
				p->action();

				GObject::pStage->DrawPeas(hdc);
				e1->DrawBlank(hdc);
				e2->DrawBlank(hdc);
				e3->DrawBlank(hdc);
				e4->DrawBlank(hdc);

				p->DrawBlank(hdc);
				e1->Draw(hdc);
				e2->Draw(hdc);
				e3->Draw(hdc);
				e4->Draw(hdc);

				p->Draw(hdc);
				DeleteDC(hdc);
				t = GetTickCount();
			}
		}
	}

	Realese(e1);//释放内存
	Realese(e2);
	Realese(e3);
	Realese(e4);

	for (int i = 0; i < STAGE_COUNT; i++)
	{
		Realese(MapArray[i]);
	}

	if (p->GetTw() == OVER)
	{
		MessageBoxA(hWnd, "出师未捷", "吃豆子提示", MB_OK);
	}
	else
	{
		MessageBoxA(hWnd, "恭喜您赢得了胜利", "吃豆子提示", MB_OK);
	}

	Realese(p);
	return (int)msg.wParam;
}

//
//  函数:  MyRegisterClass()
//
//  目的:  注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex; //WNDCLASSEX是一个结构体,它是我们使用的窗口类。
	wcex.cbSize = sizeof(WNDCLASSEX); //窗口类结构体所占大小。
	wcex.style = CS_HREDRAW | CS_VREDRAW; //窗口的样式,CS_HREDRAW | CS_VREDRAW代表了窗口在水平和竖直方向运动时,窗口的会面重绘。
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance; //本应用程序的实例
	wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_PACMAN)); //程序图标
	wcex.hCursor = LoadCursor(NULL, IDC_ARROW); //鼠标图标
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //背景色
	wcex.lpszMenuName = MAKEINTRESOURCE(IDC_PACMAN); //菜单的名称
	wcex.lpszClassName = szWindowClass; //窗口类的名称
	wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); //窗口小图标

	return RegisterClassEx(&wcex); //注册窗口类
}

//
//   函数: InitInstance(HINSTANCE, int)
//
//   目的: 保存实例句柄并创建主窗口
//
//   注释:
//
//        在此函数中,我们在全局变量中保存实例句柄并
//        创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow, HWND& hWnd)
{
	hInst = hInstance; // 将实例句柄存储在全局变量中
	hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
		0, 0, WLENTH, WHIGHT, NULL, NULL, hInstance, NULL);
	if (!hWnd)
	{
		return FALSE;
	}
	ShowWindow(hWnd, nCmdShow);
	UpdateWindow(hWnd);
	return TRUE;
}

//
//  函数:  WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目的:    处理主窗口的消息。
//
//  WM_COMMAND	- 处理应用程序菜单
//  WM_PAINT	- 绘制主窗口
//  WM_DESTROY	- 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	int wmId, wmEvent;
	PAINTSTRUCT ps;
	HDC hdc;

	switch (message)
	{
	case WM_COMMAND:
		wmId = LOWORD(wParam);
		wmEvent = HIWORD(wParam);
		// 分析菜单选择: 
		switch (wmId)
		{
		case IDM_ABOUT:
			DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
			break;
		case IDM_EXIT:
			DestroyWindow(hWnd);
			break;
		default:
			return DefWindowProc(hWnd, message, wParam, lParam); //windows API函数,默认的窗口过程函数
		}
		break;
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		// TODO:  在此添加任意绘图代码...
		EndPaint(hWnd, &ps);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	UNREFERENCED_PARAMETER(lParam);
	switch (message)
	{
	case WM_INITDIALOG:
		return (INT_PTR)TRUE;

	case WM_COMMAND:
		if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
		{
			EndDialog(hDlg, LOWORD(wParam));
			return (INT_PTR)TRUE;
		}
		break;
	}
	return (INT_PTR)FALSE;
}

void ResetGObjects() //玩家获得上一张图的胜利后,分为两种情况:进入下一关或者游戏结束
{
	p->SetPosition(P_ROW, P_ARRAY);
	e1->SetPosition(E_ROW, E_ARRAY);
	e2->SetPosition(E_ROW, E_ARRAY);
	e3->SetPosition(E_ROW, E_ARRAY);
	e4->SetPosition(E_ROW, E_ARRAY);
}

游戏结果展示:

(一)、gif动态图

Java吃豆子游戏 一个吃豆子的游戏_初始化


(二)、运行结果说明:

简化版 “吃豆子游戏—pacman” 实现一般,游戏运行时经常会出现bug,但有时又能正常运行成功。本项目项目作为C++较难一点的入门项目,难度适中,能够使读者对面向对象编程思想,类的各种性质:封装、多态、继承,虚函数,构造函数,析构函数,内存控制等的知识点更加熟悉。