bridgeL

卫星发射实验场

90%还原南宫梦吃豆人

MyPacman 作者 bridgeL

开发软件:VS2019

使用的图形库 easyx, 版本2020-1-9
使用的图片素材 /pacman2/res 来自于网络
项目github地址: https://github.com/bridgeL/pacman2

本文对应版本v1.4.7

特性:迷宫;四只怪物,A*算法,差异化追逐;大力丸,用于反杀怪物;怪物会感受恐惧,四散躲避

游戏运行画面展示

核心框架概述

类图/设计流图

类关系

CMover 派生

CMonster 
CPacman 

CFlash 容器

CFlashGroup 

CPoint 组合

CRect 

多线程

游戏一开始时从资源文件载入图片素材,初始化各个对象,并设置两个独立线程:按键输入线程、定时器计数线程

按键输入线程

DWORD WINAPI keyboard_thread(PVOID)
{
	while (1)
	{
		int a = _getch();
		if (a == 224)
		{
			a = _getch();
			switch (a)
			{
			case 72: key = 'w'; break;
			case 80: key = 's'; break;
			case 75: key = 'a'; break;
			case 77: key = 'd'; break;
			}
		}
		else if (a == 13)
		{
			key = ' ';
		}
		else
		{
			key = a;
		}
	}

	return 0L;
}

小键盘的方向键输入为两个char字符,且第一个字符值为224,需要特殊处理

key为全局变量,用于存放捕获到的键值,在主循环的时候使用key时需要注意清空key,防止一key多用

定时器计数线程

void HpSleep(int ms)
{
	static clock_t oldclock = clock();		// 静态变量,记录上一次 tick

	oldclock += ms * CLOCKS_PER_SEC / 1000;	// 更新 tick

	if (clock() > oldclock)					// 如果已经超时,无需延时
		oldclock = clock();
	else
		while (clock() < oldclock)			// 延时
			Sleep(1);						// 释放 CPU 控制权,降低 CPU 占用率
}

DWORD WINAPI time_thread(PVOID param)
{
	int fps = (int)param;
	int ms = 1000 / fps;
	while (1)
	{
		HpSleep(ms);
		update_event = 1;
		cnt1++;
	}

	return 0L;
}

控制帧率,update_event 为全局变量,在主循环使用的时候需要注意清空update_event ,防止一update_event 多用

页 Page

游戏被划分为三页,游戏菜单页,游戏进行页,游戏结束页(这里暂时未设计class来封装页,因此页仅仅是由三个函数构成的一个概念)。

主循环根据页码(全局变量),执行对应的页的函数,页函数有三类:

  • 初始化函数
  • 按键处理函数
  • 帧更新函数(可缺省)

初始化函数用于初始化该页必需的变量成员,并绘制一些图片,例如游戏底图、UI界面等不会在帧更新时频繁变动的图像

按键处理函数从按键输入缓存(全局变量)读取按键操作,并根据对应的页码修改页的变量成员,以及执行一些迫不得已的绘制操作(不建议)

帧更新函数主要用于刷新页的一些需要随时间变化的变量成员

页与页的切换通过专门的页切换函数实现,该函数负责修改页码(全局变量),并结束上一页,调用页初始化函数,开启下一页

游戏进行页 Gaming Page

游戏进行页主要负责四件事:

  • 清除元素旧图案
  • 更新元素位置、状态
  • 绘制元素新图案
  • FlushBatchDraw();(更新帧图像)

使用FlushBatchDraw()的好处是显而易见的:隐藏了绘制图片的中间环节的画面,保证游戏观感良好

移动者 CMover

CMover是游戏最主要的类,实现了迷宫中的受限移动,提供了绘制自身图像的API(动画序列、非静图,且可以根据方向设置对应的动画序列)

通过Move()(无检查前进),Go()(有检查前进,判断与墙的距离),Turn()(有检查转弯,并适当修正位置)实现了CMover在迷宫中的受限移动

通过自定义的CFlashGroup实现了对动画序列的封装,通过自定义的CAStar实现了对A*寻路算法的封装

动画序列 CFlash 及其容器 CFlashGroup

CFlash中存放了一组动画序列的图片,例如吃豆人向左移动的一套图(3张)

CFlashGroup存放了多个CFlash,例如存放吃豆人四个方向移动的动画

A*算法 CAStar

CAStar中存放了一个结点矩阵,通过地图信息初始化这一矩阵,随后进行A*算法寻路,为怪物提供追逐的方向

参考资料 https://blog.csdn.net/m0_37290785/article/details/93203624

对A star的实现做了一定的优化:

取消close_list,直接使用哈希表存储节点是否关闭,大大提高了A star的计算速度

在初始化节点网络时计算并存储各个节点的距离代价(H值),为后来计算总代价(F值)提供方便

核心代码展示

CMover的实现

//zh_CN.GBK
#pragma once
// 此文档主要定义了 mover类,以及其派生的 monster类

#include "define.h"
#include "flash.h"
#include "astar.h"

// 引用该库才能使用 TransparentBlt 函数
#pragma comment( lib, "MSIMG32.LIB")

class CMover
{
protected:
	IMAGE* p_background;		// 背景图片的指针
	IMAGE back;					// 为了加快clear的速度,设置back成员储存每次的背景图
	CFlashGroup faces;	// 各方向动画序列的容器

	CRect rect;					// 当前的位置

	CPoint burn;				// 出生地点

	int* map;					// 地图的指针

	double speed;
	double speed_d;

	int new_dir;				// 新的运动方向,加入此属性是为了改善游戏手感
	int dir;					// 运动方向


public:
	CMover() :p_background(NULL), map(NULL), speed(0), speed_d(0), new_dir(DIR_NONE), dir(DIR_NONE) 
	{
		faces = CFlashGroup(EAT_FLASH_TIME);
	}

	// 将初始化分解为一系列函数,请按照顺序执行
	void init_speed(double s)
	{
		// 设置速度
		speed = s;
	}

	void init_map(int* m)
	{
		// 设置地图地址
		map = m;
	}

	void init_rect(CRect r)
	{
		// 设置当前位置
		rect = r;
		burn = rect.site;
	}

	void init_img(IMAGE* p_back, IMAGE* p_face, int r, int c)
	{
		// 设置背景图指针
		p_background = p_back;

		// 根据图片生成四个方向的动画序列
		int h = rect.shape.x;
		int w = rect.shape.y;

		SetWorkingImage(p_face);

		for (int i = 0; i < r; i++)
		{
			CFlash flash;
			for (int j = 0; j < c; j++)
			{
				IMAGE face;
				int x = i * h;
				int y = j * w;
				getimage(&face, y, x, w, h);
				flash.Add(face);
			}
			faces.Add(flash);
		}

		SetWorkingImage(NULL);
	}

	void Reset()
	{
		rect.site = burn;
		dir = DIR_NONE;
		new_dir = DIR_NONE;
	}

	// 设置/读取 当前位置的左上角坐标
	CPoint GetSite() { return rect.site; }

	// 读取 当前位置的中心点坐标
	CPoint GetCenter()
	{
		CPoint center;
		center.x = rect.site.x + rect.shape.x / 2;
		center.y = rect.site.y + rect.shape.y / 2;
		return center;
	};

	void SetFlash(int d)
	{
		switch (d)
		{
		case DIR_DOWN:  faces.SetIdx(3);  break;
		case DIR_UP:	faces.SetIdx(2);  break;
		case DIR_RIGHT: faces.SetIdx(0);  break;
		case DIR_LEFT:	faces.SetIdx(1);  break;
		}
	}

	void Move(double ss)
	{
		int s = (int)ss;
		speed_d += (ss - s);
		if (speed_d >= 1)
		{
			s++;
			speed_d--;
		}

		switch (dir)
		{
		case DIR_DOWN:  rect.site.x += s; break;	// 下
		case DIR_RIGHT: rect.site.y += s; break;	// 右
		case DIR_UP:	rect.site.x -= s; break;	// 上
		case DIR_LEFT:	rect.site.y -= s; break;	// 左
		}
	}

	void SetNewDir(int d)
	{
		new_dir = d;
	}

	void GetNearCross(int& dx, int& dy, int& i, int& j)
	{
		CPoint c = GetCenter();

		// 距离最靠近的格点的距离
		dx = (c.x + BLOCK_SIZE / 2) % BLOCK_SIZE - BLOCK_SIZE / 2;
		dy = (c.y + BLOCK_SIZE / 2) % BLOCK_SIZE - BLOCK_SIZE / 2;

		int x = c.x - dx;
		int y = c.y - dy;

		i = x / BLOCK_SIZE;
		j = y / BLOCK_SIZE;
	}

	bool Turn()
	{
		// 转向判定
		// 我们要求吃豆人像火车一样,总是位于固定的轨道上,不能自由移动到随意的坐标,
		// 因此在转向的时候,需要进行严格的审查


		// 是否有转向,
			// 是否在转弯容限内,
				// 转弯后是否撞墙
					// 均通过则正确转弯,按新方向前进,并更新动画序列
				// 撞墙了,不许转弯,按旧方向前进
			// 不在转弯容县内,不许转弯,按旧方向前进
		// 没有转向,按旧方向前进

		if (new_dir == DIR_NONE)
		{
			dir = DIR_NONE;
			return 1;
		}
		else if (abs(new_dir) != abs(dir) )	// 是否有转向
		{
			// 转弯限制
			const int t = MOVE_TOLERENCE;	// 宽容门限

			int dx, dy, i, j;
			GetNearCross(dx, dy, i, j);

			bool turn_flag = 0;

			if ((abs(new_dir) == 2 && abs(dx) < t) || (abs(new_dir) == 1 && abs(dy) < t))
				turn_flag = 1;

			if (turn_flag)		// 是否在转弯容限内
			{
				// 下一个格点
				switch (new_dir)
				{
				case DIR_DOWN:  i++;  break;
				case DIR_UP:	i--;  break;
				case DIR_RIGHT: j++;  break;
				case DIR_LEFT:	j--;  break;
				}

				bool edge_flag = 0;
				if (j < 0 || j >= MAP_COLUMN)
					edge_flag = 1;

				if (!edge_flag)	// 是否在边界
				{
					bool wall_flag = 0;

					if (map[i * MAP_COLUMN + j] == 3)
						wall_flag = 1;

					if (!wall_flag)	// 转弯后是否撞墙
					{
						// 方向赋值,并修改动画

						dir = new_dir;	

						// 坐标修正
						rect.site.x -= dx;
						rect.site.y -= dy;

						return 1;
					}
					else
					{
						return 0;
					}

				}
				else	// 如果超过边界,不许转弯
				{
					return 0;
				}

			}
			else	// 转弯后撞墙,不许转弯
			{
				return 0;
			}

		}
		else	// 可能是掉头或者从DIR_NONE起步
		{
			dir = new_dir;
			return 1;
		}
	}

	bool Go()
	{
		// 计算离自己最近的节点,并知道自己与此节点的相对位置
		// 根据 移动方向 和 与节点的相对位置, 下一个节点的坐标
		// 计算自己与下一个节点的距离
		// 判断三种状态
			// 不会撞到/不是墙
			// 会撞到,但仍然有一定距离
			// 已经撞到了,没有距离

		int dx, dy, i, j;
		GetNearCross(dx, dy, i, j);

		switch (dir)
		{
		case DIR_DOWN:  if (dx >= 0) i++; break;	// 下
		case DIR_RIGHT: if (dy >= 0) j++; break;	// 右
		case DIR_UP:	if (dx <= 0) i--; break;	// 上
		case DIR_LEFT:	if (dy <= 0) j--; break;	// 左
		}

		// 如果超过边界,就不进行撞墙检查
		if (j < 0 || j >= MAP_COLUMN)
		{
			// 移动
			Move(speed);

			if (rect.site.y < 0)
				rect.site.y += GAME_WIDTH;
			else if (rect.site.y >= GAME_WIDTH)
				rect.site.y -= GAME_WIDTH;

			return 1;
		}


		int s = (int)speed;	// 移动距离

		if (map[i * MAP_COLUMN + j] == 3)	// 墙
		{
			const int t = BLOCK_SIZE - PERSON_SIZE / 2;	// 墙壁的厚度

			// 计算与墙的距离
			int instance = (int)speed;

			switch (dir)
			{
			case DIR_DOWN:  instance = (i * BLOCK_SIZE - t) - (rect.site.x + rect.shape.x);	break;
			case DIR_RIGHT: instance = (j * BLOCK_SIZE - t) - (rect.site.y + rect.shape.y); break;
			case DIR_UP:	instance = (rect.site.x) - (i * BLOCK_SIZE + t);				break;
			case DIR_LEFT:	instance = (rect.site.y) - (j * BLOCK_SIZE + t);				break;
			}

			
			if (instance == 0)
			{
				return 0;
			}

			s = instance;
		}

		// 移动
		Move(s);

		return 1;
	}

	void Update()
	{
		faces.Update();

		// 转向
		if (Turn())
			SetFlash(dir);

		// 前进
		Go();
	}

private:
	// 透明贴图函数
	// 参数:
	//		dstimg: 目标 IMAGE 对象指针。NULL 表示默认窗体
	//		x, y:	目标贴图位置
	//		srcimg: 源 IMAGE 对象指针。NULL 表示默认窗体
	//		transparentcolor: 透明色。srcimg 的该颜色并不会复制到 dstimg 上,从而实现透明贴图
	void transparentimage(IMAGE* dstimg, int x, int y, IMAGE* srcimg, UINT transparentcolor)
	{
		HDC dstDC = GetImageHDC(dstimg);
		HDC srcDC = GetImageHDC(srcimg);
		int w = srcimg->getwidth();
		int h = srcimg->getheight();

		// 使用 Windows GDI 函数实现透明位图
		TransparentBlt(dstDC, x, y, w, h, srcDC, 0, 0, w, h, transparentcolor);
	}

public:
	void Draw()
	{
		// 为了美观,
		// 绘图坐标draw 和 实际坐标site 之间存在固定偏差 bias = BLOCK_SIZE / 2
		// center = site + shape / 2
		// draw = center + bias - shape / 2 
		//      = site + bias
		transparentimage(NULL, rect.site.y + BLOCK_SIZE / 2, rect.site.x + BLOCK_SIZE / 2, &faces.GetImage(), BLACK);
		//putimage(rect.site.y + BLOCK_SIZE / 2, rect.site.x + BLOCK_SIZE / 2, &faces.GetImage());
	}

	void Clear()
	{
		SetWorkingImage(p_background);
		getimage(&back, rect.site.y + BLOCK_SIZE / 2, rect.site.x + BLOCK_SIZE / 2, rect.shape.y, rect.shape.x);

		SetWorkingImage(NULL);
		putimage(rect.site.y + BLOCK_SIZE / 2, rect.site.x + BLOCK_SIZE / 2, &back);
	}

	int GetDir() { return dir; }

};

CAStar的实现

//zh_CN.GBK
#pragma once
#include "define.h"

class CAStarNode
{
public:
	int father;
	int G;
	int H;
	bool close;
	bool open;

	CAStarNode() : father(0), G(0), H(0), close(0), open(0) {}
};


class CAStar
{
private:
	CAStarNode ns[MAP_CNT];	// 点集

	vector<int> open_list;		// 开放列表
	//list<int> close_list;		// 关闭列表 // 二维地图关闭列表的大小成n^2增长,遍历耗时极高,因此使用分布式存储,直接访问结果
								// 而开放列表的数量级较小,可以使用list形式,耗时不是很多

	int s;	// 开始结点(怪物)
	int e;	// 结束结点(玩家)

	int w_inst;	// 距离权重,此值越大,monster越注重距离控制
	int w_path;	// 路径权重,此值越大,monster越注重短路径

	COLORREF color;
	bool path_show;
	int cnt;

public:
	CAStar() :s(0), e(0), w_inst(1), w_path(1), path_show(0), color(RED), cnt(0) {}

	void SwitchPathShow(COLORREF c)
	{
		if (path_show)
			path_show = 0;
		else
			path_show = 1;

		color = c;
	}

	void SetStyle(int instance, int path)
	{
		w_inst = instance;
		w_path = path;
	}

	// 初始化
	void init(int* map, CPoint sp, CPoint ep)
	{
		open_list.clear();
		//close_list.clear();
		
		e = ep.x * MAP_COLUMN + ep.y;
		s = sp.x * MAP_COLUMN + sp.y;

		for (int i = 0; i < MAP_CNT; i++)
		{
			//if (map[i] == 3)
				//close_list.push_back(i);
			ns[i].close = (map[i] == 3) ? 1 : 0;
			ns[i].open = 0;
			ns[i].G = 0;
			
			int dk = abs(i - e);
			ns[i].H = (dk / MAP_COLUMN + dk % MAP_COLUMN) * w_inst;

			ns[i].father = 0;
		}
	}

	int FindMinF()
	{
		int i_min = 0;
		int k_min = 0;
		int F_min = 1000000;

		int n = open_list.size();
		for (int i = 0; i < n; i++)
		{
			int k = open_list[i];
			int F = ns[k].G + ns[k].H;
			if (F < F_min)
			{
				F_min = F;
				k_min = k;
				i_min = i;
			}
		}

		if (k_min != 0)
		{
			open_list[i_min] = open_list[n - 1];
			open_list.pop_back();
		}

		return k_min;
	}

	void BuildAWay()
	{
		open_list.push_back(s); // 添加第一个结点
		ns[s].open = 1;

		int k_current = s;	// 设置起点

		while (1)
		{
			if (k_current == e)
				break;

			if (open_list.size() == 0)
				break;

			// 查询下一个结点
			k_current = FindMinF();
			ns[k_current].close = 1;


			// 搜寻周围结点
			for (int i = 0; i < 4; i++)
			{
				int ni = k_current / MAP_COLUMN;
				int nj = k_current % MAP_COLUMN;

				switch (i)
				{
				case 0: ni--; break;
				case 1: ni++; break;
				case 2: nj--; break;
				case 3: nj++; break;
				}

				if (ni < 0 || ni >= MAP_ROW || nj < 0 || nj >= MAP_COLUMN)
					continue;

				int k_next = ni * MAP_COLUMN + nj;

				if (!ns[k_next].close)			// 没加入close ( 墙,或者探查完毕的结点 )
				{
					int G_old = ns[k_next].G;
					int G_new = ns[k_current].G + w_path;

					if (ns[k_next].open)		// 已经加入了open
					{
						// 优化路径
						if (G_old > G_new )	
						{
							ns[k_next].G = G_new;
							ns[k_next].father = k_current;
						}
					}
					else
					{
						ns[k_next].G = G_new;
						ns[k_next].father = k_current;

						open_list.push_back(k_next);			// 添加进open_list
						ns[k_next].open = 1;
					}
				}

			}
		}
	}


	// 获得方向
	int GetDir()
	{
		if (path_show)
		{
			cnt++;
			if (cnt > 5)
				cnt = 0;

			if (cnt == 0)
				setlinecolor(BLACK);
			else
				setlinecolor(color);
		}
		

		int k[2] = { 0 };
		
		k[0] = e;

		while (1)
		{
			k[1] = ns[k[0]].father;

			if (k[1] == 0 || k[1] == s)
				break;


			if (path_show)
			{
				int xi = k[0] / MAP_COLUMN * BLOCK_SIZE + BLOCK_SIZE / 2;
				int xj = k[0] % MAP_COLUMN * BLOCK_SIZE + BLOCK_SIZE / 2;
				int yi = k[1] / MAP_COLUMN * BLOCK_SIZE + BLOCK_SIZE / 2;
				int yj = k[1] % MAP_COLUMN * BLOCK_SIZE + BLOCK_SIZE / 2;
				line(xj, xi, yj, yi);
			}


			k[0] = k[1];
	
		}

		if (k[0] - k[1] == MAP_COLUMN)	return DIR_DOWN;
		if (k[0] - k[1] == -MAP_COLUMN)	return DIR_UP;
		if (k[0] - k[1] == -1)			return DIR_LEFT;
		if (k[0] - k[1] == 1)			return DIR_RIGHT;

		return DIR_NONE;
	}



};

/**/

CMonster的智能躲避

// 可供预先选择的避难点 42个
const static int safe_row[7] = { 1,5,8,18,20,23,25 };
const static int safe_column[6] = { 1,5,9,11,15,19 };

// 吃豆人所在的位置需要避免被选定为避难点
int danger_row_idx = 0;
for (int i = 0; i < 6; i++)
	if (safe_row[i] < pi && pi < safe_row[i + 1])
	{
		danger_row_idx = i;
		break;
	}

int danger_column_idx = 0;
for (int i = 0; i < 5; i++)
	if (safe_column[i] < pj && pj < safe_column[i + 1])
	{
		danger_column_idx = i;
		break;
	}


// 当已经到达避难点,或第一次确定避难点,或吃豆人来到避难点附近时,计算新避难点的坐标
if ((escape_rc.x == mi && escape_rc.y == mj ) || (escape_rc.x == 0 && escape_rc.y == 0) 
	||(abs(escape_rc.x - pi) <= 10 && abs(escape_rc.y- pj) <= 5))
{
	
	// 避开危险的吃豆人选择避难点
	while (1)
	{
		int r = rand() % 7;
		if (abs(r - danger_row_idx) > 1)
		{
			escape_rc.x = safe_row[r];
			break;
		}
	}

	while (1)
	{
		int c = rand() % 6;
		if (abs(c - danger_column_idx) > 1)
		{
			escape_rc.y = safe_column[c];
			break;
		}
	}
	//cout << "escape_rc = (" << escape_rc.x << "," << escape_rc.y << ")\n";
}


// A* 算法,计算移动方向
brain.init(map, CPoint(mi, mj), CPoint(escape_rc.x, escape_rc.y));

// 将吃豆人附近的地段关闭,实现绕路的特性
if (abs(mi - pi) <= 4 && abs(mj - pj) <= 3)
{
	// 离吃豆人较近时关闭较小的地段,防止把自己也关进去
	for (int i = -1; i < 2; i++)
	{
		for (int j = -1; j < 2; j++)
		{
			brain.SetNodeWall(pi + i, pj + j);
		}
	}
}
else
{
	for (int i = -3; i < 4; i++)
	{
		for (int j = -2; j < 3; j++)
		{
			brain.SetNodeWall(pi + i, pj + j);
		}
	}
}

// 构建一条逃避的路径
brain.BuildAWay();

// 获取当前逃避的方向
int dir = brain.GetDir();
SetNewDir(dir);

// 转向
if (Turn())
	SetFlash(dir);

// 前进
Go();

全部代码下载

项目github地址: https://github.com/bridgeL/pacman2

文档内容

mover.h

CMover
CMonster
CPacman

flash.h

CFlash
CFlashGroup

define.h

各类宏定义
CPoint
CRect

astar.h

实现a*寻路算法

game.h/game.cpp

存放各个游戏页的初始化、处理、更新函数
键盘输入捕获线程
定时器线程(控制帧率)

app.cpp

游戏主循环