基于STM32的极简双像素Flappy Bird游戏开发实战

📅 2026/8/19 11:19:00
基于STM32的极简双像素Flappy Bird游戏开发实战
1. 项目概述当经典像素鸟遇上双像素几年前一款名为《Flappy Bird》的像素风小游戏席卷全球其简单的点击操作和虐心的难度让人又爱又恨。作为一名嵌入式开发爱好者我总在想能不能用最少的硬件资源把这种极致的“简单”与“挑战”复刻出来于是“2-pixel Flappy Bird”这个想法诞生了。它的核心目标就是用仅仅两个发光像素点来模拟那只上下翻飞的小鸟和不断出现的管道障碍在微控制器上实现一个完整的、可交互的游戏逻辑。你可能会问两个像素点怎么玩游戏这正是项目的趣味所在。我们不再需要复杂的图形库或高分屏而是回归到最本质的交互一个像素代表小鸟另一个像素代表即将到来的管道缺口。玩家通过一个按键控制“小鸟”像素在上下两个位置间跳跃以穿过由“管道缺口”像素构成的移动障碍。整个游戏世界将运行在一块像STM32F103C8T6即常说的“蓝色药丸”开发板这样的微控制器上并通过一个最简单的双色LED灯珠或者更酷炫的、可编程的NeoPixel LED来呈现。这个项目麻雀虽小五脏俱全。它涉及了嵌入式开发中几个非常核心且有趣的技术点GPIO通用输入输出的精准控制与读取用来检测按键和驱动LED定时器中断用于生成稳定的游戏时钟帧控制管道移动和游戏逻辑更新以及状态机编程思想来管理游戏的各种状态开始、进行中、碰撞检测、结束。对于初学者这是一个绝佳的、综合性强的入门实战项目对于老手则是一次对代码精简和硬件抽象能力的挑战。我们甚至可以利用像DUE Link这样的调试工具深入观察GPIO电平变化和程序运行状态解决那些“灯亮了但代码读回来是0”的诡异问题。2. 核心硬件与平台选型解析2.1 微控制器为何是STM32F103C8T6选择STM32F103C8T6作为本项目的主控几乎是性价比和社区生态的最优解。这款芯片基于ARM Cortex-M3内核主频72MHz拥有64KB Flash和20KB RAM性能对于驱动两个LED和运行一个简单游戏逻辑绰绰有余。更重要的是它丰富的GPIO资源和强大的定时器正是我们项目所需的。它的GPIO口可以灵活配置成推挽输出直接驱动LED、上拉输入读取按键等多种模式。我们项目需要至少3个GPIO一个用于控制“小鸟”LED一个用于控制“管道”LED一个用于连接按键。STM32F103C8T6轻松满足。此外其内置的SysTick定时器或通用定时器如TIM2可以非常方便地配置成毫秒级中断作为我们游戏的“心跳”确保游戏帧率稳定不受主循环中其他代码的影响。市面上大量的开发板如Blue Pill和成熟的HAL库、标准库支持也极大地降低了开发门槛。注意虽然STM32F103系列很流行但需注意市面上有部分兼容芯片或翻新芯片其稳定性可能略有差异。如果遇到非常奇怪的GPIO行为在排查软件之余硬件本身也值得怀疑。2.2 显示单元从普通LED到NeoPixel显示方案是整个项目的视觉核心有两种主流选择方案一双色LED或两个独立LED这是最简单直接的方式。使用一个双色共阳或共阴LED一个封装内红绿两个芯片或者干脆用两个独立的单色LED。STM32的GPIO直接通过一个限流电阻通常220Ω-1kΩ驱动即可。优点是电路简单编程直观直接操作GPIO输出高低电平。缺点是可显示状态有限亮/灭且颜色固定。方案二WS2812B NeoPixel LED这是一个更具趣味性的选择。WS2812B是一种智能控制LED只需一根数据线GPIO即可控制串联的多个LED每个LED的颜色RGB和亮度均可独立编程。对于我们“2-pixel”的需求只需要两颗WS2812B即可。优点是显示效果炫酷“小鸟”和“管道”可以用不同颜色区分甚至可以实现小鸟闪烁、管道渐变等效果大大增强了游戏表现力。缺点是需要编写特定的时序函数来驱动对时序精度要求较高。考虑到项目的名称“2-pixel”和扩展趣味性本项目的后续实现将基于一颗集成了两个WS2812B的LED模块进行讲解。这既能体现“像素”感又保留了足够的可玩性。2.3 输入与调试按键与DUE Link输入设备就是一个简单的常开按键一端接地另一端连接到STM32的某个GPIO引脚并在该GPIO上启用内部上拉电阻。这样按键未按下时GPIO读到的是高电平由于上拉按键按下时GPIO被拉到地读到低电平。通过检测这个下降沿来触发小鸟的“跳跃”动作。关于GPIO读取的“坑”你可能会在网络热词中看到“gpio接口回读是0但是实际是高电平”的问题。这通常有几种原因1引脚配置错误比如配置成了输出模式却尝试读取2硬件连接问题如虚焊、短路3软件上读取时机不对在电平尚未稳定时读取4最隐蔽的配置了复用功能但未彻底断开。在我们的按键读取中确保配置为“上拉输入”模式并在代码中加入简单的软件防抖例如检测到低电平后延时10ms再读一次就能避免大部分问题。调试工具上除了经典的串口打印像DUE Link这类基于CMSIS-DAP的调试器非常好用。它不仅能下载程序还能进行单步调试、实时查看变量值以及监控GPIO引脚的电平状态。当游戏行为异常时你可以用它来确认按键按下时GPIO电平是否真的变低了或者LED控制引脚输出的波形是否正确是排查硬件连接和底层驱动问题的利器。3. 系统设计与软件架构拆解3.1 状态机游戏逻辑的骨架游戏程序本质上是状态驱动的。我们用状态机State Machine来清晰定义和管理游戏流程。一个简单的状态机可以包含以下几个状态MENU菜单状态游戏启动后的初始状态等待玩家按键开始。PLAYING游戏进行状态核心状态。在此状态下系统需要更新游戏时钟控制管道移动速度。检测按键输入更新小鸟位置跳跃或下落。根据物理规则简单的重力加速度模拟计算小鸟的下落速度与位置。检测碰撞判断小鸟像素是否与管道像素“重合”即位置判断。更新LED显示渲染当前帧。GAME_OVER游戏结束状态碰撞发生后进入此状态。通常会让小鸟LED闪烁并等待玩家按键以重启游戏返回MENU状态。使用枚举类型enum定义这些状态并用一个全局变量gameState来记录当前状态。主循环或定时器中断服务函数根据gameState的值来执行相应的逻辑。这种设计使得程序结构清晰易于理解和维护。3.2 定时器中断游戏的“心跳”游戏动画的流畅性依赖于稳定的帧率。我们绝不能在主循环里用delay()函数来延时这会阻塞所有其他操作导致按键响应迟钝。正确的做法是使用定时器中断。我们可以配置一个定时器如SysTick或TIM2使其每20毫秒产生一次中断。这个中断服务函数ISR就是我们的游戏引擎核心在PLAYING状态下每次中断触发就让“管道”的坐标向左移动一个单位例如从LED索引位置1移动到0。当管道移出屏幕最左端后就将其重置到屏幕最右端并随机生成一个新的“缺口”高度即管道像素的垂直位置。同时更新小鸟的下落速度velocity gravity和位置bird_pos velocity。这里的“位置”是一个浮点数或定点数用于计算最终映射到LED的物理索引上。设置一个标志位如updateDisplayFlag 1通知主循环该刷新显示了。这样游戏逻辑的更新物理、碰撞与显示刷新被定时器严格同步而与主循环中其他代码如按键扫描解耦确保了游戏的响应性和一致性。3.3 驱动层抽象点亮WS2812B驱动WS2812B是项目的硬件底层关键。WS2812B采用单线归零码协议对0码、1码和RESET码的时序要求非常严格手册要求误差±150ns。在72MHz的STM32上我们可以用以下几种方法方法一精准延时循环这是最直接但效率较低的方法。用汇编指令或精心调整的nop循环来产生0.4us400ns和0.8us左右的高低电平。这种方法代码简单但占用CPU且易受中断影响。// 示例发送一个比特位简化版需根据实际时钟调整 void sendBit(bool bitVal) { if(bitVal) { // 发送‘1’码高电平约0.8us低电平约0.4us GPIO_SetBits(LED_PORT, LED_PIN); delay_ns(800); // 需要实现一个纳秒级延时 GPIO_ResetBits(LED_PORT, LED_PIN); delay_ns(400); } else { // 发送‘0’码高电平约0.4us低电平约0.8us GPIO_SetBits(LED_PORT, LED_PIN); delay_ns(400); GPIO_ResetBits(LED_PORT, LED_PIN); delay_ns(800); } }方法二SPI模拟推荐这是一种更稳定、更常用的技巧。将SPI的时钟SCK和数据线MOSI短接通过SPI发送特定的字节数据利用SPI时钟的边沿来“塑造”出WS2812B所需的数据波形。例如设置SPI时钟为3.2MHz每位周期约312.5ns那么发送0xF0二进制11110000可以产生一个较宽的高电平脉冲对应WS2812B的‘1’码发送0xC0二进制11000000则产生一个较窄的高电平脉冲对应‘0’码。这种方法利用了硬件外设时序精准且不占用CPU。方法三PWMDMA高级这是最专业、CPU开销最低的方法。将WS2812B的数据引脚连接到定时器的PWM输出通道通过调整PWM的占空比来产生不同宽度的脉冲0码和1码。然后使用DMA将代表一整帧LED数据的PWM占空比数组自动搬运到定时器比较寄存器实现“无CPU干预”的数据发送。这种方法复杂但效率极高适合驱动大量LED。对于我们的2像素项目SPI模拟法在复杂度和稳定性上取得了最佳平衡是推荐实现方式。4. 核心代码实现与分步详解4.1 硬件初始化与引脚配置首先我们需要初始化所有用到的外设。这里以STM32CubeMX生成HAL库代码为例进行说明。系统时钟初始化确保系统运行在72MHz。GPIO初始化按键引脚配置为输入模式并启用内部上拉电阻。例如KEY_PIN在GPIOA的PIN_0。LED数据引脚配置为推挽输出模式。如果使用SPI模拟法这个引脚后续会被SPI的MOSI功能复用。例如LED_DATA_PIN在GPIOA的PIN_7(SPI1_MOSI)。定时器初始化配置一个基本定时器如TIM2用于游戏逻辑更新。设置预分频器和自动重载值使其产生20ms50Hz的更新中断。在CubeMX中使能定时器更新中断。SPI初始化如果使用SPI模拟法配置SPI为主机发送模式仅发送。时钟极性CPOL和时钟相位CPHA通常设为0模式0。最关键的是波特率预分频器要计算出一个接近3.2MHz的时钟。例如当APB2时钟为72MHz时设置预分频为SPI_BAUDRATEPRESCALER_16可以得到4.5MHz的SPI时钟设置为_32则得到2.25MHz。需要通过示波器或逻辑分析仪微调找到能稳定驱动WS2812B的速率。数据大小设为8位。中断优先级配置将定时器中断的优先级设置为一个合适的值如默认优先级并确保全局中断已开启。4.2 游戏逻辑实现游戏逻辑主要集中在定时器中断服务函数和主循环的状态处理中。全局变量定义typedef enum {MENU, PLAYING, GAME_OVER} GameState; GameState gameState MENU; float bird_position 1.0; // 小鸟位置0.0代表顶部LED1.0代表底部LED float bird_velocity 0.0; // 小鸟下落速度 const float gravity 0.05; // 重力加速度 const float jump_force -0.8; // 跳跃力度向上为负 int pipe_position 1; // 管道水平位置0/1代表两个LED的索引 int pipe_gap_height 0; // 管道缺口位置0或1随机 int score 0; uint8_t updateDisplayFlag 0;定时器中断服务函数TIM2_IRQHandlervoid TIM2_IRQHandler(void) { if(__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE); if(gameState PLAYING) { // 1. 移动管道 pipe_position--; if(pipe_position 0) { pipe_position 1; // 移回最右端 pipe_gap_height HAL_GetTick() % 2; // 简单随机生成0或1 score; } // 2. 更新小鸟物理状态 bird_velocity gravity; bird_position bird_velocity; // 3. 边界与碰撞检测 if(bird_position 0.0) bird_position 0.0; if(bird_position 1.0) { bird_position 1.0; // 碰到地面游戏结束 gameState GAME_OVER; } // 碰撞检测小鸟像素和管道像素是否在同一位置 // 我们将浮点数位置四舍五入到最近的整数索引0或1 int bird_led_index (int)(bird_position 0.5); if(pipe_position 0 bird_led_index ! pipe_gap_height) { // 管道在当前列且小鸟不在缺口位置发生碰撞 gameState GAME_OVER; } updateDisplayFlag 1; // 标记需要更新显示 } } }主循环中的按键扫描与状态处理while (1) { // 按键扫描带防抖 static uint32_t lastKeyPressTime 0; if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) GPIO_PIN_RESET) { // 按键按下为低电平 HAL_Delay(10); // 简单防抖延时 if(HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) GPIO_PIN_RESET) { uint32_t now HAL_GetTick(); if(now - lastKeyPressTime 200) { // 防止连按 lastKeyPressTime now; switch(gameState) { case MENU: gameState PLAYING; bird_position 0.5; bird_velocity 0.0; pipe_position 1; score 0; break; case PLAYING: // 按下跳跃 bird_velocity jump_force; break; case GAME_OVER: gameState MENU; break; } } } } // 显示更新 if(updateDisplayFlag) { updateDisplayFlag 0; renderFrame(); // 调用渲染函数更新WS2812B数据 } }4.3 WS2812B渲染函数实现这是连接游戏逻辑和硬件的桥梁。我们需要根据游戏状态和变量计算出两个LED应该显示的颜色并通过SPI发送出去。假设我们使用SPI模拟法并定义LED_INDEX_BIRD 0第一个LED代表小鸟。LED_INDEX_PIPE 1第二个LED代表管道缺口。颜色小鸟为绿色0, 255, 0管道为红色255, 0, 0背景/非缺口处为关闭0,0,0。void renderFrame() { uint8_t led_data[2][3]; // 存储两个LED的GRB值WS2812B是GRB顺序 // 初始化所有LED为关闭 for(int i0; i2; i) { led_data[i][0] 0; // G led_data[i][1] 0; // R led_data[i][2] 0; // B } switch(gameState) { case MENU: // 菜单状态两个LED慢速呼吸或交替闪烁 { uint8_t brightness (HAL_GetTick() / 50) % 256; if(brightness 127) brightness 255 - brightness; brightness brightness * 2; led_data[LED_INDEX_BIRD][1] brightness; // 红色呼吸 led_data[LED_INDEX_PIPE][0] brightness; // 绿色呼吸 } break; case PLAYING: // 游戏状态 // 1. 绘制小鸟 int bird_led_idx (int)(bird_position 0.5); // 四舍五入到0或1 led_data[bird_led_idx][0] 255; // 小鸟是绿色 // 2. 绘制管道缺口 // 管道在水平位置 pipe_position (0或1) // 缺口在垂直位置 pipe_gap_height (0或1) // 我们需要在管道所在的列点亮缺口位置的LED // 注意我们的“屏幕”只有垂直方向的两个像素。管道移动是水平方向在两个LED索引间切换。 // 因此当 pipe_position 为0时表示管道在当前列左侧LED。 // 此时如果小鸟不在缺口位置就会碰撞。显示上我们让缺口位置的LED显示红色。 // 非缺口位置的LED在管道列应保持熄灭已在初始化中设为0。 led_data[pipe_position][1] (pipe_gap_height 0) ? 255 : 0; // 顶部缺口红灭 // 实际上为了更直观我们可以让管道列的非缺口LED显示一个暗色比如暗红表示管道本体 // 但为了极简的“2-pixel”精神这里只点亮缺口。 // 更清晰的逻辑管道列的两个LED一个亮红缺口一个熄灭管道本体。 // 但我们的“列”只有一个物理LED。所以这里的显示是一种抽象当管道移动到某列时该列的LED显示红色代表“可通过的缺口”。 // 碰撞检测已经通过逻辑完成显示只是为了提示玩家。 // 让我们修改一下管道列LED亮红色表示这是“安全缺口”。 led_data[pipe_position][1] 255; // 管道列显示红色 // 而小鸟如果是绿色当它飞到管道列红色LED时如果位置正确即 bird_led_idx pipe_position // 红绿叠加会显示黄色表示成功通过。这是一种有趣的视觉反馈。 break; case GAME_OVER: // 游戏结束小鸟LED快速闪烁红色 { uint8_t blink (HAL_GetTick() / 100) % 2; if(blink) { led_data[LED_INDEX_BIRD][1] 255; // 红色 } } led_data[LED_INDEX_PIPE][1] 255; // 管道保持红色 break; } // 将 led_data 数组通过SPI发送出去 sendWS2812Data(led_data, 2); }SPI发送函数sendWS2812Datavoid sendWS2812Data(uint8_t (*led_array)[3], uint8_t num_leds) { // 将每个LED的GRB字节转换为SPI需要发送的比特流 // 假设我们通过实验确定发送 0xF8 代表 WS2812B 的 10xC0 代表 0 const uint8_t code_1 0xF8; // 对应较宽高电平 const uint8_t code_0 0xC0; // 对应较窄高电平 uint8_t spi_buffer[24 * num_leds 1]; // 每个LED 24bit (8bit*3)1是为了预留 uint16_t buf_index 0; for(int i0; inum_leds; i) { // WS2812B数据顺序是GRB uint8_t *color led_array[i]; // color[0]G, color[1]R, color[2]B for(int c0; c3; c) { uint8_t byte color[c]; for(int bit7; bit0; bit--) { // 高位先发 if(byte (1bit)) { spi_buffer[buf_index] code_1; } else { spi_buffer[buf_index] code_0; } } } } // 发送RESET码低电平持续50us以上 // 在SPI发送完所有数据后将数据线拉低一段时间。 // 我们可以发送足够多的0x00对应全低电平来实现。 for(int i0; i50; i) { // 发送50个0x00具体数量需根据SPI时钟计算 spi_buffer[buf_index] 0x00; } // 使用HAL_SPI_Transmit发送整个缓冲区 HAL_SPI_Transmit(hspi1, spi_buffer, buf_index, HAL_MAX_DELAY); // 重要发送完成后需要将MOSI引脚设置为高电平或根据WS2812B要求 // 否则最后一个低电平可能会被误认为是RESET码的一部分。 HAL_GPIO_WritePin(LED_DATA_GPIO_Port, LED_DATA_Pin, GPIO_PIN_SET); }5. 调试、优化与深度问题排查5.1 GPIO电平读取异常排查实录在整合调试时按键失灵是最常见的问题。假设代码逻辑正确但按键按下后HAL_GPIO_ReadPin始终返回1高电平。请按以下步骤排查检查硬件连接这是第一步也是最容易出错的一步。用万用表蜂鸣档检查按键引脚到STM32引脚是否导通按键另一端是否确实接地。检查开发板上的引脚编号是否与代码中定义的端口和引脚号一致。我遇到过好几次原理图引脚号和实际PCB丝印对不上的情况。确认GPIO模式在初始化代码或CubeMX配置中确认按键引脚被正确配置为“GPIO Input”模式并且上拉电阻Pull-up已启用。如果配置成了浮空输入No pull-up and no pull-down引脚电平不确定极易受干扰。软件防抖机械按键存在抖动按下和松开瞬间会产生一系列毛刺脉冲。我们的代码中虽然加入了10ms延时防抖但更稳健的做法是使用状态机进行消抖// 更健壮的按键扫描状态机 typedef enum {RELEASED, PRESS_DETECTED, PRESSED, RELEASE_DETECTED} KeyState; KeyState keyState RELEASED; uint32_t lastDebounceTime 0; #define DEBOUNCE_DELAY 20 // 消抖时间单位ms void scanKey() { uint8_t currentPinState HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin); uint32_t now HAL_GetTick(); switch(keyState) { case RELEASED: if(currentPinState 0) { // 检测到低电平按下 lastDebounceTime now; keyState PRESS_DETECTED; } break; case PRESS_DETECTED: if(now - lastDebounceTime DEBOUNCE_DELAY) { if(currentPinState 0) { // 确认仍是低电平 keyState PRESSED; // 触发按键按下事件 onKeyPressed(); } else { keyState RELEASED; // 是抖动回到释放状态 } } break; case PRESSED: if(currentPinState 1) { // 检测到高电平释放 lastDebounceTime now; keyState RELEASE_DETECTED; } break; case RELEASE_DETECTED: if(now - lastDebounceTime DEBOUNCE_DELAY) { if(currentPinState 1) { // 确认已释放 keyState RELEASED; // 触发按键释放事件如果需要 } else { keyState PRESSED; } } break; } }使用调试器验证连接DUE Link或ST-Link在调试模式下设置断点或者实时查看GPIO寄存器的值。在STM32CubeIDE中你可以使用“Live Expression”或“SFRs”视图来监控特定GPIO引脚的电平。这是判断问题出在硬件还是软件的终极手段。检查复用功能如果这个引脚还被其他外设如串口、SPI复用即使你没有初始化那个外设某些复用功能也可能被默认激活影响GPIO的输入输出行为。确保在CubeMX中该引脚除了GPIO_Input外没有其他复用的黄色警告标志。5.2 WS2812B驱动不稳定问题解决如果LED显示乱码、颜色不对或完全不亮问题通常出在时序上。测量SPI时钟用示波器测量SPI的SCK引脚确认实际频率。WS2812B对0码和1码的高电平时间非常敏感。调整SPI的预分频值直到用逻辑分析仪解码出的波形符合WS2812B的时序要求T0H约400ns T1H约800ns误差±150ns。检查数据顺序WS2812B的数据格式是GRB而不是常见的RGB。发送颜色数据时顺序错了显示的颜色就会完全不对。RESET码长度两个数据帧之间需要至少50us的低电平复位信号。在我们的sendWS2812Data函数中通过发送多个0x00SPI数据为0时MOSI为低电平来实现。你需要根据你的SPI时钟频率计算需要发送多少个0x00。例如SPI时钟为2.25MHz则每个bit周期约444ns。要产生50us低电平需要发送50000ns / 444ns ≈ 113个0x00。宁多勿少但过多会影响刷新率。中断干扰如果发送WS2812B数据的过程中被高优先级中断打断会导致时序出现毛刺可能使WS2812B解析错误。解决方法有两种一是在发送WS2812B数据前关闭全局中断__disable_irq()发送完再开启__enable_irq()二是将WS2812B的发送放在主循环中并确保发送期间不会被游戏逻辑更新的定时器中断长时间打断。对于2个LED数据量极小关闭中断几微秒是可以接受的。电源与接地WS2812B工作电压通常是5V而STM32是3.3V。虽然3.3V信号有时也能驱动但为了稳定建议在数据线上加一个电平转换电路如74HCT245或者使用3.3V供电的WS2812B变种如WS2812B-3V3。同时确保电源能提供足够的电流每个LED全白亮时可能消耗60mA两个就是120mA开发板的3.3V LDO可能负担不起最好使用外部5V电源单独供电并共地。5.3 游戏难度与体验调优基础功能完成后可以调整参数让游戏更好玩重力与跳跃力度调整gravity和jump_force常量。重力越大小鸟下落越快难度越高跳跃力越大单次按键跳得越高。需要反复测试找到一个有挑战性但又可控的平衡点。管道移动速度在定时器中断中每次中断管道移动的距离pipe_position--决定了水平速度。你也可以不每次移动1而是移动0.5需要引入更精细的位置变量让移动看起来更平滑。随机性pipe_gap_height的随机生成算法可以更复杂。简单的HAL_GetTick() % 2可能让缺口连续出现在同一位置。可以引入一个简单的伪随机数序列或者根据时间戳进行更复杂的运算增加不确定性。视觉反馈在GAME_OVER状态可以让两个LED交替闪烁或者实现一个简单的“爆炸”动画快速红黄交替。在PLAYING状态当小鸟成功穿过管道时可以让两个LED短暂变成黄色作为奖励反馈。分数显示由于我们没有屏幕可以通过LED的闪烁模式来编码分数。例如游戏结束后用小鸟LED闪烁次数来表示得分长间隔代表十位短间隔代表个位。这个“2-pixel Flappy Bird”项目从构思到实现是一次对嵌入式系统核心概念——GPIO控制、定时器中断、状态机、外设驱动SPI——的集中实践。它证明了即使资源极其有限创意和扎实的编程也能创造出有趣的交互体验。当你看到两个小小的像素灯随着你的按键节奏明灭跳动并需要你全神贯注去“穿过”那移动的红点时那种纯粹的游戏乐趣和成就感是任何复杂游戏都无法替代的。