基于APDS-9960与Arduino的智能篮球框:非接触式进球检测与声光反馈系统
1. 项目概述与核心思路
作为一个喜欢在宿舍里随手投几个篮放松一下的嵌入式爱好者,我总觉得那个挂在门后的迷你篮球框少了点什么。它安静、机械,进球时只有球网那一声轻微的“唰”,缺乏那种在街机厅里投进关键球时,灯光闪烁、音效激昂的兴奋感。于是,一个念头冒了出来:能不能用我手头的电子元件,给这个普通的篮球框注入一些“灵魂”,把它变成一个迷你的、智能的街机篮球机?
这个项目的核心目标非常明确:当篮球穿过篮筐时,系统需要自动检测到这个事件,并触发一系列预设的多媒体反馈,包括炫酷的LED灯光动画和应景的音效。这听起来像是一个典型的“输入-处理-输出”嵌入式系统。输入是“进球”这个物理事件,处理核心是一块微控制器,而输出则是灯光和声音。关键在于,如何可靠、即时且低成本地捕捉“进球”这一瞬间。
经过一番选型,我最终确定了以Arduino Nano RP2040 Connect作为大脑,搭配APDS-9960传感器作为“眼睛”的方案。Arduino Nano RP2040 Connect 板载了Wi-Fi/蓝牙模块和麦克风,扩展性很强,但在这个项目中,我们更看重它强大的RP2040双核处理器和CircuitPython的良好支持,这能让开发,尤其是音频文件播放,变得简单很多。而APDS-9960是一个集成了接近感应、手势识别、颜色和光强度检测的多合一传感器,我们这里主要利用它的**接近感应(Proximity)**功能。它的原理是发射红外光,并检测从前方物体反射回来的光强度。物体越近,反射信号越强,传感器读出的数值就越大。我们可以把传感器巧妙地安装在篮筐下方,当篮球(一个相对较大的物体)穿过篮筐、经过传感器前方时,会引发接近读数的剧烈变化,从而被微控制器捕捉到,判定为一次有效进球。
整个系统的逻辑链条因此变得清晰:APDS-9960持续监测篮筐区域的接近数据 -> Arduino Nano RP2040 Connect 循环读取该数据 -> 当数据超过设定的阈值时,判定进球发生 -> 控制器随即调用函数,控制LED灯带播放特定的光效动画,并通过音频模块播放随机选取的庆祝音效。这个方案避免了复杂的机械触发装置,利用非接触式的光学传感,既保持了篮筐原有的简洁结构,又实现了精准的电子化交互。
2. 核心组件选型与功能解析
2.1 微控制器:为什么是Arduino Nano RP2040 Connect?
在众多Arduino开发板中,选择Nano RP2040 Connect主要基于以下几点考量:
- 性能与内存:其核心RP2040微控制器拥有双核Arm Cortex-M0+处理器和264KB的SRAM。对于需要实时处理传感器数据、管理LED动画(可能需要用到
neopixel库)以及解码播放WAV音频文件的任务来说,充足的内存和计算能力是流畅运行的基础。普通的8位AVR单片机(如Uno所用的ATmega328P)在处理多个并发任务和较大音频文件时会非常吃力。 - CircuitPython原生支持:这款板子被设计为对CircuitPython友好。CircuitPython是一种基于Python的解释型语言,对于快速原型开发、文件系统操作(如直接读取SD卡或内置存储中的音频文件)以及硬件控制来说,语法简单直观,调试信息通过串口输出也非常方便。相比于需要编译、上传的Arduino C++,在迭代修改代码、尤其是调整音效和灯光逻辑时,CircuitPython的“保存即运行”特性极大地提升了开发效率。
- 丰富的板载资源:板载的IMU(惯性测量单元)和麦克风在本项目中虽未使用,但为未来升级(例如,检测投篮力度或增加声控启动)预留了可能。其小巧的Nano尺寸也便于集成到有限的空间内。
- 供电与接口:通过Micro-USB接口供电,可以直接连接移动电源,使得整个装置无需依赖墙插,可以随意挂在任何一扇门上,实现了真正的便携性。
2.2 感知核心:APDS-9960传感器工作机制详解
APDS-9960是本项目的“智能”之源。它通过一个内置的红外LED(IRED)和一组光电二极管(PD)来工作。
- 发射端:传感器内部的IRED会发出调制的红外光。
- 接收端:光电二极管负责接收从外部物体反射回来的红外光。
- 接近感应原理:当有物体靠近传感器正面时,反射回的光线会增强。传感器内部的模拟前端(AFE)和ADC会将这个光强信号转换为数字值,即接近数据(Proximity Data)。这个值通常在0到255之间(8位分辨率),数值越大,代表物体离传感器越近、反射越强。
在篮球框的应用场景中,我们需要将传感器固定在篮筐正下方,并使其红外发射/接收窗口朝向篮筐中心。在静止状态下(无球通过),传感器会读到一个稳定的背景值,可能来自篮板或环境的反射。当一个篮球(尤其是颜色较深、红外反射特性与背景不同的球)快速穿过篮筐时,它会短暂地遮挡并强烈反射红外光,导致接近读数产生一个明显的脉冲式峰值。我们的代码就是要去捕获这个峰值。
注意:环境光,特别是含有红外成分的光源(如白炽灯、太阳光),会对APDS-9960造成干扰。因此,在安装时,应尽量避免传感器窗口直对强光源。APDS-9960本身有环境光抑制功能,但在代码初始化时,确保正确配置其增益和积分时间参数,以适应具体的安装环境,这一点至关重要。
2.3 反馈单元:LED与音频系统的构建
LED灯带:我选用的是常见的WS2812B(俗称NeoPixel)可寻址RGB LED灯条。它的优点在于只需要微控制器的一个数字IO引脚,通过单线串行协议就能控制上百颗LED的每一个的颜色和亮度,非常适合创建复杂的流动、渐变动画。我们将它环绕粘贴在篮板背面,让光线从边缘透出,形成“光晕”效果,既醒目又不刺眼。
音频系统:为了播放高质量的WAV音效,我使用了一个小型的、带有功放的auxiliary端口迷你音箱,通过3.5mm音频线连接。Arduino Nano RP2040 Connect本身没有模拟音频输出,但我们可以利用其I2S数字音频接口。在CircuitPython中,有现成的audiobusio和audioio库支持,可以非常方便地将数字音频数据通过I2S传输给一个兼容的I2S DAC解码模块或直接驱动某些支持I2S输入的功放板。另一种更简单的方案是使用一个PAM8302之类的D类音频放大器模块,将板载的PWM输出模拟成音频信号,但音质会稍差。本项目为了获得更好的街机音效体验,推荐使用I2S方案。
3. 系统搭建与硬件连接实操
3.1 物料清单与准备
在开始焊接和粘贴之前,请确保你备齐了以下所有物品:
- 核心控制器:Arduino Nano RP2040 Connect × 1
- 感知模块:Adafruit APDS-9960传感器(带STEMMA QT/Qwiic接口) × 1
- 反馈模块:
- WS2812B LED灯带(30灯/米,长度根据篮板周长裁剪) × 1段
- I2S音频解码放大板(如MAX98357A模块) × 1
- 小型扬声器(4Ω 3W) × 1
- 3.5mm音频线(如果使用aux音箱方案) × 1
- 连接与结构:
- STEMMA QT/Qwiic连接线(用于APDS-9960) × 1
- 杜邦线(公对公、公对母)若干
- 微型面包板或PCB原型板 × 1(用于可靠连接)
- 5V移动电源(输出能力≥2A) × 1
- Micro-USB数据/充电线 × 1
- 激光切割或3D打印的外壳图纸与材料(亚克力或木板)
- 强力双面胶、尼龙扎带、电工胶布
- 迷你篮球框套件(基础款) × 1
3.2 电路连接详解与示意图
安全可靠的连接是项目稳定的基础。以下是各模块与Arduino Nano RP2040 Connect的引脚连接方案:
1. APDS-9960传感器连接(使用I2C接口)由于采用了STEMMA QT接口,连接最为简单:
- 将STEMMA QT线的一端插入APDS-9960传感器,另一端插入Arduino Nano RP2040 Connect上对应的Qwiic/STEMMA QT接口。如果没有,则需要手动连接:
- APDS-9960 VIN -> Arduino 3.3V
- APDS-9960 GND -> Arduino GND
- APDS-9960 SDA -> Arduino GPIOSDA (D12)
- APDS-9960 SCL -> Arduino GPIOSCL (D13)
注意:务必确认使用3.3V供电,因为Nano RP2040 Connect的逻辑电平是3.3V,5V可能会损坏传感器。
2. WS2812B LED灯带连接
- LED灯带VCC-> Arduino5V输出引脚(或直接接移动电源的5V输出,但需共地)
- LED灯带GND-> ArduinoGND
- LED灯带DIN (数据输入)-> Arduino 任意数字IO引脚,例如GPIO D6
3. I2S音频模块连接(以MAX98357A为例)
- MAX98357AVIN-> Arduino5V
- MAX98357AGND-> ArduinoGND
- MAX98357ABCLK (位时钟)-> ArduinoGPIO D27(RP2040的I2S BCK)
- MAX98357ALRCLK (左右时钟)-> ArduinoGPIO D28(RP2040的I2S WS)
- MAX98357ADIN (数据)-> ArduinoGPIO D29(RP2040的I2S DATA)
- MAX98357ASD (关机)-> 接高电平(如3.3V)或悬空(模块内部有上拉)
- 扬声器正负极连接到模块的SPK+和SPK-
供电方案:整个系统的电流消耗主要来自LED灯带(全白高亮时电流很大)。建议将移动电源的5V输出直接连接到面包板的电源轨,然后从此电源轨分别为Arduino(通过Vin或5V引脚)、LED灯带和音频模块供电。Arduino的USB口仅用于编程。务必确保你的移动电源能提供至少2A的持续电流。
3.3 机械结构设计与安装
外壳制作:为了保护电子元件免受篮球撞击和震动,一个定制的外壳必不可少。我使用Fusion 360设计了一个简单的、底部开口的盒子,侧边留有走线孔和传感器窗口。然后将设计文件导出为DXF格式,用激光切割机在3mm厚的亚克力板上进行切割。你也可以使用3D打印,但激光切割亚克力板速度更快,外观也更整洁。用亚克力胶水将各面板粘合。
现场安装步骤:
- 固定核心单元:将连接好所有线缆的Arduino、面包板和音频模块整体放入亚克力外壳中。用尼龙扎带或泡棉胶固定内部元件,防止晃动。然后用强力魔术贴(Command Strip)将整个外壳牢固地粘贴在篮球板背面的中央上方位置。魔术贴的好处是未来需要调试或拆卸时非常方便,且不留痕迹。
- 安装传感器:这是最关键的一步。将APDS-9960传感器用胶水或小型夹具,固定在篮筐铁圈的正下方,确保其感应窗口朝上,正对篮筐网兜的中心区域。调整其角度,使其既能“看到”通过的篮球,又不会被篮网日常摆动所误触发。可以用一小块黑色热缩管或胶带包裹传感器主体,减少环境光干扰。
- 布置LED灯带:将WS2812B灯带沿着篮球板背面的边缘粘贴一圈。如果篮板是透明的,效果会像霓虹灯边框一样酷;如果是不透明的,光线从边缘漫射出来也能形成氛围光。注意灯带的数据流向,确保DIN端连接到控制器。
- 安装扬声器:将小型扬声器用胶粘或扎带固定在外壳侧面或篮板背面,出声孔不要被遮挡。
- 走线与美化:使用电工胶布或线缆收纳管,将连接传感器、LED灯带的线缆整齐地捆扎并固定在篮板背面,避免垂落影响美观和安全性。
4. CircuitPython代码深度剖析与实现
4.1 开发环境搭建与库管理
首先,你需要将Arduino Nano RP2040 Connect刷入CircuitPython固件。
- 访问CircuitPython官网,找到Arduino Nano RP2040 Connect的专用
.uf2固件文件并下载。 - 按住板子上的“BOOT”按钮不放,同时通过USB线连接到电脑,然后释放按钮。此时电脑会识别到一个名为“RPI-RP2”的U盘。
- 将下载好的
.uf2文件拖入该U盘。盘符会自动刷新,变成名为“CIRCUITPY”的驱动器,这表明CircuitPython环境已就绪。
接下来是库文件。打开“CIRCUITPY”驱动器,你会看到一些默认文件夹。我们需要将必要的库文件复制到其中的lib文件夹内。本项目需要的核心库包括:
adafruit_apds9960.mpy:用于驱动APDS-9960传感器。adafruit_bus_device:总线设备支持库。adafruit_pixelbuf.mpy:NeoPixel底层支持。neopixel.mpy:用于控制WS2812B LED灯带。adafruit_ticks.mpy:提供时间管理功能,用于非阻塞式延迟。audiobusio和audioio:用于I2S音频播放(如果使用PWM音频,则可能需要pwmio和simpleio)。
这些库都可以从Adafruit的CircuitPython库包中获取。将对应的.mpy文件复制到lib文件夹即可。
4.2 主程序逻辑与代码分解
主程序code.py(CircuitPython启动后自动运行的文件)的结构如下。我们将分段解析:
import time import board import digitalio import neopixel import audiobusio import audioio from adafruit_apds9960.apds9960 import APDS9960 import random import os # --- 1. 硬件初始化 --- # 初始化I2C总线用于APDS-9960 i2c = board.I2C() # 使用默认的I2C引脚(GPIO12 SDA, GPIO13 SCL) apds = APDS9960(i2c) apds.enable_proximity = True # 使能接近感应功能 # 可选:设置接近感应的增益和积分时间,以适应具体环境 # apds.proximity_gain = 2 # 增益级别 (0:1x, 1:2x, 2:4x, 3:8x) # apds.proximity_integration_time = 10 # 积分时间(毫秒) # 初始化NeoPixel LED灯带 # 假设灯带有30颗LED,连接到板子的D6引脚 NUM_PIXELS = 30 pixel_pin = board.D6 pixels = neopixel.NeoPixel(pixel_pin, NUM_PIXELS, brightness=0.3, auto_write=False) # 初始化I2S音频输出 # 引脚定义根据之前的连接方案 i2s_bclk = board.D27 # 位时钟 i2s_wsel = board.D28 # 字选择(左右时钟) i2s_data = board.D29 # 数据线 audio = audiobusio.I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data) # 定义音频文件列表 startup_sound = "start.wav" basket_sounds = ["2-points.wav", "3-points.wav", "what_a_shot.wav", "cheer.wav"] # --- 2. 全局变量与阈值设定 --- PROXIMITY_THRESHOLD = 50 # 接近感应阈值,需要根据实际测试调整 DEBOUNCE_TIME_MS = 500 # 防抖时间(毫秒),防止一次进球触发多次 last_trigger_time = 0 # 记录上次触发时间 # --- 3. 功能函数定义 --- def play_sound(filename): """播放指定的WAV音频文件""" try: with open(filename, "rb") as wave_file: wave = audioio.WaveFile(wave_file) audio.play(wave) while audio.playing: pass # 等待播放完毕(阻塞式)。如需非阻塞,需更复杂设计。 except OSError: print(f"Could not find or play file: {filename}") def startup_sequence(): """开机启动动画和音效""" print("Game Start!") # 1. 播放启动音效 play_sound(startup_sound) # 2. LED红色闪烁动画 for _ in range(3): pixels.fill((255, 0, 0)) # 红色 pixels.show() time.sleep(0.2) pixels.fill((0, 0, 0)) # 熄灭 pixels.show() time.sleep(0.2) # 最后保持熄灭或低亮度待机状态 pixels.fill((10, 10, 10)) # 低亮度白光作为待机背光 pixels.show() def basket_made_sequence(): """进球庆祝序列""" print("Basket Made!") # 1. 随机选择一个庆祝音效播放 sound_to_play = random.choice(basket_sounds) play_sound(sound_to_play) # 2. 同步播放红-黄-金闪烁动画 colors = [(255, 0, 0), (255, 255, 0), (255, 215, 0)] # 红,黄,金 for color in colors: for i in range(NUM_PIXELS): pixels[i] = color pixels.show() time.sleep(0.02) # 形成追逐效果 time.sleep(0.1) pixels.fill((0, 0, 0)) pixels.show() time.sleep(0.05) # 恢复待机状态 pixels.fill((10, 10, 10)) pixels.show() # --- 4. 主循环 --- # 首次启动,执行开机序列 startup_sequence() print("Ready to play! Sensing for baskets...") while True: current_time = time.monotonic_ns() // 1_000_000 # 获取当前毫秒时间 # 读取接近感应值 prox_value = apds.proximity # 打印数值用于调试和阈值校准(完成后可注释掉) # print(f"Proximity: {prox_value}") # 判断逻辑:数值超过阈值,且距离上次触发已过防抖时间 if prox_value > PROXIMITY_THRESHOLD and (current_time - last_trigger_time) > DEBOUNCE_TIME_MS: print(f"Triggered! Value: {prox_value}") last_trigger_time = current_time basket_made_sequence() # 短暂延迟,降低CPU占用。注意:在非阻塞音频播放设计中,这里需要更精细的时间管理。 time.sleep(0.01) # 10毫秒的采样间隔代码关键点解析:
- 阈值
PROXIMITY_THRESHOLD:这是整个系统的灵敏度开关。数值设置过低,环境干扰可能引起误触发;设置过高,可能无法检测到快速通过的篮球。必须通过实际测试来校准。在串口监视器中观察篮球通过时的峰值读数,将此峰值减去一些安全余量(比如20-30%),作为最终阈值。 - 防抖
DEBOUNCE_TIME_MS:篮球穿过传感器区域可能需要几十到上百毫秒,期间会产生一连串的高读数。防抖机制确保在一次进球事件中,只触发一次庆祝序列,避免音效和动画重叠播放。 - 非阻塞设计考虑:当前的
play_sound函数是阻塞的,即播放音效时,主循环会暂停,无法检测新的进球。这对于街机游戏来说通常可以接受,因为两次进球本身就有时间间隔。如果你希望实现更实时的响应,或者播放背景音乐,就需要使用非阻塞音频播放,这涉及到状态机和更复杂的时间调度,是下一步优化的方向。 - 音频文件格式:CircuitPython的
audioio库通常支持未压缩的16位单声道或立体声WAV文件,采样率推荐为22050 Hz或更低,以节省内存和CPU资源。可以使用Audacity等软件将MP3转换为符合要求的WAV格式。
4.3 音效与光效的创作技巧
音效制作:
- 从经典篮球游戏或免费音效网站(如freesound.org)寻找“篮网声”、“得分音效”、“观众欢呼”等素材。
- 使用Audacity打开下载的MP3或WAV文件。
- 关键步骤:点击菜单栏的【轨道】->【重采样】,将采样率设置为22050 Hz。然后点击【文件】->【导出】->【导出为WAV】,在格式选项中选择**“Signed 16-bit PCM”**。这样得到的WAV文件兼容性最好。
- 将处理好的
start.wav,2-points.wav等文件,直接复制到“CIRCUITPY”驱动器的根目录下。
LED动画编程进阶: 上面的示例使用了简单的颜色填充和追逐效果。neopixel库的强大之处在于可以逐颗控制LED。你可以设计更复杂的动画,例如:
- 得分波浪:从传感器位置(篮筐下方)向两侧扩散光波。
- 彩虹循环:进球后,整个篮板边缘循环显示彩虹渐变。
- 进度条:模拟得分累计,LED灯一颗颗点亮。
这需要你学习一些颜色计算(如HSV到RGB的转换)和动画帧更新的逻辑。网络上有很多NeoPixel的动画库和示例代码可以参考。
5. 调试、优化与问题排查实录
5.1 传感器校准与阈值确定
这是项目成功最关键的一步。请按以下流程操作:
- 将全部硬件连接好,上传并运行一个仅包含传感器读取和串口打印的简单测试程序。
- 打开Mu Editor或任何串口监视器,设置波特率为115200。
- 观察静止状态下(无球)的接近读数。记录这个基础值。可能因环境光在5-30之间波动。
- 多次投球,让篮球以正常速度穿过篮筐。观察串口输出的峰值。记录每次的峰值读数。
- 分析数据:如果峰值读数远高于基础值(例如基础值20,峰值200),说明传感器位置良好。如果峰值不明显,需要调整传感器角度,使其更正对篮球轨迹。
- 设定阈值:取多次峰值读数的最小值,乘以一个系数(如0.7),作为初始阈值。例如,最小峰值是180,那么阈值可设为126。将这个值填入代码中的
PROXIMITY_THRESHOLD。 - 进行实战测试:投球10-20次。目标是零误触发(球没进不响)和零漏触发(球进了必响)。如果误触发,适当提高阈值;如果漏触发,适当降低阈值,或检查传感器是否被遮挡、篮球反射率是否太低(可尝试更换篮球或在球上贴一小片反光胶带)。
5.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查与解决步骤 |
|---|---|---|
| 上电后无任何反应 | 1. 供电问题 2. CircuitPython未正确刷入 3. 代码文件错误 | 1. 检查移动电源开关、USB线连接,用万用表测量5V和GND间电压。 2. 重新按住BOOT键上电,查看电脑是否出现“RPI-RP2”盘符,重新拖入UF2文件。 3. 确认“CIRCUITPY”盘根目录下的主程序文件名为 code.py。 |
| 串口无传感器数据输出 | 1. I2C连接错误 2. 传感器损坏 3. 库文件缺失 | 1. 检查VIN(3.3V)、GND、SDA、SCL四根线是否接对、接牢。 2. 运行一个简单的I2C扫描程序,查看是否能找到APDS-9960的地址(通常是0x39)。 3. 确认 lib文件夹内有adafruit_apds9960.mpy等库文件。 |
| LED灯带不亮或颜色错乱 | 1. 电源功率不足 2. 数据线连接错误 3. NeoPixel对象初始化错误 | 1. LED全亮时电流很大,确保使用能提供2A以上的5V电源,并检查电源线是否过细。 2. 确认DIN线连接到了正确的Arduino引脚,且方向正确(DIN接控制器,DOUT接下一段)。 3. 检查代码中 NeoPixel初始化时的引脚、数量是否正确。 |
| 有音效但声音小或失真 | 1. 音频文件格式不兼容 2. I2S接线错误或时钟不匹配 3. 扬声器阻抗/功率不匹配 | 1. 严格按照前文所述,用Audacity将音频重采样为22050Hz, 16-bit PCM WAV格式。 2. 仔细核对BCLK, LRCLK, DATA三根线与代码中定义的引脚是否一致。 3. 检查I2S音频模块(如MAX98357A)的增益设置跳线,尝试调高增益。确保扬声器是4Ω或8Ω的。 |
| 进球检测不稳定(时灵时不灵) | 1. 传感器阈值设置不当 2. 环境光干扰强烈 3. 防抖时间太短或太长 | 1. 重新进行系统的传感器校准流程,精细调整阈值。 2. 尝试在传感器窗口上方加一个短的遮光罩(用黑色热缩管或胶带卷成筒)。 3. 调整 DEBOUNCE_TIME_MS,确保它能覆盖单次进球产生的整个读数脉冲宽度(通常500ms足够)。 |
| 系统运行一段时间后死机 | 1. 电源电压跌落 2. 内存泄漏(在复杂动画中可能出现) 3. 过热 | 1. 使用万用表监控5V电压在LED全亮时的波动,如果低于4.7V,请更换输出能力更强的移动电源。 2. 简化LED动画逻辑,避免在循环中不断创建新的对象。确保使用 auto_write=False和pixels.show()的组合来控制刷新。3. 确保外壳有通风孔,避免阳光直射。 |
5.3 项目优化与扩展思路
当基础功能稳定运行后,你可以考虑以下升级,让这个街机篮球框更具吸引力:
- 分数统计与显示:增加一个OLED屏幕(通过I2C连接),实时显示当前得分、连中次数、命中率等。每次进球后,在屏幕上显示炫酷的得分动画。
- 游戏模式多样化:利用板载的按键或通过蓝牙连接手机App,切换游戏模式。例如“限时挑战赛”(60秒内投进越多分越高)、“完美连中”(要求连续投中,中断则扣分)。
- 无线化与数据同步:利用板载的Wi-Fi或蓝牙模块,将得分数据上传到云端服务器或手机App,创建排行榜,实现多人远程竞技。
- 增强反馈:在篮板内部安装一个微型振动电机,当球砸板或打铁时给予触觉反馈。或者,增加一个舵机控制的“记分牌翻页”机构,每得10分就翻动一下。
- 低功耗优化:如果使用电池供电,可以加入红外感应或超声波传感器,当检测到玩家靠近时自动唤醒系统,无人时进入深度睡眠,极大延长续航。
这个项目最迷人的地方在于,它从一个简单的想法出发,融合了传感器技术、嵌入式编程、数字音频、灯光控制和简单的结构设计,最终创造出了一个充满乐趣和成就感的物理交互产品。每一次篮球入网时随之而来的声光盛宴,都是对动手创造者的最佳奖赏。
