-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.cpp
1443 lines (1221 loc) · 32 KB
/
main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include <math.h>
#include <time.h>
#include <graphics.h>
#include "ini.hpp"
// 引用 Windows Multimedia API
#pragma comment(lib, "Winmm.lib")
/////////////////// 常量定义 ///////////////////
// 圆周率
#define PI 3.1415926f
// 程序界面属性
#define STATE_BAR_HEIGHT 150 // 状态栏高度
#define WINDOW_BOTTOM_MARGIN 187 // 窗口底部边距(留白)
// 方块总边长
#define SIDE_LEN (g_nInnerSideLen + 2 * g_nInnerInterval)
// 挡板属性
#define BOARD_HALF_WIDTH 40 // 挡板半宽
#define BOARD_HALF_THICKNESS 3 // 挡板半厚度
#define BOARD_Y_DISTACE 100 // 挡板比地图底部低多少
#define BOARD_BALL_DISTANCE 16 // 待发射小球离挡板多远
#define BOARD_Y (g_nMapH * SIDE_LEN + BOARD_Y_DISTACE) // 挡板 y 坐标
// 小球属性
#define BALL_MAX_NUM 1024 // 最大小球数量
#define BALL_LIFE_NUM 4 // 球有几条命
#define BALL_SPEED 2.4f // 小球速度
// 算法设置
#define PROCESS_REPEAT 3 // 在一帧内计算几次游戏状态
// 方块颜色
#define COLOR_WALL RGB(163, 163, 162) // 墙:灰色
#define COLOR_BRICK_R RGB(214, 63, 37) // 红 R
#define COLOR_BRICK_Y RGB(228, 174, 31) // 黄 Y
#define COLOR_BRICK_G RGB(15, 216, 15) // 绿 G
#define COLOR_BRICK_C RGB(39, 253, 251) // 青 C
#define COLOR_BRICK_B RGB(68, 112, 202) // 蓝 B
#define COLOR_BRICK_W RGB(234, 234, 234) // 白 W
// 方块类型标记
#define EMPTY 0
#define WALL 1
#define BRICK 2
// 道具类型标记
#define PROP_3X 0 // 每个小球向外发射三个小球
#define PROP_SHOOT_3 1 // 发射三个小球
#define PROP_HEART 2 // 加一条命
#define PROP_MAX_NUM 1024 // 最大道具数量
// 道具下落速度
#define PROP_DROP_SPEED 0.2f
// 碰撞结果
#define HIT_NONE 0 // 未发生任何碰撞
#define HIT_NORMAL 1 // 发生一次普通碰撞(与矩形水平边或竖直边的碰撞)
#define HIT_VERTEX 2 // 与矩形顶点发生了碰撞
// 游戏状态
#define GAME_WIN 1 // 赢
#define GAME_LOSE 2 // 输
// 资源路径
#define SETTINGS_PATH _T("./BricksBeater.ini") // 配置文件
#define LEVEL_FOLDER_PATH ("./res/level") // 关卡文件存储路径(文件夹)
#define MUSIC_PING_PATH _T("./res/music/ping.mp3") // 碰撞声 1
#define MUSIC_PONG_PATH _T("./res/music/pong.mp3") // 碰撞声 2
#define MUSIC_PROP_PATH _T("./res/music/prop.mp3") // 领道具的声音
#define IMAGE_RESTART _T("./res/pic/restart.gif")
#define IMAGE_3X_PATH _T("./res/pic/prop_3x.gif")
#define IMAGE_SHOOT_3_PATH _T("./res/pic/prop_shoot_3.gif")
#define IMAGE_HEART_PATH _T("./res/pic/prop_heart.gif")
/////////////////// 全局变量 ///////////////////
////////// 游戏基础设定和资源
int g_nMapW; // 地图宽(单位:方块)
int g_nMapH; // 地图高(单位:方块)
int g_nLevelNum; // 关卡数
int g_nCurrentLevel; // 当前是第几关(从 0 开始数)
char*** g_pppszOriginalLevelMap; // 各关卡原始地图
// 调试模式配置
bool g_bDebugMode = false; // 是否处于调试模式(在配置文件中设置的)
bool g_bOnlyVertexCollisionSound = false; // 只有小球与矩形顶点发生碰撞时才发出声音
bool g_bBigScene = false; // 大场景模式
// 大场景模式配置
int g_nInnerSideLen_BigScene = 16; // 大场景模式下的方块内填充矩形边长
int g_nBallRadius_BigScene = 10; // 大场景模式下的小球半径
// 方块大小属性
int g_nInnerSideLen = 7; // 方块内填充矩形边长
int g_nInnerInterval = 1; // 方块外周留空边距
// 小球半径
int g_nBallRadius = 3;
// 道具出现概率(百分比)
int g_nProbability_3X = 8;
int g_nProbability_Shoot_3 = 12;
int g_nProbability_Heart = 5;
// 图像资源
IMAGE g_imgRestart;
IMAGE g_img3X;
IMAGE g_imgShoot3;
IMAGE g_imgHeart;
// “重新开始” 按钮区域
RECT g_rctRestartBtn;
//////////
bool g_bInMenu = true; // 是否还在主菜单
// 游戏状态
enum GameState { Gaming, GameWin, GameFail };
GameState g_enuGameState = Gaming;
// 砖块
struct Brick
{
int type; // 方块类型(砖块被打掉之后会被改为 EMPTY)
COLORREF color; // 方块颜色
RECT rct; // 砖块碰撞箱位置
}*g_pstMap;
int g_nBoardCenterX; // 挡板中心 x 坐标
// 小球
struct Ball
{
bool alive; // 该小球是否已经出界了
float x;
float y;
float vx;
float vy;
}g_pstBalls[BALL_MAX_NUM];
int g_nGeneratedBallNum; // 已经生成出的小球数量(小球出界不减数)
bool g_bBallWait; // 是否正在等待发射小球
int g_nRemainingBallNum; // 还剩几个球
// 道具
struct Prop
{
int type; // 种类
bool used; // 是否已被使用
float x;
float y;
}g_pstProps[PROP_MAX_NUM];
int g_nGeneratedPropNum; // 已经生成的道具数量
/////////////////// 函数定义 ///////////////////
// 加载游戏资源
void InitResource()
{
// 读取配置
g_nMapW = GetIniFileInfoInt(SETTINGS_PATH, _T("game"), _T("width"), 0);
g_nMapH = GetIniFileInfoInt(SETTINGS_PATH, _T("game"), _T("height"), 0);
g_nLevelNum = GetIniFileInfoInt(SETTINGS_PATH, _T("game"), _T("level_num"), 0);
g_nCurrentLevel = GetIniFileInfoInt(SETTINGS_PATH, _T("game"), _T("begin_level"), 1) - 1;
g_bDebugMode = GetIniFileInfoInt(SETTINGS_PATH, _T("debug"), _T("enable_debug_mode"), 0);
if (g_bDebugMode)
{
g_bOnlyVertexCollisionSound =
GetIniFileInfoInt(SETTINGS_PATH, _T("debug"), _T("only_vertex_collision_sound"), 0);
g_bBigScene =
GetIniFileInfoInt(SETTINGS_PATH, _T("debug"), _T("enable_big_scene"), 0);
g_nInnerSideLen_BigScene =
GetIniFileInfoInt(SETTINGS_PATH, _T("debug"), _T("big_scene_brick_inner_side_len"), 0);
g_nBallRadius_BigScene =
GetIniFileInfoInt(SETTINGS_PATH, _T("debug"), _T("big_scene_ball_radius"), 0);
// 简单模式
if (GetIniFileInfoInt(SETTINGS_PATH, _T("debug"), _T("easy_mode"), 0))
{
g_nProbability_3X = 100;
}
}
if (g_bBigScene)
{
g_nInnerSideLen = g_nInnerSideLen_BigScene;
g_nBallRadius = g_nBallRadius_BigScene;
}
// 加载关卡
// 分配每个关卡的内存
g_pppszOriginalLevelMap = new char** [g_nLevelNum];
for (int i = 0; i < g_nLevelNum; i++)
{
// 读取每个关卡文件
FILE* fp;
char pszPath[256] = {};
sprintf_s(pszPath, "%s/level_%d.dat", LEVEL_FOLDER_PATH, i + 1);
if (fopen_s(&fp, pszPath, "r"))
{
MessageBox(nullptr, _T("Error"), _T("加载关卡失败"), MB_OK);
exit(-1);
}
// 分配每一行的内存
g_pppszOriginalLevelMap[i] = new char* [g_nMapH];
for (int y = 0; y < g_nMapH; y++)
{
// 分配每个字符的内存
g_pppszOriginalLevelMap[i][y] = new char[g_nMapW + 1];
memset(g_pppszOriginalLevelMap[i][y], 0, (g_nMapW + 1) * sizeof(char));
// 读取一行
fread_s(g_pppszOriginalLevelMap[i][y], g_nMapW + 1, g_nMapW, 1, fp);
fseek(fp, 2, SEEK_CUR); // 跳过换行符
}
fclose(fp);
}
// 加载音像资源
mciSendString(_T("open ") MUSIC_PING_PATH _T(" alias ping"), NULL, 0, NULL);
mciSendString(_T("open ") MUSIC_PONG_PATH _T(" alias pong"), NULL, 0, NULL);
mciSendString(_T("open ") MUSIC_PROP_PATH _T(" alias prop"), NULL, 0, NULL);
loadimage(&g_imgRestart, IMAGE_RESTART);
loadimage(&g_img3X, IMAGE_3X_PATH);
loadimage(&g_imgShoot3, IMAGE_SHOOT_3_PATH);
loadimage(&g_imgHeart, IMAGE_HEART_PATH);
}
// 释放资源
void FreeResource()
{
// 释放原始关卡地图内存
for (int i = 0; i < g_nLevelNum; i++)
{
for (int y = 0; y < g_nMapH; y++)
{
delete[] g_pppszOriginalLevelMap[i][y]; // 释放当前行指向的字符
}
delete[] g_pppszOriginalLevelMap[i]; // 释放当前关卡的所有行
}
delete[] g_pppszOriginalLevelMap; // 释放所有关卡
g_pppszOriginalLevelMap = nullptr;
// 停止播放并关闭音乐
mciSendString(_T("stop ping"), NULL, 0, NULL);
mciSendString(_T("stop pong"), NULL, 0, NULL);
mciSendString(_T("stop prop"), NULL, 0, NULL);
mciSendString(_T("close ping"), NULL, 0, NULL);
mciSendString(_T("close pong"), NULL, 0, NULL);
mciSendString(_T("close prop"), NULL, 0, NULL);
}
// 播放碰撞声
// prop 是否播放道具声
void PlayHitSound(bool prop)
{
static bool flag = true; // 乒乓碰撞声交替标记
static clock_t record = 0; // 上次操作的记录
if (prop)
{
mciSendString(_T("play prop from 0"), NULL, 0, NULL);
}
else
{
// 避免过快操作,否则会一直从 0 开始播放,发不出声音
if (clock() - record > 30)
{
if (flag)
mciSendString(_T("play ping from 0"), NULL, 0, NULL);
else
mciSendString(_T("play pong from 0"), NULL, 0, NULL);
record = clock();
flag = !flag;
}
}
}
// 初始化一个关卡
void InitLevel()
{
g_pstMap = new Brick[g_nMapW * g_nMapH];
for (int x = 0; x < g_nMapW; x++)
{
for (int y = 0; y < g_nMapH; y++)
{
Brick* pBrick = &g_pstMap[x + y * g_nMapW];
int sx = x * SIDE_LEN;
int sy = y * SIDE_LEN;
// 设置碰撞箱
pBrick->rct = { sx, sy, sx + SIDE_LEN,sy + SIDE_LEN };
switch (g_pppszOriginalLevelMap[g_nCurrentLevel][y][x])
{
// 墙
case '#':
pBrick->type = WALL;
pBrick->color = COLOR_WALL;
break;
// 各种砖块
case 'R':
pBrick->type = BRICK;
pBrick->color = COLOR_BRICK_R;
break;
case 'Y':
pBrick->type = BRICK;
pBrick->color = COLOR_BRICK_Y;
break;
case 'G':
pBrick->type = BRICK;
pBrick->color = COLOR_BRICK_G;
break;
case 'C':
pBrick->type = BRICK;
pBrick->color = COLOR_BRICK_C;
break;
case 'B':
pBrick->type = BRICK;
pBrick->color = COLOR_BRICK_B;
break;
case 'W':
pBrick->type = BRICK;
pBrick->color = COLOR_BRICK_W;
break;
// 其它的当空气
default:
pBrick->type = EMPTY;
pBrick->color = BLACK; // 颜色没啥用
break;
}
}
}
g_nBoardCenterX = getwidth() / 2;
g_nGeneratedBallNum = 0;
g_bBallWait = true;
g_nRemainingBallNum = BALL_LIFE_NUM;
int pos_x = 15;
int pos_y = 15;
g_rctRestartBtn = {
getwidth() - g_imgRestart.getwidth() - pos_x,
pos_y,
getwidth() - pos_x,
pos_y + g_imgRestart.getheight()
};
g_nGeneratedPropNum = 0;
}
// 释放当前关卡内存
void FreeLevel()
{
delete[] g_pstMap;
g_pstMap = nullptr;
}
// 绘制道具
void DrawProp()
{
for (int i = 0; i < g_nGeneratedPropNum; i++)
{
if (g_pstProps[i].used)
continue;
IMAGE* pimg = nullptr;
switch (g_pstProps[i].type)
{
case PROP_3X: pimg = &g_img3X; break;
case PROP_SHOOT_3: pimg = &g_imgShoot3; break;
case PROP_HEART: pimg = &g_imgHeart; break;
}
if (pimg)
putimage((int)g_pstProps[i].x, (int)g_pstProps[i].y, pimg);
}
}
// 绘制地图
void DrawMap()
{
for (int i = 0; i < g_nMapW * g_nMapH; i++)
{
if (g_pstMap[i].type != EMPTY)
{
RECT rct = g_pstMap[i].rct;
setfillcolor(g_pstMap[i].color);
solidrectangle(
rct.left + g_nInnerInterval,
rct.top + g_nInnerInterval,
rct.right - g_nInnerInterval - 1,
rct.bottom - g_nInnerInterval - 1
);
}
}
}
// 绘制小球
void DrawBall()
{
setfillcolor(WHITE);
// 小球还没发射
if (g_bBallWait)
{
solidcircle(g_nBoardCenterX, BOARD_Y - BOARD_BALL_DISTANCE, g_nBallRadius);
}
// 小球运动中
else
{
for (int i = 0; i < g_nGeneratedBallNum; i++)
{
if (g_pstBalls[i].alive)
{
solidcircle((int)g_pstBalls[i].x, (int)g_pstBalls[i].y, g_nBallRadius);
}
}
}
}
// 绘制挡板
void DrawBoard()
{
setfillcolor(WHITE);
solidrectangle(
g_nBoardCenterX - BOARD_HALF_WIDTH,
BOARD_Y - BOARD_HALF_THICKNESS,
g_nBoardCenterX + BOARD_HALF_WIDTH,
BOARD_Y + BOARD_HALF_THICKNESS
);
}
// 绘制状态栏
void DrawStateBar()
{
// 还没开始游戏
if (g_bInMenu)
{
return;
}
int x = getwidth() / 2;
setfillcolor(WHITE);
solidcircle(x - 20, 28, 10); // 画个球
// 显示关卡信息
TCHAR lpszLevel[64] = {};
wsprintf(lpszLevel, _T("Level %d / %d"), g_nCurrentLevel + 1, g_nLevelNum);
settextstyle(28, 0, _T("System"));
outtextxy(10, 22, lpszLevel);
// 显示一下还剩几个球
TCHAR lpszBuf[12] = {};
wsprintf(lpszBuf, _T("%02d"), g_nRemainingBallNum);
settextstyle(24, 0, _T("System"));
outtextxy(x, 22, lpszBuf);
// “重新开始” 按钮
putimage(g_rctRestartBtn.left, g_rctRestartBtn.top, &g_imgRestart);
}
// 在屏幕中央输出大字
void DrawTitle(LPCTSTR text)
{
setfillcolor(BLACK);
solidrectangle(0, 20, getwidth(), 140);
settextstyle(48, 0, _T("System"));
int w = textwidth(text);
int h = textheight(text);
outtextxy((getwidth() - w) / 2, 20 + (120 - h) / 2, text);
}
// 在屏幕中央输出小字
void DrawSmallText(LPCTSTR text)
{
setfillcolor(BLACK);
solidrectangle(0, 150, getwidth(), 200);
settextstyle(24, 0, _T("System"));
int w = textwidth(text);
int h = textheight(text);
outtextxy((getwidth() - w) / 2, 150 + (50 - h) / 2, text);
}
// 胜利界面
void Win()
{
DrawTitle(_T("Clear"));
if (g_nCurrentLevel == g_nLevelNum - 1)
DrawSmallText(_T("Back to menu"));
else
DrawSmallText(_T("Next level"));
}
// 失败界面
void Fail()
{
DrawSmallText(_T("Restart"));
}
// 绘制菜单
void DrawMenu()
{
// 已开始游戏
if (!g_bInMenu)
{
if (g_enuGameState == GameWin)
{
Win();
}
else if (g_enuGameState == GameFail)
{
Fail();
}
else
{
return;
}
FlushBatchDraw();
// 等按下按键
while (true)
{
ExMessage msg = getmessage(EX_MOUSE);
if (msg.message == WM_LBUTTONUP
&& (msg.y > 150 + STATE_BAR_HEIGHT && msg.y < 200 + STATE_BAR_HEIGHT))
{
break;
}
}
FreeLevel();
bool show_menu = false; // 是否继续显示主菜单
// 若是在胜利界面,则需要进入下一个关卡
if (g_enuGameState == GameWin)
{
// 关卡打完了,回到主界面
if (g_nCurrentLevel == g_nLevelNum - 1)
{
g_nCurrentLevel = 0;
g_bInMenu = true;
show_menu = true;
}
else
{
g_nCurrentLevel++;
}
}
InitLevel();
g_enuGameState = Gaming;
if (!show_menu)
return;
}
DrawTitle(_T("Bricks Beater"));
// 致敬原游戏
DrawSmallText(_T("Salute Many Bricks Breaker"));
//DrawSmallText(_T("Salute "));
//DrawSmallText(_T("Many Bricks Breaker"), true);
settextstyle(20, 0, _T("System"));
LPCTSTR lpszTip = _T("Click to start");
int w = textwidth(lpszTip);
outtextxy((getwidth() - w) / 2, BOARD_Y + BOARD_HALF_THICKNESS + 10, lpszTip);
}
// 获取点到矩形的最小距离
float GetDistance_PointToRect(float x, float y, RECT rct)
{
float x_rct, y_rct; // 保存矩形内到目标点最近的点
if (x >= rct.left && x <= rct.right)
x_rct = x;
else
x_rct = (float)(fabsf(x - rct.left) < fabsf(x - rct.right) ? rct.left : rct.right);
if (y >= rct.top && y <= rct.bottom)
y_rct = y;
else
y_rct = (float)(fabsf(y - rct.top) < fabsf(y - rct.bottom) ? rct.top : rct.bottom);
float dx = x - x_rct;
float dy = y - y_rct;
return sqrtf(dx * dx + dy * dy);
}
// 根据圆的轨迹直线,获取圆与某点相切时的圆心坐标
// 返回是否存在相切
bool GetTangentCirclePoint(
float x0, // 切点坐标
float y0,
float x1, // 圆心轨迹直线上的一点(更早运动到的点)
float y1,
float x2, // 圆心轨迹直线上的另一点(其实运动不到的点)
float y2,
float r, // 圆半径
float* p_out_x, // 输出圆心坐标
float* p_out_y
)
{
// 斜率不存在时
if (fabsf(x1 - x2) < 0.00001f)
{
// 计算相切时圆心与切点的竖直距离
float d2 = r * r - (x0 - x1) * (x0 - x1);
if (d2 < 0)
return false;
float d = sqrtf(d2);
// 求出两组解
float _y1 = y0 + d;
float _y2 = y0 - d;
// 保留离 (x1, y1) 更近的解
float _y_closer = fabsf(y1 - _y1) < fabsf(y1 - _y2) ? _y1 : _y2;
*p_out_x = x1;
*p_out_y = _y_closer;
return true;
}
// 圆心轨迹直线方程:y - y1 = (y2 - y1) / (x2 - x1) * (x - x1)
// 即:y = kx - kx1 + y1
// 圆的方程:(x - x0) ^ 2 + (y - y0) ^ 2 = r ^ 2
// 联立得二次函数,如下。
float k = (y2 - y1) / (x2 - x1); // 直线斜率
float m = -k * x1 + y1 - y0; // 部分常数
float a = k * k + 1; // 二次函数的 abc 系数
float b = 2 * (k * m - x0);
float c = x0 * x0 + m * m - r * r;
float delta = b * b - 4 * a * c; // 判别式
if (delta < 0) // 无解
return false;
float sqrt_delta = sqrtf(delta); // 判别式开根号
float _x1 = (-b + sqrt_delta) / (2 * a); // 两个根
float _x2 = (-b - sqrt_delta) / (2 * a);
// 保留离 (x1, y1) 更近的解
float _x_closer = fabsf(x1 - _x1) < fabsf(x1 - _x2) ? _x1 : _x2;
float _y = k * _x_closer - k * x1 + y1;
*p_out_x = _x_closer;
*p_out_y = _y;
return true;
}
// 单个碰撞箱和单个小球的碰撞处理
// ball 小球
// rct 碰撞箱区域
// is_board 是否为挡板碰撞判定
// left, up, right, down 该碰撞箱四周分别是否有活动空间
//
// 返回值:参见 HIT_ 系列宏
int SingleRect_BallHit(Ball* ball, RECT rct, bool is_board, bool left = true, bool up = true, bool right = true, bool down = true)
{
// 得到小球上一帧的坐标
float last_x = (ball->x - ball->vx);
float last_y = (ball->y - ball->vy);
// 如果小球不慎进入砖块内部,此时应该回到上一帧,并使速度反向(同时加以扰动)
if (ball->x >= rct.left && ball->x <= rct.right
&& ball->y >= rct.top && ball->y <= rct.bottom)
{
ball->x = last_x;
ball->y = last_y;
// 如果上一帧仍然陷在里面
if (ball->x >= rct.left && ball->x <= rct.right
&& ball->y >= rct.top && ball->y <= rct.bottom)
{
// 强制弹出小球
if (ball->vx > 0 && left)
{
ball->x = (float)(rct.left - g_nBallRadius);
}
else if (ball->vx < 0 && right)
{
ball->x = (float)(rct.right + g_nBallRadius);
}
if (ball->vy > 0 && up)
{
ball->y = (float)(rct.top - g_nBallRadius);
}
else if (ball->vy < 0 && down)
{
ball->y = (float)(rct.bottom + g_nBallRadius);
}
}
ball->vx *= -1 + rand() % 10 / 1000.f;
ball->vy *= -1 + rand() % 10 / 1000.f;
// 对于挡板问题的特殊处理:直接跳出来
if (is_board)
{
ball->y = (float)(BOARD_Y - BOARD_HALF_THICKNESS - g_nBallRadius - 1);
}
return HIT_NORMAL;
}
// 首先需要保证矩形和圆有重叠
if (!(GetDistance_PointToRect(ball->x, ball->y, rct) <= g_nBallRadius * 0.98f))
{
return HIT_NONE;
}
int return_flag = HIT_NONE;
// 穿越碰撞箱边界标记
bool cross_left =
rct.left > ball->x
&& fabsf(ball->x - rct.left) <= g_nBallRadius
&& left;
bool cross_right =
ball->x > rct.right
&& fabsf(ball->x - rct.right) <= g_nBallRadius
&& right;
bool cross_top =
rct.top > ball->y
&& fabsf(ball->y - rct.top) <= g_nBallRadius
&& up;
bool cross_bottom =
ball->y > rct.bottom
&& fabsf(ball->y - rct.bottom) <= g_nBallRadius
&& down;
// 标记是否需要判断顶点碰撞
bool vertex_judge_flag = true;
float fVertex_X = 0; // 判定顶点碰撞时使用的顶点
float fVertex_Y = 0;
if (cross_left && cross_top) // 左上角
{
//vertex_judge_flag = true;
fVertex_X = (float)rct.left;
fVertex_Y = (float)rct.top;
}
else if (cross_right && cross_top) // 右上角
{
//vertex_judge_flag = true;
fVertex_X = (float)rct.right;
fVertex_Y = (float)rct.top;
}
else if (cross_left && cross_bottom) // 左下角
{
//vertex_judge_flag = true;
fVertex_X = (float)rct.left;
fVertex_Y = (float)rct.bottom;
}
else if (cross_right && cross_bottom) // 右下角
{
//vertex_judge_flag = true;
fVertex_X = (float)rct.right;
fVertex_Y = (float)rct.bottom;
}
// 如果没有同时穿越 xy 两个方向,就需要再评估用哪个顶点
else
{
// 如果穿越上下边界,则就只需要决定使用左边还是右边的顶点
if (cross_top || cross_bottom)
{
fVertex_Y = cross_top ? (float)rct.top : (float)rct.bottom; // 首先就可以确定 Y
// 若左右都有空地,则在左右顶点中选距离近的
if (left && right)
{
fVertex_X =
(fabsf(ball->x - rct.left) < fabsf(ball->x - rct.right)) ?
(float)rct.left : (float)rct.right;
}
// 否则哪边空选哪边
else if (left) fVertex_X = (float)rct.left;
else if (right) fVertex_X = (float)rct.right;
// 要是两边都没空地,就不可能发生顶点碰撞
else
{
vertex_judge_flag = false;
}
}
// 如果穿越左右边界
else if (cross_left || cross_right)
{
fVertex_X = cross_left ? (float)rct.left : (float)rct.right;
if (up && down)
{
fVertex_Y =
(fabsf(ball->y - rct.top) < fabsf(ball->y - rct.bottom)) ?
(float)rct.top : (float)rct.bottom;
}
else if (up) fVertex_Y = (float)rct.top;
else if (down) fVertex_Y = (float)rct.bottom;
else
{
vertex_judge_flag = false;
}
}
else
{
vertex_judge_flag = false;
}
}
// 优先判断是不是顶点碰撞
bool isVertexCollision = false; // 标记是否发生顶点碰撞
if (vertex_judge_flag) // 处理顶点碰撞问题
{
// 获取碰撞时的小球圆心坐标(即与顶点相切时的坐标)
float fCollisionX, fCollisionY;
if (!GetTangentCirclePoint(
fVertex_X,
fVertex_Y,
last_x,
last_y,
ball->x,
ball->y,
(float)g_nBallRadius,
&fCollisionX,
&fCollisionY
))
{
// 没有相切,说明顶点碰撞不成立
goto tag_after_vertex_colision;
}
// 如果是真的相切,则相切时矩形到圆心的最近距离应该等于小球半径
// 但如果此时小于半径,那么说明是假相切
if (GetDistance_PointToRect(fCollisionX, fCollisionY, rct) < g_nBallRadius * 0.98f /* 允许一点误差 */)
{
goto tag_after_vertex_colision;
}
// 计算碰撞时,小球圆心到碰撞点的坐标差
float f_dx = fCollisionX - fVertex_X;
float f_dy = fCollisionY - fVertex_Y;
// 求反射面弧度
float f_radianReflectingSurface = atan2f(f_dy, f_dx);
// 求法线弧度
float f_radianNormal = f_radianReflectingSurface + PI / 2 /* 或 - PI / 2 */;
// 求小球入射弧度
float f_radianIncidence = atan2f(ball->vy, ball->vx);
// 将小球速度沿法线对称,求得新的速度弧度
float f_radianReflection = 2 * f_radianNormal - f_radianIncidence;
// 求速度
ball->vx = cosf(f_radianReflection) * BALL_SPEED;
ball->vy = sinf(f_radianReflection) * BALL_SPEED;
// 修正小球坐标到相切时的坐标
ball->x = fCollisionX;
ball->y = fCollisionY;
isVertexCollision = true; // 标记发生顶点碰撞
}
tag_after_vertex_colision:
// 已完成顶点碰撞
if (isVertexCollision)
{
return_flag = HIT_VERTEX;
}
// 基础碰撞
else
{
// 跨越碰撞箱左右边界,则水平速度反转
if (cross_left || cross_right)
{
ball->vx = -ball->vx;
return_flag = HIT_NORMAL;
}
// 跨越碰撞箱上下边界,则竖直速度反转
if (cross_top || cross_bottom)
{
// 与挡板碰撞时,需要根据小球和挡板的碰撞位置改变其 vx
if (is_board)
{
ball->vx = (float)(ball->x - g_nBoardCenterX) / BOARD_HALF_WIDTH * BALL_SPEED;
if (fabsf(ball->vx) > BALL_SPEED)
{
ball->vx = ball->vx > 0 ? BALL_SPEED : -BALL_SPEED;
ball->vy = 0;
}
else
{
ball->vy = (ball->vy < 0 ? 1 : -1) * sqrtf(BALL_SPEED * BALL_SPEED - ball->vx * ball->vx);
}
}
else
{
ball->vy = -ball->vy;
}
return_flag = HIT_NORMAL;
}
}
// 播放碰撞声
// 非调试模式下,只要有碰撞就发出声音
if (!g_bOnlyVertexCollisionSound
&& return_flag != HIT_NONE)
{
PlayHitSound(false);
}
// 调试模式下,只有顶点碰撞发出声音
else if (return_flag == HIT_VERTEX)
{
PlayHitSound(false);
}
// 回溯坐标,即发生碰撞后,把小球从墙里“拔”出来(回到上一帧的位置),避免穿墙效果
// 如果发生的是顶点碰撞,那么在前面就已经进行了相切位置修正,就不需要回溯坐标了
if (!isVertexCollision && return_flag != HIT_NONE)
{
ball->x = last_x;
ball->y = last_y;
}
return return_flag;
}// SingleRect_BallHit
// 消除砖块后,调用此函数幸运创建道具
// x, y 被消除砖块的位置
void GeneratePropByLuck(float x, float y)
{
// 道具已满
if (g_nGeneratedPropNum == PROP_MAX_NUM)
{
return;
}
// 道具表
int pnProps[3][2] = {
{PROP_3X,g_nProbability_3X},
{PROP_SHOOT_3,g_nProbability_Shoot_3},
{PROP_HEART,g_nProbability_Heart},
};
// 随机选取一个道具
int choice = rand() % 3;
// 根据概率决定是否生成这个道具
if (rand() % 100 < pnProps[choice][1])
{
// 创建道具
Prop prop;
prop.type = pnProps[choice][0];
prop.used = false;
prop.x = x;
prop.y = y;