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