【编程开发】C++ 打砖块开发日志


【编程开发】C++ 打砖块开发日志

\[\text{这是一篇写给自己看的日记,非制作教程} \]

因为开始写文章的时候 \(\text{UI}\) 已经完成很大一部分了,没有留下多少历史截图,只能强行还原到记忆中的样子,至于代码.....谔谔....懒得改回去了。

还有些踩过的坑记不清楚就直接跳过不扯了。

2020 年 2 月 ?日

之前在学习 \(\text{EasyX}\) 的时候,觉得库里面有一些自带的函数用起来很不方便,于是自己搞了一个头文件,按照平时的使用习惯写了一些常用函数。但这些还远远不够,后续又添加了各种各样的东西。

开坑啦!不管三七二十一,先尝试做一个开始界面吧!

看了看 \(4399\) 上的打砖块 ,经过一系列比对,决定把界面设置成 \(480 \times 820\),并在最下方取出 \(y \in [720,819]\) 的区域作为 \(\text{debug}\) 区域,方便中间查错(一开始忽略了界面像素点的问题,居然天真的以为 \(820\) 也可以用,实际上可用区域应该是 \([0 \sim 479,0 \sim 819]\))。

然后是背景图,随便挑了一张 绅士,不支持一波?)画的\(\text{3D}\) 小球图片,修成 \(19 \times 19\):。总感觉哪里怪怪的啊(像是套了个胖次)。改亮度后看起来好一点了:,但还是很丑。

算了,先把他画出来再说吧。依旧是暴力枚举(获取需要的像素点):

inline void getbkimg(){
    for(Re i=0;i<=18;++i)
        for(Re j=0;j<=18;++j)
            if(pan[i][j])
                P[++cnt]={i,j},ballpimg[cnt]=getimage(i,j,i,j);
}

inline void print(){
    Re x=Ox-r,y=Oy-B.r;
    for(Re i=1;i<=cnt;++i)
    	putimage({x+P[i].x,y+P[i].y},ballpimg[i]);
}

感觉怪怪的。。。。。

无论怎么改都丑得要死,几乎快要放弃挣扎的时候,再次突发奇想:我直接弄个由内到外的黑白渐变看看?

\(\text{woc}\)!这么帅!\(\text{i}\)\(\text{i}\) 了。

至于之前的小球样式也没有废弃,丢在一边好了(这使我萌发出了新的灵感:设计多种不同样式的小球。当然,这都是后话了,目前就用这两个好了,等后续开发吧)


2020 年 2 月 24 日

早上学习了 \(\text{prufer}\) 序列并做了两道炒鸡大水题。下午从表哥那里嫖到了个他自己买的 \(\text{vpn}\),速度飞起,于是逛了一下午的 \(\text{Youtube}\) 。晚上好像抽出时间写了一点点,但记不清了。


2020 年 2 月 25 日

\(\text{update:}\) 调整了游戏页面中菜单的位置,研究新的配色,优化了代码,添加选关界面逻辑判断。

原本是准备今天做小球运动以及反弹判定的,但在这之前却出现了意外,感觉游戏界面(见下图)上方的黑色生命框和亮青色菜单选项混在一起丑的一 \(\text{p}\) 啊啊啊啊啊!!我受不了,我现在就要改配色!

考虑到自己的艺术细胞可能不太够用,于是向 \(\text{Silent_E}\) 发起了求助:

(我带泥萌打)

经过一系列激♂烈的讨论,最终还是没想出什么好的设计(其实主要是在尝试红配棕,但是太暗了所以都不好看),失败品这里只放一个,就不一一列举了(太多了,会显得文章特别长):

吃午饭时再次突发奇想:我如果换一张色调单一一些的图片会不会好点呢?

发现即使是没有进行调整的原图放上去也比前面的好看:

只是可怜了白毛啊,明明想让她也上镜的 QAQ。

此时在换背景的过程中出现了意外:因为代码复制时忘记改细节,导致背景图片全部使用了亮色调的,但是选项框背景却使用了暗色调。

虽然是个意外,但这样的效果却出奇的好,于是我又进行了一系列魔改,成果如下:

在调色的过程中,因为要不断地尝试各种各样的颜色,而几个的选项都散布在各个地方,改来改去很不方便,于是我对代码进行了优化:整合选项,统一使用一个结构体。

下午重新设计好选项样式后,又添加了选择关卡界面的判断:通过一关后解锁下一关,当尝试进入未解锁关卡时提示 \(\text{FBI Warning!!!}\)

\(25\) 日晚开始写这篇文章。

其实今天下午本来是要学习的,但由于那个突然出现的灵感,又荒废了一个下午。。。。。


2020 年 2 月 27 日

沉迷于多项式算法无法自拔。 ~~

\(\text{update:}\) 晚上对日志进行了重新排版,并单独捞出来作为一篇文章。


2021年 10 月 12 日

\(\text{update:}\) 截止目前,完成了玩家小球的检测移动,优化图像闪烁问题。

大一了。

从两周前的某堂无聊计算机课开始,我又重拾了这项工程。

记得当时半途弃坑的其中一个原因是:小球碰撞检测太难。

这个问题一直困扰着我,每当想要继续写时就会因为这个问题而退缩。

至到今天,我突然意识到,用浮点数储存坐标进行数学计算,然后取整绘图呀...先前一直在想如何判好一个锯齿状圆的碰撞转向...

我真傻,真的。

此外,图像动起来后,反复刷新产生的闪烁现象较为明显,需使用BeginBatchDraw();FlushBatchDraw();


2021年 10 月 15 日

\(\text{update:}\) 添加小球与四个方向的碰撞判定,优化绘图速度,添加死亡动画游戏暂停功能。

写了很长一段代码:

inline void reverse_dir(Re flagx,Re falgy){//翻转运动方向
	if(flagx)Dir.x=-Dir.x,DB_Dir.x=-DB_Dir.x;
	if(falgy)Dir.y=-Dir.y,DB_Dir.y=-DB_Dir.y;
}
inline int judge_wall(Range &R1,DB_Point &DB_P,DB_Point &DB_P_){//判断是否撞墙
	if(DB_P_.y>=R1.y2)return -2;//碰下【dead】
	Re p1=dcmp(DB_P_.x-R1.x1);//p1<=0 碰左壁
	Re p2=dcmp(DB_P_.y-R1.y1);//p2<=0 碰上壁
	Re p3=dcmp(DB_P_.x-R1.x2);//p3>=0 碰右壁
	if(p1<=0&&p2<=0){//碰左 且 碰上
		if(p1==0&&p2==0)DB_P_.x=R1.x1,DB_P_.y=R1.y1,reverse_dir(1,1);//角落碰撞(同时碰左壁和上壁)
		else{
			LD t1=(R1.x1-DB_P.x)/DB_Dir.x,t2=(R1.y1-DB_P.y)/DB_Dir.y;
			if(dcmp(t1-t2)<0)DB_P_.x=R1.x1, reverse_dir(1,0);//先碰左壁
			else DB_P_.y=R1.y1, reverse_dir(0,1);//先碰上壁
		}
	}
	else if(p3>=0&&p2>=0){//碰右 且 碰上
		DB_P_.x=max(DB_P_.x,1.0*R1.x1),DB_P_.y=max(DB_P_.y,1.0*R1.y1);
		DB_P_.x=min(DB_P_.x,1.0*R1.x2),DB_P_.y=min(DB_P_.y,1.0*R1.y2);
	}
	else if(p1<=0)DB_P_.x=R1.x1,reverse_dir(1,0);//碰左
	else if(p2<=0)DB_P_.y=R1.y1, reverse_dir(0,1);//碰上
	else if(p3>=0)DB_P_.x=R1.x2,reverse_dir(1,0);//碰右
}
inline int updata(Range R0,Re speed0=1){//更新小球位置
	if(speed0!=speed)calc_DB_Dir(speed0);//改变速度
	Range R1=Range(R0.x1+r,R0.y1+r,R0.x2-r,R0.y2-r);
	DB_Point DB_P_old=DB_P;DB_P+=DB_Dir;
			//DB_P_.x=max(DB_P_.x,R1.x1),DB_P_.y=max(DB_P_.y,R1.y1);//P.x-r>=gamebox.x1,P.y-r>=gamebox.y1
			//DB_P_.x=min(DB_P_.x,R1.x2),DB_P_.y=min(DB_P_.y,R1.y2);//P.x+r<=gamebox.x2,P.y+r<=gamebox.y2
	Re cmd=judge_wall(R1,DB_P_old,DB_P);//判断是否撞墙
	if(cmd==-2)return -2;//【dead】

	Point P_=Point(floor(DB_P.x+eps),floor(DB_P.y+eps));
	if(P_!=P)refresh(P_);//更新图像
	return 0;
}

但此时却出现了一个很麻烦的问题:这里更新小球状态的 updata函数只要一开就卡得飞起。

没办法,只好尝试着能不能卡下常了(保精度的dcmp函数第一个被下刀....)。

然而,并没有什么卵用。

调了一波,发现问题在于小球刷新图像时高频使用putimagegetimage函数:

#define bs style[styletype]
inline void print_background(){
	Re x=P.x-r,y=P.y-r;
	for(Re i=1;i<=bs.cnt;++i)
		putimage({x+bs.P[i].x,y+bs.P[i].y},background_img[i]);
}
inline void print(){
	if(styletype==1){
		Re x=P.x-r,y=P.y-r;
		for(Re i=1;i<=bs.cnt;++i)
			putimage({x+bs.P[i].x,y+bs.P[i].y},bs.ballpimg[i]);
	}
	else if(styletype==2){
		for(Re r=9-1;r>=1;--r){
			Re tmp=int(255.0*(9-r+1)/9.0);
			drawcircle({P.x,P.y},r,RGB(tmp,tmp,tmp));
			drawcircle_({P.x,P.y},r,RGB(tmp,tmp,tmp));
		}
	}
}

inline void playgame_ball_getimage(){
	Re x=P.x-r,y=P.y-r;
	for(Re i=1;i<=bs.cnt;++i)
		background_img[i]=getimage(Point(x+bs.P[i].x,y+bs.P[i].y));
}
#undef bs

emm...一个一个摸像素点确实是有点蠢,背景图的话,其实可以直接扣一个矩阵下来。不过小球的话就没法避免枚举了。

试着优化了一下,没想到跑得飞快:

inline void playgame_ball_getimage(){ background_img=getimage(P.x-r,P.y-r,P.x+r,P.y+r); }
inline void print_background(){ putimage(P.x-r,P.y-r,background_img); }

但如果是第一种小球(枚举法输出图像),还是会慢一些,不过,通过调整小球运动速度可以做到大致相等。

此外,添加死亡动画游戏暂停功能

inline void dead_anime(Re HP){//【死亡动画】
	for(Re O=1;O<=5;++O){//闪烁次数
		HPB[HP].print_background(),game_ball.print_background();
		print(gamebox,_T("【Oh yeah~ You dead!!!】"),red,40,200);
		Sleep(200);
		HPB[HP].print(),game_ball.print();
		print(gamebox,_T("【Oh yeah~ You dead!!!】"),darkgray,40,200);
		Sleep(200);
	}
}
inline void game_pause(){//【游戏暂停】
	printdebug(_T("【游戏暂停】输入任意键以继续..."),white,28,100);
	system("pause"),refresh_debug_level_info();
}

2021年 10 月 19 日

今天开始尝试思考小球砖块

这东西并不好做,而且越想越麻烦越想越复杂。这是一个难点,网上很多人写的都很水。但我是个完美主义者,希望尽量地去实现一个真实物理引擎。

小球碰平面当然是镜面反射,但也有一个问题:程序实现的时候,是每隔一段时间更新小球位置,判断是否碰撞当然是需要判断图形是否有交叉区域(甚至小球可能刷新后的位置直接就穿到了砖块里面去)。

然后我在网上搜了一下,发现了一个叫做“分离轴定理”的黑科技。具体见:

  • javascript的2D空间碰撞检测
  • 碰撞检测之分离轴定理算法讲解

另外,在查找资料的时候,意外发现了一个小 \(\text{trick}\),游戏要流畅运行大概需要 \(\text{60}\) 帧每秒,也就是每秒进行 \(\text{60}\) 次刷新,那么每一次刷新就需要控制在 \(\text{16ms}\) 以内。这东西看起来很显然,但事实上我此前都从未想到加以利用:根据这个刷新所需的运行时间大致区间来调整延迟。

小测试了一波,每两次刷新的间隔大约是 \(11\sim 18ms\)(算上固定 \(10ms\)\(\text{Sleep}\)),撞墙的时候最高可以达到 \(25ms\),也就是大约 \(40\sim 90\) 帧的亚子,目前来看大致合格吧。不过当后面各种检测越来越多并加上砖块刷新后就需要调整了。有了这个标准之后调 \(\text{Sleep}\) 参数会舒服许多。

回到碰撞检测的第二种情况:小球碰砖块尖角。

一开始我的想法比较 \(\text{naive}\),转换成圆心位置判定嘛,然后碰了尖角之后反弹原路返回。

如图(灰色为砖块),首先把本次刷新前的圆心位置和刷新后的圆心位置连起来,与外围的补充边界求交点,得到发生碰撞时的位置,如果是在蓝色区域就镜面反弹,如果红色就按照原来进入的方向反弹回去。

但想来想去总感觉哪儿有点问题,于是去问室友,emm...果不其然,被狠狠地教育了一波。

他提供的解释大概是这样:碰撞的那一瞬间,在碰撞点处做圆的切线,然后将“运动的曲线(圆弧)去撞点(砖块尖角)”转化为“运动的点(砖块尖角)去碰切面(圆的切线)”。(相对运动转换参照物嘛)

一语点醒梦中人。

后来我仔细思考了一下,实际情况应该长这样:

对于第一种情况,原路返回。

对于第二种情况,碰撞前的速度和碰撞后的速度嘤该关于 \(O_2A\) 对称(转换参照物后,就相当于是点 \(A\) 在切面上发生了镜面反弹)。其中 \(O_2\) 为发生碰撞时圆心的位置,\(A\) 为砖块尖角。

哎...太麻烦了,还是写简单点吧...我屈服了行吧....

来自大佬室友的吐槽:你这打砖块整个真实物理引擎玩家不得急死?尖角一碰就掉下去了玩个锤子啊,别人都是一碰就上去,到你这一碰就往下飞...


2021年 10 月 26 日

注意到一个问题:如果小球运动轨迹为垂直向上下左右,需要人为创造偏转一个角度,否则可能会在一个地方反复横跳。

发现求交点有点麻烦,以前的算几板子里没有直线与圆求交的封装代码(似乎三角剖分里面有,但比较乱)。


2021年 11 月 2 日

\(\text{update:}\) 完善小球发射时的运动方向判定;增加线与圆求交点;增加小球碰玩家的部分判定:上壁及左右壁镜面反弹,左上及右上两角原路返回。

挖坑:左上及右上两角反弹的方式待定;关于上壁反弹:给玩家设定“左”,“中”,“右”三个区域,中间镜面反弹,左右按照一定规律偏转角度。


2021年 11 月 9 日

\(\text{update:}\) 添加道具效果:伸长

发现一件很恐怖的事情。
之前我写了小球碰玩家左右壁的判定,也就是说,允许小球在玩家区域稍作逗留。

那么,当我更新玩家位置的时候,就需要判定玩家撞小球。仔细想想发现这玩意痛苦地一p.....

琢磨了好久,晚上回寝室猛然发现,玩家碰小球这一部分其实很简单,因为玩家移动只有单纯的左右平移,所以只需要计算小球左右两边能拦截玩家的位置即可。


2021年 11 月 12 日

\(\text{update:}\) 部分完成玩家移动时被小球拦截的现象。

当然,问题并没有完全解决:可能会出现玩家追着小球跑,小球却不与其发生碰撞(运动方向向外时显然撞不了。向里时也可能出现,因为我存储玩家坐标没有用实数,于是小球与玩家中间会有一些间隙,如果玩家不停地追,就会一直保持这个间隙,导致无法碰撞)
所以,还需要实现:玩家移动时被小球拦截,触发小球碰撞改变运动轨迹。


2021年 11 月 16 日

\(\text{update:}\) 微改小球碰撞变轨方案;完成玩家被拦截后触发小球碰撞变轨;增加道具效果:加速减速

inline int judge_player(Player &py,DB_Point &st,DB_Point &ed){//【碰撞检测】判断与玩家的碰撞情况
    DB_Point CP;Re flag=0;LD dis_CPP=2e9;
			
    DB_Point A=st,B=ed;

    if(dcmp(ed.y+r-py.R.y1)<0)return 0;//小剪枝:如果运动轨迹的终点还没到玩家区域

    DB_Point up_A=DB_Point(py.R.x1,py.R.y1-r),up_B=DB_Point(py.R.x2,py.R.y1-r);
    if(GMT::pan_cross_LL(A,B,up_A,up_B)){//1.上线段
        CP=GMT::cross_LL(A,B,up_A,up_B); flag=UP_LINE; dis_CPP=GMT::Len(CP-st);
    }

    DB_Point left_A=DB_Point(py.R.x1-r,py.R.y1),left_B=DB_Point(py.R.x1-r,py.R.y2);
    if(GMT::pan_cross_LL(A,B,left_A,left_B)){//2.左线段
        DB_Point tmp_CP=GMT::cross_LL(A,B,left_A,left_B); LD tmp_dis=GMT::Len(tmp_CP-st);
        if(tmp_dis

发现玩家碰小球还是很是别扭——没有设定加速度方案。
但这玩意儿实现起来....不用想就知道很毒瘤,放弃ing...

在测试加速减速效果时,试出了一种特殊情况,把我给整不会了...

(蓝色箭头为小球运动方向)

小球被卡在角落里惹...

虽然,经过测试后发现程序崩溃的原因不在这里,但这提醒了我:要是小球卡住不动咋办TAT


2021年 11 月 19 日

\(\text{No update.}\)

问题是真她喵的多。前面为了简化判定,禁止了垂直移动/水平移动的情况(通过添加偏移量的形式),于是乎,穿墙现象再一次出现...

不过,说到底,在正常游戏中,极低的玩家高度很难造成这种问题,还有前面的一些问题也是,几乎可以说是在做没有用处的工作(除非有熊孩子玩家恶意卡我bug...)。

如何解决?
小球垂直/水平移动 解禁。

那么,代价呢?
出现大量周期运动卡死的情形。

所以,我选择舍小我为大我,忽略这个问题。(希望不要有熊孩子,不要有熊孩子,不要有熊孩子


2021年 11 月 23 日

\(\text{update:}\) 微调小球颜色;加入砖块信息。

发现一个让人头疼的bug,调了很久才发现是因为对之前自己预设的绘图函数不熟悉,使用的时候少了个参数...

哎,也难怪,时间间隔太久,很多东西都忘了。


2021年 11 月 30 日

\(\text{update:}\) 完善砖块信息;实现小球砖块及消灭砖块功能;添加通关动画

发现'drawtext‘里的垂直居中参数居中了个锤子,md,还得自己来调,烦呐。

注意到小球在运动时可能会截到砖块作为背景,即出现小球背景和砖块背景互相嵌套覆盖的情况(导致砖块消失后产生残留色块),仔细想了想,发现这东西竟然异常难搞,自闭...


2021年 12 月 1 日

\(\text{update:}\) 修复背景图相互覆盖 \(\text{bug}\) ;修复碰撞检测重大 \(\text{bug}\) ;调整小球玩家的碰撞方案;调整背景模糊度;添加开发者模式关卡制作功能。

昨天注意的问题想了很久,最后发现只需要在砖块消失前后添加一点小步骤就可以了:消失前一瞬间让小球被小球背景所覆盖,然后让砖块消失,再获取小球的新背景并输出小球。

//砖块受到攻击
print_background();//砖块变化前先抠掉小球
brick[bk_Rid].vanish();//砖块变化
playgame_ball_getimage();//重新获取小球背景图像
print();//重新输出小球

测试的时候出现了很混乱的状况,各种奇怪数据乱跳,调了一个多小时,发现写栈的时候,同一个点被重复加入了几百次,各种越界起飞...

此外,终于调整了小球碰玩家方案:
正中央一半的区域为镜面反射,左右两边依赖于与中心区域的距离调整偏转角,最小 \(15^{\circ}\) 最大 \(75^{\circ}\),左右两个尖角直接使用 \(15^{\circ}\) 偏角。

if(flag){//发生碰撞,改变运动轨迹
    if(flag==2||flag==3){ ed=CP,reverse(flag); return flag; }//(镜面反弹) 2.左线段 3.右线段
    if(flag==1){//1.上线段
        if(dcmp(CP.x-py.center_L)>=0&&dcmp(CP.x-py.center_R)<=0){ ed=CP,reverse(flag); return flag; }//(镜面反弹) 中心区域
        LD rad;
        if(CP.xpy.center_R)rad=toPi(75.0-60.0*(CP.x-py.center_R)/(py.lenth/2.0)),Dir_ratio.x= cos(rad);//(特殊反弹) 右特区
        Dir_ratio.y=-sin(rad);
        calc_DB_Dir(speed);
        ed=CP; return flag;
    }
        if(flag==5||flag==6){//5.左上圆弧 6.右上圆弧
        LD rad=toPi(15.0);//碰圆弧直接15°最小偏转角
        if(flag==5)Dir_ratio.x=-cos(rad);//(特殊反弹) 5.左上圆弧
        if(flag==6)Dir_ratio.x= cos(rad);//(特殊反弹) 6.右上圆弧
        Dir_ratio.y=-sin(rad);
        calc_DB_Dir(speed);
        ed=CP; return flag;
    }
}
else return 0;//未发生碰撞

然后,还做了一些其他调整,写了一个关卡制作页面,做了前 \(6\) 关。

到目前为止,最核心的游戏机制就完成地差不多了。

后面还有各种各样的小球buff玩家buff砖块硬度特殊关卡设计等等一系列东西都属于玩法扩展了。
再就是最重要的:设计好看的 \(\text{UI}\)...

相关