一、问题背景

在很多贪吃蛇实现中,蛇身移动往往通过“整体数组搬运”完成:每走一步,把身体数组整体后移一位。

for (int i = length - 1; i > 0; --i) {
    body[i] = body[i - 1];
}
body[0] = new_head;

这种方式在蛇身较短时问题不大,但一旦长度增加,就会有一些致命问题

  1. 每移动一步,都会整体搬运蛇身数组时间复杂度是 O(N),
  2. 蛇越长,每一帧越慢
  3. 在嵌入式设备、多人模式、低端平台上尤其明显

但实际上,蛇身移动的本质只有一句话:
4. 新蛇头进入一个格子,旧蛇尾离开一个格子。

本文介绍一种工程化的蛇身移动实现:
使用循环队列管理蛇身节点索引,并在每一步移动中复用旧节点,实现 O(1) 复杂度的蛇身更新。

二、整体架构流程

优化目标:

  1. 单步移动 固定时间复杂度 O(1)
  2. 不使用 memmove不动态 malloc
  3. 适合嵌入式 / 多人 / 网络同步场景

核心思想:
用循环队列保存“蛇身段在数组中的索引”,
每一步把队首的身体段取出来,改写成新的“脖子段”,再放回队尾

如下图所示,大概分为三个步骤进行:
在这里插入图片描述

三、蛇身数据结构设计

typedef struct {
	char type;
	char direction;
	XY 	position;  // 简单的x、y结构体,存储坐标
} BodySegment;  // 蛇身的坐标、类型、方向数据

typedef struct {
    int length;                         // 蛇身总长度

    BodySegment segments[MAX_SEGMENTS]; // 实际存放蛇身段数据
    int body_queue[MAX_SEGMENTS];       // 存 segments 的下标(循环队列)

    int body_length;                    // 队列长度
    int body_head;                      // 队首索引
    int body_tail;                      // 队尾索引
} Snake;

角色分工非常清晰:
segments[ ]:真正存放身体段数据
body_queue[ ]:只存“哪些 segments 属于蛇身”的索引
循环队列:负责 O(1) 管理蛇身顺序

整个队列和蛇身方向相反,即蛇头后的第一节蛇身为队尾,从这里入队,最后一节蛇身为队首,从这里出队。最后一节出队->在队首入队,来实现用队列管理的单步移动。

四、循环队列操作(O(1))

// 入队:把一个身体段索引加入蛇身
void snake_body_enqueue(Snake* s, int seg_index) {
    if (s->body_length >= MAX_SEGMENTS - 1) return;

    s->body_queue[s->body_tail] = seg_index;
    s->body_tail = (s->body_tail + 1) % MAX_SEGMENTS;
    s->body_length++;
}

// 出队:取出最靠近蛇尾的身体段索引
int snake_body_dequeue(Snake* s) {
    if (s->body_length == 0) return -1;

    int index = s->body_queue[s->body_head];
    s->body_head = (s->body_head + 1) % MAX_SEGMENTS;
    s->body_length--;
    return index;
}

// 查看队首(不出队)
int snake_body_peek_head(Snake* s) {
    if (s->body_length == 0) return -1;
    return s->body_queue[s->body_head];
}

五、单步移动的完整流程(不吃食物)

下面是最关键的优化部分(在移动顺序上,采用“蛇尾 → 蛇身 → 蛇头”的更新策略,使得整个过程无需额外保存坐标,进一步降低了计算和状态维护成本):

  1. 旧蛇尾前移(逻辑上的“尾巴收缩”)
// 获取队首(最末尾身体)坐标
int tail_body_index = snake_body_peek_head(s);

// 蛇尾移动到该身体段的位置
segments[TAIL_INDEX].position  = segments[tail_body_index].position;
segments[TAIL_INDEX].direction = segments[tail_body_index].direction;
segments[TAIL_INDEX].type      = BODY_TYPE_TAIL;
注意:这里没有移动数组,只是改了蛇尾的位置
  1. 复用一个身体段,作为新的“脖子段”
// 最后一节蛇身从队首出队
int reuse_index = snake_body_dequeue(s);

// 原蛇头所在格,成为新的身体段
segments[reuse_index].position  = segments[HEAD_INDEX].position;
segments[reuse_index].direction = segments[HEAD_INDEX].direction;
segments[reuse_index].type      = new_neck_type;   // STRAIGHT / TURN
segments[reuse_index].Turn      = new_turn;

// 入队放回队列尾部
snake_body_enqueue(s, reuse_index);

这一步是性能优化的核心:

不创建新节点
不搬运任何数组
只是“拿一个旧节点,改写后复用”
  1. 蛇头前进一步
segments[HEAD_INDEX].position  = next_pos;
segments[HEAD_INDEX].direction = next_direction;
segments[HEAD_INDEX].type      = BODY_TYPE_HEAD;

至此,一步移动完成。

六、吃食物时的差异:只增长,不收缩

吃到食物时,蛇身不会缩尾巴:

snake->length++;

int new_index = length; 
snake_body_enqueue(s, new_index); // 入队一个新蛇身下标

// 新段同样放在“原蛇头位置”
segments[new_index].position  = segments[HEAD_INDEX].position;
segments[new_index].direction = segments[HEAD_INDEX].direction;
segments[new_index].type      = new_neck_type;
segments[new_index].Turn      = new_turn;

逻辑依然清晰:
普通移动:出队-入队复用身体段
吃食物:直接入队新增身体段

七、性能对比

实现方式 单步复杂度 蛇长 100 时
整体搬运数组 O(N) 每帧搬 100 个结构体
本文方案 O(1) 固定更新 3~4 个节点

在多人蛇 / 嵌入式 / 网络同步场景中优势非常明显。

八、总结

这套蛇身移动实现的关键不是“算法多复杂”,而是:

把问题本质想清楚
用合适的数据结构表达它
避免一切不必要的 O(N) 操作
蛇身不是在“整体移动”,而是在“复用节点、更新关系”。

这种思路不仅适用于贪吃蛇,也适用于大量“队列式实体更新”的游戏与嵌入式系统。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐