[{"data":1,"prerenderedAt":12738},["ShallowReactive",2],{"\u002Fdevlog\u002Fxggame-bird\u002F11-save-system":3,"devlog-chapters-xggame-bird":1483},{"id":4,"title":5,"body":6,"cover":1472,"date":1473,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":1478,"navigation":326,"path":1479,"seo":1480,"stem":1481,"toc":326,"__hash__":1482},"devlog\u002Fdevlog\u002Fxggame-bird\u002F11-save-system.md","存档系统 SaveManager",{"type":7,"value":8,"toc":1447},"minimark",[9,13,21,26,29,67,86,93,104,107,164,172,191,201,208,266,276,289,293,300,648,654,683,686,693,702,711,718,760,773,796,805,814,821,844,847,856,864,870,887,898,901,932,949,970,988,995,1001,1032,1038,1057,1061,1064,1216,1223,1238,1245,1259,1262,1306,1309,1317,1321,1326,1333,1355,1369,1372,1404,1414,1417,1420,1440,1443],[10,11,12],"p",{},"游戏里已经有「最高分」了 — 但你关掉游戏再打开，它就变回 0 了 😢",[10,14,15,16,20],{},"这一章做一个最小的存档系统，让最高分",[17,18,19],"strong",{},"真正留在硬盘上","。",[22,23,25],"h2",{"id":24},"为什么现在存不住","为什么现在存不住？",[10,27,28],{},"回头看 GameManager 里的这行：",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-gdscript shiki shiki-themes vitesse-light vitesse-dark","var high_score: int = 0\n","gdscript","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,56,60,63],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"s5TCs","var",[40,49,51],{"class":50},"s9nN2"," high_score",[40,53,55],{"class":54},"si6no",":",[40,57,59],{"class":58},"s_NWU"," int",[40,61,62],{"class":54}," =",[40,64,66],{"class":65},"sqbOQ"," 0\n",[10,68,69,70,73,74,77,78,81,82,85],{},"变量是存在",[17,71,72],{},"内存","里的。内存的特点就是",[17,75,76],{},"断电即失"," — 游戏进程一关，这个 ",[37,79,80],{},"0"," 和你打出来的 ",[37,83,84],{},"999"," 一起烟消云散。",[10,87,88,89,92],{},"想留住它，就得写到",[17,90,91],{},"硬盘","上的一个文件里。",[22,94,96,99,100,103],{"id":95},"res-和-user-别写错地方",[37,97,98],{},"res:\u002F\u002F"," 和 ",[37,101,102],{},"user:\u002F\u002F"," — 别写错地方",[10,105,106],{},"Godot 里有两个特殊路径前缀，这是新手第一个坑：",[108,109,110,126],"table",{},[111,112,113],"thead",{},[114,115,116,120,123],"tr",{},[117,118,119],"th",{},"路径",[117,121,122],{},"指向哪",[117,124,125],{},"能不能写",[127,128,129,149],"tbody",{},[114,130,131,136,143],{},[132,133,134],"td",{},[37,135,98],{},[132,137,138,139,142],{},"你的",[17,140,141],{},"项目文件夹","（游戏本体）",[132,144,145,146],{},"❌ 导出后",[17,147,148],{},"只读",[114,150,151,155,161],{},[132,152,153],{},[37,154,102],{},[132,156,157,158],{},"玩家电脑上的",[17,159,160],{},"用户数据目录",[132,162,163],{},"✅ 可读可写",[10,165,166],{},[17,167,168,169,171],{},"为什么 ",[37,170,98],{}," 不能写？",[10,173,174,175,177,178,181,182,187,188,190],{},"游戏导出之后，所有 ",[37,176,98],{}," 里的东西会被打包进一个 ",[37,179,180],{},".pck"," 文件（你在 ",[183,184,186],"a",{"href":185},"\u002Fdevlog\u002Fxggame-bird\u002F09-export","第 9 章"," 导出时见过它）。打包好的文件是",[17,189,148],{},"的 — 就像你不能修改一个已经刻好的光盘。",[10,192,193,194,196,197,200],{},"在编辑器里测试的时候 ",[37,195,98],{}," 可能写得进去，",[17,198,199],{},"但导出之后必定失败"," — 这个坑特别隐蔽，一定要记住。",[10,202,203],{},[17,204,205,207],{},[37,206,102],{}," 实际在哪？",[108,209,210,220],{},[111,211,212],{},[114,213,214,217],{},[117,215,216],{},"平台",[117,218,219],{},"真实路径",[127,221,222,232,242,252],{},[114,223,224,227],{},[132,225,226],{},"Windows",[132,228,229],{},[37,230,231],{},"%APPDATA%\\Godot\\app_userdata\\XGGame-Bird\\",[114,233,234,237],{},[132,235,236],{},"macOS",[132,238,239],{},[37,240,241],{},"~\u002FLibrary\u002FApplication Support\u002FGodot\u002Fapp_userdata\u002FXGGame-Bird\u002F",[114,243,244,247],{},[132,245,246],{},"Linux",[132,248,249],{},[37,250,251],{},"~\u002F.local\u002Fshare\u002Fgodot\u002Fapp_userdata\u002FXGGame-Bird\u002F",[114,253,254,259],{},[132,255,256],{},[17,257,258],{},"Web",[132,260,261,262,265],{},"浏览器的 ",[17,263,264],{},"IndexedDB","（不是真文件）",[267,268,269],"blockquote",{},[10,270,271,272,275],{},"编辑器里可以直接",[17,273,274],{},"项目 → 打开用户数据文件夹","跳过去看，调试存档很方便。",[267,277,278],{},[10,279,280,281,284,285,288],{},"⚠️ ",[17,282,283],{},"Web 版要注意","：存档在浏览器的 IndexedDB 里，玩家",[17,286,287],{},"清缓存 \u002F 换浏览器 \u002F 用无痕模式","，存档就没了。我们这个游戏只存一个最高分，丢了无所谓；但如果以后做长流程游戏要上 Web，就得考虑云存档了。",[22,290,292],{"id":291},"创建-savemanager","创建 SaveManager",[10,294,295,296,299],{},"新建文件 ",[37,297,298],{},"scripts\u002Fautoload\u002Fsave_manager.gd","：",[30,301,303],{"className":32,"code":302,"language":34,"meta":35,"style":35},"extends Node\n## 存档管理：读写 user:\u002F\u002Fsave.json\n\nconst SAVE_PATH := \"user:\u002F\u002Fsave.json\"\n\nfunc save_game() -> void:\n    var data := {\n        \"high_score\": GameManager.high_score,\n    }\n    var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)\n    file.store_string(JSON.stringify(data))\n\nfunc load_game() -> void:\n    if not FileAccess.file_exists(SAVE_PATH):\n        return  # 首次游玩，没有存档，用默认值\n    var file := FileAccess.open(SAVE_PATH, FileAccess.READ)\n    var data = JSON.parse_string(file.get_as_text())\n    if data == null:\n        return  # 存档损坏，防崩\n    GameManager.high_score = data.get(\"high_score\", 0)\n",[37,304,305,314,321,328,344,349,371,385,407,413,450,479,484,500,525,534,564,594,609,617],{"__ignoreMap":35},[40,306,307,311],{"class":42,"line":43},[40,308,310],{"class":309},"sTPum","extends",[40,312,313],{"class":58}," Node\n",[40,315,317],{"class":42,"line":316},2,[40,318,320],{"class":319},"snYqZ","## 存档管理：读写 user:\u002F\u002Fsave.json\n",[40,322,324],{"class":42,"line":323},3,[40,325,327],{"emptyLinePlaceholder":326},true,"\n",[40,329,331,334,337,340],{"class":42,"line":330},4,[40,332,333],{"class":46},"const",[40,335,336],{"class":50}," SAVE_PATH",[40,338,339],{"class":54}," :=",[40,341,343],{"class":342},"spP0B"," \"user:\u002F\u002Fsave.json\"\n",[40,345,347],{"class":42,"line":346},5,[40,348,327],{"emptyLinePlaceholder":326},[40,350,352,355,359,362,365,368],{"class":42,"line":351},6,[40,353,354],{"class":46},"func",[40,356,358],{"class":357},"s_xSY"," save_game",[40,360,361],{"class":54},"()",[40,363,364],{"class":46}," ->",[40,366,367],{"class":58}," void",[40,369,370],{"class":54},":\n",[40,372,374,377,380,382],{"class":42,"line":373},7,[40,375,376],{"class":46},"    var",[40,378,379],{"class":50}," data",[40,381,339],{"class":54},[40,383,384],{"class":54}," {\n",[40,386,388,391,395,398,401,404],{"class":42,"line":387},8,[40,389,390],{"class":342},"        \"high_score\"",[40,392,394],{"class":393},"s8w-G",": ",[40,396,397],{"class":58},"GameManager",[40,399,400],{"class":393},".",[40,402,403],{"class":50},"high_score",[40,405,406],{"class":393},",\n",[40,408,410],{"class":42,"line":409},9,[40,411,412],{"class":54},"    }\n",[40,414,416,418,421,423,426,428,431,434,437,440,442,444,447],{"class":42,"line":415},10,[40,417,376],{"class":46},[40,419,420],{"class":50}," file",[40,422,339],{"class":54},[40,424,425],{"class":58}," FileAccess",[40,427,400],{"class":393},[40,429,430],{"class":357},"open",[40,432,433],{"class":54},"(",[40,435,436],{"class":50},"SAVE_PATH",[40,438,439],{"class":54},",",[40,441,425],{"class":58},[40,443,400],{"class":54},[40,445,446],{"class":309},"WRITE",[40,448,449],{"class":54},")\n",[40,451,453,456,458,461,463,466,468,471,473,476],{"class":42,"line":452},11,[40,454,455],{"class":50},"    file",[40,457,400],{"class":393},[40,459,460],{"class":357},"store_string",[40,462,433],{"class":54},[40,464,465],{"class":58},"JSON",[40,467,400],{"class":393},[40,469,470],{"class":357},"stringify",[40,472,433],{"class":54},[40,474,475],{"class":50},"data",[40,477,478],{"class":54},"))\n",[40,480,482],{"class":42,"line":481},12,[40,483,327],{"emptyLinePlaceholder":326},[40,485,487,489,492,494,496,498],{"class":42,"line":486},13,[40,488,354],{"class":46},[40,490,491],{"class":357}," load_game",[40,493,361],{"class":54},[40,495,364],{"class":46},[40,497,367],{"class":58},[40,499,370],{"class":54},[40,501,503,506,509,511,513,516,518,520,523],{"class":42,"line":502},14,[40,504,505],{"class":309},"    if",[40,507,508],{"class":46}," not",[40,510,425],{"class":58},[40,512,400],{"class":393},[40,514,515],{"class":357},"file_exists",[40,517,433],{"class":54},[40,519,436],{"class":50},[40,521,522],{"class":54},")",[40,524,370],{"class":393},[40,526,528,531],{"class":42,"line":527},15,[40,529,530],{"class":309},"        return",[40,532,533],{"class":319},"  # 首次游玩，没有存档，用默认值\n",[40,535,537,539,541,543,545,547,549,551,553,555,557,559,562],{"class":42,"line":536},16,[40,538,376],{"class":46},[40,540,420],{"class":50},[40,542,339],{"class":54},[40,544,425],{"class":58},[40,546,400],{"class":393},[40,548,430],{"class":357},[40,550,433],{"class":54},[40,552,436],{"class":50},[40,554,439],{"class":54},[40,556,425],{"class":58},[40,558,400],{"class":54},[40,560,561],{"class":309},"READ",[40,563,449],{"class":54},[40,565,567,569,571,573,576,578,581,583,586,588,591],{"class":42,"line":566},17,[40,568,376],{"class":46},[40,570,379],{"class":50},[40,572,62],{"class":54},[40,574,575],{"class":58}," JSON",[40,577,400],{"class":393},[40,579,580],{"class":357},"parse_string",[40,582,433],{"class":54},[40,584,585],{"class":50},"file",[40,587,400],{"class":393},[40,589,590],{"class":357},"get_as_text",[40,592,593],{"class":54},"())\n",[40,595,597,599,601,604,607],{"class":42,"line":596},18,[40,598,505],{"class":309},[40,600,379],{"class":50},[40,602,603],{"class":46}," ==",[40,605,606],{"class":309}," null",[40,608,370],{"class":393},[40,610,612,614],{"class":42,"line":611},19,[40,613,530],{"class":309},[40,615,616],{"class":319},"  # 存档损坏，防崩\n",[40,618,620,623,625,627,629,631,633,636,638,641,643,646],{"class":42,"line":619},20,[40,621,622],{"class":58},"    GameManager",[40,624,400],{"class":54},[40,626,403],{"class":50},[40,628,62],{"class":54},[40,630,379],{"class":50},[40,632,400],{"class":393},[40,634,635],{"class":357},"get",[40,637,433],{"class":54},[40,639,640],{"class":342},"\"high_score\"",[40,642,439],{"class":54},[40,644,645],{"class":65}," 0",[40,647,449],{"class":54},[10,649,650,651,20],{},"然后照例注册 Autoload，命名 ",[37,652,653],{},"SaveManager",[267,655,656],{},[10,657,658,299,661,663,664,666,667,670,671,674,675,678,679,20],{},[17,659,660],{},"顺序很重要",[37,662,653],{}," 必须排在 ",[37,665,397],{}," ",[17,668,669],{},"上面"," — 因为下面我们会在 GameManager 的 ",[37,672,673],{},"_ready()"," 里调用 ",[37,676,677],{},"SaveManager.load_game()","。完整顺序见 ",[183,680,682],{"href":681},"\u002Fdevlog\u002Fxggame-bird\u002F10-event-bus","上一章的表格",[22,684,685],{"id":685},"逐行拆解",[687,688,690],"h3",{"id":689},"const-save_path-usersavejson",[37,691,692],{},"const SAVE_PATH := \"user:\u002F\u002Fsave.json\"",[10,694,695,697,698,701],{},[37,696,333],{}," 是",[17,699,700],{},"常量"," — 定义之后不能改。",[10,703,704,705,707,708,710],{},"存档路径这种\"全文件都要用、但永远不会变\"的值，用 ",[37,706,333],{}," 而不是 ",[37,709,47],{},"：写错了编辑器会直接报错，而不是等到运行时才发现路径拼错。",[687,712,714,717],{"id":713},"是什么",[37,715,716],{},":="," 是什么？",[30,719,721],{"className":32,"code":720,"language":34,"meta":35,"style":35},"var data := { ... }     # 用了 :=\nvar data = { ... }      # 没用\n",[37,722,723,743],{"__ignoreMap":35},[40,724,725,727,729,731,734,737,740],{"class":42,"line":43},[40,726,47],{"class":46},[40,728,379],{"class":50},[40,730,339],{"class":54},[40,732,733],{"class":54}," {",[40,735,736],{"class":393}," ... ",[40,738,739],{"class":54},"}",[40,741,742],{"class":319},"     # 用了 :=\n",[40,744,745,747,749,751,753,755,757],{"class":42,"line":316},[40,746,47],{"class":46},[40,748,379],{"class":50},[40,750,62],{"class":54},[40,752,733],{"class":54},[40,754,736],{"class":393},[40,756,739],{"class":54},[40,758,759],{"class":319},"      # 没用\n",[10,761,762,764,765,768,769,772],{},[37,763,716],{}," 叫",[17,766,767],{},"类型推断赋值","。意思是「变量的类型",[17,770,771],{},"由右边的值自动决定","，之后就锁死了」。",[774,775,776,790],"ul",{},[777,778,779,782,783,785,786,789],"li",{},[37,780,781],{},"var data := {}"," → Godot 自动判定 ",[37,784,475],{}," 是 ",[37,787,788],{},"Dictionary","，以后你想给它塞个数字，编辑器直接报错",[777,791,792,795],{},[37,793,794],{},"var data = {}"," → 类型随便变，写错了要等运行时才炸",[10,797,798,804],{},[17,799,800,801,803],{},"能用 ",[37,802,716],{}," 就用","，等于免费多一层错误检查。",[687,806,808,99,811],{"id":807},"jsonstringify-和-jsonparse_string",[37,809,810],{},"JSON.stringify()",[37,812,813],{},"JSON.parse_string()",[10,815,816,817,820],{},"硬盘上的文件本质是",[17,818,819],{},"一串文本","，没法直接存 Godot 的字典对象。所以要转换：",[774,822,823,835],{},[777,824,825,828,829,299,832],{},[37,826,827],{},"JSON.stringify(data)"," — 把字典",[17,830,831],{},"变成文本",[37,833,834],{},"{\"high_score\":42}",[777,836,837,840,841],{},[37,838,839],{},"JSON.parse_string(文本)"," — 把文本",[17,842,843],{},"变回字典",[10,845,846],{},"一存一取，正好是一对反操作。",[267,848,849],{},[10,850,851,852,855],{},"为什么选 JSON 而不是 Godot 自己的二进制格式？因为 JSON 是",[17,853,854],{},"纯文本","，你可以直接用记事本打开看里面存了啥 — 调试的时候方便太多了。",[687,857,859,860,863],{"id":858},"为什么没有-fileclose","为什么没有 ",[37,861,862],{},"file.close()","？",[10,865,866,867,869],{},"Godot 3 里必须手动 ",[37,868,862],{},"，忘了就可能丢数据。",[10,871,872,875,876,879,880,883,884,886],{},[17,873,874],{},"Godot 4 不用了"," — ",[37,877,878],{},"FileAccess"," 用的是",[17,881,882],{},"引用计数","：当 ",[37,885,585],{}," 这个变量随着函数结束而消失时，Godot 会自动帮你关闭并写入。",[10,888,889,890,893,894,897],{},"不过如果你想",[17,891,892],{},"立刻","确保写进硬盘（比如后面还要马上读它），可以显式写一句 ",[37,895,896],{},"file = null"," 来提前触发关闭。",[687,899,900],{"id":900},"两道防崩保险",[30,902,904],{"className":32,"code":903,"language":34,"meta":35,"style":35},"if not FileAccess.file_exists(SAVE_PATH):\n    return\n",[37,905,906,927],{"__ignoreMap":35},[40,907,908,911,913,915,917,919,921,923,925],{"class":42,"line":43},[40,909,910],{"class":309},"if",[40,912,508],{"class":46},[40,914,425],{"class":58},[40,916,400],{"class":393},[40,918,515],{"class":357},[40,920,433],{"class":54},[40,922,436],{"class":50},[40,924,522],{"class":54},[40,926,370],{"class":393},[40,928,929],{"class":42,"line":316},[40,930,931],{"class":309},"    return\n",[10,933,934,937,938,940,941,944,945,948],{},[17,935,936],{},"第一次玩游戏时存档文件根本不存在","。不判断就直接 ",[37,939,430],{},"，会拿到一个 ",[37,942,943],{},"null","，下一行 ",[37,946,947],{},"file.get_as_text()"," 立刻崩。",[30,950,952],{"className":32,"code":951,"language":34,"meta":35,"style":35},"if data == null:\n    return\n",[37,953,954,966],{"__ignoreMap":35},[40,955,956,958,960,962,964],{"class":42,"line":43},[40,957,910],{"class":309},[40,959,379],{"class":50},[40,961,603],{"class":46},[40,963,606],{"class":309},[40,965,370],{"class":393},[40,967,968],{"class":42,"line":316},[40,969,931],{"class":309},[10,971,972,973,976,977,980,981,983,984,987],{},"存档文件",[17,974,975],{},"存在但内容坏了","（写到一半断电、玩家手贱改坏了），",[37,978,979],{},"JSON.parse_string"," 会返回 ",[37,982,943],{},"。不判断的话下一行 ",[37,985,986],{},"data.get(...)"," 一样崩。",[10,989,990,991,994],{},"这两个判断的效果是：",[17,992,993],{},"读档失败就静悄悄用默认值继续玩","，而不是弹一个报错把玩家挡在门外。对一个休闲小游戏来说，这是对的取舍。",[687,996,998],{"id":997},"datagethigh_score-0",[37,999,1000],{},"data.get(\"high_score\", 0)",[30,1002,1004],{"className":32,"code":1003,"language":34,"meta":35,"style":35},"GameManager.high_score = data.get(\"high_score\", 0)\n",[37,1005,1006],{"__ignoreMap":35},[40,1007,1008,1010,1012,1014,1016,1018,1020,1022,1024,1026,1028,1030],{"class":42,"line":43},[40,1009,397],{"class":58},[40,1011,400],{"class":54},[40,1013,403],{"class":50},[40,1015,62],{"class":54},[40,1017,379],{"class":50},[40,1019,400],{"class":393},[40,1021,635],{"class":357},[40,1023,433],{"class":54},[40,1025,640],{"class":342},[40,1027,439],{"class":54},[40,1029,645],{"class":65},[40,1031,449],{"class":54},[10,1033,1034,1037],{},[37,1035,1036],{},".get(键, 默认值)"," — 取不到就用默认值。",[10,1039,1040,1041,1044,1045,1048,1049,1052,1053,1056],{},"这一手是为了",[17,1042,1043],{},"向后兼容","：假如以后你给存档加了新字段（比如 ",[37,1046,1047],{},"total_play_count","），老玩家的存档文件里没有这个键。用 ",[37,1050,1051],{},".get()"," 带默认值，老存档照样能读；直接写 ",[37,1054,1055],{},"data[\"total_play_count\"]"," 就会崩。",[22,1058,1060],{"id":1059},"接进-gamemanager","接进 GameManager",[10,1062,1063],{},"改两个地方：",[30,1065,1067],{"className":32,"code":1066,"language":34,"meta":35,"style":35},"func _ready() -> void:\n    SaveManager.load_game()   # 👈 启动时读档\n    EventBus.point_scored.connect(add_score)\n    EventBus.bird_died.connect(game_over)\n\n## 游戏结束\nfunc game_over():\n    set_state(GameState.GAME_OVER)\n    if high_score \u003C total_score:\n        high_score = total_score\n        SaveManager.save_game()   # 👈 只在破纪录时写档\n",[37,1068,1069,1084,1099,1121,1141,1145,1150,1160,1177,1191,1201],{"__ignoreMap":35},[40,1070,1071,1073,1076,1078,1080,1082],{"class":42,"line":43},[40,1072,354],{"class":46},[40,1074,1075],{"class":357}," _ready",[40,1077,361],{"class":54},[40,1079,364],{"class":46},[40,1081,367],{"class":58},[40,1083,370],{"class":54},[40,1085,1086,1089,1091,1094,1096],{"class":42,"line":316},[40,1087,1088],{"class":58},"    SaveManager",[40,1090,400],{"class":393},[40,1092,1093],{"class":357},"load_game",[40,1095,361],{"class":54},[40,1097,1098],{"class":319},"   # 👈 启动时读档\n",[40,1100,1101,1104,1106,1109,1111,1114,1116,1119],{"class":42,"line":323},[40,1102,1103],{"class":58},"    EventBus",[40,1105,400],{"class":54},[40,1107,1108],{"class":50},"point_scored",[40,1110,400],{"class":393},[40,1112,1113],{"class":357},"connect",[40,1115,433],{"class":54},[40,1117,1118],{"class":50},"add_score",[40,1120,449],{"class":54},[40,1122,1123,1125,1127,1130,1132,1134,1136,1139],{"class":42,"line":330},[40,1124,1103],{"class":58},[40,1126,400],{"class":54},[40,1128,1129],{"class":50},"bird_died",[40,1131,400],{"class":393},[40,1133,1113],{"class":357},[40,1135,433],{"class":54},[40,1137,1138],{"class":50},"game_over",[40,1140,449],{"class":54},[40,1142,1143],{"class":42,"line":346},[40,1144,327],{"emptyLinePlaceholder":326},[40,1146,1147],{"class":42,"line":351},[40,1148,1149],{"class":319},"## 游戏结束\n",[40,1151,1152,1154,1157],{"class":42,"line":373},[40,1153,354],{"class":46},[40,1155,1156],{"class":357}," game_over",[40,1158,1159],{"class":54},"():\n",[40,1161,1162,1165,1167,1170,1172,1175],{"class":42,"line":387},[40,1163,1164],{"class":357},"    set_state",[40,1166,433],{"class":54},[40,1168,1169],{"class":58},"GameState",[40,1171,400],{"class":54},[40,1173,1174],{"class":309},"GAME_OVER",[40,1176,449],{"class":54},[40,1178,1179,1181,1183,1186,1189],{"class":42,"line":409},[40,1180,505],{"class":309},[40,1182,51],{"class":50},[40,1184,1185],{"class":46}," \u003C",[40,1187,1188],{"class":50}," total_score",[40,1190,370],{"class":393},[40,1192,1193,1196,1198],{"class":42,"line":415},[40,1194,1195],{"class":50},"        high_score",[40,1197,62],{"class":54},[40,1199,1200],{"class":50}," total_score\n",[40,1202,1203,1206,1208,1211,1213],{"class":42,"line":452},[40,1204,1205],{"class":58},"        SaveManager",[40,1207,400],{"class":393},[40,1209,1210],{"class":357},"save_game",[40,1212,361],{"class":54},[40,1214,1215],{"class":319},"   # 👈 只在破纪录时写档\n",[687,1217,1219,1220,1222],{"id":1218},"读档时机_ready-里越早越好","读档时机：",[37,1221,673],{}," 里，越早越好",[10,1224,1225,1227,1228,1231,1232,1234,1235,1237],{},[37,1226,673],{}," 是这个节点",[17,1229,1230],{},"准备就绪时执行一次","的函数。Autoload 的 ",[37,1233,673],{}," 在游戏刚启动时就跑，比任何游戏场景都早 — 所以等玩家看到菜单的时候，",[37,1236,403],{}," 已经是硬盘里的真实值了。",[687,1239,1241,1242],{"id":1240},"写档时机只在破纪录的那一刻","写档时机：",[17,1243,1244],{},"只在破纪录的那一刻",[10,1246,1247,1248,1251,1252,1255,1256,1258],{},"注意这行 ",[37,1249,1250],{},"SaveManager.save_game()"," 的",[17,1253,1254],{},"缩进位置"," — 它在 ",[37,1257,910],{}," 里面。",[10,1260,1261],{},"这不是随便放的：",[108,1263,1264,1274],{},[111,1265,1266],{},[114,1267,1268,1271],{},[117,1269,1270],{},"写档时机",[117,1272,1273],{},"后果",[127,1275,1276,1284,1292],{},[114,1277,1278,1281],{},[132,1279,1280],{},"每帧写",[132,1282,1283],{},"💀 一秒 60 次硬盘 IO，游戏卡成幻灯片",[114,1285,1286,1289],{},[132,1287,1288],{},"每次死亡都写",[132,1290,1291],{},"😐 能用，但大部分时候在写一模一样的内容，纯浪费",[114,1293,1294,1299],{},[132,1295,1296],{},[17,1297,1298],{},"只在破纪录时写",[132,1300,1301,1302,1305],{},"✅ 只有数据",[17,1303,1304],{},"真的变了","才写",[10,1307,1308],{},"多数玩家一局游戏破不了几次纪录，所以实际写盘次数可能一整晚就几次。",[267,1310,1311],{},[10,1312,1313,1314],{},"这是个通用原则：",[17,1315,1316],{},"存档写入要由\"数据变化\"驱动，不要由\"时间\"或\"事件\"驱动。",[22,1318,1320],{"id":1319},"要不要给存档加密","要不要给存档加密？",[10,1322,1323],{},[17,1324,1325],{},"单机小游戏：不要。",[10,1327,1328,1329,1332],{},"Godot 确实提供了 ",[37,1330,1331],{},"FileAccess.open_encrypted_with_pass()","，但对这个游戏来说是负收益：",[774,1334,1335,1342,1349],{},[777,1336,1337,1338,1341],{},"玩家改自己的最高分 — ",[17,1339,1340],{},"不伤害任何人","，没有排行榜、没有联机、没有内购",[777,1343,1344,1345,1348],{},"加密之后",[17,1346,1347],{},"你自己也没法用记事本看存档了","，调试变麻烦",[777,1350,1351,1352],{},"密钥就硬编码在游戏里，稍微懂点的人一样能扒出来 — ",[17,1353,1354],{},"挡不住真想改的人，只烦到了自己",[10,1356,1357,1358,1361,1362,1361,1365,1368],{},"什么时候才需要加密？有",[17,1359,1360],{},"在线排行榜","、有",[17,1363,1364],{},"联机对战",[17,1366,1367],{},"内购解锁","的时候 — 那时候作弊会影响到别的玩家。",[22,1370,1371],{"id":1371},"测试一下",[1373,1374,1375,1378,1384,1391],"ol",{},[777,1376,1377],{},"玩一局，打出一个分数（比如 5 分）",[777,1379,1380,1383],{},[17,1381,1382],{},"完全关掉游戏","，重新运行",[777,1385,1386,1387,1390],{},"结束画面的 ",[37,1388,1389],{},"High Score"," 应该还是 5 ✅",[777,1392,1393,1394,1396,1397,1400,1401],{},"去 ",[17,1395,274],{},"，能看到 ",[37,1398,1399],{},"save.json","，记事本打开是 ",[37,1402,1403],{},"{\"high_score\":5}",[10,1405,1406,1407,1409,1410,666,1412,20],{},"如果第 3 步失败，先检查 Autoload 里 ",[37,1408,653],{}," 是不是排在 ",[37,1411,397],{},[17,1413,669],{},[1415,1416],"hr",{},[10,1418,1419],{},"到这里，《XGGame-Bird》的完整结构就都记录完了：",[774,1421,1422,1425,1428,1431,1434,1437],{},[777,1423,1424],{},"✅ 从零搭出一只会飞的鸟",[777,1426,1427],{},"✅ 水管、死区、计分",[777,1429,1430],{},"✅ GameManager 管状态，EventBus 管通信",[777,1432,1433],{},"✅ 美术、音效、UI",[777,1435,1436],{},"✅ 导出上架 itch.io",[777,1438,1439],{},"✅ 存档留住最高分",[10,1441,1442],{},"下一款游戏见 💪",[1444,1445,1446],"style",{},"html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}",{"title":35,"searchDepth":316,"depth":316,"links":1448},[1449,1450,1452,1453,1464,1470,1471],{"id":24,"depth":316,"text":25},{"id":95,"depth":316,"text":1451},"res:\u002F\u002F 和 user:\u002F\u002F — 别写错地方",{"id":291,"depth":316,"text":292},{"id":685,"depth":316,"text":685,"children":1454},[1455,1456,1458,1460,1462,1463],{"id":689,"depth":323,"text":692},{"id":713,"depth":323,"text":1457},":= 是什么？",{"id":807,"depth":323,"text":1459},"JSON.stringify() 和 JSON.parse_string()",{"id":858,"depth":323,"text":1461},"为什么没有 file.close()？",{"id":900,"depth":323,"text":900},{"id":997,"depth":323,"text":1000},{"id":1059,"depth":316,"text":1060,"children":1465},[1466,1468],{"id":1218,"depth":323,"text":1467},"读档时机：_ready() 里，越早越好",{"id":1240,"depth":323,"text":1469},"写档时机：只在破纪录的那一刻",{"id":1319,"depth":316,"text":1320},{"id":1371,"depth":316,"text":1371},null,"2026-09-03","XGGame-Bird Godot","md","XGGame-Bird","https:\u002F\u002Fgithub.com\u002FXXGGG\u002FXGGame-Bird",{},"\u002Fdevlog\u002Fxggame-bird\u002F11-save-system",{"title":5,"description":1474},"devlog\u002Fxggame-bird\u002F11-save-system","jjMJ4TX8XGiQKcEGk7qkmZ_xiWx5rLCIE6Kxn0teNVg",[1484,1697,2730,3420,4957,6414,7688,8238,9075,10050,11692],{"id":1485,"title":1486,"body":1487,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":1692,"navigation":326,"path":1693,"seo":1694,"stem":1695,"toc":326,"__hash__":1696},"devlog\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started.md","安装 Godot 及项目设置",{"type":7,"value":1488,"toc":1683},[1489,1493,1503,1506,1513,1516,1522,1529,1535,1538,1544,1547,1550,1553,1559,1562,1568,1602,1607,1611,1617,1623,1664,1669,1678,1680],[22,1490,1492],{"id":1491},"下载与安装-godot","下载与安装 Godot",[267,1494,1495],{},[10,1496,1497,1498],{},"官网：",[183,1499,1500],{"href":1500,"rel":1501},"https:\u002F\u002Fgodotengine.org\u002F",[1502],"nofollow",[10,1504,1505],{},"下载后解压即可使用，无需安装。",[10,1507,1508],{},[1509,1510],"img",{"alt":1511,"src":1512},"01-getting-started-Godot","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-Godot.png",[22,1514,1515],{"id":1515},"新建项目",[10,1517,1518,1519,20],{},"打开 Godot，新建一个项目，取名 ",[37,1520,1521],{},"Bird",[10,1523,1524,1525,1528],{},"渲染器选择 ",[17,1526,1527],{},"【兼容】","— 这样后续可以导出为网页版在浏览器里玩。",[10,1530,1531],{},[1509,1532],{"alt":1533,"src":1534},"01-getting-started-Godot-Bird","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-Godot-Bird.png",[10,1536,1537],{},"创建完成后进入编辑器：",[10,1539,1540],{},[1509,1541],{"alt":1542,"src":1543},"01-getting-started-界面","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-%E7%95%8C%E9%9D%A2.png",[22,1545,1546],{"id":1546},"项目设置",[10,1548,1549],{},"因为这是一个手机竖屏游戏，所以需要先调整窗口尺寸。",[10,1551,1552],{},"路径：项目 → 项目设置 → 显示 → 窗口",[10,1554,1555],{},[1509,1556],{"alt":1557,"src":1558},"01-getting-started-项目设置位置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-%E9%A1%B9%E7%9B%AE%E8%AE%BE%E7%BD%AE%E4%BD%8D%E7%BD%AE.png",[687,1560,1561],{"id":1561},"窗口尺寸",[10,1563,1564],{},[1509,1565],{"alt":1566,"src":1567},"01-getting-started-项目设置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-%E9%A1%B9%E7%9B%AE%E8%AE%BE%E7%BD%AE.png",[108,1569,1570,1580],{},[111,1571,1572],{},[114,1573,1574,1577],{},[117,1575,1576],{},"设置项",[117,1578,1579],{},"值",[127,1581,1582,1592],{},[114,1583,1584,1587],{},[132,1585,1586],{},"视口宽度 (Viewport Width)",[132,1588,1589],{},[37,1590,1591],{},"720",[114,1593,1594,1597],{},[132,1595,1596],{},"视口高度 (Viewport Height)",[132,1598,1599],{},[37,1600,1601],{},"1280",[267,1603,1604],{},[10,1605,1606],{},"视口是游戏的实际分辨率。",[687,1608,1610],{"id":1609},"拉伸适配-与-强制竖屏","拉伸适配 与 强制竖屏",[10,1612,1613],{},[1509,1614],{"alt":1615,"src":1616},"01-getting-started-项目设置2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-%E9%A1%B9%E7%9B%AE%E8%AE%BE%E7%BD%AE2.png",[10,1618,1619,1622],{},[17,1620,1621],{},"「拉伸」"," 部分：",[108,1624,1625,1636],{},[111,1626,1627],{},[114,1628,1629,1631,1633],{},[117,1630,1576],{},[117,1632,1579],{},[117,1634,1635],{},"作用",[127,1637,1638,1651],{},[114,1639,1640,1643,1648],{},[132,1641,1642],{},"模式 (Mode)",[132,1644,1645],{},[37,1646,1647],{},"canvas_items",[132,1649,1650],{},"保持 2D 图形清晰",[114,1652,1653,1656,1661],{},[132,1654,1655],{},"比例 (Aspect)",[132,1657,1658],{},[37,1659,1660],{},"keep",[132,1662,1663],{},"保持宽高比不变形",[10,1665,1666,1622],{},[17,1667,1668],{},"「手持」",[774,1670,1671],{},[777,1672,1673,1674,1677],{},"方向 (Orientation)：选择 ",[37,1675,1676],{},"portrait","（竖屏）",[1415,1679],{},[10,1681,1682],{},"设置完成！接下来就可以开始创建「小鸟」了。",{"title":35,"searchDepth":316,"depth":316,"links":1684},[1685,1686,1687],{"id":1491,"depth":316,"text":1492},{"id":1515,"depth":316,"text":1515},{"id":1546,"depth":316,"text":1546,"children":1688},[1689,1690],{"id":1561,"depth":323,"text":1561},{"id":1609,"depth":323,"text":1610},"2026-05-12",{},"\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started",{"title":1486,"description":1474},"devlog\u002Fxggame-bird\u002F01-getting-started","xfXIiVt-cjGYA7F0hi9cWLKLIpkdWG1VTKPqgfQQ7ts",{"id":1698,"title":1699,"body":1700,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":2725,"navigation":326,"path":2726,"seo":2727,"stem":2728,"toc":326,"__hash__":2729},"devlog\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird.md","创建主角：小鸟",{"type":7,"value":1701,"toc":2714},[1702,1706,1713,1716,1719,1741,1751,1757,1760,1769,1775,1778,1788,1794,1797,1803,1806,1811,1817,1823,1829,2071,2075,2088,2096,2145,2169,2175,2178,2205,2218,2363,2366,2407,2486,2496,2534,2562,2602,2633,2638,2650,2673,2676,2682,2689,2708,2711],[22,1703,1705],{"id":1704},"创建节点的方法","创建节点的方法：",[10,1707,1708,1709],{},"创建节点\u002F创建Node，点击加号「+」，然后输入需要的节点名称即可创建\n",[1509,1710],{"alt":1711,"src":1712},"02-create-bird-创建节点的方法","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E5%88%9B%E5%BB%BA%E8%8A%82%E7%82%B9%E7%9A%84%E6%96%B9%E6%B3%95.png",[22,1714,1699],{"id":1715},"创建主角小鸟",[10,1717,1718],{},"我们需要用到 3 个节点来组成小鸟：",[774,1720,1721],{},[777,1722,1723,1726,1727],{},[37,1724,1725],{},"CharacterBody2D"," — 角色节点（控制移动）\n",[774,1728,1729,1735],{},[777,1730,1731,1734],{},[37,1732,1733],{},"Sprite2D"," — 图片节点（显示外观）",[777,1736,1737,1740],{},[37,1738,1739],{},"CollisionShape2D"," — 碰撞体节点（检测碰撞）",[10,1742,1743,1744,1746,1747,1750],{},"先用 Godot 自带的图标当小鸟，把它拖到 ",[37,1745,1733],{}," 的 ",[37,1748,1749],{},"Texture"," 上即可：",[10,1752,1753],{},[1509,1754],{"alt":1755,"src":1756},"02-create-bird-创建小鸟","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E5%88%9B%E5%BB%BA%E5%B0%8F%E9%B8%9F.png",[687,1758,1759],{"id":1759},"保存场景",[10,1761,1762,1765,1766],{},[37,1763,1764],{},"Ctrl + S"," 保存场景，建一个文件夹专门放场景文件：",[37,1767,1768],{},"scenes\u002Fbird.tscn",[10,1770,1771],{},[1509,1772],{"alt":1773,"src":1774},"02-create-bird-保存场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E4%BF%9D%E5%AD%98%E5%9C%BA%E6%99%AF.png",[687,1776,1777],{"id":1777},"设置碰撞体",[10,1779,1780,1781,1783,1784,1787],{},"选中 ",[37,1782,1739],{},"，在右侧面板选一个形状(矩形：",[37,1785,1786],{},"RectangleShape2D",")，缩放到和图片差不多大：",[10,1789,1790],{},[1509,1791],{"alt":1792,"src":1793},"02-create-bird-碰撞体1","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E7%A2%B0%E6%92%9E%E4%BD%931.png",[10,1795,1796],{},"这个蓝色矩形就是碰撞体，游戏里小鸟撞到东西靠的就是它：",[10,1798,1799],{},[1509,1800],{"alt":1801,"src":1802},"02-create-bird-碰撞体2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E7%A2%B0%E6%92%9E%E4%BD%932.png",[22,1804,1805],{"id":1805},"编写脚本",[10,1807,1780,1808,1810],{},[37,1809,1725],{}," 节点，点击右上角的 📜 图标创建脚本：",[10,1812,1813],{},[1509,1814],{"alt":1815,"src":1816},"02-create-bird-创建脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E5%88%9B%E5%BB%BA%E8%84%9A%E6%9C%AC.png",[10,1818,1819,1820],{},"同样建一个文件夹专门放脚本：",[37,1821,1822],{},"scripts\u002Fbird.gd",[10,1824,1825],{},[1509,1826],{"alt":1827,"src":1828},"02-create-bird-脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E8%84%9A%E6%9C%AC.png",[30,1830,1832],{"className":32,"code":1831,"language":34,"meta":35,"style":35},"extends CharacterBody2D # 创建时自带的\n\n@export var gravity_scale := 2.0       # 重力倍率（控制掉落快慢）\n@export var jump_force := -500.0       # 跳跃力量（负值 = 向上）\n@export var max_fall_speed := 1000.0   # 最大掉落速度\n\nfunc _physics_process(delta: float) -> void:\n    # 1. 施加重力：每帧让小鸟往下掉\n    if not is_on_floor():\n        velocity += get_gravity() * delta * gravity_scale\n\n    # 2. 限速：防止掉太快\n    if velocity.y > max_fall_speed:\n        velocity.y = max_fall_speed\n\n    # 3. 按键跳跃：按下就给一个向上的速度\n    if Input.is_action_just_pressed(\"fly\"):\n        velocity.y = jump_force\n\n    move_and_slide()\n",[37,1833,1834,1844,1848,1867,1887,1904,1908,1933,1938,1951,1975,1979,1984,2003,2016,2020,2025,2046,2059,2063],{"__ignoreMap":35},[40,1835,1836,1838,1841],{"class":42,"line":43},[40,1837,310],{"class":309},[40,1839,1840],{"class":58}," CharacterBody2D",[40,1842,1843],{"class":319}," # 创建时自带的\n",[40,1845,1846],{"class":42,"line":316},[40,1847,327],{"emptyLinePlaceholder":326},[40,1849,1850,1853,1856,1859,1861,1864],{"class":42,"line":323},[40,1851,1852],{"class":357},"@export",[40,1854,1855],{"class":46}," var",[40,1857,1858],{"class":50}," gravity_scale",[40,1860,339],{"class":54},[40,1862,1863],{"class":65}," 2.0",[40,1865,1866],{"class":319},"       # 重力倍率（控制掉落快慢）\n",[40,1868,1869,1871,1873,1876,1878,1881,1884],{"class":42,"line":330},[40,1870,1852],{"class":357},[40,1872,1855],{"class":46},[40,1874,1875],{"class":50}," jump_force",[40,1877,339],{"class":54},[40,1879,1880],{"class":46}," -",[40,1882,1883],{"class":65},"500.0",[40,1885,1886],{"class":319},"       # 跳跃力量（负值 = 向上）\n",[40,1888,1889,1891,1893,1896,1898,1901],{"class":42,"line":346},[40,1890,1852],{"class":357},[40,1892,1855],{"class":46},[40,1894,1895],{"class":50}," max_fall_speed",[40,1897,339],{"class":54},[40,1899,1900],{"class":65}," 1000.0",[40,1902,1903],{"class":319},"   # 最大掉落速度\n",[40,1905,1906],{"class":42,"line":351},[40,1907,327],{"emptyLinePlaceholder":326},[40,1909,1910,1912,1915,1917,1920,1922,1925,1927,1929,1931],{"class":42,"line":373},[40,1911,354],{"class":46},[40,1913,1914],{"class":357}," _physics_process",[40,1916,433],{"class":54},[40,1918,1919],{"class":393},"delta",[40,1921,55],{"class":54},[40,1923,1924],{"class":58}," float",[40,1926,522],{"class":54},[40,1928,364],{"class":46},[40,1930,367],{"class":58},[40,1932,370],{"class":54},[40,1934,1935],{"class":42,"line":387},[40,1936,1937],{"class":319},"    # 1. 施加重力：每帧让小鸟往下掉\n",[40,1939,1940,1942,1944,1947,1949],{"class":42,"line":409},[40,1941,505],{"class":309},[40,1943,508],{"class":46},[40,1945,1946],{"class":357}," is_on_floor",[40,1948,361],{"class":54},[40,1950,370],{"class":393},[40,1952,1953,1956,1959,1962,1964,1967,1970,1972],{"class":42,"line":415},[40,1954,1955],{"class":50},"        velocity",[40,1957,1958],{"class":46}," +=",[40,1960,1961],{"class":357}," get_gravity",[40,1963,361],{"class":54},[40,1965,1966],{"class":46}," *",[40,1968,1969],{"class":50}," delta",[40,1971,1966],{"class":46},[40,1973,1974],{"class":50}," gravity_scale\n",[40,1976,1977],{"class":42,"line":452},[40,1978,327],{"emptyLinePlaceholder":326},[40,1980,1981],{"class":42,"line":481},[40,1982,1983],{"class":319},"    # 2. 限速：防止掉太快\n",[40,1985,1986,1988,1991,1993,1996,1999,2001],{"class":42,"line":486},[40,1987,505],{"class":309},[40,1989,1990],{"class":50}," velocity",[40,1992,400],{"class":54},[40,1994,1995],{"class":50},"y",[40,1997,1998],{"class":46}," >",[40,2000,1895],{"class":50},[40,2002,370],{"class":393},[40,2004,2005,2007,2009,2011,2013],{"class":42,"line":502},[40,2006,1955],{"class":50},[40,2008,400],{"class":54},[40,2010,1995],{"class":50},[40,2012,62],{"class":54},[40,2014,2015],{"class":50}," max_fall_speed\n",[40,2017,2018],{"class":42,"line":527},[40,2019,327],{"emptyLinePlaceholder":326},[40,2021,2022],{"class":42,"line":536},[40,2023,2024],{"class":319},"    # 3. 按键跳跃：按下就给一个向上的速度\n",[40,2026,2027,2029,2032,2034,2037,2039,2042,2044],{"class":42,"line":566},[40,2028,505],{"class":309},[40,2030,2031],{"class":58}," Input",[40,2033,400],{"class":393},[40,2035,2036],{"class":357},"is_action_just_pressed",[40,2038,433],{"class":54},[40,2040,2041],{"class":342},"\"fly\"",[40,2043,522],{"class":54},[40,2045,370],{"class":393},[40,2047,2048,2050,2052,2054,2056],{"class":42,"line":596},[40,2049,1955],{"class":50},[40,2051,400],{"class":54},[40,2053,1995],{"class":50},[40,2055,62],{"class":54},[40,2057,2058],{"class":50}," jump_force\n",[40,2060,2061],{"class":42,"line":611},[40,2062,327],{"emptyLinePlaceholder":326},[40,2064,2065,2068],{"class":42,"line":619},[40,2066,2067],{"class":357},"    move_and_slide",[40,2069,2070],{"class":54},"()\n",[687,2072,2074],{"id":2073},"这段代码在干嘛","这段代码在干嘛？",[30,2076,2078],{"className":32,"code":2077,"language":34,"meta":35,"style":35},"extends CharacterBody2D # 创建时自带的\n",[37,2079,2080],{"__ignoreMap":35},[40,2081,2082,2084,2086],{"class":42,"line":43},[40,2083,310],{"class":309},[40,2085,1840],{"class":58},[40,2087,1843],{"class":319},[774,2089,2090],{},[777,2091,2092,2093,2095],{},"为 ",[37,2094,1725],{}," 节点创建脚本时会自带的代码。告诉 Godot：这个脚本是给「角色」用的（一般不用理会）。",[30,2097,2099],{"className":32,"code":2098,"language":34,"meta":35,"style":35},"@export var gravity_scale := 2.0       # 重力倍率（控制掉落快慢）\n@export var jump_force := -500.0       # 跳跃力量（负值 = 向上）\n@export var max_fall_speed := 1000.0   # 最大掉落速度\n",[37,2100,2101,2115,2131],{"__ignoreMap":35},[40,2102,2103,2105,2107,2109,2111,2113],{"class":42,"line":43},[40,2104,1852],{"class":357},[40,2106,1855],{"class":46},[40,2108,1858],{"class":50},[40,2110,339],{"class":54},[40,2112,1863],{"class":65},[40,2114,1866],{"class":319},[40,2116,2117,2119,2121,2123,2125,2127,2129],{"class":42,"line":316},[40,2118,1852],{"class":357},[40,2120,1855],{"class":46},[40,2122,1875],{"class":50},[40,2124,339],{"class":54},[40,2126,1880],{"class":46},[40,2128,1883],{"class":65},[40,2130,1886],{"class":319},[40,2132,2133,2135,2137,2139,2141,2143],{"class":42,"line":323},[40,2134,1852],{"class":357},[40,2136,1855],{"class":46},[40,2138,1895],{"class":50},[40,2140,339],{"class":54},[40,2142,1900],{"class":65},[40,2144,1903],{"class":319},[774,2146,2147,2152,2158,2164],{},[777,2148,2149,2151],{},[37,2150,47],{}," 定义变量",[777,2153,2154,2157],{},[37,2155,2156],{},"jump_force"," 等是自定义的变量名，",[777,2159,2160,2163],{},[37,2161,2162],{},"-500.0"," 是我们定义的数值！",[777,2165,2166,2168],{},[37,2167,1852],{}," 加在前面是为了让 Godot 把变量放到面板上，方便我们后续调整。👇",[10,2170,2171],{},[1509,2172],{"alt":2173,"src":2174},"02-create-bird-右侧面板","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E5%8F%B3%E4%BE%A7%E9%9D%A2%E6%9D%BF.png",[10,2176,2177],{},"它就是我们说的「数值」，例如一般游戏中用来定义角色的「攻击力」「血量」......",[30,2179,2181],{"className":32,"code":2180,"language":34,"meta":35,"style":35},"func _physics_process(delta: float) -> void:\n",[37,2182,2183],{"__ignoreMap":35},[40,2184,2185,2187,2189,2191,2193,2195,2197,2199,2201,2203],{"class":42,"line":43},[40,2186,354],{"class":46},[40,2188,1914],{"class":357},[40,2190,433],{"class":54},[40,2192,1919],{"class":393},[40,2194,55],{"class":54},[40,2196,1924],{"class":58},[40,2198,522],{"class":54},[40,2200,364],{"class":46},[40,2202,367],{"class":58},[40,2204,370],{"class":54},[774,2206,2207,2212],{},[777,2208,2209,2211],{},[37,2210,354],{},"： 用来定义方法",[777,2213,2214,2217],{},[37,2215,2216],{},"_physics_process(delta)","：游戏每一帧都会自动执行这个函数（大约每秒 60 次）",[30,2219,2221],{"className":32,"code":2220,"language":34,"meta":35,"style":35},"func _physics_process(delta: float) -> void:\n    # 1. 施加重力：每帧让小鸟往下掉\n    if not is_on_floor():\n        velocity += get_gravity() * delta * gravity_scale\n\n    # 2. 限速：防止掉太快\n    if velocity.y > max_fall_speed:\n        velocity.y = max_fall_speed\n\n    # 3. 按键跳跃：按下就给一个向上的速度\n    if Input.is_action_just_pressed(\"fly\"):\n        velocity.y = jump_force\n\n    move_and_slide()\n",[37,2222,2223,2245,2249,2261,2279,2283,2287,2303,2315,2319,2323,2341,2353,2357],{"__ignoreMap":35},[40,2224,2225,2227,2229,2231,2233,2235,2237,2239,2241,2243],{"class":42,"line":43},[40,2226,354],{"class":46},[40,2228,1914],{"class":357},[40,2230,433],{"class":54},[40,2232,1919],{"class":393},[40,2234,55],{"class":54},[40,2236,1924],{"class":58},[40,2238,522],{"class":54},[40,2240,364],{"class":46},[40,2242,367],{"class":58},[40,2244,370],{"class":54},[40,2246,2247],{"class":42,"line":316},[40,2248,1937],{"class":319},[40,2250,2251,2253,2255,2257,2259],{"class":42,"line":323},[40,2252,505],{"class":309},[40,2254,508],{"class":46},[40,2256,1946],{"class":357},[40,2258,361],{"class":54},[40,2260,370],{"class":393},[40,2262,2263,2265,2267,2269,2271,2273,2275,2277],{"class":42,"line":330},[40,2264,1955],{"class":50},[40,2266,1958],{"class":46},[40,2268,1961],{"class":357},[40,2270,361],{"class":54},[40,2272,1966],{"class":46},[40,2274,1969],{"class":50},[40,2276,1966],{"class":46},[40,2278,1974],{"class":50},[40,2280,2281],{"class":42,"line":346},[40,2282,327],{"emptyLinePlaceholder":326},[40,2284,2285],{"class":42,"line":351},[40,2286,1983],{"class":319},[40,2288,2289,2291,2293,2295,2297,2299,2301],{"class":42,"line":373},[40,2290,505],{"class":309},[40,2292,1990],{"class":50},[40,2294,400],{"class":54},[40,2296,1995],{"class":50},[40,2298,1998],{"class":46},[40,2300,1895],{"class":50},[40,2302,370],{"class":393},[40,2304,2305,2307,2309,2311,2313],{"class":42,"line":387},[40,2306,1955],{"class":50},[40,2308,400],{"class":54},[40,2310,1995],{"class":50},[40,2312,62],{"class":54},[40,2314,2015],{"class":50},[40,2316,2317],{"class":42,"line":409},[40,2318,327],{"emptyLinePlaceholder":326},[40,2320,2321],{"class":42,"line":415},[40,2322,2024],{"class":319},[40,2324,2325,2327,2329,2331,2333,2335,2337,2339],{"class":42,"line":452},[40,2326,505],{"class":309},[40,2328,2031],{"class":58},[40,2330,400],{"class":393},[40,2332,2036],{"class":357},[40,2334,433],{"class":54},[40,2336,2041],{"class":342},[40,2338,522],{"class":54},[40,2340,370],{"class":393},[40,2342,2343,2345,2347,2349,2351],{"class":42,"line":481},[40,2344,1955],{"class":50},[40,2346,400],{"class":54},[40,2348,1995],{"class":50},[40,2350,62],{"class":54},[40,2352,2058],{"class":50},[40,2354,2355],{"class":42,"line":486},[40,2356,327],{"emptyLinePlaceholder":326},[40,2358,2359,2361],{"class":42,"line":502},[40,2360,2067],{"class":357},[40,2362,2070],{"class":54},[10,2364,2365],{},"其中：",[30,2367,2369],{"className":32,"code":2368,"language":34,"meta":35,"style":35},"# 1. 施加重力：每帧让小鸟往下掉\nif not is_on_floor():\n    velocity += get_gravity() * delta * gravity_scale\n",[37,2370,2371,2376,2388],{"__ignoreMap":35},[40,2372,2373],{"class":42,"line":43},[40,2374,2375],{"class":319},"# 1. 施加重力：每帧让小鸟往下掉\n",[40,2377,2378,2380,2382,2384,2386],{"class":42,"line":316},[40,2379,910],{"class":309},[40,2381,508],{"class":46},[40,2383,1946],{"class":357},[40,2385,361],{"class":54},[40,2387,370],{"class":393},[40,2389,2390,2393,2395,2397,2399,2401,2403,2405],{"class":42,"line":323},[40,2391,2392],{"class":50},"    velocity",[40,2394,1958],{"class":46},[40,2396,1961],{"class":357},[40,2398,361],{"class":54},[40,2400,1966],{"class":46},[40,2402,1969],{"class":50},[40,2404,1966],{"class":46},[40,2406,1974],{"class":50},[774,2408,2409,2437],{},[777,2410,2411,2413,2414],{},[37,2412,910],{}," 是用来【判断】的\n",[774,2415,2416,2422,2431],{},[777,2417,2418,2421],{},[37,2419,2420],{},"not"," 是否定句",[777,2423,2424,2427,2428,2430],{},[37,2425,2426],{},"is_on_floor()"," 是 Godot引擎 为 ",[37,2429,1725],{},"节点赋予的属性",[777,2432,2433,2436],{},[37,2434,2435],{},"if not is_on_floor():"," 的意思是“如果 ‘角色’ 没有 站在地面时”\n是的，如果角色没有在地面，那就让角色往下掉！但是要以什么速度往下掉呢？",[777,2438,2439,2442],{},[37,2440,2441],{},"velocity += get_gravity() * delta * gravity_scale",[774,2443,2444,2460,2466,2471,2477],{},[777,2445,2446,2449,2450,2452,2453,2456,2457,2459],{},[37,2447,2448],{},"velocity"," 是 Godot 给 ",[37,2451,1725],{}," 内置的「速度」属性。它是一个二维数值，有 ",[37,2454,2455],{},"x","（左右速度）和 ",[37,2458,1995],{},"（上下速度）两个分量。初始值是 0（小鸟不动）",[777,2461,2462,2465],{},[37,2463,2464],{},"get_gravity()"," 是 Godot 内置方法，返回项目设置里的「重力」值（默认向下 980）",[777,2467,2468,2470],{},[37,2469,1919],{}," 是「上一帧到这一帧的时间间隔」（秒）。乘上它能保证不同帧率（60fps \u002F 120fps）下游戏速度一致",[777,2472,2473,2476],{},[37,2474,2475],{},"gravity_scale"," 是我们自己定义的「重力倍率」，越大掉得越快",[777,2478,2479,2482,2483],{},[37,2480,2481],{},"+="," 是「累加」的意思，等价于 ",[37,2484,2485],{},"velocity = velocity + ...",[10,2487,2488,2489,2495],{},"所以这一行的意思是：",[17,2490,2491,2492,2494],{},"每一帧给 ",[37,2493,2448],{}," 累加一点点重力","。速度越累越大 → 小鸟越掉越快，这就是物理课上学过的「自由落体」。",[30,2497,2499],{"className":32,"code":2498,"language":34,"meta":35,"style":35},"# 2. 限速：防止掉太快\nif velocity.y > max_fall_speed:\n    velocity.y = max_fall_speed\n",[37,2500,2501,2506,2522],{"__ignoreMap":35},[40,2502,2503],{"class":42,"line":43},[40,2504,2505],{"class":319},"# 2. 限速：防止掉太快\n",[40,2507,2508,2510,2512,2514,2516,2518,2520],{"class":42,"line":316},[40,2509,910],{"class":309},[40,2511,1990],{"class":50},[40,2513,400],{"class":54},[40,2515,1995],{"class":50},[40,2517,1998],{"class":46},[40,2519,1895],{"class":50},[40,2521,370],{"class":393},[40,2523,2524,2526,2528,2530,2532],{"class":42,"line":323},[40,2525,2392],{"class":50},[40,2527,400],{"class":54},[40,2529,1995],{"class":50},[40,2531,62],{"class":54},[40,2533,2015],{"class":50},[774,2535,2536,2546,2556],{},[777,2537,2538,2541,2542,2545],{},[37,2539,2540],{},"velocity.y"," 就是上下方向的速度（",[17,2543,2544],{},"往下为正、往上为负","，这是 Godot 的坐标系约定）",[777,2547,2548,2549,2551,2552,2555],{},"如果当前下落速度 ",[37,2550,2540],{}," 超过了我们设的最大值 ",[37,2553,2554],{},"max_fall_speed","，就把它「卡」在最大值上",[777,2557,2558,2561],{},[17,2559,2560],{},"为什么需要？"," 因为重力会一直累加，没有这个限制小鸟会越掉越快，最后快得看不见",[30,2563,2565],{"className":32,"code":2564,"language":34,"meta":35,"style":35},"# 3. 按键跳跃：按下就给一个向上的速度\nif Input.is_action_just_pressed(\"fly\"):\n    velocity.y = jump_force\n",[37,2566,2567,2572,2590],{"__ignoreMap":35},[40,2568,2569],{"class":42,"line":43},[40,2570,2571],{"class":319},"# 3. 按键跳跃：按下就给一个向上的速度\n",[40,2573,2574,2576,2578,2580,2582,2584,2586,2588],{"class":42,"line":316},[40,2575,910],{"class":309},[40,2577,2031],{"class":58},[40,2579,400],{"class":393},[40,2581,2036],{"class":357},[40,2583,433],{"class":54},[40,2585,2041],{"class":342},[40,2587,522],{"class":54},[40,2589,370],{"class":393},[40,2591,2592,2594,2596,2598,2600],{"class":42,"line":323},[40,2593,2392],{"class":50},[40,2595,400],{"class":54},[40,2597,1995],{"class":50},[40,2599,62],{"class":54},[40,2601,2058],{"class":50},[774,2603,2604,2614,2626],{},[777,2605,2606,2609,2610,2613],{},[37,2607,2608],{},"Input.is_action_just_pressed(\"fly\")"," 检测「fly」这个按键",[17,2611,2612],{},"刚刚被按下","（一次按下只触发一次，长按不会连续触发）",[777,2615,2616,2617,2619,2620,2622,2623,2625],{},"一旦按下，直接把 ",[37,2618,2540],{}," 设为 ",[37,2621,2156],{},"（前面定义的 ",[37,2624,2162],{},"，负数 = 向上）",[777,2627,2628,2629,2632],{},"这就是「跳跃」：瞬间把速度变成向上，",[17,2630,2631],{},"覆盖掉","之前累加的下落速度",[267,2634,2635],{},[10,2636,2637],{},"「fly」是什么按键？我们还没告诉 Godot，下面就来配置快捷键。",[30,2639,2641],{"className":32,"code":2640,"language":34,"meta":35,"style":35},"move_and_slide()\n",[37,2642,2643],{"__ignoreMap":35},[40,2644,2645,2648],{"class":42,"line":43},[40,2646,2647],{"class":357},"move_and_slide",[40,2649,2070],{"class":54},[774,2651,2652,2658,2667],{},[777,2653,2654,2655,2657],{},"Godot 给 ",[37,2656,1725],{}," 内置的方法",[777,2659,2660,2666],{},[17,2661,2662,2663,2665],{},"根据 ",[37,2664,2448],{}," 真正移动小鸟","，并自动处理碰撞（撞到东西就停下来）",[777,2668,2669,2670,2672],{},"没有这一行，前面算好的 ",[37,2671,2448],{}," 都只是「空想」，小鸟不会真的动起来",[687,2674,2675],{"id":2675},"配置跳跃按键",[10,2677,2678,2679,2681],{},"代码里用了 ",[37,2680,2608],{},"，所以我们需要在项目设置里添加这个按键映射：",[10,2683,2684,2685],{},"路径：项目 -> 项目设置 -> 输入映射 (Input Map)\n",[1509,2686],{"alt":2687,"src":2688},"02-create-bird-配置跳跃按键","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E9%85%8D%E7%BD%AE%E8%B7%B3%E8%B7%83%E6%8C%89%E9%94%AE.png",[1373,2690,2691,2698],{},[777,2692,2693,2694,2697],{},"在上方输入 ",[37,2695,2696],{},"fly","，点击「添加」",[777,2699,2700,2701,2703,2704,2707],{},"点击 ",[37,2702,2696],{}," 右边的 ",[37,2705,2706],{},"+","，按下你想要的按键（比如空格键）",[10,2709,2710],{},"这样按空格键小鸟就会往上跳了！不过目前这只小鸟还没有舞台，下一章，我们将为它搭建一个可以飞的场景。",[1444,2712,2713],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":35,"searchDepth":316,"depth":316,"links":2715},[2716,2717,2721],{"id":1704,"depth":316,"text":1705},{"id":1715,"depth":316,"text":1699,"children":2718},[2719,2720],{"id":1759,"depth":323,"text":1759},{"id":1777,"depth":323,"text":1777},{"id":1805,"depth":316,"text":1805,"children":2722},[2723,2724],{"id":2073,"depth":323,"text":2074},{"id":2675,"depth":323,"text":2675},{},"\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird",{"title":1699,"description":1474},"devlog\u002Fxggame-bird\u002F02-create-bird","4ulFauc0NCvEFhdOh3vFco0Uc_Za6ZBezoBXrpjuq6s",{"id":2731,"title":2732,"body":2733,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":3415,"navigation":326,"path":3416,"seo":3417,"stem":3418,"toc":326,"__hash__":3419},"devlog\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene.md","游戏场景",{"type":7,"value":2734,"toc":3405},[2735,2738,2741,2744,2759,2762,2777,2780,2787,2791,2802,2806,2817,2820,2825,2831,2834,2837,2844,2846,2869,2875,2885,2891,2912,2915,2918,2929,2932,2935,2938,2941,2961,2968,3383,3395,3399,3402],[10,2736,2737],{},"这一章我们来完善游戏世界：搭建舞台、调整摄像机、加边界。",[22,2739,2740],{"id":2740},"创建游戏场景",[10,2742,2743],{},"小鸟做好了，但现在还没有一个「舞台」让它表演。我们需要创建一个游戏主场景，把小鸟放进去，再加一个摄像机来\"看\"它。",[10,2745,2746,2747,2750,2751,2754,2755],{},"新建场景，根节点选 ",[37,2748,2749],{},"Node2D","，保存为 ",[37,2752,2753],{},"scenes\u002Fgame.tscn","：\n",[1509,2756],{"alt":2757,"src":2758},"03-game-scene-创建游戏场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E5%88%9B%E5%BB%BA%E6%B8%B8%E6%88%8F%E5%9C%BA%E6%99%AF.png",[10,2760,2761],{},"使用到的节点：",[774,2763,2764],{},[777,2765,2766,2768,2769],{},[37,2767,2749],{}," — 游戏主场景\n",[774,2770,2771],{},[777,2772,2773,2776],{},[37,2774,2775],{},"Camera2D"," — 摄像机（决定屏幕看到哪里）",[22,2778,2779],{"id":2779},"调整摄像机位置",[10,2781,2782,2783],{},"调整摄像机（固定）位置：\n",[1509,2784],{"alt":2785,"src":2786},"03-game-scene-调整摄像机位置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%B0%83%E6%95%B4%E6%91%84%E5%83%8F%E6%9C%BA%E4%BD%8D%E7%BD%AE.png",[22,2788,2790],{"id":2789},"导入小鸟角色","导入“小鸟”角色",[10,2792,2793,2794,2797,2798],{},"把之前做好的 ",[37,2795,2796],{},"bird.tscn"," 拖进场景中，小鸟就出现了：\n",[1509,2799],{"alt":2800,"src":2801},"03-game-scene-导入bird","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E5%AF%BC%E5%85%A5bird.png",[22,2803,2805],{"id":2804},"设置主场景然后测试","设置【主场景】然后测试",[10,2807,2808,2809,2812,2813],{},"然后把 ",[37,2810,2811],{},"game.tscn"," 设置为【主场景】，点击运行游戏：\n",[1509,2814],{"alt":2815,"src":2816},"03-game-scene-设置主场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%AE%BE%E7%BD%AE%E4%B8%BB%E5%9C%BA%E6%99%AF.png",[10,2818,2819],{},"这时候小鸟就在游戏场景里了，点击【鼠标左键】或【空格键】小鸟就会起飞！",[267,2821,2822],{},[10,2823,2824],{},"确保你已经按上一章配置了【fly】按键映射！",[10,2826,2827],{},[1509,2828],{"alt":2829,"src":2830},"03-game-scene-运行游戏","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%BF%90%E8%A1%8C%E6%B8%B8%E6%88%8F.png",[10,2832,2833],{},"不过现在不按按键的话，小鸟就会一直往下掉，掉到屏幕外面去。别急，接下来我们会给游戏场景加上边界。",[22,2835,2836],{"id":2836},"添加边界",[10,2838,2839,2840,2843],{},"为了让小鸟不会掉出屏幕，我们给游戏世界加上",[17,2841,2842],{},"底部","边界。",[10,2845,2761],{},[774,2847,2848],{},[777,2849,2850,2853,2854],{},[37,2851,2852],{},"StaticBody2D"," — 静态物理体（不动，但能碰撞）\n",[774,2855,2856],{},[777,2857,2858,2860,2861],{},[37,2859,1739],{}," — 碰撞形状\n",[774,2862,2863],{},[777,2864,2865,2868],{},[37,2866,2867],{},"WorldBoundaryShape2D"," — 选择这个形状（世界边界）",[10,2870,2871],{},[1509,2872],{"alt":2873,"src":2874},"03-game-scene-边界","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%BE%B9%E7%95%8C.png",[10,2876,2877,2878,2880,2881,2884],{},"然后选择 ",[37,2879,2852],{}," 节点往下拖到边缘底部，因为后续要在该节点里",[17,2882,2883],{},"加入","【死亡区域（Killzone）】，这样才能触发游戏结束的代码。",[10,2886,2887],{},[1509,2888],{"alt":2889,"src":2890},"03-game-scene-边界2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%BE%B9%E7%95%8C2.png",[1373,2892,2893,2899,2909],{},[777,2894,2895,2896,2898],{},"先选择 ",[37,2897,2852],{}," 节点",[777,2900,2901,2902],{},"然后点击上方的【编辑所选节点】\n",[1373,2903,2904],{},[777,2905,2906,2907,2898],{},"避免拖拽时不小心选中 ",[37,2908,1739],{},[777,2910,2911],{},"然后往下拖拽到摄像机和画面边缘。",[22,2913,2914],{"id":2914},"测试运行",[10,2916,2917],{},"会发现几个问题，不过都不是问题！",[1373,2919,2920,2923,2926],{},[777,2921,2922],{},"小鸟在原地高低移动，没有往前飞！但是这是正常的，就是要小鸟在原地高低移动。后续我们只要设置背景和障碍物往左移动就会让玩家以为【小鸟】在往右👉飞。",[777,2924,2925],{},"小鸟没有点击【fly】就会倒地，但是游戏没有结束，别着急，后面就会完善代码！",[777,2927,2928],{},"与原版的小鸟有所不同，原本的《Flappy Bird》小鸟在飞的时候会有种“头重脚轻”的感觉：就是在飞的时候仰头，不飞的时候头垂下。在玩家连点的时候有种“扑腾”的感觉。",[10,2930,2931],{},"所以接下来我们就先来优化小鸟的代码脚本，让它可以“扑腾”",[22,2933,2934],{"id":2934},"优化小鸟的旋转",[10,2936,2937],{},"在原版 Flappy Bird 里，小鸟不是一直直挺挺的 — 往上飞的时候头会抬起来，往下掉的时候头会低下去，看起来更生动。",[10,2939,2940],{},"我们来实现这个效果，思路很简单：",[774,2942,2943,2949,2955],{},[777,2944,2945,2948],{},[17,2946,2947],{},"往上飞"," → 头朝上（抬头 -30°）",[777,2950,2951,2954],{},[17,2952,2953],{},"往下掉"," → 头朝下（低头 90°）",[777,2956,2957,2960],{},[17,2958,2959],{},"旋转要平滑过渡","，不能一下子就转过去（那样看起来很僵硬）",[10,2962,2963,2964,2967],{},"在 ",[37,2965,2966],{},"bird.gd"," 里加一个旋转函数：",[30,2969,2971],{"className":32,"code":2970,"language":34,"meta":35,"style":35},"extends CharacterBody2D\n\n@export var gravity_scale := 2.0\n@export var jump_force := -400.0\n@export var max_fall_speed := 1000.0\n@export var rotate_speed := 4.0   # 旋转过渡速度（越大越快）\n\nfunc _physics_process(delta: float) -> void:\n    # 重力\n    if not is_on_floor():\n        velocity += get_gravity() * delta * gravity_scale\n\n    # 限速\n    if velocity.y > max_fall_speed:\n        velocity.y = max_fall_speed\n\n    # 跳跃\n    if Input.is_action_just_pressed(\"fly\"):\n        velocity.y = 0\n        velocity.y = jump_force\n\n    # 旋转\n    handle_rotation(delta)\n    move_and_slide()\n\nfunc handle_rotation(delta: float) -> void:\n    var target_rotation = 0.0\n\n    if velocity.y \u003C 0:\n        # 往上飞 → 抬头\n        target_rotation = deg_to_rad(-30)\n    else:\n        # 往下掉 → 低头\n        target_rotation = deg_to_rad(90)\n\n    # lerp_angle：让旋转平滑过渡，不会一下子\"弹\"过去\n    rotation = lerp_angle(rotation, target_rotation, rotate_speed * delta)\n",[37,2972,2973,2980,2984,2997,3012,3025,3042,3046,3068,3073,3085,3103,3107,3112,3128,3140,3144,3149,3167,3179,3191,3196,3202,3214,3221,3226,3250,3263,3268,3285,3291,3312,3320,3326,3342,3347,3353],{"__ignoreMap":35},[40,2974,2975,2977],{"class":42,"line":43},[40,2976,310],{"class":309},[40,2978,2979],{"class":58}," CharacterBody2D\n",[40,2981,2982],{"class":42,"line":316},[40,2983,327],{"emptyLinePlaceholder":326},[40,2985,2986,2988,2990,2992,2994],{"class":42,"line":323},[40,2987,1852],{"class":357},[40,2989,1855],{"class":46},[40,2991,1858],{"class":50},[40,2993,339],{"class":54},[40,2995,2996],{"class":65}," 2.0\n",[40,2998,2999,3001,3003,3005,3007,3009],{"class":42,"line":330},[40,3000,1852],{"class":357},[40,3002,1855],{"class":46},[40,3004,1875],{"class":50},[40,3006,339],{"class":54},[40,3008,1880],{"class":46},[40,3010,3011],{"class":65},"400.0\n",[40,3013,3014,3016,3018,3020,3022],{"class":42,"line":346},[40,3015,1852],{"class":357},[40,3017,1855],{"class":46},[40,3019,1895],{"class":50},[40,3021,339],{"class":54},[40,3023,3024],{"class":65}," 1000.0\n",[40,3026,3027,3029,3031,3034,3036,3039],{"class":42,"line":351},[40,3028,1852],{"class":357},[40,3030,1855],{"class":46},[40,3032,3033],{"class":50}," rotate_speed",[40,3035,339],{"class":54},[40,3037,3038],{"class":65}," 4.0",[40,3040,3041],{"class":319},"   # 旋转过渡速度（越大越快）\n",[40,3043,3044],{"class":42,"line":373},[40,3045,327],{"emptyLinePlaceholder":326},[40,3047,3048,3050,3052,3054,3056,3058,3060,3062,3064,3066],{"class":42,"line":387},[40,3049,354],{"class":46},[40,3051,1914],{"class":357},[40,3053,433],{"class":54},[40,3055,1919],{"class":393},[40,3057,55],{"class":54},[40,3059,1924],{"class":58},[40,3061,522],{"class":54},[40,3063,364],{"class":46},[40,3065,367],{"class":58},[40,3067,370],{"class":54},[40,3069,3070],{"class":42,"line":409},[40,3071,3072],{"class":319},"    # 重力\n",[40,3074,3075,3077,3079,3081,3083],{"class":42,"line":415},[40,3076,505],{"class":309},[40,3078,508],{"class":46},[40,3080,1946],{"class":357},[40,3082,361],{"class":54},[40,3084,370],{"class":393},[40,3086,3087,3089,3091,3093,3095,3097,3099,3101],{"class":42,"line":452},[40,3088,1955],{"class":50},[40,3090,1958],{"class":46},[40,3092,1961],{"class":357},[40,3094,361],{"class":54},[40,3096,1966],{"class":46},[40,3098,1969],{"class":50},[40,3100,1966],{"class":46},[40,3102,1974],{"class":50},[40,3104,3105],{"class":42,"line":481},[40,3106,327],{"emptyLinePlaceholder":326},[40,3108,3109],{"class":42,"line":486},[40,3110,3111],{"class":319},"    # 限速\n",[40,3113,3114,3116,3118,3120,3122,3124,3126],{"class":42,"line":502},[40,3115,505],{"class":309},[40,3117,1990],{"class":50},[40,3119,400],{"class":54},[40,3121,1995],{"class":50},[40,3123,1998],{"class":46},[40,3125,1895],{"class":50},[40,3127,370],{"class":393},[40,3129,3130,3132,3134,3136,3138],{"class":42,"line":527},[40,3131,1955],{"class":50},[40,3133,400],{"class":54},[40,3135,1995],{"class":50},[40,3137,62],{"class":54},[40,3139,2015],{"class":50},[40,3141,3142],{"class":42,"line":536},[40,3143,327],{"emptyLinePlaceholder":326},[40,3145,3146],{"class":42,"line":566},[40,3147,3148],{"class":319},"    # 跳跃\n",[40,3150,3151,3153,3155,3157,3159,3161,3163,3165],{"class":42,"line":596},[40,3152,505],{"class":309},[40,3154,2031],{"class":58},[40,3156,400],{"class":393},[40,3158,2036],{"class":357},[40,3160,433],{"class":54},[40,3162,2041],{"class":342},[40,3164,522],{"class":54},[40,3166,370],{"class":393},[40,3168,3169,3171,3173,3175,3177],{"class":42,"line":611},[40,3170,1955],{"class":50},[40,3172,400],{"class":54},[40,3174,1995],{"class":50},[40,3176,62],{"class":54},[40,3178,66],{"class":65},[40,3180,3181,3183,3185,3187,3189],{"class":42,"line":619},[40,3182,1955],{"class":50},[40,3184,400],{"class":54},[40,3186,1995],{"class":50},[40,3188,62],{"class":54},[40,3190,2058],{"class":50},[40,3192,3194],{"class":42,"line":3193},21,[40,3195,327],{"emptyLinePlaceholder":326},[40,3197,3199],{"class":42,"line":3198},22,[40,3200,3201],{"class":319},"    # 旋转\n",[40,3203,3205,3208,3210,3212],{"class":42,"line":3204},23,[40,3206,3207],{"class":357},"    handle_rotation",[40,3209,433],{"class":54},[40,3211,1919],{"class":50},[40,3213,449],{"class":54},[40,3215,3217,3219],{"class":42,"line":3216},24,[40,3218,2067],{"class":357},[40,3220,2070],{"class":54},[40,3222,3224],{"class":42,"line":3223},25,[40,3225,327],{"emptyLinePlaceholder":326},[40,3227,3229,3231,3234,3236,3238,3240,3242,3244,3246,3248],{"class":42,"line":3228},26,[40,3230,354],{"class":46},[40,3232,3233],{"class":357}," handle_rotation",[40,3235,433],{"class":54},[40,3237,1919],{"class":393},[40,3239,55],{"class":54},[40,3241,1924],{"class":58},[40,3243,522],{"class":54},[40,3245,364],{"class":46},[40,3247,367],{"class":58},[40,3249,370],{"class":54},[40,3251,3253,3255,3258,3260],{"class":42,"line":3252},27,[40,3254,376],{"class":46},[40,3256,3257],{"class":50}," target_rotation",[40,3259,62],{"class":54},[40,3261,3262],{"class":65}," 0.0\n",[40,3264,3266],{"class":42,"line":3265},28,[40,3267,327],{"emptyLinePlaceholder":326},[40,3269,3271,3273,3275,3277,3279,3281,3283],{"class":42,"line":3270},29,[40,3272,505],{"class":309},[40,3274,1990],{"class":50},[40,3276,400],{"class":54},[40,3278,1995],{"class":50},[40,3280,1185],{"class":46},[40,3282,645],{"class":65},[40,3284,370],{"class":393},[40,3286,3288],{"class":42,"line":3287},30,[40,3289,3290],{"class":319},"        # 往上飞 → 抬头\n",[40,3292,3294,3297,3299,3302,3304,3307,3310],{"class":42,"line":3293},31,[40,3295,3296],{"class":50},"        target_rotation",[40,3298,62],{"class":54},[40,3300,3301],{"class":357}," deg_to_rad",[40,3303,433],{"class":54},[40,3305,3306],{"class":46},"-",[40,3308,3309],{"class":65},"30",[40,3311,449],{"class":54},[40,3313,3315,3318],{"class":42,"line":3314},32,[40,3316,3317],{"class":309},"    else",[40,3319,370],{"class":393},[40,3321,3323],{"class":42,"line":3322},33,[40,3324,3325],{"class":319},"        # 往下掉 → 低头\n",[40,3327,3329,3331,3333,3335,3337,3340],{"class":42,"line":3328},34,[40,3330,3296],{"class":50},[40,3332,62],{"class":54},[40,3334,3301],{"class":357},[40,3336,433],{"class":54},[40,3338,3339],{"class":65},"90",[40,3341,449],{"class":54},[40,3343,3345],{"class":42,"line":3344},35,[40,3346,327],{"emptyLinePlaceholder":326},[40,3348,3350],{"class":42,"line":3349},36,[40,3351,3352],{"class":319},"    # lerp_angle：让旋转平滑过渡，不会一下子\"弹\"过去\n",[40,3354,3356,3359,3361,3364,3366,3369,3371,3373,3375,3377,3379,3381],{"class":42,"line":3355},37,[40,3357,3358],{"class":50},"    rotation",[40,3360,62],{"class":54},[40,3362,3363],{"class":357}," lerp_angle",[40,3365,433],{"class":54},[40,3367,3368],{"class":50},"rotation",[40,3370,439],{"class":54},[40,3372,3257],{"class":50},[40,3374,439],{"class":54},[40,3376,3033],{"class":50},[40,3378,1966],{"class":46},[40,3380,1969],{"class":50},[40,3382,449],{"class":54},[267,3384,3385],{},[10,3386,3387,3390,3391,3394],{},[37,3388,3389],{},"lerp_angle"," 的作用就像是给旋转加了\"缓动\"，从当前角度慢慢转到目标角度，而不是瞬间跳过去。",[37,3392,3393],{},"rotate_speed"," 越大转得越快，可以自己调到手感合适为止。",[22,3396,3398],{"id":3397},"再次测试","再次测试！",[10,3400,3401],{},"再次运行游戏，看看小鸟是不是有了那种「扑腾扑腾」的灵动感？是不是和原版《Flappy Bird》一个味儿了 ✨",[1444,3403,3404],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":35,"searchDepth":316,"depth":316,"links":3406},[3407,3408,3409,3410,3411,3412,3413,3414],{"id":2740,"depth":316,"text":2740},{"id":2779,"depth":316,"text":2779},{"id":2789,"depth":316,"text":2790},{"id":2804,"depth":316,"text":2805},{"id":2836,"depth":316,"text":2836},{"id":2914,"depth":316,"text":2914},{"id":2934,"depth":316,"text":2934},{"id":3397,"depth":316,"text":3398},{},"\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene",{"title":2732,"description":1474},"devlog\u002Fxggame-bird\u002F03-game-scene","8FuNcn0nabe5yIBK8x_umOGAktOPpJxM53aPIKn6ApQ",{"id":3421,"title":3422,"body":3423,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":4952,"navigation":326,"path":4953,"seo":4954,"stem":4955,"toc":326,"__hash__":4956},"devlog\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles.md","障碍物",{"type":7,"value":3424,"toc":4935},[3425,3428,3448,3466,3470,3473,3495,3501,3510,3513,3520,3539,3546,3551,3618,3624,3635,3641,3644,3650,3657,3663,3667,3673,3686,3758,3784,3906,3910,3917,3923,3927,3930,3950,3956,4188,4194,4198,4243,4250,4255,4261,4266,4539,4541,4552,4554,4578,4584,4587,4593,4604,4610,4620,4626,4629,4830,4833,4886,4901,4903,4906,4920,4926,4932],[10,3426,3427],{},"Flappy Bird 最经典的玩法 — 上下成对的水管，小鸟要从中间缝隙穿过去。这一章我们做：",[1373,3429,3430,3436,3442],{},[777,3431,3432,3435],{},[17,3433,3434],{},"障碍物本体"," — 上下两根水管 + 一个得分区",[777,3437,3438,3441],{},[17,3439,3440],{},"死区 Killzone"," — 撞上就死",[777,3443,3444,3447],{},[17,3445,3446],{},"障碍物生成器"," — 定时刷新、自动移动、屏幕外销毁",[267,3449,3450],{},[10,3451,3452,3453,3456,3457,3459,3460,3465],{},"这里用 ",[37,3454,3455],{},"Area2D","（死区）而不是 ",[37,3458,2852],{},"，因为 ",[17,3461,3462,3464],{},[37,3463,3455],{}," 不会真的\"卡住\"小鸟，只会触发信号","。这样我们就能在信号里写\"游戏结束\"的逻辑，而不是把小鸟物理地卡在水管里动弹不得。",[22,3467,3469],{"id":3468},"创建死区-killzone","创建死区 Killzone",[10,3471,3472],{},"先创建死区，使用到的节点：",[774,3474,3475],{},[777,3476,3477,3479,3480,3483,3484],{},[37,3478,3455],{}," — 死区根节点（重命名：",[37,3481,3482],{},"Killzone","）\n",[774,3485,3486],{},[777,3487,3488,3490,3491,3494],{},[37,3489,1739],{}," — 碰撞形状（",[17,3492,3493],{},"先留空","，下面会解释为什么）",[10,3496,3497],{},[1509,3498],{"alt":3499,"src":3500},"04-obstacles-死区","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E6%AD%BB%E5%8C%BA.png",[10,3502,3503,3505,3506,3509],{},[37,3504,1739],{}," 暂时",[17,3507,3508],{},"不要选形状","，因为后续这个死区既可以放到\"障碍物\"上，也可以放到\"地板\"上（小鸟没按飞掉地上也会 Game Over）。形状到时候根据位置再单独设置。",[22,3511,3512],{"id":3512},"障碍物的组成",[10,3514,3515,3516,3519],{},"每个障碍物由 ",[17,3517,3518],{},"3 个区域"," 组成：",[774,3521,3522,3528,3533],{},[777,3523,3524,3527],{},[17,3525,3526],{},"上水管","（Killzone — 死区）→ 撞到就死",[777,3529,3530,3527],{},[17,3531,3532],{},"下水管",[777,3534,3535,3538],{},[17,3536,3537],{},"中间的得分区","（Goal）→ 穿过加 1 分",[10,3540,3541,3542,3545],{},"这里我们同样先用 Godot 的默认素材（也就是默认的 ",[37,3543,3544],{},"icon.svg"," 来作为图片精灵图）",[267,3547,3548],{},[10,3549,3550],{},"等后续我们再一起更新美术资源素材！",[774,3552,3553],{},[777,3554,3555,3557,3558,3483,3561],{},[37,3556,2749],{},"（重命名：",[37,3559,3560],{},"PillarPair",[774,3562,3563,3583,3604],{},[777,3564,3565,3567,3568],{},[37,3566,3482],{},"（我们刚刚创建的死区）\n",[774,3569,3570,3578],{},[777,3571,3572,3574,3575,3577],{},[37,3573,1733],{}," — 放入 Godot 默认素材（",[37,3576,3544],{},"）",[777,3579,3580,3582],{},[37,3581,1739],{}," — 碰撞形状（矩形，调整到一根水管的大小）",[777,3584,3585,3588,3589,3591,3592],{},[37,3586,3587],{},"Killzone2","（复制 ",[37,3590,3482],{}," 一份，改名即可）\n",[774,3593,3594,3600],{},[777,3595,3596,3574,3598,3577],{},[37,3597,1733],{},[37,3599,3544],{},[777,3601,3602,3582],{},[37,3603,1739],{},[777,3605,3606,3557,3608,3483,3611],{},[37,3607,3455],{},[37,3609,3610],{},"Goal",[774,3612,3613],{},[777,3614,3615,3617],{},[37,3616,1739],{}," — 得分区域",[10,3619,3620],{},[1509,3621],{"alt":3622,"src":3623},"04-obstacles-阻碍物","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E9%98%BB%E7%A2%8D%E7%89%A9.png",[10,3625,3626,3627,3629,3630,99,3632,3634],{},"这里可以调整碰撞体的颜色，用红色来区分。\n然后在设置好一个 ",[37,3628,3482],{}," 里的 ",[37,3631,1733],{},[37,3633,1739],{}," 后可以复制",[10,3636,3637],{},[1509,3638],{"alt":3639,"src":3640},"04-obstacles-阻碍物2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E9%98%BB%E7%A2%8D%E7%89%A92.png",[10,3642,3643],{},"然后调整两个的位置，在中间空出一道间隙。",[10,3645,3646],{},[1509,3647],{"alt":3648,"src":3649},"04-obstacles-得分区域","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E5%BE%97%E5%88%86%E5%8C%BA%E5%9F%9F.png",[10,3651,3652,3653,3656],{},"然后把得分区域",[17,3654,3655],{},"放在中间偏后","的位置，这样只有完全通过的时候才算得分！",[10,3658,3659,3660],{},"障碍物设置好后，就可以保存该场景为：",[37,3661,3662],{},"scenes\u002Fpillar_pair.tscn",[687,3664,3666],{"id":3665},"给-killzone-挂脚本","给 Killzone 挂脚本",[10,3668,3669],{},[1509,3670],{"alt":3671,"src":3672},"04-obstacles-死区脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E6%AD%BB%E5%8C%BA%E8%84%9A%E6%9C%AC.png",[10,3674,1780,3675,3677,3678,3681,3682,3685],{},[37,3676,3455],{}," 节点（Killzone） → 右侧【节点】面板找到 ",[37,3679,3680],{},"body_entered"," 信号 → 双击 → 连接到自己。Godot 会自动生成 ",[37,3683,3684],{},"_on_body_entered"," 函数：",[30,3687,3689],{"className":32,"code":3688,"language":34,"meta":35,"style":35},"extends Area2D\n\nfunc _on_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\":\n        print(\"撞击死亡！\")\n",[37,3690,3691,3698,3702,3727,3746],{"__ignoreMap":35},[40,3692,3693,3695],{"class":42,"line":43},[40,3694,310],{"class":309},[40,3696,3697],{"class":58}," Area2D\n",[40,3699,3700],{"class":42,"line":316},[40,3701,327],{"emptyLinePlaceholder":326},[40,3703,3704,3706,3709,3711,3714,3716,3719,3721,3723,3725],{"class":42,"line":323},[40,3705,354],{"class":46},[40,3707,3708],{"class":357}," _on_body_entered",[40,3710,433],{"class":54},[40,3712,3713],{"class":393},"body",[40,3715,55],{"class":54},[40,3717,3718],{"class":58}," Node2D",[40,3720,522],{"class":54},[40,3722,364],{"class":46},[40,3724,367],{"class":58},[40,3726,370],{"class":54},[40,3728,3729,3731,3734,3736,3739,3741,3744],{"class":42,"line":330},[40,3730,505],{"class":309},[40,3732,3733],{"class":50}," body",[40,3735,400],{"class":54},[40,3737,3738],{"class":50},"name",[40,3740,603],{"class":46},[40,3742,3743],{"class":342}," \"Bird\"",[40,3745,370],{"class":393},[40,3747,3748,3751,3753,3756],{"class":42,"line":346},[40,3749,3750],{"class":357},"        print",[40,3752,433],{"class":54},[40,3754,3755],{"class":342},"\"撞击死亡！\"",[40,3757,449],{"class":54},[774,3759,3760,3768,3774],{},[777,3761,3762,299,3764,3767],{},[37,3763,3680],{},[17,3765,3766],{},"任何物理体进入这个区域","时触发的信号",[777,3769,3770,3773],{},[37,3771,3772],{},"if body.name == \"Bird\":"," — 判断进来的是不是小鸟（防止其他东西误触发）",[777,3775,3776,3777,3780,3781,3783],{},"暂时用 ",[37,3778,3779],{},"print"," 打印日志，下一章做完 ",[37,3782,397],{}," 后再换成真正的\"游戏结束\"逻辑",[267,3785,3786,3793,3893],{},[10,3787,3788,3789,3792],{},"💡 ",[17,3790,3791],{},"后续优化预览","（下一章 GameManager + 第 8 章 AudioManager 后会改成这样，现在不要这么写）：",[30,3794,3796],{"className":32,"code":3795,"language":34,"meta":35,"style":35},"extends Area2D\n\nfunc _on_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\" and GameManager.current_state == GameManager.GameState.PLAYING:\n        AudioManager.play_die()\n        GameManager.game_over()\n",[37,3797,3798,3804,3808,3830,3870,3882],{"__ignoreMap":35},[40,3799,3800,3802],{"class":42,"line":43},[40,3801,310],{"class":309},[40,3803,3697],{"class":58},[40,3805,3806],{"class":42,"line":316},[40,3807,327],{"emptyLinePlaceholder":326},[40,3809,3810,3812,3814,3816,3818,3820,3822,3824,3826,3828],{"class":42,"line":323},[40,3811,354],{"class":46},[40,3813,3708],{"class":357},[40,3815,433],{"class":54},[40,3817,3713],{"class":393},[40,3819,55],{"class":54},[40,3821,3718],{"class":58},[40,3823,522],{"class":54},[40,3825,364],{"class":46},[40,3827,367],{"class":58},[40,3829,370],{"class":54},[40,3831,3832,3834,3836,3838,3840,3842,3844,3847,3850,3852,3855,3857,3859,3861,3863,3865,3868],{"class":42,"line":330},[40,3833,505],{"class":309},[40,3835,3733],{"class":50},[40,3837,400],{"class":54},[40,3839,3738],{"class":50},[40,3841,603],{"class":46},[40,3843,3743],{"class":342},[40,3845,3846],{"class":46}," and",[40,3848,3849],{"class":58}," GameManager",[40,3851,400],{"class":54},[40,3853,3854],{"class":50},"current_state",[40,3856,603],{"class":46},[40,3858,3849],{"class":58},[40,3860,400],{"class":54},[40,3862,1169],{"class":50},[40,3864,400],{"class":54},[40,3866,3867],{"class":309},"PLAYING",[40,3869,370],{"class":393},[40,3871,3872,3875,3877,3880],{"class":42,"line":346},[40,3873,3874],{"class":58},"        AudioManager",[40,3876,400],{"class":393},[40,3878,3879],{"class":357},"play_die",[40,3881,2070],{"class":54},[40,3883,3884,3887,3889,3891],{"class":42,"line":351},[40,3885,3886],{"class":58},"        GameManager",[40,3888,400],{"class":393},[40,3890,1138],{"class":357},[40,3892,2070],{"class":54},[774,3894,3895,3900],{},[777,3896,3897,3899],{},[37,3898,397],{}," 是整个游戏的整体管理",[777,3901,3902,3905],{},[37,3903,3904],{},"AudioManager"," 是整个游戏的音效管理",[687,3907,3909],{"id":3908},"地板也可以变成-killzone","地板也可以变成 Killzone",[10,3911,3912,3913,3916],{},"之前第 3 章做的底部边界，",[17,3914,3915],{},"也可以变成死区"," — 小鸟掉到地上就算游戏结束。",[10,3918,3919],{},[1509,3920],{"alt":3921,"src":3922},"04-obstacles-地板死区","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E5%9C%B0%E6%9D%BF%E6%AD%BB%E5%8C%BA.png",[22,3924,3926],{"id":3925},"障碍物移动-销毁-得分","障碍物移动 + 销毁 + 得分",[10,3928,3929],{},"障碍物根节点要做三件事：",[1373,3931,3932,3938,3944],{},[777,3933,3934,3937],{},[17,3935,3936],{},"往左移动"," — 营造小鸟在向右飞的错觉",[777,3939,3940,3943],{},[17,3941,3942],{},"离开屏幕后销毁"," — 不然会一直累积，性能爆炸",[777,3945,3946,3949],{},[17,3947,3948],{},"检测小鸟穿过"," — 加分",[10,3951,3952,3953,3955],{},"这些都写在障碍物根节点（",[37,3954,2749],{},"）的脚本里：",[30,3957,3959],{"className":32,"code":3958,"language":34,"meta":35,"style":35},"extends Node2D\n\n@export var speed := 200.0\n@onready var goal: Area2D = $Goal\n\nfunc _physics_process(delta: float) -> void:\n    # 统一向左移动\n    position.x -= speed * delta\n    # 离开屏幕后自动销毁\n    if position.x \u003C -500:\n        queue_free()\n\n# 当物体进入得分区域【得分】\nfunc _on_goal_body_entered(body: Node2D) -> void:\n    # 检查进入的是不是小鸟（防止其他东西误触发）\n    if body.name == \"Bird\":\n        print(\"得分！\")\n        # 重要：得分后立即禁用这个检测区域，防止重复得分\n        # 使用 set_deferred 是因为在碰撞回调中不能直接修改物理属性\n        goal.set_deferred(\"monitoring\", false)\n",[37,3960,3961,3968,3972,3986,4010,4014,4036,4041,4060,4065,4085,4092,4096,4101,4124,4129,4145,4156,4161,4166],{"__ignoreMap":35},[40,3962,3963,3965],{"class":42,"line":43},[40,3964,310],{"class":309},[40,3966,3967],{"class":58}," Node2D\n",[40,3969,3970],{"class":42,"line":316},[40,3971,327],{"emptyLinePlaceholder":326},[40,3973,3974,3976,3978,3981,3983],{"class":42,"line":323},[40,3975,1852],{"class":357},[40,3977,1855],{"class":46},[40,3979,3980],{"class":50}," speed",[40,3982,339],{"class":54},[40,3984,3985],{"class":65}," 200.0\n",[40,3987,3988,3991,3993,3996,3998,4001,4003,4006],{"class":42,"line":330},[40,3989,3990],{"class":357},"@onready",[40,3992,1855],{"class":46},[40,3994,3995],{"class":50}," goal",[40,3997,55],{"class":54},[40,3999,4000],{"class":58}," Area2D",[40,4002,62],{"class":54},[40,4004,4005],{"class":309}," $",[40,4007,4009],{"class":4008},"sfsYZ","Goal\n",[40,4011,4012],{"class":42,"line":346},[40,4013,327],{"emptyLinePlaceholder":326},[40,4015,4016,4018,4020,4022,4024,4026,4028,4030,4032,4034],{"class":42,"line":351},[40,4017,354],{"class":46},[40,4019,1914],{"class":357},[40,4021,433],{"class":54},[40,4023,1919],{"class":393},[40,4025,55],{"class":54},[40,4027,1924],{"class":58},[40,4029,522],{"class":54},[40,4031,364],{"class":46},[40,4033,367],{"class":58},[40,4035,370],{"class":54},[40,4037,4038],{"class":42,"line":373},[40,4039,4040],{"class":319},"    # 统一向左移动\n",[40,4042,4043,4046,4048,4050,4053,4055,4057],{"class":42,"line":387},[40,4044,4045],{"class":50},"    position",[40,4047,400],{"class":54},[40,4049,2455],{"class":50},[40,4051,4052],{"class":46}," -=",[40,4054,3980],{"class":50},[40,4056,1966],{"class":46},[40,4058,4059],{"class":50}," delta\n",[40,4061,4062],{"class":42,"line":409},[40,4063,4064],{"class":319},"    # 离开屏幕后自动销毁\n",[40,4066,4067,4069,4072,4074,4076,4078,4080,4083],{"class":42,"line":415},[40,4068,505],{"class":309},[40,4070,4071],{"class":50}," position",[40,4073,400],{"class":54},[40,4075,2455],{"class":50},[40,4077,1185],{"class":46},[40,4079,1880],{"class":46},[40,4081,4082],{"class":65},"500",[40,4084,370],{"class":393},[40,4086,4087,4090],{"class":42,"line":452},[40,4088,4089],{"class":357},"        queue_free",[40,4091,2070],{"class":54},[40,4093,4094],{"class":42,"line":481},[40,4095,327],{"emptyLinePlaceholder":326},[40,4097,4098],{"class":42,"line":486},[40,4099,4100],{"class":319},"# 当物体进入得分区域【得分】\n",[40,4102,4103,4105,4108,4110,4112,4114,4116,4118,4120,4122],{"class":42,"line":502},[40,4104,354],{"class":46},[40,4106,4107],{"class":357}," _on_goal_body_entered",[40,4109,433],{"class":54},[40,4111,3713],{"class":393},[40,4113,55],{"class":54},[40,4115,3718],{"class":58},[40,4117,522],{"class":54},[40,4119,364],{"class":46},[40,4121,367],{"class":58},[40,4123,370],{"class":54},[40,4125,4126],{"class":42,"line":527},[40,4127,4128],{"class":319},"    # 检查进入的是不是小鸟（防止其他东西误触发）\n",[40,4130,4131,4133,4135,4137,4139,4141,4143],{"class":42,"line":536},[40,4132,505],{"class":309},[40,4134,3733],{"class":50},[40,4136,400],{"class":54},[40,4138,3738],{"class":50},[40,4140,603],{"class":46},[40,4142,3743],{"class":342},[40,4144,370],{"class":393},[40,4146,4147,4149,4151,4154],{"class":42,"line":566},[40,4148,3750],{"class":357},[40,4150,433],{"class":54},[40,4152,4153],{"class":342},"\"得分！\"",[40,4155,449],{"class":54},[40,4157,4158],{"class":42,"line":596},[40,4159,4160],{"class":319},"        # 重要：得分后立即禁用这个检测区域，防止重复得分\n",[40,4162,4163],{"class":42,"line":611},[40,4164,4165],{"class":319},"        # 使用 set_deferred 是因为在碰撞回调中不能直接修改物理属性\n",[40,4167,4168,4171,4173,4176,4178,4181,4183,4186],{"class":42,"line":619},[40,4169,4170],{"class":50},"        goal",[40,4172,400],{"class":393},[40,4174,4175],{"class":357},"set_deferred",[40,4177,433],{"class":54},[40,4179,4180],{"class":342},"\"monitoring\"",[40,4182,439],{"class":54},[40,4184,4185],{"class":309}," false",[40,4187,449],{"class":54},[10,4189,4190],{},[1509,4191],{"alt":4192,"src":4193},"04-obstacles-障碍物脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E9%9A%9C%E7%A2%8D%E7%89%A9%E8%84%9A%E6%9C%AC.png",[687,4195,4197],{"id":4196},"这段代码做了啥","这段代码做了啥？",[774,4199,4200,4206,4215,4221,4231],{},[777,4201,4202,4205],{},[37,4203,4204],{},"@export var speed := 200.0"," — 把移动速度暴露到面板，方便实时调",[777,4207,4208,4211,4212,4214],{},[37,4209,4210],{},"@onready var goal: Area2D = $Goal"," — 拿到子节点 ",[37,4213,3610],{}," 的引用（用于得分后禁用它）",[777,4216,4217,4220],{},[37,4218,4219],{},"position.x -= speed * delta"," — 每帧整个障碍物往左移一点",[777,4222,4223,4226,4227,4230],{},[37,4224,4225],{},"if position.x \u003C -500: queue_free()"," — 飞出屏幕外就",[17,4228,4229],{},"销毁自己","，回收内存",[777,4232,4233,4236,4237,4239,4240,3577],{},[37,4234,4235],{},"_on_goal_body_entered"," — 小鸟进入 ",[37,4238,3610],{}," 时触发（同样要在编辑器里",[17,4241,4242],{},"连接信号",[687,4244,4246,4247,863],{"id":4245},"为什么要-set_deferredmonitoring-false","为什么要 ",[37,4248,4249],{},"set_deferred(\"monitoring\", false)",[10,4251,4252,20],{},[17,4253,4254],{},"为了防止重复得分",[10,4256,4257,4258,4260],{},"如果不禁用，小鸟身上的碰撞体可能在 Goal 区域里横跨多帧，每帧都触发一次 ",[37,4259,3680],{},"，你就从 1 分跳到 5 分了 😅",[10,4262,4263,4265],{},[37,4264,4175],{}," 是因为在物理回调里不能直接修改物理属性，要\"延迟到下一帧\"再改。",[267,4267,4268,4273,4518,4521],{},[10,4269,3788,4270,4272],{},[17,4271,3791],{},"（下一章 GameManager + 第 8 章 AudioManager 后会改成这样）：",[30,4274,4276],{"className":32,"code":4275,"language":34,"meta":35,"style":35},"extends Node2D\n\n@export var speed := 200.0\n@onready var goal: Area2D = $Goal\n\nfunc _physics_process(delta: float) -> void:\n    if GameManager.current_state == GameManager.GameState.PLAYING:\n        position.x -= speed * delta\n        if position.x \u003C -500:\n            queue_free()\n\nfunc _on_goal_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\":\n        AudioManager.play_score()\n        if GameManager.has_method(\"add_score\"):\n            GameManager.add_score(1)\n        goal.set_deferred(\"monitoring\", false)\n",[37,4277,4278,4284,4288,4300,4318,4322,4344,4368,4385,4404,4411,4415,4437,4453,4464,4484,4500],{"__ignoreMap":35},[40,4279,4280,4282],{"class":42,"line":43},[40,4281,310],{"class":309},[40,4283,3967],{"class":58},[40,4285,4286],{"class":42,"line":316},[40,4287,327],{"emptyLinePlaceholder":326},[40,4289,4290,4292,4294,4296,4298],{"class":42,"line":323},[40,4291,1852],{"class":357},[40,4293,1855],{"class":46},[40,4295,3980],{"class":50},[40,4297,339],{"class":54},[40,4299,3985],{"class":65},[40,4301,4302,4304,4306,4308,4310,4312,4314,4316],{"class":42,"line":330},[40,4303,3990],{"class":357},[40,4305,1855],{"class":46},[40,4307,3995],{"class":50},[40,4309,55],{"class":54},[40,4311,4000],{"class":58},[40,4313,62],{"class":54},[40,4315,4005],{"class":309},[40,4317,4009],{"class":4008},[40,4319,4320],{"class":42,"line":346},[40,4321,327],{"emptyLinePlaceholder":326},[40,4323,4324,4326,4328,4330,4332,4334,4336,4338,4340,4342],{"class":42,"line":351},[40,4325,354],{"class":46},[40,4327,1914],{"class":357},[40,4329,433],{"class":54},[40,4331,1919],{"class":393},[40,4333,55],{"class":54},[40,4335,1924],{"class":58},[40,4337,522],{"class":54},[40,4339,364],{"class":46},[40,4341,367],{"class":58},[40,4343,370],{"class":54},[40,4345,4346,4348,4350,4352,4354,4356,4358,4360,4362,4364,4366],{"class":42,"line":373},[40,4347,505],{"class":309},[40,4349,3849],{"class":58},[40,4351,400],{"class":54},[40,4353,3854],{"class":50},[40,4355,603],{"class":46},[40,4357,3849],{"class":58},[40,4359,400],{"class":54},[40,4361,1169],{"class":50},[40,4363,400],{"class":54},[40,4365,3867],{"class":309},[40,4367,370],{"class":393},[40,4369,4370,4373,4375,4377,4379,4381,4383],{"class":42,"line":387},[40,4371,4372],{"class":50},"        position",[40,4374,400],{"class":54},[40,4376,2455],{"class":50},[40,4378,4052],{"class":46},[40,4380,3980],{"class":50},[40,4382,1966],{"class":46},[40,4384,4059],{"class":50},[40,4386,4387,4390,4392,4394,4396,4398,4400,4402],{"class":42,"line":409},[40,4388,4389],{"class":309},"        if",[40,4391,4071],{"class":50},[40,4393,400],{"class":54},[40,4395,2455],{"class":50},[40,4397,1185],{"class":46},[40,4399,1880],{"class":46},[40,4401,4082],{"class":65},[40,4403,370],{"class":393},[40,4405,4406,4409],{"class":42,"line":415},[40,4407,4408],{"class":357},"            queue_free",[40,4410,2070],{"class":54},[40,4412,4413],{"class":42,"line":452},[40,4414,327],{"emptyLinePlaceholder":326},[40,4416,4417,4419,4421,4423,4425,4427,4429,4431,4433,4435],{"class":42,"line":481},[40,4418,354],{"class":46},[40,4420,4107],{"class":357},[40,4422,433],{"class":54},[40,4424,3713],{"class":393},[40,4426,55],{"class":54},[40,4428,3718],{"class":58},[40,4430,522],{"class":54},[40,4432,364],{"class":46},[40,4434,367],{"class":58},[40,4436,370],{"class":54},[40,4438,4439,4441,4443,4445,4447,4449,4451],{"class":42,"line":486},[40,4440,505],{"class":309},[40,4442,3733],{"class":50},[40,4444,400],{"class":54},[40,4446,3738],{"class":50},[40,4448,603],{"class":46},[40,4450,3743],{"class":342},[40,4452,370],{"class":393},[40,4454,4455,4457,4459,4462],{"class":42,"line":502},[40,4456,3874],{"class":58},[40,4458,400],{"class":393},[40,4460,4461],{"class":357},"play_score",[40,4463,2070],{"class":54},[40,4465,4466,4468,4470,4472,4475,4477,4480,4482],{"class":42,"line":527},[40,4467,4389],{"class":309},[40,4469,3849],{"class":58},[40,4471,400],{"class":393},[40,4473,4474],{"class":357},"has_method",[40,4476,433],{"class":54},[40,4478,4479],{"class":342},"\"add_score\"",[40,4481,522],{"class":54},[40,4483,370],{"class":393},[40,4485,4486,4489,4491,4493,4495,4498],{"class":42,"line":536},[40,4487,4488],{"class":58},"            GameManager",[40,4490,400],{"class":393},[40,4492,1118],{"class":357},[40,4494,433],{"class":54},[40,4496,4497],{"class":65},"1",[40,4499,449],{"class":54},[40,4501,4502,4504,4506,4508,4510,4512,4514,4516],{"class":42,"line":566},[40,4503,4170],{"class":50},[40,4505,400],{"class":393},[40,4507,4175],{"class":357},[40,4509,433],{"class":54},[40,4511,4180],{"class":342},[40,4513,439],{"class":54},[40,4515,4185],{"class":309},[40,4517,449],{"class":54},[10,4519,4520],{},"升级点：",[774,4522,4523,4530],{},[777,4524,4525,4526,4529],{},"加了 ",[37,4527,4528],{},"if GameManager.current_state == PLAYING"," — 游戏暂停\u002F结束时不再移动",[777,4531,4532,4533,99,4536],{},"得分时调用 ",[37,4534,4535],{},"GameManager.add_score(1)",[37,4537,4538],{},"AudioManager.play_score()",[22,4540,3446],{"id":3446},[10,4542,4543,4544,4547,4548,4551],{},"光有一根水管不够，需要",[17,4545,4546],{},"源源不断的水管","。用 ",[37,4549,4550],{},"Timer"," 节点定时生成：",[10,4553,2761],{},[774,4555,4556],{},[777,4557,4558,4560,4561,3483,4564],{},[37,4559,2749],{}," — 生成器根节点（重命名：",[37,4562,4563],{},"PillarSpawner",[774,4565,4566],{},[777,4567,4568,4570,4571,4574,4575,3577],{},[37,4569,4550],{}," — 定时器（",[37,4572,4573],{},"Wait Time = 2.5s","，勾选 ",[37,4576,4577],{},"Autostart",[10,4579,4580],{},[1509,4581],{"alt":4582,"src":4583},"04-obstacles-生成器节点","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E7%94%9F%E6%88%90%E5%99%A8%E8%8A%82%E7%82%B9.png",[10,4585,4586],{},"设置 Timer 节点的属性：",[10,4588,4589],{},[1509,4590],{"alt":4591,"src":4592},"04-obstacles-生成器节点2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E7%94%9F%E6%88%90%E5%99%A8%E8%8A%82%E7%82%B92.png",[10,4594,4595,4596,4599,4600,4603],{},"把 ",[37,4597,4598],{},"pillar_pair.tscn"," 拖到 ",[37,4601,4602],{},"Pillar Scene"," 槽里：",[10,4605,4606],{},[1509,4607],{"alt":4608,"src":4609},"04-obstacles-生成器节点3","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E7%94%9F%E6%88%90%E5%99%A8%E8%8A%82%E7%82%B93.png",[10,4611,4612,4613,4615,4616,4619],{},"最后，把 ",[37,4614,4563],{}," 整个节点",[17,4617,4618],{},"拖到游戏窗口右边外面","（这样水管才会从屏幕右侧\"飞\"进来）：",[10,4621,4622],{},[1509,4623],{"alt":4624,"src":4625},"04-obstacles-生成器节点4","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E7%94%9F%E6%88%90%E5%99%A8%E8%8A%82%E7%82%B94.png",[687,4627,4628],{"id":4628},"生成器脚本",[30,4630,4632],{"className":32,"code":4631,"language":34,"meta":35,"style":35},"extends Node2D\n\n@export var pillar_scene: PackedScene\n@export var y_range := 250.0\n\nfunc _on_timer_timeout() -> void:\n    spawn_pillar()\n\nfunc spawn_pillar() -> void:\n    var new_pillar = pillar_scene.instantiate()\n\n    var spawn_pos = global_position\n    spawn_pos.y += randf_range(-y_range, y_range)\n\n    new_pillar.global_position = spawn_pos\n\n    # 重点：加到主场景或者专门的容器里，防止柱子跟着生成器动\n    get_tree().current_scene.add_child(new_pillar)\n",[37,4633,4634,4640,4644,4658,4672,4676,4691,4698,4702,4717,4735,4739,4751,4778,4782,4797,4801,4806],{"__ignoreMap":35},[40,4635,4636,4638],{"class":42,"line":43},[40,4637,310],{"class":309},[40,4639,3967],{"class":58},[40,4641,4642],{"class":42,"line":316},[40,4643,327],{"emptyLinePlaceholder":326},[40,4645,4646,4648,4650,4653,4655],{"class":42,"line":323},[40,4647,1852],{"class":357},[40,4649,1855],{"class":46},[40,4651,4652],{"class":50}," pillar_scene",[40,4654,55],{"class":54},[40,4656,4657],{"class":58}," PackedScene\n",[40,4659,4660,4662,4664,4667,4669],{"class":42,"line":330},[40,4661,1852],{"class":357},[40,4663,1855],{"class":46},[40,4665,4666],{"class":50}," y_range",[40,4668,339],{"class":54},[40,4670,4671],{"class":65}," 250.0\n",[40,4673,4674],{"class":42,"line":346},[40,4675,327],{"emptyLinePlaceholder":326},[40,4677,4678,4680,4683,4685,4687,4689],{"class":42,"line":351},[40,4679,354],{"class":46},[40,4681,4682],{"class":357}," _on_timer_timeout",[40,4684,361],{"class":54},[40,4686,364],{"class":46},[40,4688,367],{"class":58},[40,4690,370],{"class":54},[40,4692,4693,4696],{"class":42,"line":373},[40,4694,4695],{"class":357},"    spawn_pillar",[40,4697,2070],{"class":54},[40,4699,4700],{"class":42,"line":387},[40,4701,327],{"emptyLinePlaceholder":326},[40,4703,4704,4706,4709,4711,4713,4715],{"class":42,"line":409},[40,4705,354],{"class":46},[40,4707,4708],{"class":357}," spawn_pillar",[40,4710,361],{"class":54},[40,4712,364],{"class":46},[40,4714,367],{"class":58},[40,4716,370],{"class":54},[40,4718,4719,4721,4724,4726,4728,4730,4733],{"class":42,"line":415},[40,4720,376],{"class":46},[40,4722,4723],{"class":50}," new_pillar",[40,4725,62],{"class":54},[40,4727,4652],{"class":50},[40,4729,400],{"class":393},[40,4731,4732],{"class":357},"instantiate",[40,4734,2070],{"class":54},[40,4736,4737],{"class":42,"line":452},[40,4738,327],{"emptyLinePlaceholder":326},[40,4740,4741,4743,4746,4748],{"class":42,"line":481},[40,4742,376],{"class":46},[40,4744,4745],{"class":50}," spawn_pos",[40,4747,62],{"class":54},[40,4749,4750],{"class":50}," global_position\n",[40,4752,4753,4756,4758,4760,4762,4765,4767,4769,4772,4774,4776],{"class":42,"line":486},[40,4754,4755],{"class":50},"    spawn_pos",[40,4757,400],{"class":54},[40,4759,1995],{"class":50},[40,4761,1958],{"class":46},[40,4763,4764],{"class":357}," randf_range",[40,4766,433],{"class":54},[40,4768,3306],{"class":46},[40,4770,4771],{"class":50},"y_range",[40,4773,439],{"class":54},[40,4775,4666],{"class":50},[40,4777,449],{"class":54},[40,4779,4780],{"class":42,"line":502},[40,4781,327],{"emptyLinePlaceholder":326},[40,4783,4784,4787,4789,4792,4794],{"class":42,"line":527},[40,4785,4786],{"class":50},"    new_pillar",[40,4788,400],{"class":54},[40,4790,4791],{"class":50},"global_position",[40,4793,62],{"class":54},[40,4795,4796],{"class":50}," spawn_pos\n",[40,4798,4799],{"class":42,"line":536},[40,4800,327],{"emptyLinePlaceholder":326},[40,4802,4803],{"class":42,"line":566},[40,4804,4805],{"class":319},"    # 重点：加到主场景或者专门的容器里，防止柱子跟着生成器动\n",[40,4807,4808,4811,4813,4815,4818,4820,4823,4825,4828],{"class":42,"line":596},[40,4809,4810],{"class":357},"    get_tree",[40,4812,361],{"class":54},[40,4814,400],{"class":393},[40,4816,4817],{"class":50},"current_scene",[40,4819,400],{"class":393},[40,4821,4822],{"class":357},"add_child",[40,4824,433],{"class":54},[40,4826,4827],{"class":50},"new_pillar",[40,4829,449],{"class":54},[687,4831,4832],{"id":4832},"关键点",[774,4834,4835,4848,4854,4862,4868,4874],{},[777,4836,4837,4840,4841],{},[37,4838,4839],{},"@export var pillar_scene: PackedScene"," — 把\"水管场景\"暴露到面板，",[17,4842,4843,4844,4847],{},"你需要在面板上把 ",[37,4845,4846],{},"pillar.tscn"," 拖进这个槽里",[777,4849,4850,4853],{},[37,4851,4852],{},"@export var y_range := 250.0"," — 水管在垂直方向上的随机偏移范围",[777,4855,4856,4859,4860,3577],{},[37,4857,4858],{},"_on_timer_timeout"," — 每次 Timer 触发就生成新的（同样要",[17,4861,4242],{},[777,4863,4864,4867],{},[37,4865,4866],{},"pillar_scene.instantiate()"," — 把场景\"实例化\"成一个真正的节点",[777,4869,4870,4873],{},[37,4871,4872],{},"randf_range(-y_range, y_range)"," — 随机一个垂直偏移",[777,4875,4876,4881,4882,4885],{},[17,4877,4878],{},[37,4879,4880],{},"get_tree().current_scene.add_child(new_pillar)"," ⚠️ 重点！把水管加到",[17,4883,4884],{},"主场景","而不是生成器自己。否则如果生成器以后会移动，水管会跟着一起动，整个画面就乱套了。",[267,4887,4888],{},[10,4889,3788,4890,4892,4893,4896,4897,4900],{},[17,4891,3791],{},"（下一章 GameManager 之后）：在 ",[37,4894,4895],{},"spawn_pillar"," 最外层包一个 ",[37,4898,4899],{},"if GameManager.current_state == GameManager.GameState.PLAYING:","，这样游戏结束后就不会继续生成水管了。",[22,4902,2914],{"id":2914},[10,4904,4905],{},"应该能看到：",[774,4907,4908,4911,4914,4917],{},[777,4909,4910],{},"✅ 水管从右往左移动",[777,4912,4913],{},"✅ 每 2.5 秒生成一根，高度随机",[777,4915,4916],{},"✅ 小鸟穿过会在控制台打印「得分！」",[777,4918,4919],{},"✅ 小鸟撞到水管或地面会在控制台打印「撞击死亡！」",[10,4921,4922],{},[1509,4923],{"alt":4924,"src":4925},"04-obstacles-尝试运行","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E5%B0%9D%E8%AF%95%E8%BF%90%E8%A1%8C.png",[10,4927,4928,4929,4931],{},"下一章我们来做 ",[17,4930,397],{},"，把\"撞击死亡\"真正变成\"游戏结束 + 显示分数 + 重新开始\"。",[1444,4933,4934],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}",{"title":35,"searchDepth":316,"depth":316,"links":4936},[4937,4938,4942,4947,4951],{"id":3468,"depth":316,"text":3469},{"id":3512,"depth":316,"text":3512,"children":4939},[4940,4941],{"id":3665,"depth":323,"text":3666},{"id":3908,"depth":323,"text":3909},{"id":3925,"depth":316,"text":3926,"children":4943},[4944,4945],{"id":4196,"depth":323,"text":4197},{"id":4245,"depth":323,"text":4946},"为什么要 set_deferred(\"monitoring\", false)？",{"id":3446,"depth":316,"text":3446,"children":4948},[4949,4950],{"id":4628,"depth":323,"text":4628},{"id":4832,"depth":323,"text":4832},{"id":2914,"depth":316,"text":2914},{},"\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles",{"title":3422,"description":1474},"devlog\u002Fxggame-bird\u002F04-obstacles","Vgp6pvSrVWBZGSu3mogKB_sUW9o-ioJjpVTu6EKg0-s",{"id":4958,"title":397,"body":4959,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":6409,"navigation":326,"path":6410,"seo":6411,"stem":6412,"toc":326,"__hash__":6413},"devlog\u002Fdevlog\u002Fxggame-bird\u002F05-game-manager.md",{"type":7,"value":4960,"toc":6384},[4961,4974,4978,4981,4999,5002,5009,5013,5018,5356,5360,5370,5385,5388,5394,5400,5425,5432,5460,5474,5476,5482,5507,5527,5574,5580,5609,5616,5632,5638,5647,5694,5711,5720,5734,5739,5743,5750,5757,5840,5847,5854,6086,6093,6289,6291,6297,6303,6372,6381],[10,4962,4963,4964,4967,4968,4971,4972,20],{},"游戏到这一步已经能玩了，但缺少",[17,4965,4966],{},"游戏状态管理","和",[17,4969,4970],{},"计分","。我们需要一个统一的管理者来掌控全局 — 这就是 ",[17,4973,397],{},[22,4975,4977],{"id":4976},"为什么需要-gamemanager","为什么需要 GameManager？",[10,4979,4980],{},"想象一下，游戏有 3 种状态：",[774,4982,4983,4989,4994],{},[777,4984,4985,4988],{},[17,4986,4987],{},"READY","（准备）— 显示\"点击开始\"",[777,4990,4991,4993],{},[17,4992,3867],{},"（进行中）— 小鸟能跳、水管在生成",[777,4995,4996,4998],{},[17,4997,1174],{},"（结束）— 显示分数、可以重开",[10,5000,5001],{},"如果让每个节点自己判断状态，比如小鸟自己看\"现在能不能跳\"、水管自己看\"现在能不能生成\"，代码会乱成一团 — 改一个地方要同步改很多处。",[10,5003,5004,5005,5008],{},"GameManager 就是",[17,5006,5007],{},"所有节点共同遵守的\"裁判\""," — 状态、分数都由它统一保管，其他节点只问它要、不自己存。",[22,5010,5012],{"id":5011},"创建-gamemanager-脚本","创建 GameManager 脚本",[10,5014,295,5015,299],{},[37,5016,5017],{},"scripts\u002Fautoload\u002Fgame_manager.gd",[30,5019,5021],{"className":32,"code":5020,"language":34,"meta":35,"style":35},"extends Node\n\n# 定义游戏状态\nenum GameState { READY, PLAYING, GAME_OVER }\nvar current_state = GameState.READY\n\nvar total_score: int = 0   # 总分\nvar high_score: int = 0    # 最高分\n\n# 信号：通知其他节点\nsignal score_changed(new_score)\nsignal state_changed(new_state)\n\n## 清空分数\nfunc clear_score():\n    total_score = 0\n    score_changed.emit(total_score)\n\n## 添加得分\nfunc add_score(amount: int = 1):\n    total_score += amount\n    score_changed.emit(total_score)\n\n## 设置当前游戏状态\nfunc set_state(new_state: GameState):\n    current_state = new_state\n    state_changed.emit(new_state)\n\n## 游戏结束\nfunc game_over():\n    set_state(GameState.GAME_OVER)\n    if high_score \u003C total_score:\n        high_score = total_score\n",[37,5022,5023,5029,5033,5038,5063,5079,5083,5100,5117,5121,5126,5141,5155,5159,5164,5173,5182,5199,5203,5208,5232,5241,5255,5259,5264,5281,5291,5306,5310,5314,5322,5336,5348],{"__ignoreMap":35},[40,5024,5025,5027],{"class":42,"line":43},[40,5026,310],{"class":309},[40,5028,313],{"class":58},[40,5030,5031],{"class":42,"line":316},[40,5032,327],{"emptyLinePlaceholder":326},[40,5034,5035],{"class":42,"line":323},[40,5036,5037],{"class":319},"# 定义游戏状态\n",[40,5039,5040,5043,5046,5048,5051,5054,5056,5058,5060],{"class":42,"line":330},[40,5041,5042],{"class":309},"enum",[40,5044,5045],{"class":58}," GameState",[40,5047,733],{"class":54},[40,5049,5050],{"class":50}," READY",[40,5052,5053],{"class":393},", ",[40,5055,3867],{"class":50},[40,5057,5053],{"class":393},[40,5059,1174],{"class":50},[40,5061,5062],{"class":54}," }\n",[40,5064,5065,5067,5070,5072,5074,5076],{"class":42,"line":346},[40,5066,47],{"class":46},[40,5068,5069],{"class":50}," current_state",[40,5071,62],{"class":54},[40,5073,5045],{"class":58},[40,5075,400],{"class":54},[40,5077,5078],{"class":309},"READY\n",[40,5080,5081],{"class":42,"line":351},[40,5082,327],{"emptyLinePlaceholder":326},[40,5084,5085,5087,5089,5091,5093,5095,5097],{"class":42,"line":373},[40,5086,47],{"class":46},[40,5088,1188],{"class":50},[40,5090,55],{"class":54},[40,5092,59],{"class":58},[40,5094,62],{"class":54},[40,5096,645],{"class":65},[40,5098,5099],{"class":319},"   # 总分\n",[40,5101,5102,5104,5106,5108,5110,5112,5114],{"class":42,"line":387},[40,5103,47],{"class":46},[40,5105,51],{"class":50},[40,5107,55],{"class":54},[40,5109,59],{"class":58},[40,5111,62],{"class":54},[40,5113,645],{"class":65},[40,5115,5116],{"class":319},"    # 最高分\n",[40,5118,5119],{"class":42,"line":409},[40,5120,327],{"emptyLinePlaceholder":326},[40,5122,5123],{"class":42,"line":415},[40,5124,5125],{"class":319},"# 信号：通知其他节点\n",[40,5127,5128,5131,5134,5136,5139],{"class":42,"line":452},[40,5129,5130],{"class":46},"signal",[40,5132,5133],{"class":357}," score_changed",[40,5135,433],{"class":54},[40,5137,5138],{"class":393},"new_score",[40,5140,449],{"class":54},[40,5142,5143,5145,5148,5150,5153],{"class":42,"line":481},[40,5144,5130],{"class":46},[40,5146,5147],{"class":357}," state_changed",[40,5149,433],{"class":54},[40,5151,5152],{"class":393},"new_state",[40,5154,449],{"class":54},[40,5156,5157],{"class":42,"line":486},[40,5158,327],{"emptyLinePlaceholder":326},[40,5160,5161],{"class":42,"line":502},[40,5162,5163],{"class":319},"## 清空分数\n",[40,5165,5166,5168,5171],{"class":42,"line":527},[40,5167,354],{"class":46},[40,5169,5170],{"class":357}," clear_score",[40,5172,1159],{"class":54},[40,5174,5175,5178,5180],{"class":42,"line":536},[40,5176,5177],{"class":50},"    total_score",[40,5179,62],{"class":54},[40,5181,66],{"class":65},[40,5183,5184,5187,5189,5192,5194,5197],{"class":42,"line":566},[40,5185,5186],{"class":50},"    score_changed",[40,5188,400],{"class":393},[40,5190,5191],{"class":357},"emit",[40,5193,433],{"class":54},[40,5195,5196],{"class":50},"total_score",[40,5198,449],{"class":54},[40,5200,5201],{"class":42,"line":596},[40,5202,327],{"emptyLinePlaceholder":326},[40,5204,5205],{"class":42,"line":611},[40,5206,5207],{"class":319},"## 添加得分\n",[40,5209,5210,5212,5215,5217,5220,5222,5224,5226,5229],{"class":42,"line":619},[40,5211,354],{"class":46},[40,5213,5214],{"class":357}," add_score",[40,5216,433],{"class":54},[40,5218,5219],{"class":393},"amount",[40,5221,55],{"class":54},[40,5223,59],{"class":58},[40,5225,62],{"class":54},[40,5227,5228],{"class":65}," 1",[40,5230,5231],{"class":54},"):\n",[40,5233,5234,5236,5238],{"class":42,"line":3193},[40,5235,5177],{"class":50},[40,5237,1958],{"class":46},[40,5239,5240],{"class":50}," amount\n",[40,5242,5243,5245,5247,5249,5251,5253],{"class":42,"line":3198},[40,5244,5186],{"class":50},[40,5246,400],{"class":393},[40,5248,5191],{"class":357},[40,5250,433],{"class":54},[40,5252,5196],{"class":50},[40,5254,449],{"class":54},[40,5256,5257],{"class":42,"line":3204},[40,5258,327],{"emptyLinePlaceholder":326},[40,5260,5261],{"class":42,"line":3216},[40,5262,5263],{"class":319},"## 设置当前游戏状态\n",[40,5265,5266,5268,5271,5273,5275,5277,5279],{"class":42,"line":3223},[40,5267,354],{"class":46},[40,5269,5270],{"class":357}," set_state",[40,5272,433],{"class":54},[40,5274,5152],{"class":393},[40,5276,55],{"class":54},[40,5278,5045],{"class":58},[40,5280,5231],{"class":54},[40,5282,5283,5286,5288],{"class":42,"line":3228},[40,5284,5285],{"class":50},"    current_state",[40,5287,62],{"class":54},[40,5289,5290],{"class":50}," new_state\n",[40,5292,5293,5296,5298,5300,5302,5304],{"class":42,"line":3252},[40,5294,5295],{"class":50},"    state_changed",[40,5297,400],{"class":393},[40,5299,5191],{"class":357},[40,5301,433],{"class":54},[40,5303,5152],{"class":50},[40,5305,449],{"class":54},[40,5307,5308],{"class":42,"line":3265},[40,5309,327],{"emptyLinePlaceholder":326},[40,5311,5312],{"class":42,"line":3270},[40,5313,1149],{"class":319},[40,5315,5316,5318,5320],{"class":42,"line":3287},[40,5317,354],{"class":46},[40,5319,1156],{"class":357},[40,5321,1159],{"class":54},[40,5323,5324,5326,5328,5330,5332,5334],{"class":42,"line":3293},[40,5325,1164],{"class":357},[40,5327,433],{"class":54},[40,5329,1169],{"class":58},[40,5331,400],{"class":54},[40,5333,1174],{"class":309},[40,5335,449],{"class":54},[40,5337,5338,5340,5342,5344,5346],{"class":42,"line":3314},[40,5339,505],{"class":309},[40,5341,51],{"class":50},[40,5343,1185],{"class":46},[40,5345,1188],{"class":50},[40,5347,370],{"class":393},[40,5349,5350,5352,5354],{"class":42,"line":3322},[40,5351,1195],{"class":50},[40,5353,62],{"class":54},[40,5355,1200],{"class":50},[22,5357,5359],{"id":5358},"autoload-全局单例","Autoload — 全局单例",[10,5361,5362,5363,5366,5367,20],{},"脚本写好了，但现在它只是个 ",[37,5364,5365],{},".gd"," 文件，",[17,5368,5369],{},"别的节点没办法访问到它",[10,5371,5372,5373,5376,5377,5380,5381,5384],{},"Godot 提供了 ",[17,5374,5375],{},"Autoload","（自动加载）机制 — 注册之后，这个脚本\u002F场景",[17,5378,5379],{},"全程只有一份","，任何节点都能直接通过名字访问，不需要 ",[37,5382,5383],{},"get_node()"," 找路径。",[687,5386,5387],{"id":5387},"注册步骤",[10,5389,5390,5391],{},"路径：",[17,5392,5393],{},"项目 → 项目设置 → 全局 → 自动加载",[10,5395,5396],{},[1509,5397],{"alt":5398,"src":5399},"05-game-manager-全局单列GM","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F05-game-manager-%E5%85%A8%E5%B1%80%E5%8D%95%E5%88%97GM.png",[1373,5401,5402,5410,5419,5422],{},[777,5403,5404,5406,5407],{},[17,5405,119],{},"：选 ",[37,5408,5409],{},"res:\u002F\u002Fscripts\u002Fautoload\u002Fgame_manager.gd",[777,5411,5412,5415,5416,5418],{},[17,5413,5414],{},"节点名称","：自动填好 ",[37,5417,397],{},"（也可以手动改）",[777,5420,5421],{},"点击「添加」",[777,5423,5424],{},"确认右侧「启用」是勾选状态",[10,5426,5427,5428,5431],{},"注册之后，任何节点的脚本里都能直接写 ",[37,5429,5430],{},"GameManager.xxx"," 来访问它，比如：",[30,5433,5435],{"className":32,"code":5434,"language":34,"meta":35,"style":35},"GameManager.add_score(1)\nGameManager.current_state\n",[37,5436,5437,5451],{"__ignoreMap":35},[40,5438,5439,5441,5443,5445,5447,5449],{"class":42,"line":43},[40,5440,397],{"class":58},[40,5442,400],{"class":393},[40,5444,1118],{"class":357},[40,5446,433],{"class":54},[40,5448,4497],{"class":65},[40,5450,449],{"class":54},[40,5452,5453,5455,5457],{"class":42,"line":316},[40,5454,397],{"class":58},[40,5456,400],{"class":54},[40,5458,5459],{"class":50},"current_state\n",[10,5461,5462,5463,5466,5467,5466,5470,5473],{},"无需 ",[37,5464,5465],{},"preload"," \u002F ",[37,5468,5469],{},"load",[37,5471,5472],{},"get_node","，超方便。",[22,5475,4197],{"id":4196},[687,5477,5479],{"id":5478},"enum-gamestate",[37,5480,5481],{},"enum GameState",[30,5483,5485],{"className":32,"code":5484,"language":34,"meta":35,"style":35},"enum GameState { READY, PLAYING, GAME_OVER }\n",[37,5486,5487],{"__ignoreMap":35},[40,5488,5489,5491,5493,5495,5497,5499,5501,5503,5505],{"class":42,"line":43},[40,5490,5042],{"class":309},[40,5492,5045],{"class":58},[40,5494,733],{"class":54},[40,5496,5050],{"class":50},[40,5498,5053],{"class":393},[40,5500,3867],{"class":50},[40,5502,5053],{"class":393},[40,5504,1174],{"class":50},[40,5506,5062],{"class":54},[10,5508,5509,5511,5512,5515,5516,5519,5520,5519,5523,5526],{},[37,5510,5042],{},"（枚举）就是给一组\"状态\"取个",[17,5513,5514],{},"好记的名字","。本质上 ",[37,5517,5518],{},"READY=0","、",[37,5521,5522],{},"PLAYING=1",[37,5524,5525],{},"GAME_OVER=2","，但写代码时用名字比写数字清楚得多：",[30,5528,5530],{"className":32,"code":5529,"language":34,"meta":35,"style":35},"# ❌ 不直观\nif current_state == 0:\n# ✅ 一眼明白\nif current_state == GameManager.GameState.READY:\n",[37,5531,5532,5537,5549,5554],{"__ignoreMap":35},[40,5533,5534],{"class":42,"line":43},[40,5535,5536],{"class":319},"# ❌ 不直观\n",[40,5538,5539,5541,5543,5545,5547],{"class":42,"line":316},[40,5540,910],{"class":309},[40,5542,5069],{"class":50},[40,5544,603],{"class":46},[40,5546,645],{"class":65},[40,5548,370],{"class":393},[40,5550,5551],{"class":42,"line":323},[40,5552,5553],{"class":319},"# ✅ 一眼明白\n",[40,5555,5556,5558,5560,5562,5564,5566,5568,5570,5572],{"class":42,"line":330},[40,5557,910],{"class":309},[40,5559,5069],{"class":50},[40,5561,603],{"class":46},[40,5563,3849],{"class":58},[40,5565,400],{"class":54},[40,5567,1169],{"class":50},[40,5569,400],{"class":54},[40,5571,4987],{"class":309},[40,5573,370],{"class":393},[687,5575,5577,5579],{"id":5576},"signal信号",[37,5578,5130],{},"（信号）",[30,5581,5583],{"className":32,"code":5582,"language":34,"meta":35,"style":35},"signal score_changed(new_score)\nsignal state_changed(new_state)\n",[37,5584,5585,5597],{"__ignoreMap":35},[40,5586,5587,5589,5591,5593,5595],{"class":42,"line":43},[40,5588,5130],{"class":46},[40,5590,5133],{"class":357},[40,5592,433],{"class":54},[40,5594,5138],{"class":393},[40,5596,449],{"class":54},[40,5598,5599,5601,5603,5605,5607],{"class":42,"line":316},[40,5600,5130],{"class":46},[40,5602,5147],{"class":357},[40,5604,433],{"class":54},[40,5606,5152],{"class":393},[40,5608,449],{"class":54},[10,5610,5611,5612,5615],{},"信号是 Godot 的「事件通知机制」。GameManager 不需要知道",[17,5613,5614],{},"谁","在听 — 它只管\"广播消息\"：",[774,5617,5618,5625],{},[777,5619,5620,5621,5624],{},"分数变了 → 喊一声 ",[37,5622,5623],{},"score_changed","，UI 接到了就更新分数显示",[777,5626,5627,5628,5631],{},"状态变了 → 喊一声 ",[37,5629,5630],{},"state_changed","，所有关心状态的节点都能响应",[10,5633,5634,5637],{},[17,5635,5636],{},"这样 GameManager 不用知道 UI 长啥样，UI 也不用主动来问 GameManager","。两边解耦，互不干扰 — 信号是 Godot 里非常核心的设计思想。",[687,5639,5641,99,5644],{"id":5640},"add_score-和-score_changedemit",[37,5642,5643],{},"add_score()",[37,5645,5646],{},"score_changed.emit()",[30,5648,5650],{"className":32,"code":5649,"language":34,"meta":35,"style":35},"func add_score(amount: int = 1):\n    total_score += amount\n    score_changed.emit(total_score)\n",[37,5651,5652,5672,5680],{"__ignoreMap":35},[40,5653,5654,5656,5658,5660,5662,5664,5666,5668,5670],{"class":42,"line":43},[40,5655,354],{"class":46},[40,5657,5214],{"class":357},[40,5659,433],{"class":54},[40,5661,5219],{"class":393},[40,5663,55],{"class":54},[40,5665,59],{"class":58},[40,5667,62],{"class":54},[40,5669,5228],{"class":65},[40,5671,5231],{"class":54},[40,5673,5674,5676,5678],{"class":42,"line":316},[40,5675,5177],{"class":50},[40,5677,1958],{"class":46},[40,5679,5240],{"class":50},[40,5681,5682,5684,5686,5688,5690,5692],{"class":42,"line":323},[40,5683,5186],{"class":50},[40,5685,400],{"class":393},[40,5687,5191],{"class":357},[40,5689,433],{"class":54},[40,5691,5196],{"class":50},[40,5693,449],{"class":54},[774,5695,5696,5702],{},[777,5697,5698,5701],{},[37,5699,5700],{},"amount: int = 1"," — 参数默认值是 1，不传就 +1，传 2 就 +2",[777,5703,5704,875,5707,5710],{},[37,5705,5706],{},"score_changed.emit(total_score)",[17,5708,5709],{},"发出信号","，把最新分数传出去",[687,5712,5714,99,5717],{"id":5713},"set_state-和-game_over",[37,5715,5716],{},"set_state()",[37,5718,5719],{},"game_over()",[10,5721,5722,5725,5726,5729,5730,5733],{},[37,5723,5724],{},"set_state"," 是修改状态的",[17,5727,5728],{},"唯一入口","，每次修改都自动 emit 信号。这样",[17,5731,5732],{},"任何状态改变都会被广播","，不会漏。",[10,5735,5736,5738],{},[37,5737,1138],{}," 不仅切换状态，还顺便更新最高分。",[22,5740,5742],{"id":5741},"把-gamemanager-接进-killzone-和-障碍物","把 GameManager 接进 Killzone 和 障碍物",[10,5744,5745,5746,5749],{},"GameManager 创建好了，现在回到 ",[183,5747,5748],{"href":4953},"上一章"," 里我们留的\"后续优化预览\"代码，把它们正式接入：",[687,5751,5753,5754,3577],{"id":5752},"死区脚本killzonegd","死区脚本（",[37,5755,5756],{},"killzone.gd",[30,5758,5760],{"className":32,"code":5759,"language":34,"meta":35,"style":35},"extends Area2D\n\nfunc _on_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\" and GameManager.current_state == GameManager.GameState.PLAYING:\n        GameManager.game_over()\n",[37,5761,5762,5768,5772,5794,5830],{"__ignoreMap":35},[40,5763,5764,5766],{"class":42,"line":43},[40,5765,310],{"class":309},[40,5767,3697],{"class":58},[40,5769,5770],{"class":42,"line":316},[40,5771,327],{"emptyLinePlaceholder":326},[40,5773,5774,5776,5778,5780,5782,5784,5786,5788,5790,5792],{"class":42,"line":323},[40,5775,354],{"class":46},[40,5777,3708],{"class":357},[40,5779,433],{"class":54},[40,5781,3713],{"class":393},[40,5783,55],{"class":54},[40,5785,3718],{"class":58},[40,5787,522],{"class":54},[40,5789,364],{"class":46},[40,5791,367],{"class":58},[40,5793,370],{"class":54},[40,5795,5796,5798,5800,5802,5804,5806,5808,5810,5812,5814,5816,5818,5820,5822,5824,5826,5828],{"class":42,"line":330},[40,5797,505],{"class":309},[40,5799,3733],{"class":50},[40,5801,400],{"class":54},[40,5803,3738],{"class":50},[40,5805,603],{"class":46},[40,5807,3743],{"class":342},[40,5809,3846],{"class":46},[40,5811,3849],{"class":58},[40,5813,400],{"class":54},[40,5815,3854],{"class":50},[40,5817,603],{"class":46},[40,5819,3849],{"class":58},[40,5821,400],{"class":54},[40,5823,1169],{"class":50},[40,5825,400],{"class":54},[40,5827,3867],{"class":309},[40,5829,370],{"class":393},[40,5831,5832,5834,5836,5838],{"class":42,"line":346},[40,5833,3886],{"class":58},[40,5835,400],{"class":393},[40,5837,1138],{"class":357},[40,5839,2070],{"class":54},[10,5841,5842,5843,5846],{},"加上 ",[37,5844,5845],{},"current_state == PLAYING"," 判断 — 防止 READY 状态（还没开始）和 GAME_OVER 状态（已经结束）时误触发。",[687,5848,5850,5851,3577],{"id":5849},"障碍物脚本pillar_pairgd","障碍物脚本（",[37,5852,5853],{},"pillar_pair.gd",[30,5855,5857],{"className":32,"code":5856,"language":34,"meta":35,"style":35},"extends Node2D\n\n@export var speed := 200.0\n@onready var goal: Area2D = $Goal\n\nfunc _physics_process(delta: float) -> void:\n    # 只在游戏进行中才移动\n    if GameManager.current_state == GameManager.GameState.PLAYING:\n        position.x -= speed * delta\n        if position.x \u003C -500:\n            queue_free()\n\nfunc _on_goal_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\":\n        if GameManager.has_method(\"add_score\"):\n            GameManager.add_score(1)\n        goal.set_deferred(\"monitoring\", false)\n",[37,5858,5859,5865,5869,5881,5899,5903,5925,5930,5954,5970,5988,5994,5998,6020,6036,6054,6068],{"__ignoreMap":35},[40,5860,5861,5863],{"class":42,"line":43},[40,5862,310],{"class":309},[40,5864,3967],{"class":58},[40,5866,5867],{"class":42,"line":316},[40,5868,327],{"emptyLinePlaceholder":326},[40,5870,5871,5873,5875,5877,5879],{"class":42,"line":323},[40,5872,1852],{"class":357},[40,5874,1855],{"class":46},[40,5876,3980],{"class":50},[40,5878,339],{"class":54},[40,5880,3985],{"class":65},[40,5882,5883,5885,5887,5889,5891,5893,5895,5897],{"class":42,"line":330},[40,5884,3990],{"class":357},[40,5886,1855],{"class":46},[40,5888,3995],{"class":50},[40,5890,55],{"class":54},[40,5892,4000],{"class":58},[40,5894,62],{"class":54},[40,5896,4005],{"class":309},[40,5898,4009],{"class":4008},[40,5900,5901],{"class":42,"line":346},[40,5902,327],{"emptyLinePlaceholder":326},[40,5904,5905,5907,5909,5911,5913,5915,5917,5919,5921,5923],{"class":42,"line":351},[40,5906,354],{"class":46},[40,5908,1914],{"class":357},[40,5910,433],{"class":54},[40,5912,1919],{"class":393},[40,5914,55],{"class":54},[40,5916,1924],{"class":58},[40,5918,522],{"class":54},[40,5920,364],{"class":46},[40,5922,367],{"class":58},[40,5924,370],{"class":54},[40,5926,5927],{"class":42,"line":373},[40,5928,5929],{"class":319},"    # 只在游戏进行中才移动\n",[40,5931,5932,5934,5936,5938,5940,5942,5944,5946,5948,5950,5952],{"class":42,"line":387},[40,5933,505],{"class":309},[40,5935,3849],{"class":58},[40,5937,400],{"class":54},[40,5939,3854],{"class":50},[40,5941,603],{"class":46},[40,5943,3849],{"class":58},[40,5945,400],{"class":54},[40,5947,1169],{"class":50},[40,5949,400],{"class":54},[40,5951,3867],{"class":309},[40,5953,370],{"class":393},[40,5955,5956,5958,5960,5962,5964,5966,5968],{"class":42,"line":409},[40,5957,4372],{"class":50},[40,5959,400],{"class":54},[40,5961,2455],{"class":50},[40,5963,4052],{"class":46},[40,5965,3980],{"class":50},[40,5967,1966],{"class":46},[40,5969,4059],{"class":50},[40,5971,5972,5974,5976,5978,5980,5982,5984,5986],{"class":42,"line":415},[40,5973,4389],{"class":309},[40,5975,4071],{"class":50},[40,5977,400],{"class":54},[40,5979,2455],{"class":50},[40,5981,1185],{"class":46},[40,5983,1880],{"class":46},[40,5985,4082],{"class":65},[40,5987,370],{"class":393},[40,5989,5990,5992],{"class":42,"line":452},[40,5991,4408],{"class":357},[40,5993,2070],{"class":54},[40,5995,5996],{"class":42,"line":481},[40,5997,327],{"emptyLinePlaceholder":326},[40,5999,6000,6002,6004,6006,6008,6010,6012,6014,6016,6018],{"class":42,"line":486},[40,6001,354],{"class":46},[40,6003,4107],{"class":357},[40,6005,433],{"class":54},[40,6007,3713],{"class":393},[40,6009,55],{"class":54},[40,6011,3718],{"class":58},[40,6013,522],{"class":54},[40,6015,364],{"class":46},[40,6017,367],{"class":58},[40,6019,370],{"class":54},[40,6021,6022,6024,6026,6028,6030,6032,6034],{"class":42,"line":502},[40,6023,505],{"class":309},[40,6025,3733],{"class":50},[40,6027,400],{"class":54},[40,6029,3738],{"class":50},[40,6031,603],{"class":46},[40,6033,3743],{"class":342},[40,6035,370],{"class":393},[40,6037,6038,6040,6042,6044,6046,6048,6050,6052],{"class":42,"line":527},[40,6039,4389],{"class":309},[40,6041,3849],{"class":58},[40,6043,400],{"class":393},[40,6045,4474],{"class":357},[40,6047,433],{"class":54},[40,6049,4479],{"class":342},[40,6051,522],{"class":54},[40,6053,370],{"class":393},[40,6055,6056,6058,6060,6062,6064,6066],{"class":42,"line":536},[40,6057,4488],{"class":58},[40,6059,400],{"class":393},[40,6061,1118],{"class":357},[40,6063,433],{"class":54},[40,6065,4497],{"class":65},[40,6067,449],{"class":54},[40,6069,6070,6072,6074,6076,6078,6080,6082,6084],{"class":42,"line":566},[40,6071,4170],{"class":50},[40,6073,400],{"class":393},[40,6075,4175],{"class":357},[40,6077,433],{"class":54},[40,6079,4180],{"class":342},[40,6081,439],{"class":54},[40,6083,4185],{"class":309},[40,6085,449],{"class":54},[687,6087,6089,6090,3577],{"id":6088},"生成器脚本pillar_spawnergd","生成器脚本（",[37,6091,6092],{},"pillar_spawner.gd",[30,6094,6096],{"className":32,"code":6095,"language":34,"meta":35,"style":35},"extends Node2D\n\n@export var pillar_scene: PackedScene\n@export var y_range := 250.0\n\nfunc _on_timer_timeout() -> void:\n    spawn_pillar()\n\nfunc spawn_pillar() -> void:\n    # 只在游戏进行中才生成\n    if GameManager.current_state == GameManager.GameState.PLAYING:\n        var new_pillar = pillar_scene.instantiate()\n        var spawn_pos = global_position\n        spawn_pos.y += randf_range(-y_range, y_range)\n        new_pillar.global_position = spawn_pos\n        get_tree().current_scene.add_child(new_pillar)\n",[37,6097,6098,6104,6108,6120,6132,6136,6150,6156,6160,6174,6179,6203,6220,6230,6255,6268],{"__ignoreMap":35},[40,6099,6100,6102],{"class":42,"line":43},[40,6101,310],{"class":309},[40,6103,3967],{"class":58},[40,6105,6106],{"class":42,"line":316},[40,6107,327],{"emptyLinePlaceholder":326},[40,6109,6110,6112,6114,6116,6118],{"class":42,"line":323},[40,6111,1852],{"class":357},[40,6113,1855],{"class":46},[40,6115,4652],{"class":50},[40,6117,55],{"class":54},[40,6119,4657],{"class":58},[40,6121,6122,6124,6126,6128,6130],{"class":42,"line":330},[40,6123,1852],{"class":357},[40,6125,1855],{"class":46},[40,6127,4666],{"class":50},[40,6129,339],{"class":54},[40,6131,4671],{"class":65},[40,6133,6134],{"class":42,"line":346},[40,6135,327],{"emptyLinePlaceholder":326},[40,6137,6138,6140,6142,6144,6146,6148],{"class":42,"line":351},[40,6139,354],{"class":46},[40,6141,4682],{"class":357},[40,6143,361],{"class":54},[40,6145,364],{"class":46},[40,6147,367],{"class":58},[40,6149,370],{"class":54},[40,6151,6152,6154],{"class":42,"line":373},[40,6153,4695],{"class":357},[40,6155,2070],{"class":54},[40,6157,6158],{"class":42,"line":387},[40,6159,327],{"emptyLinePlaceholder":326},[40,6161,6162,6164,6166,6168,6170,6172],{"class":42,"line":409},[40,6163,354],{"class":46},[40,6165,4708],{"class":357},[40,6167,361],{"class":54},[40,6169,364],{"class":46},[40,6171,367],{"class":58},[40,6173,370],{"class":54},[40,6175,6176],{"class":42,"line":415},[40,6177,6178],{"class":319},"    # 只在游戏进行中才生成\n",[40,6180,6181,6183,6185,6187,6189,6191,6193,6195,6197,6199,6201],{"class":42,"line":452},[40,6182,505],{"class":309},[40,6184,3849],{"class":58},[40,6186,400],{"class":54},[40,6188,3854],{"class":50},[40,6190,603],{"class":46},[40,6192,3849],{"class":58},[40,6194,400],{"class":54},[40,6196,1169],{"class":50},[40,6198,400],{"class":54},[40,6200,3867],{"class":309},[40,6202,370],{"class":393},[40,6204,6205,6208,6210,6212,6214,6216,6218],{"class":42,"line":481},[40,6206,6207],{"class":46},"        var",[40,6209,4723],{"class":50},[40,6211,62],{"class":54},[40,6213,4652],{"class":50},[40,6215,400],{"class":393},[40,6217,4732],{"class":357},[40,6219,2070],{"class":54},[40,6221,6222,6224,6226,6228],{"class":42,"line":486},[40,6223,6207],{"class":46},[40,6225,4745],{"class":50},[40,6227,62],{"class":54},[40,6229,4750],{"class":50},[40,6231,6232,6235,6237,6239,6241,6243,6245,6247,6249,6251,6253],{"class":42,"line":502},[40,6233,6234],{"class":50},"        spawn_pos",[40,6236,400],{"class":54},[40,6238,1995],{"class":50},[40,6240,1958],{"class":46},[40,6242,4764],{"class":357},[40,6244,433],{"class":54},[40,6246,3306],{"class":46},[40,6248,4771],{"class":50},[40,6250,439],{"class":54},[40,6252,4666],{"class":50},[40,6254,449],{"class":54},[40,6256,6257,6260,6262,6264,6266],{"class":42,"line":527},[40,6258,6259],{"class":50},"        new_pillar",[40,6261,400],{"class":54},[40,6263,4791],{"class":50},[40,6265,62],{"class":54},[40,6267,4796],{"class":50},[40,6269,6270,6273,6275,6277,6279,6281,6283,6285,6287],{"class":42,"line":536},[40,6271,6272],{"class":357},"        get_tree",[40,6274,361],{"class":54},[40,6276,400],{"class":393},[40,6278,4817],{"class":50},[40,6280,400],{"class":393},[40,6282,4822],{"class":357},[40,6284,433],{"class":54},[40,6286,4827],{"class":50},[40,6288,449],{"class":54},[22,6290,2914],{"id":2914},[10,6292,6293,6294,20],{},"现在还没有 UI，所以视觉上看不出区别 — ",[17,6295,6296],{},"但状态系统已经在跑了",[10,6298,6299,6300,6302],{},"你可以在 ",[37,6301,2966],{}," 里临时加个调试：",[30,6304,6306],{"className":32,"code":6305,"language":34,"meta":35,"style":35},"func _physics_process(delta: float) -> void:\n    if Input.is_action_just_pressed(\"fly\"):\n        print(\"当前状态:\", GameManager.current_state)\n        # ...\n",[37,6307,6308,6330,6348,6367],{"__ignoreMap":35},[40,6309,6310,6312,6314,6316,6318,6320,6322,6324,6326,6328],{"class":42,"line":43},[40,6311,354],{"class":46},[40,6313,1914],{"class":357},[40,6315,433],{"class":54},[40,6317,1919],{"class":393},[40,6319,55],{"class":54},[40,6321,1924],{"class":58},[40,6323,522],{"class":54},[40,6325,364],{"class":46},[40,6327,367],{"class":58},[40,6329,370],{"class":54},[40,6331,6332,6334,6336,6338,6340,6342,6344,6346],{"class":42,"line":316},[40,6333,505],{"class":309},[40,6335,2031],{"class":58},[40,6337,400],{"class":393},[40,6339,2036],{"class":357},[40,6341,433],{"class":54},[40,6343,2041],{"class":342},[40,6345,522],{"class":54},[40,6347,370],{"class":393},[40,6349,6350,6352,6354,6357,6359,6361,6363,6365],{"class":42,"line":323},[40,6351,3750],{"class":357},[40,6353,433],{"class":54},[40,6355,6356],{"class":342},"\"当前状态:\"",[40,6358,439],{"class":54},[40,6360,3849],{"class":58},[40,6362,400],{"class":54},[40,6364,3854],{"class":50},[40,6366,449],{"class":54},[40,6368,6369],{"class":42,"line":330},[40,6370,6371],{"class":319},"        # ...\n",[10,6373,6374,6375,6377,6378,6380],{},"不过这时候默认是 ",[37,6376,4987],{}," 状态，水管不会生成、撞死也不会触发 — 因为我们还没有\"开始游戏\"的入口。下一章做 UI 时会加上一个开始按钮，把状态切到 ",[37,6379,3867],{},"，整个流程才会真正跑起来。",[1444,6382,6383],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}",{"title":35,"searchDepth":316,"depth":316,"links":6385},[6386,6387,6388,6391,6400,6408],{"id":4976,"depth":316,"text":4977},{"id":5011,"depth":316,"text":5012},{"id":5358,"depth":316,"text":5359,"children":6389},[6390],{"id":5387,"depth":323,"text":5387},{"id":4196,"depth":316,"text":4197,"children":6392},[6393,6394,6396,6398],{"id":5478,"depth":323,"text":5481},{"id":5576,"depth":323,"text":6395},"signal（信号）",{"id":5640,"depth":323,"text":6397},"add_score() 和 score_changed.emit()",{"id":5713,"depth":323,"text":6399},"set_state() 和 game_over()",{"id":5741,"depth":316,"text":5742,"children":6401},[6402,6404,6406],{"id":5752,"depth":323,"text":6403},"死区脚本（killzone.gd）",{"id":5849,"depth":323,"text":6405},"障碍物脚本（pillar_pair.gd）",{"id":6088,"depth":323,"text":6407},"生成器脚本（pillar_spawner.gd）",{"id":2914,"depth":316,"text":2914},{},"\u002Fdevlog\u002Fxggame-bird\u002F05-game-manager",{"title":397,"description":1474},"devlog\u002Fxggame-bird\u002F05-game-manager","FNw8591Sth7D2Iaz9Dt3V0d-rXtMLyEjwyH5oxagnGQ",{"id":6415,"title":6416,"body":6417,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":7683,"navigation":326,"path":7684,"seo":7685,"stem":7686,"toc":326,"__hash__":7687},"devlog\u002Fdevlog\u002Fxggame-bird\u002F06-ui.md","UI 界面 + 主场景",{"type":7,"value":6418,"toc":7663},[6419,6422,6455,6458,6463,6471,6477,6484,6486,6513,6528,6534,6537,6550,6598,6609,6615,6628,6631,6729,6735,6741,7109,7111,7185,7192,7244,7253,7256,7265,7270,7273,7281,7382,7396,7402,7405,7414,7420,7587,7605,7617,7620,7626,7629,7635,7637,7640,7657,7660],[10,6420,6421],{},"GameManager 把状态和分数管理好了，这一章让玩家\"看得见\"。我们要做：",[1373,6423,6424,6434,6443,6449],{},[777,6425,6426,6429,6430,6433],{},[17,6427,6428],{},"开始菜单","（",[37,6431,6432],{},"menu.tscn","）— 一个开始按钮",[777,6435,6436,6429,6439,6442],{},[17,6437,6438],{},"游戏主场景",[37,6440,6441],{},"main.tscn","）— 整合小鸟、水管、UI",[777,6444,6445,6448],{},[17,6446,6447],{},"分数 UI"," — 实时显示当前分数",[777,6450,6451,6454],{},[17,6452,6453],{},"结束画面"," — 显示最终分数 + 重新开始按钮",[22,6456,6457],{"id":6457},"整体流程",[267,6459,6460],{},[10,6461,6462],{},"整理一下我们要搭的\"两个场景\"的关系：",[30,6464,6469],{"className":6465,"code":6467,"language":6468},[6466],"language-text","menu.tscn（开始菜单）\n   ↓ 点击「开始」按钮\nmain.tscn（游戏主场景）\n   ├─ READY 状态：等待玩家按下 fly\n   ├─ PLAYING 状态：正常游戏\n   └─ GAME_OVER 状态：显示分数 + 重新开始按钮\n","text",[37,6470,6467],{"__ignoreMap":35},[22,6472,6474,6475],{"id":6473},"开始菜单-menutscn","开始菜单 ",[37,6476,6432],{},[10,6478,2746,6479,2750,6481,20],{},[37,6480,2749],{},[37,6482,6483],{},"scenes\u002Fmenu.tscn",[10,6485,2761],{},[774,6487,6488],{},[777,6489,6490,6492,6493],{},[37,6491,2749],{}," — 菜单根节点\n",[774,6494,6495],{},[777,6496,6497,6500,6501],{},[37,6498,6499],{},"CanvasLayer"," — UI 图层（不随摄像机移动）\n",[774,6502,6503],{},[777,6504,6505,6508,6509,6512],{},[37,6506,6507],{},"Button","（重命名 ",[37,6510,6511],{},"StartButton","）— 开始按钮",[267,6514,6515],{},[10,6516,6517,6518,6520,6521,6523,6524,6527],{},"为什么 UI 要放在 ",[37,6519,6499],{}," 下？因为 ",[37,6522,6499],{}," 的内容",[17,6525,6526],{},"不会被摄像机变换影响","，永远固定在屏幕上。",[10,6529,6530],{},[1509,6531],{"alt":6532,"src":6533},"06-ui-menu-场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F06-ui-menu-%E5%9C%BA%E6%99%AF.png",[687,6535,6536],{"id":6536},"菜单脚本",[10,6538,6539,6540,6543,6544,1746,6546,6549],{},"选中根节点挂脚本 ",[37,6541,6542],{},"scripts\u002Fmenu.gd","，然后在【节点】面板把 ",[37,6545,6511],{},[37,6547,6548],{},"pressed"," 信号连接到根节点：",[30,6551,6553],{"className":32,"code":6552,"language":34,"meta":35,"style":35},"extends Node2D\n\nfunc _on_start_button_pressed() -> void:\n    get_tree().change_scene_to_file(\"res:\u002F\u002Fscenes\u002Fmain.tscn\")\n",[37,6554,6555,6561,6565,6580],{"__ignoreMap":35},[40,6556,6557,6559],{"class":42,"line":43},[40,6558,310],{"class":309},[40,6560,3967],{"class":58},[40,6562,6563],{"class":42,"line":316},[40,6564,327],{"emptyLinePlaceholder":326},[40,6566,6567,6569,6572,6574,6576,6578],{"class":42,"line":323},[40,6568,354],{"class":46},[40,6570,6571],{"class":357}," _on_start_button_pressed",[40,6573,361],{"class":54},[40,6575,364],{"class":46},[40,6577,367],{"class":58},[40,6579,370],{"class":54},[40,6581,6582,6584,6586,6588,6591,6593,6596],{"class":42,"line":330},[40,6583,4810],{"class":357},[40,6585,361],{"class":54},[40,6587,400],{"class":393},[40,6589,6590],{"class":357},"change_scene_to_file",[40,6592,433],{"class":54},[40,6594,6595],{"class":342},"\"res:\u002F\u002Fscenes\u002Fmain.tscn\"",[40,6597,449],{"class":54},[774,6599,6600],{},[777,6601,6602,6605,6606,6608],{},[37,6603,6604],{},"change_scene_to_file()"," — 切换到指定场景，路径填你保存 ",[37,6607,6441],{}," 的位置",[22,6610,6612,6613],{"id":6611},"游戏主场景-maintscn","游戏主场景 ",[37,6614,6441],{},[10,6616,6617,6618,6621,6622,6624,6625,6627],{},"现在把之前章节做的所有东西",[17,6619,6620],{},"整合到一起","。如果之前你已经做了 ",[37,6623,2811],{},"，可以直接改名为 ",[37,6626,6441],{},"，或者新建一个。",[10,6629,6630],{},"使用到的节点（核心结构）：",[774,6632,6633],{},[777,6634,6635,6637,6638,3483,6641],{},[37,6636,2749],{}," — 主场景根节点（挂 ",[37,6639,6640],{},"main.gd",[774,6642,6643,6648,6661,6669,6676,6681],{},[777,6644,6645,6647],{},[37,6646,2775],{}," — 摄像机",[777,6649,6650,6508,6653,6656,6657,3577],{},[37,6651,6652],{},"Parallax2D",[37,6654,6655],{},"BG","）— 滚动背景（",[183,6658,6660],{"href":6659},"\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets","第 7 章会做",[777,6662,6663,6508,6665,6668],{},[37,6664,6652],{},[37,6666,6667],{},"Progress","）— 滚动地面",[777,6670,6671,6673,6674,3577],{},[37,6672,1521],{}," — 小鸟（之前做的 ",[37,6675,2796],{},[777,6677,6678,6680],{},[37,6679,4563],{}," — 障碍物生成器",[777,6682,6683,6685,6686],{},[37,6684,6499],{}," — UI 层\n",[774,6687,6688,6697],{},[777,6689,6690,6508,6693,6696],{},[37,6691,6692],{},"Label",[37,6694,6695],{},"ScoreLabel","）— 分数显示",[777,6698,6699,6508,6702,6705,6706],{},[37,6700,6701],{},"Panel",[37,6703,6704],{},"GameOverPanel","）— 结束面板\n",[774,6707,6708,6713,6721],{},[777,6709,6710,6712],{},[37,6711,6692],{}," — \"Game Over\"",[777,6714,6715,6429,6717,6720],{},[37,6716,6692],{},[37,6718,6719],{},"FinalScore","）— 最终分数",[777,6722,6723,6429,6725,6728],{},[37,6724,6507],{},[37,6726,6727],{},"RestartButton","）— 重新开始",[10,6730,6731],{},[1509,6732],{"alt":6733,"src":6734},"06-ui-main-场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F06-ui-main-%E5%9C%BA%E6%99%AF.png",[22,6736,6738,6739],{"id":6737},"主场景脚本-maingd","主场景脚本 ",[37,6740,6640],{},[30,6742,6744],{"className":32,"code":6743,"language":34,"meta":35,"style":35},"extends Node2D\n\nfunc _ready() -> void:\n    # 刚加载场景，设置为准备状态\n    GameManager.set_state(GameManager.GameState.READY)\n    GameManager.state_changed.connect(_on_game_state)  # 监听信号\n    Engine.time_scale = 0  # 冻结游戏\n    GameManager.clear_score()  # 游戏开始前清空分数\n\nfunc _input(event: InputEvent) -> void:\n    # 准备状态下，按一下 fly 就开始游戏\n    if GameManager.current_state == GameManager.GameState.READY:\n        if event.is_action_pressed(\"fly\"):\n            start_game()\n\nfunc start_game():\n    GameManager.set_state(GameManager.GameState.PLAYING)\n    Engine.time_scale = 1  # 解冻游戏\n\n## 重新开始游戏\nfunc _on_restart_button_pressed() -> void:\n    get_tree().reload_current_scene()\n\nfunc _on_game_state(new_state) -> void:\n    # 游戏结束时，让背景和地面停止滚动\n    if new_state == GameManager.GameState.GAME_OVER:\n        $BG.autoscroll.x = 0\n        $Progress.autoscroll.x = 0\n",[37,6745,6746,6752,6756,6770,6775,6797,6819,6836,6850,6854,6879,6884,6908,6928,6935,6939,6948,6970,6985,6989,6994,7009,7022,7026,7045,7050,7071,7091],{"__ignoreMap":35},[40,6747,6748,6750],{"class":42,"line":43},[40,6749,310],{"class":309},[40,6751,3967],{"class":58},[40,6753,6754],{"class":42,"line":316},[40,6755,327],{"emptyLinePlaceholder":326},[40,6757,6758,6760,6762,6764,6766,6768],{"class":42,"line":323},[40,6759,354],{"class":46},[40,6761,1075],{"class":357},[40,6763,361],{"class":54},[40,6765,364],{"class":46},[40,6767,367],{"class":58},[40,6769,370],{"class":54},[40,6771,6772],{"class":42,"line":330},[40,6773,6774],{"class":319},"    # 刚加载场景，设置为准备状态\n",[40,6776,6777,6779,6781,6783,6785,6787,6789,6791,6793,6795],{"class":42,"line":346},[40,6778,622],{"class":58},[40,6780,400],{"class":393},[40,6782,5724],{"class":357},[40,6784,433],{"class":54},[40,6786,397],{"class":58},[40,6788,400],{"class":54},[40,6790,1169],{"class":50},[40,6792,400],{"class":54},[40,6794,4987],{"class":309},[40,6796,449],{"class":54},[40,6798,6799,6801,6803,6805,6807,6809,6811,6814,6816],{"class":42,"line":351},[40,6800,622],{"class":58},[40,6802,400],{"class":54},[40,6804,5630],{"class":50},[40,6806,400],{"class":393},[40,6808,1113],{"class":357},[40,6810,433],{"class":54},[40,6812,6813],{"class":50},"_on_game_state",[40,6815,522],{"class":54},[40,6817,6818],{"class":319},"  # 监听信号\n",[40,6820,6821,6824,6826,6829,6831,6833],{"class":42,"line":373},[40,6822,6823],{"class":58},"    Engine",[40,6825,400],{"class":54},[40,6827,6828],{"class":50},"time_scale",[40,6830,62],{"class":54},[40,6832,645],{"class":65},[40,6834,6835],{"class":319},"  # 冻结游戏\n",[40,6837,6838,6840,6842,6845,6847],{"class":42,"line":387},[40,6839,622],{"class":58},[40,6841,400],{"class":393},[40,6843,6844],{"class":357},"clear_score",[40,6846,361],{"class":54},[40,6848,6849],{"class":319},"  # 游戏开始前清空分数\n",[40,6851,6852],{"class":42,"line":409},[40,6853,327],{"emptyLinePlaceholder":326},[40,6855,6856,6858,6861,6863,6866,6868,6871,6873,6875,6877],{"class":42,"line":415},[40,6857,354],{"class":46},[40,6859,6860],{"class":357}," _input",[40,6862,433],{"class":54},[40,6864,6865],{"class":393},"event",[40,6867,55],{"class":54},[40,6869,6870],{"class":58}," InputEvent",[40,6872,522],{"class":54},[40,6874,364],{"class":46},[40,6876,367],{"class":58},[40,6878,370],{"class":54},[40,6880,6881],{"class":42,"line":452},[40,6882,6883],{"class":319},"    # 准备状态下，按一下 fly 就开始游戏\n",[40,6885,6886,6888,6890,6892,6894,6896,6898,6900,6902,6904,6906],{"class":42,"line":481},[40,6887,505],{"class":309},[40,6889,3849],{"class":58},[40,6891,400],{"class":54},[40,6893,3854],{"class":50},[40,6895,603],{"class":46},[40,6897,3849],{"class":58},[40,6899,400],{"class":54},[40,6901,1169],{"class":50},[40,6903,400],{"class":54},[40,6905,4987],{"class":309},[40,6907,370],{"class":393},[40,6909,6910,6912,6915,6917,6920,6922,6924,6926],{"class":42,"line":486},[40,6911,4389],{"class":309},[40,6913,6914],{"class":50}," event",[40,6916,400],{"class":393},[40,6918,6919],{"class":357},"is_action_pressed",[40,6921,433],{"class":54},[40,6923,2041],{"class":342},[40,6925,522],{"class":54},[40,6927,370],{"class":393},[40,6929,6930,6933],{"class":42,"line":502},[40,6931,6932],{"class":357},"            start_game",[40,6934,2070],{"class":54},[40,6936,6937],{"class":42,"line":527},[40,6938,327],{"emptyLinePlaceholder":326},[40,6940,6941,6943,6946],{"class":42,"line":536},[40,6942,354],{"class":46},[40,6944,6945],{"class":357}," start_game",[40,6947,1159],{"class":54},[40,6949,6950,6952,6954,6956,6958,6960,6962,6964,6966,6968],{"class":42,"line":566},[40,6951,622],{"class":58},[40,6953,400],{"class":393},[40,6955,5724],{"class":357},[40,6957,433],{"class":54},[40,6959,397],{"class":58},[40,6961,400],{"class":54},[40,6963,1169],{"class":50},[40,6965,400],{"class":54},[40,6967,3867],{"class":309},[40,6969,449],{"class":54},[40,6971,6972,6974,6976,6978,6980,6982],{"class":42,"line":596},[40,6973,6823],{"class":58},[40,6975,400],{"class":54},[40,6977,6828],{"class":50},[40,6979,62],{"class":54},[40,6981,5228],{"class":65},[40,6983,6984],{"class":319},"  # 解冻游戏\n",[40,6986,6987],{"class":42,"line":611},[40,6988,327],{"emptyLinePlaceholder":326},[40,6990,6991],{"class":42,"line":619},[40,6992,6993],{"class":319},"## 重新开始游戏\n",[40,6995,6996,6998,7001,7003,7005,7007],{"class":42,"line":3193},[40,6997,354],{"class":46},[40,6999,7000],{"class":357}," _on_restart_button_pressed",[40,7002,361],{"class":54},[40,7004,364],{"class":46},[40,7006,367],{"class":58},[40,7008,370],{"class":54},[40,7010,7011,7013,7015,7017,7020],{"class":42,"line":3198},[40,7012,4810],{"class":357},[40,7014,361],{"class":54},[40,7016,400],{"class":393},[40,7018,7019],{"class":357},"reload_current_scene",[40,7021,2070],{"class":54},[40,7023,7024],{"class":42,"line":3204},[40,7025,327],{"emptyLinePlaceholder":326},[40,7027,7028,7030,7033,7035,7037,7039,7041,7043],{"class":42,"line":3216},[40,7029,354],{"class":46},[40,7031,7032],{"class":357}," _on_game_state",[40,7034,433],{"class":54},[40,7036,5152],{"class":393},[40,7038,522],{"class":54},[40,7040,364],{"class":46},[40,7042,367],{"class":58},[40,7044,370],{"class":54},[40,7046,7047],{"class":42,"line":3223},[40,7048,7049],{"class":319},"    # 游戏结束时，让背景和地面停止滚动\n",[40,7051,7052,7054,7057,7059,7061,7063,7065,7067,7069],{"class":42,"line":3228},[40,7053,505],{"class":309},[40,7055,7056],{"class":50}," new_state",[40,7058,603],{"class":46},[40,7060,3849],{"class":58},[40,7062,400],{"class":54},[40,7064,1169],{"class":50},[40,7066,400],{"class":54},[40,7068,1174],{"class":309},[40,7070,370],{"class":393},[40,7072,7073,7076,7078,7080,7083,7085,7087,7089],{"class":42,"line":3252},[40,7074,7075],{"class":309},"        $",[40,7077,6655],{"class":4008},[40,7079,400],{"class":54},[40,7081,7082],{"class":50},"autoscroll",[40,7084,400],{"class":54},[40,7086,2455],{"class":50},[40,7088,62],{"class":54},[40,7090,66],{"class":65},[40,7092,7093,7095,7097,7099,7101,7103,7105,7107],{"class":42,"line":3265},[40,7094,7075],{"class":309},[40,7096,6667],{"class":4008},[40,7098,400],{"class":54},[40,7100,7082],{"class":50},[40,7102,400],{"class":54},[40,7104,2455],{"class":50},[40,7106,62],{"class":54},[40,7108,66],{"class":65},[687,7110,4197],{"id":4196},[108,7112,7113,7122],{},[111,7114,7115],{},[114,7116,7117,7120],{},[117,7118,7119],{},"函数",[117,7121,1635],{},[127,7123,7124,7137,7150,7162,7172],{},[114,7125,7126,7130],{},[132,7127,7128],{},[37,7129,673],{},[132,7131,7132,7133,7136],{},"场景一加载就执行：把状态设为 READY、监听状态变化、",[17,7134,7135],{},"冻结整个游戏","、清空分数",[114,7138,7139,7144],{},[132,7140,7141],{},[37,7142,7143],{},"_input()",[132,7145,7146,7147],{},"监听全局输入：在 READY 状态下，按下 fly 就调用 ",[37,7148,7149],{},"start_game()",[114,7151,7152,7156],{},[132,7153,7154],{},[37,7155,7149],{},[132,7157,7158,7159],{},"把状态切到 PLAYING，",[17,7160,7161],{},"解冻游戏",[114,7163,7164,7169],{},[132,7165,7166],{},[37,7167,7168],{},"_on_restart_button_pressed()",[132,7170,7171],{},"重启按钮：直接重新加载当前场景",[114,7173,7174,7179],{},[132,7175,7176],{},[37,7177,7178],{},"_on_game_state()",[132,7180,7181,7182,7184],{},"监听 GameManager 的 ",[37,7183,5630],{}," 信号，GAME_OVER 时停止背景滚动",[687,7186,7188,7191],{"id":7187},"enginetime_scale-全局时间缩放",[37,7189,7190],{},"Engine.time_scale"," — 全局时间缩放",[30,7193,7195],{"className":32,"code":7194,"language":34,"meta":35,"style":35},"Engine.time_scale = 0  # 完全冻结\nEngine.time_scale = 1  # 正常速度\nEngine.time_scale = 0.5  # 慢动作\n",[37,7196,7197,7213,7228],{"__ignoreMap":35},[40,7198,7199,7202,7204,7206,7208,7210],{"class":42,"line":43},[40,7200,7201],{"class":58},"Engine",[40,7203,400],{"class":54},[40,7205,6828],{"class":50},[40,7207,62],{"class":54},[40,7209,645],{"class":65},[40,7211,7212],{"class":319},"  # 完全冻结\n",[40,7214,7215,7217,7219,7221,7223,7225],{"class":42,"line":316},[40,7216,7201],{"class":58},[40,7218,400],{"class":54},[40,7220,6828],{"class":50},[40,7222,62],{"class":54},[40,7224,5228],{"class":65},[40,7226,7227],{"class":319},"  # 正常速度\n",[40,7229,7230,7232,7234,7236,7238,7241],{"class":42,"line":323},[40,7231,7201],{"class":58},[40,7233,400],{"class":54},[40,7235,6828],{"class":50},[40,7237,62],{"class":54},[40,7239,7240],{"class":65}," 0.5",[40,7242,7243],{"class":319},"  # 慢动作\n",[10,7245,7246,7247,7249,7250,20],{},"这是 Godot 的\"全局慢放\u002F暂停\"开关，影响所有用 ",[37,7248,1919],{}," 计算的逻辑（包括小鸟下落、水管移动、Timer 等）。",[17,7251,7252],{},"比一个一个写\"如果暂停就别动\"省心得多",[687,7254,7255],{"id":7255},"信号的连接",[10,7257,7258,7261,7262,20],{},[37,7259,7260],{},"GameManager.state_changed.connect(_on_game_state)"," — 这就是",[17,7263,7264],{},"订阅信号",[267,7266,7267],{},[10,7268,7269],{},"你也可以在编辑器的【节点】面板里手动连接信号，效果一样。代码连接的好处是更明确、便于版本管理。",[22,7271,6447],{"id":7272},"分数-ui",[10,7274,7275,7277,7278,299],{},[37,7276,6695],{}," 挂个脚本 ",[37,7279,7280],{},"score_label.gd",[30,7282,7284],{"className":32,"code":7283,"language":34,"meta":35,"style":35},"extends Label\n\nfunc _ready() -> void:\n    text = \"0\"\n    GameManager.score_changed.connect(_on_score_changed)\n\nfunc _on_score_changed(new_score: int) -> void:\n    text = str(new_score)\n",[37,7285,7286,7293,7297,7311,7321,7340,7344,7367],{"__ignoreMap":35},[40,7287,7288,7290],{"class":42,"line":43},[40,7289,310],{"class":309},[40,7291,7292],{"class":58}," Label\n",[40,7294,7295],{"class":42,"line":316},[40,7296,327],{"emptyLinePlaceholder":326},[40,7298,7299,7301,7303,7305,7307,7309],{"class":42,"line":323},[40,7300,354],{"class":46},[40,7302,1075],{"class":357},[40,7304,361],{"class":54},[40,7306,364],{"class":46},[40,7308,367],{"class":58},[40,7310,370],{"class":54},[40,7312,7313,7316,7318],{"class":42,"line":330},[40,7314,7315],{"class":50},"    text",[40,7317,62],{"class":54},[40,7319,7320],{"class":342}," \"0\"\n",[40,7322,7323,7325,7327,7329,7331,7333,7335,7338],{"class":42,"line":346},[40,7324,622],{"class":58},[40,7326,400],{"class":54},[40,7328,5623],{"class":50},[40,7330,400],{"class":393},[40,7332,1113],{"class":357},[40,7334,433],{"class":54},[40,7336,7337],{"class":50},"_on_score_changed",[40,7339,449],{"class":54},[40,7341,7342],{"class":42,"line":351},[40,7343,327],{"emptyLinePlaceholder":326},[40,7345,7346,7348,7351,7353,7355,7357,7359,7361,7363,7365],{"class":42,"line":373},[40,7347,354],{"class":46},[40,7349,7350],{"class":357}," _on_score_changed",[40,7352,433],{"class":54},[40,7354,5138],{"class":393},[40,7356,55],{"class":54},[40,7358,59],{"class":58},[40,7360,522],{"class":54},[40,7362,364],{"class":46},[40,7364,367],{"class":58},[40,7366,370],{"class":54},[40,7368,7369,7371,7373,7376,7378,7380],{"class":42,"line":387},[40,7370,7315],{"class":50},[40,7372,62],{"class":54},[40,7374,7375],{"class":357}," str",[40,7377,433],{"class":54},[40,7379,5138],{"class":50},[40,7381,449],{"class":54},[774,7383,7384,7393],{},[777,7385,7386,7389,7390,7392],{},[37,7387,7388],{},"_ready"," 里订阅 ",[37,7391,5623],{}," 信号",[777,7394,7395],{},"每次分数变化，自动更新 Label 文本",[10,7397,7398,7401],{},[17,7399,7400],{},"这就是信号解耦的好处"," — Label 不需要主动去问 GameManager 当前几分，GameManager 也不知道有这么个 Label 存在，但分数照样能正确显示。",[22,7403,7404],{"id":7404},"结束面板",[10,7406,7407,7409,7410,7413],{},[37,7408,6704],{}," 默认隐藏（在编辑器把 ",[37,7411,7412],{},"visible"," 取消勾选），监听到 GAME_OVER 状态时才显示。",[10,7415,7416,7417,299],{},"挂脚本 ",[37,7418,7419],{},"game_over_panel.gd",[30,7421,7423],{"className":32,"code":7422,"language":34,"meta":35,"style":35},"extends Panel\n\n@onready var final_score: Label = $FinalScore\n\nfunc _ready() -> void:\n    visible = false\n    GameManager.state_changed.connect(_on_state_changed)\n\nfunc _on_state_changed(new_state) -> void:\n    if new_state == GameManager.GameState.GAME_OVER:\n        final_score.text = \"得分：%d\" % GameManager.total_score\n        visible = true\n",[37,7424,7425,7432,7436,7457,7461,7475,7485,7504,7508,7527,7547,7577],{"__ignoreMap":35},[40,7426,7427,7429],{"class":42,"line":43},[40,7428,310],{"class":309},[40,7430,7431],{"class":58}," Panel\n",[40,7433,7434],{"class":42,"line":316},[40,7435,327],{"emptyLinePlaceholder":326},[40,7437,7438,7440,7442,7445,7447,7450,7452,7454],{"class":42,"line":323},[40,7439,3990],{"class":357},[40,7441,1855],{"class":46},[40,7443,7444],{"class":50}," final_score",[40,7446,55],{"class":54},[40,7448,7449],{"class":58}," Label",[40,7451,62],{"class":54},[40,7453,4005],{"class":309},[40,7455,7456],{"class":4008},"FinalScore\n",[40,7458,7459],{"class":42,"line":330},[40,7460,327],{"emptyLinePlaceholder":326},[40,7462,7463,7465,7467,7469,7471,7473],{"class":42,"line":346},[40,7464,354],{"class":46},[40,7466,1075],{"class":357},[40,7468,361],{"class":54},[40,7470,364],{"class":46},[40,7472,367],{"class":58},[40,7474,370],{"class":54},[40,7476,7477,7480,7482],{"class":42,"line":351},[40,7478,7479],{"class":50},"    visible",[40,7481,62],{"class":54},[40,7483,7484],{"class":309}," false\n",[40,7486,7487,7489,7491,7493,7495,7497,7499,7502],{"class":42,"line":373},[40,7488,622],{"class":58},[40,7490,400],{"class":54},[40,7492,5630],{"class":50},[40,7494,400],{"class":393},[40,7496,1113],{"class":357},[40,7498,433],{"class":54},[40,7500,7501],{"class":50},"_on_state_changed",[40,7503,449],{"class":54},[40,7505,7506],{"class":42,"line":387},[40,7507,327],{"emptyLinePlaceholder":326},[40,7509,7510,7512,7515,7517,7519,7521,7523,7525],{"class":42,"line":409},[40,7511,354],{"class":46},[40,7513,7514],{"class":357}," _on_state_changed",[40,7516,433],{"class":54},[40,7518,5152],{"class":393},[40,7520,522],{"class":54},[40,7522,364],{"class":46},[40,7524,367],{"class":58},[40,7526,370],{"class":54},[40,7528,7529,7531,7533,7535,7537,7539,7541,7543,7545],{"class":42,"line":415},[40,7530,505],{"class":309},[40,7532,7056],{"class":50},[40,7534,603],{"class":46},[40,7536,3849],{"class":58},[40,7538,400],{"class":54},[40,7540,1169],{"class":50},[40,7542,400],{"class":54},[40,7544,1174],{"class":309},[40,7546,370],{"class":393},[40,7548,7549,7552,7554,7556,7558,7561,7564,7567,7570,7572,7574],{"class":42,"line":452},[40,7550,7551],{"class":50},"        final_score",[40,7553,400],{"class":54},[40,7555,6468],{"class":50},[40,7557,62],{"class":54},[40,7559,7560],{"class":342}," \"得分：",[40,7562,7563],{"class":4008},"%d",[40,7565,7566],{"class":342},"\"",[40,7568,7569],{"class":46}," %",[40,7571,3849],{"class":58},[40,7573,400],{"class":54},[40,7575,7576],{"class":50},"total_score\n",[40,7578,7579,7582,7584],{"class":42,"line":481},[40,7580,7581],{"class":50},"        visible",[40,7583,62],{"class":54},[40,7585,7586],{"class":309}," true\n",[774,7588,7589,7594,7597],{},[777,7590,7591,7592],{},"监听 ",[37,7593,5630],{},[777,7595,7596],{},"GAME_OVER 时显示面板 + 填上最终分数",[777,7598,7599,7601,7602],{},[37,7600,7563],{}," 是字符串占位符，等同于 ",[37,7603,7604],{},"str(GameManager.total_score)",[267,7606,7607],{},[10,7608,7609,7610,1746,7612,7614,7615,20],{},"⚠️ 别忘了把 ",[37,7611,6727],{},[37,7613,6548],{}," 信号连接到 main 场景根节点的 ",[37,7616,7168],{},[22,7618,7619],{"id":7619},"设置主场景",[10,7621,7622,7623,7625],{},"最后把【主场景】改成 ",[37,7624,6432],{},"（这样游戏启动直接到菜单）：",[10,7627,7628],{},"路径：项目 → 项目设置 → 常规 → 运行 → 主场景",[10,7630,7631],{},[1509,7632],{"alt":7633,"src":7634},"06-ui-主场景设置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F06-ui-%E4%B8%BB%E5%9C%BA%E6%99%AF%E8%AE%BE%E7%BD%AE.png",[22,7636,2914],{"id":2914},[10,7638,7639],{},"按下运行键，整个流程应该是：",[1373,7641,7642,7645,7648,7651,7654],{},[777,7643,7644],{},"✅ 启动 → 显示菜单 + 开始按钮",[777,7646,7647],{},"✅ 点击开始 → 切换到游戏场景，画面冻结，等待按键",[777,7649,7650],{},"✅ 按下 fly → 游戏开始，水管生成，小鸟可控",[777,7652,7653],{},"✅ 撞死 → 显示结束面板 + 最终分数",[777,7655,7656],{},"✅ 点重新开始 → 回到游戏初始状态",[10,7658,7659],{},"下一章我们替换美术资源，把这些灰色矩形变成真正的小鸟和水管 🎨",[1444,7661,7662],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}",{"title":35,"searchDepth":316,"depth":316,"links":7664},[7665,7666,7670,7672,7679,7680,7681,7682],{"id":6457,"depth":316,"text":6457},{"id":6473,"depth":316,"text":7667,"children":7668},"开始菜单 menu.tscn",[7669],{"id":6536,"depth":323,"text":6536},{"id":6611,"depth":316,"text":7671},"游戏主场景 main.tscn",{"id":6737,"depth":316,"text":7673,"children":7674},"主场景脚本 main.gd",[7675,7676,7678],{"id":4196,"depth":323,"text":4197},{"id":7187,"depth":323,"text":7677},"Engine.time_scale — 全局时间缩放",{"id":7255,"depth":323,"text":7255},{"id":7272,"depth":316,"text":6447},{"id":7404,"depth":316,"text":7404},{"id":7619,"depth":316,"text":7619},{"id":2914,"depth":316,"text":2914},{},"\u002Fdevlog\u002Fxggame-bird\u002F06-ui",{"title":6416,"description":1474},"devlog\u002Fxggame-bird\u002F06-ui","MaW5eGhaEWyzREhq8HxUBpIIw5GmRnx7tB25QX3pkfw",{"id":7689,"title":7690,"body":7691,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":8234,"navigation":326,"path":6659,"seo":8235,"stem":8236,"toc":326,"__hash__":8237},"devlog\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets.md","替换美术资源",{"type":7,"value":7692,"toc":8222},[7693,7696,7699,7714,7717,7762,7765,7791,7797,7801,7811,7817,7823,7826,7840,7846,7855,7864,7867,7880,7886,7897,7900,7904,7911,7924,7927,7932,7950,7956,7961,8021,8026,8029,8037,8056,8070,8076,8087,8090,8098,8177,8184,8186,8189,8203,8209,8212,8219],[10,7694,7695],{},"到目前为止小鸟还是 Godot 的\"机器人头\"，水管也是个矩形 — 这一章给游戏换上真正的美术资源，从「白板原型」一秒变成「真游戏」。✨",[10,7697,7698],{},"我们要做：",[1373,7700,7701,7704,7707],{},[777,7702,7703],{},"准备 + 导入美术素材",[777,7705,7706],{},"替换小鸟、水管的精灵图",[777,7708,7709,7710,7713],{},"加上",[17,7711,7712],{},"滚动","的地面和背景（这是 Flappy Bird \"横向飞行错觉\"的灵魂）",[22,7715,7716],{"id":7716},"准备美术资源",[267,7718,7719,7732,7735,7738],{},[10,7720,7721,7722,7731],{},"这个游戏的",[17,7723,7724,7725,7730],{},"像素美术全部是我自己用 ",[183,7726,7729],{"href":7727,"rel":7728},"https:\u002F\u002Fwww.aseprite.org\u002F",[1502],"Aseprite"," 画的"," — 小鸟、水管、背景、地面、UI 按钮和引导手指。",[10,7733,7734],{},"画像素画其实门槛比想象中低：小鸟就是 16×16 的一小块，水管更简单，纯色矩形加个边就行。第一次画不好看也没关系，先跑起来比先画好看重要得多。",[10,7736,7737],{},"如果你不想自己画，可以从这些免费站找：",[774,7739,7740,7747,7755],{},[777,7741,7742],{},[183,7743,7746],{"href":7744,"rel":7745},"https:\u002F\u002Fitch.io\u002Fgame-assets\u002Ffree",[1502],"itch.io 免费素材",[777,7748,7749,7754],{},[183,7750,7753],{"href":7751,"rel":7752},"https:\u002F\u002Fkenney.nl\u002F",[1502],"Kenney.nl","（CC0 免版权）",[777,7756,7757],{},[183,7758,7761],{"href":7759,"rel":7760},"https:\u002F\u002Fopengameart.org\u002F",[1502],"OpenGameArt",[10,7763,7764],{},"需要这几样：",[774,7766,7767,7773,7779,7785],{},[777,7768,7769,7772],{},[37,7770,7771],{},"bird.png"," — 小鸟",[777,7774,7775,7778],{},[37,7776,7777],{},"pipe.png"," — 水管",[777,7780,7781,7784],{},[37,7782,7783],{},"progress.png"," — 跑道",[777,7786,7787,7790],{},[37,7788,7789],{},"bg.png"," — 背景（最好做成左右无缝拼接的）",[10,7792,7793],{},[1509,7794],{"alt":7795,"src":7796},"07-art-assets-素材","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E7%B4%A0%E6%9D%90.png",[22,7798,7800],{"id":7799},"导入设置像素美术别变模糊","导入设置：像素美术别变模糊",[10,7802,4595,7803,7806,7807,7810],{},[37,7804,7805],{},".png"," 文件拖进 Godot 项目，默认导入会用",[17,7808,7809],{},"双线性插值","把像素图变得模糊糊的，这不是我们要的复古感觉。",[10,7812,7813],{},[1509,7814],{"alt":7815,"src":7816},"07-art-assets-导入设置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E5%AF%BC%E5%85%A5%E8%AE%BE%E7%BD%AE.png",[10,7818,7819,7820,20],{},"项目设置 → 渲染 → 纹理 → ",[37,7821,7822],{},"画布纹理 > 默认纹理过滤 = Nearest",[22,7824,7825],{"id":7825},"替换小鸟",[10,7827,7828,7829,7831,7832,7834,7835,7837,7838,20],{},"打开 ",[37,7830,2796],{},"，选中 ",[37,7833,1733],{}," 节点，把它的 ",[37,7836,1749],{}," 换成 ",[37,7839,7771],{},[10,7841,7842],{},[1509,7843],{"alt":7844,"src":7845},"07-art-assets-小鸟替换","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E5%B0%8F%E9%B8%9F%E6%9B%BF%E6%8D%A2.png",[10,7847,7848,7851,7852,7854],{},[17,7849,7850],{},"碰撞体也要对应调整","：选中 ",[37,7853,1739],{},"，把圆形缩放到正好包住小鸟。",[267,7856,7857],{},[10,7858,7859,7860,7863],{},"💡 小贴士：碰撞体可以",[17,7861,7862],{},"略小于图片","（特别是小鸟）。这样玩家会觉得\"我明明擦边过去了\"的判定更宽容，手感更好。这是 Flappy Bird 类游戏的隐藏秘诀之一。",[22,7865,7866],{"id":7866},"替换水管",[10,7868,7828,7869,7871,7872,7874,7875,7877,7878,20],{},[37,7870,4598],{},"，选中两个 ",[37,7873,1733],{},"（上下水管），把它们的 ",[37,7876,1749],{}," 都换成 ",[37,7879,7777],{},[10,7881,7882],{},[1509,7883],{"alt":7884,"src":7885},"07-art-assets-水管替换","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E6%B0%B4%E7%AE%A1%E6%9B%BF%E6%8D%A2.png",[10,7887,7888,7889,7892,7893,7896],{},"上水管要",[17,7890,7891],{},"垂直翻转","（在 Inspector 里勾选 ",[37,7894,7895],{},"Flip V","）。",[10,7898,7899],{},"碰撞体同样调整到包住水管的大小。",[22,7901,7903],{"id":7902},"滚动地面-背景重头戏","滚动地面 + 背景（重头戏 🎬）",[10,7905,7906,7907,7910],{},"Flappy Bird 的精髓 — 小鸟其实在原地扑腾，",[17,7908,7909],{},"背景和地面在向左移动","，制造出\"鸟在向右飞\"的错觉。",[10,7912,7913,7914,7916,7917,7919,7920,7923],{},"Godot 4.3+ 有专门的 ",[37,7915,6652],{}," 节点，自带 ",[37,7918,7082],{},"（自动滚动）和 ",[37,7921,7922],{},"repeat","（无缝循环），非常好用。",[687,7925,7926],{"id":7926},"添加滚动背景",[10,7928,2963,7929,7931],{},[37,7930,6441],{}," 的根节点下添加：",[774,7933,7934],{},[777,7935,7936,6508,7938,3483,7940],{},[37,7937,6652],{},[37,7939,6655],{},[774,7941,7942],{},[777,7943,7944,875,7946,2619,7948],{},[37,7945,1733],{},[37,7947,1749],{},[37,7949,7789],{},[10,7951,7952],{},[1509,7953],{"alt":7954,"src":7955},"07-art-assets-背景节点","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E8%83%8C%E6%99%AF%E8%8A%82%E7%82%B9.png",[10,7957,1780,7958,7960],{},[37,7959,6655],{},"，在 Inspector 里设置：",[108,7962,7963,7974],{},[111,7964,7965],{},[114,7966,7967,7970,7972],{},[117,7968,7969],{},"属性",[117,7971,1579],{},[117,7973,1635],{},[127,7975,7976,7992,8005],{},[114,7977,7978,7983,7989],{},[132,7979,7980],{},[37,7981,7982],{},"Autoscroll > x",[132,7984,7985,7988],{},[37,7986,7987],{},"-50","（或你喜欢的速度）",[132,7990,7991],{},"每秒向左移动多少像素",[114,7993,7994,7999,8002],{},[132,7995,7996],{},[37,7997,7998],{},"Repeat Size > x",[132,8000,8001],{},"背景图宽度",[132,8003,8004],{},"让它无缝循环",[114,8006,8007,8012,8018],{},[132,8008,8009],{},[37,8010,8011],{},"Repeat Times",[132,8013,8014,8017],{},[37,8015,8016],{},"3"," 或更大",[132,8019,8020],{},"重复多少次（保证屏幕外有备用）",[267,8022,8023],{},[10,8024,8025],{},"负值表示向左滚动；正值表示向右。",[687,8027,8028],{"id":8028},"添加滚动地面",[10,8030,8031,8032,6508,8034,8036],{},"同样的方式，加一个 ",[37,8033,6652],{},[37,8035,6667],{},"）：",[774,8038,8039],{},[777,8040,8041,6429,8043,3483,8045],{},[37,8042,6652],{},[37,8044,6667],{},[774,8046,8047],{},[777,8048,8049,875,8051,2619,8053],{},[37,8050,1733],{},[37,8052,1749],{},[37,8054,8055],{},"ground.png",[10,8057,8058,8061,8062,8065,8066,8069],{},[37,8059,8060],{},"Autoscroll.x"," 可以",[17,8063,8064],{},"比背景快","（比如 ",[37,8067,8068],{},"-150","），制造\"近景快、远景慢\"的视差感（这就是 Parallax 这个词的来源）。",[10,8071,8072],{},[1509,8073],{"alt":8074,"src":8075},"07-art-assets-地面节点","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E5%9C%B0%E9%9D%A2%E8%8A%82%E7%82%B9.png",[267,8077,8078],{},[10,8079,8080,8081,8083,8084,8086],{},"💡 这两个节点的 z-index 注意一下：背景 ",[37,8082,6655],{}," 要在最底层（z 最小），地面 ",[37,8085,6667],{}," 在小鸟之上（z 最大，遮住可能的视觉穿帮）。",[22,8088,8089],{"id":8089},"让背景在游戏结束时停下来",[10,8091,8092,8093,8097],{},"还记得 ",[183,8094,8096],{"href":8095},"\u002Fdevlog\u002Fxggame-bird\u002F06-ui#%E4%B8%BB%E5%9C%BA%E6%99%AF%E8%84%9A%E6%9C%AC-maingd","上一章 main.gd"," 里这两行吗？",[30,8099,8101],{"className":32,"code":8100,"language":34,"meta":35,"style":35},"func _on_game_state(new_state) -> void:\n    if new_state == GameManager.GameState.GAME_OVER:\n        $BG.autoscroll.x = 0\n        $Progress.autoscroll.x = 0\n",[37,8102,8103,8121,8141,8159],{"__ignoreMap":35},[40,8104,8105,8107,8109,8111,8113,8115,8117,8119],{"class":42,"line":43},[40,8106,354],{"class":46},[40,8108,7032],{"class":357},[40,8110,433],{"class":54},[40,8112,5152],{"class":393},[40,8114,522],{"class":54},[40,8116,364],{"class":46},[40,8118,367],{"class":58},[40,8120,370],{"class":54},[40,8122,8123,8125,8127,8129,8131,8133,8135,8137,8139],{"class":42,"line":316},[40,8124,505],{"class":309},[40,8126,7056],{"class":50},[40,8128,603],{"class":46},[40,8130,3849],{"class":58},[40,8132,400],{"class":54},[40,8134,1169],{"class":50},[40,8136,400],{"class":54},[40,8138,1174],{"class":309},[40,8140,370],{"class":393},[40,8142,8143,8145,8147,8149,8151,8153,8155,8157],{"class":42,"line":323},[40,8144,7075],{"class":309},[40,8146,6655],{"class":4008},[40,8148,400],{"class":54},[40,8150,7082],{"class":50},[40,8152,400],{"class":54},[40,8154,2455],{"class":50},[40,8156,62],{"class":54},[40,8158,66],{"class":65},[40,8160,8161,8163,8165,8167,8169,8171,8173,8175],{"class":42,"line":330},[40,8162,7075],{"class":309},[40,8164,6667],{"class":4008},[40,8166,400],{"class":54},[40,8168,7082],{"class":50},[40,8170,400],{"class":54},[40,8172,2455],{"class":50},[40,8174,62],{"class":54},[40,8176,66],{"class":65},[10,8178,8179,8180,8183],{},"它做的就是：",[17,8181,8182],{},"游戏结束时把背景\u002F地面的滚动速度设为 0","。这样画面整体定住，更有\"撞死\"的仪式感。",[22,8185,2914],{"id":2914},[10,8187,8188],{},"现在游戏应该长这样：",[774,8190,8191,8194,8197,8200],{},[777,8192,8193],{},"✅ 真正的小鸟在画面里扑腾",[777,8195,8196],{},"✅ 水管是绿色（或你的素材颜色）的管子",[777,8198,8199],{},"✅ 背景缓缓向左滚动 + 地面快速向左滚动 → 视差错觉",[777,8201,8202],{},"✅ 撞死瞬间，背景立刻定住",[10,8204,8205],{},[1509,8206],{"alt":8207,"src":8208},"07-art-assets-效果","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E6%95%88%E6%9E%9C.png",[10,8210,8211],{},"是不是一瞬间像个游戏了？🎮",[10,8213,8214,8215,8218],{},"下一章给它加上",[17,8216,8217],{},"音效"," — 翅膀扇动、得分、碰撞，最后一点\"灵魂\"。",[1444,8220,8221],{},"html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":35,"searchDepth":316,"depth":316,"links":8223},[8224,8225,8226,8227,8228,8232,8233],{"id":7716,"depth":316,"text":7716},{"id":7799,"depth":316,"text":7800},{"id":7825,"depth":316,"text":7825},{"id":7866,"depth":316,"text":7866},{"id":7902,"depth":316,"text":7903,"children":8229},[8230,8231],{"id":7926,"depth":323,"text":7926},{"id":8028,"depth":323,"text":8028},{"id":8089,"depth":316,"text":8089},{"id":2914,"depth":316,"text":2914},{},{"title":7690,"description":1474},"devlog\u002Fxggame-bird\u002F07-art-assets","qtF7fMJTBYiFgezflU7I_VErLrbV0KYDVkhUn3TQMRo",{"id":8239,"title":8240,"body":8241,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":9070,"navigation":326,"path":9071,"seo":9072,"stem":9073,"toc":326,"__hash__":9074},"devlog\u002Fdevlog\u002Fxggame-bird\u002F08-sound.md","音效 AudioManager",{"type":7,"value":8242,"toc":9050},[8243,8246,8269,8272,8318,8325,8345,8351,8363,8371,8378,8400,8404,8410,8412,8455,8461,8465,8468,8487,8491,8497,8651,8666,8670,8675,8698,8704,8713,8716,8719,8725,8775,8781,8886,8892,8977,8979,8982,8993,9006,9010,9020,9041,9044,9047],[10,8244,8245],{},"游戏没声音就像炒菜没盐 — 没那个味儿。这一章加上：",[774,8247,8248,8255,8262],{},[777,8249,8250,8251,8254],{},"🪶 ",[17,8252,8253],{},"翅膀扇动声","（小鸟跳跃时）",[777,8256,8257,8258,8261],{},"⭐ ",[17,8259,8260],{},"得分声","（穿过水管时）",[777,8263,8264,8265,8268],{},"💥 ",[17,8266,8267],{},"碰撞声","（撞死时）",[22,8270,8271],{"id":8271},"准备音效文件",[267,8273,8274,8287,8299],{},[10,8275,8276,8277,8286],{},"这三个音效",[17,8278,8279,8280,8285],{},"是我自己用 ",[183,8281,8284],{"href":8282,"rel":8283},"https:\u002F\u002Fsfbgames.itch.io\u002Fchiptone",[1502],"ChipTone"," 生成的"," — 浏览器打开就能用，不用注册。",[10,8288,8289,8292,8293,8298],{},[17,8290,8291],{},"小游戏的音效建议「生成」而不是「找」"," — 生成出来的版权归自己，不用记来源、不用署名，\n而且 8-bit 音色天生就贴复古像素风。同类工具还有 ",[183,8294,8297],{"href":8295,"rel":8296},"https:\u002F\u002Fsfxr.me\u002F",[1502],"sfxr","（点一下随机生成，更快）。",[10,8300,8301,8302,8307,8308,8313,8314,8317],{},"真要去下载的话：",[183,8303,8306],{"href":8304,"rel":8305},"https:\u002F\u002Fpixabay.com\u002Fsound-effects\u002F",[1502],"Pixabay","（免署名最省心）、\n",[183,8309,8312],{"href":8310,"rel":8311},"https:\u002F\u002Ffreesound.org\u002F",[1502],"freesound","（库最深，但",[17,8315,8316],{},"必须筛 CC0","，很多是 CC-BY 要署名）。",[10,8319,8320,8321,8324],{},"需要 3 个文件，放到 ",[37,8322,8323],{},"audio\u002F"," 目录下：",[774,8326,8327,8333,8339],{},[777,8328,8329,8332],{},[37,8330,8331],{},"wing.wav"," — 扑翅膀",[777,8334,8335,8338],{},[37,8336,8337],{},"score.wav"," — 得分",[777,8340,8341,8344],{},[37,8342,8343],{},"strike.wav"," — 撞击",[10,8346,8347],{},[1509,8348],{"alt":8349,"src":8350},"08-sound-音效文件","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F08-sound-%E9%9F%B3%E6%95%88%E6%96%87%E4%BB%B6.png",[267,8352,8353],{},[10,8354,8355,8356,707,8359,8362],{},"💡 推荐用 ",[37,8357,8358],{},".wav",[37,8360,8361],{},".mp3","：体积大一点，但延迟低、循环不卡顿。短音效用 wav 是标配。",[22,8364,8366,8367,8370],{"id":8365},"audiomanager-是个场景不是脚本","AudioManager 是个",[17,8368,8369],{},"场景","，不是脚本",[10,8372,8373,8374,8377],{},"和 GameManager 不一样，AudioManager 我们用 ",[17,8375,8376],{},"场景（.tscn）"," 来做，而不是单纯的脚本。",[10,8379,8380,8383,8384,8387,8388,8391,8392,8395,8396,8399],{},[17,8381,8382],{},"为什么？"," 因为每个音效都需要一个 ",[37,8385,8386],{},"AudioStreamPlayer"," 节点 — 与其在脚本里 ",[37,8389,8390],{},"new AudioStreamPlayer()","，不如直接在场景里把所有 player 节点摆好，",[17,8393,8394],{},"音效文件也提前在 Inspector 里预设好","，调用时直接 ",[37,8397,8398],{},".play()"," 就行。",[22,8401,8403],{"id":8402},"创建-audiomanager-场景","创建 AudioManager 场景",[10,8405,8406,8407,20],{},"新建场景，保存为 ",[37,8408,8409],{},"scenes\u002Fautoload\u002Faudio_manager.tscn",[10,8411,2761],{},[774,8413,8414],{},[777,8415,8416,6508,8419,8421,8422],{},[37,8417,8418],{},"Node",[37,8420,3904],{},"）— 根节点\n",[774,8423,8424],{},[777,8425,8426,6508,8428,8431,8432],{},[37,8427,8418],{},[37,8429,8430],{},"SFX","）— 音效组\n",[774,8433,8434,8441,8448],{},[777,8435,8436,6508,8438,3577],{},[37,8437,8386],{},[37,8439,8440],{},"wing_sfx",[777,8442,8443,6508,8445,3577],{},[37,8444,8386],{},[37,8446,8447],{},"score_sfx",[777,8449,8450,6508,8452,3577],{},[37,8451,8386],{},[37,8453,8454],{},"strike_sfx",[10,8456,8457],{},[1509,8458],{"alt":8459,"src":8460},"08-sound-scene-结构","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F08-sound-scene-%E7%BB%93%E6%9E%84.png",[687,8462,8464],{"id":8463},"每个-audiostreamplayer-的设置","每个 AudioStreamPlayer 的设置",[10,8466,8467],{},"选中每个 player，在右侧 Inspector 里：",[774,8469,8470,8481],{},[777,8471,8472,8475,8476,4599,8478,8480],{},[17,8473,8474],{},"Stream","：拖入对应的音效文件（如 ",[37,8477,8331],{},[37,8479,8440],{}," 的 Stream 槽）",[777,8482,8483,8486],{},[17,8484,8485],{},"Volume dB","：根据音量调整（-6 dB 是温和的衰减，-12 dB 是明显的降低）",[22,8488,8490],{"id":8489},"audiomanager-脚本","AudioManager 脚本",[10,8492,8493,8494,299],{},"给根节点挂脚本 ",[37,8495,8496],{},"audio_manager.gd",[30,8498,8500],{"className":32,"code":8499,"language":34,"meta":35,"style":35},"extends Node\n\n# 在检查器里预载好你的音效文件\n@onready var score_sfx: AudioStreamPlayer = $SFX\u002Fscore_sfx\n@onready var strike_sfx: AudioStreamPlayer = $SFX\u002Fstrike_sfx\n@onready var wing_sfx: AudioStreamPlayer = $SFX\u002Fwing_sfx\n\nfunc play_score():\n    score_sfx.play()\n\nfunc play_die():\n    strike_sfx.play()\n\nfunc play_wing():\n    wing_sfx.play()\n",[37,8501,8502,8508,8512,8517,8538,8558,8578,8582,8591,8603,8607,8616,8627,8631,8640],{"__ignoreMap":35},[40,8503,8504,8506],{"class":42,"line":43},[40,8505,310],{"class":309},[40,8507,313],{"class":58},[40,8509,8510],{"class":42,"line":316},[40,8511,327],{"emptyLinePlaceholder":326},[40,8513,8514],{"class":42,"line":323},[40,8515,8516],{"class":319},"# 在检查器里预载好你的音效文件\n",[40,8518,8519,8521,8523,8526,8528,8531,8533,8535],{"class":42,"line":330},[40,8520,3990],{"class":357},[40,8522,1855],{"class":46},[40,8524,8525],{"class":50}," score_sfx",[40,8527,55],{"class":54},[40,8529,8530],{"class":58}," AudioStreamPlayer",[40,8532,62],{"class":54},[40,8534,4005],{"class":309},[40,8536,8537],{"class":4008},"SFX\u002Fscore_sfx\n",[40,8539,8540,8542,8544,8547,8549,8551,8553,8555],{"class":42,"line":346},[40,8541,3990],{"class":357},[40,8543,1855],{"class":46},[40,8545,8546],{"class":50}," strike_sfx",[40,8548,55],{"class":54},[40,8550,8530],{"class":58},[40,8552,62],{"class":54},[40,8554,4005],{"class":309},[40,8556,8557],{"class":4008},"SFX\u002Fstrike_sfx\n",[40,8559,8560,8562,8564,8567,8569,8571,8573,8575],{"class":42,"line":351},[40,8561,3990],{"class":357},[40,8563,1855],{"class":46},[40,8565,8566],{"class":50}," wing_sfx",[40,8568,55],{"class":54},[40,8570,8530],{"class":58},[40,8572,62],{"class":54},[40,8574,4005],{"class":309},[40,8576,8577],{"class":4008},"SFX\u002Fwing_sfx\n",[40,8579,8580],{"class":42,"line":373},[40,8581,327],{"emptyLinePlaceholder":326},[40,8583,8584,8586,8589],{"class":42,"line":387},[40,8585,354],{"class":46},[40,8587,8588],{"class":357}," play_score",[40,8590,1159],{"class":54},[40,8592,8593,8596,8598,8601],{"class":42,"line":409},[40,8594,8595],{"class":50},"    score_sfx",[40,8597,400],{"class":393},[40,8599,8600],{"class":357},"play",[40,8602,2070],{"class":54},[40,8604,8605],{"class":42,"line":415},[40,8606,327],{"emptyLinePlaceholder":326},[40,8608,8609,8611,8614],{"class":42,"line":452},[40,8610,354],{"class":46},[40,8612,8613],{"class":357}," play_die",[40,8615,1159],{"class":54},[40,8617,8618,8621,8623,8625],{"class":42,"line":481},[40,8619,8620],{"class":50},"    strike_sfx",[40,8622,400],{"class":393},[40,8624,8600],{"class":357},[40,8626,2070],{"class":54},[40,8628,8629],{"class":42,"line":486},[40,8630,327],{"emptyLinePlaceholder":326},[40,8632,8633,8635,8638],{"class":42,"line":502},[40,8634,354],{"class":46},[40,8636,8637],{"class":357}," play_wing",[40,8639,1159],{"class":54},[40,8641,8642,8645,8647,8649],{"class":42,"line":527},[40,8643,8644],{"class":50},"    wing_sfx",[40,8646,400],{"class":393},[40,8648,8600],{"class":357},[40,8650,2070],{"class":54},[774,8652,8653,8659],{},[777,8654,8655,8658],{},[37,8656,8657],{},"@onready var xxx = $SFX\u002Fxxx"," — 拿到子节点的引用",[777,8660,8661,8662,8665],{},"方法本身就一行：调用对应 player 的 ",[37,8663,8664],{},"play()"," 即可",[22,8667,8669],{"id":8668},"注册为-autoload","注册为 Autoload",[10,8671,8672,8673,299],{},"和 GameManager 一样，",[17,8674,5393],{},[108,8676,8677,8685],{},[111,8678,8679],{},[114,8680,8681,8683],{},[117,8682,119],{},[117,8684,5414],{},[127,8686,8687],{},[114,8688,8689,8694],{},[132,8690,8691],{},[37,8692,8693],{},"res:\u002F\u002Fscenes\u002Fautoload\u002Faudio_manager.tscn",[132,8695,8696],{},[37,8697,3904],{},[10,8699,8700],{},[1509,8701],{"alt":8702,"src":8703},"08-sound-autoload","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F08-sound-autoload.png",[267,8705,8706],{},[10,8707,8708,8709,8712],{},"⚠️ 注意这里加载的是 ",[17,8710,8711],{},"场景文件 (.tscn)","，不是脚本 (.gd)。Godot 的 Autoload 同时支持两者。",[22,8714,8715],{"id":8715},"在游戏里调用",[10,8717,8718],{},"现在任何节点都能直接喊 AudioManager 一声 — 在合适的位置加上音效调用：",[687,8720,8722,8723,3577],{"id":8721},"小鸟跳跃时birdgd","小鸟跳跃时（",[37,8724,2966],{},[30,8726,8728],{"className":32,"code":8727,"language":34,"meta":35,"style":35},"if Input.is_action_just_pressed(\"fly\"):\n    velocity.y = jump_force\n    AudioManager.play_wing()  # 👈 加这一行\n",[37,8729,8730,8748,8760],{"__ignoreMap":35},[40,8731,8732,8734,8736,8738,8740,8742,8744,8746],{"class":42,"line":43},[40,8733,910],{"class":309},[40,8735,2031],{"class":58},[40,8737,400],{"class":393},[40,8739,2036],{"class":357},[40,8741,433],{"class":54},[40,8743,2041],{"class":342},[40,8745,522],{"class":54},[40,8747,370],{"class":393},[40,8749,8750,8752,8754,8756,8758],{"class":42,"line":316},[40,8751,2392],{"class":50},[40,8753,400],{"class":54},[40,8755,1995],{"class":50},[40,8757,62],{"class":54},[40,8759,2058],{"class":50},[40,8761,8762,8765,8767,8770,8772],{"class":42,"line":323},[40,8763,8764],{"class":58},"    AudioManager",[40,8766,400],{"class":393},[40,8768,8769],{"class":357},"play_wing",[40,8771,361],{"class":54},[40,8773,8774],{"class":319},"  # 👈 加这一行\n",[687,8776,8778,8779,3577],{"id":8777},"穿过水管得分时pillar_pairgd","穿过水管得分时（",[37,8780,5853],{},[30,8782,8784],{"className":32,"code":8783,"language":34,"meta":35,"style":35},"func _on_goal_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\":\n        AudioManager.play_score()  # 👈 加这一行\n        if GameManager.has_method(\"add_score\"):\n            GameManager.add_score(1)\n        goal.set_deferred(\"monitoring\", false)\n",[37,8785,8786,8808,8824,8836,8854,8868],{"__ignoreMap":35},[40,8787,8788,8790,8792,8794,8796,8798,8800,8802,8804,8806],{"class":42,"line":43},[40,8789,354],{"class":46},[40,8791,4107],{"class":357},[40,8793,433],{"class":54},[40,8795,3713],{"class":393},[40,8797,55],{"class":54},[40,8799,3718],{"class":58},[40,8801,522],{"class":54},[40,8803,364],{"class":46},[40,8805,367],{"class":58},[40,8807,370],{"class":54},[40,8809,8810,8812,8814,8816,8818,8820,8822],{"class":42,"line":316},[40,8811,505],{"class":309},[40,8813,3733],{"class":50},[40,8815,400],{"class":54},[40,8817,3738],{"class":50},[40,8819,603],{"class":46},[40,8821,3743],{"class":342},[40,8823,370],{"class":393},[40,8825,8826,8828,8830,8832,8834],{"class":42,"line":323},[40,8827,3874],{"class":58},[40,8829,400],{"class":393},[40,8831,4461],{"class":357},[40,8833,361],{"class":54},[40,8835,8774],{"class":319},[40,8837,8838,8840,8842,8844,8846,8848,8850,8852],{"class":42,"line":330},[40,8839,4389],{"class":309},[40,8841,3849],{"class":58},[40,8843,400],{"class":393},[40,8845,4474],{"class":357},[40,8847,433],{"class":54},[40,8849,4479],{"class":342},[40,8851,522],{"class":54},[40,8853,370],{"class":393},[40,8855,8856,8858,8860,8862,8864,8866],{"class":42,"line":346},[40,8857,4488],{"class":58},[40,8859,400],{"class":393},[40,8861,1118],{"class":357},[40,8863,433],{"class":54},[40,8865,4497],{"class":65},[40,8867,449],{"class":54},[40,8869,8870,8872,8874,8876,8878,8880,8882,8884],{"class":42,"line":351},[40,8871,4170],{"class":50},[40,8873,400],{"class":393},[40,8875,4175],{"class":357},[40,8877,433],{"class":54},[40,8879,4180],{"class":342},[40,8881,439],{"class":54},[40,8883,4185],{"class":309},[40,8885,449],{"class":54},[687,8887,8889,8890,3577],{"id":8888},"撞死时killzonegd","撞死时（",[37,8891,5756],{},[30,8893,8895],{"className":32,"code":8894,"language":34,"meta":35,"style":35},"func _on_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\" and GameManager.current_state == GameManager.GameState.PLAYING:\n        AudioManager.play_die()  # 👈 加这一行\n        GameManager.game_over()\n",[37,8896,8897,8919,8955,8967],{"__ignoreMap":35},[40,8898,8899,8901,8903,8905,8907,8909,8911,8913,8915,8917],{"class":42,"line":43},[40,8900,354],{"class":46},[40,8902,3708],{"class":357},[40,8904,433],{"class":54},[40,8906,3713],{"class":393},[40,8908,55],{"class":54},[40,8910,3718],{"class":58},[40,8912,522],{"class":54},[40,8914,364],{"class":46},[40,8916,367],{"class":58},[40,8918,370],{"class":54},[40,8920,8921,8923,8925,8927,8929,8931,8933,8935,8937,8939,8941,8943,8945,8947,8949,8951,8953],{"class":42,"line":316},[40,8922,505],{"class":309},[40,8924,3733],{"class":50},[40,8926,400],{"class":54},[40,8928,3738],{"class":50},[40,8930,603],{"class":46},[40,8932,3743],{"class":342},[40,8934,3846],{"class":46},[40,8936,3849],{"class":58},[40,8938,400],{"class":54},[40,8940,3854],{"class":50},[40,8942,603],{"class":46},[40,8944,3849],{"class":58},[40,8946,400],{"class":54},[40,8948,1169],{"class":50},[40,8950,400],{"class":54},[40,8952,3867],{"class":309},[40,8954,370],{"class":393},[40,8956,8957,8959,8961,8963,8965],{"class":42,"line":323},[40,8958,3874],{"class":58},[40,8960,400],{"class":393},[40,8962,3879],{"class":357},[40,8964,361],{"class":54},[40,8966,8774],{"class":319},[40,8968,8969,8971,8973,8975],{"class":42,"line":330},[40,8970,3886],{"class":58},[40,8972,400],{"class":393},[40,8974,1138],{"class":357},[40,8976,2070],{"class":54},[22,8978,2914],{"id":2914},[10,8980,8981],{},"戴上耳机，应该能听到：",[774,8983,8984,8987,8990],{},[777,8985,8986],{},"✅ 按 fly → 翅膀扇动声",[777,8988,8989],{},"✅ 穿过水管 → 清脆的得分音",[777,8991,8992],{},"✅ 撞死 → 一声闷响",[10,8994,8995,8996,8999,9000,9002,9003,20],{},"如果声音太大\u002F太小，去 ",[37,8997,8998],{},"audio_manager.tscn"," 里调 ",[37,9001,8485],{},"，",[17,9004,9005],{},"不用改代码",[22,9007,9009],{"id":9008},"可选背景音乐-bgm","（可选）背景音乐 BGM",[10,9011,9012,9013,9015,9016,9019],{},"你可以再加一个 ",[37,9014,8386],{},"（命名 ",[37,9017,9018],{},"BGM","），在 Inspector 里：",[774,9021,9022,9027,9033],{},[777,9023,9024,9026],{},[17,9025,8474],{},"：拖入背景音乐",[777,9028,9029,9032],{},[17,9030,9031],{},"Autoplay","：✅ 勾选（场景一加载就播放）",[777,9034,9035,9038,9039],{},[17,9036,9037],{},"Loop","：在 Stream 的导入设置里勾选 ",[37,9040,9037],{},[10,9042,9043],{},"这样游戏一开始就有背景音乐。",[10,9045,9046],{},"下一章我们打包导出游戏，发布到 itch.io！🚀",[1444,9048,9049],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}",{"title":35,"searchDepth":316,"depth":316,"links":9051},[9052,9053,9055,9058,9059,9060,9068,9069],{"id":8271,"depth":316,"text":8271},{"id":8365,"depth":316,"text":9054},"AudioManager 是个场景，不是脚本",{"id":8402,"depth":316,"text":8403,"children":9056},[9057],{"id":8463,"depth":323,"text":8464},{"id":8489,"depth":316,"text":8490},{"id":8668,"depth":316,"text":8669},{"id":8715,"depth":316,"text":8715,"children":9061},[9062,9064,9066],{"id":8721,"depth":323,"text":9063},"小鸟跳跃时（bird.gd）",{"id":8777,"depth":323,"text":9065},"穿过水管得分时（pillar_pair.gd）",{"id":8888,"depth":323,"text":9067},"撞死时（killzone.gd）",{"id":2914,"depth":316,"text":2914},{"id":9008,"depth":316,"text":9009},{},"\u002Fdevlog\u002Fxggame-bird\u002F08-sound",{"title":8240,"description":1474},"devlog\u002Fxggame-bird\u002F08-sound","SiovhngD622ncVG4wlzeyF7zsqoTjpr2hNkyrHGYjik",{"id":9076,"title":9077,"body":9078,"cover":1472,"date":1691,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":10046,"navigation":326,"path":185,"seo":10047,"stem":10048,"toc":326,"__hash__":10049},"devlog\u002Fdevlog\u002Fxggame-bird\u002F09-export.md","导出 + 发布到 itch.io",{"type":7,"value":9079,"toc":10025},[9080,9086,9089,9108,9121,9125,9128,9135,9138,9144,9148,9151,9161,9167,9170,9253,9256,9288,9296,9300,9305,9362,9372,9377,9405,9439,9447,9450,9456,9459,9465,9517,9524,9528,9533,9540,9543,9549,9552,9641,9645,9651,9690,9701,9715,9721,9725,9731,9774,9798,9801,9807,9833,9840,9844,9847,9909,9913,9920,9940,9947,9951,9954,9978,9981,9983,9987,9990,10010,10019,10022],[10,9081,9082,9083,20],{},"游戏做完了！最激动人心的一步 — ",[17,9084,9085],{},"让别人玩到你的游戏",[10,9087,9088],{},"我们要做两件事：",[1373,9090,9091,9097],{},[777,9092,9093,9096],{},[17,9094,9095],{},"导出为 Web 版（HTML5）"," — 在浏览器里就能玩",[777,9098,9099,9107],{},[17,9100,9101,9102],{},"发布到 ",[183,9103,9106],{"href":9104,"rel":9105},"https:\u002F\u002Fitch.io",[1502],"itch.io"," — 全世界都能搜到、能玩",[267,9109,9110],{},[10,9111,9112,9113,9116,9117,9120],{},"还记得我们在 ",[183,9114,9115],{"href":1693},"第 1 章","选了「兼容」渲染器吗？就是为了",[17,9118,9119],{},"这一步","！🎉",[22,9122,9124],{"id":9123},"安装-web-导出模板","安装 Web 导出模板",[10,9126,9127],{},"第一次导出 Web 版需要下载导出模板：",[10,9129,9130,9131,9134],{},"路径：编辑器 → ",[17,9132,9133],{},"管理导出模板"," (Manage Export Templates)",[10,9136,9137],{},"如果是首次安装，点击「下载并安装」，等几分钟下载完成即可。",[10,9139,9140],{},[1509,9141],{"alt":9142,"src":9143},"09-export-导出模板","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F09-export-%E5%AF%BC%E5%87%BA%E6%A8%A1%E6%9D%BF.png",[22,9145,9147],{"id":9146},"配置-web-导出","配置 Web 导出",[10,9149,9150],{},"路径：项目 → 导出 (Export)",[10,9152,9153,9154,9157,9158,9160],{},"点击「",[17,9155,9156],{},"添加","」→ 选择 ",[17,9159,258],{}," 平台。",[10,9162,9163],{},[1509,9164],{"alt":9165,"src":9166},"09-export-配置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F09-export-%E9%85%8D%E7%BD%AE.png",[687,9168,9169],{"id":9169},"关键设置",[108,9171,9172,9185],{},[111,9173,9174],{},[114,9175,9176,9179,9182],{},[117,9177,9178],{},"项",[117,9180,9181],{},"推荐值",[117,9183,9184],{},"说明",[127,9186,9187,9200,9212,9227,9240],{},[114,9188,9189,9194,9197],{},[132,9190,9191],{},[37,9192,9193],{},"Custom HTML Shell",[132,9195,9196],{},"留空",[132,9198,9199],{},"用 Godot 默认模板即可",[114,9201,9202,9207,9209],{},[132,9203,9204],{},[37,9205,9206],{},"Head Include",[132,9208,9196],{},[132,9210,9211],{},"如果想自定义页面 meta，可以加",[114,9213,9214,9219,9224],{},[132,9215,9216],{},[37,9217,9218],{},"Canvas Resize Policy",[132,9220,9221],{},[37,9222,9223],{},"Adaptive",[132,9225,9226],{},"自适应窗口大小",[114,9228,9229,9234,9237],{},[132,9230,9231],{},[37,9232,9233],{},"Focus Canvas on Start",[132,9235,9236],{},"✅ 启用",[132,9238,9239],{},"启动就聚焦，方便按键响应",[114,9241,9242,9247,9250],{},[132,9243,9244],{},[37,9245,9246],{},"Experimental Virtual Keyboard",[132,9248,9249],{},"看需求",[132,9251,9252],{},"移动端用得上",[22,9254,9255],{"id":9255},"导出游戏",[1373,9257,9258,9264,9271,9281],{},[777,9259,9260,9261,9263],{},"在【导出】面板选中 ",[17,9262,258],{}," 配置",[777,9265,9266,9267,9270],{},"点击右下角「",[17,9268,9269],{},"导出项目","」",[777,9272,9273,9274,9277,9278,3577],{},"选择一个",[17,9275,9276],{},"空文件夹","保存（比如 ",[37,9279,9280],{},"builds\u002Fweb\u002F",[777,9282,9283,9284,9287],{},"文件名填 ",[37,9285,9286],{},"index.html","（itch.io 上传约定）",[267,9289,9290],{},[10,9291,280,9292,9295],{},[17,9293,9294],{},"必须导出到空文件夹"," — 否则会和已有文件混在一起。",[687,9297,9299],{"id":9298},"️-两个最容易踩的坑","⚠️ 两个最容易踩的坑",[10,9301,9302],{},[17,9303,9304],{},"坑一：底部有两个按钮，长得很像，只有一个是对的。",[108,9306,9307,9320],{},[111,9308,9309],{},[114,9310,9311,9314,9317],{},[117,9312,9313],{},"按钮",[117,9315,9316],{},"产出",[117,9318,9319],{},"用途",[127,9321,9322,9348],{},[114,9323,9324,9329,9342],{},[132,9325,9326,9328],{},[17,9327,9269],{},"（Export Project）",[132,9330,9331,9333,9334,9333,9337,9333,9340],{},[37,9332,9286],{}," + ",[37,9335,9336],{},".js",[37,9338,9339],{},".wasm",[37,9341,180],{},[132,9343,9344,9345],{},"✅ ",[17,9346,9347],{},"你要的",[114,9349,9350,9353,9359],{},[132,9351,9352],{},"导出 PCK\u002FZIP",[132,9354,9355,9356],{},"只有资源数据包，",[17,9357,9358],{},"没有 index.html",[132,9360,9361],{},"❌ 给 DLC \u002F MOD \u002F 热更用的",[10,9363,9364,9365,666,9368,9371],{},"点错第二个，你会得到一个能打开但里面全是 ",[37,9366,9367],{},"scenes\u002F",[37,9369,9370],{},"scripts\u002F"," 的压缩包，itch.io 加载不出来。",[10,9373,9374],{},[17,9375,9376],{},"坑二：Web 导出永远输出散文件，Godot 不会帮你打 zip。",[10,9378,9379,9380,9383,9384,9387,9388,9390,9391,9398,9399,9401,9402,9404],{},"导出路径的作用是",[17,9381,9382],{},"决定基础文件名","，不是决定打包格式。你要是把路径填成\n",[37,9385,9386],{},"XGGame-Bird.zip","，Godot 会拿 ",[37,9389,1476],{}," 当基础名生成一堆文件，\n然后",[17,9392,9393,9394,9397],{},"把主 HTML 写进那个 ",[37,9395,9396],{},".zip"," 路径"," —— 你得到一个后缀是 ",[37,9400,9396],{}," 的 HTML 文件，\n",[37,9403,585],{}," 命令一看就露馅：",[30,9406,9410],{"className":9407,"code":9408,"language":9409,"meta":35,"style":35},"language-bash shiki shiki-themes vitesse-light vitesse-dark","$ file XGGame-Bird.zip\nXGGame-Bird.zip: HTML document text     # 根本不是压缩包\n","bash",[37,9411,9412,9422],{"__ignoreMap":35},[40,9413,9414,9417,9419],{"class":42,"line":43},[40,9415,9416],{"class":357},"$",[40,9418,420],{"class":342},[40,9420,9421],{"class":342}," XGGame-Bird.zip\n",[40,9423,9424,9427,9430,9433,9436],{"class":42,"line":316},[40,9425,9426],{"class":357},"XGGame-Bird.zip:",[40,9428,9429],{"class":342}," HTML",[40,9431,9432],{"class":342}," document",[40,9434,9435],{"class":342}," text",[40,9437,9438],{"class":319},"     # 根本不是压缩包\n",[10,9440,9441,9446],{},[17,9442,9443,9444],{},"所以文件名老老实实填 ",[37,9445,9286],{},"，zip 留到下面那步自己压。",[10,9448,9449],{},"导出后这个目录会出现一堆文件：",[30,9451,9454],{"className":9452,"code":9453,"language":6468},[6466],"builds\u002Fweb\u002F\n├── index.html\n├── index.js\n├── index.pck       ← 游戏资源\n├── index.wasm      ← 游戏引擎（WebAssembly）\n├── index.audio.worklet.js\n└── index.icon.png  ← 图标（可选）\n",[37,9455,9453],{"__ignoreMap":35},[22,9457,9458],{"id":9458},"本地测试",[10,9460,9461,9464],{},[17,9462,9463],{},"Web 导出无法直接双击 index.html 打开","（浏览器安全策略会阻止 wasm 加载）。需要起一个本地 server：",[30,9466,9468],{"className":9407,"code":9467,"language":9409,"meta":35,"style":35},"# Python 自带 server（最方便）\ncd builds\u002Fweb\npython -m http.server 8000\n\n# 或者 Node.js\nnpx serve builds\u002Fweb\n",[37,9469,9470,9475,9484,9498,9502,9507],{"__ignoreMap":35},[40,9471,9472],{"class":42,"line":43},[40,9473,9474],{"class":319},"# Python 自带 server（最方便）\n",[40,9476,9477,9481],{"class":42,"line":316},[40,9478,9480],{"class":9479},"sHLBJ","cd",[40,9482,9483],{"class":342}," builds\u002Fweb\n",[40,9485,9486,9489,9492,9495],{"class":42,"line":323},[40,9487,9488],{"class":357},"python",[40,9490,9491],{"class":4008}," -m",[40,9493,9494],{"class":342}," http.server",[40,9496,9497],{"class":65}," 8000\n",[40,9499,9500],{"class":42,"line":330},[40,9501,327],{"emptyLinePlaceholder":326},[40,9503,9504],{"class":42,"line":346},[40,9505,9506],{"class":319},"# 或者 Node.js\n",[40,9508,9509,9512,9515],{"class":42,"line":351},[40,9510,9511],{"class":357},"npx",[40,9513,9514],{"class":342}," serve",[40,9516,9483],{"class":342},[10,9518,9519,9520,9523],{},"打开浏览器访问 ",[37,9521,9522],{},"http:\u002F\u002Flocalhost:8000","，能玩就说明导出成功。",[22,9525,9527],{"id":9526},"注册-itchio","注册 itch.io",[267,9529,9530],{},[10,9531,9532],{},"已有账号可以跳过这一步。",[10,9534,1393,9535,9539],{},[183,9536,9106],{"href":9537,"rel":9538},"https:\u002F\u002Fitch.io\u002Fregister",[1502]," 注册账号，确认邮箱即可。",[22,9541,9542],{"id":9542},"上传游戏",[10,9544,9545,9546,20],{},"登录后，右上角头像 → ",[17,9547,9548],{},"Upload new project",[687,9550,9551],{"id":9551},"填写项目信息",[108,9553,9554,9564],{},[111,9555,9556],{},[114,9557,9558,9561],{},[117,9559,9560],{},"字段",[117,9562,9563],{},"填写建议",[127,9565,9566,9576,9584,9592,9603,9616,9630],{},[114,9567,9568,9571],{},[132,9569,9570],{},"Title",[132,9572,9573,9574,3577],{},"游戏名（如 ",[37,9575,1476],{},[114,9577,9578,9581],{},[132,9579,9580],{},"Project URL",[132,9582,9583],{},"URL slug（自动生成，可改）",[114,9585,9586,9589],{},[132,9587,9588],{},"Short description",[132,9590,9591],{},"一句话简介（搜索结果会显示）",[114,9593,9594,9597],{},[132,9595,9596],{},"Classification",[132,9598,9599,9600],{},"选 ",[37,9601,9602],{},"Games",[114,9604,9605,9608],{},[132,9606,9607],{},"Kind of project",[132,9609,9610,9615],{},[17,9611,9599,9612],{},[37,9613,9614],{},"HTML"," ← 关键！",[114,9617,9618,9621],{},[132,9619,9620],{},"Release status",[132,9622,9623,9626,9627],{},[37,9624,9625],{},"Released"," 或 ",[37,9628,9629],{},"Prototype",[114,9631,9632,9635],{},[132,9633,9634],{},"Pricing",[132,9636,9637,9640],{},[37,9638,9639],{},"$0 or donate"," （免费 \u002F 自愿打赏）",[22,9642,9644],{"id":9643},"上传文件-打包成-zip","上传文件 — 打包成 zip",[10,9646,9647,9648,299],{},"itch.io 要求 Web 游戏",[17,9649,9650],{},"打包成 zip 上传",[30,9652,9654],{"className":9407,"code":9653,"language":9409,"meta":35,"style":35},"# 进入 builds\u002Fweb 目录\ncd builds\u002Fweb\n\n# 打包成 zip（不要有外层文件夹，要 index.html 在 zip 根目录）\nzip -r ..\u002Fxggame-bird-web.zip .\n",[37,9655,9656,9661,9667,9671,9676],{"__ignoreMap":35},[40,9657,9658],{"class":42,"line":43},[40,9659,9660],{"class":319},"# 进入 builds\u002Fweb 目录\n",[40,9662,9663,9665],{"class":42,"line":316},[40,9664,9480],{"class":9479},[40,9666,9483],{"class":342},[40,9668,9669],{"class":42,"line":323},[40,9670,327],{"emptyLinePlaceholder":326},[40,9672,9673],{"class":42,"line":330},[40,9674,9675],{"class":319},"# 打包成 zip（不要有外层文件夹，要 index.html 在 zip 根目录）\n",[40,9677,9678,9681,9684,9687],{"class":42,"line":346},[40,9679,9680],{"class":357},"zip",[40,9682,9683],{"class":4008}," -r",[40,9685,9686],{"class":342}," ..\u002Fxggame-bird-web.zip",[40,9688,9689],{"class":342}," .\n",[10,9691,9692,9693,9696,9697,9700],{},"或者直接在文件管理器里：选中 ",[37,9694,9695],{},"builds\u002Fweb"," 里的",[17,9698,9699],{},"所有文件","（注意是文件，不是文件夹）→ 右键 → 压缩为 zip。",[267,9702,9703],{},[10,9704,280,9705,9708,9709,9002,9711,9714],{},[17,9706,9707],{},"常见坑","：zip 解压后必须直接看到 ",[37,9710,9286],{},[17,9712,9713],{},"不能有外层文件夹","。否则 itch.io 加载不出来。",[10,9716,9717,9718,20],{},"上传 zip 后，勾选 ",[17,9719,9720],{},"「This file will be played in the browser」",[22,9722,9724],{"id":9723},"配置嵌入设置embed-options","配置嵌入设置（Embed Options）",[10,9726,9727,9728,299],{},"下拉找到 ",[17,9729,9730],{},"Embed options",[108,9732,9733,9741],{},[111,9734,9735],{},[114,9736,9737,9739],{},[117,9738,9178],{},[117,9740,1579],{},[127,9742,9743,9754,9766],{},[114,9744,9745,9748],{},[132,9746,9747],{},"Viewport dimensions",[132,9749,9750,9753],{},[37,9751,9752],{},"608 × 1080","（见下方说明）",[114,9755,9756,9759],{},[132,9757,9758],{},"Frame options",[132,9760,9761,9762,9765],{},"一般勾 ",[37,9763,9764],{},"Fullscreen button"," 让玩家可全屏",[114,9767,9768,9771],{},[132,9769,9770],{},"Mobile friendly",[132,9772,9773],{},"如果适配了移动端，勾上",[267,9775,9776],{},[10,9777,9778,9781,9782,9785,9786,9789,9790,9793,9794,9797],{},[17,9779,9780],{},"这个数字怎么来的","：项目设置里视口是 ",[37,9783,9784],{},"720 × 1280","，但窗口高度覆盖设成了 ",[37,9787,9788],{},"1080","。\n按 9:16 等比换算，宽就是 ",[37,9791,9792],{},"720 ÷ 1280 × 1080 ≈ 608","。\n",[17,9795,9796],{},"关键是比例对，绝对值可以小一点"," —— 1280 高在笔记本上会超出屏幕，玩家要滚动才能看全。",[22,9799,9800],{"id":9800},"上传封面图和截图",[10,9802,9803,9804,299],{},"往下翻到 ",[17,9805,9806],{},"Cover image",[774,9808,9809,9821,9827],{},[777,9810,9811,9813,9814,9626,9817,9820],{},[17,9812,9806],{}," — 主图，建议 ",[17,9815,9816],{},"630 × 500",[17,9818,9819],{},"315 × 250","，会出现在 itch.io 列表里",[777,9822,9823,9826],{},[17,9824,9825],{},"Screenshots"," — 游戏截图，2-4 张就够了",[777,9828,9829,9832],{},[17,9830,9831],{},"Banner"," — 横幅图（可选）",[10,9834,9835,9836,9839],{},"封面图建议用游戏的",[17,9837,9838],{},"最高光时刻","做截图 + 加点文字，吸引点击。",[22,9841,9843],{"id":9842},"设置标签-分类","设置标签 + 分类",[10,9845,9846],{},"帮助玩家发现你的游戏：",[774,9848,9849,9857,9880,9888,9903],{},[777,9850,9851,394,9854],{},[17,9852,9853],{},"Genre",[37,9855,9856],{},"Action",[777,9858,9859,394,9862,5053,9865,5053,9868,5053,9871,5053,9874,5053,9877],{},[17,9860,9861],{},"Tags",[37,9863,9864],{},"flappy",[37,9866,9867],{},"casual",[37,9869,9870],{},"pixel-art",[37,9872,9873],{},"arcade",[37,9875,9876],{},"endless",[37,9878,9879],{},"godot",[777,9881,9882,394,9885],{},[17,9883,9884],{},"Average session",[37,9886,9887],{},"A few minutes",[777,9889,9890,394,9893,5053,9896,5053,9899,9902],{},[17,9891,9892],{},"Inputs",[37,9894,9895],{},"Mouse",[37,9897,9898],{},"Keyboard",[37,9900,9901],{},"Touchscreen","（如果支持）",[777,9904,9905,9908],{},[17,9906,9907],{},"Accessibility",": 看实际情况",[22,9910,9912],{"id":9911},"发布","发布！",[10,9914,9915,9916,9919],{},"设置好后，把 ",[17,9917,9918],{},"Visibility"," 改成：",[774,9921,9922,9928,9934],{},[777,9923,9924,9927],{},[17,9925,9926],{},"Public"," — 完全公开，任何人都能搜到",[777,9929,9930,9933],{},[17,9931,9932],{},"Restricted"," — 只有有链接的人能访问（适合先给朋友测试）",[777,9935,9936,9939],{},[17,9937,9938],{},"Draft"," — 草稿，只有自己能看",[10,9941,9942,9943,9946],{},"点击右下角 ",[17,9944,9945],{},"Save & view page","，欣赏一下自己的成果 🎉",[22,9948,9950],{"id":9949},"分享出去","分享出去！",[10,9952,9953],{},"把 itch.io 的链接发到：",[774,9955,9956,9959,9967,9975],{},[777,9957,9958],{},"朋友圈、Twitter、小红书、抖音视频简介",[777,9960,9961,9966],{},[183,9962,9965],{"href":9963,"rel":9964},"https:\u002F\u002Freddit.com\u002Fr\u002Fgodot",[1502],"r\u002Fgodot"," — Godot 社区",[777,9968,9969,9974],{},[183,9970,9973],{"href":9971,"rel":9972},"https:\u002F\u002Fitch.io\u002Fboard\u002F4\u002Frelease-announcements",[1502],"Itch.io 论坛"," — Release Announcements",[777,9976,9977],{},"Discord 游戏开发群",[10,9979,9980],{},"记得在我的博客评论区也贴一下你的 itch.io 链接，互相玩一下！🎮",[1415,9982],{},[22,9984,9986],{"id":9985},"整个系列到这就完结了","整个系列到这就完结了 🎉",[10,9988,9989],{},"回顾一下我们完成的事：",[774,9991,9992,9995,9998,10001,10004],{},[777,9993,9994],{},"✅ 从零搭建一个 Flappy Bird 类游戏",[777,9996,9997],{},"✅ 学会了 Godot 的核心概念：节点、场景、信号、Autoload、Parallax",[777,9999,10000],{},"✅ 实现了完整的游戏循环：菜单 → 游戏 → 结束 → 重开",[777,10002,10003],{},"✅ 加上美术 + 音效，让游戏有了\"灵魂\"",[777,10005,9344,10006,10009],{},[17,10007,10008],{},"公开发布","，全世界都能玩",[10,10011,10012,10013,10018],{},"如果你跟着做完了整个流程，请一定告诉我！可以发我作品链接 → ",[183,10014,10017],{"href":10015,"rel":10016},"https:\u002F\u002Ftwitter.com\u002FX_XXGGG",[1502],"Twitter @X_XXGGG"," \u002F 或博客评论。",[10,10020,10021],{},"下一个游戏，我们再战 💪",[1444,10023,10024],{},"html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHLBJ, html code.shiki .sHLBJ{--shiki-default:#998418;--shiki-dark:#B8A965}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}",{"title":35,"searchDepth":316,"depth":316,"links":10026},[10027,10028,10031,10034,10035,10036,10039,10040,10041,10042,10043,10044,10045],{"id":9123,"depth":316,"text":9124},{"id":9146,"depth":316,"text":9147,"children":10029},[10030],{"id":9169,"depth":323,"text":9169},{"id":9255,"depth":316,"text":9255,"children":10032},[10033],{"id":9298,"depth":323,"text":9299},{"id":9458,"depth":316,"text":9458},{"id":9526,"depth":316,"text":9527},{"id":9542,"depth":316,"text":9542,"children":10037},[10038],{"id":9551,"depth":323,"text":9551},{"id":9643,"depth":316,"text":9644},{"id":9723,"depth":316,"text":9724},{"id":9800,"depth":316,"text":9800},{"id":9842,"depth":316,"text":9843},{"id":9911,"depth":316,"text":9912},{"id":9949,"depth":316,"text":9950},{"id":9985,"depth":316,"text":9986},{},{"title":9077,"description":1474},"devlog\u002Fxggame-bird\u002F09-export","7kR0-U83ZUcnBTtOk-3TlGWhgE2EQXxpHX1iIEI8rzc",{"id":10051,"title":10052,"body":10053,"cover":1472,"date":1473,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":11688,"navigation":326,"path":681,"seo":11689,"stem":11690,"toc":326,"__hash__":11691},"devlog\u002Fdevlog\u002Fxggame-bird\u002F10-event-bus.md","重构：信号总线 EventBus",{"type":7,"value":10054,"toc":11665},[10055,10058,10067,10070,10077,10176,10182,10185,10209,10216,10223,10227,10230,10245,10250,10256,10273,10277,10282,10363,10370,10374,10377,10380,10400,10407,10413,10420,10428,10437,10441,10457,10468,10537,10542,10562,10571,10574,10580,10668,10675,10681,10881,10886,10889,10895,10899,11068,11076,11105,11127,11131,11341,11344,11377,11393,11405,11415,11423,11429,11434,11502,11505,11512,11519,11522,11577,11585,11589,11646,11653,11655,11662],[10,10056,10057],{},"游戏已经能玩、能上架了。但回头看代码，有个地方越写越别扭 — 这一章我们把它重构掉。",[267,10059,10060],{},[10,10061,10062,10063,10066],{},"这章不加新功能，",[17,10064,10065],{},"只改结构","。改完游戏行为一模一样，但以后加东西会轻松很多。",[22,10068,10069],{"id":10069},"先看问题出在哪",[10,10071,10072,10073,10076],{},"回到 ",[183,10074,10075],{"href":6410},"第 5 章"," 的死区脚本，现在长这样：",[30,10078,10080],{"className":32,"code":10079,"language":34,"meta":35,"style":35},"extends Area2D\n\nfunc _on_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\" and GameManager.current_state == GameManager.GameState.PLAYING:\n        AudioManager.play_die()    # 通知音频：放死亡音效\n        GameManager.game_over()    # 通知管理器：游戏结束\n",[37,10081,10082,10088,10092,10114,10150,10163],{"__ignoreMap":35},[40,10083,10084,10086],{"class":42,"line":43},[40,10085,310],{"class":309},[40,10087,3697],{"class":58},[40,10089,10090],{"class":42,"line":316},[40,10091,327],{"emptyLinePlaceholder":326},[40,10093,10094,10096,10098,10100,10102,10104,10106,10108,10110,10112],{"class":42,"line":323},[40,10095,354],{"class":46},[40,10097,3708],{"class":357},[40,10099,433],{"class":54},[40,10101,3713],{"class":393},[40,10103,55],{"class":54},[40,10105,3718],{"class":58},[40,10107,522],{"class":54},[40,10109,364],{"class":46},[40,10111,367],{"class":58},[40,10113,370],{"class":54},[40,10115,10116,10118,10120,10122,10124,10126,10128,10130,10132,10134,10136,10138,10140,10142,10144,10146,10148],{"class":42,"line":330},[40,10117,505],{"class":309},[40,10119,3733],{"class":50},[40,10121,400],{"class":54},[40,10123,3738],{"class":50},[40,10125,603],{"class":46},[40,10127,3743],{"class":342},[40,10129,3846],{"class":46},[40,10131,3849],{"class":58},[40,10133,400],{"class":54},[40,10135,3854],{"class":50},[40,10137,603],{"class":46},[40,10139,3849],{"class":58},[40,10141,400],{"class":54},[40,10143,1169],{"class":50},[40,10145,400],{"class":54},[40,10147,3867],{"class":309},[40,10149,370],{"class":393},[40,10151,10152,10154,10156,10158,10160],{"class":42,"line":346},[40,10153,3874],{"class":58},[40,10155,400],{"class":393},[40,10157,3879],{"class":357},[40,10159,361],{"class":54},[40,10161,10162],{"class":319},"    # 通知音频：放死亡音效\n",[40,10164,10165,10167,10169,10171,10173],{"class":42,"line":351},[40,10166,3886],{"class":58},[40,10168,400],{"class":393},[40,10170,1138],{"class":357},[40,10172,361],{"class":54},[40,10174,10175],{"class":319},"    # 通知管理器：游戏结束\n",[10,10177,10178,10179,20],{},"看起来没毛病。但你数一下：",[17,10180,10181],{},"小鸟撞一次墙，这个脚本要挨个去通知 2 个人",[10,10183,10184],{},"现在假设你想加点东西：",[774,10186,10187,10197,10203],{},[777,10188,10189,10190,10193,10194],{},"想加",[17,10191,10192],{},"屏幕震动"," → 回来改一行 ",[37,10195,10196],{},"CameraShake.shake()",[777,10198,10189,10199,10202],{},[17,10200,10201],{},"死亡粒子特效"," → 再回来改一行",[777,10204,10189,10205,10208],{},[17,10206,10207],{},"成就统计","（死亡次数 +1）→ 再回来改一行",[10,10210,10211,10212,10215],{},"每加一个功能，就要回来改这个死区脚本一次。而死区本身的职责其实只有一句话：",[17,10213,10214],{},"\"我检测到小鸟撞上来了\"","。它凭什么要知道游戏里有音频系统、有相机、有成就系统？",[10,10217,10218,10219,10222],{},"这就是所谓的 ",[17,10220,10221],{},"「发信人必须认识所有收信人」"," — 耦合。",[22,10224,10226],{"id":10225},"换个思路广播","换个思路：广播",[10,10228,10229],{},"想象一下机场广播：",[774,10231,10232,10235,10242],{},[777,10233,10234],{},"广播员喊一句「CA1234 航班开始登机」",[777,10236,10237,10238,10241],{},"他",[17,10239,10240],{},"不需要知道","候机厅里坐了谁",[777,10243,10244],{},"谁买了这班机票，谁自己站起来往登机口走",[10,10246,10247],{},[17,10248,10249],{},"广播员和乘客互不认识，但信息传到了。",[10,10251,10252,10253,20],{},"我们要做的就是给游戏装一个广播站 — ",[17,10254,10255],{},"EventBus（信号总线）",[774,10257,10258,10261,10267],{},[777,10259,10260],{},"死区只负责喊：「鸟死了！」",[777,10262,10263,10264],{},"至于谁听见了、听见了要干啥，",[17,10265,10266],{},"死区一概不管",[777,10268,10269,10270],{},"以后想加震屏、加特效、加成就，只要让新模块自己去\"听\"这个广播，",[17,10271,10272],{},"死区一行都不用改",[22,10274,10276],{"id":10275},"创建-eventbus","创建 EventBus",[10,10278,295,10279,299],{},[37,10280,10281],{},"scripts\u002Fautoload\u002Fevent_bus.gd",[30,10283,10285],{"className":32,"code":10284,"language":34,"meta":35,"style":35},"extends Node\n## 全局信号总线：只定义信号，不写逻辑\n## 谁发生了事就在这里喊，谁关心就自己连接\n\n@warning_ignore(\"unused_signal\")\nsignal bird_died                  # 小鸟死亡\n\n@warning_ignore(\"unused_signal\")\nsignal point_scored(amount: int)  # 得分\n",[37,10286,10287,10293,10298,10303,10307,10319,10329,10333,10343],{"__ignoreMap":35},[40,10288,10289,10291],{"class":42,"line":43},[40,10290,310],{"class":309},[40,10292,313],{"class":58},[40,10294,10295],{"class":42,"line":316},[40,10296,10297],{"class":319},"## 全局信号总线：只定义信号，不写逻辑\n",[40,10299,10300],{"class":42,"line":323},[40,10301,10302],{"class":319},"## 谁发生了事就在这里喊，谁关心就自己连接\n",[40,10304,10305],{"class":42,"line":330},[40,10306,327],{"emptyLinePlaceholder":326},[40,10308,10309,10312,10314,10317],{"class":42,"line":346},[40,10310,10311],{"class":357},"@warning_ignore",[40,10313,433],{"class":54},[40,10315,10316],{"class":342},"\"unused_signal\"",[40,10318,449],{"class":54},[40,10320,10321,10323,10326],{"class":42,"line":351},[40,10322,5130],{"class":46},[40,10324,10325],{"class":357}," bird_died",[40,10327,10328],{"class":319},"                  # 小鸟死亡\n",[40,10330,10331],{"class":42,"line":373},[40,10332,327],{"emptyLinePlaceholder":326},[40,10334,10335,10337,10339,10341],{"class":42,"line":387},[40,10336,10311],{"class":357},[40,10338,433],{"class":54},[40,10340,10316],{"class":342},[40,10342,449],{"class":54},[40,10344,10345,10347,10350,10352,10354,10356,10358,10360],{"class":42,"line":409},[40,10346,5130],{"class":46},[40,10348,10349],{"class":357}," point_scored",[40,10351,433],{"class":54},[40,10353,5219],{"class":393},[40,10355,55],{"class":54},[40,10357,59],{"class":58},[40,10359,522],{"class":54},[40,10361,10362],{"class":319},"  # 得分\n",[10,10364,10365,10366,10369],{},"就这么点东西。",[17,10367,10368],{},"整个文件没有一个函数"," — 这是故意的。",[687,10371,10373],{"id":10372},"为什么-eventbus-里不能写逻辑","为什么 EventBus 里不能写逻辑？",[10,10375,10376],{},"这是新手最容易踩的坑：一看到\"全局单例\"就忍不住往里塞代码，最后 EventBus 变成第二个 GameManager，两边职责打架。",[10,10378,10379],{},"记住这条规矩：",[774,10381,10382,10388,10394],{},[777,10383,10384,10387],{},[17,10385,10386],{},"EventBus 只管「喊」"," — 它是个空壳广播站，只定义\"有哪些事件\"",[777,10389,10390,10393],{},[17,10391,10392],{},"GameManager 只管「游戏规则」"," — 分数怎么算、状态怎么转",[777,10395,10396,10397],{},"一个是喇叭，一个是裁判，",[17,10398,10399],{},"不要合并",[687,10401,10403,10406],{"id":10402},"warning_ignoreunused_signal-是干嘛的",[37,10404,10405],{},"@warning_ignore(\"unused_signal\")"," 是干嘛的？",[10,10408,10409,10410,20],{},"如果不加这行，Godot 编辑器会给你画一条黄色波浪线警告：",[17,10411,10412],{},"「这个信号定义了但从来没在本文件里 emit 过」",[10,10414,10415,10416,10419],{},"Godot 这个检查本身是好意 — 通常你定义了信号却不发，多半是写漏了。但 EventBus 的",[17,10417,10418],{},"设计初衷就是\"在别的文件里 emit\"","，所以这个警告在这里是误报。",[10,10421,10422,10424,10425,20],{},[37,10423,10405],{}," 就是告诉编辑器：",[17,10426,10427],{},"这里我是故意的，别提醒我了",[267,10429,10430],{},[10,10431,10432,10433,10436],{},"注意它只对",[17,10434,10435],{},"紧接着的下一行","生效。所以两个信号要各写一次，不能只在文件顶上写一个。",[687,10438,10440],{"id":10439},"注册-autoload-顺序很重要","注册 Autoload — 顺序很重要",[10,10442,10443,10444,10446,10447,10449,10450,10453,10454,20],{},"和 ",[183,10445,10075],{"href":6410}," 一样，",[17,10448,5393],{},"，加入 ",[37,10451,10452],{},"event_bus.gd","，命名 ",[37,10455,10456],{},"EventBus",[10,10458,10459,10460,10463,10464,10467],{},"但这次有个",[17,10461,10462],{},"关键细节","：Autoload 列表是",[17,10465,10466],{},"从上往下依次加载","的，最终顺序要是这样 👇",[108,10469,10470,10483],{},[111,10471,10472],{},[114,10473,10474,10477,10480],{},[117,10475,10476],{},"顺序",[117,10478,10479],{},"名称",[117,10481,10482],{},"为什么在这个位置",[127,10484,10485,10500,10512,10525],{},[114,10486,10487,10489,10493],{},[132,10488,4497],{},[132,10490,10491],{},[37,10492,10456],{},[132,10494,10495,10496,10499],{},"大家都要连它，必须",[17,10497,10498],{},"第一个","准备好",[114,10501,10502,10505,10509],{},[132,10503,10504],{},"2",[132,10506,10507],{},[37,10508,653],{},[132,10510,10511],{},"下一章讲，GameManager 要用它读档",[114,10513,10514,10516,10520],{},[132,10515,8016],{},[132,10517,10518],{},[37,10519,397],{},[132,10521,10522,10524],{},[37,10523,673],{}," 里要连 EventBus 的信号",[114,10526,10527,10530,10534],{},[132,10528,10529],{},"4",[132,10531,10532],{},[37,10533,3904],{},[132,10535,10536],{},"同样要连 EventBus 的信号",[10,10538,10539],{},[17,10540,10541],{},"为什么顺序会出问题？",[10,10543,10544,10545,10547,10548,10551,10552,10554,10555,10557,10558,10561],{},"因为 GameManager 的 ",[37,10546,673],{}," 里会写 ",[37,10549,10550],{},"EventBus.point_scored.connect(...)","。如果 EventBus 排在 GameManager 后面，那么 GameManager 跑 ",[37,10553,673],{}," 的那一刻，",[37,10556,10456],{}," 这个名字",[17,10559,10560],{},"还不存在"," — 游戏会直接报错崩掉。",[267,10563,10564],{},[10,10565,10566,10567,10570],{},"在自动加载列表里可以直接",[17,10568,10569],{},"拖动条目","调整上下顺序。",[22,10572,10573],{"id":10573},"改造发信方",[687,10575,10577,10578,3577],{"id":10576},"死区killzonegd","死区（",[37,10579,5756],{},[30,10581,10583],{"className":32,"code":10582,"language":34,"meta":35,"style":35},"extends Area2D\n\nfunc _on_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\" and GameManager.current_state == GameManager.GameState.PLAYING:\n        EventBus.bird_died.emit()\n",[37,10584,10585,10591,10595,10617,10653],{"__ignoreMap":35},[40,10586,10587,10589],{"class":42,"line":43},[40,10588,310],{"class":309},[40,10590,3697],{"class":58},[40,10592,10593],{"class":42,"line":316},[40,10594,327],{"emptyLinePlaceholder":326},[40,10596,10597,10599,10601,10603,10605,10607,10609,10611,10613,10615],{"class":42,"line":323},[40,10598,354],{"class":46},[40,10600,3708],{"class":357},[40,10602,433],{"class":54},[40,10604,3713],{"class":393},[40,10606,55],{"class":54},[40,10608,3718],{"class":58},[40,10610,522],{"class":54},[40,10612,364],{"class":46},[40,10614,367],{"class":58},[40,10616,370],{"class":54},[40,10618,10619,10621,10623,10625,10627,10629,10631,10633,10635,10637,10639,10641,10643,10645,10647,10649,10651],{"class":42,"line":330},[40,10620,505],{"class":309},[40,10622,3733],{"class":50},[40,10624,400],{"class":54},[40,10626,3738],{"class":50},[40,10628,603],{"class":46},[40,10630,3743],{"class":342},[40,10632,3846],{"class":46},[40,10634,3849],{"class":58},[40,10636,400],{"class":54},[40,10638,3854],{"class":50},[40,10640,603],{"class":46},[40,10642,3849],{"class":58},[40,10644,400],{"class":54},[40,10646,1169],{"class":50},[40,10648,400],{"class":54},[40,10650,3867],{"class":309},[40,10652,370],{"class":393},[40,10654,10655,10658,10660,10662,10664,10666],{"class":42,"line":346},[40,10656,10657],{"class":58},"        EventBus",[40,10659,400],{"class":54},[40,10661,1129],{"class":50},[40,10663,400],{"class":393},[40,10665,5191],{"class":357},[40,10667,2070],{"class":54},[10,10669,10670,10671,10674],{},"原来的两行通知，变成",[17,10672,10673],{},"一行广播","。这个脚本现在完全不知道音频系统和 GameManager 的存在了 — 它只知道\"鸟死了\"这件事发生了。",[687,10676,10678,10679,3577],{"id":10677},"障碍物pillar_pairgd","障碍物（",[37,10680,5853],{},[30,10682,10684],{"className":32,"code":10683,"language":34,"meta":35,"style":35},"extends Node2D\n\n@export var speed := 200.0\n@onready var goal: Area2D = $Goal\n\nfunc _physics_process(delta: float) -> void:\n    if GameManager.current_state == GameManager.GameState.PLAYING:\n        position.x -= speed * delta\n        if position.x \u003C -500:\n            queue_free()\n\n# 小鸟穿过得分区\nfunc _on_goal_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\":\n        EventBus.point_scored.emit(1)\n",[37,10685,10686,10692,10696,10708,10726,10730,10752,10776,10792,10810,10816,10820,10825,10847,10863],{"__ignoreMap":35},[40,10687,10688,10690],{"class":42,"line":43},[40,10689,310],{"class":309},[40,10691,3967],{"class":58},[40,10693,10694],{"class":42,"line":316},[40,10695,327],{"emptyLinePlaceholder":326},[40,10697,10698,10700,10702,10704,10706],{"class":42,"line":323},[40,10699,1852],{"class":357},[40,10701,1855],{"class":46},[40,10703,3980],{"class":50},[40,10705,339],{"class":54},[40,10707,3985],{"class":65},[40,10709,10710,10712,10714,10716,10718,10720,10722,10724],{"class":42,"line":330},[40,10711,3990],{"class":357},[40,10713,1855],{"class":46},[40,10715,3995],{"class":50},[40,10717,55],{"class":54},[40,10719,4000],{"class":58},[40,10721,62],{"class":54},[40,10723,4005],{"class":309},[40,10725,4009],{"class":4008},[40,10727,10728],{"class":42,"line":346},[40,10729,327],{"emptyLinePlaceholder":326},[40,10731,10732,10734,10736,10738,10740,10742,10744,10746,10748,10750],{"class":42,"line":351},[40,10733,354],{"class":46},[40,10735,1914],{"class":357},[40,10737,433],{"class":54},[40,10739,1919],{"class":393},[40,10741,55],{"class":54},[40,10743,1924],{"class":58},[40,10745,522],{"class":54},[40,10747,364],{"class":46},[40,10749,367],{"class":58},[40,10751,370],{"class":54},[40,10753,10754,10756,10758,10760,10762,10764,10766,10768,10770,10772,10774],{"class":42,"line":373},[40,10755,505],{"class":309},[40,10757,3849],{"class":58},[40,10759,400],{"class":54},[40,10761,3854],{"class":50},[40,10763,603],{"class":46},[40,10765,3849],{"class":58},[40,10767,400],{"class":54},[40,10769,1169],{"class":50},[40,10771,400],{"class":54},[40,10773,3867],{"class":309},[40,10775,370],{"class":393},[40,10777,10778,10780,10782,10784,10786,10788,10790],{"class":42,"line":387},[40,10779,4372],{"class":50},[40,10781,400],{"class":54},[40,10783,2455],{"class":50},[40,10785,4052],{"class":46},[40,10787,3980],{"class":50},[40,10789,1966],{"class":46},[40,10791,4059],{"class":50},[40,10793,10794,10796,10798,10800,10802,10804,10806,10808],{"class":42,"line":409},[40,10795,4389],{"class":309},[40,10797,4071],{"class":50},[40,10799,400],{"class":54},[40,10801,2455],{"class":50},[40,10803,1185],{"class":46},[40,10805,1880],{"class":46},[40,10807,4082],{"class":65},[40,10809,370],{"class":393},[40,10811,10812,10814],{"class":42,"line":415},[40,10813,4408],{"class":357},[40,10815,2070],{"class":54},[40,10817,10818],{"class":42,"line":452},[40,10819,327],{"emptyLinePlaceholder":326},[40,10821,10822],{"class":42,"line":481},[40,10823,10824],{"class":319},"# 小鸟穿过得分区\n",[40,10826,10827,10829,10831,10833,10835,10837,10839,10841,10843,10845],{"class":42,"line":486},[40,10828,354],{"class":46},[40,10830,4107],{"class":357},[40,10832,433],{"class":54},[40,10834,3713],{"class":393},[40,10836,55],{"class":54},[40,10838,3718],{"class":58},[40,10840,522],{"class":54},[40,10842,364],{"class":46},[40,10844,367],{"class":58},[40,10846,370],{"class":54},[40,10848,10849,10851,10853,10855,10857,10859,10861],{"class":42,"line":502},[40,10850,505],{"class":309},[40,10852,3733],{"class":50},[40,10854,400],{"class":54},[40,10856,3738],{"class":50},[40,10858,603],{"class":46},[40,10860,3743],{"class":342},[40,10862,370],{"class":393},[40,10864,10865,10867,10869,10871,10873,10875,10877,10879],{"class":42,"line":527},[40,10866,10657],{"class":58},[40,10868,400],{"class":54},[40,10870,1108],{"class":50},[40,10872,400],{"class":393},[40,10874,5191],{"class":357},[40,10876,433],{"class":54},[40,10878,4497],{"class":65},[40,10880,449],{"class":54},[10,10882,10883,10884,20],{},"同样，原来那一坨「播音效 + 调用 add_score + 禁用区域」三件事，缩成一行 ",[37,10885,5191],{},[22,10887,10888],{"id":10888},"改造收信方",[10,10890,10891,10892],{},"发信方轻松了，那\"接活\"的代码去哪了？",[17,10893,10894],{},"搬到了关心这件事的人自己家里。",[687,10896,10898],{"id":10897},"gamemanager-自己去听","GameManager 自己去听",[30,10900,10902],{"className":32,"code":10901,"language":34,"meta":35,"style":35},"extends Node\n\nenum GameState { READY, PLAYING, GAME_OVER }\nvar current_state = GameState.READY\n\nvar total_score: int = 0\nvar high_score: int = 0\nsignal score_changed(new_score)\nsignal state_changed(new_state)\n\nfunc _ready() -> void:\n    ## 我关心「得分」，得分了我就加分\n    EventBus.point_scored.connect(add_score)\n    ## 我关心「小鸟死亡」，死了我就结算\n    EventBus.bird_died.connect(game_over)\n",[37,10903,10904,10910,10914,10934,10948,10952,10966,10980,10992,11004,11008,11022,11027,11045,11050],{"__ignoreMap":35},[40,10905,10906,10908],{"class":42,"line":43},[40,10907,310],{"class":309},[40,10909,313],{"class":58},[40,10911,10912],{"class":42,"line":316},[40,10913,327],{"emptyLinePlaceholder":326},[40,10915,10916,10918,10920,10922,10924,10926,10928,10930,10932],{"class":42,"line":323},[40,10917,5042],{"class":309},[40,10919,5045],{"class":58},[40,10921,733],{"class":54},[40,10923,5050],{"class":50},[40,10925,5053],{"class":393},[40,10927,3867],{"class":50},[40,10929,5053],{"class":393},[40,10931,1174],{"class":50},[40,10933,5062],{"class":54},[40,10935,10936,10938,10940,10942,10944,10946],{"class":42,"line":330},[40,10937,47],{"class":46},[40,10939,5069],{"class":50},[40,10941,62],{"class":54},[40,10943,5045],{"class":58},[40,10945,400],{"class":54},[40,10947,5078],{"class":309},[40,10949,10950],{"class":42,"line":346},[40,10951,327],{"emptyLinePlaceholder":326},[40,10953,10954,10956,10958,10960,10962,10964],{"class":42,"line":351},[40,10955,47],{"class":46},[40,10957,1188],{"class":50},[40,10959,55],{"class":54},[40,10961,59],{"class":58},[40,10963,62],{"class":54},[40,10965,66],{"class":65},[40,10967,10968,10970,10972,10974,10976,10978],{"class":42,"line":373},[40,10969,47],{"class":46},[40,10971,51],{"class":50},[40,10973,55],{"class":54},[40,10975,59],{"class":58},[40,10977,62],{"class":54},[40,10979,66],{"class":65},[40,10981,10982,10984,10986,10988,10990],{"class":42,"line":387},[40,10983,5130],{"class":46},[40,10985,5133],{"class":357},[40,10987,433],{"class":54},[40,10989,5138],{"class":393},[40,10991,449],{"class":54},[40,10993,10994,10996,10998,11000,11002],{"class":42,"line":409},[40,10995,5130],{"class":46},[40,10997,5147],{"class":357},[40,10999,433],{"class":54},[40,11001,5152],{"class":393},[40,11003,449],{"class":54},[40,11005,11006],{"class":42,"line":415},[40,11007,327],{"emptyLinePlaceholder":326},[40,11009,11010,11012,11014,11016,11018,11020],{"class":42,"line":452},[40,11011,354],{"class":46},[40,11013,1075],{"class":357},[40,11015,361],{"class":54},[40,11017,364],{"class":46},[40,11019,367],{"class":58},[40,11021,370],{"class":54},[40,11023,11024],{"class":42,"line":481},[40,11025,11026],{"class":319},"    ## 我关心「得分」，得分了我就加分\n",[40,11028,11029,11031,11033,11035,11037,11039,11041,11043],{"class":42,"line":486},[40,11030,1103],{"class":58},[40,11032,400],{"class":54},[40,11034,1108],{"class":50},[40,11036,400],{"class":393},[40,11038,1113],{"class":357},[40,11040,433],{"class":54},[40,11042,1118],{"class":50},[40,11044,449],{"class":54},[40,11046,11047],{"class":42,"line":502},[40,11048,11049],{"class":319},"    ## 我关心「小鸟死亡」，死了我就结算\n",[40,11051,11052,11054,11056,11058,11060,11062,11064,11066],{"class":42,"line":527},[40,11053,1103],{"class":58},[40,11055,400],{"class":54},[40,11057,1129],{"class":50},[40,11059,400],{"class":393},[40,11061,1113],{"class":357},[40,11063,433],{"class":54},[40,11065,1138],{"class":50},[40,11067,449],{"class":54},[10,11069,11070,11072,11073,20],{},[37,11071,1113],{}," 的意思是：",[17,11074,11075],{},"「这个信号响的时候，请调用我的这个函数」",[774,11077,11078,11089,11095],{},[777,11079,11080,11083,11084,666,11086],{},[37,11081,11082],{},"EventBus.point_scored.connect(add_score)"," — 注意 ",[37,11085,1118],{},[17,11087,11088],{},"后面没有括号",[777,11090,11091,11092,11094],{},"有括号 ",[37,11093,5643],{}," = \"现在就执行它，把返回值给我\"",[777,11096,11097,11098,11100,11101,11104],{},"没括号 ",[37,11099,1118],{}," = \"把这个函数",[17,11102,11103],{},"本身","交给你，等信号响了你再执行\"",[10,11106,11107,11108,299,11111,11113,11114,11117,11118,11120,11121,11123,11124,20],{},"信号带的参数会",[17,11109,11110],{},"自动传进去",[37,11112,1108],{}," 定义时带了 ",[37,11115,11116],{},"amount: int","，emit 时传了 ",[37,11119,4497],{},"，所以 ",[37,11122,1118],{}," 会收到 ",[37,11125,11126],{},"amount = 1",[687,11128,11130],{"id":11129},"audiomanager-自己去听","AudioManager 自己去听",[30,11132,11134],{"className":32,"code":11133,"language":34,"meta":35,"style":35},"extends Node\n\n@onready var score_sfx: AudioStreamPlayer = $SFX\u002Fscore_sfx\n@onready var strike_sfx: AudioStreamPlayer = $SFX\u002Fstrike_sfx\n@onready var wing_sfx: AudioStreamPlayer = $SFX\u002Fwing_sfx\n\nfunc _ready() -> void:\n    ## 得分了我就放得分音效\n    EventBus.point_scored.connect(func(_amount): play_score())\n    ## 死了我就放撞击音效\n    EventBus.bird_died.connect(play_die)\n\nfunc play_score():\n    score_sfx.play()\n\nfunc play_die():\n    strike_sfx.play()\n\nfunc play_wing():\n    wing_sfx.play()\n",[37,11135,11136,11142,11146,11164,11182,11200,11204,11218,11223,11252,11257,11275,11279,11287,11297,11301,11309,11319,11323,11331],{"__ignoreMap":35},[40,11137,11138,11140],{"class":42,"line":43},[40,11139,310],{"class":309},[40,11141,313],{"class":58},[40,11143,11144],{"class":42,"line":316},[40,11145,327],{"emptyLinePlaceholder":326},[40,11147,11148,11150,11152,11154,11156,11158,11160,11162],{"class":42,"line":323},[40,11149,3990],{"class":357},[40,11151,1855],{"class":46},[40,11153,8525],{"class":50},[40,11155,55],{"class":54},[40,11157,8530],{"class":58},[40,11159,62],{"class":54},[40,11161,4005],{"class":309},[40,11163,8537],{"class":4008},[40,11165,11166,11168,11170,11172,11174,11176,11178,11180],{"class":42,"line":330},[40,11167,3990],{"class":357},[40,11169,1855],{"class":46},[40,11171,8546],{"class":50},[40,11173,55],{"class":54},[40,11175,8530],{"class":58},[40,11177,62],{"class":54},[40,11179,4005],{"class":309},[40,11181,8557],{"class":4008},[40,11183,11184,11186,11188,11190,11192,11194,11196,11198],{"class":42,"line":346},[40,11185,3990],{"class":357},[40,11187,1855],{"class":46},[40,11189,8566],{"class":50},[40,11191,55],{"class":54},[40,11193,8530],{"class":58},[40,11195,62],{"class":54},[40,11197,4005],{"class":309},[40,11199,8577],{"class":4008},[40,11201,11202],{"class":42,"line":351},[40,11203,327],{"emptyLinePlaceholder":326},[40,11205,11206,11208,11210,11212,11214,11216],{"class":42,"line":373},[40,11207,354],{"class":46},[40,11209,1075],{"class":357},[40,11211,361],{"class":54},[40,11213,364],{"class":46},[40,11215,367],{"class":58},[40,11217,370],{"class":54},[40,11219,11220],{"class":42,"line":387},[40,11221,11222],{"class":319},"    ## 得分了我就放得分音效\n",[40,11224,11225,11227,11229,11231,11233,11235,11237,11239,11241,11244,11246,11248,11250],{"class":42,"line":409},[40,11226,1103],{"class":58},[40,11228,400],{"class":54},[40,11230,1108],{"class":50},[40,11232,400],{"class":393},[40,11234,1113],{"class":357},[40,11236,433],{"class":54},[40,11238,354],{"class":46},[40,11240,433],{"class":54},[40,11242,11243],{"class":393},"_amount",[40,11245,522],{"class":54},[40,11247,394],{"class":393},[40,11249,4461],{"class":357},[40,11251,593],{"class":54},[40,11253,11254],{"class":42,"line":415},[40,11255,11256],{"class":319},"    ## 死了我就放撞击音效\n",[40,11258,11259,11261,11263,11265,11267,11269,11271,11273],{"class":42,"line":452},[40,11260,1103],{"class":58},[40,11262,400],{"class":54},[40,11264,1129],{"class":50},[40,11266,400],{"class":393},[40,11268,1113],{"class":357},[40,11270,433],{"class":54},[40,11272,3879],{"class":50},[40,11274,449],{"class":54},[40,11276,11277],{"class":42,"line":481},[40,11278,327],{"emptyLinePlaceholder":326},[40,11280,11281,11283,11285],{"class":42,"line":486},[40,11282,354],{"class":46},[40,11284,8588],{"class":357},[40,11286,1159],{"class":54},[40,11288,11289,11291,11293,11295],{"class":42,"line":502},[40,11290,8595],{"class":50},[40,11292,400],{"class":393},[40,11294,8600],{"class":357},[40,11296,2070],{"class":54},[40,11298,11299],{"class":42,"line":527},[40,11300,327],{"emptyLinePlaceholder":326},[40,11302,11303,11305,11307],{"class":42,"line":536},[40,11304,354],{"class":46},[40,11306,8613],{"class":357},[40,11308,1159],{"class":54},[40,11310,11311,11313,11315,11317],{"class":42,"line":566},[40,11312,8620],{"class":50},[40,11314,400],{"class":393},[40,11316,8600],{"class":357},[40,11318,2070],{"class":54},[40,11320,11321],{"class":42,"line":596},[40,11322,327],{"emptyLinePlaceholder":326},[40,11324,11325,11327,11329],{"class":42,"line":611},[40,11326,354],{"class":46},[40,11328,8637],{"class":357},[40,11330,1159],{"class":54},[40,11332,11333,11335,11337,11339],{"class":42,"line":619},[40,11334,8644],{"class":50},[40,11336,400],{"class":393},[40,11338,8600],{"class":357},[40,11340,2070],{"class":54},[10,11342,11343],{},"这里有个小技巧：",[30,11345,11347],{"className":32,"code":11346,"language":34,"meta":35,"style":35},"EventBus.point_scored.connect(func(_amount): play_score())\n",[37,11348,11349],{"__ignoreMap":35},[40,11350,11351,11353,11355,11357,11359,11361,11363,11365,11367,11369,11371,11373,11375],{"class":42,"line":43},[40,11352,10456],{"class":58},[40,11354,400],{"class":54},[40,11356,1108],{"class":50},[40,11358,400],{"class":393},[40,11360,1113],{"class":357},[40,11362,433],{"class":54},[40,11364,354],{"class":46},[40,11366,433],{"class":54},[40,11368,11243],{"class":393},[40,11370,522],{"class":54},[40,11372,394],{"class":393},[40,11374,4461],{"class":357},[40,11376,593],{"class":54},[10,11378,11379,7392,11381,11384,11385,11388,11389,11392],{},[37,11380,1108],{},[17,11382,11383],{},"带一个参数","，但 ",[37,11386,11387],{},"play_score()"," 这个函数",[17,11390,11391],{},"不接收参数"," — 直接连会因为参数数量对不上而报错。",[10,11394,11395,11396,6429,11399,11402,11403,20],{},"所以中间垫了一个",[17,11397,11398],{},"匿名函数",[37,11400,11401],{},"func(...): ..."," 这种没有名字的临时函数）：它负责接住那个参数，然后调用不带参数的 ",[37,11404,11387],{},[10,11406,11407,11408,11410,11411,11414],{},"参数名前面的下划线 ",[37,11409,11243],{}," 是 GDScript 的约定：",[17,11412,11413],{},"「这个参数我收到了，但我用不上」","。加了下划线，编辑器就不会警告你\"变量未使用\"。",[22,11416,11418,11419,11422],{"id":11417},"什么时候不该用-eventbus","什么时候",[17,11420,11421],{},"不该","用 EventBus？",[10,11424,11425,11426,20],{},"这点很重要 — EventBus 好用，但",[17,11427,11428],{},"不是所有通信都该走广播",[10,11430,11431,11432,8036],{},"看小鸟自己扇翅膀的代码（",[37,11433,2966],{},[30,11435,11437],{"className":32,"code":11436,"language":34,"meta":35,"style":35},"if Input.is_action_just_pressed(\"fly\") and GameManager.current_state == GameManager.GameState.PLAYING:\n    velocity.y = jump_force\n    AudioManager.play_wing()   # 👈 这里是直接调用，没走 EventBus\n",[37,11438,11439,11477,11489],{"__ignoreMap":35},[40,11440,11441,11443,11445,11447,11449,11451,11453,11455,11457,11459,11461,11463,11465,11467,11469,11471,11473,11475],{"class":42,"line":43},[40,11442,910],{"class":309},[40,11444,2031],{"class":58},[40,11446,400],{"class":393},[40,11448,2036],{"class":357},[40,11450,433],{"class":54},[40,11452,2041],{"class":342},[40,11454,522],{"class":54},[40,11456,3846],{"class":46},[40,11458,3849],{"class":58},[40,11460,400],{"class":54},[40,11462,3854],{"class":50},[40,11464,603],{"class":46},[40,11466,3849],{"class":58},[40,11468,400],{"class":54},[40,11470,1169],{"class":50},[40,11472,400],{"class":54},[40,11474,3867],{"class":309},[40,11476,370],{"class":393},[40,11478,11479,11481,11483,11485,11487],{"class":42,"line":316},[40,11480,2392],{"class":50},[40,11482,400],{"class":54},[40,11484,1995],{"class":50},[40,11486,62],{"class":54},[40,11488,2058],{"class":50},[40,11490,11491,11493,11495,11497,11499],{"class":42,"line":323},[40,11492,8764],{"class":58},[40,11494,400],{"class":393},[40,11496,8769],{"class":357},[40,11498,361],{"class":54},[40,11500,11501],{"class":319},"   # 👈 这里是直接调用，没走 EventBus\n",[10,11503,11504],{},"为什么这里可以直接调用？",[10,11506,11507,11508,11511],{},"因为",[17,11509,11510],{},"翅膀声只有小鸟自己关心","。没有第二个系统需要知道\"小鸟扇了一下翅膀\"这件事 — 相机不关心、成就不关心、UI 也不关心。",[10,11513,11514,11515,11518],{},"给一个只有一个听众的事件建广播站，就是",[17,11516,11517],{},"过度设计","：白白多一层间接，读代码的人还得多跳一次才知道最后发生了什么。",[10,11520,11521],{},"判断标准很简单：",[108,11523,11524,11534],{},[111,11525,11526],{},[114,11527,11528,11531],{},[117,11529,11530],{},"情况",[117,11532,11533],{},"用什么",[127,11535,11536,11548,11559,11566],{},[114,11537,11538,11545],{},[132,11539,11540,11541,11544],{},"一件事，",[17,11542,11543],{},"多个互不相干的系统","都要响应",[132,11546,11547],{},"✅ EventBus 广播",[114,11549,11550,11556],{},[132,11551,11540,11552,11555],{},[17,11553,11554],{},"只有一个","对象关心",[132,11557,11558],{},"✅ 直接调用",[114,11560,11561,11564],{},[132,11562,11563],{},"父节点操作自己的子节点",[132,11565,11558],{},[114,11567,11568,11571],{},[132,11569,11570],{},"子节点要通知父节点",[132,11572,11573,11574,11576],{},"✅ 节点自己的 ",[37,11575,5130],{}," 就够了，不用上总线",[267,11578,11579],{},[10,11580,11581,11582],{},"一句话：",[17,11583,11584],{},"EventBus 是给\"跨系统\"的事件用的，不是用来取代所有函数调用的。",[22,11586,11588],{"id":11587},"重构完对比一下","重构完，对比一下",[108,11590,11591,11603],{},[111,11592,11593],{},[114,11594,11595,11597,11600],{},[117,11596],{},[117,11598,11599],{},"重构前",[117,11601,11602],{},"重构后",[127,11604,11605,11618,11633],{},[114,11606,11607,11610,11613],{},[132,11608,11609],{},"死区要认识几个系统",[132,11611,11612],{},"2 个（音频 + 管理器）",[132,11614,11615],{},[17,11616,11617],{},"0 个",[114,11619,11620,11623,11628],{},[132,11621,11622],{},"加\"死亡震屏\"要改几个文件",[132,11624,11625,11626],{},"改 ",[37,11627,5756],{},[132,11629,11630],{},[17,11631,11632],{},"不改，新模块自己去听",[114,11634,11635,11638,11641],{},[132,11636,11637],{},"死区的职责",[132,11639,11640],{},"检测碰撞 + 派活",[132,11642,11643],{},[17,11644,11645],{},"只检测碰撞",[10,11647,11648,11649,11652],{},"游戏跑起来的",[17,11650,11651],{},"表现完全一样"," — 但下次你想加东西的时候，会真切感受到差别。",[1415,11654],{},[10,11656,11657,11658,11661],{},"下一章我们做",[17,11659,11660],{},"存档系统","，让最高分在关掉游戏之后还能留住 💾",[1444,11663,11664],{},"html pre.shiki code .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}html pre.shiki code .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}html pre.shiki code .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .s9nN2, html code.shiki .s9nN2{--shiki-default:#B07D48;--shiki-dark:#BD976A}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}",{"title":35,"searchDepth":316,"depth":316,"links":11666},[11667,11668,11669,11675,11681,11685,11687],{"id":10069,"depth":316,"text":10069},{"id":10225,"depth":316,"text":10226},{"id":10275,"depth":316,"text":10276,"children":11670},[11671,11672,11674],{"id":10372,"depth":323,"text":10373},{"id":10402,"depth":323,"text":11673},"@warning_ignore(\"unused_signal\") 是干嘛的？",{"id":10439,"depth":323,"text":10440},{"id":10573,"depth":316,"text":10573,"children":11676},[11677,11679],{"id":10576,"depth":323,"text":11678},"死区（killzone.gd）",{"id":10677,"depth":323,"text":11680},"障碍物（pillar_pair.gd）",{"id":10888,"depth":316,"text":10888,"children":11682},[11683,11684],{"id":10897,"depth":323,"text":10898},{"id":11129,"depth":323,"text":11130},{"id":11417,"depth":316,"text":11686},"什么时候不该用 EventBus？",{"id":11587,"depth":316,"text":11588},{},{"title":10052,"description":1474},"devlog\u002Fxggame-bird\u002F10-event-bus","W3HMBLBxC2-jN7KJuIIR_FEm9LOKBAKl9g6Ff2CfXSU",{"id":4,"title":5,"body":11693,"cover":1472,"date":1473,"description":1474,"extension":1475,"game":1476,"github":1477,"icon":1472,"meta":12736,"navigation":326,"path":1479,"seo":12737,"stem":1481,"toc":326,"__hash__":1482},{"type":7,"value":11694,"toc":12717},[11695,11697,11701,11703,11705,11723,11733,11737,11743,11745,11785,11791,11801,11807,11813,11859,11865,11873,11875,11879,12143,12147,12165,12167,12171,12177,12183,12187,12223,12231,12245,12251,12257,12261,12277,12279,12285,12289,12293,12303,12309,12311,12339,12349,12369,12379,12383,12387,12417,12421,12431,12433,12435,12563,12567,12577,12581,12589,12591,12625,12627,12633,12635,12639,12643,12657,12665,12667,12687,12695,12697,12699,12713,12715],[10,11696,12],{},[10,11698,15,11699,20],{},[17,11700,19],{},[22,11702,25],{"id":24},[10,11704,28],{},[30,11706,11707],{"className":32,"code":33,"language":34,"meta":35,"style":35},[37,11708,11709],{"__ignoreMap":35},[40,11710,11711,11713,11715,11717,11719,11721],{"class":42,"line":43},[40,11712,47],{"class":46},[40,11714,51],{"class":50},[40,11716,55],{"class":54},[40,11718,59],{"class":58},[40,11720,62],{"class":54},[40,11722,66],{"class":65},[10,11724,69,11725,73,11727,77,11729,81,11731,85],{},[17,11726,72],{},[17,11728,76],{},[37,11730,80],{},[37,11732,84],{},[10,11734,88,11735,92],{},[17,11736,91],{},[22,11738,11739,99,11741,103],{"id":95},[37,11740,98],{},[37,11742,102],{},[10,11744,106],{},[108,11746,11747,11757],{},[111,11748,11749],{},[114,11750,11751,11753,11755],{},[117,11752,119],{},[117,11754,122],{},[117,11756,125],{},[127,11758,11759,11773],{},[114,11760,11761,11765,11769],{},[132,11762,11763],{},[37,11764,98],{},[132,11766,138,11767,142],{},[17,11768,141],{},[132,11770,145,11771],{},[17,11772,148],{},[114,11774,11775,11779,11783],{},[132,11776,11777],{},[37,11778,102],{},[132,11780,157,11781],{},[17,11782,160],{},[132,11784,163],{},[10,11786,11787],{},[17,11788,168,11789,171],{},[37,11790,98],{},[10,11792,174,11793,177,11795,181,11797,187,11799,190],{},[37,11794,98],{},[37,11796,180],{},[183,11798,186],{"href":185},[17,11800,148],{},[10,11802,193,11803,196,11805,200],{},[37,11804,98],{},[17,11806,199],{},[10,11808,11809],{},[17,11810,11811,207],{},[37,11812,102],{},[108,11814,11815,11823],{},[111,11816,11817],{},[114,11818,11819,11821],{},[117,11820,216],{},[117,11822,219],{},[127,11824,11825,11833,11841,11849],{},[114,11826,11827,11829],{},[132,11828,226],{},[132,11830,11831],{},[37,11832,231],{},[114,11834,11835,11837],{},[132,11836,236],{},[132,11838,11839],{},[37,11840,241],{},[114,11842,11843,11845],{},[132,11844,246],{},[132,11846,11847],{},[37,11848,251],{},[114,11850,11851,11855],{},[132,11852,11853],{},[17,11854,258],{},[132,11856,261,11857,265],{},[17,11858,264],{},[267,11860,11861],{},[10,11862,271,11863,275],{},[17,11864,274],{},[267,11866,11867],{},[10,11868,280,11869,284,11871,288],{},[17,11870,283],{},[17,11872,287],{},[22,11874,292],{"id":291},[10,11876,295,11877,299],{},[37,11878,298],{},[30,11880,11881],{"className":32,"code":302,"language":34,"meta":35,"style":35},[37,11882,11883,11889,11893,11897,11907,11911,11925,11935,11949,11953,11981,12003,12007,12021,12041,12047,12075,12099,12111,12117],{"__ignoreMap":35},[40,11884,11885,11887],{"class":42,"line":43},[40,11886,310],{"class":309},[40,11888,313],{"class":58},[40,11890,11891],{"class":42,"line":316},[40,11892,320],{"class":319},[40,11894,11895],{"class":42,"line":323},[40,11896,327],{"emptyLinePlaceholder":326},[40,11898,11899,11901,11903,11905],{"class":42,"line":330},[40,11900,333],{"class":46},[40,11902,336],{"class":50},[40,11904,339],{"class":54},[40,11906,343],{"class":342},[40,11908,11909],{"class":42,"line":346},[40,11910,327],{"emptyLinePlaceholder":326},[40,11912,11913,11915,11917,11919,11921,11923],{"class":42,"line":351},[40,11914,354],{"class":46},[40,11916,358],{"class":357},[40,11918,361],{"class":54},[40,11920,364],{"class":46},[40,11922,367],{"class":58},[40,11924,370],{"class":54},[40,11926,11927,11929,11931,11933],{"class":42,"line":373},[40,11928,376],{"class":46},[40,11930,379],{"class":50},[40,11932,339],{"class":54},[40,11934,384],{"class":54},[40,11936,11937,11939,11941,11943,11945,11947],{"class":42,"line":387},[40,11938,390],{"class":342},[40,11940,394],{"class":393},[40,11942,397],{"class":58},[40,11944,400],{"class":393},[40,11946,403],{"class":50},[40,11948,406],{"class":393},[40,11950,11951],{"class":42,"line":409},[40,11952,412],{"class":54},[40,11954,11955,11957,11959,11961,11963,11965,11967,11969,11971,11973,11975,11977,11979],{"class":42,"line":415},[40,11956,376],{"class":46},[40,11958,420],{"class":50},[40,11960,339],{"class":54},[40,11962,425],{"class":58},[40,11964,400],{"class":393},[40,11966,430],{"class":357},[40,11968,433],{"class":54},[40,11970,436],{"class":50},[40,11972,439],{"class":54},[40,11974,425],{"class":58},[40,11976,400],{"class":54},[40,11978,446],{"class":309},[40,11980,449],{"class":54},[40,11982,11983,11985,11987,11989,11991,11993,11995,11997,11999,12001],{"class":42,"line":452},[40,11984,455],{"class":50},[40,11986,400],{"class":393},[40,11988,460],{"class":357},[40,11990,433],{"class":54},[40,11992,465],{"class":58},[40,11994,400],{"class":393},[40,11996,470],{"class":357},[40,11998,433],{"class":54},[40,12000,475],{"class":50},[40,12002,478],{"class":54},[40,12004,12005],{"class":42,"line":481},[40,12006,327],{"emptyLinePlaceholder":326},[40,12008,12009,12011,12013,12015,12017,12019],{"class":42,"line":486},[40,12010,354],{"class":46},[40,12012,491],{"class":357},[40,12014,361],{"class":54},[40,12016,364],{"class":46},[40,12018,367],{"class":58},[40,12020,370],{"class":54},[40,12022,12023,12025,12027,12029,12031,12033,12035,12037,12039],{"class":42,"line":502},[40,12024,505],{"class":309},[40,12026,508],{"class":46},[40,12028,425],{"class":58},[40,12030,400],{"class":393},[40,12032,515],{"class":357},[40,12034,433],{"class":54},[40,12036,436],{"class":50},[40,12038,522],{"class":54},[40,12040,370],{"class":393},[40,12042,12043,12045],{"class":42,"line":527},[40,12044,530],{"class":309},[40,12046,533],{"class":319},[40,12048,12049,12051,12053,12055,12057,12059,12061,12063,12065,12067,12069,12071,12073],{"class":42,"line":536},[40,12050,376],{"class":46},[40,12052,420],{"class":50},[40,12054,339],{"class":54},[40,12056,425],{"class":58},[40,12058,400],{"class":393},[40,12060,430],{"class":357},[40,12062,433],{"class":54},[40,12064,436],{"class":50},[40,12066,439],{"class":54},[40,12068,425],{"class":58},[40,12070,400],{"class":54},[40,12072,561],{"class":309},[40,12074,449],{"class":54},[40,12076,12077,12079,12081,12083,12085,12087,12089,12091,12093,12095,12097],{"class":42,"line":566},[40,12078,376],{"class":46},[40,12080,379],{"class":50},[40,12082,62],{"class":54},[40,12084,575],{"class":58},[40,12086,400],{"class":393},[40,12088,580],{"class":357},[40,12090,433],{"class":54},[40,12092,585],{"class":50},[40,12094,400],{"class":393},[40,12096,590],{"class":357},[40,12098,593],{"class":54},[40,12100,12101,12103,12105,12107,12109],{"class":42,"line":596},[40,12102,505],{"class":309},[40,12104,379],{"class":50},[40,12106,603],{"class":46},[40,12108,606],{"class":309},[40,12110,370],{"class":393},[40,12112,12113,12115],{"class":42,"line":611},[40,12114,530],{"class":309},[40,12116,616],{"class":319},[40,12118,12119,12121,12123,12125,12127,12129,12131,12133,12135,12137,12139,12141],{"class":42,"line":619},[40,12120,622],{"class":58},[40,12122,400],{"class":54},[40,12124,403],{"class":50},[40,12126,62],{"class":54},[40,12128,379],{"class":50},[40,12130,400],{"class":393},[40,12132,635],{"class":357},[40,12134,433],{"class":54},[40,12136,640],{"class":342},[40,12138,439],{"class":54},[40,12140,645],{"class":65},[40,12142,449],{"class":54},[10,12144,650,12145,20],{},[37,12146,653],{},[267,12148,12149],{},[10,12150,12151,299,12153,663,12155,666,12157,670,12159,674,12161,678,12163,20],{},[17,12152,660],{},[37,12154,653],{},[37,12156,397],{},[17,12158,669],{},[37,12160,673],{},[37,12162,677],{},[183,12164,682],{"href":681},[22,12166,685],{"id":685},[687,12168,12169],{"id":689},[37,12170,692],{},[10,12172,12173,697,12175,701],{},[37,12174,333],{},[17,12176,700],{},[10,12178,704,12179,707,12181,710],{},[37,12180,333],{},[37,12182,47],{},[687,12184,12185,717],{"id":713},[37,12186,716],{},[30,12188,12189],{"className":32,"code":720,"language":34,"meta":35,"style":35},[37,12190,12191,12207],{"__ignoreMap":35},[40,12192,12193,12195,12197,12199,12201,12203,12205],{"class":42,"line":43},[40,12194,47],{"class":46},[40,12196,379],{"class":50},[40,12198,339],{"class":54},[40,12200,733],{"class":54},[40,12202,736],{"class":393},[40,12204,739],{"class":54},[40,12206,742],{"class":319},[40,12208,12209,12211,12213,12215,12217,12219,12221],{"class":42,"line":316},[40,12210,47],{"class":46},[40,12212,379],{"class":50},[40,12214,62],{"class":54},[40,12216,733],{"class":54},[40,12218,736],{"class":393},[40,12220,739],{"class":54},[40,12222,759],{"class":319},[10,12224,12225,764,12227,768,12229,772],{},[37,12226,716],{},[17,12228,767],{},[17,12230,771],{},[774,12232,12233,12241],{},[777,12234,12235,782,12237,785,12239,789],{},[37,12236,781],{},[37,12238,475],{},[37,12240,788],{},[777,12242,12243,795],{},[37,12244,794],{},[10,12246,12247,804],{},[17,12248,800,12249,803],{},[37,12250,716],{},[687,12252,12253,99,12255],{"id":807},[37,12254,810],{},[37,12256,813],{},[10,12258,816,12259,820],{},[17,12260,819],{},[774,12262,12263,12271],{},[777,12264,12265,828,12267,299,12269],{},[37,12266,827],{},[17,12268,831],{},[37,12270,834],{},[777,12272,12273,840,12275],{},[37,12274,839],{},[17,12276,843],{},[10,12278,846],{},[267,12280,12281],{},[10,12282,851,12283,855],{},[17,12284,854],{},[687,12286,859,12287,863],{"id":858},[37,12288,862],{},[10,12290,866,12291,869],{},[37,12292,862],{},[10,12294,12295,875,12297,879,12299,883,12301,886],{},[17,12296,874],{},[37,12298,878],{},[17,12300,882],{},[37,12302,585],{},[10,12304,889,12305,893,12307,897],{},[17,12306,892],{},[37,12308,896],{},[687,12310,900],{"id":900},[30,12312,12313],{"className":32,"code":903,"language":34,"meta":35,"style":35},[37,12314,12315,12335],{"__ignoreMap":35},[40,12316,12317,12319,12321,12323,12325,12327,12329,12331,12333],{"class":42,"line":43},[40,12318,910],{"class":309},[40,12320,508],{"class":46},[40,12322,425],{"class":58},[40,12324,400],{"class":393},[40,12326,515],{"class":357},[40,12328,433],{"class":54},[40,12330,436],{"class":50},[40,12332,522],{"class":54},[40,12334,370],{"class":393},[40,12336,12337],{"class":42,"line":316},[40,12338,931],{"class":309},[10,12340,12341,937,12343,940,12345,944,12347,948],{},[17,12342,936],{},[37,12344,430],{},[37,12346,943],{},[37,12348,947],{},[30,12350,12351],{"className":32,"code":951,"language":34,"meta":35,"style":35},[37,12352,12353,12365],{"__ignoreMap":35},[40,12354,12355,12357,12359,12361,12363],{"class":42,"line":43},[40,12356,910],{"class":309},[40,12358,379],{"class":50},[40,12360,603],{"class":46},[40,12362,606],{"class":309},[40,12364,370],{"class":393},[40,12366,12367],{"class":42,"line":316},[40,12368,931],{"class":309},[10,12370,972,12371,976,12373,980,12375,983,12377,987],{},[17,12372,975],{},[37,12374,979],{},[37,12376,943],{},[37,12378,986],{},[10,12380,990,12381,994],{},[17,12382,993],{},[687,12384,12385],{"id":997},[37,12386,1000],{},[30,12388,12389],{"className":32,"code":1003,"language":34,"meta":35,"style":35},[37,12390,12391],{"__ignoreMap":35},[40,12392,12393,12395,12397,12399,12401,12403,12405,12407,12409,12411,12413,12415],{"class":42,"line":43},[40,12394,397],{"class":58},[40,12396,400],{"class":54},[40,12398,403],{"class":50},[40,12400,62],{"class":54},[40,12402,379],{"class":50},[40,12404,400],{"class":393},[40,12406,635],{"class":357},[40,12408,433],{"class":54},[40,12410,640],{"class":342},[40,12412,439],{"class":54},[40,12414,645],{"class":65},[40,12416,449],{"class":54},[10,12418,12419,1037],{},[37,12420,1036],{},[10,12422,1040,12423,1044,12425,1048,12427,1052,12429,1056],{},[17,12424,1043],{},[37,12426,1047],{},[37,12428,1051],{},[37,12430,1055],{},[22,12432,1060],{"id":1059},[10,12434,1063],{},[30,12436,12437],{"className":32,"code":1066,"language":34,"meta":35,"style":35},[37,12438,12439,12453,12465,12483,12501,12505,12509,12517,12531,12543,12551],{"__ignoreMap":35},[40,12440,12441,12443,12445,12447,12449,12451],{"class":42,"line":43},[40,12442,354],{"class":46},[40,12444,1075],{"class":357},[40,12446,361],{"class":54},[40,12448,364],{"class":46},[40,12450,367],{"class":58},[40,12452,370],{"class":54},[40,12454,12455,12457,12459,12461,12463],{"class":42,"line":316},[40,12456,1088],{"class":58},[40,12458,400],{"class":393},[40,12460,1093],{"class":357},[40,12462,361],{"class":54},[40,12464,1098],{"class":319},[40,12466,12467,12469,12471,12473,12475,12477,12479,12481],{"class":42,"line":323},[40,12468,1103],{"class":58},[40,12470,400],{"class":54},[40,12472,1108],{"class":50},[40,12474,400],{"class":393},[40,12476,1113],{"class":357},[40,12478,433],{"class":54},[40,12480,1118],{"class":50},[40,12482,449],{"class":54},[40,12484,12485,12487,12489,12491,12493,12495,12497,12499],{"class":42,"line":330},[40,12486,1103],{"class":58},[40,12488,400],{"class":54},[40,12490,1129],{"class":50},[40,12492,400],{"class":393},[40,12494,1113],{"class":357},[40,12496,433],{"class":54},[40,12498,1138],{"class":50},[40,12500,449],{"class":54},[40,12502,12503],{"class":42,"line":346},[40,12504,327],{"emptyLinePlaceholder":326},[40,12506,12507],{"class":42,"line":351},[40,12508,1149],{"class":319},[40,12510,12511,12513,12515],{"class":42,"line":373},[40,12512,354],{"class":46},[40,12514,1156],{"class":357},[40,12516,1159],{"class":54},[40,12518,12519,12521,12523,12525,12527,12529],{"class":42,"line":387},[40,12520,1164],{"class":357},[40,12522,433],{"class":54},[40,12524,1169],{"class":58},[40,12526,400],{"class":54},[40,12528,1174],{"class":309},[40,12530,449],{"class":54},[40,12532,12533,12535,12537,12539,12541],{"class":42,"line":409},[40,12534,505],{"class":309},[40,12536,51],{"class":50},[40,12538,1185],{"class":46},[40,12540,1188],{"class":50},[40,12542,370],{"class":393},[40,12544,12545,12547,12549],{"class":42,"line":415},[40,12546,1195],{"class":50},[40,12548,62],{"class":54},[40,12550,1200],{"class":50},[40,12552,12553,12555,12557,12559,12561],{"class":42,"line":452},[40,12554,1205],{"class":58},[40,12556,400],{"class":393},[40,12558,1210],{"class":357},[40,12560,361],{"class":54},[40,12562,1215],{"class":319},[687,12564,1219,12565,1222],{"id":1218},[37,12566,673],{},[10,12568,12569,1227,12571,1231,12573,1234,12575,1237],{},[37,12570,673],{},[17,12572,1230],{},[37,12574,673],{},[37,12576,403],{},[687,12578,1241,12579],{"id":1240},[17,12580,1244],{},[10,12582,1247,12583,1251,12585,1255,12587,1258],{},[37,12584,1250],{},[17,12586,1254],{},[37,12588,910],{},[10,12590,1261],{},[108,12592,12593,12601],{},[111,12594,12595],{},[114,12596,12597,12599],{},[117,12598,1270],{},[117,12600,1273],{},[127,12602,12603,12609,12615],{},[114,12604,12605,12607],{},[132,12606,1280],{},[132,12608,1283],{},[114,12610,12611,12613],{},[132,12612,1288],{},[132,12614,1291],{},[114,12616,12617,12621],{},[132,12618,12619],{},[17,12620,1298],{},[132,12622,1301,12623,1305],{},[17,12624,1304],{},[10,12626,1308],{},[267,12628,12629],{},[10,12630,1313,12631],{},[17,12632,1316],{},[22,12634,1320],{"id":1319},[10,12636,12637],{},[17,12638,1325],{},[10,12640,1328,12641,1332],{},[37,12642,1331],{},[774,12644,12645,12649,12653],{},[777,12646,1337,12647,1341],{},[17,12648,1340],{},[777,12650,1344,12651,1348],{},[17,12652,1347],{},[777,12654,1351,12655],{},[17,12656,1354],{},[10,12658,1357,12659,1361,12661,1361,12663,1368],{},[17,12660,1360],{},[17,12662,1364],{},[17,12664,1367],{},[22,12666,1371],{"id":1371},[1373,12668,12669,12671,12675,12679],{},[777,12670,1377],{},[777,12672,12673,1383],{},[17,12674,1382],{},[777,12676,1386,12677,1390],{},[37,12678,1389],{},[777,12680,1393,12681,1396,12683,1400,12685],{},[17,12682,274],{},[37,12684,1399],{},[37,12686,1403],{},[10,12688,1406,12689,1409,12691,666,12693,20],{},[37,12690,653],{},[37,12692,397],{},[17,12694,669],{},[1415,12696],{},[10,12698,1419],{},[774,12700,12701,12703,12705,12707,12709,12711],{},[777,12702,1424],{},[777,12704,1427],{},[777,12706,1430],{},[777,12708,1433],{},[777,12710,1436],{},[777,12712,1439],{},[10,12714,1442],{},[1444,12716,1446],{},{"title":35,"searchDepth":316,"depth":316,"links":12718},[12719,12720,12721,12722,12730,12734,12735],{"id":24,"depth":316,"text":25},{"id":95,"depth":316,"text":1451},{"id":291,"depth":316,"text":292},{"id":685,"depth":316,"text":685,"children":12723},[12724,12725,12726,12727,12728,12729],{"id":689,"depth":323,"text":692},{"id":713,"depth":323,"text":1457},{"id":807,"depth":323,"text":1459},{"id":858,"depth":323,"text":1461},{"id":900,"depth":323,"text":900},{"id":997,"depth":323,"text":1000},{"id":1059,"depth":316,"text":1060,"children":12731},[12732,12733],{"id":1218,"depth":323,"text":1467},{"id":1240,"depth":323,"text":1469},{"id":1319,"depth":316,"text":1320},{"id":1371,"depth":316,"text":1371},{},{"title":5,"description":1474},1788955184102]