定时生成与过期
Sep 5
这是这个游戏最特别的一节。经典贪吃蛇里食物永远只有一个;这里场上可能有 0~3 个食物、一颗随时会消失的星星,玩家要做选择。数据结构只多了一个数组,体验完全不一样。
三个 Timer,三个函数
依次选中 FoodTimer、StarSpawnTimer、StarLifeTimer → 节点面板 → timeout() → 连接到 Main:
## 每 3 秒:场上不够 3 个食物就长一个
func _on_food_timer_timeout() -> void:
if foods.size() >= MAX_FOOD:
return # 够多了,这次不长
var cell := random_empty_cell()
if cell != NO_CELL:
foods.append(cell)
redraw()
## 每 12 秒:没有星星就放一颗,并开始倒数它的寿命
func _on_star_spawn_timer_timeout() -> void:
if star != NO_CELL:
return # 已经有一颗了
star = random_empty_cell()
if star != NO_CELL:
$StarLifeTimer.start() # 从现在起倒数 5 秒
redraw()
## 5 秒到:没人吃 → 星星消失
func _on_star_life_timer_timeout() -> void:
star = NO_CELL
redraw()
然后回到第 3 章:把 foods 那行手放的 Vector2i(60, 27) 删掉改成 [],在 _ready() 里加一句 _on_food_timer_timeout()——开局立刻长一个,不用干等 3 秒。
为什么开局要手动调一次 _on_food_timer_timeout(),不是继续手放一个坐标?手放的坐标是写死的。用同一套生成逻辑,就不会有"两套规则"。一个函数既能被 Timer 叫,也能被你手动叫——这就是函数的意义。
三个容易漏的判断
为什么 spawn 之后都要判断 != NO_CELL:棋盘满了 random_empty_cell() 会返回 NO_CELL。不判断,(-1, -1) 就会被塞进 foods,画到屏幕外,还会让 in foods 出现幽灵食物。凡是可能返回"没有"的函数,调用方必须检查。
为什么吃到星星时要 $StarLifeTimer.stop()(第 4 章那行):不停的话,你 2 秒就吃了,3 秒后它照样响,star = NO_CELL——但这时候可能已经又生成了新的一颗,它被无辜清掉。Timer 是有状态的东西,用完要收。
为什么 StarLifeTimer 是 One Shot、不 Autostart:它是星星的寿命。星星出现那一刻才开始倒数,倒数一次就够了。其他三个是"每隔一段时间做一次",要一直重复。
一个上线后才发现的顺序 bug
_ready() 里我一开始是先 _on_food_timer_timeout() 再建蛇。食物的"空格子"判断要看到蛇,蛇还没出生时它以为整个棋盘都是空的,于是食物可能长在蛇出生的格子上。改成先建蛇再生食物。初始化顺序:先有主角,再生世界。

到这里核心循环完整了:蛇能跑、能转、能死,食物按时间长,星星会出现会消失。剩下全是外壳。