瓦片边移动边绘制的性能优化
1.在 drawflag控制检测上,加入 drawsmallflag 进行 选择性再调控。
2.mousex mousey 更新来源变为 MOUSEMOVE LBUTTONDOWN RBUTTONDOWN 三个情况。
3. 记录旧瓦片的绘制,减少一次长按绘制时,同一瓦片被绘制次数。
使用easyx 开发 devc++ 开发。
使用标志位,一个是用于判断执行哪些模块,另一方面是通过不同标志位的合并拆分,实现条理运行代码。正如一个是 flag-x 和flag_y这两个参数同时为零和moveflag=0;而moveflag成为控制后续选择渲染步骤的控制变量。
以及想象中的模块代码是什么样子的,比仅仅是表面的标志位区分,但是由于标志位局限于所属模块,不能确定此模块运行时,其他模块的作用,而且有时标志位无法设置,导致没有标志位,就需要按情况不断拆分合并代码,最后发现按三个条件的8种情况满足情况。实际上是功能的实现,如是否绘制和是否按键移动,导致的如下四个排列组合
绘制而键盘控制画布不移动,
绘制而健康控制画布移动
不绘制而键盘控制画布不移动,
不绘制而键盘控制画布移动
而这四种组合,对应的坐标数据运算和贴图,都有各自不同的优化,这也是在把得到的功能重新拆分,在把多个if if if 的判断中多次修改,发现可以按找如上的排列组合,全部变成 if else if else这样的形式,而这就是主函数里三组 选择 分支产生的由来。也,只找出必须的运算,来理顺各个层次之间的联系,实现精确优化。
考虑到拖拽时鼠标和画布同时移动,导致无法绘制,于是就可以单独处理鼠标拖拽,解决键盘控制画布移动和鼠标拖拽移动,这里是鼠标拖拽优先级高,在实现原理上,其实是由于鼠标拖拽需要暂存鼠标坐标和画布原坐标,而键盘控制改变当前原坐标,不会改变拖拽时暂存的数据,而鼠标拖拽计算出的数据是在键盘影响完,重新写入数据,所以键盘不会对鼠标移动产生影响。因为标志位的检测
因为拖拽的检测放在最后,所以只有四种情况都检测完毕,才会执行拖拽检测,
而控制位导致绘制时不能拖拽,绘制时鼠标坐标和画布坐标同时移动也会导致绘制失败,所以,四种情况里,穷举一下,有绘制不能拖拽,有键盘移动,不能拖拽。绘制的两种情况不行,不绘制而键盘移动也不行,只能不绘制不拖拽。
而且由于一开始的stitch 分支的拖拽标志位,拖拽不会被键盘打断,而moveflag标准位,键盘不会被拖拽打断。
在数据计算,拖拽是第一位的,在贴图层面,绘制,和键盘移动是第一位的,这样可以实现优先级随指变化而变化。
#include <graphics.h>
// 使用关键字 inline 声明为内联函数,减少贴图函数频繁调用的开销导致的卡顿。
// 缓冲区纹理映射函数:bkmesh 映射目标,map 映射总网格,pentable:纹理集,bkmeshmapi,bkmeshmapj:映射起始点,tilenum:横,纵映射的数量,pixnum:一个映射块的边长,单位:像素。
inline void freshmesh(IMAGE* bkmesh, int** map, IMAGE* pentable, int bkmeshmapi, int bkmeshmapj, int tilenum, int pixnum)
{
int pennumber = 0; // 暂存每一次循环的映射代号
IMAGE pen = NULL; // 所找到的纹理
int left = 0; // 这是每次循环所找到的纹理对应映射地址
int top = 0;
SetWorkingImage(bkmesh); // 设置绘图目标为游戏背景采样区,刷新采样区,刷新寄存区
for (int i = bkmeshmapi; i < bkmeshmapi + tilenum; i++)
{
left = 0;
for (int j = bkmeshmapj; j < bkmeshmapj + tilenum; j++)
{
pennumber = map[i][j]; // 读取游戏大地图数组序号
pen = pentable[pennumber]; // 根据序号查找对应贴图
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] = j % 9; // 初始化游戏大地图 map 的参数,参数范围 0-9
}
}
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;
bkgametop = 0;
bkmeshdeskleft = 700; // 游戏背景缓冲区左上角将会在屏幕的(700,0)处
bkmeshdesktop = 0;
bkmeshgameleft = 0;
bkmeshgametop = 0;
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
map=initmap(1000,1000);
int pentableleft; // 忘了初始化调色盘了,这里设置调色盘左上角在屏幕的坐标
int pentabletop;
IMAGE* pentable; // 调色板其实就是贴图数组
pentableleft = 0; // 调色盘左上角将会在屏幕的(0,0)处
pentabletop = 0;
pentable = new IMAGE[10];
for (int i = 0; i < 10; i++)
{
pentable[i] = IMAGE(30, 30);
SetWorkingImage(&pentable[i]); // 给调色板绘制颜色
setfillcolor(RGB(i * 20, i * 20, i * 20)); // 这里初始化调色盘的颜色
fillrectangle(0, 0, pixnum, pixnum); // 在调色板上绘制颜色(纹理)
}
int left; // 初始化绘制采样区所需的坐标,相对于采样区,(0,0)就是采样区左上角顶点坐标
int top;
left = 0;
top = 0;
// 往缓冲区刷入贴图
SetWorkingImage(bkmesh); // 设置绘图目标为游戏背景采样区,刷新采样区,刷新寄存区
for (int i = bkmeshmapi; i < bkmeshmapi + 30; i++)
{
left = 0;
for (int j = bkmeshmapj; j < bkmeshmapj + 30; j++)
{
int pennumber = map[i][j]; // 读取游戏大地图数组序号
IMAGE pen = pentable[pennumber]; // 根据序号查找对应贴图
putimage(left, top, &pen); // 把贴图画到采样区
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]); // 绘制绘图板
}
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 = 0; // 默认黑色笔刷
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; // 判断是否需要刷新
int oldbkmeshgamey;
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 > 0 && m.x < 30 && m.y < 300)
pentake = m.y / 30; // 选择贴图对应的代号
break;
case WM_LBUTTONUP:
drawflag = 0;
drawsmallflag=0;
olddrawi=0;
olddrawj=0;
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)
break;
case WM_KEYDOWN:
switch (m.vkcode) // 键盘移动控制
{
case 0x41: // A
flag_x-=speed;
moveflag=1;
break;
case 0x57: // W
flag_y-=speed;
break;
case 0x44: // D
flag_x+=speed;
break;
case 0x53: // S
flag_y+=speed;
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;
}
break;
case WM_MOUSEMOVE:
if(mousex!=m.x||mousey!=m.y)
{
mousex=m.x;
mousey=m.y;
}
break;
}
}
// 开始根据指令运行坐标变化
if (draftflag == 1)
{
bkgameleft = draftoldgamex - (mousex - draftoldmx); // bkgameleft-draftoldgamex=-(mousex-draftoldmx)
bkgametop = draftoldgamey - (mousey - draftoldmy); // bkgametop-draftoldgamey=-(mousey - draftoldmy)
}
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 纵坐标方向移动距离相同
if(olddrawi!=drawy/pixnum||olddrawj!=drawx/pixnum)
{
drawsmallflag=1;
olddrawi=drawy / pixnum;
olddrawj=drawx / pixnum;
map[olddrawi][olddrawj] = pentake; // 注意 map[y][x],而不是 map[x][y],因为判断第几行,是通过 y 来控制上下移动的,判断第几列,是通过 x 左右移动的。
}
else
{
drawsmallflag=0; // 检测到是上一次绘制的瓦片,则不再刷新贴图与缓冲区。
}
} // 对绘制进行分类计算数据,剥离特殊情况的重复绘制,仅仅是 flag_x,或者flag_y不为零时取消重复绘制判断。
else if(drawflag == 1 && mousex > bkdeskleft && mousey > bkdesktop && mousex < bkdeskleft + 300 && mousey < bkdesktop + 300)
{
moveflag=1;
while(flag_x<-10) // 限制在合适速度范围
flag_x+=10;
while(flag_x>10)
flag_x-=10;
while(flag_y<-10)
flag_y+=10;
while(flag_y>10)
flag_y-=10;
bkgameleft+=flag_x; // 更新游戏背景寄存区左上角坐标
bkgametop+=flag_y;
drawx = bkgameleft + (mousex - bkdeskleft); // drawx-bkgameleft=m.x-bkdeskleft 横坐标方向移动距离相同
drawy = bkgametop + (mousey - bkdesktop); // drawy-bkgametop=m.y-bkdesktop 纵坐标方向移动距离相同
if(olddrawi!=drawy/pixnum||olddrawj!=drawx/pixnum)
{
drawsmallflag=1;
olddrawi=drawy / pixnum;
olddrawj=drawx / pixnum;
map[olddrawi][olddrawj] = pentake; // 注意 map[y][x],而不是 map[x][y],因为判断第几行,是通过 y 来控制上下移动的,判断第几列,是通过 x 左右移动的。
}
else
{
drawsmallflag=0; // 检测到是上一次绘制的瓦片,则不再刷新贴图与缓冲区。
}
}
else if(drawflag==0&&flag_x!=0||flag_y!=0)
{
moveflag=1;
while(flag_x<-10) // 限制在合适速度范围
flag_x+=10;
while(flag_x>10)
flag_x-=10;
while(flag_y<-10)
flag_y+=10;
while(flag_y>10)
flag_y-=10;
bkgameleft+=flag_x;
bkgametop+=flag_y;
}
else
{
// 既不绘制也不移动也不拖拽
}
// 根据计算出的坐标数据进行绘制,分多种情况分别绘制,减少函数重复调用与无效调用
if(drawsmallflag==1&&moveflag==0)
{
freshmesh(bkmesh, map, pentable, 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)
{
if(bkgameleft<0) // 网格越界检测并调整
bkgameleft=0;
if(bkgametop<0)
bkgametop=0;
if(bkgameleft>30*300)
bkgameleft=30*300;
if(bkgametop>30*300)
bkgametop=30*300;
while (bkgameleft < bkmeshgameleft) // 更新游戏采样区坐标,一些简单换算
bkmeshgameleft -=270;
while (bkgametop <bkmeshgametop )
bkmeshgametop -= 270;
while(bkgameleft>bkmeshgameleft+270+270)
bkmeshgameleft += 270;
while(bkgametop>bkmeshgametop+270+270)
bkmeshgametop += 270;
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
freshmesh(bkmesh, map, pentable, 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) // 分类渲染, drawflag==0 时,再选择性刷新缓冲区
{
if(bkgameleft<0) // 网格越界检测并调整
bkgameleft=0;
if(bkgametop<0)
bkgametop=0;
if(bkgameleft>30*300)
bkgameleft=30*300;
if(bkgametop>30*300)
bkgametop=30*300;
while (bkgameleft < bkmeshgameleft) // 更新游戏采样区坐标,一些简单换算,由于频繁调用函数在这里产生了明显的卡顿影响,所以这里就不再封装成函数
bkmeshgameleft -=270;
while (bkgametop <bkmeshgametop )
bkmeshgametop -= 270;
while(bkgameleft>bkmeshgameleft+270+270)
bkmeshgameleft += 270;
while(bkgametop>bkmeshgametop+270+270)
bkmeshgametop += 270;
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
if(oldbkmeshgamex!=bkmeshgameleft||oldbkmeshgamey!=bkmeshgametop) // 判断是否更新采样区
{
freshmesh(bkmesh, map, pentable, 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);
}
else if(draftflag) // 分类渲染-只拖拽
{
if(bkgameleft<0) // 网格越界检测并调整
bkgameleft=0;
if(bkgametop<0)
bkgametop=0;
if(bkgameleft>30*300)
bkgameleft=30*300;
if(bkgametop>30*300)
bkgametop=30*300;
while (bkgameleft < bkmeshgameleft) // 更新游戏采样区坐标,一些简单换算
bkmeshgameleft -=270;
while (bkgametop <bkmeshgametop )
bkmeshgametop -= 270;
while(bkgameleft>bkmeshgameleft+270+270)
bkmeshgameleft += 270;
while(bkgametop>bkmeshgametop+270+270)
bkmeshgametop += 270;
bkmeshmapi = bkmeshgametop / pixnum;
bkmeshmapj = bkmeshgameleft / pixnum;
if(oldbkmeshgamex!=bkmeshgameleft||oldbkmeshgamey!=bkmeshgametop) // 判断是否更新采样区
{
freshmesh(bkmesh, map, pentable, 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);
}
Sleep(20); // 休眠 20 毫秒,减少 CPU 占用
}
return 0;
}
// 边绘制边移动:最大:边界 2.30GHz 3%占用,内部 2.25Ghz。仅绘制:1.8GHz, 拖拽:0.99GHz,旧瓦片绘制检测,又减少重复绘制,显著平稳,峰值降低