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