这次将为大家介绍一个使用Arduino制作的独特电子作品项目,将会分【前篇】和【后篇】两部分进行介绍。为我们介绍这个非常有趣的电子制作项目的是平原真先生,他是一位以事物之间的关系为主题进行探索的艺术家。平原先生同时也是大阪艺术大学的副教授,迄今为止,他使用计算机和电子器件制作了很多媒体艺术作品。在Device
Plus上,平原先生也发表了一些极具意义的电子作品,例如“用Arduino制作的太阳能电池板供电数字生态箱”和“用Arduino和TOF距离传感器制作甜甜圈播放器”。在后篇中,我们将会见证数字滚球迷宫是如何完成的。
前言
大家好!我是平原。本文是“用Arduino和加速度传感器制作数字滚球迷宫”的后篇。在前篇中,我们组装了电子电路,写入了Arduino程序,并确认了电子部件的工作情况。接下来,在本文中我们将完成实物迷宫的制作,并使用游戏引擎Unity实际制作游戏。请大家也一起参与挑战!
本项目的组成
本项目共分为六个部分。
在前篇中,我们完成了作品规划、电子电路和Arduino程序。
在后篇中,我们将按照以下1到3的步骤进行制作。
● 制作迷宫外壳
● 创建迷宫的3D模型
● Unity程序
1. 制作迷宫外壳
迷宫的设计是决定游戏可玩性的重要因素。我们需要一边想象小球如何滚动一边制作迷宫。
我们按照以下步骤制作迷宫外壳:
1.设计盒子
2.设计迷宫
3.切割材料
4.组装盒子和迷宫
5.固定电子部件
6.粘贴标记
步骤1:设计盒子
盒子由5块椴木单板粘合而成。盒子的尺寸为宽145mm、宽145mm、高70mm,以便里面有足够的空间放置部件。板的厚度为4mm。为了使角部贴合在一起,设计时要考虑到厚度。在背面的板上打一个可以让USB数据线穿过的孔。请使用Illustrator设计要裁剪的形状。请参阅分发文件CutBox.ai。
步骤2:设计迷宫
在这个迷宫中,将正方形的盘面分成10个方格×10个方格,并设置围墙。起点在左下角,终点在右上角。我创建了3种不同难度级别的迷宫。这里创建的迷宫数据不仅会用作激光加工机的切割数据,还会用于创建要在Unity中使用的3D模型。
在Illustrator中,将正方形分隔成10个方格 x 10个方格并绘制之间围墙。使用网格功能会很方便。需要一边想象小球的滚动一边思考迷宫的设计。
如果从头开始创建比较麻烦,可以参考下面的迷宫自动生成工具。
>>自动创建迷宫
完成后,调整线条的粗细,勾勒出整体轮廓,然后调整其大小以适应145mm x 145mm的盒子。
迷宫的地板与盒子底部的形状相同。将墙壁与地板粘在一起形成迷宫。然后雕刻起点(S)和终点(G)。
创建具有3个难度级别的迷宫。1级没有特别的关卡(左)。2级的起点和终点之间没有连接,无法直接到达终点,小球在途中掉入一个洞中,就会穿越到另一个洞(中间)。3级的中心被挖空成圆形,这部分会像旋转门一样移动(右)。请参阅分发文件中的CutMaze.ai。
步骤3:切割材料
请使用创建的数据用激光加工机对椴木单板进行切割。椴木单板的厚度为4mm,请根据要使用的设备进行切割所需的设置。
切割后的迷宫。迷宫的墙壁很薄,应该小心处理,以免它们断裂。
步骤4:组装盒子和迷宫
用木材专用胶粘贴盒子。这种胶即使用量很少也可以粘得很牢固,不要用量太大哦。
以同样的方式将迷宫的地板和墙壁粘在一起。请用牙签等在迷宫的墙壁背面粘上少量的胶,然后根据地板和墙壁的位置进行粘贴。注意不要混淆表面和背面,以及起点和终点的位置。
步骤5:固定电子部件
使用3D打印机制作一个面包板底座,将底部抬高28mm,使反射式光电传感器处于接触盖子背面的高度。请使用分发文件BreadboadHolder.stl。如果您正好有一个高度合适的盒子,也可以用它来代替。
将面包板嵌入支架中,并用双面胶将支架和Arduino UNO固定在盒子上。
步骤6:粘贴标记
制作标记用于反射式光电传感器的读取。请在不干胶纸上打印两个30mm x 30mm正方形并列的矩形。由于反射式光电传感器对喷墨打印机的墨水没有反应,所以请用黑色的笔涂抹为黑白、白黑、黑黑。
将标记粘贴在盖子背面的反射式光电传感器的位置。下面的照片中,从左到右的难度等级依次为1级、2级和3级。1级难度的迷宫,反射式光电传感器1为黑色,反射式光电传感器2为白色。难度的差异是通过白色和黑色的组合来判断的。
2. 创建迷宫的3D模型
我们按照以下步骤创建迷宫的3D模型:
1.从Illustrator导出SVG文件
2.将SVG文件导入Fusion360
3.利用推挤功能创建立体模型
4.输出OBJ文件
步骤1:从Illustrator导出SVG文件
将Illustrator中的迷宫数据导出为SVG文件。移动在“1.制作迷宫外壳”的步骤2中创建的迷宫轮廓数据,使中心为0,0。从Illustrator菜单中选择[文件] > [另存为],然后选择SVG作为文件类型。请创建从1级到3级共3个文件。
步骤2:将SVG文件导入Fusion360
将步骤1中导出的SVG文件导入到3D CAD软件Fusion 360中。Fusion360的安装方法请参考其官网。
请从Fusio 360的菜单中,选择 [插入] > [插入SVG], 将步骤1中导出的SVG文件导入Fusio 360。导入的形状是草图。
步骤3:通过推挤功能创建立体模型
推挤迷宫的形状,使其成为立体模型。请将整个草图向下推挤5mm以使其成为地板。接下来,请仅将墙壁部分向上推出5mm。
在2级难度迷宫的地板上打两个用来穿越的孔,孔径约为Φ8mm。
3级难度的迷宫,在中央挖空Φ48mm的圆圈,制作两层地板。
步骤4:输出OBJ文件
需要从Fusion 360导出OBJ文件。请从Fusion 360的菜单中选择[文件] > [导出] ,然后将文件导出为OBJ文件。由于该过程需要在云端完成,因此需要一些时间。完成后,该文件将会进入下载文件夹中。分发文件MazeLv1.obj、MazeLv2.obj、MazeLv3-A.obj、MazeLv3-B.obj是完成后的数据。
3. Unity程序
1.终于到了Unity中的程序部分了。请按照以下步骤操作:
2.在Unity中创建一个新项目
3.加载3D模型
4.配置小球
5.运用物理运算
6.安装Serial Port Utility Pro
7.主程序
8.设置终点
9.2级关卡
10.3级关卡
11.切换迷宫
步骤1:在Unity中创建一个新项目
本次使用的Unity版本为2019.4.8f1 Personal。Unity的安装方法请参考其官网。
请用3D模式创建一个新的Unity项目。
步骤2:加载3D模型
加载迷宫的3D模型。将Fusio 360导出的OBJ文件拖放到Unity项目面板中的Asset文件夹中。
创建一个空的GameObject来挂载迷宫。请从Unity菜单中,选择[Game Objects(游戏对象)] > [Create Empty(创建空对象)]。 将新建的GameObject重命名为“MazeObject”,并将Transform的位置设置为0。
将3D模型配置于stage上。将刚刚加载的地图的3D模型“MazeLv1”拖放到层级面板的“MazeObject”中。如果直接这样用,大小和旋转会很奇怪,因此需要进行调整以使其处于正确的位置。请通过检视面板的Transform,将旋转设置为X270、Y180,将缩放设置为0.2。以同样的方式设置2级的3D模型。3级由“MazeLV3-A”和“MazeLV3-B”两个3D模型组成,因此请创建一个名为“MazeLv3”的空GameObject,并将这两个3D模型放入其中。
需要更改每个级别的颜色。请从Unity菜单中选择 [Asset] > [Create] > [Material],创建三个材质。名称分别是“MatLv1”、“MatLv2”和“MatLv3”。在材质的Inspector面板的反照率(Albedo)项目中更改颜色。我们将“MatLv1”设置为浅蓝色,“MatLv2”设置为黄色,“MatLv3”设置为橙色。请将创建的材质拖放到“MazeLv1”、“MazeLv2”、“MazeLv3-A”、“MazeLv3-B”中的网格上。可以看到迷宫已被着色。
当前,三个迷宫是重叠显示的,但“MazeLv2”和“MazeLv3”处于初始状态,可以将它们禁用,在Inspector面板中名称的左侧取消选中即可。
步骤3:配置小球
将小球配置在起点。请从Unity菜单中,选择[Game Objects(游戏对象)] > [3D Objects(3D对象)] > [Sphere(球体)]。对象的名称是“Sphere”。更改为从正上方向下看的视角,更容易对齐位置。我想让小球从空中落下,所以将y设置为5.0左右。
步骤4:运用物理运算
给3D模型赋予判断碰撞的功能。请在“MazeObject”中选择“MazeLv1”,在Unity菜单中选择[Component(组件)] > [Physics(物理)] > [Rigid Body(刚体)]。从刚体的设置项目中取消选中“Use Gravity(使用重力)”并选中“Is Kinematic(开启动力学)”。碰撞检测项请选择“连续且动态地进行碰撞检测”。需要减少墙壁的摩擦力。然后请从Unity菜单中选择[Component(组件)] > [Physics(物理)] > [Mesh Collider(网格碰撞器)]。网格碰撞器设置项目中的网格,请选择“MazeLv1”中包含的网格。
将刚体和网格碰撞器附加给“MazeLv2”、“MazeLv3-A”和“MazeLv3-B”并进行相同的设置。
最后,将刚体附加给小球。选择“Sphere”,然后从Unity菜单中选择 [Component(组件)] > [Physics(物理)]> [Rigid Body(刚体)]。我希望“Sphere(球体)”根据重力移动,因此仍保持选中“UseGravity(使用重力)”。
如果在这个阶段执行程序,小球应该会落下并碰撞迷宫。您可以试试看这部分的运行情况。
步骤5:安装Serial Port Utility Pro
Unity与Arduino之间的通信仅通过标准功能也能实现,但这次我们将使用名为“Serial Port Utility Pro (SPUP)” 的工具。使用SPUP进行通信时,您可以不必担心运行环境的差异。这个工具在资源商店的售价为79美元,不过您可以从其官网下载试用版。试用版需要在通信量达到一定程度时重新启动Unity。我们可以在开发过程中使用试用版,在想长期使用时购买它。
>> Serial Port Utility Pro
下载试用版后,请解压缩ZIP文件。从Unity菜单中,请选择[Assets(资产)] > [Import Package(导入包)] > [Custom Package(自定义包)] ,然后选择ZIP文件中包含的“spup_v2_free.unitypackage”。在接下来出现的窗口中,请选择并导入包中的所有文件。
然后,创建一个空的GameObject,并附加SPUP组件。请从Unity菜单中,选择[Game Objects(游戏对象)] > [Create Empty(创建空对象)]。 将新创建的游戏对象重命名为“SPUPObject”。选择“SPUPObject”后,请从Unity菜单中选择[Component] > [SerialPort] > [SerialPort Utility Pro],添加组件。
设置为连接Arduino UNO。使用USB数据线将Arduino UNO连接到您的电脑,在已连接状态下按下“SerialPort Utility Pro”上显示“Show the devices connected to this PC”的黄色按钮。如果您按下在新窗口中打开的按钮,将会自动输入VenderID、ProductID和Serial Number。需要使通信速度与Arduino程序中设置的值相匹配。请从BaudRate的下拉菜单中选择57600bps。需要设置接收来自Arduino的数据的方法。请将“Read Protocol”设置为“Line Feed Data To String”。
步骤6:主程序
创建一个用SPUP执行串行通信并根据加速度传感器模块的值移动迷宫的程序。这是本项目的核心程序。
请从Unity菜单中选择[Assets] > [Create] > [C# Script],创建一个C#文件,并将其命名为“MazeScript”。双击C#文件打开编辑器,输入以下代码。
MazeScript.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using System; public class MazeScript : MonoBehaviour { public SerialPortUtility.SerialPortUtilityPro serialPort = null; Vector3 accSensor;//加速度传感器 int photoRef1 = 0;//反射式光电传感器1 int photoRef2 = 0;//反射式光电传感器2 int threshold = 700;//反射式光电传感器阈值 int mazeType = 0;//0:无 public GameObject mazeLv1;//迷宫Lv1的游戏对象 public GameObject mazeLv2;//迷宫Lv2的游戏对象 public GameObject mazeLv3;//迷宫Lv3的游戏对象 public GameObject ball;//小球的游戏对象 public GameObject goalText;//显示终点 public Vector3 cameraPosOffset;//小球和相机的距离 Vector3 ballDefaultPos; void Start () { ballDefaultPos = ball.transform.position;//记录小球的初始位置 reset(); } void Update () { //摄像头 Camera.main.transform.position = ball.transform.position + cameraPosOffset; Camera.main.transform.LookAt(ball.transform.position); // 旋转 Vector3 rotationEuler = new Vector3(Mathf.Atan2(accSensor.x, accSensor.z) / Mathf.PI *180, 0, Mathf.Atan2(accSensor.y, accSensor.z) / Mathf.PI *180); Quaternion rotation = Quaternion.Euler(rotationEuler); gameObject.transform.rotation = Quaternion.Lerp(gameObject.transform.rotation,rotation, 0.1f); // 复位 if(Input.GetKeyDown(KeyCode.Space)) { reset(); } } public void reset() { setMaze();//迷宫的初始化 ball.transform.position = ballDefaultPos; ball.GetComponent<Rigidbody>().velocity = Vector3.zero; ball.GetComponent<Rigidbody>().angularVelocity = Vector3.zero; goalText.SetActive(false); stopVibe();//振动电机停止 } public void setMaze() { Debug.Log("photoRef1:" + photoRef1 +" photoRef2:" + photoRef2); if( (photoRef1 == 0) && (photoRef2 == 0)) { //如果值为 0,则不执行任何操作 return; }else if( (photoRef1 < threshold) && (photoRef2 > threshold) ) { //1级 mazeType = 1; mazeLv1.SetActive(true); mazeLv2.SetActive(false); mazeLv3.SetActive(false); }else if( (photoRef1 > threshold) && (photoRef2 < threshold) ) { //2级 mazeType = 2; mazeLv1.SetActive(false); mazeLv2.SetActive(true); mazeLv3.SetActive(false); }else if( (photoRef1 < threshold) && (photoRef2 < threshold) ) { //3级 mazeType = 3; mazeLv1.SetActive(false); mazeLv2.SetActive(false); mazeLv3.SetActive(true); } } public void ReadComplete(object data) { var text = data as List<string>; if(text.Count != 5) return; accSensor = new Vector3( float.Parse(text[0]), float.Parse(text[1]), float.Parse(text[2])); photoRef1 = int.Parse(text[3]); photoRef2 = int.Parse(text[4]); } public void startVibe() { //使振动电机运转的命令 serialPort.WriteCRLF("start"); } public void stopVibe() { //使振动电机停止的命令 serialPort.WriteCRLF("stop"); } }
请将“MazeScript”拖放到“MazeObject”。
从“MazeObject”的检视面板中设置“MazeScript”。将“SPUPObject”拖放到Serial Port(串行端口)。将“MazeObject”中的“MazeLv1、Lv2、Lv3”分别拖放到“Maze Lv1、Lv2、Lv3”项目中。将“Sphere”拖放到“Ball”上。Goal Text(终点文本)将在稍后设置,因此暂时将其留空。Camera Pos Offset用来设置摄像头和小球保持多少距离。请输入x 0、y 5、z -3。
在“SPUPObject”的检视面板的SerialPortUtilityPro设置中,有一个叫做“Read Complete Event Object(Object)”的项目。该项目用来指定接收到串行通信时调用的函数。这里为“MazeScript”的 ReadComplete函数。
请单击项目右下角的加号图标。将Maze Object(迷宫对象)拖放到“无(对象)”字段。单击右上角的Nofunction下拉菜单,然后选择[MazeScript] > [ReadComplete]。
如果在这里执行程序,应该可以收到来自Arduino的加速度传感器模块数据,并且迷宫应该可以动了。那就试试这部分程序的运行情况吧。然后,该休息一下了!
步骤7:设置终点
我们需要创建一个当成功到达终点时做出反应的机制。在终点设置一个用于判断碰触终点的透明对象(触发器),当小球碰到触发器时,会显示文本对象。
请从Unity菜单中选择 [Game Object] > [3D Object] > [Cube],创建一个立方体。将其重命名为“GoalTrigger”,并将其放置在层级面板的“MazeObject”中。移动到地图右上角的终点附近,通过缩放来调整大小。请取消选中“Mesh Renderer(网格渲染器)”,并将其设为隐藏,选中“BoxClider”的“设为触发器”。
然后需要配置文本对象。请从Unity菜单中选择[Game Object] > [3D Object] > [3DText]。将其重命名为“GoalText”,并将其放置在层级面板的“MazeObject”中。当使Transform的X旋转90度时,应该可以看到“HelloWorld”字样。在检视面板的文本项目中输入“GOAL!!!”。改变Transform的位置,将其移动到终点附近,将缩放设置为 0.2,并将字体大小设置为 100。
将用于判定到达终点的程序附加到“GoalObject”。
请从Unity菜单中选择[Assets] > [Create] > [C# Script(C#脚本)],创建一个C#文件,并将其命名为“GoalScript”。双击C#文件打开编辑器,输入以下代码。
GoalScript.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GoalScript : MonoBehaviour { public GameObject goalText; MazeScript MazeScript; void Start() { MazeScript = transform.root.GetComponent<MazeScript>(); } void OnTriggerEnter(Collider other) { //接触时调用的事件 if(other.name == "Sphere"){//如果名字是Shpere, goalText.SetActive(true);//显示终点文本 MazeScript.startVibe();//发送使振动电机运转的命令 } } }
请将“GoalScript”拖放到“GoalTrigger”,将“GoalText”拖放到“Goal Script”的“Goal Text”项目中。
从“MazeObject”的检视面板中,将“GoalText”拖放到“MazeScript”的“GoalText”栏中。
如果在这里执行程序,当到达终点时会显示“GOAL!!!”,并且振动电机应该会振动。要停止振动时,请按空格键重置游戏。终于像游戏样了!
步骤8:2级关卡
需要给2级难度的迷宫添加扭曲功能。将触发器配置在地板开孔的下方,然后将小球移动到扭曲对象的坐标处。
首先,请禁用“MazeLv1”并激活“MazeLv2”。就像设置到达终点判定机制一样,制作一个立方体,将其放在地板开孔的下方,然后选中“BoxClider”的“设为触发器”。对象的名称是“WarpIn1”和“WarpIn2”。
接下来,需要配置对象,用来指定要扭曲的目标。请从Unity的菜单中选择[Game Object] > [Create Empty]。对象的名称是“WarpOut1”和“WarpOut2”。将其配置在另一个孔的附近。
请将“WarpIn1”、“WarpIn2”、“WarpOut1”和“WarpOut2”放入层级面板的“MazeLv2”中。
需要编写一个脚本来控制扭曲。请创建C#文件并在编辑器中输入以下代码:
WarpScript.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class WarpScript : MonoBehaviour { public GameObject warpOut; void OnTriggerEnter(Collider other) { if(other.name == "Sphere"){ other.transform.position = warpOut.transform.position; } } }
请将“WarpScript.cs”附加到“WarpIn1”和“WarpIn2”中,并将“WarpOut1”和“WarpOut2”拖放到“Warip Out”项。
当执行这部分程序时,小球掉进洞里,会穿越到另一个洞附近。
步骤9:3级关卡
3级难度的迷宫由两部分组成。迷宫外侧为“MazeLv3-A”,中间圆形挖空部分为“MazeLv3-B”。需要使中心部分缓慢旋转。请禁用“MazeLv2”并激活“MaseLv3”。然后编写使对象旋转的代码。请创建一个C#文件并输入以下代码:
RotateScript.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class RotateScript : MonoBehaviour { void Update() { transform.Rotate(new Vector3(0.0f, 0.0f, 0.05f)); } }
将“RotateScript.cs”附加到“MazeLv3-B”后,中心部分将会旋转。碰撞检测功能也会正常工作。
步骤10:切换迷宫
终于完成了这三个迷宫。最后,我们需要进行一些设置,使替换实物迷宫时3D模型迷宫也会切换。请在1级实物迷宫放置在盒子上的状态下,运行Unity项目。当按下空格键时,控制面板上将会显示如下所示的反射式光电传感器反应值。
photoRef1:451 photoRef2:971
在1级实物迷宫的背面,在反射式光电传感器1的位置贴有黑色标记,在反射式光电传感器2的位置贴有白色标记。也就是说,反应值为黑色451,白色971。根据反射式光电传感器的高度,该值可能会有所不同,但存在“黑色<白色”的关系。
1级是黑白,2级是白黑,3级是黑黑,所以需要确定白色和黑色的阈值,当满足每个条件时就切换迷宫。
请看“MazeScript.cs”的第13行。在这里设置阈值。我们将阈值设为700,该值大致位于白色(451)和黑色(971)的中间。
int threshold = 700;//反射式光电传感器阈值
在按下空格键时调用的setMaze()中,处理不同的情况。
public void setMaze() { Debug.Log("photoRef1:" + photoRef1 +" photoRef2:" + photoRef2); if( (photoRef1 == 0) && (photoRef2 == 0)) { //如果值为 0,则不执行任何操作 return; }else if( (photoRef1 < threshold) && (photoRef2 > threshold) ) { //1级 mazeType = 1; mazeLv1.SetActive(true); mazeLv2.SetActive(false); mazeLv3.SetActive(false); }else if( (photoRef1 > threshold) && (photoRef2 < threshold) ) { //2级 mazeType = 2; mazeLv1.SetActive(false); mazeLv2.SetActive(true); mazeLv3.SetActive(false); }else if( (photoRef1 < threshold) && (photoRef2 < threshold) ) { //3级 mazeType = 3; mazeLv1.SetActive(false); mazeLv2.SetActive(false); mazeLv3.SetActive(true); } }
已完成的Unity项目请参考分发文件中的“UnityApp”。但是,免费版的SerialPortUtilityPro无法分发文件,因此删除了SerialPortUtilityPro包和“SPUPObject”。请参阅步骤5进行设置。
4. 结束语
大家辛苦了!这个“数字滚球迷宫”怎么样?我认为这是一个使用Arduino和传感器将实物和数字内容混合在一起的有趣作品。
在本文中,我们制作了三个stage,还制作了要穿越的洞和旋转的墙壁。此外,还可以根据创意和想法创建各种stage,比如貌似有粘性而缓慢移动的地板、进入内部会弹出到空中的大炮、以及追球的对手角色。请大家勇敢地尝试制作原创stage作品!
来源:ROHM R课堂
作者:平原真——以事物之间的关系为主题进行探索的艺术家。他使用电脑和电子设备创作了很多媒体艺术作品,近年来,主要使用木材和石头等天然素材创作三维立体作品。日本长冈造形大学副教授。Oggoroggo Products的代表。著有《Arduino实战! 通过电子制作让梦想成真》(OHMSHA)。https://makotohirahara.com/