设置菜单
Sep 7
菜单场景和游戏场景是两个世界,玩家在菜单里选的"穿墙",游戏怎么知道?直觉是"菜单把值传给游戏"。但 change_scene_to_file 会先销毁菜单,再创建游戏——游戏出生那一刻,菜单已经不在了,没人可传。
答案:放到一个"两边都在的时候它都在"的地方——Autoload,也就是 GameManager。菜单往里写,游戏从里读。这就是"单一数据源":以后任何地方想知道"现在是穿墙模式吗",都只问 GameManager 一个人。
GameManager 加三个字段
## ===== 开局设置:菜单写、游戏读,只存这一份 =====
var wrap_mode := false # true = 穿墙,false = 撞墙死(菜单里选)
var cell_px := 20 # 每格像素,本版固定 20 → 96×54 格
var snake_color := Color("#63de8c") # 蛇的皮肤,本版固定
后两个这一版菜单里根本不露出来,为什么也放进 GameManager?因为它们是"设置",只是这版不让玩家改。放在这,游戏代码读的是"设置"而不是写死的数字;以后想让玩家选棋盘大小、选皮肤,菜单一露,游戏一字不改。先按设置写,露不露是菜单的事。
阶段 4 的菜单先用最丑的控件
scenes/menu.tscn,然后文件系统里右键它 → 设为主场景:
Menu (Node2D) ← 挂脚本 scripts/menu.gd
└── CanvasLayer
└── VBoxContainer
├── Title (Label)
├── WrapToggle (CheckButton) ← "穿墙模式"
└── StartButton (Button)
extends Node2D
@onready var wrap_toggle: CheckButton = $CanvasLayer/VBoxContainer/WrapToggle
func _ready() -> void:
wrap_toggle.grab_focus() # 手柄:一进菜单就得有一个控件亮着
func _on_start_button_pressed() -> void:
GameManager.wrap_mode = wrap_toggle.button_pressed # 勾上 = 穿墙
get_tree().change_scene_to_file("res://scenes/main.tscn")
为什么用最丑的 CheckButton?外壳阶段先要"能用":能选、能传、能进游戏。好不好看是第 10 章的事,那时候换控件只动菜单,GameManager 和游戏代码不动——这就是把设置放 GameManager 的回报。
主场景是按 F5(以及玩家双击游戏图标)时第一个加载的场景。之前设成 main.tscn 只是为了能按 F5;现在入口变成"先菜单、再游戏",入口变了,主场景就得跟着变。不改的话 F5 直接进游戏、跳过菜单。
常量什么时候该变成变量
第 3 章说 GRID_W 是常量"定了不能改",现在它要跟着设置走,怎么办?它的意思一直是"游戏运行中不能改"。现在格子大小是一项设置、穿墙是玩家开局前选的——都是"开局读一次的变量",不再是常量。判断标准:这个值会因为设置或玩家的选择而不同吗?会 → 变量;永远一样 → 常量。
main.gd 顶部:
const BOARD_W := 1920 # 画面宽,和项目设置的视口一致
const BOARD_H := 1080
const START_LENGTH := 3 # 蛇初始几节
var grid_w := 96 # 原来是 const GRID_W
var grid_h := 54
var cell_size := 20
var snake_body: Array[Vector2i] = [] # 第 0 个是头,_ready() 里生成
_ready() 里:
# 先有蛇,再生世界:食物要避开蛇出生的格子
cell_size = GameManager.cell_px
grid_w = BOARD_W / cell_size
grid_h = BOARD_H / cell_size
var mid := Vector2i(grid_w / 2, grid_h / 2) # 蛇从正中间出发
for i in START_LENGTH:
snake_body.append(Vector2i(mid.x - i, mid.y)) # i=0 是头,往左一节节排
_on_food_timer_timeout() # 开局先长一个食物(避开蛇)
redraw()
用循环生成初始蛇身,而不是手写三个坐标——以后想改成 5 节,改一个数。
一行实现穿墙
if GameManager.wrap_mode:
new_head.x = posmod(new_head.x, grid_w) # -1 → 95,96 → 0
new_head.y = posmod(new_head.y, grid_h)
elif new_head.x < 0 or new_head.x >= grid_w \
or new_head.y < 0 or new_head.y >= grid_h:
EventBus.snake_died.emit()
return
posmod:positive modulo,正取余。普通 % 对负数给负结果(-1 % 20 = -1),posmod(-1, 20) = 19。从左边 x = -1 出去,要从最右边 x = 95 进来——正好是 posmod 的答案。周期性的东西用取余。
验证"设置"这条线通了:勾穿墙 → 蛇从左边出去右边进来;再把 cell_px 临时改成 12 → F5 → 格子变小蛇变小(改回 20)。菜单露不露,游戏都读得到。

下一章:暂停菜单——你第一次做的外壳,也是坑最多的一个。