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