The Tangible Level Editor is a peripheral for a platformer-style game where the configuration of specially-designed blocks in the real world are mapped in the virtual world allowing players to create levels by arranging said blocks. It is a device designed and built by me, Elliot Brodzki and Richard Pianka during our undergraduate studies at Worcester Polytechnic Institute.
Motivation
It was designed to be a tangible substitute to level creation using standard controllers. Typically a virtual world is created by an artist with a keyboard and mouse. Such peripheral devices do not resonate with very young users hence the interface itself is occluding a child's access to the creative potential of building virtual environments. Most children are acquainted with toy blocks at an early age so it makes sense that the interfaces should be design with the familiarity of toy blocks in mind. The Tangible Level Editor is this interface.Implementation
The Tangible Level Editor is a proof of concept input device that uses an Arduino microcontroller, a resistor network, a multiplexer, and auxiliary devices such as the speaker and joystick.
Top-Level Diagram
The diagram below shows a top-level view of the system. Note that the speaker and joystick, neither of which are shown, are actually connected to the microcontroller.
Module A - Blocks & Workbench
Notice how the blocks and the workbench form a resistor network. See how each column of blocks form a series resistance circuit. All the resistors have the same value and if we provide a source voltage of a known magnitude, we can determine the voltage across the last resistor, the resistor embedded in the workbench, corresponding to the number of blocks in that column by using Ohm's Law. In other words, there is a one-to-one mapping of the number of blocks in a column and the voltage value in the embedded resistor at that column.
Module B - Multiplexor
This means that each column's embedded resistor needs to be constantly measured for its voltage value. Since there are more resistors to measure than there are pins on the microcontroller to measure them, the solution is to multiplex the many resistors with one pin. We used an Analog Devices ADG line multiplexor to multiplex the eight resistors to one Analog-to-Digital pin in the Arduino.
Module C - Microcontroller
The Arduino sends out three bit signals to the multiplexor to tell which eight resistor it needs to connect to in sequential order as you can see from the code snippet below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | using UnityEngine; using System.Collections; public class LevelCreationScript: MonoBehaviour { private Controllers controllerScript; private int[] levelState; private bool levelChanged = false; public Transform[] platforms; void Start() { controllerScript = (Controllers) gameObject.GetComponent("Controllers"); levelState = new int[8]; for (int i = 0; i < 8; i++) levelState[i] = 0; } void FixedUpdate() { for (int i = 0; i < 8; i++) { if (controllerScript.pos[i] != levelState[i]) levelChanged = true; } if (levelChanged) { for (int i = 0; i < 8; i++) { if (controllerScript.pos[i] != levelState[i]) { GameObject col = GameObject.FindWithTag("col" + i); if (col != null) { if (controllerScript.pos[i] != 0) { GameObject plat = GameObject.Instantiate(platforms[controllerScript.pos[i] - 1].gameObject, col.transform.position, Quaternion.identity) as GameObject; plat.transform.parent = col.transform; plat.transform.Translate(0, (controllerScript.pos[i] - 1) * 10 + 10, 0); } else col.transform.DetachChildren(); } } } levelChanged = false; for (int k = 0; k < 8; k++) levelState[k] = controllerScript.pos[k]; } } } |
Also, the voltage source of the series circuit in each column will come from the Arduino's 5V power pin.
Module D - Virtual Environment
We used Unity to create the virtual environment. Below shows the script that takes the data transmitted from the Arduino to the PC and converts it into elevated platforms rendered in the game engine.
An example of a created level is shown in the screen capture below.
The complete script including those used to animate and control the virtual avatar can be found in the appendix.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | using UnityEngine; using System.Collections; public class LevelCreationScript: MonoBehaviour { private Controllers controllerScript; private int[] levelState; private bool levelChanged = false; public Transform[] platforms; void Start() { controllerScript = (Controllers) gameObject.GetComponent("Controllers"); levelState = new int[8]; for (int i = 0; i < 8; i++) levelState[i] = 0; } void FixedUpdate() { for (int i = 0; i < 8; i++) { if (controllerScript.pos[i] != levelState[i]) levelChanged = true; } if (levelChanged) { for (int i = 0; i < 8; i++) { if (controllerScript.pos[i] != levelState[i]) { GameObject col = GameObject.FindWithTag("col" + i); if (col != null) { if (controllerScript.pos[i] != 0) { GameObject plat = GameObject.Instantiate(platforms[controllerScript.pos[i] - 1].gameObject, col.transform.position, Quaternion.identity) as GameObject; plat.transform.parent = col.transform; plat.transform.Translate(0, (controllerScript.pos[i] - 1) * 10 + 10, 0); } else col.transform.DetachChildren(); } } } levelChanged = false; for (int k = 0; k < 8; k++) levelState[k] = controllerScript.pos[k]; } } } |
An example of a created level is shown in the screen capture below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | using UnityEngine; using System.Collections; using System.IO.Ports; public class WinScript : MonoBehaviour { public int forceRange = 1; public int numObjects = 100; public Transform spawn; private Controllers controllerScript; // Use this for initialization void Start () { controllerScript = spawn.GetComponent<Controllers>(); } void OnTriggerEnter(Collider other) { if(other.tag == "Player") { Vector3 pos = other.transform.position; controllerScript.sp.WriteLine("0"); Destroy(other.gameObject); for(int i = 0; i < numObjects; i++) { GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Cube); sphere.transform.localScale = new Vector3(5, 5, 5); sphere.transform.position = pos; sphere.AddComponent("Rigidbody"); int force1 = Random.Range(-forceRange, forceRange); int force2 = Random.Range(-forceRange, forceRange); int force3 = Random.Range(-forceRange, forceRange); sphere.transform.rigidbody.AddForce(force1, force2, force3); } } } } |
The complete script including those used to animate and control the virtual avatar can be found in the appendix.
Module E - Speaker & Joystick
We wanted to showcase the power of the Arduino as well so we had speakers and a joystick wired to it. After the level has been created, when the virtual avatar reach the other side of the level, a melody gets played through the speaker. The Arduino code snippet below shows how we did this.
The joystick is a simple mechanical stick that adjusts the resistance of its internal potentiometer allowing shifts in the stick to produce different voltage levels that can be read by the Arduino. Below is a photo of the joystick we made and the Arduino code to use it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // from example code void playTone(int tone, int duration) { for (long i = 0; i < duration * 1000L; i += tone * 2) { digitalWrite(SPEAKER, HIGH); delayMicroseconds(tone); digitalWrite(SPEAKER, LOW); delayMicroseconds(tone); } } // from example code void playNote(char note, int duration) { char names[] = { 'c', 'd', 'e', 'f', 'g', 'a', 'b', 'C' }; int tones[] = { 1915, 1700, 1519, 1432, 1275, 1136, 1014, 956 }; // play the tone corresponding to the note name for (int i = 0; i < 8; i++) { if (names[i] == note) { playTone(tones[i], duration); } } } if (Serial.available() > 0) { byte input = Serial.read(); // hit a field if (input == '0') { playNote('g', 30); delay(30); playNote('b', 30); delay(30); playNote('a', 30); delay(30); playNote('a', 30); delay(30); playNote('C', 30); } else if (input == '1') { playNote('c', 50); delay(50); playNote('c', 50); } else if (input == '2') { playNote('b', 30); } } |
The joystick is a simple mechanical stick that adjusts the resistance of its internal potentiometer allowing shifts in the stick to produce different voltage levels that can be read by the Arduino. Below is a photo of the joystick we made and the Arduino code to use it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | int val = analogRead(1); if (val < 350) { Serial.print('+'); } else if (val > 750) { Serial.print('-'); } else { Serial.print('0'); } |
Module Amalgamation
Connecting all the modules together produces the completed Tangible Level Editor. Pictures of our finished prototype are shown below.Video Demonstration
Here we demonstrate the final prototype and provide only the pithy details on how it all works.Conclusion
After using the Tangible Level Editor, building levels with a tangible interface did very much feel more intuitive than a traditional mouse and keyboard. As adults we were able to understand how the blocks were a metaphor for the virtual environment; however, we would need to have children to play it for themselves to see if they too are able to comprehend that same idea.
This article was first published in 2009.
I thinks you need this post to learn electronics
ReplyDelete