[{"data":1,"prerenderedAt":13094},["ShallowReactive",2],{"\u002Fdevlog\u002Fxggame-bird\u002F10-event-bus":3,"devlog-chapters-xggame-bird":1841},{"id":4,"title":5,"body":6,"cover":1830,"date":1831,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":1836,"navigation":65,"path":1837,"seo":1838,"stem":1839,"toc":65,"__hash__":1840},"devlog\u002Fdevlog\u002Fxggame-bird\u002F10-event-bus.md","重构：信号总线 EventBus",{"type":7,"value":8,"toc":1807},"minimark",[9,13,24,28,37,188,195,198,224,231,238,242,245,260,265,271,288,292,299,388,395,400,403,406,426,433,439,446,454,463,467,484,495,576,581,601,610,613,621,712,719,726,962,967,970,976,980,1170,1178,1209,1231,1235,1468,1471,1504,1521,1534,1544,1552,1558,1565,1642,1645,1652,1659,1662,1717,1725,1729,1786,1793,1796,1803],[10,11,12],"p",{},"游戏已经能玩、能上架了。但回头看代码，有个地方越写越别扭 — 这一章我们把它重构掉。",[14,15,16],"blockquote",{},[10,17,18,19,23],{},"这章不加新功能，",[20,21,22],"strong",{},"只改结构","。改完游戏行为一模一样，但以后加东西会轻松很多。",[25,26,27],"h2",{"id":27},"先看问题出在哪",[10,29,30,31,36],{},"回到 ",[32,33,35],"a",{"href":34},"\u002Fdevlog\u002Fxggame-bird\u002F05-game-manager","第 5 章"," 的死区脚本，现在长这样：",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-gdscript shiki shiki-themes vitesse-light vitesse-dark","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","gdscript","",[45,46,47,60,67,104,154,172],"code",{"__ignoreMap":43},[48,49,52,56],"span",{"class":50,"line":51},"line",1,[48,53,55],{"class":54},"sTPum","extends",[48,57,59],{"class":58},"s_NWU"," Area2D\n",[48,61,63],{"class":50,"line":62},2,[48,64,66],{"emptyLinePlaceholder":65},true,"\n",[48,68,70,74,78,82,86,89,92,95,98,101],{"class":50,"line":69},3,[48,71,73],{"class":72},"s5TCs","func",[48,75,77],{"class":76},"s_xSY"," _on_body_entered",[48,79,81],{"class":80},"si6no","(",[48,83,85],{"class":84},"s8w-G","body",[48,87,88],{"class":80},":",[48,90,91],{"class":58}," Node2D",[48,93,94],{"class":80},")",[48,96,97],{"class":72}," ->",[48,99,100],{"class":58}," void",[48,102,103],{"class":80},":\n",[48,105,107,110,114,117,120,123,127,130,133,135,138,140,142,144,147,149,152],{"class":50,"line":106},4,[48,108,109],{"class":54},"    if",[48,111,113],{"class":112},"s9nN2"," body",[48,115,116],{"class":80},".",[48,118,119],{"class":112},"name",[48,121,122],{"class":72}," ==",[48,124,126],{"class":125},"spP0B"," \"Bird\"",[48,128,129],{"class":72}," and",[48,131,132],{"class":58}," GameManager",[48,134,116],{"class":80},[48,136,137],{"class":112},"current_state",[48,139,122],{"class":72},[48,141,132],{"class":58},[48,143,116],{"class":80},[48,145,146],{"class":112},"GameState",[48,148,116],{"class":80},[48,150,151],{"class":54},"PLAYING",[48,153,103],{"class":84},[48,155,157,160,162,165,168],{"class":50,"line":156},5,[48,158,159],{"class":58},"        AudioManager",[48,161,116],{"class":84},[48,163,164],{"class":76},"play_die",[48,166,167],{"class":80},"()",[48,169,171],{"class":170},"snYqZ","    # 通知音频：放死亡音效\n",[48,173,175,178,180,183,185],{"class":50,"line":174},6,[48,176,177],{"class":58},"        GameManager",[48,179,116],{"class":84},[48,181,182],{"class":76},"game_over",[48,184,167],{"class":80},[48,186,187],{"class":170},"    # 通知管理器：游戏结束\n",[10,189,190,191,194],{},"看起来没毛病。但你数一下：",[20,192,193],{},"小鸟撞一次墙，这个脚本要挨个去通知 2 个人","。",[10,196,197],{},"现在假设你想加点东西：",[199,200,201,212,218],"ul",{},[202,203,204,205,208,209],"li",{},"想加",[20,206,207],{},"屏幕震动"," → 回来改一行 ",[45,210,211],{},"CameraShake.shake()",[202,213,204,214,217],{},[20,215,216],{},"死亡粒子特效"," → 再回来改一行",[202,219,204,220,223],{},[20,221,222],{},"成就统计","（死亡次数 +1）→ 再回来改一行",[10,225,226,227,230],{},"每加一个功能，就要回来改这个死区脚本一次。而死区本身的职责其实只有一句话：",[20,228,229],{},"\"我检测到小鸟撞上来了\"","。它凭什么要知道游戏里有音频系统、有相机、有成就系统？",[10,232,233,234,237],{},"这就是所谓的 ",[20,235,236],{},"「发信人必须认识所有收信人」"," — 耦合。",[25,239,241],{"id":240},"换个思路广播","换个思路：广播",[10,243,244],{},"想象一下机场广播：",[199,246,247,250,257],{},[202,248,249],{},"广播员喊一句「CA1234 航班开始登机」",[202,251,252,253,256],{},"他",[20,254,255],{},"不需要知道","候机厅里坐了谁",[202,258,259],{},"谁买了这班机票，谁自己站起来往登机口走",[10,261,262],{},[20,263,264],{},"广播员和乘客互不认识，但信息传到了。",[10,266,267,268,194],{},"我们要做的就是给游戏装一个广播站 — ",[20,269,270],{},"EventBus（信号总线）",[199,272,273,276,282],{},[202,274,275],{},"死区只负责喊：「鸟死了！」",[202,277,278,279],{},"至于谁听见了、听见了要干啥，",[20,280,281],{},"死区一概不管",[202,283,284,285],{},"以后想加震屏、加特效、加成就，只要让新模块自己去\"听\"这个广播，",[20,286,287],{},"死区一行都不用改",[25,289,291],{"id":290},"创建-eventbus","创建 EventBus",[10,293,294,295,298],{},"新建文件 ",[45,296,297],{},"scripts\u002Fautoload\u002Fevent_bus.gd","：",[38,300,302],{"className":40,"code":301,"language":42,"meta":43,"style":43},"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",[45,303,304,311,316,321,325,338,349,354,365],{"__ignoreMap":43},[48,305,306,308],{"class":50,"line":51},[48,307,55],{"class":54},[48,309,310],{"class":58}," Node\n",[48,312,313],{"class":50,"line":62},[48,314,315],{"class":170},"## 全局信号总线：只定义信号，不写逻辑\n",[48,317,318],{"class":50,"line":69},[48,319,320],{"class":170},"## 谁发生了事就在这里喊，谁关心就自己连接\n",[48,322,323],{"class":50,"line":106},[48,324,66],{"emptyLinePlaceholder":65},[48,326,327,330,332,335],{"class":50,"line":156},[48,328,329],{"class":76},"@warning_ignore",[48,331,81],{"class":80},[48,333,334],{"class":125},"\"unused_signal\"",[48,336,337],{"class":80},")\n",[48,339,340,343,346],{"class":50,"line":174},[48,341,342],{"class":72},"signal",[48,344,345],{"class":76}," bird_died",[48,347,348],{"class":170},"                  # 小鸟死亡\n",[48,350,352],{"class":50,"line":351},7,[48,353,66],{"emptyLinePlaceholder":65},[48,355,357,359,361,363],{"class":50,"line":356},8,[48,358,329],{"class":76},[48,360,81],{"class":80},[48,362,334],{"class":125},[48,364,337],{"class":80},[48,366,368,370,373,375,378,380,383,385],{"class":50,"line":367},9,[48,369,342],{"class":72},[48,371,372],{"class":76}," point_scored",[48,374,81],{"class":80},[48,376,377],{"class":84},"amount",[48,379,88],{"class":80},[48,381,382],{"class":58}," int",[48,384,94],{"class":80},[48,386,387],{"class":170},"  # 得分\n",[10,389,390,391,394],{},"就这么点东西。",[20,392,393],{},"整个文件没有一个函数"," — 这是故意的。",[396,397,399],"h3",{"id":398},"为什么-eventbus-里不能写逻辑","为什么 EventBus 里不能写逻辑？",[10,401,402],{},"这是新手最容易踩的坑：一看到\"全局单例\"就忍不住往里塞代码，最后 EventBus 变成第二个 GameManager，两边职责打架。",[10,404,405],{},"记住这条规矩：",[199,407,408,414,420],{},[202,409,410,413],{},[20,411,412],{},"EventBus 只管「喊」"," — 它是个空壳广播站，只定义\"有哪些事件\"",[202,415,416,419],{},[20,417,418],{},"GameManager 只管「游戏规则」"," — 分数怎么算、状态怎么转",[202,421,422,423],{},"一个是喇叭，一个是裁判，",[20,424,425],{},"不要合并",[396,427,429,432],{"id":428},"warning_ignoreunused_signal-是干嘛的",[45,430,431],{},"@warning_ignore(\"unused_signal\")"," 是干嘛的？",[10,434,435,436,194],{},"如果不加这行，Godot 编辑器会给你画一条黄色波浪线警告：",[20,437,438],{},"「这个信号定义了但从来没在本文件里 emit 过」",[10,440,441,442,445],{},"Godot 这个检查本身是好意 — 通常你定义了信号却不发，多半是写漏了。但 EventBus 的",[20,443,444],{},"设计初衷就是\"在别的文件里 emit\"","，所以这个警告在这里是误报。",[10,447,448,450,451,194],{},[45,449,431],{}," 就是告诉编辑器：",[20,452,453],{},"这里我是故意的，别提醒我了",[14,455,456],{},[10,457,458,459,462],{},"注意它只对",[20,460,461],{},"紧接着的下一行","生效。所以两个信号要各写一次，不能只在文件顶上写一个。",[396,464,466],{"id":465},"注册-autoload-顺序很重要","注册 Autoload — 顺序很重要",[10,468,469,470,472,473,476,477,480,481,194],{},"和 ",[32,471,35],{"href":34}," 一样，",[20,474,475],{},"项目 → 项目设置 → 全局 → 自动加载","，加入 ",[45,478,479],{},"event_bus.gd","，命名 ",[45,482,483],{},"EventBus",[10,485,486,487,490,491,494],{},"但这次有个",[20,488,489],{},"关键细节","：Autoload 列表是",[20,492,493],{},"从上往下依次加载","的，最终顺序要是这样 👇",[496,497,498,514],"table",{},[499,500,501],"thead",{},[502,503,504,508,511],"tr",{},[505,506,507],"th",{},"顺序",[505,509,510],{},"名称",[505,512,513],{},"为什么在这个位置",[515,516,517,534,547,563],"tbody",{},[502,518,519,523,527],{},[520,521,522],"td",{},"1",[520,524,525],{},[45,526,483],{},[520,528,529,530,533],{},"大家都要连它，必须",[20,531,532],{},"第一个","准备好",[502,535,536,539,544],{},[520,537,538],{},"2",[520,540,541],{},[45,542,543],{},"SaveManager",[520,545,546],{},"下一章讲，GameManager 要用它读档",[502,548,549,552,557],{},[520,550,551],{},"3",[520,553,554],{},[45,555,556],{},"GameManager",[520,558,559,562],{},[45,560,561],{},"_ready()"," 里要连 EventBus 的信号",[502,564,565,568,573],{},[520,566,567],{},"4",[520,569,570],{},[45,571,572],{},"AudioManager",[520,574,575],{},"同样要连 EventBus 的信号",[10,577,578],{},[20,579,580],{},"为什么顺序会出问题？",[10,582,583,584,586,587,590,591,593,594,596,597,600],{},"因为 GameManager 的 ",[45,585,561],{}," 里会写 ",[45,588,589],{},"EventBus.point_scored.connect(...)","。如果 EventBus 排在 GameManager 后面，那么 GameManager 跑 ",[45,592,561],{}," 的那一刻，",[45,595,483],{}," 这个名字",[20,598,599],{},"还不存在"," — 游戏会直接报错崩掉。",[14,602,603],{},[10,604,605,606,609],{},"在自动加载列表里可以直接",[20,607,608],{},"拖动条目","调整上下顺序。",[25,611,612],{"id":612},"改造发信方",[396,614,616,617,620],{"id":615},"死区killzonegd","死区（",[45,618,619],{},"killzone.gd","）",[38,622,624],{"className":40,"code":623,"language":42,"meta":43,"style":43},"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",[45,625,626,632,636,658,694],{"__ignoreMap":43},[48,627,628,630],{"class":50,"line":51},[48,629,55],{"class":54},[48,631,59],{"class":58},[48,633,634],{"class":50,"line":62},[48,635,66],{"emptyLinePlaceholder":65},[48,637,638,640,642,644,646,648,650,652,654,656],{"class":50,"line":69},[48,639,73],{"class":72},[48,641,77],{"class":76},[48,643,81],{"class":80},[48,645,85],{"class":84},[48,647,88],{"class":80},[48,649,91],{"class":58},[48,651,94],{"class":80},[48,653,97],{"class":72},[48,655,100],{"class":58},[48,657,103],{"class":80},[48,659,660,662,664,666,668,670,672,674,676,678,680,682,684,686,688,690,692],{"class":50,"line":106},[48,661,109],{"class":54},[48,663,113],{"class":112},[48,665,116],{"class":80},[48,667,119],{"class":112},[48,669,122],{"class":72},[48,671,126],{"class":125},[48,673,129],{"class":72},[48,675,132],{"class":58},[48,677,116],{"class":80},[48,679,137],{"class":112},[48,681,122],{"class":72},[48,683,132],{"class":58},[48,685,116],{"class":80},[48,687,146],{"class":112},[48,689,116],{"class":80},[48,691,151],{"class":54},[48,693,103],{"class":84},[48,695,696,699,701,704,706,709],{"class":50,"line":156},[48,697,698],{"class":58},"        EventBus",[48,700,116],{"class":80},[48,702,703],{"class":112},"bird_died",[48,705,116],{"class":84},[48,707,708],{"class":76},"emit",[48,710,711],{"class":80},"()\n",[10,713,714,715,718],{},"原来的两行通知，变成",[20,716,717],{},"一行广播","。这个脚本现在完全不知道音频系统和 GameManager 的存在了 — 它只知道\"鸟死了\"这件事发生了。",[396,720,722,723,620],{"id":721},"障碍物pillar_pairgd","障碍物（",[45,724,725],{},"pillar_pair.gd",[38,727,729],{"className":40,"code":728,"language":42,"meta":43,"style":43},"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",[45,730,731,738,742,760,785,789,814,838,859,882,890,895,901,925,942],{"__ignoreMap":43},[48,732,733,735],{"class":50,"line":51},[48,734,55],{"class":54},[48,736,737],{"class":58}," Node2D\n",[48,739,740],{"class":50,"line":62},[48,741,66],{"emptyLinePlaceholder":65},[48,743,744,747,750,753,756],{"class":50,"line":69},[48,745,746],{"class":76},"@export",[48,748,749],{"class":72}," var",[48,751,752],{"class":112}," speed",[48,754,755],{"class":80}," :=",[48,757,759],{"class":758},"sqbOQ"," 200.0\n",[48,761,762,765,767,770,772,775,778,781],{"class":50,"line":106},[48,763,764],{"class":76},"@onready",[48,766,749],{"class":72},[48,768,769],{"class":112}," goal",[48,771,88],{"class":80},[48,773,774],{"class":58}," Area2D",[48,776,777],{"class":80}," =",[48,779,780],{"class":54}," $",[48,782,784],{"class":783},"sfsYZ","Goal\n",[48,786,787],{"class":50,"line":156},[48,788,66],{"emptyLinePlaceholder":65},[48,790,791,793,796,798,801,803,806,808,810,812],{"class":50,"line":174},[48,792,73],{"class":72},[48,794,795],{"class":76}," _physics_process",[48,797,81],{"class":80},[48,799,800],{"class":84},"delta",[48,802,88],{"class":80},[48,804,805],{"class":58}," float",[48,807,94],{"class":80},[48,809,97],{"class":72},[48,811,100],{"class":58},[48,813,103],{"class":80},[48,815,816,818,820,822,824,826,828,830,832,834,836],{"class":50,"line":351},[48,817,109],{"class":54},[48,819,132],{"class":58},[48,821,116],{"class":80},[48,823,137],{"class":112},[48,825,122],{"class":72},[48,827,132],{"class":58},[48,829,116],{"class":80},[48,831,146],{"class":112},[48,833,116],{"class":80},[48,835,151],{"class":54},[48,837,103],{"class":84},[48,839,840,843,845,848,851,853,856],{"class":50,"line":356},[48,841,842],{"class":112},"        position",[48,844,116],{"class":80},[48,846,847],{"class":112},"x",[48,849,850],{"class":72}," -=",[48,852,752],{"class":112},[48,854,855],{"class":72}," *",[48,857,858],{"class":112}," delta\n",[48,860,861,864,867,869,871,874,877,880],{"class":50,"line":367},[48,862,863],{"class":54},"        if",[48,865,866],{"class":112}," position",[48,868,116],{"class":80},[48,870,847],{"class":112},[48,872,873],{"class":72}," \u003C",[48,875,876],{"class":72}," -",[48,878,879],{"class":758},"500",[48,881,103],{"class":84},[48,883,885,888],{"class":50,"line":884},10,[48,886,887],{"class":76},"            queue_free",[48,889,711],{"class":80},[48,891,893],{"class":50,"line":892},11,[48,894,66],{"emptyLinePlaceholder":65},[48,896,898],{"class":50,"line":897},12,[48,899,900],{"class":170},"# 小鸟穿过得分区\n",[48,902,904,906,909,911,913,915,917,919,921,923],{"class":50,"line":903},13,[48,905,73],{"class":72},[48,907,908],{"class":76}," _on_goal_body_entered",[48,910,81],{"class":80},[48,912,85],{"class":84},[48,914,88],{"class":80},[48,916,91],{"class":58},[48,918,94],{"class":80},[48,920,97],{"class":72},[48,922,100],{"class":58},[48,924,103],{"class":80},[48,926,928,930,932,934,936,938,940],{"class":50,"line":927},14,[48,929,109],{"class":54},[48,931,113],{"class":112},[48,933,116],{"class":80},[48,935,119],{"class":112},[48,937,122],{"class":72},[48,939,126],{"class":125},[48,941,103],{"class":84},[48,943,945,947,949,952,954,956,958,960],{"class":50,"line":944},15,[48,946,698],{"class":58},[48,948,116],{"class":80},[48,950,951],{"class":112},"point_scored",[48,953,116],{"class":84},[48,955,708],{"class":76},[48,957,81],{"class":80},[48,959,522],{"class":758},[48,961,337],{"class":80},[10,963,964,965,194],{},"同样，原来那一坨「播音效 + 调用 add_score + 禁用区域」三件事，缩成一行 ",[45,966,708],{},[25,968,969],{"id":969},"改造收信方",[10,971,972,973],{},"发信方轻松了，那\"接活\"的代码去哪了？",[20,974,975],{},"搬到了关心这件事的人自己家里。",[396,977,979],{"id":978},"gamemanager-自己去听","GameManager 自己去听",[38,981,983],{"className":40,"code":982,"language":42,"meta":43,"style":43},"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",[45,984,985,991,995,1022,1039,1043,1059,1074,1088,1102,1106,1121,1126,1147,1152],{"__ignoreMap":43},[48,986,987,989],{"class":50,"line":51},[48,988,55],{"class":54},[48,990,310],{"class":58},[48,992,993],{"class":50,"line":62},[48,994,66],{"emptyLinePlaceholder":65},[48,996,997,1000,1003,1006,1009,1012,1014,1016,1019],{"class":50,"line":69},[48,998,999],{"class":54},"enum",[48,1001,1002],{"class":58}," GameState",[48,1004,1005],{"class":80}," {",[48,1007,1008],{"class":112}," READY",[48,1010,1011],{"class":84},", ",[48,1013,151],{"class":112},[48,1015,1011],{"class":84},[48,1017,1018],{"class":112},"GAME_OVER",[48,1020,1021],{"class":80}," }\n",[48,1023,1024,1027,1030,1032,1034,1036],{"class":50,"line":106},[48,1025,1026],{"class":72},"var",[48,1028,1029],{"class":112}," current_state",[48,1031,777],{"class":80},[48,1033,1002],{"class":58},[48,1035,116],{"class":80},[48,1037,1038],{"class":54},"READY\n",[48,1040,1041],{"class":50,"line":156},[48,1042,66],{"emptyLinePlaceholder":65},[48,1044,1045,1047,1050,1052,1054,1056],{"class":50,"line":174},[48,1046,1026],{"class":72},[48,1048,1049],{"class":112}," total_score",[48,1051,88],{"class":80},[48,1053,382],{"class":58},[48,1055,777],{"class":80},[48,1057,1058],{"class":758}," 0\n",[48,1060,1061,1063,1066,1068,1070,1072],{"class":50,"line":351},[48,1062,1026],{"class":72},[48,1064,1065],{"class":112}," high_score",[48,1067,88],{"class":80},[48,1069,382],{"class":58},[48,1071,777],{"class":80},[48,1073,1058],{"class":758},[48,1075,1076,1078,1081,1083,1086],{"class":50,"line":356},[48,1077,342],{"class":72},[48,1079,1080],{"class":76}," score_changed",[48,1082,81],{"class":80},[48,1084,1085],{"class":84},"new_score",[48,1087,337],{"class":80},[48,1089,1090,1092,1095,1097,1100],{"class":50,"line":367},[48,1091,342],{"class":72},[48,1093,1094],{"class":76}," state_changed",[48,1096,81],{"class":80},[48,1098,1099],{"class":84},"new_state",[48,1101,337],{"class":80},[48,1103,1104],{"class":50,"line":884},[48,1105,66],{"emptyLinePlaceholder":65},[48,1107,1108,1110,1113,1115,1117,1119],{"class":50,"line":892},[48,1109,73],{"class":72},[48,1111,1112],{"class":76}," _ready",[48,1114,167],{"class":80},[48,1116,97],{"class":72},[48,1118,100],{"class":58},[48,1120,103],{"class":80},[48,1122,1123],{"class":50,"line":897},[48,1124,1125],{"class":170},"    ## 我关心「得分」，得分了我就加分\n",[48,1127,1128,1131,1133,1135,1137,1140,1142,1145],{"class":50,"line":903},[48,1129,1130],{"class":58},"    EventBus",[48,1132,116],{"class":80},[48,1134,951],{"class":112},[48,1136,116],{"class":84},[48,1138,1139],{"class":76},"connect",[48,1141,81],{"class":80},[48,1143,1144],{"class":112},"add_score",[48,1146,337],{"class":80},[48,1148,1149],{"class":50,"line":927},[48,1150,1151],{"class":170},"    ## 我关心「小鸟死亡」，死了我就结算\n",[48,1153,1154,1156,1158,1160,1162,1164,1166,1168],{"class":50,"line":944},[48,1155,1130],{"class":58},[48,1157,116],{"class":80},[48,1159,703],{"class":112},[48,1161,116],{"class":84},[48,1163,1139],{"class":76},[48,1165,81],{"class":80},[48,1167,182],{"class":112},[48,1169,337],{"class":80},[10,1171,1172,1174,1175,194],{},[45,1173,1139],{}," 的意思是：",[20,1176,1177],{},"「这个信号响的时候，请调用我的这个函数」",[199,1179,1180,1192,1199],{},[202,1181,1182,1185,1186,1188,1189],{},[45,1183,1184],{},"EventBus.point_scored.connect(add_score)"," — 注意 ",[45,1187,1144],{}," ",[20,1190,1191],{},"后面没有括号",[202,1193,1194,1195,1198],{},"有括号 ",[45,1196,1197],{},"add_score()"," = \"现在就执行它，把返回值给我\"",[202,1200,1201,1202,1204,1205,1208],{},"没括号 ",[45,1203,1144],{}," = \"把这个函数",[20,1206,1207],{},"本身","交给你，等信号响了你再执行\"",[10,1210,1211,1212,298,1215,1217,1218,1221,1222,1224,1225,1227,1228,194],{},"信号带的参数会",[20,1213,1214],{},"自动传进去",[45,1216,951],{}," 定义时带了 ",[45,1219,1220],{},"amount: int","，emit 时传了 ",[45,1223,522],{},"，所以 ",[45,1226,1144],{}," 会收到 ",[45,1229,1230],{},"amount = 1",[396,1232,1234],{"id":1233},"audiomanager-自己去听","AudioManager 自己去听",[38,1236,1238],{"className":40,"code":1237,"language":42,"meta":43,"style":43},"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",[45,1239,1240,1246,1250,1271,1291,1311,1315,1329,1334,1366,1371,1389,1393,1403,1415,1419,1429,1441,1446,1456],{"__ignoreMap":43},[48,1241,1242,1244],{"class":50,"line":51},[48,1243,55],{"class":54},[48,1245,310],{"class":58},[48,1247,1248],{"class":50,"line":62},[48,1249,66],{"emptyLinePlaceholder":65},[48,1251,1252,1254,1256,1259,1261,1264,1266,1268],{"class":50,"line":69},[48,1253,764],{"class":76},[48,1255,749],{"class":72},[48,1257,1258],{"class":112}," score_sfx",[48,1260,88],{"class":80},[48,1262,1263],{"class":58}," AudioStreamPlayer",[48,1265,777],{"class":80},[48,1267,780],{"class":54},[48,1269,1270],{"class":783},"SFX\u002Fscore_sfx\n",[48,1272,1273,1275,1277,1280,1282,1284,1286,1288],{"class":50,"line":106},[48,1274,764],{"class":76},[48,1276,749],{"class":72},[48,1278,1279],{"class":112}," strike_sfx",[48,1281,88],{"class":80},[48,1283,1263],{"class":58},[48,1285,777],{"class":80},[48,1287,780],{"class":54},[48,1289,1290],{"class":783},"SFX\u002Fstrike_sfx\n",[48,1292,1293,1295,1297,1300,1302,1304,1306,1308],{"class":50,"line":156},[48,1294,764],{"class":76},[48,1296,749],{"class":72},[48,1298,1299],{"class":112}," wing_sfx",[48,1301,88],{"class":80},[48,1303,1263],{"class":58},[48,1305,777],{"class":80},[48,1307,780],{"class":54},[48,1309,1310],{"class":783},"SFX\u002Fwing_sfx\n",[48,1312,1313],{"class":50,"line":174},[48,1314,66],{"emptyLinePlaceholder":65},[48,1316,1317,1319,1321,1323,1325,1327],{"class":50,"line":351},[48,1318,73],{"class":72},[48,1320,1112],{"class":76},[48,1322,167],{"class":80},[48,1324,97],{"class":72},[48,1326,100],{"class":58},[48,1328,103],{"class":80},[48,1330,1331],{"class":50,"line":356},[48,1332,1333],{"class":170},"    ## 得分了我就放得分音效\n",[48,1335,1336,1338,1340,1342,1344,1346,1348,1350,1352,1355,1357,1360,1363],{"class":50,"line":367},[48,1337,1130],{"class":58},[48,1339,116],{"class":80},[48,1341,951],{"class":112},[48,1343,116],{"class":84},[48,1345,1139],{"class":76},[48,1347,81],{"class":80},[48,1349,73],{"class":72},[48,1351,81],{"class":80},[48,1353,1354],{"class":84},"_amount",[48,1356,94],{"class":80},[48,1358,1359],{"class":84},": ",[48,1361,1362],{"class":76},"play_score",[48,1364,1365],{"class":80},"())\n",[48,1367,1368],{"class":50,"line":884},[48,1369,1370],{"class":170},"    ## 死了我就放撞击音效\n",[48,1372,1373,1375,1377,1379,1381,1383,1385,1387],{"class":50,"line":892},[48,1374,1130],{"class":58},[48,1376,116],{"class":80},[48,1378,703],{"class":112},[48,1380,116],{"class":84},[48,1382,1139],{"class":76},[48,1384,81],{"class":80},[48,1386,164],{"class":112},[48,1388,337],{"class":80},[48,1390,1391],{"class":50,"line":897},[48,1392,66],{"emptyLinePlaceholder":65},[48,1394,1395,1397,1400],{"class":50,"line":903},[48,1396,73],{"class":72},[48,1398,1399],{"class":76}," play_score",[48,1401,1402],{"class":80},"():\n",[48,1404,1405,1408,1410,1413],{"class":50,"line":927},[48,1406,1407],{"class":112},"    score_sfx",[48,1409,116],{"class":84},[48,1411,1412],{"class":76},"play",[48,1414,711],{"class":80},[48,1416,1417],{"class":50,"line":944},[48,1418,66],{"emptyLinePlaceholder":65},[48,1420,1422,1424,1427],{"class":50,"line":1421},16,[48,1423,73],{"class":72},[48,1425,1426],{"class":76}," play_die",[48,1428,1402],{"class":80},[48,1430,1432,1435,1437,1439],{"class":50,"line":1431},17,[48,1433,1434],{"class":112},"    strike_sfx",[48,1436,116],{"class":84},[48,1438,1412],{"class":76},[48,1440,711],{"class":80},[48,1442,1444],{"class":50,"line":1443},18,[48,1445,66],{"emptyLinePlaceholder":65},[48,1447,1449,1451,1454],{"class":50,"line":1448},19,[48,1450,73],{"class":72},[48,1452,1453],{"class":76}," play_wing",[48,1455,1402],{"class":80},[48,1457,1459,1462,1464,1466],{"class":50,"line":1458},20,[48,1460,1461],{"class":112},"    wing_sfx",[48,1463,116],{"class":84},[48,1465,1412],{"class":76},[48,1467,711],{"class":80},[10,1469,1470],{},"这里有个小技巧：",[38,1472,1474],{"className":40,"code":1473,"language":42,"meta":43,"style":43},"EventBus.point_scored.connect(func(_amount): play_score())\n",[45,1475,1476],{"__ignoreMap":43},[48,1477,1478,1480,1482,1484,1486,1488,1490,1492,1494,1496,1498,1500,1502],{"class":50,"line":51},[48,1479,483],{"class":58},[48,1481,116],{"class":80},[48,1483,951],{"class":112},[48,1485,116],{"class":84},[48,1487,1139],{"class":76},[48,1489,81],{"class":80},[48,1491,73],{"class":72},[48,1493,81],{"class":80},[48,1495,1354],{"class":84},[48,1497,94],{"class":80},[48,1499,1359],{"class":84},[48,1501,1362],{"class":76},[48,1503,1365],{"class":80},[10,1505,1506,1508,1509,1512,1513,1516,1517,1520],{},[45,1507,951],{}," 信号",[20,1510,1511],{},"带一个参数","，但 ",[45,1514,1515],{},"play_score()"," 这个函数",[20,1518,1519],{},"不接收参数"," — 直接连会因为参数数量对不上而报错。",[10,1522,1523,1524,1527,1528,1531,1532,194],{},"所以中间垫了一个",[20,1525,1526],{},"匿名函数","（",[45,1529,1530],{},"func(...): ..."," 这种没有名字的临时函数）：它负责接住那个参数，然后调用不带参数的 ",[45,1533,1515],{},[10,1535,1536,1537,1539,1540,1543],{},"参数名前面的下划线 ",[45,1538,1354],{}," 是 GDScript 的约定：",[20,1541,1542],{},"「这个参数我收到了，但我用不上」","。加了下划线，编辑器就不会警告你\"变量未使用\"。",[25,1545,1547,1548,1551],{"id":1546},"什么时候不该用-eventbus","什么时候",[20,1549,1550],{},"不该","用 EventBus？",[10,1553,1554,1555,194],{},"这点很重要 — EventBus 好用，但",[20,1556,1557],{},"不是所有通信都该走广播",[10,1559,1560,1561,1564],{},"看小鸟自己扇翅膀的代码（",[45,1562,1563],{},"bird.gd","）：",[38,1566,1568],{"className":40,"code":1567,"language":42,"meta":43,"style":43},"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",[45,1569,1570,1612,1627],{"__ignoreMap":43},[48,1571,1572,1575,1578,1580,1583,1585,1588,1590,1592,1594,1596,1598,1600,1602,1604,1606,1608,1610],{"class":50,"line":51},[48,1573,1574],{"class":54},"if",[48,1576,1577],{"class":58}," Input",[48,1579,116],{"class":84},[48,1581,1582],{"class":76},"is_action_just_pressed",[48,1584,81],{"class":80},[48,1586,1587],{"class":125},"\"fly\"",[48,1589,94],{"class":80},[48,1591,129],{"class":72},[48,1593,132],{"class":58},[48,1595,116],{"class":80},[48,1597,137],{"class":112},[48,1599,122],{"class":72},[48,1601,132],{"class":58},[48,1603,116],{"class":80},[48,1605,146],{"class":112},[48,1607,116],{"class":80},[48,1609,151],{"class":54},[48,1611,103],{"class":84},[48,1613,1614,1617,1619,1622,1624],{"class":50,"line":62},[48,1615,1616],{"class":112},"    velocity",[48,1618,116],{"class":80},[48,1620,1621],{"class":112},"y",[48,1623,777],{"class":80},[48,1625,1626],{"class":112}," jump_force\n",[48,1628,1629,1632,1634,1637,1639],{"class":50,"line":69},[48,1630,1631],{"class":58},"    AudioManager",[48,1633,116],{"class":84},[48,1635,1636],{"class":76},"play_wing",[48,1638,167],{"class":80},[48,1640,1641],{"class":170},"   # 👈 这里是直接调用，没走 EventBus\n",[10,1643,1644],{},"为什么这里可以直接调用？",[10,1646,1647,1648,1651],{},"因为",[20,1649,1650],{},"翅膀声只有小鸟自己关心","。没有第二个系统需要知道\"小鸟扇了一下翅膀\"这件事 — 相机不关心、成就不关心、UI 也不关心。",[10,1653,1654,1655,1658],{},"给一个只有一个听众的事件建广播站，就是",[20,1656,1657],{},"过度设计","：白白多一层间接，读代码的人还得多跳一次才知道最后发生了什么。",[10,1660,1661],{},"判断标准很简单：",[496,1663,1664,1674],{},[499,1665,1666],{},[502,1667,1668,1671],{},[505,1669,1670],{},"情况",[505,1672,1673],{},"用什么",[515,1675,1676,1688,1699,1706],{},[502,1677,1678,1685],{},[520,1679,1680,1681,1684],{},"一件事，",[20,1682,1683],{},"多个互不相干的系统","都要响应",[520,1686,1687],{},"✅ EventBus 广播",[502,1689,1690,1696],{},[520,1691,1680,1692,1695],{},[20,1693,1694],{},"只有一个","对象关心",[520,1697,1698],{},"✅ 直接调用",[502,1700,1701,1704],{},[520,1702,1703],{},"父节点操作自己的子节点",[520,1705,1698],{},[502,1707,1708,1711],{},[520,1709,1710],{},"子节点要通知父节点",[520,1712,1713,1714,1716],{},"✅ 节点自己的 ",[45,1715,342],{}," 就够了，不用上总线",[14,1718,1719],{},[10,1720,1721,1722],{},"一句话：",[20,1723,1724],{},"EventBus 是给\"跨系统\"的事件用的，不是用来取代所有函数调用的。",[25,1726,1728],{"id":1727},"重构完对比一下","重构完，对比一下",[496,1730,1731,1743],{},[499,1732,1733],{},[502,1734,1735,1737,1740],{},[505,1736],{},[505,1738,1739],{},"重构前",[505,1741,1742],{},"重构后",[515,1744,1745,1758,1773],{},[502,1746,1747,1750,1753],{},[520,1748,1749],{},"死区要认识几个系统",[520,1751,1752],{},"2 个（音频 + 管理器）",[520,1754,1755],{},[20,1756,1757],{},"0 个",[502,1759,1760,1763,1768],{},[520,1761,1762],{},"加\"死亡震屏\"要改几个文件",[520,1764,1765,1766],{},"改 ",[45,1767,619],{},[520,1769,1770],{},[20,1771,1772],{},"不改，新模块自己去听",[502,1774,1775,1778,1781],{},[520,1776,1777],{},"死区的职责",[520,1779,1780],{},"检测碰撞 + 派活",[520,1782,1783],{},[20,1784,1785],{},"只检测碰撞",[10,1787,1788,1789,1792],{},"游戏跑起来的",[20,1790,1791],{},"表现完全一样"," — 但下次你想加东西的时候，会真切感受到差别。",[1794,1795],"hr",{},[10,1797,1798,1799,1802],{},"下一章我们做",[20,1800,1801],{},"存档系统","，让最高分在关掉游戏之后还能留住 💾",[1804,1805,1806],"style",{},"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":43,"searchDepth":62,"depth":62,"links":1808},[1809,1810,1811,1817,1823,1827,1829],{"id":27,"depth":62,"text":27},{"id":240,"depth":62,"text":241},{"id":290,"depth":62,"text":291,"children":1812},[1813,1814,1816],{"id":398,"depth":69,"text":399},{"id":428,"depth":69,"text":1815},"@warning_ignore(\"unused_signal\") 是干嘛的？",{"id":465,"depth":69,"text":466},{"id":612,"depth":62,"text":612,"children":1818},[1819,1821],{"id":615,"depth":69,"text":1820},"死区（killzone.gd）",{"id":721,"depth":69,"text":1822},"障碍物（pillar_pair.gd）",{"id":969,"depth":62,"text":969,"children":1824},[1825,1826],{"id":978,"depth":69,"text":979},{"id":1233,"depth":69,"text":1234},{"id":1546,"depth":62,"text":1828},"什么时候不该用 EventBus？",{"id":1727,"depth":62,"text":1728},null,"2026-09-03","XGGame-Bird Godot","md","XGGame-Bird","https:\u002F\u002Fgithub.com\u002FXXGGG\u002FXGGame-Bird",{},"\u002Fdevlog\u002Fxggame-bird\u002F10-event-bus",{"title":5,"description":1832},"devlog\u002Fxggame-bird\u002F10-event-bus","W3HMBLBxC2-jN7KJuIIR_FEm9LOKBAKl9g6Ff2CfXSU",[1842,2055,3075,3767,5271,6716,7989,8537,9359,10341,11743],{"id":1843,"title":1844,"body":1845,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":2050,"navigation":65,"path":2051,"seo":2052,"stem":2053,"toc":65,"__hash__":2054},"devlog\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started.md","安装 Godot 及项目设置",{"type":7,"value":1846,"toc":2041},[1847,1851,1861,1864,1871,1874,1880,1887,1893,1896,1902,1905,1908,1911,1917,1920,1926,1960,1965,1969,1975,1981,2022,2027,2036,2038],[25,1848,1850],{"id":1849},"下载与安装-godot","下载与安装 Godot",[14,1852,1853],{},[10,1854,1855,1856],{},"官网：",[32,1857,1858],{"href":1858,"rel":1859},"https:\u002F\u002Fgodotengine.org\u002F",[1860],"nofollow",[10,1862,1863],{},"下载后解压即可使用，无需安装。",[10,1865,1866],{},[1867,1868],"img",{"alt":1869,"src":1870},"01-getting-started-Godot","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-Godot.png",[25,1872,1873],{"id":1873},"新建项目",[10,1875,1876,1877,194],{},"打开 Godot，新建一个项目，取名 ",[45,1878,1879],{},"Bird",[10,1881,1882,1883,1886],{},"渲染器选择 ",[20,1884,1885],{},"【兼容】","— 这样后续可以导出为网页版在浏览器里玩。",[10,1888,1889],{},[1867,1890],{"alt":1891,"src":1892},"01-getting-started-Godot-Bird","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-Godot-Bird.png",[10,1894,1895],{},"创建完成后进入编辑器：",[10,1897,1898],{},[1867,1899],{"alt":1900,"src":1901},"01-getting-started-界面","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-%E7%95%8C%E9%9D%A2.png",[25,1903,1904],{"id":1904},"项目设置",[10,1906,1907],{},"因为这是一个手机竖屏游戏，所以需要先调整窗口尺寸。",[10,1909,1910],{},"路径：项目 → 项目设置 → 显示 → 窗口",[10,1912,1913],{},[1867,1914],{"alt":1915,"src":1916},"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",[396,1918,1919],{"id":1919},"窗口尺寸",[10,1921,1922],{},[1867,1923],{"alt":1924,"src":1925},"01-getting-started-项目设置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started-%E9%A1%B9%E7%9B%AE%E8%AE%BE%E7%BD%AE.png",[496,1927,1928,1938],{},[499,1929,1930],{},[502,1931,1932,1935],{},[505,1933,1934],{},"设置项",[505,1936,1937],{},"值",[515,1939,1940,1950],{},[502,1941,1942,1945],{},[520,1943,1944],{},"视口宽度 (Viewport Width)",[520,1946,1947],{},[45,1948,1949],{},"720",[502,1951,1952,1955],{},[520,1953,1954],{},"视口高度 (Viewport Height)",[520,1956,1957],{},[45,1958,1959],{},"1280",[14,1961,1962],{},[10,1963,1964],{},"视口是游戏的实际分辨率。",[396,1966,1968],{"id":1967},"拉伸适配-与-强制竖屏","拉伸适配 与 强制竖屏",[10,1970,1971],{},[1867,1972],{"alt":1973,"src":1974},"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,1976,1977,1980],{},[20,1978,1979],{},"「拉伸」"," 部分：",[496,1982,1983,1994],{},[499,1984,1985],{},[502,1986,1987,1989,1991],{},[505,1988,1934],{},[505,1990,1937],{},[505,1992,1993],{},"作用",[515,1995,1996,2009],{},[502,1997,1998,2001,2006],{},[520,1999,2000],{},"模式 (Mode)",[520,2002,2003],{},[45,2004,2005],{},"canvas_items",[520,2007,2008],{},"保持 2D 图形清晰",[502,2010,2011,2014,2019],{},[520,2012,2013],{},"比例 (Aspect)",[520,2015,2016],{},[45,2017,2018],{},"keep",[520,2020,2021],{},"保持宽高比不变形",[10,2023,2024,1980],{},[20,2025,2026],{},"「手持」",[199,2028,2029],{},[202,2030,2031,2032,2035],{},"方向 (Orientation)：选择 ",[45,2033,2034],{},"portrait","（竖屏）",[1794,2037],{},[10,2039,2040],{},"设置完成！接下来就可以开始创建「小鸟」了。",{"title":43,"searchDepth":62,"depth":62,"links":2042},[2043,2044,2045],{"id":1849,"depth":62,"text":1850},{"id":1873,"depth":62,"text":1873},{"id":1904,"depth":62,"text":1904,"children":2046},[2047,2048],{"id":1919,"depth":69,"text":1919},{"id":1967,"depth":69,"text":1968},"2026-05-12",{},"\u002Fdevlog\u002Fxggame-bird\u002F01-getting-started",{"title":1844,"description":1832},"devlog\u002Fxggame-bird\u002F01-getting-started","xfXIiVt-cjGYA7F0hi9cWLKLIpkdWG1VTKPqgfQQ7ts",{"id":2056,"title":2057,"body":2058,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":3070,"navigation":65,"path":3071,"seo":3072,"stem":3073,"toc":65,"__hash__":3074},"devlog\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird.md","创建主角：小鸟",{"type":7,"value":2059,"toc":3059},[2060,2064,2071,2074,2077,2099,2109,2115,2118,2127,2133,2136,2146,2152,2155,2161,2164,2169,2175,2181,2187,2417,2421,2434,2442,2491,2515,2521,2524,2551,2564,2709,2712,2752,2830,2840,2878,2906,2946,2977,2982,2994,3017,3020,3026,3033,3053,3056],[25,2061,2063],{"id":2062},"创建节点的方法","创建节点的方法：",[10,2065,2066,2067],{},"创建节点\u002F创建Node，点击加号「+」，然后输入需要的节点名称即可创建\n",[1867,2068],{"alt":2069,"src":2070},"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",[25,2072,2057],{"id":2073},"创建主角小鸟",[10,2075,2076],{},"我们需要用到 3 个节点来组成小鸟：",[199,2078,2079],{},[202,2080,2081,2084,2085],{},[45,2082,2083],{},"CharacterBody2D"," — 角色节点（控制移动）\n",[199,2086,2087,2093],{},[202,2088,2089,2092],{},[45,2090,2091],{},"Sprite2D"," — 图片节点（显示外观）",[202,2094,2095,2098],{},[45,2096,2097],{},"CollisionShape2D"," — 碰撞体节点（检测碰撞）",[10,2100,2101,2102,2104,2105,2108],{},"先用 Godot 自带的图标当小鸟，把它拖到 ",[45,2103,2091],{}," 的 ",[45,2106,2107],{},"Texture"," 上即可：",[10,2110,2111],{},[1867,2112],{"alt":2113,"src":2114},"02-create-bird-创建小鸟","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E5%88%9B%E5%BB%BA%E5%B0%8F%E9%B8%9F.png",[396,2116,2117],{"id":2117},"保存场景",[10,2119,2120,2123,2124],{},[45,2121,2122],{},"Ctrl + S"," 保存场景，建一个文件夹专门放场景文件：",[45,2125,2126],{},"scenes\u002Fbird.tscn",[10,2128,2129],{},[1867,2130],{"alt":2131,"src":2132},"02-create-bird-保存场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E4%BF%9D%E5%AD%98%E5%9C%BA%E6%99%AF.png",[396,2134,2135],{"id":2135},"设置碰撞体",[10,2137,2138,2139,2141,2142,2145],{},"选中 ",[45,2140,2097],{},"，在右侧面板选一个形状(矩形：",[45,2143,2144],{},"RectangleShape2D",")，缩放到和图片差不多大：",[10,2147,2148],{},[1867,2149],{"alt":2150,"src":2151},"02-create-bird-碰撞体1","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E7%A2%B0%E6%92%9E%E4%BD%931.png",[10,2153,2154],{},"这个蓝色矩形就是碰撞体，游戏里小鸟撞到东西靠的就是它：",[10,2156,2157],{},[1867,2158],{"alt":2159,"src":2160},"02-create-bird-碰撞体2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E7%A2%B0%E6%92%9E%E4%BD%932.png",[25,2162,2163],{"id":2163},"编写脚本",[10,2165,2138,2166,2168],{},[45,2167,2083],{}," 节点，点击右上角的 📜 图标创建脚本：",[10,2170,2171],{},[1867,2172],{"alt":2173,"src":2174},"02-create-bird-创建脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E5%88%9B%E5%BB%BA%E8%84%9A%E6%9C%AC.png",[10,2176,2177,2178],{},"同样建一个文件夹专门放脚本：",[45,2179,2180],{},"scripts\u002Fbird.gd",[10,2182,2183],{},[1867,2184],{"alt":2185,"src":2186},"02-create-bird-脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E8%84%9A%E6%9C%AC.png",[38,2188,2190],{"className":40,"code":2189,"language":42,"meta":43,"style":43},"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",[45,2191,2192,2202,2206,2223,2242,2259,2263,2285,2290,2304,2327,2331,2336,2354,2367,2371,2376,2394,2406,2410],{"__ignoreMap":43},[48,2193,2194,2196,2199],{"class":50,"line":51},[48,2195,55],{"class":54},[48,2197,2198],{"class":58}," CharacterBody2D",[48,2200,2201],{"class":170}," # 创建时自带的\n",[48,2203,2204],{"class":50,"line":62},[48,2205,66],{"emptyLinePlaceholder":65},[48,2207,2208,2210,2212,2215,2217,2220],{"class":50,"line":69},[48,2209,746],{"class":76},[48,2211,749],{"class":72},[48,2213,2214],{"class":112}," gravity_scale",[48,2216,755],{"class":80},[48,2218,2219],{"class":758}," 2.0",[48,2221,2222],{"class":170},"       # 重力倍率（控制掉落快慢）\n",[48,2224,2225,2227,2229,2232,2234,2236,2239],{"class":50,"line":106},[48,2226,746],{"class":76},[48,2228,749],{"class":72},[48,2230,2231],{"class":112}," jump_force",[48,2233,755],{"class":80},[48,2235,876],{"class":72},[48,2237,2238],{"class":758},"500.0",[48,2240,2241],{"class":170},"       # 跳跃力量（负值 = 向上）\n",[48,2243,2244,2246,2248,2251,2253,2256],{"class":50,"line":156},[48,2245,746],{"class":76},[48,2247,749],{"class":72},[48,2249,2250],{"class":112}," max_fall_speed",[48,2252,755],{"class":80},[48,2254,2255],{"class":758}," 1000.0",[48,2257,2258],{"class":170},"   # 最大掉落速度\n",[48,2260,2261],{"class":50,"line":174},[48,2262,66],{"emptyLinePlaceholder":65},[48,2264,2265,2267,2269,2271,2273,2275,2277,2279,2281,2283],{"class":50,"line":351},[48,2266,73],{"class":72},[48,2268,795],{"class":76},[48,2270,81],{"class":80},[48,2272,800],{"class":84},[48,2274,88],{"class":80},[48,2276,805],{"class":58},[48,2278,94],{"class":80},[48,2280,97],{"class":72},[48,2282,100],{"class":58},[48,2284,103],{"class":80},[48,2286,2287],{"class":50,"line":356},[48,2288,2289],{"class":170},"    # 1. 施加重力：每帧让小鸟往下掉\n",[48,2291,2292,2294,2297,2300,2302],{"class":50,"line":367},[48,2293,109],{"class":54},[48,2295,2296],{"class":72}," not",[48,2298,2299],{"class":76}," is_on_floor",[48,2301,167],{"class":80},[48,2303,103],{"class":84},[48,2305,2306,2309,2312,2315,2317,2319,2322,2324],{"class":50,"line":884},[48,2307,2308],{"class":112},"        velocity",[48,2310,2311],{"class":72}," +=",[48,2313,2314],{"class":76}," get_gravity",[48,2316,167],{"class":80},[48,2318,855],{"class":72},[48,2320,2321],{"class":112}," delta",[48,2323,855],{"class":72},[48,2325,2326],{"class":112}," gravity_scale\n",[48,2328,2329],{"class":50,"line":892},[48,2330,66],{"emptyLinePlaceholder":65},[48,2332,2333],{"class":50,"line":897},[48,2334,2335],{"class":170},"    # 2. 限速：防止掉太快\n",[48,2337,2338,2340,2343,2345,2347,2350,2352],{"class":50,"line":903},[48,2339,109],{"class":54},[48,2341,2342],{"class":112}," velocity",[48,2344,116],{"class":80},[48,2346,1621],{"class":112},[48,2348,2349],{"class":72}," >",[48,2351,2250],{"class":112},[48,2353,103],{"class":84},[48,2355,2356,2358,2360,2362,2364],{"class":50,"line":927},[48,2357,2308],{"class":112},[48,2359,116],{"class":80},[48,2361,1621],{"class":112},[48,2363,777],{"class":80},[48,2365,2366],{"class":112}," max_fall_speed\n",[48,2368,2369],{"class":50,"line":944},[48,2370,66],{"emptyLinePlaceholder":65},[48,2372,2373],{"class":50,"line":1421},[48,2374,2375],{"class":170},"    # 3. 按键跳跃：按下就给一个向上的速度\n",[48,2377,2378,2380,2382,2384,2386,2388,2390,2392],{"class":50,"line":1431},[48,2379,109],{"class":54},[48,2381,1577],{"class":58},[48,2383,116],{"class":84},[48,2385,1582],{"class":76},[48,2387,81],{"class":80},[48,2389,1587],{"class":125},[48,2391,94],{"class":80},[48,2393,103],{"class":84},[48,2395,2396,2398,2400,2402,2404],{"class":50,"line":1443},[48,2397,2308],{"class":112},[48,2399,116],{"class":80},[48,2401,1621],{"class":112},[48,2403,777],{"class":80},[48,2405,1626],{"class":112},[48,2407,2408],{"class":50,"line":1448},[48,2409,66],{"emptyLinePlaceholder":65},[48,2411,2412,2415],{"class":50,"line":1458},[48,2413,2414],{"class":76},"    move_and_slide",[48,2416,711],{"class":80},[396,2418,2420],{"id":2419},"这段代码在干嘛","这段代码在干嘛？",[38,2422,2424],{"className":40,"code":2423,"language":42,"meta":43,"style":43},"extends CharacterBody2D # 创建时自带的\n",[45,2425,2426],{"__ignoreMap":43},[48,2427,2428,2430,2432],{"class":50,"line":51},[48,2429,55],{"class":54},[48,2431,2198],{"class":58},[48,2433,2201],{"class":170},[199,2435,2436],{},[202,2437,2438,2439,2441],{},"为 ",[45,2440,2083],{}," 节点创建脚本时会自带的代码。告诉 Godot：这个脚本是给「角色」用的（一般不用理会）。",[38,2443,2445],{"className":40,"code":2444,"language":42,"meta":43,"style":43},"@export var gravity_scale := 2.0       # 重力倍率（控制掉落快慢）\n@export var jump_force := -500.0       # 跳跃力量（负值 = 向上）\n@export var max_fall_speed := 1000.0   # 最大掉落速度\n",[45,2446,2447,2461,2477],{"__ignoreMap":43},[48,2448,2449,2451,2453,2455,2457,2459],{"class":50,"line":51},[48,2450,746],{"class":76},[48,2452,749],{"class":72},[48,2454,2214],{"class":112},[48,2456,755],{"class":80},[48,2458,2219],{"class":758},[48,2460,2222],{"class":170},[48,2462,2463,2465,2467,2469,2471,2473,2475],{"class":50,"line":62},[48,2464,746],{"class":76},[48,2466,749],{"class":72},[48,2468,2231],{"class":112},[48,2470,755],{"class":80},[48,2472,876],{"class":72},[48,2474,2238],{"class":758},[48,2476,2241],{"class":170},[48,2478,2479,2481,2483,2485,2487,2489],{"class":50,"line":69},[48,2480,746],{"class":76},[48,2482,749],{"class":72},[48,2484,2250],{"class":112},[48,2486,755],{"class":80},[48,2488,2255],{"class":758},[48,2490,2258],{"class":170},[199,2492,2493,2498,2504,2510],{},[202,2494,2495,2497],{},[45,2496,1026],{}," 定义变量",[202,2499,2500,2503],{},[45,2501,2502],{},"jump_force"," 等是自定义的变量名，",[202,2505,2506,2509],{},[45,2507,2508],{},"-500.0"," 是我们定义的数值！",[202,2511,2512,2514],{},[45,2513,746],{}," 加在前面是为了让 Godot 把变量放到面板上，方便我们后续调整。👇",[10,2516,2517],{},[1867,2518],{"alt":2519,"src":2520},"02-create-bird-右侧面板","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird-%E5%8F%B3%E4%BE%A7%E9%9D%A2%E6%9D%BF.png",[10,2522,2523],{},"它就是我们说的「数值」，例如一般游戏中用来定义角色的「攻击力」「血量」......",[38,2525,2527],{"className":40,"code":2526,"language":42,"meta":43,"style":43},"func _physics_process(delta: float) -> void:\n",[45,2528,2529],{"__ignoreMap":43},[48,2530,2531,2533,2535,2537,2539,2541,2543,2545,2547,2549],{"class":50,"line":51},[48,2532,73],{"class":72},[48,2534,795],{"class":76},[48,2536,81],{"class":80},[48,2538,800],{"class":84},[48,2540,88],{"class":80},[48,2542,805],{"class":58},[48,2544,94],{"class":80},[48,2546,97],{"class":72},[48,2548,100],{"class":58},[48,2550,103],{"class":80},[199,2552,2553,2558],{},[202,2554,2555,2557],{},[45,2556,73],{},"： 用来定义方法",[202,2559,2560,2563],{},[45,2561,2562],{},"_physics_process(delta)","：游戏每一帧都会自动执行这个函数（大约每秒 60 次）",[38,2565,2567],{"className":40,"code":2566,"language":42,"meta":43,"style":43},"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",[45,2568,2569,2591,2595,2607,2625,2629,2633,2649,2661,2665,2669,2687,2699,2703],{"__ignoreMap":43},[48,2570,2571,2573,2575,2577,2579,2581,2583,2585,2587,2589],{"class":50,"line":51},[48,2572,73],{"class":72},[48,2574,795],{"class":76},[48,2576,81],{"class":80},[48,2578,800],{"class":84},[48,2580,88],{"class":80},[48,2582,805],{"class":58},[48,2584,94],{"class":80},[48,2586,97],{"class":72},[48,2588,100],{"class":58},[48,2590,103],{"class":80},[48,2592,2593],{"class":50,"line":62},[48,2594,2289],{"class":170},[48,2596,2597,2599,2601,2603,2605],{"class":50,"line":69},[48,2598,109],{"class":54},[48,2600,2296],{"class":72},[48,2602,2299],{"class":76},[48,2604,167],{"class":80},[48,2606,103],{"class":84},[48,2608,2609,2611,2613,2615,2617,2619,2621,2623],{"class":50,"line":106},[48,2610,2308],{"class":112},[48,2612,2311],{"class":72},[48,2614,2314],{"class":76},[48,2616,167],{"class":80},[48,2618,855],{"class":72},[48,2620,2321],{"class":112},[48,2622,855],{"class":72},[48,2624,2326],{"class":112},[48,2626,2627],{"class":50,"line":156},[48,2628,66],{"emptyLinePlaceholder":65},[48,2630,2631],{"class":50,"line":174},[48,2632,2335],{"class":170},[48,2634,2635,2637,2639,2641,2643,2645,2647],{"class":50,"line":351},[48,2636,109],{"class":54},[48,2638,2342],{"class":112},[48,2640,116],{"class":80},[48,2642,1621],{"class":112},[48,2644,2349],{"class":72},[48,2646,2250],{"class":112},[48,2648,103],{"class":84},[48,2650,2651,2653,2655,2657,2659],{"class":50,"line":356},[48,2652,2308],{"class":112},[48,2654,116],{"class":80},[48,2656,1621],{"class":112},[48,2658,777],{"class":80},[48,2660,2366],{"class":112},[48,2662,2663],{"class":50,"line":367},[48,2664,66],{"emptyLinePlaceholder":65},[48,2666,2667],{"class":50,"line":884},[48,2668,2375],{"class":170},[48,2670,2671,2673,2675,2677,2679,2681,2683,2685],{"class":50,"line":892},[48,2672,109],{"class":54},[48,2674,1577],{"class":58},[48,2676,116],{"class":84},[48,2678,1582],{"class":76},[48,2680,81],{"class":80},[48,2682,1587],{"class":125},[48,2684,94],{"class":80},[48,2686,103],{"class":84},[48,2688,2689,2691,2693,2695,2697],{"class":50,"line":897},[48,2690,2308],{"class":112},[48,2692,116],{"class":80},[48,2694,1621],{"class":112},[48,2696,777],{"class":80},[48,2698,1626],{"class":112},[48,2700,2701],{"class":50,"line":903},[48,2702,66],{"emptyLinePlaceholder":65},[48,2704,2705,2707],{"class":50,"line":927},[48,2706,2414],{"class":76},[48,2708,711],{"class":80},[10,2710,2711],{},"其中：",[38,2713,2715],{"className":40,"code":2714,"language":42,"meta":43,"style":43},"# 1. 施加重力：每帧让小鸟往下掉\nif not is_on_floor():\n    velocity += get_gravity() * delta * gravity_scale\n",[45,2716,2717,2722,2734],{"__ignoreMap":43},[48,2718,2719],{"class":50,"line":51},[48,2720,2721],{"class":170},"# 1. 施加重力：每帧让小鸟往下掉\n",[48,2723,2724,2726,2728,2730,2732],{"class":50,"line":62},[48,2725,1574],{"class":54},[48,2727,2296],{"class":72},[48,2729,2299],{"class":76},[48,2731,167],{"class":80},[48,2733,103],{"class":84},[48,2735,2736,2738,2740,2742,2744,2746,2748,2750],{"class":50,"line":69},[48,2737,1616],{"class":112},[48,2739,2311],{"class":72},[48,2741,2314],{"class":76},[48,2743,167],{"class":80},[48,2745,855],{"class":72},[48,2747,2321],{"class":112},[48,2749,855],{"class":72},[48,2751,2326],{"class":112},[199,2753,2754,2782],{},[202,2755,2756,2758,2759],{},[45,2757,1574],{}," 是用来【判断】的\n",[199,2760,2761,2767,2776],{},[202,2762,2763,2766],{},[45,2764,2765],{},"not"," 是否定句",[202,2768,2769,2772,2773,2775],{},[45,2770,2771],{},"is_on_floor()"," 是 Godot引擎 为 ",[45,2774,2083],{},"节点赋予的属性",[202,2777,2778,2781],{},[45,2779,2780],{},"if not is_on_floor():"," 的意思是“如果 ‘角色’ 没有 站在地面时”\n是的，如果角色没有在地面，那就让角色往下掉！但是要以什么速度往下掉呢？",[202,2783,2784,2787],{},[45,2785,2786],{},"velocity += get_gravity() * delta * gravity_scale",[199,2788,2789,2804,2810,2815,2821],{},[202,2790,2791,2794,2795,2797,2798,2800,2801,2803],{},[45,2792,2793],{},"velocity"," 是 Godot 给 ",[45,2796,2083],{}," 内置的「速度」属性。它是一个二维数值，有 ",[45,2799,847],{},"（左右速度）和 ",[45,2802,1621],{},"（上下速度）两个分量。初始值是 0（小鸟不动）",[202,2805,2806,2809],{},[45,2807,2808],{},"get_gravity()"," 是 Godot 内置方法，返回项目设置里的「重力」值（默认向下 980）",[202,2811,2812,2814],{},[45,2813,800],{}," 是「上一帧到这一帧的时间间隔」（秒）。乘上它能保证不同帧率（60fps \u002F 120fps）下游戏速度一致",[202,2816,2817,2820],{},[45,2818,2819],{},"gravity_scale"," 是我们自己定义的「重力倍率」，越大掉得越快",[202,2822,2823,2826,2827],{},[45,2824,2825],{},"+="," 是「累加」的意思，等价于 ",[45,2828,2829],{},"velocity = velocity + ...",[10,2831,2832,2833,2839],{},"所以这一行的意思是：",[20,2834,2835,2836,2838],{},"每一帧给 ",[45,2837,2793],{}," 累加一点点重力","。速度越累越大 → 小鸟越掉越快，这就是物理课上学过的「自由落体」。",[38,2841,2843],{"className":40,"code":2842,"language":42,"meta":43,"style":43},"# 2. 限速：防止掉太快\nif velocity.y > max_fall_speed:\n    velocity.y = max_fall_speed\n",[45,2844,2845,2850,2866],{"__ignoreMap":43},[48,2846,2847],{"class":50,"line":51},[48,2848,2849],{"class":170},"# 2. 限速：防止掉太快\n",[48,2851,2852,2854,2856,2858,2860,2862,2864],{"class":50,"line":62},[48,2853,1574],{"class":54},[48,2855,2342],{"class":112},[48,2857,116],{"class":80},[48,2859,1621],{"class":112},[48,2861,2349],{"class":72},[48,2863,2250],{"class":112},[48,2865,103],{"class":84},[48,2867,2868,2870,2872,2874,2876],{"class":50,"line":69},[48,2869,1616],{"class":112},[48,2871,116],{"class":80},[48,2873,1621],{"class":112},[48,2875,777],{"class":80},[48,2877,2366],{"class":112},[199,2879,2880,2890,2900],{},[202,2881,2882,2885,2886,2889],{},[45,2883,2884],{},"velocity.y"," 就是上下方向的速度（",[20,2887,2888],{},"往下为正、往上为负","，这是 Godot 的坐标系约定）",[202,2891,2892,2893,2895,2896,2899],{},"如果当前下落速度 ",[45,2894,2884],{}," 超过了我们设的最大值 ",[45,2897,2898],{},"max_fall_speed","，就把它「卡」在最大值上",[202,2901,2902,2905],{},[20,2903,2904],{},"为什么需要？"," 因为重力会一直累加，没有这个限制小鸟会越掉越快，最后快得看不见",[38,2907,2909],{"className":40,"code":2908,"language":42,"meta":43,"style":43},"# 3. 按键跳跃：按下就给一个向上的速度\nif Input.is_action_just_pressed(\"fly\"):\n    velocity.y = jump_force\n",[45,2910,2911,2916,2934],{"__ignoreMap":43},[48,2912,2913],{"class":50,"line":51},[48,2914,2915],{"class":170},"# 3. 按键跳跃：按下就给一个向上的速度\n",[48,2917,2918,2920,2922,2924,2926,2928,2930,2932],{"class":50,"line":62},[48,2919,1574],{"class":54},[48,2921,1577],{"class":58},[48,2923,116],{"class":84},[48,2925,1582],{"class":76},[48,2927,81],{"class":80},[48,2929,1587],{"class":125},[48,2931,94],{"class":80},[48,2933,103],{"class":84},[48,2935,2936,2938,2940,2942,2944],{"class":50,"line":69},[48,2937,1616],{"class":112},[48,2939,116],{"class":80},[48,2941,1621],{"class":112},[48,2943,777],{"class":80},[48,2945,1626],{"class":112},[199,2947,2948,2958,2970],{},[202,2949,2950,2953,2954,2957],{},[45,2951,2952],{},"Input.is_action_just_pressed(\"fly\")"," 检测「fly」这个按键",[20,2955,2956],{},"刚刚被按下","（一次按下只触发一次，长按不会连续触发）",[202,2959,2960,2961,2963,2964,2966,2967,2969],{},"一旦按下，直接把 ",[45,2962,2884],{}," 设为 ",[45,2965,2502],{},"（前面定义的 ",[45,2968,2508],{},"，负数 = 向上）",[202,2971,2972,2973,2976],{},"这就是「跳跃」：瞬间把速度变成向上，",[20,2974,2975],{},"覆盖掉","之前累加的下落速度",[14,2978,2979],{},[10,2980,2981],{},"「fly」是什么按键？我们还没告诉 Godot，下面就来配置快捷键。",[38,2983,2985],{"className":40,"code":2984,"language":42,"meta":43,"style":43},"move_and_slide()\n",[45,2986,2987],{"__ignoreMap":43},[48,2988,2989,2992],{"class":50,"line":51},[48,2990,2991],{"class":76},"move_and_slide",[48,2993,711],{"class":80},[199,2995,2996,3002,3011],{},[202,2997,2998,2999,3001],{},"Godot 给 ",[45,3000,2083],{}," 内置的方法",[202,3003,3004,3010],{},[20,3005,3006,3007,3009],{},"根据 ",[45,3008,2793],{}," 真正移动小鸟","，并自动处理碰撞（撞到东西就停下来）",[202,3012,3013,3014,3016],{},"没有这一行，前面算好的 ",[45,3015,2793],{}," 都只是「空想」，小鸟不会真的动起来",[396,3018,3019],{"id":3019},"配置跳跃按键",[10,3021,3022,3023,3025],{},"代码里用了 ",[45,3024,2952],{},"，所以我们需要在项目设置里添加这个按键映射：",[10,3027,3028,3029],{},"路径：项目 -> 项目设置 -> 输入映射 (Input Map)\n",[1867,3030],{"alt":3031,"src":3032},"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",[3034,3035,3036,3043],"ol",{},[202,3037,3038,3039,3042],{},"在上方输入 ",[45,3040,3041],{},"fly","，点击「添加」",[202,3044,3045,3046,3048,3049,3052],{},"点击 ",[45,3047,3041],{}," 右边的 ",[45,3050,3051],{},"+","，按下你想要的按键（比如空格键）",[10,3054,3055],{},"这样按空格键小鸟就会往上跳了！不过目前这只小鸟还没有舞台，下一章，我们将为它搭建一个可以飞的场景。",[1804,3057,3058],{},"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":43,"searchDepth":62,"depth":62,"links":3060},[3061,3062,3066],{"id":2062,"depth":62,"text":2063},{"id":2073,"depth":62,"text":2057,"children":3063},[3064,3065],{"id":2117,"depth":69,"text":2117},{"id":2135,"depth":69,"text":2135},{"id":2163,"depth":62,"text":2163,"children":3067},[3068,3069],{"id":2419,"depth":69,"text":2420},{"id":3019,"depth":69,"text":3019},{},"\u002Fdevlog\u002Fxggame-bird\u002F02-create-bird",{"title":2057,"description":1832},"devlog\u002Fxggame-bird\u002F02-create-bird","4ulFauc0NCvEFhdOh3vFco0Uc_Za6ZBezoBXrpjuq6s",{"id":3076,"title":3077,"body":3078,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":3762,"navigation":65,"path":3763,"seo":3764,"stem":3765,"toc":65,"__hash__":3766},"devlog\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene.md","游戏场景",{"type":7,"value":3079,"toc":3752},[3080,3083,3086,3089,3104,3107,3122,3125,3132,3136,3147,3151,3162,3165,3170,3176,3179,3182,3189,3191,3214,3220,3230,3236,3257,3260,3263,3274,3277,3280,3283,3286,3306,3312,3730,3742,3746,3749],[10,3081,3082],{},"这一章我们来完善游戏世界：搭建舞台、调整摄像机、加边界。",[25,3084,3085],{"id":3085},"创建游戏场景",[10,3087,3088],{},"小鸟做好了，但现在还没有一个「舞台」让它表演。我们需要创建一个游戏主场景，把小鸟放进去，再加一个摄像机来\"看\"它。",[10,3090,3091,3092,3095,3096,3099,3100],{},"新建场景，根节点选 ",[45,3093,3094],{},"Node2D","，保存为 ",[45,3097,3098],{},"scenes\u002Fgame.tscn","：\n",[1867,3101],{"alt":3102,"src":3103},"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,3105,3106],{},"使用到的节点：",[199,3108,3109],{},[202,3110,3111,3113,3114],{},[45,3112,3094],{}," — 游戏主场景\n",[199,3115,3116],{},[202,3117,3118,3121],{},[45,3119,3120],{},"Camera2D"," — 摄像机（决定屏幕看到哪里）",[25,3123,3124],{"id":3124},"调整摄像机位置",[10,3126,3127,3128],{},"调整摄像机（固定）位置：\n",[1867,3129],{"alt":3130,"src":3131},"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",[25,3133,3135],{"id":3134},"导入小鸟角色","导入“小鸟”角色",[10,3137,3138,3139,3142,3143],{},"把之前做好的 ",[45,3140,3141],{},"bird.tscn"," 拖进场景中，小鸟就出现了：\n",[1867,3144],{"alt":3145,"src":3146},"03-game-scene-导入bird","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E5%AF%BC%E5%85%A5bird.png",[25,3148,3150],{"id":3149},"设置主场景然后测试","设置【主场景】然后测试",[10,3152,3153,3154,3157,3158],{},"然后把 ",[45,3155,3156],{},"game.tscn"," 设置为【主场景】，点击运行游戏：\n",[1867,3159],{"alt":3160,"src":3161},"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,3163,3164],{},"这时候小鸟就在游戏场景里了，点击【鼠标左键】或【空格键】小鸟就会起飞！",[14,3166,3167],{},[10,3168,3169],{},"确保你已经按上一章配置了【fly】按键映射！",[10,3171,3172],{},[1867,3173],{"alt":3174,"src":3175},"03-game-scene-运行游戏","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%BF%90%E8%A1%8C%E6%B8%B8%E6%88%8F.png",[10,3177,3178],{},"不过现在不按按键的话，小鸟就会一直往下掉，掉到屏幕外面去。别急，接下来我们会给游戏场景加上边界。",[25,3180,3181],{"id":3181},"添加边界",[10,3183,3184,3185,3188],{},"为了让小鸟不会掉出屏幕，我们给游戏世界加上",[20,3186,3187],{},"底部","边界。",[10,3190,3106],{},[199,3192,3193],{},[202,3194,3195,3198,3199],{},[45,3196,3197],{},"StaticBody2D"," — 静态物理体（不动，但能碰撞）\n",[199,3200,3201],{},[202,3202,3203,3205,3206],{},[45,3204,2097],{}," — 碰撞形状\n",[199,3207,3208],{},[202,3209,3210,3213],{},[45,3211,3212],{},"WorldBoundaryShape2D"," — 选择这个形状（世界边界）",[10,3215,3216],{},[1867,3217],{"alt":3218,"src":3219},"03-game-scene-边界","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%BE%B9%E7%95%8C.png",[10,3221,3222,3223,3225,3226,3229],{},"然后选择 ",[45,3224,3197],{}," 节点往下拖到边缘底部，因为后续要在该节点里",[20,3227,3228],{},"加入","【死亡区域（Killzone）】，这样才能触发游戏结束的代码。",[10,3231,3232],{},[1867,3233],{"alt":3234,"src":3235},"03-game-scene-边界2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene-%E8%BE%B9%E7%95%8C2.png",[3034,3237,3238,3244,3254],{},[202,3239,3240,3241,3243],{},"先选择 ",[45,3242,3197],{}," 节点",[202,3245,3246,3247],{},"然后点击上方的【编辑所选节点】\n",[3034,3248,3249],{},[202,3250,3251,3252,3243],{},"避免拖拽时不小心选中 ",[45,3253,2097],{},[202,3255,3256],{},"然后往下拖拽到摄像机和画面边缘。",[25,3258,3259],{"id":3259},"测试运行",[10,3261,3262],{},"会发现几个问题，不过都不是问题！",[3034,3264,3265,3268,3271],{},[202,3266,3267],{},"小鸟在原地高低移动，没有往前飞！但是这是正常的，就是要小鸟在原地高低移动。后续我们只要设置背景和障碍物往左移动就会让玩家以为【小鸟】在往右👉飞。",[202,3269,3270],{},"小鸟没有点击【fly】就会倒地，但是游戏没有结束，别着急，后面就会完善代码！",[202,3272,3273],{},"与原版的小鸟有所不同，原本的《Flappy Bird》小鸟在飞的时候会有种“头重脚轻”的感觉：就是在飞的时候仰头，不飞的时候头垂下。在玩家连点的时候有种“扑腾”的感觉。",[10,3275,3276],{},"所以接下来我们就先来优化小鸟的代码脚本，让它可以“扑腾”",[25,3278,3279],{"id":3279},"优化小鸟的旋转",[10,3281,3282],{},"在原版 Flappy Bird 里，小鸟不是一直直挺挺的 — 往上飞的时候头会抬起来，往下掉的时候头会低下去，看起来更生动。",[10,3284,3285],{},"我们来实现这个效果，思路很简单：",[199,3287,3288,3294,3300],{},[202,3289,3290,3293],{},[20,3291,3292],{},"往上飞"," → 头朝上（抬头 -30°）",[202,3295,3296,3299],{},[20,3297,3298],{},"往下掉"," → 头朝下（低头 90°）",[202,3301,3302,3305],{},[20,3303,3304],{},"旋转要平滑过渡","，不能一下子就转过去（那样看起来很僵硬）",[10,3307,3308,3309,3311],{},"在 ",[45,3310,1563],{}," 里加一个旋转函数：",[38,3313,3315],{"className":40,"code":3314,"language":42,"meta":43,"style":43},"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",[45,3316,3317,3324,3328,3341,3356,3369,3386,3390,3412,3417,3429,3447,3451,3456,3472,3484,3488,3493,3511,3523,3535,3540,3546,3558,3565,3570,3594,3608,3613,3631,3637,3658,3666,3672,3688,3693,3699],{"__ignoreMap":43},[48,3318,3319,3321],{"class":50,"line":51},[48,3320,55],{"class":54},[48,3322,3323],{"class":58}," CharacterBody2D\n",[48,3325,3326],{"class":50,"line":62},[48,3327,66],{"emptyLinePlaceholder":65},[48,3329,3330,3332,3334,3336,3338],{"class":50,"line":69},[48,3331,746],{"class":76},[48,3333,749],{"class":72},[48,3335,2214],{"class":112},[48,3337,755],{"class":80},[48,3339,3340],{"class":758}," 2.0\n",[48,3342,3343,3345,3347,3349,3351,3353],{"class":50,"line":106},[48,3344,746],{"class":76},[48,3346,749],{"class":72},[48,3348,2231],{"class":112},[48,3350,755],{"class":80},[48,3352,876],{"class":72},[48,3354,3355],{"class":758},"400.0\n",[48,3357,3358,3360,3362,3364,3366],{"class":50,"line":156},[48,3359,746],{"class":76},[48,3361,749],{"class":72},[48,3363,2250],{"class":112},[48,3365,755],{"class":80},[48,3367,3368],{"class":758}," 1000.0\n",[48,3370,3371,3373,3375,3378,3380,3383],{"class":50,"line":174},[48,3372,746],{"class":76},[48,3374,749],{"class":72},[48,3376,3377],{"class":112}," rotate_speed",[48,3379,755],{"class":80},[48,3381,3382],{"class":758}," 4.0",[48,3384,3385],{"class":170},"   # 旋转过渡速度（越大越快）\n",[48,3387,3388],{"class":50,"line":351},[48,3389,66],{"emptyLinePlaceholder":65},[48,3391,3392,3394,3396,3398,3400,3402,3404,3406,3408,3410],{"class":50,"line":356},[48,3393,73],{"class":72},[48,3395,795],{"class":76},[48,3397,81],{"class":80},[48,3399,800],{"class":84},[48,3401,88],{"class":80},[48,3403,805],{"class":58},[48,3405,94],{"class":80},[48,3407,97],{"class":72},[48,3409,100],{"class":58},[48,3411,103],{"class":80},[48,3413,3414],{"class":50,"line":367},[48,3415,3416],{"class":170},"    # 重力\n",[48,3418,3419,3421,3423,3425,3427],{"class":50,"line":884},[48,3420,109],{"class":54},[48,3422,2296],{"class":72},[48,3424,2299],{"class":76},[48,3426,167],{"class":80},[48,3428,103],{"class":84},[48,3430,3431,3433,3435,3437,3439,3441,3443,3445],{"class":50,"line":892},[48,3432,2308],{"class":112},[48,3434,2311],{"class":72},[48,3436,2314],{"class":76},[48,3438,167],{"class":80},[48,3440,855],{"class":72},[48,3442,2321],{"class":112},[48,3444,855],{"class":72},[48,3446,2326],{"class":112},[48,3448,3449],{"class":50,"line":897},[48,3450,66],{"emptyLinePlaceholder":65},[48,3452,3453],{"class":50,"line":903},[48,3454,3455],{"class":170},"    # 限速\n",[48,3457,3458,3460,3462,3464,3466,3468,3470],{"class":50,"line":927},[48,3459,109],{"class":54},[48,3461,2342],{"class":112},[48,3463,116],{"class":80},[48,3465,1621],{"class":112},[48,3467,2349],{"class":72},[48,3469,2250],{"class":112},[48,3471,103],{"class":84},[48,3473,3474,3476,3478,3480,3482],{"class":50,"line":944},[48,3475,2308],{"class":112},[48,3477,116],{"class":80},[48,3479,1621],{"class":112},[48,3481,777],{"class":80},[48,3483,2366],{"class":112},[48,3485,3486],{"class":50,"line":1421},[48,3487,66],{"emptyLinePlaceholder":65},[48,3489,3490],{"class":50,"line":1431},[48,3491,3492],{"class":170},"    # 跳跃\n",[48,3494,3495,3497,3499,3501,3503,3505,3507,3509],{"class":50,"line":1443},[48,3496,109],{"class":54},[48,3498,1577],{"class":58},[48,3500,116],{"class":84},[48,3502,1582],{"class":76},[48,3504,81],{"class":80},[48,3506,1587],{"class":125},[48,3508,94],{"class":80},[48,3510,103],{"class":84},[48,3512,3513,3515,3517,3519,3521],{"class":50,"line":1448},[48,3514,2308],{"class":112},[48,3516,116],{"class":80},[48,3518,1621],{"class":112},[48,3520,777],{"class":80},[48,3522,1058],{"class":758},[48,3524,3525,3527,3529,3531,3533],{"class":50,"line":1458},[48,3526,2308],{"class":112},[48,3528,116],{"class":80},[48,3530,1621],{"class":112},[48,3532,777],{"class":80},[48,3534,1626],{"class":112},[48,3536,3538],{"class":50,"line":3537},21,[48,3539,66],{"emptyLinePlaceholder":65},[48,3541,3543],{"class":50,"line":3542},22,[48,3544,3545],{"class":170},"    # 旋转\n",[48,3547,3549,3552,3554,3556],{"class":50,"line":3548},23,[48,3550,3551],{"class":76},"    handle_rotation",[48,3553,81],{"class":80},[48,3555,800],{"class":112},[48,3557,337],{"class":80},[48,3559,3561,3563],{"class":50,"line":3560},24,[48,3562,2414],{"class":76},[48,3564,711],{"class":80},[48,3566,3568],{"class":50,"line":3567},25,[48,3569,66],{"emptyLinePlaceholder":65},[48,3571,3573,3575,3578,3580,3582,3584,3586,3588,3590,3592],{"class":50,"line":3572},26,[48,3574,73],{"class":72},[48,3576,3577],{"class":76}," handle_rotation",[48,3579,81],{"class":80},[48,3581,800],{"class":84},[48,3583,88],{"class":80},[48,3585,805],{"class":58},[48,3587,94],{"class":80},[48,3589,97],{"class":72},[48,3591,100],{"class":58},[48,3593,103],{"class":80},[48,3595,3597,3600,3603,3605],{"class":50,"line":3596},27,[48,3598,3599],{"class":72},"    var",[48,3601,3602],{"class":112}," target_rotation",[48,3604,777],{"class":80},[48,3606,3607],{"class":758}," 0.0\n",[48,3609,3611],{"class":50,"line":3610},28,[48,3612,66],{"emptyLinePlaceholder":65},[48,3614,3616,3618,3620,3622,3624,3626,3629],{"class":50,"line":3615},29,[48,3617,109],{"class":54},[48,3619,2342],{"class":112},[48,3621,116],{"class":80},[48,3623,1621],{"class":112},[48,3625,873],{"class":72},[48,3627,3628],{"class":758}," 0",[48,3630,103],{"class":84},[48,3632,3634],{"class":50,"line":3633},30,[48,3635,3636],{"class":170},"        # 往上飞 → 抬头\n",[48,3638,3640,3643,3645,3648,3650,3653,3656],{"class":50,"line":3639},31,[48,3641,3642],{"class":112},"        target_rotation",[48,3644,777],{"class":80},[48,3646,3647],{"class":76}," deg_to_rad",[48,3649,81],{"class":80},[48,3651,3652],{"class":72},"-",[48,3654,3655],{"class":758},"30",[48,3657,337],{"class":80},[48,3659,3661,3664],{"class":50,"line":3660},32,[48,3662,3663],{"class":54},"    else",[48,3665,103],{"class":84},[48,3667,3669],{"class":50,"line":3668},33,[48,3670,3671],{"class":170},"        # 往下掉 → 低头\n",[48,3673,3675,3677,3679,3681,3683,3686],{"class":50,"line":3674},34,[48,3676,3642],{"class":112},[48,3678,777],{"class":80},[48,3680,3647],{"class":76},[48,3682,81],{"class":80},[48,3684,3685],{"class":758},"90",[48,3687,337],{"class":80},[48,3689,3691],{"class":50,"line":3690},35,[48,3692,66],{"emptyLinePlaceholder":65},[48,3694,3696],{"class":50,"line":3695},36,[48,3697,3698],{"class":170},"    # lerp_angle：让旋转平滑过渡，不会一下子\"弹\"过去\n",[48,3700,3702,3705,3707,3710,3712,3715,3718,3720,3722,3724,3726,3728],{"class":50,"line":3701},37,[48,3703,3704],{"class":112},"    rotation",[48,3706,777],{"class":80},[48,3708,3709],{"class":76}," lerp_angle",[48,3711,81],{"class":80},[48,3713,3714],{"class":112},"rotation",[48,3716,3717],{"class":80},",",[48,3719,3602],{"class":112},[48,3721,3717],{"class":80},[48,3723,3377],{"class":112},[48,3725,855],{"class":72},[48,3727,2321],{"class":112},[48,3729,337],{"class":80},[14,3731,3732],{},[10,3733,3734,3737,3738,3741],{},[45,3735,3736],{},"lerp_angle"," 的作用就像是给旋转加了\"缓动\"，从当前角度慢慢转到目标角度，而不是瞬间跳过去。",[45,3739,3740],{},"rotate_speed"," 越大转得越快，可以自己调到手感合适为止。",[25,3743,3745],{"id":3744},"再次测试","再次测试！",[10,3747,3748],{},"再次运行游戏，看看小鸟是不是有了那种「扑腾扑腾」的灵动感？是不是和原版《Flappy Bird》一个味儿了 ✨",[1804,3750,3751],{},"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":43,"searchDepth":62,"depth":62,"links":3753},[3754,3755,3756,3757,3758,3759,3760,3761],{"id":3085,"depth":62,"text":3085},{"id":3124,"depth":62,"text":3124},{"id":3134,"depth":62,"text":3135},{"id":3149,"depth":62,"text":3150},{"id":3181,"depth":62,"text":3181},{"id":3259,"depth":62,"text":3259},{"id":3279,"depth":62,"text":3279},{"id":3744,"depth":62,"text":3745},{},"\u002Fdevlog\u002Fxggame-bird\u002F03-game-scene",{"title":3077,"description":1832},"devlog\u002Fxggame-bird\u002F03-game-scene","8FuNcn0nabe5yIBK8x_umOGAktOPpJxM53aPIKn6ApQ",{"id":3768,"title":3769,"body":3770,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":5266,"navigation":65,"path":5267,"seo":5268,"stem":5269,"toc":65,"__hash__":5270},"devlog\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles.md","障碍物",{"type":7,"value":3771,"toc":5249},[3772,3775,3795,3813,3817,3820,3842,3848,3857,3860,3867,3886,3893,3898,3964,3970,3982,3988,3991,3997,4004,4010,4014,4020,4033,4098,4124,4238,4242,4249,4255,4259,4262,4282,4288,4506,4512,4516,4561,4569,4574,4580,4585,4853,4855,4866,4868,4892,4898,4901,4907,4918,4924,4934,4940,4943,5144,5147,5200,5215,5217,5220,5234,5240,5246],[10,3773,3774],{},"Flappy Bird 最经典的玩法 — 上下成对的水管，小鸟要从中间缝隙穿过去。这一章我们做：",[3034,3776,3777,3783,3789],{},[202,3778,3779,3782],{},[20,3780,3781],{},"障碍物本体"," — 上下两根水管 + 一个得分区",[202,3784,3785,3788],{},[20,3786,3787],{},"死区 Killzone"," — 撞上就死",[202,3790,3791,3794],{},[20,3792,3793],{},"障碍物生成器"," — 定时刷新、自动移动、屏幕外销毁",[14,3796,3797],{},[10,3798,3799,3800,3803,3804,3806,3807,3812],{},"这里用 ",[45,3801,3802],{},"Area2D","（死区）而不是 ",[45,3805,3197],{},"，因为 ",[20,3808,3809,3811],{},[45,3810,3802],{}," 不会真的\"卡住\"小鸟，只会触发信号","。这样我们就能在信号里写\"游戏结束\"的逻辑，而不是把小鸟物理地卡在水管里动弹不得。",[25,3814,3816],{"id":3815},"创建死区-killzone","创建死区 Killzone",[10,3818,3819],{},"先创建死区，使用到的节点：",[199,3821,3822],{},[202,3823,3824,3826,3827,3830,3831],{},[45,3825,3802],{}," — 死区根节点（重命名：",[45,3828,3829],{},"Killzone","）\n",[199,3832,3833],{},[202,3834,3835,3837,3838,3841],{},[45,3836,2097],{}," — 碰撞形状（",[20,3839,3840],{},"先留空","，下面会解释为什么）",[10,3843,3844],{},[1867,3845],{"alt":3846,"src":3847},"04-obstacles-死区","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E6%AD%BB%E5%8C%BA.png",[10,3849,3850,3852,3853,3856],{},[45,3851,2097],{}," 暂时",[20,3854,3855],{},"不要选形状","，因为后续这个死区既可以放到\"障碍物\"上，也可以放到\"地板\"上（小鸟没按飞掉地上也会 Game Over）。形状到时候根据位置再单独设置。",[25,3858,3859],{"id":3859},"障碍物的组成",[10,3861,3862,3863,3866],{},"每个障碍物由 ",[20,3864,3865],{},"3 个区域"," 组成：",[199,3868,3869,3875,3880],{},[202,3870,3871,3874],{},[20,3872,3873],{},"上水管","（Killzone — 死区）→ 撞到就死",[202,3876,3877,3874],{},[20,3878,3879],{},"下水管",[202,3881,3882,3885],{},[20,3883,3884],{},"中间的得分区","（Goal）→ 穿过加 1 分",[10,3887,3888,3889,3892],{},"这里我们同样先用 Godot 的默认素材（也就是默认的 ",[45,3890,3891],{},"icon.svg"," 来作为图片精灵图）",[14,3894,3895],{},[10,3896,3897],{},"等后续我们再一起更新美术资源素材！",[199,3899,3900],{},[202,3901,3902,3904,3905,3830,3908],{},[45,3903,3094],{},"（重命名：",[45,3906,3907],{},"PillarPair",[199,3909,3910,3929,3950],{},[202,3911,3912,3914,3915],{},[45,3913,3829],{},"（我们刚刚创建的死区）\n",[199,3916,3917,3924],{},[202,3918,3919,3921,3922,620],{},[45,3920,2091],{}," — 放入 Godot 默认素材（",[45,3923,3891],{},[202,3925,3926,3928],{},[45,3927,2097],{}," — 碰撞形状（矩形，调整到一根水管的大小）",[202,3930,3931,3934,3935,3937,3938],{},[45,3932,3933],{},"Killzone2","（复制 ",[45,3936,3829],{}," 一份，改名即可）\n",[199,3939,3940,3946],{},[202,3941,3942,3921,3944,620],{},[45,3943,2091],{},[45,3945,3891],{},[202,3947,3948,3928],{},[45,3949,2097],{},[202,3951,3952,3904,3954,3830,3957],{},[45,3953,3802],{},[45,3955,3956],{},"Goal",[199,3958,3959],{},[202,3960,3961,3963],{},[45,3962,2097],{}," — 得分区域",[10,3965,3966],{},[1867,3967],{"alt":3968,"src":3969},"04-obstacles-阻碍物","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E9%98%BB%E7%A2%8D%E7%89%A9.png",[10,3971,3972,3973,3975,3976,3978,3979,3981],{},"这里可以调整碰撞体的颜色，用红色来区分。\n然后在设置好一个 ",[45,3974,3829],{}," 里的 ",[45,3977,2091],{}," 和 ",[45,3980,2097],{}," 后可以复制",[10,3983,3984],{},[1867,3985],{"alt":3986,"src":3987},"04-obstacles-阻碍物2","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E9%98%BB%E7%A2%8D%E7%89%A92.png",[10,3989,3990],{},"然后调整两个的位置，在中间空出一道间隙。",[10,3992,3993],{},[1867,3994],{"alt":3995,"src":3996},"04-obstacles-得分区域","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E5%BE%97%E5%88%86%E5%8C%BA%E5%9F%9F.png",[10,3998,3999,4000,4003],{},"然后把得分区域",[20,4001,4002],{},"放在中间偏后","的位置，这样只有完全通过的时候才算得分！",[10,4005,4006,4007],{},"障碍物设置好后，就可以保存该场景为：",[45,4008,4009],{},"scenes\u002Fpillar_pair.tscn",[396,4011,4013],{"id":4012},"给-killzone-挂脚本","给 Killzone 挂脚本",[10,4015,4016],{},[1867,4017],{"alt":4018,"src":4019},"04-obstacles-死区脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E6%AD%BB%E5%8C%BA%E8%84%9A%E6%9C%AC.png",[10,4021,2138,4022,4024,4025,4028,4029,4032],{},[45,4023,3802],{}," 节点（Killzone） → 右侧【节点】面板找到 ",[45,4026,4027],{},"body_entered"," 信号 → 双击 → 连接到自己。Godot 会自动生成 ",[45,4030,4031],{},"_on_body_entered"," 函数：",[38,4034,4036],{"className":40,"code":4035,"language":42,"meta":43,"style":43},"extends Area2D\n\nfunc _on_body_entered(body: Node2D) -> void:\n    if body.name == \"Bird\":\n        print(\"撞击死亡！\")\n",[45,4037,4038,4044,4048,4070,4086],{"__ignoreMap":43},[48,4039,4040,4042],{"class":50,"line":51},[48,4041,55],{"class":54},[48,4043,59],{"class":58},[48,4045,4046],{"class":50,"line":62},[48,4047,66],{"emptyLinePlaceholder":65},[48,4049,4050,4052,4054,4056,4058,4060,4062,4064,4066,4068],{"class":50,"line":69},[48,4051,73],{"class":72},[48,4053,77],{"class":76},[48,4055,81],{"class":80},[48,4057,85],{"class":84},[48,4059,88],{"class":80},[48,4061,91],{"class":58},[48,4063,94],{"class":80},[48,4065,97],{"class":72},[48,4067,100],{"class":58},[48,4069,103],{"class":80},[48,4071,4072,4074,4076,4078,4080,4082,4084],{"class":50,"line":106},[48,4073,109],{"class":54},[48,4075,113],{"class":112},[48,4077,116],{"class":80},[48,4079,119],{"class":112},[48,4081,122],{"class":72},[48,4083,126],{"class":125},[48,4085,103],{"class":84},[48,4087,4088,4091,4093,4096],{"class":50,"line":156},[48,4089,4090],{"class":76},"        print",[48,4092,81],{"class":80},[48,4094,4095],{"class":125},"\"撞击死亡！\"",[48,4097,337],{"class":80},[199,4099,4100,4108,4114],{},[202,4101,4102,298,4104,4107],{},[45,4103,4027],{},[20,4105,4106],{},"任何物理体进入这个区域","时触发的信号",[202,4109,4110,4113],{},[45,4111,4112],{},"if body.name == \"Bird\":"," — 判断进来的是不是小鸟（防止其他东西误触发）",[202,4115,4116,4117,4120,4121,4123],{},"暂时用 ",[45,4118,4119],{},"print"," 打印日志，下一章做完 ",[45,4122,556],{}," 后再换成真正的\"游戏结束\"逻辑",[14,4125,4126,4133,4226],{},[10,4127,4128,4129,4132],{},"💡 ",[20,4130,4131],{},"后续优化预览","（下一章 GameManager + 第 8 章 AudioManager 后会改成这样，现在不要这么写）：",[38,4134,4136],{"className":40,"code":4135,"language":42,"meta":43,"style":43},"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",[45,4137,4138,4144,4148,4170,4206,4216],{"__ignoreMap":43},[48,4139,4140,4142],{"class":50,"line":51},[48,4141,55],{"class":54},[48,4143,59],{"class":58},[48,4145,4146],{"class":50,"line":62},[48,4147,66],{"emptyLinePlaceholder":65},[48,4149,4150,4152,4154,4156,4158,4160,4162,4164,4166,4168],{"class":50,"line":69},[48,4151,73],{"class":72},[48,4153,77],{"class":76},[48,4155,81],{"class":80},[48,4157,85],{"class":84},[48,4159,88],{"class":80},[48,4161,91],{"class":58},[48,4163,94],{"class":80},[48,4165,97],{"class":72},[48,4167,100],{"class":58},[48,4169,103],{"class":80},[48,4171,4172,4174,4176,4178,4180,4182,4184,4186,4188,4190,4192,4194,4196,4198,4200,4202,4204],{"class":50,"line":106},[48,4173,109],{"class":54},[48,4175,113],{"class":112},[48,4177,116],{"class":80},[48,4179,119],{"class":112},[48,4181,122],{"class":72},[48,4183,126],{"class":125},[48,4185,129],{"class":72},[48,4187,132],{"class":58},[48,4189,116],{"class":80},[48,4191,137],{"class":112},[48,4193,122],{"class":72},[48,4195,132],{"class":58},[48,4197,116],{"class":80},[48,4199,146],{"class":112},[48,4201,116],{"class":80},[48,4203,151],{"class":54},[48,4205,103],{"class":84},[48,4207,4208,4210,4212,4214],{"class":50,"line":156},[48,4209,159],{"class":58},[48,4211,116],{"class":84},[48,4213,164],{"class":76},[48,4215,711],{"class":80},[48,4217,4218,4220,4222,4224],{"class":50,"line":174},[48,4219,177],{"class":58},[48,4221,116],{"class":84},[48,4223,182],{"class":76},[48,4225,711],{"class":80},[199,4227,4228,4233],{},[202,4229,4230,4232],{},[45,4231,556],{}," 是整个游戏的整体管理",[202,4234,4235,4237],{},[45,4236,572],{}," 是整个游戏的音效管理",[396,4239,4241],{"id":4240},"地板也可以变成-killzone","地板也可以变成 Killzone",[10,4243,4244,4245,4248],{},"之前第 3 章做的底部边界，",[20,4246,4247],{},"也可以变成死区"," — 小鸟掉到地上就算游戏结束。",[10,4250,4251],{},[1867,4252],{"alt":4253,"src":4254},"04-obstacles-地板死区","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E5%9C%B0%E6%9D%BF%E6%AD%BB%E5%8C%BA.png",[25,4256,4258],{"id":4257},"障碍物移动-销毁-得分","障碍物移动 + 销毁 + 得分",[10,4260,4261],{},"障碍物根节点要做三件事：",[3034,4263,4264,4270,4276],{},[202,4265,4266,4269],{},[20,4267,4268],{},"往左移动"," — 营造小鸟在向右飞的错觉",[202,4271,4272,4275],{},[20,4273,4274],{},"离开屏幕后销毁"," — 不然会一直累积，性能爆炸",[202,4277,4278,4281],{},[20,4279,4280],{},"检测小鸟穿过"," — 加分",[10,4283,4284,4285,4287],{},"这些都写在障碍物根节点（",[45,4286,3094],{},"）的脚本里：",[38,4289,4291],{"className":40,"code":4290,"language":42,"meta":43,"style":43},"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",[45,4292,4293,4299,4303,4315,4333,4337,4359,4364,4381,4386,4404,4411,4415,4420,4442,4447,4463,4474,4479,4484],{"__ignoreMap":43},[48,4294,4295,4297],{"class":50,"line":51},[48,4296,55],{"class":54},[48,4298,737],{"class":58},[48,4300,4301],{"class":50,"line":62},[48,4302,66],{"emptyLinePlaceholder":65},[48,4304,4305,4307,4309,4311,4313],{"class":50,"line":69},[48,4306,746],{"class":76},[48,4308,749],{"class":72},[48,4310,752],{"class":112},[48,4312,755],{"class":80},[48,4314,759],{"class":758},[48,4316,4317,4319,4321,4323,4325,4327,4329,4331],{"class":50,"line":106},[48,4318,764],{"class":76},[48,4320,749],{"class":72},[48,4322,769],{"class":112},[48,4324,88],{"class":80},[48,4326,774],{"class":58},[48,4328,777],{"class":80},[48,4330,780],{"class":54},[48,4332,784],{"class":783},[48,4334,4335],{"class":50,"line":156},[48,4336,66],{"emptyLinePlaceholder":65},[48,4338,4339,4341,4343,4345,4347,4349,4351,4353,4355,4357],{"class":50,"line":174},[48,4340,73],{"class":72},[48,4342,795],{"class":76},[48,4344,81],{"class":80},[48,4346,800],{"class":84},[48,4348,88],{"class":80},[48,4350,805],{"class":58},[48,4352,94],{"class":80},[48,4354,97],{"class":72},[48,4356,100],{"class":58},[48,4358,103],{"class":80},[48,4360,4361],{"class":50,"line":351},[48,4362,4363],{"class":170},"    # 统一向左移动\n",[48,4365,4366,4369,4371,4373,4375,4377,4379],{"class":50,"line":356},[48,4367,4368],{"class":112},"    position",[48,4370,116],{"class":80},[48,4372,847],{"class":112},[48,4374,850],{"class":72},[48,4376,752],{"class":112},[48,4378,855],{"class":72},[48,4380,858],{"class":112},[48,4382,4383],{"class":50,"line":367},[48,4384,4385],{"class":170},"    # 离开屏幕后自动销毁\n",[48,4387,4388,4390,4392,4394,4396,4398,4400,4402],{"class":50,"line":884},[48,4389,109],{"class":54},[48,4391,866],{"class":112},[48,4393,116],{"class":80},[48,4395,847],{"class":112},[48,4397,873],{"class":72},[48,4399,876],{"class":72},[48,4401,879],{"class":758},[48,4403,103],{"class":84},[48,4405,4406,4409],{"class":50,"line":892},[48,4407,4408],{"class":76},"        queue_free",[48,4410,711],{"class":80},[48,4412,4413],{"class":50,"line":897},[48,4414,66],{"emptyLinePlaceholder":65},[48,4416,4417],{"class":50,"line":903},[48,4418,4419],{"class":170},"# 当物体进入得分区域【得分】\n",[48,4421,4422,4424,4426,4428,4430,4432,4434,4436,4438,4440],{"class":50,"line":927},[48,4423,73],{"class":72},[48,4425,908],{"class":76},[48,4427,81],{"class":80},[48,4429,85],{"class":84},[48,4431,88],{"class":80},[48,4433,91],{"class":58},[48,4435,94],{"class":80},[48,4437,97],{"class":72},[48,4439,100],{"class":58},[48,4441,103],{"class":80},[48,4443,4444],{"class":50,"line":944},[48,4445,4446],{"class":170},"    # 检查进入的是不是小鸟（防止其他东西误触发）\n",[48,4448,4449,4451,4453,4455,4457,4459,4461],{"class":50,"line":1421},[48,4450,109],{"class":54},[48,4452,113],{"class":112},[48,4454,116],{"class":80},[48,4456,119],{"class":112},[48,4458,122],{"class":72},[48,4460,126],{"class":125},[48,4462,103],{"class":84},[48,4464,4465,4467,4469,4472],{"class":50,"line":1431},[48,4466,4090],{"class":76},[48,4468,81],{"class":80},[48,4470,4471],{"class":125},"\"得分！\"",[48,4473,337],{"class":80},[48,4475,4476],{"class":50,"line":1443},[48,4477,4478],{"class":170},"        # 重要：得分后立即禁用这个检测区域，防止重复得分\n",[48,4480,4481],{"class":50,"line":1448},[48,4482,4483],{"class":170},"        # 使用 set_deferred 是因为在碰撞回调中不能直接修改物理属性\n",[48,4485,4486,4489,4491,4494,4496,4499,4501,4504],{"class":50,"line":1458},[48,4487,4488],{"class":112},"        goal",[48,4490,116],{"class":84},[48,4492,4493],{"class":76},"set_deferred",[48,4495,81],{"class":80},[48,4497,4498],{"class":125},"\"monitoring\"",[48,4500,3717],{"class":80},[48,4502,4503],{"class":54}," false",[48,4505,337],{"class":80},[10,4507,4508],{},[1867,4509],{"alt":4510,"src":4511},"04-obstacles-障碍物脚本","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E9%9A%9C%E7%A2%8D%E7%89%A9%E8%84%9A%E6%9C%AC.png",[396,4513,4515],{"id":4514},"这段代码做了啥","这段代码做了啥？",[199,4517,4518,4524,4533,4539,4549],{},[202,4519,4520,4523],{},[45,4521,4522],{},"@export var speed := 200.0"," — 把移动速度暴露到面板，方便实时调",[202,4525,4526,4529,4530,4532],{},[45,4527,4528],{},"@onready var goal: Area2D = $Goal"," — 拿到子节点 ",[45,4531,3956],{}," 的引用（用于得分后禁用它）",[202,4534,4535,4538],{},[45,4536,4537],{},"position.x -= speed * delta"," — 每帧整个障碍物往左移一点",[202,4540,4541,4544,4545,4548],{},[45,4542,4543],{},"if position.x \u003C -500: queue_free()"," — 飞出屏幕外就",[20,4546,4547],{},"销毁自己","，回收内存",[202,4550,4551,4554,4555,4557,4558,620],{},[45,4552,4553],{},"_on_goal_body_entered"," — 小鸟进入 ",[45,4556,3956],{}," 时触发（同样要在编辑器里",[20,4559,4560],{},"连接信号",[396,4562,4564,4565,4568],{"id":4563},"为什么要-set_deferredmonitoring-false","为什么要 ",[45,4566,4567],{},"set_deferred(\"monitoring\", false)","？",[10,4570,4571,194],{},[20,4572,4573],{},"为了防止重复得分",[10,4575,4576,4577,4579],{},"如果不禁用，小鸟身上的碰撞体可能在 Goal 区域里横跨多帧，每帧都触发一次 ",[45,4578,4027],{},"，你就从 1 分跳到 5 分了 😅",[10,4581,4582,4584],{},[45,4583,4493],{}," 是因为在物理回调里不能直接修改物理属性，要\"延迟到下一帧\"再改。",[14,4586,4587,4592,4832,4835],{},[10,4588,4128,4589,4591],{},[20,4590,4131],{},"（下一章 GameManager + 第 8 章 AudioManager 后会改成这样）：",[38,4593,4595],{"className":40,"code":4594,"language":42,"meta":43,"style":43},"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",[45,4596,4597,4603,4607,4619,4637,4641,4663,4687,4703,4721,4727,4731,4753,4769,4779,4799,4814],{"__ignoreMap":43},[48,4598,4599,4601],{"class":50,"line":51},[48,4600,55],{"class":54},[48,4602,737],{"class":58},[48,4604,4605],{"class":50,"line":62},[48,4606,66],{"emptyLinePlaceholder":65},[48,4608,4609,4611,4613,4615,4617],{"class":50,"line":69},[48,4610,746],{"class":76},[48,4612,749],{"class":72},[48,4614,752],{"class":112},[48,4616,755],{"class":80},[48,4618,759],{"class":758},[48,4620,4621,4623,4625,4627,4629,4631,4633,4635],{"class":50,"line":106},[48,4622,764],{"class":76},[48,4624,749],{"class":72},[48,4626,769],{"class":112},[48,4628,88],{"class":80},[48,4630,774],{"class":58},[48,4632,777],{"class":80},[48,4634,780],{"class":54},[48,4636,784],{"class":783},[48,4638,4639],{"class":50,"line":156},[48,4640,66],{"emptyLinePlaceholder":65},[48,4642,4643,4645,4647,4649,4651,4653,4655,4657,4659,4661],{"class":50,"line":174},[48,4644,73],{"class":72},[48,4646,795],{"class":76},[48,4648,81],{"class":80},[48,4650,800],{"class":84},[48,4652,88],{"class":80},[48,4654,805],{"class":58},[48,4656,94],{"class":80},[48,4658,97],{"class":72},[48,4660,100],{"class":58},[48,4662,103],{"class":80},[48,4664,4665,4667,4669,4671,4673,4675,4677,4679,4681,4683,4685],{"class":50,"line":351},[48,4666,109],{"class":54},[48,4668,132],{"class":58},[48,4670,116],{"class":80},[48,4672,137],{"class":112},[48,4674,122],{"class":72},[48,4676,132],{"class":58},[48,4678,116],{"class":80},[48,4680,146],{"class":112},[48,4682,116],{"class":80},[48,4684,151],{"class":54},[48,4686,103],{"class":84},[48,4688,4689,4691,4693,4695,4697,4699,4701],{"class":50,"line":356},[48,4690,842],{"class":112},[48,4692,116],{"class":80},[48,4694,847],{"class":112},[48,4696,850],{"class":72},[48,4698,752],{"class":112},[48,4700,855],{"class":72},[48,4702,858],{"class":112},[48,4704,4705,4707,4709,4711,4713,4715,4717,4719],{"class":50,"line":367},[48,4706,863],{"class":54},[48,4708,866],{"class":112},[48,4710,116],{"class":80},[48,4712,847],{"class":112},[48,4714,873],{"class":72},[48,4716,876],{"class":72},[48,4718,879],{"class":758},[48,4720,103],{"class":84},[48,4722,4723,4725],{"class":50,"line":884},[48,4724,887],{"class":76},[48,4726,711],{"class":80},[48,4728,4729],{"class":50,"line":892},[48,4730,66],{"emptyLinePlaceholder":65},[48,4732,4733,4735,4737,4739,4741,4743,4745,4747,4749,4751],{"class":50,"line":897},[48,4734,73],{"class":72},[48,4736,908],{"class":76},[48,4738,81],{"class":80},[48,4740,85],{"class":84},[48,4742,88],{"class":80},[48,4744,91],{"class":58},[48,4746,94],{"class":80},[48,4748,97],{"class":72},[48,4750,100],{"class":58},[48,4752,103],{"class":80},[48,4754,4755,4757,4759,4761,4763,4765,4767],{"class":50,"line":903},[48,4756,109],{"class":54},[48,4758,113],{"class":112},[48,4760,116],{"class":80},[48,4762,119],{"class":112},[48,4764,122],{"class":72},[48,4766,126],{"class":125},[48,4768,103],{"class":84},[48,4770,4771,4773,4775,4777],{"class":50,"line":927},[48,4772,159],{"class":58},[48,4774,116],{"class":84},[48,4776,1362],{"class":76},[48,4778,711],{"class":80},[48,4780,4781,4783,4785,4787,4790,4792,4795,4797],{"class":50,"line":944},[48,4782,863],{"class":54},[48,4784,132],{"class":58},[48,4786,116],{"class":84},[48,4788,4789],{"class":76},"has_method",[48,4791,81],{"class":80},[48,4793,4794],{"class":125},"\"add_score\"",[48,4796,94],{"class":80},[48,4798,103],{"class":84},[48,4800,4801,4804,4806,4808,4810,4812],{"class":50,"line":1421},[48,4802,4803],{"class":58},"            GameManager",[48,4805,116],{"class":84},[48,4807,1144],{"class":76},[48,4809,81],{"class":80},[48,4811,522],{"class":758},[48,4813,337],{"class":80},[48,4815,4816,4818,4820,4822,4824,4826,4828,4830],{"class":50,"line":1431},[48,4817,4488],{"class":112},[48,4819,116],{"class":84},[48,4821,4493],{"class":76},[48,4823,81],{"class":80},[48,4825,4498],{"class":125},[48,4827,3717],{"class":80},[48,4829,4503],{"class":54},[48,4831,337],{"class":80},[10,4833,4834],{},"升级点：",[199,4836,4837,4844],{},[202,4838,4839,4840,4843],{},"加了 ",[45,4841,4842],{},"if GameManager.current_state == PLAYING"," — 游戏暂停\u002F结束时不再移动",[202,4845,4846,4847,3978,4850],{},"得分时调用 ",[45,4848,4849],{},"GameManager.add_score(1)",[45,4851,4852],{},"AudioManager.play_score()",[25,4854,3793],{"id":3793},[10,4856,4857,4858,4861,4862,4865],{},"光有一根水管不够，需要",[20,4859,4860],{},"源源不断的水管","。用 ",[45,4863,4864],{},"Timer"," 节点定时生成：",[10,4867,3106],{},[199,4869,4870],{},[202,4871,4872,4874,4875,3830,4878],{},[45,4873,3094],{}," — 生成器根节点（重命名：",[45,4876,4877],{},"PillarSpawner",[199,4879,4880],{},[202,4881,4882,4884,4885,4888,4889,620],{},[45,4883,4864],{}," — 定时器（",[45,4886,4887],{},"Wait Time = 2.5s","，勾选 ",[45,4890,4891],{},"Autostart",[10,4893,4894],{},[1867,4895],{"alt":4896,"src":4897},"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,4899,4900],{},"设置 Timer 节点的属性：",[10,4902,4903],{},[1867,4904],{"alt":4905,"src":4906},"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,4908,4909,4910,4913,4914,4917],{},"把 ",[45,4911,4912],{},"pillar_pair.tscn"," 拖到 ",[45,4915,4916],{},"Pillar Scene"," 槽里：",[10,4919,4920],{},[1867,4921],{"alt":4922,"src":4923},"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,4925,4926,4927,4929,4930,4933],{},"最后，把 ",[45,4928,4877],{}," 整个节点",[20,4931,4932],{},"拖到游戏窗口右边外面","（这样水管才会从屏幕右侧\"飞\"进来）：",[10,4935,4936],{},[1867,4937],{"alt":4938,"src":4939},"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",[396,4941,4942],{"id":4942},"生成器脚本",[38,4944,4946],{"className":40,"code":4945,"language":42,"meta":43,"style":43},"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",[45,4947,4948,4954,4958,4972,4986,4990,5005,5012,5016,5031,5049,5053,5065,5092,5096,5111,5115,5120],{"__ignoreMap":43},[48,4949,4950,4952],{"class":50,"line":51},[48,4951,55],{"class":54},[48,4953,737],{"class":58},[48,4955,4956],{"class":50,"line":62},[48,4957,66],{"emptyLinePlaceholder":65},[48,4959,4960,4962,4964,4967,4969],{"class":50,"line":69},[48,4961,746],{"class":76},[48,4963,749],{"class":72},[48,4965,4966],{"class":112}," pillar_scene",[48,4968,88],{"class":80},[48,4970,4971],{"class":58}," PackedScene\n",[48,4973,4974,4976,4978,4981,4983],{"class":50,"line":106},[48,4975,746],{"class":76},[48,4977,749],{"class":72},[48,4979,4980],{"class":112}," y_range",[48,4982,755],{"class":80},[48,4984,4985],{"class":758}," 250.0\n",[48,4987,4988],{"class":50,"line":156},[48,4989,66],{"emptyLinePlaceholder":65},[48,4991,4992,4994,4997,4999,5001,5003],{"class":50,"line":174},[48,4993,73],{"class":72},[48,4995,4996],{"class":76}," _on_timer_timeout",[48,4998,167],{"class":80},[48,5000,97],{"class":72},[48,5002,100],{"class":58},[48,5004,103],{"class":80},[48,5006,5007,5010],{"class":50,"line":351},[48,5008,5009],{"class":76},"    spawn_pillar",[48,5011,711],{"class":80},[48,5013,5014],{"class":50,"line":356},[48,5015,66],{"emptyLinePlaceholder":65},[48,5017,5018,5020,5023,5025,5027,5029],{"class":50,"line":367},[48,5019,73],{"class":72},[48,5021,5022],{"class":76}," spawn_pillar",[48,5024,167],{"class":80},[48,5026,97],{"class":72},[48,5028,100],{"class":58},[48,5030,103],{"class":80},[48,5032,5033,5035,5038,5040,5042,5044,5047],{"class":50,"line":884},[48,5034,3599],{"class":72},[48,5036,5037],{"class":112}," new_pillar",[48,5039,777],{"class":80},[48,5041,4966],{"class":112},[48,5043,116],{"class":84},[48,5045,5046],{"class":76},"instantiate",[48,5048,711],{"class":80},[48,5050,5051],{"class":50,"line":892},[48,5052,66],{"emptyLinePlaceholder":65},[48,5054,5055,5057,5060,5062],{"class":50,"line":897},[48,5056,3599],{"class":72},[48,5058,5059],{"class":112}," spawn_pos",[48,5061,777],{"class":80},[48,5063,5064],{"class":112}," global_position\n",[48,5066,5067,5070,5072,5074,5076,5079,5081,5083,5086,5088,5090],{"class":50,"line":903},[48,5068,5069],{"class":112},"    spawn_pos",[48,5071,116],{"class":80},[48,5073,1621],{"class":112},[48,5075,2311],{"class":72},[48,5077,5078],{"class":76}," randf_range",[48,5080,81],{"class":80},[48,5082,3652],{"class":72},[48,5084,5085],{"class":112},"y_range",[48,5087,3717],{"class":80},[48,5089,4980],{"class":112},[48,5091,337],{"class":80},[48,5093,5094],{"class":50,"line":927},[48,5095,66],{"emptyLinePlaceholder":65},[48,5097,5098,5101,5103,5106,5108],{"class":50,"line":944},[48,5099,5100],{"class":112},"    new_pillar",[48,5102,116],{"class":80},[48,5104,5105],{"class":112},"global_position",[48,5107,777],{"class":80},[48,5109,5110],{"class":112}," spawn_pos\n",[48,5112,5113],{"class":50,"line":1421},[48,5114,66],{"emptyLinePlaceholder":65},[48,5116,5117],{"class":50,"line":1431},[48,5118,5119],{"class":170},"    # 重点：加到主场景或者专门的容器里，防止柱子跟着生成器动\n",[48,5121,5122,5125,5127,5129,5132,5134,5137,5139,5142],{"class":50,"line":1443},[48,5123,5124],{"class":76},"    get_tree",[48,5126,167],{"class":80},[48,5128,116],{"class":84},[48,5130,5131],{"class":112},"current_scene",[48,5133,116],{"class":84},[48,5135,5136],{"class":76},"add_child",[48,5138,81],{"class":80},[48,5140,5141],{"class":112},"new_pillar",[48,5143,337],{"class":80},[396,5145,5146],{"id":5146},"关键点",[199,5148,5149,5162,5168,5176,5182,5188],{},[202,5150,5151,5154,5155],{},[45,5152,5153],{},"@export var pillar_scene: PackedScene"," — 把\"水管场景\"暴露到面板，",[20,5156,5157,5158,5161],{},"你需要在面板上把 ",[45,5159,5160],{},"pillar.tscn"," 拖进这个槽里",[202,5163,5164,5167],{},[45,5165,5166],{},"@export var y_range := 250.0"," — 水管在垂直方向上的随机偏移范围",[202,5169,5170,5173,5174,620],{},[45,5171,5172],{},"_on_timer_timeout"," — 每次 Timer 触发就生成新的（同样要",[20,5175,4560],{},[202,5177,5178,5181],{},[45,5179,5180],{},"pillar_scene.instantiate()"," — 把场景\"实例化\"成一个真正的节点",[202,5183,5184,5187],{},[45,5185,5186],{},"randf_range(-y_range, y_range)"," — 随机一个垂直偏移",[202,5189,5190,5195,5196,5199],{},[20,5191,5192],{},[45,5193,5194],{},"get_tree().current_scene.add_child(new_pillar)"," ⚠️ 重点！把水管加到",[20,5197,5198],{},"主场景","而不是生成器自己。否则如果生成器以后会移动，水管会跟着一起动，整个画面就乱套了。",[14,5201,5202],{},[10,5203,4128,5204,5206,5207,5210,5211,5214],{},[20,5205,4131],{},"（下一章 GameManager 之后）：在 ",[45,5208,5209],{},"spawn_pillar"," 最外层包一个 ",[45,5212,5213],{},"if GameManager.current_state == GameManager.GameState.PLAYING:","，这样游戏结束后就不会继续生成水管了。",[25,5216,3259],{"id":3259},[10,5218,5219],{},"应该能看到：",[199,5221,5222,5225,5228,5231],{},[202,5223,5224],{},"✅ 水管从右往左移动",[202,5226,5227],{},"✅ 每 2.5 秒生成一根，高度随机",[202,5229,5230],{},"✅ 小鸟穿过会在控制台打印「得分！」",[202,5232,5233],{},"✅ 小鸟撞到水管或地面会在控制台打印「撞击死亡！」",[10,5235,5236],{},[1867,5237],{"alt":5238,"src":5239},"04-obstacles-尝试运行","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles-%E5%B0%9D%E8%AF%95%E8%BF%90%E8%A1%8C.png",[10,5241,5242,5243,5245],{},"下一章我们来做 ",[20,5244,556],{},"，把\"撞击死亡\"真正变成\"游戏结束 + 显示分数 + 重新开始\"。",[1804,5247,5248],{},"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":43,"searchDepth":62,"depth":62,"links":5250},[5251,5252,5256,5261,5265],{"id":3815,"depth":62,"text":3816},{"id":3859,"depth":62,"text":3859,"children":5253},[5254,5255],{"id":4012,"depth":69,"text":4013},{"id":4240,"depth":69,"text":4241},{"id":4257,"depth":62,"text":4258,"children":5257},[5258,5259],{"id":4514,"depth":69,"text":4515},{"id":4563,"depth":69,"text":5260},"为什么要 set_deferred(\"monitoring\", false)？",{"id":3793,"depth":62,"text":3793,"children":5262},[5263,5264],{"id":4942,"depth":69,"text":4942},{"id":5146,"depth":69,"text":5146},{"id":3259,"depth":62,"text":3259},{},"\u002Fdevlog\u002Fxggame-bird\u002F04-obstacles",{"title":3769,"description":1832},"devlog\u002Fxggame-bird\u002F04-obstacles","Vgp6pvSrVWBZGSu3mogKB_sUW9o-ioJjpVTu6EKg0-s",{"id":5272,"title":556,"body":5273,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":6712,"navigation":65,"path":34,"seo":6713,"stem":6714,"toc":65,"__hash__":6715},"devlog\u002Fdevlog\u002Fxggame-bird\u002F05-game-manager.md",{"type":7,"value":5274,"toc":6687},[5275,5288,5292,5295,5313,5316,5323,5327,5332,5661,5665,5675,5690,5693,5698,5704,5730,5737,5765,5779,5781,5787,5812,5832,5879,5885,5914,5921,5937,5943,5951,5998,6016,6025,6039,6044,6048,6055,6061,6144,6151,6157,6389,6396,6592,6594,6600,6606,6675,6684],[10,5276,5277,5278,5281,5282,5285,5286,194],{},"游戏到这一步已经能玩了，但缺少",[20,5279,5280],{},"游戏状态管理","和",[20,5283,5284],{},"计分","。我们需要一个统一的管理者来掌控全局 — 这就是 ",[20,5287,556],{},[25,5289,5291],{"id":5290},"为什么需要-gamemanager","为什么需要 GameManager？",[10,5293,5294],{},"想象一下，游戏有 3 种状态：",[199,5296,5297,5303,5308],{},[202,5298,5299,5302],{},[20,5300,5301],{},"READY","（准备）— 显示\"点击开始\"",[202,5304,5305,5307],{},[20,5306,151],{},"（进行中）— 小鸟能跳、水管在生成",[202,5309,5310,5312],{},[20,5311,1018],{},"（结束）— 显示分数、可以重开",[10,5314,5315],{},"如果让每个节点自己判断状态，比如小鸟自己看\"现在能不能跳\"、水管自己看\"现在能不能生成\"，代码会乱成一团 — 改一个地方要同步改很多处。",[10,5317,5318,5319,5322],{},"GameManager 就是",[20,5320,5321],{},"所有节点共同遵守的\"裁判\""," — 状态、分数都由它统一保管，其他节点只问它要、不自己存。",[25,5324,5326],{"id":5325},"创建-gamemanager-脚本","创建 GameManager 脚本",[10,5328,294,5329,298],{},[45,5330,5331],{},"scripts\u002Fautoload\u002Fgame_manager.gd",[38,5333,5335],{"className":40,"code":5334,"language":42,"meta":43,"style":43},"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",[45,5336,5337,5343,5347,5352,5372,5386,5390,5407,5424,5428,5433,5445,5457,5461,5466,5475,5484,5500,5504,5509,5532,5541,5555,5559,5564,5581,5591,5606,5610,5615,5624,5639,5651],{"__ignoreMap":43},[48,5338,5339,5341],{"class":50,"line":51},[48,5340,55],{"class":54},[48,5342,310],{"class":58},[48,5344,5345],{"class":50,"line":62},[48,5346,66],{"emptyLinePlaceholder":65},[48,5348,5349],{"class":50,"line":69},[48,5350,5351],{"class":170},"# 定义游戏状态\n",[48,5353,5354,5356,5358,5360,5362,5364,5366,5368,5370],{"class":50,"line":106},[48,5355,999],{"class":54},[48,5357,1002],{"class":58},[48,5359,1005],{"class":80},[48,5361,1008],{"class":112},[48,5363,1011],{"class":84},[48,5365,151],{"class":112},[48,5367,1011],{"class":84},[48,5369,1018],{"class":112},[48,5371,1021],{"class":80},[48,5373,5374,5376,5378,5380,5382,5384],{"class":50,"line":156},[48,5375,1026],{"class":72},[48,5377,1029],{"class":112},[48,5379,777],{"class":80},[48,5381,1002],{"class":58},[48,5383,116],{"class":80},[48,5385,1038],{"class":54},[48,5387,5388],{"class":50,"line":174},[48,5389,66],{"emptyLinePlaceholder":65},[48,5391,5392,5394,5396,5398,5400,5402,5404],{"class":50,"line":351},[48,5393,1026],{"class":72},[48,5395,1049],{"class":112},[48,5397,88],{"class":80},[48,5399,382],{"class":58},[48,5401,777],{"class":80},[48,5403,3628],{"class":758},[48,5405,5406],{"class":170},"   # 总分\n",[48,5408,5409,5411,5413,5415,5417,5419,5421],{"class":50,"line":356},[48,5410,1026],{"class":72},[48,5412,1065],{"class":112},[48,5414,88],{"class":80},[48,5416,382],{"class":58},[48,5418,777],{"class":80},[48,5420,3628],{"class":758},[48,5422,5423],{"class":170},"    # 最高分\n",[48,5425,5426],{"class":50,"line":367},[48,5427,66],{"emptyLinePlaceholder":65},[48,5429,5430],{"class":50,"line":884},[48,5431,5432],{"class":170},"# 信号：通知其他节点\n",[48,5434,5435,5437,5439,5441,5443],{"class":50,"line":892},[48,5436,342],{"class":72},[48,5438,1080],{"class":76},[48,5440,81],{"class":80},[48,5442,1085],{"class":84},[48,5444,337],{"class":80},[48,5446,5447,5449,5451,5453,5455],{"class":50,"line":897},[48,5448,342],{"class":72},[48,5450,1094],{"class":76},[48,5452,81],{"class":80},[48,5454,1099],{"class":84},[48,5456,337],{"class":80},[48,5458,5459],{"class":50,"line":903},[48,5460,66],{"emptyLinePlaceholder":65},[48,5462,5463],{"class":50,"line":927},[48,5464,5465],{"class":170},"## 清空分数\n",[48,5467,5468,5470,5473],{"class":50,"line":944},[48,5469,73],{"class":72},[48,5471,5472],{"class":76}," clear_score",[48,5474,1402],{"class":80},[48,5476,5477,5480,5482],{"class":50,"line":1421},[48,5478,5479],{"class":112},"    total_score",[48,5481,777],{"class":80},[48,5483,1058],{"class":758},[48,5485,5486,5489,5491,5493,5495,5498],{"class":50,"line":1431},[48,5487,5488],{"class":112},"    score_changed",[48,5490,116],{"class":84},[48,5492,708],{"class":76},[48,5494,81],{"class":80},[48,5496,5497],{"class":112},"total_score",[48,5499,337],{"class":80},[48,5501,5502],{"class":50,"line":1443},[48,5503,66],{"emptyLinePlaceholder":65},[48,5505,5506],{"class":50,"line":1448},[48,5507,5508],{"class":170},"## 添加得分\n",[48,5510,5511,5513,5516,5518,5520,5522,5524,5526,5529],{"class":50,"line":1458},[48,5512,73],{"class":72},[48,5514,5515],{"class":76}," add_score",[48,5517,81],{"class":80},[48,5519,377],{"class":84},[48,5521,88],{"class":80},[48,5523,382],{"class":58},[48,5525,777],{"class":80},[48,5527,5528],{"class":758}," 1",[48,5530,5531],{"class":80},"):\n",[48,5533,5534,5536,5538],{"class":50,"line":3537},[48,5535,5479],{"class":112},[48,5537,2311],{"class":72},[48,5539,5540],{"class":112}," amount\n",[48,5542,5543,5545,5547,5549,5551,5553],{"class":50,"line":3542},[48,5544,5488],{"class":112},[48,5546,116],{"class":84},[48,5548,708],{"class":76},[48,5550,81],{"class":80},[48,5552,5497],{"class":112},[48,5554,337],{"class":80},[48,5556,5557],{"class":50,"line":3548},[48,5558,66],{"emptyLinePlaceholder":65},[48,5560,5561],{"class":50,"line":3560},[48,5562,5563],{"class":170},"## 设置当前游戏状态\n",[48,5565,5566,5568,5571,5573,5575,5577,5579],{"class":50,"line":3567},[48,5567,73],{"class":72},[48,5569,5570],{"class":76}," set_state",[48,5572,81],{"class":80},[48,5574,1099],{"class":84},[48,5576,88],{"class":80},[48,5578,1002],{"class":58},[48,5580,5531],{"class":80},[48,5582,5583,5586,5588],{"class":50,"line":3572},[48,5584,5585],{"class":112},"    current_state",[48,5587,777],{"class":80},[48,5589,5590],{"class":112}," new_state\n",[48,5592,5593,5596,5598,5600,5602,5604],{"class":50,"line":3596},[48,5594,5595],{"class":112},"    state_changed",[48,5597,116],{"class":84},[48,5599,708],{"class":76},[48,5601,81],{"class":80},[48,5603,1099],{"class":112},[48,5605,337],{"class":80},[48,5607,5608],{"class":50,"line":3610},[48,5609,66],{"emptyLinePlaceholder":65},[48,5611,5612],{"class":50,"line":3615},[48,5613,5614],{"class":170},"## 游戏结束\n",[48,5616,5617,5619,5622],{"class":50,"line":3633},[48,5618,73],{"class":72},[48,5620,5621],{"class":76}," game_over",[48,5623,1402],{"class":80},[48,5625,5626,5629,5631,5633,5635,5637],{"class":50,"line":3639},[48,5627,5628],{"class":76},"    set_state",[48,5630,81],{"class":80},[48,5632,146],{"class":58},[48,5634,116],{"class":80},[48,5636,1018],{"class":54},[48,5638,337],{"class":80},[48,5640,5641,5643,5645,5647,5649],{"class":50,"line":3660},[48,5642,109],{"class":54},[48,5644,1065],{"class":112},[48,5646,873],{"class":72},[48,5648,1049],{"class":112},[48,5650,103],{"class":84},[48,5652,5653,5656,5658],{"class":50,"line":3668},[48,5654,5655],{"class":112},"        high_score",[48,5657,777],{"class":80},[48,5659,5660],{"class":112}," total_score\n",[25,5662,5664],{"id":5663},"autoload-全局单例","Autoload — 全局单例",[10,5666,5667,5668,5671,5672,194],{},"脚本写好了，但现在它只是个 ",[45,5669,5670],{},".gd"," 文件，",[20,5673,5674],{},"别的节点没办法访问到它",[10,5676,5677,5678,5681,5682,5685,5686,5689],{},"Godot 提供了 ",[20,5679,5680],{},"Autoload","（自动加载）机制 — 注册之后，这个脚本\u002F场景",[20,5683,5684],{},"全程只有一份","，任何节点都能直接通过名字访问，不需要 ",[45,5687,5688],{},"get_node()"," 找路径。",[396,5691,5692],{"id":5692},"注册步骤",[10,5694,5695,5696],{},"路径：",[20,5697,475],{},[10,5699,5700],{},[1867,5701],{"alt":5702,"src":5703},"05-game-manager-全局单列GM","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F05-game-manager-%E5%85%A8%E5%B1%80%E5%8D%95%E5%88%97GM.png",[3034,5705,5706,5715,5724,5727],{},[202,5707,5708,5711,5712],{},[20,5709,5710],{},"路径","：选 ",[45,5713,5714],{},"res:\u002F\u002Fscripts\u002Fautoload\u002Fgame_manager.gd",[202,5716,5717,5720,5721,5723],{},[20,5718,5719],{},"节点名称","：自动填好 ",[45,5722,556],{},"（也可以手动改）",[202,5725,5726],{},"点击「添加」",[202,5728,5729],{},"确认右侧「启用」是勾选状态",[10,5731,5732,5733,5736],{},"注册之后，任何节点的脚本里都能直接写 ",[45,5734,5735],{},"GameManager.xxx"," 来访问它，比如：",[38,5738,5740],{"className":40,"code":5739,"language":42,"meta":43,"style":43},"GameManager.add_score(1)\nGameManager.current_state\n",[45,5741,5742,5756],{"__ignoreMap":43},[48,5743,5744,5746,5748,5750,5752,5754],{"class":50,"line":51},[48,5745,556],{"class":58},[48,5747,116],{"class":84},[48,5749,1144],{"class":76},[48,5751,81],{"class":80},[48,5753,522],{"class":758},[48,5755,337],{"class":80},[48,5757,5758,5760,5762],{"class":50,"line":62},[48,5759,556],{"class":58},[48,5761,116],{"class":80},[48,5763,5764],{"class":112},"current_state\n",[10,5766,5767,5768,5771,5772,5771,5775,5778],{},"无需 ",[45,5769,5770],{},"preload"," \u002F ",[45,5773,5774],{},"load",[45,5776,5777],{},"get_node","，超方便。",[25,5780,4515],{"id":4514},[396,5782,5784],{"id":5783},"enum-gamestate",[45,5785,5786],{},"enum GameState",[38,5788,5790],{"className":40,"code":5789,"language":42,"meta":43,"style":43},"enum GameState { READY, PLAYING, GAME_OVER }\n",[45,5791,5792],{"__ignoreMap":43},[48,5793,5794,5796,5798,5800,5802,5804,5806,5808,5810],{"class":50,"line":51},[48,5795,999],{"class":54},[48,5797,1002],{"class":58},[48,5799,1005],{"class":80},[48,5801,1008],{"class":112},[48,5803,1011],{"class":84},[48,5805,151],{"class":112},[48,5807,1011],{"class":84},[48,5809,1018],{"class":112},[48,5811,1021],{"class":80},[10,5813,5814,5816,5817,5820,5821,5824,5825,5824,5828,5831],{},[45,5815,999],{},"（枚举）就是给一组\"状态\"取个",[20,5818,5819],{},"好记的名字","。本质上 ",[45,5822,5823],{},"READY=0","、",[45,5826,5827],{},"PLAYING=1",[45,5829,5830],{},"GAME_OVER=2","，但写代码时用名字比写数字清楚得多：",[38,5833,5835],{"className":40,"code":5834,"language":42,"meta":43,"style":43},"# ❌ 不直观\nif current_state == 0:\n# ✅ 一眼明白\nif current_state == GameManager.GameState.READY:\n",[45,5836,5837,5842,5854,5859],{"__ignoreMap":43},[48,5838,5839],{"class":50,"line":51},[48,5840,5841],{"class":170},"# ❌ 不直观\n",[48,5843,5844,5846,5848,5850,5852],{"class":50,"line":62},[48,5845,1574],{"class":54},[48,5847,1029],{"class":112},[48,5849,122],{"class":72},[48,5851,3628],{"class":758},[48,5853,103],{"class":84},[48,5855,5856],{"class":50,"line":69},[48,5857,5858],{"class":170},"# ✅ 一眼明白\n",[48,5860,5861,5863,5865,5867,5869,5871,5873,5875,5877],{"class":50,"line":106},[48,5862,1574],{"class":54},[48,5864,1029],{"class":112},[48,5866,122],{"class":72},[48,5868,132],{"class":58},[48,5870,116],{"class":80},[48,5872,146],{"class":112},[48,5874,116],{"class":80},[48,5876,5301],{"class":54},[48,5878,103],{"class":84},[396,5880,5882,5884],{"id":5881},"signal信号",[45,5883,342],{},"（信号）",[38,5886,5888],{"className":40,"code":5887,"language":42,"meta":43,"style":43},"signal score_changed(new_score)\nsignal state_changed(new_state)\n",[45,5889,5890,5902],{"__ignoreMap":43},[48,5891,5892,5894,5896,5898,5900],{"class":50,"line":51},[48,5893,342],{"class":72},[48,5895,1080],{"class":76},[48,5897,81],{"class":80},[48,5899,1085],{"class":84},[48,5901,337],{"class":80},[48,5903,5904,5906,5908,5910,5912],{"class":50,"line":62},[48,5905,342],{"class":72},[48,5907,1094],{"class":76},[48,5909,81],{"class":80},[48,5911,1099],{"class":84},[48,5913,337],{"class":80},[10,5915,5916,5917,5920],{},"信号是 Godot 的「事件通知机制」。GameManager 不需要知道",[20,5918,5919],{},"谁","在听 — 它只管\"广播消息\"：",[199,5922,5923,5930],{},[202,5924,5925,5926,5929],{},"分数变了 → 喊一声 ",[45,5927,5928],{},"score_changed","，UI 接到了就更新分数显示",[202,5931,5932,5933,5936],{},"状态变了 → 喊一声 ",[45,5934,5935],{},"state_changed","，所有关心状态的节点都能响应",[10,5938,5939,5942],{},[20,5940,5941],{},"这样 GameManager 不用知道 UI 长啥样，UI 也不用主动来问 GameManager","。两边解耦，互不干扰 — 信号是 Godot 里非常核心的设计思想。",[396,5944,5946,3978,5948],{"id":5945},"add_score-和-score_changedemit",[45,5947,1197],{},[45,5949,5950],{},"score_changed.emit()",[38,5952,5954],{"className":40,"code":5953,"language":42,"meta":43,"style":43},"func add_score(amount: int = 1):\n    total_score += amount\n    score_changed.emit(total_score)\n",[45,5955,5956,5976,5984],{"__ignoreMap":43},[48,5957,5958,5960,5962,5964,5966,5968,5970,5972,5974],{"class":50,"line":51},[48,5959,73],{"class":72},[48,5961,5515],{"class":76},[48,5963,81],{"class":80},[48,5965,377],{"class":84},[48,5967,88],{"class":80},[48,5969,382],{"class":58},[48,5971,777],{"class":80},[48,5973,5528],{"class":758},[48,5975,5531],{"class":80},[48,5977,5978,5980,5982],{"class":50,"line":62},[48,5979,5479],{"class":112},[48,5981,2311],{"class":72},[48,5983,5540],{"class":112},[48,5985,5986,5988,5990,5992,5994,5996],{"class":50,"line":69},[48,5987,5488],{"class":112},[48,5989,116],{"class":84},[48,5991,708],{"class":76},[48,5993,81],{"class":80},[48,5995,5497],{"class":112},[48,5997,337],{"class":80},[199,5999,6000,6006],{},[202,6001,6002,6005],{},[45,6003,6004],{},"amount: int = 1"," — 参数默认值是 1，不传就 +1，传 2 就 +2",[202,6007,6008,6011,6012,6015],{},[45,6009,6010],{},"score_changed.emit(total_score)"," — ",[20,6013,6014],{},"发出信号","，把最新分数传出去",[396,6017,6019,3978,6022],{"id":6018},"set_state-和-game_over",[45,6020,6021],{},"set_state()",[45,6023,6024],{},"game_over()",[10,6026,6027,6030,6031,6034,6035,6038],{},[45,6028,6029],{},"set_state"," 是修改状态的",[20,6032,6033],{},"唯一入口","，每次修改都自动 emit 信号。这样",[20,6036,6037],{},"任何状态改变都会被广播","，不会漏。",[10,6040,6041,6043],{},[45,6042,182],{}," 不仅切换状态，还顺便更新最高分。",[25,6045,6047],{"id":6046},"把-gamemanager-接进-killzone-和-障碍物","把 GameManager 接进 Killzone 和 障碍物",[10,6049,6050,6051,6054],{},"GameManager 创建好了，现在回到 ",[32,6052,6053],{"href":5267},"上一章"," 里我们留的\"后续优化预览\"代码，把它们正式接入：",[396,6056,6058,6059,620],{"id":6057},"死区脚本killzonegd","死区脚本（",[45,6060,619],{},[38,6062,6064],{"className":40,"code":6063,"language":42,"meta":43,"style":43},"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",[45,6065,6066,6072,6076,6098,6134],{"__ignoreMap":43},[48,6067,6068,6070],{"class":50,"line":51},[48,6069,55],{"class":54},[48,6071,59],{"class":58},[48,6073,6074],{"class":50,"line":62},[48,6075,66],{"emptyLinePlaceholder":65},[48,6077,6078,6080,6082,6084,6086,6088,6090,6092,6094,6096],{"class":50,"line":69},[48,6079,73],{"class":72},[48,6081,77],{"class":76},[48,6083,81],{"class":80},[48,6085,85],{"class":84},[48,6087,88],{"class":80},[48,6089,91],{"class":58},[48,6091,94],{"class":80},[48,6093,97],{"class":72},[48,6095,100],{"class":58},[48,6097,103],{"class":80},[48,6099,6100,6102,6104,6106,6108,6110,6112,6114,6116,6118,6120,6122,6124,6126,6128,6130,6132],{"class":50,"line":106},[48,6101,109],{"class":54},[48,6103,113],{"class":112},[48,6105,116],{"class":80},[48,6107,119],{"class":112},[48,6109,122],{"class":72},[48,6111,126],{"class":125},[48,6113,129],{"class":72},[48,6115,132],{"class":58},[48,6117,116],{"class":80},[48,6119,137],{"class":112},[48,6121,122],{"class":72},[48,6123,132],{"class":58},[48,6125,116],{"class":80},[48,6127,146],{"class":112},[48,6129,116],{"class":80},[48,6131,151],{"class":54},[48,6133,103],{"class":84},[48,6135,6136,6138,6140,6142],{"class":50,"line":156},[48,6137,177],{"class":58},[48,6139,116],{"class":84},[48,6141,182],{"class":76},[48,6143,711],{"class":80},[10,6145,6146,6147,6150],{},"加上 ",[45,6148,6149],{},"current_state == PLAYING"," 判断 — 防止 READY 状态（还没开始）和 GAME_OVER 状态（已经结束）时误触发。",[396,6152,6154,6155,620],{"id":6153},"障碍物脚本pillar_pairgd","障碍物脚本（",[45,6156,725],{},[38,6158,6160],{"className":40,"code":6159,"language":42,"meta":43,"style":43},"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",[45,6161,6162,6168,6172,6184,6202,6206,6228,6233,6257,6273,6291,6297,6301,6323,6339,6357,6371],{"__ignoreMap":43},[48,6163,6164,6166],{"class":50,"line":51},[48,6165,55],{"class":54},[48,6167,737],{"class":58},[48,6169,6170],{"class":50,"line":62},[48,6171,66],{"emptyLinePlaceholder":65},[48,6173,6174,6176,6178,6180,6182],{"class":50,"line":69},[48,6175,746],{"class":76},[48,6177,749],{"class":72},[48,6179,752],{"class":112},[48,6181,755],{"class":80},[48,6183,759],{"class":758},[48,6185,6186,6188,6190,6192,6194,6196,6198,6200],{"class":50,"line":106},[48,6187,764],{"class":76},[48,6189,749],{"class":72},[48,6191,769],{"class":112},[48,6193,88],{"class":80},[48,6195,774],{"class":58},[48,6197,777],{"class":80},[48,6199,780],{"class":54},[48,6201,784],{"class":783},[48,6203,6204],{"class":50,"line":156},[48,6205,66],{"emptyLinePlaceholder":65},[48,6207,6208,6210,6212,6214,6216,6218,6220,6222,6224,6226],{"class":50,"line":174},[48,6209,73],{"class":72},[48,6211,795],{"class":76},[48,6213,81],{"class":80},[48,6215,800],{"class":84},[48,6217,88],{"class":80},[48,6219,805],{"class":58},[48,6221,94],{"class":80},[48,6223,97],{"class":72},[48,6225,100],{"class":58},[48,6227,103],{"class":80},[48,6229,6230],{"class":50,"line":351},[48,6231,6232],{"class":170},"    # 只在游戏进行中才移动\n",[48,6234,6235,6237,6239,6241,6243,6245,6247,6249,6251,6253,6255],{"class":50,"line":356},[48,6236,109],{"class":54},[48,6238,132],{"class":58},[48,6240,116],{"class":80},[48,6242,137],{"class":112},[48,6244,122],{"class":72},[48,6246,132],{"class":58},[48,6248,116],{"class":80},[48,6250,146],{"class":112},[48,6252,116],{"class":80},[48,6254,151],{"class":54},[48,6256,103],{"class":84},[48,6258,6259,6261,6263,6265,6267,6269,6271],{"class":50,"line":367},[48,6260,842],{"class":112},[48,6262,116],{"class":80},[48,6264,847],{"class":112},[48,6266,850],{"class":72},[48,6268,752],{"class":112},[48,6270,855],{"class":72},[48,6272,858],{"class":112},[48,6274,6275,6277,6279,6281,6283,6285,6287,6289],{"class":50,"line":884},[48,6276,863],{"class":54},[48,6278,866],{"class":112},[48,6280,116],{"class":80},[48,6282,847],{"class":112},[48,6284,873],{"class":72},[48,6286,876],{"class":72},[48,6288,879],{"class":758},[48,6290,103],{"class":84},[48,6292,6293,6295],{"class":50,"line":892},[48,6294,887],{"class":76},[48,6296,711],{"class":80},[48,6298,6299],{"class":50,"line":897},[48,6300,66],{"emptyLinePlaceholder":65},[48,6302,6303,6305,6307,6309,6311,6313,6315,6317,6319,6321],{"class":50,"line":903},[48,6304,73],{"class":72},[48,6306,908],{"class":76},[48,6308,81],{"class":80},[48,6310,85],{"class":84},[48,6312,88],{"class":80},[48,6314,91],{"class":58},[48,6316,94],{"class":80},[48,6318,97],{"class":72},[48,6320,100],{"class":58},[48,6322,103],{"class":80},[48,6324,6325,6327,6329,6331,6333,6335,6337],{"class":50,"line":927},[48,6326,109],{"class":54},[48,6328,113],{"class":112},[48,6330,116],{"class":80},[48,6332,119],{"class":112},[48,6334,122],{"class":72},[48,6336,126],{"class":125},[48,6338,103],{"class":84},[48,6340,6341,6343,6345,6347,6349,6351,6353,6355],{"class":50,"line":944},[48,6342,863],{"class":54},[48,6344,132],{"class":58},[48,6346,116],{"class":84},[48,6348,4789],{"class":76},[48,6350,81],{"class":80},[48,6352,4794],{"class":125},[48,6354,94],{"class":80},[48,6356,103],{"class":84},[48,6358,6359,6361,6363,6365,6367,6369],{"class":50,"line":1421},[48,6360,4803],{"class":58},[48,6362,116],{"class":84},[48,6364,1144],{"class":76},[48,6366,81],{"class":80},[48,6368,522],{"class":758},[48,6370,337],{"class":80},[48,6372,6373,6375,6377,6379,6381,6383,6385,6387],{"class":50,"line":1431},[48,6374,4488],{"class":112},[48,6376,116],{"class":84},[48,6378,4493],{"class":76},[48,6380,81],{"class":80},[48,6382,4498],{"class":125},[48,6384,3717],{"class":80},[48,6386,4503],{"class":54},[48,6388,337],{"class":80},[396,6390,6392,6393,620],{"id":6391},"生成器脚本pillar_spawnergd","生成器脚本（",[45,6394,6395],{},"pillar_spawner.gd",[38,6397,6399],{"className":40,"code":6398,"language":42,"meta":43,"style":43},"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",[45,6400,6401,6407,6411,6423,6435,6439,6453,6459,6463,6477,6482,6506,6523,6533,6558,6571],{"__ignoreMap":43},[48,6402,6403,6405],{"class":50,"line":51},[48,6404,55],{"class":54},[48,6406,737],{"class":58},[48,6408,6409],{"class":50,"line":62},[48,6410,66],{"emptyLinePlaceholder":65},[48,6412,6413,6415,6417,6419,6421],{"class":50,"line":69},[48,6414,746],{"class":76},[48,6416,749],{"class":72},[48,6418,4966],{"class":112},[48,6420,88],{"class":80},[48,6422,4971],{"class":58},[48,6424,6425,6427,6429,6431,6433],{"class":50,"line":106},[48,6426,746],{"class":76},[48,6428,749],{"class":72},[48,6430,4980],{"class":112},[48,6432,755],{"class":80},[48,6434,4985],{"class":758},[48,6436,6437],{"class":50,"line":156},[48,6438,66],{"emptyLinePlaceholder":65},[48,6440,6441,6443,6445,6447,6449,6451],{"class":50,"line":174},[48,6442,73],{"class":72},[48,6444,4996],{"class":76},[48,6446,167],{"class":80},[48,6448,97],{"class":72},[48,6450,100],{"class":58},[48,6452,103],{"class":80},[48,6454,6455,6457],{"class":50,"line":351},[48,6456,5009],{"class":76},[48,6458,711],{"class":80},[48,6460,6461],{"class":50,"line":356},[48,6462,66],{"emptyLinePlaceholder":65},[48,6464,6465,6467,6469,6471,6473,6475],{"class":50,"line":367},[48,6466,73],{"class":72},[48,6468,5022],{"class":76},[48,6470,167],{"class":80},[48,6472,97],{"class":72},[48,6474,100],{"class":58},[48,6476,103],{"class":80},[48,6478,6479],{"class":50,"line":884},[48,6480,6481],{"class":170},"    # 只在游戏进行中才生成\n",[48,6483,6484,6486,6488,6490,6492,6494,6496,6498,6500,6502,6504],{"class":50,"line":892},[48,6485,109],{"class":54},[48,6487,132],{"class":58},[48,6489,116],{"class":80},[48,6491,137],{"class":112},[48,6493,122],{"class":72},[48,6495,132],{"class":58},[48,6497,116],{"class":80},[48,6499,146],{"class":112},[48,6501,116],{"class":80},[48,6503,151],{"class":54},[48,6505,103],{"class":84},[48,6507,6508,6511,6513,6515,6517,6519,6521],{"class":50,"line":897},[48,6509,6510],{"class":72},"        var",[48,6512,5037],{"class":112},[48,6514,777],{"class":80},[48,6516,4966],{"class":112},[48,6518,116],{"class":84},[48,6520,5046],{"class":76},[48,6522,711],{"class":80},[48,6524,6525,6527,6529,6531],{"class":50,"line":903},[48,6526,6510],{"class":72},[48,6528,5059],{"class":112},[48,6530,777],{"class":80},[48,6532,5064],{"class":112},[48,6534,6535,6538,6540,6542,6544,6546,6548,6550,6552,6554,6556],{"class":50,"line":927},[48,6536,6537],{"class":112},"        spawn_pos",[48,6539,116],{"class":80},[48,6541,1621],{"class":112},[48,6543,2311],{"class":72},[48,6545,5078],{"class":76},[48,6547,81],{"class":80},[48,6549,3652],{"class":72},[48,6551,5085],{"class":112},[48,6553,3717],{"class":80},[48,6555,4980],{"class":112},[48,6557,337],{"class":80},[48,6559,6560,6563,6565,6567,6569],{"class":50,"line":944},[48,6561,6562],{"class":112},"        new_pillar",[48,6564,116],{"class":80},[48,6566,5105],{"class":112},[48,6568,777],{"class":80},[48,6570,5110],{"class":112},[48,6572,6573,6576,6578,6580,6582,6584,6586,6588,6590],{"class":50,"line":1421},[48,6574,6575],{"class":76},"        get_tree",[48,6577,167],{"class":80},[48,6579,116],{"class":84},[48,6581,5131],{"class":112},[48,6583,116],{"class":84},[48,6585,5136],{"class":76},[48,6587,81],{"class":80},[48,6589,5141],{"class":112},[48,6591,337],{"class":80},[25,6593,3259],{"id":3259},[10,6595,6596,6597,194],{},"现在还没有 UI，所以视觉上看不出区别 — ",[20,6598,6599],{},"但状态系统已经在跑了",[10,6601,6602,6603,6605],{},"你可以在 ",[45,6604,1563],{}," 里临时加个调试：",[38,6607,6609],{"className":40,"code":6608,"language":42,"meta":43,"style":43},"func _physics_process(delta: float) -> void:\n    if Input.is_action_just_pressed(\"fly\"):\n        print(\"当前状态:\", GameManager.current_state)\n        # ...\n",[45,6610,6611,6633,6651,6670],{"__ignoreMap":43},[48,6612,6613,6615,6617,6619,6621,6623,6625,6627,6629,6631],{"class":50,"line":51},[48,6614,73],{"class":72},[48,6616,795],{"class":76},[48,6618,81],{"class":80},[48,6620,800],{"class":84},[48,6622,88],{"class":80},[48,6624,805],{"class":58},[48,6626,94],{"class":80},[48,6628,97],{"class":72},[48,6630,100],{"class":58},[48,6632,103],{"class":80},[48,6634,6635,6637,6639,6641,6643,6645,6647,6649],{"class":50,"line":62},[48,6636,109],{"class":54},[48,6638,1577],{"class":58},[48,6640,116],{"class":84},[48,6642,1582],{"class":76},[48,6644,81],{"class":80},[48,6646,1587],{"class":125},[48,6648,94],{"class":80},[48,6650,103],{"class":84},[48,6652,6653,6655,6657,6660,6662,6664,6666,6668],{"class":50,"line":69},[48,6654,4090],{"class":76},[48,6656,81],{"class":80},[48,6658,6659],{"class":125},"\"当前状态:\"",[48,6661,3717],{"class":80},[48,6663,132],{"class":58},[48,6665,116],{"class":80},[48,6667,137],{"class":112},[48,6669,337],{"class":80},[48,6671,6672],{"class":50,"line":106},[48,6673,6674],{"class":170},"        # ...\n",[10,6676,6677,6678,6680,6681,6683],{},"不过这时候默认是 ",[45,6679,5301],{}," 状态，水管不会生成、撞死也不会触发 — 因为我们还没有\"开始游戏\"的入口。下一章做 UI 时会加上一个开始按钮，把状态切到 ",[45,6682,151],{},"，整个流程才会真正跑起来。",[1804,6685,6686],{},"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":43,"searchDepth":62,"depth":62,"links":6688},[6689,6690,6691,6694,6703,6711],{"id":5290,"depth":62,"text":5291},{"id":5325,"depth":62,"text":5326},{"id":5663,"depth":62,"text":5664,"children":6692},[6693],{"id":5692,"depth":69,"text":5692},{"id":4514,"depth":62,"text":4515,"children":6695},[6696,6697,6699,6701],{"id":5783,"depth":69,"text":5786},{"id":5881,"depth":69,"text":6698},"signal（信号）",{"id":5945,"depth":69,"text":6700},"add_score() 和 score_changed.emit()",{"id":6018,"depth":69,"text":6702},"set_state() 和 game_over()",{"id":6046,"depth":62,"text":6047,"children":6704},[6705,6707,6709],{"id":6057,"depth":69,"text":6706},"死区脚本（killzone.gd）",{"id":6153,"depth":69,"text":6708},"障碍物脚本（pillar_pair.gd）",{"id":6391,"depth":69,"text":6710},"生成器脚本（pillar_spawner.gd）",{"id":3259,"depth":62,"text":3259},{},{"title":556,"description":1832},"devlog\u002Fxggame-bird\u002F05-game-manager","FNw8591Sth7D2Iaz9Dt3V0d-rXtMLyEjwyH5oxagnGQ",{"id":6717,"title":6718,"body":6719,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":7984,"navigation":65,"path":7985,"seo":7986,"stem":7987,"toc":65,"__hash__":7988},"devlog\u002Fdevlog\u002Fxggame-bird\u002F06-ui.md","UI 界面 + 主场景",{"type":7,"value":6720,"toc":7964},[6721,6724,6756,6759,6764,6772,6778,6785,6787,6814,6829,6835,6838,6851,6899,6910,6916,6929,6932,7030,7036,7042,7411,7413,7487,7494,7546,7555,7558,7567,7572,7575,7583,7684,7697,7703,7706,7715,7721,7888,7906,7918,7921,7927,7930,7936,7938,7941,7958,7961],[10,6722,6723],{},"GameManager 把状态和分数管理好了，这一章让玩家\"看得见\"。我们要做：",[3034,6725,6726,6735,6744,6750],{},[202,6727,6728,1527,6731,6734],{},[20,6729,6730],{},"开始菜单",[45,6732,6733],{},"menu.tscn","）— 一个开始按钮",[202,6736,6737,1527,6740,6743],{},[20,6738,6739],{},"游戏主场景",[45,6741,6742],{},"main.tscn","）— 整合小鸟、水管、UI",[202,6745,6746,6749],{},[20,6747,6748],{},"分数 UI"," — 实时显示当前分数",[202,6751,6752,6755],{},[20,6753,6754],{},"结束画面"," — 显示最终分数 + 重新开始按钮",[25,6757,6758],{"id":6758},"整体流程",[14,6760,6761],{},[10,6762,6763],{},"整理一下我们要搭的\"两个场景\"的关系：",[38,6765,6770],{"className":6766,"code":6768,"language":6769},[6767],"language-text","menu.tscn（开始菜单）\n   ↓ 点击「开始」按钮\nmain.tscn（游戏主场景）\n   ├─ READY 状态：等待玩家按下 fly\n   ├─ PLAYING 状态：正常游戏\n   └─ GAME_OVER 状态：显示分数 + 重新开始按钮\n","text",[45,6771,6768],{"__ignoreMap":43},[25,6773,6775,6776],{"id":6774},"开始菜单-menutscn","开始菜单 ",[45,6777,6733],{},[10,6779,3091,6780,3095,6782,194],{},[45,6781,3094],{},[45,6783,6784],{},"scenes\u002Fmenu.tscn",[10,6786,3106],{},[199,6788,6789],{},[202,6790,6791,6793,6794],{},[45,6792,3094],{}," — 菜单根节点\n",[199,6795,6796],{},[202,6797,6798,6801,6802],{},[45,6799,6800],{},"CanvasLayer"," — UI 图层（不随摄像机移动）\n",[199,6803,6804],{},[202,6805,6806,6809,6810,6813],{},[45,6807,6808],{},"Button","（重命名 ",[45,6811,6812],{},"StartButton","）— 开始按钮",[14,6815,6816],{},[10,6817,6818,6819,6821,6822,6824,6825,6828],{},"为什么 UI 要放在 ",[45,6820,6800],{}," 下？因为 ",[45,6823,6800],{}," 的内容",[20,6826,6827],{},"不会被摄像机变换影响","，永远固定在屏幕上。",[10,6830,6831],{},[1867,6832],{"alt":6833,"src":6834},"06-ui-menu-场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F06-ui-menu-%E5%9C%BA%E6%99%AF.png",[396,6836,6837],{"id":6837},"菜单脚本",[10,6839,6840,6841,6844,6845,2104,6847,6850],{},"选中根节点挂脚本 ",[45,6842,6843],{},"scripts\u002Fmenu.gd","，然后在【节点】面板把 ",[45,6846,6812],{},[45,6848,6849],{},"pressed"," 信号连接到根节点：",[38,6852,6854],{"className":40,"code":6853,"language":42,"meta":43,"style":43},"extends Node2D\n\nfunc _on_start_button_pressed() -> void:\n    get_tree().change_scene_to_file(\"res:\u002F\u002Fscenes\u002Fmain.tscn\")\n",[45,6855,6856,6862,6866,6881],{"__ignoreMap":43},[48,6857,6858,6860],{"class":50,"line":51},[48,6859,55],{"class":54},[48,6861,737],{"class":58},[48,6863,6864],{"class":50,"line":62},[48,6865,66],{"emptyLinePlaceholder":65},[48,6867,6868,6870,6873,6875,6877,6879],{"class":50,"line":69},[48,6869,73],{"class":72},[48,6871,6872],{"class":76}," _on_start_button_pressed",[48,6874,167],{"class":80},[48,6876,97],{"class":72},[48,6878,100],{"class":58},[48,6880,103],{"class":80},[48,6882,6883,6885,6887,6889,6892,6894,6897],{"class":50,"line":106},[48,6884,5124],{"class":76},[48,6886,167],{"class":80},[48,6888,116],{"class":84},[48,6890,6891],{"class":76},"change_scene_to_file",[48,6893,81],{"class":80},[48,6895,6896],{"class":125},"\"res:\u002F\u002Fscenes\u002Fmain.tscn\"",[48,6898,337],{"class":80},[199,6900,6901],{},[202,6902,6903,6906,6907,6909],{},[45,6904,6905],{},"change_scene_to_file()"," — 切换到指定场景，路径填你保存 ",[45,6908,6742],{}," 的位置",[25,6911,6913,6914],{"id":6912},"游戏主场景-maintscn","游戏主场景 ",[45,6915,6742],{},[10,6917,6918,6919,6922,6923,6925,6926,6928],{},"现在把之前章节做的所有东西",[20,6920,6921],{},"整合到一起","。如果之前你已经做了 ",[45,6924,3156],{},"，可以直接改名为 ",[45,6927,6742],{},"，或者新建一个。",[10,6930,6931],{},"使用到的节点（核心结构）：",[199,6933,6934],{},[202,6935,6936,6938,6939,3830,6942],{},[45,6937,3094],{}," — 主场景根节点（挂 ",[45,6940,6941],{},"main.gd",[199,6943,6944,6949,6962,6970,6977,6982],{},[202,6945,6946,6948],{},[45,6947,3120],{}," — 摄像机",[202,6950,6951,6809,6954,6957,6958,620],{},[45,6952,6953],{},"Parallax2D",[45,6955,6956],{},"BG","）— 滚动背景（",[32,6959,6961],{"href":6960},"\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets","第 7 章会做",[202,6963,6964,6809,6966,6969],{},[45,6965,6953],{},[45,6967,6968],{},"Progress","）— 滚动地面",[202,6971,6972,6974,6975,620],{},[45,6973,1879],{}," — 小鸟（之前做的 ",[45,6976,3141],{},[202,6978,6979,6981],{},[45,6980,4877],{}," — 障碍物生成器",[202,6983,6984,6986,6987],{},[45,6985,6800],{}," — UI 层\n",[199,6988,6989,6998],{},[202,6990,6991,6809,6994,6997],{},[45,6992,6993],{},"Label",[45,6995,6996],{},"ScoreLabel","）— 分数显示",[202,6999,7000,6809,7003,7006,7007],{},[45,7001,7002],{},"Panel",[45,7004,7005],{},"GameOverPanel","）— 结束面板\n",[199,7008,7009,7014,7022],{},[202,7010,7011,7013],{},[45,7012,6993],{}," — \"Game Over\"",[202,7015,7016,1527,7018,7021],{},[45,7017,6993],{},[45,7019,7020],{},"FinalScore","）— 最终分数",[202,7023,7024,1527,7026,7029],{},[45,7025,6808],{},[45,7027,7028],{},"RestartButton","）— 重新开始",[10,7031,7032],{},[1867,7033],{"alt":7034,"src":7035},"06-ui-main-场景","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F06-ui-main-%E5%9C%BA%E6%99%AF.png",[25,7037,7039,7040],{"id":7038},"主场景脚本-maingd","主场景脚本 ",[45,7041,6941],{},[38,7043,7045],{"className":40,"code":7044,"language":42,"meta":43,"style":43},"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",[45,7046,7047,7053,7057,7071,7076,7099,7121,7138,7152,7156,7181,7186,7210,7230,7237,7241,7250,7272,7287,7291,7296,7311,7324,7328,7347,7352,7373,7393],{"__ignoreMap":43},[48,7048,7049,7051],{"class":50,"line":51},[48,7050,55],{"class":54},[48,7052,737],{"class":58},[48,7054,7055],{"class":50,"line":62},[48,7056,66],{"emptyLinePlaceholder":65},[48,7058,7059,7061,7063,7065,7067,7069],{"class":50,"line":69},[48,7060,73],{"class":72},[48,7062,1112],{"class":76},[48,7064,167],{"class":80},[48,7066,97],{"class":72},[48,7068,100],{"class":58},[48,7070,103],{"class":80},[48,7072,7073],{"class":50,"line":106},[48,7074,7075],{"class":170},"    # 刚加载场景，设置为准备状态\n",[48,7077,7078,7081,7083,7085,7087,7089,7091,7093,7095,7097],{"class":50,"line":156},[48,7079,7080],{"class":58},"    GameManager",[48,7082,116],{"class":84},[48,7084,6029],{"class":76},[48,7086,81],{"class":80},[48,7088,556],{"class":58},[48,7090,116],{"class":80},[48,7092,146],{"class":112},[48,7094,116],{"class":80},[48,7096,5301],{"class":54},[48,7098,337],{"class":80},[48,7100,7101,7103,7105,7107,7109,7111,7113,7116,7118],{"class":50,"line":174},[48,7102,7080],{"class":58},[48,7104,116],{"class":80},[48,7106,5935],{"class":112},[48,7108,116],{"class":84},[48,7110,1139],{"class":76},[48,7112,81],{"class":80},[48,7114,7115],{"class":112},"_on_game_state",[48,7117,94],{"class":80},[48,7119,7120],{"class":170},"  # 监听信号\n",[48,7122,7123,7126,7128,7131,7133,7135],{"class":50,"line":351},[48,7124,7125],{"class":58},"    Engine",[48,7127,116],{"class":80},[48,7129,7130],{"class":112},"time_scale",[48,7132,777],{"class":80},[48,7134,3628],{"class":758},[48,7136,7137],{"class":170},"  # 冻结游戏\n",[48,7139,7140,7142,7144,7147,7149],{"class":50,"line":356},[48,7141,7080],{"class":58},[48,7143,116],{"class":84},[48,7145,7146],{"class":76},"clear_score",[48,7148,167],{"class":80},[48,7150,7151],{"class":170},"  # 游戏开始前清空分数\n",[48,7153,7154],{"class":50,"line":367},[48,7155,66],{"emptyLinePlaceholder":65},[48,7157,7158,7160,7163,7165,7168,7170,7173,7175,7177,7179],{"class":50,"line":884},[48,7159,73],{"class":72},[48,7161,7162],{"class":76}," _input",[48,7164,81],{"class":80},[48,7166,7167],{"class":84},"event",[48,7169,88],{"class":80},[48,7171,7172],{"class":58}," InputEvent",[48,7174,94],{"class":80},[48,7176,97],{"class":72},[48,7178,100],{"class":58},[48,7180,103],{"class":80},[48,7182,7183],{"class":50,"line":892},[48,7184,7185],{"class":170},"    # 准备状态下，按一下 fly 就开始游戏\n",[48,7187,7188,7190,7192,7194,7196,7198,7200,7202,7204,7206,7208],{"class":50,"line":897},[48,7189,109],{"class":54},[48,7191,132],{"class":58},[48,7193,116],{"class":80},[48,7195,137],{"class":112},[48,7197,122],{"class":72},[48,7199,132],{"class":58},[48,7201,116],{"class":80},[48,7203,146],{"class":112},[48,7205,116],{"class":80},[48,7207,5301],{"class":54},[48,7209,103],{"class":84},[48,7211,7212,7214,7217,7219,7222,7224,7226,7228],{"class":50,"line":903},[48,7213,863],{"class":54},[48,7215,7216],{"class":112}," event",[48,7218,116],{"class":84},[48,7220,7221],{"class":76},"is_action_pressed",[48,7223,81],{"class":80},[48,7225,1587],{"class":125},[48,7227,94],{"class":80},[48,7229,103],{"class":84},[48,7231,7232,7235],{"class":50,"line":927},[48,7233,7234],{"class":76},"            start_game",[48,7236,711],{"class":80},[48,7238,7239],{"class":50,"line":944},[48,7240,66],{"emptyLinePlaceholder":65},[48,7242,7243,7245,7248],{"class":50,"line":1421},[48,7244,73],{"class":72},[48,7246,7247],{"class":76}," start_game",[48,7249,1402],{"class":80},[48,7251,7252,7254,7256,7258,7260,7262,7264,7266,7268,7270],{"class":50,"line":1431},[48,7253,7080],{"class":58},[48,7255,116],{"class":84},[48,7257,6029],{"class":76},[48,7259,81],{"class":80},[48,7261,556],{"class":58},[48,7263,116],{"class":80},[48,7265,146],{"class":112},[48,7267,116],{"class":80},[48,7269,151],{"class":54},[48,7271,337],{"class":80},[48,7273,7274,7276,7278,7280,7282,7284],{"class":50,"line":1443},[48,7275,7125],{"class":58},[48,7277,116],{"class":80},[48,7279,7130],{"class":112},[48,7281,777],{"class":80},[48,7283,5528],{"class":758},[48,7285,7286],{"class":170},"  # 解冻游戏\n",[48,7288,7289],{"class":50,"line":1448},[48,7290,66],{"emptyLinePlaceholder":65},[48,7292,7293],{"class":50,"line":1458},[48,7294,7295],{"class":170},"## 重新开始游戏\n",[48,7297,7298,7300,7303,7305,7307,7309],{"class":50,"line":3537},[48,7299,73],{"class":72},[48,7301,7302],{"class":76}," _on_restart_button_pressed",[48,7304,167],{"class":80},[48,7306,97],{"class":72},[48,7308,100],{"class":58},[48,7310,103],{"class":80},[48,7312,7313,7315,7317,7319,7322],{"class":50,"line":3542},[48,7314,5124],{"class":76},[48,7316,167],{"class":80},[48,7318,116],{"class":84},[48,7320,7321],{"class":76},"reload_current_scene",[48,7323,711],{"class":80},[48,7325,7326],{"class":50,"line":3548},[48,7327,66],{"emptyLinePlaceholder":65},[48,7329,7330,7332,7335,7337,7339,7341,7343,7345],{"class":50,"line":3560},[48,7331,73],{"class":72},[48,7333,7334],{"class":76}," _on_game_state",[48,7336,81],{"class":80},[48,7338,1099],{"class":84},[48,7340,94],{"class":80},[48,7342,97],{"class":72},[48,7344,100],{"class":58},[48,7346,103],{"class":80},[48,7348,7349],{"class":50,"line":3567},[48,7350,7351],{"class":170},"    # 游戏结束时，让背景和地面停止滚动\n",[48,7353,7354,7356,7359,7361,7363,7365,7367,7369,7371],{"class":50,"line":3572},[48,7355,109],{"class":54},[48,7357,7358],{"class":112}," new_state",[48,7360,122],{"class":72},[48,7362,132],{"class":58},[48,7364,116],{"class":80},[48,7366,146],{"class":112},[48,7368,116],{"class":80},[48,7370,1018],{"class":54},[48,7372,103],{"class":84},[48,7374,7375,7378,7380,7382,7385,7387,7389,7391],{"class":50,"line":3596},[48,7376,7377],{"class":54},"        $",[48,7379,6956],{"class":783},[48,7381,116],{"class":80},[48,7383,7384],{"class":112},"autoscroll",[48,7386,116],{"class":80},[48,7388,847],{"class":112},[48,7390,777],{"class":80},[48,7392,1058],{"class":758},[48,7394,7395,7397,7399,7401,7403,7405,7407,7409],{"class":50,"line":3610},[48,7396,7377],{"class":54},[48,7398,6968],{"class":783},[48,7400,116],{"class":80},[48,7402,7384],{"class":112},[48,7404,116],{"class":80},[48,7406,847],{"class":112},[48,7408,777],{"class":80},[48,7410,1058],{"class":758},[396,7412,4515],{"id":4514},[496,7414,7415,7424],{},[499,7416,7417],{},[502,7418,7419,7422],{},[505,7420,7421],{},"函数",[505,7423,1993],{},[515,7425,7426,7439,7452,7464,7474],{},[502,7427,7428,7432],{},[520,7429,7430],{},[45,7431,561],{},[520,7433,7434,7435,7438],{},"场景一加载就执行：把状态设为 READY、监听状态变化、",[20,7436,7437],{},"冻结整个游戏","、清空分数",[502,7440,7441,7446],{},[520,7442,7443],{},[45,7444,7445],{},"_input()",[520,7447,7448,7449],{},"监听全局输入：在 READY 状态下，按下 fly 就调用 ",[45,7450,7451],{},"start_game()",[502,7453,7454,7458],{},[520,7455,7456],{},[45,7457,7451],{},[520,7459,7460,7461],{},"把状态切到 PLAYING，",[20,7462,7463],{},"解冻游戏",[502,7465,7466,7471],{},[520,7467,7468],{},[45,7469,7470],{},"_on_restart_button_pressed()",[520,7472,7473],{},"重启按钮：直接重新加载当前场景",[502,7475,7476,7481],{},[520,7477,7478],{},[45,7479,7480],{},"_on_game_state()",[520,7482,7483,7484,7486],{},"监听 GameManager 的 ",[45,7485,5935],{}," 信号，GAME_OVER 时停止背景滚动",[396,7488,7490,7493],{"id":7489},"enginetime_scale-全局时间缩放",[45,7491,7492],{},"Engine.time_scale"," — 全局时间缩放",[38,7495,7497],{"className":40,"code":7496,"language":42,"meta":43,"style":43},"Engine.time_scale = 0  # 完全冻结\nEngine.time_scale = 1  # 正常速度\nEngine.time_scale = 0.5  # 慢动作\n",[45,7498,7499,7515,7530],{"__ignoreMap":43},[48,7500,7501,7504,7506,7508,7510,7512],{"class":50,"line":51},[48,7502,7503],{"class":58},"Engine",[48,7505,116],{"class":80},[48,7507,7130],{"class":112},[48,7509,777],{"class":80},[48,7511,3628],{"class":758},[48,7513,7514],{"class":170},"  # 完全冻结\n",[48,7516,7517,7519,7521,7523,7525,7527],{"class":50,"line":62},[48,7518,7503],{"class":58},[48,7520,116],{"class":80},[48,7522,7130],{"class":112},[48,7524,777],{"class":80},[48,7526,5528],{"class":758},[48,7528,7529],{"class":170},"  # 正常速度\n",[48,7531,7532,7534,7536,7538,7540,7543],{"class":50,"line":69},[48,7533,7503],{"class":58},[48,7535,116],{"class":80},[48,7537,7130],{"class":112},[48,7539,777],{"class":80},[48,7541,7542],{"class":758}," 0.5",[48,7544,7545],{"class":170},"  # 慢动作\n",[10,7547,7548,7549,7551,7552,194],{},"这是 Godot 的\"全局慢放\u002F暂停\"开关，影响所有用 ",[45,7550,800],{}," 计算的逻辑（包括小鸟下落、水管移动、Timer 等）。",[20,7553,7554],{},"比一个一个写\"如果暂停就别动\"省心得多",[396,7556,7557],{"id":7557},"信号的连接",[10,7559,7560,7563,7564,194],{},[45,7561,7562],{},"GameManager.state_changed.connect(_on_game_state)"," — 这就是",[20,7565,7566],{},"订阅信号",[14,7568,7569],{},[10,7570,7571],{},"你也可以在编辑器的【节点】面板里手动连接信号，效果一样。代码连接的好处是更明确、便于版本管理。",[25,7573,6748],{"id":7574},"分数-ui",[10,7576,7577,7579,7580,298],{},[45,7578,6996],{}," 挂个脚本 ",[45,7581,7582],{},"score_label.gd",[38,7584,7586],{"className":40,"code":7585,"language":42,"meta":43,"style":43},"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",[45,7587,7588,7595,7599,7613,7623,7642,7646,7669],{"__ignoreMap":43},[48,7589,7590,7592],{"class":50,"line":51},[48,7591,55],{"class":54},[48,7593,7594],{"class":58}," Label\n",[48,7596,7597],{"class":50,"line":62},[48,7598,66],{"emptyLinePlaceholder":65},[48,7600,7601,7603,7605,7607,7609,7611],{"class":50,"line":69},[48,7602,73],{"class":72},[48,7604,1112],{"class":76},[48,7606,167],{"class":80},[48,7608,97],{"class":72},[48,7610,100],{"class":58},[48,7612,103],{"class":80},[48,7614,7615,7618,7620],{"class":50,"line":106},[48,7616,7617],{"class":112},"    text",[48,7619,777],{"class":80},[48,7621,7622],{"class":125}," \"0\"\n",[48,7624,7625,7627,7629,7631,7633,7635,7637,7640],{"class":50,"line":156},[48,7626,7080],{"class":58},[48,7628,116],{"class":80},[48,7630,5928],{"class":112},[48,7632,116],{"class":84},[48,7634,1139],{"class":76},[48,7636,81],{"class":80},[48,7638,7639],{"class":112},"_on_score_changed",[48,7641,337],{"class":80},[48,7643,7644],{"class":50,"line":174},[48,7645,66],{"emptyLinePlaceholder":65},[48,7647,7648,7650,7653,7655,7657,7659,7661,7663,7665,7667],{"class":50,"line":351},[48,7649,73],{"class":72},[48,7651,7652],{"class":76}," _on_score_changed",[48,7654,81],{"class":80},[48,7656,1085],{"class":84},[48,7658,88],{"class":80},[48,7660,382],{"class":58},[48,7662,94],{"class":80},[48,7664,97],{"class":72},[48,7666,100],{"class":58},[48,7668,103],{"class":80},[48,7670,7671,7673,7675,7678,7680,7682],{"class":50,"line":356},[48,7672,7617],{"class":112},[48,7674,777],{"class":80},[48,7676,7677],{"class":76}," str",[48,7679,81],{"class":80},[48,7681,1085],{"class":112},[48,7683,337],{"class":80},[199,7685,7686,7694],{},[202,7687,7688,7691,7692,1508],{},[45,7689,7690],{},"_ready"," 里订阅 ",[45,7693,5928],{},[202,7695,7696],{},"每次分数变化，自动更新 Label 文本",[10,7698,7699,7702],{},[20,7700,7701],{},"这就是信号解耦的好处"," — Label 不需要主动去问 GameManager 当前几分，GameManager 也不知道有这么个 Label 存在，但分数照样能正确显示。",[25,7704,7705],{"id":7705},"结束面板",[10,7707,7708,7710,7711,7714],{},[45,7709,7005],{}," 默认隐藏（在编辑器把 ",[45,7712,7713],{},"visible"," 取消勾选），监听到 GAME_OVER 状态时才显示。",[10,7716,7717,7718,298],{},"挂脚本 ",[45,7719,7720],{},"game_over_panel.gd",[38,7722,7724],{"className":40,"code":7723,"language":42,"meta":43,"style":43},"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",[45,7725,7726,7733,7737,7758,7762,7776,7786,7805,7809,7828,7848,7878],{"__ignoreMap":43},[48,7727,7728,7730],{"class":50,"line":51},[48,7729,55],{"class":54},[48,7731,7732],{"class":58}," Panel\n",[48,7734,7735],{"class":50,"line":62},[48,7736,66],{"emptyLinePlaceholder":65},[48,7738,7739,7741,7743,7746,7748,7751,7753,7755],{"class":50,"line":69},[48,7740,764],{"class":76},[48,7742,749],{"class":72},[48,7744,7745],{"class":112}," final_score",[48,7747,88],{"class":80},[48,7749,7750],{"class":58}," Label",[48,7752,777],{"class":80},[48,7754,780],{"class":54},[48,7756,7757],{"class":783},"FinalScore\n",[48,7759,7760],{"class":50,"line":106},[48,7761,66],{"emptyLinePlaceholder":65},[48,7763,7764,7766,7768,7770,7772,7774],{"class":50,"line":156},[48,7765,73],{"class":72},[48,7767,1112],{"class":76},[48,7769,167],{"class":80},[48,7771,97],{"class":72},[48,7773,100],{"class":58},[48,7775,103],{"class":80},[48,7777,7778,7781,7783],{"class":50,"line":174},[48,7779,7780],{"class":112},"    visible",[48,7782,777],{"class":80},[48,7784,7785],{"class":54}," false\n",[48,7787,7788,7790,7792,7794,7796,7798,7800,7803],{"class":50,"line":351},[48,7789,7080],{"class":58},[48,7791,116],{"class":80},[48,7793,5935],{"class":112},[48,7795,116],{"class":84},[48,7797,1139],{"class":76},[48,7799,81],{"class":80},[48,7801,7802],{"class":112},"_on_state_changed",[48,7804,337],{"class":80},[48,7806,7807],{"class":50,"line":356},[48,7808,66],{"emptyLinePlaceholder":65},[48,7810,7811,7813,7816,7818,7820,7822,7824,7826],{"class":50,"line":367},[48,7812,73],{"class":72},[48,7814,7815],{"class":76}," _on_state_changed",[48,7817,81],{"class":80},[48,7819,1099],{"class":84},[48,7821,94],{"class":80},[48,7823,97],{"class":72},[48,7825,100],{"class":58},[48,7827,103],{"class":80},[48,7829,7830,7832,7834,7836,7838,7840,7842,7844,7846],{"class":50,"line":884},[48,7831,109],{"class":54},[48,7833,7358],{"class":112},[48,7835,122],{"class":72},[48,7837,132],{"class":58},[48,7839,116],{"class":80},[48,7841,146],{"class":112},[48,7843,116],{"class":80},[48,7845,1018],{"class":54},[48,7847,103],{"class":84},[48,7849,7850,7853,7855,7857,7859,7862,7865,7868,7871,7873,7875],{"class":50,"line":892},[48,7851,7852],{"class":112},"        final_score",[48,7854,116],{"class":80},[48,7856,6769],{"class":112},[48,7858,777],{"class":80},[48,7860,7861],{"class":125}," \"得分：",[48,7863,7864],{"class":783},"%d",[48,7866,7867],{"class":125},"\"",[48,7869,7870],{"class":72}," %",[48,7872,132],{"class":58},[48,7874,116],{"class":80},[48,7876,7877],{"class":112},"total_score\n",[48,7879,7880,7883,7885],{"class":50,"line":897},[48,7881,7882],{"class":112},"        visible",[48,7884,777],{"class":80},[48,7886,7887],{"class":54}," true\n",[199,7889,7890,7895,7898],{},[202,7891,7892,7893],{},"监听 ",[45,7894,5935],{},[202,7896,7897],{},"GAME_OVER 时显示面板 + 填上最终分数",[202,7899,7900,7902,7903],{},[45,7901,7864],{}," 是字符串占位符，等同于 ",[45,7904,7905],{},"str(GameManager.total_score)",[14,7907,7908],{},[10,7909,7910,7911,2104,7913,7915,7916,194],{},"⚠️ 别忘了把 ",[45,7912,7028],{},[45,7914,6849],{}," 信号连接到 main 场景根节点的 ",[45,7917,7470],{},[25,7919,7920],{"id":7920},"设置主场景",[10,7922,7923,7924,7926],{},"最后把【主场景】改成 ",[45,7925,6733],{},"（这样游戏启动直接到菜单）：",[10,7928,7929],{},"路径：项目 → 项目设置 → 常规 → 运行 → 主场景",[10,7931,7932],{},[1867,7933],{"alt":7934,"src":7935},"06-ui-主场景设置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F06-ui-%E4%B8%BB%E5%9C%BA%E6%99%AF%E8%AE%BE%E7%BD%AE.png",[25,7937,3259],{"id":3259},[10,7939,7940],{},"按下运行键，整个流程应该是：",[3034,7942,7943,7946,7949,7952,7955],{},[202,7944,7945],{},"✅ 启动 → 显示菜单 + 开始按钮",[202,7947,7948],{},"✅ 点击开始 → 切换到游戏场景，画面冻结，等待按键",[202,7950,7951],{},"✅ 按下 fly → 游戏开始，水管生成，小鸟可控",[202,7953,7954],{},"✅ 撞死 → 显示结束面板 + 最终分数",[202,7956,7957],{},"✅ 点重新开始 → 回到游戏初始状态",[10,7959,7960],{},"下一章我们替换美术资源，把这些灰色矩形变成真正的小鸟和水管 🎨",[1804,7962,7963],{},"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":43,"searchDepth":62,"depth":62,"links":7965},[7966,7967,7971,7973,7980,7981,7982,7983],{"id":6758,"depth":62,"text":6758},{"id":6774,"depth":62,"text":7968,"children":7969},"开始菜单 menu.tscn",[7970],{"id":6837,"depth":69,"text":6837},{"id":6912,"depth":62,"text":7972},"游戏主场景 main.tscn",{"id":7038,"depth":62,"text":7974,"children":7975},"主场景脚本 main.gd",[7976,7977,7979],{"id":4514,"depth":69,"text":4515},{"id":7489,"depth":69,"text":7978},"Engine.time_scale — 全局时间缩放",{"id":7557,"depth":69,"text":7557},{"id":7574,"depth":62,"text":6748},{"id":7705,"depth":62,"text":7705},{"id":7920,"depth":62,"text":7920},{"id":3259,"depth":62,"text":3259},{},"\u002Fdevlog\u002Fxggame-bird\u002F06-ui",{"title":6718,"description":1832},"devlog\u002Fxggame-bird\u002F06-ui","MaW5eGhaEWyzREhq8HxUBpIIw5GmRnx7tB25QX3pkfw",{"id":7990,"title":7991,"body":7992,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":8533,"navigation":65,"path":6960,"seo":8534,"stem":8535,"toc":65,"__hash__":8536},"devlog\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets.md","替换美术资源",{"type":7,"value":7993,"toc":8521},[7994,7997,8000,8015,8018,8063,8066,8092,8098,8102,8112,8118,8124,8127,8141,8147,8156,8165,8168,8181,8187,8198,8201,8205,8212,8225,8228,8233,8251,8257,8262,8321,8326,8329,8336,8355,8369,8375,8386,8389,8397,8476,8483,8485,8488,8502,8508,8511,8518],[10,7995,7996],{},"到目前为止小鸟还是 Godot 的\"机器人头\"，水管也是个矩形 — 这一章给游戏换上真正的美术资源，从「白板原型」一秒变成「真游戏」。✨",[10,7998,7999],{},"我们要做：",[3034,8001,8002,8005,8008],{},[202,8003,8004],{},"准备 + 导入美术素材",[202,8006,8007],{},"替换小鸟、水管的精灵图",[202,8009,8010,8011,8014],{},"加上",[20,8012,8013],{},"滚动","的地面和背景（这是 Flappy Bird \"横向飞行错觉\"的灵魂）",[25,8016,8017],{"id":8017},"准备美术资源",[14,8019,8020,8033,8036,8039],{},[10,8021,8022,8023,8032],{},"这个游戏的",[20,8024,8025,8026,8031],{},"像素美术全部是我自己用 ",[32,8027,8030],{"href":8028,"rel":8029},"https:\u002F\u002Fwww.aseprite.org\u002F",[1860],"Aseprite"," 画的"," — 小鸟、水管、背景、地面、UI 按钮和引导手指。",[10,8034,8035],{},"画像素画其实门槛比想象中低：小鸟就是 16×16 的一小块，水管更简单，纯色矩形加个边就行。第一次画不好看也没关系，先跑起来比先画好看重要得多。",[10,8037,8038],{},"如果你不想自己画，可以从这些免费站找：",[199,8040,8041,8048,8056],{},[202,8042,8043],{},[32,8044,8047],{"href":8045,"rel":8046},"https:\u002F\u002Fitch.io\u002Fgame-assets\u002Ffree",[1860],"itch.io 免费素材",[202,8049,8050,8055],{},[32,8051,8054],{"href":8052,"rel":8053},"https:\u002F\u002Fkenney.nl\u002F",[1860],"Kenney.nl","（CC0 免版权）",[202,8057,8058],{},[32,8059,8062],{"href":8060,"rel":8061},"https:\u002F\u002Fopengameart.org\u002F",[1860],"OpenGameArt",[10,8064,8065],{},"需要这几样：",[199,8067,8068,8074,8080,8086],{},[202,8069,8070,8073],{},[45,8071,8072],{},"bird.png"," — 小鸟",[202,8075,8076,8079],{},[45,8077,8078],{},"pipe.png"," — 水管",[202,8081,8082,8085],{},[45,8083,8084],{},"progress.png"," — 跑道",[202,8087,8088,8091],{},[45,8089,8090],{},"bg.png"," — 背景（最好做成左右无缝拼接的）",[10,8093,8094],{},[1867,8095],{"alt":8096,"src":8097},"07-art-assets-素材","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E7%B4%A0%E6%9D%90.png",[25,8099,8101],{"id":8100},"导入设置像素美术别变模糊","导入设置：像素美术别变模糊",[10,8103,4909,8104,8107,8108,8111],{},[45,8105,8106],{},".png"," 文件拖进 Godot 项目，默认导入会用",[20,8109,8110],{},"双线性插值","把像素图变得模糊糊的，这不是我们要的复古感觉。",[10,8113,8114],{},[1867,8115],{"alt":8116,"src":8117},"07-art-assets-导入设置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E5%AF%BC%E5%85%A5%E8%AE%BE%E7%BD%AE.png",[10,8119,8120,8121,194],{},"项目设置 → 渲染 → 纹理 → ",[45,8122,8123],{},"画布纹理 > 默认纹理过滤 = Nearest",[25,8125,8126],{"id":8126},"替换小鸟",[10,8128,8129,8130,8132,8133,8135,8136,8138,8139,194],{},"打开 ",[45,8131,3141],{},"，选中 ",[45,8134,2091],{}," 节点，把它的 ",[45,8137,2107],{}," 换成 ",[45,8140,8072],{},[10,8142,8143],{},[1867,8144],{"alt":8145,"src":8146},"07-art-assets-小鸟替换","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E5%B0%8F%E9%B8%9F%E6%9B%BF%E6%8D%A2.png",[10,8148,8149,8152,8153,8155],{},[20,8150,8151],{},"碰撞体也要对应调整","：选中 ",[45,8154,2097],{},"，把圆形缩放到正好包住小鸟。",[14,8157,8158],{},[10,8159,8160,8161,8164],{},"💡 小贴士：碰撞体可以",[20,8162,8163],{},"略小于图片","（特别是小鸟）。这样玩家会觉得\"我明明擦边过去了\"的判定更宽容，手感更好。这是 Flappy Bird 类游戏的隐藏秘诀之一。",[25,8166,8167],{"id":8167},"替换水管",[10,8169,8129,8170,8172,8173,8175,8176,8178,8179,194],{},[45,8171,4912],{},"，选中两个 ",[45,8174,2091],{},"（上下水管），把它们的 ",[45,8177,2107],{}," 都换成 ",[45,8180,8078],{},[10,8182,8183],{},[1867,8184],{"alt":8185,"src":8186},"07-art-assets-水管替换","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E6%B0%B4%E7%AE%A1%E6%9B%BF%E6%8D%A2.png",[10,8188,8189,8190,8193,8194,8197],{},"上水管要",[20,8191,8192],{},"垂直翻转","（在 Inspector 里勾选 ",[45,8195,8196],{},"Flip V","）。",[10,8199,8200],{},"碰撞体同样调整到包住水管的大小。",[25,8202,8204],{"id":8203},"滚动地面-背景重头戏","滚动地面 + 背景（重头戏 🎬）",[10,8206,8207,8208,8211],{},"Flappy Bird 的精髓 — 小鸟其实在原地扑腾，",[20,8209,8210],{},"背景和地面在向左移动","，制造出\"鸟在向右飞\"的错觉。",[10,8213,8214,8215,8217,8218,8220,8221,8224],{},"Godot 4.3+ 有专门的 ",[45,8216,6953],{}," 节点，自带 ",[45,8219,7384],{},"（自动滚动）和 ",[45,8222,8223],{},"repeat","（无缝循环），非常好用。",[396,8226,8227],{"id":8227},"添加滚动背景",[10,8229,3308,8230,8232],{},[45,8231,6742],{}," 的根节点下添加：",[199,8234,8235],{},[202,8236,8237,6809,8239,3830,8241],{},[45,8238,6953],{},[45,8240,6956],{},[199,8242,8243],{},[202,8244,8245,6011,8247,2963,8249],{},[45,8246,2091],{},[45,8248,2107],{},[45,8250,8090],{},[10,8252,8253],{},[1867,8254],{"alt":8255,"src":8256},"07-art-assets-背景节点","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E8%83%8C%E6%99%AF%E8%8A%82%E7%82%B9.png",[10,8258,2138,8259,8261],{},[45,8260,6956],{},"，在 Inspector 里设置：",[496,8263,8264,8275],{},[499,8265,8266],{},[502,8267,8268,8271,8273],{},[505,8269,8270],{},"属性",[505,8272,1937],{},[505,8274,1993],{},[515,8276,8277,8293,8306],{},[502,8278,8279,8284,8290],{},[520,8280,8281],{},[45,8282,8283],{},"Autoscroll > x",[520,8285,8286,8289],{},[45,8287,8288],{},"-50","（或你喜欢的速度）",[520,8291,8292],{},"每秒向左移动多少像素",[502,8294,8295,8300,8303],{},[520,8296,8297],{},[45,8298,8299],{},"Repeat Size > x",[520,8301,8302],{},"背景图宽度",[520,8304,8305],{},"让它无缝循环",[502,8307,8308,8313,8318],{},[520,8309,8310],{},[45,8311,8312],{},"Repeat Times",[520,8314,8315,8317],{},[45,8316,551],{}," 或更大",[520,8319,8320],{},"重复多少次（保证屏幕外有备用）",[14,8322,8323],{},[10,8324,8325],{},"负值表示向左滚动；正值表示向右。",[396,8327,8328],{"id":8328},"添加滚动地面",[10,8330,8331,8332,6809,8334,1564],{},"同样的方式，加一个 ",[45,8333,6953],{},[45,8335,6968],{},[199,8337,8338],{},[202,8339,8340,1527,8342,3830,8344],{},[45,8341,6953],{},[45,8343,6968],{},[199,8345,8346],{},[202,8347,8348,6011,8350,2963,8352],{},[45,8349,2091],{},[45,8351,2107],{},[45,8353,8354],{},"ground.png",[10,8356,8357,8360,8361,8364,8365,8368],{},[45,8358,8359],{},"Autoscroll.x"," 可以",[20,8362,8363],{},"比背景快","（比如 ",[45,8366,8367],{},"-150","），制造\"近景快、远景慢\"的视差感（这就是 Parallax 这个词的来源）。",[10,8370,8371],{},[1867,8372],{"alt":8373,"src":8374},"07-art-assets-地面节点","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E5%9C%B0%E9%9D%A2%E8%8A%82%E7%82%B9.png",[14,8376,8377],{},[10,8378,8379,8380,8382,8383,8385],{},"💡 这两个节点的 z-index 注意一下：背景 ",[45,8381,6956],{}," 要在最底层（z 最小），地面 ",[45,8384,6968],{}," 在小鸟之上（z 最大，遮住可能的视觉穿帮）。",[25,8387,8388],{"id":8388},"让背景在游戏结束时停下来",[10,8390,8391,8392,8396],{},"还记得 ",[32,8393,8395],{"href":8394},"\u002Fdevlog\u002Fxggame-bird\u002F06-ui#%E4%B8%BB%E5%9C%BA%E6%99%AF%E8%84%9A%E6%9C%AC-maingd","上一章 main.gd"," 里这两行吗？",[38,8398,8400],{"className":40,"code":8399,"language":42,"meta":43,"style":43},"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",[45,8401,8402,8420,8440,8458],{"__ignoreMap":43},[48,8403,8404,8406,8408,8410,8412,8414,8416,8418],{"class":50,"line":51},[48,8405,73],{"class":72},[48,8407,7334],{"class":76},[48,8409,81],{"class":80},[48,8411,1099],{"class":84},[48,8413,94],{"class":80},[48,8415,97],{"class":72},[48,8417,100],{"class":58},[48,8419,103],{"class":80},[48,8421,8422,8424,8426,8428,8430,8432,8434,8436,8438],{"class":50,"line":62},[48,8423,109],{"class":54},[48,8425,7358],{"class":112},[48,8427,122],{"class":72},[48,8429,132],{"class":58},[48,8431,116],{"class":80},[48,8433,146],{"class":112},[48,8435,116],{"class":80},[48,8437,1018],{"class":54},[48,8439,103],{"class":84},[48,8441,8442,8444,8446,8448,8450,8452,8454,8456],{"class":50,"line":69},[48,8443,7377],{"class":54},[48,8445,6956],{"class":783},[48,8447,116],{"class":80},[48,8449,7384],{"class":112},[48,8451,116],{"class":80},[48,8453,847],{"class":112},[48,8455,777],{"class":80},[48,8457,1058],{"class":758},[48,8459,8460,8462,8464,8466,8468,8470,8472,8474],{"class":50,"line":106},[48,8461,7377],{"class":54},[48,8463,6968],{"class":783},[48,8465,116],{"class":80},[48,8467,7384],{"class":112},[48,8469,116],{"class":80},[48,8471,847],{"class":112},[48,8473,777],{"class":80},[48,8475,1058],{"class":758},[10,8477,8478,8479,8482],{},"它做的就是：",[20,8480,8481],{},"游戏结束时把背景\u002F地面的滚动速度设为 0","。这样画面整体定住，更有\"撞死\"的仪式感。",[25,8484,3259],{"id":3259},[10,8486,8487],{},"现在游戏应该长这样：",[199,8489,8490,8493,8496,8499],{},[202,8491,8492],{},"✅ 真正的小鸟在画面里扑腾",[202,8494,8495],{},"✅ 水管是绿色（或你的素材颜色）的管子",[202,8497,8498],{},"✅ 背景缓缓向左滚动 + 地面快速向左滚动 → 视差错觉",[202,8500,8501],{},"✅ 撞死瞬间，背景立刻定住",[10,8503,8504],{},[1867,8505],{"alt":8506,"src":8507},"07-art-assets-效果","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F07-art-assets-%E6%95%88%E6%9E%9C.png",[10,8509,8510],{},"是不是一瞬间像个游戏了？🎮",[10,8512,8513,8514,8517],{},"下一章给它加上",[20,8515,8516],{},"音效"," — 翅膀扇动、得分、碰撞，最后一点\"灵魂\"。",[1804,8519,8520],{},"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":43,"searchDepth":62,"depth":62,"links":8522},[8523,8524,8525,8526,8527,8531,8532],{"id":8017,"depth":62,"text":8017},{"id":8100,"depth":62,"text":8101},{"id":8126,"depth":62,"text":8126},{"id":8167,"depth":62,"text":8167},{"id":8203,"depth":62,"text":8204,"children":8528},[8529,8530],{"id":8227,"depth":69,"text":8227},{"id":8328,"depth":69,"text":8328},{"id":8388,"depth":62,"text":8388},{"id":3259,"depth":62,"text":3259},{},{"title":7991,"description":1832},"devlog\u002Fxggame-bird\u002F07-art-assets","qtF7fMJTBYiFgezflU7I_VErLrbV0KYDVkhUn3TQMRo",{"id":8538,"title":8539,"body":8540,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":9354,"navigation":65,"path":9355,"seo":9356,"stem":9357,"toc":65,"__hash__":9358},"devlog\u002Fdevlog\u002Fxggame-bird\u002F08-sound.md","音效 AudioManager",{"type":7,"value":8541,"toc":9334},[8542,8545,8568,8571,8617,8624,8644,8650,8663,8671,8678,8700,8704,8710,8712,8755,8761,8765,8768,8787,8791,8797,8937,8952,8956,8961,8984,8990,8999,9002,9005,9011,9059,9065,9170,9176,9261,9263,9266,9277,9290,9294,9304,9325,9328,9331],[10,8543,8544],{},"游戏没声音就像炒菜没盐 — 没那个味儿。这一章加上：",[199,8546,8547,8554,8561],{},[202,8548,8549,8550,8553],{},"🪶 ",[20,8551,8552],{},"翅膀扇动声","（小鸟跳跃时）",[202,8555,8556,8557,8560],{},"⭐ ",[20,8558,8559],{},"得分声","（穿过水管时）",[202,8562,8563,8564,8567],{},"💥 ",[20,8565,8566],{},"碰撞声","（撞死时）",[25,8569,8570],{"id":8570},"准备音效文件",[14,8572,8573,8586,8598],{},[10,8574,8575,8576,8585],{},"这三个音效",[20,8577,8578,8579,8584],{},"是我自己用 ",[32,8580,8583],{"href":8581,"rel":8582},"https:\u002F\u002Fsfbgames.itch.io\u002Fchiptone",[1860],"ChipTone"," 生成的"," — 浏览器打开就能用，不用注册。",[10,8587,8588,8591,8592,8597],{},[20,8589,8590],{},"小游戏的音效建议「生成」而不是「找」"," — 生成出来的版权归自己，不用记来源、不用署名，\n而且 8-bit 音色天生就贴复古像素风。同类工具还有 ",[32,8593,8596],{"href":8594,"rel":8595},"https:\u002F\u002Fsfxr.me\u002F",[1860],"sfxr","（点一下随机生成，更快）。",[10,8599,8600,8601,8606,8607,8612,8613,8616],{},"真要去下载的话：",[32,8602,8605],{"href":8603,"rel":8604},"https:\u002F\u002Fpixabay.com\u002Fsound-effects\u002F",[1860],"Pixabay","（免署名最省心）、\n",[32,8608,8611],{"href":8609,"rel":8610},"https:\u002F\u002Ffreesound.org\u002F",[1860],"freesound","（库最深，但",[20,8614,8615],{},"必须筛 CC0","，很多是 CC-BY 要署名）。",[10,8618,8619,8620,8623],{},"需要 3 个文件，放到 ",[45,8621,8622],{},"audio\u002F"," 目录下：",[199,8625,8626,8632,8638],{},[202,8627,8628,8631],{},[45,8629,8630],{},"wing.wav"," — 扑翅膀",[202,8633,8634,8637],{},[45,8635,8636],{},"score.wav"," — 得分",[202,8639,8640,8643],{},[45,8641,8642],{},"strike.wav"," — 撞击",[10,8645,8646],{},[1867,8647],{"alt":8648,"src":8649},"08-sound-音效文件","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F08-sound-%E9%9F%B3%E6%95%88%E6%96%87%E4%BB%B6.png",[14,8651,8652],{},[10,8653,8654,8655,8658,8659,8662],{},"💡 推荐用 ",[45,8656,8657],{},".wav"," 而不是 ",[45,8660,8661],{},".mp3","：体积大一点，但延迟低、循环不卡顿。短音效用 wav 是标配。",[25,8664,8666,8667,8670],{"id":8665},"audiomanager-是个场景不是脚本","AudioManager 是个",[20,8668,8669],{},"场景","，不是脚本",[10,8672,8673,8674,8677],{},"和 GameManager 不一样，AudioManager 我们用 ",[20,8675,8676],{},"场景（.tscn）"," 来做，而不是单纯的脚本。",[10,8679,8680,8683,8684,8687,8688,8691,8692,8695,8696,8699],{},[20,8681,8682],{},"为什么？"," 因为每个音效都需要一个 ",[45,8685,8686],{},"AudioStreamPlayer"," 节点 — 与其在脚本里 ",[45,8689,8690],{},"new AudioStreamPlayer()","，不如直接在场景里把所有 player 节点摆好，",[20,8693,8694],{},"音效文件也提前在 Inspector 里预设好","，调用时直接 ",[45,8697,8698],{},".play()"," 就行。",[25,8701,8703],{"id":8702},"创建-audiomanager-场景","创建 AudioManager 场景",[10,8705,8706,8707,194],{},"新建场景，保存为 ",[45,8708,8709],{},"scenes\u002Fautoload\u002Faudio_manager.tscn",[10,8711,3106],{},[199,8713,8714],{},[202,8715,8716,6809,8719,8721,8722],{},[45,8717,8718],{},"Node",[45,8720,572],{},"）— 根节点\n",[199,8723,8724],{},[202,8725,8726,6809,8728,8731,8732],{},[45,8727,8718],{},[45,8729,8730],{},"SFX","）— 音效组\n",[199,8733,8734,8741,8748],{},[202,8735,8736,6809,8738,620],{},[45,8737,8686],{},[45,8739,8740],{},"wing_sfx",[202,8742,8743,6809,8745,620],{},[45,8744,8686],{},[45,8746,8747],{},"score_sfx",[202,8749,8750,6809,8752,620],{},[45,8751,8686],{},[45,8753,8754],{},"strike_sfx",[10,8756,8757],{},[1867,8758],{"alt":8759,"src":8760},"08-sound-scene-结构","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F08-sound-scene-%E7%BB%93%E6%9E%84.png",[396,8762,8764],{"id":8763},"每个-audiostreamplayer-的设置","每个 AudioStreamPlayer 的设置",[10,8766,8767],{},"选中每个 player，在右侧 Inspector 里：",[199,8769,8770,8781],{},[202,8771,8772,8775,8776,4913,8778,8780],{},[20,8773,8774],{},"Stream","：拖入对应的音效文件（如 ",[45,8777,8630],{},[45,8779,8740],{}," 的 Stream 槽）",[202,8782,8783,8786],{},[20,8784,8785],{},"Volume dB","：根据音量调整（-6 dB 是温和的衰减，-12 dB 是明显的降低）",[25,8788,8790],{"id":8789},"audiomanager-脚本","AudioManager 脚本",[10,8792,8793,8794,298],{},"给根节点挂脚本 ",[45,8795,8796],{},"audio_manager.gd",[38,8798,8800],{"className":40,"code":8799,"language":42,"meta":43,"style":43},"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",[45,8801,8802,8808,8812,8817,8835,8853,8871,8875,8883,8893,8897,8905,8915,8919,8927],{"__ignoreMap":43},[48,8803,8804,8806],{"class":50,"line":51},[48,8805,55],{"class":54},[48,8807,310],{"class":58},[48,8809,8810],{"class":50,"line":62},[48,8811,66],{"emptyLinePlaceholder":65},[48,8813,8814],{"class":50,"line":69},[48,8815,8816],{"class":170},"# 在检查器里预载好你的音效文件\n",[48,8818,8819,8821,8823,8825,8827,8829,8831,8833],{"class":50,"line":106},[48,8820,764],{"class":76},[48,8822,749],{"class":72},[48,8824,1258],{"class":112},[48,8826,88],{"class":80},[48,8828,1263],{"class":58},[48,8830,777],{"class":80},[48,8832,780],{"class":54},[48,8834,1270],{"class":783},[48,8836,8837,8839,8841,8843,8845,8847,8849,8851],{"class":50,"line":156},[48,8838,764],{"class":76},[48,8840,749],{"class":72},[48,8842,1279],{"class":112},[48,8844,88],{"class":80},[48,8846,1263],{"class":58},[48,8848,777],{"class":80},[48,8850,780],{"class":54},[48,8852,1290],{"class":783},[48,8854,8855,8857,8859,8861,8863,8865,8867,8869],{"class":50,"line":174},[48,8856,764],{"class":76},[48,8858,749],{"class":72},[48,8860,1299],{"class":112},[48,8862,88],{"class":80},[48,8864,1263],{"class":58},[48,8866,777],{"class":80},[48,8868,780],{"class":54},[48,8870,1310],{"class":783},[48,8872,8873],{"class":50,"line":351},[48,8874,66],{"emptyLinePlaceholder":65},[48,8876,8877,8879,8881],{"class":50,"line":356},[48,8878,73],{"class":72},[48,8880,1399],{"class":76},[48,8882,1402],{"class":80},[48,8884,8885,8887,8889,8891],{"class":50,"line":367},[48,8886,1407],{"class":112},[48,8888,116],{"class":84},[48,8890,1412],{"class":76},[48,8892,711],{"class":80},[48,8894,8895],{"class":50,"line":884},[48,8896,66],{"emptyLinePlaceholder":65},[48,8898,8899,8901,8903],{"class":50,"line":892},[48,8900,73],{"class":72},[48,8902,1426],{"class":76},[48,8904,1402],{"class":80},[48,8906,8907,8909,8911,8913],{"class":50,"line":897},[48,8908,1434],{"class":112},[48,8910,116],{"class":84},[48,8912,1412],{"class":76},[48,8914,711],{"class":80},[48,8916,8917],{"class":50,"line":903},[48,8918,66],{"emptyLinePlaceholder":65},[48,8920,8921,8923,8925],{"class":50,"line":927},[48,8922,73],{"class":72},[48,8924,1453],{"class":76},[48,8926,1402],{"class":80},[48,8928,8929,8931,8933,8935],{"class":50,"line":944},[48,8930,1461],{"class":112},[48,8932,116],{"class":84},[48,8934,1412],{"class":76},[48,8936,711],{"class":80},[199,8938,8939,8945],{},[202,8940,8941,8944],{},[45,8942,8943],{},"@onready var xxx = $SFX\u002Fxxx"," — 拿到子节点的引用",[202,8946,8947,8948,8951],{},"方法本身就一行：调用对应 player 的 ",[45,8949,8950],{},"play()"," 即可",[25,8953,8955],{"id":8954},"注册为-autoload","注册为 Autoload",[10,8957,8958,8959,298],{},"和 GameManager 一样，",[20,8960,475],{},[496,8962,8963,8971],{},[499,8964,8965],{},[502,8966,8967,8969],{},[505,8968,5710],{},[505,8970,5719],{},[515,8972,8973],{},[502,8974,8975,8980],{},[520,8976,8977],{},[45,8978,8979],{},"res:\u002F\u002Fscenes\u002Fautoload\u002Faudio_manager.tscn",[520,8981,8982],{},[45,8983,572],{},[10,8985,8986],{},[1867,8987],{"alt":8988,"src":8989},"08-sound-autoload","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F08-sound-autoload.png",[14,8991,8992],{},[10,8993,8994,8995,8998],{},"⚠️ 注意这里加载的是 ",[20,8996,8997],{},"场景文件 (.tscn)","，不是脚本 (.gd)。Godot 的 Autoload 同时支持两者。",[25,9000,9001],{"id":9001},"在游戏里调用",[10,9003,9004],{},"现在任何节点都能直接喊 AudioManager 一声 — 在合适的位置加上音效调用：",[396,9006,9008,9009,620],{"id":9007},"小鸟跳跃时birdgd","小鸟跳跃时（",[45,9010,1563],{},[38,9012,9014],{"className":40,"code":9013,"language":42,"meta":43,"style":43},"if Input.is_action_just_pressed(\"fly\"):\n    velocity.y = jump_force\n    AudioManager.play_wing()  # 👈 加这一行\n",[45,9015,9016,9034,9046],{"__ignoreMap":43},[48,9017,9018,9020,9022,9024,9026,9028,9030,9032],{"class":50,"line":51},[48,9019,1574],{"class":54},[48,9021,1577],{"class":58},[48,9023,116],{"class":84},[48,9025,1582],{"class":76},[48,9027,81],{"class":80},[48,9029,1587],{"class":125},[48,9031,94],{"class":80},[48,9033,103],{"class":84},[48,9035,9036,9038,9040,9042,9044],{"class":50,"line":62},[48,9037,1616],{"class":112},[48,9039,116],{"class":80},[48,9041,1621],{"class":112},[48,9043,777],{"class":80},[48,9045,1626],{"class":112},[48,9047,9048,9050,9052,9054,9056],{"class":50,"line":69},[48,9049,1631],{"class":58},[48,9051,116],{"class":84},[48,9053,1636],{"class":76},[48,9055,167],{"class":80},[48,9057,9058],{"class":170},"  # 👈 加这一行\n",[396,9060,9062,9063,620],{"id":9061},"穿过水管得分时pillar_pairgd","穿过水管得分时（",[45,9064,725],{},[38,9066,9068],{"className":40,"code":9067,"language":42,"meta":43,"style":43},"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",[45,9069,9070,9092,9108,9120,9138,9152],{"__ignoreMap":43},[48,9071,9072,9074,9076,9078,9080,9082,9084,9086,9088,9090],{"class":50,"line":51},[48,9073,73],{"class":72},[48,9075,908],{"class":76},[48,9077,81],{"class":80},[48,9079,85],{"class":84},[48,9081,88],{"class":80},[48,9083,91],{"class":58},[48,9085,94],{"class":80},[48,9087,97],{"class":72},[48,9089,100],{"class":58},[48,9091,103],{"class":80},[48,9093,9094,9096,9098,9100,9102,9104,9106],{"class":50,"line":62},[48,9095,109],{"class":54},[48,9097,113],{"class":112},[48,9099,116],{"class":80},[48,9101,119],{"class":112},[48,9103,122],{"class":72},[48,9105,126],{"class":125},[48,9107,103],{"class":84},[48,9109,9110,9112,9114,9116,9118],{"class":50,"line":69},[48,9111,159],{"class":58},[48,9113,116],{"class":84},[48,9115,1362],{"class":76},[48,9117,167],{"class":80},[48,9119,9058],{"class":170},[48,9121,9122,9124,9126,9128,9130,9132,9134,9136],{"class":50,"line":106},[48,9123,863],{"class":54},[48,9125,132],{"class":58},[48,9127,116],{"class":84},[48,9129,4789],{"class":76},[48,9131,81],{"class":80},[48,9133,4794],{"class":125},[48,9135,94],{"class":80},[48,9137,103],{"class":84},[48,9139,9140,9142,9144,9146,9148,9150],{"class":50,"line":156},[48,9141,4803],{"class":58},[48,9143,116],{"class":84},[48,9145,1144],{"class":76},[48,9147,81],{"class":80},[48,9149,522],{"class":758},[48,9151,337],{"class":80},[48,9153,9154,9156,9158,9160,9162,9164,9166,9168],{"class":50,"line":174},[48,9155,4488],{"class":112},[48,9157,116],{"class":84},[48,9159,4493],{"class":76},[48,9161,81],{"class":80},[48,9163,4498],{"class":125},[48,9165,3717],{"class":80},[48,9167,4503],{"class":54},[48,9169,337],{"class":80},[396,9171,9173,9174,620],{"id":9172},"撞死时killzonegd","撞死时（",[45,9175,619],{},[38,9177,9179],{"className":40,"code":9178,"language":42,"meta":43,"style":43},"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",[45,9180,9181,9203,9239,9251],{"__ignoreMap":43},[48,9182,9183,9185,9187,9189,9191,9193,9195,9197,9199,9201],{"class":50,"line":51},[48,9184,73],{"class":72},[48,9186,77],{"class":76},[48,9188,81],{"class":80},[48,9190,85],{"class":84},[48,9192,88],{"class":80},[48,9194,91],{"class":58},[48,9196,94],{"class":80},[48,9198,97],{"class":72},[48,9200,100],{"class":58},[48,9202,103],{"class":80},[48,9204,9205,9207,9209,9211,9213,9215,9217,9219,9221,9223,9225,9227,9229,9231,9233,9235,9237],{"class":50,"line":62},[48,9206,109],{"class":54},[48,9208,113],{"class":112},[48,9210,116],{"class":80},[48,9212,119],{"class":112},[48,9214,122],{"class":72},[48,9216,126],{"class":125},[48,9218,129],{"class":72},[48,9220,132],{"class":58},[48,9222,116],{"class":80},[48,9224,137],{"class":112},[48,9226,122],{"class":72},[48,9228,132],{"class":58},[48,9230,116],{"class":80},[48,9232,146],{"class":112},[48,9234,116],{"class":80},[48,9236,151],{"class":54},[48,9238,103],{"class":84},[48,9240,9241,9243,9245,9247,9249],{"class":50,"line":69},[48,9242,159],{"class":58},[48,9244,116],{"class":84},[48,9246,164],{"class":76},[48,9248,167],{"class":80},[48,9250,9058],{"class":170},[48,9252,9253,9255,9257,9259],{"class":50,"line":106},[48,9254,177],{"class":58},[48,9256,116],{"class":84},[48,9258,182],{"class":76},[48,9260,711],{"class":80},[25,9262,3259],{"id":3259},[10,9264,9265],{},"戴上耳机，应该能听到：",[199,9267,9268,9271,9274],{},[202,9269,9270],{},"✅ 按 fly → 翅膀扇动声",[202,9272,9273],{},"✅ 穿过水管 → 清脆的得分音",[202,9275,9276],{},"✅ 撞死 → 一声闷响",[10,9278,9279,9280,9283,9284,9286,9287,194],{},"如果声音太大\u002F太小，去 ",[45,9281,9282],{},"audio_manager.tscn"," 里调 ",[45,9285,8785],{},"，",[20,9288,9289],{},"不用改代码",[25,9291,9293],{"id":9292},"可选背景音乐-bgm","（可选）背景音乐 BGM",[10,9295,9296,9297,9299,9300,9303],{},"你可以再加一个 ",[45,9298,8686],{},"（命名 ",[45,9301,9302],{},"BGM","），在 Inspector 里：",[199,9305,9306,9311,9317],{},[202,9307,9308,9310],{},[20,9309,8774],{},"：拖入背景音乐",[202,9312,9313,9316],{},[20,9314,9315],{},"Autoplay","：✅ 勾选（场景一加载就播放）",[202,9318,9319,9322,9323],{},[20,9320,9321],{},"Loop","：在 Stream 的导入设置里勾选 ",[45,9324,9321],{},[10,9326,9327],{},"这样游戏一开始就有背景音乐。",[10,9329,9330],{},"下一章我们打包导出游戏，发布到 itch.io！🚀",[1804,9332,9333],{},"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":43,"searchDepth":62,"depth":62,"links":9335},[9336,9337,9339,9342,9343,9344,9352,9353],{"id":8570,"depth":62,"text":8570},{"id":8665,"depth":62,"text":9338},"AudioManager 是个场景，不是脚本",{"id":8702,"depth":62,"text":8703,"children":9340},[9341],{"id":8763,"depth":69,"text":8764},{"id":8789,"depth":62,"text":8790},{"id":8954,"depth":62,"text":8955},{"id":9001,"depth":62,"text":9001,"children":9345},[9346,9348,9350],{"id":9007,"depth":69,"text":9347},"小鸟跳跃时（bird.gd）",{"id":9061,"depth":69,"text":9349},"穿过水管得分时（pillar_pair.gd）",{"id":9172,"depth":69,"text":9351},"撞死时（killzone.gd）",{"id":3259,"depth":62,"text":3259},{"id":9292,"depth":62,"text":9293},{},"\u002Fdevlog\u002Fxggame-bird\u002F08-sound",{"title":8539,"description":1832},"devlog\u002Fxggame-bird\u002F08-sound","SiovhngD622ncVG4wlzeyF7zsqoTjpr2hNkyrHGYjik",{"id":9360,"title":9361,"body":9362,"cover":1830,"date":2049,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":10336,"navigation":65,"path":10337,"seo":10338,"stem":10339,"toc":65,"__hash__":10340},"devlog\u002Fdevlog\u002Fxggame-bird\u002F09-export.md","导出 + 发布到 itch.io",{"type":7,"value":9363,"toc":10315},[9364,9370,9373,9392,9405,9409,9412,9419,9422,9428,9432,9435,9446,9452,9455,9538,9541,9573,9582,9586,9591,9649,9659,9664,9693,9728,9736,9739,9745,9748,9754,9806,9813,9817,9822,9830,9833,9839,9842,9931,9935,9941,9980,9991,10005,10011,10015,10021,10064,10088,10091,10097,10123,10130,10134,10137,10199,10203,10210,10230,10237,10241,10244,10268,10271,10273,10277,10280,10300,10309,10312],[10,9365,9366,9367,194],{},"游戏做完了！最激动人心的一步 — ",[20,9368,9369],{},"让别人玩到你的游戏",[10,9371,9372],{},"我们要做两件事：",[3034,9374,9375,9381],{},[202,9376,9377,9380],{},[20,9378,9379],{},"导出为 Web 版（HTML5）"," — 在浏览器里就能玩",[202,9382,9383,9391],{},[20,9384,9385,9386],{},"发布到 ",[32,9387,9390],{"href":9388,"rel":9389},"https:\u002F\u002Fitch.io",[1860],"itch.io"," — 全世界都能搜到、能玩",[14,9393,9394],{},[10,9395,9396,9397,9400,9401,9404],{},"还记得我们在 ",[32,9398,9399],{"href":2051},"第 1 章","选了「兼容」渲染器吗？就是为了",[20,9402,9403],{},"这一步","！🎉",[25,9406,9408],{"id":9407},"安装-web-导出模板","安装 Web 导出模板",[10,9410,9411],{},"第一次导出 Web 版需要下载导出模板：",[10,9413,9414,9415,9418],{},"路径：编辑器 → ",[20,9416,9417],{},"管理导出模板"," (Manage Export Templates)",[10,9420,9421],{},"如果是首次安装，点击「下载并安装」，等几分钟下载完成即可。",[10,9423,9424],{},[1867,9425],{"alt":9426,"src":9427},"09-export-导出模板","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F09-export-%E5%AF%BC%E5%87%BA%E6%A8%A1%E6%9D%BF.png",[25,9429,9431],{"id":9430},"配置-web-导出","配置 Web 导出",[10,9433,9434],{},"路径：项目 → 导出 (Export)",[10,9436,9437,9438,9441,9442,9445],{},"点击「",[20,9439,9440],{},"添加","」→ 选择 ",[20,9443,9444],{},"Web"," 平台。",[10,9447,9448],{},[1867,9449],{"alt":9450,"src":9451},"09-export-配置","\u002Fimg\u002Fdevlog\u002Fxggame-bird\u002F09-export-%E9%85%8D%E7%BD%AE.png",[396,9453,9454],{"id":9454},"关键设置",[496,9456,9457,9470],{},[499,9458,9459],{},[502,9460,9461,9464,9467],{},[505,9462,9463],{},"项",[505,9465,9466],{},"推荐值",[505,9468,9469],{},"说明",[515,9471,9472,9485,9497,9512,9525],{},[502,9473,9474,9479,9482],{},[520,9475,9476],{},[45,9477,9478],{},"Custom HTML Shell",[520,9480,9481],{},"留空",[520,9483,9484],{},"用 Godot 默认模板即可",[502,9486,9487,9492,9494],{},[520,9488,9489],{},[45,9490,9491],{},"Head Include",[520,9493,9481],{},[520,9495,9496],{},"如果想自定义页面 meta，可以加",[502,9498,9499,9504,9509],{},[520,9500,9501],{},[45,9502,9503],{},"Canvas Resize Policy",[520,9505,9506],{},[45,9507,9508],{},"Adaptive",[520,9510,9511],{},"自适应窗口大小",[502,9513,9514,9519,9522],{},[520,9515,9516],{},[45,9517,9518],{},"Focus Canvas on Start",[520,9520,9521],{},"✅ 启用",[520,9523,9524],{},"启动就聚焦，方便按键响应",[502,9526,9527,9532,9535],{},[520,9528,9529],{},[45,9530,9531],{},"Experimental Virtual Keyboard",[520,9533,9534],{},"看需求",[520,9536,9537],{},"移动端用得上",[25,9539,9540],{"id":9540},"导出游戏",[3034,9542,9543,9549,9556,9566],{},[202,9544,9545,9546,9548],{},"在【导出】面板选中 ",[20,9547,9444],{}," 配置",[202,9550,9551,9552,9555],{},"点击右下角「",[20,9553,9554],{},"导出项目","」",[202,9557,9558,9559,9562,9563,620],{},"选择一个",[20,9560,9561],{},"空文件夹","保存（比如 ",[45,9564,9565],{},"builds\u002Fweb\u002F",[202,9567,9568,9569,9572],{},"文件名填 ",[45,9570,9571],{},"index.html","（itch.io 上传约定）",[14,9574,9575],{},[10,9576,9577,9578,9581],{},"⚠️ ",[20,9579,9580],{},"必须导出到空文件夹"," — 否则会和已有文件混在一起。",[396,9583,9585],{"id":9584},"️-两个最容易踩的坑","⚠️ 两个最容易踩的坑",[10,9587,9588],{},[20,9589,9590],{},"坑一：底部有两个按钮，长得很像，只有一个是对的。",[496,9592,9593,9606],{},[499,9594,9595],{},[502,9596,9597,9600,9603],{},[505,9598,9599],{},"按钮",[505,9601,9602],{},"产出",[505,9604,9605],{},"用途",[515,9607,9608,9635],{},[502,9609,9610,9615,9629],{},[520,9611,9612,9614],{},[20,9613,9554],{},"（Export Project）",[520,9616,9617,9619,9620,9619,9623,9619,9626],{},[45,9618,9571],{}," + ",[45,9621,9622],{},".js",[45,9624,9625],{},".wasm",[45,9627,9628],{},".pck",[520,9630,9631,9632],{},"✅ ",[20,9633,9634],{},"你要的",[502,9636,9637,9640,9646],{},[520,9638,9639],{},"导出 PCK\u002FZIP",[520,9641,9642,9643],{},"只有资源数据包，",[20,9644,9645],{},"没有 index.html",[520,9647,9648],{},"❌ 给 DLC \u002F MOD \u002F 热更用的",[10,9650,9651,9652,1188,9655,9658],{},"点错第二个，你会得到一个能打开但里面全是 ",[45,9653,9654],{},"scenes\u002F",[45,9656,9657],{},"scripts\u002F"," 的压缩包，itch.io 加载不出来。",[10,9660,9661],{},[20,9662,9663],{},"坑二：Web 导出永远输出散文件，Godot 不会帮你打 zip。",[10,9665,9666,9667,9670,9671,9674,9675,9677,9678,9685,9686,9688,9689,9692],{},"导出路径的作用是",[20,9668,9669],{},"决定基础文件名","，不是决定打包格式。你要是把路径填成\n",[45,9672,9673],{},"XGGame-Bird.zip","，Godot 会拿 ",[45,9676,1834],{}," 当基础名生成一堆文件，\n然后",[20,9679,9680,9681,9684],{},"把主 HTML 写进那个 ",[45,9682,9683],{},".zip"," 路径"," —— 你得到一个后缀是 ",[45,9687,9683],{}," 的 HTML 文件，\n",[45,9690,9691],{},"file"," 命令一看就露馅：",[38,9694,9698],{"className":9695,"code":9696,"language":9697,"meta":43,"style":43},"language-bash shiki shiki-themes vitesse-light vitesse-dark","$ file XGGame-Bird.zip\nXGGame-Bird.zip: HTML document text     # 根本不是压缩包\n","bash",[45,9699,9700,9711],{"__ignoreMap":43},[48,9701,9702,9705,9708],{"class":50,"line":51},[48,9703,9704],{"class":76},"$",[48,9706,9707],{"class":125}," file",[48,9709,9710],{"class":125}," XGGame-Bird.zip\n",[48,9712,9713,9716,9719,9722,9725],{"class":50,"line":62},[48,9714,9715],{"class":76},"XGGame-Bird.zip:",[48,9717,9718],{"class":125}," HTML",[48,9720,9721],{"class":125}," document",[48,9723,9724],{"class":125}," text",[48,9726,9727],{"class":170},"     # 根本不是压缩包\n",[10,9729,9730,9735],{},[20,9731,9732,9733],{},"所以文件名老老实实填 ",[45,9734,9571],{},"，zip 留到下面那步自己压。",[10,9737,9738],{},"导出后这个目录会出现一堆文件：",[38,9740,9743],{"className":9741,"code":9742,"language":6769},[6767],"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",[45,9744,9742],{"__ignoreMap":43},[25,9746,9747],{"id":9747},"本地测试",[10,9749,9750,9753],{},[20,9751,9752],{},"Web 导出无法直接双击 index.html 打开","（浏览器安全策略会阻止 wasm 加载）。需要起一个本地 server：",[38,9755,9757],{"className":9695,"code":9756,"language":9697,"meta":43,"style":43},"# Python 自带 server（最方便）\ncd builds\u002Fweb\npython -m http.server 8000\n\n# 或者 Node.js\nnpx serve builds\u002Fweb\n",[45,9758,9759,9764,9773,9787,9791,9796],{"__ignoreMap":43},[48,9760,9761],{"class":50,"line":51},[48,9762,9763],{"class":170},"# Python 自带 server（最方便）\n",[48,9765,9766,9770],{"class":50,"line":62},[48,9767,9769],{"class":9768},"sHLBJ","cd",[48,9771,9772],{"class":125}," builds\u002Fweb\n",[48,9774,9775,9778,9781,9784],{"class":50,"line":69},[48,9776,9777],{"class":76},"python",[48,9779,9780],{"class":783}," -m",[48,9782,9783],{"class":125}," http.server",[48,9785,9786],{"class":758}," 8000\n",[48,9788,9789],{"class":50,"line":106},[48,9790,66],{"emptyLinePlaceholder":65},[48,9792,9793],{"class":50,"line":156},[48,9794,9795],{"class":170},"# 或者 Node.js\n",[48,9797,9798,9801,9804],{"class":50,"line":174},[48,9799,9800],{"class":76},"npx",[48,9802,9803],{"class":125}," serve",[48,9805,9772],{"class":125},[10,9807,9808,9809,9812],{},"打开浏览器访问 ",[45,9810,9811],{},"http:\u002F\u002Flocalhost:8000","，能玩就说明导出成功。",[25,9814,9816],{"id":9815},"注册-itchio","注册 itch.io",[14,9818,9819],{},[10,9820,9821],{},"已有账号可以跳过这一步。",[10,9823,9824,9825,9829],{},"去 ",[32,9826,9390],{"href":9827,"rel":9828},"https:\u002F\u002Fitch.io\u002Fregister",[1860]," 注册账号，确认邮箱即可。",[25,9831,9832],{"id":9832},"上传游戏",[10,9834,9835,9836,194],{},"登录后，右上角头像 → ",[20,9837,9838],{},"Upload new project",[396,9840,9841],{"id":9841},"填写项目信息",[496,9843,9844,9854],{},[499,9845,9846],{},[502,9847,9848,9851],{},[505,9849,9850],{},"字段",[505,9852,9853],{},"填写建议",[515,9855,9856,9866,9874,9882,9893,9906,9920],{},[502,9857,9858,9861],{},[520,9859,9860],{},"Title",[520,9862,9863,9864,620],{},"游戏名（如 ",[45,9865,1834],{},[502,9867,9868,9871],{},[520,9869,9870],{},"Project URL",[520,9872,9873],{},"URL slug（自动生成，可改）",[502,9875,9876,9879],{},[520,9877,9878],{},"Short description",[520,9880,9881],{},"一句话简介（搜索结果会显示）",[502,9883,9884,9887],{},[520,9885,9886],{},"Classification",[520,9888,9889,9890],{},"选 ",[45,9891,9892],{},"Games",[502,9894,9895,9898],{},[520,9896,9897],{},"Kind of project",[520,9899,9900,9905],{},[20,9901,9889,9902],{},[45,9903,9904],{},"HTML"," ← 关键！",[502,9907,9908,9911],{},[520,9909,9910],{},"Release status",[520,9912,9913,9916,9917],{},[45,9914,9915],{},"Released"," 或 ",[45,9918,9919],{},"Prototype",[502,9921,9922,9925],{},[520,9923,9924],{},"Pricing",[520,9926,9927,9930],{},[45,9928,9929],{},"$0 or donate"," （免费 \u002F 自愿打赏）",[25,9932,9934],{"id":9933},"上传文件-打包成-zip","上传文件 — 打包成 zip",[10,9936,9937,9938,298],{},"itch.io 要求 Web 游戏",[20,9939,9940],{},"打包成 zip 上传",[38,9942,9944],{"className":9695,"code":9943,"language":9697,"meta":43,"style":43},"# 进入 builds\u002Fweb 目录\ncd builds\u002Fweb\n\n# 打包成 zip（不要有外层文件夹，要 index.html 在 zip 根目录）\nzip -r ..\u002Fxggame-bird-web.zip .\n",[45,9945,9946,9951,9957,9961,9966],{"__ignoreMap":43},[48,9947,9948],{"class":50,"line":51},[48,9949,9950],{"class":170},"# 进入 builds\u002Fweb 目录\n",[48,9952,9953,9955],{"class":50,"line":62},[48,9954,9769],{"class":9768},[48,9956,9772],{"class":125},[48,9958,9959],{"class":50,"line":69},[48,9960,66],{"emptyLinePlaceholder":65},[48,9962,9963],{"class":50,"line":106},[48,9964,9965],{"class":170},"# 打包成 zip（不要有外层文件夹，要 index.html 在 zip 根目录）\n",[48,9967,9968,9971,9974,9977],{"class":50,"line":156},[48,9969,9970],{"class":76},"zip",[48,9972,9973],{"class":783}," -r",[48,9975,9976],{"class":125}," ..\u002Fxggame-bird-web.zip",[48,9978,9979],{"class":125}," .\n",[10,9981,9982,9983,9986,9987,9990],{},"或者直接在文件管理器里：选中 ",[45,9984,9985],{},"builds\u002Fweb"," 里的",[20,9988,9989],{},"所有文件","（注意是文件，不是文件夹）→ 右键 → 压缩为 zip。",[14,9992,9993],{},[10,9994,9577,9995,9998,9999,9286,10001,10004],{},[20,9996,9997],{},"常见坑","：zip 解压后必须直接看到 ",[45,10000,9571],{},[20,10002,10003],{},"不能有外层文件夹","。否则 itch.io 加载不出来。",[10,10006,10007,10008,194],{},"上传 zip 后，勾选 ",[20,10009,10010],{},"「This file will be played in the browser」",[25,10012,10014],{"id":10013},"配置嵌入设置embed-options","配置嵌入设置（Embed Options）",[10,10016,10017,10018,298],{},"下拉找到 ",[20,10019,10020],{},"Embed options",[496,10022,10023,10031],{},[499,10024,10025],{},[502,10026,10027,10029],{},[505,10028,9463],{},[505,10030,1937],{},[515,10032,10033,10044,10056],{},[502,10034,10035,10038],{},[520,10036,10037],{},"Viewport dimensions",[520,10039,10040,10043],{},[45,10041,10042],{},"608 × 1080","（见下方说明）",[502,10045,10046,10049],{},[520,10047,10048],{},"Frame options",[520,10050,10051,10052,10055],{},"一般勾 ",[45,10053,10054],{},"Fullscreen button"," 让玩家可全屏",[502,10057,10058,10061],{},[520,10059,10060],{},"Mobile friendly",[520,10062,10063],{},"如果适配了移动端，勾上",[14,10065,10066],{},[10,10067,10068,10071,10072,10075,10076,10079,10080,10083,10084,10087],{},[20,10069,10070],{},"这个数字怎么来的","：项目设置里视口是 ",[45,10073,10074],{},"720 × 1280","，但窗口高度覆盖设成了 ",[45,10077,10078],{},"1080","。\n按 9:16 等比换算，宽就是 ",[45,10081,10082],{},"720 ÷ 1280 × 1080 ≈ 608","。\n",[20,10085,10086],{},"关键是比例对，绝对值可以小一点"," —— 1280 高在笔记本上会超出屏幕，玩家要滚动才能看全。",[25,10089,10090],{"id":10090},"上传封面图和截图",[10,10092,10093,10094,298],{},"往下翻到 ",[20,10095,10096],{},"Cover image",[199,10098,10099,10111,10117],{},[202,10100,10101,10103,10104,9916,10107,10110],{},[20,10102,10096],{}," — 主图，建议 ",[20,10105,10106],{},"630 × 500",[20,10108,10109],{},"315 × 250","，会出现在 itch.io 列表里",[202,10112,10113,10116],{},[20,10114,10115],{},"Screenshots"," — 游戏截图，2-4 张就够了",[202,10118,10119,10122],{},[20,10120,10121],{},"Banner"," — 横幅图（可选）",[10,10124,10125,10126,10129],{},"封面图建议用游戏的",[20,10127,10128],{},"最高光时刻","做截图 + 加点文字，吸引点击。",[25,10131,10133],{"id":10132},"设置标签-分类","设置标签 + 分类",[10,10135,10136],{},"帮助玩家发现你的游戏：",[199,10138,10139,10147,10170,10178,10193],{},[202,10140,10141,1359,10144],{},[20,10142,10143],{},"Genre",[45,10145,10146],{},"Action",[202,10148,10149,1359,10152,1011,10155,1011,10158,1011,10161,1011,10164,1011,10167],{},[20,10150,10151],{},"Tags",[45,10153,10154],{},"flappy",[45,10156,10157],{},"casual",[45,10159,10160],{},"pixel-art",[45,10162,10163],{},"arcade",[45,10165,10166],{},"endless",[45,10168,10169],{},"godot",[202,10171,10172,1359,10175],{},[20,10173,10174],{},"Average session",[45,10176,10177],{},"A few minutes",[202,10179,10180,1359,10183,1011,10186,1011,10189,10192],{},[20,10181,10182],{},"Inputs",[45,10184,10185],{},"Mouse",[45,10187,10188],{},"Keyboard",[45,10190,10191],{},"Touchscreen","（如果支持）",[202,10194,10195,10198],{},[20,10196,10197],{},"Accessibility",": 看实际情况",[25,10200,10202],{"id":10201},"发布","发布！",[10,10204,10205,10206,10209],{},"设置好后，把 ",[20,10207,10208],{},"Visibility"," 改成：",[199,10211,10212,10218,10224],{},[202,10213,10214,10217],{},[20,10215,10216],{},"Public"," — 完全公开，任何人都能搜到",[202,10219,10220,10223],{},[20,10221,10222],{},"Restricted"," — 只有有链接的人能访问（适合先给朋友测试）",[202,10225,10226,10229],{},[20,10227,10228],{},"Draft"," — 草稿，只有自己能看",[10,10231,10232,10233,10236],{},"点击右下角 ",[20,10234,10235],{},"Save & view page","，欣赏一下自己的成果 🎉",[25,10238,10240],{"id":10239},"分享出去","分享出去！",[10,10242,10243],{},"把 itch.io 的链接发到：",[199,10245,10246,10249,10257,10265],{},[202,10247,10248],{},"朋友圈、Twitter、小红书、抖音视频简介",[202,10250,10251,10256],{},[32,10252,10255],{"href":10253,"rel":10254},"https:\u002F\u002Freddit.com\u002Fr\u002Fgodot",[1860],"r\u002Fgodot"," — Godot 社区",[202,10258,10259,10264],{},[32,10260,10263],{"href":10261,"rel":10262},"https:\u002F\u002Fitch.io\u002Fboard\u002F4\u002Frelease-announcements",[1860],"Itch.io 论坛"," — Release Announcements",[202,10266,10267],{},"Discord 游戏开发群",[10,10269,10270],{},"记得在我的博客评论区也贴一下你的 itch.io 链接，互相玩一下！🎮",[1794,10272],{},[25,10274,10276],{"id":10275},"整个系列到这就完结了","整个系列到这就完结了 🎉",[10,10278,10279],{},"回顾一下我们完成的事：",[199,10281,10282,10285,10288,10291,10294],{},[202,10283,10284],{},"✅ 从零搭建一个 Flappy Bird 类游戏",[202,10286,10287],{},"✅ 学会了 Godot 的核心概念：节点、场景、信号、Autoload、Parallax",[202,10289,10290],{},"✅ 实现了完整的游戏循环：菜单 → 游戏 → 结束 → 重开",[202,10292,10293],{},"✅ 加上美术 + 音效，让游戏有了\"灵魂\"",[202,10295,9631,10296,10299],{},[20,10297,10298],{},"公开发布","，全世界都能玩",[10,10301,10302,10303,10308],{},"如果你跟着做完了整个流程，请一定告诉我！可以发我作品链接 → ",[32,10304,10307],{"href":10305,"rel":10306},"https:\u002F\u002Ftwitter.com\u002FX_XXGGG",[1860],"Twitter @X_XXGGG"," \u002F 或博客评论。",[10,10310,10311],{},"下一个游戏，我们再战 💪",[1804,10313,10314],{},"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":43,"searchDepth":62,"depth":62,"links":10316},[10317,10318,10321,10324,10325,10326,10329,10330,10331,10332,10333,10334,10335],{"id":9407,"depth":62,"text":9408},{"id":9430,"depth":62,"text":9431,"children":10319},[10320],{"id":9454,"depth":69,"text":9454},{"id":9540,"depth":62,"text":9540,"children":10322},[10323],{"id":9584,"depth":69,"text":9585},{"id":9747,"depth":62,"text":9747},{"id":9815,"depth":62,"text":9816},{"id":9832,"depth":62,"text":9832,"children":10327},[10328],{"id":9841,"depth":69,"text":9841},{"id":9933,"depth":62,"text":9934},{"id":10013,"depth":62,"text":10014},{"id":10090,"depth":62,"text":10090},{"id":10132,"depth":62,"text":10133},{"id":10201,"depth":62,"text":10202},{"id":10239,"depth":62,"text":10240},{"id":10275,"depth":62,"text":10276},{},"\u002Fdevlog\u002Fxggame-bird\u002F09-export",{"title":9361,"description":1832},"devlog\u002Fxggame-bird\u002F09-export","7kR0-U83ZUcnBTtOk-3TlGWhgE2EQXxpHX1iIEI8rzc",{"id":4,"title":5,"body":10342,"cover":1830,"date":1831,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":11741,"navigation":65,"path":1837,"seo":11742,"stem":1839,"toc":65,"__hash__":1840},{"type":7,"value":10343,"toc":11722},[10344,10346,10352,10354,10358,10454,10458,10460,10476,10480,10484,10486,10488,10498,10502,10506,10518,10520,10524,10596,10600,10602,10604,10606,10620,10624,10628,10632,10638,10644,10646,10656,10662,10720,10724,10736,10742,10744,10748,10834,10838,10842,11040,11044,11046,11050,11052,11218,11224,11244,11258,11260,11466,11468,11500,11510,11518,11524,11528,11532,11536,11602,11604,11608,11612,11614,11656,11662,11664,11710,11714,11716,11720],[10,10345,12],{},[14,10347,10348],{},[10,10349,18,10350,23],{},[20,10351,22],{},[25,10353,27],{"id":27},[10,10355,30,10356,36],{},[32,10357,35],{"href":34},[38,10359,10360],{"className":40,"code":41,"language":42,"meta":43,"style":43},[45,10361,10362,10368,10372,10394,10430,10442],{"__ignoreMap":43},[48,10363,10364,10366],{"class":50,"line":51},[48,10365,55],{"class":54},[48,10367,59],{"class":58},[48,10369,10370],{"class":50,"line":62},[48,10371,66],{"emptyLinePlaceholder":65},[48,10373,10374,10376,10378,10380,10382,10384,10386,10388,10390,10392],{"class":50,"line":69},[48,10375,73],{"class":72},[48,10377,77],{"class":76},[48,10379,81],{"class":80},[48,10381,85],{"class":84},[48,10383,88],{"class":80},[48,10385,91],{"class":58},[48,10387,94],{"class":80},[48,10389,97],{"class":72},[48,10391,100],{"class":58},[48,10393,103],{"class":80},[48,10395,10396,10398,10400,10402,10404,10406,10408,10410,10412,10414,10416,10418,10420,10422,10424,10426,10428],{"class":50,"line":106},[48,10397,109],{"class":54},[48,10399,113],{"class":112},[48,10401,116],{"class":80},[48,10403,119],{"class":112},[48,10405,122],{"class":72},[48,10407,126],{"class":125},[48,10409,129],{"class":72},[48,10411,132],{"class":58},[48,10413,116],{"class":80},[48,10415,137],{"class":112},[48,10417,122],{"class":72},[48,10419,132],{"class":58},[48,10421,116],{"class":80},[48,10423,146],{"class":112},[48,10425,116],{"class":80},[48,10427,151],{"class":54},[48,10429,103],{"class":84},[48,10431,10432,10434,10436,10438,10440],{"class":50,"line":156},[48,10433,159],{"class":58},[48,10435,116],{"class":84},[48,10437,164],{"class":76},[48,10439,167],{"class":80},[48,10441,171],{"class":170},[48,10443,10444,10446,10448,10450,10452],{"class":50,"line":174},[48,10445,177],{"class":58},[48,10447,116],{"class":84},[48,10449,182],{"class":76},[48,10451,167],{"class":80},[48,10453,187],{"class":170},[10,10455,190,10456,194],{},[20,10457,193],{},[10,10459,197],{},[199,10461,10462,10468,10472],{},[202,10463,204,10464,208,10466],{},[20,10465,207],{},[45,10467,211],{},[202,10469,204,10470,217],{},[20,10471,216],{},[202,10473,204,10474,223],{},[20,10475,222],{},[10,10477,226,10478,230],{},[20,10479,229],{},[10,10481,233,10482,237],{},[20,10483,236],{},[25,10485,241],{"id":240},[10,10487,244],{},[199,10489,10490,10492,10496],{},[202,10491,249],{},[202,10493,252,10494,256],{},[20,10495,255],{},[202,10497,259],{},[10,10499,10500],{},[20,10501,264],{},[10,10503,267,10504,194],{},[20,10505,270],{},[199,10507,10508,10510,10514],{},[202,10509,275],{},[202,10511,278,10512],{},[20,10513,281],{},[202,10515,284,10516],{},[20,10517,287],{},[25,10519,291],{"id":290},[10,10521,294,10522,298],{},[45,10523,297],{},[38,10525,10526],{"className":40,"code":301,"language":42,"meta":43,"style":43},[45,10527,10528,10534,10538,10542,10546,10556,10564,10568,10578],{"__ignoreMap":43},[48,10529,10530,10532],{"class":50,"line":51},[48,10531,55],{"class":54},[48,10533,310],{"class":58},[48,10535,10536],{"class":50,"line":62},[48,10537,315],{"class":170},[48,10539,10540],{"class":50,"line":69},[48,10541,320],{"class":170},[48,10543,10544],{"class":50,"line":106},[48,10545,66],{"emptyLinePlaceholder":65},[48,10547,10548,10550,10552,10554],{"class":50,"line":156},[48,10549,329],{"class":76},[48,10551,81],{"class":80},[48,10553,334],{"class":125},[48,10555,337],{"class":80},[48,10557,10558,10560,10562],{"class":50,"line":174},[48,10559,342],{"class":72},[48,10561,345],{"class":76},[48,10563,348],{"class":170},[48,10565,10566],{"class":50,"line":351},[48,10567,66],{"emptyLinePlaceholder":65},[48,10569,10570,10572,10574,10576],{"class":50,"line":356},[48,10571,329],{"class":76},[48,10573,81],{"class":80},[48,10575,334],{"class":125},[48,10577,337],{"class":80},[48,10579,10580,10582,10584,10586,10588,10590,10592,10594],{"class":50,"line":367},[48,10581,342],{"class":72},[48,10583,372],{"class":76},[48,10585,81],{"class":80},[48,10587,377],{"class":84},[48,10589,88],{"class":80},[48,10591,382],{"class":58},[48,10593,94],{"class":80},[48,10595,387],{"class":170},[10,10597,390,10598,394],{},[20,10599,393],{},[396,10601,399],{"id":398},[10,10603,402],{},[10,10605,405],{},[199,10607,10608,10612,10616],{},[202,10609,10610,413],{},[20,10611,412],{},[202,10613,10614,419],{},[20,10615,418],{},[202,10617,422,10618],{},[20,10619,425],{},[396,10621,10622,432],{"id":428},[45,10623,431],{},[10,10625,435,10626,194],{},[20,10627,438],{},[10,10629,441,10630,445],{},[20,10631,444],{},[10,10633,10634,450,10636,194],{},[45,10635,431],{},[20,10637,453],{},[14,10639,10640],{},[10,10641,458,10642,462],{},[20,10643,461],{},[396,10645,466],{"id":465},[10,10647,469,10648,472,10650,476,10652,480,10654,194],{},[32,10649,35],{"href":34},[20,10651,475],{},[45,10653,479],{},[45,10655,483],{},[10,10657,486,10658,490,10660,494],{},[20,10659,489],{},[20,10661,493],{},[496,10663,10664,10674],{},[499,10665,10666],{},[502,10667,10668,10670,10672],{},[505,10669,507],{},[505,10671,510],{},[505,10673,513],{},[515,10675,10676,10688,10698,10710],{},[502,10677,10678,10680,10684],{},[520,10679,522],{},[520,10681,10682],{},[45,10683,483],{},[520,10685,529,10686,533],{},[20,10687,532],{},[502,10689,10690,10692,10696],{},[520,10691,538],{},[520,10693,10694],{},[45,10695,543],{},[520,10697,546],{},[502,10699,10700,10702,10706],{},[520,10701,551],{},[520,10703,10704],{},[45,10705,556],{},[520,10707,10708,562],{},[45,10709,561],{},[502,10711,10712,10714,10718],{},[520,10713,567],{},[520,10715,10716],{},[45,10717,572],{},[520,10719,575],{},[10,10721,10722],{},[20,10723,580],{},[10,10725,583,10726,586,10728,590,10730,593,10732,596,10734,600],{},[45,10727,561],{},[45,10729,589],{},[45,10731,561],{},[45,10733,483],{},[20,10735,599],{},[14,10737,10738],{},[10,10739,605,10740,609],{},[20,10741,608],{},[25,10743,612],{"id":612},[396,10745,616,10746,620],{"id":615},[45,10747,619],{},[38,10749,10750],{"className":40,"code":623,"language":42,"meta":43,"style":43},[45,10751,10752,10758,10762,10784,10820],{"__ignoreMap":43},[48,10753,10754,10756],{"class":50,"line":51},[48,10755,55],{"class":54},[48,10757,59],{"class":58},[48,10759,10760],{"class":50,"line":62},[48,10761,66],{"emptyLinePlaceholder":65},[48,10763,10764,10766,10768,10770,10772,10774,10776,10778,10780,10782],{"class":50,"line":69},[48,10765,73],{"class":72},[48,10767,77],{"class":76},[48,10769,81],{"class":80},[48,10771,85],{"class":84},[48,10773,88],{"class":80},[48,10775,91],{"class":58},[48,10777,94],{"class":80},[48,10779,97],{"class":72},[48,10781,100],{"class":58},[48,10783,103],{"class":80},[48,10785,10786,10788,10790,10792,10794,10796,10798,10800,10802,10804,10806,10808,10810,10812,10814,10816,10818],{"class":50,"line":106},[48,10787,109],{"class":54},[48,10789,113],{"class":112},[48,10791,116],{"class":80},[48,10793,119],{"class":112},[48,10795,122],{"class":72},[48,10797,126],{"class":125},[48,10799,129],{"class":72},[48,10801,132],{"class":58},[48,10803,116],{"class":80},[48,10805,137],{"class":112},[48,10807,122],{"class":72},[48,10809,132],{"class":58},[48,10811,116],{"class":80},[48,10813,146],{"class":112},[48,10815,116],{"class":80},[48,10817,151],{"class":54},[48,10819,103],{"class":84},[48,10821,10822,10824,10826,10828,10830,10832],{"class":50,"line":156},[48,10823,698],{"class":58},[48,10825,116],{"class":80},[48,10827,703],{"class":112},[48,10829,116],{"class":84},[48,10831,708],{"class":76},[48,10833,711],{"class":80},[10,10835,714,10836,718],{},[20,10837,717],{},[396,10839,722,10840,620],{"id":721},[45,10841,725],{},[38,10843,10844],{"className":40,"code":728,"language":42,"meta":43,"style":43},[45,10845,10846,10852,10856,10868,10886,10890,10912,10936,10952,10970,10976,10980,10984,11006,11022],{"__ignoreMap":43},[48,10847,10848,10850],{"class":50,"line":51},[48,10849,55],{"class":54},[48,10851,737],{"class":58},[48,10853,10854],{"class":50,"line":62},[48,10855,66],{"emptyLinePlaceholder":65},[48,10857,10858,10860,10862,10864,10866],{"class":50,"line":69},[48,10859,746],{"class":76},[48,10861,749],{"class":72},[48,10863,752],{"class":112},[48,10865,755],{"class":80},[48,10867,759],{"class":758},[48,10869,10870,10872,10874,10876,10878,10880,10882,10884],{"class":50,"line":106},[48,10871,764],{"class":76},[48,10873,749],{"class":72},[48,10875,769],{"class":112},[48,10877,88],{"class":80},[48,10879,774],{"class":58},[48,10881,777],{"class":80},[48,10883,780],{"class":54},[48,10885,784],{"class":783},[48,10887,10888],{"class":50,"line":156},[48,10889,66],{"emptyLinePlaceholder":65},[48,10891,10892,10894,10896,10898,10900,10902,10904,10906,10908,10910],{"class":50,"line":174},[48,10893,73],{"class":72},[48,10895,795],{"class":76},[48,10897,81],{"class":80},[48,10899,800],{"class":84},[48,10901,88],{"class":80},[48,10903,805],{"class":58},[48,10905,94],{"class":80},[48,10907,97],{"class":72},[48,10909,100],{"class":58},[48,10911,103],{"class":80},[48,10913,10914,10916,10918,10920,10922,10924,10926,10928,10930,10932,10934],{"class":50,"line":351},[48,10915,109],{"class":54},[48,10917,132],{"class":58},[48,10919,116],{"class":80},[48,10921,137],{"class":112},[48,10923,122],{"class":72},[48,10925,132],{"class":58},[48,10927,116],{"class":80},[48,10929,146],{"class":112},[48,10931,116],{"class":80},[48,10933,151],{"class":54},[48,10935,103],{"class":84},[48,10937,10938,10940,10942,10944,10946,10948,10950],{"class":50,"line":356},[48,10939,842],{"class":112},[48,10941,116],{"class":80},[48,10943,847],{"class":112},[48,10945,850],{"class":72},[48,10947,752],{"class":112},[48,10949,855],{"class":72},[48,10951,858],{"class":112},[48,10953,10954,10956,10958,10960,10962,10964,10966,10968],{"class":50,"line":367},[48,10955,863],{"class":54},[48,10957,866],{"class":112},[48,10959,116],{"class":80},[48,10961,847],{"class":112},[48,10963,873],{"class":72},[48,10965,876],{"class":72},[48,10967,879],{"class":758},[48,10969,103],{"class":84},[48,10971,10972,10974],{"class":50,"line":884},[48,10973,887],{"class":76},[48,10975,711],{"class":80},[48,10977,10978],{"class":50,"line":892},[48,10979,66],{"emptyLinePlaceholder":65},[48,10981,10982],{"class":50,"line":897},[48,10983,900],{"class":170},[48,10985,10986,10988,10990,10992,10994,10996,10998,11000,11002,11004],{"class":50,"line":903},[48,10987,73],{"class":72},[48,10989,908],{"class":76},[48,10991,81],{"class":80},[48,10993,85],{"class":84},[48,10995,88],{"class":80},[48,10997,91],{"class":58},[48,10999,94],{"class":80},[48,11001,97],{"class":72},[48,11003,100],{"class":58},[48,11005,103],{"class":80},[48,11007,11008,11010,11012,11014,11016,11018,11020],{"class":50,"line":927},[48,11009,109],{"class":54},[48,11011,113],{"class":112},[48,11013,116],{"class":80},[48,11015,119],{"class":112},[48,11017,122],{"class":72},[48,11019,126],{"class":125},[48,11021,103],{"class":84},[48,11023,11024,11026,11028,11030,11032,11034,11036,11038],{"class":50,"line":944},[48,11025,698],{"class":58},[48,11027,116],{"class":80},[48,11029,951],{"class":112},[48,11031,116],{"class":84},[48,11033,708],{"class":76},[48,11035,81],{"class":80},[48,11037,522],{"class":758},[48,11039,337],{"class":80},[10,11041,964,11042,194],{},[45,11043,708],{},[25,11045,969],{"id":969},[10,11047,972,11048],{},[20,11049,975],{},[396,11051,979],{"id":978},[38,11053,11054],{"className":40,"code":982,"language":42,"meta":43,"style":43},[45,11055,11056,11062,11066,11086,11100,11104,11118,11132,11144,11156,11160,11174,11178,11196,11200],{"__ignoreMap":43},[48,11057,11058,11060],{"class":50,"line":51},[48,11059,55],{"class":54},[48,11061,310],{"class":58},[48,11063,11064],{"class":50,"line":62},[48,11065,66],{"emptyLinePlaceholder":65},[48,11067,11068,11070,11072,11074,11076,11078,11080,11082,11084],{"class":50,"line":69},[48,11069,999],{"class":54},[48,11071,1002],{"class":58},[48,11073,1005],{"class":80},[48,11075,1008],{"class":112},[48,11077,1011],{"class":84},[48,11079,151],{"class":112},[48,11081,1011],{"class":84},[48,11083,1018],{"class":112},[48,11085,1021],{"class":80},[48,11087,11088,11090,11092,11094,11096,11098],{"class":50,"line":106},[48,11089,1026],{"class":72},[48,11091,1029],{"class":112},[48,11093,777],{"class":80},[48,11095,1002],{"class":58},[48,11097,116],{"class":80},[48,11099,1038],{"class":54},[48,11101,11102],{"class":50,"line":156},[48,11103,66],{"emptyLinePlaceholder":65},[48,11105,11106,11108,11110,11112,11114,11116],{"class":50,"line":174},[48,11107,1026],{"class":72},[48,11109,1049],{"class":112},[48,11111,88],{"class":80},[48,11113,382],{"class":58},[48,11115,777],{"class":80},[48,11117,1058],{"class":758},[48,11119,11120,11122,11124,11126,11128,11130],{"class":50,"line":351},[48,11121,1026],{"class":72},[48,11123,1065],{"class":112},[48,11125,88],{"class":80},[48,11127,382],{"class":58},[48,11129,777],{"class":80},[48,11131,1058],{"class":758},[48,11133,11134,11136,11138,11140,11142],{"class":50,"line":356},[48,11135,342],{"class":72},[48,11137,1080],{"class":76},[48,11139,81],{"class":80},[48,11141,1085],{"class":84},[48,11143,337],{"class":80},[48,11145,11146,11148,11150,11152,11154],{"class":50,"line":367},[48,11147,342],{"class":72},[48,11149,1094],{"class":76},[48,11151,81],{"class":80},[48,11153,1099],{"class":84},[48,11155,337],{"class":80},[48,11157,11158],{"class":50,"line":884},[48,11159,66],{"emptyLinePlaceholder":65},[48,11161,11162,11164,11166,11168,11170,11172],{"class":50,"line":892},[48,11163,73],{"class":72},[48,11165,1112],{"class":76},[48,11167,167],{"class":80},[48,11169,97],{"class":72},[48,11171,100],{"class":58},[48,11173,103],{"class":80},[48,11175,11176],{"class":50,"line":897},[48,11177,1125],{"class":170},[48,11179,11180,11182,11184,11186,11188,11190,11192,11194],{"class":50,"line":903},[48,11181,1130],{"class":58},[48,11183,116],{"class":80},[48,11185,951],{"class":112},[48,11187,116],{"class":84},[48,11189,1139],{"class":76},[48,11191,81],{"class":80},[48,11193,1144],{"class":112},[48,11195,337],{"class":80},[48,11197,11198],{"class":50,"line":927},[48,11199,1151],{"class":170},[48,11201,11202,11204,11206,11208,11210,11212,11214,11216],{"class":50,"line":944},[48,11203,1130],{"class":58},[48,11205,116],{"class":80},[48,11207,703],{"class":112},[48,11209,116],{"class":84},[48,11211,1139],{"class":76},[48,11213,81],{"class":80},[48,11215,182],{"class":112},[48,11217,337],{"class":80},[10,11219,11220,1174,11222,194],{},[45,11221,1139],{},[20,11223,1177],{},[199,11225,11226,11234,11238],{},[202,11227,11228,1185,11230,1188,11232],{},[45,11229,1184],{},[45,11231,1144],{},[20,11233,1191],{},[202,11235,1194,11236,1198],{},[45,11237,1197],{},[202,11239,1201,11240,1204,11242,1208],{},[45,11241,1144],{},[20,11243,1207],{},[10,11245,1211,11246,298,11248,1217,11250,1221,11252,1224,11254,1227,11256,194],{},[20,11247,1214],{},[45,11249,951],{},[45,11251,1220],{},[45,11253,522],{},[45,11255,1144],{},[45,11257,1230],{},[396,11259,1234],{"id":1233},[38,11261,11262],{"className":40,"code":1237,"language":42,"meta":43,"style":43},[45,11263,11264,11270,11274,11292,11310,11328,11332,11346,11350,11378,11382,11400,11404,11412,11422,11426,11434,11444,11448,11456],{"__ignoreMap":43},[48,11265,11266,11268],{"class":50,"line":51},[48,11267,55],{"class":54},[48,11269,310],{"class":58},[48,11271,11272],{"class":50,"line":62},[48,11273,66],{"emptyLinePlaceholder":65},[48,11275,11276,11278,11280,11282,11284,11286,11288,11290],{"class":50,"line":69},[48,11277,764],{"class":76},[48,11279,749],{"class":72},[48,11281,1258],{"class":112},[48,11283,88],{"class":80},[48,11285,1263],{"class":58},[48,11287,777],{"class":80},[48,11289,780],{"class":54},[48,11291,1270],{"class":783},[48,11293,11294,11296,11298,11300,11302,11304,11306,11308],{"class":50,"line":106},[48,11295,764],{"class":76},[48,11297,749],{"class":72},[48,11299,1279],{"class":112},[48,11301,88],{"class":80},[48,11303,1263],{"class":58},[48,11305,777],{"class":80},[48,11307,780],{"class":54},[48,11309,1290],{"class":783},[48,11311,11312,11314,11316,11318,11320,11322,11324,11326],{"class":50,"line":156},[48,11313,764],{"class":76},[48,11315,749],{"class":72},[48,11317,1299],{"class":112},[48,11319,88],{"class":80},[48,11321,1263],{"class":58},[48,11323,777],{"class":80},[48,11325,780],{"class":54},[48,11327,1310],{"class":783},[48,11329,11330],{"class":50,"line":174},[48,11331,66],{"emptyLinePlaceholder":65},[48,11333,11334,11336,11338,11340,11342,11344],{"class":50,"line":351},[48,11335,73],{"class":72},[48,11337,1112],{"class":76},[48,11339,167],{"class":80},[48,11341,97],{"class":72},[48,11343,100],{"class":58},[48,11345,103],{"class":80},[48,11347,11348],{"class":50,"line":356},[48,11349,1333],{"class":170},[48,11351,11352,11354,11356,11358,11360,11362,11364,11366,11368,11370,11372,11374,11376],{"class":50,"line":367},[48,11353,1130],{"class":58},[48,11355,116],{"class":80},[48,11357,951],{"class":112},[48,11359,116],{"class":84},[48,11361,1139],{"class":76},[48,11363,81],{"class":80},[48,11365,73],{"class":72},[48,11367,81],{"class":80},[48,11369,1354],{"class":84},[48,11371,94],{"class":80},[48,11373,1359],{"class":84},[48,11375,1362],{"class":76},[48,11377,1365],{"class":80},[48,11379,11380],{"class":50,"line":884},[48,11381,1370],{"class":170},[48,11383,11384,11386,11388,11390,11392,11394,11396,11398],{"class":50,"line":892},[48,11385,1130],{"class":58},[48,11387,116],{"class":80},[48,11389,703],{"class":112},[48,11391,116],{"class":84},[48,11393,1139],{"class":76},[48,11395,81],{"class":80},[48,11397,164],{"class":112},[48,11399,337],{"class":80},[48,11401,11402],{"class":50,"line":897},[48,11403,66],{"emptyLinePlaceholder":65},[48,11405,11406,11408,11410],{"class":50,"line":903},[48,11407,73],{"class":72},[48,11409,1399],{"class":76},[48,11411,1402],{"class":80},[48,11413,11414,11416,11418,11420],{"class":50,"line":927},[48,11415,1407],{"class":112},[48,11417,116],{"class":84},[48,11419,1412],{"class":76},[48,11421,711],{"class":80},[48,11423,11424],{"class":50,"line":944},[48,11425,66],{"emptyLinePlaceholder":65},[48,11427,11428,11430,11432],{"class":50,"line":1421},[48,11429,73],{"class":72},[48,11431,1426],{"class":76},[48,11433,1402],{"class":80},[48,11435,11436,11438,11440,11442],{"class":50,"line":1431},[48,11437,1434],{"class":112},[48,11439,116],{"class":84},[48,11441,1412],{"class":76},[48,11443,711],{"class":80},[48,11445,11446],{"class":50,"line":1443},[48,11447,66],{"emptyLinePlaceholder":65},[48,11449,11450,11452,11454],{"class":50,"line":1448},[48,11451,73],{"class":72},[48,11453,1453],{"class":76},[48,11455,1402],{"class":80},[48,11457,11458,11460,11462,11464],{"class":50,"line":1458},[48,11459,1461],{"class":112},[48,11461,116],{"class":84},[48,11463,1412],{"class":76},[48,11465,711],{"class":80},[10,11467,1470],{},[38,11469,11470],{"className":40,"code":1473,"language":42,"meta":43,"style":43},[45,11471,11472],{"__ignoreMap":43},[48,11473,11474,11476,11478,11480,11482,11484,11486,11488,11490,11492,11494,11496,11498],{"class":50,"line":51},[48,11475,483],{"class":58},[48,11477,116],{"class":80},[48,11479,951],{"class":112},[48,11481,116],{"class":84},[48,11483,1139],{"class":76},[48,11485,81],{"class":80},[48,11487,73],{"class":72},[48,11489,81],{"class":80},[48,11491,1354],{"class":84},[48,11493,94],{"class":80},[48,11495,1359],{"class":84},[48,11497,1362],{"class":76},[48,11499,1365],{"class":80},[10,11501,11502,1508,11504,1512,11506,1516,11508,1520],{},[45,11503,951],{},[20,11505,1511],{},[45,11507,1515],{},[20,11509,1519],{},[10,11511,1523,11512,1527,11514,1531,11516,194],{},[20,11513,1526],{},[45,11515,1530],{},[45,11517,1515],{},[10,11519,1536,11520,1539,11522,1543],{},[45,11521,1354],{},[20,11523,1542],{},[25,11525,1547,11526,1551],{"id":1546},[20,11527,1550],{},[10,11529,1554,11530,194],{},[20,11531,1557],{},[10,11533,1560,11534,1564],{},[45,11535,1563],{},[38,11537,11538],{"className":40,"code":1567,"language":42,"meta":43,"style":43},[45,11539,11540,11578,11590],{"__ignoreMap":43},[48,11541,11542,11544,11546,11548,11550,11552,11554,11556,11558,11560,11562,11564,11566,11568,11570,11572,11574,11576],{"class":50,"line":51},[48,11543,1574],{"class":54},[48,11545,1577],{"class":58},[48,11547,116],{"class":84},[48,11549,1582],{"class":76},[48,11551,81],{"class":80},[48,11553,1587],{"class":125},[48,11555,94],{"class":80},[48,11557,129],{"class":72},[48,11559,132],{"class":58},[48,11561,116],{"class":80},[48,11563,137],{"class":112},[48,11565,122],{"class":72},[48,11567,132],{"class":58},[48,11569,116],{"class":80},[48,11571,146],{"class":112},[48,11573,116],{"class":80},[48,11575,151],{"class":54},[48,11577,103],{"class":84},[48,11579,11580,11582,11584,11586,11588],{"class":50,"line":62},[48,11581,1616],{"class":112},[48,11583,116],{"class":80},[48,11585,1621],{"class":112},[48,11587,777],{"class":80},[48,11589,1626],{"class":112},[48,11591,11592,11594,11596,11598,11600],{"class":50,"line":69},[48,11593,1631],{"class":58},[48,11595,116],{"class":84},[48,11597,1636],{"class":76},[48,11599,167],{"class":80},[48,11601,1641],{"class":170},[10,11603,1644],{},[10,11605,1647,11606,1651],{},[20,11607,1650],{},[10,11609,1654,11610,1658],{},[20,11611,1657],{},[10,11613,1661],{},[496,11615,11616,11624],{},[499,11617,11618],{},[502,11619,11620,11622],{},[505,11621,1670],{},[505,11623,1673],{},[515,11625,11626,11634,11642,11648],{},[502,11627,11628,11632],{},[520,11629,1680,11630,1684],{},[20,11631,1683],{},[520,11633,1687],{},[502,11635,11636,11640],{},[520,11637,1680,11638,1695],{},[20,11639,1694],{},[520,11641,1698],{},[502,11643,11644,11646],{},[520,11645,1703],{},[520,11647,1698],{},[502,11649,11650,11652],{},[520,11651,1710],{},[520,11653,1713,11654,1716],{},[45,11655,342],{},[14,11657,11658],{},[10,11659,1721,11660],{},[20,11661,1724],{},[25,11663,1728],{"id":1727},[496,11665,11666,11676],{},[499,11667,11668],{},[502,11669,11670,11672,11674],{},[505,11671],{},[505,11673,1739],{},[505,11675,1742],{},[515,11677,11678,11688,11700],{},[502,11679,11680,11682,11684],{},[520,11681,1749],{},[520,11683,1752],{},[520,11685,11686],{},[20,11687,1757],{},[502,11689,11690,11692,11696],{},[520,11691,1762],{},[520,11693,1765,11694],{},[45,11695,619],{},[520,11697,11698],{},[20,11699,1772],{},[502,11701,11702,11704,11706],{},[520,11703,1777],{},[520,11705,1780],{},[520,11707,11708],{},[20,11709,1785],{},[10,11711,1788,11712,1792],{},[20,11713,1791],{},[1794,11715],{},[10,11717,1798,11718,1802],{},[20,11719,1801],{},[1804,11721,1806],{},{"title":43,"searchDepth":62,"depth":62,"links":11723},[11724,11725,11726,11731,11735,11739,11740],{"id":27,"depth":62,"text":27},{"id":240,"depth":62,"text":241},{"id":290,"depth":62,"text":291,"children":11727},[11728,11729,11730],{"id":398,"depth":69,"text":399},{"id":428,"depth":69,"text":1815},{"id":465,"depth":69,"text":466},{"id":612,"depth":62,"text":612,"children":11732},[11733,11734],{"id":615,"depth":69,"text":1820},{"id":721,"depth":69,"text":1822},{"id":969,"depth":62,"text":969,"children":11736},[11737,11738],{"id":978,"depth":69,"text":979},{"id":1233,"depth":69,"text":1234},{"id":1546,"depth":62,"text":1828},{"id":1727,"depth":62,"text":1728},{},{"title":5,"description":1832},{"id":11744,"title":11745,"body":11746,"cover":1830,"date":1831,"description":1832,"extension":1833,"game":1834,"github":1835,"icon":1830,"meta":13089,"navigation":65,"path":13090,"seo":13091,"stem":13092,"toc":65,"__hash__":13093},"devlog\u002Fdevlog\u002Fxggame-bird\u002F11-save-system.md","存档系统 SaveManager",{"type":7,"value":11747,"toc":13064},[11748,11751,11757,11761,11764,11783,11802,11809,11819,11822,11872,11880,11896,11906,11913,11970,11979,11991,11995,12000,12298,12303,12329,12332,12338,12347,12355,12362,12403,12416,12437,12446,12455,12462,12485,12488,12497,12504,12510,12526,12537,12540,12570,12587,12608,12626,12633,12639,12670,12676,12695,12699,12702,12837,12844,12859,12866,12880,12883,12927,12930,12938,12942,12947,12954,12976,12990,12993,13023,13033,13035,13038,13058,13061],[10,11749,11750],{},"游戏里已经有「最高分」了 — 但你关掉游戏再打开，它就变回 0 了 😢",[10,11752,11753,11754,194],{},"这一章做一个最小的存档系统，让最高分",[20,11755,11756],{},"真正留在硬盘上",[25,11758,11760],{"id":11759},"为什么现在存不住","为什么现在存不住？",[10,11762,11763],{},"回头看 GameManager 里的这行：",[38,11765,11767],{"className":40,"code":11766,"language":42,"meta":43,"style":43},"var high_score: int = 0\n",[45,11768,11769],{"__ignoreMap":43},[48,11770,11771,11773,11775,11777,11779,11781],{"class":50,"line":51},[48,11772,1026],{"class":72},[48,11774,1065],{"class":112},[48,11776,88],{"class":80},[48,11778,382],{"class":58},[48,11780,777],{"class":80},[48,11782,1058],{"class":758},[10,11784,11785,11786,11789,11790,11793,11794,11797,11798,11801],{},"变量是存在",[20,11787,11788],{},"内存","里的。内存的特点就是",[20,11791,11792],{},"断电即失"," — 游戏进程一关，这个 ",[45,11795,11796],{},"0"," 和你打出来的 ",[45,11799,11800],{},"999"," 一起烟消云散。",[10,11803,11804,11805,11808],{},"想留住它，就得写到",[20,11806,11807],{},"硬盘","上的一个文件里。",[25,11810,11812,3978,11815,11818],{"id":11811},"res-和-user-别写错地方",[45,11813,11814],{},"res:\u002F\u002F",[45,11816,11817],{},"user:\u002F\u002F"," — 别写错地方",[10,11820,11821],{},"Godot 里有两个特殊路径前缀，这是新手第一个坑：",[496,11823,11824,11836],{},[499,11825,11826],{},[502,11827,11828,11830,11833],{},[505,11829,5710],{},[505,11831,11832],{},"指向哪",[505,11834,11835],{},"能不能写",[515,11837,11838,11857],{},[502,11839,11840,11844,11851],{},[520,11841,11842],{},[45,11843,11814],{},[520,11845,11846,11847,11850],{},"你的",[20,11848,11849],{},"项目文件夹","（游戏本体）",[520,11852,11853,11854],{},"❌ 导出后",[20,11855,11856],{},"只读",[502,11858,11859,11863,11869],{},[520,11860,11861],{},[45,11862,11817],{},[520,11864,11865,11866],{},"玩家电脑上的",[20,11867,11868],{},"用户数据目录",[520,11870,11871],{},"✅ 可读可写",[10,11873,11874],{},[20,11875,11876,11877,11879],{},"为什么 ",[45,11878,11814],{}," 不能写？",[10,11881,11882,11883,11885,11886,11888,11889,11892,11893,11895],{},"游戏导出之后，所有 ",[45,11884,11814],{}," 里的东西会被打包进一个 ",[45,11887,9628],{}," 文件（你在 ",[32,11890,11891],{"href":10337},"第 9 章"," 导出时见过它）。打包好的文件是",[20,11894,11856],{},"的 — 就像你不能修改一个已经刻好的光盘。",[10,11897,11898,11899,11901,11902,11905],{},"在编辑器里测试的时候 ",[45,11900,11814],{}," 可能写得进去，",[20,11903,11904],{},"但导出之后必定失败"," — 这个坑特别隐蔽，一定要记住。",[10,11907,11908],{},[20,11909,11910,11912],{},[45,11911,11817],{}," 实际在哪？",[496,11914,11915,11925],{},[499,11916,11917],{},[502,11918,11919,11922],{},[505,11920,11921],{},"平台",[505,11923,11924],{},"真实路径",[515,11926,11927,11937,11947,11957],{},[502,11928,11929,11932],{},[520,11930,11931],{},"Windows",[520,11933,11934],{},[45,11935,11936],{},"%APPDATA%\\Godot\\app_userdata\\XGGame-Bird\\",[502,11938,11939,11942],{},[520,11940,11941],{},"macOS",[520,11943,11944],{},[45,11945,11946],{},"~\u002FLibrary\u002FApplication Support\u002FGodot\u002Fapp_userdata\u002FXGGame-Bird\u002F",[502,11948,11949,11952],{},[520,11950,11951],{},"Linux",[520,11953,11954],{},[45,11955,11956],{},"~\u002F.local\u002Fshare\u002Fgodot\u002Fapp_userdata\u002FXGGame-Bird\u002F",[502,11958,11959,11963],{},[520,11960,11961],{},[20,11962,9444],{},[520,11964,11965,11966,11969],{},"浏览器的 ",[20,11967,11968],{},"IndexedDB","（不是真文件）",[14,11971,11972],{},[10,11973,11974,11975,11978],{},"编辑器里可以直接",[20,11976,11977],{},"项目 → 打开用户数据文件夹","跳过去看，调试存档很方便。",[14,11980,11981],{},[10,11982,9577,11983,11986,11987,11990],{},[20,11984,11985],{},"Web 版要注意","：存档在浏览器的 IndexedDB 里，玩家",[20,11988,11989],{},"清缓存 \u002F 换浏览器 \u002F 用无痕模式","，存档就没了。我们这个游戏只存一个最高分，丢了无所谓；但如果以后做长流程游戏要上 Web，就得考虑云存档了。",[25,11992,11994],{"id":11993},"创建-savemanager","创建 SaveManager",[10,11996,294,11997,298],{},[45,11998,11999],{},"scripts\u002Fautoload\u002Fsave_manager.gd",[38,12001,12003],{"className":40,"code":12002,"language":42,"meta":43,"style":43},"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",[45,12004,12005,12011,12016,12020,12033,12037,12052,12064,12081,12086,12118,12146,12150,12165,12186,12194,12223,12250,12263,12270],{"__ignoreMap":43},[48,12006,12007,12009],{"class":50,"line":51},[48,12008,55],{"class":54},[48,12010,310],{"class":58},[48,12012,12013],{"class":50,"line":62},[48,12014,12015],{"class":170},"## 存档管理：读写 user:\u002F\u002Fsave.json\n",[48,12017,12018],{"class":50,"line":69},[48,12019,66],{"emptyLinePlaceholder":65},[48,12021,12022,12025,12028,12030],{"class":50,"line":106},[48,12023,12024],{"class":72},"const",[48,12026,12027],{"class":112}," SAVE_PATH",[48,12029,755],{"class":80},[48,12031,12032],{"class":125}," \"user:\u002F\u002Fsave.json\"\n",[48,12034,12035],{"class":50,"line":156},[48,12036,66],{"emptyLinePlaceholder":65},[48,12038,12039,12041,12044,12046,12048,12050],{"class":50,"line":174},[48,12040,73],{"class":72},[48,12042,12043],{"class":76}," save_game",[48,12045,167],{"class":80},[48,12047,97],{"class":72},[48,12049,100],{"class":58},[48,12051,103],{"class":80},[48,12053,12054,12056,12059,12061],{"class":50,"line":351},[48,12055,3599],{"class":72},[48,12057,12058],{"class":112}," data",[48,12060,755],{"class":80},[48,12062,12063],{"class":80}," {\n",[48,12065,12066,12069,12071,12073,12075,12078],{"class":50,"line":356},[48,12067,12068],{"class":125},"        \"high_score\"",[48,12070,1359],{"class":84},[48,12072,556],{"class":58},[48,12074,116],{"class":84},[48,12076,12077],{"class":112},"high_score",[48,12079,12080],{"class":84},",\n",[48,12082,12083],{"class":50,"line":367},[48,12084,12085],{"class":80},"    }\n",[48,12087,12088,12090,12092,12094,12097,12099,12102,12104,12107,12109,12111,12113,12116],{"class":50,"line":884},[48,12089,3599],{"class":72},[48,12091,9707],{"class":112},[48,12093,755],{"class":80},[48,12095,12096],{"class":58}," FileAccess",[48,12098,116],{"class":84},[48,12100,12101],{"class":76},"open",[48,12103,81],{"class":80},[48,12105,12106],{"class":112},"SAVE_PATH",[48,12108,3717],{"class":80},[48,12110,12096],{"class":58},[48,12112,116],{"class":80},[48,12114,12115],{"class":54},"WRITE",[48,12117,337],{"class":80},[48,12119,12120,12123,12125,12128,12130,12133,12135,12138,12140,12143],{"class":50,"line":892},[48,12121,12122],{"class":112},"    file",[48,12124,116],{"class":84},[48,12126,12127],{"class":76},"store_string",[48,12129,81],{"class":80},[48,12131,12132],{"class":58},"JSON",[48,12134,116],{"class":84},[48,12136,12137],{"class":76},"stringify",[48,12139,81],{"class":80},[48,12141,12142],{"class":112},"data",[48,12144,12145],{"class":80},"))\n",[48,12147,12148],{"class":50,"line":897},[48,12149,66],{"emptyLinePlaceholder":65},[48,12151,12152,12154,12157,12159,12161,12163],{"class":50,"line":903},[48,12153,73],{"class":72},[48,12155,12156],{"class":76}," load_game",[48,12158,167],{"class":80},[48,12160,97],{"class":72},[48,12162,100],{"class":58},[48,12164,103],{"class":80},[48,12166,12167,12169,12171,12173,12175,12178,12180,12182,12184],{"class":50,"line":927},[48,12168,109],{"class":54},[48,12170,2296],{"class":72},[48,12172,12096],{"class":58},[48,12174,116],{"class":84},[48,12176,12177],{"class":76},"file_exists",[48,12179,81],{"class":80},[48,12181,12106],{"class":112},[48,12183,94],{"class":80},[48,12185,103],{"class":84},[48,12187,12188,12191],{"class":50,"line":944},[48,12189,12190],{"class":54},"        return",[48,12192,12193],{"class":170},"  # 首次游玩，没有存档，用默认值\n",[48,12195,12196,12198,12200,12202,12204,12206,12208,12210,12212,12214,12216,12218,12221],{"class":50,"line":1421},[48,12197,3599],{"class":72},[48,12199,9707],{"class":112},[48,12201,755],{"class":80},[48,12203,12096],{"class":58},[48,12205,116],{"class":84},[48,12207,12101],{"class":76},[48,12209,81],{"class":80},[48,12211,12106],{"class":112},[48,12213,3717],{"class":80},[48,12215,12096],{"class":58},[48,12217,116],{"class":80},[48,12219,12220],{"class":54},"READ",[48,12222,337],{"class":80},[48,12224,12225,12227,12229,12231,12234,12236,12239,12241,12243,12245,12248],{"class":50,"line":1431},[48,12226,3599],{"class":72},[48,12228,12058],{"class":112},[48,12230,777],{"class":80},[48,12232,12233],{"class":58}," JSON",[48,12235,116],{"class":84},[48,12237,12238],{"class":76},"parse_string",[48,12240,81],{"class":80},[48,12242,9691],{"class":112},[48,12244,116],{"class":84},[48,12246,12247],{"class":76},"get_as_text",[48,12249,1365],{"class":80},[48,12251,12252,12254,12256,12258,12261],{"class":50,"line":1443},[48,12253,109],{"class":54},[48,12255,12058],{"class":112},[48,12257,122],{"class":72},[48,12259,12260],{"class":54}," null",[48,12262,103],{"class":84},[48,12264,12265,12267],{"class":50,"line":1448},[48,12266,12190],{"class":54},[48,12268,12269],{"class":170},"  # 存档损坏，防崩\n",[48,12271,12272,12274,12276,12278,12280,12282,12284,12287,12289,12292,12294,12296],{"class":50,"line":1458},[48,12273,7080],{"class":58},[48,12275,116],{"class":80},[48,12277,12077],{"class":112},[48,12279,777],{"class":80},[48,12281,12058],{"class":112},[48,12283,116],{"class":84},[48,12285,12286],{"class":76},"get",[48,12288,81],{"class":80},[48,12290,12291],{"class":125},"\"high_score\"",[48,12293,3717],{"class":80},[48,12295,3628],{"class":758},[48,12297,337],{"class":80},[10,12299,12300,12301,194],{},"然后照例注册 Autoload，命名 ",[45,12302,543],{},[14,12304,12305],{},[10,12306,12307,298,12310,12312,12313,1188,12315,12318,12319,12321,12322,12325,12326,194],{},[20,12308,12309],{},"顺序很重要",[45,12311,543],{}," 必须排在 ",[45,12314,556],{},[20,12316,12317],{},"上面"," — 因为下面我们会在 GameManager 的 ",[45,12320,561],{}," 里调用 ",[45,12323,12324],{},"SaveManager.load_game()","。完整顺序见 ",[32,12327,12328],{"href":1837},"上一章的表格",[25,12330,12331],{"id":12331},"逐行拆解",[396,12333,12335],{"id":12334},"const-save_path-usersavejson",[45,12336,12337],{},"const SAVE_PATH := \"user:\u002F\u002Fsave.json\"",[10,12339,12340,12342,12343,12346],{},[45,12341,12024],{}," 是",[20,12344,12345],{},"常量"," — 定义之后不能改。",[10,12348,12349,12350,8658,12352,12354],{},"存档路径这种\"全文件都要用、但永远不会变\"的值，用 ",[45,12351,12024],{},[45,12353,1026],{},"：写错了编辑器会直接报错，而不是等到运行时才发现路径拼错。",[396,12356,12358,12361],{"id":12357},"是什么",[45,12359,12360],{},":="," 是什么？",[38,12363,12365],{"className":40,"code":12364,"language":42,"meta":43,"style":43},"var data := { ... }     # 用了 :=\nvar data = { ... }      # 没用\n",[45,12366,12367,12386],{"__ignoreMap":43},[48,12368,12369,12371,12373,12375,12377,12380,12383],{"class":50,"line":51},[48,12370,1026],{"class":72},[48,12372,12058],{"class":112},[48,12374,755],{"class":80},[48,12376,1005],{"class":80},[48,12378,12379],{"class":84}," ... ",[48,12381,12382],{"class":80},"}",[48,12384,12385],{"class":170},"     # 用了 :=\n",[48,12387,12388,12390,12392,12394,12396,12398,12400],{"class":50,"line":62},[48,12389,1026],{"class":72},[48,12391,12058],{"class":112},[48,12393,777],{"class":80},[48,12395,1005],{"class":80},[48,12397,12379],{"class":84},[48,12399,12382],{"class":80},[48,12401,12402],{"class":170},"      # 没用\n",[10,12404,12405,12407,12408,12411,12412,12415],{},[45,12406,12360],{}," 叫",[20,12409,12410],{},"类型推断赋值","。意思是「变量的类型",[20,12413,12414],{},"由右边的值自动决定","，之后就锁死了」。",[199,12417,12418,12431],{},[202,12419,12420,12423,12424,12426,12427,12430],{},[45,12421,12422],{},"var data := {}"," → Godot 自动判定 ",[45,12425,12142],{}," 是 ",[45,12428,12429],{},"Dictionary","，以后你想给它塞个数字，编辑器直接报错",[202,12432,12433,12436],{},[45,12434,12435],{},"var data = {}"," → 类型随便变，写错了要等运行时才炸",[10,12438,12439,12445],{},[20,12440,12441,12442,12444],{},"能用 ",[45,12443,12360],{}," 就用","，等于免费多一层错误检查。",[396,12447,12449,3978,12452],{"id":12448},"jsonstringify-和-jsonparse_string",[45,12450,12451],{},"JSON.stringify()",[45,12453,12454],{},"JSON.parse_string()",[10,12456,12457,12458,12461],{},"硬盘上的文件本质是",[20,12459,12460],{},"一串文本","，没法直接存 Godot 的字典对象。所以要转换：",[199,12463,12464,12476],{},[202,12465,12466,12469,12470,298,12473],{},[45,12467,12468],{},"JSON.stringify(data)"," — 把字典",[20,12471,12472],{},"变成文本",[45,12474,12475],{},"{\"high_score\":42}",[202,12477,12478,12481,12482],{},[45,12479,12480],{},"JSON.parse_string(文本)"," — 把文本",[20,12483,12484],{},"变回字典",[10,12486,12487],{},"一存一取，正好是一对反操作。",[14,12489,12490],{},[10,12491,12492,12493,12496],{},"为什么选 JSON 而不是 Godot 自己的二进制格式？因为 JSON 是",[20,12494,12495],{},"纯文本","，你可以直接用记事本打开看里面存了啥 — 调试的时候方便太多了。",[396,12498,12500,12501,4568],{"id":12499},"为什么没有-fileclose","为什么没有 ",[45,12502,12503],{},"file.close()",[10,12505,12506,12507,12509],{},"Godot 3 里必须手动 ",[45,12508,12503],{},"，忘了就可能丢数据。",[10,12511,12512,6011,12515,12518,12519,12522,12523,12525],{},[20,12513,12514],{},"Godot 4 不用了",[45,12516,12517],{},"FileAccess"," 用的是",[20,12520,12521],{},"引用计数","：当 ",[45,12524,9691],{}," 这个变量随着函数结束而消失时，Godot 会自动帮你关闭并写入。",[10,12527,12528,12529,12532,12533,12536],{},"不过如果你想",[20,12530,12531],{},"立刻","确保写进硬盘（比如后面还要马上读它），可以显式写一句 ",[45,12534,12535],{},"file = null"," 来提前触发关闭。",[396,12538,12539],{"id":12539},"两道防崩保险",[38,12541,12543],{"className":40,"code":12542,"language":42,"meta":43,"style":43},"if not FileAccess.file_exists(SAVE_PATH):\n    return\n",[45,12544,12545,12565],{"__ignoreMap":43},[48,12546,12547,12549,12551,12553,12555,12557,12559,12561,12563],{"class":50,"line":51},[48,12548,1574],{"class":54},[48,12550,2296],{"class":72},[48,12552,12096],{"class":58},[48,12554,116],{"class":84},[48,12556,12177],{"class":76},[48,12558,81],{"class":80},[48,12560,12106],{"class":112},[48,12562,94],{"class":80},[48,12564,103],{"class":84},[48,12566,12567],{"class":50,"line":62},[48,12568,12569],{"class":54},"    return\n",[10,12571,12572,12575,12576,12578,12579,12582,12583,12586],{},[20,12573,12574],{},"第一次玩游戏时存档文件根本不存在","。不判断就直接 ",[45,12577,12101],{},"，会拿到一个 ",[45,12580,12581],{},"null","，下一行 ",[45,12584,12585],{},"file.get_as_text()"," 立刻崩。",[38,12588,12590],{"className":40,"code":12589,"language":42,"meta":43,"style":43},"if data == null:\n    return\n",[45,12591,12592,12604],{"__ignoreMap":43},[48,12593,12594,12596,12598,12600,12602],{"class":50,"line":51},[48,12595,1574],{"class":54},[48,12597,12058],{"class":112},[48,12599,122],{"class":72},[48,12601,12260],{"class":54},[48,12603,103],{"class":84},[48,12605,12606],{"class":50,"line":62},[48,12607,12569],{"class":54},[10,12609,12610,12611,12614,12615,12618,12619,12621,12622,12625],{},"存档文件",[20,12612,12613],{},"存在但内容坏了","（写到一半断电、玩家手贱改坏了），",[45,12616,12617],{},"JSON.parse_string"," 会返回 ",[45,12620,12581],{},"。不判断的话下一行 ",[45,12623,12624],{},"data.get(...)"," 一样崩。",[10,12627,12628,12629,12632],{},"这两个判断的效果是：",[20,12630,12631],{},"读档失败就静悄悄用默认值继续玩","，而不是弹一个报错把玩家挡在门外。对一个休闲小游戏来说，这是对的取舍。",[396,12634,12636],{"id":12635},"datagethigh_score-0",[45,12637,12638],{},"data.get(\"high_score\", 0)",[38,12640,12642],{"className":40,"code":12641,"language":42,"meta":43,"style":43},"GameManager.high_score = data.get(\"high_score\", 0)\n",[45,12643,12644],{"__ignoreMap":43},[48,12645,12646,12648,12650,12652,12654,12656,12658,12660,12662,12664,12666,12668],{"class":50,"line":51},[48,12647,556],{"class":58},[48,12649,116],{"class":80},[48,12651,12077],{"class":112},[48,12653,777],{"class":80},[48,12655,12058],{"class":112},[48,12657,116],{"class":84},[48,12659,12286],{"class":76},[48,12661,81],{"class":80},[48,12663,12291],{"class":125},[48,12665,3717],{"class":80},[48,12667,3628],{"class":758},[48,12669,337],{"class":80},[10,12671,12672,12675],{},[45,12673,12674],{},".get(键, 默认值)"," — 取不到就用默认值。",[10,12677,12678,12679,12682,12683,12686,12687,12690,12691,12694],{},"这一手是为了",[20,12680,12681],{},"向后兼容","：假如以后你给存档加了新字段（比如 ",[45,12684,12685],{},"total_play_count","），老玩家的存档文件里没有这个键。用 ",[45,12688,12689],{},".get()"," 带默认值，老存档照样能读；直接写 ",[45,12692,12693],{},"data[\"total_play_count\"]"," 就会崩。",[25,12696,12698],{"id":12697},"接进-gamemanager","接进 GameManager",[10,12700,12701],{},"改两个地方：",[38,12703,12705],{"className":40,"code":12704,"language":42,"meta":43,"style":43},"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",[45,12706,12707,12721,12736,12754,12772,12776,12780,12788,12802,12814,12822],{"__ignoreMap":43},[48,12708,12709,12711,12713,12715,12717,12719],{"class":50,"line":51},[48,12710,73],{"class":72},[48,12712,1112],{"class":76},[48,12714,167],{"class":80},[48,12716,97],{"class":72},[48,12718,100],{"class":58},[48,12720,103],{"class":80},[48,12722,12723,12726,12728,12731,12733],{"class":50,"line":62},[48,12724,12725],{"class":58},"    SaveManager",[48,12727,116],{"class":84},[48,12729,12730],{"class":76},"load_game",[48,12732,167],{"class":80},[48,12734,12735],{"class":170},"   # 👈 启动时读档\n",[48,12737,12738,12740,12742,12744,12746,12748,12750,12752],{"class":50,"line":69},[48,12739,1130],{"class":58},[48,12741,116],{"class":80},[48,12743,951],{"class":112},[48,12745,116],{"class":84},[48,12747,1139],{"class":76},[48,12749,81],{"class":80},[48,12751,1144],{"class":112},[48,12753,337],{"class":80},[48,12755,12756,12758,12760,12762,12764,12766,12768,12770],{"class":50,"line":106},[48,12757,1130],{"class":58},[48,12759,116],{"class":80},[48,12761,703],{"class":112},[48,12763,116],{"class":84},[48,12765,1139],{"class":76},[48,12767,81],{"class":80},[48,12769,182],{"class":112},[48,12771,337],{"class":80},[48,12773,12774],{"class":50,"line":156},[48,12775,66],{"emptyLinePlaceholder":65},[48,12777,12778],{"class":50,"line":174},[48,12779,5614],{"class":170},[48,12781,12782,12784,12786],{"class":50,"line":351},[48,12783,73],{"class":72},[48,12785,5621],{"class":76},[48,12787,1402],{"class":80},[48,12789,12790,12792,12794,12796,12798,12800],{"class":50,"line":356},[48,12791,5628],{"class":76},[48,12793,81],{"class":80},[48,12795,146],{"class":58},[48,12797,116],{"class":80},[48,12799,1018],{"class":54},[48,12801,337],{"class":80},[48,12803,12804,12806,12808,12810,12812],{"class":50,"line":367},[48,12805,109],{"class":54},[48,12807,1065],{"class":112},[48,12809,873],{"class":72},[48,12811,1049],{"class":112},[48,12813,103],{"class":84},[48,12815,12816,12818,12820],{"class":50,"line":884},[48,12817,5655],{"class":112},[48,12819,777],{"class":80},[48,12821,5660],{"class":112},[48,12823,12824,12827,12829,12832,12834],{"class":50,"line":892},[48,12825,12826],{"class":58},"        SaveManager",[48,12828,116],{"class":84},[48,12830,12831],{"class":76},"save_game",[48,12833,167],{"class":80},[48,12835,12836],{"class":170},"   # 👈 只在破纪录时写档\n",[396,12838,12840,12841,12843],{"id":12839},"读档时机_ready-里越早越好","读档时机：",[45,12842,561],{}," 里，越早越好",[10,12845,12846,12848,12849,12852,12853,12855,12856,12858],{},[45,12847,561],{}," 是这个节点",[20,12850,12851],{},"准备就绪时执行一次","的函数。Autoload 的 ",[45,12854,561],{}," 在游戏刚启动时就跑，比任何游戏场景都早 — 所以等玩家看到菜单的时候，",[45,12857,12077],{}," 已经是硬盘里的真实值了。",[396,12860,12862,12863],{"id":12861},"写档时机只在破纪录的那一刻","写档时机：",[20,12864,12865],{},"只在破纪录的那一刻",[10,12867,12868,12869,12872,12873,12876,12877,12879],{},"注意这行 ",[45,12870,12871],{},"SaveManager.save_game()"," 的",[20,12874,12875],{},"缩进位置"," — 它在 ",[45,12878,1574],{}," 里面。",[10,12881,12882],{},"这不是随便放的：",[496,12884,12885,12895],{},[499,12886,12887],{},[502,12888,12889,12892],{},[505,12890,12891],{},"写档时机",[505,12893,12894],{},"后果",[515,12896,12897,12905,12913],{},[502,12898,12899,12902],{},[520,12900,12901],{},"每帧写",[520,12903,12904],{},"💀 一秒 60 次硬盘 IO，游戏卡成幻灯片",[502,12906,12907,12910],{},[520,12908,12909],{},"每次死亡都写",[520,12911,12912],{},"😐 能用，但大部分时候在写一模一样的内容，纯浪费",[502,12914,12915,12920],{},[520,12916,12917],{},[20,12918,12919],{},"只在破纪录时写",[520,12921,12922,12923,12926],{},"✅ 只有数据",[20,12924,12925],{},"真的变了","才写",[10,12928,12929],{},"多数玩家一局游戏破不了几次纪录，所以实际写盘次数可能一整晚就几次。",[14,12931,12932],{},[10,12933,12934,12935],{},"这是个通用原则：",[20,12936,12937],{},"存档写入要由\"数据变化\"驱动，不要由\"时间\"或\"事件\"驱动。",[25,12939,12941],{"id":12940},"要不要给存档加密","要不要给存档加密？",[10,12943,12944],{},[20,12945,12946],{},"单机小游戏：不要。",[10,12948,12949,12950,12953],{},"Godot 确实提供了 ",[45,12951,12952],{},"FileAccess.open_encrypted_with_pass()","，但对这个游戏来说是负收益：",[199,12955,12956,12963,12970],{},[202,12957,12958,12959,12962],{},"玩家改自己的最高分 — ",[20,12960,12961],{},"不伤害任何人","，没有排行榜、没有联机、没有内购",[202,12964,12965,12966,12969],{},"加密之后",[20,12967,12968],{},"你自己也没法用记事本看存档了","，调试变麻烦",[202,12971,12972,12973],{},"密钥就硬编码在游戏里，稍微懂点的人一样能扒出来 — ",[20,12974,12975],{},"挡不住真想改的人，只烦到了自己",[10,12977,12978,12979,12982,12983,12982,12986,12989],{},"什么时候才需要加密？有",[20,12980,12981],{},"在线排行榜","、有",[20,12984,12985],{},"联机对战",[20,12987,12988],{},"内购解锁","的时候 — 那时候作弊会影响到别的玩家。",[25,12991,12992],{"id":12992},"测试一下",[3034,12994,12995,12998,13004,13011],{},[202,12996,12997],{},"玩一局，打出一个分数（比如 5 分）",[202,12999,13000,13003],{},[20,13001,13002],{},"完全关掉游戏","，重新运行",[202,13005,13006,13007,13010],{},"结束画面的 ",[45,13008,13009],{},"High Score"," 应该还是 5 ✅",[202,13012,9824,13013,13015,13016,13019,13020],{},[20,13014,11977],{},"，能看到 ",[45,13017,13018],{},"save.json","，记事本打开是 ",[45,13021,13022],{},"{\"high_score\":5}",[10,13024,13025,13026,13028,13029,1188,13031,194],{},"如果第 3 步失败，先检查 Autoload 里 ",[45,13027,543],{}," 是不是排在 ",[45,13030,556],{},[20,13032,12317],{},[1794,13034],{},[10,13036,13037],{},"到这里，《XGGame-Bird》的完整结构就都记录完了：",[199,13039,13040,13043,13046,13049,13052,13055],{},[202,13041,13042],{},"✅ 从零搭出一只会飞的鸟",[202,13044,13045],{},"✅ 水管、死区、计分",[202,13047,13048],{},"✅ GameManager 管状态，EventBus 管通信",[202,13050,13051],{},"✅ 美术、音效、UI",[202,13053,13054],{},"✅ 导出上架 itch.io",[202,13056,13057],{},"✅ 存档留住最高分",[10,13059,13060],{},"下一款游戏见 💪",[1804,13062,13063],{},"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":43,"searchDepth":62,"depth":62,"links":13065},[13066,13067,13069,13070,13081,13087,13088],{"id":11759,"depth":62,"text":11760},{"id":11811,"depth":62,"text":13068},"res:\u002F\u002F 和 user:\u002F\u002F — 别写错地方",{"id":11993,"depth":62,"text":11994},{"id":12331,"depth":62,"text":12331,"children":13071},[13072,13073,13075,13077,13079,13080],{"id":12334,"depth":69,"text":12337},{"id":12357,"depth":69,"text":13074},":= 是什么？",{"id":12448,"depth":69,"text":13076},"JSON.stringify() 和 JSON.parse_string()",{"id":12499,"depth":69,"text":13078},"为什么没有 file.close()？",{"id":12539,"depth":69,"text":12539},{"id":12635,"depth":69,"text":12638},{"id":12697,"depth":62,"text":12698,"children":13082},[13083,13085],{"id":12839,"depth":69,"text":13084},"读档时机：_ready() 里，越早越好",{"id":12861,"depth":69,"text":13086},"写档时机：只在破纪录的那一刻",{"id":12940,"depth":62,"text":12941},{"id":12992,"depth":62,"text":12992},{},"\u002Fdevlog\u002Fxggame-bird\u002F11-save-system",{"title":11745,"description":1832},"devlog\u002Fxggame-bird\u002F11-save-system","jjMJ4TX8XGiQKcEGk7qkmZ_xiWx5rLCIE6Kxn0teNVg",1788955184059]