C/C++实现小猴吃香蕉游戏

1.项目介绍

        本项目采用C/C++实现,利用easyX图形库完成对图像的处理,easyX的下载和安装方法可以在官网找到。项目名称叫”小猴吃香蕉“,玩法类似于推箱子,玩家运行项目后通过按下键盘上的W、A、S、D和”Esc“键来控制游戏方向的方向和退出,并在玩家成功通关以后播放祝贺动画。效果展示如下图所示:

2.开发日志

        本项目代码篇幅并不算太长,所以并没有采用C++里常用的类和对象用来封装接口和成员变量。所以本次开发可以采用两种方式:1.可以直接在主程序中将所以的函数接口写好并调用。2.也可设计头文件或静态库减轻代码的冗余度。这里我们采用的第二种开发方式,设计头文件,来实现对程序整体的一个微封装。那么事不宜迟,开始进行我们的项目实现吧。

3.概要设计

        做任何的软件之前都要思考一件事情,那就是如何制作软件,相信有过计算机类本科经历的读者首先想到了”软件设计方法“或”软件工程“这两门课程,里面最重要的环节就是概要设计,能够完成一个项目的概要设计的话,后面的编码工作也会进行得势如破竹,当然没有系统学过这两门课程的读者也不用担心,本项目模块比较简单,不会涉及到这两门课程的难点。为了简化流程,我已经设计好了本项目的层次结构图,可读性也是非常清晰的,如下图所示:

4.接口实现— 初始化

        有了层次结构图,我们就知道要设计哪些接口,但是在实现具体的接口之前,我们需要提前先写好主程序流(直观的流图过于简单,这里直接省略),这样才能理顺开发思路,代码如下:


#include <iostream>
#include "tools.h"   // 游戏运行头文件
#include "outlook.h" // 游戏全局变量头文件

using namespace std;

void congratulation();

int main(void) {
	init();         // 初始化
	control();      // 控制

	congratulation();  // 结束动画

	system("pause");
	return 0;
}

void congratulation() {
	IMAGE bg;
	loadimage(&bg, "background2.png", SCR_SIZE, SCR_SIZE);
	RECT rec = { 0, 0, SCR_SIZE, SCR_SIZE };
	putimage(0, 0, &bg);
	settextstyle(20, 0, "宋体");
	setbkmode(TRANSPARENT);
	drawtext("小猴子吃到了所有的香蕉,恭喜通关哦", &rec, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}

写好上述的主程序流代码后,可以看到编译器的语法检查有非常多的报错提示,不要担心,这正是我们需要的结果,这些报错提示都是因为主函数里只有调用,并没有找到具体的实现,我们根据这些报错信息逐步完善接口就行。本节先设计初始化接口,首先设计写好头文件outlook.h,这个头文件的作用是用extern声明导入outlook.cpp里定义的全局变量,代码如下:

#pragma once
#include <graphics.h>

#define PIX_SIZE 63     //  地图道具尺寸
#define MAP_SIZE 8     //  地图尺寸
#define SCR_SIZE 504     //  窗口尺寸
// 方向键宏定义
#define choice_up 'w'
#define choice_down 's'
#define choice_left 'a'
#define choice_right 'd'

enum _direction {
	UP,
	DOWN,
	LEFT,
	RIGHT
};// 方向枚举

enum _elemMap {
	TREE,
	GRAS,
	FRUT,
	SNAK,
	MOKY,
	EATN,
	ALL
};// 道具枚举

typedef struct _POS {
	int li;
	int col;
}POS; // 记录位置的结构体
// 引入outlook.cpp中的全局变量
extern IMAGE img[ALL];
extern IMAGE bg;
extern POS snakePos;
extern int map[MAP_SIZE][MAP_SIZE];

有了outlook.h的声明以后, 还需要在outlook.cpp里对这些外部声明的变量进行初始化,这样outlook.h才能找到引入外部变量的源头,outlook.cpp的代码如下:

#include <graphics.h>
#include "outlook.h"



IMAGE img[ALL]; // 声明一下道具数组
IMAGE bg; 
POS snakePos; // 声明一下小蛇的位置

int map[MAP_SIZE][MAP_SIZE] = {
	{TREE, TREE, TREE, TREE, TREE, TREE, TREE, TREE},
	{TREE, TREE, GRAS, SNAK, TREE, TREE, TREE, TREE},
	{TREE, TREE, GRAS, MOKY, GRAS, GRAS, TREE, TREE},
	{TREE, TREE, TREE, GRAS, TREE, GRAS, TREE, TREE},
	{TREE, FRUT, GRAS, GRAS, GRAS, GRAS, GRAS, TREE},
	{TREE, FRUT, MOKY, GRAS, TREE, TREE, GRAS, TREE},
	{TREE, FRUT, GRAS, GRAS, GRAS, MOKY, GRAS, TREE},
	{TREE, TREE, TREE, TREE, TREE, TREE, TREE, TREE}
};// 初始化的地图,根据自己需要还能设计出更多的关卡

OK,写完这两个文件以后就可以关闭它们了,之后再想使用这些全局变量的时候,只需要在对应的文件下面 #include "outlook.h" 就行。做完这些准备工作以后,就可以开始写接口了,由于我们的主程序已经写好,为了便于实现接口,我们需要再新建一个tools.cpp文件,用来专门写接口,注意非主源文件是不需要再定义main函数,所以tools.cpp的代码如下:

#include <iostream>
#include "tools.h"
#include "outlook.h"
#include <conio.h>

using namespace std;

void init() {
    // 加载道具图片,图片资源可以自行寻找,当然我也会在文章末尾另附我的资源
	loadimage(&bg, "background2.png", SCR_SIZE, SCR_SIZE, true);
	loadimage(&img[TREE], "tree.png", PIX_SIZE, PIX_SIZE, true);
	loadimage(&img[GRAS], "grass.png", PIX_SIZE, PIX_SIZE, true);
	loadimage(&img[FRUT], "fruit.png", PIX_SIZE, PIX_SIZE, true);
	loadimage(&img[SNAK], "snake.png", PIX_SIZE, PIX_SIZE, true);
	loadimage(&img[MOKY], "monkey.png", PIX_SIZE, PIX_SIZE, true);
	loadimage(&img[EATN], "monkey.png", PIX_SIZE, PIX_SIZE, true);

	initgraph(SCR_SIZE, SCR_SIZE); // 创建窗口
    生成地图中....
	putimage(0, 0, &bg);
	for (int i = 0; i < MAP_SIZE; i++) {
		for (int j = 0; j < MAP_SIZE; j++) {
			if (map[i][j] == SNAK) {
				snakePos.li = i;
				snakePos.col = j;
			}
			putimagePNG(j * PIX_SIZE, i * PIX_SIZE, &img[GRAS]);
			putimagePNG(j * PIX_SIZE, i * PIX_SIZE, &img[map[i][j]]);
		}
	}

}

整个 void init() 就是我们实现好了的初始化模块,它具有打印窗口和生成地图的功能。

5.接口实现—控制

        有了初始化接口以后,接下来就该开始游戏的游玩过程了,设计思路就是不断从控制台获取玩家的按键信息,一旦捕获到按键信息,就立刻开始执行计算位置,更改贴图等算法,实现视觉上的小蛇移动,那么在实现接口算法之前呢,还要补充一点,就是由于tools.cpp文件也属于被调用的文件,那么他必须有与之相对应的头文件,这样才能够被main文件识别并调用,创建一个头文件 tools.h 并编写对应的声明即可,tools.h的代码如下:

#pragma once

#include <graphics.h>
#include "outlook.h"


// 打印透明png图片的接口,实现原理看不懂不用管,x为载入图片的X坐标,y为Y坐标
void putimagePNG(int x, int y, IMAGE* picture); 

// 初始化界面
void init();

// 控制游戏
void control();

// 道具移动
void snake_move(int direct);

// 贴图置换
void elem_change(POS* pos, enum _elemMap prop);

// 判断胜利
bool isWIN();

头文件 tools.h 写好以后关闭,然后继续在 tools.cpp 里继续实现控制接口,控制接口就是持续捕获玩家的按键信息,很好理解,代码如下:

void control() {
	
	do {
		if (_kbhit()) { // 如果玩家敲击键盘
			char msg = _getch();
			if (msg == choice_up) { // 如果玩家按了 w 键
				snake_move(UP);
			}
			else if (msg == choice_down) { // 如果玩家按了 s 键
				snake_move(DOWN);
			}
			else if (msg == choice_left) { // ......
				snake_move(LEFT);
			}
			else if (msg == choice_right) { // .....
				snake_move(RIGHT);
			}
			else if (msg == VK_ESCAPE) { // 如果玩家按了Esc键
				break;
			}
			if (isWIN()) { // 判断胜利了么
				break;
			}
		}
		Sleep(100);
	} while (1);
}

写好控制接口的代码以后,我们又可以看到在control接口里又多出了几个新的接口,这是为了降低模块和接口之间的耦合性,不得已而为之, 逐步击破就行,首先看到第一个接口就是按照正常按键后的 snake_move(方向) 接口,我们来实现它,原理就是根据传递过来的方向参数,计算出小蛇和猴子下一步的位置,然后根据小蛇和猴子的位置关系,更换对应的贴图,代码如下:

void snake_move(int direct) {

	struct _POS nextPOS = snakePos; // 小蛇下一步的位置
	struct _POS nextBOX = snakePos; // 猴子下一步的位置
	// 计算下一步
	switch (direct) {
		case UP: {
			nextPOS.li--;
			nextBOX.li -= 2;
			break;
		}

		case DOWN: {
			nextPOS.li++;
			nextBOX.li += 2;
			break;
		}

		case LEFT: {
			nextPOS.col--;
			nextBOX.col -= 2;
			break;
		}

		case RIGHT: {
			nextPOS.col++;
			nextBOX.col += 2;
			break;
		}
	}

    // 根据位置关系和道具情况判断怎么置换贴图
	int index = map[nextPOS.li][nextPOS.col];
	int index_box = map[nextBOX.li][nextBOX.col];
	if ( index == GRAS) {
		elem_change(&snakePos, GRAS);
		elem_change(&nextPOS, SNAK);
		snakePos = nextPOS;	
	}
	else if (index == MOKY && index_box != TREE) {
		elem_change(&snakePos, GRAS);
		elem_change(&nextPOS, GRAS);
		elem_change(&nextPOS, SNAK);
		elem_change(&nextBOX, MOKY);
		snakePos = nextPOS;
	}

}

写好snake_move移动计算接口以后,只需要在完成贴图置换elem_change接口就可以实现小蛇和猴子的移动了,贴图置换接口很简单,根据移动接口已经计算好的位置参数,进行数组的迭代更替和贴图覆盖即可,代码如下:

void elem_change(POS* pos, enum _elemMap prop) {
    // 数组迭代
	if (map[pos->li][pos->col] == FRUT && prop == MOKY) {
		map[pos->li][pos->col] = EATN;
	}
	else {
		map[pos->li][pos->col] = prop;
	}
    // 贴图覆盖
	putimagePNG(pos->col * PIX_SIZE, pos->li * PIX_SIZE, &img[GRAS]);
	putimagePNG(pos->col * PIX_SIZE, pos->li * PIX_SIZE, &img[prop]);
}

如果此时正常写完代码并运行程序的话是可以看到小蛇在移动的,但这里并没有采用easyX提供好的打印图片接口 putimage(背景无法透明化),而是设计了一个新的打印接口putimagePNG,这个接口相当困难,主要是利用贝叶斯公式得到像素的 RGB 信息,再利用位运算进行边缘化处理,最终生成打印一张背景透明的png图片,看不懂的读者可以不用管这个接口的实现原理,这属于easyX图形库的版本底层缺陷,跟项目编程的技术能力无关,那么代码如下:

void putimagePNG(int x, int y, IMAGE* picture) //x为载入图片的X坐标,y为Y坐标
{
	// 变量初始化
	DWORD* dst = GetImageBuffer();    // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带
	DWORD* draw = GetImageBuffer();
	DWORD* src = GetImageBuffer(picture); //获取picture的显存指针
	int picture_width = picture->getwidth(); //获取picture的宽度,EASYX自带
	int picture_height = picture->getheight(); //获取picture的高度,EASYX自带
	int graphWidth = getwidth();       //获取绘图区的宽度,EASYX自带
	int graphHeight = getheight();     //获取绘图区的高度,EASYX自带
	int dstX = 0;    //在显存里像素的角标

	// 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算
	for (int iy = 0; iy < picture_height; iy++)
	{
		for (int ix = 0; ix < picture_width; ix++)
		{
			int srcX = ix + iy * picture_width; //在显存里像素的角标
			int sa = ((src[srcX] & 0xff000000) >> 24); //0xAArrggbb;AA是透明度
			int sr = ((src[srcX] & 0xff0000) >> 16); //获取RGB里的R
			int sg = ((src[srcX] & 0xff00) >> 8);   //G
			int sb = src[srcX] & 0xff;              //B
			if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight)
			{
				dstX = (ix + x) + (iy + y) * graphWidth; //在显存里像素的角标
				int dr = ((dst[dstX] & 0xff0000) >> 16);
				int dg = ((dst[dstX] & 0xff00) >> 8);
				int db = dst[dstX] & 0xff;
				draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16)  //公式: Cp=αp*FP+(1-αp)*BP  ; αp=sa/255 , FP=sr , BP=dr
					| ((sg * sa / 255 + dg * (255 - sa) / 255) << 8)         //αp=sa/255 , FP=sg , BP=dg
					| (sb * sa / 255 + db * (255 - sa) / 255);              //αp=sa/255 , FP=sb , BP=db
			}
		}
	}
}

实现完这一步的接口之后,游戏的控制接口算是终于完成了,此时运行项目就会发现,在控制台输入相应的热键操作后,小蛇和猴子就能够正常移动。 

6.接口实现—结束

        控制接口实现完成后,项目已经接近尾声,而控制接口里已经调用过结束接口,所以结束接口只需要继续在tools.cpp里实现一下就行,那么结束接口 isWIN 的代码如下:

bool isWIN() {
	bool flag = true;
	for (int i = 0; i < MAP_SIZE; i++) {
		for (int j = 0; j < MAP_SIZE; j++) {
			if (map[i][j] == FRUT) {
				flag = false;
			}
		}
	}

	return flag;
}

正常这里还应该播放结束动画,但作者前期为了调试程序方便,播放动画的接口直接在主函数里实现并调用了,所以这里就算彻底完工了,恭喜小猴吃香蕉游戏项目竣工! 

7. 测试&资源链接

        生成解决方案或编译运行后,即可正常游玩,如下图,记得热键是在控制台使用哦:

本项目用到的图片资源链接:https://pan.baidu.com/s/1DY8vIKv8Vr5NhhsU_Xeh_g 
提取码:Akgy

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员Akgry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值