谢夏戈 @ xiexiage.com

定时生成与过期

Sep 5

这是这个游戏最特别的一节。经典贪吃蛇里食物永远只有一个;这里场上可能有 0~3 个食物、一颗随时会消失的星星,玩家要做选择。数据结构只多了一个数组,体验完全不一样。

三个 Timer,三个函数

依次选中 FoodTimerStarSpawnTimerStarLifeTimer → 节点面板 → 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() 再建蛇。食物的"空格子"判断要看到蛇,蛇还没出生时它以为整个棋盘都是空的,于是食物可能长在蛇出生的格子上。改成先建蛇再生食物。初始化顺序:先有主角,再生世界。

05-timers-星星

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

2023-PRESENT © 谢夏戈