-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFinal_Project.cpp
More file actions
6121 lines (5475 loc) · 246 KB
/
Copy pathFinal_Project.cpp
File metadata and controls
6121 lines (5475 loc) · 246 KB
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 <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
#include <mmsystem.h>
#include <windows.h>
#pragma comment(lib, "winmm.lib")
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#define TINYOBJLOADER_IMPLEMENTATION
#include "glut.h"
#include "tiny_obj_loader.h"
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
// 網路相關
bool initNetwork();
void checkForConnections();
void handleClientCommands();
void cleanupNetwork();
// 視窗/全螢幕 切換用
bool gFullScreen = false;
int gWinPosX = 50, gWinPosY = 50; // 回到視窗模式時要擺回來的位置
int gWinWidth = 1640, gWinHeight = 720; // 回到視窗模式時用的大小 (啟動時就是它)
bool previewCamMoveForward = false;
bool previewCamMoveBackward = false;
bool previewCamMoveLeft = false;
bool previewCamMoveRight = false;
GLuint texMenu = 0;
GLuint texSettlement = 0; // 結算畫面背景紋理
GLuint texAsphalt = 0; // 柏油路紋理 ID
GLuint texSand = 0;
GLuint texGrass = 0;
using namespace std;
HWND g_hWnd = NULL;
int winW = 1280, winH = 720; // updated in reshape
enum GameMode { MODE_MENU, MODE_SOLO, MODE_PVP, MODE_AI, MODE_RESULT, MODE_LOADING, MODE_TRACK_PREVIEW };
GameMode gameMode = MODE_MENU; // 初始顯示選單
GameMode previousGameMode = MODE_MENU; // 儲存切換到結算畫面前的模式
GameMode targetGameMode = MODE_MENU; // 儲存從載入畫面要跳轉的模式
float finalTimeP1 = 0.0f; // 玩家1完成時間
float finalTimeP2 = 0.0f; // 玩家2完成時間
int finalRankP1 = 0; // 玩家1排名
int finalRankP2 = 0; // 玩家2排名
bool p1Finished = false; // 玩家1是否完成比賽
bool p2Finished = false; // 玩家2是否完成比賽
int frameCount = 0;
int previousTime = 0;
float fps = 0.0f;
void drawFPS();
float loadingProgress = 0.0f;
float loadingTime = 0.0f;
const float LOADING_DURATION = 1.0f;
struct UIButton {
int x, y, w, h;
const char* text;
};
bool isSettingsMenuOpen = false;
UIButton btnSettingsIcon{15, 0, 40, 40, "S"};
UIButton btnReturnToMenuFromSettings{0, 0, 200, 50, "Return & Reset"};
// ---- 背景音樂相關 ----
bool backgroundMusicEnabled = true; // 預設開啟背景音樂
bool musicPlaying = false; // 目前音樂是否正在播放
UIButton btnToggleMusic{0, 0, 200, 50, "Music: ON"}; // 設定選單中的音樂開關按鈕
// ---- 音效相關 ----
bool sfxEnabled = true; // 預設開啟音效
bool isWindPlaying = false; // 風聲是否正在播放
UIButton btnToggleSFX{0, 0, 200, 50, "SFX: ON"}; // 設定選單中的音效開關按鈕
struct MaterialInfo {
string name;
float ambient[3] = {0.2f, 0.2f, 0.2f};
float diffuse[3] = {0.8f, 0.8f, 0.8f};
float specular[3] = {1.0f, 1.0f, 1.0f};
float shininess = 32.0f;
GLuint texture_id = 0; // 漫反射紋理 ID
};
struct MeshPart {
vector<float> vertices;
vector<float> normals;
vector<float> texcoords;
int material_index = -1; // 對應到材質庫中的索引
};
struct ModelObject {
vector<MeshPart> meshes;
vector<MaterialInfo> materials;
};
ModelObject mustangModel, CasteliaCity, BusGameMap, Cliff, TropicalIslands, Cone1, Cone2; // 為野馬模型新增一個物件/**
UIButton btnSolo{50, winH / 2 + 100, 180, 60, "Solo"};
UIButton btnPVP{50, winH / 2 + 20, 180, 60, "1 v 1"};
UIButton btnAI{50, winH / 2 - 60, 180, 60, "1 v PC"};
UIButton btnPreview{50, winH / 2 - 140, 180, 60, "Track Preview"}; // 新增賽道預覽按鈕
// Leaderboard
const char* SCORE_FILE = "logs/scores.txt";
vector<float> scores; // 依成績秒數遞增排序
const int TOP_N = 8; // 只顯示前 8 名
const int DUST_NUM = 80;
const float CAM_H = 6.0f; // 基本高度
const float CAM_DIST = 8.0f; // 與車後距離
const float CAM_POS_SMOOTH_RATE = 5.0f; // 位置平滑率:數值越大,跟隨越快 (反應更靈敏,平滑度降低)
const float CAM_LOOK_SMOOTH_RATE = 10.0f; // 視角目標點平滑率
const float CAM_FOV_SMOOTH_RATE = 10.0f; // FOV平滑率
const float MAX_DIST_LEASH_TOLERANCE = 10.0f; // 拴繩容忍距離:攝影機實際位置可以比理想跟隨距離 (dynamicDist) 遠多少
const float LOOK_AHEAD = 6.0f; // 視線往前看多少
const float BASE_FOV = 60.0f;
const float MAX_FOV = 70.0f; // 全速時的視角
const float MIN_CAM_DIST = 2.0f; // 最小相機距離(高速時)
const float MAX_CAM_DIST = 8.0f; // 最大相機距離(低速時)
void loadScores() {
scores.clear();
ifstream in(SCORE_FILE);
float t;
while (in >> t) scores.push_back(t);
sort(scores.begin(), scores.end());
}
void saveScore(float t) { // append + 重新排序
ofstream out(SCORE_FILE, ios::app);
out << fixed << setprecision(2) << t << '\n';
out.close();
loadScores();
}
struct Dust {
float x, y; // 2D 位置
float r; // 半徑
float vx, vy; // 速度 (px/s) >0 右上
float a; // alpha
};
vector<Dust> dust;
struct State {
float x, y, z, heading, speed;
};
struct Cloud {
float x, y; // 左下角座標
float w, h; // 雲寬高
float speed; // px/sec
};
vector<Cloud> clouds;
struct Vec3 {
float x{}, y{}, z{};
Vec3() = default;
Vec3(float X, float Y, float Z) : x(X), y(Y), z(Z) {}
Vec3 operator+(const Vec3& o) const {
return {x + o.x, y + o.y, z + o.z};
}
Vec3 operator-(const Vec3& o) const {
return {x - o.x, y - o.y, z - o.z};
}
Vec3 operator*(float s) const {
return {x * s, y * s, z * s};
}
};
// 滑鼠拖曳相關變數
bool isMouseDragging = false;
int lastMouseX = 0, lastMouseY = 0;
Vec3 originalCamPos, originalLookTarget; // 保存原始攝像機位置
Vec3 originalCam2Pos, originalLookTarget2; // 第二台車的攝像機
float originalFov, originalFov2; // 原始視野
bool startCameraRestoration = false; // 控制攝影機恢復動畫
/* ============ Camera ============ */
struct Camera {
Vec3 pos; // 畫面所在位置
float fov = 60.0f; // 目前 FOV
Vec3 lookTarget; // 新增:儲存平滑化後的目標點
} cam, cam2; // Add a second camera for car2
const float CAR_COLLISION_RADIUS = 4.5f; // 車輛碰撞半徑 (可根據模型大小微調)
const float COLLISION_RESTITUTION = 0.8f; // 碰撞恢復係數 (0.0 = 完全無彈性, 1.0 = 完全彈性)
void handleCarCollision();
static float length(const Vec3& v) {
return sqrtf(v.x * v.x + v.y * v.y + v.z * v.z);
}
static Vec3 normalize(const Vec3& v) {
float l = length(v);
if (l < 1e-6f) return {0, 0, 0};
return {v.x / l, v.y / l, v.z / l};
}
// 外積函式
static Vec3 cross(const Vec3& a, const Vec3& b) {
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
}
// 記錄上一幀車輛是否在終點區域內,以檢測“進入”
static bool car1WasInFinishVolume = false;
static bool car2WasInFinishVolume = false;
static Vec3 catmull(const Vec3& p0, const Vec3& p1, const Vec3& p2, const Vec3& p3, float t) {
float t2 = t * t, t3 = t2 * t;
return (p1 * 2.0f + (p2 - p0) * t + (p0 * 2.0f - p1 * 5.0f + p2 * 4.0f - p3) * t2 +
(p3 - p0 + (p1 - p2) * 3.0f) * t3) *
0.5f;
}
vector<Vec3> ctrl = {{360, 0, 0}, {360, 0, 200}, {240, 0, 360}, {120, 0, 450}, {-150, 0, 430},
{-280, 0, 300}, {-330, 0, 120}, {-400, 0, 0}, {-360, 0, -120}, {-240, 0, -240},
{-160, 0, -360}, {-40, 0, -400}, {80, 0, -320}, {120, 0, -200}};
vector<Vec3> trackCtrl;
vector<Vec3> trackSamples;
// ---- 起點 / 終點拱門位置與朝向 ----
Vec3 gateStartPos, gateFinishPos;
float gateStartHdg = 0.0f, gateFinishHdg = 0.0f;
constexpr int SAMPLE_DENSITY = 2000;
float trackLength = 0.0f;
const float ROAD_HALF_W = 20.0f;
// ----- 路面分區 -----
const float SAND_HALF_W = 60.0f; // 沙地外緣(中心線 ±20u)
const float MAX_F_ROAD = 120.0f; // 柏油最高速
const float MAX_F_SAND = 70.0f; // 沙地最高速
const float MAX_F_GRASS = 25.0f; // 草地最高速(失控)
// Mini‑map extents
float mapMinX, mapMaxX, mapMinZ, mapMaxZ;
int prevMs = 0;
// --- 即時排名距離量測 ---
float travelP1 = 0.0f; // 賽道進度 (car)
float travelP2 = 0.0f; // 賽道進度 (car2)
int lastCheckpointP1 = 0; // 車輛1最後經過的賽道檢查點索引
int lastCheckpointP2 = 0; // 車輛2最後經過的賽道檢查點索引
int lapP1 = 0; // 車輛1的圈數
int lapP2 = 0; // 車輛2的圈數
bool countdownActive = false;
float countdownLeft = 0.0f; // 秒
bool forceSettlementActive = false; // 強制結算倒計時標誌
float forceSettlementTimer = 0.0f; // 強制結算倒計時(秒)
const float FORCE_SETTLEMENT_TIME = 10.0f; // 強制結算等待時間(秒)
// ---- 終點偵測 ----
Vec3 finishVolumeMin; // 終點空間的最小角點 (min x, min y, min z)
Vec3 finishVolumeMax; // 終點空間的最大角點 (max x, max y, max z)
const float FINISH_VOLUME_WIDTH = ROAD_HALF_W * 5.0f; // 終點空間的寬度 (略大於路寬)
const float FINISH_VOLUME_HEIGHT = 10.0f; // 終點空間的高度 (確保能覆蓋車輛)
const float FINISH_VOLUME_DEPTH = 5.0f; // 終點空間的深度 (沿賽道方向的厚度)
// ---- 賽車計時器 ----
float raceTime = 0.0f; // 累積秒數
bool timerRunning = true;
bool firstFrame = true; // 用來取得第一幀的側向基準
const int UI_BTN_W0 = 180;
const int UI_BTN_H0 = 60;
const int UI_BTN_X0 = 50;
const int UI_BTN_GAP = 80; // 垂直間距 (PVP ↔ AI)
const int UI_TITLE_OF = 40; // 標題距上方按鈕的位移
void drawPlayerLabel(int playerNum, bool isLeft);
void drawVerticalDivider();
void drawSpeedometer(float speed, bool isLeftSide = false);
void drawLiveRanking(bool isLeftSide = false);
void drawForceSettlementTimer(bool isLeftSide = false); // 添加強制結算倒計時顯示函數聲明
void drawHUDTimer(bool isLeftSide = false); // 更新計時器函數聲明
void drawRail(const Vec3& start, const Vec3& end, float beam_height, float beam_depth); // 繪製護欄組件
void drawTrackBarriers(); // 繪製賽道圍欄
void drawCurbstones(); // 繪製紅白路緣石
void drawLoadingScreen(); // 繪製載入畫面
void drawFPS(); // 繪製FPS的函式
void drawEnvironmentModels();
void drawTrackPreviewUI();
void drawFinishVolume(); // 繪製終點檢測空間
void fullResetToMenu();
void drawSettingsElements(int, int); // 繪製設定按鈕和彈出選單
void drawBackground();
void drawTrafficCones();
void setImeMode(bool active) {
if (g_hWnd == NULL) {
// 第一次呼叫時,找到目前的視窗並儲存其控制代碼
g_hWnd = FindWindow(NULL, "Speed Racing");
if (g_hWnd == NULL) {
cerr << "[ERROR] Cannot find window to set IME mode." << endl;
return;
}
}
// 取得輸入法上下文 (Input Method Context)
HIMC hImc = ImmGetContext(g_hWnd);
if (hImc) {
// 開啟或關閉輸入法
ImmSetOpenStatus(hImc, active);
// 釋放上下文
ImmReleaseContext(g_hWnd, hImc);
}
}
// ---- 背景音樂控制函式 ----
void playBackgroundMusic(const char* filePath) {
if (!backgroundMusicEnabled || musicPlaying) return;
mciSendString("close bgmusic", NULL, 0, NULL);
string openCmd = "open \"" + string(filePath) + "\" alias bgmusic";
if (mciSendString(openCmd.c_str(), NULL, 0, NULL) == 0) {
mciSendString("setaudio bgmusic volume to 100", NULL, 0, NULL);
if (mciSendString("play bgmusic repeat", NULL, 0, NULL) == 0) {
musicPlaying = true;
cout << "[INFO] Background music started: " << filePath << endl;
} else {
cerr << "[ERROR] Failed to play background music." << endl;
mciSendString("close bgmusic", NULL, 0, NULL);
}
} else {
cerr << "[ERROR] Failed to open background music file: " << filePath << endl;
}
}
void stopBackgroundMusic() {
if (musicPlaying) {
mciSendString("stop bgmusic", NULL, 0, NULL);
mciSendString("close bgmusic", NULL, 0, NULL);
musicPlaying = false;
cout << "[INFO] Background music stopped." << endl;
}
mciSendString("close sfx", NULL, 0, NULL);
}
// ---- 播放一次性音效的函數 ----
// 輔助函數:印出詳細的 MCI 錯誤訊息
void printMCIError(DWORD_PTR dwError) {
char errorText[256];
if (mciGetErrorString(dwError, errorText, sizeof(errorText))) {
// 如果能成功取得錯誤字串
cerr << "[MCI ERROR] Code " << dwError << ": " << errorText << endl;
} else {
// 如果連錯誤字串都拿不到
cerr << "[MCI ERROR] Code " << dwError << ": An unknown error occurred." << endl;
}
}
// ---- 播放循環音效的通用函數 ----
void playLoopingSound(const char* alias, const char* filePath) {
string openCmd = "open \"" + string(filePath) + "\" type mpegvideo alias " + string(alias);
if (mciSendString(openCmd.c_str(), NULL, 0, NULL) == 0) {
string playCmd = "play " + string(alias) + " repeat";
mciSendString(playCmd.c_str(), NULL, 0, NULL);
} else {
cerr << "[ERROR] Failed to open looping sound file: " << filePath << endl;
}
}
// ---- 停止循環音效的通用函數 ----
void stopLoopingSound(const char* alias) {
string closeCmd = "close " + string(alias);
mciSendString(closeCmd.c_str(), NULL, 0, NULL);
}
void playSoundEffect(const char* filePath) {
if (!sfxEnabled) {
return;
}
// 之前會關閉其他同名音效,保留這個行為
mciSendString("close click", NULL, 0, NULL);
// *** 核心修改:移除 "type waveaudio",讓MCI自動判斷 ***
// 並且使用一個專門的別名 "click" 來播放按鈕音效,避免與其他音效衝突
string openCmd = "open \"" + string(filePath) + "\" alias click";
DWORD_PTR dwError = mciSendString(openCmd.c_str(), NULL, 0, NULL);
if (dwError == 0) {
// 從頭開始播放
mciSendString("play click from 0", NULL, 0, NULL);
} else {
cerr << "[ERROR] Failed to open sound effect file: " << filePath << endl;
printMCIError(dwError);
}
}
// ---- 背景音樂控制函式 ----
void toggleBackgroundMusic() {
backgroundMusicEnabled = !backgroundMusicEnabled;
btnToggleMusic.text = backgroundMusicEnabled ? "Music: ON" : "Music: OFF";
if (backgroundMusicEnabled) {
if (gameMode == MODE_MENU && !musicPlaying) {
playBackgroundMusic("audio/Menu.mp3");
} else if ((gameMode == MODE_SOLO || gameMode == MODE_PVP || gameMode == MODE_AI) && !musicPlaying) {
playBackgroundMusic("audio/game.mp3");
} else if (gameMode == MODE_RESULT && !musicPlaying) {
playBackgroundMusic("audio/Result.mp3");
}
} else {
stopBackgroundMusic();
}
std::cout << "[INFO] Background music " << (backgroundMusicEnabled ? "enabled" : "disabled") << std::endl;
}
// ---- 音效(SFX)控制函式 ----
void toggleSfx() {
sfxEnabled = !sfxEnabled;
btnToggleSFX.text = sfxEnabled ? "SFX: ON" : "SFX: OFF";
if (!sfxEnabled) {
// 如果關閉音效,立刻停止風聲
if (isWindPlaying) {
stopLoopingSound("wind");
isWindPlaying = false;
}
}
std::cout << "[INFO] Sound effects " << (sfxEnabled ? "enabled" : "disabled") << std::endl;
}
float lateralOffsetFromCenter(const Vec3& pos) {
float best2 = FLT_MAX;
for (const auto& p : trackSamples) {
Vec3 d = pos - p;
float d2 = d.x * d.x + d.z * d.z;
if (d2 < best2) best2 = d2;
}
return sqrtf(best2);
}
struct PlayerInputAction {
float timestamp; // 從比賽開始的時間戳 (秒)
unsigned char key; // 按下的鍵 (例如 'W', 'A', 'S', 'D', 或特殊鍵的代碼)
bool isDown; // true 表示按下, false 表示釋放
};
class Car {
public:
float x = 0, y = 0, z = 0, speed = 0, heading = 0;
vector<State> log;
bool kW = false, kS = false, kA = false, kD = false;
float current_steer_force = 0.0f; // 目前的轉向力度 (-1.0 到 1.0)
const float STEER_CHANGE_RATE = 2.5f; // 方向盤轉動/回正速率
// 較小的值會使轉向和回正更慢、更平滑
// 較大的值會使轉向和回正更快、更靈敏
bool isAIControlledByReplay = false; // 新增:標記是否由 Solo 記錄控制
vector<State> replayLogToFollow; // 新增:儲存要播放的 Solo 記錄
int currentReplayFrameIndex = 0; // 新增:目前播放到 Solo 記錄的哪一幀
vector<PlayerInputAction> recordedInputs; // 儲存玩家的按鍵操作序列
bool isAIControlledByInputReplay = false; // 新標記,表示由按鍵重播控制
int currentReplayInputActionIndex = 0; // 目前播放到哪個按鍵操作
void update(float dt) {
if (isAIControlledByInputReplay) {
// 檢查是否有下一個按鍵操作,並且時間戳已到達或超過
while (currentReplayInputActionIndex < recordedInputs.size() &&
raceTime >= recordedInputs[currentReplayInputActionIndex].timestamp) {
const PlayerInputAction& action = recordedInputs[currentReplayInputActionIndex];
char keyChar = action.key; // 記錄的是大寫鍵
if (keyChar == 'W')
kW = action.isDown;
else if (keyChar == 'S')
kS = action.isDown;
else if (keyChar == 'A')
kA = action.isDown;
else if (keyChar == 'D')
kD = action.isDown;
currentReplayInputActionIndex++;
}
} else if (isAIControlledByReplay) {
if (currentReplayFrameIndex < replayLogToFollow.size()) {
const State& targetState = replayLogToFollow[currentReplayFrameIndex];
this->x = targetState.x;
this->y = targetState.y;
this->z = targetState.z;
this->heading = targetState.heading;
this->speed = targetState.speed;
currentReplayFrameIndex++;
} else {
this->speed *= 0.98f;
}
log.push_back({x, y, z, heading, speed});
return;
}
float lateral = lateralOffsetFromCenter({x, y, z});
bool onRoad = (lateral <= ROAD_HALF_W);
bool onSand = (lateral > ROAD_HALF_W && lateral <= SAND_HALF_W);
float localMaxF = onRoad ? MAX_F_ROAD : onSand ? MAX_F_SAND : MAX_F_GRASS;
const float ACC_BASE = 12.0f;
const float ACC = ACC_BASE * (localMaxF / MAX_F_ROAD);
const float BRAKE = 24.0f * (localMaxF / MAX_F_ROAD);
const float TURN = 90.0f * (localMaxF / MAX_F_ROAD);
float friction = onRoad ? 0.998f : onSand ? 0.99f : 0.90f;
/*------------------------------------------------------------
1) 引擎 / 煞車 / 自然滑行
------------------------------------------------------------*/
if (kW)
speed += ACC * dt;
else if (kS)
speed -= BRAKE * dt;
else
speed *= friction;
/*------------------------------------------------------------
2) 平滑處理轉向輸入並更新 current_steer_force
------------------------------------------------------------*/
float target_steer_force = 0.0f;
if (kA) target_steer_force = 1.0f; // 左轉
if (kD) target_steer_force = -1.0f; // 右轉 (如果kA和kD同時按下,它們會互相抵消或取決於最後一個if)
if (current_steer_force < target_steer_force) {
current_steer_force += STEER_CHANGE_RATE * dt;
if (current_steer_force > target_steer_force) current_steer_force = target_steer_force;
} else if (current_steer_force > target_steer_force) {
current_steer_force -= STEER_CHANGE_RATE * dt;
if (current_steer_force < target_steer_force) current_steer_force = target_steer_force;
}
current_steer_force = max(-1.0f, min(1.0f, current_steer_force));
/*------------------------------------------------------------
2.5) 轉向拖速 (使用平滑後的 current_steer_force)
------------------------------------------------------------*/
const float TURN_DRAG_K = 1.2f;
float vNorm = fabsf(speed) / localMaxF; // 0 ~ 1
speed -= fabsf(current_steer_force) * TURN_DRAG_K * vNorm * vNorm * localMaxF * dt;
/*------------------------------------------------------------
3) 更新方向角與位置 (使用平滑後的 current_steer_force)
------------------------------------------------------------*/
heading += current_steer_force * TURN * dt * (speed / localMaxF);
float r = heading * M_PI / 180;
x += sinf(r) * speed * dt;
z += cosf(r) * speed * dt;
/*------------------------------------------------------------
4) 速度邊界
------------------------------------------------------------*/
const float MAX_R = -20.0f;
speed = max(MAX_R, min(speed, localMaxF));
log.push_back({x, y, z, heading, speed});
}
void saveLog(const string& fn = "state_log.csv") {
ofstream o(fn);
o << "x,y,z,heading,speed\n";
for (auto& s : log) o << s.x << "," << s.y << "," << s.z << "," << s.heading << "," << s.speed << "\n";
cerr << "Saved " << log.size() << " frames to " << fn << "\n";
}
} car, car2;
struct SoloReplayInfo {
float time; // 完成時間
string logFilePath; // 對應的 .csv 檔案路徑
bool operator<(const SoloReplayInfo& other) const {
return time < other.time; // 時間越少越好
}
};
vector<SoloReplayInfo> topSoloReplays;
const char* REPLAY_INFO_FILE = "logs/solo_replays.txt"; // 儲存前三名記錄資訊的檔案
const int MAX_TOP_REPLAYS = 3; // 只保留前三名
static const GLfloat PLANE[4] = {0.f, 1.f, 0.f, -0.02f};
static const GLfloat LIGHT[4] = {0.35f, 1.f, 0.25f, 0.0f}; // w=0
static GLfloat SHADOW[16];
// ---- 更新引擎/環境音效 ----
void updateEngineSound() {
// 如果音效被禁用,則確保風聲是停止的並直接返回
if (!sfxEnabled) {
if (isWindPlaying) {
stopLoopingSound("wind");
isWindPlaying = false;
}
return;
}
// 當車速超過一個閾值時播放風聲
const float WIND_SPEED_THRESHOLD = 2.0f;
// *** 核心修改:判斷任一車輛是否有速度 ***
bool anyCarMoving = false;
if (gameMode == MODE_SOLO) {
// 單人模式下,只判斷 car 1
anyCarMoving = (car.speed > WIND_SPEED_THRESHOLD);
} else if (gameMode == MODE_PVP || gameMode == MODE_AI) {
// 多人模式下,判斷 car 1 或 car 2
anyCarMoving = (car.speed > WIND_SPEED_THRESHOLD || car2.speed > WIND_SPEED_THRESHOLD);
}
if (anyCarMoving) {
if (!isWindPlaying) {
playLoopingSound("wind", "audio/windNoise.mp3");
isWindPlaying = true;
}
} else {
if (isWindPlaying) {
stopLoopingSound("wind");
isWindPlaying = false;
}
}
}
// 載入 Solo 操作記錄資訊
void loadTopSoloReplays() {
topSoloReplays.clear();
ifstream inFile(REPLAY_INFO_FILE);
if (!inFile) {
cerr << "[INFO] No solo replay info file found (" << REPLAY_INFO_FILE << "). Starting fresh." << endl;
return;
}
SoloReplayInfo tempInfo;
while (inFile >> tempInfo.time >> tempInfo.logFilePath) topSoloReplays.push_back(tempInfo);
inFile.close();
sort(topSoloReplays.begin(), topSoloReplays.end()); // 確保按時間排序
cout << "[INFO] Loaded " << topSoloReplays.size() << " solo replay entries." << endl;
}
// 儲存 Solo 操作記錄資訊到檔案
void saveTopSoloReplaysToFile() {
ofstream outFile(REPLAY_INFO_FILE);
if (!outFile) {
cerr << "[ERROR] Could not open solo replay info file for writing: " << REPLAY_INFO_FILE << endl;
return;
}
for (const auto& replay : topSoloReplays)
outFile << fixed << setprecision(2) << replay.time << " " << replay.logFilePath << endl;
outFile.close();
cout << "[INFO] Saved " << topSoloReplays.size() << " solo replay entries to " << REPLAY_INFO_FILE << endl;
}
// 當 Solo 模式完成時,儲存操作記錄並更新排行榜
void saveSoloOperationAndManageReplays(float newTime) {
if (gameMode != MODE_SOLO) return;
time_t now_time = time(nullptr);
tm* ltm = localtime(&now_time);
char buffer[80];
strftime(buffer, sizeof(buffer), "logs/solo_input_log_%Y%m%d_%H%M%S.inputrec", ltm);
string newLogFilename(buffer);
ofstream outFile(newLogFilename);
if (!outFile) {
cerr << "[ERROR] Could not open input replay file for writing: " << newLogFilename << endl;
return;
}
outFile << "timestamp,key,is_down" << endl;
for (const auto& inputAction : car.recordedInputs)
outFile << fixed << setprecision(4) << inputAction.timestamp << "," << inputAction.key << ","
<< (inputAction.isDown ? 1 : 0) << endl;
outFile.close();
cout << "[INFO] Saved " << car.recordedInputs.size() << " input actions to " << newLogFilename << endl;
topSoloReplays.push_back({newTime, newLogFilename});
sort(topSoloReplays.begin(), topSoloReplays.end());
while (topSoloReplays.size() > MAX_TOP_REPLAYS) {
SoloReplayInfo replayToRemove = topSoloReplays.back();
if (remove(replayToRemove.logFilePath.c_str()) != 0)
cerr << "[ERROR] Failed to delete old input replay file: " << replayToRemove.logFilePath << endl;
else
cout << "[INFO] Deleted old input replay file: " << replayToRemove.logFilePath << endl;
topSoloReplays.pop_back();
}
saveTopSoloReplaysToFile();
}
bool loadInputReplayLogFromFile(const string& filePath, vector<PlayerInputAction>& inputLog) {
inputLog.clear();
ifstream inFile(filePath);
if (!inFile) {
cerr << "[ERROR] Could not open input replay log file: " << filePath << endl;
return false;
}
string line;
if (!getline(inFile, line)) {
cerr << "[ERROR] Input replay log file is empty or cannot read header: " << filePath << endl;
inFile.close();
return false;
}
PlayerInputAction tempAction;
int isDownInt;
int lineNum = 1; // 從數據的第1行開始算 (標題行之後)
while (getline(inFile, line)) {
lineNum++;
stringstream ss(line);
char keyChar; // 用於讀取單個按鍵字元
char comma1, comma2;
if (ss >> tempAction.timestamp && // 1. 讀取時間戳
ss >> comma1 && comma1 == ',' && // 2. 讀取並驗證第一個逗號
ss >> keyChar && // 3. 讀取按鍵字元
ss >> comma2 && comma2 == ',' && // 4. 讀取並驗證第二個逗號
ss >> isDownInt) { // 5. 讀取 isDown (0 或 1)
tempAction.key = static_cast<unsigned char>(keyChar); // 賦值給 PlayerInputAction 的 key
tempAction.isDown = (isDownInt == 1);
inputLog.push_back(tempAction);
} else {
cerr << "[WARNING] Failed to parse line " << lineNum << " in input replay log: \"" << line << "\"" << endl;
if (ss.fail())
cerr << " Stream state (fail): badbit=" << ss.bad() << ", failbit=" << ss.fail()
<< ", eofbit=" << ss.eof() << endl;
else if (ss.bad())
cerr << " Stream state (bad): badbit=" << ss.bad() << ", failbit=" << ss.fail()
<< ", eofbit=" << ss.eof() << endl;
ss.clear();
}
}
inFile.close();
cout << "[INFO] Loaded " << inputLog.size() << " input actions from replay: " << filePath << endl;
return !inputLog.empty();
}
bool loadReplayLogFromFile(const string& filePath, vector<State>& replayLog) {
replayLog.clear();
ifstream inFile(filePath);
if (!inFile) {
cerr << "[ERROR] Could not open replay log file: " << filePath << endl;
return false;
}
string line;
getline(inFile, line); // 跳過標題行 "x,y,z,heading,speed"
State tempState;
char comma; // 用於讀取逗號
while (getline(inFile, line)) {
stringstream ss(line);
if (ss >> tempState.x >> comma >> tempState.y >> comma >> tempState.z >> comma >> tempState.heading >> comma >>
tempState.speed) {
replayLog.push_back(tempState);
} else {
cerr << "[WARNING] Failed to parse line in replay log: " << line << endl;
}
}
inFile.close();
cout << "[INFO] Loaded " << replayLog.size() << " frames from replay log: " << filePath << endl;
return !replayLog.empty();
}
// ---- 三角錐相關 ----
struct TrafficCone {
Vec3 pos; // 位置
Vec3 velocity; // 速度 (被撞飛時)
Vec3 angularVelocity; // 角速度 (被撞飛時旋轉)
float rotationAngle; // 當前旋轉角度
Vec3 rotationAxis; // 旋轉軸
bool isHit; // 是否被撞飛
bool isActive; // 是否在場景中活動 (可用於重置或移除)
float scale; // 大小比例
ModelObject* model; // 指向要使用的模型 (例如 Cone1 或 Cone2)
TrafficCone() : isHit(false), isActive(true), rotationAngle(0.0f), scale(1.0f), model(nullptr) {
rotationAxis = {0.0f, 1.0f, 0.0f}; // 預設繞Y軸旋轉
}
};
vector<TrafficCone> trafficCones;
const float CONE_COLLISION_RADIUS_CAR = 1.5f; // 車輛與三角錐的碰撞半徑
const float CONE_COLLISION_RADIUS_CONE = 0.5f; // 三角錐本身的碰撞半徑 (用於視覺)
const float CONE_PLACEMENT_OFFSET_MIN = ROAD_HALF_W + 2.0f; // 離賽道中心線最小距離
const float CONE_PLACEMENT_OFFSET_MAX = SAND_HALF_W - 2.0f; // 離賽道中心線最大距離 (確保在沙地上)
const float CONE_HIT_IMPULSE_MIN = 20.0f;
const float CONE_HIT_IMPULSE_MAX = 40.0f;
const float CONE_GRAVITY = -18.0f;
const float CONE_FRICTION = 0.98f;
const float CONE_ANGULAR_FRICTION = 0.97f;
const float CONE_MIN_VELOCITY_SQR = 0.1f * 0.1f; // 速度平方小於此值則停止
const float CONE_MIN_ANGULAR_VELOCITY_SQR = 0.5f * 0.5f; // 角速度平方小於此值則停止旋轉
void initializeTrafficCones();
void updateTrafficCones(float dt);
void handleCarConeCollision(Car& carToCollide, float dt);
// 穿越終點偵測
bool canFinishP1 = (lastCheckpointP1 > trackSamples.size() * 0.50f);
bool canFinishP2 = (lastCheckpointP2 > trackSamples.size() * 0.50f);
const int LAPS_TO_FINISH = 1; // 設定比賽需要完成的圈數
int nearestSampleIndex(const Vec3& pos) {
int bestIdx = 0;
float best2 = FLT_MAX;
for (int i = 0; i < (int)trackSamples.size(); ++i) {
Vec3 d = pos - trackSamples[i];
float d2 = d.x * d.x + d.z * d.z;
if (d2 < best2) {
best2 = d2;
bestIdx = i;
}
}
return bestIdx;
}
bool loadMultiMaterialModel(ModelObject& model, const string& modelPath) {
tinyobj::attrib_t attrib;
vector<tinyobj::shape_t> shapes;
vector<tinyobj::material_t> materials;
string warn, err;
string mtl_basedir = modelPath.substr(0, modelPath.find_last_of("/\\") + 1);
if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, modelPath.c_str(), mtl_basedir.c_str())) {
cerr << "[ERROR] Failed to load model: " << modelPath << endl;
if (!warn.empty()) cerr << "[WARN] " << warn << endl;
if (!err.empty()) cerr << "[ERROR] " << err << endl;
return false;
}
if (!warn.empty()) cout << "[WARN] tinyobj: " << warn << endl; // 顯示警告
cout << "[INFO] tinyobj::LoadObj finished. Shapes: " << shapes.size() << ", Materials: " << materials.size()
<< endl;
// --- 1. 預先載入所有材質和紋理 ---
map<string, GLuint> texture_cache; // 避免重複載入相同紋理
model.materials.reserve(materials.size());
for (size_t i = 0; i < materials.size(); ++i) {
const auto& mat = materials[i];
cout << "[INFO] Material " << (i + 1) << "/" << materials.size() << ": " << mat.name;
MaterialInfo material_info;
material_info.name = mat.name;
memcpy(material_info.ambient, mat.ambient, sizeof(float) * 3);
memcpy(material_info.diffuse, mat.diffuse, sizeof(float) * 3);
memcpy(material_info.specular, mat.specular, sizeof(float) * 3);
material_info.shininess = mat.shininess;
if (!mat.diffuse_texname.empty()) {
string texture_filename_from_mtl = mat.diffuse_texname;
replace(texture_filename_from_mtl.begin(), texture_filename_from_mtl.end(), '\\', '/');
string texture_path = mtl_basedir + texture_filename_from_mtl;
cout << " -> Tex: " << texture_filename_from_mtl;
// 檢查快取
if (texture_cache.find(texture_path) != texture_cache.end()) {
material_info.texture_id = texture_cache[texture_path];
cout << " (cached)" << endl;
} else {
cout << " (loading from: " << texture_path << ")" << endl;
int w, h, n;
unsigned char* data = stbi_load(texture_path.c_str(), &w, &h, &n, 0);
if (data) {
GLuint tex_id;
glGenTextures(1, &tex_id);
glBindTexture(GL_TEXTURE_2D, tex_id);
GLenum format = (n == 3) ? GL_RGB : GL_RGBA;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, format, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGBA, w, h, format, GL_UNSIGNED_BYTE,
data); // 使用 GLU 建立 Mipmap
stbi_image_free(data);
material_info.texture_id = tex_id;
texture_cache[texture_path] = tex_id; // 存入快取
} else {
cerr << "[ERROR] Failed to load texture: " << texture_path << " reason: " << stbi_failure_reason()
<< endl;
}
}
} else {
cout << " (no diffuse texture)" << endl; // 如果沒有漫反射紋理則換行
}
model.materials.push_back(material_info);
}
cout << "[INFO] All materials processed." << endl;
// --- 2. 根據材質ID,將頂點分組到不同的 MeshPart ---
for (size_t s = 0; s < shapes.size(); ++s) { // -1 表示還沒有分配材質
const auto& shape = shapes[s];
int current_material_id = -1;
for (size_t i = 0; i < shape.mesh.indices.size() / 3; i++) {
int face_material_id = shape.mesh.material_ids[i];
// 如果這個面的材質和上一個不同,就需要建立或切換到對應的 MeshPart
if (face_material_id != current_material_id) {
current_material_id = face_material_id;
// 尋找是否已經有對應該材質的 MeshPart
auto it = find_if(model.meshes.begin(), model.meshes.end(),
[&](const MeshPart& m) { return m.material_index == current_material_id; });
if (it == model.meshes.end()) {
// 沒找到,建立一個新的 MeshPart
MeshPart new_mesh;
new_mesh.material_index = current_material_id;
model.meshes.push_back(new_mesh);
}
}
MeshPart& current_mesh = model.meshes.back();
// 處理這個面的三個頂點
for (int j = 0; j < 3; j++) {
tinyobj::index_t idx = shape.mesh.indices[3 * i + j];
current_mesh.vertices.push_back(attrib.vertices[3 * idx.vertex_index + 0]);
current_mesh.vertices.push_back(attrib.vertices[3 * idx.vertex_index + 1]);
current_mesh.vertices.push_back(attrib.vertices[3 * idx.vertex_index + 2]);
if (idx.normal_index >= 0) {
current_mesh.normals.push_back(attrib.normals[3 * idx.normal_index + 0]);
current_mesh.normals.push_back(attrib.normals[3 * idx.normal_index + 1]);
current_mesh.normals.push_back(attrib.normals[3 * idx.normal_index + 2]);
}
if (idx.texcoord_index >= 0) {
current_mesh.texcoords.push_back(attrib.texcoords[2 * idx.texcoord_index + 0]);
current_mesh.texcoords.push_back(attrib.texcoords[2 * idx.texcoord_index + 1]);
}
}
}
}
cout << "[INFO] All shapes processed. Model has " << model.meshes.size() << " mesh parts." << endl;
return true;
}
// ---- 繪製FPS的函式 ----
void drawFPS() {
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
gluOrtho2D(0, winW, 0, winH); // 左下 (0,0)
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
char fpsText[32];
sprintf(fpsText, "FPS: %.1f", fps);
glColor3f(1.0f, 1.0f, 1.0f);
// 設定文字位置在右上角
int textWidth = 0;
for (char* p = fpsText; *p; ++p) textWidth += glutBitmapWidth(GLUT_BITMAP_HELVETICA_18, *p);
int x = winW - textWidth - 10; // 10 pixels from the right edge
int y = winH - 20; // 20 pixels from the top edge
glRasterPos2i(x, y);
for (char* p = fpsText; *p; ++p) glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, *p);
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glPopMatrix();
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
}
// ---- 能繪製多材質模型的函式 ----
void drawMultiMaterialModel(const ModelObject& model) {
glPushAttrib(GL_LIGHTING_BIT | GL_TEXTURE_BIT | GL_ENABLE_BIT | GL_COLOR_BUFFER_BIT | GL_CURRENT_BIT);
glEnable(GL_COLOR_MATERIAL);
for (const auto& mesh : model.meshes) {
if (mesh.vertices.empty()) continue;
// --- 1. 設定材質 ---
if (mesh.material_index >= 0 && mesh.material_index < model.materials.size()) {
const MaterialInfo& mat = model.materials[mesh.material_index];
// 設定顏色和光照屬性
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, mat.ambient);
glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, mat.diffuse);
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, mat.specular);
glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, mat.shininess);
// 綁定紋理
if (mat.texture_id > 0) {
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, mat.texture_id);
// 讓紋理顏色與光照顏色混合
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
} else {
glDisable(GL_TEXTURE_2D);
}
} else {
// 沒有材質,使用預設值
glDisable(GL_TEXTURE_2D);
}
// --- 2. 傳遞頂點數據並繪製 ---
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer(3, GL_FLOAT, 0, mesh.vertices.data());
if (!mesh.normals.empty()) {
glEnableClientState(GL_NORMAL_ARRAY);
glNormalPointer(GL_FLOAT, 0, mesh.normals.data());
}
if (!mesh.texcoords.empty()) {
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer(2, GL_FLOAT, 0, mesh.texcoords.data());
}
glDrawArrays(GL_TRIANGLES, 0, mesh.vertices.size() / 3);
// 清理 Client State
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
}
glPopAttrib(); // 還原 OpenGL 狀態
}
void buildTrack() {
vector<Vec3> tmp = ctrl;
tmp.push_back(ctrl[0]);
tmp.push_back(ctrl[1]);
int SEGS = (int)tmp.size() - 3;
vector<Vec3> raw;
for (int s = 0; s < SEGS; ++s) {
for (int i = 0; i < SAMPLE_DENSITY / SEGS; ++i) {
float t = (float)i / (SAMPLE_DENSITY / SEGS);
raw.push_back(catmull(tmp[s], tmp[s + 1], tmp[s + 2], tmp[s + 3], t));
}
}
float len = 0.0f;
for (size_t i = 1; i < raw.size(); ++i) len += length(raw[i] - raw[i - 1]);
const float TARGET_LEN = 8100.0f;
float scale = TARGET_LEN / len;
trackCtrl.clear();
trackCtrl.reserve(ctrl.size());
for (auto& p : ctrl) trackCtrl.push_back({p.x * scale, p.y, p.z * scale});
vector<Vec3> c2 = trackCtrl;
c2.push_back(trackCtrl[0]);
c2.push_back(trackCtrl[1]);
SEGS = (int)c2.size() - 3;
trackSamples.clear();
for (int s = 0; s < SEGS; ++s) {
for (int i = 0; i < SAMPLE_DENSITY / SEGS; ++i) {
float t = (float)i / (SAMPLE_DENSITY / SEGS);
trackSamples.push_back(catmull(c2[s], c2[s + 1], c2[s + 2], c2[s + 3], t));
}
}
trackLength = 0.0f;
for (size_t i = 1; i < trackSamples.size(); ++i) trackLength += length(trackSamples[i] - trackSamples[i - 1]);
mapMinX = mapMaxX = trackSamples[0].x;
mapMinZ = mapMaxZ = trackSamples[0].z;
for (auto& p : trackSamples) {
mapMinX = min(mapMinX, p.x);
mapMaxX = max(mapMaxX, p.x);
mapMinZ = min(mapMinZ, p.z);
mapMaxZ = max(mapMaxZ, p.z);
}
float padX = (mapMaxX - mapMinX) * 0.05f;
float padZ = (mapMaxZ - mapMinZ) * 0.05f;
mapMinX -= padX;
mapMaxX += padX;
mapMinZ -= padZ;
mapMaxZ += padZ;
cerr << "[Track] len = " << trackLength << "u\n";
// ---- 設定拱門 ----
Vec3 dir0 = normalize(trackSamples[1] - trackSamples[0]);
// 將起點拱門向前移動5個單位
gateStartPos = trackSamples[0] + dir0 * 5.0f;
gateStartHdg = atan2f(dir0.x, dir0.z) * 180.0f / M_PI;
int midIdx = (int)trackSamples.size() - 1; // 這是正確的
gateFinishPos = trackSamples[midIdx];
Vec3 dirF = normalize(trackSamples[(midIdx + 1) % trackSamples.size()] - gateFinishPos);
gateFinishHdg = atan2f(dirF.x, dirF.z) * 180.0f / M_PI;
// ---- 計算終點空間邊界 ----