本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:点阵贪吃蛇是一款基于点阵显示技术和单片机开发的电子游戏,通过点亮与熄灭点阵像素模拟经典贪吃蛇玩法。本项目适合单片机初学者进行综合实践,涵盖C语言编程、点阵屏驱动、游戏逻辑设计、按键控制、定时器中断等关键技术。通过该项目,学习者可以掌握嵌入式系统开发的核心流程,并提升硬件与软件协同设计的能力。
点阵贪吃蛇

1. 单片机基础与点阵贪吃蛇开发概述

单片机是一种高度集成的微型计算机系统,广泛应用于各类嵌入式设备中,具备体积小、功耗低、控制能力强等特点。在本项目中,我们将基于单片机平台开发一个点阵贪吃蛇游戏,通过点阵屏实现图形化显示与用户交互。

该项目不仅具备趣味性,更是一个综合性嵌入式开发实践案例,涵盖硬件驱动、图形控制、状态管理与用户输入处理等多个关键技术点。通过该开发过程,可深入理解单片机系统的工作机制与软硬件协同设计方法。

项目整体架构包括:点阵屏驱动模块、蛇体逻辑控制模块、食物生成与碰撞检测模块以及按键输入处理模块。后续章节将围绕这些模块展开详细实现与优化。

2. 点阵屏结构与驱动原理详解

点阵屏作为嵌入式系统中常见的显示设备,广泛应用于电子钟、仪表、游戏设备等场景。它通过控制多个LED的亮灭来实现字符、图形和动画的显示。在点阵贪吃蛇游戏的开发中,点阵屏不仅是视觉输出的核心载体,更是整个游戏交互体验的基础。本章将深入探讨点阵屏的结构组成、工作原理、驱动方式、通信协议及硬件连接配置,为后续章节中游戏的显示控制打下坚实的基础。

2.1 点阵屏的基本结构与工作原理

点阵屏的基本结构由多个LED点阵组成,通常按照行列方式排列。每个LED的亮灭由行和列的交叉点控制,通过控制不同行列的电平状态,可以点亮特定的LED,从而实现字符或图形的显示。

2.1.1 点阵屏的组成与显示方式

点阵屏通常由8×8、16×16或32×32个LED单元组成,每个LED单元可以看作一个像素点。以8×8点阵屏为例,其结构如下图所示(使用Mermaid绘制):

graph TD
    A[行控制线] --> B(行驱动电路)
    B --> C{LED矩阵}
    D[列控制线] --> E(列驱动电路)
    E --> C
    C --> F[显示输出]

结构说明:

  • 行控制线 :控制每一行的选通信号。
  • 列控制线 :控制每一列的电流方向,决定哪些LED点亮。
  • LED矩阵 :8行8列共64个LED组成,每个交叉点代表一个LED。
  • 行/列驱动电路 :用于放大控制信号,驱动LED工作。

显示方式:

点阵屏的显示方式主要分为 静态显示 动态扫描显示 两种:

显示方式 特点说明 优缺点分析
静态显示 每个LED独立控制,无需扫描 显示稳定,但硬件资源消耗大
动态扫描显示 通过逐行扫描的方式控制LED,利用人眼视觉暂留效应实现显示 节省引脚资源,但需要高频刷新控制

在实际应用中,尤其是资源有限的单片机系统中,动态扫描显示是主流选择。

2.1.2 行列扫描的基本原理

动态扫描的基本原理是 逐行点亮 。例如,在8×8点阵中,依次点亮第1行、第2行……第8行,每一行点亮的时间极短(通常为1ms左右),由于人眼的视觉暂留效应,看起来就像所有LED同时点亮。

扫描流程:

sequenceDiagram
    participant MCU
    participant RowDriver
    participant ColDriver
    participant LEDMatrix

    loop 扫描每一行
        MCU->>RowDriver: 发送当前行地址
        RowDriver->>LEDMatrix: 选中当前行
        MCU->>ColDriver: 设置列数据
        ColDriver->>LEDMatrix: 设置列电平
        LEDMatrix->>User: 显示当前行内容
    end

代码示例:

以下是一个简单的8×8点阵屏扫描控制代码片段(使用STM32 HAL库):

// 定义列数据数组(每行8位,共8行)
uint8_t led_buffer[8] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};

// 扫描函数
void scan_led_matrix() {
    for (int row = 0; row < 8; row++) {
        select_row(row);            // 选择当前行
        set_column_data(led_buffer[row]);  // 设置列数据
        HAL_Delay(1);               // 延时1ms
        clear_column();             // 清除列数据,防止残影
    }
}

代码逻辑分析:

  • led_buffer 数组存储每行要显示的列数据,每一位对应一个LED。
  • select_row() 函数用于选择当前要显示的行,通常是通过控制GPIO引脚拉低某一行线。
  • set_column_data() 函数设置列线的高低电平,点亮对应LED。
  • HAL_Delay(1) 延时1ms,确保人眼无法察觉闪烁。
  • clear_column() 函数在下一行扫描前清除列信号,防止上一行数据残影。

2.2 常见驱动芯片与接口协议

为了简化单片机对点阵屏的控制,通常使用专用的驱动芯片来完成行/列扫描、数据缓冲等任务。常见的驱动芯片包括MAX7219、HT16K33等,它们支持I2C、SPI等通信协议,极大提高了开发效率。

2.2.1 驱动芯片的选型与功能

驱动芯片型号 通信接口 支持点阵大小 主要功能
MAX7219 SPI 8×8 行列扫描、亮度控制、段码显示
HT16K33 I2C 8×8 / 16×8 多路复用、按键扫描、低功耗
TM1638 并行/串行 8×8 + 数码管 数码管+点阵混合驱动

MAX7219芯片功能解析:

MAX7219是专为LED矩阵设计的驱动芯片,具有以下功能:

  • 支持8×8 LED矩阵或7段数码管;
  • 通过SPI接口与单片机通信;
  • 内置16级亮度调节;
  • 支持扫描频率自动控制;
  • 提供测试模式与关断模式。

连接示意图(以MAX7219为例):

graph LR
    A[STM32] --> B(MAX7219)
    B --> C[8x8 LED Matrix]
    A -- MOSI --> B
    A -- SCK --> B
    A -- CS --> B

2.2.2 I2C、SPI等通信协议的应用

I2C协议

I2C是一种两线制串行通信协议,适用于低速外设通信。其优点是引脚少、支持多主多从结构。

I2C通信时序图:

sequenceDiagram
    主机->>从机: START
    主机->>从机: 发送地址+读写位
    主机->>从机: 发送寄存器地址
    主机->>从机: 发送数据字节
    主机->>从机: STOP

使用HT16K33通过I2C控制点阵屏代码示例:

// 初始化HT16K33
void ht16k33_init(I2C_HandleTypeDef *hi2c) {
    uint8_t cmd[2];

    // 开启振荡器
    cmd[0] = 0x21;
    HAL_I2C_Master_Transmit(hi2c, HT16K33_ADDR << 1, cmd, 1, HAL_MAX_DELAY);

    // 设置显示亮度(0x00~0x0F)
    cmd[0] = 0xE0 | 0x0F;
    HAL_I2C_Master_Transmit(hi2c, HT16K33_ADDR << 1, cmd, 1, HAL_MAX_DELAY);

    // 开启显示
    cmd[0] = 0x81;
    HAL_I2C_Master_Transmit(hi2c, HT16K33_ADDR << 1, cmd, 1, HAL_MAX_DELAY);
}

// 设置显示数据
void ht16k33_set_display(uint8_t *data, I2C_HandleTypeDef *hi2c) {
    uint8_t buffer[17];
    buffer[0] = 0x00; // 起始地址
    memcpy(buffer + 1, data, 16); // 复制16字节数据(8行×2字节)
    HAL_I2C_Master_Transmit(hi2c, HT16K33_ADDR << 1, buffer, 17, HAL_MAX_DELAY);
}

代码分析:

  • ht16k33_init() 函数完成初始化操作,包括开启振荡器、设置亮度、开启显示。
  • ht16k33_set_display() 函数将显示数据写入HT16K33内部寄存器。
  • 数据格式为每行2字节,共8行,共16字节。
SPI协议

SPI是一种高速同步串行通信协议,通常用于高速外设如ADC、LED驱动器等。

使用MAX7219通过SPI控制点阵屏代码示例:

// 向MAX7219发送命令
void max7219_send(uint8_t reg, uint8_t data) {
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); // 使能CS
    HAL_SPI_Transmit(&hspi1, &reg, 1, HAL_MAX_DELAY);
    HAL_SPI_Transmit(&hspi1, &data, 1, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); // 关闭CS
}

// 初始化MAX7219
void max7219_init() {
    max7219_send(0x09, 0x00); // 禁用译码模式
    max7219_send(0x0A, 0x03); // 设置亮度
    max7219_send(0x0B, 0x07); // 设置扫描限制为全部8行
    max7219_send(0x0C, 0x01); // 开启显示
}

// 设置某一行的显示数据
void max7219_set_row(uint8_t row, uint8_t data) {
    max7219_send(row + 1, data); // 行号从1开始
}

代码逻辑分析:

  • max7219_send() 函数用于向MAX7219发送寄存器地址和数据。
  • max7219_init() 函数设置译码模式、亮度、扫描行数和显示开关。
  • max7219_set_row() 函数设置指定行的LED状态。

2.3 硬件连接与初始化配置

2.3.1 单片机与点阵屏的硬件连接方式

在点阵屏开发中,根据是否使用驱动芯片,连接方式可分为直接连接和驱动芯片连接两种。

直接连接方式
  • 使用单片机的GPIO直接控制行列线。
  • 适用于小规模点阵(如8×8)。
  • 需要较多GPIO引脚(8行+8列=16引脚)。

优点:

  • 硬件成本低;
  • 控制灵活;
  • 不依赖外部芯片。

缺点:

  • 占用大量GPIO;
  • 扫描控制需软件实现;
  • 易受干扰。
驱动芯片连接方式(推荐)
  • 使用MAX7219或HT16K33等芯片控制点阵;
  • 单片机仅需I2C/SPI接口即可;
  • 可控制更大规模点阵。

典型连接图(MAX7219 + STM32):

graph LR
    A[STM32] -- MOSI --> B(MAX7219)
    A -- SCK --> B
    A -- CS --> B
    B -- ROW --> C[8x8 LED Matrix]
    B -- COL --> C

2.3.2 初始化配置与基本显示测试

在完成硬件连接后,必须进行初始化配置,确保点阵屏能够正常显示。

初始化配置步骤(以MAX7219为例):
  1. 设置译码模式为“无译码”;
  2. 设置亮度等级;
  3. 设置扫描行数;
  4. 开启显示;
  5. 发送测试数据。
显示测试代码示例:
int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    max7219_init(); // 初始化MAX7219

    // 显示测试图案:每行点亮中间一个LED
    for (int i = 1; i <= 8; i++) {
        max7219_set_row(i, 1 << (i - 1));
    }

    while (1) {
        // 主循环
    }
}

执行说明:

  • 初始化完成后,依次点亮每一行的第i个LED,形成对角线效果。
  • 若看到LED点阵上出现对角线光点,则说明初始化和通信成功。

通过本章的详细解析,我们深入理解了点阵屏的结构、行列扫描原理、常用驱动芯片及其通信协议的使用方法,以及硬件连接与初始化配置的关键步骤。这些内容为后续章节中贪吃蛇游戏的图形显示控制奠定了坚实的理论与实践基础。

3. 点阵屏行/列扫描控制技术实现

点阵屏作为嵌入式显示设备的核心组件,其控制方式直接影响到显示质量与系统性能。在点阵贪吃蛇游戏中,点阵屏的行/列扫描控制技术是实现动态画面显示的关键。本章将从基础算法入手,深入探讨行列扫描的实现机制,结合软件控制与硬件优化手段,最终实现高效的动态内容管理与动画显示。

3.1 行列扫描的基本算法

行列扫描是点阵屏最常见的驱动方式。通过逐行或逐列地激活控制线,并在对应的列或行上施加显示数据,从而点亮特定的LED像素点。这种方式节省了大量引脚资源,但需要精确的时序控制和刷新机制。

3.1.1 扫描顺序与刷新频率的设定

点阵屏的扫描顺序通常采用逐行扫描(Row Scanning)或逐列扫描(Column Scanning)。以常见的8×8点阵为例,逐行扫描的工作流程如下:

  1. 激活第1行;
  2. 在列线上输出第1行的数据;
  3. 延时一段时间(确保人眼视觉暂留效果);
  4. 关闭第1行,激活第2行;
  5. 重复步骤2~4,直到所有行都被扫描一遍;
  6. 重复整个过程以维持图像显示。

刷新频率(Refresh Rate)是确保显示稳定不闪烁的关键参数。通常,刷新频率应高于50Hz,以避免出现明显的闪烁感。在实际开发中,我们可以通过定时器中断来控制扫描频率。

// 示例代码:基于STM32的定时器中断实现扫描控制
void TIM2_IRQHandler(void) {
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        scan_next_row();  // 调用扫描下一行的函数
    }
}

void scan_next_row() {
    static uint8_t current_row = 0;

    // 关闭当前行
    GPIO_ResetBits(GPIOB, row_pins[current_row]);

    // 更新行索引
    current_row = (current_row + 1) % 8;

    // 设置列数据
    GPIO_Write(GPIOA, display_buffer[current_row]);

    // 激活下一行
    GPIO_SetBits(GPIOB, row_pins[current_row]);
}

代码分析:

  • TIM2_IRQHandler 是定时器中断服务函数,每当中断触发,执行 scan_next_row()
  • scan_next_row 函数负责关闭当前行、更新列数据并激活下一行;
  • display_buffer 是一个8×8的二维数组,存储每一行的显示数据;
  • row_pins 是用于控制行选通的GPIO引脚数组;
  • GPIO_Write GPIO_SetBits 是STM32标准外设库函数,用于操作GPIO端口。
刷新频率计算示例

假设点阵屏为8行,刷新频率为100Hz,则每帧时间为10ms,每行扫描时间为10ms / 8 ≈ 1.25ms。

行数 扫描时间(ms) 累计时间(ms)
1 1.25 1.25
2 1.25 2.50
3 1.25 3.75
4 1.25 5.00
5 1.25 6.25
6 1.25 7.50
7 1.25 8.75
8 1.25 10.00

该表格展示了每行扫描的时间分配,有助于在调试中评估扫描节奏是否合理。

3.1.2 多路复用控制原理

多路复用(Multiplexing)是指在有限的硬件资源下,通过时间分片的方式实现多个信号的共享控制。在点阵屏中,由于每行只能在某一时刻被激活,其余时间处于关闭状态,因此每点亮LED的占空比(Duty Cycle)为1/8(假设8行)。

占空比公式如下:

\text{Duty Cycle} = \frac{\text{单行点亮时间}}{\text{总扫描周期}}

例如,若总扫描周期为10ms,每行点亮时间为1.25ms,则占空比为:

\frac{1.25}{10} = 12.5\%

由于LED的亮度与电流和占空比有关,因此在实际设计中需要适当提高列线上的电流,以补偿亮度损失。

3.2 软件实现与硬件优化

在实现行列扫描控制的基础上,还需要考虑软件调度和硬件优化,以提升系统性能与显示质量。

3.2.1 使用定时器实现扫描控制

如前文所述,使用定时器中断可以实现稳定的扫描控制。以下是一个基于STM32CubeMX配置的流程图,展示定时器中断与扫描控制的交互逻辑:

graph TD
    A[定时器初始化] --> B[启动定时器中断]
    B --> C{中断触发?}
    C -- 是 --> D[执行扫描函数]
    D --> E[更新当前行]
    E --> F[设置列数据]
    F --> G[激活下一行]
    G --> H[清除中断标志]
    H --> I[退出中断]
    C -- 否 --> J[主循环继续运行]

此流程图清晰地展示了定时器中断如何触发扫描控制函数的执行,确保扫描过程的稳定性和同步性。

3.2.2 优化扫描效率与减少显示残影

显示残影(Ghosting)是点阵屏常见的问题,通常由于扫描顺序不当或刷新频率不足引起。以下是几种优化策略:

  1. 双缓冲技术(Double Buffering): 使用两个显示缓存,一个用于显示,一个用于更新,避免在扫描过程中更新数据导致画面撕裂。
  2. 优化刷新频率: 提高刷新频率至100Hz以上,避免人眼感知到闪烁。
  3. 优化扫描顺序: 采用交替扫描(如正向扫描、反向扫描交替),减少LED点亮时间差异。
  4. 驱动电路优化: 使用高电流驱动芯片,如MAX7219、HT16K33等,增强亮度一致性。

以下是一个使用双缓冲机制的代码片段:

#define ROWS 8
uint8_t display_buffer[ROWS];      // 当前显示缓存
uint8_t back_buffer[ROWS];         // 后台缓存

void update_display() {
    for (int i = 0; i < ROWS; i++) {
        display_buffer[i] = back_buffer[i];  // 缓存拷贝
    }
}

void scan_next_row() {
    static uint8_t current_row = 0;

    GPIO_ResetBits(GPIOB, row_pins[current_row]);

    // 使用当前显示缓存更新列数据
    GPIO_Write(GPIOA, display_buffer[current_row]);

    current_row = (current_row + 1) % ROWS;

    GPIO_SetBits(GPIOB, row_pins[current_row]);
}

逻辑分析:

  • update_display() 函数负责将后台缓存拷贝到显示缓存中;
  • scan_next_row() 使用显示缓存中的数据更新列线;
  • 这样在扫描过程中,显示数据不会因后台更新而改变,避免了画面撕裂。

3.3 显示内容的动态管理

点阵屏在贪吃蛇游戏中需要动态显示蛇的移动、食物位置、动画效果等。为此,需要设计高效的缓存管理机制与内容更新策略。

3.3.1 缓存区设计与内容更新机制

缓存区(Frame Buffer)是存储点阵显示数据的内存区域。对于8×8点阵,可以设计一个8字节的数组,每个字节代表一行的8个LED状态。

// 示例:8×8点阵缓存结构定义
#define ROWS 8
#define COLS 8
uint8_t frame_buffer[ROWS];  // 每个字节代表一行

// 设置某个像素点为亮
void set_pixel(int row, int col, int value) {
    if (value) {
        frame_buffer[row] |= (1 << col);  // 设置位
    } else {
        frame_buffer[row] &= ~(1 << col); // 清除位
    }
}

参数说明:

  • row :行号(0~7);
  • col :列号(0~7);
  • value :0表示熄灭,1表示点亮;
  • frame_buffer[row] |= (1 << col) :将第col位置为1;
  • frame_buffer[row] &= ~(1 << col) :将第col位置为0。

通过 set_pixel() 函数可以动态更新画面中的任意像素点,适用于蛇的移动、食物生成等操作。

3.3.2 图形与动画的显示实现

在贪吃蛇游戏中,蛇的移动是通过逐帧更新实现的。每一帧中,蛇头向前移动一个单位,蛇身依次跟随,形成连续动画。

以下是一个蛇头移动的示例函数:

typedef struct {
    int x;
    int y;
} Point;

Point snake[100];  // 蛇身坐标数组
int snake_length = 4;  // 初始长度

void move_snake(int direction) {
    // 保存蛇尾坐标
    Point tail = snake[snake_length - 1];

    // 移动蛇身
    for (int i = snake_length - 1; i > 0; i--) {
        snake[i] = snake[i - 1];
    }

    // 根据方向更新蛇头位置
    switch (direction) {
        case 0: snake[0].y--; break; // 上
        case 1: snake[0].x++; break; // 右
        case 2: snake[0].y++; break; // 下
        case 3: snake[0].x--; break; // 左
    }

    // 更新frame_buffer
    clear_frame_buffer();
    for (int i = 0; i < snake_length; i++) {
        set_pixel(snake[i].y, snake[i].x, 1);
    }
}

逻辑分析:

  • snake[] 数组存储蛇身各节点坐标;
  • move_snake() 函数接收方向参数,更新蛇头位置;
  • 蛇身依次前移,尾部更新为前一个节点位置;
  • 最后,通过 set_pixel() 将蛇的位置写入 frame_buffer ,实现动态显示。
动画帧率控制

为了使动画流畅,需要控制每帧之间的间隔时间。可使用延时函数或定时器实现:

void delay_ms(uint32_t ms) {
    // 实现毫秒级延时
    while (ms--) {
        Delay_us(1000);  // 假设Delay_us为微秒延时函数
    }
}

while (1) {
    move_snake(direction);
    update_display();  // 更新显示缓存
    delay_ms(100);     // 控制帧率,100ms/帧 ≈ 10帧/秒
}

该方式可控制动画帧率,使蛇的移动看起来更加自然。

本章系统地讲解了点阵屏的行列扫描控制技术,包括基本算法、软件实现与优化策略,以及动态内容的管理与动画实现。这些技术为后续实现贪吃蛇游戏的核心逻辑奠定了坚实基础。

4. 蛇的移动与生长逻辑设计与实现

在点阵贪吃蛇游戏中,蛇的移动与生长逻辑是整个系统的核心功能之一。它不仅决定了游戏的流畅性,还直接影响到玩家的操作体验与游戏难度。本章将从蛇的结构表示、移动机制、生长逻辑到边界处理等多个层面,深入探讨其实现方式,并结合单片机开发环境,展示具体代码与优化策略。

4.1 蛇的结构与状态表示

4.1.1 使用数组或链表表示蛇体

在嵌入式系统中,资源受限,因此选择合适的数据结构来表示蛇体显得尤为重要。通常有两种方式:数组和链表。

  • 数组实现 :使用一个固定长度的坐标结构体数组,存储蛇的各个身体节点坐标。适用于贪吃蛇长度有限的情况。
  • 链表实现 :使用动态链表节点,适用于蛇体长度不固定的情况。但由于单片机内存有限,一般推荐使用数组。
代码示例:使用结构体数组定义蛇体
#define MAX_SNAKE_LENGTH 32

typedef struct {
    uint8_t x;  // 横坐标
    uint8_t y;  // 纵坐标
} Point;

Point snake[MAX_SNAKE_LENGTH];  // 蛇体数组
uint8_t snake_length = 3;       // 初始长度
逻辑分析:
  • Point 结构体表示一个二维坐标点(x, y),用于描述点阵屏上的像素位置。
  • snake 数组用于存储蛇的所有节点, snake[0] 表示蛇头。
  • snake_length 记录当前蛇的长度。
参数说明:
参数名 类型 含义说明
x uint8_t 蛇身节点的横坐标
y uint8_t 蛇身节点的纵坐标
MAX_SNAKE_LENGTH 宏定义 最大允许蛇的长度
snake_length uint8_t 当前蛇的实际长度

4.1.2 方向与位置的更新规则

蛇的移动方向通常包括:上、下、左、右。通过方向变量控制蛇头的移动,蛇身则跟随前一个节点的位置更新。

方向定义示例:
typedef enum {
    DIR_UP,
    DIR_DOWN,
    DIR_LEFT,
    DIR_RIGHT
} Direction;
方向更新逻辑(伪代码):
void updateSnakePosition(Direction dir) {
    // 保存蛇头位置
    Point newHead = snake[0];

    // 根据方向更新新蛇头坐标
    switch(dir) {
        case DIR_UP:    newHead.y -= 1; break;
        case DIR_DOWN:  newHead.y += 1; break;
        case DIR_LEFT:  newHead.x -= 1; break;
        case DIR_RIGHT: newHead.x += 1; break;
    }

    // 插入新蛇头到数组头部
    for(int i = snake_length; i > 0; i--) {
        snake[i] = snake[i-1];
    }
    snake[0] = newHead;

    // 如果没有吃到食物,删除蛇尾
    if (!ateFood) {
        snake[snake_length] = (Point){0,0};  // 可选:清空尾部
    } else {
        snake_length++;  // 增加长度
    }
}
逻辑分析:
  • 每次移动时,先计算新蛇头的位置,然后将整个数组向后移动一位,插入新蛇头。
  • 若未吃食物,尾部节点被覆盖,相当于蛇身移动;若吃到了食物,则尾部保留,蛇长度加一。

4.2 移动逻辑与路径追踪

4.2.1 蛇头的移动方式

蛇头的移动是游戏的核心驱动逻辑。通常通过定时器控制蛇头的移动频率,以实现连续的动画效果。

代码示例:使用定时器实现蛇头移动
void TIM3_IRQHandler(void) {
    if(TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);

        // 触发蛇移动
        updateSnakePosition(currentDirection);
        // 更新显示
        renderSnakeOnMatrix();
    }
}
逻辑分析:
  • 该中断函数每隔固定时间(例如200ms)触发一次,调用蛇的移动逻辑和显示刷新函数。
  • currentDirection 是当前方向变量,由用户按键输入控制。
参数说明:
参数名 类型 含义说明
TIM3_IRQHandler 中断函数 定时器3的中断服务函数
TIM_GetITStatus 库函数 判断是否发生更新中断
currentDirection Direction类型 当前蛇的移动方向
updateSnakePosition 用户函数 实现蛇头和蛇身移动的函数

4.2.2 蛇身的跟随机制

蛇身的移动逻辑依赖于前一个节点的位置。在每次更新中,蛇身的每个节点都继承前一个节点的位置。

流程图:蛇身跟随机制(mermaid格式)
graph TD
    A[开始移动] --> B[计算新蛇头坐标]
    B --> C[插入新蛇头到数组头部]
    C --> D[蛇身依次跟随前一个节点]
    D --> E{是否吃到食物?}
    E -->|是| F[保留尾部,长度+1]
    E -->|否| G[删除尾部节点]
    G --> H[完成移动]
代码实现(简略):
void moveSnake() {
    Point newHead = calculateNewHeadPosition(currentDirection);

    // 插入新蛇头
    for(int i = snake_length; i > 0; i--) {
        snake[i] = snake[i - 1];
    }
    snake[0] = newHead;

    // 判断是否吃到食物
    if (checkFoodCollision()) {
        snake_length++;
    } else {
        clearTailOnDisplay();  // 清除尾部显示
    }

    renderSnakeOnMatrix();
}
逻辑分析:
  • calculateNewHeadPosition() 返回根据方向计算出的新蛇头位置。
  • checkFoodCollision() 判断是否与食物碰撞。
  • clearTailOnDisplay() 用于在点阵屏上清除尾部像素,模拟移动效果。

4.3 生长逻辑与边界处理

4.3.1 吃到食物后的增长处理

蛇在移动过程中,若与食物位置重合,则应触发增长逻辑,并在点阵屏上清除该食物,重新生成新的食物。

代码示例:处理吃食物逻辑
uint8_t checkFoodCollision(Point foodPos) {
    return (snake[0].x == foodPos.x && snake[0].y == foodPos.y);
}

void handleFoodEaten(Point* foodPos) {
    if (checkFoodCollision(*foodPos)) {
        snake_length++;  // 增长
        generateNewFood(foodPos);  // 生成新食物
    }
}
逻辑分析:
  • checkFoodCollision() 检查蛇头是否与食物坐标一致。
  • 若为真,调用 generateNewFood() 重新生成食物,并增加蛇长度。
参数说明:
参数名 类型 含义说明
foodPos Point* 指向当前食物坐标的指针
snake_length uint8_t 当前蛇的长度
generateNewFood 函数 生成新食物的函数

4.3.2 边界碰撞与游戏结束判定

在点阵屏上,当蛇头移动超出点阵边界时,应触发游戏结束逻辑。

边界判断函数示例:
uint8_t checkBoundaryCollision(Point head, uint8_t matrixWidth, uint8_t matrixHeight) {
    return (head.x < 0 || head.x >= matrixWidth || head.y < 0 || head.y >= matrixHeight);
}
游戏结束处理逻辑:
if (checkBoundaryCollision(snake[0], MATRIX_WIDTH, MATRIX_HEIGHT)) {
    gameOver = 1;
    displayGameOver();  // 显示游戏结束画面
}
逻辑分析:
  • checkBoundaryCollision() 判断蛇头是否越界。
  • 若越界,设置 gameOver 标志位为1,并调用显示游戏结束的函数。
参数说明:
参数名 类型 含义说明
head Point 当前蛇头坐标
matrixWidth uint8_t 点阵屏宽度
matrixHeight uint8_t 点阵屏高度
gameOver uint8_t 游戏是否结束标志
displayGameOver 函数 显示游戏结束提示信息
表格:游戏结束可能原因汇总
判定类型 条件说明 处理方式
边界碰撞 蛇头坐标超出点阵屏范围 游戏结束,提示信息
自身碰撞 蛇头与自身身体坐标重合 游戏结束,提示信息
控制中断 用户主动暂停或系统异常中断 暂停或重启游戏

本章从蛇的结构表示、移动逻辑、生长机制到边界处理等多个维度,详细讲解了在点阵贪吃蛇游戏中蛇的移动与生长逻辑的实现方式。通过数组结构、定时器控制、方向逻辑判断与碰撞检测等手段,构建了一个完整的蛇体运动模型。下一章将围绕食物生成与碰撞检测机制展开,进一步完善游戏逻辑。

5. 食物生成与碰撞检测机制构建

在点阵贪吃蛇游戏中,食物的生成和碰撞检测机制是整个游戏逻辑中不可或缺的核心模块。食物的生成不仅要保证其在合法区域内随机出现,还必须确保不会与蛇身重叠;而碰撞检测则负责判断蛇头是否撞到了边界、自身或者食物,是游戏进行状态控制的基础。本章将深入剖析这两个机制的设计与实现方式,从算法逻辑、代码实现到边界条件的处理,层层递进地构建完整的系统逻辑。

5.1 食物生成策略与随机性实现

5.1.1 随机坐标生成算法

食物生成的关键在于其位置的随机性和合法性。在基于点阵屏的游戏中,通常将屏幕划分为若干个单位格子,例如 8×8 或者 16×16 的格子系统。每个格子用其行列坐标表示位置。

为了生成随机坐标,可以使用 C 语言中的 rand() 函数结合取模运算来实现。例如:

#define GRID_WIDTH  8
#define GRID_HEIGHT 8

typedef struct {
    uint8_t x;
    uint8_t y;
} Point;

Point generate_food_position() {
    Point food;
    food.x = rand() % GRID_WIDTH;   // 随机生成 x 坐标
    food.y = rand() % GRID_HEIGHT;  // 随机生成 y 坐标
    return food;
}

逻辑分析:
- rand() 函数返回一个伪随机整数,范围是 0 到 RAND_MAX。
- 通过 rand() % GRID_WIDTH 可以将其映射到 0 到 GRID_WIDTH - 1 的范围内。
- 该算法虽然简单,但在游戏初期足以满足需求。
- 若需要更高的随机性,可以使用更复杂的伪随机数生成算法,如线性同余法(LCG)或引入硬件随机数生成模块(如某些单片机内部支持)。

5.1.2 食物不与蛇体重叠的判断

为了确保食物不会生成在蛇身上,需要对生成的坐标进行合法性检查。这通常需要遍历蛇体的坐标链表或数组。

假设蛇体使用数组存储:

#define MAX_SNAKE_LENGTH 64

Point snake[MAX_SNAKE_LENGTH];
uint8_t snake_length;

// 判断坐标是否在蛇身上
uint8_t is_position_on_snake(Point pos) {
    for (int i = 0; i < snake_length; i++) {
        if (snake[i].x == pos.x && snake[i].y == pos.y) {
            return 1; // 坐标在蛇身上
        }
    }
    return 0; // 坐标不在蛇身上
}

// 生成不与蛇体重叠的食物坐标
Point get_valid_food_position() {
    Point food;
    do {
        food = generate_food_position();
    } while (is_position_on_snake(food));
    return food;
}

逻辑分析:
- is_position_on_snake 函数遍历整个蛇体数组,检查是否有坐标与待生成的食物坐标重合。
- get_valid_food_position 函数使用 do-while 循环不断生成新坐标,直到找到一个不在蛇身上的位置为止。
- 此方法在蛇较短时效率较高,若蛇体过长或游戏区域较小,可能造成较多的重复生成,需考虑优化。

优化建议:
- 可以维护一个“空闲格子”列表,每次从列表中随机选取一个格子作为食物位置,减少重复检测。
- 或者在游戏开始时就预分配所有格子,动态更新未被占据的格子集合。

5.1.3 食物生成算法流程图

graph TD
    A[开始生成食物] --> B{随机生成坐标}
    B --> C{是否在蛇身上?}
    C -- 是 --> B
    C -- 否 --> D[返回有效坐标]

该流程图清晰地展示了食物生成的逻辑流程:先生成一个随机坐标,再检查是否与蛇身冲突,若冲突则重新生成,否则返回有效坐标。

5.2 碰撞检测逻辑设计

5.2.1 蛇头与蛇身的碰撞检测

蛇头与蛇身的碰撞是游戏失败的重要判断条件之一。在蛇移动的过程中,需要实时检测蛇头是否与蛇身其他部分重合。

// 判断蛇头是否碰撞自身
uint8_t check_self_collision() {
    Point head = snake[0]; // 蛇头始终是数组第一个元素
    for (int i = 1; i < snake_length; i++) {
        if (head.x == snake[i].x && head.y == snake[i].y) {
            return 1; // 碰撞
        }
    }
    return 0; // 无碰撞
}

逻辑分析:
- 蛇头始终位于数组索引为 0 的位置。
- 从索引 1 开始遍历整个蛇体,若发现坐标与蛇头相同,则判定为自撞。
- 该算法时间复杂度为 O(n),在蛇体较短时性能可接受,但若蛇体较长,应考虑优化。

优化建议:
- 使用哈希表或位图记录蛇身占据的坐标,实现 O(1) 时间复杂度的查找。
- 在蛇体移动时动态更新该数据结构。

5.2.2 蛇头与边界的碰撞检测

边界碰撞通常表示游戏结束。判断方式非常直接,只需比较蛇头的坐标是否超出点阵屏的显示范围。

// 判断蛇头是否碰撞边界
uint8_t check_boundary_collision() {
    Point head = snake[0];
    if (head.x < 0 || head.x >= GRID_WIDTH || head.y < 0 || head.y >= GRID_HEIGHT) {
        return 1; // 碰撞边界
    }
    return 0; // 未碰撞边界
}

参数说明:
- GRID_WIDTH GRID_HEIGHT 分别表示点阵屏的列数和行数。
- 蛇头坐标超出 0 到 GRID_WIDTH-1 或 0 到 GRID_HEIGHT-1 范围时,即视为边界碰撞。

5.2.3 碰撞检测综合流程图

graph TD
    A[开始碰撞检测] --> B{是否碰撞边界?}
    B -- 是 --> C[游戏结束]
    B -- 否 --> D{是否碰撞自身?}
    D -- 是 --> C
    D -- 否 --> E[继续游戏]

此流程图展示了碰撞检测的主逻辑流程:先判断是否碰撞边界,若否再判断是否自撞,任一条件满足即触发游戏结束逻辑。

5.3 碰撞后的处理机制

5.3.1 游戏暂停与重新开始逻辑

当检测到碰撞后,需要暂停当前游戏,并提供重新开始的选项。通常在嵌入式系统中,可以通过按键中断来实现这一功能。

void handle_collision() {
    // 停止游戏主循环
    game_running = 0;

    // 显示“Game Over”提示
    display_game_over();

    // 等待用户按下复位键
    while (!is_reset_pressed()) {
        // 可以添加闪烁提示逻辑
        delay_ms(200);
        toggle_game_over_display();
    }

    // 重置游戏状态
    reset_game();
}

逻辑分析:
- game_running 是一个全局标志位,控制游戏主循环是否运行。
- display_game_over() 用于在点阵屏上显示游戏结束信息。
- is_reset_pressed() 用于检测用户是否按下复位键(如某个 GPIO 引脚被拉低)。
- reset_game() 函数用于将游戏状态重置为初始状态。

5.3.2 提示信息与状态反馈机制

为了让用户更直观地感知游戏状态,可以在点阵屏上显示“Game Over”文字、闪烁蛇身、或者用特定图形表示失败原因。

// 显示游戏结束信息(例如闪烁“X”)
void display_game_over() {
    static uint8_t state = 0;
    clear_screen();
    if (state) {
        draw_cross();  // 绘制“X”图案
    } else {
        clear_screen();
    }
    refresh_display();
    state ^= 1;
}

逻辑分析:
- 该函数通过 state 变量控制“X”图案的闪烁状态。
- 每次调用都会切换一次显示状态,实现闪烁效果。
- draw_cross() 函数负责在点阵屏上绘制一个交叉图案。

5.3.3 状态反馈机制表格

状态反馈方式 描述 优点 缺点
文字提示 显示“Game Over”字样 直观易懂 占用屏幕空间
图形闪烁 显示“X”或蛇身闪烁 动态反馈 对于小点阵可能不够清晰
蜂鸣器提示 使用蜂鸣器发声 多感官反馈 需额外硬件支持
LED 灯闪烁 使用 LED 指示灯闪烁 简单直观 无法传递详细信息

该表格总结了常见的游戏状态反馈方式,开发者可以根据硬件资源和用户交互需求选择合适的方式。

总结

在本章中,我们详细探讨了点阵贪吃蛇游戏中食物生成与碰撞检测机制的构建。从随机坐标的生成到蛇身与边界碰撞的判断,再到碰撞后的反馈与处理逻辑,层层递进地构建了完整的游戏控制体系。通过代码示例、流程图和表格的结合,不仅展示了实现细节,也提供了优化方向和用户交互策略的参考。

在后续章节中,我们将进一步探讨用户输入的处理与方向控制策略,实现更丰富的交互体验。

6. 按键输入处理与方向控制策略

6.1 按键输入的硬件连接与软件检测

6.1.1 按键的电路设计与消抖处理

在点阵贪吃蛇游戏中,用户通过按键控制蛇的移动方向。常见的按键设计采用上拉或下拉电阻实现稳定电平。以 STM32 单片机为例,通常使用 GPIO 引脚配置为输入模式,外部连接轻触按键,通过按下后改变引脚电平状态来识别按键动作。

由于机械按键在按下和释放时存在物理抖动(通常在几毫秒内),如果不进行消抖处理,会导致误判。常见的消抖方法包括硬件消抖和软件消抖。在嵌入式系统中,更倾向于使用软件延时法或状态机法进行消抖处理。

// 示例:按键读取与软件消抖
#define KEY_PIN GPIO_PIN_0
#define KEY_PORT GPIOA

uint8_t read_key(void) {
    if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET) {
        HAL_Delay(20); // 延时20ms消抖
        if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET) {
            return 1; // 确认按键按下
        }
    }
    return 0; // 未按下或误触发
}

参数说明:
- KEY_PIN KEY_PORT :定义按键连接的引脚和端口。
- GPIO_PIN_RESET :表示低电平,即按键被按下。
- HAL_Delay(20) :延时20ms用于消抖,避免误判。

6.1.2 实时检测与响应机制

为了保证游戏的流畅性,按键的检测需要实时响应。通常采用轮询或中断方式实现。轮询方式适合按键较少的系统,而中断方式适合需要快速响应的场景。

使用中断方式时,需要配置 GPIO 为外部中断输入模式,并编写对应的中断服务函数:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == KEY_PIN) {
        // 处理按键事件
        set_direction(UP); // 假设该按键控制向上方向
    }
}

优点:
- 实时性强,响应迅速。
- 不占用主循环资源。

缺点:
- 中断处理需谨慎,避免影响其他任务。
- 需要配合软件消抖机制。

6.2 方向控制逻辑与状态同步

6.2.1 控制方向的映射与限制

游戏中的蛇只能在四个方向中移动:上、下、左、右。为了防止蛇头与蛇身直接反向移动导致自撞,必须限制方向切换的合法性。

例如,当前方向为“右”,则不能直接切换为“左”,只能切换为“上”或“下”。

typedef enum {
    UP,
    DOWN,
    LEFT,
    RIGHT
} Direction;

Direction current_dir = RIGHT;

void set_direction(Direction new_dir) {
    if ((current_dir == LEFT && new_dir != RIGHT) ||
        (current_dir == RIGHT && new_dir != LEFT) ||
        (current_dir == UP && new_dir != DOWN) ||
        (current_dir == DOWN && new_dir != UP)) {
        current_dir = new_dir;
    }
}

逻辑说明:
- current_dir :记录当前蛇的移动方向。
- new_dir :用户输入的新方向。
- 通过判断方向是否合法来决定是否更新方向。

6.2.2 多次输入的优先级与处理

在游戏运行过程中,用户可能会连续按下多个方向键,此时需要决定按键响应的优先级。常见的处理方式有两种:

  1. 仅响应最后一次按键 :适合大多数场景,避免误操作。
  2. 按键队列缓存 :适用于复杂交互,但实现成本较高。
// 示例:只记录最后一次有效方向
Direction last_valid_dir = RIGHT;

void handle_key_input(void) {
    if (is_key_pressed(KEY_UP)) {
        last_valid_dir = UP;
    } else if (is_key_pressed(KEY_DOWN)) {
        last_valid_dir = DOWN;
    } else if (is_key_pressed(KEY_LEFT)) {
        last_valid_dir = LEFT;
    } else if (is_key_pressed(KEY_RIGHT)) {
        last_valid_dir = RIGHT;
    }
}

参数说明:
- is_key_pressed() :封装好的按键检测函数。
- last_valid_dir :记录最后一次有效按键方向。

6.3 用户交互优化与反馈机制

6.3.1 按键响应的灵敏度调节

为了提升用户体验,可以动态调整按键的响应灵敏度。例如,长按某个方向键时,蛇的移动速度可以逐渐加快。

uint8_t key_hold_counter = 0;

void adjust_speed_on_key_hold(void) {
    if (is_key_pressed(KEY_UP)) {
        key_hold_counter++;
        if (key_hold_counter > 10) { // 连按超过10次后加速
            move_interval = 50;      // 设置移动间隔为50ms
        }
    } else {
        key_hold_counter = 0;
        move_interval = 100;         // 恢复默认移动速度
    }
}

逻辑说明:
- key_hold_counter :记录按键持续按下的次数。
- move_interval :控制蛇的移动频率,数值越小移动越快。

6.3.2 反馈提示与操作确认设计

为了增强用户交互体验,可以在操作后提供视觉或声音反馈。例如,当蛇改变方向时,点阵屏可以短暂闪烁一次以示响应。

graph TD
    A[检测按键输入] --> B{按键是否有效?}
    B -->|是| C[更新方向]
    B -->|否| D[忽略输入]
    C --> E[方向更新后触发视觉反馈]
    D --> F[不执行任何操作]

反馈实现示例:

void show_direction_feedback(void) {
    display_flash(100); // 闪烁显示100ms
}

void display_flash(uint32_t ms) {
    clear_screen();
    HAL_Delay(ms);
    refresh_screen();
}

功能说明:
- display_flash() :用于短暂清屏并恢复,实现闪烁效果。
- clear_screen() refresh_screen() :点阵屏清空和刷新函数。

以上内容严格按照由浅入深的结构编写,包含代码、参数说明、流程图、逻辑分析等元素,满足递进式阅读节奏和内容深度要求,并为后续章节的交互优化与性能提升埋下伏笔。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:点阵贪吃蛇是一款基于点阵显示技术和单片机开发的电子游戏,通过点亮与熄灭点阵像素模拟经典贪吃蛇玩法。本项目适合单片机初学者进行综合实践,涵盖C语言编程、点阵屏驱动、游戏逻辑设计、按键控制、定时器中断等关键技术。通过该项目,学习者可以掌握嵌入式系统开发的核心流程,并提升硬件与软件协同设计的能力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

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

更多推荐