原型1:蛇是一个数组
Sep 3
贪吃蛇玩法早就定了,为什么还要"白块原型"?为了逻辑与表现分离:先让数据对,再让画面好看。这样出 bug 时能一眼定位——数组里坐标就是错的 → 逻辑 bug;数组对但画错位置 → 渲染 bug。一上来就连美术一起做,出了问题得同时怀疑两层,调试时间翻倍。
先换脑子:贪吃蛇和小鸟的根本区别
| 小鸟(连续) | 贪吃蛇(离散) | |
|---|---|---|
| 位置 | 任意小数 (317.4, 892.1) | 只能在格子上 (5, 8) |
| 移动 | 每帧挪一点点 | 每 0.15 秒跳一整格 |
| 谁在驱动 | _physics_process(delta) 每秒 60 次 | Timer 到点发一次信号 |
| 碰撞 | 物理引擎算 | 比较两个格子坐标相不相等 |
这是整个项目最重要的观念转变:贪吃蛇里几乎没有"物理",只有"坐标"和"时间"。
搭场景
scenes/main.tscn,根节点 Node2D 命名 Main,下面 8 个子节点:
Main (Node2D) ← 挂脚本 scripts/main.gd
├── Board (ColorRect) ← 棋盘底色,Size 1920×1080
├── SnakeCells (Node2D) ← 空容器,蛇的方块生成在它下面
├── FoodCells (Node2D) ← 空容器,食物的方块生成在它下面
├── StarCell (ColorRect) ← 星星,Size 20×20,Visible 先关掉
├── MoveTimer (Timer) ← Wait Time 0.15,Autostart
├── FoodTimer (Timer) ← Wait Time 3,Autostart
├── StarSpawnTimer (Timer) ← Wait Time 12,Autostart
└── StarLifeTimer (Timer) ← Wait Time 5,One Shot,不 Autostart
为什么蛇和食物是"空容器",星星却直接是一个方块?数量不固定的东西(蛇身 3~400 节,食物 0~3 个)用"容器 + 动态生成子节点"来画;数量固定最多 1 个的,直接放一个节点,用显示/隐藏控制。"有几个"决定"怎么画"。
为什么 4 个 Timer 不合成一个?一个 Timer 只管一件事,名字就是说明书:看到 StarLifeTimer 在跑,就知道"星星还活着"。合成一个省 3 个节点,换来一堆 if 判断"这次响是为了什么"。节点便宜,脑子贵。
数据结构
extends Node2D
## ===== 常量:整个游戏只认这几个数 =====
const GRID_W := 96 # 横向几格
const GRID_H := 54 # 纵向几格
const CELL_SIZE := 20 # 一格是几个像素 96 × 20 = 1920,54 × 20 = 1080
const MAX_FOOD := 3 # 场上最多几个普通食物
const NO_CELL := Vector2i(-1, -1) # 「没有」—— 一个棋盘上不存在的格子
## ===== 数据:蛇 = 坐标数组,第 0 个是头 =====
var snake_body: Array[Vector2i] = [
Vector2i(48, 27), # 头(正中间)
Vector2i(47, 27),
Vector2i(46, 27), # 尾
]
var direction := Vector2i.RIGHT # 现在正在往哪走
var next_direction := Vector2i.RIGHT # 玩家刚按的方向,下一步才生效
## ===== 数据:食物 =====
var foods: Array[Vector2i] = [Vector2i(60, 27)] # 普通食物,先手放一个
var star := NO_CELL # 星星,最多 1 颗;NO_CELL 表示现在没有
func _ready() -> void:
print(snake_body)
几行关键的解释:
Vector2i:Vector 2 integer,只存整数的二维坐标。格子坐标天然是整数;用Vector2会出现(5.0000001, 8),比较"相等"会失败Array[Vector2i]:类型化数组,只装Vector2i,塞别的当场报错- 第 0 个是头:移动 = 头部插一格 + 尾部删一格。头放数组开头,
insert(0, ...)最直观 foods是数组,star不是:和场景结构一一对应——数据结构和场景结构长得一样NO_CELL = (-1, -1):哨兵值。Vector2i类型的变量不能存null,那"现在没有星星"怎么表示?用一个棋盘上不可能出现的坐标当"没有"
F5,输出面板打印 [(48, 27), (47, 27), (46, 27)]。画面还是没东西,正常——先验数据,再画。
让蛇"动"起来(先在控制台动)
选中 MoveTimer → 节点面板 → 信号 → 双击 timeout() → 连接到 Main:
func _on_move_timer_timeout() -> void:
direction = next_direction # 玩家最近按的方向,现在才真正生效
var new_head := snake_body[0] + direction
snake_body.insert(0, new_head) # ① 头部插入新格子
snake_body.pop_back() # ② 删掉尾巴
print(snake_body)
为什么用 Timer,不用小鸟那种 _physics_process:_physics_process 每秒跑 60 次,适合"每帧挪一点点"。贪吃蛇是"每 0.15 秒跳一格",用 Timer 到点才动一次,天然就是离散的。
为什么移动是"插头删尾",不是"每一节往前挪":
移动前 [头(48,27)] [(47,27)] [(46,27)]
↓ 向右
① 插头 [新头(49,27)] [(48,27)] [(47,27)] [(46,27)]
② 删尾 [新头(49,27)] [(48,27)] [(47,27)]
逐节挪:蛇 50 节要算 50 次,而且要从尾往头算(不然会覆盖)。插头删尾:永远只操作 2 个元素,不管多长。而且"吃到食物"只要"不删尾"就自动变长——一行都不用多写。 这是贪吃蛇最漂亮的设计。
F5,输出面板每 0.15 秒打印一次,头的 x 一直加 1。数据先对了,下一章画出来。