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