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