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