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