## 编写一个反应时间测试的游戏 [[mcu|微控制器]]不仅出现在工业设备中,还广泛用在家庭中的许多电子产品中,包括玩具和游戏。 在本节中,我们将设计一款简单的反应时间测试游戏,看看我们的朋友中谁会在灯熄灭的一瞬间能最快点击按键。 对反应时间的研究被称为心理计时法,虽然这是一门硬科学,它也是许多基于技能的游戏的基础,包括我们即将创建的游戏。我们的反应时间 - 我们的大脑来响应并判断、发送信号去执行的过程是以毫秒计的,人类的平均反应时间大约在200-250毫秒范围, 但有些人可以达到更快的反应时间, 通过这个游戏就给他们一个机会来展示他们的优势! 这个游戏是使用一个LED和按钮创建一个简单的反应计时游戏,可以一个人测试,也可以两个人一起玩,我们需要用到[[rpi_pico|Pico]]核心板、学习板上任何颜色的[[LED]],1个或2个按键开关。 ### 1. 单人游戏 在这个游戏的程序中,使用学习板上的一颗LED作为输出设备,取代了我们在游戏机上通常使用的电视;使用学习板上的一个按键进行控制,而我们的[[rpi_pico|Pico]]是游戏主机,尽管比我们通常看到的要小得多! 现在我们需要真正地编写游戏。像往常一样,将我们的Pico连接到树莓派或其它电脑上,并加载[[Thonny_ide|Thonny]]、创建一个新程序、并通过导入machine库来启动它,这样我们就可以控制Pico的GPIO引脚: import machine 我们还需要导入utime库: import utime 此外,我们还需要一个新的库 - urandom,它是一个创建随机数的库,在这个游戏中是一个关键的部分,能使得游戏更有趣,并可以防止已经玩过这个游戏的玩家通过简单地倒数固定的秒点击运行按钮,从而影响了测试的真实效果。 接下来,设置一个按下的变量为False(稍后详细介绍),并设置我们正在使用的两个引脚: * 用于LED的GP16(黄色的LED) * 用于按钮开关的GP12 pressed = False led = machine.Pin(16, machine.Pin.OUT) button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) 在前面的章节中,我们已经学会如何在主程序或单独的线程中处理按键的响应。不过,这一次我们将采用一种不同的、更灵活的方法 - 中断请求(IRQs)。这个名字听起来很复杂,但其实很简单,想象你正在一页一页地读一本书,有人走到你面前问你一个问题。这个人正在执行一个中断请求: 要求你停止正在做的事情,回答他们的问题,然后让你回去读你的书。 MicroPython中断请求以完全相同的方式工作,它允许某些东西中断主程序,在本例中是按下一个按钮开关。在某些方面,它类似于一个线程,因为有一段代码位于主程序之外。不过,与线程不同的是代码不会持续运行,它只在触发中断时运行。 首先为中断定义一个处理程序,这被称为回调函数,是中断触发时运行的代码。与任何类型的嵌套代码一样,处理程序的代码——第一行之后的所有代码——每一层都需要缩进四个空格,Thonny会自动为我们做这件事。 def button_handler(pin): global pressed if not pressed: pressed=True print(pin) 这个处理程序首先检查被按下的变量的状态,然后将其设置为True以忽略进一步的按键按压(从而结束游戏)。然后输出有关触发中断的引脚的信息。这不是太重要的时刻,我们目前只有一个引脚(GP12)配置为一个输入, 所以中断将总是来自那个引脚,让我们测试中断比较容易。 继续下面的程序,记住删除Thonny自动创建的缩进,以下代码不是处理程序的一部分: led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) 这段代码对我们来说马上就很熟悉了: - 第一行将连接到GP16引脚的LED打开; - 下一行暂停了程序; - 最后一行再次关闭LED,这是给玩家的信号,该摁下按键了。 我们使用的延迟时间不是固定的,而是通过调用urandom库生成的随机时间 - 暂停程序5到10秒之间-“均匀”部分指的是这两个数字之间的均匀分布。 不过,目前还没有什么东西等着按下按钮。我们需要设置中断,通过在我们的程序底部输入以下行: button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) 设置一个中断需要两个东西:触发器和处理程序。触发器告诉你的Pico它应该寻找一个有效的信号来中断它正在做的事情,我们在前面的程序中定义的处理程序是触发中断后运行的代码。在这个程序中,我们的触发信号为IRQ_FALLING,这意味着中断被触发时,引脚的值从高(由于内部上拉电阻的作用,默认状态为高电平)到低,当连接到“地”的按钮被按下, IRQ_RISING的触发信号会做相反的事情,当引脚从低到高时触发中断。 在我们的电路中,一旦按下按键,IRQ_FALLING将被触发, IRQ_RISING只在按钮被释放时触发。 **中断申请的上升沿和下降沿** 如果我们需要编写一个触发中断的程序,无论管脚信号是上升还是下降都可以触发,可以使用管道或竖条符号( | )来组合这两个触发信号: 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(16, machine.Pin.OUT) button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) def button_handler(pin): global pressed if not pressed: pressed=True print(pin) led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) 单击“运行”按钮并将程序作为Reaction_Game.py 保存到Pico。 我们会看到LED亮起:这是让我们把手指放在按钮上准备就绪的信号。 当LED熄灭时,尽快按下按钮。 当我们按下按钮时,它会触发我们之前编写的处理程序代码。查看Shell区域我们会看到Pico打印了一条消息,确认中断是由引脚GP16触发的。我们还将看到另一个详细信息: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.ticks_diff()一个函数它提供了触发这行代码的时间与变量timer_start中保存的参考点之间的差异。 第二行打印结果,使用了格式化的打印方式,文本或字符串的第一位告诉用户后面的数字是什么意思; + 表示接下来的任何内容都应该与该字符串一起打印。在这种情况下,接下来是内容timer_reaction变量 - 以毫秒为单位获取计时器参考点与按下按钮并触发中断之间的差异。 最后,最后一行再连接一个字符串,这样用户就知道这个数字是以毫秒为单位测量的,而不是其它一些单位,如秒或微秒。注意间距:我们会看到'was'之后和第一个字符串的结束引号之前有一个尾随空格,第二个字符串的开引号之后和'milliseconds'这个词之前有一个前导空格。如果没有这些,连接的字符串将打印类似“Your reaction time was323ms”的内容。 我们的程序现在应该是这样的: import machine import utime import urandom pressed = False led = machine.Pin(16, machine.Pin.OUT) button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) 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(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) timer_start = utime.ticks_ms() button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) **挑战:定制** 你能调整你的游戏让LED保持点亮更长时间吗?保持点亮的时间更短怎么办? 您能否个性化打印到Shell区域的消息,并添加第二条祝贺玩家的消息? 再次单击运行按钮,等待LED熄灭,然后按下按钮。这一次,我们将看到一条信息,告诉我们按下按钮的速度,而不是有关触发中断的引脚的报告 - 测量我们的反应时间。再次单击“运行”按钮,看看这次我们是否可以更快地按下按钮 - 在这个游戏中,我们要争取尽可能低的分数! ### 2. 两人游戏 单人游戏很有趣,但也能让其他的朋友参与进来就更好了。我们可以先邀请他们玩我们编写好的游戏,然后比较我们的高分(或者说低分),看看谁的反应时间最快。然后,我们可以修改游戏,让多个人可以进行正面交锋! 如果要设计两个人能玩的游戏,我们需要另外的一个按钮,返回到Thonny中的程序,找到设置第一个按钮的位置,在这一行的正下方,添加: right_button = machine.Pin(13, machine.Pin.IN, machine.Pin.PULL_UP) 修改一下前面的代码,让先前的按键跟这个按键的命名一致起来: left_button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) 我们也需要在程序的其它地方进行相同的更改。滚动到代码的底部,将设置中断触发器的行更改为: left_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) 在其下方添加另一行以在新按钮上设置中断触发器: right_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) 我们的程序现在应该是这样的: import machine import utime import urandom pressed = False led = machine.Pin(16, machine.Pin.OUT) left_button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) right_button = machine.Pin(13, machine.Pin.IN, machine.Pin.PULL_UP) 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(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) timer_start = utime.ticks_ms() right_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) left_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) **中断和处理程序** 我们创建的每个中断都需要一个处理程序,但一个处理程序可以处理任意数量的中断。 在这个程序的情况下,我们有两个中断都进入同一个处理程序, 这意味着无论哪个中断触发,它们都会运行相同的代码。一个不同的程序可能有两个处理程序,让每个中断运行不同的代码,这完全取决于我们需要程序来做什么。 单击“运行”图标,等待LED熄灭,然后按下左侧的按钮开关:我们将看到游戏与以前相同,将我们的反应时间打印到Shell区域。 再次单击“运行”图标,但这次当LED熄灭时,按右侧按钮:游戏将正常运行,正常打印我们的反应时间。 为了让游戏更刺激一点,我们可以让它报告两个玩家中的哪一个是第一个按下按钮的。 返回程序顶部,就在我们设置LED和两个按钮的下方,添加以下内容: fastest_button = None 这会设置一个新变量,fastest_button,并将其初始值设置为None, 因为还没有按下任何按钮。 接下来,转到按钮处理程序的底部并删除处理计时器和打印的两行 - 然后将它们替换为: global fastest_button fastest_button = pin 请记住,这些行需要缩进四个空格,以便[[MicroPython]]知道它们是函数的一部分。这两行允许我们的函数改变,而不是仅仅读取,fastest_button变量,并将它设置为包含触发中断的引脚的详细信息,我们的游戏在本章前面打印到Shell区域的相同细节,包括触发引脚的编号。 现在转到程序的底部,并添加以下两行: while fastest_button is None: utime.sleep(1) 这会创建一个循环,但它不是一个无限循环:在这里,我们已经告诉[[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”条件,它查看fastest_button变量是否is left_button - 表示IRQ是由左侧按钮触发的。 如果是这样,它将打印一条消息——下面的行缩进四个空格,以便MicroPython知道只有在条件为真时才应该运行它——祝贺左手玩家,其按钮连接到GP12。 下一行不应缩进,将条件扩展为“elif”——“else if”的缩写,表示“如果第一个条件不为真,请检查下一个条件”。 这次它查看fastest_button变量是否为right_button ——如果是,则打印一条消息祝贺右手玩家,其按钮连接到GP13。 我们完成的程序应如下所示: import machine import utime import urandom pressed = False led = machine.Pin(16, machine.Pin.OUT) left_button = machine.Pin(12, machine.Pin.IN, machine.Pin.PULL_UP) right_button = machine.Pin(13, machine.Pin.IN, machine.Pin.PULL_UP) fastest_button = None def button_handler(pin): global pressed if not pressed: pressed=True global fastest_button fastest_button = pin led.value(0) utime.sleep(urandom.uniform(5, 10)) led.value(1) timer_start = utime.ticks_ms() left_button.irq(trigger=machine.Pin.IRQ_FALLING, handler=button_handler) right_button.irq(trigger=machine.Pin.IRQ_FALLING, 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区域保持空白,并且没有带回»›提示符;那是因为主线程仍在运行,位于我们创建的循环中。 现在按下连接到引脚GP12的左侧按钮, 我们会看到一条祝贺我们打印到Shell的消息——您的左手是赢家! 再次单击运行并尝试推送 LED熄灭后右侧的按钮:我们会看到另一条消息打印出来,这次是祝贺您的右手。 再次单击“运行”,这一次将一根手指放在每个按钮上:同时按下它们,看看你的右手还是左手更快! 现在我们已经创建了一个两人游戏,我们可以邀请朋友们一起玩了,看看他们中谁的反应时间最快! **挑战:时序** 你能修改打印的消息吗? \\ 可以加第三个按钮,让三个人同时玩吗?\\ 有没有上限,您可以添加多少个按钮? \\ 您能否将计时器重新添加到您的程序中,以便告诉获胜玩家他们的反应时间有多快?\\