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