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