构建基于 Arduino 的激光游戏,第 2 部分: 动手实践
开始之前
无论您是 Arduino 新手还是经验丰富的构建者,此项目都有适合您的内容。没有比创建交互式物理对象更令人满意的事情了,因为在需要中断或者需要修改时,您知道所有部件的位置以及所有部件的工作原理。'Duino Tag 枪是适合独立完成或与朋友共同完成的优秀项目。要完成此项目,您至少应当基本了解电子学(您应当知道寄存器是什么,但是不必知道其中的深奥原理),并且了解编程(您应当知道循环和变量是什么,但是您不必解析 Big O Notation)。您可以勇敢地进行动手实践。
关于本系列
在本系列中,我们将使用 Arduino 技术来创建名为 'Duino Tag 的基本交互式激光游戏:
第 1 部分:Arduino 基础知识:了解一些 Arduino 基础知识,布置项目,并且做一个帮助您了解红外线工作原理的实验。
第 2 部分:构建和测试 'Duino Tag 枪的接收器部分,包括测试。
第 3 部分:构建发射器并完成 'Duino Tag 枪。
关于本教程
要继续学习本教程,您无需具有任何电子学工作经验,尽管具有使用电子元件的经验肯定会有帮助。对于微控制器经验也是如此。如果您使用过微控制器,则有一定的优势,但是记住 Arduino 平台非常适合没有相应经验的人员。首先,您应当愿意拓展自己的技能。使用电子器件和微控制器会是一种有益的经验。大多数软件工程师没有机会为与物理世界交互的设备编写代码,而 Arduino 提供了使用交互式设备的低成本入口点。
本教程主要讨论游戏的构建。在了解了 Arduino 的基本知识之后,本教程将用大部分篇幅讨论如何构建和测试 'Duino Tag 枪的接收器。您将编写很多代码。我们已经遇到了类似于 “鸡与蛋” 的问题:没有要传输的信号,我们就无法实际测试接收器,同样地,没有接收传输信号的接收器,我们也无法实际测试传输过程。幸运的是,有一个优秀的解决方案能够出色地解决这个问题。
系统要求
对于本教程,我们需要一些工具和设备。参见 教程:构建基于 Arduino 的激光游戏,第 1 部分:Arduino 基础知识 中的列表,其中包括 Arduino 硬件和软件。以下是需要的基本配置。
红外线 LED
任何红外线 LED 都可以,但是 LED 越明亮,效果越好。
红外线传感器
撰写本系列时使用的是 TSOP2138YA 红外线传感器(也是 All Electronics 的产品)。
100 ohm 电阻
这类电阻的表面斑纹为棕色-黑色-棕色。
0.1uF 电容
需要一个 0.1uF 的电容。
开关
需要一个单极瞬态开关。
压电(Piezo)元素
需要一个已焊接了导线的元素。
电线
22 规格的实心线或绞线。
还需要一种连接元件的方式,使用实验电路板或将焊接在一起都可以。
要解决 “鸡与蛋” 问题,需要使用一种通用的遥控装置或 Sony Electronics 提供的遥控装置。本系列中的示例使用来自 Sony 康宝 VHS/DVD 播放器的遥控装置,也使用来自分线盒的遥控装置成功进行了测试。
教程:构建基于 Arduino 的激光游戏,第 1 部分:Arduino 基础知识 已经提到,如果与正在构建自己的 'Duino Tag 枪的其他人合作,这个项目会更加有趣。如果有两把枪可供使用,测试工作将更加轻松。如果您决定独自工作,可以自己建立两把枪,这样会使第 2 和 3 部分的学习更加轻松。在第 3 部分中将所有部件结合起来时尤其如此。
现在,我们将重点放在制造并运行基本的接收器上。首先,您需要了解一下红外线传感器。
红外线传感器基础
市场上有许多类型的红外线 (IR) 传感器。我们选择 TSOP2138YA,因为它的成本低廉,而且很容易购买到。
传感器
传感器(如下所示)将照片传感器和一个前置放大器组合在一起。大多数这种类型的 IR 传感器将两种功能组合到一个元件中,这样会更加方便。传感器的外壳充当着 IR 过滤器。正如 教程:构建基于 Arduino 的激光游戏,第 1 部分:Arduino 基础知识 所讨论的,IR 到处都可以买到。这种传感器以及其他这类传感器的外壳可用于过滤掉背景 IR。该传感器使用 38 kHz 的载波频率。这是常用的 IR 频率,意味着我们的枪也需要以 38 kHz 的频率传输。这个传感器具有一个简单的 3 引脚接口:
1.引脚 1 在一个 38 kHz IR 信号被感知到时提供一个信号。
2.引脚 2 连接供应电压。
3.引脚 3 用于接地。
传感器的运行原理就是这么简单。
图 1. TSOP2138TA 传感器
现在连接一些元件,并使传感器运转起来。
连接传感器
以下指令假设您使用实验电路板来连接元件。如果进行焊接,可以根据需要调整指令。下图展示了一个微型实验电路板,它被附加到一个从 NKC Electronics 购买的 Arduino 保护装置上。
对于 IR 传感器的连接,需要使用电阻和电容。这个传感器的规格说明建议添加一个电容和电阻来 “抑制电源供应干扰”。当最终确定枪的配置时,需要将这些附加元件连接到离传感器尽可能近的地方。通常,人们通过将传感器、电容和电阻装配在小型印刷电路板或 PCB 上来实现这一点。
传感器从引脚 1 输出,该引脚应该连接到 Arduino 板的引脚 2 上。
供应电压通过引脚 2 传入传感器,它应该通过 100-ohm 电阻。将 Arduino 板上的 +5v 引脚连接到电阻,将电阻连接到 IR 传感器上的引脚 2。
IR 传感器通过引脚 3 接地。0.1uF 电容应该连接在传感器上的引脚 2 与 3 之间,如下图所示。
图 2. 连接传感器
现在已经连接好了传感器,接下来设置两个支持元件。这需要用到压电元素和红色的 LED。(与第 1 部分中使用的红色 LED 相同)。
将 LED 的正引脚连接到 Arduino 板上的引脚 13,将 LED 的负引脚接地。正常情况下,您应该需要添加一个电阻,以防止烧坏 LED,但是 Arduino 已包含了一个连接到引脚 13 的电阻。您将使用这个 LED 来指示正在接收一个信号。
同样地,当接收一个信号时,您将使用压电元素来产生声音。将压电元素的正极导线连接到 Arduino 的引脚 12,将负极导线连接到接地引脚。
这样,传感器就连接完成了,结果应该与图 3 中的传感器类似。
图 3. 连接完成
现在可以开始编写传感器的代码了。但是要确定数据传输的方法,您首先需要确定一个简单的协议。
设置 'Duino Tag 协议
当开枪时,发射器中的 IR LED 广播一部分编码数据。在我们能够实际解码该数据之前,我们需要知道该数据将会是什么。这样做需要明确地建立一些假设。
'Duino Tag 不是欺骗者玩的游戏。
这意味着每个人都根据规则玩游戏。我们将建立一些协议,确保没有人能够进行欺骗。如果您需要这样一个协议,可以自由修改需要的代码。但是,预防欺骗的最好方式是在游戏开始之前想每个游戏者的枪上传相同的软件。本教程不会介绍加密或解密信号,也不会尝试阻止人们欺骗。
团队在本质上是有机的,不会受到硬件的约束。
这支持所有类型的行为,比如建立口头上的合作、背叛您的同盟者,以及偶尔向您的朋友射击。
可能需要设置不同的弹药类型和不同的游戏者级别。
应该在协议中为这些设置保留一定的空间。
有时可能需要一个裁判。
裁判应该能够更改游戏者的级别或弹药,对枪进行重置等等。
上面列出了这些假设,我们将以二进制形式传输数据,因此余下的惟一问题是我们应该为协议提供多少字节?如果您将协议大体分为两部分,第一部分表示 “人物”,第二部分表示 “事件”。比如,游戏者 1(人物)以强度 2 进行射击(事件),或者裁判 2(人物)把您复活(事件)。
我们首先假设每 6 位游戏者需要一个裁判,12 位游戏者参与的游戏已经非常大。使用 3 位,您可以传输 16 个惟一编码 —— 已经足够了。同样地,16 个操作远远能够满足一个游戏者或裁判开始游戏的需求了。您能够进行16 种射击类型,或执行 16 种不同的管理操作。您总共需要 8 位 — 前 4 位对应于游戏者的编号,后 4 位将对应于一种操作(射击强度或裁判操作)。现在您大体了解了期望的情形,接下来为传感器编写一些代码。
编写传感器代码
传感器代码以 4 部分的形式编写。首先,您需要在 setup
函数中设置引脚并运行一个简单的测试序列。然后构造一个函数来解码传入的 IR 信号。这个函数构造完成之后,设置 Loop
函数来监听信号。最后,选择一个函数来播放一些音调。代码编写完之后,就可以开始测试了。
设置引脚和变量
在深入研究 setup
函数之前,我们应该设置一些变量。使用变量来保存引脚分配,可以轻松地对 Arduino 板进行再次了解,无需大量更改代码。当将元件连接到 Arduino 时,您将引脚 2 分配给传感器,将引脚 12 分配给扬声器,将引脚 13 分配给反馈 LED(如清单 1 所示)。
清单 1. 保存引脚分配的变量
int sensorPin = 2; // Sensor pin 1 int speakerPin = 12; // Positive Lead on the Piezo int blinkPin = 13; // Positive Leg of the LED we will use to indicate // signal is received还需要 3 个变量来设置传感器的阈值。一个变量设置发射器将发送的 “起始位” 的阈值,另外两个变量以传感器信号脉冲长度的形式表示 0 和 1 值。
清单 2. 用于设置传感器的阈值的变量
int startBit = 2000; // This pulse sets the threshold for a transmission start bit int one = 1000; // This pulse sets the threshold for a transmission that // represents a 1 int zero = 400; // This pulse sets the threshold for a transmission that // represents a 0
还需要一个数组来保存 IR 解码的结果。
清单 3. 保存 IR 解码结果的数组
int ret[2]; // Used to hold results from IR sensing.
这些变量在遇到用于解码传入数据的函数时才会发挥作用。我们现在将它们放在一边,主要构造 setup
函数。这个函数需要设置各种引脚的模式,然后它应该提供反馈来指示系统已经准备好 —— 一两次闪烁的灯光和蜂鸣声。
设置引脚模式
首先,您需要设置引脚模式,如清单 4 所示。有 1 个输入引脚(引脚 2)和 2 个输出引脚(引脚 12 和 13)。
清单 4. 设置引脚模式
void setup() { pinMode(blinkPin, OUTPUT); pinMode(speakerPin, OUTPUT); pinMode(sensorPin, INPUT);我们让灯光闪烁 3 次,每次播放一个音调。
清单 5. 设置灯光和音调
for (int i = 1;i < 4;i++) { digitalWrite(blinkPin, HIGH); playTone(900*i, 200); digitalWrite(blinkPin, LOW); delay(200); }借助
playTone
函数,第 1 个数字控制音调,第 2 个数字控制持续时间。最终结果是以不同音调播放的 3 个音符,构成一个不错的启动序列(下一节将更详细地对此进行讨论)。最后,为了进行测试,我们对串口监视器进行设置,以便您能够在监视器中观察结果。
清单 6. 设置串口监视器
Serial.begin(9600); Serial.println("Ready: "); }
setup
函数已经完成。您已经正确设置了引脚模式,甚至初始化了一个串口连接。现在,我们看一下将用于解码从 IR 传感器传入的数据的函数。
解码 IR
开始解码 IR 之前,我们知道我们需要两个很小的数组来保存两部分数据 —who
和 what
—,还需要一个数组来保存两个返回值。
清单 7. 保存数据的两个数组
void senseIR() { int who[4]; int what[4];我们在前面设置了将在此函数中使用的 3 个变量:startBit、1 和 0。当使用
pulseIn
函数时,这些变量将设置一些阈值,用于解释传感器引脚的行为。
pulseIn
函数是使用 Arduino 语言编写的,用于测量将一个指定的引脚(本例中为传感器引脚)设置为高或低的持续时间。pulseIn
函数的测量值以微秒为单位返回。使用这个函数,您能够解码正在发送的数据。但是,您需要设置一些阈值,用于确定表示 0 或 1 的时间长度。您还需要一些信息来指示您处于数据集的开始位置,我们选择 startBit 来完成此任务。startBit
变量指示:
1.2,000 微秒长的脉冲将用于表示数据集的开始。
2.超过 1,000 微秒的脉冲将用于表示 1。
3.400 到 1,000 微秒的脉冲将用于表示 0。
接下来要做的就是等待一个 startBit
。当等待 startBit
时,需要确保反馈 LED 已关闭。但是,只要得到一个 startBit
,就打开反馈 LED。
清单 8. 打开反馈 LED
while(pulseIn(sensorPin, LOW) < startBit) { digitalWrite(blinkPin, LOW); }现在,您已经收到了一个digitalWrite(blinkPin, HIGH);
startBit
,您需要读取接下来的 8 个脉冲,将它们分配给合适的数组。
清单 9. 将脉冲分配给合适的数组
who[0] = pulseIn(sensorPin, LOW); who[1] = pulseIn(sensorPin, LOW); who[2] = pulseIn(sensorPin, LOW); who[3] = pulseIn(sensorPin, LOW); what[0] = pulseIn(sensorPin, LOW); what[1] = pulseIn(sensorPin, LOW); what[2] = pulseIn(sensorPin, LOW); what[3] = pulseIn(sensorPin, LOW);将数据分配给数组之后,将?据标准化为一组简单的 0 和 1,并发出这些二进制数以供转换。在此过程中,您将向串口监视器输出有价值的消息。清单 10 适用于
who
数组,但是 what
数组的代码几乎一样。
清单 10. who
数组
Serial.println("---who---"); for(int i=0;i<=3;i++) { Serial.println(who[i]); if(who[i] > one) { who[i] = 1; } else if (who[i] > zero) { who[i] = 0; } else { // Since the data is neither zero or one, we have an error Serial.println("unknown player"); ret[0] = -1; return; } } ret[0]=convert(who); Serial.println(ret[0]);可以通过任何合适的方式解码数据。这个函数被抽象为
convert
函数,如下所示。
清单 11. 解码数据
int convert(int bits[]) { int result = 0; int seed = 1; for(int i=3;i>=0;i--) { if(bits[i] == 1) { result += seed; } seed = seed * 2; } return result; }在决定以任何方式更改协议时,可以通过对函数进行抽象来很大程度地简化工作。现在,我们已经有了解码红外线信号的代码,您需要设置
loop
函数来监听信号,并执行相应的行为。
loop
函数
现在,loop
函数非常简单。loop
函数主要供 senseIR
函数调用,其中,while
循环等待一个开始位。但是,一旦 senseIR
函数返回,您将在前面声明的 ret
数组中查看结果,并操作这些结果。这意味着要忽略无效的值,返回一个有效值时播放一个音调,并将该值转储到串口监视器。
清单 12. loop
函数
void loop() { senseIR();放置了更多元件之后,可以根据返回值执行操作。至于现在,这些元件对于测试已经足够了。您还需要构造前面提到过的if (ret[
playTone
函数。
playTone
函数
playTone
函数(如清单 13 所示)来自 Arduino 站点上的 Melody 教程(参见 参考资料)。这是一段公共域代码,用于向我们的压电元素发送一个方波,为我们创建音调。这个函数很简单,它接受两个参数:音调(一个数值)和播放该音调的长度(以微秒为单位)。
清单 13. playTone
函数
void playTone(int tone, int duration) { for (long i = 0; i < duration * 1000L; i += tone * 2) { digitalWrite(speakerPin, HIGH); delayMicroseconds(tone); digitalWrite(speakerPin, LOW); delayMicroseconds(tone); } }现在您无需太深入地研究这个函数。如果您想要 'Duino Tag 枪播放悦耳的音调或更加有趣的声音,可以参阅 Arduino 站点上的 Audio 教程。现在,您已经具备了开始测试所需的所有条件。
测试传感器
完成代码之后(或者上传了本教程的 代码归档文件 的 Duino_Tag_Sensor 文件之后,您就可以开始测试了。现在,您需要前面提到的遥控装置:
1.打开串口监视器,它将重设 Arduino 板。您应该能够看到启动序列,以及闪烁的 LED 和相关联的蜂鸣声。如果没有看到启动序列,请仔细检查您的连接。
2.在您的 IR 传感器上指定测试遥控装置并开始按下按钮。您应该听到扬声器发出蜂鸣声,看到反馈 LED 在闪烁。您还应该从串口监视器窗口获得一些反馈。监视器输出显示某个按钮按下操作导致人物 4 执行了操作 9。
图 4. 串口监视器窗口中的反馈
3.使用您的遥控装置进行试验,看看还能获得其他哪些返回值。如果无法从 IR 传感器获得任何读数,请仔细检查连线并确保遥控装置电量充足。
现在传感器已经能够正常工作(并经过了测试),您一定感到非常激动。您最初可能对如何实现这一功能一筹莫展,或者以前从未使用过 IR。恭喜您成功了!
在下一节中,我们将对传感器进行设置,为第 3 部分做准备。让我们为游戏者和裁判建立一些基本行为,以便当发射器发送信息时,您已经准备好进行游戏。在完成第 3 部分之前,对下一节中的代码进行测试比较困难,但是如果借助遥控装置,您将发现一些可用于传输的出色编码组。例如,在我的遥控装置上,Volume Up 的人物/事件始终是 4/9,而 Volume Down 的结果始终是 12/9。
处理解码结果
现在,您能够解码这些正在传输的微型数据集了,并且需要对它们进行一些处理。
建立游戏者和裁判
首先确定谁是游戏者,谁是裁判,以及游戏者编号和开始级别。您还应该设置一些变量来保存允许的射击次数和命中次数,以及您拥有的射击和命中次数。尽管您不会使用射击次数,但是仍然可以定义它们。所有这些都在脚本顶部定义。
清单 14. 设置变量来保存射击和命中次数
int playerLine = 14; // Any player ID >= this value is a referee, // <= this value is a player; int myCode = 1; // This is your unique player code; int myLevel = 1; // This is your starting level; int maxShots = 6; // You can fire this many safe shots; int maxHits = 6; // After this many hits you are dead; int myShots = maxShots;// You can fire 6 safe shots; int myHits = maxHits; // After 6 hits you are dead;
当将代码上传到多把枪时,所需做的是更改每把新枪的 myCode 变量的值。其余的代码可以保持不变。可以使用 DIP 开关来完成这一任务,使用其他方法可能产生硬件问题。但是您可能很想动态地对枪进行重新编码。
现在让我们确定一下这些操作本身到底是什么。如果您以裁判开始,这些操作将更容易定义,您很快就会明白其中的原因。首先定义裁判能够执行的 4 种操作:
1.升级(操作 0)
2.降级(操作 1)
3.重设弹药(操作 2)
4.复活(操作 3)
由于升级和降级需要有一个范围,您应该定义两个变量来保存游戏者的最高和最低级别。
清单 15. 保存游戏者的最高和最低级别的变量
int maxLevel = 9; // You cannot be promoted past level 9; int minLevel = 0; // You cannot be demoted past level 0
接下来,定义游戏者能够执行的操作。实际上只有两个操作:命中和答复。因为,您可以将游戏者的级别信息与命中次数一起发送,可以包含级别 0 的命中次数(操作 0)、级别 1 的命中次数(操作 1)等,直到到达最大值,本例中的最大值为级别 9 的命中次数(操作 9)。通过将级别信息和命中次数一起传递,可以在以后进行有趣的修改,将这些级别与 'Duino Tag 枪的行为结合起来。
答复仅包含两种形式:成功和失败。成功意味着您命中了,或者您的裁判任务生效了。失败意味着您的命中无效,或者裁判任务无法完成,比如对某人的升级超过了可用等级。指定答复成功(操作 14)和答复失败(操作 15)。
所有这些操作都可以以变量的形式存储,如下所示。
清单 16. 将操作存储在变量中
int refPromote = 0; // The refCode for promotion; int refDemote = 1; // The refCode for demotion; int refReset = 2; // The refCode for ammo reset; int refRevive = 3; // The refCode for revival;int replySucc = 14; // the player code for Success;
int replyFail = 15; // the player code for Failed;
定义了这些操作之后,就应该在 senseIR
函数中处理这些操作了。首先检查游戏者代码是否位于裁判与游戏者的分界线的上面:
if (ret[0] >= playerLine) {
因为您处于裁判区域,因此看一下返回代码。对于升级,如果游戏者的级别没有超出最大值,对它们进行升级并播放叮当音。
清单 17. 如果游戏者未超出对高级别,对其进行升级并播放一个叮当音
if (ret[1] == refPromote) { // Promote if (myLevel < maxLevel) { Serial.println("PROMOTED!"); myLevel++; playTone(900, 50); playTone(1800, 50); playTone(2700, 50); }
对于降级,如果游戏者的级别未到达最低级别,则对其进行降级并播放一个不同的叮当音。
清单 18. 如果游戏者的级别未到达最低级别,对其进行降级并播放一个不同的叮当音
} else if (ret[1] == refDemote) { // demote if (myLevel > minLevel) { Serial.println("DEMOTED!"); myLevel--; } playTone(2700, 50); playTone(1800, 50); playTone(900, 50);对于弹药重设,将游戏者的射击次数设置到最大值并播放一个叮当音。 清单 19. 在进行弹药设置时,将游戏者的射击自处设置到最大值并播放一个叮当音
} else if (ret[1] == refReset) { Serial.println("AMMO RESET!"); myShots = maxShots; playTone(900, 50); playTone(450, 50); playTone(900, 50); playTone(450, 50); playTone(900, 50); playTone(450, 50);最后,如果游戏者被复活,重设他的射击次数、命中次数以及级别,并播放一个悦耳的音调。
清单 20. 如果游戏者被复活,重设射击次数、命中次数和级别,并播放一个音调
} else if (ret[1] == refRevive) { Serial.println("REVIVED!"); myShots = maxShots; myHits = maxHits; myLevel = 1; playTone(900, 50); playTone(1800, 50); playTone(900, 50); playTone(1800, 50); playTone(900, 50); playTone(800, 50); }游戏者操作您已经设置了所有的裁判操作。但是游戏者操作如何设置呢?它们更容易处理,因为它们的种类更少。对于成功答复,应该播放一个表示成功的声音。
清单 21. 成功答复
} else { if (ret[1] == replySucc) { playTone(9000, 50); playTone(450, 50); playTone(9000, 50); Serial.println("SUCCESS!");对于失败答复,应该播放一个表示未成功的声音。
清单 22. 失败答复
} else if (ret[1] == replyFail) { playTone(450, 50); playTone(9000, 50); playTone(450, 50); Serial.println("FAILED!"); }在您尝试射击时,命中答复也将播放一个声音,提醒您刚才射击您的游戏者比您更出色。清单 23. 命中答复 if (ret[1] <= maxLevel && ret[1] >= myLevel && myHits > 0) { Serial.println("HIT!"); myHits--; playTone(9000, 50); playTone(900, 50); playTone(9000, 50); playTone(900, 50); } }
对于本例,不要过分追求音调的完美,它们主要用于测试和充当占位符。在您拥有可行的发射器可??测试时,您可以调整音调,直到您它们感到满意为止。
至于现在,如果您的遥控装置返回了可用的测试代码,则可以更改变量的值,尝试用于解码红外线信号的代码的不同部分。拥有经过完整编码的接收器固然不错,但是拥有一个发射器实际上会更好,难道不是吗?第 3 部分将讨论发射器。
结束语
在本教程中,您构建并测试了 'Duino Tag 枪的一个接收器。您对代码进行了处理并开始了游戏的构建。
您现在已准备好构建发射器了。连接好元件,为阅读第 3 部分做好准备。在第 3 部分中,您将非常快速地构建发射器,然后了解如何扩展发射器的功能范围,并了解一些封装选项。您将探索改进系统的方式,以及未来 'Duino Tag 项目的可能实现方式。如果拥有了 教程:构建基于 Arduino 的激光游戏,第 1 部分:Arduino 基础知识 中列出的所有元件,那么您的进展情况一定不错。对于第 3 部分,请确保具备一个单击瞬态开关、一个红外线 LED、一个 82 ohm 电阻以及一个 1k ohm 电阻。其他需要的元件您已经拥有了。