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