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