创建一个简单的反应计时游戏,使用一个LED和按钮,可以供一个人玩,也可以两个人一起玩。

微控制器不仅出现在工业设备中,还为家庭中的许多电子产品提供动力,包括玩具和游戏。在本章中,你将创造一款简单的反应时间游戏,看看你的朋友中谁会在灯熄灭的一瞬间最快点击按键。

对反应时间的研究被称为心理计时法,虽然这是一门硬科学,它也是许多基于技能的游戏的基础,包括你即将创建的游戏。你的反应时间 - 你的大脑来响应并判断、发送信号去执行的过程,以毫秒计:人类的平均反应时间大约是200-250毫秒,但有些人喜欢得更快的反应时间, 给他们一个真正的优势在游戏中!

对于这个项目,你需要用到你的Pico核心板、学习板上任何颜色的LED,两个按键开关。

LED是输出设备,取代了你通常在游戏机上使用的电视; 所述按钮开关为控制器 而你的Pico是游戏主机,尽管比你通常看到的要小得多!

现在你需要真正地编写游戏。像往常一样,将你的Pico连接到你的树莓派或其他电脑,并加载Thonny、创建一个新程序、并通过导入machine库来启动它,这样你就可以控制你的Pico的GPIO引脚:

import machine

你还需要utime库:

import utime

此外,你将需要一个新的库:urandom,它处理创建随机数-一个关键的部分,使游戏有趣,并使用在这个游戏中,以防止玩家谁已经玩它之前简单地倒数固定的秒点击运行按钮。

接下来,设置一个按下的变量为False(稍后详细介绍),并设置您正在使用的两个引脚:用于LED的GP15和用于按钮开关的GP14。

pressed = False
led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)

在前面的章节中,您已经在主程序或单独的线程中处理了按钮开关。不过,这一次您将采用一种不同的、更灵活的方法:中断请求(IRQs)。这个名字听起来很复杂,但其实很简单:想象你正在一页一页地读一本书,有人走到你面前问你一个问题。这个人正在执行一个中断请求:要求你停止正在做的事情,回答他们的问题,然后让你回去读你的书。 MicroPython中断请求以完全相同的方式工作:它允许某些东西中断主程序,在本例中是按下一个按钮开关。在某些方面,它类似于一个线程,因为有一段代码位于主程序之外。不过,与线程不同的是,代码不会持续运行:它只在触发中断时运行。

首先为中断定义一个处理程序。这被称为回调函数,是中断触发时运行的代码。与任何类型的嵌套代码一样,处理程序的代码——第一行之后的所有代码——每一层都需要缩进四个空格;托尼会自动为你做这件事。

def button_handler(pin): 
    global pressed
    if not pressed: 
        pressed=True
        print(pin)

这个处理程序首先检查被按下的变量的状态,然后将其设置为True以忽略进一步的按键按压(从而结束游戏)。然后输出有关触发中断的引脚的信息。这不是太重要的时刻-你只有一个引脚配置为一个输入,GP14,所以中断将总是来自那个引脚-但让你测试你的中断很容易。

继续下面的程序,记住删除Thonny自动创建的缩进-以下代码不是处理程序的一部分:

led.value(1) 
utime.sleep(urandom.uniform(5, 10)) 
led.value(0)

这段代码对你来说马上就很熟悉了:第一行将连接到GP15引脚的LED打开;下一行暂停了程序;最后一行再次关闭LED -球员的信号按下按钮。不是使用固定的延迟,然而,它利用urandom库暂停程序5到10秒之间-“均匀”部分指的是这两个数字之间的均匀分布。

不过,目前还没有什么东西等着按下按钮。你需要设置中断,通过在你的程序底部输入以下行:

button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

设置一个中断需要两个东西:触发器和处理程序。触发器告诉你的Pico它应该寻找一个有效的信号来中断它正在做的事情;您在前面的程序中定义的处理程序是触发中断后运行的代码。

在这个程序中,你的触发信号为IRQ_RISING, 这意味着中断被触发时,引脚的值从低(由于内部下拉电阻的作用,默认状态为低电平)到高,当连接到3V3的按钮被按下,

IRQ_FALLING的触发信号会做相反的事情,当引脚从高到低时触发中断。

在电路的情况下,IRQ_RISING将触发一旦按钮被按下,

IRQ_FALLING只在按钮被释放时触发。

中断申请的上升沿和下降沿

如果你需要编写一个触发中断的程序,无论管脚信号是上升还是下降都可以触发,你可以使用管道或竖条符号( | )来组合这两个触发信号:

button.irq(trigger= machine.Pin.IRQ_RISING | machine.Pin.IRQ_FALLING,handler = button_handler)

你的程序应该是这样的:

import machine import utime import urandom
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)
def button_handler(pin): global pressed
if not pressed: pressed=True
print(pin)
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

单击“运行”按钮并将程序作为 Reaction_Game.py 保存到您的Pico。 您会看到LED亮起:这是您的手指放在按钮上准备就绪的信号。

当LED熄灭时,尽快按下按钮。

当您按下按钮时,它会触发您之前编写的处理程序代码。 查看Shell区域:您会看到Pico打印了一条消息,确认中断是由引脚GP14触发的。 您还将看到另一个详细信息:mode=IN 告诉您该引脚已配置为输入。 不过,这条信息对游戏的意义不大:为此,您需要一种方法来计时玩家的反应速度。 首先从按钮处理程序中删除行 print(pin) - 您不再需要它了。

转到程序底部并添加一个新行,就在设置中断的位置上方:

timer_start = utime.ticks_ms()

这将创建一个名为timer_start的新变量

并用utime.ticks_ms()函数的输出填充它,该函数计算自utime库开始计数以来经过的毫秒数。

这提供了一个参考点:刚好在LED熄灭之后和就在中断触发器准备好读取按钮按下之前的时间。

接下来,返回按钮处理程序并添加以下两行,记住它们需要缩进四个空格,以便 MicroPython 知道它们构成嵌套代码的一部分:

timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start)
print("Your reaction time was " + str(timer_reaction) + " milliseconds!")

第一行创建了另一个变量,这次是实际触发中断的时间——换句话说,当你按下按钮时。不过,它不是像以前那样简单地从 utime.ticks_ms() 读取数据

而是使用 utime.ticksdiff()一个函数它提供了触发这行代码的时间与变量timerstart中保存的参考点之间的差异。

第二行打印结果,但使用连接来很好地格式化它。文本或字符串的第一位告诉用户后面的数字是什么意思; + 表示接下来的任何内容都应该与该字符串一起打印。在这种情况下,接下来是内容

timer_reaction 变量 - 以毫秒为单位获取计时器参考点与按下按钮并触发中断之间的差异。

最后,最后一行再连接一个字符串,这样用户就知道这个数字是以毫秒为单位测量的,而不是其他一些单位,如秒或微秒。注意间距:你会看到'was'之后和第一个字符串的结束引号之前有一个尾随空格,第二个字符串的开引号之后和'milliseconds'这个词之前有一个前导空格。如果没有这些,连接的字符串将打印类似“您的反应时间为 323 毫秒”的内容。 你的程序现在应该是这样的:

import machine 
import utime 
import urandom
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)
def button_handler(pin): global pressed
if not pressed: pressed=True
timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start)
print("Your reaction time was " + str(timer_reaction) + " milliseconds!")
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
timer_start = utime.ticks_ms() button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

挑战:定制

你能调整你的游戏让 LED 保持点亮更长时间吗? 保持点亮的时间更短怎么办? 您能否个性化打印到 Shell 区域的消息,并添加第二条祝贺玩家的消息?

再次单击运行按钮,等待 LED 熄灭,然后按下按钮。这一次,您将看到一条线,告诉您按下按钮的速度,而不是有关触发中断的引脚的报告 - 测量您的反应时间。再次单击“运行”按钮,看看这次您是否可以更快地按下按钮 - 在这个游戏中,您要争取尽可能低的分数!

单人游戏很有趣,但让您的朋友参与进来就更好了。您可以先邀请他们玩您的游戏,然后比较您的高分(或者说低分),看看谁的反应时间最快。然后,您可以修改您的游戏,让您进行正面交锋!

首先向您的电路添加第二个按钮。与第一个按钮的接线方式相同,一条腿连接到面包板的电源轨,另一条腿连接到 GP16 引脚 - GP14 的整个电路板上的引脚,连接 LED 的位置,位于 Pico 的对角.确保两个按钮间隔足够远,以便每个玩家都有空间将手指放在按钮上。完成的电路应如图 6-2 所示。 尽管您的第二个按钮现在已连接到 Pico,但它还不知道如何处理它。返回到您在 Thonny 中的程序,找到您设置第一个按钮的位置。在这一行的正下方,添加:

right_button = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_DOWN)

您会注意到名称现在指定了您正在使用的按钮:面包板上的右侧按钮。 为避免混淆,请编辑上面的行,以便您清楚显示板上唯一的按钮现在是左侧按钮:

left_button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN)
 
您也需要在程序的其他地方进行相同的更改。 滚动到底部,您的代码并将设置中断触发器的行更改为:
 
<code python>
left_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

在其下方添加另一行以在新按钮上设置中断触发器:

right_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

你的程序现在应该是这样的:

import machine 
import utime 
import urandom
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
left_button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN) right_button = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_DOWN)
def button_handler(pin): global pressed
if not pressed: pressed=True
timer_reaction = utime.ticks_diff(utime.ticks_ms(), timer_start)
print("Your reaction time was " + str(timer_reaction) + " milliseconds!")
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
timer_start = utime.ticks_ms() right_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler) left_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)

中断和处理程序

您创建的每个中断都需要一个处理程序,但一个处理程序可以处理任意数量的中断。 在这个程序的情况下,你有两个中断都进入同一个处理程序——这意味着无论哪个中断触发,它们都会运行相同的代码。 一个不同的程序可能有两个处理程序,让每个中断运行不同的代码——这完全取决于你需要你的程序做什么。

单击“运行”图标,等待 LED 熄灭,然后按下左侧的按钮开关:您将看到游戏与以前相同,将您的反应时间打印到 Shell 区域。 再次单击“运行”图标,但这次当 LED 熄灭时,按右侧按钮:游戏将正常运行,正常打印您的反应时间。 为了让游戏更刺激一点,你可以让它报告两个玩家中的哪一个是第一个按下按钮的。 返回程序顶部,就在您设置 LED 和两个按钮的下方,并添加以下内容:

fastest_button = None

这会设置一个新变量,fastestbutton,并将其初始值设置为 None——因为还没有按下任何按钮。 接下来,转到按钮处理程序的底部并删除处理计时器和打印的两行 - 然后将它们替换为: <code python> global fastestbutton fastest_button = pin </code>

请记住,这些行需要缩进四个空格,以便 MicroPython 知道它们是函数的一部分。 这两行允许你的函数改变,而不是仅仅读取,fastestbutton 变量,并将它设置为包含触发中断的引脚的详细信息——你的游戏在本章前面打印到 Shell 区域的相同细节,包括 触发引脚的编号。 现在转到程序的底部,并添加以下两行: <code python> while fastestbutton is None:

  utime.sleep(1)

</code>

这会创建一个循环,但它不是一个无限循环:在这里,您已经告诉 MicroPython 只有当 fast_button 变量仍然为零时才在循环中运行代码 - 这是在程序开始时初始化的值。 实际上,这会暂停程序的主线程,直到中断处理程序更改变量的值。 如果两个玩家都没有按下按钮,程序将简单地暂停。 最后,您需要一种方法来确定哪位玩家获胜并祝贺他们。 在程序底部键入以下内容,确保删除 Thonny 在第一行为您创建的四个空格缩进——这些行不构成循环的一部分:

if fastest_button is left_button: 
    print("Left Player wins!")
elif fastest_button is right_button: 
    print("Right Player wins!")

第一行设置了一个“if”条件,它查看fastestbutton变量是否is leftbutton - 表示 IRQ 是由左侧按钮触发的。 如果是这样,它将打印一条消息——下面的行缩进四个空格,以便 MicroPython 知道只有在条件为真时才应该运行它——祝贺左手玩家,其按钮连接到 GP14。

下一行不应缩进,将条件扩展为“elif”——“else if”的缩写,表示“如果第一个条件不为真,请检查下一个条件”。 这次它查看fastestbutton变量是否为rightbutton——如果是,则打印一条消息祝贺右手玩家,其按钮连接到GP16。 您完成的程序应如下所示:

import machine 
import utime 
import urandom
 
pressed = False
led = machine.Pin(15, machine.Pin.OUT)
left_button = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_DOWN) 
right_button = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_DOWN) 
fastest_button = None
 
def button_handler(pin): 
    global pressed
    if not pressed: 
        pressed=True
        global fastest_button 
        fastest_button = pin
 
led.value(1)
utime.sleep(urandom.uniform(5, 10))
led.value(0)
timer_start = utime.ticks_ms() 
left_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler) 
right_button.irq(trigger=machine.Pin.IRQ_RISING, handler=button_handler)
 
while fastest_button is None: 
    utime.sleep(1)
if fastest_button is left_button: 
    print("Left Player wins!")
elif fastest_button is right_button: 
    print("Right Player wins!")

按下运行按钮并等待LED熄灭,但现在不要按下任何一个按钮开关。 您会看到Shell区域保持空白,并且没有带回»›提示符;那是因为主线程仍在运行,位于您创建的循环中。

现在按下连接到引脚GP14的左侧按钮。 您会看到一条祝贺您打印到Shell的消息——您的左手是赢家! 再次单击运行并尝试推送 LED 熄灭后右侧的按钮:您会看到另一条消息打印出来,这次是祝贺您的右手。 再次单击“运行”,这一次将一根手指放在每个按钮上:同时按下它们,看看你的右手还是左手更快!

现在您已经创建了一个两人游戏,您可以邀请您的朋友一起玩,看看你们中谁的反应时间最快!

挑战:时序

你能修改打印的消息吗? 可以加第三个按钮,让三个人同时玩吗? 有没有上限,您可以添加多少个按钮? 您能否将计时器重新添加到您的程序中,以便告诉获胜玩家他们的反应时间有多快?