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