[{"data":1,"prerenderedAt":10181},["ShallowReactive",2],{"\u002Fdevlog\u002Fxggame-snake\u002F06-systems":3,"devlog-chapters-xggame-snake":1706},{"id":4,"title":5,"body":6,"cover":1695,"date":1696,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":1701,"navigation":129,"path":1702,"seo":1703,"stem":1704,"toc":129,"__hash__":1705},"devlog\u002Fdevlog\u002Fxggame-snake\u002F06-systems.md","系统：喇叭、裁判、档案",{"type":7,"value":8,"toc":1687},"minimark",[9,18,25,90,95,204,207,224,233,237,719,763,770,784,1031,1046,1067,1073,1077,1418,1436,1439,1658,1680,1683],[10,11,12,13,17],"p",{},"阶段 2 明明跑通了，为什么要拆？因为接下来的东西——分数显示、暂停、结算、存档、设置菜单——全都要问同一个问题：\"现在什么状态？分数多少？玩家选了什么？\"这些数据现在散在 ",[14,15,16],"code",{},"main.gd"," 里，菜单场景、UI 场景想拿到它们，要么互相引用（一团乱），要么放到一个大家都能叫到的地方——Autoload。",[10,19,20,21],{},"为什么不一开始就拆？一开始拆，你会在四个文件之间来回跳，逻辑还没跑通就先迷路。",[22,23,24],"strong",{},"先能跑，再拆干净。",[26,27,28,44],"table",{},[29,30,31],"thead",{},[32,33,34,38,41],"tr",{},[35,36,37],"th",{},"单例",[35,39,40],{},"只管",[35,42,43],{},"比喻",[45,46,47,64,77],"tbody",{},[32,48,49,55,61],{},[50,51,52],"td",{},[14,53,54],{},"EventBus",[50,56,57,58],{},"喊一声「发生了 X」，",[22,59,60],{},"零逻辑",[50,62,63],{},"🔊 喇叭",[32,65,66,71,74],{},[50,67,68],{},[14,69,70],{},"GameManager",[50,72,73],{},"状态、分数、最高分、开局设置",[50,75,76],{},"⚖️ 裁判",[32,78,79,84,87],{},[50,80,81],{},[14,82,83],{},"SaveManager",[50,85,86],{},"读写存档文件",[50,88,89],{},"💾 档案柜",[91,92,94],"h2",{"id":93},"eventbus整个文件就两个信号","EventBus：整个文件就两个信号",[96,97,102],"pre",{"className":98,"code":99,"language":100,"meta":101,"style":101},"language-gdscript shiki shiki-themes vitesse-light vitesse-dark","extends Node\n## 全局信号总线：只定义信号，谁关心谁自己来连\n\n@warning_ignore(\"unused_signal\")\nsignal food_eaten(points: int)     # 吃到东西了，值几分（普通食物 1、星星 2）\n\n@warning_ignore(\"unused_signal\")\nsignal snake_died                  # 蛇死了\n","gdscript","",[14,103,104,117,124,131,149,177,182,193],{"__ignoreMap":101},[105,106,109,113],"span",{"class":107,"line":108},"line",1,[105,110,112],{"class":111},"sTPum","extends",[105,114,116],{"class":115},"s_NWU"," Node\n",[105,118,120],{"class":107,"line":119},2,[105,121,123],{"class":122},"snYqZ","## 全局信号总线：只定义信号，谁关心谁自己来连\n",[105,125,127],{"class":107,"line":126},3,[105,128,130],{"emptyLinePlaceholder":129},true,"\n",[105,132,134,138,142,146],{"class":107,"line":133},4,[105,135,137],{"class":136},"s_xSY","@warning_ignore",[105,139,141],{"class":140},"si6no","(",[105,143,145],{"class":144},"spP0B","\"unused_signal\"",[105,147,148],{"class":140},")\n",[105,150,152,156,159,161,165,168,171,174],{"class":107,"line":151},5,[105,153,155],{"class":154},"s5TCs","signal",[105,157,158],{"class":136}," food_eaten",[105,160,141],{"class":140},[105,162,164],{"class":163},"s8w-G","points",[105,166,167],{"class":140},":",[105,169,170],{"class":115}," int",[105,172,173],{"class":140},")",[105,175,176],{"class":122},"     # 吃到东西了，值几分（普通食物 1、星星 2）\n",[105,178,180],{"class":107,"line":179},6,[105,181,130],{"emptyLinePlaceholder":129},[105,183,185,187,189,191],{"class":107,"line":184},7,[105,186,137],{"class":136},[105,188,141],{"class":140},[105,190,145],{"class":144},[105,192,148],{"class":140},[105,194,196,198,201],{"class":107,"line":195},8,[105,197,155],{"class":154},[105,199,200],{"class":136}," snake_died",[105,202,203],{"class":122},"                  # 蛇死了\n",[10,205,206],{},"一个函数都不许有。它是喇叭，喇叭只负责\"喊\"，喊完谁该做什么是听的人自己的事。一旦往里塞逻辑，它就变成第二个 GameManager，两边职责打架。",[10,208,209,210,213,214,216,217,220,221],{},"为什么 ",[14,211,212],{},"food_eaten"," 带一个数字，不拆成 ",[14,215,212],{}," 和 ",[14,218,219],{},"star_eaten","？听众关心的是\"值几分\"，不是\"是哪种\"。加分的人拿到 2 就加 2；放音效的人看到 2 就放星星音。以后加第三种食物值 5 分，信号一个字不用改。",[22,222,223],{},"信号的参数是\"事实的细节\"，不是\"指令\"。",[10,225,226,229,230],{},[14,227,228],{},"@warning_ignore(\"unused_signal\")","：不加，编辑器会画黄线警告\"这个信号定义了但没在本文件里发出\"。EventBus 的设计就是\"在别的文件里发\"，所以这是误报。这算 Godot 的设计失误吗？谈不上——检查器一次只看一个文件，没法知道别的文件会不会发这个信号；而\"定义了却没发\"在 99% 的情况下确实是写错了。它选择宁可误报也不漏报，然后给你一行注解当逃生口。",[22,231,232],{},"黄线是警告（请你确认），红线才是错误（跑不起来）。",[91,234,236],{"id":235},"gamemanager状态机-分数","GameManager：状态机 + 分数",[96,238,240],{"className":98,"code":239,"language":100,"meta":101,"style":101},"extends Node\n## 裁判：状态和分数只存这一份，别人要么问它、要么听它喊\n\nenum GameState { READY, PLAYING, PAUSED, GAME_OVER }\nvar current_state := GameState.READY\n\nvar total_score := 0\nvar high_score := 0\n\nsignal score_changed(new_score: int)\nsignal state_changed(new_state: GameState)\n\nfunc _ready() -> void:\n    SaveManager.load_game()                       # 开机读档\n    EventBus.food_eaten.connect(add_score)        # 听「吃到 N 分」→ 加 N 分\n    EventBus.snake_died.connect(game_over)        # 听「死了」→ 结算\n\n## 改状态的唯一入口 —— 每次改都自动广播，不会漏\nfunc set_state(new_state: GameState) -> void:\n    current_state = new_state\n    state_changed.emit(new_state)\n\nfunc add_score(points: int) -> void:\n    total_score += points\n    score_changed.emit(total_score)\n\nfunc reset_score() -> void:\n    total_score = 0\n    score_changed.emit(total_score)\n\nfunc game_over() -> void:\n    set_state(GameState.GAME_OVER)\n    if total_score > high_score:\n        high_score = total_score\n        SaveManager.save_game()                   # 只在破纪录时写盘\n",[14,241,242,248,253,257,291,310,314,327,338,343,362,381,386,407,423,448,472,477,483,507,519,536,541,565,577,594,599,615,624,639,644,660,677,692,703],{"__ignoreMap":101},[105,243,244,246],{"class":107,"line":108},[105,245,112],{"class":111},[105,247,116],{"class":115},[105,249,250],{"class":107,"line":119},[105,251,252],{"class":122},"## 裁判：状态和分数只存这一份，别人要么问它、要么听它喊\n",[105,254,255],{"class":107,"line":126},[105,256,130],{"emptyLinePlaceholder":129},[105,258,259,262,265,268,272,275,278,280,283,285,288],{"class":107,"line":133},[105,260,261],{"class":111},"enum",[105,263,264],{"class":115}," GameState",[105,266,267],{"class":140}," {",[105,269,271],{"class":270},"s9nN2"," READY",[105,273,274],{"class":163},", ",[105,276,277],{"class":270},"PLAYING",[105,279,274],{"class":163},[105,281,282],{"class":270},"PAUSED",[105,284,274],{"class":163},[105,286,287],{"class":270},"GAME_OVER",[105,289,290],{"class":140}," }\n",[105,292,293,296,299,302,304,307],{"class":107,"line":151},[105,294,295],{"class":154},"var",[105,297,298],{"class":270}," current_state",[105,300,301],{"class":140}," :=",[105,303,264],{"class":115},[105,305,306],{"class":140},".",[105,308,309],{"class":111},"READY\n",[105,311,312],{"class":107,"line":179},[105,313,130],{"emptyLinePlaceholder":129},[105,315,316,318,321,323],{"class":107,"line":184},[105,317,295],{"class":154},[105,319,320],{"class":270}," total_score",[105,322,301],{"class":140},[105,324,326],{"class":325},"sqbOQ"," 0\n",[105,328,329,331,334,336],{"class":107,"line":195},[105,330,295],{"class":154},[105,332,333],{"class":270}," high_score",[105,335,301],{"class":140},[105,337,326],{"class":325},[105,339,341],{"class":107,"line":340},9,[105,342,130],{"emptyLinePlaceholder":129},[105,344,346,348,351,353,356,358,360],{"class":107,"line":345},10,[105,347,155],{"class":154},[105,349,350],{"class":136}," score_changed",[105,352,141],{"class":140},[105,354,355],{"class":163},"new_score",[105,357,167],{"class":140},[105,359,170],{"class":115},[105,361,148],{"class":140},[105,363,365,367,370,372,375,377,379],{"class":107,"line":364},11,[105,366,155],{"class":154},[105,368,369],{"class":136}," state_changed",[105,371,141],{"class":140},[105,373,374],{"class":163},"new_state",[105,376,167],{"class":140},[105,378,264],{"class":115},[105,380,148],{"class":140},[105,382,384],{"class":107,"line":383},12,[105,385,130],{"emptyLinePlaceholder":129},[105,387,389,392,395,398,401,404],{"class":107,"line":388},13,[105,390,391],{"class":154},"func",[105,393,394],{"class":136}," _ready",[105,396,397],{"class":140},"()",[105,399,400],{"class":154}," ->",[105,402,403],{"class":115}," void",[105,405,406],{"class":140},":\n",[105,408,410,413,415,418,420],{"class":107,"line":409},14,[105,411,412],{"class":115},"    SaveManager",[105,414,306],{"class":163},[105,416,417],{"class":136},"load_game",[105,419,397],{"class":140},[105,421,422],{"class":122},"                       # 开机读档\n",[105,424,426,429,431,433,435,438,440,443,445],{"class":107,"line":425},15,[105,427,428],{"class":115},"    EventBus",[105,430,306],{"class":140},[105,432,212],{"class":270},[105,434,306],{"class":163},[105,436,437],{"class":136},"connect",[105,439,141],{"class":140},[105,441,442],{"class":270},"add_score",[105,444,173],{"class":140},[105,446,447],{"class":122},"        # 听「吃到 N 分」→ 加 N 分\n",[105,449,451,453,455,458,460,462,464,467,469],{"class":107,"line":450},16,[105,452,428],{"class":115},[105,454,306],{"class":140},[105,456,457],{"class":270},"snake_died",[105,459,306],{"class":163},[105,461,437],{"class":136},[105,463,141],{"class":140},[105,465,466],{"class":270},"game_over",[105,468,173],{"class":140},[105,470,471],{"class":122},"        # 听「死了」→ 结算\n",[105,473,475],{"class":107,"line":474},17,[105,476,130],{"emptyLinePlaceholder":129},[105,478,480],{"class":107,"line":479},18,[105,481,482],{"class":122},"## 改状态的唯一入口 —— 每次改都自动广播，不会漏\n",[105,484,486,488,491,493,495,497,499,501,503,505],{"class":107,"line":485},19,[105,487,391],{"class":154},[105,489,490],{"class":136}," set_state",[105,492,141],{"class":140},[105,494,374],{"class":163},[105,496,167],{"class":140},[105,498,264],{"class":115},[105,500,173],{"class":140},[105,502,400],{"class":154},[105,504,403],{"class":115},[105,506,406],{"class":140},[105,508,510,513,516],{"class":107,"line":509},20,[105,511,512],{"class":270},"    current_state",[105,514,515],{"class":140}," =",[105,517,518],{"class":270}," new_state\n",[105,520,522,525,527,530,532,534],{"class":107,"line":521},21,[105,523,524],{"class":270},"    state_changed",[105,526,306],{"class":163},[105,528,529],{"class":136},"emit",[105,531,141],{"class":140},[105,533,374],{"class":270},[105,535,148],{"class":140},[105,537,539],{"class":107,"line":538},22,[105,540,130],{"emptyLinePlaceholder":129},[105,542,544,546,549,551,553,555,557,559,561,563],{"class":107,"line":543},23,[105,545,391],{"class":154},[105,547,548],{"class":136}," add_score",[105,550,141],{"class":140},[105,552,164],{"class":163},[105,554,167],{"class":140},[105,556,170],{"class":115},[105,558,173],{"class":140},[105,560,400],{"class":154},[105,562,403],{"class":115},[105,564,406],{"class":140},[105,566,568,571,574],{"class":107,"line":567},24,[105,569,570],{"class":270},"    total_score",[105,572,573],{"class":154}," +=",[105,575,576],{"class":270}," points\n",[105,578,580,583,585,587,589,592],{"class":107,"line":579},25,[105,581,582],{"class":270},"    score_changed",[105,584,306],{"class":163},[105,586,529],{"class":136},[105,588,141],{"class":140},[105,590,591],{"class":270},"total_score",[105,593,148],{"class":140},[105,595,597],{"class":107,"line":596},26,[105,598,130],{"emptyLinePlaceholder":129},[105,600,602,604,607,609,611,613],{"class":107,"line":601},27,[105,603,391],{"class":154},[105,605,606],{"class":136}," reset_score",[105,608,397],{"class":140},[105,610,400],{"class":154},[105,612,403],{"class":115},[105,614,406],{"class":140},[105,616,618,620,622],{"class":107,"line":617},28,[105,619,570],{"class":270},[105,621,515],{"class":140},[105,623,326],{"class":325},[105,625,627,629,631,633,635,637],{"class":107,"line":626},29,[105,628,582],{"class":270},[105,630,306],{"class":163},[105,632,529],{"class":136},[105,634,141],{"class":140},[105,636,591],{"class":270},[105,638,148],{"class":140},[105,640,642],{"class":107,"line":641},30,[105,643,130],{"emptyLinePlaceholder":129},[105,645,647,649,652,654,656,658],{"class":107,"line":646},31,[105,648,391],{"class":154},[105,650,651],{"class":136}," game_over",[105,653,397],{"class":140},[105,655,400],{"class":154},[105,657,403],{"class":115},[105,659,406],{"class":140},[105,661,663,666,668,671,673,675],{"class":107,"line":662},32,[105,664,665],{"class":136},"    set_state",[105,667,141],{"class":140},[105,669,670],{"class":115},"GameState",[105,672,306],{"class":140},[105,674,287],{"class":111},[105,676,148],{"class":140},[105,678,680,683,685,688,690],{"class":107,"line":679},33,[105,681,682],{"class":111},"    if",[105,684,320],{"class":270},[105,686,687],{"class":154}," >",[105,689,333],{"class":270},[105,691,406],{"class":163},[105,693,695,698,700],{"class":107,"line":694},34,[105,696,697],{"class":270},"        high_score",[105,699,515],{"class":140},[105,701,702],{"class":270}," total_score\n",[105,704,706,709,711,714,716],{"class":107,"line":705},35,[105,707,708],{"class":115},"        SaveManager",[105,710,306],{"class":163},[105,712,713],{"class":136},"save_game",[105,715,397],{"class":140},[105,717,718],{"class":122},"                   # 只在破纪录时写盘\n",[720,721,722,735,744,753],"ul",{},[723,724,725,728,729,731,732],"li",{},[14,726,727],{},"enum GameState","：本质是 0、1、2、3，但写名字比写数字清楚一百倍。游戏在任何时刻只处于其中一种——这就叫\"状态机\"。比小鸟多一个 ",[14,730,282],{},"，因为这个游戏有暂停菜单：",[22,733,734],{},"状态是玩法决定的",[723,736,737,740,741,743],{},[14,738,739],{},"set_state()"," 是唯一入口：所有改状态的地方都走这一个函数，它顺手 ",[14,742,529],{},"。\"状态变了但没人收到通知\"这种 bug 不可能发生",[723,745,746,749,750,752],{},[14,747,748],{},"food_eaten.connect(add_score)","：信号带 1 个参数，接它的函数就要收 1 个参数。",[14,751,442],{}," 后面没有括号——有括号是\"现在就执行\"，没括号是\"把这个函数交给你，信号响了你再执行\"",[723,754,755,758,759,762],{},[14,756,757],{},"save_game()"," 缩进在 ",[14,760,761],{},"if"," 里：只有数据真的变了才写盘。每帧写会卡，每次死都写是白写",[91,764,766,767,769],{"id":765},"把-maingd-改成只管蛇不管分数","把 ",[14,768,16],{}," 改成\"只管蛇，不管分数\"",[10,771,772,773,776,777,780,781,783],{},"三处改动：",[14,774,775],{},"_ready()"," 里连上状态、清分、设 PLAYING；移动函数开头加一行守卫，",[14,778,779],{},"die()"," 和吃东西改成发信号；删掉 ",[14,782,779],{},"，换成监听状态：",[96,785,787],{"className":98,"code":786,"language":100,"meta":101,"style":101},"func _on_move_timer_timeout() -> void:\n    if GameManager.current_state != GameManager.GameState.PLAYING:\n        return                                  # 不是进行中，什么都不做\n    # ……撞墙 \u002F 撞自己：\n        EventBus.snake_died.emit()\n        return\n    # ……吃到：\n        EventBus.food_eaten.emit(1)             # 喊一声「吃到 1 分」，加分和音效各自去听\n    # ……吃到星星：\n        EventBus.food_eaten.emit(2)\n\nfunc _on_state_changed(new_state: GameManager.GameState) -> void:\n    if new_state == GameManager.GameState.GAME_OVER:\n        for timer in [$MoveTimer, $FoodTimer, $StarSpawnTimer, $StarLifeTimer]:\n            timer.stop()                        # 死了，四个计时器全停\n",[14,788,789,804,831,839,844,860,865,870,892,897,916,920,947,969,1016],{"__ignoreMap":101},[105,790,791,793,796,798,800,802],{"class":107,"line":108},[105,792,391],{"class":154},[105,794,795],{"class":136}," _on_move_timer_timeout",[105,797,397],{"class":140},[105,799,400],{"class":154},[105,801,403],{"class":115},[105,803,406],{"class":140},[105,805,806,808,811,813,816,819,821,823,825,827,829],{"class":107,"line":119},[105,807,682],{"class":111},[105,809,810],{"class":115}," GameManager",[105,812,306],{"class":140},[105,814,815],{"class":270},"current_state",[105,817,818],{"class":154}," !=",[105,820,810],{"class":115},[105,822,306],{"class":140},[105,824,670],{"class":270},[105,826,306],{"class":140},[105,828,277],{"class":111},[105,830,406],{"class":163},[105,832,833,836],{"class":107,"line":126},[105,834,835],{"class":111},"        return",[105,837,838],{"class":122},"                                  # 不是进行中，什么都不做\n",[105,840,841],{"class":107,"line":133},[105,842,843],{"class":122},"    # ……撞墙 \u002F 撞自己：\n",[105,845,846,849,851,853,855,857],{"class":107,"line":151},[105,847,848],{"class":115},"        EventBus",[105,850,306],{"class":140},[105,852,457],{"class":270},[105,854,306],{"class":163},[105,856,529],{"class":136},[105,858,859],{"class":140},"()\n",[105,861,862],{"class":107,"line":179},[105,863,864],{"class":111},"        return\n",[105,866,867],{"class":107,"line":184},[105,868,869],{"class":122},"    # ……吃到：\n",[105,871,872,874,876,878,880,882,884,887,889],{"class":107,"line":195},[105,873,848],{"class":115},[105,875,306],{"class":140},[105,877,212],{"class":270},[105,879,306],{"class":163},[105,881,529],{"class":136},[105,883,141],{"class":140},[105,885,886],{"class":325},"1",[105,888,173],{"class":140},[105,890,891],{"class":122},"             # 喊一声「吃到 1 分」，加分和音效各自去听\n",[105,893,894],{"class":107,"line":340},[105,895,896],{"class":122},"    # ……吃到星星：\n",[105,898,899,901,903,905,907,909,911,914],{"class":107,"line":345},[105,900,848],{"class":115},[105,902,306],{"class":140},[105,904,212],{"class":270},[105,906,306],{"class":163},[105,908,529],{"class":136},[105,910,141],{"class":140},[105,912,913],{"class":325},"2",[105,915,148],{"class":140},[105,917,918],{"class":107,"line":364},[105,919,130],{"emptyLinePlaceholder":129},[105,921,922,924,927,929,931,933,935,937,939,941,943,945],{"class":107,"line":383},[105,923,391],{"class":154},[105,925,926],{"class":136}," _on_state_changed",[105,928,141],{"class":140},[105,930,374],{"class":163},[105,932,167],{"class":140},[105,934,810],{"class":115},[105,936,306],{"class":140},[105,938,670],{"class":270},[105,940,173],{"class":140},[105,942,400],{"class":154},[105,944,403],{"class":115},[105,946,406],{"class":140},[105,948,949,951,954,957,959,961,963,965,967],{"class":107,"line":388},[105,950,682],{"class":111},[105,952,953],{"class":270}," new_state",[105,955,956],{"class":154}," ==",[105,958,810],{"class":115},[105,960,306],{"class":140},[105,962,670],{"class":270},[105,964,306],{"class":140},[105,966,287],{"class":111},[105,968,406],{"class":163},[105,970,971,974,977,980,983,986,990,992,994,997,999,1001,1004,1006,1008,1011,1014],{"class":107,"line":409},[105,972,973],{"class":111},"        for",[105,975,976],{"class":270}," timer",[105,978,979],{"class":111}," in",[105,981,982],{"class":140}," [",[105,984,985],{"class":111},"$",[105,987,989],{"class":988},"sfsYZ","MoveTimer",[105,991,274],{"class":163},[105,993,985],{"class":111},[105,995,996],{"class":988},"FoodTimer",[105,998,274],{"class":163},[105,1000,985],{"class":111},[105,1002,1003],{"class":988},"StarSpawnTimer",[105,1005,274],{"class":163},[105,1007,985],{"class":111},[105,1009,1010],{"class":988},"StarLifeTimer",[105,1012,1013],{"class":140},"]",[105,1015,406],{"class":163},[105,1017,1018,1021,1023,1026,1028],{"class":107,"line":425},[105,1019,1020],{"class":270},"            timer",[105,1022,306],{"class":163},[105,1024,1025],{"class":136},"stop",[105,1027,397],{"class":140},[105,1029,1030],{"class":122},"                        # 死了，四个计时器全停\n",[10,1032,209,1033,1035,1036,1039,1040,1042,1043,1045],{},[14,1034,16],{}," 撞墙时只\"喊一声\"，不自己停 Timer？因为\"死了之后该发生什么\"不是蛇该操心的事：要结算分数（GameManager 的事）、要弹面板（UI 的事）、要放音效（AudioManager 的事）。",[22,1037,1038],{},"蛇只知道\"我撞了\"，喊一声，谁关心谁自己来。"," 以后想加\"死亡震屏\"，是新增一个文件去听 ",[14,1041,457],{},"，",[14,1044,16],{}," 一个字不用改。",[10,1047,1048,1049,1051,1052,1055,1056,1058,1059,1062,1063,1066],{},"那 Timer 谁停？",[14,1050,16],{}," 自己听 ",[14,1053,1054],{},"state_changed","，看到 ",[14,1057,287],{}," 就停。它听的是\"状态变了\"这个",[22,1060,1061],{},"结果","，而不是自己去",[22,1064,1065],{},"决定","结果。这个区别就是\"裁判\"和\"球员\"的区别。",[10,1068,1069,1070,1072],{},"为什么四个 Timer 全停，不只停 ",[14,1071,989],{},"？只停移动，死了以后食物还在长、星星还在出现，画面看起来像没死。\"结束\"要对所有会动的东西生效。",[91,1074,1076],{"id":1075},"savemanager和小鸟几乎一样","SaveManager：和小鸟几乎一样",[96,1078,1080],{"className":98,"code":1079,"language":100,"meta":101,"style":101},"extends Node\n## 存档：读写 user:\u002F\u002Fsave.json\n\nconst SAVE_PATH := \"user:\u002F\u002Fsave.json\"\n\nfunc save_game() -> void:\n    var data := { \"high_score\": GameManager.high_score }\n    var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)\n    if file == null:\n        push_warning(\"存档写入失败：%s\" % FileAccess.get_open_error())\n        return\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",[14,1081,1082,1088,1093,1097,1110,1114,1129,1156,1190,1203,1232,1236,1264,1268,1283,1305,1312,1341,1369,1381,1388],{"__ignoreMap":101},[105,1083,1084,1086],{"class":107,"line":108},[105,1085,112],{"class":111},[105,1087,116],{"class":115},[105,1089,1090],{"class":107,"line":119},[105,1091,1092],{"class":122},"## 存档：读写 user:\u002F\u002Fsave.json\n",[105,1094,1095],{"class":107,"line":126},[105,1096,130],{"emptyLinePlaceholder":129},[105,1098,1099,1102,1105,1107],{"class":107,"line":133},[105,1100,1101],{"class":154},"const",[105,1103,1104],{"class":270}," SAVE_PATH",[105,1106,301],{"class":140},[105,1108,1109],{"class":144}," \"user:\u002F\u002Fsave.json\"\n",[105,1111,1112],{"class":107,"line":151},[105,1113,130],{"emptyLinePlaceholder":129},[105,1115,1116,1118,1121,1123,1125,1127],{"class":107,"line":179},[105,1117,391],{"class":154},[105,1119,1120],{"class":136}," save_game",[105,1122,397],{"class":140},[105,1124,400],{"class":154},[105,1126,403],{"class":115},[105,1128,406],{"class":140},[105,1130,1131,1134,1137,1139,1141,1144,1147,1149,1151,1154],{"class":107,"line":184},[105,1132,1133],{"class":154},"    var",[105,1135,1136],{"class":270}," data",[105,1138,301],{"class":140},[105,1140,267],{"class":140},[105,1142,1143],{"class":144}," \"high_score\"",[105,1145,1146],{"class":163},": ",[105,1148,70],{"class":115},[105,1150,306],{"class":163},[105,1152,1153],{"class":270},"high_score",[105,1155,290],{"class":140},[105,1157,1158,1160,1163,1165,1168,1170,1173,1175,1178,1181,1183,1185,1188],{"class":107,"line":195},[105,1159,1133],{"class":154},[105,1161,1162],{"class":270}," file",[105,1164,301],{"class":140},[105,1166,1167],{"class":115}," FileAccess",[105,1169,306],{"class":163},[105,1171,1172],{"class":136},"open",[105,1174,141],{"class":140},[105,1176,1177],{"class":270},"SAVE_PATH",[105,1179,1180],{"class":140},",",[105,1182,1167],{"class":115},[105,1184,306],{"class":140},[105,1186,1187],{"class":111},"WRITE",[105,1189,148],{"class":140},[105,1191,1192,1194,1196,1198,1201],{"class":107,"line":340},[105,1193,682],{"class":111},[105,1195,1162],{"class":270},[105,1197,956],{"class":154},[105,1199,1200],{"class":111}," null",[105,1202,406],{"class":163},[105,1204,1205,1208,1210,1213,1216,1219,1222,1224,1226,1229],{"class":107,"line":345},[105,1206,1207],{"class":136},"        push_warning",[105,1209,141],{"class":140},[105,1211,1212],{"class":144},"\"存档写入失败：",[105,1214,1215],{"class":988},"%s",[105,1217,1218],{"class":144},"\"",[105,1220,1221],{"class":154}," %",[105,1223,1167],{"class":115},[105,1225,306],{"class":163},[105,1227,1228],{"class":136},"get_open_error",[105,1230,1231],{"class":140},"())\n",[105,1233,1234],{"class":107,"line":364},[105,1235,864],{"class":111},[105,1237,1238,1241,1243,1246,1248,1251,1253,1256,1258,1261],{"class":107,"line":383},[105,1239,1240],{"class":270},"    file",[105,1242,306],{"class":163},[105,1244,1245],{"class":136},"store_string",[105,1247,141],{"class":140},[105,1249,1250],{"class":115},"JSON",[105,1252,306],{"class":163},[105,1254,1255],{"class":136},"stringify",[105,1257,141],{"class":140},[105,1259,1260],{"class":270},"data",[105,1262,1263],{"class":140},"))\n",[105,1265,1266],{"class":107,"line":388},[105,1267,130],{"emptyLinePlaceholder":129},[105,1269,1270,1272,1275,1277,1279,1281],{"class":107,"line":409},[105,1271,391],{"class":154},[105,1273,1274],{"class":136}," load_game",[105,1276,397],{"class":140},[105,1278,400],{"class":154},[105,1280,403],{"class":115},[105,1282,406],{"class":140},[105,1284,1285,1287,1290,1292,1294,1297,1299,1301,1303],{"class":107,"line":425},[105,1286,682],{"class":111},[105,1288,1289],{"class":154}," not",[105,1291,1167],{"class":115},[105,1293,306],{"class":163},[105,1295,1296],{"class":136},"file_exists",[105,1298,141],{"class":140},[105,1300,1177],{"class":270},[105,1302,173],{"class":140},[105,1304,406],{"class":163},[105,1306,1307,1309],{"class":107,"line":450},[105,1308,835],{"class":111},[105,1310,1311],{"class":122},"                                  # 首次游玩，没存档\n",[105,1313,1314,1316,1318,1320,1322,1324,1326,1328,1330,1332,1334,1336,1339],{"class":107,"line":474},[105,1315,1133],{"class":154},[105,1317,1162],{"class":270},[105,1319,301],{"class":140},[105,1321,1167],{"class":115},[105,1323,306],{"class":163},[105,1325,1172],{"class":136},[105,1327,141],{"class":140},[105,1329,1177],{"class":270},[105,1331,1180],{"class":140},[105,1333,1167],{"class":115},[105,1335,306],{"class":140},[105,1337,1338],{"class":111},"READ",[105,1340,148],{"class":140},[105,1342,1343,1345,1347,1349,1352,1354,1357,1359,1362,1364,1367],{"class":107,"line":479},[105,1344,1133],{"class":154},[105,1346,1136],{"class":270},[105,1348,515],{"class":140},[105,1350,1351],{"class":115}," JSON",[105,1353,306],{"class":163},[105,1355,1356],{"class":136},"parse_string",[105,1358,141],{"class":140},[105,1360,1361],{"class":270},"file",[105,1363,306],{"class":163},[105,1365,1366],{"class":136},"get_as_text",[105,1368,1231],{"class":140},[105,1370,1371,1373,1375,1377,1379],{"class":107,"line":485},[105,1372,682],{"class":111},[105,1374,1136],{"class":270},[105,1376,956],{"class":154},[105,1378,1200],{"class":111},[105,1380,406],{"class":163},[105,1382,1383,1385],{"class":107,"line":509},[105,1384,835],{"class":111},[105,1386,1387],{"class":122},"                                  # 存档损坏，防崩\n",[105,1389,1390,1393,1395,1397,1399,1401,1403,1406,1408,1411,1413,1416],{"class":107,"line":521},[105,1391,1392],{"class":115},"    GameManager",[105,1394,306],{"class":140},[105,1396,1153],{"class":270},[105,1398,515],{"class":140},[105,1400,1136],{"class":270},[105,1402,306],{"class":163},[105,1404,1405],{"class":136},"get",[105,1407,141],{"class":140},[105,1409,1410],{"class":144},"\"high_score\"",[105,1412,1180],{"class":140},[105,1414,1415],{"class":325}," 0",[105,1417,148],{"class":140},[10,1419,1420,1421,1424,1425,1428,1429,1432,1433,1435],{},"这就是\"外壳可复用\"：搬小鸟的，改一个字段名。",[14,1422,1423],{},"res:\u002F\u002F"," 是游戏本体，导出后打包成 ",[14,1426,1427],{},".pck"," 只读；",[14,1430,1431],{},"user:\u002F\u002F"," 是玩家电脑上的用户数据目录，可读可写。编辑器里往 ",[14,1434,1423],{}," 写可能成功、导出后必定失败——这坑特别隐蔽。",[91,1437,1438],{"id":1438},"音效也是听信号",[96,1440,1442],{"className":98,"code":1441,"language":100,"meta":101,"style":101},"extends Node\n## 音响：只负责 play()，不判断该不该播\n\n@onready var eat_sfx: AudioStreamPlayer = $SFX\u002FEatSfx\n@onready var star_sfx: AudioStreamPlayer = $SFX\u002FStarSfx\n@onready var die_sfx: AudioStreamPlayer = $SFX\u002FDieSfx\n\nfunc _ready() -> void:\n    EventBus.food_eaten.connect(_on_food_eaten)\n    EventBus.snake_died.connect(die_sfx.play)     # 没参数的信号，可以直接把 play 交给它\n\nfunc _on_food_eaten(points: int) -> void:\n    if points >= 2:\n        star_sfx.play()\n    else:\n        eat_sfx.play()\n",[14,1443,1444,1450,1455,1459,1483,1503,1523,1527,1541,1560,1587,1591,1614,1629,1640,1647],{"__ignoreMap":101},[105,1445,1446,1448],{"class":107,"line":108},[105,1447,112],{"class":111},[105,1449,116],{"class":115},[105,1451,1452],{"class":107,"line":119},[105,1453,1454],{"class":122},"## 音响：只负责 play()，不判断该不该播\n",[105,1456,1457],{"class":107,"line":126},[105,1458,130],{"emptyLinePlaceholder":129},[105,1460,1461,1464,1467,1470,1472,1475,1477,1480],{"class":107,"line":133},[105,1462,1463],{"class":136},"@onready",[105,1465,1466],{"class":154}," var",[105,1468,1469],{"class":270}," eat_sfx",[105,1471,167],{"class":140},[105,1473,1474],{"class":115}," AudioStreamPlayer",[105,1476,515],{"class":140},[105,1478,1479],{"class":111}," $",[105,1481,1482],{"class":988},"SFX\u002FEatSfx\n",[105,1484,1485,1487,1489,1492,1494,1496,1498,1500],{"class":107,"line":151},[105,1486,1463],{"class":136},[105,1488,1466],{"class":154},[105,1490,1491],{"class":270}," star_sfx",[105,1493,167],{"class":140},[105,1495,1474],{"class":115},[105,1497,515],{"class":140},[105,1499,1479],{"class":111},[105,1501,1502],{"class":988},"SFX\u002FStarSfx\n",[105,1504,1505,1507,1509,1512,1514,1516,1518,1520],{"class":107,"line":179},[105,1506,1463],{"class":136},[105,1508,1466],{"class":154},[105,1510,1511],{"class":270}," die_sfx",[105,1513,167],{"class":140},[105,1515,1474],{"class":115},[105,1517,515],{"class":140},[105,1519,1479],{"class":111},[105,1521,1522],{"class":988},"SFX\u002FDieSfx\n",[105,1524,1525],{"class":107,"line":184},[105,1526,130],{"emptyLinePlaceholder":129},[105,1528,1529,1531,1533,1535,1537,1539],{"class":107,"line":195},[105,1530,391],{"class":154},[105,1532,394],{"class":136},[105,1534,397],{"class":140},[105,1536,400],{"class":154},[105,1538,403],{"class":115},[105,1540,406],{"class":140},[105,1542,1543,1545,1547,1549,1551,1553,1555,1558],{"class":107,"line":340},[105,1544,428],{"class":115},[105,1546,306],{"class":140},[105,1548,212],{"class":270},[105,1550,306],{"class":163},[105,1552,437],{"class":136},[105,1554,141],{"class":140},[105,1556,1557],{"class":270},"_on_food_eaten",[105,1559,148],{"class":140},[105,1561,1562,1564,1566,1568,1570,1572,1574,1577,1579,1582,1584],{"class":107,"line":345},[105,1563,428],{"class":115},[105,1565,306],{"class":140},[105,1567,457],{"class":270},[105,1569,306],{"class":163},[105,1571,437],{"class":136},[105,1573,141],{"class":140},[105,1575,1576],{"class":270},"die_sfx",[105,1578,306],{"class":140},[105,1580,1581],{"class":270},"play",[105,1583,173],{"class":140},[105,1585,1586],{"class":122},"     # 没参数的信号，可以直接把 play 交给它\n",[105,1588,1589],{"class":107,"line":364},[105,1590,130],{"emptyLinePlaceholder":129},[105,1592,1593,1595,1598,1600,1602,1604,1606,1608,1610,1612],{"class":107,"line":383},[105,1594,391],{"class":154},[105,1596,1597],{"class":136}," _on_food_eaten",[105,1599,141],{"class":140},[105,1601,164],{"class":163},[105,1603,167],{"class":140},[105,1605,170],{"class":115},[105,1607,173],{"class":140},[105,1609,400],{"class":154},[105,1611,403],{"class":115},[105,1613,406],{"class":140},[105,1615,1616,1618,1621,1624,1627],{"class":107,"line":388},[105,1617,682],{"class":111},[105,1619,1620],{"class":270}," points",[105,1622,1623],{"class":154}," >=",[105,1625,1626],{"class":325}," 2",[105,1628,406],{"class":163},[105,1630,1631,1634,1636,1638],{"class":107,"line":409},[105,1632,1633],{"class":270},"        star_sfx",[105,1635,306],{"class":163},[105,1637,1581],{"class":136},[105,1639,859],{"class":140},[105,1641,1642,1645],{"class":107,"line":425},[105,1643,1644],{"class":111},"    else",[105,1646,406],{"class":163},[105,1648,1649,1652,1654,1656],{"class":107,"line":450},[105,1650,1651],{"class":270},"        eat_sfx",[105,1653,306],{"class":163},[105,1655,1581],{"class":136},[105,1657,859],{"class":140},[10,1659,1660,1661,1663,1664,1667,1668,1671,1672,1675,1676,1679],{},"一个不报错、但没声音的坑：",[14,1662,212],{}," 不能直接 ",[14,1665,1666],{},"connect(eat_sfx.play)","。",[14,1669,1670],{},"play()"," 其实有一个参数——从第几秒开始播。信号带着 ",[14,1673,1674],{},"points = 2"," 过来，Godot 会老老实实调 ",[14,1677,1678],{},"play(2)","：从第 2 秒开始播一个 0.3 秒的音效 = 什么都听不到，而且一个错都不报。所以带参数的信号，要自己写一个函数接住参数再决定播什么。",[10,1681,1682],{},"下一章做外壳：菜单怎么把玩家的选择交给游戏。",[1684,1685,1686],"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 .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 .si6no, html code.shiki .si6no{--shiki-default:#999999;--shiki-dark:#666666}html pre.shiki code .spP0B, html code.shiki .spP0B{--shiki-default:#B56959;--shiki-dark:#C98A7D}html pre.shiki code .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}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 .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":101,"searchDepth":119,"depth":119,"links":1688},[1689,1690,1691,1693,1694],{"id":93,"depth":119,"text":94},{"id":235,"depth":119,"text":236},{"id":765,"depth":119,"text":1692},"把 main.gd 改成\"只管蛇，不管分数\"",{"id":1075,"depth":119,"text":1076},{"id":1438,"depth":119,"text":1438},null,"2026-09-06","XGGame-Snake Godot","md","XGGame-Snake","https:\u002F\u002Fgithub.com\u002FXXGGG\u002FXGGame-Snake",{},"\u002Fdevlog\u002Fxggame-snake\u002F06-systems",{"title":5,"description":1697},"devlog\u002Fxggame-snake\u002F06-systems","qSjB3Pzf525dDGKOCA04ufOEKkttkzIf8Z-_FHf8EQM",[1707,2003,2453,3172,4610,4979,6327,7079,8097,8461,9656,9842],{"id":1708,"title":1709,"body":1710,"cover":1695,"date":1997,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":1998,"navigation":129,"path":1999,"seo":2000,"stem":2001,"toc":129,"__hash__":2002},"devlog\u002Fdevlog\u002Fxggame-snake\u002F01-design.md","立项",{"type":7,"value":1711,"toc":1992},[1712,1719,1722,1725,1800,1804,1811,1814,1818,1825,1831,1836,1963,1966,1980,1983,1989],[10,1713,1714,1715,1718],{},"贪吃蛇的玩法早就定了，为什么还要立项？因为做到一半你一定会想加东西——\"要不要加道具？加穿墙？加皮肤？\"。",[22,1716,1717],{},"「不做什么」现在不写死，之后就没有任何东西能拦住你。"," 小鸟能按时做完，就是因为一开始写死了\"无关卡、无道具、无皮肤\"。",[91,1720,1721],{"id":1721},"立项表",[10,1723,1724],{},"这张表就是这个游戏的全部规则，代码只是把它翻译成 Godot 听得懂的话：",[26,1726,1727,1737],{},[29,1728,1729],{},[32,1730,1731,1734],{},[35,1732,1733],{},"项",[35,1735,1736],{},"内容",[45,1738,1739,1747,1755,1763,1771,1782,1792],{},[32,1740,1741,1744],{},[50,1742,1743],{},"核心玩法",[50,1745,1746],{},"控制蛇移动，吃到普通食物变长 +1 分；吃到星星变长 +2 分",[32,1748,1749,1752],{},[50,1750,1751],{},"食物规则",[50,1753,1754],{},"普通食物每 3 秒长一个，场上最多 3 个，吃掉不立刻补。星星每 12 秒一颗，会发光，5 秒没人吃就消失",[32,1756,1757,1760],{},[50,1758,1759],{},"开局设置",[50,1761,1762],{},"撞墙模式 \u002F 穿墙模式，开始界面用 ◀ ▶ 左右选",[32,1764,1765,1768],{},[50,1766,1767],{},"胜负",[50,1769,1770],{},"撞到自己 → 结束；撞墙模式下撞墙 → 结束；穿墙模式下从对面出来",[32,1772,1773,1776],{},[50,1774,1775],{},"操作",[50,1777,1778,1779],{},"键盘（方向键 \u002F WASD \u002F Esc）和手柄（十字键 \u002F 左摇杆 \u002F Start），",[22,1780,1781],{},"全程不碰鼠标也能玩完",[32,1783,1784,1789],{},[50,1785,1786],{},[22,1787,1788],{},"【不做】",[50,1790,1791],{},"无关卡、无第二种道具、无联机、无排行榜、无像素美术（纯色方块）、无背景音乐",[32,1793,1794,1797],{},[50,1795,1796],{},"完成标准",[50,1798,1799],{},"菜单选模式 → 游戏 → 暂停\u002F继续 → 撞死 → 看分数和最高分 → 重开 \u002F 回菜单；只用手柄能走完；导出 Web",[91,1801,1803],{"id":1802},"为什么食物要按时间长吃掉不补","为什么食物要\"按时间长、吃掉不补\"",[10,1805,1806,1807,1810],{},"经典贪吃蛇里食物永远只有一个，玩家只有一件事：去吃它。改成按时间生成之后，场上可能有 0～3 个食物、一颗随时会消失的星星——玩家要",[22,1808,1809],{},"做选择","：去远的那个？等近处长出来？绕过去抢星星值不值？",[10,1812,1813],{},"这些选择就是\"玩法\"。数据结构只多了一个数组，体验完全不一样。这是我在这个项目里最满意的一个决定。",[91,1815,1817],{"id":1816},"实体清单想清楚再动手","实体清单：想清楚再动手",[10,1819,1820,1821,1824],{},"小鸟那次的真正根因，不是\"忘了配碰撞层\"，是一上来就写代码、没先问\"谁要检测谁\"，最后只能用 ",[14,1822,1823],{},"body.name"," 打补丁。这次先拿纸笔回答两个问题：",[10,1826,1827,1830],{},[22,1828,1829],{},"A：这个游戏里有哪些「东西」？"," → 蛇头 · 蛇身 · 普通食物（0～3 个）· 星星（0 或 1 个，会过期）· 墙",[10,1832,1833],{},[22,1834,1835],{},"B：谁碰到谁会怎样？用什么判定？",[26,1837,1838,1853],{},[29,1839,1840],{},[32,1841,1842,1845,1848,1850],{},[35,1843,1844],{},"谁",[35,1846,1847],{},"碰到谁",[35,1849,1061],{},[35,1851,1852],{},"用什么判定",[45,1854,1855,1869,1882,1897,1912,1928,1946],{},[32,1856,1857,1860,1863,1866],{},[50,1858,1859],{},"蛇头",[50,1861,1862],{},"墙（撞墙模式）",[50,1864,1865],{},"死",[50,1867,1868],{},"坐标超出 0～格数-1？→ 数字比较",[32,1870,1871,1873,1876,1879],{},[50,1872,1859],{},[50,1874,1875],{},"墙（穿墙模式）",[50,1877,1878],{},"从对面出来",[50,1880,1881],{},"坐标绕一圈 → 取余",[32,1883,1884,1886,1889,1891],{},[50,1885,1859],{},[50,1887,1888],{},"蛇身",[50,1890,1865],{},[50,1892,1893,1894],{},"坐标在数组里？→ ",[14,1895,1896],{},"in",[32,1898,1899,1901,1904,1907],{},[50,1900,1859],{},[50,1902,1903],{},"普通食物",[50,1905,1906],{},"变长 +1",[50,1908,1909,1910],{},"坐标在食物数组里？→ ",[14,1911,1896],{},[32,1913,1914,1916,1919,1922],{},[50,1915,1859],{},[50,1917,1918],{},"星星",[50,1920,1921],{},"变长 +2",[50,1923,1924,1925],{},"两个坐标相等？→ ",[14,1926,1927],{},"==",[32,1929,1930,1932,1937,1940],{},[50,1931,1918],{},[50,1933,1934],{},[22,1935,1936],{},"时间",[50,1938,1939],{},"5 秒后消失",[50,1941,1942,1945],{},[14,1943,1944],{},"Timer","（One Shot）",[32,1947,1948,1951,1955,1958],{},[50,1949,1950],{},"棋盘",[50,1952,1953],{},[22,1954,1936],{},[50,1956,1957],{},"每 3 秒长一个食物",[50,1959,1960,1962],{},[14,1961,1944],{},"（重复）",[10,1964,1965],{},"填完，结论自己就出来了：",[720,1967,1968,1974],{},[723,1969,1970,1971],{},"前五行全是坐标运算 → ",[22,1972,1973],{},"不需要物理引擎，不用配碰撞层，不用 Area2D",[723,1975,1976,1977],{},"后两行多出来一个对手叫\"时间\" → ",[22,1978,1979],{},"答案是 Timer",[10,1981,1982],{},"什么时候才需要物理引擎？当表里出现\"两个不规则形状重叠了吗\"\"它会被挡住吗\"这种回答。下一个需要它的游戏（打砖块）填这张表时答案会变成\"形状重叠\"——那时再配图层，就知道自己在配什么。",[1984,1985,1986],"blockquote",{},[10,1987,1988],{},"这一步的价值不是\"配图层\"，是\"想清楚再动手\"。很多游戏（网格类、回合制、卡牌）根本用不到物理引擎，硬配图层只是白做功。",[10,1990,1991],{},"下一章开 Godot，把这些数变成项目设置。",{"title":101,"searchDepth":119,"depth":119,"links":1993},[1994,1995,1996],{"id":1721,"depth":119,"text":1721},{"id":1802,"depth":119,"text":1803},{"id":1816,"depth":119,"text":1817},"2026-09-01",{},"\u002Fdevlog\u002Fxggame-snake\u002F01-design",{"title":1709,"description":1697},"devlog\u002Fxggame-snake\u002F01-design","dspGLT_dt9cPUJFowsqBo5wvyalTU63UddPK3JCXLQQ",{"id":2004,"title":2005,"body":2006,"cover":1695,"date":2447,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":2448,"navigation":129,"path":2449,"seo":2450,"stem":2451,"toc":129,"__hash__":2452},"devlog\u002Fdevlog\u002Fxggame-snake\u002F02-setup.md","项目设置",{"type":7,"value":2007,"toc":2440},[2008,2011,2015,2022,2030,2040,2057,2061,2064,2113,2123,2126,2133,2137,2144,2257,2271,2280,2286,2290,2304,2378,2392,2398,2401,2427,2437],[10,2009,2010],{},"为什么\"设置\"先于\"代码\"？因为分辨率、格子数、按键名全都会变成代码里的常量和字符串。先在设置里定下来，代码只要抄一次；反过来先写代码再改设置，每个用到的地方都要改。",[91,2012,2014],{"id":2013},"目录先建场景后建","目录先建，场景后建",[10,2016,2017,2018,2021],{},"新建项目（渲染器选",[22,2019,2020],{},"兼容","，Web 导出的硬要求），第一件事不是新建场景，是建文件夹：",[96,2023,2028],{"className":2024,"code":2026,"language":2027},[2025],"language-text","assets\u002F\n├── fonts\u002F\n├── audio\u002Fsfx\u002F\n└── ui\u002F\nautoload\u002F        ← 4 个全局单例的 .gd 和 .tscn 全放这\nscenes\u002F\nscripts\u002F\ntheme\u002F\nbuild\u002F           ← 导出产物，不进仓库\n","text",[14,2029,2026],{"__ignoreMap":101},[10,2031,2032,2039],{},[22,2033,2034,2035,2038],{},"没有 ",[14,2036,2037],{},"scenes\u002F"," 就先建场景，它会落在根目录，以后挪要改一堆引用。"," 先有抽屉，再往里放东西。",[10,2041,2042,2045,2046,2049,2050,2053,2054],{},[14,2043,2044],{},"autoload\u002F"," 单独一层，不拆成 ",[14,2047,2048],{},"scenes\u002Fautoload\u002F"," + ",[14,2051,2052],{},"scripts\u002Fautoload\u002F","：全局单例是一个\"类别\"，总共 4～5 个文件，拆到两处只是让你多点两次。",[22,2055,2056],{},"按\"是什么\"分，不按\"文件类型\"分。",[91,2058,2060],{"id":2059},"分辨率先定格子再算画面","分辨率：先定格子，再算画面",[10,2062,2063],{},"项目 → 项目设置 → 显示 → 窗口：",[26,2065,2066,2075],{},[29,2067,2068],{},[32,2069,2070,2072],{},[35,2071,1733],{},[35,2073,2074],{},"值",[45,2076,2077,2085,2095,2105],{},[32,2078,2079,2082],{},[50,2080,2081],{},"视口宽 \u002F 高",[50,2083,2084],{},"1920 × 1080",[32,2086,2087,2090],{},[50,2088,2089],{},"拉伸 → 模式",[50,2091,2092],{},[14,2093,2094],{},"canvas_items",[32,2096,2097,2100],{},[50,2098,2099],{},"拉伸 → 比例",[50,2101,2102],{},[14,2103,2104],{},"keep",[32,2106,2107,2110],{},[50,2108,2109],{},"尺寸 → 窗口宽度 \u002F 高度覆盖",[50,2111,2112],{},"1280 × 720",[10,2114,2115,2116,2119,2120],{},"每格 20 像素 → 96 × 54 格。为什么是 20？一格的像素数必须",[22,2117,2118],{},"同时整除 1920 和 1080","，蛇才不会卡在半格上。能整除的只有 8 · 10 · 12 · 15 · 20 · 24 · 30 · 40 · 60 · 120——想换档位只能从这里挑，挑 16 会得到 67.5 格。",[22,2121,2122],{},"格子类游戏先定每格像素，再算格数，永远这个顺序。",[10,2124,2125],{},"\"窗口覆盖 1280×720\"是什么：视口是\"画有多大\"，窗口是\"看画的框有多大\"。画是 1080p 的，框在显示器上开到 1080p 会连标题栏一起超出屏幕。框缩到 720p，画照样是 1080p，拉伸模式会把它等比缩进去。发布后玩家的窗口多大都一样，这两项只影响自己测试。",[10,2127,2128],{},[2129,2130],"img",{"alt":2131,"src":2132},"02-setup-窗口设置","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F02-setup-%E7%AA%97%E5%8F%A3%E8%AE%BE%E7%BD%AE.png",[91,2134,2136],{"id":2135},"输入映射键盘和手柄一起绑","输入映射：键盘和手柄一起绑",[10,2138,2139,2140,2143],{},"项目 → 项目设置 → 输入映射。",[22,2141,2142],{},"每个动作既绑键盘也绑手柄","——手柄不是一个功能，是一种假设，从第一天就得成立：",[26,2145,2146,2159],{},[29,2147,2148],{},[32,2149,2150,2153,2156],{},[35,2151,2152],{},"动作名",[35,2154,2155],{},"键盘",[35,2157,2158],{},"手柄",[45,2160,2161,2174,2187,2200,2213,2226,2242],{},[32,2162,2163,2168,2171],{},[50,2164,2165],{},[14,2166,2167],{},"move_up",[50,2169,2170],{},"↑ 和 W",[50,2172,2173],{},"十字键 ↑ 和 左摇杆往上",[32,2175,2176,2181,2184],{},[50,2177,2178],{},[14,2179,2180],{},"move_down",[50,2182,2183],{},"↓ 和 S",[50,2185,2186],{},"十字键 ↓ 和 左摇杆往下",[32,2188,2189,2194,2197],{},[50,2190,2191],{},[14,2192,2193],{},"move_left",[50,2195,2196],{},"← 和 A",[50,2198,2199],{},"十字键 ← 和 左摇杆往左",[32,2201,2202,2207,2210],{},[50,2203,2204],{},[14,2205,2206],{},"move_right",[50,2208,2209],{},"→ 和 D",[50,2211,2212],{},"十字键 → 和 左摇杆往右",[32,2214,2215,2220,2223],{},[50,2216,2217],{},[14,2218,2219],{},"pause",[50,2221,2222],{},"Esc 和 P",[50,2224,2225],{},"Start",[32,2227,2228,2234,2237],{},[50,2229,2230,2233],{},[14,2231,2232],{},"ui_accept","（内置）",[50,2235,2236],{},"已有 Enter \u002F 空格",[50,2238,2239],{},[22,2240,2241],{},"补一个 A",[32,2243,2244,2249,2252],{},[50,2245,2246,2233],{},[14,2247,2248],{},"ui_cancel",[50,2250,2251],{},"已有 Esc",[50,2253,2254],{},[22,2255,2256],{},"补一个 B",[10,2258,2259,2260,2266,2267,2270],{},"最后两行要勾「显示内置动作」才看得到。",[22,2261,2262,2263,2265],{},"Godot 4 默认 ",[14,2264,2232],{}," 只有 Enter 和空格，没有手柄键","（方向类的 ",[14,2268,2269],{},"ui_up"," 等有），不补的话手柄能移焦点但按不下按钮——这个坑我在第 9 章栽了一次才知道。",[10,2272,2273,2274,2276,2277],{},"为什么要给按键起名字，代码里直接判断\"按了 W\"不行吗？行，但你看上面那张表：一个名字挂了 4 个键。代码只认 ",[14,2275,2167],{},"，至于是 W、↑、十字键还是摇杆，代码根本不知道也不用知道。",[22,2278,2279],{},"配置和代码分离。",[10,2281,2282],{},[2129,2283],{"alt":2284,"src":2285},"02-setup-输入映射","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F02-setup-%E8%BE%93%E5%85%A5%E6%98%A0%E5%B0%84.png",[91,2287,2289],{"id":2288},"autoload-骨架先把名字占住","Autoload 骨架：先把名字占住",[10,2291,2292,2293,2296,2297,2299,2300,2303],{},"阶段 2 的代码会写 ",[14,2294,2295],{},"EventBus.snake_died.emit()"," 这种句子——",[14,2298,54],{}," 这个名字必须先存在，代码才不报错。所以现在就建 4 个\"空\"文件，每个脚本只写一行 ",[14,2301,2302],{},"extends Node","：",[26,2305,2306,2319],{},[29,2307,2308],{},[32,2309,2310,2313,2316],{},[35,2311,2312],{},"顺序",[35,2314,2315],{},"名称",[35,2317,2318],{},"路径",[45,2320,2321,2336,2349,2363],{},[32,2322,2323,2327,2331],{},[50,2324,2325],{},[22,2326,886],{},[50,2328,2329],{},[14,2330,54],{},[50,2332,2333],{},[14,2334,2335],{},"res:\u002F\u002Fautoload\u002Fevent_bus.gd",[32,2337,2338,2340,2344],{},[50,2339,913],{},[50,2341,2342],{},[14,2343,83],{},[50,2345,2346],{},[14,2347,2348],{},"res:\u002F\u002Fautoload\u002Fsave_manager.gd",[32,2350,2351,2354,2358],{},[50,2352,2353],{},"3",[50,2355,2356],{},[14,2357,70],{},[50,2359,2360],{},[14,2361,2362],{},"res:\u002F\u002Fautoload\u002Fgame_manager.gd",[32,2364,2365,2368,2373],{},[50,2366,2367],{},"4",[50,2369,2370],{},[14,2371,2372],{},"AudioManager",[50,2374,2375],{},[14,2376,2377],{},"res:\u002F\u002Fautoload\u002Faudio_manager.tscn",[10,2379,2380,2381,2384,2385,2388,2389],{},"顺序不能乱：列表从上往下依次加载，",[14,2382,2383],{},"GameManager._ready()"," 里会写 ",[14,2386,2387],{},"EventBus.xxx.connect(...)","，如果 EventBus 排在它后面，那一刻这个名字还不存在，游戏直接崩。",[22,2390,2391],{},"规矩：谁被别人连，谁排前面。EventBus 永远第一。",[10,2393,2394],{},[2129,2395],{"alt":2396,"src":2397},"02-setup-自动加载","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F02-setup-%E8%87%AA%E5%8A%A8%E5%8A%A0%E8%BD%BD.png",[91,2399,2400],{"id":2400},"git",[10,2402,2403,2404,2407,2408,2411,2412,2411,2415,2418,2419,2422,2423,2426],{},"项目 → 版本控制 → 生成版本控制元数据，",[14,2405,2406],{},".gitignore"," 里加 ",[14,2409,2410],{},"build\u002F","、",[14,2413,2414],{},"*.zip",[14,2416,2417],{},".DS_Store","，根目录建一个空的 ",[14,2420,2421],{},"CREDITS.md","，然后 ",[14,2424,2425],{},"git init -b main","、第一条提交。",[10,2428,2429,2430,2433,2434,2436],{},"为什么现在就 git，代码都还没写？因为下一章会大改。有了第一条提交，改坏了随时能回到\"干净的骨架\"。",[22,2431,2432],{},"git 是后悔药，后悔药要在犯错之前买。"," ",[14,2435,2421],{}," 现在就建，是因为小鸟那次三个音效的来源完全想不起来了——以后每引入一份素材当场写一行。",[10,2438,2439],{},"下一章写代码，先让蛇在控制台里\"动\"起来。",{"title":101,"searchDepth":119,"depth":119,"links":2441},[2442,2443,2444,2445,2446],{"id":2013,"depth":119,"text":2014},{"id":2059,"depth":119,"text":2060},{"id":2135,"depth":119,"text":2136},{"id":2288,"depth":119,"text":2289},{"id":2400,"depth":119,"text":2400},"2026-09-02",{},"\u002Fdevlog\u002Fxggame-snake\u002F02-setup",{"title":2005,"description":1697},"devlog\u002Fxggame-snake\u002F02-setup","5cE2MyvWU2DCeFS0duNxsvEIBxPmRKFgGH84GPFUPMQ",{"id":2454,"title":2455,"body":2456,"cover":1695,"date":3166,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":3167,"navigation":129,"path":3168,"seo":3169,"stem":3170,"toc":129,"__hash__":3171},"devlog\u002Fdevlog\u002Fxggame-snake\u002F03-snake-array.md","原型1：蛇是一个数组",{"type":7,"value":2457,"toc":3160},[2458,2465,2469,2548,2551,2554,2568,2574,2580,2589,2592,2931,2934,3000,3007,3011,3023,3125,3136,3141,3147,3154,3157],[10,2459,2460,2461,2464],{},"贪吃蛇玩法早就定了，为什么还要\"白块原型\"？为了",[22,2462,2463],{},"逻辑与表现分离","：先让数据对，再让画面好看。这样出 bug 时能一眼定位——数组里坐标就是错的 → 逻辑 bug；数组对但画错位置 → 渲染 bug。一上来就连美术一起做，出了问题得同时怀疑两层，调试时间翻倍。",[91,2466,2468],{"id":2467},"先换脑子贪吃蛇和小鸟的根本区别","先换脑子：贪吃蛇和小鸟的根本区别",[26,2470,2471,2483],{},[29,2472,2473],{},[32,2474,2475,2477,2480],{},[35,2476],{},[35,2478,2479],{},"小鸟（连续）",[35,2481,2482],{},"贪吃蛇（离散）",[45,2484,2485,2504,2517,2535],{},[32,2486,2487,2490,2496],{},[50,2488,2489],{},"位置",[50,2491,2492,2493],{},"任意小数 ",[14,2494,2495],{},"(317.4, 892.1)",[50,2497,2498,2433,2501],{},[22,2499,2500],{},"只能在格子上",[14,2502,2503],{},"(5, 8)",[32,2505,2506,2509,2512],{},[50,2507,2508],{},"移动",[50,2510,2511],{},"每帧挪一点点",[50,2513,2514],{},[22,2515,2516],{},"每 0.15 秒跳一整格",[32,2518,2519,2522,2528],{},[50,2520,2521],{},"谁在驱动",[50,2523,2524,2527],{},[14,2525,2526],{},"_physics_process(delta)"," 每秒 60 次",[50,2529,2530],{},[22,2531,2532,2534],{},[14,2533,1944],{}," 到点发一次信号",[32,2536,2537,2540,2543],{},[50,2538,2539],{},"碰撞",[50,2541,2542],{},"物理引擎算",[50,2544,2545],{},[22,2546,2547],{},"比较两个格子坐标相不相等",[10,2549,2550],{},"这是整个项目最重要的观念转变：贪吃蛇里几乎没有\"物理\"，只有\"坐标\"和\"时间\"。",[91,2552,2553],{"id":2553},"搭场景",[10,2555,2556,2559,2560,2563,2564,2567],{},[14,2557,2558],{},"scenes\u002Fmain.tscn","，根节点 ",[14,2561,2562],{},"Node2D"," 命名 ",[14,2565,2566],{},"Main","，下面 8 个子节点：",[96,2569,2572],{"className":2570,"code":2571,"language":2027},[2025],"Main (Node2D)                    ← 挂脚本 scripts\u002Fmain.gd\n├── Board (ColorRect)            ← 棋盘底色，Size 1920×1080\n├── SnakeCells (Node2D)          ← 空容器，蛇的方块生成在它下面\n├── FoodCells (Node2D)           ← 空容器，食物的方块生成在它下面\n├── StarCell (ColorRect)         ← 星星，Size 20×20，Visible 先关掉\n├── MoveTimer (Timer)            ← Wait Time 0.15，Autostart\n├── FoodTimer (Timer)            ← Wait Time 3，Autostart\n├── StarSpawnTimer (Timer)       ← Wait Time 12，Autostart\n└── StarLifeTimer (Timer)        ← Wait Time 5，One Shot，不 Autostart\n",[14,2573,2571],{"__ignoreMap":101},[10,2575,2576,2577],{},"为什么蛇和食物是\"空容器\"，星星却直接是一个方块？数量不固定的东西（蛇身 3～400 节，食物 0～3 个）用\"容器 + 动态生成子节点\"来画；数量固定最多 1 个的，直接放一个节点，用显示\u002F隐藏控制。",[22,2578,2579],{},"\"有几个\"决定\"怎么画\"。",[10,2581,2582,2583,2585,2586,2588],{},"为什么 4 个 Timer 不合成一个？一个 Timer 只管一件事，名字就是说明书：看到 ",[14,2584,1010],{}," 在跑，就知道\"星星还活着\"。合成一个省 3 个节点，换来一堆 ",[14,2587,761],{}," 判断\"这次响是为了什么\"。节点便宜，脑子贵。",[91,2590,2591],{"id":2591},"数据结构",[96,2593,2595],{"className":98,"code":2594,"language":100,"meta":101,"style":101},"extends Node2D\n\n## ===== 常量：整个游戏只认这几个数 =====\nconst GRID_W := 96                     # 横向几格\nconst GRID_H := 54                     # 纵向几格\nconst CELL_SIZE := 20                  # 一格是几个像素   96 × 20 = 1920，54 × 20 = 1080\nconst MAX_FOOD := 3                    # 场上最多几个普通食物\nconst NO_CELL := Vector2i(-1, -1)      # 「没有」—— 一个棋盘上不存在的格子\n\n## ===== 数据：蛇 = 坐标数组，第 0 个是头 =====\nvar snake_body: Array[Vector2i] = [\n    Vector2i(48, 27),   # 头（正中间）\n    Vector2i(47, 27),\n    Vector2i(46, 27),   # 尾\n]\nvar direction := Vector2i.RIGHT        # 现在正在往哪走\nvar next_direction := Vector2i.RIGHT   # 玩家刚按的方向，下一步才生效\n\n## ===== 数据：食物 =====\nvar foods: Array[Vector2i] = [Vector2i(60, 27)]   # 普通食物，先手放一个\nvar star := NO_CELL                    # 星星，最多 1 颗；NO_CELL 表示现在没有\n\nfunc _ready() -> void:\n    print(snake_body)\n",[14,2596,2597,2604,2608,2613,2628,2643,2658,2673,2703,2707,2712,2737,2760,2778,2798,2803,2822,2840,2844,2849,2887,2901,2905,2919],{"__ignoreMap":101},[105,2598,2599,2601],{"class":107,"line":108},[105,2600,112],{"class":111},[105,2602,2603],{"class":115}," Node2D\n",[105,2605,2606],{"class":107,"line":119},[105,2607,130],{"emptyLinePlaceholder":129},[105,2609,2610],{"class":107,"line":126},[105,2611,2612],{"class":122},"## ===== 常量：整个游戏只认这几个数 =====\n",[105,2614,2615,2617,2620,2622,2625],{"class":107,"line":133},[105,2616,1101],{"class":154},[105,2618,2619],{"class":270}," GRID_W",[105,2621,301],{"class":140},[105,2623,2624],{"class":325}," 96",[105,2626,2627],{"class":122},"                     # 横向几格\n",[105,2629,2630,2632,2635,2637,2640],{"class":107,"line":151},[105,2631,1101],{"class":154},[105,2633,2634],{"class":270}," GRID_H",[105,2636,301],{"class":140},[105,2638,2639],{"class":325}," 54",[105,2641,2642],{"class":122},"                     # 纵向几格\n",[105,2644,2645,2647,2650,2652,2655],{"class":107,"line":179},[105,2646,1101],{"class":154},[105,2648,2649],{"class":270}," CELL_SIZE",[105,2651,301],{"class":140},[105,2653,2654],{"class":325}," 20",[105,2656,2657],{"class":122},"                  # 一格是几个像素   96 × 20 = 1920，54 × 20 = 1080\n",[105,2659,2660,2662,2665,2667,2670],{"class":107,"line":184},[105,2661,1101],{"class":154},[105,2663,2664],{"class":270}," MAX_FOOD",[105,2666,301],{"class":140},[105,2668,2669],{"class":325}," 3",[105,2671,2672],{"class":122},"                    # 场上最多几个普通食物\n",[105,2674,2675,2677,2680,2682,2685,2687,2690,2692,2694,2696,2698,2700],{"class":107,"line":195},[105,2676,1101],{"class":154},[105,2678,2679],{"class":270}," NO_CELL",[105,2681,301],{"class":140},[105,2683,2684],{"class":115}," Vector2i",[105,2686,141],{"class":140},[105,2688,2689],{"class":154},"-",[105,2691,886],{"class":325},[105,2693,274],{"class":163},[105,2695,2689],{"class":154},[105,2697,886],{"class":325},[105,2699,173],{"class":140},[105,2701,2702],{"class":122},"      # 「没有」—— 一个棋盘上不存在的格子\n",[105,2704,2705],{"class":107,"line":340},[105,2706,130],{"emptyLinePlaceholder":129},[105,2708,2709],{"class":107,"line":345},[105,2710,2711],{"class":122},"## ===== 数据：蛇 = 坐标数组，第 0 个是头 =====\n",[105,2713,2714,2716,2719,2721,2724,2727,2730,2732,2734],{"class":107,"line":364},[105,2715,295],{"class":154},[105,2717,2718],{"class":270}," snake_body",[105,2720,167],{"class":140},[105,2722,2723],{"class":115}," Array",[105,2725,2726],{"class":140},"[",[105,2728,2729],{"class":115},"Vector2i",[105,2731,1013],{"class":140},[105,2733,515],{"class":140},[105,2735,2736],{"class":140}," [\n",[105,2738,2739,2742,2744,2747,2749,2752,2754,2757],{"class":107,"line":383},[105,2740,2741],{"class":115},"    Vector2i",[105,2743,141],{"class":140},[105,2745,2746],{"class":325},"48",[105,2748,274],{"class":163},[105,2750,2751],{"class":325},"27",[105,2753,173],{"class":140},[105,2755,2756],{"class":163},",   ",[105,2758,2759],{"class":122},"# 头（正中间）\n",[105,2761,2762,2764,2766,2769,2771,2773,2775],{"class":107,"line":388},[105,2763,2741],{"class":115},[105,2765,141],{"class":140},[105,2767,2768],{"class":325},"47",[105,2770,274],{"class":163},[105,2772,2751],{"class":325},[105,2774,173],{"class":140},[105,2776,2777],{"class":163},",\n",[105,2779,2780,2782,2784,2787,2789,2791,2793,2795],{"class":107,"line":409},[105,2781,2741],{"class":115},[105,2783,141],{"class":140},[105,2785,2786],{"class":325},"46",[105,2788,274],{"class":163},[105,2790,2751],{"class":325},[105,2792,173],{"class":140},[105,2794,2756],{"class":163},[105,2796,2797],{"class":122},"# 尾\n",[105,2799,2800],{"class":107,"line":425},[105,2801,2802],{"class":140},"]\n",[105,2804,2805,2807,2810,2812,2814,2816,2819],{"class":107,"line":450},[105,2806,295],{"class":154},[105,2808,2809],{"class":270}," direction",[105,2811,301],{"class":140},[105,2813,2684],{"class":115},[105,2815,306],{"class":140},[105,2817,2818],{"class":111},"RIGHT",[105,2820,2821],{"class":122},"        # 现在正在往哪走\n",[105,2823,2824,2826,2829,2831,2833,2835,2837],{"class":107,"line":474},[105,2825,295],{"class":154},[105,2827,2828],{"class":270}," next_direction",[105,2830,301],{"class":140},[105,2832,2684],{"class":115},[105,2834,306],{"class":140},[105,2836,2818],{"class":111},[105,2838,2839],{"class":122},"   # 玩家刚按的方向，下一步才生效\n",[105,2841,2842],{"class":107,"line":479},[105,2843,130],{"emptyLinePlaceholder":129},[105,2845,2846],{"class":107,"line":485},[105,2847,2848],{"class":122},"## ===== 数据：食物 =====\n",[105,2850,2851,2853,2856,2858,2860,2862,2864,2866,2868,2870,2872,2874,2877,2879,2881,2884],{"class":107,"line":509},[105,2852,295],{"class":154},[105,2854,2855],{"class":270}," foods",[105,2857,167],{"class":140},[105,2859,2723],{"class":115},[105,2861,2726],{"class":140},[105,2863,2729],{"class":115},[105,2865,1013],{"class":140},[105,2867,515],{"class":140},[105,2869,982],{"class":140},[105,2871,2729],{"class":115},[105,2873,141],{"class":140},[105,2875,2876],{"class":325},"60",[105,2878,274],{"class":163},[105,2880,2751],{"class":325},[105,2882,2883],{"class":140},")]",[105,2885,2886],{"class":122},"   # 普通食物，先手放一个\n",[105,2888,2889,2891,2894,2896,2898],{"class":107,"line":521},[105,2890,295],{"class":154},[105,2892,2893],{"class":270}," star",[105,2895,301],{"class":140},[105,2897,2679],{"class":270},[105,2899,2900],{"class":122},"                    # 星星，最多 1 颗；NO_CELL 表示现在没有\n",[105,2902,2903],{"class":107,"line":538},[105,2904,130],{"emptyLinePlaceholder":129},[105,2906,2907,2909,2911,2913,2915,2917],{"class":107,"line":543},[105,2908,391],{"class":154},[105,2910,394],{"class":136},[105,2912,397],{"class":140},[105,2914,400],{"class":154},[105,2916,403],{"class":115},[105,2918,406],{"class":140},[105,2920,2921,2924,2926,2929],{"class":107,"line":567},[105,2922,2923],{"class":136},"    print",[105,2925,141],{"class":140},[105,2927,2928],{"class":270},"snake_body",[105,2930,148],{"class":140},[10,2932,2933],{},"几行关键的解释：",[720,2935,2936,2951,2962,2972,2985],{},[723,2937,2938,2942,2943,2946,2947,2950],{},[22,2939,2940],{},[14,2941,2729],{},"：Vector 2 integer，只存整数的二维坐标。格子坐标天然是整数；用 ",[14,2944,2945],{},"Vector2"," 会出现 ",[14,2948,2949],{},"(5.0000001, 8)","，比较\"相等\"会失败",[723,2952,2953,2958,2959,2961],{},[22,2954,2955],{},[14,2956,2957],{},"Array[Vector2i]","：类型化数组，只装 ",[14,2960,2729],{},"，塞别的当场报错",[723,2963,2964,2967,2968,2971],{},[22,2965,2966],{},"第 0 个是头","：移动 = 头部插一格 + 尾部删一格。头放数组开头，",[14,2969,2970],{},"insert(0, ...)"," 最直观",[723,2973,2974,2984],{},[22,2975,2976,2979,2980,2983],{},[14,2977,2978],{},"foods"," 是数组，",[14,2981,2982],{},"star"," 不是","：和场景结构一一对应——数据结构和场景结构长得一样",[723,2986,2987,2992,2993,2995,2996,2999],{},[22,2988,2989],{},[14,2990,2991],{},"NO_CELL = (-1, -1)","：哨兵值。",[14,2994,2729],{}," 类型的变量不能存 ",[14,2997,2998],{},"null","，那\"现在没有星星\"怎么表示？用一个棋盘上不可能出现的坐标当\"没有\"",[10,3001,3002,3003,3006],{},"F5，输出面板打印 ",[14,3004,3005],{},"[(48, 27), (47, 27), (46, 27)]","。画面还是没东西，正常——先验数据，再画。",[91,3008,3010],{"id":3009},"让蛇动起来先在控制台动","让蛇\"动\"起来（先在控制台动）",[10,3012,3013,3014,3016,3017,3020,3021,2303],{},"选中 ",[14,3015,989],{}," → 节点面板 → 信号 → 双击 ",[14,3018,3019],{},"timeout()"," → 连接到 ",[14,3022,2566],{},[96,3024,3026],{"className":98,"code":3025,"language":100,"meta":101,"style":101},"func _on_move_timer_timeout() -> void:\n    direction = next_direction          # 玩家最近按的方向，现在才真正生效\n    var new_head := snake_body[0] + direction\n    snake_body.insert(0, new_head)      # ① 头部插入新格子\n    snake_body.pop_back()               # ② 删掉尾巴\n    print(snake_body)\n",[14,3027,3028,3042,3054,3078,3101,3115],{"__ignoreMap":101},[105,3029,3030,3032,3034,3036,3038,3040],{"class":107,"line":108},[105,3031,391],{"class":154},[105,3033,795],{"class":136},[105,3035,397],{"class":140},[105,3037,400],{"class":154},[105,3039,403],{"class":115},[105,3041,406],{"class":140},[105,3043,3044,3047,3049,3051],{"class":107,"line":119},[105,3045,3046],{"class":270},"    direction",[105,3048,515],{"class":140},[105,3050,2828],{"class":270},[105,3052,3053],{"class":122},"          # 玩家最近按的方向，现在才真正生效\n",[105,3055,3056,3058,3061,3063,3065,3067,3070,3072,3075],{"class":107,"line":126},[105,3057,1133],{"class":154},[105,3059,3060],{"class":270}," new_head",[105,3062,301],{"class":140},[105,3064,2718],{"class":270},[105,3066,2726],{"class":140},[105,3068,3069],{"class":325},"0",[105,3071,1013],{"class":140},[105,3073,3074],{"class":154}," +",[105,3076,3077],{"class":270}," direction\n",[105,3079,3080,3083,3085,3088,3090,3092,3094,3096,3098],{"class":107,"line":133},[105,3081,3082],{"class":270},"    snake_body",[105,3084,306],{"class":163},[105,3086,3087],{"class":136},"insert",[105,3089,141],{"class":140},[105,3091,3069],{"class":325},[105,3093,1180],{"class":140},[105,3095,3060],{"class":270},[105,3097,173],{"class":140},[105,3099,3100],{"class":122},"      # ① 头部插入新格子\n",[105,3102,3103,3105,3107,3110,3112],{"class":107,"line":151},[105,3104,3082],{"class":270},[105,3106,306],{"class":163},[105,3108,3109],{"class":136},"pop_back",[105,3111,397],{"class":140},[105,3113,3114],{"class":122},"               # ② 删掉尾巴\n",[105,3116,3117,3119,3121,3123],{"class":107,"line":179},[105,3118,2923],{"class":136},[105,3120,141],{"class":140},[105,3122,2928],{"class":270},[105,3124,148],{"class":140},[10,3126,3127,2303,3133,3135],{},[22,3128,3129,3130],{},"为什么用 Timer，不用小鸟那种 ",[14,3131,3132],{},"_physics_process",[14,3134,3132],{}," 每秒跑 60 次，适合\"每帧挪一点点\"。贪吃蛇是\"每 0.15 秒跳一格\"，用 Timer 到点才动一次，天然就是离散的。",[10,3137,3138,2303],{},[22,3139,3140],{},"为什么移动是\"插头删尾\"，不是\"每一节往前挪\"",[96,3142,3145],{"className":3143,"code":3144,"language":2027},[2025],"移动前  [头(48,27)] [(47,27)] [(46,27)]\n        ↓ 向右\n① 插头  [新头(49,27)] [(48,27)] [(47,27)] [(46,27)]\n② 删尾  [新头(49,27)] [(48,27)] [(47,27)]\n",[14,3146,3144],{"__ignoreMap":101},[10,3148,3149,3150,3153],{},"逐节挪：蛇 50 节要算 50 次，而且要从尾往头算（不然会覆盖）。插头删尾：永远只操作 2 个元素，不管多长。",[22,3151,3152],{},"而且\"吃到食物\"只要\"不删尾\"就自动变长——一行都不用多写。"," 这是贪吃蛇最漂亮的设计。",[10,3155,3156],{},"F5，输出面板每 0.15 秒打印一次，头的 x 一直加 1。数据先对了，下一章画出来。",[1684,3158,3159],{},"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 .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 .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);}",{"title":101,"searchDepth":119,"depth":119,"links":3161},[3162,3163,3164,3165],{"id":2467,"depth":119,"text":2468},{"id":2553,"depth":119,"text":2553},{"id":2591,"depth":119,"text":2591},{"id":3009,"depth":119,"text":3010},"2026-09-03",{},"\u002Fdevlog\u002Fxggame-snake\u002F03-snake-array",{"title":2455,"description":1697},"devlog\u002Fxggame-snake\u002F03-snake-array","xHXPui_5OOXPvhwukHEoLGlvpvRQnLEf-cDzaqo-eBM",{"id":3173,"title":3174,"body":3175,"cover":1695,"date":4604,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":4605,"navigation":129,"path":4606,"seo":4607,"stem":4608,"toc":129,"__hash__":4609},"devlog\u002Fdevlog\u002Fxggame-snake\u002F04-draw-and-collide.md","原型2：画面是数据的镜子",{"type":7,"value":3176,"toc":4598},[3177,3180,3195,3609,3665,3672,3687,3693,3696,3910,3913,3922,3948,3951,3964,3968,3977,4343,4372,4375,4558,4561,4592,4595],[91,3178,3179],{"id":3179},"画出来",[10,3181,3182,3183,3185,3186,216,3188,3191,3192,2303],{},"加在 ",[14,3184,16],{}," 里，并在 ",[14,3187,775],{},[14,3189,3190],{},"_on_move_timer_timeout()"," 末尾各调一次 ",[14,3193,3194],{},"redraw()",[96,3196,3198],{"className":98,"code":3197,"language":100,"meta":101,"style":101},"## 照着数据画方块。每次都先清空再重画 —— 简单，先跑通\nfunc redraw() -> void:\n    for cell in $SnakeCells.get_children():\n        cell.queue_free()                       # 删掉上一次画的蛇\n    for cell in $FoodCells.get_children():\n        cell.queue_free()                       # 删掉上一次画的食物\n\n    for grid_pos in snake_body:\n        _draw_cell($SnakeCells, grid_pos, Color.GREEN)\n    for grid_pos in foods:\n        _draw_cell($FoodCells, grid_pos, Color.RED)\n\n    $StarCell.visible = star != NO_CELL         # 有星星才显示\n    $StarCell.size = Vector2(CELL_SIZE, CELL_SIZE)   # 尺寸也由代码定，别信场景里的数\n    $StarCell.position = Vector2(star) * CELL_SIZE\n\n## 在某个容器下画一个格子。蛇和食物都用它\nfunc _draw_cell(parent: Node, grid_pos: Vector2i, color: Color) -> void:\n    var rect := ColorRect.new()\n    rect.size = Vector2(CELL_SIZE, CELL_SIZE)\n    rect.position = Vector2(grid_pos) * CELL_SIZE   # 格子坐标 → 像素坐标\n    rect.color = color\n    parent.add_child(rect)\n",[14,3199,3200,3205,3220,3244,3259,3280,3293,3297,3310,3337,3349,3374,3378,3402,3432,3459,3463,3468,3510,3529,3552,3578,3592],{"__ignoreMap":101},[105,3201,3202],{"class":107,"line":108},[105,3203,3204],{"class":122},"## 照着数据画方块。每次都先清空再重画 —— 简单，先跑通\n",[105,3206,3207,3209,3212,3214,3216,3218],{"class":107,"line":119},[105,3208,391],{"class":154},[105,3210,3211],{"class":136}," redraw",[105,3213,397],{"class":140},[105,3215,400],{"class":154},[105,3217,403],{"class":115},[105,3219,406],{"class":140},[105,3221,3222,3225,3228,3230,3232,3235,3237,3240,3242],{"class":107,"line":126},[105,3223,3224],{"class":111},"    for",[105,3226,3227],{"class":270}," cell",[105,3229,979],{"class":111},[105,3231,1479],{"class":111},[105,3233,3234],{"class":988},"SnakeCells",[105,3236,306],{"class":163},[105,3238,3239],{"class":136},"get_children",[105,3241,397],{"class":140},[105,3243,406],{"class":163},[105,3245,3246,3249,3251,3254,3256],{"class":107,"line":133},[105,3247,3248],{"class":270},"        cell",[105,3250,306],{"class":163},[105,3252,3253],{"class":136},"queue_free",[105,3255,397],{"class":140},[105,3257,3258],{"class":122},"                       # 删掉上一次画的蛇\n",[105,3260,3261,3263,3265,3267,3269,3272,3274,3276,3278],{"class":107,"line":151},[105,3262,3224],{"class":111},[105,3264,3227],{"class":270},[105,3266,979],{"class":111},[105,3268,1479],{"class":111},[105,3270,3271],{"class":988},"FoodCells",[105,3273,306],{"class":163},[105,3275,3239],{"class":136},[105,3277,397],{"class":140},[105,3279,406],{"class":163},[105,3281,3282,3284,3286,3288,3290],{"class":107,"line":179},[105,3283,3248],{"class":270},[105,3285,306],{"class":163},[105,3287,3253],{"class":136},[105,3289,397],{"class":140},[105,3291,3292],{"class":122},"                       # 删掉上一次画的食物\n",[105,3294,3295],{"class":107,"line":184},[105,3296,130],{"emptyLinePlaceholder":129},[105,3298,3299,3301,3304,3306,3308],{"class":107,"line":195},[105,3300,3224],{"class":111},[105,3302,3303],{"class":270}," grid_pos",[105,3305,979],{"class":111},[105,3307,2718],{"class":270},[105,3309,406],{"class":163},[105,3311,3312,3315,3317,3319,3321,3323,3325,3327,3330,3332,3335],{"class":107,"line":340},[105,3313,3314],{"class":136},"        _draw_cell",[105,3316,141],{"class":140},[105,3318,985],{"class":111},[105,3320,3234],{"class":988},[105,3322,1180],{"class":140},[105,3324,3303],{"class":270},[105,3326,1180],{"class":140},[105,3328,3329],{"class":115}," Color",[105,3331,306],{"class":140},[105,3333,3334],{"class":111},"GREEN",[105,3336,148],{"class":140},[105,3338,3339,3341,3343,3345,3347],{"class":107,"line":345},[105,3340,3224],{"class":111},[105,3342,3303],{"class":270},[105,3344,979],{"class":111},[105,3346,2855],{"class":270},[105,3348,406],{"class":163},[105,3350,3351,3353,3355,3357,3359,3361,3363,3365,3367,3369,3372],{"class":107,"line":364},[105,3352,3314],{"class":136},[105,3354,141],{"class":140},[105,3356,985],{"class":111},[105,3358,3271],{"class":988},[105,3360,1180],{"class":140},[105,3362,3303],{"class":270},[105,3364,1180],{"class":140},[105,3366,3329],{"class":115},[105,3368,306],{"class":140},[105,3370,3371],{"class":111},"RED",[105,3373,148],{"class":140},[105,3375,3376],{"class":107,"line":383},[105,3377,130],{"emptyLinePlaceholder":129},[105,3379,3380,3383,3386,3388,3391,3393,3395,3397,3399],{"class":107,"line":388},[105,3381,3382],{"class":111},"    $",[105,3384,3385],{"class":988},"StarCell",[105,3387,306],{"class":140},[105,3389,3390],{"class":270},"visible",[105,3392,515],{"class":140},[105,3394,2893],{"class":270},[105,3396,818],{"class":154},[105,3398,2679],{"class":270},[105,3400,3401],{"class":122},"         # 有星星才显示\n",[105,3403,3404,3406,3408,3410,3413,3415,3418,3420,3423,3425,3427,3429],{"class":107,"line":409},[105,3405,3382],{"class":111},[105,3407,3385],{"class":988},[105,3409,306],{"class":140},[105,3411,3412],{"class":270},"size",[105,3414,515],{"class":140},[105,3416,3417],{"class":115}," Vector2",[105,3419,141],{"class":140},[105,3421,3422],{"class":270},"CELL_SIZE",[105,3424,274],{"class":163},[105,3426,3422],{"class":270},[105,3428,173],{"class":140},[105,3430,3431],{"class":122},"   # 尺寸也由代码定，别信场景里的数\n",[105,3433,3434,3436,3438,3440,3443,3445,3447,3449,3451,3453,3456],{"class":107,"line":425},[105,3435,3382],{"class":111},[105,3437,3385],{"class":988},[105,3439,306],{"class":140},[105,3441,3442],{"class":270},"position",[105,3444,515],{"class":140},[105,3446,3417],{"class":115},[105,3448,141],{"class":140},[105,3450,2982],{"class":270},[105,3452,173],{"class":140},[105,3454,3455],{"class":154}," *",[105,3457,3458],{"class":270}," CELL_SIZE\n",[105,3460,3461],{"class":107,"line":450},[105,3462,130],{"emptyLinePlaceholder":129},[105,3464,3465],{"class":107,"line":474},[105,3466,3467],{"class":122},"## 在某个容器下画一个格子。蛇和食物都用它\n",[105,3469,3470,3472,3475,3477,3480,3482,3485,3487,3489,3491,3493,3495,3498,3500,3502,3504,3506,3508],{"class":107,"line":479},[105,3471,391],{"class":154},[105,3473,3474],{"class":136}," _draw_cell",[105,3476,141],{"class":140},[105,3478,3479],{"class":163},"parent",[105,3481,167],{"class":140},[105,3483,3484],{"class":115}," Node",[105,3486,1180],{"class":140},[105,3488,3303],{"class":163},[105,3490,167],{"class":140},[105,3492,2684],{"class":115},[105,3494,1180],{"class":140},[105,3496,3497],{"class":163}," color",[105,3499,167],{"class":140},[105,3501,3329],{"class":115},[105,3503,173],{"class":140},[105,3505,400],{"class":154},[105,3507,403],{"class":115},[105,3509,406],{"class":140},[105,3511,3512,3514,3517,3519,3522,3524,3527],{"class":107,"line":485},[105,3513,1133],{"class":154},[105,3515,3516],{"class":270}," rect",[105,3518,301],{"class":140},[105,3520,3521],{"class":115}," ColorRect",[105,3523,306],{"class":163},[105,3525,3526],{"class":136},"new",[105,3528,859],{"class":140},[105,3530,3531,3534,3536,3538,3540,3542,3544,3546,3548,3550],{"class":107,"line":509},[105,3532,3533],{"class":270},"    rect",[105,3535,306],{"class":140},[105,3537,3412],{"class":270},[105,3539,515],{"class":140},[105,3541,3417],{"class":115},[105,3543,141],{"class":140},[105,3545,3422],{"class":270},[105,3547,274],{"class":163},[105,3549,3422],{"class":270},[105,3551,148],{"class":140},[105,3553,3554,3556,3558,3560,3562,3564,3566,3569,3571,3573,3575],{"class":107,"line":521},[105,3555,3533],{"class":270},[105,3557,306],{"class":140},[105,3559,3442],{"class":270},[105,3561,515],{"class":140},[105,3563,3417],{"class":115},[105,3565,141],{"class":140},[105,3567,3568],{"class":270},"grid_pos",[105,3570,173],{"class":140},[105,3572,3455],{"class":154},[105,3574,2649],{"class":270},[105,3576,3577],{"class":122},"   # 格子坐标 → 像素坐标\n",[105,3579,3580,3582,3584,3587,3589],{"class":107,"line":538},[105,3581,3533],{"class":270},[105,3583,306],{"class":140},[105,3585,3586],{"class":270},"color",[105,3588,515],{"class":140},[105,3590,3591],{"class":270}," color\n",[105,3593,3594,3597,3599,3602,3604,3607],{"class":107,"line":543},[105,3595,3596],{"class":270},"    parent",[105,3598,306],{"class":163},[105,3600,3601],{"class":136},"add_child",[105,3603,141],{"class":140},[105,3605,3606],{"class":270},"rect",[105,3608,148],{"class":140},[720,3610,3611,3632,3638,3655],{},[723,3612,3613,3616,3617,3619,3620,1667,3623,3625,3626,3628,3629,3631],{},[14,3614,3615],{},"Vector2(grid_pos) * CELL_SIZE","：格子 ",[14,3618,2503],{}," × 20 = 像素 ",[14,3621,3622],{},"(100, 160)",[14,3624,3442],{}," 要 ",[14,3627,2945],{},"（小数型），所以先把 ",[14,3630,2729],{}," 转过去再乘",[723,3633,3634,3637],{},[14,3635,3636],{},"queue_free()","：把节点标记为\"这一帧结束后删掉\"，比立刻删安全",[723,3639,3640,3643,3644,3647,3648,3651,3652,3654],{},[14,3641,3642],{},"$StarCell.visible = star != NO_CELL","：右边算出来是 ",[14,3645,3646],{},"true"," \u002F ",[14,3649,3650],{},"false","，直接赋给 ",[14,3653,3390],{},"，一行顶四行 if",[723,3656,3657,3658,3661,3662],{},"为什么抽一个 ",[14,3659,3660],{},"_draw_cell()","：蛇和食物画法一模一样，只是颜色和放哪不同。",[22,3663,3664],{},"同样的 5 行要写第二遍的时候，就该抽成函数",[10,3666,3667,3668,3671],{},"为什么每次都清空重画，不是挪现有方块？白块阶段追求\"最不容易错\"，不追求快。清空重画的逻辑只有一句话：",[22,3669,3670],{},"画面 = 数据","。你改数组，画面跟着变；绝对不要反过来（读方块的位置去推蛇在哪）。这就是\"数据驱动\"\"单一数据源\"在代码里的样子。",[1984,3673,3674],{},[10,3675,3676,3677,3679,3680,3682,3683,3686],{},"这里踩过一个坑：星星画出来是 4 格。因为 ",[14,3678,3385],{}," 是场景里摆好的节点，尺寸是当时在检查器里填的 40（Godot 新建 ColorRect 的默认值），40 ÷ 20 = 2，2×2 就是 4 格。同一个数存了两份——代码里一份、场景里一份——迟早对不上。所以上面 ",[14,3681,3194],{}," 里那句 ",[14,3684,3685],{},"$StarCell.size = ..."," 把尺寸也收归代码管。",[10,3688,3689],{},[2129,3690],{"alt":3691,"src":3692},"04-draw-白块","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F04-draw-%E7%99%BD%E5%9D%97.png",[91,3694,3695],{"id":3695},"方向控制",[96,3697,3699],{"className":98,"code":3698,"language":100,"meta":101,"style":101},"func _input(event: InputEvent) -> void:\n    if event.is_action_pressed(\"move_up\") and direction != Vector2i.DOWN:\n        next_direction = Vector2i.UP\n    elif event.is_action_pressed(\"move_down\") and direction != Vector2i.UP:\n        next_direction = Vector2i.DOWN\n    elif event.is_action_pressed(\"move_left\") and direction != Vector2i.RIGHT:\n        next_direction = Vector2i.LEFT\n    elif event.is_action_pressed(\"move_right\") and direction != Vector2i.LEFT:\n        next_direction = Vector2i.RIGHT\n",[14,3700,3701,3726,3761,3775,3808,3821,3852,3865,3897],{"__ignoreMap":101},[105,3702,3703,3705,3708,3710,3713,3715,3718,3720,3722,3724],{"class":107,"line":108},[105,3704,391],{"class":154},[105,3706,3707],{"class":136}," _input",[105,3709,141],{"class":140},[105,3711,3712],{"class":163},"event",[105,3714,167],{"class":140},[105,3716,3717],{"class":115}," InputEvent",[105,3719,173],{"class":140},[105,3721,400],{"class":154},[105,3723,403],{"class":115},[105,3725,406],{"class":140},[105,3727,3728,3730,3733,3735,3738,3740,3743,3745,3748,3750,3752,3754,3756,3759],{"class":107,"line":119},[105,3729,682],{"class":111},[105,3731,3732],{"class":270}," event",[105,3734,306],{"class":163},[105,3736,3737],{"class":136},"is_action_pressed",[105,3739,141],{"class":140},[105,3741,3742],{"class":144},"\"move_up\"",[105,3744,173],{"class":140},[105,3746,3747],{"class":154}," and",[105,3749,2809],{"class":270},[105,3751,818],{"class":154},[105,3753,2684],{"class":115},[105,3755,306],{"class":140},[105,3757,3758],{"class":111},"DOWN",[105,3760,406],{"class":163},[105,3762,3763,3766,3768,3770,3772],{"class":107,"line":126},[105,3764,3765],{"class":270},"        next_direction",[105,3767,515],{"class":140},[105,3769,2684],{"class":115},[105,3771,306],{"class":140},[105,3773,3774],{"class":111},"UP\n",[105,3776,3777,3780,3782,3784,3786,3788,3791,3793,3795,3797,3799,3801,3803,3806],{"class":107,"line":133},[105,3778,3779],{"class":111},"    elif",[105,3781,3732],{"class":270},[105,3783,306],{"class":163},[105,3785,3737],{"class":136},[105,3787,141],{"class":140},[105,3789,3790],{"class":144},"\"move_down\"",[105,3792,173],{"class":140},[105,3794,3747],{"class":154},[105,3796,2809],{"class":270},[105,3798,818],{"class":154},[105,3800,2684],{"class":115},[105,3802,306],{"class":140},[105,3804,3805],{"class":111},"UP",[105,3807,406],{"class":163},[105,3809,3810,3812,3814,3816,3818],{"class":107,"line":151},[105,3811,3765],{"class":270},[105,3813,515],{"class":140},[105,3815,2684],{"class":115},[105,3817,306],{"class":140},[105,3819,3820],{"class":111},"DOWN\n",[105,3822,3823,3825,3827,3829,3831,3833,3836,3838,3840,3842,3844,3846,3848,3850],{"class":107,"line":179},[105,3824,3779],{"class":111},[105,3826,3732],{"class":270},[105,3828,306],{"class":163},[105,3830,3737],{"class":136},[105,3832,141],{"class":140},[105,3834,3835],{"class":144},"\"move_left\"",[105,3837,173],{"class":140},[105,3839,3747],{"class":154},[105,3841,2809],{"class":270},[105,3843,818],{"class":154},[105,3845,2684],{"class":115},[105,3847,306],{"class":140},[105,3849,2818],{"class":111},[105,3851,406],{"class":163},[105,3853,3854,3856,3858,3860,3862],{"class":107,"line":184},[105,3855,3765],{"class":270},[105,3857,515],{"class":140},[105,3859,2684],{"class":115},[105,3861,306],{"class":140},[105,3863,3864],{"class":111},"LEFT\n",[105,3866,3867,3869,3871,3873,3875,3877,3880,3882,3884,3886,3888,3890,3892,3895],{"class":107,"line":195},[105,3868,3779],{"class":111},[105,3870,3732],{"class":270},[105,3872,306],{"class":163},[105,3874,3737],{"class":136},[105,3876,141],{"class":140},[105,3878,3879],{"class":144},"\"move_right\"",[105,3881,173],{"class":140},[105,3883,3747],{"class":154},[105,3885,2809],{"class":270},[105,3887,818],{"class":154},[105,3889,2684],{"class":115},[105,3891,306],{"class":140},[105,3893,3894],{"class":111},"LEFT",[105,3896,406],{"class":163},[105,3898,3899,3901,3903,3905,3907],{"class":107,"line":340},[105,3900,3765],{"class":270},[105,3902,515],{"class":140},[105,3904,2684],{"class":115},[105,3906,306],{"class":140},[105,3908,3909],{"class":111},"RIGHT\n",[10,3911,3912],{},"这一节全是为了两个 bug：",[10,3914,3915,3918,3919,1667],{},[22,3916,3917],{},"① 不能 180° 掉头。"," 向右跑时按左，新头会正好落在第二节上，瞬间\"撞到自己\"暴毙。所以每个方向都判断 ",[14,3920,3921],{},"direction != 相反方向",[10,3923,3924,3934,3935,3937,3938,3940,3941,3943,3944,3947],{},[22,3925,3926,3927,3930,3931,1667],{},"② 输入只改 ",[14,3928,3929],{},"next_direction","，不直接改 ",[14,3932,3933],{},"direction"," 想象你向右跑，在同一个 0.15 秒里连按 ↑ 再按 ←：如果直接改 ",[14,3936,3933],{},"，第一下把它改成 UP，第二下检查\"UP 的反方向是 DOWN，← 不是 DOWN\"，于是放行 → ",[14,3939,3933],{}," 变成 LEFT → 但蛇还没真的动过，等于原地从向右掉头成向左，撞死自己。用 ",[14,3942,3929],{}," 暂存，真正移动那一刻才 ",[14,3945,3946],{},"direction = next_direction","，判断永远基于\"上一步真的走了的方向\"。",[10,3949,3950],{},"插上手柄，十字键和摇杆同样能转——代码一个字没为手柄多写，因为用的是第 2 章起的动作名。",[1984,3952,3953],{},[10,3954,3955,3956,3959,3960,3963],{},"顺便说一个和代码无关、但害我查了半小时的事：Godot 4.4 起游戏默认",[22,3957,3958],{},"嵌在编辑器里","跑，「游戏」标签的工具栏有 ",[22,3961,3962],{},"输入 \u002F 2D \u002F 3D"," 三个模式，只有「输入」模式按键才进游戏；2D \u002F 3D 是运行时点选节点用的，会把输入截走。代码没错的时候，先看这个开关。",[91,3965,3967],{"id":3966},"三种碰撞-吃","三种碰撞 + 吃",[10,3969,766,3970,3972,3973,3976],{},[14,3971,3190],{}," 改成完整版。注意判定要在\"插头\"",[22,3974,3975],{},"之前","——先算出下一格是哪，再看那一格能不能去，能去才真的走：",[96,3978,3980],{"className":98,"code":3979,"language":100,"meta":101,"style":101},"func _on_move_timer_timeout() -> void:\n    direction = next_direction\n    var new_head := snake_body[0] + direction\n\n    # ① 撞墙：任何一个分量跑出范围\n    if new_head.x \u003C 0 or new_head.x >= GRID_W \\\n    or new_head.y \u003C 0 or new_head.y >= GRID_H:\n        die()\n        return\n\n    # ② 撞自己：新头的位置已经在身体里\n    if new_head in snake_body:\n        die()\n        return\n\n    snake_body.insert(0, new_head)\n\n    # ③ 吃：三种情况的共同点 —— 吃到了就不删尾\n    if new_head in foods:\n        foods.erase(new_head)                   # 从食物列表里拿走这一个\n        print(\"+1\")\n    elif new_head == star:\n        star = NO_CELL\n        $StarLifeTimer.stop()                   # 它的寿命倒计时也不用跑了\n        print(\"+2\")\n    else:\n        snake_body.pop_back()                   # 什么都没吃到 → 删尾\n\n    redraw()\n\nfunc die() -> void:\n    $MoveTimer.stop()\n    print(\"died, length = \", snake_body.size())\n",[14,3981,3982,3996,4005,4025,4029,4034,4066,4096,4103,4107,4111,4116,4128,4134,4138,4142,4160,4164,4169,4181,4201,4213,4225,4235,4251,4262,4268,4282,4286,4293,4297,4312,4324],{"__ignoreMap":101},[105,3983,3984,3986,3988,3990,3992,3994],{"class":107,"line":108},[105,3985,391],{"class":154},[105,3987,795],{"class":136},[105,3989,397],{"class":140},[105,3991,400],{"class":154},[105,3993,403],{"class":115},[105,3995,406],{"class":140},[105,3997,3998,4000,4002],{"class":107,"line":119},[105,3999,3046],{"class":270},[105,4001,515],{"class":140},[105,4003,4004],{"class":270}," next_direction\n",[105,4006,4007,4009,4011,4013,4015,4017,4019,4021,4023],{"class":107,"line":126},[105,4008,1133],{"class":154},[105,4010,3060],{"class":270},[105,4012,301],{"class":140},[105,4014,2718],{"class":270},[105,4016,2726],{"class":140},[105,4018,3069],{"class":325},[105,4020,1013],{"class":140},[105,4022,3074],{"class":154},[105,4024,3077],{"class":270},[105,4026,4027],{"class":107,"line":133},[105,4028,130],{"emptyLinePlaceholder":129},[105,4030,4031],{"class":107,"line":151},[105,4032,4033],{"class":122},"    # ① 撞墙：任何一个分量跑出范围\n",[105,4035,4036,4038,4040,4042,4045,4048,4050,4053,4055,4057,4059,4061,4063],{"class":107,"line":179},[105,4037,682],{"class":111},[105,4039,3060],{"class":270},[105,4041,306],{"class":140},[105,4043,4044],{"class":270},"x",[105,4046,4047],{"class":154}," \u003C",[105,4049,1415],{"class":325},[105,4051,4052],{"class":154}," or",[105,4054,3060],{"class":270},[105,4056,306],{"class":140},[105,4058,4044],{"class":270},[105,4060,1623],{"class":154},[105,4062,2619],{"class":270},[105,4064,4065],{"class":140}," \\\n",[105,4067,4068,4071,4073,4075,4078,4080,4082,4084,4086,4088,4090,4092,4094],{"class":107,"line":184},[105,4069,4070],{"class":154},"    or",[105,4072,3060],{"class":270},[105,4074,306],{"class":140},[105,4076,4077],{"class":270},"y",[105,4079,4047],{"class":154},[105,4081,1415],{"class":325},[105,4083,4052],{"class":154},[105,4085,3060],{"class":270},[105,4087,306],{"class":140},[105,4089,4077],{"class":270},[105,4091,1623],{"class":154},[105,4093,2634],{"class":270},[105,4095,406],{"class":163},[105,4097,4098,4101],{"class":107,"line":195},[105,4099,4100],{"class":136},"        die",[105,4102,859],{"class":140},[105,4104,4105],{"class":107,"line":340},[105,4106,864],{"class":111},[105,4108,4109],{"class":107,"line":345},[105,4110,130],{"emptyLinePlaceholder":129},[105,4112,4113],{"class":107,"line":364},[105,4114,4115],{"class":122},"    # ② 撞自己：新头的位置已经在身体里\n",[105,4117,4118,4120,4122,4124,4126],{"class":107,"line":383},[105,4119,682],{"class":111},[105,4121,3060],{"class":270},[105,4123,979],{"class":154},[105,4125,2718],{"class":270},[105,4127,406],{"class":163},[105,4129,4130,4132],{"class":107,"line":388},[105,4131,4100],{"class":136},[105,4133,859],{"class":140},[105,4135,4136],{"class":107,"line":409},[105,4137,864],{"class":111},[105,4139,4140],{"class":107,"line":425},[105,4141,130],{"emptyLinePlaceholder":129},[105,4143,4144,4146,4148,4150,4152,4154,4156,4158],{"class":107,"line":450},[105,4145,3082],{"class":270},[105,4147,306],{"class":163},[105,4149,3087],{"class":136},[105,4151,141],{"class":140},[105,4153,3069],{"class":325},[105,4155,1180],{"class":140},[105,4157,3060],{"class":270},[105,4159,148],{"class":140},[105,4161,4162],{"class":107,"line":474},[105,4163,130],{"emptyLinePlaceholder":129},[105,4165,4166],{"class":107,"line":479},[105,4167,4168],{"class":122},"    # ③ 吃：三种情况的共同点 —— 吃到了就不删尾\n",[105,4170,4171,4173,4175,4177,4179],{"class":107,"line":485},[105,4172,682],{"class":111},[105,4174,3060],{"class":270},[105,4176,979],{"class":154},[105,4178,2855],{"class":270},[105,4180,406],{"class":163},[105,4182,4183,4186,4188,4191,4193,4196,4198],{"class":107,"line":509},[105,4184,4185],{"class":270},"        foods",[105,4187,306],{"class":163},[105,4189,4190],{"class":136},"erase",[105,4192,141],{"class":140},[105,4194,4195],{"class":270},"new_head",[105,4197,173],{"class":140},[105,4199,4200],{"class":122},"                   # 从食物列表里拿走这一个\n",[105,4202,4203,4206,4208,4211],{"class":107,"line":521},[105,4204,4205],{"class":136},"        print",[105,4207,141],{"class":140},[105,4209,4210],{"class":144},"\"+1\"",[105,4212,148],{"class":140},[105,4214,4215,4217,4219,4221,4223],{"class":107,"line":538},[105,4216,3779],{"class":111},[105,4218,3060],{"class":270},[105,4220,956],{"class":154},[105,4222,2893],{"class":270},[105,4224,406],{"class":163},[105,4226,4227,4230,4232],{"class":107,"line":543},[105,4228,4229],{"class":270},"        star",[105,4231,515],{"class":140},[105,4233,4234],{"class":270}," NO_CELL\n",[105,4236,4237,4240,4242,4244,4246,4248],{"class":107,"line":567},[105,4238,4239],{"class":111},"        $",[105,4241,1010],{"class":988},[105,4243,306],{"class":163},[105,4245,1025],{"class":136},[105,4247,397],{"class":140},[105,4249,4250],{"class":122},"                   # 它的寿命倒计时也不用跑了\n",[105,4252,4253,4255,4257,4260],{"class":107,"line":579},[105,4254,4205],{"class":136},[105,4256,141],{"class":140},[105,4258,4259],{"class":144},"\"+2\"",[105,4261,148],{"class":140},[105,4263,4264,4266],{"class":107,"line":596},[105,4265,1644],{"class":111},[105,4267,406],{"class":163},[105,4269,4270,4273,4275,4277,4279],{"class":107,"line":601},[105,4271,4272],{"class":270},"        snake_body",[105,4274,306],{"class":163},[105,4276,3109],{"class":136},[105,4278,397],{"class":140},[105,4280,4281],{"class":122},"                   # 什么都没吃到 → 删尾\n",[105,4283,4284],{"class":107,"line":617},[105,4285,130],{"emptyLinePlaceholder":129},[105,4287,4288,4291],{"class":107,"line":626},[105,4289,4290],{"class":136},"    redraw",[105,4292,859],{"class":140},[105,4294,4295],{"class":107,"line":641},[105,4296,130],{"emptyLinePlaceholder":129},[105,4298,4299,4301,4304,4306,4308,4310],{"class":107,"line":646},[105,4300,391],{"class":154},[105,4302,4303],{"class":136}," die",[105,4305,397],{"class":140},[105,4307,400],{"class":154},[105,4309,403],{"class":115},[105,4311,406],{"class":140},[105,4313,4314,4316,4318,4320,4322],{"class":107,"line":662},[105,4315,3382],{"class":111},[105,4317,989],{"class":988},[105,4319,306],{"class":163},[105,4321,1025],{"class":136},[105,4323,859],{"class":140},[105,4325,4326,4328,4330,4333,4335,4337,4339,4341],{"class":107,"line":679},[105,4327,2923],{"class":136},[105,4329,141],{"class":140},[105,4331,4332],{"class":144},"\"died, length = \"",[105,4334,1180],{"class":140},[105,4336,2718],{"class":270},[105,4338,306],{"class":163},[105,4340,3412],{"class":136},[105,4342,1231],{"class":140},[720,4344,4345,4354,4366],{},[723,4346,4347,4350,4351,4353],{},[14,4348,4349],{},"new_head in snake_body","：数组支持 ",[14,4352,1896],{},"，\"里面有没有这个元素\"。「撞到自己」这么复杂的问题一行解决——因为蛇身本来就是坐标数组，这就是选对数据结构的价值",[723,4355,4356,4357,4359,4360,4362,4363],{},"为什么吃食物用 ",[14,4358,1896],{},"，吃星星用 ",[14,4361,1927],{},"：和第 1 章那张实体表一一对应——食物有好几个，要问\"在不在列表里\"；星星只有一颗，直接问\"是不是它\"。",[22,4364,4365],{},"数据结构决定判定写法",[723,4367,4368,4371],{},[14,4369,4370],{},"return","：死了就立刻退出，后面的插头、重画都不执行。不写它，死了还会再走一步",[91,4373,4374],{"id":4374},"随机挑一个空格子",[96,4376,4378],{"className":98,"code":4377,"language":100,"meta":101,"style":101},"## 在所有空格子里随机挑一个；棋盘满了返回 NO_CELL\nfunc random_empty_cell() -> Vector2i:\n    var empty_cells: Array[Vector2i] = []\n    for x in GRID_W:\n        for y in GRID_H:\n            var cell := Vector2i(x, y)\n            if cell in snake_body or cell in foods or cell == star:\n                continue                        # 被占了，跳过这格\n            empty_cells.append(cell)\n    if empty_cells.is_empty():\n        return NO_CELL\n    return empty_cells.pick_random()\n",[14,4379,4380,4385,4400,4422,4435,4448,4469,4498,4506,4523,4538,4544],{"__ignoreMap":101},[105,4381,4382],{"class":107,"line":108},[105,4383,4384],{"class":122},"## 在所有空格子里随机挑一个；棋盘满了返回 NO_CELL\n",[105,4386,4387,4389,4392,4394,4396,4398],{"class":107,"line":119},[105,4388,391],{"class":154},[105,4390,4391],{"class":136}," random_empty_cell",[105,4393,397],{"class":140},[105,4395,400],{"class":154},[105,4397,2684],{"class":115},[105,4399,406],{"class":140},[105,4401,4402,4404,4407,4409,4411,4413,4415,4417,4419],{"class":107,"line":126},[105,4403,1133],{"class":154},[105,4405,4406],{"class":270}," empty_cells",[105,4408,167],{"class":140},[105,4410,2723],{"class":115},[105,4412,2726],{"class":140},[105,4414,2729],{"class":115},[105,4416,1013],{"class":140},[105,4418,515],{"class":140},[105,4420,4421],{"class":140}," []\n",[105,4423,4424,4426,4429,4431,4433],{"class":107,"line":133},[105,4425,3224],{"class":111},[105,4427,4428],{"class":270}," x",[105,4430,979],{"class":111},[105,4432,2619],{"class":270},[105,4434,406],{"class":163},[105,4436,4437,4439,4442,4444,4446],{"class":107,"line":151},[105,4438,973],{"class":111},[105,4440,4441],{"class":270}," y",[105,4443,979],{"class":111},[105,4445,2634],{"class":270},[105,4447,406],{"class":163},[105,4449,4450,4453,4455,4457,4459,4461,4463,4465,4467],{"class":107,"line":179},[105,4451,4452],{"class":154},"            var",[105,4454,3227],{"class":270},[105,4456,301],{"class":140},[105,4458,2684],{"class":115},[105,4460,141],{"class":140},[105,4462,4044],{"class":270},[105,4464,274],{"class":163},[105,4466,4077],{"class":270},[105,4468,148],{"class":140},[105,4470,4471,4474,4476,4478,4480,4482,4484,4486,4488,4490,4492,4494,4496],{"class":107,"line":184},[105,4472,4473],{"class":111},"            if",[105,4475,3227],{"class":270},[105,4477,979],{"class":154},[105,4479,2718],{"class":270},[105,4481,4052],{"class":154},[105,4483,3227],{"class":270},[105,4485,979],{"class":154},[105,4487,2855],{"class":270},[105,4489,4052],{"class":154},[105,4491,3227],{"class":270},[105,4493,956],{"class":154},[105,4495,2893],{"class":270},[105,4497,406],{"class":163},[105,4499,4500,4503],{"class":107,"line":195},[105,4501,4502],{"class":111},"                continue",[105,4504,4505],{"class":122},"                        # 被占了，跳过这格\n",[105,4507,4508,4511,4513,4516,4518,4521],{"class":107,"line":340},[105,4509,4510],{"class":270},"            empty_cells",[105,4512,306],{"class":163},[105,4514,4515],{"class":136},"append",[105,4517,141],{"class":140},[105,4519,4520],{"class":270},"cell",[105,4522,148],{"class":140},[105,4524,4525,4527,4529,4531,4534,4536],{"class":107,"line":345},[105,4526,682],{"class":111},[105,4528,4406],{"class":270},[105,4530,306],{"class":163},[105,4532,4533],{"class":136},"is_empty",[105,4535,397],{"class":140},[105,4537,406],{"class":163},[105,4539,4540,4542],{"class":107,"line":364},[105,4541,835],{"class":111},[105,4543,4234],{"class":270},[105,4545,4546,4549,4551,4553,4556],{"class":107,"line":383},[105,4547,4548],{"class":111},"    return",[105,4550,4406],{"class":270},[105,4552,306],{"class":163},[105,4554,4555],{"class":136},"pick_random",[105,4557,859],{"class":140},[10,4559,4560],{},"为什么\"先列出所有空格再随机挑\"，不是\"随机挑一格，被占了就重挑\"？蛇很长时随机撞到蛇身的概率极高，可能循环几百次，理论上会死循环。先列空格再挑，一次搞定，而且顺便处理了\"填满通关\"这个边界情况。",[720,4562,4563,4573,4583],{},[723,4564,4565,4568,4569,4572],{},[14,4566,4567],{},"is_empty()","：数组是不是空的，等于 ",[14,4570,4571],{},"size() == 0","，但读起来是一句人话。没有空格子 = 棋盘被填满了，从空数组里拿东西会报错，所以先问一句",[723,4574,4575,4578,4579,4582],{},[14,4576,4577],{},"pick_random()","：等概率返回其中一个元素，不删除它。能自己写 ",[14,4580,4581],{},"empty_cells[randi() % empty_cells.size()]","，但一个词说清楚的事就别写一行算式——引擎自带的方法先查有没有，再自己写",[723,4584,4585,4588,4589,4591],{},[14,4586,4587],{},"continue","：跳过这一轮循环剩下的部分，直接看下一个格子。和 ",[14,4590,4370],{},"（退出整个函数）不一样",[10,4593,4594],{},"到这里，撞墙会停、吃到会变长、绕回来撞自己会停。下一章让食物自己长出来、让星星自己消失。",[1684,4596,4597],{},"html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}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 .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 .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":101,"searchDepth":119,"depth":119,"links":4599},[4600,4601,4602,4603],{"id":3179,"depth":119,"text":3179},{"id":3695,"depth":119,"text":3695},{"id":3966,"depth":119,"text":3967},{"id":4374,"depth":119,"text":4374},"2026-09-04",{},"\u002Fdevlog\u002Fxggame-snake\u002F04-draw-and-collide",{"title":3174,"description":1697},"devlog\u002Fxggame-snake\u002F04-draw-and-collide","ceXE2WSOzbKu4LgRJrOtAugEk9uijTOZLqQ6YUIaBs8",{"id":4611,"title":4612,"body":4613,"cover":1695,"date":4973,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":4974,"navigation":129,"path":4975,"seo":4976,"stem":4977,"toc":129,"__hash__":4978},"devlog\u002Fdevlog\u002Fxggame-snake\u002F05-timers.md","定时生成与过期",{"type":7,"value":4614,"toc":4968},[4615,4618,4622,4636,4856,4877,4883,4886,4917,4933,4941,4945,4956,4962,4965],[10,4616,4617],{},"这是这个游戏最特别的一节。经典贪吃蛇里食物永远只有一个；这里场上可能有 0～3 个食物、一颗随时会消失的星星，玩家要做选择。数据结构只多了一个数组，体验完全不一样。",[91,4619,4621],{"id":4620},"三个-timer三个函数","三个 Timer，三个函数",[10,4623,4624,4625,2411,4627,2411,4629,4631,4632,3020,4634,2303],{},"依次选中 ",[14,4626,996],{},[14,4628,1003],{},[14,4630,1010],{}," → 节点面板 → ",[14,4633,3019],{},[14,4635,2566],{},[96,4637,4639],{"className":98,"code":4638,"language":100,"meta":101,"style":101},"## 每 3 秒：场上不够 3 个食物就长一个\nfunc _on_food_timer_timeout() -> void:\n    if foods.size() >= MAX_FOOD:\n        return                                  # 够多了，这次不长\n    var cell := random_empty_cell()\n    if cell != NO_CELL:\n        foods.append(cell)\n    redraw()\n\n## 每 12 秒：没有星星就放一颗，并开始倒数它的寿命\nfunc _on_star_spawn_timer_timeout() -> void:\n    if star != NO_CELL:\n        return                                  # 已经有一颗了\n    star = random_empty_cell()\n    if star != NO_CELL:\n        $StarLifeTimer.start()                  # 从现在起倒数 5 秒\n    redraw()\n\n## 5 秒到：没人吃 → 星星消失\nfunc _on_star_life_timer_timeout() -> void:\n    star = NO_CELL\n    redraw()\n",[14,4640,4641,4646,4661,4679,4686,4698,4710,4724,4730,4734,4739,4754,4766,4773,4784,4796,4812,4818,4822,4827,4842,4850],{"__ignoreMap":101},[105,4642,4643],{"class":107,"line":108},[105,4644,4645],{"class":122},"## 每 3 秒：场上不够 3 个食物就长一个\n",[105,4647,4648,4650,4653,4655,4657,4659],{"class":107,"line":119},[105,4649,391],{"class":154},[105,4651,4652],{"class":136}," _on_food_timer_timeout",[105,4654,397],{"class":140},[105,4656,400],{"class":154},[105,4658,403],{"class":115},[105,4660,406],{"class":140},[105,4662,4663,4665,4667,4669,4671,4673,4675,4677],{"class":107,"line":126},[105,4664,682],{"class":111},[105,4666,2855],{"class":270},[105,4668,306],{"class":163},[105,4670,3412],{"class":136},[105,4672,397],{"class":140},[105,4674,1623],{"class":154},[105,4676,2664],{"class":270},[105,4678,406],{"class":163},[105,4680,4681,4683],{"class":107,"line":133},[105,4682,835],{"class":111},[105,4684,4685],{"class":122},"                                  # 够多了，这次不长\n",[105,4687,4688,4690,4692,4694,4696],{"class":107,"line":151},[105,4689,1133],{"class":154},[105,4691,3227],{"class":270},[105,4693,301],{"class":140},[105,4695,4391],{"class":136},[105,4697,859],{"class":140},[105,4699,4700,4702,4704,4706,4708],{"class":107,"line":179},[105,4701,682],{"class":111},[105,4703,3227],{"class":270},[105,4705,818],{"class":154},[105,4707,2679],{"class":270},[105,4709,406],{"class":163},[105,4711,4712,4714,4716,4718,4720,4722],{"class":107,"line":184},[105,4713,4185],{"class":270},[105,4715,306],{"class":163},[105,4717,4515],{"class":136},[105,4719,141],{"class":140},[105,4721,4520],{"class":270},[105,4723,148],{"class":140},[105,4725,4726,4728],{"class":107,"line":195},[105,4727,4290],{"class":136},[105,4729,859],{"class":140},[105,4731,4732],{"class":107,"line":340},[105,4733,130],{"emptyLinePlaceholder":129},[105,4735,4736],{"class":107,"line":345},[105,4737,4738],{"class":122},"## 每 12 秒：没有星星就放一颗，并开始倒数它的寿命\n",[105,4740,4741,4743,4746,4748,4750,4752],{"class":107,"line":364},[105,4742,391],{"class":154},[105,4744,4745],{"class":136}," _on_star_spawn_timer_timeout",[105,4747,397],{"class":140},[105,4749,400],{"class":154},[105,4751,403],{"class":115},[105,4753,406],{"class":140},[105,4755,4756,4758,4760,4762,4764],{"class":107,"line":383},[105,4757,682],{"class":111},[105,4759,2893],{"class":270},[105,4761,818],{"class":154},[105,4763,2679],{"class":270},[105,4765,406],{"class":163},[105,4767,4768,4770],{"class":107,"line":388},[105,4769,835],{"class":111},[105,4771,4772],{"class":122},"                                  # 已经有一颗了\n",[105,4774,4775,4778,4780,4782],{"class":107,"line":409},[105,4776,4777],{"class":270},"    star",[105,4779,515],{"class":140},[105,4781,4391],{"class":136},[105,4783,859],{"class":140},[105,4785,4786,4788,4790,4792,4794],{"class":107,"line":425},[105,4787,682],{"class":111},[105,4789,2893],{"class":270},[105,4791,818],{"class":154},[105,4793,2679],{"class":270},[105,4795,406],{"class":163},[105,4797,4798,4800,4802,4804,4807,4809],{"class":107,"line":450},[105,4799,4239],{"class":111},[105,4801,1010],{"class":988},[105,4803,306],{"class":163},[105,4805,4806],{"class":136},"start",[105,4808,397],{"class":140},[105,4810,4811],{"class":122},"                  # 从现在起倒数 5 秒\n",[105,4813,4814,4816],{"class":107,"line":474},[105,4815,4290],{"class":136},[105,4817,859],{"class":140},[105,4819,4820],{"class":107,"line":479},[105,4821,130],{"emptyLinePlaceholder":129},[105,4823,4824],{"class":107,"line":485},[105,4825,4826],{"class":122},"## 5 秒到：没人吃 → 星星消失\n",[105,4828,4829,4831,4834,4836,4838,4840],{"class":107,"line":509},[105,4830,391],{"class":154},[105,4832,4833],{"class":136}," _on_star_life_timer_timeout",[105,4835,397],{"class":140},[105,4837,400],{"class":154},[105,4839,403],{"class":115},[105,4841,406],{"class":140},[105,4843,4844,4846,4848],{"class":107,"line":521},[105,4845,4777],{"class":270},[105,4847,515],{"class":140},[105,4849,4234],{"class":270},[105,4851,4852,4854],{"class":107,"line":538},[105,4853,4290],{"class":136},[105,4855,859],{"class":140},[10,4857,4858,4859,4861,4862,4865,4866,4869,4870,4872,4873,4876],{},"然后回到第 3 章：把 ",[14,4860,2978],{}," 那行手放的 ",[14,4863,4864],{},"Vector2i(60, 27)"," 删掉改成 ",[14,4867,4868],{},"[]","，在 ",[14,4871,775],{}," 里加一句 ",[14,4874,4875],{},"_on_food_timer_timeout()","——开局立刻长一个，不用干等 3 秒。",[10,4878,4879,4880,4882],{},"为什么开局要手动调一次 ",[14,4881,4875],{},"，不是继续手放一个坐标？手放的坐标是写死的。用同一套生成逻辑，就不会有\"两套规则\"。一个函数既能被 Timer 叫，也能被你手动叫——这就是函数的意义。",[91,4884,4885],{"id":4885},"三个容易漏的判断",[10,4887,4888,4894,4895,4898,4899,4902,4903,4906,4907,4909,4910,4913,4914],{},[22,4889,4890,4891],{},"为什么 spawn 之后都要判断 ",[14,4892,4893],{},"!= NO_CELL","：棋盘满了 ",[14,4896,4897],{},"random_empty_cell()"," 会返回 ",[14,4900,4901],{},"NO_CELL","。不判断，",[14,4904,4905],{},"(-1, -1)"," 就会被塞进 ",[14,4908,2978],{},"，画到屏幕外，还会让 ",[14,4911,4912],{},"in foods"," 出现幽灵食物。",[22,4915,4916],{},"凡是可能返回\"没有\"的函数，调用方必须检查。",[10,4918,4919,4925,4926,4929,4930],{},[22,4920,4921,4922],{},"为什么吃到星星时要 ",[14,4923,4924],{},"$StarLifeTimer.stop()","（第 4 章那行）：不停的话，你 2 秒就吃了，3 秒后它照样响，",[14,4927,4928],{},"star = NO_CELL","——但这时候可能已经又生成了新的一颗，它被无辜清掉。",[22,4931,4932],{},"Timer 是有状态的东西，用完要收。",[10,4934,4935,4940],{},[22,4936,209,4937,4939],{},[14,4938,1010],{}," 是 One Shot、不 Autostart","：它是星星的寿命。星星出现那一刻才开始倒数，倒数一次就够了。其他三个是\"每隔一段时间做一次\"，要一直重复。",[91,4942,4944],{"id":4943},"一个上线后才发现的顺序-bug","一个上线后才发现的顺序 bug",[10,4946,4947,4949,4950,4952,4953],{},[14,4948,775],{}," 里我一开始是先 ",[14,4951,4875],{}," 再建蛇。食物的\"空格子\"判断要看到蛇，蛇还没出生时它以为整个棋盘都是空的，于是食物可能长在蛇出生的格子上。改成先建蛇再生食物。",[22,4954,4955],{},"初始化顺序：先有主角，再生世界。",[10,4957,4958],{},[2129,4959],{"alt":4960,"src":4961},"05-timers-星星","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F05-timers-%E6%98%9F%E6%98%9F.png",[10,4963,4964],{},"到这里核心循环完整了：蛇能跑、能转、能死，食物按时间长，星星会出现会消失。剩下全是外壳。",[1684,4966,4967],{},"html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}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 .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 .s8w-G, html code.shiki .s8w-G{--shiki-default:#393A34;--shiki-dark:#DBD7CAEE}html pre.shiki code .sfsYZ, html code.shiki .sfsYZ{--shiki-default:#A65E2B;--shiki-dark:#C99076}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":101,"searchDepth":119,"depth":119,"links":4969},[4970,4971,4972],{"id":4620,"depth":119,"text":4621},{"id":4885,"depth":119,"text":4885},{"id":4943,"depth":119,"text":4944},"2026-09-05",{},"\u002Fdevlog\u002Fxggame-snake\u002F05-timers",{"title":4612,"description":1697},"devlog\u002Fxggame-snake\u002F05-timers","xUycJSF2Vdp0jH_G06FPQIcT9sJaF3PVraGZxFP9cBM",{"id":4,"title":5,"body":4980,"cover":1695,"date":1696,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":6325,"navigation":129,"path":1702,"seo":6326,"stem":1704,"toc":129,"__hash__":1705},{"type":7,"value":4981,"toc":6318},[4982,4986,4990,5036,5038,5106,5108,5118,5124,5126,5516,5544,5548,5556,5770,5780,5792,5796,5798,6094,6104,6106,6302,6314,6316],[10,4983,12,4984,17],{},[14,4985,16],{},[10,4987,20,4988],{},[22,4989,24],{},[26,4991,4992,5002],{},[29,4993,4994],{},[32,4995,4996,4998,5000],{},[35,4997,37],{},[35,4999,40],{},[35,5001,43],{},[45,5003,5004,5016,5026],{},[32,5005,5006,5010,5014],{},[50,5007,5008],{},[14,5009,54],{},[50,5011,57,5012],{},[22,5013,60],{},[50,5015,63],{},[32,5017,5018,5022,5024],{},[50,5019,5020],{},[14,5021,70],{},[50,5023,73],{},[50,5025,76],{},[32,5027,5028,5032,5034],{},[50,5029,5030],{},[14,5031,83],{},[50,5033,86],{},[50,5035,89],{},[91,5037,94],{"id":93},[96,5039,5040],{"className":98,"code":99,"language":100,"meta":101,"style":101},[14,5041,5042,5048,5052,5056,5066,5084,5088,5098],{"__ignoreMap":101},[105,5043,5044,5046],{"class":107,"line":108},[105,5045,112],{"class":111},[105,5047,116],{"class":115},[105,5049,5050],{"class":107,"line":119},[105,5051,123],{"class":122},[105,5053,5054],{"class":107,"line":126},[105,5055,130],{"emptyLinePlaceholder":129},[105,5057,5058,5060,5062,5064],{"class":107,"line":133},[105,5059,137],{"class":136},[105,5061,141],{"class":140},[105,5063,145],{"class":144},[105,5065,148],{"class":140},[105,5067,5068,5070,5072,5074,5076,5078,5080,5082],{"class":107,"line":151},[105,5069,155],{"class":154},[105,5071,158],{"class":136},[105,5073,141],{"class":140},[105,5075,164],{"class":163},[105,5077,167],{"class":140},[105,5079,170],{"class":115},[105,5081,173],{"class":140},[105,5083,176],{"class":122},[105,5085,5086],{"class":107,"line":179},[105,5087,130],{"emptyLinePlaceholder":129},[105,5089,5090,5092,5094,5096],{"class":107,"line":184},[105,5091,137],{"class":136},[105,5093,141],{"class":140},[105,5095,145],{"class":144},[105,5097,148],{"class":140},[105,5099,5100,5102,5104],{"class":107,"line":195},[105,5101,155],{"class":154},[105,5103,200],{"class":136},[105,5105,203],{"class":122},[10,5107,206],{},[10,5109,209,5110,213,5112,216,5114,220,5116],{},[14,5111,212],{},[14,5113,212],{},[14,5115,219],{},[22,5117,223],{},[10,5119,5120,229,5122],{},[14,5121,228],{},[22,5123,232],{},[91,5125,236],{"id":235},[96,5127,5128],{"className":98,"code":239,"language":100,"meta":101,"style":101},[14,5129,5130,5136,5140,5144,5168,5182,5186,5196,5206,5210,5226,5242,5246,5260,5272,5292,5312,5316,5320,5342,5350,5364,5368,5390,5398,5412,5416,5430,5438,5452,5456,5470,5484,5496,5504],{"__ignoreMap":101},[105,5131,5132,5134],{"class":107,"line":108},[105,5133,112],{"class":111},[105,5135,116],{"class":115},[105,5137,5138],{"class":107,"line":119},[105,5139,252],{"class":122},[105,5141,5142],{"class":107,"line":126},[105,5143,130],{"emptyLinePlaceholder":129},[105,5145,5146,5148,5150,5152,5154,5156,5158,5160,5162,5164,5166],{"class":107,"line":133},[105,5147,261],{"class":111},[105,5149,264],{"class":115},[105,5151,267],{"class":140},[105,5153,271],{"class":270},[105,5155,274],{"class":163},[105,5157,277],{"class":270},[105,5159,274],{"class":163},[105,5161,282],{"class":270},[105,5163,274],{"class":163},[105,5165,287],{"class":270},[105,5167,290],{"class":140},[105,5169,5170,5172,5174,5176,5178,5180],{"class":107,"line":151},[105,5171,295],{"class":154},[105,5173,298],{"class":270},[105,5175,301],{"class":140},[105,5177,264],{"class":115},[105,5179,306],{"class":140},[105,5181,309],{"class":111},[105,5183,5184],{"class":107,"line":179},[105,5185,130],{"emptyLinePlaceholder":129},[105,5187,5188,5190,5192,5194],{"class":107,"line":184},[105,5189,295],{"class":154},[105,5191,320],{"class":270},[105,5193,301],{"class":140},[105,5195,326],{"class":325},[105,5197,5198,5200,5202,5204],{"class":107,"line":195},[105,5199,295],{"class":154},[105,5201,333],{"class":270},[105,5203,301],{"class":140},[105,5205,326],{"class":325},[105,5207,5208],{"class":107,"line":340},[105,5209,130],{"emptyLinePlaceholder":129},[105,5211,5212,5214,5216,5218,5220,5222,5224],{"class":107,"line":345},[105,5213,155],{"class":154},[105,5215,350],{"class":136},[105,5217,141],{"class":140},[105,5219,355],{"class":163},[105,5221,167],{"class":140},[105,5223,170],{"class":115},[105,5225,148],{"class":140},[105,5227,5228,5230,5232,5234,5236,5238,5240],{"class":107,"line":364},[105,5229,155],{"class":154},[105,5231,369],{"class":136},[105,5233,141],{"class":140},[105,5235,374],{"class":163},[105,5237,167],{"class":140},[105,5239,264],{"class":115},[105,5241,148],{"class":140},[105,5243,5244],{"class":107,"line":383},[105,5245,130],{"emptyLinePlaceholder":129},[105,5247,5248,5250,5252,5254,5256,5258],{"class":107,"line":388},[105,5249,391],{"class":154},[105,5251,394],{"class":136},[105,5253,397],{"class":140},[105,5255,400],{"class":154},[105,5257,403],{"class":115},[105,5259,406],{"class":140},[105,5261,5262,5264,5266,5268,5270],{"class":107,"line":409},[105,5263,412],{"class":115},[105,5265,306],{"class":163},[105,5267,417],{"class":136},[105,5269,397],{"class":140},[105,5271,422],{"class":122},[105,5273,5274,5276,5278,5280,5282,5284,5286,5288,5290],{"class":107,"line":425},[105,5275,428],{"class":115},[105,5277,306],{"class":140},[105,5279,212],{"class":270},[105,5281,306],{"class":163},[105,5283,437],{"class":136},[105,5285,141],{"class":140},[105,5287,442],{"class":270},[105,5289,173],{"class":140},[105,5291,447],{"class":122},[105,5293,5294,5296,5298,5300,5302,5304,5306,5308,5310],{"class":107,"line":450},[105,5295,428],{"class":115},[105,5297,306],{"class":140},[105,5299,457],{"class":270},[105,5301,306],{"class":163},[105,5303,437],{"class":136},[105,5305,141],{"class":140},[105,5307,466],{"class":270},[105,5309,173],{"class":140},[105,5311,471],{"class":122},[105,5313,5314],{"class":107,"line":474},[105,5315,130],{"emptyLinePlaceholder":129},[105,5317,5318],{"class":107,"line":479},[105,5319,482],{"class":122},[105,5321,5322,5324,5326,5328,5330,5332,5334,5336,5338,5340],{"class":107,"line":485},[105,5323,391],{"class":154},[105,5325,490],{"class":136},[105,5327,141],{"class":140},[105,5329,374],{"class":163},[105,5331,167],{"class":140},[105,5333,264],{"class":115},[105,5335,173],{"class":140},[105,5337,400],{"class":154},[105,5339,403],{"class":115},[105,5341,406],{"class":140},[105,5343,5344,5346,5348],{"class":107,"line":509},[105,5345,512],{"class":270},[105,5347,515],{"class":140},[105,5349,518],{"class":270},[105,5351,5352,5354,5356,5358,5360,5362],{"class":107,"line":521},[105,5353,524],{"class":270},[105,5355,306],{"class":163},[105,5357,529],{"class":136},[105,5359,141],{"class":140},[105,5361,374],{"class":270},[105,5363,148],{"class":140},[105,5365,5366],{"class":107,"line":538},[105,5367,130],{"emptyLinePlaceholder":129},[105,5369,5370,5372,5374,5376,5378,5380,5382,5384,5386,5388],{"class":107,"line":543},[105,5371,391],{"class":154},[105,5373,548],{"class":136},[105,5375,141],{"class":140},[105,5377,164],{"class":163},[105,5379,167],{"class":140},[105,5381,170],{"class":115},[105,5383,173],{"class":140},[105,5385,400],{"class":154},[105,5387,403],{"class":115},[105,5389,406],{"class":140},[105,5391,5392,5394,5396],{"class":107,"line":567},[105,5393,570],{"class":270},[105,5395,573],{"class":154},[105,5397,576],{"class":270},[105,5399,5400,5402,5404,5406,5408,5410],{"class":107,"line":579},[105,5401,582],{"class":270},[105,5403,306],{"class":163},[105,5405,529],{"class":136},[105,5407,141],{"class":140},[105,5409,591],{"class":270},[105,5411,148],{"class":140},[105,5413,5414],{"class":107,"line":596},[105,5415,130],{"emptyLinePlaceholder":129},[105,5417,5418,5420,5422,5424,5426,5428],{"class":107,"line":601},[105,5419,391],{"class":154},[105,5421,606],{"class":136},[105,5423,397],{"class":140},[105,5425,400],{"class":154},[105,5427,403],{"class":115},[105,5429,406],{"class":140},[105,5431,5432,5434,5436],{"class":107,"line":617},[105,5433,570],{"class":270},[105,5435,515],{"class":140},[105,5437,326],{"class":325},[105,5439,5440,5442,5444,5446,5448,5450],{"class":107,"line":626},[105,5441,582],{"class":270},[105,5443,306],{"class":163},[105,5445,529],{"class":136},[105,5447,141],{"class":140},[105,5449,591],{"class":270},[105,5451,148],{"class":140},[105,5453,5454],{"class":107,"line":641},[105,5455,130],{"emptyLinePlaceholder":129},[105,5457,5458,5460,5462,5464,5466,5468],{"class":107,"line":646},[105,5459,391],{"class":154},[105,5461,651],{"class":136},[105,5463,397],{"class":140},[105,5465,400],{"class":154},[105,5467,403],{"class":115},[105,5469,406],{"class":140},[105,5471,5472,5474,5476,5478,5480,5482],{"class":107,"line":662},[105,5473,665],{"class":136},[105,5475,141],{"class":140},[105,5477,670],{"class":115},[105,5479,306],{"class":140},[105,5481,287],{"class":111},[105,5483,148],{"class":140},[105,5485,5486,5488,5490,5492,5494],{"class":107,"line":679},[105,5487,682],{"class":111},[105,5489,320],{"class":270},[105,5491,687],{"class":154},[105,5493,333],{"class":270},[105,5495,406],{"class":163},[105,5497,5498,5500,5502],{"class":107,"line":694},[105,5499,697],{"class":270},[105,5501,515],{"class":140},[105,5503,702],{"class":270},[105,5505,5506,5508,5510,5512,5514],{"class":107,"line":705},[105,5507,708],{"class":115},[105,5509,306],{"class":163},[105,5511,713],{"class":136},[105,5513,397],{"class":140},[105,5515,718],{"class":122},[720,5517,5518,5526,5532,5538],{},[723,5519,5520,728,5522,731,5524],{},[14,5521,727],{},[14,5523,282],{},[22,5525,734],{},[723,5527,5528,740,5530,743],{},[14,5529,739],{},[14,5531,529],{},[723,5533,5534,749,5536,752],{},[14,5535,748],{},[14,5537,442],{},[723,5539,5540,758,5542,762],{},[14,5541,757],{},[14,5543,761],{},[91,5545,766,5546,769],{"id":765},[14,5547,16],{},[10,5549,772,5550,776,5552,780,5554,783],{},[14,5551,775],{},[14,5553,779],{},[14,5555,779],{},[96,5557,5558],{"className":98,"code":786,"language":100,"meta":101,"style":101},[14,5559,5560,5574,5598,5604,5608,5622,5626,5630,5650,5654,5672,5676,5702,5722,5758],{"__ignoreMap":101},[105,5561,5562,5564,5566,5568,5570,5572],{"class":107,"line":108},[105,5563,391],{"class":154},[105,5565,795],{"class":136},[105,5567,397],{"class":140},[105,5569,400],{"class":154},[105,5571,403],{"class":115},[105,5573,406],{"class":140},[105,5575,5576,5578,5580,5582,5584,5586,5588,5590,5592,5594,5596],{"class":107,"line":119},[105,5577,682],{"class":111},[105,5579,810],{"class":115},[105,5581,306],{"class":140},[105,5583,815],{"class":270},[105,5585,818],{"class":154},[105,5587,810],{"class":115},[105,5589,306],{"class":140},[105,5591,670],{"class":270},[105,5593,306],{"class":140},[105,5595,277],{"class":111},[105,5597,406],{"class":163},[105,5599,5600,5602],{"class":107,"line":126},[105,5601,835],{"class":111},[105,5603,838],{"class":122},[105,5605,5606],{"class":107,"line":133},[105,5607,843],{"class":122},[105,5609,5610,5612,5614,5616,5618,5620],{"class":107,"line":151},[105,5611,848],{"class":115},[105,5613,306],{"class":140},[105,5615,457],{"class":270},[105,5617,306],{"class":163},[105,5619,529],{"class":136},[105,5621,859],{"class":140},[105,5623,5624],{"class":107,"line":179},[105,5625,864],{"class":111},[105,5627,5628],{"class":107,"line":184},[105,5629,869],{"class":122},[105,5631,5632,5634,5636,5638,5640,5642,5644,5646,5648],{"class":107,"line":195},[105,5633,848],{"class":115},[105,5635,306],{"class":140},[105,5637,212],{"class":270},[105,5639,306],{"class":163},[105,5641,529],{"class":136},[105,5643,141],{"class":140},[105,5645,886],{"class":325},[105,5647,173],{"class":140},[105,5649,891],{"class":122},[105,5651,5652],{"class":107,"line":340},[105,5653,896],{"class":122},[105,5655,5656,5658,5660,5662,5664,5666,5668,5670],{"class":107,"line":345},[105,5657,848],{"class":115},[105,5659,306],{"class":140},[105,5661,212],{"class":270},[105,5663,306],{"class":163},[105,5665,529],{"class":136},[105,5667,141],{"class":140},[105,5669,913],{"class":325},[105,5671,148],{"class":140},[105,5673,5674],{"class":107,"line":364},[105,5675,130],{"emptyLinePlaceholder":129},[105,5677,5678,5680,5682,5684,5686,5688,5690,5692,5694,5696,5698,5700],{"class":107,"line":383},[105,5679,391],{"class":154},[105,5681,926],{"class":136},[105,5683,141],{"class":140},[105,5685,374],{"class":163},[105,5687,167],{"class":140},[105,5689,810],{"class":115},[105,5691,306],{"class":140},[105,5693,670],{"class":270},[105,5695,173],{"class":140},[105,5697,400],{"class":154},[105,5699,403],{"class":115},[105,5701,406],{"class":140},[105,5703,5704,5706,5708,5710,5712,5714,5716,5718,5720],{"class":107,"line":388},[105,5705,682],{"class":111},[105,5707,953],{"class":270},[105,5709,956],{"class":154},[105,5711,810],{"class":115},[105,5713,306],{"class":140},[105,5715,670],{"class":270},[105,5717,306],{"class":140},[105,5719,287],{"class":111},[105,5721,406],{"class":163},[105,5723,5724,5726,5728,5730,5732,5734,5736,5738,5740,5742,5744,5746,5748,5750,5752,5754,5756],{"class":107,"line":409},[105,5725,973],{"class":111},[105,5727,976],{"class":270},[105,5729,979],{"class":111},[105,5731,982],{"class":140},[105,5733,985],{"class":111},[105,5735,989],{"class":988},[105,5737,274],{"class":163},[105,5739,985],{"class":111},[105,5741,996],{"class":988},[105,5743,274],{"class":163},[105,5745,985],{"class":111},[105,5747,1003],{"class":988},[105,5749,274],{"class":163},[105,5751,985],{"class":111},[105,5753,1010],{"class":988},[105,5755,1013],{"class":140},[105,5757,406],{"class":163},[105,5759,5760,5762,5764,5766,5768],{"class":107,"line":425},[105,5761,1020],{"class":270},[105,5763,306],{"class":163},[105,5765,1025],{"class":136},[105,5767,397],{"class":140},[105,5769,1030],{"class":122},[10,5771,209,5772,1035,5774,1039,5776,1042,5778,1045],{},[14,5773,16],{},[22,5775,1038],{},[14,5777,457],{},[14,5779,16],{},[10,5781,1048,5782,1051,5784,1055,5786,1058,5788,1062,5790,1066],{},[14,5783,16],{},[14,5785,1054],{},[14,5787,287],{},[22,5789,1061],{},[22,5791,1065],{},[10,5793,1069,5794,1072],{},[14,5795,989],{},[91,5797,1076],{"id":1075},[96,5799,5800],{"className":98,"code":1079,"language":100,"meta":101,"style":101},[14,5801,5802,5808,5812,5816,5826,5830,5844,5866,5894,5906,5928,5932,5954,5958,5972,5992,5998,6026,6050,6062,6068],{"__ignoreMap":101},[105,5803,5804,5806],{"class":107,"line":108},[105,5805,112],{"class":111},[105,5807,116],{"class":115},[105,5809,5810],{"class":107,"line":119},[105,5811,1092],{"class":122},[105,5813,5814],{"class":107,"line":126},[105,5815,130],{"emptyLinePlaceholder":129},[105,5817,5818,5820,5822,5824],{"class":107,"line":133},[105,5819,1101],{"class":154},[105,5821,1104],{"class":270},[105,5823,301],{"class":140},[105,5825,1109],{"class":144},[105,5827,5828],{"class":107,"line":151},[105,5829,130],{"emptyLinePlaceholder":129},[105,5831,5832,5834,5836,5838,5840,5842],{"class":107,"line":179},[105,5833,391],{"class":154},[105,5835,1120],{"class":136},[105,5837,397],{"class":140},[105,5839,400],{"class":154},[105,5841,403],{"class":115},[105,5843,406],{"class":140},[105,5845,5846,5848,5850,5852,5854,5856,5858,5860,5862,5864],{"class":107,"line":184},[105,5847,1133],{"class":154},[105,5849,1136],{"class":270},[105,5851,301],{"class":140},[105,5853,267],{"class":140},[105,5855,1143],{"class":144},[105,5857,1146],{"class":163},[105,5859,70],{"class":115},[105,5861,306],{"class":163},[105,5863,1153],{"class":270},[105,5865,290],{"class":140},[105,5867,5868,5870,5872,5874,5876,5878,5880,5882,5884,5886,5888,5890,5892],{"class":107,"line":195},[105,5869,1133],{"class":154},[105,5871,1162],{"class":270},[105,5873,301],{"class":140},[105,5875,1167],{"class":115},[105,5877,306],{"class":163},[105,5879,1172],{"class":136},[105,5881,141],{"class":140},[105,5883,1177],{"class":270},[105,5885,1180],{"class":140},[105,5887,1167],{"class":115},[105,5889,306],{"class":140},[105,5891,1187],{"class":111},[105,5893,148],{"class":140},[105,5895,5896,5898,5900,5902,5904],{"class":107,"line":340},[105,5897,682],{"class":111},[105,5899,1162],{"class":270},[105,5901,956],{"class":154},[105,5903,1200],{"class":111},[105,5905,406],{"class":163},[105,5907,5908,5910,5912,5914,5916,5918,5920,5922,5924,5926],{"class":107,"line":345},[105,5909,1207],{"class":136},[105,5911,141],{"class":140},[105,5913,1212],{"class":144},[105,5915,1215],{"class":988},[105,5917,1218],{"class":144},[105,5919,1221],{"class":154},[105,5921,1167],{"class":115},[105,5923,306],{"class":163},[105,5925,1228],{"class":136},[105,5927,1231],{"class":140},[105,5929,5930],{"class":107,"line":364},[105,5931,864],{"class":111},[105,5933,5934,5936,5938,5940,5942,5944,5946,5948,5950,5952],{"class":107,"line":383},[105,5935,1240],{"class":270},[105,5937,306],{"class":163},[105,5939,1245],{"class":136},[105,5941,141],{"class":140},[105,5943,1250],{"class":115},[105,5945,306],{"class":163},[105,5947,1255],{"class":136},[105,5949,141],{"class":140},[105,5951,1260],{"class":270},[105,5953,1263],{"class":140},[105,5955,5956],{"class":107,"line":388},[105,5957,130],{"emptyLinePlaceholder":129},[105,5959,5960,5962,5964,5966,5968,5970],{"class":107,"line":409},[105,5961,391],{"class":154},[105,5963,1274],{"class":136},[105,5965,397],{"class":140},[105,5967,400],{"class":154},[105,5969,403],{"class":115},[105,5971,406],{"class":140},[105,5973,5974,5976,5978,5980,5982,5984,5986,5988,5990],{"class":107,"line":425},[105,5975,682],{"class":111},[105,5977,1289],{"class":154},[105,5979,1167],{"class":115},[105,5981,306],{"class":163},[105,5983,1296],{"class":136},[105,5985,141],{"class":140},[105,5987,1177],{"class":270},[105,5989,173],{"class":140},[105,5991,406],{"class":163},[105,5993,5994,5996],{"class":107,"line":450},[105,5995,835],{"class":111},[105,5997,1311],{"class":122},[105,5999,6000,6002,6004,6006,6008,6010,6012,6014,6016,6018,6020,6022,6024],{"class":107,"line":474},[105,6001,1133],{"class":154},[105,6003,1162],{"class":270},[105,6005,301],{"class":140},[105,6007,1167],{"class":115},[105,6009,306],{"class":163},[105,6011,1172],{"class":136},[105,6013,141],{"class":140},[105,6015,1177],{"class":270},[105,6017,1180],{"class":140},[105,6019,1167],{"class":115},[105,6021,306],{"class":140},[105,6023,1338],{"class":111},[105,6025,148],{"class":140},[105,6027,6028,6030,6032,6034,6036,6038,6040,6042,6044,6046,6048],{"class":107,"line":479},[105,6029,1133],{"class":154},[105,6031,1136],{"class":270},[105,6033,515],{"class":140},[105,6035,1351],{"class":115},[105,6037,306],{"class":163},[105,6039,1356],{"class":136},[105,6041,141],{"class":140},[105,6043,1361],{"class":270},[105,6045,306],{"class":163},[105,6047,1366],{"class":136},[105,6049,1231],{"class":140},[105,6051,6052,6054,6056,6058,6060],{"class":107,"line":485},[105,6053,682],{"class":111},[105,6055,1136],{"class":270},[105,6057,956],{"class":154},[105,6059,1200],{"class":111},[105,6061,406],{"class":163},[105,6063,6064,6066],{"class":107,"line":509},[105,6065,835],{"class":111},[105,6067,1387],{"class":122},[105,6069,6070,6072,6074,6076,6078,6080,6082,6084,6086,6088,6090,6092],{"class":107,"line":521},[105,6071,1392],{"class":115},[105,6073,306],{"class":140},[105,6075,1153],{"class":270},[105,6077,515],{"class":140},[105,6079,1136],{"class":270},[105,6081,306],{"class":163},[105,6083,1405],{"class":136},[105,6085,141],{"class":140},[105,6087,1410],{"class":144},[105,6089,1180],{"class":140},[105,6091,1415],{"class":325},[105,6093,148],{"class":140},[10,6095,1420,6096,1424,6098,1428,6100,1432,6102,1435],{},[14,6097,1423],{},[14,6099,1427],{},[14,6101,1431],{},[14,6103,1423],{},[91,6105,1438],{"id":1438},[96,6107,6108],{"className":98,"code":1441,"language":100,"meta":101,"style":101},[14,6109,6110,6116,6120,6124,6142,6160,6178,6182,6196,6214,6238,6242,6264,6276,6286,6292],{"__ignoreMap":101},[105,6111,6112,6114],{"class":107,"line":108},[105,6113,112],{"class":111},[105,6115,116],{"class":115},[105,6117,6118],{"class":107,"line":119},[105,6119,1454],{"class":122},[105,6121,6122],{"class":107,"line":126},[105,6123,130],{"emptyLinePlaceholder":129},[105,6125,6126,6128,6130,6132,6134,6136,6138,6140],{"class":107,"line":133},[105,6127,1463],{"class":136},[105,6129,1466],{"class":154},[105,6131,1469],{"class":270},[105,6133,167],{"class":140},[105,6135,1474],{"class":115},[105,6137,515],{"class":140},[105,6139,1479],{"class":111},[105,6141,1482],{"class":988},[105,6143,6144,6146,6148,6150,6152,6154,6156,6158],{"class":107,"line":151},[105,6145,1463],{"class":136},[105,6147,1466],{"class":154},[105,6149,1491],{"class":270},[105,6151,167],{"class":140},[105,6153,1474],{"class":115},[105,6155,515],{"class":140},[105,6157,1479],{"class":111},[105,6159,1502],{"class":988},[105,6161,6162,6164,6166,6168,6170,6172,6174,6176],{"class":107,"line":179},[105,6163,1463],{"class":136},[105,6165,1466],{"class":154},[105,6167,1511],{"class":270},[105,6169,167],{"class":140},[105,6171,1474],{"class":115},[105,6173,515],{"class":140},[105,6175,1479],{"class":111},[105,6177,1522],{"class":988},[105,6179,6180],{"class":107,"line":184},[105,6181,130],{"emptyLinePlaceholder":129},[105,6183,6184,6186,6188,6190,6192,6194],{"class":107,"line":195},[105,6185,391],{"class":154},[105,6187,394],{"class":136},[105,6189,397],{"class":140},[105,6191,400],{"class":154},[105,6193,403],{"class":115},[105,6195,406],{"class":140},[105,6197,6198,6200,6202,6204,6206,6208,6210,6212],{"class":107,"line":340},[105,6199,428],{"class":115},[105,6201,306],{"class":140},[105,6203,212],{"class":270},[105,6205,306],{"class":163},[105,6207,437],{"class":136},[105,6209,141],{"class":140},[105,6211,1557],{"class":270},[105,6213,148],{"class":140},[105,6215,6216,6218,6220,6222,6224,6226,6228,6230,6232,6234,6236],{"class":107,"line":345},[105,6217,428],{"class":115},[105,6219,306],{"class":140},[105,6221,457],{"class":270},[105,6223,306],{"class":163},[105,6225,437],{"class":136},[105,6227,141],{"class":140},[105,6229,1576],{"class":270},[105,6231,306],{"class":140},[105,6233,1581],{"class":270},[105,6235,173],{"class":140},[105,6237,1586],{"class":122},[105,6239,6240],{"class":107,"line":364},[105,6241,130],{"emptyLinePlaceholder":129},[105,6243,6244,6246,6248,6250,6252,6254,6256,6258,6260,6262],{"class":107,"line":383},[105,6245,391],{"class":154},[105,6247,1597],{"class":136},[105,6249,141],{"class":140},[105,6251,164],{"class":163},[105,6253,167],{"class":140},[105,6255,170],{"class":115},[105,6257,173],{"class":140},[105,6259,400],{"class":154},[105,6261,403],{"class":115},[105,6263,406],{"class":140},[105,6265,6266,6268,6270,6272,6274],{"class":107,"line":388},[105,6267,682],{"class":111},[105,6269,1620],{"class":270},[105,6271,1623],{"class":154},[105,6273,1626],{"class":325},[105,6275,406],{"class":163},[105,6277,6278,6280,6282,6284],{"class":107,"line":409},[105,6279,1633],{"class":270},[105,6281,306],{"class":163},[105,6283,1581],{"class":136},[105,6285,859],{"class":140},[105,6287,6288,6290],{"class":107,"line":425},[105,6289,1644],{"class":111},[105,6291,406],{"class":163},[105,6293,6294,6296,6298,6300],{"class":107,"line":450},[105,6295,1651],{"class":270},[105,6297,306],{"class":163},[105,6299,1581],{"class":136},[105,6301,859],{"class":140},[10,6303,1660,6304,1663,6306,1667,6308,1671,6310,1675,6312,1679],{},[14,6305,212],{},[14,6307,1666],{},[14,6309,1670],{},[14,6311,1674],{},[14,6313,1678],{},[10,6315,1682],{},[1684,6317,1686],{},{"title":101,"searchDepth":119,"depth":119,"links":6319},[6320,6321,6322,6323,6324],{"id":93,"depth":119,"text":94},{"id":235,"depth":119,"text":236},{"id":765,"depth":119,"text":1692},{"id":1075,"depth":119,"text":1076},{"id":1438,"depth":119,"text":1438},{},{"title":5,"description":1697},{"id":6328,"title":6329,"body":6330,"cover":1695,"date":7073,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":7074,"navigation":129,"path":7075,"seo":7076,"stem":7077,"toc":129,"__hash__":7078},"devlog\u002Fdevlog\u002Fxggame-snake\u002F07-settings-menu.md","设置菜单",{"type":7,"value":6331,"toc":7067},[6332,6343,6346,6350,6410,6416,6420,6429,6435,6562,6565,6575,6578,6592,6597,6705,6710,6873,6876,6879,7027,7048,7055,7061,7064],[10,6333,6334,6335,6338,6339,6342],{},"菜单场景和游戏场景是两个世界，玩家在菜单里选的\"穿墙\"，游戏怎么知道？直觉是\"菜单把值传给游戏\"。但 ",[14,6336,6337],{},"change_scene_to_file"," 会",[22,6340,6341],{},"先销毁菜单，再创建游戏","——游戏出生那一刻，菜单已经不在了，没人可传。",[10,6344,6345],{},"答案：放到一个\"两边都在的时候它都在\"的地方——Autoload，也就是 GameManager。菜单往里写，游戏从里读。这就是\"单一数据源\"：以后任何地方想知道\"现在是穿墙模式吗\"，都只问 GameManager 一个人。",[91,6347,6349],{"id":6348},"gamemanager-加三个字段","GameManager 加三个字段",[96,6351,6353],{"className":98,"code":6352,"language":100,"meta":101,"style":101},"## ===== 开局设置：菜单写、游戏读，只存这一份 =====\nvar wrap_mode := false                 # true = 穿墙，false = 撞墙死（菜单里选）\nvar cell_px := 20                      # 每格像素，本版固定 20 → 96×54 格\nvar snake_color := Color(\"#63de8c\")    # 蛇的皮肤，本版固定\n",[14,6354,6355,6360,6375,6389],{"__ignoreMap":101},[105,6356,6357],{"class":107,"line":108},[105,6358,6359],{"class":122},"## ===== 开局设置：菜单写、游戏读，只存这一份 =====\n",[105,6361,6362,6364,6367,6369,6372],{"class":107,"line":119},[105,6363,295],{"class":154},[105,6365,6366],{"class":270}," wrap_mode",[105,6368,301],{"class":140},[105,6370,6371],{"class":111}," false",[105,6373,6374],{"class":122},"                 # true = 穿墙，false = 撞墙死（菜单里选）\n",[105,6376,6377,6379,6382,6384,6386],{"class":107,"line":126},[105,6378,295],{"class":154},[105,6380,6381],{"class":270}," cell_px",[105,6383,301],{"class":140},[105,6385,2654],{"class":325},[105,6387,6388],{"class":122},"                      # 每格像素，本版固定 20 → 96×54 格\n",[105,6390,6391,6393,6396,6398,6400,6402,6405,6407],{"class":107,"line":133},[105,6392,295],{"class":154},[105,6394,6395],{"class":270}," snake_color",[105,6397,301],{"class":140},[105,6399,3329],{"class":115},[105,6401,141],{"class":140},[105,6403,6404],{"class":144},"\"#63de8c\"",[105,6406,173],{"class":140},[105,6408,6409],{"class":122},"    # 蛇的皮肤，本版固定\n",[10,6411,6412,6413],{},"后两个这一版菜单里根本不露出来，为什么也放进 GameManager？因为它们是\"设置\"，只是这版不让玩家改。放在这，游戏代码读的是\"设置\"而不是写死的数字；以后想让玩家选棋盘大小、选皮肤，菜单一露，游戏一字不改。",[22,6414,6415],{},"先按设置写，露不露是菜单的事。",[91,6417,6419],{"id":6418},"阶段-4-的菜单先用最丑的控件","阶段 4 的菜单先用最丑的控件",[10,6421,6422,6425,6426,2303],{},[14,6423,6424],{},"scenes\u002Fmenu.tscn","，然后文件系统里右键它 → ",[22,6427,6428],{},"设为主场景",[96,6430,6433],{"className":6431,"code":6432,"language":2027},[2025],"Menu (Node2D)                          ← 挂脚本 scripts\u002Fmenu.gd\n└── CanvasLayer\n    └── VBoxContainer\n        ├── Title (Label)\n        ├── WrapToggle (CheckButton)   ← \"穿墙模式\"\n        └── StartButton (Button)\n",[14,6434,6432],{"__ignoreMap":101},[96,6436,6438],{"className":98,"code":6437,"language":100,"meta":101,"style":101},"extends Node2D\n\n@onready var wrap_toggle: CheckButton = $CanvasLayer\u002FVBoxContainer\u002FWrapToggle\n\nfunc _ready() -> void:\n    wrap_toggle.grab_focus()                        # 手柄：一进菜单就得有一个控件亮着\n\nfunc _on_start_button_pressed() -> void:\n    GameManager.wrap_mode = wrap_toggle.button_pressed     # 勾上 = 穿墙\n    get_tree().change_scene_to_file(\"res:\u002F\u002Fscenes\u002Fmain.tscn\")\n",[14,6439,6440,6446,6450,6471,6475,6489,6504,6508,6523,6544],{"__ignoreMap":101},[105,6441,6442,6444],{"class":107,"line":108},[105,6443,112],{"class":111},[105,6445,2603],{"class":115},[105,6447,6448],{"class":107,"line":119},[105,6449,130],{"emptyLinePlaceholder":129},[105,6451,6452,6454,6456,6459,6461,6464,6466,6468],{"class":107,"line":126},[105,6453,1463],{"class":136},[105,6455,1466],{"class":154},[105,6457,6458],{"class":270}," wrap_toggle",[105,6460,167],{"class":140},[105,6462,6463],{"class":115}," CheckButton",[105,6465,515],{"class":140},[105,6467,1479],{"class":111},[105,6469,6470],{"class":988},"CanvasLayer\u002FVBoxContainer\u002FWrapToggle\n",[105,6472,6473],{"class":107,"line":133},[105,6474,130],{"emptyLinePlaceholder":129},[105,6476,6477,6479,6481,6483,6485,6487],{"class":107,"line":151},[105,6478,391],{"class":154},[105,6480,394],{"class":136},[105,6482,397],{"class":140},[105,6484,400],{"class":154},[105,6486,403],{"class":115},[105,6488,406],{"class":140},[105,6490,6491,6494,6496,6499,6501],{"class":107,"line":179},[105,6492,6493],{"class":270},"    wrap_toggle",[105,6495,306],{"class":163},[105,6497,6498],{"class":136},"grab_focus",[105,6500,397],{"class":140},[105,6502,6503],{"class":122},"                        # 手柄：一进菜单就得有一个控件亮着\n",[105,6505,6506],{"class":107,"line":184},[105,6507,130],{"emptyLinePlaceholder":129},[105,6509,6510,6512,6515,6517,6519,6521],{"class":107,"line":195},[105,6511,391],{"class":154},[105,6513,6514],{"class":136}," _on_start_button_pressed",[105,6516,397],{"class":140},[105,6518,400],{"class":154},[105,6520,403],{"class":115},[105,6522,406],{"class":140},[105,6524,6525,6527,6529,6532,6534,6536,6538,6541],{"class":107,"line":340},[105,6526,1392],{"class":115},[105,6528,306],{"class":140},[105,6530,6531],{"class":270},"wrap_mode",[105,6533,515],{"class":140},[105,6535,6458],{"class":270},[105,6537,306],{"class":140},[105,6539,6540],{"class":270},"button_pressed",[105,6542,6543],{"class":122},"     # 勾上 = 穿墙\n",[105,6545,6546,6549,6551,6553,6555,6557,6560],{"class":107,"line":345},[105,6547,6548],{"class":136},"    get_tree",[105,6550,397],{"class":140},[105,6552,306],{"class":163},[105,6554,6337],{"class":136},[105,6556,141],{"class":140},[105,6558,6559],{"class":144},"\"res:\u002F\u002Fscenes\u002Fmain.tscn\"",[105,6561,148],{"class":140},[10,6563,6564],{},"为什么用最丑的 CheckButton？外壳阶段先要\"能用\"：能选、能传、能进游戏。好不好看是第 10 章的事，那时候换控件只动菜单，GameManager 和游戏代码不动——这就是把设置放 GameManager 的回报。",[10,6566,6567,6570,6571,6574],{},[22,6568,6569],{},"主场景","是按 F5（以及玩家双击游戏图标）时第一个加载的场景。之前设成 ",[14,6572,6573],{},"main.tscn"," 只是为了能按 F5；现在入口变成\"先菜单、再游戏\"，入口变了，主场景就得跟着变。不改的话 F5 直接进游戏、跳过菜单。",[91,6576,6577],{"id":6577},"常量什么时候该变成变量",[10,6579,6580,6581,6584,6585,6588,6589],{},"第 3 章说 ",[14,6582,6583],{},"GRID_W"," 是常量\"定了不能改\"，现在它要跟着设置走，怎么办？它的意思一直是\"",[22,6586,6587],{},"游戏运行中","不能改\"。现在格子大小是一项设置、穿墙是玩家开局前选的——都是\"开局读一次的变量\"，不再是常量。",[22,6590,6591],{},"判断标准：这个值会因为设置或玩家的选择而不同吗？会 → 变量；永远一样 → 常量。",[10,6593,6594,6596],{},[14,6595,16],{}," 顶部：",[96,6598,6600],{"className":98,"code":6599,"language":100,"meta":101,"style":101},"const BOARD_W := 1920                  # 画面宽，和项目设置的视口一致\nconst BOARD_H := 1080\nconst START_LENGTH := 3                # 蛇初始几节\nvar grid_w := 96                       # 原来是 const GRID_W\nvar grid_h := 54\nvar cell_size := 20\nvar snake_body: Array[Vector2i] = []   # 第 0 个是头，_ready() 里生成\n",[14,6601,6602,6617,6629,6643,6657,6669,6681],{"__ignoreMap":101},[105,6603,6604,6606,6609,6611,6614],{"class":107,"line":108},[105,6605,1101],{"class":154},[105,6607,6608],{"class":270}," BOARD_W",[105,6610,301],{"class":140},[105,6612,6613],{"class":325}," 1920",[105,6615,6616],{"class":122},"                  # 画面宽，和项目设置的视口一致\n",[105,6618,6619,6621,6624,6626],{"class":107,"line":119},[105,6620,1101],{"class":154},[105,6622,6623],{"class":270}," BOARD_H",[105,6625,301],{"class":140},[105,6627,6628],{"class":325}," 1080\n",[105,6630,6631,6633,6636,6638,6640],{"class":107,"line":126},[105,6632,1101],{"class":154},[105,6634,6635],{"class":270}," START_LENGTH",[105,6637,301],{"class":140},[105,6639,2669],{"class":325},[105,6641,6642],{"class":122},"                # 蛇初始几节\n",[105,6644,6645,6647,6650,6652,6654],{"class":107,"line":133},[105,6646,295],{"class":154},[105,6648,6649],{"class":270}," grid_w",[105,6651,301],{"class":140},[105,6653,2624],{"class":325},[105,6655,6656],{"class":122},"                       # 原来是 const GRID_W\n",[105,6658,6659,6661,6664,6666],{"class":107,"line":151},[105,6660,295],{"class":154},[105,6662,6663],{"class":270}," grid_h",[105,6665,301],{"class":140},[105,6667,6668],{"class":325}," 54\n",[105,6670,6671,6673,6676,6678],{"class":107,"line":179},[105,6672,295],{"class":154},[105,6674,6675],{"class":270}," cell_size",[105,6677,301],{"class":140},[105,6679,6680],{"class":325}," 20\n",[105,6682,6683,6685,6687,6689,6691,6693,6695,6697,6699,6702],{"class":107,"line":184},[105,6684,295],{"class":154},[105,6686,2718],{"class":270},[105,6688,167],{"class":140},[105,6690,2723],{"class":115},[105,6692,2726],{"class":140},[105,6694,2729],{"class":115},[105,6696,1013],{"class":140},[105,6698,515],{"class":140},[105,6700,6701],{"class":140}," []",[105,6703,6704],{"class":122},"   # 第 0 个是头，_ready() 里生成\n",[10,6706,6707,6709],{},[14,6708,775],{}," 里：",[96,6711,6713],{"className":98,"code":6712,"language":100,"meta":101,"style":101},"    # 先有蛇，再生世界：食物要避开蛇出生的格子\n    cell_size = GameManager.cell_px\n    grid_w = BOARD_W \u002F cell_size\n    grid_h = BOARD_H \u002F cell_size\n\n    var mid := Vector2i(grid_w \u002F 2, grid_h \u002F 2)                    # 蛇从正中间出发\n    for i in START_LENGTH:\n        snake_body.append(Vector2i(mid.x - i, mid.y))   # i=0 是头，往左一节节排\n\n    _on_food_timer_timeout()                # 开局先长一个食物（避开蛇）\n    redraw()\n",[14,6714,6715,6720,6734,6749,6762,6766,6800,6813,6853,6857,6867],{"__ignoreMap":101},[105,6716,6717],{"class":107,"line":108},[105,6718,6719],{"class":122},"    # 先有蛇，再生世界：食物要避开蛇出生的格子\n",[105,6721,6722,6725,6727,6729,6731],{"class":107,"line":119},[105,6723,6724],{"class":270},"    cell_size",[105,6726,515],{"class":140},[105,6728,810],{"class":115},[105,6730,306],{"class":140},[105,6732,6733],{"class":270},"cell_px\n",[105,6735,6736,6739,6741,6743,6746],{"class":107,"line":126},[105,6737,6738],{"class":270},"    grid_w",[105,6740,515],{"class":140},[105,6742,6608],{"class":270},[105,6744,6745],{"class":154}," \u002F",[105,6747,6748],{"class":270}," cell_size\n",[105,6750,6751,6754,6756,6758,6760],{"class":107,"line":133},[105,6752,6753],{"class":270},"    grid_h",[105,6755,515],{"class":140},[105,6757,6623],{"class":270},[105,6759,6745],{"class":154},[105,6761,6748],{"class":270},[105,6763,6764],{"class":107,"line":151},[105,6765,130],{"emptyLinePlaceholder":129},[105,6767,6768,6770,6773,6775,6777,6779,6782,6784,6786,6788,6791,6793,6795,6797],{"class":107,"line":179},[105,6769,1133],{"class":154},[105,6771,6772],{"class":270}," mid",[105,6774,301],{"class":140},[105,6776,2684],{"class":115},[105,6778,141],{"class":140},[105,6780,6781],{"class":270},"grid_w",[105,6783,6745],{"class":154},[105,6785,1626],{"class":325},[105,6787,274],{"class":163},[105,6789,6790],{"class":270},"grid_h",[105,6792,6745],{"class":154},[105,6794,1626],{"class":325},[105,6796,173],{"class":140},[105,6798,6799],{"class":122},"                    # 蛇从正中间出发\n",[105,6801,6802,6804,6807,6809,6811],{"class":107,"line":184},[105,6803,3224],{"class":111},[105,6805,6806],{"class":270}," i",[105,6808,979],{"class":111},[105,6810,6635],{"class":270},[105,6812,406],{"class":163},[105,6814,6815,6817,6819,6821,6823,6825,6827,6830,6832,6834,6837,6839,6841,6843,6845,6847,6850],{"class":107,"line":195},[105,6816,4272],{"class":270},[105,6818,306],{"class":163},[105,6820,4515],{"class":136},[105,6822,141],{"class":140},[105,6824,2729],{"class":115},[105,6826,141],{"class":140},[105,6828,6829],{"class":270},"mid",[105,6831,306],{"class":163},[105,6833,4044],{"class":270},[105,6835,6836],{"class":154}," -",[105,6838,6806],{"class":270},[105,6840,274],{"class":163},[105,6842,6829],{"class":270},[105,6844,306],{"class":163},[105,6846,4077],{"class":270},[105,6848,6849],{"class":140},"))",[105,6851,6852],{"class":122},"   # i=0 是头，往左一节节排\n",[105,6854,6855],{"class":107,"line":340},[105,6856,130],{"emptyLinePlaceholder":129},[105,6858,6859,6862,6864],{"class":107,"line":345},[105,6860,6861],{"class":136},"    _on_food_timer_timeout",[105,6863,397],{"class":140},[105,6865,6866],{"class":122},"                # 开局先长一个食物（避开蛇）\n",[105,6868,6869,6871],{"class":107,"line":364},[105,6870,4290],{"class":136},[105,6872,859],{"class":140},[10,6874,6875],{},"用循环生成初始蛇身，而不是手写三个坐标——以后想改成 5 节，改一个数。",[91,6877,6878],{"id":6878},"一行实现穿墙",[96,6880,6882],{"className":98,"code":6881,"language":100,"meta":101,"style":101},"    if GameManager.wrap_mode:\n        new_head.x = posmod(new_head.x, grid_w)   # -1 → 95，96 → 0\n        new_head.y = posmod(new_head.y, grid_h)\n    elif new_head.x \u003C 0 or new_head.x >= grid_w \\\n    or new_head.y \u003C 0 or new_head.y >= grid_h:\n        EventBus.snake_died.emit()\n        return\n",[14,6883,6884,6896,6927,6953,6981,7009,7023],{"__ignoreMap":101},[105,6885,6886,6888,6890,6892,6894],{"class":107,"line":108},[105,6887,682],{"class":111},[105,6889,810],{"class":115},[105,6891,306],{"class":140},[105,6893,6531],{"class":270},[105,6895,406],{"class":163},[105,6897,6898,6901,6903,6905,6907,6910,6912,6914,6916,6918,6920,6922,6924],{"class":107,"line":119},[105,6899,6900],{"class":270},"        new_head",[105,6902,306],{"class":140},[105,6904,4044],{"class":270},[105,6906,515],{"class":140},[105,6908,6909],{"class":136}," posmod",[105,6911,141],{"class":140},[105,6913,4195],{"class":270},[105,6915,306],{"class":140},[105,6917,4044],{"class":270},[105,6919,1180],{"class":140},[105,6921,6649],{"class":270},[105,6923,173],{"class":140},[105,6925,6926],{"class":122},"   # -1 → 95，96 → 0\n",[105,6928,6929,6931,6933,6935,6937,6939,6941,6943,6945,6947,6949,6951],{"class":107,"line":126},[105,6930,6900],{"class":270},[105,6932,306],{"class":140},[105,6934,4077],{"class":270},[105,6936,515],{"class":140},[105,6938,6909],{"class":136},[105,6940,141],{"class":140},[105,6942,4195],{"class":270},[105,6944,306],{"class":140},[105,6946,4077],{"class":270},[105,6948,1180],{"class":140},[105,6950,6663],{"class":270},[105,6952,148],{"class":140},[105,6954,6955,6957,6959,6961,6963,6965,6967,6969,6971,6973,6975,6977,6979],{"class":107,"line":133},[105,6956,3779],{"class":111},[105,6958,3060],{"class":270},[105,6960,306],{"class":140},[105,6962,4044],{"class":270},[105,6964,4047],{"class":154},[105,6966,1415],{"class":325},[105,6968,4052],{"class":154},[105,6970,3060],{"class":270},[105,6972,306],{"class":140},[105,6974,4044],{"class":270},[105,6976,1623],{"class":154},[105,6978,6649],{"class":270},[105,6980,4065],{"class":140},[105,6982,6983,6985,6987,6989,6991,6993,6995,6997,6999,7001,7003,7005,7007],{"class":107,"line":151},[105,6984,4070],{"class":154},[105,6986,3060],{"class":270},[105,6988,306],{"class":140},[105,6990,4077],{"class":270},[105,6992,4047],{"class":154},[105,6994,1415],{"class":325},[105,6996,4052],{"class":154},[105,6998,3060],{"class":270},[105,7000,306],{"class":140},[105,7002,4077],{"class":270},[105,7004,1623],{"class":154},[105,7006,6663],{"class":270},[105,7008,406],{"class":163},[105,7010,7011,7013,7015,7017,7019,7021],{"class":107,"line":179},[105,7012,848],{"class":115},[105,7014,306],{"class":140},[105,7016,457],{"class":270},[105,7018,306],{"class":163},[105,7020,529],{"class":136},[105,7022,859],{"class":140},[105,7024,7025],{"class":107,"line":184},[105,7026,864],{"class":111},[10,7028,7029,7032,7033,7036,7037,7040,7041,7044,7045],{},[14,7030,7031],{},"posmod","：positive modulo，正取余。普通 ",[14,7034,7035],{},"%"," 对负数给负结果（",[14,7038,7039],{},"-1 % 20 = -1","），",[14,7042,7043],{},"posmod(-1, 20) = 19","。从左边 x = -1 出去，要从最右边 x = 95 进来——正好是 posmod 的答案。",[22,7046,7047],{},"周期性的东西用取余。",[10,7049,7050,7051,7054],{},"验证\"设置\"这条线通了：勾穿墙 → 蛇从左边出去右边进来；再把 ",[14,7052,7053],{},"cell_px"," 临时改成 12 → F5 → 格子变小蛇变小（改回 20）。菜单露不露，游戏都读得到。",[10,7056,7057],{},[2129,7058],{"alt":7059,"src":7060},"07-settings-穿墙","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F07-settings-%E7%A9%BF%E5%A2%99.png",[10,7062,7063],{},"下一章：暂停菜单——你第一次做的外壳，也是坑最多的一个。",[1684,7065,7066],{},"html pre.shiki code .snYqZ, html code.shiki .snYqZ{--shiki-default:#A0ADA0;--shiki-dark:#758575DD}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 .sTPum, html code.shiki .sTPum{--shiki-default:#1E754F;--shiki-dark:#4D9375}html pre.shiki code .sqbOQ, html code.shiki .sqbOQ{--shiki-default:#2F798A;--shiki-dark:#4C9A91}html pre.shiki code .s_NWU, html code.shiki .s_NWU{--shiki-default:#2E8F82;--shiki-dark:#5DA994}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 .s_xSY, html code.shiki .s_xSY{--shiki-default:#59873A;--shiki-dark:#80A665}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}",{"title":101,"searchDepth":119,"depth":119,"links":7068},[7069,7070,7071,7072],{"id":6348,"depth":119,"text":6349},{"id":6418,"depth":119,"text":6419},{"id":6577,"depth":119,"text":6577},{"id":6878,"depth":119,"text":6878},"2026-09-07",{},"\u002Fdevlog\u002Fxggame-snake\u002F07-settings-menu",{"title":6329,"description":1697},"devlog\u002Fxggame-snake\u002F07-settings-menu","x-lJkhAlJo2lIzAAtXrOi4og3XIQzwSCWXgTMuvKksM",{"id":7080,"title":7081,"body":7082,"cover":1695,"date":8091,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":8092,"navigation":129,"path":8093,"seo":8094,"stem":8095,"toc":129,"__hash__":8096},"devlog\u002Fdevlog\u002Fxggame-snake\u002F08-pause.md","暂停与结算",{"type":7,"value":7083,"toc":8084},[7084,7088,7106,7258,7267,7276,7280,7291,7302,7354,7374,7394,7803,7806,7854,7863,7866,7891,7898,7901,7905,7923,8066,8072,8078,8081],[91,7085,7087],{"id":7086},"ui-层与分数","UI 层与分数",[10,7089,7090,7092,7093,2563,7096,7099,7100,2563,7103,2303],{},[14,7091,2566],{}," 下加 ",[14,7094,7095],{},"CanvasLayer",[14,7097,7098],{},"UI","，里面 ",[14,7101,7102],{},"Label",[14,7104,7105],{},"ScoreLabel",[96,7107,7109],{"className":98,"code":7108,"language":100,"meta":101,"style":101},"extends CanvasLayer\n## UI 只是显示器：自己不存任何数据，全靠听信号刷新\n\n@onready var score_label: Label = $ScoreLabel\n\nfunc _ready() -> void:\n    GameManager.score_changed.connect(_on_score_changed)\n    score_label.text = str(GameManager.total_score)     # 开局同步一次\n\nfunc _on_score_changed(new_score: int) -> void:\n    score_label.text = str(new_score)\n",[14,7110,7111,7118,7123,7127,7148,7152,7166,7186,7213,7217,7240],{"__ignoreMap":101},[105,7112,7113,7115],{"class":107,"line":108},[105,7114,112],{"class":111},[105,7116,7117],{"class":115}," CanvasLayer\n",[105,7119,7120],{"class":107,"line":119},[105,7121,7122],{"class":122},"## UI 只是显示器：自己不存任何数据，全靠听信号刷新\n",[105,7124,7125],{"class":107,"line":126},[105,7126,130],{"emptyLinePlaceholder":129},[105,7128,7129,7131,7133,7136,7138,7141,7143,7145],{"class":107,"line":133},[105,7130,1463],{"class":136},[105,7132,1466],{"class":154},[105,7134,7135],{"class":270}," score_label",[105,7137,167],{"class":140},[105,7139,7140],{"class":115}," Label",[105,7142,515],{"class":140},[105,7144,1479],{"class":111},[105,7146,7147],{"class":988},"ScoreLabel\n",[105,7149,7150],{"class":107,"line":151},[105,7151,130],{"emptyLinePlaceholder":129},[105,7153,7154,7156,7158,7160,7162,7164],{"class":107,"line":179},[105,7155,391],{"class":154},[105,7157,394],{"class":136},[105,7159,397],{"class":140},[105,7161,400],{"class":154},[105,7163,403],{"class":115},[105,7165,406],{"class":140},[105,7167,7168,7170,7172,7175,7177,7179,7181,7184],{"class":107,"line":184},[105,7169,1392],{"class":115},[105,7171,306],{"class":140},[105,7173,7174],{"class":270},"score_changed",[105,7176,306],{"class":163},[105,7178,437],{"class":136},[105,7180,141],{"class":140},[105,7182,7183],{"class":270},"_on_score_changed",[105,7185,148],{"class":140},[105,7187,7188,7191,7193,7195,7197,7200,7202,7204,7206,7208,7210],{"class":107,"line":195},[105,7189,7190],{"class":270},"    score_label",[105,7192,306],{"class":140},[105,7194,2027],{"class":270},[105,7196,515],{"class":140},[105,7198,7199],{"class":136}," str",[105,7201,141],{"class":140},[105,7203,70],{"class":115},[105,7205,306],{"class":140},[105,7207,591],{"class":270},[105,7209,173],{"class":140},[105,7211,7212],{"class":122},"     # 开局同步一次\n",[105,7214,7215],{"class":107,"line":340},[105,7216,130],{"emptyLinePlaceholder":129},[105,7218,7219,7221,7224,7226,7228,7230,7232,7234,7236,7238],{"class":107,"line":345},[105,7220,391],{"class":154},[105,7222,7223],{"class":136}," _on_score_changed",[105,7225,141],{"class":140},[105,7227,355],{"class":163},[105,7229,167],{"class":140},[105,7231,170],{"class":115},[105,7233,173],{"class":140},[105,7235,400],{"class":154},[105,7237,403],{"class":115},[105,7239,406],{"class":140},[105,7241,7242,7244,7246,7248,7250,7252,7254,7256],{"class":107,"line":364},[105,7243,7190],{"class":270},[105,7245,306],{"class":140},[105,7247,2027],{"class":270},[105,7249,515],{"class":140},[105,7251,7199],{"class":136},[105,7253,141],{"class":140},[105,7255,355],{"class":270},[105,7257,148],{"class":140},[10,7259,209,7260,7262,7263,7266],{},[14,7261,775],{}," 里要\"开局同步一次\"？信号只在",[22,7264,7265],{},"变化时","才响。UI 刚创建出来那一刻还没人喊过，不主动读一次，标签会是空的。",[10,7268,7269,7270,7272,7273,7275],{},"为什么 UI 放在 ",[14,7271,7095],{}," 下？",[14,7274,7095],{}," 上的东西不受摄像机影响，永远贴在屏幕上。贪吃蛇没有移动的摄像机，现在看不出区别，但这是习惯——以后做有镜头的游戏，UI 不放这里会跟着地图跑。",[91,7277,7279],{"id":7278},"暂停菜单先讲坑再动手","暂停菜单：先讲坑再动手",[10,7281,7282,7283,7286,7287,7290],{},"Godot 的\"暂停\"是一句话：",[14,7284,7285],{},"get_tree().paused = true","。它会冻结整棵场景树——",[22,7288,7289],{},"包括你的暂停菜单本身","。菜单被冻住 = 按钮点不了、按 Esc 也没反应，你就再也出不来了。",[10,7292,7293,7294,7297,7298,7301],{},"解法是给菜单设 ",[14,7295,7296],{},"process_mode = Always","（\"暂停时我照常工作\"）。但 ",[14,7299,7300],{},"process_mode"," 会向下传染：一个节点设了 Always，它的所有子节点默认跟着 Always。所以要精确地只给该活着的节点设 Always：",[26,7303,7304,7317],{},[29,7305,7306],{},[32,7307,7308,7311,7314],{},[35,7309,7310],{},"档",[35,7312,7313],{},"意思",[35,7315,7316],{},"给谁",[45,7318,7319,7330,7341],{},[32,7320,7321,7324,7327],{},[50,7322,7323],{},"Inherit（默认）",[50,7325,7326],{},"跟父节点一样",[50,7328,7329],{},"大多数节点",[32,7331,7332,7335,7338],{},[50,7333,7334],{},"Pausable",[50,7336,7337],{},"暂停时停",[50,7339,7340],{},"蛇、四个 Timer、任何游戏逻辑",[32,7342,7343,7348,7351],{},[50,7344,7345],{},[22,7346,7347],{},"Always",[50,7349,7350],{},"暂停时照常跑",[50,7352,7353],{},"暂停菜单、监听 Esc 的节点",[10,7355,7356,2433,7365,7367,7368,7370,7371,7373],{},[22,7357,7358,7359,7361,7362,7364],{},"Always 设在 ",[14,7360,7098],{}," 上，不是 ",[14,7363,2566],{}," 上。",[14,7366,2566],{}," 设 Always 会传染给四个 Timer，暂停时蛇照走、食物照长。设在 ",[14,7369,7098],{}," 上，传染范围正好是\"所有面板和按钮\"，而 ",[14,7372,2566],{}," 及其下的 Timer 保持 Pausable。传染是个特性，关键是把它设在正确的分支上。",[10,7375,7376,7092,7378,2563,7381,7384,7385,2303,7388,2411,7391,2303],{},[14,7377,7098],{},[14,7379,7380],{},"Panel",[14,7382,7383],{},"PauseMenu","，里面两个 ",[14,7386,7387],{},"Button",[14,7389,7390],{},"ResumeButton",[14,7392,7393],{},"RestartButton",[96,7395,7397],{"className":98,"code":7396,"language":100,"meta":101,"style":101},"@onready var pause_menu: Panel = $PauseMenu\n@onready var resume_button: Button = $PauseMenu\u002FHBoxContainer\u002FResumeButton\n\nfunc _unhandled_input(event: InputEvent) -> void:\n    if not event.is_action_pressed(\"pause\"):\n        return\n    match GameManager.current_state:\n        GameManager.GameState.PLAYING:\n            get_tree().paused = true\n            GameManager.set_state(GameManager.GameState.PAUSED)\n        GameManager.GameState.PAUSED:\n            get_tree().paused = false\n            GameManager.set_state(GameManager.GameState.PLAYING)\n\nfunc _on_state_changed(new_state: GameManager.GameState) -> void:\n    pause_menu.visible = (new_state == GameManager.GameState.PAUSED)\n    if pause_menu.visible:\n        resume_button.grab_focus()           # 手柄：面板一出现，焦点就给\"继续\"\n\nfunc _on_resume_button_pressed() -> void:\n    get_tree().paused = false\n    GameManager.set_state(GameManager.GameState.PLAYING)\n\nfunc _on_restart_button_pressed() -> void:\n    get_tree().paused = false            # ⚠️ 先解冻，再重载\n    get_tree().reload_current_scene()\n",[14,7398,7399,7420,7441,7445,7468,7489,7493,7506,7521,7538,7562,7576,7591,7613,7617,7643,7673,7685,7699,7703,7718,7732,7754,7758,7773,7790],{"__ignoreMap":101},[105,7400,7401,7403,7405,7408,7410,7413,7415,7417],{"class":107,"line":108},[105,7402,1463],{"class":136},[105,7404,1466],{"class":154},[105,7406,7407],{"class":270}," pause_menu",[105,7409,167],{"class":140},[105,7411,7412],{"class":115}," Panel",[105,7414,515],{"class":140},[105,7416,1479],{"class":111},[105,7418,7419],{"class":988},"PauseMenu\n",[105,7421,7422,7424,7426,7429,7431,7434,7436,7438],{"class":107,"line":119},[105,7423,1463],{"class":136},[105,7425,1466],{"class":154},[105,7427,7428],{"class":270}," resume_button",[105,7430,167],{"class":140},[105,7432,7433],{"class":115}," Button",[105,7435,515],{"class":140},[105,7437,1479],{"class":111},[105,7439,7440],{"class":988},"PauseMenu\u002FHBoxContainer\u002FResumeButton\n",[105,7442,7443],{"class":107,"line":126},[105,7444,130],{"emptyLinePlaceholder":129},[105,7446,7447,7449,7452,7454,7456,7458,7460,7462,7464,7466],{"class":107,"line":133},[105,7448,391],{"class":154},[105,7450,7451],{"class":136}," _unhandled_input",[105,7453,141],{"class":140},[105,7455,3712],{"class":163},[105,7457,167],{"class":140},[105,7459,3717],{"class":115},[105,7461,173],{"class":140},[105,7463,400],{"class":154},[105,7465,403],{"class":115},[105,7467,406],{"class":140},[105,7469,7470,7472,7474,7476,7478,7480,7482,7485,7487],{"class":107,"line":151},[105,7471,682],{"class":111},[105,7473,1289],{"class":154},[105,7475,3732],{"class":270},[105,7477,306],{"class":163},[105,7479,3737],{"class":136},[105,7481,141],{"class":140},[105,7483,7484],{"class":144},"\"pause\"",[105,7486,173],{"class":140},[105,7488,406],{"class":163},[105,7490,7491],{"class":107,"line":179},[105,7492,864],{"class":111},[105,7494,7495,7498,7500,7502,7504],{"class":107,"line":184},[105,7496,7497],{"class":111},"    match",[105,7499,810],{"class":115},[105,7501,306],{"class":140},[105,7503,815],{"class":270},[105,7505,406],{"class":163},[105,7507,7508,7511,7513,7515,7517,7519],{"class":107,"line":195},[105,7509,7510],{"class":115},"        GameManager",[105,7512,306],{"class":140},[105,7514,670],{"class":270},[105,7516,306],{"class":140},[105,7518,277],{"class":111},[105,7520,406],{"class":163},[105,7522,7523,7526,7528,7530,7533,7535],{"class":107,"line":340},[105,7524,7525],{"class":136},"            get_tree",[105,7527,397],{"class":140},[105,7529,306],{"class":163},[105,7531,7532],{"class":270},"paused",[105,7534,515],{"class":140},[105,7536,7537],{"class":111}," true\n",[105,7539,7540,7543,7545,7548,7550,7552,7554,7556,7558,7560],{"class":107,"line":345},[105,7541,7542],{"class":115},"            GameManager",[105,7544,306],{"class":163},[105,7546,7547],{"class":136},"set_state",[105,7549,141],{"class":140},[105,7551,70],{"class":115},[105,7553,306],{"class":140},[105,7555,670],{"class":270},[105,7557,306],{"class":140},[105,7559,282],{"class":111},[105,7561,148],{"class":140},[105,7563,7564,7566,7568,7570,7572,7574],{"class":107,"line":364},[105,7565,7510],{"class":115},[105,7567,306],{"class":140},[105,7569,670],{"class":270},[105,7571,306],{"class":140},[105,7573,282],{"class":111},[105,7575,406],{"class":163},[105,7577,7578,7580,7582,7584,7586,7588],{"class":107,"line":383},[105,7579,7525],{"class":136},[105,7581,397],{"class":140},[105,7583,306],{"class":163},[105,7585,7532],{"class":270},[105,7587,515],{"class":140},[105,7589,7590],{"class":111}," false\n",[105,7592,7593,7595,7597,7599,7601,7603,7605,7607,7609,7611],{"class":107,"line":388},[105,7594,7542],{"class":115},[105,7596,306],{"class":163},[105,7598,7547],{"class":136},[105,7600,141],{"class":140},[105,7602,70],{"class":115},[105,7604,306],{"class":140},[105,7606,670],{"class":270},[105,7608,306],{"class":140},[105,7610,277],{"class":111},[105,7612,148],{"class":140},[105,7614,7615],{"class":107,"line":409},[105,7616,130],{"emptyLinePlaceholder":129},[105,7618,7619,7621,7623,7625,7627,7629,7631,7633,7635,7637,7639,7641],{"class":107,"line":425},[105,7620,391],{"class":154},[105,7622,926],{"class":136},[105,7624,141],{"class":140},[105,7626,374],{"class":163},[105,7628,167],{"class":140},[105,7630,810],{"class":115},[105,7632,306],{"class":140},[105,7634,670],{"class":270},[105,7636,173],{"class":140},[105,7638,400],{"class":154},[105,7640,403],{"class":115},[105,7642,406],{"class":140},[105,7644,7645,7648,7650,7652,7654,7657,7659,7661,7663,7665,7667,7669,7671],{"class":107,"line":450},[105,7646,7647],{"class":270},"    pause_menu",[105,7649,306],{"class":140},[105,7651,3390],{"class":270},[105,7653,515],{"class":140},[105,7655,7656],{"class":140}," (",[105,7658,374],{"class":270},[105,7660,956],{"class":154},[105,7662,810],{"class":115},[105,7664,306],{"class":163},[105,7666,670],{"class":115},[105,7668,306],{"class":163},[105,7670,282],{"class":270},[105,7672,148],{"class":140},[105,7674,7675,7677,7679,7681,7683],{"class":107,"line":474},[105,7676,682],{"class":111},[105,7678,7407],{"class":270},[105,7680,306],{"class":140},[105,7682,3390],{"class":270},[105,7684,406],{"class":163},[105,7686,7687,7690,7692,7694,7696],{"class":107,"line":479},[105,7688,7689],{"class":270},"        resume_button",[105,7691,306],{"class":163},[105,7693,6498],{"class":136},[105,7695,397],{"class":140},[105,7697,7698],{"class":122},"           # 手柄：面板一出现，焦点就给\"继续\"\n",[105,7700,7701],{"class":107,"line":485},[105,7702,130],{"emptyLinePlaceholder":129},[105,7704,7705,7707,7710,7712,7714,7716],{"class":107,"line":509},[105,7706,391],{"class":154},[105,7708,7709],{"class":136}," _on_resume_button_pressed",[105,7711,397],{"class":140},[105,7713,400],{"class":154},[105,7715,403],{"class":115},[105,7717,406],{"class":140},[105,7719,7720,7722,7724,7726,7728,7730],{"class":107,"line":521},[105,7721,6548],{"class":136},[105,7723,397],{"class":140},[105,7725,306],{"class":163},[105,7727,7532],{"class":270},[105,7729,515],{"class":140},[105,7731,7590],{"class":111},[105,7733,7734,7736,7738,7740,7742,7744,7746,7748,7750,7752],{"class":107,"line":538},[105,7735,1392],{"class":115},[105,7737,306],{"class":163},[105,7739,7547],{"class":136},[105,7741,141],{"class":140},[105,7743,70],{"class":115},[105,7745,306],{"class":140},[105,7747,670],{"class":270},[105,7749,306],{"class":140},[105,7751,277],{"class":111},[105,7753,148],{"class":140},[105,7755,7756],{"class":107,"line":543},[105,7757,130],{"emptyLinePlaceholder":129},[105,7759,7760,7762,7765,7767,7769,7771],{"class":107,"line":567},[105,7761,391],{"class":154},[105,7763,7764],{"class":136}," _on_restart_button_pressed",[105,7766,397],{"class":140},[105,7768,400],{"class":154},[105,7770,403],{"class":115},[105,7772,406],{"class":140},[105,7774,7775,7777,7779,7781,7783,7785,7787],{"class":107,"line":579},[105,7776,6548],{"class":136},[105,7778,397],{"class":140},[105,7780,306],{"class":163},[105,7782,7532],{"class":270},[105,7784,515],{"class":140},[105,7786,6371],{"class":111},[105,7788,7789],{"class":122},"            # ⚠️ 先解冻，再重载\n",[105,7791,7792,7794,7796,7798,7801],{"class":107,"line":596},[105,7793,6548],{"class":136},[105,7795,397],{"class":140},[105,7797,306],{"class":163},[105,7799,7800],{"class":136},"reload_current_scene",[105,7802,859],{"class":140},[10,7804,7805],{},"三个\"为什么\"：",[720,7807,7808,7830,7841],{},[723,7809,7810,7819,7820,7822,7823,7826,7827,7829],{},[22,7811,7812,7813,7816,7817],{},"Esc 的监听为什么写在 ",[14,7814,7815],{},"ui.gd"," 而不是 ",[14,7818,16],{},"：暂停时 ",[14,7821,2566],{}," 是 Pausable，它的 ",[14,7824,7825],{},"_input"," 也会停，写在那里按 Esc 就出不来了。",[14,7828,7098],{}," 是 Always，暂停时它还醒着",[723,7831,7832,2303,7838,7840],{},[22,7833,7834,7835],{},"重开前为什么要先 ",[14,7836,7837],{},"paused = false",[14,7839,7532],{}," 是场景树的属性，不属于任何场景。重载场景时它不会自动复位，你会得到一个一开局就冻住的新场景。这是暂停菜单第二常见的坑",[723,7842,7843,7853],{},[22,7844,7845,7848,7849,7852],{},[14,7846,7847],{},"grab_focus()"," 为什么写在 ",[14,7850,7851],{},"visible = true"," 之后","：隐藏的控件拿不到焦点。先显示，再给焦点，顺序反了手柄就按不动",[91,7855,7857,216,7860,7862],{"id":7856},"_unhandled_input-和-_input一封信的三站",[14,7858,7859],{},"_unhandled_input",[14,7861,7825],{},"：一封信的三站",[10,7864,7865],{},"一个按键事件在 Godot 里按固定顺序传递：",[7867,7868,7869,7875,7885],"ol",{},[723,7870,7871,7874],{},[14,7872,7873],{},"_input()"," —— 所有节点最先看到，相当于门口保安先把每封信都瞧一眼",[723,7876,7877,7880,7881,7884],{},[22,7878,7879],{},"UI 控件"," —— 有焦点的按钮、文本框。",[22,7882,7883],{},"是它要的键就收下（\"吃掉\"），后面的站就收不到了","：Enter \u002F 空格 \u002F 手柄 A 给有焦点的按钮，字母给正在输入的文本框",[723,7886,7887,7890],{},[14,7888,7889],{},"_unhandled_input()"," —— 最后一站，只收到 ② 没要的，相当于没人认领的信送到总务处",[10,7892,7893,7894,7897],{},"例子：暂停面板开着，焦点在\"继续\"上。玩家按 Enter → ② 按钮要这个键，吃掉，触发 pressed，③ 什么都没收到。玩家按 Esc → 没有控件要 Esc → 掉到 ③ → 我们的暂停切换。",[22,7895,7896],{},"所以游戏逻辑的按键写在 ③","：让 UI 先挑，剩下的才是游戏的。写在 ① 会出现\"按一下空格，按钮和游戏动作同时触发\"的双响。",[10,7899,7900],{},"第 4 章的方向键用的是 ①，没出事是因为暂停时有 PLAYING 守卫挡着；规矩上游戏按键都该用 ③。",[91,7902,7904],{"id":7903},"结算面板-回菜单","结算面板 + 回菜单",[10,7906,7907,7909,7910,7099,7913,7916,7917,2411,7920,2303],{},[14,7908,7098],{}," 下再加 ",[14,7911,7912],{},"GameOverPanel",[14,7914,7915],{},"FinalScore","（Label）、",[14,7918,7919],{},"GameOverRestartButton",[14,7921,7922],{},"MenuButton",[96,7924,7926],{"className":98,"code":7925,"language":100,"meta":101,"style":101},"    if new_state == GameManager.GameState.GAME_OVER:\n        game_over_panel.visible = true\n        final_score.text = \"Score %d   Best %d\" % [GameManager.total_score, GameManager.high_score]\n        game_over_restart_button.grab_focus()\n\nfunc _on_menu_button_pressed() -> void:\n    get_tree().paused = false\n    get_tree().change_scene_to_file(\"res:\u002F\u002Fscenes\u002Fmenu.tscn\")\n",[14,7927,7928,7948,7961,8005,8016,8020,8035,8049],{"__ignoreMap":101},[105,7929,7930,7932,7934,7936,7938,7940,7942,7944,7946],{"class":107,"line":108},[105,7931,682],{"class":111},[105,7933,953],{"class":270},[105,7935,956],{"class":154},[105,7937,810],{"class":115},[105,7939,306],{"class":140},[105,7941,670],{"class":270},[105,7943,306],{"class":140},[105,7945,287],{"class":111},[105,7947,406],{"class":163},[105,7949,7950,7953,7955,7957,7959],{"class":107,"line":119},[105,7951,7952],{"class":270},"        game_over_panel",[105,7954,306],{"class":140},[105,7956,3390],{"class":270},[105,7958,515],{"class":140},[105,7960,7537],{"class":111},[105,7962,7963,7966,7968,7970,7972,7975,7978,7981,7983,7985,7987,7989,7991,7993,7995,7997,7999,8001,8003],{"class":107,"line":126},[105,7964,7965],{"class":270},"        final_score",[105,7967,306],{"class":140},[105,7969,2027],{"class":270},[105,7971,515],{"class":140},[105,7973,7974],{"class":144}," \"Score ",[105,7976,7977],{"class":988},"%d",[105,7979,7980],{"class":144},"   Best ",[105,7982,7977],{"class":988},[105,7984,1218],{"class":144},[105,7986,1221],{"class":154},[105,7988,982],{"class":140},[105,7990,70],{"class":115},[105,7992,306],{"class":163},[105,7994,591],{"class":270},[105,7996,274],{"class":163},[105,7998,70],{"class":115},[105,8000,306],{"class":163},[105,8002,1153],{"class":270},[105,8004,2802],{"class":140},[105,8006,8007,8010,8012,8014],{"class":107,"line":133},[105,8008,8009],{"class":270},"        game_over_restart_button",[105,8011,306],{"class":163},[105,8013,6498],{"class":136},[105,8015,859],{"class":140},[105,8017,8018],{"class":107,"line":151},[105,8019,130],{"emptyLinePlaceholder":129},[105,8021,8022,8024,8027,8029,8031,8033],{"class":107,"line":179},[105,8023,391],{"class":154},[105,8025,8026],{"class":136}," _on_menu_button_pressed",[105,8028,397],{"class":140},[105,8030,400],{"class":154},[105,8032,403],{"class":115},[105,8034,406],{"class":140},[105,8036,8037,8039,8041,8043,8045,8047],{"class":107,"line":184},[105,8038,6548],{"class":136},[105,8040,397],{"class":140},[105,8042,306],{"class":163},[105,8044,7532],{"class":270},[105,8046,515],{"class":140},[105,8048,7590],{"class":111},[105,8050,8051,8053,8055,8057,8059,8061,8064],{"class":107,"line":195},[105,8052,6548],{"class":136},[105,8054,397],{"class":140},[105,8056,306],{"class":163},[105,8058,6337],{"class":136},[105,8060,141],{"class":140},[105,8062,8063],{"class":144},"\"res:\u002F\u002Fscenes\u002Fmenu.tscn\"",[105,8065,148],{"class":140},[10,8067,8068,8069],{},"为什么要有\"回菜单\"？立项表说模式\"开局前选\"。没有回菜单的路，玩家想换模式只能关游戏重开。",[22,8070,8071],{},"每个\"进去\"的路都要有\"出来\"的路。",[10,8073,8074],{},[2129,8075],{"alt":8076,"src":8077},"08-pause-暂停","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F08-pause-%E6%9A%82%E5%81%9C.png",[10,8079,8080],{},"现在整条链通了：菜单 → 游戏 → 暂停\u002F继续 → 撞死 → 结算 → 重开 \u002F 回菜单。下一章拔掉鼠标，只用手柄再走一遍——你会发现按 A 没反应。",[1684,8082,8083],{},"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}",{"title":101,"searchDepth":119,"depth":119,"links":8085},[8086,8087,8088,8090],{"id":7086,"depth":119,"text":7087},{"id":7278,"depth":119,"text":7279},{"id":7856,"depth":119,"text":8089},"_unhandled_input 和 _input：一封信的三站",{"id":7903,"depth":119,"text":7904},"2026-09-08",{},"\u002Fdevlog\u002Fxggame-snake\u002F08-pause",{"title":7081,"description":1697},"devlog\u002Fxggame-snake\u002F08-pause","KPS4wU5QYAirHHdGb4SNzDeRPWUmumL_iI4litJtzIc",{"id":8098,"title":2158,"body":8099,"cover":1695,"date":8455,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":8456,"navigation":129,"path":8457,"seo":8458,"stem":8459,"toc":129,"__hash__":8460},"devlog\u002Fdevlog\u002Fxggame-snake\u002F09-gamepad.md",{"type":7,"value":8100,"toc":8449},[8101,8108,8111,8163,8167,8170,8198,8217,8224,8332,8377,8380,8384,8393,8396,8399,8436,8441,8446],[10,8102,8103,8104,8107],{},"为什么这一章单独列？因为\"我测过了\"十有八九是用鼠标测的。鼠标点哪个就是哪个，不需要\"当前选中\"这个概念；",[22,8105,8106],{},"手柄没有指针，界面必须自己记住\"现在轮到哪个按钮\"","——这就是焦点。鼠标能用、手柄不能用，是新手最常交出去的成品。Steam Deck 玩家打开你的游戏，第一个按钮按不动，就关了。",[91,8109,8110],{"id":8110},"焦点三条规矩",[26,8112,8113,8123],{},[29,8114,8115],{},[32,8116,8117,8120],{},[35,8118,8119],{},"规矩",[35,8121,8122],{},"为什么",[45,8124,8125,8138,8153],{},[32,8126,8127,8135],{},[50,8128,8129],{},[22,8130,8131,8132,8134],{},"① 每个面板出现时，",[14,8133,7847],{}," 给它的第一个按钮",[50,8136,8137],{},"不给，手柄按 A 是按在\"空气\"上。第 7、8 章各有一行就是干这个的",[32,8139,8140,8150],{},[50,8141,8142],{},[22,8143,8144,8145,8147,8148],{},"② 先 ",[14,8146,7851],{},"，再 ",[14,8149,7847],{},[50,8151,8152],{},"隐藏的控件拿不到焦点，顺序反了等于没写",[32,8154,8155,8160],{},[50,8156,8157],{},[22,8158,8159],{},"③ 同一个容器里的控件，十字键自动上下跳",[50,8161,8162],{},"Godot 自带；跨面板不会自动跳，所以每个面板都要自己给一次焦点",[91,8164,8166],{"id":8165},"十字键能移亮框按-a-却按不下按钮","十字键能移亮框，按 A 却按不下按钮",[10,8168,8169],{},"我当时的现象：菜单里十字键上下能在控件之间跳，亮框也在动，但按 A 什么都不发生，键盘 Enter 却可以。",[10,8171,8172,8173,8175,8176,8179,8180,8183,8184,8187,8188,8191,8192,8194,8195,8197],{},"按钮响应手柄，靠的是 Godot 内置动作 ",[14,8174,2232],{},"。去源码里查了一下（",[14,8177,8178],{},"core\u002Finput\u002Finput_map.cpp","）：",[22,8181,8182],{},"Godot 4 默认只给它绑了 Enter、小键盘 Enter、空格，没有手柄键","。方向类的 ",[14,8185,8186],{},"ui_up \u002F ui_down \u002F ui_left \u002F ui_right"," 有十字键和左摇杆，",[14,8189,8190],{},"ui_select"," 有手柄 Y，但确认（",[14,8193,2232],{},"）和取消（",[14,8196,2248],{},"）都没有。所以十字键能移亮框、A 按不下去，是引擎默认值就这样。",[10,8199,8200,8201,8203,8204,8207,8208,8203,8210,8212,8213,8216],{},"修法：项目设置 → 输入映射 → 勾「显示内置动作」→ ",[14,8202,2232],{}," → ",[14,8205,8206],{},"+"," → 按手柄 A；",[14,8209,2248],{},[14,8211,8206],{}," → 按 B。没有\"一键给所有内置动作加手柄\"的开关，但需要补的也只有这两个，而且存在 ",[14,8214,8215],{},"project.godot"," 里，以后抽模板项目会跟着走。",[10,8218,8219,8220,8223],{},"补完还不行？在 ",[14,8221,8222],{},"menu.gd"," 里加探针：",[96,8225,8227],{"className":98,"code":8226,"language":100,"meta":101,"style":101},"func _input(event: InputEvent) -> void:\n    if event is InputEventJoypadButton and event.pressed:\n        print(\"手柄 \", Input.get_joy_name(0), \" | 按钮编号 \", event.button_index, \" | 认成确认键=\", event.is_action_pressed(\"ui_accept\"))\n",[14,8228,8229,8251,8274],{"__ignoreMap":101},[105,8230,8231,8233,8235,8237,8239,8241,8243,8245,8247,8249],{"class":107,"line":108},[105,8232,391],{"class":154},[105,8234,3707],{"class":136},[105,8236,141],{"class":140},[105,8238,3712],{"class":163},[105,8240,167],{"class":140},[105,8242,3717],{"class":115},[105,8244,173],{"class":140},[105,8246,400],{"class":154},[105,8248,403],{"class":115},[105,8250,406],{"class":140},[105,8252,8253,8255,8257,8260,8263,8265,8267,8269,8272],{"class":107,"line":119},[105,8254,682],{"class":111},[105,8256,3732],{"class":270},[105,8258,8259],{"class":111}," is",[105,8261,8262],{"class":115}," InputEventJoypadButton",[105,8264,3747],{"class":154},[105,8266,3732],{"class":270},[105,8268,306],{"class":140},[105,8270,8271],{"class":270},"pressed",[105,8273,406],{"class":163},[105,8275,8276,8278,8280,8283,8285,8288,8290,8293,8295,8297,8300,8303,8305,8307,8309,8312,8314,8317,8319,8321,8323,8325,8327,8330],{"class":107,"line":126},[105,8277,4205],{"class":136},[105,8279,141],{"class":140},[105,8281,8282],{"class":144},"\"手柄 \"",[105,8284,1180],{"class":140},[105,8286,8287],{"class":115}," Input",[105,8289,306],{"class":163},[105,8291,8292],{"class":136},"get_joy_name",[105,8294,141],{"class":140},[105,8296,3069],{"class":325},[105,8298,8299],{"class":140},"),",[105,8301,8302],{"class":144}," \" | 按钮编号 \"",[105,8304,1180],{"class":140},[105,8306,3732],{"class":270},[105,8308,306],{"class":140},[105,8310,8311],{"class":270},"button_index",[105,8313,1180],{"class":140},[105,8315,8316],{"class":144}," \" | 认成确认键=\"",[105,8318,1180],{"class":140},[105,8320,3732],{"class":270},[105,8322,306],{"class":163},[105,8324,3737],{"class":136},[105,8326,141],{"class":140},[105,8328,8329],{"class":144},"\"ui_accept\"",[105,8331,1263],{"class":140},[26,8333,8334,8343],{},[29,8335,8336],{},[32,8337,8338,8341],{},[35,8339,8340],{},"按 A 时打印",[35,8342,7313],{},[45,8344,8345,8356,8369],{},[32,8346,8347,8353],{},[50,8348,8349,8350],{},"有编号，",[14,8351,8352],{},"=true",[50,8354,8355],{},"已经认成确认键了，问题在焦点——回去看亮框在不在按钮上",[32,8357,8358,8363],{},[50,8359,8349,8360],{},[14,8361,8362],{},"=false",[50,8364,8365,8366,8368],{},"这个键还没加进 ",[14,8367,2232],{},"（或加的时候按错了键）",[32,8370,8371,8374],{},[50,8372,8373],{},"什么都不打印",[50,8375,8376],{},"面键事件没进 Godot：关掉 Steam 再试（Steam 会接管手柄）",[10,8378,8379],{},"B \u002F X \u002F Y \"没反应\"是正常的——B 只对应\"取消\"，X、Y 默认没绑任何 UI 动作。",[91,8381,8383],{"id":8382},"另一个和手柄无关但害我半小时的开关","另一个和手柄无关、但害我半小时的开关",[10,8385,8386,8387,8389,8390,8392],{},"Godot 4.4 起游戏默认",[22,8388,3958],{},"跑。「游戏」标签的工具栏有三个模式：",[22,8391,3962],{},"。只有「输入」模式按键才进游戏；2D \u002F 3D 是运行时用鼠标点选节点的，会把输入截走。代码没错的时候，问题十有八九在\"事件有没有送到\"，先看这个开关。",[91,8394,8395],{"id":8395},"手柄专项验收",[10,8397,8398],{},"把鼠标推开，只用手柄走完整条链，每一步都看清楚亮框在哪：",[720,8400,8403,8412,8418,8424,8430],{"className":8401},[8402],"contains-task-list",[723,8404,8407,8411],{"className":8405},[8406],"task-list-item",[8408,8409],"input",{"disabled":129,"type":8410},"checkbox"," 菜单：十字键能在选项和 Start 之间跳；A 能切换选项",[723,8413,8415,8417],{"className":8414},[8406],[8408,8416],{"disabled":129,"type":8410}," Start → 进游戏，十字键 \u002F 摇杆能转弯",[723,8419,8421,8423],{"className":8420},[8406],[8408,8422],{"disabled":129,"type":8410}," Start 键 → 暂停面板出现，\"继续\"亮框；A → 继续",[723,8425,8427,8429],{"className":8426},[8406],[8408,8428],{"disabled":129,"type":8410}," 再暂停 → 下到\"重开\" → A → 新一局，蛇在动",[723,8431,8433,8435],{"className":8432},[8406],[8408,8434],{"disabled":129,"type":8410}," 撞死 → 结算面板，\"重开\"亮框；下到 Menu → A → 回到菜单，焦点在选项上",[10,8437,8438,8439,1667],{},"五条全过，这个游戏就是 Steam Deck 可玩的。有一条卡住 = 那个面板缺 ",[14,8440,7847],{},[1984,8442,8443],{},[10,8444,8445],{},"这条清单以后每个游戏都要跑。手柄不是一个功能，是一种假设——第一天就绑，最后补要把所有 UI 的焦点重走一遍。",[1684,8447,8448],{},"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 .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}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":101,"searchDepth":119,"depth":119,"links":8450},[8451,8452,8453,8454],{"id":8110,"depth":119,"text":8110},{"id":8165,"depth":119,"text":8166},{"id":8382,"depth":119,"text":8383},{"id":8395,"depth":119,"text":8395},"2026-09-09",{},"\u002Fdevlog\u002Fxggame-snake\u002F09-gamepad",{"title":2158,"description":1697},"devlog\u002Fxggame-snake\u002F09-gamepad","ODux3tSV1z2aFWa-UTs_47JEfak-BN2jGi0oxme1LDM",{"id":8462,"title":8463,"body":8464,"cover":1695,"date":9650,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":9651,"navigation":129,"path":9652,"seo":9653,"stem":9654,"toc":129,"__hash__":9655},"devlog\u002Fdevlog\u002Fxggame-snake\u002F10-ui-theme.md","UI：皮、总表、左右选",{"type":7,"value":8465,"toc":9644},[8466,8473,8478,8482,8497,8514,8601,8611,8630,8634,8641,8737,8744,8750,8754,8761,8796,8807,8813,8827,8831,8838,8844,8851,9479,9523,9532,9621,9632,9638,9641],[10,8467,8468,8469,8472],{},"贪吃蛇没有游戏画面要画（纯色方块就是它的经典视觉语言），美术的功夫全花在 UI 上——这反而最值得练：",[22,8470,8471],{},"UI 是每个游戏都要的，游戏画面每个游戏都不一样。"," 而且这一章一张图都不用找：圆角、边框、渐变全是 Godot 自己画的。",[1984,8474,8475],{},[10,8476,8477],{},"写法约定：菜单路径写中文；属性名照检查器写英文，括号里是中文意思。",[91,8479,8481],{"id":8480},"皮styleboxflat","皮：StyleBoxFlat",[10,8483,8484,8485,8488,8489,8492,8493,8496],{},"控件的\"皮\"叫 ",[22,8486,8487],{},"StyleBox","。Godot 画按钮时先画皮，再在皮上写字。",[14,8490,8491],{},"StyleBoxFlat"," 是纯几何画的皮，",[14,8494,8495],{},"StyleBoxTexture"," 是用图片当皮。",[10,8498,8499,8500,8503,8504,8506,8507,8510,8511,2303],{},"文件系统 → ",[14,8501,8502],{},"theme\u002F"," 右键 → 新建资源 → 搜 ",[14,8505,8491],{},"（",[22,8508,8509],{},"要点一下让它高亮","，只搜不点建出来的是空白 Resource）→ ",[14,8512,8513],{},"btn_normal.tres",[26,8515,8516,8529],{},[29,8517,8518],{},[32,8519,8520,8523,8526],{},[35,8521,8522],{},"检查器里的名字",[35,8524,8525],{},"中文",[35,8527,8528],{},"填",[45,8530,8531,8542,8553,8566,8577,8588],{},[32,8532,8533,8536,8539],{},[50,8534,8535],{},"Draw Center",[50,8537,8538],{},"画填充",[50,8540,8541],{},"取消（空心）",[32,8543,8544,8547,8550],{},[50,8545,8546],{},"Border Width",[50,8548,8549],{},"边框粗细",[50,8551,8552],{},"四边 2",[32,8554,8555,8558,8561],{},[50,8556,8557],{},"Border Color",[50,8559,8560],{},"边框色",[50,8562,8563],{},[14,8564,8565],{},"#3a4064",[32,8567,8568,8571,8574],{},[50,8569,8570],{},"Corner Radius",[50,8572,8573],{},"圆角",[50,8575,8576],{},"四角 12",[32,8578,8579,8582,8585],{},[50,8580,8581],{},"Content Margins",[50,8583,8584],{},"内边距（文字离框多远）",[50,8586,8587],{},"左右 24，上下 12",[32,8589,8590,8593,8596],{},[50,8591,8592],{},"Expand Margins",[50,8594,8595],{},"外扩",[50,8597,8598],{},[22,8599,8600],{},"全部保持 0",[10,8602,8603,8606,8607,8610],{},[22,8604,8605],{},"Content Margins 和 Expand Margins 长得像，意思相反","：Content 是内边距，框会变大；Expand 是把皮画到框",[22,8608,8609],{},"外面","，框不变、画面溢出、点击区比看到的小、相邻按钮互相压。我第一次填错了这一排，然后发现\"按钮间隔比设的小\"，把容器间距调到 50 去补——补错了地方。",[10,8612,8613,8614,8617,8618,8621,8622,8625,8626,8629],{},"然后右键复制出其余的皮：",[14,8615,8616],{},"btn_hover","（边框换强调色）、",[14,8619,8620],{},"btn_pressed","（实心强调色，无边）、",[14,8623,8624],{},"btn_hover_pressed","（实心再亮一点）、",[14,8627,8628],{},"focus","（空心，白边 3）。为什么没选中是空心、选中是实心？人眼对\"有没有填充\"比\"填的什么颜色\"敏感得多。两态都实心只换底色，反差永远不够。",[91,8631,8633],{"id":8632},"按钮的-11-个槽","按钮的 11 个槽",[10,8635,8636,8637,8640],{},"选一个按钮 → 检查器 → ",[22,8638,8639],{},"Theme Overrides（主题覆盖）→ Styles","，11 个槽各是什么：",[26,8642,8643,8656],{},[29,8644,8645],{},[32,8646,8647,8650,8653],{},[35,8648,8649],{},"槽",[35,8651,8652],{},"什么时候显示",[35,8654,8655],{},"放哪张皮",[45,8657,8658,8671,8683,8698,8710,8726],{},[32,8659,8660,8663,8666],{},[50,8661,8662],{},"Normal",[50,8664,8665],{},"平时",[50,8667,8668],{},[14,8669,8670],{},"btn_normal",[32,8672,8673,8676,8679],{},[50,8674,8675],{},"Hover",[50,8677,8678],{},"鼠标停在上面",[50,8680,8681],{},[14,8682,8616],{},[32,8684,8685,8688,8694],{},[50,8686,8687],{},"Pressed",[50,8689,8690,8691],{},"鼠标按住；",[22,8692,8693],{},"开关按钮被选中时一直是它",[50,8695,8696],{},[14,8697,8620],{},[32,8699,8700,8703,8706],{},[50,8701,8702],{},"Hover Pressed",[50,8704,8705],{},"已选中的按钮被鼠标停住",[50,8707,8708],{},[14,8709,8624],{},[32,8711,8712,8715,8722],{},[50,8713,8714],{},"Focus",[50,8716,8717,8718,8721],{},"手柄 \u002F 键盘选到它，",[22,8719,8720],{},"叠加","在上面四种之上",[50,8723,8724],{},[14,8725,8628],{},[32,8727,8728,8731,8734],{},[50,8729,8730],{},"Disabled、5 个 * Mirrored",[50,8732,8733],{},"禁用；从右往左的语言",[50,8735,8736],{},"不用",[10,8738,8739,8740,8743],{},"两个细节：Godot 没有单独的\"选中\"槽，开关按钮被按下后就一直处于\"按下\"状态，所以选中态就是 Pressed。Focus 不算一种状态——一个选中的按钮同时有焦点，画的是 Pressed 皮 + Focus 白边，所以 ",[14,8741,8742],{},"focus.tres"," 要\"不画中心、只画白边\"，才能叠在任何状态上。",[10,8745,8746],{},[2129,8747],{"alt":8748,"src":8749},"10-ui-槽位","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F10-ui-%E6%A7%BD%E4%BD%8D.png",[91,8751,8753],{"id":8752},"总表theme","总表：Theme",[10,8755,8756,8757,8760],{},"覆盖只属于那一个节点。菜单、暂停面板、结算面板几十个控件一个个贴？",[22,8758,8759],{},"Theme 是一张\"类型 → 样式\"的总表","：告诉 Godot\"凡是 Button 都长这样\"，全项目一次生效。",[10,8762,8763,8765,8766,8203,8769,8772,8773,8775,8776,8779,8780,2411,8782,3647,8785,8788,8789,8791,8792,8795],{},[14,8764,8502],{}," 右键 → 新建资源 → ",[14,8767,8768],{},"Theme",[14,8770,8771],{},"main.tres"," → 双击，底部出现主题编辑器 → 「+」添加类型 → ",[14,8774,7387],{}," → 每一项旁边点「+」才算加进总表：Styles 五个槽拖对应的 ",[14,8777,8778],{},".tres","，Colors 四个字色，Font Sizes。再加 ",[14,8781,7102],{},[14,8783,8784],{},"HBoxContainer",[14,8786,8787],{},"VBoxContainer"," 的 separation。选中 ",[14,8790,8771],{}," 本身，检查器里 ",[22,8793,8794],{},"Default Font"," 拖入字体文件——所有控件从此都用它。",[10,8797,8798,8799,8802,8803,8806],{},"最后 ",[22,8800,8801],{},"项目设置 → GUI → Theme → Custom"," 填这个文件。为什么装在项目设置里，不是拖到根节点上？Theme 沿 Control 父子链往下传，但 UI 挂在 CanvasLayer 下，",[22,8804,8805],{},"CanvasLayer 不是 Control","，链在这断了。项目级主题不走链，直接生效。",[10,8808,8809,8810],{},"回到那个按钮，把 Theme Overrides 全部右键恢复默认——它长得和刚才一模一样，只是皮来自总表了。F5，",[22,8811,8812],{},"暂停面板和结算面板的按钮一个字没改就换了皮。",[10,8814,8815,8816,8819,8820,8823,8824],{},"想要第二种长相（主按钮）？用",[22,8817,8818],{},"类型变体（Type Variation）","：主题编辑器里手动输入一个类型名如 ",[14,8821,8822],{},"PrimaryButton","、基类型选 Button、换几张皮，然后在那个节点的 Theme → Type Variation 填名字。",[22,8825,8826],{},"同一类控件的第二种长相 → 变体；只有这一个节点不一样 → 覆盖。",[91,8828,8830],{"id":8829},"左右选自己封装一个组件","左右选：自己封装一个组件",[10,8832,8833,8834,8837],{},"菜单里我不想用下拉（OptionButton 点开的列表是独立的 PopupMenu 窗口，字号改不动、手柄还要多按一层），要的是难度选择那种 ◀ 撞墙模式 ▶——游戏里最手柄友好的选项形式：一个焦点，左右键切值。Godot 没有文字版的左右选（",[14,8835,8836],{},"SpinBox"," 只能选数字），所以自己拼：",[96,8839,8842],{"className":8840,"code":8841,"language":2027},[2025],"OptionSelector (Button)          ← 根：能拿焦点、自动穿 Theme 的皮；Text 留空\n└── Row (HBoxContainer)          ← Anchors Preset = Full Rect；Mouse Filter = Pass\n    ├── LeftArrow (Button)       ← ◀；Flat；Focus Mode = None\n    ├── Value (Label)            ← 居中；Fill + Expand\n    └── RightArrow (Button)      ← ▶；Flat；Focus Mode = None\n",[14,8843,8841],{"__ignoreMap":101},[10,8845,8846,8847,8850],{},"根用 Button，因为它",[22,8848,8849],{},"能拿焦点、自动有 Theme 的悬停色和焦点白边","。两个箭头也是 Button，但 Focus Mode = None：能点，拿不到焦点——手柄玩家看到的永远是\"整个选项框亮着\"，而不是焦点在三个小东西之间跳。",[96,8852,8854],{"className":98,"code":8853,"language":100,"meta":101,"style":101},"extends Button\n## 左右选：◀ 值 ▶。手柄左右 \u002F 点箭头切换；按 A 或点中间 = 下一个\nsignal changed(index: int)\n\n@export var options: Array[String] = [\"选项 A\", \"选项 B\"]\n@export var index := 0\nvar _tween: Tween\n\nfunc _ready() -> void:\n    text = \"\"                                    # 根按钮不显示字，字由 Value 显示\n    %LeftArrow.pressed.connect(_step.bind(-1))\n    %RightArrow.pressed.connect(_step.bind(1))\n    pressed.connect(_step.bind(1))               # 点中间 \u002F 手柄 A = 下一个\n    %Value.text = options[index]\n\nfunc _gui_input(event: InputEvent) -> void:\n    if event.is_action_pressed(\"ui_left\"):\n        _step(-1)\n        accept_event()                           # 吃掉事件，不然焦点会跳到左边的控件\n    elif event.is_action_pressed(\"ui_right\"):\n        _step(1)\n        accept_event()\n\n## 往某个方向走一步；到头了绕回去\nfunc _step(dir: int) -> void:\n    index = wrapi(index + dir, 0, options.size())\n    %Value.text = options[index]\n    changed.emit(index)\n    _nudge(dir)\n\n## 文字往切换方向弹一下再回来\nfunc _nudge(dir: int) -> void:\n    if _tween:\n        _tween.kill()\n    %Value.offset_transform_enabled = true\n    %Value.offset_transform_position.x = 16 * dir\n    _tween = create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)\n    _tween.tween_property(%Value, \"offset_transform_position:x\", 0.0, 0.18)\n",[14,8855,8856,8863,8868,8886,8890,8925,8938,8950,8954,8968,8981,9015,9044,9070,9091,9095,9118,9137,9150,9160,9179,9189,9195,9199,9204,9228,9261,9281,9296,9307,9311,9316,9339,9347,9359,9374,9400,9446],{"__ignoreMap":101},[105,8857,8858,8860],{"class":107,"line":108},[105,8859,112],{"class":111},[105,8861,8862],{"class":115}," Button\n",[105,8864,8865],{"class":107,"line":119},[105,8866,8867],{"class":122},"## 左右选：◀ 值 ▶。手柄左右 \u002F 点箭头切换；按 A 或点中间 = 下一个\n",[105,8869,8870,8872,8875,8877,8880,8882,8884],{"class":107,"line":126},[105,8871,155],{"class":154},[105,8873,8874],{"class":136}," changed",[105,8876,141],{"class":140},[105,8878,8879],{"class":163},"index",[105,8881,167],{"class":140},[105,8883,170],{"class":115},[105,8885,148],{"class":140},[105,8887,8888],{"class":107,"line":133},[105,8889,130],{"emptyLinePlaceholder":129},[105,8891,8892,8895,8897,8900,8902,8904,8906,8909,8911,8913,8915,8918,8920,8923],{"class":107,"line":151},[105,8893,8894],{"class":136},"@export",[105,8896,1466],{"class":154},[105,8898,8899],{"class":270}," options",[105,8901,167],{"class":140},[105,8903,2723],{"class":115},[105,8905,2726],{"class":140},[105,8907,8908],{"class":115},"String",[105,8910,1013],{"class":140},[105,8912,515],{"class":140},[105,8914,982],{"class":140},[105,8916,8917],{"class":144},"\"选项 A\"",[105,8919,274],{"class":163},[105,8921,8922],{"class":144},"\"选项 B\"",[105,8924,2802],{"class":140},[105,8926,8927,8929,8931,8934,8936],{"class":107,"line":179},[105,8928,8894],{"class":136},[105,8930,1466],{"class":154},[105,8932,8933],{"class":270}," index",[105,8935,301],{"class":140},[105,8937,326],{"class":325},[105,8939,8940,8942,8945,8947],{"class":107,"line":184},[105,8941,295],{"class":154},[105,8943,8944],{"class":270}," _tween",[105,8946,167],{"class":140},[105,8948,8949],{"class":115}," Tween\n",[105,8951,8952],{"class":107,"line":195},[105,8953,130],{"emptyLinePlaceholder":129},[105,8955,8956,8958,8960,8962,8964,8966],{"class":107,"line":340},[105,8957,391],{"class":154},[105,8959,394],{"class":136},[105,8961,397],{"class":140},[105,8963,400],{"class":154},[105,8965,403],{"class":115},[105,8967,406],{"class":140},[105,8969,8970,8973,8975,8978],{"class":107,"line":345},[105,8971,8972],{"class":270},"    text",[105,8974,515],{"class":140},[105,8976,8977],{"class":144}," \"\"",[105,8979,8980],{"class":122},"                                    # 根按钮不显示字，字由 Value 显示\n",[105,8982,8983,8986,8989,8991,8993,8995,8997,8999,9002,9004,9007,9009,9011,9013],{"class":107,"line":364},[105,8984,8985],{"class":111},"    %",[105,8987,8988],{"class":988},"LeftArrow",[105,8990,306],{"class":140},[105,8992,8271],{"class":270},[105,8994,306],{"class":163},[105,8996,437],{"class":136},[105,8998,141],{"class":140},[105,9000,9001],{"class":270},"_step",[105,9003,306],{"class":163},[105,9005,9006],{"class":136},"bind",[105,9008,141],{"class":140},[105,9010,2689],{"class":154},[105,9012,886],{"class":325},[105,9014,1263],{"class":140},[105,9016,9017,9019,9022,9024,9026,9028,9030,9032,9034,9036,9038,9040,9042],{"class":107,"line":383},[105,9018,8985],{"class":111},[105,9020,9021],{"class":988},"RightArrow",[105,9023,306],{"class":140},[105,9025,8271],{"class":270},[105,9027,306],{"class":163},[105,9029,437],{"class":136},[105,9031,141],{"class":140},[105,9033,9001],{"class":270},[105,9035,306],{"class":163},[105,9037,9006],{"class":136},[105,9039,141],{"class":140},[105,9041,886],{"class":325},[105,9043,1263],{"class":140},[105,9045,9046,9049,9051,9053,9055,9057,9059,9061,9063,9065,9067],{"class":107,"line":388},[105,9047,9048],{"class":270},"    pressed",[105,9050,306],{"class":163},[105,9052,437],{"class":136},[105,9054,141],{"class":140},[105,9056,9001],{"class":270},[105,9058,306],{"class":163},[105,9060,9006],{"class":136},[105,9062,141],{"class":140},[105,9064,886],{"class":325},[105,9066,6849],{"class":140},[105,9068,9069],{"class":122},"               # 点中间 \u002F 手柄 A = 下一个\n",[105,9071,9072,9074,9077,9079,9081,9083,9085,9087,9089],{"class":107,"line":409},[105,9073,8985],{"class":111},[105,9075,9076],{"class":988},"Value",[105,9078,306],{"class":140},[105,9080,2027],{"class":270},[105,9082,515],{"class":140},[105,9084,8899],{"class":270},[105,9086,2726],{"class":140},[105,9088,8879],{"class":270},[105,9090,2802],{"class":140},[105,9092,9093],{"class":107,"line":425},[105,9094,130],{"emptyLinePlaceholder":129},[105,9096,9097,9099,9102,9104,9106,9108,9110,9112,9114,9116],{"class":107,"line":450},[105,9098,391],{"class":154},[105,9100,9101],{"class":136}," _gui_input",[105,9103,141],{"class":140},[105,9105,3712],{"class":163},[105,9107,167],{"class":140},[105,9109,3717],{"class":115},[105,9111,173],{"class":140},[105,9113,400],{"class":154},[105,9115,403],{"class":115},[105,9117,406],{"class":140},[105,9119,9120,9122,9124,9126,9128,9130,9133,9135],{"class":107,"line":474},[105,9121,682],{"class":111},[105,9123,3732],{"class":270},[105,9125,306],{"class":163},[105,9127,3737],{"class":136},[105,9129,141],{"class":140},[105,9131,9132],{"class":144},"\"ui_left\"",[105,9134,173],{"class":140},[105,9136,406],{"class":163},[105,9138,9139,9142,9144,9146,9148],{"class":107,"line":479},[105,9140,9141],{"class":136},"        _step",[105,9143,141],{"class":140},[105,9145,2689],{"class":154},[105,9147,886],{"class":325},[105,9149,148],{"class":140},[105,9151,9152,9155,9157],{"class":107,"line":485},[105,9153,9154],{"class":136},"        accept_event",[105,9156,397],{"class":140},[105,9158,9159],{"class":122},"                           # 吃掉事件，不然焦点会跳到左边的控件\n",[105,9161,9162,9164,9166,9168,9170,9172,9175,9177],{"class":107,"line":509},[105,9163,3779],{"class":111},[105,9165,3732],{"class":270},[105,9167,306],{"class":163},[105,9169,3737],{"class":136},[105,9171,141],{"class":140},[105,9173,9174],{"class":144},"\"ui_right\"",[105,9176,173],{"class":140},[105,9178,406],{"class":163},[105,9180,9181,9183,9185,9187],{"class":107,"line":521},[105,9182,9141],{"class":136},[105,9184,141],{"class":140},[105,9186,886],{"class":325},[105,9188,148],{"class":140},[105,9190,9191,9193],{"class":107,"line":538},[105,9192,9154],{"class":136},[105,9194,859],{"class":140},[105,9196,9197],{"class":107,"line":543},[105,9198,130],{"emptyLinePlaceholder":129},[105,9200,9201],{"class":107,"line":567},[105,9202,9203],{"class":122},"## 往某个方向走一步；到头了绕回去\n",[105,9205,9206,9208,9211,9213,9216,9218,9220,9222,9224,9226],{"class":107,"line":579},[105,9207,391],{"class":154},[105,9209,9210],{"class":136}," _step",[105,9212,141],{"class":140},[105,9214,9215],{"class":163},"dir",[105,9217,167],{"class":140},[105,9219,170],{"class":115},[105,9221,173],{"class":140},[105,9223,400],{"class":154},[105,9225,403],{"class":115},[105,9227,406],{"class":140},[105,9229,9230,9233,9235,9238,9240,9242,9244,9247,9249,9251,9253,9255,9257,9259],{"class":107,"line":596},[105,9231,9232],{"class":270},"    index",[105,9234,515],{"class":140},[105,9236,9237],{"class":136}," wrapi",[105,9239,141],{"class":140},[105,9241,8879],{"class":270},[105,9243,3074],{"class":154},[105,9245,9246],{"class":270}," dir",[105,9248,1180],{"class":140},[105,9250,1415],{"class":325},[105,9252,1180],{"class":140},[105,9254,8899],{"class":270},[105,9256,306],{"class":163},[105,9258,3412],{"class":136},[105,9260,1231],{"class":140},[105,9262,9263,9265,9267,9269,9271,9273,9275,9277,9279],{"class":107,"line":601},[105,9264,8985],{"class":111},[105,9266,9076],{"class":988},[105,9268,306],{"class":140},[105,9270,2027],{"class":270},[105,9272,515],{"class":140},[105,9274,8899],{"class":270},[105,9276,2726],{"class":140},[105,9278,8879],{"class":270},[105,9280,2802],{"class":140},[105,9282,9283,9286,9288,9290,9292,9294],{"class":107,"line":617},[105,9284,9285],{"class":270},"    changed",[105,9287,306],{"class":163},[105,9289,529],{"class":136},[105,9291,141],{"class":140},[105,9293,8879],{"class":270},[105,9295,148],{"class":140},[105,9297,9298,9301,9303,9305],{"class":107,"line":626},[105,9299,9300],{"class":136},"    _nudge",[105,9302,141],{"class":140},[105,9304,9215],{"class":270},[105,9306,148],{"class":140},[105,9308,9309],{"class":107,"line":641},[105,9310,130],{"emptyLinePlaceholder":129},[105,9312,9313],{"class":107,"line":646},[105,9314,9315],{"class":122},"## 文字往切换方向弹一下再回来\n",[105,9317,9318,9320,9323,9325,9327,9329,9331,9333,9335,9337],{"class":107,"line":662},[105,9319,391],{"class":154},[105,9321,9322],{"class":136}," _nudge",[105,9324,141],{"class":140},[105,9326,9215],{"class":163},[105,9328,167],{"class":140},[105,9330,170],{"class":115},[105,9332,173],{"class":140},[105,9334,400],{"class":154},[105,9336,403],{"class":115},[105,9338,406],{"class":140},[105,9340,9341,9343,9345],{"class":107,"line":679},[105,9342,682],{"class":111},[105,9344,8944],{"class":270},[105,9346,406],{"class":163},[105,9348,9349,9352,9354,9357],{"class":107,"line":694},[105,9350,9351],{"class":270},"        _tween",[105,9353,306],{"class":163},[105,9355,9356],{"class":136},"kill",[105,9358,859],{"class":140},[105,9360,9361,9363,9365,9367,9370,9372],{"class":107,"line":705},[105,9362,8985],{"class":111},[105,9364,9076],{"class":988},[105,9366,306],{"class":140},[105,9368,9369],{"class":270},"offset_transform_enabled",[105,9371,515],{"class":140},[105,9373,7537],{"class":111},[105,9375,9377,9379,9381,9383,9386,9388,9390,9392,9395,9397],{"class":107,"line":9376},36,[105,9378,8985],{"class":111},[105,9380,9076],{"class":988},[105,9382,306],{"class":140},[105,9384,9385],{"class":270},"offset_transform_position",[105,9387,306],{"class":140},[105,9389,4044],{"class":270},[105,9391,515],{"class":140},[105,9393,9394],{"class":325}," 16",[105,9396,3455],{"class":154},[105,9398,9399],{"class":270}," dir\n",[105,9401,9403,9406,9408,9411,9413,9415,9418,9420,9423,9425,9428,9430,9432,9435,9437,9439,9441,9444],{"class":107,"line":9402},37,[105,9404,9405],{"class":270},"    _tween",[105,9407,515],{"class":140},[105,9409,9410],{"class":136}," create_tween",[105,9412,397],{"class":140},[105,9414,306],{"class":163},[105,9416,9417],{"class":136},"set_trans",[105,9419,141],{"class":140},[105,9421,9422],{"class":115},"Tween",[105,9424,306],{"class":140},[105,9426,9427],{"class":111},"TRANS_BACK",[105,9429,173],{"class":140},[105,9431,306],{"class":163},[105,9433,9434],{"class":136},"set_ease",[105,9436,141],{"class":140},[105,9438,9422],{"class":115},[105,9440,306],{"class":140},[105,9442,9443],{"class":111},"EASE_OUT",[105,9445,148],{"class":140},[105,9447,9449,9451,9453,9456,9458,9460,9462,9464,9467,9469,9472,9474,9477],{"class":107,"line":9448},38,[105,9450,9405],{"class":270},[105,9452,306],{"class":163},[105,9454,9455],{"class":136},"tween_property",[105,9457,141],{"class":140},[105,9459,7035],{"class":111},[105,9461,9076],{"class":988},[105,9463,1180],{"class":140},[105,9465,9466],{"class":144}," \"offset_transform_position:x\"",[105,9468,1180],{"class":140},[105,9470,9471],{"class":325}," 0.0",[105,9473,1180],{"class":140},[105,9475,9476],{"class":325}," 0.18",[105,9478,148],{"class":140},[720,9480,9481,9490,9500,9509],{},[723,9482,9483,9486,9487,9489],{},[14,9484,9485],{},"_gui_input","：只有拿到焦点的控件才会收到键盘 \u002F 手柄事件——和 ",[14,9488,7825],{}," 的\"全世界都听\"不同",[723,9491,9492,9495,9496,9499],{},[14,9493,9494],{},"accept_event()","：宣布\"这个事件我处理完了\"。不写，Godot 接着拿 ",[14,9497,9498],{},"ui_left"," 去做焦点导航——你按一下左，值切了、焦点也飞了",[723,9501,9502,9505,9506,9508],{},[14,9503,9504],{},"wrapi(x, 0, n)","：整数绕回，最后一个再往右 = 第一个。和第 7 章穿墙的 ",[14,9507,7031],{}," 是同一个思路",[723,9510,9511,9514,9515,9518,9519,9522],{},[14,9512,9513],{},"offset_transform_*","：Godot 4.7 新增的 Control 属性，视觉上挪、转、缩，",[22,9516,9517],{},"布局不受影响","（以前容器一重排就把你改的 scale 抹掉）。默认绕中心、默认只改画面不改鼠标判定——和 CSS 的 ",[14,9520,9521],{},"transform"," 一个意思",[10,9524,9525,9526,1042,9529,9531],{},"菜单里放一个实例，检查器 Options 填 ",[14,9527,9528],{},"[\"撞墙模式\", \"穿墙模式\"]",[14,9530,8222],{}," 缩成：",[96,9533,9535],{"className":98,"code":9534,"language":100,"meta":101,"style":101},"func _ready() -> void:\n    %ModeSelector.grab_focus()\n\nfunc _on_start_button_pressed() -> void:\n    GameManager.wrap_mode = %ModeSelector.index == 1\n    get_tree().change_scene_to_file(\"res:\u002F\u002Fscenes\u002Fmain.tscn\")\n",[14,9536,9537,9551,9564,9568,9582,9605],{"__ignoreMap":101},[105,9538,9539,9541,9543,9545,9547,9549],{"class":107,"line":108},[105,9540,391],{"class":154},[105,9542,394],{"class":136},[105,9544,397],{"class":140},[105,9546,400],{"class":154},[105,9548,403],{"class":115},[105,9550,406],{"class":140},[105,9552,9553,9555,9558,9560,9562],{"class":107,"line":119},[105,9554,8985],{"class":111},[105,9556,9557],{"class":988},"ModeSelector",[105,9559,306],{"class":163},[105,9561,6498],{"class":136},[105,9563,859],{"class":140},[105,9565,9566],{"class":107,"line":126},[105,9567,130],{"emptyLinePlaceholder":129},[105,9569,9570,9572,9574,9576,9578,9580],{"class":107,"line":133},[105,9571,391],{"class":154},[105,9573,6514],{"class":136},[105,9575,397],{"class":140},[105,9577,400],{"class":154},[105,9579,403],{"class":115},[105,9581,406],{"class":140},[105,9583,9584,9586,9588,9590,9592,9594,9596,9598,9600,9602],{"class":107,"line":151},[105,9585,1392],{"class":115},[105,9587,306],{"class":140},[105,9589,6531],{"class":270},[105,9591,515],{"class":140},[105,9593,1221],{"class":111},[105,9595,9557],{"class":988},[105,9597,306],{"class":140},[105,9599,8879],{"class":270},[105,9601,956],{"class":154},[105,9603,9604],{"class":325}," 1\n",[105,9606,9607,9609,9611,9613,9615,9617,9619],{"class":107,"line":179},[105,9608,6548],{"class":136},[105,9610,397],{"class":140},[105,9612,306],{"class":163},[105,9614,6337],{"class":136},[105,9616,141],{"class":140},[105,9618,6559],{"class":144},[105,9620,148],{"class":140},[10,9622,9623,9624,9631],{},"前端把它叫组件，Godot 把它叫场景，一回事。",[22,9625,9626,9627,9630],{},"你的组件库就是 ",[14,9628,9629],{},"scenes\u002Fui\u002F"," 这个文件夹","，它会原样搬进下一个游戏。",[10,9633,9634],{},[2129,9635],{"alt":9636,"src":9637},"10-ui-左右选","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F10-ui-%E5%B7%A6%E5%8F%B3%E9%80%89.png",[10,9639,9640],{},"UI 重搭之后，回去把第 9 章的五条再跑一遍——焦点最容易丢。",[1684,9642,9643],{},"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 .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 .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 .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":101,"searchDepth":119,"depth":119,"links":9645},[9646,9647,9648,9649],{"id":8480,"depth":119,"text":8481},{"id":8632,"depth":119,"text":8633},{"id":8752,"depth":119,"text":8753},{"id":8829,"depth":119,"text":8830},"2026-09-10",{},"\u002Fdevlog\u002Fxggame-snake\u002F10-ui-theme",{"title":8463,"description":1697},"devlog\u002Fxggame-snake\u002F10-ui-theme","oTB5ykDFUjeZQpyQQYJejlLIwmNSnKxfrkMdspK8sbY",{"id":9657,"title":9658,"body":9659,"cover":1695,"date":9836,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":9837,"navigation":129,"path":9838,"seo":9839,"stem":9840,"toc":129,"__hash__":9841},"devlog\u002Fdevlog\u002Fxggame-snake\u002F11-assets.md","图标、启动画面与音效",{"type":7,"value":9660,"toc":9829},[9661,9665,9668,9685,9688,9695,9702,9713,9720,9734,9737,9741,9752,9767,9775,9779,9794,9797,9806,9826],[91,9662,9664],{"id":9663},"图标aseprite-出场","图标：Aseprite 出场",[10,9666,9667],{},"这个游戏唯一要\"画\"的东西。16 像素级别的图标有五条规矩：",[7867,9669,9670,9673,9676,9679,9682],{},[723,9671,9672],{},"先画剪影——全涂黑也能认出是蛇",[723,9674,9675],{},"1 像素深色轮廓，任何底色上都看得清",[723,9677,9678],{},"只用 3 个绿：本色、阴影、高光",[723,9680,9681],{},"眼睛 = 2×2 白 + 1 黑，靠前靠上——这是\"活\"的关键",[723,9683,9684],{},"一个识别物（舌头）用对比色",[10,9686,9687],{},"我第一版的问题是头和身子一样粗（剪影读不出\"头\"）、眼睛太大又居中（像贴纸）。第二版把眼睛做大靠上、舌头分叉，就对了。",[10,9689,9690,9691,9694],{},"画可以是 15×15（奇数画布有真正的中心像素，对称好画），但",[22,9692,9693],{},"画布用 16","：图标的世界按 8 的倍数运转（项目图标 128、ico 要 16\u002F32\u002F48\u002F256），15 放 8 倍是 120，到哪都差一点，会被非整数缩放搞糊。Sprite → Canvas Size → 16，多出的一圈透明。",[10,9696,9697,9698,9701],{},"导出规矩只有一条：",[22,9699,9700],{},"游戏里用的按 100% 导出，让 Godot 用 Nearest 放大；游戏外用的（项目图标、网站）在 Aseprite 里按整数倍放大再导","——网站和系统放大用的是平滑算法，16×16 交过去必糊。File → Export As，Resize 填百分比，它自带最近邻放大。",[10,9703,9704,9705,9708,9709,9712],{},"项目设置 → 应用 → 配置 → 图标 填 ",[14,9706,9707],{},"assets\u002Fui\u002FSnake.png","，Web 导出会自动拿它当浏览器标签页图标。项目根目录那个 Godot 默认的 ",[14,9710,9711],{},"icon.svg"," 删掉。",[91,9714,9716,9719],{"id":9715},"indexpng-不是图标是启动画面",[14,9717,9718],{},"index.png"," 不是图标，是启动画面",[10,9721,9722,9723,9725,9726,9729,9730,9733],{},"导出网页版后文件夹里有个 ",[14,9724,9718],{},"，一看是 Godot 的 logo。它是",[22,9727,9728],{},"启动画面（boot splash）","——网页壳加载 wasm 那几秒显示的图。项目设置里 ",[14,9731,9732],{},"show_image = false"," 只关引擎启动后那一瞬，网页壳照样要一张图，没指定就用默认的。",[10,9735,9736],{},"项目设置 → 应用 → 启动画面：Image 选自己的 logo，Bg Color 填游戏底色，像素图取消勾选「使用滤镜」。",[91,9738,9740],{"id":9739},"音效先生成再找","音效：先生成，再找",[10,9742,9743,9744,9747,9748,9751],{},"小游戏的音效优先",[22,9745,9746],{},"生成","而不是",[22,9749,9750],{},"下载","：生成的版权归自己，风格天生统一，不用记来源。这次三条音效（吃、星星、撞死）用的是我自己写的一个网页版 8-bit 生成器，原理和 sfxr \u002F ChipTone 一样：波形 → 音高曲线 → 音量包络 → 滤波，一个采样一个采样算出来，拼个 WAV 头下载。",[10,9753,9754,9755,9758,9759,9762,9763,9766],{},"真要下载的话，权威的几个：",[22,9756,9757],{},"freesound.org","（最大的 CC 库，搜索时先勾 CC0）、",[22,9760,9761],{},"Sonniss GDC 音效包","（每年免费放出的商业级大包）、",[22,9764,9765],{},"Kenney.nl","（全 CC0，独立游戏标配）。带 NC（非商用）的一律不用，哪怕游戏免费。",[10,9768,9769,9771,9772,9774],{},[14,9770,2372],{}," 的代码在第 6 章——它只听信号、只负责 ",[14,9773,1670],{},"，不判断该不该播。",[91,9776,9778],{"id":9777},"字体与-credits","字体与 CREDITS",[10,9780,9781,9782,9785,9786,9789,9790,9793],{},"标题那个手写体一开始是用 SystemFont 调的系统字体——",[22,9783,9784],{},"只在自己电脑上看得到","，别人的电脑、Steam Deck、网页版上都会退回默认字。查了一下它（Caveat）是 Google Fonts 的 OFL 字体，下载 ",[14,9787,9788],{},".ttf"," 放进 ",[14,9791,9792],{},"assets\u002Ffonts\u002F","、场景里换成文件，就能合法打包。",[10,9795,9796],{},"按钮用的方舟像素字体也是 OFL 1.1。OFL 要点：可免费商用、可嵌入游戏、可修改；不能单独把字体文件拿去卖；保留版权与许可声明。",[10,9798,9799,9801,9802,9805],{},[14,9800,2421],{}," 从第 2 章就建着，规矩是",[22,9803,9804],{},"引入素材的同一分钟写这里","。三类各一节：",[720,9807,9808,9814,9820],{},[723,9809,9810,9813],{},[22,9811,9812],{},"美术","：谁画的、什么工具、有没有第三方",[723,9815,9816,9819],{},[22,9817,9818],{},"字体","：一张表——字体 \u002F 文件 \u002F 作者 \u002F 来源网址 \u002F 协议 \u002F 用在哪",[723,9821,9822,9825],{},[22,9823,9824],{},"音效","：生成器产出的一句话说明\"版权归本人\"，列文件和用途。用自制工具生成同理，写工具名和网址；工具自己的代码协议（比如照 jsfxr 改的 MIT）只管工具代码，不管它生成的声音",[10,9827,9828],{},"下一章导出、上架、开源。",{"title":101,"searchDepth":119,"depth":119,"links":9830},[9831,9832,9834,9835],{"id":9663,"depth":119,"text":9664},{"id":9715,"depth":119,"text":9833},"index.png 不是图标，是启动画面",{"id":9739,"depth":119,"text":9740},{"id":9777,"depth":119,"text":9778},"2026-09-11",{},"\u002Fdevlog\u002Fxggame-snake\u002F11-assets",{"title":9658,"description":1697},"devlog\u002Fxggame-snake\u002F11-assets","PSqPfPJXRctYshrOx22g0aoc5UXGzedxjiLjSNApNzM",{"id":9843,"title":9844,"body":9845,"cover":1695,"date":10175,"description":1697,"extension":1698,"game":1699,"github":1700,"icon":1695,"meta":10176,"navigation":129,"path":10177,"seo":10178,"stem":10179,"toc":129,"__hash__":10180},"devlog\u002Fdevlog\u002Fxggame-snake\u002F12-export-and-review.md","导出、上架与复盘",{"type":7,"value":9846,"toc":10169},[9847,9851,9854,9884,9903,9919,9922,9952,9966,9969,9984,9987,10036,10039,10042,10064,10067,10070,10076,10087,10093,10131,10137,10140,10163,10166],[91,9848,9850],{"id":9849},"导出三处名字必须一致","导出：三处名字必须一致",[10,9852,9853],{},"项目 → 导出 → Web 预设：",[720,9855,9856,9867,9877],{},[723,9857,9858,9859,9862,9863,9866],{},"导出路径 ",[14,9860,9861],{},"build\u002Fweb\u002Findex.html","——",[22,9864,9865],{},"必须叫 index.html","，itch 的硬要求",[723,9868,9869,9870,9873,9874],{},"资源页的「",[22,9871,9872],{},"排除","文件筛选器」（Exclude Filter，不是上面那行 Include）填 ",[14,9875,9876],{},"build\u002F*",[723,9878,9879,9881,9882],{},[14,9880,2406],{}," 里 ",[14,9883,2410],{},[10,9885,9886,9887,9889,9890,9892,9893,9896,9897,9899,9900,9902],{},"导出目录名、筛选器、",[14,9888,2406],{}," 三处写的名字必须一致。我这次 ",[14,9891,2406],{}," 写的 ",[14,9894,9895],{},"builds\u002F","、文件夹叫 ",[14,9898,2410],{},"——差一个字母，11 MB 的产物差点进仓库；筛选器又填进了 Include 栏，pck 把 ",[14,9901,2410],{}," 自己打了进去（和小鸟同一个坑的另一种踩法）。",[10,9904,9905,9906,9908,9909,9914,9915,9918],{},"更稳的根治：在 ",[14,9907,2410],{}," 里放一个空文件 ",[22,9910,9911],{},[14,9912,9913],{},".gdignore","，编辑器就完全不扫描它——不导入、不打包、文件系统面板里也不显示。连导出产物里的 PNG 被反向导入生成 ",[14,9916,9917],{},".import"," 垃圾的问题也一起没了。",[10,9920,9921],{},"导完必须验一下有没有把自己打进去：",[96,9923,9927],{"className":9924,"code":9925,"language":9926,"meta":101,"style":101},"language-bash shiki shiki-themes vitesse-light vitesse-dark","strings build\u002Fweb\u002Findex.pck | grep -c res:\u002F\u002Fbuild     # 必须是 0\n","bash",[14,9928,9929],{"__ignoreMap":101},[105,9930,9931,9934,9937,9940,9943,9946,9949],{"class":107,"line":108},[105,9932,9933],{"class":136},"strings",[105,9935,9936],{"class":144}," build\u002Fweb\u002Findex.pck",[105,9938,9939],{"class":154}," |",[105,9941,9942],{"class":136}," grep",[105,9944,9945],{"class":988}," -c",[105,9947,9948],{"class":144}," res:\u002F\u002Fbuild",[105,9950,9951],{"class":122},"     # 必须是 0\n",[10,9953,9954,9955,8203,9958,9961,9962,9965],{},"点「导出项目」，不是「导出 PCK\u002FZIP」（后者只有资源包，没有 index.html）。本地试玩：",[14,9956,9957],{},"cd build\u002Fweb",[14,9959,9960],{},"python -m http.server 8000"," → 浏览器开 ",[14,9963,9964],{},"localhost:8000","。浏览器里插上手柄再试一遍——先随便按一个手柄键，浏览器才会把手柄交给网页。",[91,9967,9968],{"id":9968},"itch",[10,9970,9971,9972,9979,9980,9983],{},"手动打 zip（Godot 不会替你打）：",[22,9973,9974,9975,9978],{},"压缩 ",[14,9976,9977],{},"web"," 文件夹里的内容，不是文件夹本身","——解压后第一层必须直接是 ",[14,9981,9982],{},"index.html","。上 itch，Embed 尺寸填 1280×720（画面会自己等比缩放，不用填 1920）。",[10,9985,9986],{},"发布要准备的图：",[26,9988,9989,10002],{},[29,9990,9991],{},[32,9992,9993,9996,9999],{},[35,9994,9995],{},"放哪",[35,9997,9998],{},"图",[35,10000,10001],{},"尺寸",[45,10003,10004,10015,10026],{},[32,10005,10006,10009,10012],{},[50,10007,10008],{},"Godot",[50,10010,10011],{},"项目图标 \u002F 启动画面",[50,10013,10014],{},"128×128 \u002F 512×512",[32,10016,10017,10020,10023],{},[50,10018,10019],{},"GitHub",[50,10021,10022],{},"README 首图 \u002F Social preview",[50,10024,10025],{},"1280×720 \u002F 1280×640",[32,10027,10028,10030,10033],{},[50,10029,9968],{},[50,10031,10032],{},"封面 \u002F 截图 3～5 张",[50,10034,10035],{},"630×500 \u002F 1920×1080",[10,10037,10038],{},"截图前关书签栏、只框游戏区域——上次截图把浏览器书签栏一起截进去了。",[91,10040,10041],{"id":10041},"开源",[10,10043,10044,10045,10048,10049,10052,10053,10055,10056,1667,10058,10061,10062,1667],{},"和小鸟一样：",[14,10046,10047],{},"LICENSE","（MIT，代码 + 原创美术）、",[14,10050,10051],{},"README.md"," 加简 \u002F 繁 \u002F 粤三个译本、",[14,10054,2421],{},"（第三方字体 OFL 列出来）、",[14,10057,2406],{},[14,10059,10060],{},"export_presets.cfg"," 提交——里面只有 Web 预设和相对路径，别人拉下来就能照样导出；一旦以后加了 Android \u002F iOS，签名密钥会被明文写进去，那天再把它加进 ",[14,10063,2406],{},[10,10065,10066],{},"推之前扫一遍：绝对路径、用户名、邮箱、token，一个都不能有。先私有仓库 push，网页上看一遍再转公开。",[91,10068,10069],{"id":10069},"复盘",[10,10071,10072,10075],{},[22,10073,10074],{},"和计划不一样的地方","（都是合理的变奏）：",[720,10077,10078,10081,10084],{},[723,10079,10080],{},"菜单只留了\"撞墙 \u002F 穿墙\"一个选项，棋盘大小和皮肤固定——每多一个可选项，菜单、代码、测试都多一份；但它们在代码里仍然当\"设置\"写，以后露出来就能选",[723,10082,10083],{},"按钮弹动、标题呼吸、入场动画这些动效跳过了，留给下一个项目专门学",[723,10085,10086],{},"音效用自制工具，图标自己画，主按钮没做变体",[10,10088,10089,10092],{},[22,10090,10091],{},"这次踩的坑","（已经写回我的标准流程）：",[7867,10094,10095,10107,10115,10122,10125,10128],{},[723,10096,10097,10098,10100,10101,10103,10104,10106],{},"导出目录 \u002F 筛选器 \u002F ",[14,10099,2406],{}," 三处名字不一致；Include 和 Exclude 填错栏 → ",[14,10102,9913],{}," 根治 + ",[14,10105,9933],{}," 自检",[723,10108,10109,10111,10112],{},[14,10110,775],{}," 里先生食物后建蛇 → 食物可能长在蛇出生的格子上。",[22,10113,10114],{},"初始化顺序：先有主角，再生世界",[723,10116,2262,10117,3647,10119,10121],{},[14,10118,2232],{},[14,10120,2248],{}," 没有手柄键，每个项目都要补",[723,10123,10124],{},"StyleBox 的 Content Margins 和 Expand Margins 填反了，然后去改容器间距补——补错了地方",[723,10126,10127],{},"场景里摆的节点尺寸和代码里的常量是两份真相（星星画成 4 格）→ 尺寸也收归代码",[723,10129,10130],{},"代码里留了给 AI 的疑问注释和教程痕迹（\"原来是 die()\"）→ 问完就删，注释只写代码现在在干什么",[10,10132,10133,10136],{},[22,10134,10135],{},"这个项目真正学到的","：不是贪吃蛇，是外壳——设置菜单怎么把选择交给游戏、暂停的 process_mode 传染、手柄焦点、Theme 总表、自己封装一个 UI 组件。第三个游戏开始，这些是在\"搬\"而不是\"写\"。",[10,10138,10139],{},"到这里，《XGGame-Snake》记录完了：",[720,10141,10142,10145,10148,10151,10154,10157,10160],{},[723,10143,10144],{},"✅ 立项表写死\"不做什么\"",[723,10146,10147],{},"✅ 蛇是一个数组，画面是它的镜子",[723,10149,10150],{},"✅ 四个 Timer 各管一件事",[723,10152,10153],{},"✅ EventBus 喊、GameManager 判、SaveManager 存",[723,10155,10156],{},"✅ 设置菜单、暂停、结算、手柄全流程",[723,10158,10159],{},"✅ Theme + 左右选组件",[723,10161,10162],{},"✅ 导出上架 itch，MIT 开源",[10,10164,10165],{},"下一款游戏见 💪",[1684,10167,10168],{},"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 .s5TCs, html code.shiki .s5TCs{--shiki-default:#AB5959;--shiki-dark:#CB7676}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}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":101,"searchDepth":119,"depth":119,"links":10170},[10171,10172,10173,10174],{"id":9849,"depth":119,"text":9850},{"id":9968,"depth":119,"text":9968},{"id":10041,"depth":119,"text":10041},{"id":10069,"depth":119,"text":10069},"2026-09-12",{},"\u002Fdevlog\u002Fxggame-snake\u002F12-export-and-review",{"title":9844,"description":1697},"devlog\u002Fxggame-snake\u002F12-export-and-review","s8CKK5i2WAjpfTGwvEkNaknJ2QqCurcJld0vG6fQnfY",1788955183110]