详细描述下自己遇到的问题
根据先生指点,实现了一个简单瓦片地图,能导入自定义贴图绘制自己的 RPG 游戏,导出为文件夹,在对应的程序里读取复现,但是只有背景移动,没有人物碰撞方格触发事件。
当时使用 C 语言开发,用了一个 .cpp 文件办出来。然后下一步想继续在这一个文件里加代码,实现 ”实时“地图编辑,类似于贴上瓦片,就会直接运行游戏事件检测这样。
总结是三个问题:
- 新代码从哪里插入?还是说不得不加个新文件再 include 一下。
- 瓦片地图编辑器的下一个代码是增加瓦片动画代码还是瓦片事件检测代码?
- 若是以插件的形式实现:角色编辑器+瓦片地图编辑器+动画编辑器+UI编辑器 来造游戏编辑器可行吗?
当前代码的使用视频参考 b站 2D RPG 地图编辑器 导入导出,配套游戏读取接口已实现
代码一共分为三部分,每个部分复制粘贴单独编译运行:
一是 810 行的编辑器部分,实现了自定义贴图导入,贴图,编辑成果一键打包导出,并在编辑窗口上实现 AWSD 一边移动,一边拖拽地图,一边在地图上绘制三者操作任意组合功能。
二是 478 行的游戏读取接口,这里只实现了地图编辑器打包导出的文件夹 DLC 的一键读取和视口移动,仅是空壳。
三是 312 行的控制台肉鸽游戏,提供一个小肉鸽游戏的移动,攻击,死亡,小怪追踪,新小怪刷新这几个功能的逻辑样例。
记得说清楚你使用的操作系统、编译环境
操作系统: win11 或 win7
编译环境: Devc++5.11 或 VS2010 或 VS2019 或 VS2022 皆可
////////////////////////////////////////////
// 程序:2D RPG 地图编辑器
// 作者:民用级脑的研发记录
// 邮箱:1309602336@qq.com
// 编译环境:Devc++/VC 2010/Visual Studio 2022,EasyX_20220901/Easyx_2023 大暑版
// 编写日期:2024-2-22
//
#undef UNICODE
#undef _UNICODE
#pragma warning(disable : 4996) // VS2022 对某些函数警告,但是为了方便移植,就无视这些警告 这样 Devc++ VC2010 VS2022 都能跑
#include <graphics.h>
#include <stdio.h>
#include <string.h>
#include <io.h>
#include <direct.h>
// 从默认文件夹中读取自定义图片
void loadfile(IMAGE*** bentablev2);
// 加载自定义图片
void loadfile_scanf(char* dirname, char* next, char* kind, IMAGE** pentablev2);
// 保存自定义文件
void savepersonalfile(char* DLC, IMAGE*** pentablev2);
// 保存自定义瓦片贴图
void savelocal(char* DLC, char* dirname, char* next, char* kind, char* sign, IMAGE** files);
// 实时渐变色小动画
inline void animation(int chararcterflag, int bkgameleft, int bkgametop, int bkmeshgameleft, int bkmeshgametop, int bkmeshdeskleft, int bkmeshdesktop);
// 边界检查,move 是平移大小,单位:像素,若使得寄存区在九宫格中心,move 的大小就是寄存区的边长,相应的 size 的大小是 move 的两倍。这样无论长宽都是 3 倍。size 是大小区域,代表不会触发缓冲区更新的范围,单位:像素
inline void checkboundary(int* bkgameleft, int* bkgametop, int gamelimitright, int gamelimitbuttom, int* bkmeshleft, int* bkmeshtop, int* meshlimit, int* meshlimitbutton, int move, int size);
// 保存游戏地图贴图数据
void savegamemap(int** map, int gamemapi, int gamemapj, IMAGE* pentable, IMAGE*** pentablev2, int pixnum, int imagenum);
// 导入游戏地图贴图数据
void loadgamemap(int** map, int* gamemapi, int* gamemapj, IMAGE* pentable, int* pixnum, int* imagenum);
// 使用关键字 inline 声明为内联函数,减少贴图函数频繁调用的开销导致的卡顿。
// 缓冲区纹理映射函数:bkmesh 映射目标,map 映射总网格,pentable:纹理集,bkmeshmapi,bkmeshmapj:映射起始点,tilenum:横,纵映射的数量,pixnum:一个映射块的边长,单位:像素。
inline void freshmesh(IMAGE* bkmesh, int** map, IMAGE* pentable, IMAGE*** pentablev2, int bkmeshmapi, int bkmeshmapj, int tilenum, int pixnum)
{
int kind = 0; // 存储代号第一位:类型
int number = 0; // 存储代号第二位:序列号
int pennumber = -1; // 暂存每一次循环的映射代号
IMAGE* pen = NULL; // 所找到的纹理
int left = 0; // 这是每次循环所找到的纹理对应映射地址
int top = 0;
SetWorkingImage(bkmesh); // 设置绘图目标为游戏背景采样区,刷新采样区,刷新寄存区
cleardevice();
for (int i = bkmeshmapi; i < bkmeshmapi + tilenum; i++)
{
left = 0;
for (int j = bkmeshmapj; j < bkmeshmapj + tilenum; j++)
{
pennumber = map[i][j]; // 读取游戏大地图数组序号
if (pennumber == -1)
{
rectangle(left, top, left + pixnum, top + pixnum);
}
else
{
kind = pennumber / 10 - 6; // 剥离第一位
number = pennumber % 10; // 剥离最后一位
if (pennumber < 10)
pen = &pentable[pennumber];
else
pen = pentablev2[kind][number]; // 根据序号查找对应贴图
putimage(left, top, pen); // 把贴图画到采样区
}
left += pixnum; // 往右移动,准备下一次绘制位置,此处贴图就会覆盖白色边框。为保证坐标变换和网格对应,算上网格宽度,也在贴图矩形内
}
top += pixnum; // 往下移动,准备下一次绘制位置,此处就会覆盖白色边框,方便定位
}
SetWorkingImage();
}
// 在纹理映射函数中产生的图片中截图,但此为演示参数作用,此处并未优化。
inline void freshbk(IMAGE* bk, IMAGE* bkmesh, int gamex, int gamey, int bkmeshmapi, int bkmeshmapj, int tilenum, int pixnum)
{
SetWorkingImage(bkmesh);
getimage(bk, gamex - bkmeshmapj * pixnum, gamey - bkmeshmapi * pixnum, tilenum * pixnum, tilenum * pixnum);
SetWorkingImage();
}
// 在屏幕显示截图
inline void showbk(IMAGE* bk, int bkdeskx, int bkdesky)
{
SetWorkingImage();
putimage(bkdeskx, bkdesky, bk);
}
// 在屏幕上显示缓冲区
inline void showbkmesh(IMAGE* bkmesh, int bkmeshdeskx, int bkmeshdesky)
{
SetWorkingImage();
putimage(bkmeshdeskx, bkmeshdesky, bkmesh);
}
// 初始化游戏地图
int** initmap(int wide, int high)
{
int** map = new int* [high]; // 二维数组动态初始化,先给二级指针挂上一个长度为 10 的指针数组
for (int i = 0; i < high; i++)
{
map[i] = new int[wide]; // 然后数组里的每个指针都挂上一个长度为 10 的 int 类型数组
}
for (int i = 0; i < high; i++)
{
for (int j = 0; j < wide; j++)
{
map[i][j] = -1; // 初始化游戏大地图 map 的参数,参数 1 默认黑色
}
}
return map;
}
int main()
{
initgraph(1640, 980, 1);
setbkcolor(GREEN);
cleardevice();
IMAGE* bk; // 背景图片寄存区
bk = new IMAGE(270, 270);
IMAGE* bkmesh; // 背景图片采样区
bkmesh = new IMAGE(270 * 3, 270 * 3);
int** map; // 游戏大地图数组,记录着整个游戏背景的贴图信息,而在运行过程中,选取部分区域的数字,对照序号与贴图,实现游戏背景绘制。其余没有选中的区域就是压缩的空间。
int pixnum; // 一个正方形瓦片的边长。单位:像素
int bkgameleft; // 背景图片寄存区左上角坐标,是在游戏里的像素坐标。(0,0)可以理解为游戏大地图的左上角顶点。
int bkgametop;
int bkmeshgameleft; // 背景图片采样区左上角坐标,是在游戏里的像素坐标。
int bkmeshgametop;
int bkmeshmapi; // 背景图片采样区左上角所对应的 map 数组序号。从 map[0][0]开始,按照 map[i][j],其中 bkmeshmapi=bkmeshtop/pixnum
int bkmeshmapj;
int bkdeskleft; // 规定在屏幕上显示游戏背景寄存区,此处记录其左上角在屏幕上的像素坐标
int bkdesktop;
int bkmeshdeskleft; // 规定在屏幕上显示游戏背景采样区,此处记录其左上角在屏幕上的像素坐标
int bkmeshdesktop;
pixnum = 30; // 进行初始化,规定各位置具体数字
bkdeskleft = 200; // 游戏背景左上角将会在屏幕的(200,200) 处
bkdesktop = 200;
bkgameleft = 0; // 由于 bkgame 控制 mesh 坐标移动,(0,0)则游戏背景完全在当前采样区移动
bkgametop = 0;
bkmeshdeskleft = 700; // 游戏背景缓冲区左上角将会在屏幕的(700,0)处
bkmeshdesktop = 0;
bkmeshgameleft = 0;
bkmeshgametop = 0;
int meshlimitright; // 使用变量暂存边界,不用每次来回计算边界
int meshlimitbuttom;
meshlimitright = 270; // 和寄存区一样大,把寄存区限制在一个九宫格中心,如果越出九宫格,九宫格整体平移
meshlimitbuttom = 270;
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
int mapi; // 读取 DLC 文件,需要变量记录循环次数,单位 瓦片贴图个数
int mapj;
int safemapi; // 一个安全的贴图范围,避免阅读超过 10000 导致的越界闪退问题。
int safemapj;
int gamelimitright; // 用于减少比较时的计算,存储边界范围
int gamelimitbuttom;
safemapi = 70; // 注意导出的是左上角开始的 100*100 部分,如果绘制出界,保存不了,需要改这一行和下一行的数字
safemapj = 70;
gamelimitright = pixnum * safemapi;
gamelimitbuttom = pixnum * safemapj;
mapi = 300;
mapj = 300;
map = initmap(mapi, mapj); // 初始化一个有 10000*10000 个贴图的地图
int pentableleft; // 忘了初始化调色盘了,这里设置调色盘左上角在屏幕的坐标
int pentabletop;
int imagenum; // 调色板数量上限,用于文件读取控制循环次数
IMAGE* pentable; // 调色板其实就是贴图数组
pentableleft = 0; // 调色盘左上角将会在屏幕的(0,0)处
pentabletop = 0;
imagenum = 10;
int pentablev2left; // 自定义贴图插槽
int pentablev2top;
int pentablev2high; // 自定义贴图的容量大小
int pentablev2wide;
pentablev2left = 10;
pentablev2top = 300;
pentablev2high = 10;
pentablev2wide = 4;
IMAGE*** pentablev2;
pentablev2 = new IMAGE * *[pentablev2wide];
for (int i = 0; i < pentablev2wide; i++)
pentablev2[i] = new IMAGE * [pentablev2high];
for (int i = 0; i < pentablev2wide; i++)
for (int j = 0; j < pentablev2high; j++)
pentablev2[i][j] = NULL;
loadfile(pentablev2); // 加载本地图片
pentable = new IMAGE[imagenum];
for (int i = 0; i < imagenum; i++)
{
pentable[i] = IMAGE(30, 30);
SetWorkingImage(&pentable[i]); // 给调色板绘制颜色
setfillcolor(RGB(i * 20, i * 20, i * 20)); // 这里初始化调色盘的颜色
fillrectangle(-1, -1, pixnum, pixnum); // 在调色板上绘制颜色(纹理)要从 -1,-1 开始绘制,把边框画到外部,不保留边框。
}
int left; // 初始化绘制采样区所需的坐标,相对于采样区,(0,0)就是采样区左上角顶点坐标
int top;
left = 0;
top = 0;
// 往缓冲区刷入贴图
SetWorkingImage(bkmesh); // 设置绘图目标为游戏背景采样区,刷新采样区,刷新寄存区
setbkcolor(RGB(200, 200, 200)); // 设置瓦片边线颜色,图片没有覆盖的背景就是边线
cleardevice();
for (int i = bkmeshmapi; i < bkmeshmapi + 30; i++)
{
left = 0;
for (int j = bkmeshmapj; j < bkmeshmapj + 30; j++)
{
int pennumber = map[i][j]; // 读取游戏大地图数组序号
if (pennumber == -1)
{
rectangle(left, top, left + pixnum, top + pixnum);
}
left += pixnum; // 往右移动,准备下一次绘制位置,
}
top += pixnum; // 往下移动,准备下一次绘制位置
}
getimage(bk, bkgameleft, bkgametop, 270, 270); // 从刚刚绘制好的采样区取样,刷新游戏背景寄存区。
// 开始往屏幕上绘图
SetWorkingImage(); // 设置电脑屏幕为绘制对象
for (int j = 0; j < 10; j++)
{
putimage(pentableleft + 10, pentabletop + j * 30, &pentable[j]); // 绘制绘图板
}
for (int i = 0; i < pentablev2wide; i++) // 绘制自定义贴图,印自定义笔刷
for (int j = 0; j < pentablev2high; j++)
if (pentablev2[i][j] != NULL)
putimage(pentablev2left + i * 30, pentablev2top + j * 30, pentablev2[i][j]);
putimage(bkdeskleft, bkdesktop, bk); // 绘制游戏背景
putimage(bkmeshdeskleft, bkmeshdesktop, bkmesh); // 显示游戏背景缓冲区
// 此时绘制完成,以上 刷贴图,采样,粘贴就是实现 RPG 游戏大地图的压缩
// 开始检测鼠标键盘功能
int drawflag; // 设置长按 flag
int drawoldmx; // 记录上一次绘制时的鼠标坐标,用于检测是否重复点击相同像素,来减少重复绘制
int drawoldmy;
int drawx; // 画笔在游戏里的位置,单位像素
int drawy;
int olddrawi; // 记录上一次绘制的瓦片,判断是否需要重新绘制
int olddrawj;
int drawsmallflag; // 在 drawflag=1 时,检测是否刷新
drawsmallflag = 0;
olddrawi = 0;
olddrawj = 0;
drawx = 0;
drawy = 0;
drawflag = 0;
drawoldmx = 0;
drawoldmy = 0;
int pentake; // 设置不绘制时贴图代号为 -1
pentake = 9; // 默认白色笔刷,对应黑色背景
int draftoldmx; // 记录刚刚拖拽时的鼠标的位置,用于坐标变换计算位移
int draftoldmy;
int draftoldgamex; // 记录刚刚拖拽时的游戏地图位置,用于坐标变换计算新的游戏坐标
int draftoldgamey;
int draftflag; // 设置拖拽 flag
draftoldmx = 0;
draftoldmy = 0;
draftoldgamex = 0;
draftoldgamey = 0;
draftflag = 0;
int moveflag; // 是否键盘控制移动
int flag_x; // 记录位移
int flag_y;
int speed; // 键盘控制视口的移动速度
speed = 5;
flag_x = 0;
flag_y = 0;
moveflag = 0;
int mousex; // 记录鼠标位置
int mousey;
mousex = 0;
mousey = 0;
int oldbkmeshgamex; // 判断拖拽时是否需要刷新 bkmesh 网格
int oldbkmeshgamey;
oldbkmeshgamex = -1;
oldbkmeshgamey = -1;
int i = -1; // 用于暂存 mesh 网格坐标
int j = -1;
int characterflag; // 用于按键时游戏小人的颜色变化
characterflag = 5;
ExMessage m;
while (1)
{
while (peekmessage(&m, EX_KEY | EX_MOUSE)) // 一次性处理完鼠标消息,参考自 https://codebus.cn/zhaoh/handle-mouse-messages-correctly
{
switch (m.message)
{
case WM_LBUTTONDOWN: // 鼠标左键按下,有两种情况,一是选择贴图,另外就是绘制贴图
if (drawflag == 0 && m.x > bkdeskleft && m.y > bkdesktop && m.x < bkdeskleft + 300 && m.y < bkdesktop + 300) // 如果之前不是长按状态 且按下左键时,鼠标在游戏背景区域内
{
drawflag = 1; // 记录为正在绘制的状态
mousex = m.x; // 记录坐标用于绘制
mousey = m.y;
}
else if (drawflag == 0 && draftflag == 0 && m.x > 0 && m.y > 10 && m.x < 40 && m.y < 300)
pentake = m.y / 30; // 选择贴图对应的代号
else if (drawflag == 0 && draftflag == 0 && m.x > 10 && m.y > 300 && m.x < 10 + pentablev2wide * 30 && m.y < 300 + pentablev2high * 30)
{
if (pentablev2[(m.x - pentablev2left) / pixnum][(m.y - pentablev2top) / pixnum] != NULL)
pentake = ((m.x - pentablev2left) / pixnum + 6) * 10 + (m.y - pentablev2top) / pixnum; // 计算 map 的二位数代号
printf("pentake2: %d\n", pentake);
}
break;
case WM_LBUTTONUP:
drawflag = 0;
drawsmallflag = 0;
olddrawi = -1;
olddrawj = -1;
drawoldmx = -1; // 清除坐标记录,保证下次按键一定绘制
drawoldmy = -1;
break;
case WM_RBUTTONDOWN: // 鼠标右键拖动
if (draftflag == 0 && m.x > bkdeskleft && m.y > bkdesktop && m.x < bkdeskleft + 270 && m.y < bkdesktop + 270)
{
draftflag = 1;
draftoldmx = m.x; // 记录鼠标坐标
draftoldmy = m.y;
mousex = m.x;
mousey = m.y;
draftoldgamex = bkgameleft; // 记录游戏背景寄存区左上角坐标
draftoldgamey = bkgametop;
}
break;
case WM_RBUTTONUP:
draftflag = 0;
bkgameleft = draftoldgamex - (m.x - draftoldmx); // bkgameleft - draftoldgamex =- (m.x - draftoldmx)
bkgametop = draftoldgamey - (m.y - draftoldmy); // bkgametop - draftoldgamey =- (m.y - draftoldmy)
mousex = -1; // draft 和 draw 共用 mousex mousey
mousey = -1;
break;
case WM_KEYDOWN:
switch (m.vkcode) // 键盘移动控制
{
case 0x41: // A
if (flag_x - speed > -10) // 限制范围,减少内存读写
flag_x -= speed;
characterflag = 1;
break;
case 0x57: // W
if (flag_y - speed > -10)
flag_y -= speed;
characterflag = 2;
break;
case 0x44: // D
if (flag_x + speed < 10)
flag_x += speed;
characterflag = 3;
break;
case 0x53: // S
moveflag = 1;
if (flag_y + speed < 10)
flag_y += speed;
characterflag = 4;
break;
case VK_F1:
savegamemap(map, mapi, mapj, pentable, pentablev2, pixnum, imagenum);
break;
case VK_F2:
loadgamemap(map, &mapi, &mapj, pentable, &pixnum, &imagenum);
gamelimitright = pixnum * safemapi;
gamelimitbuttom = pixnum * safemapj;
for (int i = 0; i < imagenum; i++) // 刷新绘图板颜色
{
pentable[i] = IMAGE(30, 30);
SetWorkingImage(&pentable[i]); // 给调色板绘制颜色
setfillcolor(RGB(i * 20, i * 20, i * 20)); // 这里初始化调色盘的颜色
fillrectangle(-1, -1, pixnum, pixnum); // 在调色板上绘制颜色(纹理)要从 -1,-1 开始绘制,把边框画到外部,不保留边框。
}
SetWorkingImage();
freshmesh(bkmesh, map, pentable, pentablev2, bkmeshmapi, bkmeshmapj, 27, pixnum); // 刷新,重新映射,其实就是开头初始化的代码,这里是给了一个封装示例,但未进行性能优化
freshbk(bk, bkmesh, bkgameleft, bkgametop, bkmeshmapi, bkmeshmapj, 9, pixnum);
showbk(bk, bkdeskleft, bkdesktop);
showbkmesh(bkmesh, bkmeshdeskleft, bkmeshdesktop);
break;
}
break;
case WM_KEYUP:
switch (m.vkcode)
{
case 0x41: // A
flag_x = 0;
break;
case 0x57: // W
flag_y = 0;
break;
case 0x44: // D
flag_x = 0;
break;
case 0x53: // S
flag_y = 0;
break;
}
if (flag_x == 0 && flag_y == 0)
characterflag = 5;
break;
case WM_MOUSEMOVE:
if (mousex != m.x || mousey != m.y)
{
mousex = m.x;
mousey = m.y;
}
break;
}
}
// 开始根据指令运行坐标变化
if (draftflag == 1)
{
if (flag_x != 0) // 实现拖拽时键盘也能控制移动
draftoldgamex += flag_x;
if (flag_y != 0)
draftoldgamey += flag_y;
bkgameleft = draftoldgamex - (mousex - draftoldmx); // bkgameleft-draftoldgamex=-(mousex-draftoldmx)
bkgametop = draftoldgamey - (mousey - draftoldmy); // bkgametop-draftoldgamey=-(mousey - draftoldmy)
if (drawflag == 1 && mousex > bkdeskleft && mousey > bkdesktop && mousex < bkdeskleft + 300 && mousey < bkdesktop + 300) // 实现边拖拽边移动边绘制
{
drawx = bkgameleft + (mousex - bkdeskleft); // drawx-bkgameleft=m.x-bkdeskleft 横坐标方向移动距离相同
drawy = bkgametop + (mousey - bkdesktop); // drawy-bkgametop=m.y-bkdesktop 纵坐标方向移动距离相同
i = drawy / pixnum;
j = drawx / pixnum;
if (olddrawi != i || olddrawj != j)
{
if (i > safemapi) // map 数组越界检测
i = safemapi;
else if (i < 0)
i = 0;
if (j > safemapj)
j = safemapj;
else if (j < 0)
j = 0;
drawsmallflag = 1;
map[i][j] = pentake; // 注意 map[y][x],而不是 map[x][y],因为判断第几行,是通过 y 来控制上下移动的,判断第几列,是通过 x 左右移动的。
olddrawi = i;
olddrawj = j;
}
else
{
drawsmallflag = 0; // 检测到是上一次绘制的瓦片,则不再刷新贴图与缓冲区。
}
}
}
else if (drawflag == 1 && flag_x == 0 && flag_y == 0 && (drawoldmx != mousex || drawoldmy != mousey) && mousex > bkdeskleft && mousey > bkdesktop && mousex < bkdeskleft + 300 && mousey < bkdesktop + 300)
{
// 注意不要越界,否则 gamex 为负数,导致数组越界闪退。
// 通过实现坐标变换与赋值达到修改游戏大地图(数组)
moveflag = 0;
drawoldmx = mousex;
drawoldmy = mousey;
drawx = bkgameleft + (mousex - bkdeskleft); // drawx-bkgameleft=mousex-bkdeskleft 横坐标方向移动距离相同
drawy = bkgametop + (mousey - bkdesktop); // drawy-bkgametop=mousey-bkdesktop 纵坐标方向移动距离相同
i = drawy / pixnum;
j = drawx / pixnum;
if (olddrawi != i || olddrawj != j)
{
drawsmallflag = 1;
map[i][j] = pentake; // 注意 map[y][x],而不是 map[x][y],因为判断第几行,是通过 y 来控制上下移动的,判断第几列,是通过 x 左右移动的。
olddrawi = i;
olddrawj = j;
}
else
{
drawsmallflag = 0; // 检测到是上一次绘制的瓦片,则不再刷新贴图与缓冲区。
}
} // 对绘制进行分类计算数据,剥离特殊情况的重复绘制,仅仅是 flag_x,或者 flag_y 不为零时取消重复绘制判断
else if (drawflag == 1 && mousex > bkdeskleft && mousey > bkdesktop && mousex < bkdeskleft + 300 && mousey < bkdesktop + 300)
{
moveflag = 1;
bkgameleft += flag_x; // 更新游戏背景寄存区左上角坐标
bkgametop += flag_y;
drawx = bkgameleft + (mousex - bkdeskleft); // drawx-bkgameleft=m.x-bkdeskleft 横坐标方向移动距离相同
drawy = bkgametop + (mousey - bkdesktop); // drawy-bkgametop=m.y-bkdesktop 纵坐标方向移动距离相同
i = drawy / pixnum;
j = drawx / pixnum;
if (olddrawi != i || olddrawj != j)
{
drawsmallflag = 1;
map[i][j] = pentake; // 注意 map[y][x],而不是 map[x][y],因为判断第几行,是通过 y 来控制上下移动的,判断第几列,是通过 x 左右移动的。
olddrawi = i;
olddrawj = j;
}
else
{
drawsmallflag = 0; // 检测到是上一次绘制的瓦片,则不再刷新贴图与缓冲区。
}
}
else if (drawflag == 0 && flag_x != 0 || flag_y != 0)
{
moveflag = 1;
bkgameleft += flag_x;
bkgametop += flag_y;
}
else
{
// 既不绘制也不移动也不拖拽
}
// 根据计算出的坐标数据进行绘制,分多种情况分别绘制,减少函数重复调用与无效调用
if (drawsmallflag == 1 && moveflag == 0)
{
freshmesh(bkmesh, map, pentable, pentablev2, bkmeshmapi, bkmeshmapj, 27, 30); // 刷新,重新映射,其实就是开头初始化的代码,这里是给了一个封装示例,但未进行性能优化
freshbk(bk, bkmesh, bkgameleft, bkgametop, bkmeshmapi, bkmeshmapj, 9, 30);
showbk(bk, bkdeskleft, bkdesktop);
showbkmesh(bkmesh, bkmeshdeskleft, bkmeshdesktop);
}
else if (moveflag == 1 && drawsmallflag == 1)
{
checkboundary(&bkgameleft, &bkgametop, gamelimitright, gamelimitbuttom, &bkmeshgameleft, &bkmeshgametop, &meshlimitright, &meshlimitbuttom, 270, 540);
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
freshmesh(bkmesh, map, pentable, pentablev2, bkmeshmapi, bkmeshmapj, 27, 30);
freshbk(bk, bkmesh, bkgameleft, bkgametop, bkmeshmapi, bkmeshmapj, 9, 30);
showbkmesh(bkmesh, bkmeshdeskleft, bkmeshdesktop);
showbk(bk, bkdeskleft, bkdesktop);
}
else if (moveflag == 1 || draftflag) // 分类渲染, drawflag==0 时,再选择性刷新缓冲区
{
checkboundary(&bkgameleft, &bkgametop, gamelimitright, gamelimitbuttom, &bkmeshgameleft, &bkmeshgametop, &meshlimitright, &meshlimitbuttom, 270, 540);
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
if (oldbkmeshgamex != bkmeshgameleft || oldbkmeshgamey != bkmeshgametop) // 判断是否更新采样区
{
freshmesh(bkmesh, map, pentable, pentablev2, bkmeshmapi, bkmeshmapj, 27, 30);
oldbkmeshgamex = bkmeshgameleft;
oldbkmeshgamey = bkmeshgametop;
showbkmesh(bkmesh, bkmeshdeskleft, bkmeshdesktop);
}
freshbk(bk, bkmesh, bkgameleft, bkgametop, bkmeshmapi, bkmeshmapj, 9, 30);
showbk(bk, bkdeskleft, bkdesktop);
}
animation(characterflag, bkgameleft, bkgametop, bkmeshgameleft, bkmeshgametop, bkmeshdeskleft, bkmeshdesktop);
Sleep(2); // 休眠 20 毫秒,减少 CPU 占用
}
return 0;
}
// 保存游戏地图的全部数据
void savegamemap(int** map, int gamemapi, int gamemapj, IMAGE* pentable, IMAGE*** pentablev2, int pixnum, int imagenum)
{
FILE* fp;
int i = 0;
int j = 0;
char dirpath[400] = { '\0' };
char filepath[400] = { '\0' };
for (i = 0; i < 100; i++)
{
sprintf(dirpath, "DLC%d", i);
if (access(dirpath, 0) == -1) // 检查 DLC 是否存在,不存在为 -1
{
break;
}
}
mkdir(dirpath); // 创建文件夹
char filename[400] = "gamemap.txt";
const char* next = "./";
strcat(filepath, dirpath);
strcat(filepath, next);
strcat(filepath, filename);
fp = fopen(filepath, "w+"); // 创建 .txt 文件
fprintf(fp, "注意此导出的游戏文件为按键 F1 后自动生成 修改汉语字符或者英文字符 或导致该 .txt 文件不可读取\n");
fprintf(fp, "warning this saved gamefile is automatically create after F1 pressed change Chinesse character or English character lead to read failed");
fprintf(fp, "pixnum %d\n", pixnum); // 正方形瓦片贴图边长信息:单位:像素
fprintf(fp, "imagenum %d\n", imagenum); // 瓦片个数
char imageindix[400] = { '\0' };
for (i = 0; i < imagenum; i++)
{
sprintf(imageindix, "tile_%d.png", i);
fprintf(fp, "%s\n", imageindix);
}
fprintf(fp, "gamemapi %d gamemapj %d\n", gamemapi, gamemapj);
for (i = 0; i < gamemapi; i++)
{
fprintf(fp, "%d", map[i][j]);
for (j = 0; j < gamemapj; j++)
{
fprintf(fp, " %d", map[i][j]);
}
fprintf(fp, "\n");
}
fclose(fp);
char imagename[400] = { '\0' };
char imagepath[400] = { '\0' };
for (i = 0; i < imagenum; i++)
{
sprintf(imagename, "tile_%d.png", i); // 数字转字符串
strcpy(imagepath, dirpath); // 函数直接从头开始粘贴,自动清除开头的字符
strcat(imagepath, next);
strcat(imagepath, imagename);
saveimage(imagepath, &pentable[i]); // 批量导出贴图
}
savepersonalfile(dirpath, pentablev2); // 创建默认文件夹并保存自定义贴图
}
// 读取 DLC 继续开发
void loadgamemap(int** map, int* gamemapi, int* gamemapj, IMAGE* pentable, int* pixnum, int* imagenum)
{
FILE* fp;
int i = 0;
int j = 0;
char dirpath[400] = { '\0' };
char filepath[400] = { '\0' };
for (i = 0; i < 100; i++)
{
sprintf(dirpath, "DLC%d", i);
if (access(dirpath, 0) == 0) // 检查 DLC 是否存在,存在为 0
{
break;
}
}
if (i == 100) // 100 次查找失败,则返回,不再读取
return;
char filename[400] = "gamemap.txt";
const char* next = "./";
strcat(filepath, dirpath);
strcat(filepath, next);
strcat(filepath, filename);
fp = fopen(filepath, "r"); // 读取 .txt 文件
fscanf(fp, "注意此导出的游戏文件为按键 F1 后自动生成 修改汉语字符或者英文字符 或导致该 .txt 文件不可读取\n");
fscanf(fp, "warning this saved gamefile is automatically create after F1 pressed change Chinesse character or English character lead to read failed");
fscanf(fp, "pixnum %d\n", pixnum); // 正方形瓦片贴图边长信息:单位:像素
fscanf(fp, "imagenum %d\n", imagenum); // 瓦片个数
char imageindix[400] = { '\0' };
char imagename[400] = { '\0' };
char imagepath[400] = { '\0' };
for (i = 0; i < *imagenum; i++)
{
fscanf(fp, "%s\n", imageindix);
strcpy(imagepath, dirpath);
strcat(imagepath, next);
strcat(imagepath, imageindix);
loadimage(&pentable[i], imagepath, *pixnum, *pixnum, false); // 批量导入贴图
}
fscanf(fp, "gamemapi %d gamemapj %d\n", gamemapi, gamemapj);
for (i = 0; i < *gamemapi; i++)
{
fscanf(fp, "%d", &map[i][j]);
for (j = 0; j < *gamemapj; j++)
{
fscanf(fp, " %d", &map[i][j]);
}
fscanf(fp, "\n");
}
fclose(fp);
}
// 检查边界
inline void checkboundary(int* bkgameleft, int* bkgametop, int gamelimitright, int gamelimitbuttom, int* bkmeshgameleft, int* bkmeshgametop, int* meshlimitright, int* meshlimitbuttom, int move, int size)
{
if (*bkgameleft < 0) // 网格越界检测并调整
*bkgameleft = 0;
else if (*bkgameleft > gamelimitright)
*bkgameleft = gamelimitright;
if (*bkgametop < 0)
*bkgametop = 0;
else if (*bkgametop > gamelimitbuttom)
*bkgametop = gamelimitbuttom;
if (*bkgameleft < *bkmeshgameleft) // 更新游戏采样区坐标,一些简单换算,由于频繁调用函数在这里产生了明显的卡顿影响,所以这里就不再封装成函数
{
*bkmeshgameleft -= move;
*meshlimitright = *bkmeshgameleft + size;
}
else if (*bkgameleft > *meshlimitright)
{
*meshlimitright += move;
*bkmeshgameleft = *meshlimitright - size;
}
if (*bkgametop < *bkmeshgametop)
{
*bkmeshgametop -= move;
*meshlimitbuttom = *bkmeshgametop + size;
}
else if (*bkgametop > *meshlimitbuttom)
{
*meshlimitbuttom += move;
*bkmeshgametop = *meshlimitbuttom - size;
}
}
// 性能:CPU 2% 占有率,峰值 1.20GHz,核显 GPU 10% 使用率
inline void animation(int characterflag, int bkgameleft, int bkgametop, int bkmeshgameleft, int bkmeshgametop, int bkmeshdeskleft, int bkmeshdesktop)
{
static int i = 1;
static int x = 10;
static int y = 10;
if (i % 3 == 0)
{
x = bkmeshdeskleft + bkgameleft - bkmeshgameleft;
y = bkmeshdesktop + bkgametop - bkmeshgametop;
i %= 125; // i 的上限 * 放大倍数 不超过 255
switch (characterflag) // 选取颜色
{
case 1:
setfillcolor(RGB(i * 2, i * 2, 0));
break;
case 2:
setfillcolor(RGB(i * 2, 0, i * 2));
break;
case 3:
setfillcolor(RGB(i * 2, 0, 0));
break;
case 4:
setfillcolor(RGB(0, i * 2, 0));
break;
case 5: // 如果没有按键,则使用默认颜色
setfillcolor(RGB(0, 0, i * 2));
break;
}
fillrectangle(x, y, x + 30, y + 30);
}
i++;
}
// 从默认文件夹中读取自定义图片
void loadfile(IMAGE*** pentablev2)
{
char brick[100] = "aa_birck_6_family";
char ground[100] = "ab_ground_7_family";
char change[100] = "ac_change_8_family";
char trick[100] = "ad_trick_9_family";
char next[100] = "./";
char png[100] = "*.png";
loadfile_scanf(brick, next, png, pentablev2[0]); // 加载用于当砖头类型的图片
loadfile_scanf(ground, next, png, pentablev2[1]);
loadfile_scanf(change, next, png, pentablev2[2]);
loadfile_scanf(trick, next, png, pentablev2[3]);
}
// 加载自定义图片
void loadfile_scanf(char* dirname, char* next, char* kind, IMAGE** pentablev2)
{
// 文件存储信息结构体
struct _finddata_t fileinfo;
// 保存文件句柄
long fHandle;
// 文件数记录器
char dirpath[100];
strcpy(dirpath, dirname);
if (access(dirpath, 0) == -1) // 检查文件夹是否存在,不存在为 -1
{
mkdir(dirpath);
return;
}
else
{
int i = -1; // 记录文件数量
char kindpath[100];
strcpy(kindpath, dirpath);
strcat(kindpath, next);
strcat(kindpath, kind);
if ((fHandle = _findfirst(kindpath, &fileinfo)) == -1L) // *是通配符,默认在当前文件夹内查找文件,这里查找 .png 文件
{
printf("当前目录: %s 下没有所需文件\n", dirname);
return;
}
else
{
char path[100];
do
{
i++;
printf("在%s 下找到文件:%s,文件大小:%d bit\n", dirname, fileinfo.name, fileinfo.size);
strcpy(path, dirname);
strcat(path, next);
strcat(path, fileinfo.name);
while (pentablev2[i] != NULL)
{
i++;
}
pentablev2[i] = new IMAGE(30, 30);
loadimage(pentablev2[i], path, 30, 30); // 根据名称读取文件
} while (_findnext(fHandle, &fileinfo) == 0);
}
// 关闭文件
_findclose(fHandle);
printf("文件数量:%d\n", i + 1);
}
}
// 在目标 DLC 里创建四个文件夹并保存
void savepersonalfile(char* DLC, IMAGE*** pentablev2)
{
char brick[100] = "aa_birck_6_family";
char ground[100] = "ab_ground_7_family";
char change[100] = "ac_change_8_family";
char trick[100] = "ad_trick_9_family";
char next[100] = "./";
char png[100] = ".png";
if (access(DLC, 0) == -1) // 检查文件夹是否存在,不存在为 -1
{
mkdir(DLC);
}
char sign[10] = {};
strcpy(sign, "6");
savelocal(DLC, brick, next, png, sign, pentablev2[0]);
strcpy(sign, "7");
savelocal(DLC, ground, next, png, sign, pentablev2[1]);
strcpy(sign, "8");
savelocal(DLC, change, next, png, sign, pentablev2[2]);
strcpy(sign, "9");
savelocal(DLC, trick, next, png, sign, pentablev2[3]);
}
// 保存自定义文件
void savelocal(char* DLC, char* dirname, char* next, char* kind, char* sign, IMAGE** files)
{
char dirpath[100] = "";
strcpy(dirpath, DLC); // 字符串拼接出相对路径
strcat(dirpath, next);
strcat(dirpath, dirname);
if (access(dirpath, 0) == -1) // 检查 DLC 内的指定文件夹是否存在,不存在为 -1
{
mkdir(dirpath);
}
char filepath[100];
char filepa[100];
char num[10];
strcpy(filepath, dirpath); // 字符串拼接出文件名前缀
strcat(filepath, next);
strcat(filepath, sign);
for (int i = 0; i < 10; i++)
{
sprintf(num, "%d", i);
strcpy(filepa, filepath); // 从头粘贴,可以覆盖之前的信息,不受上一次保存的影响
strcat(filepa, num);
strcat(filepa, kind);
if (files[i] != NULL)
{
saveimage(filepa, files[i]);
printf("file save %s\n", filepa);
}
}
}
第二部分:这里是瓦片地图编辑器导出 DLC 文件夹之后,提供的自动导入示例代码,复制粘贴即可运行
附:这里只实现了地图编辑器打包导出的文件夹 DLC 的一键读取和视口移动,其他游戏功能打算参考第三部分代码,但是自行设计增加没头绪,需要临门一脚。
////////////////////////////////////////////
// 程序:DLC 自动读取加载框架示例
// 作者:民用级脑的研发记录
// 邮箱:1309602336@qq.com
// 编译环境:Devc++/VC 2010/Visual Studio 2022,EasyX_20220901/Easyx_2023 大暑版
// 编写日期:2024-2-22
//
#undef UNICODE
#undef _UNICODE
#pragma warning(disable : 4996) // VS2022 对某些函数警告,但是为了方便移植,就无视这些警告,这样 Devc++ VS2022 都能跑
#include <graphics.h>
#include <stdio.h>
#include <string.h>
#include <io.h>
#include <direct.h>
// 初始化地图
int** initmap(int wide, int high);
// 从默认文件夹中读取自定义图片
void loadfile(char* DLC, IMAGE*** pentablev2);
// 加载自定义图片
void loadfile_scanf(char* dirname, char* next, char* kind, IMAGE** pentablev2);
// 导入游戏地图贴图数据
void loadgamemap(int** map, int* gamemapi, int* gamemapj, IMAGE* pentable, IMAGE*** pentablev2, int* pixnum, int* imagenum);
// 刷新网格缓冲区
inline void freshmesh(IMAGE* bkmesh, int** map, IMAGE* pentable, IMAGE*** pentablev2, int bkmeshmapi, int bkmeshmapj, int tilenum, int pixnum);
// 在屏幕上显示缓冲区
inline void showbkmesh(IMAGE* bkmesh, int bkmeshdeskx, int bkmeshdesky);
// 在纹理映射函数中产生的图片中截图,但此为演示参数作用,此处并未优化。
inline void freshbk(IMAGE* bk, IMAGE* bkmesh, int gamex, int gamey, int bkmeshmapi, int bkmeshmapj, int tilenum, int pixnum);
// 在屏幕显示截图
inline void showbk(IMAGE* bk, int bkdeskx, int bkdesky);
// 检查边界
inline void checkboundary(int* bkgameleft, int* bkgametop, int gamelimitright, int gamelimitbuttom, int* bkmeshgameleft, int* bkmeshgametop, int* meshlimitright, int* meshlimitbuttom, int move, int size);
// 实时渐变色小动画
inline void animation(int chararcterflag, int bkgameleft, int bkgametop, int bkmeshgameleft, int bkmeshgametop, int bkmeshdeskleft, int bkmeshdesktop);
int main()
{
initgraph(1600, 800);
setbkcolor(GREEN);
cleardevice();
IMAGE* bk; // 背景图片寄存区
bk = new IMAGE(270, 270);
IMAGE* bkmesh; // 背景图片采样区
bkmesh = new IMAGE(270 * 3, 270 * 3);
int** map; // 游戏大地图数组,记录着整个游戏背景的贴图信息,而在运行过程中,选取部分区域的数字,对照序号与贴图,实现游戏背景绘制。其余没有选中的区域就是压缩的空间。
int pixnum; // 一个正方形瓦片的边长。单位:像素
int bkgameleft; // 背景图片寄存区左上角坐标,是在游戏里的像素坐标。(0,0)可以理解为游戏大地图的左上角顶点。
int bkgametop;
int bkmeshgameleft; // 背景图片采样区左上角坐标,是在游戏里的像素坐标。
int bkmeshgametop;
int bkmeshmapi; // 背景图片采样区左上角所对应的 map 数组序号。从 map[0][0]开始,按照 map[i][j],其中 bkmeshmapi=bkmeshtop/pixnum
int bkmeshmapj;
int bkdeskleft; // 规定在屏幕上显示游戏背景寄存区,此处记录其左上角在屏幕上的像素坐标
int bkdesktop;
int bkmeshdeskleft; // 规定在屏幕上显示游戏背景采样区,此处记录其左上角在屏幕上的像素坐标
int bkmeshdesktop;
pixnum = 30; // 进行初始化,规定各位置具体数字
bkdeskleft = 200; // 游戏背景左上角将会在屏幕的(200,200) 处
bkdesktop = 200;
bkgameleft = 0; // 由于 bkgame 控制 mesh 坐标移动,(0,0)则游戏背景完全在当前采样区移动
bkgametop = 0;
bkmeshdeskleft = 700; // 游戏背景缓冲区左上角将会在屏幕的(700,0)处
bkmeshdesktop = 0;
bkmeshgameleft = 0;
bkmeshgametop = 0;
int meshlimitright; // 使用变量暂存边界,不用每次来回计算边界
int meshlimitbuttom;
meshlimitright = 270; // 和寄存区一样大,把寄存区限制在一个九宫格中心,如果越出九宫格,九宫格整体平移
meshlimitbuttom = 270;
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
int mapi; // 读取 DLC 文件,需要变量记录循环次数,单位 瓦片贴图个数
int mapj;
int safemapi; // 一个安全的贴图范围,避免阅读超过 10000 导致的越界闪退问题。
int safemapj;
int gamelimitright; // 用于减少比较时的计算,存储边界范围
int gamelimitbuttom;
safemapi = 70; // 注意导出的是左上角开始的 100*100 部分,如果绘制出界,保存不了,需要改这一行和下一行的数字
safemapj = 70;
gamelimitright = pixnum * safemapi;
gamelimitbuttom = pixnum * safemapj;
mapi = 300;
mapj = 300;
int imagenum; // 调色板数量上限,用于文件读取控制循环次数
IMAGE* pentable; // 调色板其实就是贴图数组
imagenum = 10;
int pentablev2high; // 自定义贴图的容量大小
int pentablev2wide;
pentablev2high = 10;
pentablev2wide = 4;
IMAGE*** pentablev2;
map = initmap(mapi, mapj);
pentable = new IMAGE[imagenum];
for (int i = 0; i < imagenum; i++)
{
pentable[i] = IMAGE(30, 30);
SetWorkingImage(&pentable[i]); // 给调色板绘制颜色
setfillcolor(RGB(i * 20, i * 20, i * 20)); // 这里初始化调色盘的颜色
fillrectangle(-1, -1, pixnum, pixnum); // 在调色板上绘制颜色(纹理)要从 -1,-1 开始绘制,把边框画到外部,不保留边框。
}
pentablev2 = new IMAGE * *[pentablev2wide];
for (int i = 0; i < pentablev2wide; i++)
pentablev2[i] = new IMAGE * [pentablev2high];
for (int i = 0; i < pentablev2wide; i++)
for (int j = 0; j < pentablev2high; j++)
pentablev2[i][j] = NULL;
loadgamemap(map, &safemapi, &safemapj, pentable, pentablev2, &pixnum, &imagenum); // 加载游戏
freshmesh(bkmesh, map, pentable, pentablev2, bkmeshmapi, bkmeshmapj, 27, pixnum);
showbkmesh(bkmesh, bkmeshdeskleft, bkmeshdesktop);
freshbk(bk, bkmesh, bkgameleft, bkgametop, bkmeshmapi, bkmeshmapj, 9, pixnum);
showbk(bk, bkdeskleft, bkdesktop);
int moveflag; // 是否键盘控制移动
int flag_x; // 记录位移
int flag_y;
int speed; // 键盘控制视口的移动速度
speed = 5;
flag_x = 0;
flag_y = 0;
moveflag = 0;
int oldbkmeshgamex; // 判断是否需要刷新 bkmesh 网格
int oldbkmeshgamey;
oldbkmeshgamex = -1;
oldbkmeshgamey = -1;
int characterflag; // 用于按键时游戏小人的颜色变化
characterflag = 5;
ExMessage m;
while (1)
{
while (peekmessage(&m, EX_KEY | EX_MOUSE))
{
switch (m.message)
{
case WM_KEYDOWN:
switch (m.vkcode) // 键盘移动控制
{
case 0x41: // A
if (flag_x - speed > -10) // 限制范围,减少内存读写
flag_x -= speed;
characterflag = 1;
break;
case 0x57: // W
if (flag_y - speed > -10)
flag_y -= speed;
characterflag = 2;
break;
case 0x44: // D
if (flag_x + speed < 10)
flag_x += speed;
characterflag = 3;
break;
case 0x53: // S
moveflag = 1;
if (flag_y + speed < 10)
flag_y += speed;
characterflag = 4;
break;
}
break;
case WM_KEYUP:
switch (m.vkcode)
{
case 0x41: // A
flag_x = 0;
break;
case 0x57: // W
flag_y = 0;
break;
case 0x44: // D
flag_x = 0;
break;
case 0x53: // S
flag_y = 0;
break;
}
if (flag_x == 0 && flag_y == 0)
characterflag = 5;
break;
}
}
if (flag_x != 0 || flag_y != 0)
{
moveflag = 1;
bkgameleft += flag_x;
bkgametop += flag_y;
}
else
{
moveflag = 0;
}
if (moveflag == 1)
{
checkboundary(&bkgameleft, &bkgametop, gamelimitright, gamelimitbuttom, &bkmeshgameleft, &bkmeshgametop, &meshlimitright, &meshlimitbuttom, 270, 540);
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
if (oldbkmeshgamex != bkmeshgameleft || oldbkmeshgamey != bkmeshgametop) // 判断是否更新采样区
{
freshmesh(bkmesh, map, pentable, pentablev2, bkmeshmapi, bkmeshmapj, 27, 30);
oldbkmeshgamex = bkmeshgameleft;
oldbkmeshgamey = bkmeshgametop;
showbkmesh(bkmesh, bkmeshdeskleft, bkmeshdesktop);
}
freshbk(bk, bkmesh, bkgameleft, bkgametop, bkmeshmapi, bkmeshmapj, 9, 30);
showbk(bk, bkdeskleft, bkdesktop);
}
animation(characterflag, bkgameleft, bkgametop, bkmeshgameleft, bkmeshgametop, bkmeshdeskleft, bkmeshdesktop); // 小方块动画
Sleep(1);
}
return 0;
}
// 初始化游戏地图
int** initmap(int wide, int high)
{
int** map = new int* [high]; // 二维数组动态初始化,先给二级指针挂上一个长度为 10 的指针数组
for (int i = 0; i < high; i++)
{
map[i] = new int[wide]; // 然后数组里的每个指针都挂上一个长度为 10 的 int 类型数组
}
for (int i = 0; i < high; i++)
{
for (int j = 0; j < wide; j++)
{
map[i][j] = -1; // 初始化游戏大地图 map 的参数,参数 1 默认黑色
}
}
return map;
}
// 读取 DLC 继续开发
void loadgamemap(int** map, int* gamemapi, int* gamemapj, IMAGE* pentable, IMAGE*** pentablev2, int* pixnum, int* imagenum)
{
FILE* fp;
int i = 0;
int j = 0;
char dirpath[400] = { '\0' };
char filepath[400] = { '\0' };
for (i = 0; i < 100; i++)
{
sprintf(dirpath, "DLC%d", i);
if (access(dirpath, 0) == 0) // 检查 DLC 是否存在,存在为 0
{
break;
}
}
if (i == 100) // 100 次查找失败,则返回,不再读取
return;
char filename[400] = "gamemap.txt";
const char* next = "./";
strcat(filepath, dirpath);
strcat(filepath, next);
strcat(filepath, filename);
fp = fopen(filepath, "r"); // 读取 .txt 文件
fscanf(fp, "注意此导出的游戏文件为按键 F1 后自动生成 修改汉语字符或者英文字符 或导致该 .txt 文件不可读取\n");
fscanf(fp, "warning this saved gamefile is automatically create after F1 pressed change Chinesse character or English character lead to read failed");
fscanf(fp, "pixnum %d\n", pixnum); // 正方形瓦片贴图边长信息:单位:像素
fscanf(fp, "imagenum %d\n", imagenum); // 瓦片个数
char imageindix[400] = { '\0' };
char imagepath[400] = { '\0' };
for (i = 0; i < *imagenum; i++)
{
fscanf(fp, "%s\n", imageindix);
strcpy(imagepath, dirpath);
strcat(imagepath, next);
strcat(imagepath, imageindix);
loadimage(&pentable[i], imagepath, *pixnum, *pixnum, false); // 批量导入贴图
}
fscanf(fp, "gamemapi %d gamemapj %d\n", gamemapi, gamemapj);
for (i = 0; i < *gamemapi; i++)
{
fscanf(fp, "%d", &map[i][j]);
for (j = 0; j < *gamemapj; j++)
{
fscanf(fp, " %d", &map[i][j]);
}
fscanf(fp, "\n");
}
fclose(fp);
printf("导入 map 部分正常\n");
loadfile(dirpath, pentablev2);
}
// 从默认文件夹中读取自定义图片
void loadfile(char* DLC, IMAGE*** pentablev2)
{
char brick[100] = "aa_birck_6_family";
char ground[100] = "ab_ground_7_family";
char change[100] = "ac_change_8_family";
char trick[100] = "ad_trick_9_family";
char next[100] = "./";
char png[100] = "*.png";
char dirpath[100] = {};
strcpy(dirpath, DLC);
strcat(dirpath, next);
strcat(dirpath, brick);
loadfile_scanf(dirpath, next, png, pentablev2[0]); // 加载用于当砖头类型的图片
strcpy(dirpath, DLC);
strcat(dirpath, next);
strcat(dirpath, ground);
loadfile_scanf(dirpath, next, png, pentablev2[1]);
strcpy(dirpath, DLC);
strcat(dirpath, next);
strcat(dirpath, change);
loadfile_scanf(dirpath, next, png, pentablev2[2]);
strcpy(dirpath, DLC);
strcat(dirpath, next);
strcat(dirpath, trick);
loadfile_scanf(dirpath, next, png, pentablev2[3]);
}
// 加载自定义图片
void loadfile_scanf(char* dirname, char* next, char* kind, IMAGE** pentablev2)
{
// 文件存储信息结构体
struct _finddata_t fileinfo;
// 保存文件句柄
long fHandle;
// 文件数记录器
char dirpath[100];
strcpy(dirpath, dirname);
if (access(dirpath, 0) == -1) // 检查文件夹是否存在,不存在为 -1
{
return;
}
else
{
int i = -1; // 记录文件数量
char kindpath[100];
strcpy(kindpath, dirpath);
strcat(kindpath, next);
strcat(kindpath, kind);
if ((fHandle = _findfirst(kindpath, &fileinfo)) == -1L) // *是通配符,默认在当前文件夹内查找文件,这里查找 .png 文件
{
printf("当前目录: %s 下没有所需文件\n", dirname);
return;
}
else
{
char path[100];
do
{
i++;
printf("在%s 下找到文件:%s,文件大小:%d bit\n", dirname, fileinfo.name, fileinfo.size);
strcpy(path, dirname);
strcat(path, next);
strcat(path, fileinfo.name);
while (pentablev2[i] != NULL)
{
i++;
}
pentablev2[i] = new IMAGE(30, 30);
loadimage(pentablev2[i], path, 30, 30); // 根据名称读取文件
} while (_findnext(fHandle, &fileinfo) == 0);
}
// 关闭文件
_findclose(fHandle);
printf("文件数量:%d\n", i + 1);
}
}
// 使用关键字 inline 声明为内联函数,减少贴图函数频繁调用的开销导致的卡顿。
// 缓冲区纹理映射函数:bkmesh 映射目标,map 映射总网格,pentable:纹理集,bkmeshmapi,bkmeshmapj:映射起始点,tilenum:横,纵映射的数量,pixnum:一个映射块的边长,单位:像素。
inline void freshmesh(IMAGE* bkmesh, int** map, IMAGE* pentable, IMAGE*** pentablev2, int bkmeshmapi, int bkmeshmapj, int tilenum, int pixnum)
{
int kind = 0; // 存储代号第一位:类型
int number = 0; // 存储代号第二位:序列号
int pennumber = -1; // 暂存每一次循环的映射代号
IMAGE* pen = NULL; // 所找到的纹理
int left = 0; // 这是每次循环所找到的纹理对应映射地址
int top = 0;
SetWorkingImage(bkmesh); // 设置绘图目标为游戏背景采样区,刷新采样区,刷新寄存区
setbkcolor(RGB(200, 200, 200));
cleardevice();
for (int i = bkmeshmapi; i < bkmeshmapi + tilenum; i++)
{
left = 0;
for (int j = bkmeshmapj; j < bkmeshmapj + tilenum; j++)
{
pennumber = map[i][j]; // 读取游戏大地图数组序号
if (pennumber == -1)
{
rectangle(left, top, left + pixnum, top + pixnum);
}
else
{
kind = pennumber / 10 - 6; // 剥离第一位
number = pennumber % 10; // 剥离最后一位
if (pennumber < 10)
pen = &pentable[pennumber];
else
pen = pentablev2[kind][number]; // 根据序号查找对应贴图
putimage(left, top, pen); // 把贴图画到采样区
}
left += pixnum; // 往右移动,准备下一次绘制位置,此处贴图就会覆盖白色边框。为保证坐标变换和网格对应,算上网格宽度,也在贴图矩形内
}
top += pixnum; // 往下移动,准备下一次绘制位置,此处就会覆盖白色边框,方便定位
}
SetWorkingImage();
}
// 在纹理映射函数中产生的图片中截图,但此为演示参数作用,此处并未优化。
inline void freshbk(IMAGE* bk, IMAGE* bkmesh, int gamex, int gamey, int bkmeshmapi, int bkmeshmapj, int tilenum, int pixnum)
{
SetWorkingImage(bkmesh);
getimage(bk, gamex - bkmeshmapj * pixnum, gamey - bkmeshmapi * pixnum, tilenum * pixnum, tilenum * pixnum);
SetWorkingImage();
}
// 在屏幕显示截图
inline void showbk(IMAGE* bk, int bkdeskx, int bkdesky)
{
SetWorkingImage();
putimage(bkdeskx, bkdesky, bk);
}
// 在屏幕上显示缓冲区
inline void showbkmesh(IMAGE* bkmesh, int bkmeshdeskx, int bkmeshdesky)
{
SetWorkingImage();
putimage(bkmeshdeskx, bkmeshdesky, bkmesh);
}
// 检查边界
inline void checkboundary(int* bkgameleft, int* bkgametop, int gamelimitright, int gamelimitbuttom, int* bkmeshgameleft, int* bkmeshgametop, int* meshlimitright, int* meshlimitbuttom, int move, int size)
{
if (*bkgameleft < 0) // 网格越界检测并调整
*bkgameleft = 0;
else if (*bkgameleft > gamelimitright)
*bkgameleft = gamelimitright;
if (*bkgametop < 0)
*bkgametop = 0;
else if (*bkgametop > gamelimitbuttom)
*bkgametop = gamelimitbuttom;
if (*bkgameleft < *bkmeshgameleft) // 更新游戏采样区坐标,一些简单换算,由于频繁调用函数在这里产生了明显的卡顿影响,所以这里就不再封装成函数
{
*bkmeshgameleft -= move;
*meshlimitright = *bkmeshgameleft + size;
}
else if (*bkgameleft > *meshlimitright)
{
*meshlimitright += move;
*bkmeshgameleft = *meshlimitright - size;
}
if (*bkgametop < *bkmeshgametop)
{
*bkmeshgametop -= move;
*meshlimitbuttom = *bkmeshgametop + size;
}
else if (*bkgametop > *meshlimitbuttom)
{
*meshlimitbuttom += move;
*bkmeshgametop = *meshlimitbuttom - size;
}
}
// 实时渐变小动画
inline void animation(int characterflag, int bkgameleft, int bkgametop, int bkmeshgameleft, int bkmeshgametop, int bkmeshdeskleft, int bkmeshdesktop)
{
static int i = 1;
static int x = 10;
static int y = 10;
if (i % 3 == 0)
{
x = bkmeshdeskleft + bkgameleft - bkmeshgameleft;
y = bkmeshdesktop + bkgametop - bkmeshgametop;
i %= 125; // i 的上限 * 放大倍数 不超过 255
switch (characterflag) // 选取颜色
{
case 1:
setfillcolor(RGB(i * 2, i * 2, 0));
break;
case 2:
setfillcolor(RGB(i * 2, 0, i * 2));
break;
case 3:
setfillcolor(RGB(i * 2, 0, 0));
break;
case 4:
setfillcolor(RGB(0, i * 2, 0));
break;
case 5: // 如果没有按键,则使用默认颜色
setfillcolor(RGB(0, 0, i * 2));
break;
}
fillrectangle(x, y, x + 30, y + 30);
}
i++;
}
第三部分是提供一个小肉鸽游戏的移动,攻击,死亡,小怪追踪,新小怪刷新这几个功能的逻辑样例,复制粘贴即可运行。
////////////////////////////////////////////
// 程序:简单的控制台字符肉鸽游戏
// 作者:民用级脑的研发记录
// 邮箱:1309602336@qq.com
// 编译环境:Devc++/VC 2010/Visual Studio 2022
// 编写日期:2024-2-22
//
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <windows.h>
#define KEY_DOWN(vKey) ((GetAsyncKeyState(vKey) & 0x8000) ? 1:0) // 判断是否按下按键,按下之后,最高位变成 1,所以需要位运算去除其他位,只检测最高位
#define KEY_DOWN_FOREGROUND(hWnd, vk) (KEY_DOWN(vk) && GetForegroundWindow() == hWnd) // 前景窗口判断
#pragma warning(disable : 4996) // VS2022 报错某些不安全函数,这里禁用 VS 的某些警告 方便在其他函数移植进 Visualstdio2022
struct enemys
{
int x;
int y;
int is_live;
};
inline void showgame(int** gamemap, int high, int wide, char* player, char* ai)
{ // 更新游戏,由于 printf 只能一行接着一行打印,所以不能覆盖先前字符,只能增加缓冲区,在缓冲区里实现覆盖
char p[10000] = { '\0' };
char pen[2];
int tile;
for (int j = 0; j < wide; j++)
strcat(p, "#");
strcat(p, "\n");
for (int i = 0; i < high; i++)
{
strcat(p, "#");
for (int j = 0; j < wide; j++)
{
tile = gamemap[i][j];
switch (tile)
{
case 0:
strcpy(pen, " ");
break;
default:
sprintf(pen, "%d", tile);
break;
}
strcat(p, pen);
}
strcat(p, "#\n");
}
for (int j = 0; j < wide; j++)
{
strcat(p, "#");
}
strcat(p, "\n");
printf("%s", p); // 加载进缓存数组,减少 printf 使用,解决 printf 速度慢导致的屏闪
}
int main()
{
int** bkmap; // 游戏背景图
int** gamemap; // 记录当前游戏地图形式
int high; // 地图高,宽
int wide;
int limitx;
int limity;
high = 20;
wide = 100;
limitx = 100;
limity = 20;
bkmap = new int* [limity]; // 初始化游戏地图
gamemap = new int* [high];
for (int i = 0; i < limity; i++)
bkmap[i] = new int[limitx];
for (int i = 0; i < high; i++)
gamemap[i] = new int[wide];
for (int i = 0; i < limity; i++)
for (int j = 0; j < limitx; j++)
bkmap[i][j] = 0;
for (int i = 0; i < high; i++)
for (int j = 0; j < wide; j++)
gamemap[i][j] = 0;
int playerx; // 玩家位置
int playery;
int gamex; // 显示区域
int gamey;
int player; // 玩家记号
player = 1;
playerx = 0;
playery = 0;
gamex = 0;
gamey = 0;
int field = 9;
int enemy; // 敌人记号
int indix; // 刷新敌人时更新数组
int enemynum; // 敌人个数
int enemymax; // 敌人上限
enemy = 2;
enemynum = 10;
enemymax = 100;
enemys ai[100]; // 敌人数组
for (int i = 0; i < 100; i++)
{
ai[i].is_live = 0;
ai[i].x = 0;
ai[i].y = 0;
}
char playerview[100] = "6";
char enemyview[100] = "2";
printf("%s", playerview);
int characterx; // 敌人位置
int charactery;
charactery = 10;
characterx = 10;
printf("%s", enemyview);
showgame(gamemap, high, wide, playerview, enemyview);
int flag_x;
int flag_y;
int is_atk;
int atk_area;
int is_buff;
flag_x = 0;
flag_y = 0;
is_atk = 0;
atk_area = 1;
is_buff = 0;
HWND hwnd;
hwnd = GetForegroundWindow(); // 获取前端窗口句柄,由于程序刚运行时是在前端,所以这就是本程序的窗口句柄
srand(time(NULL)); // 设置随机数种子
int gameover;
gameover = 0;
int cnt = 0; // 敌人计算位置,每两帧移动一次
while (1)
{
flag_x = 0; // 默认不按键时速度为 0
flag_y = 0;
if (KEY_DOWN_FOREGROUND(hwnd, 0x41))
{
flag_x -= 1;
}
if (KEY_DOWN_FOREGROUND(hwnd, 0x57))
{
flag_y -= 1;
}
if (KEY_DOWN_FOREGROUND(hwnd, 0x44))
{
flag_x += 1;
}
if (KEY_DOWN_FOREGROUND(hwnd, 0x53))
{
flag_y += 1;
}
if (is_atk == 0 && KEY_DOWN_FOREGROUND(hwnd, 0x4A))
{
is_atk = 1;
}
if (is_buff == 0 && KEY_DOWN_FOREGROUND(hwnd, 0x4B))
{
is_buff = 1;
}
if (KEY_DOWN_FOREGROUND(hwnd, VK_ESCAPE))
{
printf("游戏退出\n");
break;
}
if (KEY_DOWN_FOREGROUND(hwnd, 0x30)) // 绘制地图
bkmap[playery][playerx] = 0;
if (KEY_DOWN_FOREGROUND(hwnd, 0x36)) // 绘制地图
bkmap[playery][playerx] = 6;
if (KEY_DOWN_FOREGROUND(hwnd, 0x37)) // 绘制地图
bkmap[playery][playerx] = 7;
if (KEY_DOWN_FOREGROUND(hwnd, 0x38)) // 绘制地图
bkmap[playery][playerx] = 8;
if (KEY_DOWN_FOREGROUND(hwnd, 0x39)) // 绘制地图
bkmap[playery][playerx] = 9;
if (flag_x > 1) // 速度限制
flag_x = 1;
else if (flag_x < -1)
flag_x = -1;
if (flag_y > 1)
flag_y = 1;
else if (flag_y < -1)
flag_y = -1;
if (flag_x) // 位移改变
playerx += flag_x;
if (flag_y)
playery += flag_y;
if (playerx >= limitx) // 角色位置限制
playerx = limitx - 1;
else if (playerx < 0)
playerx = 0;
if (playery >= limity)
playery = limity - 1;
else if (playery < 0)
playery = 0;
for (int i = 0; i < high; i++)
for (int j = 0; j < wide; j++)
if (gamemap[i][j] != bkmap[i + gamey][j + gamex]) // 去除上次动作记录
gamemap[i][j] = bkmap[i + gamey][j + gamex];
if (is_atk % 12 != 0) // 攻击
{
if (playerx < atk_area || playerx >= limitx - atk_area || playery < atk_area || playery >= limity - atk_area)
{
is_atk = 0;
}
else
{
is_atk++;
if (characterx - playerx >= -atk_area && charactery - playery >= -atk_area && characterx - playerx <= atk_area && charactery - playery <= atk_area) // 是否攻击到敌人
field = 0;
for (int n = 0; n < 100; n++)
{
if (ai[n].is_live)
{
if (ai[n].x - playerx >= -atk_area && ai[n].y - playery >= -atk_area && ai[n].x - playerx <= atk_area && ai[n].y - playery <= atk_area) // 是否攻击到敌群
ai[n].is_live = 0;
}
}
for (int i = playery - atk_area; i <= playery + atk_area; i++)
for (int j = playerx - atk_area; j <= playerx + atk_area; j++) // 范围攻击记号
{
gamemap[i][j] = is_atk;
if (bkmap[i][j] != 0)
bkmap[i][j] = 0;
}
}
}
else is_atk = 0;
for (int n = 0; n < 100; n++) // 打印存活敌人
if (ai[n].is_live)
gamemap[ai[n].y][ai[n].x] = enemy;
gamemap[charactery][characterx] = field; // 敌人比攻击晚赋值,所以敌人可以覆盖攻击图标
if (is_buff % 22 != 0) // buff 持续时间 12 帧
{
is_buff++;
atk_area = 3;
player = is_buff;
}
else
{
is_buff = 0;
atk_area = 1;
player = 1;
}
indix = 0;
enemynum = 0;
cnt++;
while (indix < enemymax) // 加入新的敌人
{
if (ai[indix].is_live)
{
enemynum++;
if (cnt % 2 == 0)
{
if (ai[indix].x < playerx)
ai[indix].x++;
else if (ai[indix].x > playerx)
ai[indix].x--;
if (ai[indix].y < playery)
ai[indix].y++;
else if (ai[indix].y > player)
ai[indix].y--;
cnt = cnt % 2;
}
if (playerx == ai[indix].x && playery == ai[indix].y)
{
gameover = 1;
player = 1;
}
}
else
{
ai[indix].x = rand() % 100;
ai[indix].y = rand() % 20;
ai[indix].is_live = 1;
enemynum++;
}
indix++;
if (enemynum >= 15)
break;
}
printf("player x %d y %d\n", playerx, playery);
gamemap[playery][playerx] = player;
showgame(gamemap, high, wide, playerview, enemyview);
while (gameover == 1)
{
printf("game over\n");
printf("按 R 重新开始\n 按 ESC 退出游戏\n");
if (KEY_DOWN_FOREGROUND(hwnd, 0x52))
{
gameover = 0;
playerx = 0;
playery = 0;
enemynum = 0;
break;
}
else if (KEY_DOWN_FOREGROUND(hwnd, VK_ESCAPE))
{
break;
}
showgame(gamemap, high, wide, playerview, enemyview);
Sleep(50);
system("cls");
}
if (gameover == 1)
break;
Sleep(100);
system("cls"); // 清屏
}
system("cls");
printf("Have a good time\n");
system("pause");
Sleep(50);
return 0;
}