[{"data":1,"prerenderedAt":9700},["ShallowReactive",2],{"\u002Fdevlog\u002Fxggame-snake\u002F08-pause":3,"devlog-chapters-xggame-snake":1146},{"id":4,"title":5,"body":6,"cover":1135,"date":1136,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":1141,"navigation":71,"path":1142,"seo":1143,"stem":1144,"toc":71,"__hash__":1145},"devlog\u002Fdevlog\u002Fxggame-snake\u002F08-pause.md","暂停与结算",{"type":7,"value":8,"toc":1128},"minimark",[9,14,37,240,252,261,265,276,287,345,366,387,832,835,886,896,899,924,931,934,938,956,1108,1114,1121,1124],[10,11,13],"h2",{"id":12},"ui-层与分数","UI 层与分数",[15,16,17,21,22,25,26,29,30,25,33,36],"p",{},[18,19,20],"code",{},"Main"," 下加 ",[18,23,24],{},"CanvasLayer"," 命名 ",[18,27,28],{},"UI","，里面 ",[18,31,32],{},"Label",[18,34,35],{},"ScoreLabel","：",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-gdscript shiki shiki-themes vitesse-light vitesse-dark","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","gdscript","",[18,45,46,59,66,73,105,110,131,158,190,195,221],{"__ignoreMap":43},[47,48,51,55],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sTPum","extends",[47,56,58],{"class":57},"s_NWU"," CanvasLayer\n",[47,60,62],{"class":49,"line":61},2,[47,63,65],{"class":64},"snYqZ","## UI 只是显示器：自己不存任何数据，全靠听信号刷新\n",[47,67,69],{"class":49,"line":68},3,[47,70,72],{"emptyLinePlaceholder":71},true,"\n",[47,74,76,80,84,88,92,95,98,101],{"class":49,"line":75},4,[47,77,79],{"class":78},"s_xSY","@onready",[47,81,83],{"class":82},"s5TCs"," var",[47,85,87],{"class":86},"s9nN2"," score_label",[47,89,91],{"class":90},"si6no",":",[47,93,94],{"class":57}," Label",[47,96,97],{"class":90}," =",[47,99,100],{"class":53}," $",[47,102,104],{"class":103},"sfsYZ","ScoreLabel\n",[47,106,108],{"class":49,"line":107},5,[47,109,72],{"emptyLinePlaceholder":71},[47,111,113,116,119,122,125,128],{"class":49,"line":112},6,[47,114,115],{"class":82},"func",[47,117,118],{"class":78}," _ready",[47,120,121],{"class":90},"()",[47,123,124],{"class":82}," ->",[47,126,127],{"class":57}," void",[47,129,130],{"class":90},":\n",[47,132,134,137,140,143,146,149,152,155],{"class":49,"line":133},7,[47,135,136],{"class":57},"    GameManager",[47,138,139],{"class":90},".",[47,141,142],{"class":86},"score_changed",[47,144,139],{"class":145},"s8w-G",[47,147,148],{"class":78},"connect",[47,150,151],{"class":90},"(",[47,153,154],{"class":86},"_on_score_changed",[47,156,157],{"class":90},")\n",[47,159,161,164,166,169,171,174,176,179,181,184,187],{"class":49,"line":160},8,[47,162,163],{"class":86},"    score_label",[47,165,139],{"class":90},[47,167,168],{"class":86},"text",[47,170,97],{"class":90},[47,172,173],{"class":78}," str",[47,175,151],{"class":90},[47,177,178],{"class":57},"GameManager",[47,180,139],{"class":90},[47,182,183],{"class":86},"total_score",[47,185,186],{"class":90},")",[47,188,189],{"class":64},"     # 开局同步一次\n",[47,191,193],{"class":49,"line":192},9,[47,194,72],{"emptyLinePlaceholder":71},[47,196,198,200,203,205,208,210,213,215,217,219],{"class":49,"line":197},10,[47,199,115],{"class":82},[47,201,202],{"class":78}," _on_score_changed",[47,204,151],{"class":90},[47,206,207],{"class":145},"new_score",[47,209,91],{"class":90},[47,211,212],{"class":57}," int",[47,214,186],{"class":90},[47,216,124],{"class":82},[47,218,127],{"class":57},[47,220,130],{"class":90},[47,222,224,226,228,230,232,234,236,238],{"class":49,"line":223},11,[47,225,163],{"class":86},[47,227,139],{"class":90},[47,229,168],{"class":86},[47,231,97],{"class":90},[47,233,173],{"class":78},[47,235,151],{"class":90},[47,237,207],{"class":86},[47,239,157],{"class":90},[15,241,242,243,246,247,251],{},"为什么 ",[18,244,245],{},"_ready()"," 里要\"开局同步一次\"？信号只在",[248,249,250],"strong",{},"变化时","才响。UI 刚创建出来那一刻还没人喊过，不主动读一次，标签会是空的。",[15,253,254,255,257,258,260],{},"为什么 UI 放在 ",[18,256,24],{}," 下？",[18,259,24],{}," 上的东西不受摄像机影响，永远贴在屏幕上。贪吃蛇没有移动的摄像机，现在看不出区别，但这是习惯——以后做有镜头的游戏，UI 不放这里会跟着地图跑。",[10,262,264],{"id":263},"暂停菜单先讲坑再动手","暂停菜单：先讲坑再动手",[15,266,267,268,271,272,275],{},"Godot 的\"暂停\"是一句话：",[18,269,270],{},"get_tree().paused = true","。它会冻结整棵场景树——",[248,273,274],{},"包括你的暂停菜单本身","。菜单被冻住 = 按钮点不了、按 Esc 也没反应，你就再也出不来了。",[15,277,278,279,282,283,286],{},"解法是给菜单设 ",[18,280,281],{},"process_mode = Always","（\"暂停时我照常工作\"）。但 ",[18,284,285],{},"process_mode"," 会向下传染：一个节点设了 Always，它的所有子节点默认跟着 Always。所以要精确地只给该活着的节点设 Always：",[288,289,290,306],"table",{},[291,292,293],"thead",{},[294,295,296,300,303],"tr",{},[297,298,299],"th",{},"档",[297,301,302],{},"意思",[297,304,305],{},"给谁",[307,308,309,321,332],"tbody",{},[294,310,311,315,318],{},[312,313,314],"td",{},"Inherit（默认）",[312,316,317],{},"跟父节点一样",[312,319,320],{},"大多数节点",[294,322,323,326,329],{},[312,324,325],{},"Pausable",[312,327,328],{},"暂停时停",[312,330,331],{},"蛇、四个 Timer、任何游戏逻辑",[294,333,334,339,342],{},[312,335,336],{},[248,337,338],{},"Always",[312,340,341],{},"暂停时照常跑",[312,343,344],{},"暂停菜单、监听 Esc 的节点",[15,346,347,356,357,359,360,362,363,365],{},[248,348,349,350,352,353,355],{},"Always 设在 ",[18,351,28],{}," 上，不是 ",[18,354,20],{}," 上。"," ",[18,358,20],{}," 设 Always 会传染给四个 Timer，暂停时蛇照走、食物照长。设在 ",[18,361,28],{}," 上，传染范围正好是\"所有面板和按钮\"，而 ",[18,364,20],{}," 及其下的 Timer 保持 Pausable。传染是个特性，关键是把它设在正确的分支上。",[15,367,368,21,370,25,373,376,377,36,380,383,384,36],{},[18,369,28],{},[18,371,372],{},"Panel",[18,374,375],{},"PauseMenu","，里面两个 ",[18,378,379],{},"Button",[18,381,382],{},"ResumeButton","、",[18,385,386],{},"RestartButton",[38,388,390],{"className":40,"code":389,"language":42,"meta":43,"style":43},"@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",[18,391,392,413,434,438,463,489,494,509,526,543,568,582,598,621,626,655,688,701,717,722,738,754,777,782,798,817],{"__ignoreMap":43},[47,393,394,396,398,401,403,406,408,410],{"class":49,"line":50},[47,395,79],{"class":78},[47,397,83],{"class":82},[47,399,400],{"class":86}," pause_menu",[47,402,91],{"class":90},[47,404,405],{"class":57}," Panel",[47,407,97],{"class":90},[47,409,100],{"class":53},[47,411,412],{"class":103},"PauseMenu\n",[47,414,415,417,419,422,424,427,429,431],{"class":49,"line":61},[47,416,79],{"class":78},[47,418,83],{"class":82},[47,420,421],{"class":86}," resume_button",[47,423,91],{"class":90},[47,425,426],{"class":57}," Button",[47,428,97],{"class":90},[47,430,100],{"class":53},[47,432,433],{"class":103},"PauseMenu\u002FHBoxContainer\u002FResumeButton\n",[47,435,436],{"class":49,"line":68},[47,437,72],{"emptyLinePlaceholder":71},[47,439,440,442,445,447,450,452,455,457,459,461],{"class":49,"line":75},[47,441,115],{"class":82},[47,443,444],{"class":78}," _unhandled_input",[47,446,151],{"class":90},[47,448,449],{"class":145},"event",[47,451,91],{"class":90},[47,453,454],{"class":57}," InputEvent",[47,456,186],{"class":90},[47,458,124],{"class":82},[47,460,127],{"class":57},[47,462,130],{"class":90},[47,464,465,468,471,474,476,479,481,485,487],{"class":49,"line":107},[47,466,467],{"class":53},"    if",[47,469,470],{"class":82}," not",[47,472,473],{"class":86}," event",[47,475,139],{"class":145},[47,477,478],{"class":78},"is_action_pressed",[47,480,151],{"class":90},[47,482,484],{"class":483},"spP0B","\"pause\"",[47,486,186],{"class":90},[47,488,130],{"class":145},[47,490,491],{"class":49,"line":112},[47,492,493],{"class":53},"        return\n",[47,495,496,499,502,504,507],{"class":49,"line":133},[47,497,498],{"class":53},"    match",[47,500,501],{"class":57}," GameManager",[47,503,139],{"class":90},[47,505,506],{"class":86},"current_state",[47,508,130],{"class":145},[47,510,511,514,516,519,521,524],{"class":49,"line":160},[47,512,513],{"class":57},"        GameManager",[47,515,139],{"class":90},[47,517,518],{"class":86},"GameState",[47,520,139],{"class":90},[47,522,523],{"class":53},"PLAYING",[47,525,130],{"class":145},[47,527,528,531,533,535,538,540],{"class":49,"line":192},[47,529,530],{"class":78},"            get_tree",[47,532,121],{"class":90},[47,534,139],{"class":145},[47,536,537],{"class":86},"paused",[47,539,97],{"class":90},[47,541,542],{"class":53}," true\n",[47,544,545,548,550,553,555,557,559,561,563,566],{"class":49,"line":197},[47,546,547],{"class":57},"            GameManager",[47,549,139],{"class":145},[47,551,552],{"class":78},"set_state",[47,554,151],{"class":90},[47,556,178],{"class":57},[47,558,139],{"class":90},[47,560,518],{"class":86},[47,562,139],{"class":90},[47,564,565],{"class":53},"PAUSED",[47,567,157],{"class":90},[47,569,570,572,574,576,578,580],{"class":49,"line":223},[47,571,513],{"class":57},[47,573,139],{"class":90},[47,575,518],{"class":86},[47,577,139],{"class":90},[47,579,565],{"class":53},[47,581,130],{"class":145},[47,583,585,587,589,591,593,595],{"class":49,"line":584},12,[47,586,530],{"class":78},[47,588,121],{"class":90},[47,590,139],{"class":145},[47,592,537],{"class":86},[47,594,97],{"class":90},[47,596,597],{"class":53}," false\n",[47,599,601,603,605,607,609,611,613,615,617,619],{"class":49,"line":600},13,[47,602,547],{"class":57},[47,604,139],{"class":145},[47,606,552],{"class":78},[47,608,151],{"class":90},[47,610,178],{"class":57},[47,612,139],{"class":90},[47,614,518],{"class":86},[47,616,139],{"class":90},[47,618,523],{"class":53},[47,620,157],{"class":90},[47,622,624],{"class":49,"line":623},14,[47,625,72],{"emptyLinePlaceholder":71},[47,627,629,631,634,636,639,641,643,645,647,649,651,653],{"class":49,"line":628},15,[47,630,115],{"class":82},[47,632,633],{"class":78}," _on_state_changed",[47,635,151],{"class":90},[47,637,638],{"class":145},"new_state",[47,640,91],{"class":90},[47,642,501],{"class":57},[47,644,139],{"class":90},[47,646,518],{"class":86},[47,648,186],{"class":90},[47,650,124],{"class":82},[47,652,127],{"class":57},[47,654,130],{"class":90},[47,656,658,661,663,666,668,671,673,676,678,680,682,684,686],{"class":49,"line":657},16,[47,659,660],{"class":86},"    pause_menu",[47,662,139],{"class":90},[47,664,665],{"class":86},"visible",[47,667,97],{"class":90},[47,669,670],{"class":90}," (",[47,672,638],{"class":86},[47,674,675],{"class":82}," ==",[47,677,501],{"class":57},[47,679,139],{"class":145},[47,681,518],{"class":57},[47,683,139],{"class":145},[47,685,565],{"class":86},[47,687,157],{"class":90},[47,689,691,693,695,697,699],{"class":49,"line":690},17,[47,692,467],{"class":53},[47,694,400],{"class":86},[47,696,139],{"class":90},[47,698,665],{"class":86},[47,700,130],{"class":145},[47,702,704,707,709,712,714],{"class":49,"line":703},18,[47,705,706],{"class":86},"        resume_button",[47,708,139],{"class":145},[47,710,711],{"class":78},"grab_focus",[47,713,121],{"class":90},[47,715,716],{"class":64},"           # 手柄：面板一出现，焦点就给\"继续\"\n",[47,718,720],{"class":49,"line":719},19,[47,721,72],{"emptyLinePlaceholder":71},[47,723,725,727,730,732,734,736],{"class":49,"line":724},20,[47,726,115],{"class":82},[47,728,729],{"class":78}," _on_resume_button_pressed",[47,731,121],{"class":90},[47,733,124],{"class":82},[47,735,127],{"class":57},[47,737,130],{"class":90},[47,739,741,744,746,748,750,752],{"class":49,"line":740},21,[47,742,743],{"class":78},"    get_tree",[47,745,121],{"class":90},[47,747,139],{"class":145},[47,749,537],{"class":86},[47,751,97],{"class":90},[47,753,597],{"class":53},[47,755,757,759,761,763,765,767,769,771,773,775],{"class":49,"line":756},22,[47,758,136],{"class":57},[47,760,139],{"class":145},[47,762,552],{"class":78},[47,764,151],{"class":90},[47,766,178],{"class":57},[47,768,139],{"class":90},[47,770,518],{"class":86},[47,772,139],{"class":90},[47,774,523],{"class":53},[47,776,157],{"class":90},[47,778,780],{"class":49,"line":779},23,[47,781,72],{"emptyLinePlaceholder":71},[47,783,785,787,790,792,794,796],{"class":49,"line":784},24,[47,786,115],{"class":82},[47,788,789],{"class":78}," _on_restart_button_pressed",[47,791,121],{"class":90},[47,793,124],{"class":82},[47,795,127],{"class":57},[47,797,130],{"class":90},[47,799,801,803,805,807,809,811,814],{"class":49,"line":800},25,[47,802,743],{"class":78},[47,804,121],{"class":90},[47,806,139],{"class":145},[47,808,537],{"class":86},[47,810,97],{"class":90},[47,812,813],{"class":53}," false",[47,815,816],{"class":64},"            # ⚠️ 先解冻，再重载\n",[47,818,820,822,824,826,829],{"class":49,"line":819},26,[47,821,743],{"class":78},[47,823,121],{"class":90},[47,825,139],{"class":145},[47,827,828],{"class":78},"reload_current_scene",[47,830,831],{"class":90},"()\n",[15,833,834],{},"三个\"为什么\"：",[836,837,838,862,873],"ul",{},[839,840,841,851,852,854,855,858,859,861],"li",{},[248,842,843,844,847,848],{},"Esc 的监听为什么写在 ",[18,845,846],{},"ui.gd"," 而不是 ",[18,849,850],{},"main.gd","：暂停时 ",[18,853,20],{}," 是 Pausable，它的 ",[18,856,857],{},"_input"," 也会停，写在那里按 Esc 就出不来了。",[18,860,28],{}," 是 Always，暂停时它还醒着",[839,863,864,36,870,872],{},[248,865,866,867],{},"重开前为什么要先 ",[18,868,869],{},"paused = false",[18,871,537],{}," 是场景树的属性，不属于任何场景。重载场景时它不会自动复位，你会得到一个一开局就冻住的新场景。这是暂停菜单第二常见的坑",[839,874,875,885],{},[248,876,877,880,881,884],{},[18,878,879],{},"grab_focus()"," 为什么写在 ",[18,882,883],{},"visible = true"," 之后","：隐藏的控件拿不到焦点。先显示，再给焦点，顺序反了手柄就按不动",[10,887,889,892,893,895],{"id":888},"_unhandled_input-和-_input一封信的三站",[18,890,891],{},"_unhandled_input"," 和 ",[18,894,857],{},"：一封信的三站",[15,897,898],{},"一个按键事件在 Godot 里按固定顺序传递：",[900,901,902,908,918],"ol",{},[839,903,904,907],{},[18,905,906],{},"_input()"," —— 所有节点最先看到，相当于门口保安先把每封信都瞧一眼",[839,909,910,913,914,917],{},[248,911,912],{},"UI 控件"," —— 有焦点的按钮、文本框。",[248,915,916],{},"是它要的键就收下（\"吃掉\"），后面的站就收不到了","：Enter \u002F 空格 \u002F 手柄 A 给有焦点的按钮，字母给正在输入的文本框",[839,919,920,923],{},[18,921,922],{},"_unhandled_input()"," —— 最后一站，只收到 ② 没要的，相当于没人认领的信送到总务处",[15,925,926,927,930],{},"例子：暂停面板开着，焦点在\"继续\"上。玩家按 Enter → ② 按钮要这个键，吃掉，触发 pressed，③ 什么都没收到。玩家按 Esc → 没有控件要 Esc → 掉到 ③ → 我们的暂停切换。",[248,928,929],{},"所以游戏逻辑的按键写在 ③","：让 UI 先挑，剩下的才是游戏的。写在 ① 会出现\"按一下空格，按钮和游戏动作同时触发\"的双响。",[15,932,933],{},"第 4 章的方向键用的是 ①，没出事是因为暂停时有 PLAYING 守卫挡着；规矩上游戏按键都该用 ③。",[10,935,937],{"id":936},"结算面板-回菜单","结算面板 + 回菜单",[15,939,940,942,943,29,946,949,950,383,953,36],{},[18,941,28],{}," 下再加 ",[18,944,945],{},"GameOverPanel",[18,947,948],{},"FinalScore","（Label）、",[18,951,952],{},"GameOverRestartButton",[18,954,955],{},"MenuButton",[38,957,959],{"className":40,"code":958,"language":42,"meta":43,"style":43},"    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",[18,960,961,983,996,1046,1057,1061,1076,1090],{"__ignoreMap":43},[47,962,963,965,968,970,972,974,976,978,981],{"class":49,"line":50},[47,964,467],{"class":53},[47,966,967],{"class":86}," new_state",[47,969,675],{"class":82},[47,971,501],{"class":57},[47,973,139],{"class":90},[47,975,518],{"class":86},[47,977,139],{"class":90},[47,979,980],{"class":53},"GAME_OVER",[47,982,130],{"class":145},[47,984,985,988,990,992,994],{"class":49,"line":61},[47,986,987],{"class":86},"        game_over_panel",[47,989,139],{"class":90},[47,991,665],{"class":86},[47,993,97],{"class":90},[47,995,542],{"class":53},[47,997,998,1001,1003,1005,1007,1010,1013,1016,1018,1021,1024,1027,1029,1031,1033,1036,1038,1040,1043],{"class":49,"line":68},[47,999,1000],{"class":86},"        final_score",[47,1002,139],{"class":90},[47,1004,168],{"class":86},[47,1006,97],{"class":90},[47,1008,1009],{"class":483}," \"Score ",[47,1011,1012],{"class":103},"%d",[47,1014,1015],{"class":483},"   Best ",[47,1017,1012],{"class":103},[47,1019,1020],{"class":483},"\"",[47,1022,1023],{"class":82}," %",[47,1025,1026],{"class":90}," [",[47,1028,178],{"class":57},[47,1030,139],{"class":145},[47,1032,183],{"class":86},[47,1034,1035],{"class":145},", ",[47,1037,178],{"class":57},[47,1039,139],{"class":145},[47,1041,1042],{"class":86},"high_score",[47,1044,1045],{"class":90},"]\n",[47,1047,1048,1051,1053,1055],{"class":49,"line":75},[47,1049,1050],{"class":86},"        game_over_restart_button",[47,1052,139],{"class":145},[47,1054,711],{"class":78},[47,1056,831],{"class":90},[47,1058,1059],{"class":49,"line":107},[47,1060,72],{"emptyLinePlaceholder":71},[47,1062,1063,1065,1068,1070,1072,1074],{"class":49,"line":112},[47,1064,115],{"class":82},[47,1066,1067],{"class":78}," _on_menu_button_pressed",[47,1069,121],{"class":90},[47,1071,124],{"class":82},[47,1073,127],{"class":57},[47,1075,130],{"class":90},[47,1077,1078,1080,1082,1084,1086,1088],{"class":49,"line":133},[47,1079,743],{"class":78},[47,1081,121],{"class":90},[47,1083,139],{"class":145},[47,1085,537],{"class":86},[47,1087,97],{"class":90},[47,1089,597],{"class":53},[47,1091,1092,1094,1096,1098,1101,1103,1106],{"class":49,"line":160},[47,1093,743],{"class":78},[47,1095,121],{"class":90},[47,1097,139],{"class":145},[47,1099,1100],{"class":78},"change_scene_to_file",[47,1102,151],{"class":90},[47,1104,1105],{"class":483},"\"res:\u002F\u002Fscenes\u002Fmenu.tscn\"",[47,1107,157],{"class":90},[15,1109,1110,1111],{},"为什么要有\"回菜单\"？立项表说模式\"开局前选\"。没有回菜单的路，玩家想换模式只能关游戏重开。",[248,1112,1113],{},"每个\"进去\"的路都要有\"出来\"的路。",[15,1115,1116],{},[1117,1118],"img",{"alt":1119,"src":1120},"08-pause-暂停","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F08-pause-%E6%9A%82%E5%81%9C.png",[15,1122,1123],{},"现在整条链通了：菜单 → 游戏 → 暂停\u002F继续 → 撞死 → 结算 → 重开 \u002F 回菜单。下一章拔掉鼠标，只用手柄再走一遍——你会发现按 A 没反应。",[1125,1126,1127],"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 .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":43,"searchDepth":61,"depth":61,"links":1129},[1130,1131,1132,1134],{"id":12,"depth":61,"text":13},{"id":263,"depth":61,"text":264},{"id":888,"depth":61,"text":1133},"_unhandled_input 和 _input：一封信的三站",{"id":936,"depth":61,"text":937},null,"2026-09-08","XGGame-Snake Godot","md","XGGame-Snake","https:\u002F\u002Fgithub.com\u002FXXGGG\u002FXGGame-Snake",{},"\u002Fdevlog\u002Fxggame-snake\u002F08-pause",{"title":5,"description":1137},"devlog\u002Fxggame-snake\u002F08-pause","KPS4wU5QYAirHHdGb4SNzDeRPWUmumL_iI4litJtzIc",[1147,1444,1893,2620,4072,4443,6001,6749,7616,7980,9175,9361],{"id":1148,"title":1149,"body":1150,"cover":1135,"date":1438,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":1439,"navigation":71,"path":1440,"seo":1441,"stem":1442,"toc":71,"__hash__":1443},"devlog\u002Fdevlog\u002Fxggame-snake\u002F01-design.md","立项",{"type":7,"value":1151,"toc":1433},[1152,1159,1162,1165,1240,1244,1251,1254,1258,1265,1271,1276,1404,1407,1421,1424,1430],[15,1153,1154,1155,1158],{},"贪吃蛇的玩法早就定了，为什么还要立项？因为做到一半你一定会想加东西——\"要不要加道具？加穿墙？加皮肤？\"。",[248,1156,1157],{},"「不做什么」现在不写死，之后就没有任何东西能拦住你。"," 小鸟能按时做完，就是因为一开始写死了\"无关卡、无道具、无皮肤\"。",[10,1160,1161],{"id":1161},"立项表",[15,1163,1164],{},"这张表就是这个游戏的全部规则，代码只是把它翻译成 Godot 听得懂的话：",[288,1166,1167,1177],{},[291,1168,1169],{},[294,1170,1171,1174],{},[297,1172,1173],{},"项",[297,1175,1176],{},"内容",[307,1178,1179,1187,1195,1203,1211,1222,1232],{},[294,1180,1181,1184],{},[312,1182,1183],{},"核心玩法",[312,1185,1186],{},"控制蛇移动，吃到普通食物变长 +1 分；吃到星星变长 +2 分",[294,1188,1189,1192],{},[312,1190,1191],{},"食物规则",[312,1193,1194],{},"普通食物每 3 秒长一个，场上最多 3 个，吃掉不立刻补。星星每 12 秒一颗，会发光，5 秒没人吃就消失",[294,1196,1197,1200],{},[312,1198,1199],{},"开局设置",[312,1201,1202],{},"撞墙模式 \u002F 穿墙模式，开始界面用 ◀ ▶ 左右选",[294,1204,1205,1208],{},[312,1206,1207],{},"胜负",[312,1209,1210],{},"撞到自己 → 结束；撞墙模式下撞墙 → 结束；穿墙模式下从对面出来",[294,1212,1213,1216],{},[312,1214,1215],{},"操作",[312,1217,1218,1219],{},"键盘（方向键 \u002F WASD \u002F Esc）和手柄（十字键 \u002F 左摇杆 \u002F Start），",[248,1220,1221],{},"全程不碰鼠标也能玩完",[294,1223,1224,1229],{},[312,1225,1226],{},[248,1227,1228],{},"【不做】",[312,1230,1231],{},"无关卡、无第二种道具、无联机、无排行榜、无像素美术（纯色方块）、无背景音乐",[294,1233,1234,1237],{},[312,1235,1236],{},"完成标准",[312,1238,1239],{},"菜单选模式 → 游戏 → 暂停\u002F继续 → 撞死 → 看分数和最高分 → 重开 \u002F 回菜单；只用手柄能走完；导出 Web",[10,1241,1243],{"id":1242},"为什么食物要按时间长吃掉不补","为什么食物要\"按时间长、吃掉不补\"",[15,1245,1246,1247,1250],{},"经典贪吃蛇里食物永远只有一个，玩家只有一件事：去吃它。改成按时间生成之后，场上可能有 0～3 个食物、一颗随时会消失的星星——玩家要",[248,1248,1249],{},"做选择","：去远的那个？等近处长出来？绕过去抢星星值不值？",[15,1252,1253],{},"这些选择就是\"玩法\"。数据结构只多了一个数组，体验完全不一样。这是我在这个项目里最满意的一个决定。",[10,1255,1257],{"id":1256},"实体清单想清楚再动手","实体清单：想清楚再动手",[15,1259,1260,1261,1264],{},"小鸟那次的真正根因，不是\"忘了配碰撞层\"，是一上来就写代码、没先问\"谁要检测谁\"，最后只能用 ",[18,1262,1263],{},"body.name"," 打补丁。这次先拿纸笔回答两个问题：",[15,1266,1267,1270],{},[248,1268,1269],{},"A：这个游戏里有哪些「东西」？"," → 蛇头 · 蛇身 · 普通食物（0～3 个）· 星星（0 或 1 个，会过期）· 墙",[15,1272,1273],{},[248,1274,1275],{},"B：谁碰到谁会怎样？用什么判定？",[288,1277,1278,1294],{},[291,1279,1280],{},[294,1281,1282,1285,1288,1291],{},[297,1283,1284],{},"谁",[297,1286,1287],{},"碰到谁",[297,1289,1290],{},"结果",[297,1292,1293],{},"用什么判定",[307,1295,1296,1310,1323,1338,1353,1369,1387],{},[294,1297,1298,1301,1304,1307],{},[312,1299,1300],{},"蛇头",[312,1302,1303],{},"墙（撞墙模式）",[312,1305,1306],{},"死",[312,1308,1309],{},"坐标超出 0～格数-1？→ 数字比较",[294,1311,1312,1314,1317,1320],{},[312,1313,1300],{},[312,1315,1316],{},"墙（穿墙模式）",[312,1318,1319],{},"从对面出来",[312,1321,1322],{},"坐标绕一圈 → 取余",[294,1324,1325,1327,1330,1332],{},[312,1326,1300],{},[312,1328,1329],{},"蛇身",[312,1331,1306],{},[312,1333,1334,1335],{},"坐标在数组里？→ ",[18,1336,1337],{},"in",[294,1339,1340,1342,1345,1348],{},[312,1341,1300],{},[312,1343,1344],{},"普通食物",[312,1346,1347],{},"变长 +1",[312,1349,1350,1351],{},"坐标在食物数组里？→ ",[18,1352,1337],{},[294,1354,1355,1357,1360,1363],{},[312,1356,1300],{},[312,1358,1359],{},"星星",[312,1361,1362],{},"变长 +2",[312,1364,1365,1366],{},"两个坐标相等？→ ",[18,1367,1368],{},"==",[294,1370,1371,1373,1378,1381],{},[312,1372,1359],{},[312,1374,1375],{},[248,1376,1377],{},"时间",[312,1379,1380],{},"5 秒后消失",[312,1382,1383,1386],{},[18,1384,1385],{},"Timer","（One Shot）",[294,1388,1389,1392,1396,1399],{},[312,1390,1391],{},"棋盘",[312,1393,1394],{},[248,1395,1377],{},[312,1397,1398],{},"每 3 秒长一个食物",[312,1400,1401,1403],{},[18,1402,1385],{},"（重复）",[15,1405,1406],{},"填完，结论自己就出来了：",[836,1408,1409,1415],{},[839,1410,1411,1412],{},"前五行全是坐标运算 → ",[248,1413,1414],{},"不需要物理引擎，不用配碰撞层，不用 Area2D",[839,1416,1417,1418],{},"后两行多出来一个对手叫\"时间\" → ",[248,1419,1420],{},"答案是 Timer",[15,1422,1423],{},"什么时候才需要物理引擎？当表里出现\"两个不规则形状重叠了吗\"\"它会被挡住吗\"这种回答。下一个需要它的游戏（打砖块）填这张表时答案会变成\"形状重叠\"——那时再配图层，就知道自己在配什么。",[1425,1426,1427],"blockquote",{},[15,1428,1429],{},"这一步的价值不是\"配图层\"，是\"想清楚再动手\"。很多游戏（网格类、回合制、卡牌）根本用不到物理引擎，硬配图层只是白做功。",[15,1431,1432],{},"下一章开 Godot，把这些数变成项目设置。",{"title":43,"searchDepth":61,"depth":61,"links":1434},[1435,1436,1437],{"id":1161,"depth":61,"text":1161},{"id":1242,"depth":61,"text":1243},{"id":1256,"depth":61,"text":1257},"2026-09-01",{},"\u002Fdevlog\u002Fxggame-snake\u002F01-design",{"title":1149,"description":1137},"devlog\u002Fxggame-snake\u002F01-design","dspGLT_dt9cPUJFowsqBo5wvyalTU63UddPK3JCXLQQ",{"id":1445,"title":1446,"body":1447,"cover":1135,"date":1887,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":1888,"navigation":71,"path":1889,"seo":1890,"stem":1891,"toc":71,"__hash__":1892},"devlog\u002Fdevlog\u002Fxggame-snake\u002F02-setup.md","项目设置",{"type":7,"value":1448,"toc":1880},[1449,1452,1456,1463,1470,1480,1497,1501,1504,1553,1563,1566,1572,1576,1583,1696,1710,1719,1725,1729,1743,1820,1834,1840,1843,1868,1877],[15,1450,1451],{},"为什么\"设置\"先于\"代码\"？因为分辨率、格子数、按键名全都会变成代码里的常量和字符串。先在设置里定下来，代码只要抄一次；反过来先写代码再改设置，每个用到的地方都要改。",[10,1453,1455],{"id":1454},"目录先建场景后建","目录先建，场景后建",[15,1457,1458,1459,1462],{},"新建项目（渲染器选",[248,1460,1461],{},"兼容","，Web 导出的硬要求），第一件事不是新建场景，是建文件夹：",[38,1464,1468],{"className":1465,"code":1467,"language":168},[1466],"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",[18,1469,1467],{"__ignoreMap":43},[15,1471,1472,1479],{},[248,1473,1474,1475,1478],{},"没有 ",[18,1476,1477],{},"scenes\u002F"," 就先建场景，它会落在根目录，以后挪要改一堆引用。"," 先有抽屉，再往里放东西。",[15,1481,1482,1485,1486,1489,1490,1493,1494],{},[18,1483,1484],{},"autoload\u002F"," 单独一层，不拆成 ",[18,1487,1488],{},"scenes\u002Fautoload\u002F"," + ",[18,1491,1492],{},"scripts\u002Fautoload\u002F","：全局单例是一个\"类别\"，总共 4～5 个文件，拆到两处只是让你多点两次。",[248,1495,1496],{},"按\"是什么\"分，不按\"文件类型\"分。",[10,1498,1500],{"id":1499},"分辨率先定格子再算画面","分辨率：先定格子，再算画面",[15,1502,1503],{},"项目 → 项目设置 → 显示 → 窗口：",[288,1505,1506,1515],{},[291,1507,1508],{},[294,1509,1510,1512],{},[297,1511,1173],{},[297,1513,1514],{},"值",[307,1516,1517,1525,1535,1545],{},[294,1518,1519,1522],{},[312,1520,1521],{},"视口宽 \u002F 高",[312,1523,1524],{},"1920 × 1080",[294,1526,1527,1530],{},[312,1528,1529],{},"拉伸 → 模式",[312,1531,1532],{},[18,1533,1534],{},"canvas_items",[294,1536,1537,1540],{},[312,1538,1539],{},"拉伸 → 比例",[312,1541,1542],{},[18,1543,1544],{},"keep",[294,1546,1547,1550],{},[312,1548,1549],{},"尺寸 → 窗口宽度 \u002F 高度覆盖",[312,1551,1552],{},"1280 × 720",[15,1554,1555,1556,1559,1560],{},"每格 20 像素 → 96 × 54 格。为什么是 20？一格的像素数必须",[248,1557,1558],{},"同时整除 1920 和 1080","，蛇才不会卡在半格上。能整除的只有 8 · 10 · 12 · 15 · 20 · 24 · 30 · 40 · 60 · 120——想换档位只能从这里挑，挑 16 会得到 67.5 格。",[248,1561,1562],{},"格子类游戏先定每格像素，再算格数，永远这个顺序。",[15,1564,1565],{},"\"窗口覆盖 1280×720\"是什么：视口是\"画有多大\"，窗口是\"看画的框有多大\"。画是 1080p 的，框在显示器上开到 1080p 会连标题栏一起超出屏幕。框缩到 720p，画照样是 1080p，拉伸模式会把它等比缩进去。发布后玩家的窗口多大都一样，这两项只影响自己测试。",[15,1567,1568],{},[1117,1569],{"alt":1570,"src":1571},"02-setup-窗口设置","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F02-setup-%E7%AA%97%E5%8F%A3%E8%AE%BE%E7%BD%AE.png",[10,1573,1575],{"id":1574},"输入映射键盘和手柄一起绑","输入映射：键盘和手柄一起绑",[15,1577,1578,1579,1582],{},"项目 → 项目设置 → 输入映射。",[248,1580,1581],{},"每个动作既绑键盘也绑手柄","——手柄不是一个功能，是一种假设，从第一天就得成立：",[288,1584,1585,1598],{},[291,1586,1587],{},[294,1588,1589,1592,1595],{},[297,1590,1591],{},"动作名",[297,1593,1594],{},"键盘",[297,1596,1597],{},"手柄",[307,1599,1600,1613,1626,1639,1652,1665,1681],{},[294,1601,1602,1607,1610],{},[312,1603,1604],{},[18,1605,1606],{},"move_up",[312,1608,1609],{},"↑ 和 W",[312,1611,1612],{},"十字键 ↑ 和 左摇杆往上",[294,1614,1615,1620,1623],{},[312,1616,1617],{},[18,1618,1619],{},"move_down",[312,1621,1622],{},"↓ 和 S",[312,1624,1625],{},"十字键 ↓ 和 左摇杆往下",[294,1627,1628,1633,1636],{},[312,1629,1630],{},[18,1631,1632],{},"move_left",[312,1634,1635],{},"← 和 A",[312,1637,1638],{},"十字键 ← 和 左摇杆往左",[294,1640,1641,1646,1649],{},[312,1642,1643],{},[18,1644,1645],{},"move_right",[312,1647,1648],{},"→ 和 D",[312,1650,1651],{},"十字键 → 和 左摇杆往右",[294,1653,1654,1659,1662],{},[312,1655,1656],{},[18,1657,1658],{},"pause",[312,1660,1661],{},"Esc 和 P",[312,1663,1664],{},"Start",[294,1666,1667,1673,1676],{},[312,1668,1669,1672],{},[18,1670,1671],{},"ui_accept","（内置）",[312,1674,1675],{},"已有 Enter \u002F 空格",[312,1677,1678],{},[248,1679,1680],{},"补一个 A",[294,1682,1683,1688,1691],{},[312,1684,1685,1672],{},[18,1686,1687],{},"ui_cancel",[312,1689,1690],{},"已有 Esc",[312,1692,1693],{},[248,1694,1695],{},"补一个 B",[15,1697,1698,1699,1705,1706,1709],{},"最后两行要勾「显示内置动作」才看得到。",[248,1700,1701,1702,1704],{},"Godot 4 默认 ",[18,1703,1671],{}," 只有 Enter 和空格，没有手柄键","（方向类的 ",[18,1707,1708],{},"ui_up"," 等有），不补的话手柄能移焦点但按不下按钮——这个坑我在第 9 章栽了一次才知道。",[15,1711,1712,1713,1715,1716],{},"为什么要给按键起名字，代码里直接判断\"按了 W\"不行吗？行，但你看上面那张表：一个名字挂了 4 个键。代码只认 ",[18,1714,1606],{},"，至于是 W、↑、十字键还是摇杆，代码根本不知道也不用知道。",[248,1717,1718],{},"配置和代码分离。",[15,1720,1721],{},[1117,1722],{"alt":1723,"src":1724},"02-setup-输入映射","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F02-setup-%E8%BE%93%E5%85%A5%E6%98%A0%E5%B0%84.png",[10,1726,1728],{"id":1727},"autoload-骨架先把名字占住","Autoload 骨架：先把名字占住",[15,1730,1731,1732,1735,1736,1739,1740,36],{},"阶段 2 的代码会写 ",[18,1733,1734],{},"EventBus.snake_died.emit()"," 这种句子——",[18,1737,1738],{},"EventBus"," 这个名字必须先存在，代码才不报错。所以现在就建 4 个\"空\"文件，每个脚本只写一行 ",[18,1741,1742],{},"extends Node",[288,1744,1745,1758],{},[291,1746,1747],{},[294,1748,1749,1752,1755],{},[297,1750,1751],{},"顺序",[297,1753,1754],{},"名称",[297,1756,1757],{},"路径",[307,1759,1760,1776,1791,1805],{},[294,1761,1762,1767,1771],{},[312,1763,1764],{},[248,1765,1766],{},"1",[312,1768,1769],{},[18,1770,1738],{},[312,1772,1773],{},[18,1774,1775],{},"res:\u002F\u002Fautoload\u002Fevent_bus.gd",[294,1777,1778,1781,1786],{},[312,1779,1780],{},"2",[312,1782,1783],{},[18,1784,1785],{},"SaveManager",[312,1787,1788],{},[18,1789,1790],{},"res:\u002F\u002Fautoload\u002Fsave_manager.gd",[294,1792,1793,1796,1800],{},[312,1794,1795],{},"3",[312,1797,1798],{},[18,1799,178],{},[312,1801,1802],{},[18,1803,1804],{},"res:\u002F\u002Fautoload\u002Fgame_manager.gd",[294,1806,1807,1810,1815],{},[312,1808,1809],{},"4",[312,1811,1812],{},[18,1813,1814],{},"AudioManager",[312,1816,1817],{},[18,1818,1819],{},"res:\u002F\u002Fautoload\u002Faudio_manager.tscn",[15,1821,1822,1823,1826,1827,1830,1831],{},"顺序不能乱：列表从上往下依次加载，",[18,1824,1825],{},"GameManager._ready()"," 里会写 ",[18,1828,1829],{},"EventBus.xxx.connect(...)","，如果 EventBus 排在它后面，那一刻这个名字还不存在，游戏直接崩。",[248,1832,1833],{},"规矩：谁被别人连，谁排前面。EventBus 永远第一。",[15,1835,1836],{},[1117,1837],{"alt":1838,"src":1839},"02-setup-自动加载","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F02-setup-%E8%87%AA%E5%8A%A8%E5%8A%A0%E8%BD%BD.png",[10,1841,1842],{"id":1842},"git",[15,1844,1845,1846,1849,1850,383,1853,383,1856,1859,1860,1863,1864,1867],{},"项目 → 版本控制 → 生成版本控制元数据，",[18,1847,1848],{},".gitignore"," 里加 ",[18,1851,1852],{},"build\u002F",[18,1854,1855],{},"*.zip",[18,1857,1858],{},".DS_Store","，根目录建一个空的 ",[18,1861,1862],{},"CREDITS.md","，然后 ",[18,1865,1866],{},"git init -b main","、第一条提交。",[15,1869,1870,1871,356,1874,1876],{},"为什么现在就 git，代码都还没写？因为下一章会大改。有了第一条提交，改坏了随时能回到\"干净的骨架\"。",[248,1872,1873],{},"git 是后悔药，后悔药要在犯错之前买。",[18,1875,1862],{}," 现在就建，是因为小鸟那次三个音效的来源完全想不起来了——以后每引入一份素材当场写一行。",[15,1878,1879],{},"下一章写代码，先让蛇在控制台里\"动\"起来。",{"title":43,"searchDepth":61,"depth":61,"links":1881},[1882,1883,1884,1885,1886],{"id":1454,"depth":61,"text":1455},{"id":1499,"depth":61,"text":1500},{"id":1574,"depth":61,"text":1575},{"id":1727,"depth":61,"text":1728},{"id":1842,"depth":61,"text":1842},"2026-09-02",{},"\u002Fdevlog\u002Fxggame-snake\u002F02-setup",{"title":1446,"description":1137},"devlog\u002Fxggame-snake\u002F02-setup","5cE2MyvWU2DCeFS0duNxsvEIBxPmRKFgGH84GPFUPMQ",{"id":1894,"title":1895,"body":1896,"cover":1135,"date":2614,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":2615,"navigation":71,"path":2616,"seo":2617,"stem":2618,"toc":71,"__hash__":2619},"devlog\u002Fdevlog\u002Fxggame-snake\u002F03-snake-array.md","原型1：蛇是一个数组",{"type":7,"value":1897,"toc":2608},[1898,1905,1909,1988,1991,1994,2006,2012,2018,2029,2032,2375,2378,2444,2451,2455,2468,2573,2584,2589,2595,2602,2605],[15,1899,1900,1901,1904],{},"贪吃蛇玩法早就定了，为什么还要\"白块原型\"？为了",[248,1902,1903],{},"逻辑与表现分离","：先让数据对，再让画面好看。这样出 bug 时能一眼定位——数组里坐标就是错的 → 逻辑 bug；数组对但画错位置 → 渲染 bug。一上来就连美术一起做，出了问题得同时怀疑两层，调试时间翻倍。",[10,1906,1908],{"id":1907},"先换脑子贪吃蛇和小鸟的根本区别","先换脑子：贪吃蛇和小鸟的根本区别",[288,1910,1911,1923],{},[291,1912,1913],{},[294,1914,1915,1917,1920],{},[297,1916],{},[297,1918,1919],{},"小鸟（连续）",[297,1921,1922],{},"贪吃蛇（离散）",[307,1924,1925,1944,1957,1975],{},[294,1926,1927,1930,1936],{},[312,1928,1929],{},"位置",[312,1931,1932,1933],{},"任意小数 ",[18,1934,1935],{},"(317.4, 892.1)",[312,1937,1938,356,1941],{},[248,1939,1940],{},"只能在格子上",[18,1942,1943],{},"(5, 8)",[294,1945,1946,1949,1952],{},[312,1947,1948],{},"移动",[312,1950,1951],{},"每帧挪一点点",[312,1953,1954],{},[248,1955,1956],{},"每 0.15 秒跳一整格",[294,1958,1959,1962,1968],{},[312,1960,1961],{},"谁在驱动",[312,1963,1964,1967],{},[18,1965,1966],{},"_physics_process(delta)"," 每秒 60 次",[312,1969,1970],{},[248,1971,1972,1974],{},[18,1973,1385],{}," 到点发一次信号",[294,1976,1977,1980,1983],{},[312,1978,1979],{},"碰撞",[312,1981,1982],{},"物理引擎算",[312,1984,1985],{},[248,1986,1987],{},"比较两个格子坐标相不相等",[15,1989,1990],{},"这是整个项目最重要的观念转变：贪吃蛇里几乎没有\"物理\"，只有\"坐标\"和\"时间\"。",[10,1992,1993],{"id":1993},"搭场景",[15,1995,1996,1999,2000,25,2003,2005],{},[18,1997,1998],{},"scenes\u002Fmain.tscn","，根节点 ",[18,2001,2002],{},"Node2D",[18,2004,20],{},"，下面 8 个子节点：",[38,2007,2010],{"className":2008,"code":2009,"language":168},[1466],"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",[18,2011,2009],{"__ignoreMap":43},[15,2013,2014,2015],{},"为什么蛇和食物是\"空容器\"，星星却直接是一个方块？数量不固定的东西（蛇身 3～400 节，食物 0～3 个）用\"容器 + 动态生成子节点\"来画；数量固定最多 1 个的，直接放一个节点，用显示\u002F隐藏控制。",[248,2016,2017],{},"\"有几个\"决定\"怎么画\"。",[15,2019,2020,2021,2024,2025,2028],{},"为什么 4 个 Timer 不合成一个？一个 Timer 只管一件事，名字就是说明书：看到 ",[18,2022,2023],{},"StarLifeTimer"," 在跑，就知道\"星星还活着\"。合成一个省 3 个节点，换来一堆 ",[18,2026,2027],{},"if"," 判断\"这次响是为了什么\"。节点便宜，脑子贵。",[10,2030,2031],{"id":2031},"数据结构",[38,2033,2035],{"className":40,"code":2034,"language":42,"meta":43,"style":43},"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",[18,2036,2037,2044,2048,2053,2071,2086,2101,2116,2146,2150,2155,2182,2205,2223,2243,2247,2266,2284,2288,2293,2331,2345,2349,2363],{"__ignoreMap":43},[47,2038,2039,2041],{"class":49,"line":50},[47,2040,54],{"class":53},[47,2042,2043],{"class":57}," Node2D\n",[47,2045,2046],{"class":49,"line":61},[47,2047,72],{"emptyLinePlaceholder":71},[47,2049,2050],{"class":49,"line":68},[47,2051,2052],{"class":64},"## ===== 常量：整个游戏只认这几个数 =====\n",[47,2054,2055,2058,2061,2064,2068],{"class":49,"line":75},[47,2056,2057],{"class":82},"const",[47,2059,2060],{"class":86}," GRID_W",[47,2062,2063],{"class":90}," :=",[47,2065,2067],{"class":2066},"sqbOQ"," 96",[47,2069,2070],{"class":64},"                     # 横向几格\n",[47,2072,2073,2075,2078,2080,2083],{"class":49,"line":107},[47,2074,2057],{"class":82},[47,2076,2077],{"class":86}," GRID_H",[47,2079,2063],{"class":90},[47,2081,2082],{"class":2066}," 54",[47,2084,2085],{"class":64},"                     # 纵向几格\n",[47,2087,2088,2090,2093,2095,2098],{"class":49,"line":112},[47,2089,2057],{"class":82},[47,2091,2092],{"class":86}," CELL_SIZE",[47,2094,2063],{"class":90},[47,2096,2097],{"class":2066}," 20",[47,2099,2100],{"class":64},"                  # 一格是几个像素   96 × 20 = 1920，54 × 20 = 1080\n",[47,2102,2103,2105,2108,2110,2113],{"class":49,"line":133},[47,2104,2057],{"class":82},[47,2106,2107],{"class":86}," MAX_FOOD",[47,2109,2063],{"class":90},[47,2111,2112],{"class":2066}," 3",[47,2114,2115],{"class":64},"                    # 场上最多几个普通食物\n",[47,2117,2118,2120,2123,2125,2128,2130,2133,2135,2137,2139,2141,2143],{"class":49,"line":160},[47,2119,2057],{"class":82},[47,2121,2122],{"class":86}," NO_CELL",[47,2124,2063],{"class":90},[47,2126,2127],{"class":57}," Vector2i",[47,2129,151],{"class":90},[47,2131,2132],{"class":82},"-",[47,2134,1766],{"class":2066},[47,2136,1035],{"class":145},[47,2138,2132],{"class":82},[47,2140,1766],{"class":2066},[47,2142,186],{"class":90},[47,2144,2145],{"class":64},"      # 「没有」—— 一个棋盘上不存在的格子\n",[47,2147,2148],{"class":49,"line":192},[47,2149,72],{"emptyLinePlaceholder":71},[47,2151,2152],{"class":49,"line":197},[47,2153,2154],{"class":64},"## ===== 数据：蛇 = 坐标数组，第 0 个是头 =====\n",[47,2156,2157,2160,2163,2165,2168,2171,2174,2177,2179],{"class":49,"line":223},[47,2158,2159],{"class":82},"var",[47,2161,2162],{"class":86}," snake_body",[47,2164,91],{"class":90},[47,2166,2167],{"class":57}," Array",[47,2169,2170],{"class":90},"[",[47,2172,2173],{"class":57},"Vector2i",[47,2175,2176],{"class":90},"]",[47,2178,97],{"class":90},[47,2180,2181],{"class":90}," [\n",[47,2183,2184,2187,2189,2192,2194,2197,2199,2202],{"class":49,"line":584},[47,2185,2186],{"class":57},"    Vector2i",[47,2188,151],{"class":90},[47,2190,2191],{"class":2066},"48",[47,2193,1035],{"class":145},[47,2195,2196],{"class":2066},"27",[47,2198,186],{"class":90},[47,2200,2201],{"class":145},",   ",[47,2203,2204],{"class":64},"# 头（正中间）\n",[47,2206,2207,2209,2211,2214,2216,2218,2220],{"class":49,"line":600},[47,2208,2186],{"class":57},[47,2210,151],{"class":90},[47,2212,2213],{"class":2066},"47",[47,2215,1035],{"class":145},[47,2217,2196],{"class":2066},[47,2219,186],{"class":90},[47,2221,2222],{"class":145},",\n",[47,2224,2225,2227,2229,2232,2234,2236,2238,2240],{"class":49,"line":623},[47,2226,2186],{"class":57},[47,2228,151],{"class":90},[47,2230,2231],{"class":2066},"46",[47,2233,1035],{"class":145},[47,2235,2196],{"class":2066},[47,2237,186],{"class":90},[47,2239,2201],{"class":145},[47,2241,2242],{"class":64},"# 尾\n",[47,2244,2245],{"class":49,"line":628},[47,2246,1045],{"class":90},[47,2248,2249,2251,2254,2256,2258,2260,2263],{"class":49,"line":657},[47,2250,2159],{"class":82},[47,2252,2253],{"class":86}," direction",[47,2255,2063],{"class":90},[47,2257,2127],{"class":57},[47,2259,139],{"class":90},[47,2261,2262],{"class":53},"RIGHT",[47,2264,2265],{"class":64},"        # 现在正在往哪走\n",[47,2267,2268,2270,2273,2275,2277,2279,2281],{"class":49,"line":690},[47,2269,2159],{"class":82},[47,2271,2272],{"class":86}," next_direction",[47,2274,2063],{"class":90},[47,2276,2127],{"class":57},[47,2278,139],{"class":90},[47,2280,2262],{"class":53},[47,2282,2283],{"class":64},"   # 玩家刚按的方向，下一步才生效\n",[47,2285,2286],{"class":49,"line":703},[47,2287,72],{"emptyLinePlaceholder":71},[47,2289,2290],{"class":49,"line":719},[47,2291,2292],{"class":64},"## ===== 数据：食物 =====\n",[47,2294,2295,2297,2300,2302,2304,2306,2308,2310,2312,2314,2316,2318,2321,2323,2325,2328],{"class":49,"line":724},[47,2296,2159],{"class":82},[47,2298,2299],{"class":86}," foods",[47,2301,91],{"class":90},[47,2303,2167],{"class":57},[47,2305,2170],{"class":90},[47,2307,2173],{"class":57},[47,2309,2176],{"class":90},[47,2311,97],{"class":90},[47,2313,1026],{"class":90},[47,2315,2173],{"class":57},[47,2317,151],{"class":90},[47,2319,2320],{"class":2066},"60",[47,2322,1035],{"class":145},[47,2324,2196],{"class":2066},[47,2326,2327],{"class":90},")]",[47,2329,2330],{"class":64},"   # 普通食物，先手放一个\n",[47,2332,2333,2335,2338,2340,2342],{"class":49,"line":740},[47,2334,2159],{"class":82},[47,2336,2337],{"class":86}," star",[47,2339,2063],{"class":90},[47,2341,2122],{"class":86},[47,2343,2344],{"class":64},"                    # 星星，最多 1 颗；NO_CELL 表示现在没有\n",[47,2346,2347],{"class":49,"line":756},[47,2348,72],{"emptyLinePlaceholder":71},[47,2350,2351,2353,2355,2357,2359,2361],{"class":49,"line":779},[47,2352,115],{"class":82},[47,2354,118],{"class":78},[47,2356,121],{"class":90},[47,2358,124],{"class":82},[47,2360,127],{"class":57},[47,2362,130],{"class":90},[47,2364,2365,2368,2370,2373],{"class":49,"line":784},[47,2366,2367],{"class":78},"    print",[47,2369,151],{"class":90},[47,2371,2372],{"class":86},"snake_body",[47,2374,157],{"class":90},[15,2376,2377],{},"几行关键的解释：",[836,2379,2380,2395,2406,2416,2429],{},[839,2381,2382,2386,2387,2390,2391,2394],{},[248,2383,2384],{},[18,2385,2173],{},"：Vector 2 integer，只存整数的二维坐标。格子坐标天然是整数；用 ",[18,2388,2389],{},"Vector2"," 会出现 ",[18,2392,2393],{},"(5.0000001, 8)","，比较\"相等\"会失败",[839,2396,2397,2402,2403,2405],{},[248,2398,2399],{},[18,2400,2401],{},"Array[Vector2i]","：类型化数组，只装 ",[18,2404,2173],{},"，塞别的当场报错",[839,2407,2408,2411,2412,2415],{},[248,2409,2410],{},"第 0 个是头","：移动 = 头部插一格 + 尾部删一格。头放数组开头，",[18,2413,2414],{},"insert(0, ...)"," 最直观",[839,2417,2418,2428],{},[248,2419,2420,2423,2424,2427],{},[18,2421,2422],{},"foods"," 是数组，",[18,2425,2426],{},"star"," 不是","：和场景结构一一对应——数据结构和场景结构长得一样",[839,2430,2431,2436,2437,2439,2440,2443],{},[248,2432,2433],{},[18,2434,2435],{},"NO_CELL = (-1, -1)","：哨兵值。",[18,2438,2173],{}," 类型的变量不能存 ",[18,2441,2442],{},"null","，那\"现在没有星星\"怎么表示？用一个棋盘上不可能出现的坐标当\"没有\"",[15,2445,2446,2447,2450],{},"F5，输出面板打印 ",[18,2448,2449],{},"[(48, 27), (47, 27), (46, 27)]","。画面还是没东西，正常——先验数据，再画。",[10,2452,2454],{"id":2453},"让蛇动起来先在控制台动","让蛇\"动\"起来（先在控制台动）",[15,2456,2457,2458,2461,2462,2465,2466,36],{},"选中 ",[18,2459,2460],{},"MoveTimer"," → 节点面板 → 信号 → 双击 ",[18,2463,2464],{},"timeout()"," → 连接到 ",[18,2467,20],{},[38,2469,2471],{"className":40,"code":2470,"language":42,"meta":43,"style":43},"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",[18,2472,2473,2488,2500,2525,2549,2563],{"__ignoreMap":43},[47,2474,2475,2477,2480,2482,2484,2486],{"class":49,"line":50},[47,2476,115],{"class":82},[47,2478,2479],{"class":78}," _on_move_timer_timeout",[47,2481,121],{"class":90},[47,2483,124],{"class":82},[47,2485,127],{"class":57},[47,2487,130],{"class":90},[47,2489,2490,2493,2495,2497],{"class":49,"line":61},[47,2491,2492],{"class":86},"    direction",[47,2494,97],{"class":90},[47,2496,2272],{"class":86},[47,2498,2499],{"class":64},"          # 玩家最近按的方向，现在才真正生效\n",[47,2501,2502,2505,2508,2510,2512,2514,2517,2519,2522],{"class":49,"line":68},[47,2503,2504],{"class":82},"    var",[47,2506,2507],{"class":86}," new_head",[47,2509,2063],{"class":90},[47,2511,2162],{"class":86},[47,2513,2170],{"class":90},[47,2515,2516],{"class":2066},"0",[47,2518,2176],{"class":90},[47,2520,2521],{"class":82}," +",[47,2523,2524],{"class":86}," direction\n",[47,2526,2527,2530,2532,2535,2537,2539,2542,2544,2546],{"class":49,"line":75},[47,2528,2529],{"class":86},"    snake_body",[47,2531,139],{"class":145},[47,2533,2534],{"class":78},"insert",[47,2536,151],{"class":90},[47,2538,2516],{"class":2066},[47,2540,2541],{"class":90},",",[47,2543,2507],{"class":86},[47,2545,186],{"class":90},[47,2547,2548],{"class":64},"      # ① 头部插入新格子\n",[47,2550,2551,2553,2555,2558,2560],{"class":49,"line":107},[47,2552,2529],{"class":86},[47,2554,139],{"class":145},[47,2556,2557],{"class":78},"pop_back",[47,2559,121],{"class":90},[47,2561,2562],{"class":64},"               # ② 删掉尾巴\n",[47,2564,2565,2567,2569,2571],{"class":49,"line":112},[47,2566,2367],{"class":78},[47,2568,151],{"class":90},[47,2570,2372],{"class":86},[47,2572,157],{"class":90},[15,2574,2575,36,2581,2583],{},[248,2576,2577,2578],{},"为什么用 Timer，不用小鸟那种 ",[18,2579,2580],{},"_physics_process",[18,2582,2580],{}," 每秒跑 60 次，适合\"每帧挪一点点\"。贪吃蛇是\"每 0.15 秒跳一格\"，用 Timer 到点才动一次，天然就是离散的。",[15,2585,2586,36],{},[248,2587,2588],{},"为什么移动是\"插头删尾\"，不是\"每一节往前挪\"",[38,2590,2593],{"className":2591,"code":2592,"language":168},[1466],"移动前  [头(48,27)] [(47,27)] [(46,27)]\n        ↓ 向右\n① 插头  [新头(49,27)] [(48,27)] [(47,27)] [(46,27)]\n② 删尾  [新头(49,27)] [(48,27)] [(47,27)]\n",[18,2594,2592],{"__ignoreMap":43},[15,2596,2597,2598,2601],{},"逐节挪：蛇 50 节要算 50 次，而且要从尾往头算（不然会覆盖）。插头删尾：永远只操作 2 个元素，不管多长。",[248,2599,2600],{},"而且\"吃到食物\"只要\"不删尾\"就自动变长——一行都不用多写。"," 这是贪吃蛇最漂亮的设计。",[15,2603,2604],{},"F5，输出面板每 0.15 秒打印一次，头的 x 一直加 1。数据先对了，下一章画出来。",[1125,2606,2607],{},"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":43,"searchDepth":61,"depth":61,"links":2609},[2610,2611,2612,2613],{"id":1907,"depth":61,"text":1908},{"id":1993,"depth":61,"text":1993},{"id":2031,"depth":61,"text":2031},{"id":2453,"depth":61,"text":2454},"2026-09-03",{},"\u002Fdevlog\u002Fxggame-snake\u002F03-snake-array",{"title":1895,"description":1137},"devlog\u002Fxggame-snake\u002F03-snake-array","xHXPui_5OOXPvhwukHEoLGlvpvRQnLEf-cDzaqo-eBM",{"id":2621,"title":2622,"body":2623,"cover":1135,"date":4066,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":4067,"navigation":71,"path":4068,"seo":4069,"stem":4070,"toc":71,"__hash__":4071},"devlog\u002Fdevlog\u002Fxggame-snake\u002F04-draw-and-collide.md","原型2：画面是数据的镜子",{"type":7,"value":2624,"toc":4060},[2625,2628,2643,3059,3116,3123,3138,3144,3147,3357,3360,3369,3395,3398,3411,3415,3425,3803,3832,3835,4020,4023,4054,4057],[10,2626,2627],{"id":2627},"画出来",[15,2629,2630,2631,2633,2634,892,2636,2639,2640,36],{},"加在 ",[18,2632,850],{}," 里，并在 ",[18,2635,245],{},[18,2637,2638],{},"_on_move_timer_timeout()"," 末尾各调一次 ",[18,2641,2642],{},"redraw()",[38,2644,2646],{"className":40,"code":2645,"language":42,"meta":43,"style":43},"## 照着数据画方块。每次都先清空再重画 —— 简单，先跑通\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",[18,2647,2648,2653,2668,2693,2708,2729,2742,2746,2759,2787,2799,2824,2828,2852,2882,2909,2913,2918,2960,2979,3002,3028,3042],{"__ignoreMap":43},[47,2649,2650],{"class":49,"line":50},[47,2651,2652],{"class":64},"## 照着数据画方块。每次都先清空再重画 —— 简单，先跑通\n",[47,2654,2655,2657,2660,2662,2664,2666],{"class":49,"line":61},[47,2656,115],{"class":82},[47,2658,2659],{"class":78}," redraw",[47,2661,121],{"class":90},[47,2663,124],{"class":82},[47,2665,127],{"class":57},[47,2667,130],{"class":90},[47,2669,2670,2673,2676,2679,2681,2684,2686,2689,2691],{"class":49,"line":68},[47,2671,2672],{"class":53},"    for",[47,2674,2675],{"class":86}," cell",[47,2677,2678],{"class":53}," in",[47,2680,100],{"class":53},[47,2682,2683],{"class":103},"SnakeCells",[47,2685,139],{"class":145},[47,2687,2688],{"class":78},"get_children",[47,2690,121],{"class":90},[47,2692,130],{"class":145},[47,2694,2695,2698,2700,2703,2705],{"class":49,"line":75},[47,2696,2697],{"class":86},"        cell",[47,2699,139],{"class":145},[47,2701,2702],{"class":78},"queue_free",[47,2704,121],{"class":90},[47,2706,2707],{"class":64},"                       # 删掉上一次画的蛇\n",[47,2709,2710,2712,2714,2716,2718,2721,2723,2725,2727],{"class":49,"line":107},[47,2711,2672],{"class":53},[47,2713,2675],{"class":86},[47,2715,2678],{"class":53},[47,2717,100],{"class":53},[47,2719,2720],{"class":103},"FoodCells",[47,2722,139],{"class":145},[47,2724,2688],{"class":78},[47,2726,121],{"class":90},[47,2728,130],{"class":145},[47,2730,2731,2733,2735,2737,2739],{"class":49,"line":112},[47,2732,2697],{"class":86},[47,2734,139],{"class":145},[47,2736,2702],{"class":78},[47,2738,121],{"class":90},[47,2740,2741],{"class":64},"                       # 删掉上一次画的食物\n",[47,2743,2744],{"class":49,"line":133},[47,2745,72],{"emptyLinePlaceholder":71},[47,2747,2748,2750,2753,2755,2757],{"class":49,"line":160},[47,2749,2672],{"class":53},[47,2751,2752],{"class":86}," grid_pos",[47,2754,2678],{"class":53},[47,2756,2162],{"class":86},[47,2758,130],{"class":145},[47,2760,2761,2764,2766,2769,2771,2773,2775,2777,2780,2782,2785],{"class":49,"line":192},[47,2762,2763],{"class":78},"        _draw_cell",[47,2765,151],{"class":90},[47,2767,2768],{"class":53},"$",[47,2770,2683],{"class":103},[47,2772,2541],{"class":90},[47,2774,2752],{"class":86},[47,2776,2541],{"class":90},[47,2778,2779],{"class":57}," Color",[47,2781,139],{"class":90},[47,2783,2784],{"class":53},"GREEN",[47,2786,157],{"class":90},[47,2788,2789,2791,2793,2795,2797],{"class":49,"line":197},[47,2790,2672],{"class":53},[47,2792,2752],{"class":86},[47,2794,2678],{"class":53},[47,2796,2299],{"class":86},[47,2798,130],{"class":145},[47,2800,2801,2803,2805,2807,2809,2811,2813,2815,2817,2819,2822],{"class":49,"line":223},[47,2802,2763],{"class":78},[47,2804,151],{"class":90},[47,2806,2768],{"class":53},[47,2808,2720],{"class":103},[47,2810,2541],{"class":90},[47,2812,2752],{"class":86},[47,2814,2541],{"class":90},[47,2816,2779],{"class":57},[47,2818,139],{"class":90},[47,2820,2821],{"class":53},"RED",[47,2823,157],{"class":90},[47,2825,2826],{"class":49,"line":584},[47,2827,72],{"emptyLinePlaceholder":71},[47,2829,2830,2833,2836,2838,2840,2842,2844,2847,2849],{"class":49,"line":600},[47,2831,2832],{"class":53},"    $",[47,2834,2835],{"class":103},"StarCell",[47,2837,139],{"class":90},[47,2839,665],{"class":86},[47,2841,97],{"class":90},[47,2843,2337],{"class":86},[47,2845,2846],{"class":82}," !=",[47,2848,2122],{"class":86},[47,2850,2851],{"class":64},"         # 有星星才显示\n",[47,2853,2854,2856,2858,2860,2863,2865,2868,2870,2873,2875,2877,2879],{"class":49,"line":623},[47,2855,2832],{"class":53},[47,2857,2835],{"class":103},[47,2859,139],{"class":90},[47,2861,2862],{"class":86},"size",[47,2864,97],{"class":90},[47,2866,2867],{"class":57}," Vector2",[47,2869,151],{"class":90},[47,2871,2872],{"class":86},"CELL_SIZE",[47,2874,1035],{"class":145},[47,2876,2872],{"class":86},[47,2878,186],{"class":90},[47,2880,2881],{"class":64},"   # 尺寸也由代码定，别信场景里的数\n",[47,2883,2884,2886,2888,2890,2893,2895,2897,2899,2901,2903,2906],{"class":49,"line":628},[47,2885,2832],{"class":53},[47,2887,2835],{"class":103},[47,2889,139],{"class":90},[47,2891,2892],{"class":86},"position",[47,2894,97],{"class":90},[47,2896,2867],{"class":57},[47,2898,151],{"class":90},[47,2900,2426],{"class":86},[47,2902,186],{"class":90},[47,2904,2905],{"class":82}," *",[47,2907,2908],{"class":86}," CELL_SIZE\n",[47,2910,2911],{"class":49,"line":657},[47,2912,72],{"emptyLinePlaceholder":71},[47,2914,2915],{"class":49,"line":690},[47,2916,2917],{"class":64},"## 在某个容器下画一个格子。蛇和食物都用它\n",[47,2919,2920,2922,2925,2927,2930,2932,2935,2937,2939,2941,2943,2945,2948,2950,2952,2954,2956,2958],{"class":49,"line":703},[47,2921,115],{"class":82},[47,2923,2924],{"class":78}," _draw_cell",[47,2926,151],{"class":90},[47,2928,2929],{"class":145},"parent",[47,2931,91],{"class":90},[47,2933,2934],{"class":57}," Node",[47,2936,2541],{"class":90},[47,2938,2752],{"class":145},[47,2940,91],{"class":90},[47,2942,2127],{"class":57},[47,2944,2541],{"class":90},[47,2946,2947],{"class":145}," color",[47,2949,91],{"class":90},[47,2951,2779],{"class":57},[47,2953,186],{"class":90},[47,2955,124],{"class":82},[47,2957,127],{"class":57},[47,2959,130],{"class":90},[47,2961,2962,2964,2967,2969,2972,2974,2977],{"class":49,"line":719},[47,2963,2504],{"class":82},[47,2965,2966],{"class":86}," rect",[47,2968,2063],{"class":90},[47,2970,2971],{"class":57}," ColorRect",[47,2973,139],{"class":145},[47,2975,2976],{"class":78},"new",[47,2978,831],{"class":90},[47,2980,2981,2984,2986,2988,2990,2992,2994,2996,2998,3000],{"class":49,"line":724},[47,2982,2983],{"class":86},"    rect",[47,2985,139],{"class":90},[47,2987,2862],{"class":86},[47,2989,97],{"class":90},[47,2991,2867],{"class":57},[47,2993,151],{"class":90},[47,2995,2872],{"class":86},[47,2997,1035],{"class":145},[47,2999,2872],{"class":86},[47,3001,157],{"class":90},[47,3003,3004,3006,3008,3010,3012,3014,3016,3019,3021,3023,3025],{"class":49,"line":740},[47,3005,2983],{"class":86},[47,3007,139],{"class":90},[47,3009,2892],{"class":86},[47,3011,97],{"class":90},[47,3013,2867],{"class":57},[47,3015,151],{"class":90},[47,3017,3018],{"class":86},"grid_pos",[47,3020,186],{"class":90},[47,3022,2905],{"class":82},[47,3024,2092],{"class":86},[47,3026,3027],{"class":64},"   # 格子坐标 → 像素坐标\n",[47,3029,3030,3032,3034,3037,3039],{"class":49,"line":756},[47,3031,2983],{"class":86},[47,3033,139],{"class":90},[47,3035,3036],{"class":86},"color",[47,3038,97],{"class":90},[47,3040,3041],{"class":86}," color\n",[47,3043,3044,3047,3049,3052,3054,3057],{"class":49,"line":779},[47,3045,3046],{"class":86},"    parent",[47,3048,139],{"class":145},[47,3050,3051],{"class":78},"add_child",[47,3053,151],{"class":90},[47,3055,3056],{"class":86},"rect",[47,3058,157],{"class":90},[836,3060,3061,3083,3089,3106],{},[839,3062,3063,3066,3067,3069,3070,3073,3074,3076,3077,3079,3080,3082],{},[18,3064,3065],{},"Vector2(grid_pos) * CELL_SIZE","：格子 ",[18,3068,1943],{}," × 20 = 像素 ",[18,3071,3072],{},"(100, 160)","。",[18,3075,2892],{}," 要 ",[18,3078,2389],{},"（小数型），所以先把 ",[18,3081,2173],{}," 转过去再乘",[839,3084,3085,3088],{},[18,3086,3087],{},"queue_free()","：把节点标记为\"这一帧结束后删掉\"，比立刻删安全",[839,3090,3091,3094,3095,3098,3099,3102,3103,3105],{},[18,3092,3093],{},"$StarCell.visible = star != NO_CELL","：右边算出来是 ",[18,3096,3097],{},"true"," \u002F ",[18,3100,3101],{},"false","，直接赋给 ",[18,3104,665],{},"，一行顶四行 if",[839,3107,3108,3109,3112,3113],{},"为什么抽一个 ",[18,3110,3111],{},"_draw_cell()","：蛇和食物画法一模一样，只是颜色和放哪不同。",[248,3114,3115],{},"同样的 5 行要写第二遍的时候，就该抽成函数",[15,3117,3118,3119,3122],{},"为什么每次都清空重画，不是挪现有方块？白块阶段追求\"最不容易错\"，不追求快。清空重画的逻辑只有一句话：",[248,3120,3121],{},"画面 = 数据","。你改数组，画面跟着变；绝对不要反过来（读方块的位置去推蛇在哪）。这就是\"数据驱动\"\"单一数据源\"在代码里的样子。",[1425,3124,3125],{},[15,3126,3127,3128,3130,3131,3133,3134,3137],{},"这里踩过一个坑：星星画出来是 4 格。因为 ",[18,3129,2835],{}," 是场景里摆好的节点，尺寸是当时在检查器里填的 40（Godot 新建 ColorRect 的默认值），40 ÷ 20 = 2，2×2 就是 4 格。同一个数存了两份——代码里一份、场景里一份——迟早对不上。所以上面 ",[18,3132,2642],{}," 里那句 ",[18,3135,3136],{},"$StarCell.size = ..."," 把尺寸也收归代码管。",[15,3139,3140],{},[1117,3141],{"alt":3142,"src":3143},"04-draw-白块","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F04-draw-%E7%99%BD%E5%9D%97.png",[10,3145,3146],{"id":3146},"方向控制",[38,3148,3150],{"className":40,"code":3149,"language":42,"meta":43,"style":43},"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",[18,3151,3152,3175,3208,3222,3255,3268,3299,3312,3344],{"__ignoreMap":43},[47,3153,3154,3156,3159,3161,3163,3165,3167,3169,3171,3173],{"class":49,"line":50},[47,3155,115],{"class":82},[47,3157,3158],{"class":78}," _input",[47,3160,151],{"class":90},[47,3162,449],{"class":145},[47,3164,91],{"class":90},[47,3166,454],{"class":57},[47,3168,186],{"class":90},[47,3170,124],{"class":82},[47,3172,127],{"class":57},[47,3174,130],{"class":90},[47,3176,3177,3179,3181,3183,3185,3187,3190,3192,3195,3197,3199,3201,3203,3206],{"class":49,"line":61},[47,3178,467],{"class":53},[47,3180,473],{"class":86},[47,3182,139],{"class":145},[47,3184,478],{"class":78},[47,3186,151],{"class":90},[47,3188,3189],{"class":483},"\"move_up\"",[47,3191,186],{"class":90},[47,3193,3194],{"class":82}," and",[47,3196,2253],{"class":86},[47,3198,2846],{"class":82},[47,3200,2127],{"class":57},[47,3202,139],{"class":90},[47,3204,3205],{"class":53},"DOWN",[47,3207,130],{"class":145},[47,3209,3210,3213,3215,3217,3219],{"class":49,"line":68},[47,3211,3212],{"class":86},"        next_direction",[47,3214,97],{"class":90},[47,3216,2127],{"class":57},[47,3218,139],{"class":90},[47,3220,3221],{"class":53},"UP\n",[47,3223,3224,3227,3229,3231,3233,3235,3238,3240,3242,3244,3246,3248,3250,3253],{"class":49,"line":75},[47,3225,3226],{"class":53},"    elif",[47,3228,473],{"class":86},[47,3230,139],{"class":145},[47,3232,478],{"class":78},[47,3234,151],{"class":90},[47,3236,3237],{"class":483},"\"move_down\"",[47,3239,186],{"class":90},[47,3241,3194],{"class":82},[47,3243,2253],{"class":86},[47,3245,2846],{"class":82},[47,3247,2127],{"class":57},[47,3249,139],{"class":90},[47,3251,3252],{"class":53},"UP",[47,3254,130],{"class":145},[47,3256,3257,3259,3261,3263,3265],{"class":49,"line":107},[47,3258,3212],{"class":86},[47,3260,97],{"class":90},[47,3262,2127],{"class":57},[47,3264,139],{"class":90},[47,3266,3267],{"class":53},"DOWN\n",[47,3269,3270,3272,3274,3276,3278,3280,3283,3285,3287,3289,3291,3293,3295,3297],{"class":49,"line":112},[47,3271,3226],{"class":53},[47,3273,473],{"class":86},[47,3275,139],{"class":145},[47,3277,478],{"class":78},[47,3279,151],{"class":90},[47,3281,3282],{"class":483},"\"move_left\"",[47,3284,186],{"class":90},[47,3286,3194],{"class":82},[47,3288,2253],{"class":86},[47,3290,2846],{"class":82},[47,3292,2127],{"class":57},[47,3294,139],{"class":90},[47,3296,2262],{"class":53},[47,3298,130],{"class":145},[47,3300,3301,3303,3305,3307,3309],{"class":49,"line":133},[47,3302,3212],{"class":86},[47,3304,97],{"class":90},[47,3306,2127],{"class":57},[47,3308,139],{"class":90},[47,3310,3311],{"class":53},"LEFT\n",[47,3313,3314,3316,3318,3320,3322,3324,3327,3329,3331,3333,3335,3337,3339,3342],{"class":49,"line":160},[47,3315,3226],{"class":53},[47,3317,473],{"class":86},[47,3319,139],{"class":145},[47,3321,478],{"class":78},[47,3323,151],{"class":90},[47,3325,3326],{"class":483},"\"move_right\"",[47,3328,186],{"class":90},[47,3330,3194],{"class":82},[47,3332,2253],{"class":86},[47,3334,2846],{"class":82},[47,3336,2127],{"class":57},[47,3338,139],{"class":90},[47,3340,3341],{"class":53},"LEFT",[47,3343,130],{"class":145},[47,3345,3346,3348,3350,3352,3354],{"class":49,"line":192},[47,3347,3212],{"class":86},[47,3349,97],{"class":90},[47,3351,2127],{"class":57},[47,3353,139],{"class":90},[47,3355,3356],{"class":53},"RIGHT\n",[15,3358,3359],{},"这一节全是为了两个 bug：",[15,3361,3362,3365,3366,3073],{},[248,3363,3364],{},"① 不能 180° 掉头。"," 向右跑时按左，新头会正好落在第二节上，瞬间\"撞到自己\"暴毙。所以每个方向都判断 ",[18,3367,3368],{},"direction != 相反方向",[15,3370,3371,3381,3382,3384,3385,3387,3388,3390,3391,3394],{},[248,3372,3373,3374,3377,3378,3073],{},"② 输入只改 ",[18,3375,3376],{},"next_direction","，不直接改 ",[18,3379,3380],{},"direction"," 想象你向右跑，在同一个 0.15 秒里连按 ↑ 再按 ←：如果直接改 ",[18,3383,3380],{},"，第一下把它改成 UP，第二下检查\"UP 的反方向是 DOWN，← 不是 DOWN\"，于是放行 → ",[18,3386,3380],{}," 变成 LEFT → 但蛇还没真的动过，等于原地从向右掉头成向左，撞死自己。用 ",[18,3389,3376],{}," 暂存，真正移动那一刻才 ",[18,3392,3393],{},"direction = next_direction","，判断永远基于\"上一步真的走了的方向\"。",[15,3396,3397],{},"插上手柄，十字键和摇杆同样能转——代码一个字没为手柄多写，因为用的是第 2 章起的动作名。",[1425,3399,3400],{},[15,3401,3402,3403,3406,3407,3410],{},"顺便说一个和代码无关、但害我查了半小时的事：Godot 4.4 起游戏默认",[248,3404,3405],{},"嵌在编辑器里","跑，「游戏」标签的工具栏有 ",[248,3408,3409],{},"输入 \u002F 2D \u002F 3D"," 三个模式，只有「输入」模式按键才进游戏；2D \u002F 3D 是运行时点选节点用的，会把输入截走。代码没错的时候，先看这个开关。",[10,3412,3414],{"id":3413},"三种碰撞-吃","三种碰撞 + 吃",[15,3416,3417,3418,3420,3421,3424],{},"把 ",[18,3419,2638],{}," 改成完整版。注意判定要在\"插头\"",[248,3422,3423],{},"之前","——先算出下一格是哪，再看那一格能不能去，能去才真的走：",[38,3426,3428],{"className":40,"code":3427,"language":42,"meta":43,"style":43},"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",[18,3429,3430,3444,3453,3473,3477,3482,3516,3546,3553,3557,3561,3566,3578,3584,3588,3592,3610,3614,3619,3631,3651,3663,3675,3685,3702,3713,3720,3735,3740,3748,3753,3769,3782],{"__ignoreMap":43},[47,3431,3432,3434,3436,3438,3440,3442],{"class":49,"line":50},[47,3433,115],{"class":82},[47,3435,2479],{"class":78},[47,3437,121],{"class":90},[47,3439,124],{"class":82},[47,3441,127],{"class":57},[47,3443,130],{"class":90},[47,3445,3446,3448,3450],{"class":49,"line":61},[47,3447,2492],{"class":86},[47,3449,97],{"class":90},[47,3451,3452],{"class":86}," next_direction\n",[47,3454,3455,3457,3459,3461,3463,3465,3467,3469,3471],{"class":49,"line":68},[47,3456,2504],{"class":82},[47,3458,2507],{"class":86},[47,3460,2063],{"class":90},[47,3462,2162],{"class":86},[47,3464,2170],{"class":90},[47,3466,2516],{"class":2066},[47,3468,2176],{"class":90},[47,3470,2521],{"class":82},[47,3472,2524],{"class":86},[47,3474,3475],{"class":49,"line":75},[47,3476,72],{"emptyLinePlaceholder":71},[47,3478,3479],{"class":49,"line":107},[47,3480,3481],{"class":64},"    # ① 撞墙：任何一个分量跑出范围\n",[47,3483,3484,3486,3488,3490,3493,3496,3499,3502,3504,3506,3508,3511,3513],{"class":49,"line":112},[47,3485,467],{"class":53},[47,3487,2507],{"class":86},[47,3489,139],{"class":90},[47,3491,3492],{"class":86},"x",[47,3494,3495],{"class":82}," \u003C",[47,3497,3498],{"class":2066}," 0",[47,3500,3501],{"class":82}," or",[47,3503,2507],{"class":86},[47,3505,139],{"class":90},[47,3507,3492],{"class":86},[47,3509,3510],{"class":82}," >=",[47,3512,2060],{"class":86},[47,3514,3515],{"class":90}," \\\n",[47,3517,3518,3521,3523,3525,3528,3530,3532,3534,3536,3538,3540,3542,3544],{"class":49,"line":133},[47,3519,3520],{"class":82},"    or",[47,3522,2507],{"class":86},[47,3524,139],{"class":90},[47,3526,3527],{"class":86},"y",[47,3529,3495],{"class":82},[47,3531,3498],{"class":2066},[47,3533,3501],{"class":82},[47,3535,2507],{"class":86},[47,3537,139],{"class":90},[47,3539,3527],{"class":86},[47,3541,3510],{"class":82},[47,3543,2077],{"class":86},[47,3545,130],{"class":145},[47,3547,3548,3551],{"class":49,"line":160},[47,3549,3550],{"class":78},"        die",[47,3552,831],{"class":90},[47,3554,3555],{"class":49,"line":192},[47,3556,493],{"class":53},[47,3558,3559],{"class":49,"line":197},[47,3560,72],{"emptyLinePlaceholder":71},[47,3562,3563],{"class":49,"line":223},[47,3564,3565],{"class":64},"    # ② 撞自己：新头的位置已经在身体里\n",[47,3567,3568,3570,3572,3574,3576],{"class":49,"line":584},[47,3569,467],{"class":53},[47,3571,2507],{"class":86},[47,3573,2678],{"class":82},[47,3575,2162],{"class":86},[47,3577,130],{"class":145},[47,3579,3580,3582],{"class":49,"line":600},[47,3581,3550],{"class":78},[47,3583,831],{"class":90},[47,3585,3586],{"class":49,"line":623},[47,3587,493],{"class":53},[47,3589,3590],{"class":49,"line":628},[47,3591,72],{"emptyLinePlaceholder":71},[47,3593,3594,3596,3598,3600,3602,3604,3606,3608],{"class":49,"line":657},[47,3595,2529],{"class":86},[47,3597,139],{"class":145},[47,3599,2534],{"class":78},[47,3601,151],{"class":90},[47,3603,2516],{"class":2066},[47,3605,2541],{"class":90},[47,3607,2507],{"class":86},[47,3609,157],{"class":90},[47,3611,3612],{"class":49,"line":690},[47,3613,72],{"emptyLinePlaceholder":71},[47,3615,3616],{"class":49,"line":703},[47,3617,3618],{"class":64},"    # ③ 吃：三种情况的共同点 —— 吃到了就不删尾\n",[47,3620,3621,3623,3625,3627,3629],{"class":49,"line":719},[47,3622,467],{"class":53},[47,3624,2507],{"class":86},[47,3626,2678],{"class":82},[47,3628,2299],{"class":86},[47,3630,130],{"class":145},[47,3632,3633,3636,3638,3641,3643,3646,3648],{"class":49,"line":724},[47,3634,3635],{"class":86},"        foods",[47,3637,139],{"class":145},[47,3639,3640],{"class":78},"erase",[47,3642,151],{"class":90},[47,3644,3645],{"class":86},"new_head",[47,3647,186],{"class":90},[47,3649,3650],{"class":64},"                   # 从食物列表里拿走这一个\n",[47,3652,3653,3656,3658,3661],{"class":49,"line":740},[47,3654,3655],{"class":78},"        print",[47,3657,151],{"class":90},[47,3659,3660],{"class":483},"\"+1\"",[47,3662,157],{"class":90},[47,3664,3665,3667,3669,3671,3673],{"class":49,"line":756},[47,3666,3226],{"class":53},[47,3668,2507],{"class":86},[47,3670,675],{"class":82},[47,3672,2337],{"class":86},[47,3674,130],{"class":145},[47,3676,3677,3680,3682],{"class":49,"line":779},[47,3678,3679],{"class":86},"        star",[47,3681,97],{"class":90},[47,3683,3684],{"class":86}," NO_CELL\n",[47,3686,3687,3690,3692,3694,3697,3699],{"class":49,"line":784},[47,3688,3689],{"class":53},"        $",[47,3691,2023],{"class":103},[47,3693,139],{"class":145},[47,3695,3696],{"class":78},"stop",[47,3698,121],{"class":90},[47,3700,3701],{"class":64},"                   # 它的寿命倒计时也不用跑了\n",[47,3703,3704,3706,3708,3711],{"class":49,"line":800},[47,3705,3655],{"class":78},[47,3707,151],{"class":90},[47,3709,3710],{"class":483},"\"+2\"",[47,3712,157],{"class":90},[47,3714,3715,3718],{"class":49,"line":819},[47,3716,3717],{"class":53},"    else",[47,3719,130],{"class":145},[47,3721,3723,3726,3728,3730,3732],{"class":49,"line":3722},27,[47,3724,3725],{"class":86},"        snake_body",[47,3727,139],{"class":145},[47,3729,2557],{"class":78},[47,3731,121],{"class":90},[47,3733,3734],{"class":64},"                   # 什么都没吃到 → 删尾\n",[47,3736,3738],{"class":49,"line":3737},28,[47,3739,72],{"emptyLinePlaceholder":71},[47,3741,3743,3746],{"class":49,"line":3742},29,[47,3744,3745],{"class":78},"    redraw",[47,3747,831],{"class":90},[47,3749,3751],{"class":49,"line":3750},30,[47,3752,72],{"emptyLinePlaceholder":71},[47,3754,3756,3758,3761,3763,3765,3767],{"class":49,"line":3755},31,[47,3757,115],{"class":82},[47,3759,3760],{"class":78}," die",[47,3762,121],{"class":90},[47,3764,124],{"class":82},[47,3766,127],{"class":57},[47,3768,130],{"class":90},[47,3770,3772,3774,3776,3778,3780],{"class":49,"line":3771},32,[47,3773,2832],{"class":53},[47,3775,2460],{"class":103},[47,3777,139],{"class":145},[47,3779,3696],{"class":78},[47,3781,831],{"class":90},[47,3783,3785,3787,3789,3792,3794,3796,3798,3800],{"class":49,"line":3784},33,[47,3786,2367],{"class":78},[47,3788,151],{"class":90},[47,3790,3791],{"class":483},"\"died, length = \"",[47,3793,2541],{"class":90},[47,3795,2162],{"class":86},[47,3797,139],{"class":145},[47,3799,2862],{"class":78},[47,3801,3802],{"class":90},"())\n",[836,3804,3805,3814,3826],{},[839,3806,3807,3810,3811,3813],{},[18,3808,3809],{},"new_head in snake_body","：数组支持 ",[18,3812,1337],{},"，\"里面有没有这个元素\"。「撞到自己」这么复杂的问题一行解决——因为蛇身本来就是坐标数组，这就是选对数据结构的价值",[839,3815,3816,3817,3819,3820,3822,3823],{},"为什么吃食物用 ",[18,3818,1337],{},"，吃星星用 ",[18,3821,1368],{},"：和第 1 章那张实体表一一对应——食物有好几个，要问\"在不在列表里\"；星星只有一颗，直接问\"是不是它\"。",[248,3824,3825],{},"数据结构决定判定写法",[839,3827,3828,3831],{},[18,3829,3830],{},"return","：死了就立刻退出，后面的插头、重画都不执行。不写它，死了还会再走一步",[10,3833,3834],{"id":3834},"随机挑一个空格子",[38,3836,3838],{"className":40,"code":3837,"language":42,"meta":43,"style":43},"## 在所有空格子里随机挑一个；棋盘满了返回 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",[18,3839,3840,3845,3860,3882,3895,3909,3930,3959,3967,3984,3999,4006],{"__ignoreMap":43},[47,3841,3842],{"class":49,"line":50},[47,3843,3844],{"class":64},"## 在所有空格子里随机挑一个；棋盘满了返回 NO_CELL\n",[47,3846,3847,3849,3852,3854,3856,3858],{"class":49,"line":61},[47,3848,115],{"class":82},[47,3850,3851],{"class":78}," random_empty_cell",[47,3853,121],{"class":90},[47,3855,124],{"class":82},[47,3857,2127],{"class":57},[47,3859,130],{"class":90},[47,3861,3862,3864,3867,3869,3871,3873,3875,3877,3879],{"class":49,"line":68},[47,3863,2504],{"class":82},[47,3865,3866],{"class":86}," empty_cells",[47,3868,91],{"class":90},[47,3870,2167],{"class":57},[47,3872,2170],{"class":90},[47,3874,2173],{"class":57},[47,3876,2176],{"class":90},[47,3878,97],{"class":90},[47,3880,3881],{"class":90}," []\n",[47,3883,3884,3886,3889,3891,3893],{"class":49,"line":75},[47,3885,2672],{"class":53},[47,3887,3888],{"class":86}," x",[47,3890,2678],{"class":53},[47,3892,2060],{"class":86},[47,3894,130],{"class":145},[47,3896,3897,3900,3903,3905,3907],{"class":49,"line":107},[47,3898,3899],{"class":53},"        for",[47,3901,3902],{"class":86}," y",[47,3904,2678],{"class":53},[47,3906,2077],{"class":86},[47,3908,130],{"class":145},[47,3910,3911,3914,3916,3918,3920,3922,3924,3926,3928],{"class":49,"line":112},[47,3912,3913],{"class":82},"            var",[47,3915,2675],{"class":86},[47,3917,2063],{"class":90},[47,3919,2127],{"class":57},[47,3921,151],{"class":90},[47,3923,3492],{"class":86},[47,3925,1035],{"class":145},[47,3927,3527],{"class":86},[47,3929,157],{"class":90},[47,3931,3932,3935,3937,3939,3941,3943,3945,3947,3949,3951,3953,3955,3957],{"class":49,"line":133},[47,3933,3934],{"class":53},"            if",[47,3936,2675],{"class":86},[47,3938,2678],{"class":82},[47,3940,2162],{"class":86},[47,3942,3501],{"class":82},[47,3944,2675],{"class":86},[47,3946,2678],{"class":82},[47,3948,2299],{"class":86},[47,3950,3501],{"class":82},[47,3952,2675],{"class":86},[47,3954,675],{"class":82},[47,3956,2337],{"class":86},[47,3958,130],{"class":145},[47,3960,3961,3964],{"class":49,"line":160},[47,3962,3963],{"class":53},"                continue",[47,3965,3966],{"class":64},"                        # 被占了，跳过这格\n",[47,3968,3969,3972,3974,3977,3979,3982],{"class":49,"line":192},[47,3970,3971],{"class":86},"            empty_cells",[47,3973,139],{"class":145},[47,3975,3976],{"class":78},"append",[47,3978,151],{"class":90},[47,3980,3981],{"class":86},"cell",[47,3983,157],{"class":90},[47,3985,3986,3988,3990,3992,3995,3997],{"class":49,"line":197},[47,3987,467],{"class":53},[47,3989,3866],{"class":86},[47,3991,139],{"class":145},[47,3993,3994],{"class":78},"is_empty",[47,3996,121],{"class":90},[47,3998,130],{"class":145},[47,4000,4001,4004],{"class":49,"line":223},[47,4002,4003],{"class":53},"        return",[47,4005,3684],{"class":86},[47,4007,4008,4011,4013,4015,4018],{"class":49,"line":584},[47,4009,4010],{"class":53},"    return",[47,4012,3866],{"class":86},[47,4014,139],{"class":145},[47,4016,4017],{"class":78},"pick_random",[47,4019,831],{"class":90},[15,4021,4022],{},"为什么\"先列出所有空格再随机挑\"，不是\"随机挑一格，被占了就重挑\"？蛇很长时随机撞到蛇身的概率极高，可能循环几百次，理论上会死循环。先列空格再挑，一次搞定，而且顺便处理了\"填满通关\"这个边界情况。",[836,4024,4025,4035,4045],{},[839,4026,4027,4030,4031,4034],{},[18,4028,4029],{},"is_empty()","：数组是不是空的，等于 ",[18,4032,4033],{},"size() == 0","，但读起来是一句人话。没有空格子 = 棋盘被填满了，从空数组里拿东西会报错，所以先问一句",[839,4036,4037,4040,4041,4044],{},[18,4038,4039],{},"pick_random()","：等概率返回其中一个元素，不删除它。能自己写 ",[18,4042,4043],{},"empty_cells[randi() % empty_cells.size()]","，但一个词说清楚的事就别写一行算式——引擎自带的方法先查有没有，再自己写",[839,4046,4047,4050,4051,4053],{},[18,4048,4049],{},"continue","：跳过这一轮循环剩下的部分，直接看下一个格子。和 ",[18,4052,3830],{},"（退出整个函数）不一样",[15,4055,4056],{},"到这里，撞墙会停、吃到会变长、绕回来撞自己会停。下一章让食物自己长出来、让星星自己消失。",[1125,4058,4059],{},"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":43,"searchDepth":61,"depth":61,"links":4061},[4062,4063,4064,4065],{"id":2627,"depth":61,"text":2627},{"id":3146,"depth":61,"text":3146},{"id":3413,"depth":61,"text":3414},{"id":3834,"depth":61,"text":3834},"2026-09-04",{},"\u002Fdevlog\u002Fxggame-snake\u002F04-draw-and-collide",{"title":2622,"description":1137},"devlog\u002Fxggame-snake\u002F04-draw-and-collide","ceXE2WSOzbKu4LgRJrOtAugEk9uijTOZLqQ6YUIaBs8",{"id":4073,"title":4074,"body":4075,"cover":1135,"date":4437,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":4438,"navigation":71,"path":4439,"seo":4440,"stem":4441,"toc":71,"__hash__":4442},"devlog\u002Fdevlog\u002Fxggame-snake\u002F05-timers.md","定时生成与过期",{"type":7,"value":4076,"toc":4432},[4077,4080,4084,4100,4320,4341,4347,4350,4381,4397,4405,4409,4420,4426,4429],[15,4078,4079],{},"这是这个游戏最特别的一节。经典贪吃蛇里食物永远只有一个；这里场上可能有 0～3 个食物、一颗随时会消失的星星，玩家要做选择。数据结构只多了一个数组，体验完全不一样。",[10,4081,4083],{"id":4082},"三个-timer三个函数","三个 Timer，三个函数",[15,4085,4086,4087,383,4090,383,4093,4095,4096,2465,4098,36],{},"依次选中 ",[18,4088,4089],{},"FoodTimer",[18,4091,4092],{},"StarSpawnTimer",[18,4094,2023],{}," → 节点面板 → ",[18,4097,2464],{},[18,4099,20],{},[38,4101,4103],{"className":40,"code":4102,"language":42,"meta":43,"style":43},"## 每 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",[18,4104,4105,4110,4125,4143,4150,4162,4174,4188,4194,4198,4203,4218,4230,4237,4248,4260,4276,4282,4286,4291,4306,4314],{"__ignoreMap":43},[47,4106,4107],{"class":49,"line":50},[47,4108,4109],{"class":64},"## 每 3 秒：场上不够 3 个食物就长一个\n",[47,4111,4112,4114,4117,4119,4121,4123],{"class":49,"line":61},[47,4113,115],{"class":82},[47,4115,4116],{"class":78}," _on_food_timer_timeout",[47,4118,121],{"class":90},[47,4120,124],{"class":82},[47,4122,127],{"class":57},[47,4124,130],{"class":90},[47,4126,4127,4129,4131,4133,4135,4137,4139,4141],{"class":49,"line":68},[47,4128,467],{"class":53},[47,4130,2299],{"class":86},[47,4132,139],{"class":145},[47,4134,2862],{"class":78},[47,4136,121],{"class":90},[47,4138,3510],{"class":82},[47,4140,2107],{"class":86},[47,4142,130],{"class":145},[47,4144,4145,4147],{"class":49,"line":75},[47,4146,4003],{"class":53},[47,4148,4149],{"class":64},"                                  # 够多了，这次不长\n",[47,4151,4152,4154,4156,4158,4160],{"class":49,"line":107},[47,4153,2504],{"class":82},[47,4155,2675],{"class":86},[47,4157,2063],{"class":90},[47,4159,3851],{"class":78},[47,4161,831],{"class":90},[47,4163,4164,4166,4168,4170,4172],{"class":49,"line":112},[47,4165,467],{"class":53},[47,4167,2675],{"class":86},[47,4169,2846],{"class":82},[47,4171,2122],{"class":86},[47,4173,130],{"class":145},[47,4175,4176,4178,4180,4182,4184,4186],{"class":49,"line":133},[47,4177,3635],{"class":86},[47,4179,139],{"class":145},[47,4181,3976],{"class":78},[47,4183,151],{"class":90},[47,4185,3981],{"class":86},[47,4187,157],{"class":90},[47,4189,4190,4192],{"class":49,"line":160},[47,4191,3745],{"class":78},[47,4193,831],{"class":90},[47,4195,4196],{"class":49,"line":192},[47,4197,72],{"emptyLinePlaceholder":71},[47,4199,4200],{"class":49,"line":197},[47,4201,4202],{"class":64},"## 每 12 秒：没有星星就放一颗，并开始倒数它的寿命\n",[47,4204,4205,4207,4210,4212,4214,4216],{"class":49,"line":223},[47,4206,115],{"class":82},[47,4208,4209],{"class":78}," _on_star_spawn_timer_timeout",[47,4211,121],{"class":90},[47,4213,124],{"class":82},[47,4215,127],{"class":57},[47,4217,130],{"class":90},[47,4219,4220,4222,4224,4226,4228],{"class":49,"line":584},[47,4221,467],{"class":53},[47,4223,2337],{"class":86},[47,4225,2846],{"class":82},[47,4227,2122],{"class":86},[47,4229,130],{"class":145},[47,4231,4232,4234],{"class":49,"line":600},[47,4233,4003],{"class":53},[47,4235,4236],{"class":64},"                                  # 已经有一颗了\n",[47,4238,4239,4242,4244,4246],{"class":49,"line":623},[47,4240,4241],{"class":86},"    star",[47,4243,97],{"class":90},[47,4245,3851],{"class":78},[47,4247,831],{"class":90},[47,4249,4250,4252,4254,4256,4258],{"class":49,"line":628},[47,4251,467],{"class":53},[47,4253,2337],{"class":86},[47,4255,2846],{"class":82},[47,4257,2122],{"class":86},[47,4259,130],{"class":145},[47,4261,4262,4264,4266,4268,4271,4273],{"class":49,"line":657},[47,4263,3689],{"class":53},[47,4265,2023],{"class":103},[47,4267,139],{"class":145},[47,4269,4270],{"class":78},"start",[47,4272,121],{"class":90},[47,4274,4275],{"class":64},"                  # 从现在起倒数 5 秒\n",[47,4277,4278,4280],{"class":49,"line":690},[47,4279,3745],{"class":78},[47,4281,831],{"class":90},[47,4283,4284],{"class":49,"line":703},[47,4285,72],{"emptyLinePlaceholder":71},[47,4287,4288],{"class":49,"line":719},[47,4289,4290],{"class":64},"## 5 秒到：没人吃 → 星星消失\n",[47,4292,4293,4295,4298,4300,4302,4304],{"class":49,"line":724},[47,4294,115],{"class":82},[47,4296,4297],{"class":78}," _on_star_life_timer_timeout",[47,4299,121],{"class":90},[47,4301,124],{"class":82},[47,4303,127],{"class":57},[47,4305,130],{"class":90},[47,4307,4308,4310,4312],{"class":49,"line":740},[47,4309,4241],{"class":86},[47,4311,97],{"class":90},[47,4313,3684],{"class":86},[47,4315,4316,4318],{"class":49,"line":756},[47,4317,3745],{"class":78},[47,4319,831],{"class":90},[15,4321,4322,4323,4325,4326,4329,4330,4333,4334,4336,4337,4340],{},"然后回到第 3 章：把 ",[18,4324,2422],{}," 那行手放的 ",[18,4327,4328],{},"Vector2i(60, 27)"," 删掉改成 ",[18,4331,4332],{},"[]","，在 ",[18,4335,245],{}," 里加一句 ",[18,4338,4339],{},"_on_food_timer_timeout()","——开局立刻长一个，不用干等 3 秒。",[15,4342,4343,4344,4346],{},"为什么开局要手动调一次 ",[18,4345,4339],{},"，不是继续手放一个坐标？手放的坐标是写死的。用同一套生成逻辑，就不会有\"两套规则\"。一个函数既能被 Timer 叫，也能被你手动叫——这就是函数的意义。",[10,4348,4349],{"id":4349},"三个容易漏的判断",[15,4351,4352,4358,4359,4362,4363,4366,4367,4370,4371,4373,4374,4377,4378],{},[248,4353,4354,4355],{},"为什么 spawn 之后都要判断 ",[18,4356,4357],{},"!= NO_CELL","：棋盘满了 ",[18,4360,4361],{},"random_empty_cell()"," 会返回 ",[18,4364,4365],{},"NO_CELL","。不判断，",[18,4368,4369],{},"(-1, -1)"," 就会被塞进 ",[18,4372,2422],{},"，画到屏幕外，还会让 ",[18,4375,4376],{},"in foods"," 出现幽灵食物。",[248,4379,4380],{},"凡是可能返回\"没有\"的函数，调用方必须检查。",[15,4382,4383,4389,4390,4393,4394],{},[248,4384,4385,4386],{},"为什么吃到星星时要 ",[18,4387,4388],{},"$StarLifeTimer.stop()","（第 4 章那行）：不停的话，你 2 秒就吃了，3 秒后它照样响，",[18,4391,4392],{},"star = NO_CELL","——但这时候可能已经又生成了新的一颗，它被无辜清掉。",[248,4395,4396],{},"Timer 是有状态的东西，用完要收。",[15,4398,4399,4404],{},[248,4400,242,4401,4403],{},[18,4402,2023],{}," 是 One Shot、不 Autostart","：它是星星的寿命。星星出现那一刻才开始倒数，倒数一次就够了。其他三个是\"每隔一段时间做一次\"，要一直重复。",[10,4406,4408],{"id":4407},"一个上线后才发现的顺序-bug","一个上线后才发现的顺序 bug",[15,4410,4411,4413,4414,4416,4417],{},[18,4412,245],{}," 里我一开始是先 ",[18,4415,4339],{}," 再建蛇。食物的\"空格子\"判断要看到蛇，蛇还没出生时它以为整个棋盘都是空的，于是食物可能长在蛇出生的格子上。改成先建蛇再生食物。",[248,4418,4419],{},"初始化顺序：先有主角，再生世界。",[15,4421,4422],{},[1117,4423],{"alt":4424,"src":4425},"05-timers-星星","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F05-timers-%E6%98%9F%E6%98%9F.png",[15,4427,4428],{},"到这里核心循环完整了：蛇能跑、能转、能死，食物按时间长，星星会出现会消失。剩下全是外壳。",[1125,4430,4431],{},"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":43,"searchDepth":61,"depth":61,"links":4433},[4434,4435,4436],{"id":4082,"depth":61,"text":4083},{"id":4349,"depth":61,"text":4349},{"id":4407,"depth":61,"text":4408},"2026-09-05",{},"\u002Fdevlog\u002Fxggame-snake\u002F05-timers",{"title":4074,"description":1137},"devlog\u002Fxggame-snake\u002F05-timers","xUycJSF2Vdp0jH_G06FPQIcT9sJaF3PVraGZxFP9cBM",{"id":4444,"title":4445,"body":4446,"cover":1135,"date":5995,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":5996,"navigation":71,"path":5997,"seo":5998,"stem":5999,"toc":71,"__hash__":6000},"devlog\u002Fdevlog\u002Fxggame-snake\u002F06-systems.md","系统：喇叭、裁判、档案",{"type":7,"value":4447,"toc":5987},[4448,4454,4460,4516,4520,4599,4602,4617,4626,4630,5065,5106,5112,5125,5349,5364,5384,5390,5394,5725,5743,5746,5960,5981,5984],[15,4449,4450,4451,4453],{},"阶段 2 明明跑通了，为什么要拆？因为接下来的东西——分数显示、暂停、结算、存档、设置菜单——全都要问同一个问题：\"现在什么状态？分数多少？玩家选了什么？\"这些数据现在散在 ",[18,4452,850],{}," 里，菜单场景、UI 场景想拿到它们，要么互相引用（一团乱），要么放到一个大家都能叫到的地方——Autoload。",[15,4455,4456,4457],{},"为什么不一开始就拆？一开始拆，你会在四个文件之间来回跳，逻辑还没跑通就先迷路。",[248,4458,4459],{},"先能跑，再拆干净。",[288,4461,4462,4475],{},[291,4463,4464],{},[294,4465,4466,4469,4472],{},[297,4467,4468],{},"单例",[297,4470,4471],{},"只管",[297,4473,4474],{},"比喻",[307,4476,4477,4492,4504],{},[294,4478,4479,4483,4489],{},[312,4480,4481],{},[18,4482,1738],{},[312,4484,4485,4486],{},"喊一声「发生了 X」，",[248,4487,4488],{},"零逻辑",[312,4490,4491],{},"🔊 喇叭",[294,4493,4494,4498,4501],{},[312,4495,4496],{},[18,4497,178],{},[312,4499,4500],{},"状态、分数、最高分、开局设置",[312,4502,4503],{},"⚖️ 裁判",[294,4505,4506,4510,4513],{},[312,4507,4508],{},[18,4509,1785],{},[312,4511,4512],{},"读写存档文件",[312,4514,4515],{},"💾 档案柜",[10,4517,4519],{"id":4518},"eventbus整个文件就两个信号","EventBus：整个文件就两个信号",[38,4521,4523],{"className":40,"code":4522,"language":42,"meta":43,"style":43},"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",[18,4524,4525,4532,4537,4541,4553,4575,4579,4589],{"__ignoreMap":43},[47,4526,4527,4529],{"class":49,"line":50},[47,4528,54],{"class":53},[47,4530,4531],{"class":57}," Node\n",[47,4533,4534],{"class":49,"line":61},[47,4535,4536],{"class":64},"## 全局信号总线：只定义信号，谁关心谁自己来连\n",[47,4538,4539],{"class":49,"line":68},[47,4540,72],{"emptyLinePlaceholder":71},[47,4542,4543,4546,4548,4551],{"class":49,"line":75},[47,4544,4545],{"class":78},"@warning_ignore",[47,4547,151],{"class":90},[47,4549,4550],{"class":483},"\"unused_signal\"",[47,4552,157],{"class":90},[47,4554,4555,4558,4561,4563,4566,4568,4570,4572],{"class":49,"line":107},[47,4556,4557],{"class":82},"signal",[47,4559,4560],{"class":78}," food_eaten",[47,4562,151],{"class":90},[47,4564,4565],{"class":145},"points",[47,4567,91],{"class":90},[47,4569,212],{"class":57},[47,4571,186],{"class":90},[47,4573,4574],{"class":64},"     # 吃到东西了，值几分（普通食物 1、星星 2）\n",[47,4576,4577],{"class":49,"line":112},[47,4578,72],{"emptyLinePlaceholder":71},[47,4580,4581,4583,4585,4587],{"class":49,"line":133},[47,4582,4545],{"class":78},[47,4584,151],{"class":90},[47,4586,4550],{"class":483},[47,4588,157],{"class":90},[47,4590,4591,4593,4596],{"class":49,"line":160},[47,4592,4557],{"class":82},[47,4594,4595],{"class":78}," snake_died",[47,4597,4598],{"class":64},"                  # 蛇死了\n",[15,4600,4601],{},"一个函数都不许有。它是喇叭，喇叭只负责\"喊\"，喊完谁该做什么是听的人自己的事。一旦往里塞逻辑，它就变成第二个 GameManager，两边职责打架。",[15,4603,242,4604,4607,4608,892,4610,4613,4614],{},[18,4605,4606],{},"food_eaten"," 带一个数字，不拆成 ",[18,4609,4606],{},[18,4611,4612],{},"star_eaten","？听众关心的是\"值几分\"，不是\"是哪种\"。加分的人拿到 2 就加 2；放音效的人看到 2 就放星星音。以后加第三种食物值 5 分，信号一个字不用改。",[248,4615,4616],{},"信号的参数是\"事实的细节\"，不是\"指令\"。",[15,4618,4619,4622,4623],{},[18,4620,4621],{},"@warning_ignore(\"unused_signal\")","：不加，编辑器会画黄线警告\"这个信号定义了但没在本文件里发出\"。EventBus 的设计就是\"在别的文件里发\"，所以这是误报。这算 Godot 的设计失误吗？谈不上——检查器一次只看一个文件，没法知道别的文件会不会发这个信号；而\"定义了却没发\"在 99% 的情况下确实是写错了。它选择宁可误报也不漏报，然后给你一行注解当逃生口。",[248,4624,4625],{},"黄线是警告（请你确认），红线才是错误（跑不起来）。",[10,4627,4629],{"id":4628},"gamemanager状态机-分数","GameManager：状态机 + 分数",[38,4631,4633],{"className":40,"code":4632,"language":42,"meta":43,"style":43},"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",[18,4634,4635,4641,4646,4650,4679,4695,4699,4711,4722,4726,4743,4760,4764,4778,4793,4816,4839,4843,4848,4871,4881,4897,4901,4924,4935,4950,4954,4969,4977,4991,4995,5010,5025,5038,5049],{"__ignoreMap":43},[47,4636,4637,4639],{"class":49,"line":50},[47,4638,54],{"class":53},[47,4640,4531],{"class":57},[47,4642,4643],{"class":49,"line":61},[47,4644,4645],{"class":64},"## 裁判：状态和分数只存这一份，别人要么问它、要么听它喊\n",[47,4647,4648],{"class":49,"line":68},[47,4649,72],{"emptyLinePlaceholder":71},[47,4651,4652,4655,4658,4661,4664,4666,4668,4670,4672,4674,4676],{"class":49,"line":75},[47,4653,4654],{"class":53},"enum",[47,4656,4657],{"class":57}," GameState",[47,4659,4660],{"class":90}," {",[47,4662,4663],{"class":86}," READY",[47,4665,1035],{"class":145},[47,4667,523],{"class":86},[47,4669,1035],{"class":145},[47,4671,565],{"class":86},[47,4673,1035],{"class":145},[47,4675,980],{"class":86},[47,4677,4678],{"class":90}," }\n",[47,4680,4681,4683,4686,4688,4690,4692],{"class":49,"line":107},[47,4682,2159],{"class":82},[47,4684,4685],{"class":86}," current_state",[47,4687,2063],{"class":90},[47,4689,4657],{"class":57},[47,4691,139],{"class":90},[47,4693,4694],{"class":53},"READY\n",[47,4696,4697],{"class":49,"line":112},[47,4698,72],{"emptyLinePlaceholder":71},[47,4700,4701,4703,4706,4708],{"class":49,"line":133},[47,4702,2159],{"class":82},[47,4704,4705],{"class":86}," total_score",[47,4707,2063],{"class":90},[47,4709,4710],{"class":2066}," 0\n",[47,4712,4713,4715,4718,4720],{"class":49,"line":160},[47,4714,2159],{"class":82},[47,4716,4717],{"class":86}," high_score",[47,4719,2063],{"class":90},[47,4721,4710],{"class":2066},[47,4723,4724],{"class":49,"line":192},[47,4725,72],{"emptyLinePlaceholder":71},[47,4727,4728,4730,4733,4735,4737,4739,4741],{"class":49,"line":197},[47,4729,4557],{"class":82},[47,4731,4732],{"class":78}," score_changed",[47,4734,151],{"class":90},[47,4736,207],{"class":145},[47,4738,91],{"class":90},[47,4740,212],{"class":57},[47,4742,157],{"class":90},[47,4744,4745,4747,4750,4752,4754,4756,4758],{"class":49,"line":223},[47,4746,4557],{"class":82},[47,4748,4749],{"class":78}," state_changed",[47,4751,151],{"class":90},[47,4753,638],{"class":145},[47,4755,91],{"class":90},[47,4757,4657],{"class":57},[47,4759,157],{"class":90},[47,4761,4762],{"class":49,"line":584},[47,4763,72],{"emptyLinePlaceholder":71},[47,4765,4766,4768,4770,4772,4774,4776],{"class":49,"line":600},[47,4767,115],{"class":82},[47,4769,118],{"class":78},[47,4771,121],{"class":90},[47,4773,124],{"class":82},[47,4775,127],{"class":57},[47,4777,130],{"class":90},[47,4779,4780,4783,4785,4788,4790],{"class":49,"line":623},[47,4781,4782],{"class":57},"    SaveManager",[47,4784,139],{"class":145},[47,4786,4787],{"class":78},"load_game",[47,4789,121],{"class":90},[47,4791,4792],{"class":64},"                       # 开机读档\n",[47,4794,4795,4798,4800,4802,4804,4806,4808,4811,4813],{"class":49,"line":628},[47,4796,4797],{"class":57},"    EventBus",[47,4799,139],{"class":90},[47,4801,4606],{"class":86},[47,4803,139],{"class":145},[47,4805,148],{"class":78},[47,4807,151],{"class":90},[47,4809,4810],{"class":86},"add_score",[47,4812,186],{"class":90},[47,4814,4815],{"class":64},"        # 听「吃到 N 分」→ 加 N 分\n",[47,4817,4818,4820,4822,4825,4827,4829,4831,4834,4836],{"class":49,"line":657},[47,4819,4797],{"class":57},[47,4821,139],{"class":90},[47,4823,4824],{"class":86},"snake_died",[47,4826,139],{"class":145},[47,4828,148],{"class":78},[47,4830,151],{"class":90},[47,4832,4833],{"class":86},"game_over",[47,4835,186],{"class":90},[47,4837,4838],{"class":64},"        # 听「死了」→ 结算\n",[47,4840,4841],{"class":49,"line":690},[47,4842,72],{"emptyLinePlaceholder":71},[47,4844,4845],{"class":49,"line":703},[47,4846,4847],{"class":64},"## 改状态的唯一入口 —— 每次改都自动广播，不会漏\n",[47,4849,4850,4852,4855,4857,4859,4861,4863,4865,4867,4869],{"class":49,"line":719},[47,4851,115],{"class":82},[47,4853,4854],{"class":78}," set_state",[47,4856,151],{"class":90},[47,4858,638],{"class":145},[47,4860,91],{"class":90},[47,4862,4657],{"class":57},[47,4864,186],{"class":90},[47,4866,124],{"class":82},[47,4868,127],{"class":57},[47,4870,130],{"class":90},[47,4872,4873,4876,4878],{"class":49,"line":724},[47,4874,4875],{"class":86},"    current_state",[47,4877,97],{"class":90},[47,4879,4880],{"class":86}," new_state\n",[47,4882,4883,4886,4888,4891,4893,4895],{"class":49,"line":740},[47,4884,4885],{"class":86},"    state_changed",[47,4887,139],{"class":145},[47,4889,4890],{"class":78},"emit",[47,4892,151],{"class":90},[47,4894,638],{"class":86},[47,4896,157],{"class":90},[47,4898,4899],{"class":49,"line":756},[47,4900,72],{"emptyLinePlaceholder":71},[47,4902,4903,4905,4908,4910,4912,4914,4916,4918,4920,4922],{"class":49,"line":779},[47,4904,115],{"class":82},[47,4906,4907],{"class":78}," add_score",[47,4909,151],{"class":90},[47,4911,4565],{"class":145},[47,4913,91],{"class":90},[47,4915,212],{"class":57},[47,4917,186],{"class":90},[47,4919,124],{"class":82},[47,4921,127],{"class":57},[47,4923,130],{"class":90},[47,4925,4926,4929,4932],{"class":49,"line":784},[47,4927,4928],{"class":86},"    total_score",[47,4930,4931],{"class":82}," +=",[47,4933,4934],{"class":86}," points\n",[47,4936,4937,4940,4942,4944,4946,4948],{"class":49,"line":800},[47,4938,4939],{"class":86},"    score_changed",[47,4941,139],{"class":145},[47,4943,4890],{"class":78},[47,4945,151],{"class":90},[47,4947,183],{"class":86},[47,4949,157],{"class":90},[47,4951,4952],{"class":49,"line":819},[47,4953,72],{"emptyLinePlaceholder":71},[47,4955,4956,4958,4961,4963,4965,4967],{"class":49,"line":3722},[47,4957,115],{"class":82},[47,4959,4960],{"class":78}," reset_score",[47,4962,121],{"class":90},[47,4964,124],{"class":82},[47,4966,127],{"class":57},[47,4968,130],{"class":90},[47,4970,4971,4973,4975],{"class":49,"line":3737},[47,4972,4928],{"class":86},[47,4974,97],{"class":90},[47,4976,4710],{"class":2066},[47,4978,4979,4981,4983,4985,4987,4989],{"class":49,"line":3742},[47,4980,4939],{"class":86},[47,4982,139],{"class":145},[47,4984,4890],{"class":78},[47,4986,151],{"class":90},[47,4988,183],{"class":86},[47,4990,157],{"class":90},[47,4992,4993],{"class":49,"line":3750},[47,4994,72],{"emptyLinePlaceholder":71},[47,4996,4997,4999,5002,5004,5006,5008],{"class":49,"line":3755},[47,4998,115],{"class":82},[47,5000,5001],{"class":78}," game_over",[47,5003,121],{"class":90},[47,5005,124],{"class":82},[47,5007,127],{"class":57},[47,5009,130],{"class":90},[47,5011,5012,5015,5017,5019,5021,5023],{"class":49,"line":3771},[47,5013,5014],{"class":78},"    set_state",[47,5016,151],{"class":90},[47,5018,518],{"class":57},[47,5020,139],{"class":90},[47,5022,980],{"class":53},[47,5024,157],{"class":90},[47,5026,5027,5029,5031,5034,5036],{"class":49,"line":3784},[47,5028,467],{"class":53},[47,5030,4705],{"class":86},[47,5032,5033],{"class":82}," >",[47,5035,4717],{"class":86},[47,5037,130],{"class":145},[47,5039,5041,5044,5046],{"class":49,"line":5040},34,[47,5042,5043],{"class":86},"        high_score",[47,5045,97],{"class":90},[47,5047,5048],{"class":86}," total_score\n",[47,5050,5052,5055,5057,5060,5062],{"class":49,"line":5051},35,[47,5053,5054],{"class":57},"        SaveManager",[47,5056,139],{"class":145},[47,5058,5059],{"class":78},"save_game",[47,5061,121],{"class":90},[47,5063,5064],{"class":64},"                   # 只在破纪录时写盘\n",[836,5066,5067,5079,5088,5097],{},[839,5068,5069,5072,5073,5075,5076],{},[18,5070,5071],{},"enum GameState","：本质是 0、1、2、3，但写名字比写数字清楚一百倍。游戏在任何时刻只处于其中一种——这就叫\"状态机\"。比小鸟多一个 ",[18,5074,565],{},"，因为这个游戏有暂停菜单：",[248,5077,5078],{},"状态是玩法决定的",[839,5080,5081,5084,5085,5087],{},[18,5082,5083],{},"set_state()"," 是唯一入口：所有改状态的地方都走这一个函数，它顺手 ",[18,5086,4890],{},"。\"状态变了但没人收到通知\"这种 bug 不可能发生",[839,5089,5090,5093,5094,5096],{},[18,5091,5092],{},"food_eaten.connect(add_score)","：信号带 1 个参数，接它的函数就要收 1 个参数。",[18,5095,4810],{}," 后面没有括号——有括号是\"现在就执行\"，没括号是\"把这个函数交给你，信号响了你再执行\"",[839,5098,5099,5102,5103,5105],{},[18,5100,5101],{},"save_game()"," 缩进在 ",[18,5104,2027],{}," 里：只有数据真的变了才写盘。每帧写会卡，每次死都写是白写",[10,5107,3417,5109,5111],{"id":5108},"把-maingd-改成只管蛇不管分数",[18,5110,850],{}," 改成\"只管蛇，不管分数\"",[15,5113,5114,5115,5117,5118,5121,5122,5124],{},"三处改动：",[18,5116,245],{}," 里连上状态、清分、设 PLAYING；移动函数开头加一行守卫，",[18,5119,5120],{},"die()"," 和吃东西改成发信号；删掉 ",[18,5123,5120],{},"，换成监听状态：",[38,5126,5128],{"className":40,"code":5127,"language":42,"meta":43,"style":43},"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",[18,5129,5130,5144,5168,5175,5180,5195,5199,5204,5225,5230,5248,5252,5278,5298,5335],{"__ignoreMap":43},[47,5131,5132,5134,5136,5138,5140,5142],{"class":49,"line":50},[47,5133,115],{"class":82},[47,5135,2479],{"class":78},[47,5137,121],{"class":90},[47,5139,124],{"class":82},[47,5141,127],{"class":57},[47,5143,130],{"class":90},[47,5145,5146,5148,5150,5152,5154,5156,5158,5160,5162,5164,5166],{"class":49,"line":61},[47,5147,467],{"class":53},[47,5149,501],{"class":57},[47,5151,139],{"class":90},[47,5153,506],{"class":86},[47,5155,2846],{"class":82},[47,5157,501],{"class":57},[47,5159,139],{"class":90},[47,5161,518],{"class":86},[47,5163,139],{"class":90},[47,5165,523],{"class":53},[47,5167,130],{"class":145},[47,5169,5170,5172],{"class":49,"line":68},[47,5171,4003],{"class":53},[47,5173,5174],{"class":64},"                                  # 不是进行中，什么都不做\n",[47,5176,5177],{"class":49,"line":75},[47,5178,5179],{"class":64},"    # ……撞墙 \u002F 撞自己：\n",[47,5181,5182,5185,5187,5189,5191,5193],{"class":49,"line":107},[47,5183,5184],{"class":57},"        EventBus",[47,5186,139],{"class":90},[47,5188,4824],{"class":86},[47,5190,139],{"class":145},[47,5192,4890],{"class":78},[47,5194,831],{"class":90},[47,5196,5197],{"class":49,"line":112},[47,5198,493],{"class":53},[47,5200,5201],{"class":49,"line":133},[47,5202,5203],{"class":64},"    # ……吃到：\n",[47,5205,5206,5208,5210,5212,5214,5216,5218,5220,5222],{"class":49,"line":160},[47,5207,5184],{"class":57},[47,5209,139],{"class":90},[47,5211,4606],{"class":86},[47,5213,139],{"class":145},[47,5215,4890],{"class":78},[47,5217,151],{"class":90},[47,5219,1766],{"class":2066},[47,5221,186],{"class":90},[47,5223,5224],{"class":64},"             # 喊一声「吃到 1 分」，加分和音效各自去听\n",[47,5226,5227],{"class":49,"line":192},[47,5228,5229],{"class":64},"    # ……吃到星星：\n",[47,5231,5232,5234,5236,5238,5240,5242,5244,5246],{"class":49,"line":197},[47,5233,5184],{"class":57},[47,5235,139],{"class":90},[47,5237,4606],{"class":86},[47,5239,139],{"class":145},[47,5241,4890],{"class":78},[47,5243,151],{"class":90},[47,5245,1780],{"class":2066},[47,5247,157],{"class":90},[47,5249,5250],{"class":49,"line":223},[47,5251,72],{"emptyLinePlaceholder":71},[47,5253,5254,5256,5258,5260,5262,5264,5266,5268,5270,5272,5274,5276],{"class":49,"line":584},[47,5255,115],{"class":82},[47,5257,633],{"class":78},[47,5259,151],{"class":90},[47,5261,638],{"class":145},[47,5263,91],{"class":90},[47,5265,501],{"class":57},[47,5267,139],{"class":90},[47,5269,518],{"class":86},[47,5271,186],{"class":90},[47,5273,124],{"class":82},[47,5275,127],{"class":57},[47,5277,130],{"class":90},[47,5279,5280,5282,5284,5286,5288,5290,5292,5294,5296],{"class":49,"line":600},[47,5281,467],{"class":53},[47,5283,967],{"class":86},[47,5285,675],{"class":82},[47,5287,501],{"class":57},[47,5289,139],{"class":90},[47,5291,518],{"class":86},[47,5293,139],{"class":90},[47,5295,980],{"class":53},[47,5297,130],{"class":145},[47,5299,5300,5302,5305,5307,5309,5311,5313,5315,5317,5319,5321,5323,5325,5327,5329,5331,5333],{"class":49,"line":623},[47,5301,3899],{"class":53},[47,5303,5304],{"class":86}," timer",[47,5306,2678],{"class":53},[47,5308,1026],{"class":90},[47,5310,2768],{"class":53},[47,5312,2460],{"class":103},[47,5314,1035],{"class":145},[47,5316,2768],{"class":53},[47,5318,4089],{"class":103},[47,5320,1035],{"class":145},[47,5322,2768],{"class":53},[47,5324,4092],{"class":103},[47,5326,1035],{"class":145},[47,5328,2768],{"class":53},[47,5330,2023],{"class":103},[47,5332,2176],{"class":90},[47,5334,130],{"class":145},[47,5336,5337,5340,5342,5344,5346],{"class":49,"line":628},[47,5338,5339],{"class":86},"            timer",[47,5341,139],{"class":145},[47,5343,3696],{"class":78},[47,5345,121],{"class":90},[47,5347,5348],{"class":64},"                        # 死了，四个计时器全停\n",[15,5350,242,5351,5353,5354,5357,5358,5360,5361,5363],{},[18,5352,850],{}," 撞墙时只\"喊一声\"，不自己停 Timer？因为\"死了之后该发生什么\"不是蛇该操心的事：要结算分数（GameManager 的事）、要弹面板（UI 的事）、要放音效（AudioManager 的事）。",[248,5355,5356],{},"蛇只知道\"我撞了\"，喊一声，谁关心谁自己来。"," 以后想加\"死亡震屏\"，是新增一个文件去听 ",[18,5359,4824],{},"，",[18,5362,850],{}," 一个字不用改。",[15,5365,5366,5367,5369,5370,5373,5374,5376,5377,5379,5380,5383],{},"那 Timer 谁停？",[18,5368,850],{}," 自己听 ",[18,5371,5372],{},"state_changed","，看到 ",[18,5375,980],{}," 就停。它听的是\"状态变了\"这个",[248,5378,1290],{},"，而不是自己去",[248,5381,5382],{},"决定","结果。这个区别就是\"裁判\"和\"球员\"的区别。",[15,5385,5386,5387,5389],{},"为什么四个 Timer 全停，不只停 ",[18,5388,2460],{},"？只停移动，死了以后食物还在长、星星还在出现，画面看起来像没死。\"结束\"要对所有会动的东西生效。",[10,5391,5393],{"id":5392},"savemanager和小鸟几乎一样","SaveManager：和小鸟几乎一样",[38,5395,5397],{"className":40,"code":5396,"language":42,"meta":43,"style":43},"extends Node\n## 存档：读写 user:\u002F\u002Fsave.json\n\nconst SAVE_PATH := \"user:\u002F\u002Fsave.json\"\n\nfunc save_game() -> void:\n    var data := { \"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",[18,5398,5399,5405,5410,5414,5426,5430,5445,5470,5503,5516,5542,5546,5574,5578,5593,5614,5621,5650,5678,5690,5697],{"__ignoreMap":43},[47,5400,5401,5403],{"class":49,"line":50},[47,5402,54],{"class":53},[47,5404,4531],{"class":57},[47,5406,5407],{"class":49,"line":61},[47,5408,5409],{"class":64},"## 存档：读写 user:\u002F\u002Fsave.json\n",[47,5411,5412],{"class":49,"line":68},[47,5413,72],{"emptyLinePlaceholder":71},[47,5415,5416,5418,5421,5423],{"class":49,"line":75},[47,5417,2057],{"class":82},[47,5419,5420],{"class":86}," SAVE_PATH",[47,5422,2063],{"class":90},[47,5424,5425],{"class":483}," \"user:\u002F\u002Fsave.json\"\n",[47,5427,5428],{"class":49,"line":107},[47,5429,72],{"emptyLinePlaceholder":71},[47,5431,5432,5434,5437,5439,5441,5443],{"class":49,"line":112},[47,5433,115],{"class":82},[47,5435,5436],{"class":78}," save_game",[47,5438,121],{"class":90},[47,5440,124],{"class":82},[47,5442,127],{"class":57},[47,5444,130],{"class":90},[47,5446,5447,5449,5452,5454,5456,5459,5462,5464,5466,5468],{"class":49,"line":133},[47,5448,2504],{"class":82},[47,5450,5451],{"class":86}," data",[47,5453,2063],{"class":90},[47,5455,4660],{"class":90},[47,5457,5458],{"class":483}," \"high_score\"",[47,5460,5461],{"class":145},": ",[47,5463,178],{"class":57},[47,5465,139],{"class":145},[47,5467,1042],{"class":86},[47,5469,4678],{"class":90},[47,5471,5472,5474,5477,5479,5482,5484,5487,5489,5492,5494,5496,5498,5501],{"class":49,"line":160},[47,5473,2504],{"class":82},[47,5475,5476],{"class":86}," file",[47,5478,2063],{"class":90},[47,5480,5481],{"class":57}," FileAccess",[47,5483,139],{"class":145},[47,5485,5486],{"class":78},"open",[47,5488,151],{"class":90},[47,5490,5491],{"class":86},"SAVE_PATH",[47,5493,2541],{"class":90},[47,5495,5481],{"class":57},[47,5497,139],{"class":90},[47,5499,5500],{"class":53},"WRITE",[47,5502,157],{"class":90},[47,5504,5505,5507,5509,5511,5514],{"class":49,"line":192},[47,5506,467],{"class":53},[47,5508,5476],{"class":86},[47,5510,675],{"class":82},[47,5512,5513],{"class":53}," null",[47,5515,130],{"class":145},[47,5517,5518,5521,5523,5526,5529,5531,5533,5535,5537,5540],{"class":49,"line":197},[47,5519,5520],{"class":78},"        push_warning",[47,5522,151],{"class":90},[47,5524,5525],{"class":483},"\"存档写入失败：",[47,5527,5528],{"class":103},"%s",[47,5530,1020],{"class":483},[47,5532,1023],{"class":82},[47,5534,5481],{"class":57},[47,5536,139],{"class":145},[47,5538,5539],{"class":78},"get_open_error",[47,5541,3802],{"class":90},[47,5543,5544],{"class":49,"line":223},[47,5545,493],{"class":53},[47,5547,5548,5551,5553,5556,5558,5561,5563,5566,5568,5571],{"class":49,"line":584},[47,5549,5550],{"class":86},"    file",[47,5552,139],{"class":145},[47,5554,5555],{"class":78},"store_string",[47,5557,151],{"class":90},[47,5559,5560],{"class":57},"JSON",[47,5562,139],{"class":145},[47,5564,5565],{"class":78},"stringify",[47,5567,151],{"class":90},[47,5569,5570],{"class":86},"data",[47,5572,5573],{"class":90},"))\n",[47,5575,5576],{"class":49,"line":600},[47,5577,72],{"emptyLinePlaceholder":71},[47,5579,5580,5582,5585,5587,5589,5591],{"class":49,"line":623},[47,5581,115],{"class":82},[47,5583,5584],{"class":78}," load_game",[47,5586,121],{"class":90},[47,5588,124],{"class":82},[47,5590,127],{"class":57},[47,5592,130],{"class":90},[47,5594,5595,5597,5599,5601,5603,5606,5608,5610,5612],{"class":49,"line":628},[47,5596,467],{"class":53},[47,5598,470],{"class":82},[47,5600,5481],{"class":57},[47,5602,139],{"class":145},[47,5604,5605],{"class":78},"file_exists",[47,5607,151],{"class":90},[47,5609,5491],{"class":86},[47,5611,186],{"class":90},[47,5613,130],{"class":145},[47,5615,5616,5618],{"class":49,"line":657},[47,5617,4003],{"class":53},[47,5619,5620],{"class":64},"                                  # 首次游玩，没存档\n",[47,5622,5623,5625,5627,5629,5631,5633,5635,5637,5639,5641,5643,5645,5648],{"class":49,"line":690},[47,5624,2504],{"class":82},[47,5626,5476],{"class":86},[47,5628,2063],{"class":90},[47,5630,5481],{"class":57},[47,5632,139],{"class":145},[47,5634,5486],{"class":78},[47,5636,151],{"class":90},[47,5638,5491],{"class":86},[47,5640,2541],{"class":90},[47,5642,5481],{"class":57},[47,5644,139],{"class":90},[47,5646,5647],{"class":53},"READ",[47,5649,157],{"class":90},[47,5651,5652,5654,5656,5658,5661,5663,5666,5668,5671,5673,5676],{"class":49,"line":703},[47,5653,2504],{"class":82},[47,5655,5451],{"class":86},[47,5657,97],{"class":90},[47,5659,5660],{"class":57}," JSON",[47,5662,139],{"class":145},[47,5664,5665],{"class":78},"parse_string",[47,5667,151],{"class":90},[47,5669,5670],{"class":86},"file",[47,5672,139],{"class":145},[47,5674,5675],{"class":78},"get_as_text",[47,5677,3802],{"class":90},[47,5679,5680,5682,5684,5686,5688],{"class":49,"line":719},[47,5681,467],{"class":53},[47,5683,5451],{"class":86},[47,5685,675],{"class":82},[47,5687,5513],{"class":53},[47,5689,130],{"class":145},[47,5691,5692,5694],{"class":49,"line":724},[47,5693,4003],{"class":53},[47,5695,5696],{"class":64},"                                  # 存档损坏，防崩\n",[47,5698,5699,5701,5703,5705,5707,5709,5711,5714,5716,5719,5721,5723],{"class":49,"line":740},[47,5700,136],{"class":57},[47,5702,139],{"class":90},[47,5704,1042],{"class":86},[47,5706,97],{"class":90},[47,5708,5451],{"class":86},[47,5710,139],{"class":145},[47,5712,5713],{"class":78},"get",[47,5715,151],{"class":90},[47,5717,5718],{"class":483},"\"high_score\"",[47,5720,2541],{"class":90},[47,5722,3498],{"class":2066},[47,5724,157],{"class":90},[15,5726,5727,5728,5731,5732,5735,5736,5739,5740,5742],{},"这就是\"外壳可复用\"：搬小鸟的，改一个字段名。",[18,5729,5730],{},"res:\u002F\u002F"," 是游戏本体，导出后打包成 ",[18,5733,5734],{},".pck"," 只读；",[18,5737,5738],{},"user:\u002F\u002F"," 是玩家电脑上的用户数据目录，可读可写。编辑器里往 ",[18,5741,5730],{}," 写可能成功、导出后必定失败——这坑特别隐蔽。",[10,5744,5745],{"id":5745},"音效也是听信号",[38,5747,5749],{"className":40,"code":5748,"language":42,"meta":43,"style":43},"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",[18,5750,5751,5757,5762,5766,5787,5807,5827,5831,5845,5864,5891,5895,5918,5932,5943,5949],{"__ignoreMap":43},[47,5752,5753,5755],{"class":49,"line":50},[47,5754,54],{"class":53},[47,5756,4531],{"class":57},[47,5758,5759],{"class":49,"line":61},[47,5760,5761],{"class":64},"## 音响：只负责 play()，不判断该不该播\n",[47,5763,5764],{"class":49,"line":68},[47,5765,72],{"emptyLinePlaceholder":71},[47,5767,5768,5770,5772,5775,5777,5780,5782,5784],{"class":49,"line":75},[47,5769,79],{"class":78},[47,5771,83],{"class":82},[47,5773,5774],{"class":86}," eat_sfx",[47,5776,91],{"class":90},[47,5778,5779],{"class":57}," AudioStreamPlayer",[47,5781,97],{"class":90},[47,5783,100],{"class":53},[47,5785,5786],{"class":103},"SFX\u002FEatSfx\n",[47,5788,5789,5791,5793,5796,5798,5800,5802,5804],{"class":49,"line":107},[47,5790,79],{"class":78},[47,5792,83],{"class":82},[47,5794,5795],{"class":86}," star_sfx",[47,5797,91],{"class":90},[47,5799,5779],{"class":57},[47,5801,97],{"class":90},[47,5803,100],{"class":53},[47,5805,5806],{"class":103},"SFX\u002FStarSfx\n",[47,5808,5809,5811,5813,5816,5818,5820,5822,5824],{"class":49,"line":112},[47,5810,79],{"class":78},[47,5812,83],{"class":82},[47,5814,5815],{"class":86}," die_sfx",[47,5817,91],{"class":90},[47,5819,5779],{"class":57},[47,5821,97],{"class":90},[47,5823,100],{"class":53},[47,5825,5826],{"class":103},"SFX\u002FDieSfx\n",[47,5828,5829],{"class":49,"line":133},[47,5830,72],{"emptyLinePlaceholder":71},[47,5832,5833,5835,5837,5839,5841,5843],{"class":49,"line":160},[47,5834,115],{"class":82},[47,5836,118],{"class":78},[47,5838,121],{"class":90},[47,5840,124],{"class":82},[47,5842,127],{"class":57},[47,5844,130],{"class":90},[47,5846,5847,5849,5851,5853,5855,5857,5859,5862],{"class":49,"line":192},[47,5848,4797],{"class":57},[47,5850,139],{"class":90},[47,5852,4606],{"class":86},[47,5854,139],{"class":145},[47,5856,148],{"class":78},[47,5858,151],{"class":90},[47,5860,5861],{"class":86},"_on_food_eaten",[47,5863,157],{"class":90},[47,5865,5866,5868,5870,5872,5874,5876,5878,5881,5883,5886,5888],{"class":49,"line":197},[47,5867,4797],{"class":57},[47,5869,139],{"class":90},[47,5871,4824],{"class":86},[47,5873,139],{"class":145},[47,5875,148],{"class":78},[47,5877,151],{"class":90},[47,5879,5880],{"class":86},"die_sfx",[47,5882,139],{"class":90},[47,5884,5885],{"class":86},"play",[47,5887,186],{"class":90},[47,5889,5890],{"class":64},"     # 没参数的信号，可以直接把 play 交给它\n",[47,5892,5893],{"class":49,"line":223},[47,5894,72],{"emptyLinePlaceholder":71},[47,5896,5897,5899,5902,5904,5906,5908,5910,5912,5914,5916],{"class":49,"line":584},[47,5898,115],{"class":82},[47,5900,5901],{"class":78}," _on_food_eaten",[47,5903,151],{"class":90},[47,5905,4565],{"class":145},[47,5907,91],{"class":90},[47,5909,212],{"class":57},[47,5911,186],{"class":90},[47,5913,124],{"class":82},[47,5915,127],{"class":57},[47,5917,130],{"class":90},[47,5919,5920,5922,5925,5927,5930],{"class":49,"line":600},[47,5921,467],{"class":53},[47,5923,5924],{"class":86}," points",[47,5926,3510],{"class":82},[47,5928,5929],{"class":2066}," 2",[47,5931,130],{"class":145},[47,5933,5934,5937,5939,5941],{"class":49,"line":623},[47,5935,5936],{"class":86},"        star_sfx",[47,5938,139],{"class":145},[47,5940,5885],{"class":78},[47,5942,831],{"class":90},[47,5944,5945,5947],{"class":49,"line":628},[47,5946,3717],{"class":53},[47,5948,130],{"class":145},[47,5950,5951,5954,5956,5958],{"class":49,"line":657},[47,5952,5953],{"class":86},"        eat_sfx",[47,5955,139],{"class":145},[47,5957,5885],{"class":78},[47,5959,831],{"class":90},[15,5961,5962,5963,5965,5966,3073,5969,5972,5973,5976,5977,5980],{},"一个不报错、但没声音的坑：",[18,5964,4606],{}," 不能直接 ",[18,5967,5968],{},"connect(eat_sfx.play)",[18,5970,5971],{},"play()"," 其实有一个参数——从第几秒开始播。信号带着 ",[18,5974,5975],{},"points = 2"," 过来，Godot 会老老实实调 ",[18,5978,5979],{},"play(2)","：从第 2 秒开始播一个 0.3 秒的音效 = 什么都听不到，而且一个错都不报。所以带参数的信号，要自己写一个函数接住参数再决定播什么。",[15,5982,5983],{},"下一章做外壳：菜单怎么把玩家的选择交给游戏。",[1125,5985,5986],{},"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":43,"searchDepth":61,"depth":61,"links":5988},[5989,5990,5991,5993,5994],{"id":4518,"depth":61,"text":4519},{"id":4628,"depth":61,"text":4629},{"id":5108,"depth":61,"text":5992},"把 main.gd 改成\"只管蛇，不管分数\"",{"id":5392,"depth":61,"text":5393},{"id":5745,"depth":61,"text":5745},"2026-09-06",{},"\u002Fdevlog\u002Fxggame-snake\u002F06-systems",{"title":4445,"description":1137},"devlog\u002Fxggame-snake\u002F06-systems","qSjB3Pzf525dDGKOCA04ufOEKkttkzIf8Z-_FHf8EQM",{"id":6002,"title":6003,"body":6004,"cover":1135,"date":6743,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":6744,"navigation":71,"path":6745,"seo":6746,"stem":6747,"toc":71,"__hash__":6748},"devlog\u002Fdevlog\u002Fxggame-snake\u002F07-settings-menu.md","设置菜单",{"type":7,"value":6005,"toc":6737},[6006,6016,6019,6023,6082,6088,6092,6101,6107,6232,6235,6245,6248,6262,6267,6375,6380,6543,6546,6549,6697,6718,6725,6731,6734],[15,6007,6008,6009,6011,6012,6015],{},"菜单场景和游戏场景是两个世界，玩家在菜单里选的\"穿墙\"，游戏怎么知道？直觉是\"菜单把值传给游戏\"。但 ",[18,6010,1100],{}," 会",[248,6013,6014],{},"先销毁菜单，再创建游戏","——游戏出生那一刻，菜单已经不在了，没人可传。",[15,6017,6018],{},"答案：放到一个\"两边都在的时候它都在\"的地方——Autoload，也就是 GameManager。菜单往里写，游戏从里读。这就是\"单一数据源\"：以后任何地方想知道\"现在是穿墙模式吗\"，都只问 GameManager 一个人。",[10,6020,6022],{"id":6021},"gamemanager-加三个字段","GameManager 加三个字段",[38,6024,6026],{"className":40,"code":6025,"language":42,"meta":43,"style":43},"## ===== 开局设置：菜单写、游戏读，只存这一份 =====\nvar wrap_mode := false                 # true = 穿墙，false = 撞墙死（菜单里选）\nvar cell_px := 20                      # 每格像素，本版固定 20 → 96×54 格\nvar snake_color := Color(\"#63de8c\")    # 蛇的皮肤，本版固定\n",[18,6027,6028,6033,6047,6061],{"__ignoreMap":43},[47,6029,6030],{"class":49,"line":50},[47,6031,6032],{"class":64},"## ===== 开局设置：菜单写、游戏读，只存这一份 =====\n",[47,6034,6035,6037,6040,6042,6044],{"class":49,"line":61},[47,6036,2159],{"class":82},[47,6038,6039],{"class":86}," wrap_mode",[47,6041,2063],{"class":90},[47,6043,813],{"class":53},[47,6045,6046],{"class":64},"                 # true = 穿墙，false = 撞墙死（菜单里选）\n",[47,6048,6049,6051,6054,6056,6058],{"class":49,"line":68},[47,6050,2159],{"class":82},[47,6052,6053],{"class":86}," cell_px",[47,6055,2063],{"class":90},[47,6057,2097],{"class":2066},[47,6059,6060],{"class":64},"                      # 每格像素，本版固定 20 → 96×54 格\n",[47,6062,6063,6065,6068,6070,6072,6074,6077,6079],{"class":49,"line":75},[47,6064,2159],{"class":82},[47,6066,6067],{"class":86}," snake_color",[47,6069,2063],{"class":90},[47,6071,2779],{"class":57},[47,6073,151],{"class":90},[47,6075,6076],{"class":483},"\"#63de8c\"",[47,6078,186],{"class":90},[47,6080,6081],{"class":64},"    # 蛇的皮肤，本版固定\n",[15,6083,6084,6085],{},"后两个这一版菜单里根本不露出来，为什么也放进 GameManager？因为它们是\"设置\"，只是这版不让玩家改。放在这，游戏代码读的是\"设置\"而不是写死的数字；以后想让玩家选棋盘大小、选皮肤，菜单一露，游戏一字不改。",[248,6086,6087],{},"先按设置写，露不露是菜单的事。",[10,6089,6091],{"id":6090},"阶段-4-的菜单先用最丑的控件","阶段 4 的菜单先用最丑的控件",[15,6093,6094,6097,6098,36],{},[18,6095,6096],{},"scenes\u002Fmenu.tscn","，然后文件系统里右键它 → ",[248,6099,6100],{},"设为主场景",[38,6102,6105],{"className":6103,"code":6104,"language":168},[1466],"Menu (Node2D)                          ← 挂脚本 scripts\u002Fmenu.gd\n└── CanvasLayer\n    └── VBoxContainer\n        ├── Title (Label)\n        ├── WrapToggle (CheckButton)   ← \"穿墙模式\"\n        └── StartButton (Button)\n",[18,6106,6104],{"__ignoreMap":43},[38,6108,6110],{"className":40,"code":6109,"language":42,"meta":43,"style":43},"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",[18,6111,6112,6118,6122,6143,6147,6161,6175,6179,6194,6215],{"__ignoreMap":43},[47,6113,6114,6116],{"class":49,"line":50},[47,6115,54],{"class":53},[47,6117,2043],{"class":57},[47,6119,6120],{"class":49,"line":61},[47,6121,72],{"emptyLinePlaceholder":71},[47,6123,6124,6126,6128,6131,6133,6136,6138,6140],{"class":49,"line":68},[47,6125,79],{"class":78},[47,6127,83],{"class":82},[47,6129,6130],{"class":86}," wrap_toggle",[47,6132,91],{"class":90},[47,6134,6135],{"class":57}," CheckButton",[47,6137,97],{"class":90},[47,6139,100],{"class":53},[47,6141,6142],{"class":103},"CanvasLayer\u002FVBoxContainer\u002FWrapToggle\n",[47,6144,6145],{"class":49,"line":75},[47,6146,72],{"emptyLinePlaceholder":71},[47,6148,6149,6151,6153,6155,6157,6159],{"class":49,"line":107},[47,6150,115],{"class":82},[47,6152,118],{"class":78},[47,6154,121],{"class":90},[47,6156,124],{"class":82},[47,6158,127],{"class":57},[47,6160,130],{"class":90},[47,6162,6163,6166,6168,6170,6172],{"class":49,"line":112},[47,6164,6165],{"class":86},"    wrap_toggle",[47,6167,139],{"class":145},[47,6169,711],{"class":78},[47,6171,121],{"class":90},[47,6173,6174],{"class":64},"                        # 手柄：一进菜单就得有一个控件亮着\n",[47,6176,6177],{"class":49,"line":133},[47,6178,72],{"emptyLinePlaceholder":71},[47,6180,6181,6183,6186,6188,6190,6192],{"class":49,"line":160},[47,6182,115],{"class":82},[47,6184,6185],{"class":78}," _on_start_button_pressed",[47,6187,121],{"class":90},[47,6189,124],{"class":82},[47,6191,127],{"class":57},[47,6193,130],{"class":90},[47,6195,6196,6198,6200,6203,6205,6207,6209,6212],{"class":49,"line":192},[47,6197,136],{"class":57},[47,6199,139],{"class":90},[47,6201,6202],{"class":86},"wrap_mode",[47,6204,97],{"class":90},[47,6206,6130],{"class":86},[47,6208,139],{"class":90},[47,6210,6211],{"class":86},"button_pressed",[47,6213,6214],{"class":64},"     # 勾上 = 穿墙\n",[47,6216,6217,6219,6221,6223,6225,6227,6230],{"class":49,"line":197},[47,6218,743],{"class":78},[47,6220,121],{"class":90},[47,6222,139],{"class":145},[47,6224,1100],{"class":78},[47,6226,151],{"class":90},[47,6228,6229],{"class":483},"\"res:\u002F\u002Fscenes\u002Fmain.tscn\"",[47,6231,157],{"class":90},[15,6233,6234],{},"为什么用最丑的 CheckButton？外壳阶段先要\"能用\"：能选、能传、能进游戏。好不好看是第 10 章的事，那时候换控件只动菜单，GameManager 和游戏代码不动——这就是把设置放 GameManager 的回报。",[15,6236,6237,6240,6241,6244],{},[248,6238,6239],{},"主场景","是按 F5（以及玩家双击游戏图标）时第一个加载的场景。之前设成 ",[18,6242,6243],{},"main.tscn"," 只是为了能按 F5；现在入口变成\"先菜单、再游戏\"，入口变了，主场景就得跟着变。不改的话 F5 直接进游戏、跳过菜单。",[10,6246,6247],{"id":6247},"常量什么时候该变成变量",[15,6249,6250,6251,6254,6255,6258,6259],{},"第 3 章说 ",[18,6252,6253],{},"GRID_W"," 是常量\"定了不能改\"，现在它要跟着设置走，怎么办？它的意思一直是\"",[248,6256,6257],{},"游戏运行中","不能改\"。现在格子大小是一项设置、穿墙是玩家开局前选的——都是\"开局读一次的变量\"，不再是常量。",[248,6260,6261],{},"判断标准：这个值会因为设置或玩家的选择而不同吗？会 → 变量；永远一样 → 常量。",[15,6263,6264,6266],{},[18,6265,850],{}," 顶部：",[38,6268,6270],{"className":40,"code":6269,"language":42,"meta":43,"style":43},"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",[18,6271,6272,6287,6299,6313,6327,6339,6351],{"__ignoreMap":43},[47,6273,6274,6276,6279,6281,6284],{"class":49,"line":50},[47,6275,2057],{"class":82},[47,6277,6278],{"class":86}," BOARD_W",[47,6280,2063],{"class":90},[47,6282,6283],{"class":2066}," 1920",[47,6285,6286],{"class":64},"                  # 画面宽，和项目设置的视口一致\n",[47,6288,6289,6291,6294,6296],{"class":49,"line":61},[47,6290,2057],{"class":82},[47,6292,6293],{"class":86}," BOARD_H",[47,6295,2063],{"class":90},[47,6297,6298],{"class":2066}," 1080\n",[47,6300,6301,6303,6306,6308,6310],{"class":49,"line":68},[47,6302,2057],{"class":82},[47,6304,6305],{"class":86}," START_LENGTH",[47,6307,2063],{"class":90},[47,6309,2112],{"class":2066},[47,6311,6312],{"class":64},"                # 蛇初始几节\n",[47,6314,6315,6317,6320,6322,6324],{"class":49,"line":75},[47,6316,2159],{"class":82},[47,6318,6319],{"class":86}," grid_w",[47,6321,2063],{"class":90},[47,6323,2067],{"class":2066},[47,6325,6326],{"class":64},"                       # 原来是 const GRID_W\n",[47,6328,6329,6331,6334,6336],{"class":49,"line":107},[47,6330,2159],{"class":82},[47,6332,6333],{"class":86}," grid_h",[47,6335,2063],{"class":90},[47,6337,6338],{"class":2066}," 54\n",[47,6340,6341,6343,6346,6348],{"class":49,"line":112},[47,6342,2159],{"class":82},[47,6344,6345],{"class":86}," cell_size",[47,6347,2063],{"class":90},[47,6349,6350],{"class":2066}," 20\n",[47,6352,6353,6355,6357,6359,6361,6363,6365,6367,6369,6372],{"class":49,"line":133},[47,6354,2159],{"class":82},[47,6356,2162],{"class":86},[47,6358,91],{"class":90},[47,6360,2167],{"class":57},[47,6362,2170],{"class":90},[47,6364,2173],{"class":57},[47,6366,2176],{"class":90},[47,6368,97],{"class":90},[47,6370,6371],{"class":90}," []",[47,6373,6374],{"class":64},"   # 第 0 个是头，_ready() 里生成\n",[15,6376,6377,6379],{},[18,6378,245],{}," 里：",[38,6381,6383],{"className":40,"code":6382,"language":42,"meta":43,"style":43},"    # 先有蛇，再生世界：食物要避开蛇出生的格子\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",[18,6384,6385,6390,6404,6419,6432,6436,6470,6483,6523,6527,6537],{"__ignoreMap":43},[47,6386,6387],{"class":49,"line":50},[47,6388,6389],{"class":64},"    # 先有蛇，再生世界：食物要避开蛇出生的格子\n",[47,6391,6392,6395,6397,6399,6401],{"class":49,"line":61},[47,6393,6394],{"class":86},"    cell_size",[47,6396,97],{"class":90},[47,6398,501],{"class":57},[47,6400,139],{"class":90},[47,6402,6403],{"class":86},"cell_px\n",[47,6405,6406,6409,6411,6413,6416],{"class":49,"line":68},[47,6407,6408],{"class":86},"    grid_w",[47,6410,97],{"class":90},[47,6412,6278],{"class":86},[47,6414,6415],{"class":82}," \u002F",[47,6417,6418],{"class":86}," cell_size\n",[47,6420,6421,6424,6426,6428,6430],{"class":49,"line":75},[47,6422,6423],{"class":86},"    grid_h",[47,6425,97],{"class":90},[47,6427,6293],{"class":86},[47,6429,6415],{"class":82},[47,6431,6418],{"class":86},[47,6433,6434],{"class":49,"line":107},[47,6435,72],{"emptyLinePlaceholder":71},[47,6437,6438,6440,6443,6445,6447,6449,6452,6454,6456,6458,6461,6463,6465,6467],{"class":49,"line":112},[47,6439,2504],{"class":82},[47,6441,6442],{"class":86}," mid",[47,6444,2063],{"class":90},[47,6446,2127],{"class":57},[47,6448,151],{"class":90},[47,6450,6451],{"class":86},"grid_w",[47,6453,6415],{"class":82},[47,6455,5929],{"class":2066},[47,6457,1035],{"class":145},[47,6459,6460],{"class":86},"grid_h",[47,6462,6415],{"class":82},[47,6464,5929],{"class":2066},[47,6466,186],{"class":90},[47,6468,6469],{"class":64},"                    # 蛇从正中间出发\n",[47,6471,6472,6474,6477,6479,6481],{"class":49,"line":133},[47,6473,2672],{"class":53},[47,6475,6476],{"class":86}," i",[47,6478,2678],{"class":53},[47,6480,6305],{"class":86},[47,6482,130],{"class":145},[47,6484,6485,6487,6489,6491,6493,6495,6497,6500,6502,6504,6507,6509,6511,6513,6515,6517,6520],{"class":49,"line":160},[47,6486,3725],{"class":86},[47,6488,139],{"class":145},[47,6490,3976],{"class":78},[47,6492,151],{"class":90},[47,6494,2173],{"class":57},[47,6496,151],{"class":90},[47,6498,6499],{"class":86},"mid",[47,6501,139],{"class":145},[47,6503,3492],{"class":86},[47,6505,6506],{"class":82}," -",[47,6508,6476],{"class":86},[47,6510,1035],{"class":145},[47,6512,6499],{"class":86},[47,6514,139],{"class":145},[47,6516,3527],{"class":86},[47,6518,6519],{"class":90},"))",[47,6521,6522],{"class":64},"   # i=0 是头，往左一节节排\n",[47,6524,6525],{"class":49,"line":192},[47,6526,72],{"emptyLinePlaceholder":71},[47,6528,6529,6532,6534],{"class":49,"line":197},[47,6530,6531],{"class":78},"    _on_food_timer_timeout",[47,6533,121],{"class":90},[47,6535,6536],{"class":64},"                # 开局先长一个食物（避开蛇）\n",[47,6538,6539,6541],{"class":49,"line":223},[47,6540,3745],{"class":78},[47,6542,831],{"class":90},[15,6544,6545],{},"用循环生成初始蛇身，而不是手写三个坐标——以后想改成 5 节，改一个数。",[10,6547,6548],{"id":6548},"一行实现穿墙",[38,6550,6552],{"className":40,"code":6551,"language":42,"meta":43,"style":43},"    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",[18,6553,6554,6566,6597,6623,6651,6679,6693],{"__ignoreMap":43},[47,6555,6556,6558,6560,6562,6564],{"class":49,"line":50},[47,6557,467],{"class":53},[47,6559,501],{"class":57},[47,6561,139],{"class":90},[47,6563,6202],{"class":86},[47,6565,130],{"class":145},[47,6567,6568,6571,6573,6575,6577,6580,6582,6584,6586,6588,6590,6592,6594],{"class":49,"line":61},[47,6569,6570],{"class":86},"        new_head",[47,6572,139],{"class":90},[47,6574,3492],{"class":86},[47,6576,97],{"class":90},[47,6578,6579],{"class":78}," posmod",[47,6581,151],{"class":90},[47,6583,3645],{"class":86},[47,6585,139],{"class":90},[47,6587,3492],{"class":86},[47,6589,2541],{"class":90},[47,6591,6319],{"class":86},[47,6593,186],{"class":90},[47,6595,6596],{"class":64},"   # -1 → 95，96 → 0\n",[47,6598,6599,6601,6603,6605,6607,6609,6611,6613,6615,6617,6619,6621],{"class":49,"line":68},[47,6600,6570],{"class":86},[47,6602,139],{"class":90},[47,6604,3527],{"class":86},[47,6606,97],{"class":90},[47,6608,6579],{"class":78},[47,6610,151],{"class":90},[47,6612,3645],{"class":86},[47,6614,139],{"class":90},[47,6616,3527],{"class":86},[47,6618,2541],{"class":90},[47,6620,6333],{"class":86},[47,6622,157],{"class":90},[47,6624,6625,6627,6629,6631,6633,6635,6637,6639,6641,6643,6645,6647,6649],{"class":49,"line":75},[47,6626,3226],{"class":53},[47,6628,2507],{"class":86},[47,6630,139],{"class":90},[47,6632,3492],{"class":86},[47,6634,3495],{"class":82},[47,6636,3498],{"class":2066},[47,6638,3501],{"class":82},[47,6640,2507],{"class":86},[47,6642,139],{"class":90},[47,6644,3492],{"class":86},[47,6646,3510],{"class":82},[47,6648,6319],{"class":86},[47,6650,3515],{"class":90},[47,6652,6653,6655,6657,6659,6661,6663,6665,6667,6669,6671,6673,6675,6677],{"class":49,"line":107},[47,6654,3520],{"class":82},[47,6656,2507],{"class":86},[47,6658,139],{"class":90},[47,6660,3527],{"class":86},[47,6662,3495],{"class":82},[47,6664,3498],{"class":2066},[47,6666,3501],{"class":82},[47,6668,2507],{"class":86},[47,6670,139],{"class":90},[47,6672,3527],{"class":86},[47,6674,3510],{"class":82},[47,6676,6333],{"class":86},[47,6678,130],{"class":145},[47,6680,6681,6683,6685,6687,6689,6691],{"class":49,"line":112},[47,6682,5184],{"class":57},[47,6684,139],{"class":90},[47,6686,4824],{"class":86},[47,6688,139],{"class":145},[47,6690,4890],{"class":78},[47,6692,831],{"class":90},[47,6694,6695],{"class":49,"line":133},[47,6696,493],{"class":53},[15,6698,6699,6702,6703,6706,6707,6710,6711,6714,6715],{},[18,6700,6701],{},"posmod","：positive modulo，正取余。普通 ",[18,6704,6705],{},"%"," 对负数给负结果（",[18,6708,6709],{},"-1 % 20 = -1","），",[18,6712,6713],{},"posmod(-1, 20) = 19","。从左边 x = -1 出去，要从最右边 x = 95 进来——正好是 posmod 的答案。",[248,6716,6717],{},"周期性的东西用取余。",[15,6719,6720,6721,6724],{},"验证\"设置\"这条线通了：勾穿墙 → 蛇从左边出去右边进来；再把 ",[18,6722,6723],{},"cell_px"," 临时改成 12 → F5 → 格子变小蛇变小（改回 20）。菜单露不露，游戏都读得到。",[15,6726,6727],{},[1117,6728],{"alt":6729,"src":6730},"07-settings-穿墙","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F07-settings-%E7%A9%BF%E5%A2%99.png",[15,6732,6733],{},"下一章：暂停菜单——你第一次做的外壳，也是坑最多的一个。",[1125,6735,6736],{},"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":43,"searchDepth":61,"depth":61,"links":6738},[6739,6740,6741,6742],{"id":6021,"depth":61,"text":6022},{"id":6090,"depth":61,"text":6091},{"id":6247,"depth":61,"text":6247},{"id":6548,"depth":61,"text":6548},"2026-09-07",{},"\u002Fdevlog\u002Fxggame-snake\u002F07-settings-menu",{"title":6003,"description":1137},"devlog\u002Fxggame-snake\u002F07-settings-menu","x-lJkhAlJo2lIzAAtXrOi4og3XIQzwSCWXgTMuvKksM",{"id":4,"title":5,"body":6750,"cover":1135,"date":1136,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":7614,"navigation":71,"path":1142,"seo":7615,"stem":1144,"toc":71,"__hash__":1145},{"type":7,"value":6751,"toc":7608},[6752,6754,6766,6906,6912,6918,6920,6926,6932,6972,6986,7000,7384,7386,7418,7424,7426,7442,7446,7448,7450,7462,7596,7600,7604,7606],[10,6753,13],{"id":12},[15,6755,6756,21,6758,25,6760,29,6762,25,6764,36],{},[18,6757,20],{},[18,6759,24],{},[18,6761,28],{},[18,6763,32],{},[18,6765,35],{},[38,6767,6768],{"className":40,"code":41,"language":42,"meta":43,"style":43},[18,6769,6770,6776,6780,6784,6802,6806,6820,6838,6862,6866,6888],{"__ignoreMap":43},[47,6771,6772,6774],{"class":49,"line":50},[47,6773,54],{"class":53},[47,6775,58],{"class":57},[47,6777,6778],{"class":49,"line":61},[47,6779,65],{"class":64},[47,6781,6782],{"class":49,"line":68},[47,6783,72],{"emptyLinePlaceholder":71},[47,6785,6786,6788,6790,6792,6794,6796,6798,6800],{"class":49,"line":75},[47,6787,79],{"class":78},[47,6789,83],{"class":82},[47,6791,87],{"class":86},[47,6793,91],{"class":90},[47,6795,94],{"class":57},[47,6797,97],{"class":90},[47,6799,100],{"class":53},[47,6801,104],{"class":103},[47,6803,6804],{"class":49,"line":107},[47,6805,72],{"emptyLinePlaceholder":71},[47,6807,6808,6810,6812,6814,6816,6818],{"class":49,"line":112},[47,6809,115],{"class":82},[47,6811,118],{"class":78},[47,6813,121],{"class":90},[47,6815,124],{"class":82},[47,6817,127],{"class":57},[47,6819,130],{"class":90},[47,6821,6822,6824,6826,6828,6830,6832,6834,6836],{"class":49,"line":133},[47,6823,136],{"class":57},[47,6825,139],{"class":90},[47,6827,142],{"class":86},[47,6829,139],{"class":145},[47,6831,148],{"class":78},[47,6833,151],{"class":90},[47,6835,154],{"class":86},[47,6837,157],{"class":90},[47,6839,6840,6842,6844,6846,6848,6850,6852,6854,6856,6858,6860],{"class":49,"line":160},[47,6841,163],{"class":86},[47,6843,139],{"class":90},[47,6845,168],{"class":86},[47,6847,97],{"class":90},[47,6849,173],{"class":78},[47,6851,151],{"class":90},[47,6853,178],{"class":57},[47,6855,139],{"class":90},[47,6857,183],{"class":86},[47,6859,186],{"class":90},[47,6861,189],{"class":64},[47,6863,6864],{"class":49,"line":192},[47,6865,72],{"emptyLinePlaceholder":71},[47,6867,6868,6870,6872,6874,6876,6878,6880,6882,6884,6886],{"class":49,"line":197},[47,6869,115],{"class":82},[47,6871,202],{"class":78},[47,6873,151],{"class":90},[47,6875,207],{"class":145},[47,6877,91],{"class":90},[47,6879,212],{"class":57},[47,6881,186],{"class":90},[47,6883,124],{"class":82},[47,6885,127],{"class":57},[47,6887,130],{"class":90},[47,6889,6890,6892,6894,6896,6898,6900,6902,6904],{"class":49,"line":223},[47,6891,163],{"class":86},[47,6893,139],{"class":90},[47,6895,168],{"class":86},[47,6897,97],{"class":90},[47,6899,173],{"class":78},[47,6901,151],{"class":90},[47,6903,207],{"class":86},[47,6905,157],{"class":90},[15,6907,242,6908,246,6910,251],{},[18,6909,245],{},[248,6911,250],{},[15,6913,254,6914,257,6916,260],{},[18,6915,24],{},[18,6917,24],{},[10,6919,264],{"id":263},[15,6921,267,6922,271,6924,275],{},[18,6923,270],{},[248,6925,274],{},[15,6927,278,6928,282,6930,286],{},[18,6929,281],{},[18,6931,285],{},[288,6933,6934,6944],{},[291,6935,6936],{},[294,6937,6938,6940,6942],{},[297,6939,299],{},[297,6941,302],{},[297,6943,305],{},[307,6945,6946,6954,6962],{},[294,6947,6948,6950,6952],{},[312,6949,314],{},[312,6951,317],{},[312,6953,320],{},[294,6955,6956,6958,6960],{},[312,6957,325],{},[312,6959,328],{},[312,6961,331],{},[294,6963,6964,6968,6970],{},[312,6965,6966],{},[248,6967,338],{},[312,6969,341],{},[312,6971,344],{},[15,6973,6974,356,6980,359,6982,362,6984,365],{},[248,6975,349,6976,352,6978,355],{},[18,6977,28],{},[18,6979,20],{},[18,6981,20],{},[18,6983,28],{},[18,6985,20],{},[15,6987,6988,21,6990,25,6992,376,6994,36,6996,383,6998,36],{},[18,6989,28],{},[18,6991,372],{},[18,6993,375],{},[18,6995,379],{},[18,6997,382],{},[18,6999,386],{},[38,7001,7002],{"className":40,"code":389,"language":42,"meta":43,"style":43},[18,7003,7004,7022,7040,7044,7066,7086,7090,7102,7116,7130,7152,7166,7180,7202,7206,7232,7260,7272,7284,7288,7302,7316,7338,7342,7356,7372],{"__ignoreMap":43},[47,7005,7006,7008,7010,7012,7014,7016,7018,7020],{"class":49,"line":50},[47,7007,79],{"class":78},[47,7009,83],{"class":82},[47,7011,400],{"class":86},[47,7013,91],{"class":90},[47,7015,405],{"class":57},[47,7017,97],{"class":90},[47,7019,100],{"class":53},[47,7021,412],{"class":103},[47,7023,7024,7026,7028,7030,7032,7034,7036,7038],{"class":49,"line":61},[47,7025,79],{"class":78},[47,7027,83],{"class":82},[47,7029,421],{"class":86},[47,7031,91],{"class":90},[47,7033,426],{"class":57},[47,7035,97],{"class":90},[47,7037,100],{"class":53},[47,7039,433],{"class":103},[47,7041,7042],{"class":49,"line":68},[47,7043,72],{"emptyLinePlaceholder":71},[47,7045,7046,7048,7050,7052,7054,7056,7058,7060,7062,7064],{"class":49,"line":75},[47,7047,115],{"class":82},[47,7049,444],{"class":78},[47,7051,151],{"class":90},[47,7053,449],{"class":145},[47,7055,91],{"class":90},[47,7057,454],{"class":57},[47,7059,186],{"class":90},[47,7061,124],{"class":82},[47,7063,127],{"class":57},[47,7065,130],{"class":90},[47,7067,7068,7070,7072,7074,7076,7078,7080,7082,7084],{"class":49,"line":107},[47,7069,467],{"class":53},[47,7071,470],{"class":82},[47,7073,473],{"class":86},[47,7075,139],{"class":145},[47,7077,478],{"class":78},[47,7079,151],{"class":90},[47,7081,484],{"class":483},[47,7083,186],{"class":90},[47,7085,130],{"class":145},[47,7087,7088],{"class":49,"line":112},[47,7089,493],{"class":53},[47,7091,7092,7094,7096,7098,7100],{"class":49,"line":133},[47,7093,498],{"class":53},[47,7095,501],{"class":57},[47,7097,139],{"class":90},[47,7099,506],{"class":86},[47,7101,130],{"class":145},[47,7103,7104,7106,7108,7110,7112,7114],{"class":49,"line":160},[47,7105,513],{"class":57},[47,7107,139],{"class":90},[47,7109,518],{"class":86},[47,7111,139],{"class":90},[47,7113,523],{"class":53},[47,7115,130],{"class":145},[47,7117,7118,7120,7122,7124,7126,7128],{"class":49,"line":192},[47,7119,530],{"class":78},[47,7121,121],{"class":90},[47,7123,139],{"class":145},[47,7125,537],{"class":86},[47,7127,97],{"class":90},[47,7129,542],{"class":53},[47,7131,7132,7134,7136,7138,7140,7142,7144,7146,7148,7150],{"class":49,"line":197},[47,7133,547],{"class":57},[47,7135,139],{"class":145},[47,7137,552],{"class":78},[47,7139,151],{"class":90},[47,7141,178],{"class":57},[47,7143,139],{"class":90},[47,7145,518],{"class":86},[47,7147,139],{"class":90},[47,7149,565],{"class":53},[47,7151,157],{"class":90},[47,7153,7154,7156,7158,7160,7162,7164],{"class":49,"line":223},[47,7155,513],{"class":57},[47,7157,139],{"class":90},[47,7159,518],{"class":86},[47,7161,139],{"class":90},[47,7163,565],{"class":53},[47,7165,130],{"class":145},[47,7167,7168,7170,7172,7174,7176,7178],{"class":49,"line":584},[47,7169,530],{"class":78},[47,7171,121],{"class":90},[47,7173,139],{"class":145},[47,7175,537],{"class":86},[47,7177,97],{"class":90},[47,7179,597],{"class":53},[47,7181,7182,7184,7186,7188,7190,7192,7194,7196,7198,7200],{"class":49,"line":600},[47,7183,547],{"class":57},[47,7185,139],{"class":145},[47,7187,552],{"class":78},[47,7189,151],{"class":90},[47,7191,178],{"class":57},[47,7193,139],{"class":90},[47,7195,518],{"class":86},[47,7197,139],{"class":90},[47,7199,523],{"class":53},[47,7201,157],{"class":90},[47,7203,7204],{"class":49,"line":623},[47,7205,72],{"emptyLinePlaceholder":71},[47,7207,7208,7210,7212,7214,7216,7218,7220,7222,7224,7226,7228,7230],{"class":49,"line":628},[47,7209,115],{"class":82},[47,7211,633],{"class":78},[47,7213,151],{"class":90},[47,7215,638],{"class":145},[47,7217,91],{"class":90},[47,7219,501],{"class":57},[47,7221,139],{"class":90},[47,7223,518],{"class":86},[47,7225,186],{"class":90},[47,7227,124],{"class":82},[47,7229,127],{"class":57},[47,7231,130],{"class":90},[47,7233,7234,7236,7238,7240,7242,7244,7246,7248,7250,7252,7254,7256,7258],{"class":49,"line":657},[47,7235,660],{"class":86},[47,7237,139],{"class":90},[47,7239,665],{"class":86},[47,7241,97],{"class":90},[47,7243,670],{"class":90},[47,7245,638],{"class":86},[47,7247,675],{"class":82},[47,7249,501],{"class":57},[47,7251,139],{"class":145},[47,7253,518],{"class":57},[47,7255,139],{"class":145},[47,7257,565],{"class":86},[47,7259,157],{"class":90},[47,7261,7262,7264,7266,7268,7270],{"class":49,"line":690},[47,7263,467],{"class":53},[47,7265,400],{"class":86},[47,7267,139],{"class":90},[47,7269,665],{"class":86},[47,7271,130],{"class":145},[47,7273,7274,7276,7278,7280,7282],{"class":49,"line":703},[47,7275,706],{"class":86},[47,7277,139],{"class":145},[47,7279,711],{"class":78},[47,7281,121],{"class":90},[47,7283,716],{"class":64},[47,7285,7286],{"class":49,"line":719},[47,7287,72],{"emptyLinePlaceholder":71},[47,7289,7290,7292,7294,7296,7298,7300],{"class":49,"line":724},[47,7291,115],{"class":82},[47,7293,729],{"class":78},[47,7295,121],{"class":90},[47,7297,124],{"class":82},[47,7299,127],{"class":57},[47,7301,130],{"class":90},[47,7303,7304,7306,7308,7310,7312,7314],{"class":49,"line":740},[47,7305,743],{"class":78},[47,7307,121],{"class":90},[47,7309,139],{"class":145},[47,7311,537],{"class":86},[47,7313,97],{"class":90},[47,7315,597],{"class":53},[47,7317,7318,7320,7322,7324,7326,7328,7330,7332,7334,7336],{"class":49,"line":756},[47,7319,136],{"class":57},[47,7321,139],{"class":145},[47,7323,552],{"class":78},[47,7325,151],{"class":90},[47,7327,178],{"class":57},[47,7329,139],{"class":90},[47,7331,518],{"class":86},[47,7333,139],{"class":90},[47,7335,523],{"class":53},[47,7337,157],{"class":90},[47,7339,7340],{"class":49,"line":779},[47,7341,72],{"emptyLinePlaceholder":71},[47,7343,7344,7346,7348,7350,7352,7354],{"class":49,"line":784},[47,7345,115],{"class":82},[47,7347,789],{"class":78},[47,7349,121],{"class":90},[47,7351,124],{"class":82},[47,7353,127],{"class":57},[47,7355,130],{"class":90},[47,7357,7358,7360,7362,7364,7366,7368,7370],{"class":49,"line":800},[47,7359,743],{"class":78},[47,7361,121],{"class":90},[47,7363,139],{"class":145},[47,7365,537],{"class":86},[47,7367,97],{"class":90},[47,7369,813],{"class":53},[47,7371,816],{"class":64},[47,7373,7374,7376,7378,7380,7382],{"class":49,"line":819},[47,7375,743],{"class":78},[47,7377,121],{"class":90},[47,7379,139],{"class":145},[47,7381,828],{"class":78},[47,7383,831],{"class":90},[15,7385,834],{},[836,7387,7388,7402,7410],{},[839,7389,7390,851,7396,854,7398,858,7400,861],{},[248,7391,843,7392,847,7394],{},[18,7393,846],{},[18,7395,850],{},[18,7397,20],{},[18,7399,857],{},[18,7401,28],{},[839,7403,7404,36,7408,872],{},[248,7405,866,7406],{},[18,7407,869],{},[18,7409,537],{},[839,7411,7412,885],{},[248,7413,7414,880,7416,884],{},[18,7415,879],{},[18,7417,883],{},[10,7419,7420,892,7422,895],{"id":888},[18,7421,891],{},[18,7423,857],{},[15,7425,898],{},[900,7427,7428,7432,7438],{},[839,7429,7430,907],{},[18,7431,906],{},[839,7433,7434,913,7436,917],{},[248,7435,912],{},[248,7437,916],{},[839,7439,7440,923],{},[18,7441,922],{},[15,7443,926,7444,930],{},[248,7445,929],{},[15,7447,933],{},[10,7449,937],{"id":936},[15,7451,7452,942,7454,29,7456,949,7458,383,7460,36],{},[18,7453,28],{},[18,7455,945],{},[18,7457,948],{},[18,7459,952],{},[18,7461,955],{},[38,7463,7464],{"className":40,"code":958,"language":42,"meta":43,"style":43},[18,7465,7466,7486,7498,7538,7548,7552,7566,7580],{"__ignoreMap":43},[47,7467,7468,7470,7472,7474,7476,7478,7480,7482,7484],{"class":49,"line":50},[47,7469,467],{"class":53},[47,7471,967],{"class":86},[47,7473,675],{"class":82},[47,7475,501],{"class":57},[47,7477,139],{"class":90},[47,7479,518],{"class":86},[47,7481,139],{"class":90},[47,7483,980],{"class":53},[47,7485,130],{"class":145},[47,7487,7488,7490,7492,7494,7496],{"class":49,"line":61},[47,7489,987],{"class":86},[47,7491,139],{"class":90},[47,7493,665],{"class":86},[47,7495,97],{"class":90},[47,7497,542],{"class":53},[47,7499,7500,7502,7504,7506,7508,7510,7512,7514,7516,7518,7520,7522,7524,7526,7528,7530,7532,7534,7536],{"class":49,"line":68},[47,7501,1000],{"class":86},[47,7503,139],{"class":90},[47,7505,168],{"class":86},[47,7507,97],{"class":90},[47,7509,1009],{"class":483},[47,7511,1012],{"class":103},[47,7513,1015],{"class":483},[47,7515,1012],{"class":103},[47,7517,1020],{"class":483},[47,7519,1023],{"class":82},[47,7521,1026],{"class":90},[47,7523,178],{"class":57},[47,7525,139],{"class":145},[47,7527,183],{"class":86},[47,7529,1035],{"class":145},[47,7531,178],{"class":57},[47,7533,139],{"class":145},[47,7535,1042],{"class":86},[47,7537,1045],{"class":90},[47,7539,7540,7542,7544,7546],{"class":49,"line":75},[47,7541,1050],{"class":86},[47,7543,139],{"class":145},[47,7545,711],{"class":78},[47,7547,831],{"class":90},[47,7549,7550],{"class":49,"line":107},[47,7551,72],{"emptyLinePlaceholder":71},[47,7553,7554,7556,7558,7560,7562,7564],{"class":49,"line":112},[47,7555,115],{"class":82},[47,7557,1067],{"class":78},[47,7559,121],{"class":90},[47,7561,124],{"class":82},[47,7563,127],{"class":57},[47,7565,130],{"class":90},[47,7567,7568,7570,7572,7574,7576,7578],{"class":49,"line":133},[47,7569,743],{"class":78},[47,7571,121],{"class":90},[47,7573,139],{"class":145},[47,7575,537],{"class":86},[47,7577,97],{"class":90},[47,7579,597],{"class":53},[47,7581,7582,7584,7586,7588,7590,7592,7594],{"class":49,"line":160},[47,7583,743],{"class":78},[47,7585,121],{"class":90},[47,7587,139],{"class":145},[47,7589,1100],{"class":78},[47,7591,151],{"class":90},[47,7593,1105],{"class":483},[47,7595,157],{"class":90},[15,7597,1110,7598],{},[248,7599,1113],{},[15,7601,7602],{},[1117,7603],{"alt":1119,"src":1120},[15,7605,1123],{},[1125,7607,1127],{},{"title":43,"searchDepth":61,"depth":61,"links":7609},[7610,7611,7612,7613],{"id":12,"depth":61,"text":13},{"id":263,"depth":61,"text":264},{"id":888,"depth":61,"text":1133},{"id":936,"depth":61,"text":937},{},{"title":5,"description":1137},{"id":7617,"title":1597,"body":7618,"cover":1135,"date":7974,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":7975,"navigation":71,"path":7976,"seo":7977,"stem":7978,"toc":71,"__hash__":7979},"devlog\u002Fdevlog\u002Fxggame-snake\u002F09-gamepad.md",{"type":7,"value":7619,"toc":7968},[7620,7627,7630,7682,7686,7689,7717,7736,7743,7851,7896,7899,7903,7912,7915,7918,7955,7960,7965],[15,7621,7622,7623,7626],{},"为什么这一章单独列？因为\"我测过了\"十有八九是用鼠标测的。鼠标点哪个就是哪个，不需要\"当前选中\"这个概念；",[248,7624,7625],{},"手柄没有指针，界面必须自己记住\"现在轮到哪个按钮\"","——这就是焦点。鼠标能用、手柄不能用，是新手最常交出去的成品。Steam Deck 玩家打开你的游戏，第一个按钮按不动，就关了。",[10,7628,7629],{"id":7629},"焦点三条规矩",[288,7631,7632,7642],{},[291,7633,7634],{},[294,7635,7636,7639],{},[297,7637,7638],{},"规矩",[297,7640,7641],{},"为什么",[307,7643,7644,7657,7672],{},[294,7645,7646,7654],{},[312,7647,7648],{},[248,7649,7650,7651,7653],{},"① 每个面板出现时，",[18,7652,879],{}," 给它的第一个按钮",[312,7655,7656],{},"不给，手柄按 A 是按在\"空气\"上。第 7、8 章各有一行就是干这个的",[294,7658,7659,7669],{},[312,7660,7661],{},[248,7662,7663,7664,7666,7667],{},"② 先 ",[18,7665,883],{},"，再 ",[18,7668,879],{},[312,7670,7671],{},"隐藏的控件拿不到焦点，顺序反了等于没写",[294,7673,7674,7679],{},[312,7675,7676],{},[248,7677,7678],{},"③ 同一个容器里的控件，十字键自动上下跳",[312,7680,7681],{},"Godot 自带；跨面板不会自动跳，所以每个面板都要自己给一次焦点",[10,7683,7685],{"id":7684},"十字键能移亮框按-a-却按不下按钮","十字键能移亮框，按 A 却按不下按钮",[15,7687,7688],{},"我当时的现象：菜单里十字键上下能在控件之间跳，亮框也在动，但按 A 什么都不发生，键盘 Enter 却可以。",[15,7690,7691,7692,7694,7695,7698,7699,7702,7703,7706,7707,7710,7711,7713,7714,7716],{},"按钮响应手柄，靠的是 Godot 内置动作 ",[18,7693,1671],{},"。去源码里查了一下（",[18,7696,7697],{},"core\u002Finput\u002Finput_map.cpp","）：",[248,7700,7701],{},"Godot 4 默认只给它绑了 Enter、小键盘 Enter、空格，没有手柄键","。方向类的 ",[18,7704,7705],{},"ui_up \u002F ui_down \u002F ui_left \u002F ui_right"," 有十字键和左摇杆，",[18,7708,7709],{},"ui_select"," 有手柄 Y，但确认（",[18,7712,1671],{},"）和取消（",[18,7715,1687],{},"）都没有。所以十字键能移亮框、A 按不下去，是引擎默认值就这样。",[15,7718,7719,7720,7722,7723,7726,7727,7722,7729,7731,7732,7735],{},"修法：项目设置 → 输入映射 → 勾「显示内置动作」→ ",[18,7721,1671],{}," → ",[18,7724,7725],{},"+"," → 按手柄 A；",[18,7728,1687],{},[18,7730,7725],{}," → 按 B。没有\"一键给所有内置动作加手柄\"的开关，但需要补的也只有这两个，而且存在 ",[18,7733,7734],{},"project.godot"," 里，以后抽模板项目会跟着走。",[15,7737,7738,7739,7742],{},"补完还不行？在 ",[18,7740,7741],{},"menu.gd"," 里加探针：",[38,7744,7746],{"className":40,"code":7745,"language":42,"meta":43,"style":43},"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",[18,7747,7748,7770,7793],{"__ignoreMap":43},[47,7749,7750,7752,7754,7756,7758,7760,7762,7764,7766,7768],{"class":49,"line":50},[47,7751,115],{"class":82},[47,7753,3158],{"class":78},[47,7755,151],{"class":90},[47,7757,449],{"class":145},[47,7759,91],{"class":90},[47,7761,454],{"class":57},[47,7763,186],{"class":90},[47,7765,124],{"class":82},[47,7767,127],{"class":57},[47,7769,130],{"class":90},[47,7771,7772,7774,7776,7779,7782,7784,7786,7788,7791],{"class":49,"line":61},[47,7773,467],{"class":53},[47,7775,473],{"class":86},[47,7777,7778],{"class":53}," is",[47,7780,7781],{"class":57}," InputEventJoypadButton",[47,7783,3194],{"class":82},[47,7785,473],{"class":86},[47,7787,139],{"class":90},[47,7789,7790],{"class":86},"pressed",[47,7792,130],{"class":145},[47,7794,7795,7797,7799,7802,7804,7807,7809,7812,7814,7816,7819,7822,7824,7826,7828,7831,7833,7836,7838,7840,7842,7844,7846,7849],{"class":49,"line":68},[47,7796,3655],{"class":78},[47,7798,151],{"class":90},[47,7800,7801],{"class":483},"\"手柄 \"",[47,7803,2541],{"class":90},[47,7805,7806],{"class":57}," Input",[47,7808,139],{"class":145},[47,7810,7811],{"class":78},"get_joy_name",[47,7813,151],{"class":90},[47,7815,2516],{"class":2066},[47,7817,7818],{"class":90},"),",[47,7820,7821],{"class":483}," \" | 按钮编号 \"",[47,7823,2541],{"class":90},[47,7825,473],{"class":86},[47,7827,139],{"class":90},[47,7829,7830],{"class":86},"button_index",[47,7832,2541],{"class":90},[47,7834,7835],{"class":483}," \" | 认成确认键=\"",[47,7837,2541],{"class":90},[47,7839,473],{"class":86},[47,7841,139],{"class":145},[47,7843,478],{"class":78},[47,7845,151],{"class":90},[47,7847,7848],{"class":483},"\"ui_accept\"",[47,7850,5573],{"class":90},[288,7852,7853,7862],{},[291,7854,7855],{},[294,7856,7857,7860],{},[297,7858,7859],{},"按 A 时打印",[297,7861,302],{},[307,7863,7864,7875,7888],{},[294,7865,7866,7872],{},[312,7867,7868,7869],{},"有编号，",[18,7870,7871],{},"=true",[312,7873,7874],{},"已经认成确认键了，问题在焦点——回去看亮框在不在按钮上",[294,7876,7877,7882],{},[312,7878,7868,7879],{},[18,7880,7881],{},"=false",[312,7883,7884,7885,7887],{},"这个键还没加进 ",[18,7886,1671],{},"（或加的时候按错了键）",[294,7889,7890,7893],{},[312,7891,7892],{},"什么都不打印",[312,7894,7895],{},"面键事件没进 Godot：关掉 Steam 再试（Steam 会接管手柄）",[15,7897,7898],{},"B \u002F X \u002F Y \"没反应\"是正常的——B 只对应\"取消\"，X、Y 默认没绑任何 UI 动作。",[10,7900,7902],{"id":7901},"另一个和手柄无关但害我半小时的开关","另一个和手柄无关、但害我半小时的开关",[15,7904,7905,7906,7908,7909,7911],{},"Godot 4.4 起游戏默认",[248,7907,3405],{},"跑。「游戏」标签的工具栏有三个模式：",[248,7910,3409],{},"。只有「输入」模式按键才进游戏；2D \u002F 3D 是运行时用鼠标点选节点的，会把输入截走。代码没错的时候，问题十有八九在\"事件有没有送到\"，先看这个开关。",[10,7913,7914],{"id":7914},"手柄专项验收",[15,7916,7917],{},"把鼠标推开，只用手柄走完整条链，每一步都看清楚亮框在哪：",[836,7919,7922,7931,7937,7943,7949],{"className":7920},[7921],"contains-task-list",[839,7923,7926,7930],{"className":7924},[7925],"task-list-item",[7927,7928],"input",{"disabled":71,"type":7929},"checkbox"," 菜单：十字键能在选项和 Start 之间跳；A 能切换选项",[839,7932,7934,7936],{"className":7933},[7925],[7927,7935],{"disabled":71,"type":7929}," Start → 进游戏，十字键 \u002F 摇杆能转弯",[839,7938,7940,7942],{"className":7939},[7925],[7927,7941],{"disabled":71,"type":7929}," Start 键 → 暂停面板出现，\"继续\"亮框；A → 继续",[839,7944,7946,7948],{"className":7945},[7925],[7927,7947],{"disabled":71,"type":7929}," 再暂停 → 下到\"重开\" → A → 新一局，蛇在动",[839,7950,7952,7954],{"className":7951},[7925],[7927,7953],{"disabled":71,"type":7929}," 撞死 → 结算面板，\"重开\"亮框；下到 Menu → A → 回到菜单，焦点在选项上",[15,7956,7957,7958,3073],{},"五条全过，这个游戏就是 Steam Deck 可玩的。有一条卡住 = 那个面板缺 ",[18,7959,879],{},[1425,7961,7962],{},[15,7963,7964],{},"这条清单以后每个游戏都要跑。手柄不是一个功能，是一种假设——第一天就绑，最后补要把所有 UI 的焦点重走一遍。",[1125,7966,7967],{},"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":43,"searchDepth":61,"depth":61,"links":7969},[7970,7971,7972,7973],{"id":7629,"depth":61,"text":7629},{"id":7684,"depth":61,"text":7685},{"id":7901,"depth":61,"text":7902},{"id":7914,"depth":61,"text":7914},"2026-09-09",{},"\u002Fdevlog\u002Fxggame-snake\u002F09-gamepad",{"title":1597,"description":1137},"devlog\u002Fxggame-snake\u002F09-gamepad","ODux3tSV1z2aFWa-UTs_47JEfak-BN2jGi0oxme1LDM",{"id":7981,"title":7982,"body":7983,"cover":1135,"date":9169,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":9170,"navigation":71,"path":9171,"seo":9172,"stem":9173,"toc":71,"__hash__":9174},"devlog\u002Fdevlog\u002Fxggame-snake\u002F10-ui-theme.md","UI：皮、总表、左右选",{"type":7,"value":7984,"toc":9163},[7985,7992,7997,8001,8016,8033,8120,8130,8149,8153,8160,8256,8263,8269,8273,8280,8315,8326,8332,8346,8350,8357,8363,8370,8998,9042,9051,9140,9151,9157,9160],[15,7986,7987,7988,7991],{},"贪吃蛇没有游戏画面要画（纯色方块就是它的经典视觉语言），美术的功夫全花在 UI 上——这反而最值得练：",[248,7989,7990],{},"UI 是每个游戏都要的，游戏画面每个游戏都不一样。"," 而且这一章一张图都不用找：圆角、边框、渐变全是 Godot 自己画的。",[1425,7993,7994],{},[15,7995,7996],{},"写法约定：菜单路径写中文；属性名照检查器写英文，括号里是中文意思。",[10,7998,8000],{"id":7999},"皮styleboxflat","皮：StyleBoxFlat",[15,8002,8003,8004,8007,8008,8011,8012,8015],{},"控件的\"皮\"叫 ",[248,8005,8006],{},"StyleBox","。Godot 画按钮时先画皮，再在皮上写字。",[18,8009,8010],{},"StyleBoxFlat"," 是纯几何画的皮，",[18,8013,8014],{},"StyleBoxTexture"," 是用图片当皮。",[15,8017,8018,8019,8022,8023,8025,8026,8029,8030,36],{},"文件系统 → ",[18,8020,8021],{},"theme\u002F"," 右键 → 新建资源 → 搜 ",[18,8024,8010],{},"（",[248,8027,8028],{},"要点一下让它高亮","，只搜不点建出来的是空白 Resource）→ ",[18,8031,8032],{},"btn_normal.tres",[288,8034,8035,8048],{},[291,8036,8037],{},[294,8038,8039,8042,8045],{},[297,8040,8041],{},"检查器里的名字",[297,8043,8044],{},"中文",[297,8046,8047],{},"填",[307,8049,8050,8061,8072,8085,8096,8107],{},[294,8051,8052,8055,8058],{},[312,8053,8054],{},"Draw Center",[312,8056,8057],{},"画填充",[312,8059,8060],{},"取消（空心）",[294,8062,8063,8066,8069],{},[312,8064,8065],{},"Border Width",[312,8067,8068],{},"边框粗细",[312,8070,8071],{},"四边 2",[294,8073,8074,8077,8080],{},[312,8075,8076],{},"Border Color",[312,8078,8079],{},"边框色",[312,8081,8082],{},[18,8083,8084],{},"#3a4064",[294,8086,8087,8090,8093],{},[312,8088,8089],{},"Corner Radius",[312,8091,8092],{},"圆角",[312,8094,8095],{},"四角 12",[294,8097,8098,8101,8104],{},[312,8099,8100],{},"Content Margins",[312,8102,8103],{},"内边距（文字离框多远）",[312,8105,8106],{},"左右 24，上下 12",[294,8108,8109,8112,8115],{},[312,8110,8111],{},"Expand Margins",[312,8113,8114],{},"外扩",[312,8116,8117],{},[248,8118,8119],{},"全部保持 0",[15,8121,8122,8125,8126,8129],{},[248,8123,8124],{},"Content Margins 和 Expand Margins 长得像，意思相反","：Content 是内边距，框会变大；Expand 是把皮画到框",[248,8127,8128],{},"外面","，框不变、画面溢出、点击区比看到的小、相邻按钮互相压。我第一次填错了这一排，然后发现\"按钮间隔比设的小\"，把容器间距调到 50 去补——补错了地方。",[15,8131,8132,8133,8136,8137,8140,8141,8144,8145,8148],{},"然后右键复制出其余的皮：",[18,8134,8135],{},"btn_hover","（边框换强调色）、",[18,8138,8139],{},"btn_pressed","（实心强调色，无边）、",[18,8142,8143],{},"btn_hover_pressed","（实心再亮一点）、",[18,8146,8147],{},"focus","（空心，白边 3）。为什么没选中是空心、选中是实心？人眼对\"有没有填充\"比\"填的什么颜色\"敏感得多。两态都实心只换底色，反差永远不够。",[10,8150,8152],{"id":8151},"按钮的-11-个槽","按钮的 11 个槽",[15,8154,8155,8156,8159],{},"选一个按钮 → 检查器 → ",[248,8157,8158],{},"Theme Overrides（主题覆盖）→ Styles","，11 个槽各是什么：",[288,8161,8162,8175],{},[291,8163,8164],{},[294,8165,8166,8169,8172],{},[297,8167,8168],{},"槽",[297,8170,8171],{},"什么时候显示",[297,8173,8174],{},"放哪张皮",[307,8176,8177,8190,8202,8217,8229,8245],{},[294,8178,8179,8182,8185],{},[312,8180,8181],{},"Normal",[312,8183,8184],{},"平时",[312,8186,8187],{},[18,8188,8189],{},"btn_normal",[294,8191,8192,8195,8198],{},[312,8193,8194],{},"Hover",[312,8196,8197],{},"鼠标停在上面",[312,8199,8200],{},[18,8201,8135],{},[294,8203,8204,8207,8213],{},[312,8205,8206],{},"Pressed",[312,8208,8209,8210],{},"鼠标按住；",[248,8211,8212],{},"开关按钮被选中时一直是它",[312,8214,8215],{},[18,8216,8139],{},[294,8218,8219,8222,8225],{},[312,8220,8221],{},"Hover Pressed",[312,8223,8224],{},"已选中的按钮被鼠标停住",[312,8226,8227],{},[18,8228,8143],{},[294,8230,8231,8234,8241],{},[312,8232,8233],{},"Focus",[312,8235,8236,8237,8240],{},"手柄 \u002F 键盘选到它，",[248,8238,8239],{},"叠加","在上面四种之上",[312,8242,8243],{},[18,8244,8147],{},[294,8246,8247,8250,8253],{},[312,8248,8249],{},"Disabled、5 个 * Mirrored",[312,8251,8252],{},"禁用；从右往左的语言",[312,8254,8255],{},"不用",[15,8257,8258,8259,8262],{},"两个细节：Godot 没有单独的\"选中\"槽，开关按钮被按下后就一直处于\"按下\"状态，所以选中态就是 Pressed。Focus 不算一种状态——一个选中的按钮同时有焦点，画的是 Pressed 皮 + Focus 白边，所以 ",[18,8260,8261],{},"focus.tres"," 要\"不画中心、只画白边\"，才能叠在任何状态上。",[15,8264,8265],{},[1117,8266],{"alt":8267,"src":8268},"10-ui-槽位","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F10-ui-%E6%A7%BD%E4%BD%8D.png",[10,8270,8272],{"id":8271},"总表theme","总表：Theme",[15,8274,8275,8276,8279],{},"覆盖只属于那一个节点。菜单、暂停面板、结算面板几十个控件一个个贴？",[248,8277,8278],{},"Theme 是一张\"类型 → 样式\"的总表","：告诉 Godot\"凡是 Button 都长这样\"，全项目一次生效。",[15,8281,8282,8284,8285,7722,8288,8291,8292,8294,8295,8298,8299,383,8301,3098,8304,8307,8308,8310,8311,8314],{},[18,8283,8021],{}," 右键 → 新建资源 → ",[18,8286,8287],{},"Theme",[18,8289,8290],{},"main.tres"," → 双击，底部出现主题编辑器 → 「+」添加类型 → ",[18,8293,379],{}," → 每一项旁边点「+」才算加进总表：Styles 五个槽拖对应的 ",[18,8296,8297],{},".tres","，Colors 四个字色，Font Sizes。再加 ",[18,8300,32],{},[18,8302,8303],{},"HBoxContainer",[18,8305,8306],{},"VBoxContainer"," 的 separation。选中 ",[18,8309,8290],{}," 本身，检查器里 ",[248,8312,8313],{},"Default Font"," 拖入字体文件——所有控件从此都用它。",[15,8316,8317,8318,8321,8322,8325],{},"最后 ",[248,8319,8320],{},"项目设置 → GUI → Theme → Custom"," 填这个文件。为什么装在项目设置里，不是拖到根节点上？Theme 沿 Control 父子链往下传，但 UI 挂在 CanvasLayer 下，",[248,8323,8324],{},"CanvasLayer 不是 Control","，链在这断了。项目级主题不走链，直接生效。",[15,8327,8328,8329],{},"回到那个按钮，把 Theme Overrides 全部右键恢复默认——它长得和刚才一模一样，只是皮来自总表了。F5，",[248,8330,8331],{},"暂停面板和结算面板的按钮一个字没改就换了皮。",[15,8333,8334,8335,8338,8339,8342,8343],{},"想要第二种长相（主按钮）？用",[248,8336,8337],{},"类型变体（Type Variation）","：主题编辑器里手动输入一个类型名如 ",[18,8340,8341],{},"PrimaryButton","、基类型选 Button、换几张皮，然后在那个节点的 Theme → Type Variation 填名字。",[248,8344,8345],{},"同一类控件的第二种长相 → 变体；只有这一个节点不一样 → 覆盖。",[10,8347,8349],{"id":8348},"左右选自己封装一个组件","左右选：自己封装一个组件",[15,8351,8352,8353,8356],{},"菜单里我不想用下拉（OptionButton 点开的列表是独立的 PopupMenu 窗口，字号改不动、手柄还要多按一层），要的是难度选择那种 ◀ 撞墙模式 ▶——游戏里最手柄友好的选项形式：一个焦点，左右键切值。Godot 没有文字版的左右选（",[18,8354,8355],{},"SpinBox"," 只能选数字），所以自己拼：",[38,8358,8361],{"className":8359,"code":8360,"language":168},[1466],"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",[18,8362,8360],{"__ignoreMap":43},[15,8364,8365,8366,8369],{},"根用 Button，因为它",[248,8367,8368],{},"能拿焦点、自动有 Theme 的悬停色和焦点白边","。两个箭头也是 Button，但 Focus Mode = None：能点，拿不到焦点——手柄玩家看到的永远是\"整个选项框亮着\"，而不是焦点在三个小东西之间跳。",[38,8371,8373],{"className":40,"code":8372,"language":42,"meta":43,"style":43},"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",[18,8374,8375,8382,8387,8405,8409,8444,8457,8469,8473,8487,8500,8534,8563,8589,8610,8614,8637,8656,8669,8679,8698,8708,8714,8718,8723,8747,8780,8800,8815,8826,8830,8835,8858,8866,8878,8893,8919,8965],{"__ignoreMap":43},[47,8376,8377,8379],{"class":49,"line":50},[47,8378,54],{"class":53},[47,8380,8381],{"class":57}," Button\n",[47,8383,8384],{"class":49,"line":61},[47,8385,8386],{"class":64},"## 左右选：◀ 值 ▶。手柄左右 \u002F 点箭头切换；按 A 或点中间 = 下一个\n",[47,8388,8389,8391,8394,8396,8399,8401,8403],{"class":49,"line":68},[47,8390,4557],{"class":82},[47,8392,8393],{"class":78}," changed",[47,8395,151],{"class":90},[47,8397,8398],{"class":145},"index",[47,8400,91],{"class":90},[47,8402,212],{"class":57},[47,8404,157],{"class":90},[47,8406,8407],{"class":49,"line":75},[47,8408,72],{"emptyLinePlaceholder":71},[47,8410,8411,8414,8416,8419,8421,8423,8425,8428,8430,8432,8434,8437,8439,8442],{"class":49,"line":107},[47,8412,8413],{"class":78},"@export",[47,8415,83],{"class":82},[47,8417,8418],{"class":86}," options",[47,8420,91],{"class":90},[47,8422,2167],{"class":57},[47,8424,2170],{"class":90},[47,8426,8427],{"class":57},"String",[47,8429,2176],{"class":90},[47,8431,97],{"class":90},[47,8433,1026],{"class":90},[47,8435,8436],{"class":483},"\"选项 A\"",[47,8438,1035],{"class":145},[47,8440,8441],{"class":483},"\"选项 B\"",[47,8443,1045],{"class":90},[47,8445,8446,8448,8450,8453,8455],{"class":49,"line":112},[47,8447,8413],{"class":78},[47,8449,83],{"class":82},[47,8451,8452],{"class":86}," index",[47,8454,2063],{"class":90},[47,8456,4710],{"class":2066},[47,8458,8459,8461,8464,8466],{"class":49,"line":133},[47,8460,2159],{"class":82},[47,8462,8463],{"class":86}," _tween",[47,8465,91],{"class":90},[47,8467,8468],{"class":57}," Tween\n",[47,8470,8471],{"class":49,"line":160},[47,8472,72],{"emptyLinePlaceholder":71},[47,8474,8475,8477,8479,8481,8483,8485],{"class":49,"line":192},[47,8476,115],{"class":82},[47,8478,118],{"class":78},[47,8480,121],{"class":90},[47,8482,124],{"class":82},[47,8484,127],{"class":57},[47,8486,130],{"class":90},[47,8488,8489,8492,8494,8497],{"class":49,"line":197},[47,8490,8491],{"class":86},"    text",[47,8493,97],{"class":90},[47,8495,8496],{"class":483}," \"\"",[47,8498,8499],{"class":64},"                                    # 根按钮不显示字，字由 Value 显示\n",[47,8501,8502,8505,8508,8510,8512,8514,8516,8518,8521,8523,8526,8528,8530,8532],{"class":49,"line":223},[47,8503,8504],{"class":53},"    %",[47,8506,8507],{"class":103},"LeftArrow",[47,8509,139],{"class":90},[47,8511,7790],{"class":86},[47,8513,139],{"class":145},[47,8515,148],{"class":78},[47,8517,151],{"class":90},[47,8519,8520],{"class":86},"_step",[47,8522,139],{"class":145},[47,8524,8525],{"class":78},"bind",[47,8527,151],{"class":90},[47,8529,2132],{"class":82},[47,8531,1766],{"class":2066},[47,8533,5573],{"class":90},[47,8535,8536,8538,8541,8543,8545,8547,8549,8551,8553,8555,8557,8559,8561],{"class":49,"line":584},[47,8537,8504],{"class":53},[47,8539,8540],{"class":103},"RightArrow",[47,8542,139],{"class":90},[47,8544,7790],{"class":86},[47,8546,139],{"class":145},[47,8548,148],{"class":78},[47,8550,151],{"class":90},[47,8552,8520],{"class":86},[47,8554,139],{"class":145},[47,8556,8525],{"class":78},[47,8558,151],{"class":90},[47,8560,1766],{"class":2066},[47,8562,5573],{"class":90},[47,8564,8565,8568,8570,8572,8574,8576,8578,8580,8582,8584,8586],{"class":49,"line":600},[47,8566,8567],{"class":86},"    pressed",[47,8569,139],{"class":145},[47,8571,148],{"class":78},[47,8573,151],{"class":90},[47,8575,8520],{"class":86},[47,8577,139],{"class":145},[47,8579,8525],{"class":78},[47,8581,151],{"class":90},[47,8583,1766],{"class":2066},[47,8585,6519],{"class":90},[47,8587,8588],{"class":64},"               # 点中间 \u002F 手柄 A = 下一个\n",[47,8590,8591,8593,8596,8598,8600,8602,8604,8606,8608],{"class":49,"line":623},[47,8592,8504],{"class":53},[47,8594,8595],{"class":103},"Value",[47,8597,139],{"class":90},[47,8599,168],{"class":86},[47,8601,97],{"class":90},[47,8603,8418],{"class":86},[47,8605,2170],{"class":90},[47,8607,8398],{"class":86},[47,8609,1045],{"class":90},[47,8611,8612],{"class":49,"line":628},[47,8613,72],{"emptyLinePlaceholder":71},[47,8615,8616,8618,8621,8623,8625,8627,8629,8631,8633,8635],{"class":49,"line":657},[47,8617,115],{"class":82},[47,8619,8620],{"class":78}," _gui_input",[47,8622,151],{"class":90},[47,8624,449],{"class":145},[47,8626,91],{"class":90},[47,8628,454],{"class":57},[47,8630,186],{"class":90},[47,8632,124],{"class":82},[47,8634,127],{"class":57},[47,8636,130],{"class":90},[47,8638,8639,8641,8643,8645,8647,8649,8652,8654],{"class":49,"line":690},[47,8640,467],{"class":53},[47,8642,473],{"class":86},[47,8644,139],{"class":145},[47,8646,478],{"class":78},[47,8648,151],{"class":90},[47,8650,8651],{"class":483},"\"ui_left\"",[47,8653,186],{"class":90},[47,8655,130],{"class":145},[47,8657,8658,8661,8663,8665,8667],{"class":49,"line":703},[47,8659,8660],{"class":78},"        _step",[47,8662,151],{"class":90},[47,8664,2132],{"class":82},[47,8666,1766],{"class":2066},[47,8668,157],{"class":90},[47,8670,8671,8674,8676],{"class":49,"line":719},[47,8672,8673],{"class":78},"        accept_event",[47,8675,121],{"class":90},[47,8677,8678],{"class":64},"                           # 吃掉事件，不然焦点会跳到左边的控件\n",[47,8680,8681,8683,8685,8687,8689,8691,8694,8696],{"class":49,"line":724},[47,8682,3226],{"class":53},[47,8684,473],{"class":86},[47,8686,139],{"class":145},[47,8688,478],{"class":78},[47,8690,151],{"class":90},[47,8692,8693],{"class":483},"\"ui_right\"",[47,8695,186],{"class":90},[47,8697,130],{"class":145},[47,8699,8700,8702,8704,8706],{"class":49,"line":740},[47,8701,8660],{"class":78},[47,8703,151],{"class":90},[47,8705,1766],{"class":2066},[47,8707,157],{"class":90},[47,8709,8710,8712],{"class":49,"line":756},[47,8711,8673],{"class":78},[47,8713,831],{"class":90},[47,8715,8716],{"class":49,"line":779},[47,8717,72],{"emptyLinePlaceholder":71},[47,8719,8720],{"class":49,"line":784},[47,8721,8722],{"class":64},"## 往某个方向走一步；到头了绕回去\n",[47,8724,8725,8727,8730,8732,8735,8737,8739,8741,8743,8745],{"class":49,"line":800},[47,8726,115],{"class":82},[47,8728,8729],{"class":78}," _step",[47,8731,151],{"class":90},[47,8733,8734],{"class":145},"dir",[47,8736,91],{"class":90},[47,8738,212],{"class":57},[47,8740,186],{"class":90},[47,8742,124],{"class":82},[47,8744,127],{"class":57},[47,8746,130],{"class":90},[47,8748,8749,8752,8754,8757,8759,8761,8763,8766,8768,8770,8772,8774,8776,8778],{"class":49,"line":819},[47,8750,8751],{"class":86},"    index",[47,8753,97],{"class":90},[47,8755,8756],{"class":78}," wrapi",[47,8758,151],{"class":90},[47,8760,8398],{"class":86},[47,8762,2521],{"class":82},[47,8764,8765],{"class":86}," dir",[47,8767,2541],{"class":90},[47,8769,3498],{"class":2066},[47,8771,2541],{"class":90},[47,8773,8418],{"class":86},[47,8775,139],{"class":145},[47,8777,2862],{"class":78},[47,8779,3802],{"class":90},[47,8781,8782,8784,8786,8788,8790,8792,8794,8796,8798],{"class":49,"line":3722},[47,8783,8504],{"class":53},[47,8785,8595],{"class":103},[47,8787,139],{"class":90},[47,8789,168],{"class":86},[47,8791,97],{"class":90},[47,8793,8418],{"class":86},[47,8795,2170],{"class":90},[47,8797,8398],{"class":86},[47,8799,1045],{"class":90},[47,8801,8802,8805,8807,8809,8811,8813],{"class":49,"line":3737},[47,8803,8804],{"class":86},"    changed",[47,8806,139],{"class":145},[47,8808,4890],{"class":78},[47,8810,151],{"class":90},[47,8812,8398],{"class":86},[47,8814,157],{"class":90},[47,8816,8817,8820,8822,8824],{"class":49,"line":3742},[47,8818,8819],{"class":78},"    _nudge",[47,8821,151],{"class":90},[47,8823,8734],{"class":86},[47,8825,157],{"class":90},[47,8827,8828],{"class":49,"line":3750},[47,8829,72],{"emptyLinePlaceholder":71},[47,8831,8832],{"class":49,"line":3755},[47,8833,8834],{"class":64},"## 文字往切换方向弹一下再回来\n",[47,8836,8837,8839,8842,8844,8846,8848,8850,8852,8854,8856],{"class":49,"line":3771},[47,8838,115],{"class":82},[47,8840,8841],{"class":78}," _nudge",[47,8843,151],{"class":90},[47,8845,8734],{"class":145},[47,8847,91],{"class":90},[47,8849,212],{"class":57},[47,8851,186],{"class":90},[47,8853,124],{"class":82},[47,8855,127],{"class":57},[47,8857,130],{"class":90},[47,8859,8860,8862,8864],{"class":49,"line":3784},[47,8861,467],{"class":53},[47,8863,8463],{"class":86},[47,8865,130],{"class":145},[47,8867,8868,8871,8873,8876],{"class":49,"line":5040},[47,8869,8870],{"class":86},"        _tween",[47,8872,139],{"class":145},[47,8874,8875],{"class":78},"kill",[47,8877,831],{"class":90},[47,8879,8880,8882,8884,8886,8889,8891],{"class":49,"line":5051},[47,8881,8504],{"class":53},[47,8883,8595],{"class":103},[47,8885,139],{"class":90},[47,8887,8888],{"class":86},"offset_transform_enabled",[47,8890,97],{"class":90},[47,8892,542],{"class":53},[47,8894,8896,8898,8900,8902,8905,8907,8909,8911,8914,8916],{"class":49,"line":8895},36,[47,8897,8504],{"class":53},[47,8899,8595],{"class":103},[47,8901,139],{"class":90},[47,8903,8904],{"class":86},"offset_transform_position",[47,8906,139],{"class":90},[47,8908,3492],{"class":86},[47,8910,97],{"class":90},[47,8912,8913],{"class":2066}," 16",[47,8915,2905],{"class":82},[47,8917,8918],{"class":86}," dir\n",[47,8920,8922,8925,8927,8930,8932,8934,8937,8939,8942,8944,8947,8949,8951,8954,8956,8958,8960,8963],{"class":49,"line":8921},37,[47,8923,8924],{"class":86},"    _tween",[47,8926,97],{"class":90},[47,8928,8929],{"class":78}," create_tween",[47,8931,121],{"class":90},[47,8933,139],{"class":145},[47,8935,8936],{"class":78},"set_trans",[47,8938,151],{"class":90},[47,8940,8941],{"class":57},"Tween",[47,8943,139],{"class":90},[47,8945,8946],{"class":53},"TRANS_BACK",[47,8948,186],{"class":90},[47,8950,139],{"class":145},[47,8952,8953],{"class":78},"set_ease",[47,8955,151],{"class":90},[47,8957,8941],{"class":57},[47,8959,139],{"class":90},[47,8961,8962],{"class":53},"EASE_OUT",[47,8964,157],{"class":90},[47,8966,8968,8970,8972,8975,8977,8979,8981,8983,8986,8988,8991,8993,8996],{"class":49,"line":8967},38,[47,8969,8924],{"class":86},[47,8971,139],{"class":145},[47,8973,8974],{"class":78},"tween_property",[47,8976,151],{"class":90},[47,8978,6705],{"class":53},[47,8980,8595],{"class":103},[47,8982,2541],{"class":90},[47,8984,8985],{"class":483}," \"offset_transform_position:x\"",[47,8987,2541],{"class":90},[47,8989,8990],{"class":2066}," 0.0",[47,8992,2541],{"class":90},[47,8994,8995],{"class":2066}," 0.18",[47,8997,157],{"class":90},[836,8999,9000,9009,9019,9028],{},[839,9001,9002,9005,9006,9008],{},[18,9003,9004],{},"_gui_input","：只有拿到焦点的控件才会收到键盘 \u002F 手柄事件——和 ",[18,9007,857],{}," 的\"全世界都听\"不同",[839,9010,9011,9014,9015,9018],{},[18,9012,9013],{},"accept_event()","：宣布\"这个事件我处理完了\"。不写，Godot 接着拿 ",[18,9016,9017],{},"ui_left"," 去做焦点导航——你按一下左，值切了、焦点也飞了",[839,9020,9021,9024,9025,9027],{},[18,9022,9023],{},"wrapi(x, 0, n)","：整数绕回，最后一个再往右 = 第一个。和第 7 章穿墙的 ",[18,9026,6701],{}," 是同一个思路",[839,9029,9030,9033,9034,9037,9038,9041],{},[18,9031,9032],{},"offset_transform_*","：Godot 4.7 新增的 Control 属性，视觉上挪、转、缩，",[248,9035,9036],{},"布局不受影响","（以前容器一重排就把你改的 scale 抹掉）。默认绕中心、默认只改画面不改鼠标判定——和 CSS 的 ",[18,9039,9040],{},"transform"," 一个意思",[15,9043,9044,9045,5360,9048,9050],{},"菜单里放一个实例，检查器 Options 填 ",[18,9046,9047],{},"[\"撞墙模式\", \"穿墙模式\"]",[18,9049,7741],{}," 缩成：",[38,9052,9054],{"className":40,"code":9053,"language":42,"meta":43,"style":43},"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",[18,9055,9056,9070,9083,9087,9101,9124],{"__ignoreMap":43},[47,9057,9058,9060,9062,9064,9066,9068],{"class":49,"line":50},[47,9059,115],{"class":82},[47,9061,118],{"class":78},[47,9063,121],{"class":90},[47,9065,124],{"class":82},[47,9067,127],{"class":57},[47,9069,130],{"class":90},[47,9071,9072,9074,9077,9079,9081],{"class":49,"line":61},[47,9073,8504],{"class":53},[47,9075,9076],{"class":103},"ModeSelector",[47,9078,139],{"class":145},[47,9080,711],{"class":78},[47,9082,831],{"class":90},[47,9084,9085],{"class":49,"line":68},[47,9086,72],{"emptyLinePlaceholder":71},[47,9088,9089,9091,9093,9095,9097,9099],{"class":49,"line":75},[47,9090,115],{"class":82},[47,9092,6185],{"class":78},[47,9094,121],{"class":90},[47,9096,124],{"class":82},[47,9098,127],{"class":57},[47,9100,130],{"class":90},[47,9102,9103,9105,9107,9109,9111,9113,9115,9117,9119,9121],{"class":49,"line":107},[47,9104,136],{"class":57},[47,9106,139],{"class":90},[47,9108,6202],{"class":86},[47,9110,97],{"class":90},[47,9112,1023],{"class":53},[47,9114,9076],{"class":103},[47,9116,139],{"class":90},[47,9118,8398],{"class":86},[47,9120,675],{"class":82},[47,9122,9123],{"class":2066}," 1\n",[47,9125,9126,9128,9130,9132,9134,9136,9138],{"class":49,"line":112},[47,9127,743],{"class":78},[47,9129,121],{"class":90},[47,9131,139],{"class":145},[47,9133,1100],{"class":78},[47,9135,151],{"class":90},[47,9137,6229],{"class":483},[47,9139,157],{"class":90},[15,9141,9142,9143,9150],{},"前端把它叫组件，Godot 把它叫场景，一回事。",[248,9144,9145,9146,9149],{},"你的组件库就是 ",[18,9147,9148],{},"scenes\u002Fui\u002F"," 这个文件夹","，它会原样搬进下一个游戏。",[15,9152,9153],{},[1117,9154],{"alt":9155,"src":9156},"10-ui-左右选","\u002Fimg\u002Fdevlog\u002Fxggame-snake\u002F10-ui-%E5%B7%A6%E5%8F%B3%E9%80%89.png",[15,9158,9159],{},"UI 重搭之后，回去把第 9 章的五条再跑一遍——焦点最容易丢。",[1125,9161,9162],{},"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":43,"searchDepth":61,"depth":61,"links":9164},[9165,9166,9167,9168],{"id":7999,"depth":61,"text":8000},{"id":8151,"depth":61,"text":8152},{"id":8271,"depth":61,"text":8272},{"id":8348,"depth":61,"text":8349},"2026-09-10",{},"\u002Fdevlog\u002Fxggame-snake\u002F10-ui-theme",{"title":7982,"description":1137},"devlog\u002Fxggame-snake\u002F10-ui-theme","oTB5ykDFUjeZQpyQQYJejlLIwmNSnKxfrkMdspK8sbY",{"id":9176,"title":9177,"body":9178,"cover":1135,"date":9355,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":9356,"navigation":71,"path":9357,"seo":9358,"stem":9359,"toc":71,"__hash__":9360},"devlog\u002Fdevlog\u002Fxggame-snake\u002F11-assets.md","图标、启动画面与音效",{"type":7,"value":9179,"toc":9348},[9180,9184,9187,9204,9207,9214,9221,9232,9239,9253,9256,9260,9271,9286,9294,9298,9313,9316,9325,9345],[10,9181,9183],{"id":9182},"图标aseprite-出场","图标：Aseprite 出场",[15,9185,9186],{},"这个游戏唯一要\"画\"的东西。16 像素级别的图标有五条规矩：",[900,9188,9189,9192,9195,9198,9201],{},[839,9190,9191],{},"先画剪影——全涂黑也能认出是蛇",[839,9193,9194],{},"1 像素深色轮廓，任何底色上都看得清",[839,9196,9197],{},"只用 3 个绿：本色、阴影、高光",[839,9199,9200],{},"眼睛 = 2×2 白 + 1 黑，靠前靠上——这是\"活\"的关键",[839,9202,9203],{},"一个识别物（舌头）用对比色",[15,9205,9206],{},"我第一版的问题是头和身子一样粗（剪影读不出\"头\"）、眼睛太大又居中（像贴纸）。第二版把眼睛做大靠上、舌头分叉，就对了。",[15,9208,9209,9210,9213],{},"画可以是 15×15（奇数画布有真正的中心像素，对称好画），但",[248,9211,9212],{},"画布用 16","：图标的世界按 8 的倍数运转（项目图标 128、ico 要 16\u002F32\u002F48\u002F256），15 放 8 倍是 120，到哪都差一点，会被非整数缩放搞糊。Sprite → Canvas Size → 16，多出的一圈透明。",[15,9215,9216,9217,9220],{},"导出规矩只有一条：",[248,9218,9219],{},"游戏里用的按 100% 导出，让 Godot 用 Nearest 放大；游戏外用的（项目图标、网站）在 Aseprite 里按整数倍放大再导","——网站和系统放大用的是平滑算法，16×16 交过去必糊。File → Export As，Resize 填百分比，它自带最近邻放大。",[15,9222,9223,9224,9227,9228,9231],{},"项目设置 → 应用 → 配置 → 图标 填 ",[18,9225,9226],{},"assets\u002Fui\u002FSnake.png","，Web 导出会自动拿它当浏览器标签页图标。项目根目录那个 Godot 默认的 ",[18,9229,9230],{},"icon.svg"," 删掉。",[10,9233,9235,9238],{"id":9234},"indexpng-不是图标是启动画面",[18,9236,9237],{},"index.png"," 不是图标，是启动画面",[15,9240,9241,9242,9244,9245,9248,9249,9252],{},"导出网页版后文件夹里有个 ",[18,9243,9237],{},"，一看是 Godot 的 logo。它是",[248,9246,9247],{},"启动画面（boot splash）","——网页壳加载 wasm 那几秒显示的图。项目设置里 ",[18,9250,9251],{},"show_image = false"," 只关引擎启动后那一瞬，网页壳照样要一张图，没指定就用默认的。",[15,9254,9255],{},"项目设置 → 应用 → 启动画面：Image 选自己的 logo，Bg Color 填游戏底色，像素图取消勾选「使用滤镜」。",[10,9257,9259],{"id":9258},"音效先生成再找","音效：先生成，再找",[15,9261,9262,9263,9266,9267,9270],{},"小游戏的音效优先",[248,9264,9265],{},"生成","而不是",[248,9268,9269],{},"下载","：生成的版权归自己，风格天生统一，不用记来源。这次三条音效（吃、星星、撞死）用的是我自己写的一个网页版 8-bit 生成器，原理和 sfxr \u002F ChipTone 一样：波形 → 音高曲线 → 音量包络 → 滤波，一个采样一个采样算出来，拼个 WAV 头下载。",[15,9272,9273,9274,9277,9278,9281,9282,9285],{},"真要下载的话，权威的几个：",[248,9275,9276],{},"freesound.org","（最大的 CC 库，搜索时先勾 CC0）、",[248,9279,9280],{},"Sonniss GDC 音效包","（每年免费放出的商业级大包）、",[248,9283,9284],{},"Kenney.nl","（全 CC0，独立游戏标配）。带 NC（非商用）的一律不用，哪怕游戏免费。",[15,9287,9288,9290,9291,9293],{},[18,9289,1814],{}," 的代码在第 6 章——它只听信号、只负责 ",[18,9292,5971],{},"，不判断该不该播。",[10,9295,9297],{"id":9296},"字体与-credits","字体与 CREDITS",[15,9299,9300,9301,9304,9305,9308,9309,9312],{},"标题那个手写体一开始是用 SystemFont 调的系统字体——",[248,9302,9303],{},"只在自己电脑上看得到","，别人的电脑、Steam Deck、网页版上都会退回默认字。查了一下它（Caveat）是 Google Fonts 的 OFL 字体，下载 ",[18,9306,9307],{},".ttf"," 放进 ",[18,9310,9311],{},"assets\u002Ffonts\u002F","、场景里换成文件，就能合法打包。",[15,9314,9315],{},"按钮用的方舟像素字体也是 OFL 1.1。OFL 要点：可免费商用、可嵌入游戏、可修改；不能单独把字体文件拿去卖；保留版权与许可声明。",[15,9317,9318,9320,9321,9324],{},[18,9319,1862],{}," 从第 2 章就建着，规矩是",[248,9322,9323],{},"引入素材的同一分钟写这里","。三类各一节：",[836,9326,9327,9333,9339],{},[839,9328,9329,9332],{},[248,9330,9331],{},"美术","：谁画的、什么工具、有没有第三方",[839,9334,9335,9338],{},[248,9336,9337],{},"字体","：一张表——字体 \u002F 文件 \u002F 作者 \u002F 来源网址 \u002F 协议 \u002F 用在哪",[839,9340,9341,9344],{},[248,9342,9343],{},"音效","：生成器产出的一句话说明\"版权归本人\"，列文件和用途。用自制工具生成同理，写工具名和网址；工具自己的代码协议（比如照 jsfxr 改的 MIT）只管工具代码，不管它生成的声音",[15,9346,9347],{},"下一章导出、上架、开源。",{"title":43,"searchDepth":61,"depth":61,"links":9349},[9350,9351,9353,9354],{"id":9182,"depth":61,"text":9183},{"id":9234,"depth":61,"text":9352},"index.png 不是图标，是启动画面",{"id":9258,"depth":61,"text":9259},{"id":9296,"depth":61,"text":9297},"2026-09-11",{},"\u002Fdevlog\u002Fxggame-snake\u002F11-assets",{"title":9177,"description":1137},"devlog\u002Fxggame-snake\u002F11-assets","PSqPfPJXRctYshrOx22g0aoc5UXGzedxjiLjSNApNzM",{"id":9362,"title":9363,"body":9364,"cover":1135,"date":9694,"description":1137,"extension":1138,"game":1139,"github":1140,"icon":1135,"meta":9695,"navigation":71,"path":9696,"seo":9697,"stem":9698,"toc":71,"__hash__":9699},"devlog\u002Fdevlog\u002Fxggame-snake\u002F12-export-and-review.md","导出、上架与复盘",{"type":7,"value":9365,"toc":9688},[9366,9370,9373,9403,9422,9438,9441,9471,9485,9488,9503,9506,9555,9558,9561,9583,9586,9589,9595,9606,9612,9650,9656,9659,9682,9685],[10,9367,9369],{"id":9368},"导出三处名字必须一致","导出：三处名字必须一致",[15,9371,9372],{},"项目 → 导出 → Web 预设：",[836,9374,9375,9386,9396],{},[839,9376,9377,9378,9381,9382,9385],{},"导出路径 ",[18,9379,9380],{},"build\u002Fweb\u002Findex.html","——",[248,9383,9384],{},"必须叫 index.html","，itch 的硬要求",[839,9387,9388,9389,9392,9393],{},"资源页的「",[248,9390,9391],{},"排除","文件筛选器」（Exclude Filter，不是上面那行 Include）填 ",[18,9394,9395],{},"build\u002F*",[839,9397,9398,9400,9401],{},[18,9399,1848],{}," 里 ",[18,9402,1852],{},[15,9404,9405,9406,9408,9409,9411,9412,9415,9416,9418,9419,9421],{},"导出目录名、筛选器、",[18,9407,1848],{}," 三处写的名字必须一致。我这次 ",[18,9410,1848],{}," 写的 ",[18,9413,9414],{},"builds\u002F","、文件夹叫 ",[18,9417,1852],{},"——差一个字母，11 MB 的产物差点进仓库；筛选器又填进了 Include 栏，pck 把 ",[18,9420,1852],{}," 自己打了进去（和小鸟同一个坑的另一种踩法）。",[15,9423,9424,9425,9427,9428,9433,9434,9437],{},"更稳的根治：在 ",[18,9426,1852],{}," 里放一个空文件 ",[248,9429,9430],{},[18,9431,9432],{},".gdignore","，编辑器就完全不扫描它——不导入、不打包、文件系统面板里也不显示。连导出产物里的 PNG 被反向导入生成 ",[18,9435,9436],{},".import"," 垃圾的问题也一起没了。",[15,9439,9440],{},"导完必须验一下有没有把自己打进去：",[38,9442,9446],{"className":9443,"code":9444,"language":9445,"meta":43,"style":43},"language-bash shiki shiki-themes vitesse-light vitesse-dark","strings build\u002Fweb\u002Findex.pck | grep -c res:\u002F\u002Fbuild     # 必须是 0\n","bash",[18,9447,9448],{"__ignoreMap":43},[47,9449,9450,9453,9456,9459,9462,9465,9468],{"class":49,"line":50},[47,9451,9452],{"class":78},"strings",[47,9454,9455],{"class":483}," build\u002Fweb\u002Findex.pck",[47,9457,9458],{"class":82}," |",[47,9460,9461],{"class":78}," grep",[47,9463,9464],{"class":103}," -c",[47,9466,9467],{"class":483}," res:\u002F\u002Fbuild",[47,9469,9470],{"class":64},"     # 必须是 0\n",[15,9472,9473,9474,7722,9477,9480,9481,9484],{},"点「导出项目」，不是「导出 PCK\u002FZIP」（后者只有资源包，没有 index.html）。本地试玩：",[18,9475,9476],{},"cd build\u002Fweb",[18,9478,9479],{},"python -m http.server 8000"," → 浏览器开 ",[18,9482,9483],{},"localhost:8000","。浏览器里插上手柄再试一遍——先随便按一个手柄键，浏览器才会把手柄交给网页。",[10,9486,9487],{"id":9487},"itch",[15,9489,9490,9491,9498,9499,9502],{},"手动打 zip（Godot 不会替你打）：",[248,9492,9493,9494,9497],{},"压缩 ",[18,9495,9496],{},"web"," 文件夹里的内容，不是文件夹本身","——解压后第一层必须直接是 ",[18,9500,9501],{},"index.html","。上 itch，Embed 尺寸填 1280×720（画面会自己等比缩放，不用填 1920）。",[15,9504,9505],{},"发布要准备的图：",[288,9507,9508,9521],{},[291,9509,9510],{},[294,9511,9512,9515,9518],{},[297,9513,9514],{},"放哪",[297,9516,9517],{},"图",[297,9519,9520],{},"尺寸",[307,9522,9523,9534,9545],{},[294,9524,9525,9528,9531],{},[312,9526,9527],{},"Godot",[312,9529,9530],{},"项目图标 \u002F 启动画面",[312,9532,9533],{},"128×128 \u002F 512×512",[294,9535,9536,9539,9542],{},[312,9537,9538],{},"GitHub",[312,9540,9541],{},"README 首图 \u002F Social preview",[312,9543,9544],{},"1280×720 \u002F 1280×640",[294,9546,9547,9549,9552],{},[312,9548,9487],{},[312,9550,9551],{},"封面 \u002F 截图 3～5 张",[312,9553,9554],{},"630×500 \u002F 1920×1080",[15,9556,9557],{},"截图前关书签栏、只框游戏区域——上次截图把浏览器书签栏一起截进去了。",[10,9559,9560],{"id":9560},"开源",[15,9562,9563,9564,9567,9568,9571,9572,9574,9575,3073,9577,9580,9581,3073],{},"和小鸟一样：",[18,9565,9566],{},"LICENSE","（MIT，代码 + 原创美术）、",[18,9569,9570],{},"README.md"," 加简 \u002F 繁 \u002F 粤三个译本、",[18,9573,1862],{},"（第三方字体 OFL 列出来）、",[18,9576,1848],{},[18,9578,9579],{},"export_presets.cfg"," 提交——里面只有 Web 预设和相对路径，别人拉下来就能照样导出；一旦以后加了 Android \u002F iOS，签名密钥会被明文写进去，那天再把它加进 ",[18,9582,1848],{},[15,9584,9585],{},"推之前扫一遍：绝对路径、用户名、邮箱、token，一个都不能有。先私有仓库 push，网页上看一遍再转公开。",[10,9587,9588],{"id":9588},"复盘",[15,9590,9591,9594],{},[248,9592,9593],{},"和计划不一样的地方","（都是合理的变奏）：",[836,9596,9597,9600,9603],{},[839,9598,9599],{},"菜单只留了\"撞墙 \u002F 穿墙\"一个选项，棋盘大小和皮肤固定——每多一个可选项，菜单、代码、测试都多一份；但它们在代码里仍然当\"设置\"写，以后露出来就能选",[839,9601,9602],{},"按钮弹动、标题呼吸、入场动画这些动效跳过了，留给下一个项目专门学",[839,9604,9605],{},"音效用自制工具，图标自己画，主按钮没做变体",[15,9607,9608,9611],{},[248,9609,9610],{},"这次踩的坑","（已经写回我的标准流程）：",[900,9613,9614,9626,9634,9641,9644,9647],{},[839,9615,9616,9617,9619,9620,9622,9623,9625],{},"导出目录 \u002F 筛选器 \u002F ",[18,9618,1848],{}," 三处名字不一致；Include 和 Exclude 填错栏 → ",[18,9621,9432],{}," 根治 + ",[18,9624,9452],{}," 自检",[839,9627,9628,9630,9631],{},[18,9629,245],{}," 里先生食物后建蛇 → 食物可能长在蛇出生的格子上。",[248,9632,9633],{},"初始化顺序：先有主角，再生世界",[839,9635,1701,9636,3098,9638,9640],{},[18,9637,1671],{},[18,9639,1687],{}," 没有手柄键，每个项目都要补",[839,9642,9643],{},"StyleBox 的 Content Margins 和 Expand Margins 填反了，然后去改容器间距补——补错了地方",[839,9645,9646],{},"场景里摆的节点尺寸和代码里的常量是两份真相（星星画成 4 格）→ 尺寸也收归代码",[839,9648,9649],{},"代码里留了给 AI 的疑问注释和教程痕迹（\"原来是 die()\"）→ 问完就删，注释只写代码现在在干什么",[15,9651,9652,9655],{},[248,9653,9654],{},"这个项目真正学到的","：不是贪吃蛇，是外壳——设置菜单怎么把选择交给游戏、暂停的 process_mode 传染、手柄焦点、Theme 总表、自己封装一个 UI 组件。第三个游戏开始，这些是在\"搬\"而不是\"写\"。",[15,9657,9658],{},"到这里，《XGGame-Snake》记录完了：",[836,9660,9661,9664,9667,9670,9673,9676,9679],{},[839,9662,9663],{},"✅ 立项表写死\"不做什么\"",[839,9665,9666],{},"✅ 蛇是一个数组，画面是它的镜子",[839,9668,9669],{},"✅ 四个 Timer 各管一件事",[839,9671,9672],{},"✅ EventBus 喊、GameManager 判、SaveManager 存",[839,9674,9675],{},"✅ 设置菜单、暂停、结算、手柄全流程",[839,9677,9678],{},"✅ Theme + 左右选组件",[839,9680,9681],{},"✅ 导出上架 itch，MIT 开源",[15,9683,9684],{},"下一款游戏见 💪",[1125,9686,9687],{},"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":43,"searchDepth":61,"depth":61,"links":9689},[9690,9691,9692,9693],{"id":9368,"depth":61,"text":9369},{"id":9487,"depth":61,"text":9487},{"id":9560,"depth":61,"text":9560},{"id":9588,"depth":61,"text":9588},"2026-09-12",{},"\u002Fdevlog\u002Fxggame-snake\u002F12-export-and-review",{"title":9363,"description":1137},"devlog\u002Fxggame-snake\u002F12-export-and-review","s8CKK5i2WAjpfTGwvEkNaknJ2QqCurcJld0vG6fQnfY",1788955183342]