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