The "Chimera" project draws inspiration from the mythical creature, the Chimera. In mythology, the Chimera is a creature composed of body parts from different animals— for example, a lion’s body, a goat’s head on its back, and a snake’s tail. This hybrid form is often used metaphorically to represent things that are intangible, elusive, or seemingly unattainable. This concept became the core idea for my project. My goal was for the audience to trigger different mechanisms by solving a series of riddles, unlocking layers of boxes, and ultimately reaching the heart of the project. The design of the box and the way in which the riddles were presented became the key aspects of how I envisioned constructing the experience.
Initially, I had planned to design boxes with multiple layers, so that a large box would contain smaller ones, creating a nesting effect. However, due to time constraints and other practical factors, I was unable to execute this plan. Instead, I opted for a single large box with various mechanisms, where the user would need to trigger them in a specific order to unlock and open the box.
Since the final project for the IMA course required integrating Arduino with Processing, I decided to incorporate a visual puzzle element into the project. I chose an image of the Chimera from the internet to serve as the final image for my puzzle. This puzzle became the central mechanism of the project: the user would control the puzzle pieces with the mouse to complete the puzzle, which in turn would drive two servos to open the box lid. To bring this vision to life, I borrowed a small screen from IMA Studio, which connects to the computer via HDMI. This setup allowed me to create the puzzle in Processing while also integrating it with the hardware.
During the user testing session, I presented the puzzle module combined with the hardware to the audience. While the puzzle received positive feedback, there were some criticisms as well. Some viewers felt that the puzzle on the small screen was visually unfriendly, and the cardboard box appeared too simple. Additionally, since I had only designed a single layer of mechanisms, the interaction was relatively basic. Based on this feedback, I decided to refine and adjust the project. Following my teacher's suggestion, I replaced the cardboard structure with a wooden box, cut using a laser cutter, which significantly improved the overall strength and durability of the design.
I also redesigned the mechanism, adding an additional layer of mechanisms. I engraved the riddle onto hard cardboard using the laser cutter, and covered the screen with two pieces of hardboard, creating a stage curtain effect. Two servos controlled the two pieces of cardboard, allowing them to open and close. Above the setup, I added three buttons in different colors— yellow, blue, and white— each representing the possible answers to the riddle. If the audience solved the riddle, they would know which button to press to trigger the mechanism and move to the next step.
With the added mechanism, I received further feedback from the audience, particularly regarding the lack of clear guidance for the users. To address this, I incorporated simple text instructions to guide the audience through the process of opening the puzzle box step by step, as I had intended. In the final IMA project presentation, these changes and adjustments proved successful. They helped guide and inspire the audience, while the various riddles and mechanisms added an element of fun and engagement, encouraging the audience to immerse themselves more enthusiastically in the experience.
A box-within-box mechanism
A single box with different layers of triggers
The inspiration for this project primarily stems from a series of previous assignments we completed in the course. For example, the connection between the servo motors and the Processing platform comes from our earlier work on Serial Communication. In the project, once the player completes the puzzle on the Processing platform, it sends a command to the Arduino control board. Upon receiving the command, the control board drives two servos to rotate by specific angles. This communication between the two platforms is the key mechanism that drives the project. The button-controlled servo system is simpler, as it only requires triggering an input command to control the servo’s movement.
In summary, the hardware code and design for this project were inspired by a series of previous assignments we worked on. The most technically challenging aspect of the project was how to design and implement the puzzle-solving mechanism itself.
The puzzle I designed for this project includes serial communication between Arduino and the Processing platform. Players need to drag and place puzzle pieces using their mouse, and once the puzzle is completed, Processing sends a signal to the Arduino board, triggering two servos controlled by audio to rotate. The puzzle itself is built using a grid function, and the image used is of the Chimera, a mythical creature that I found online. The image is divided into smaller sections, creating a 3×4 grid of puzzle pieces. These pieces are managed using the column and rows functions, which are defined at the beginning of the code.
Once the image is loaded, each puzzle piece is extracted and stored in a Tile array. At the start of the game, the puzzle pieces are randomly placed using the random function, and their positions are recalculated to simulate a scrambled puzzle layout. In the setup() function, the puzzle piece size is calculated by dividing the image dimensions by the number of columns and rows, and each piece is controlled by a Tile object, which stores the position, size, and depth of the pieces. These pieces are arranged using the shuffleTiles() function, which ensures that they are randomly distributed across the screen and that no two pieces overlap.
In the draw() function, the puzzle continuously checks whether the player has completed it by verifying if each piece is in its correct position. Once the puzzle is finished, a "You Win!" message is displayed on the screen. At this point, the game sends a signal value (1) to the Arduino board via the sendSerialData() function. This signal marks the completion of the puzzle and triggers the servo motors to rotate, signaling the end of the game.
The user interaction in this project primarily involves controlling the mouse to grab and place puzzle pieces. I use the mousePressed() function to detect when the user clicks on and selects a puzzle piece. Once selected, the user can drag the piece around by moving the mouse. The mouseDragged() function continuously updates the position of the selected puzzle piece, making it follow the mouse. When the user releases the mouse button, the mouseReleased() function checks the new position of the puzzle piece. If the piece is near its correct position, it will snap into place.
During this process, the puzzle's completion state is repeatedly checked. When all pieces are correctly placed, the isCorrectlyPlaced value is set to true, indicating that the puzzle is complete. One of the most challenging aspects of this process is determining each piece's position and ensuring it "snaps" to the correct spot. We achieve this by checking the position of the dragged piece using selectedTile.x and selectedTile.y. The tileWidth and tileHeight functions define the size of each piece, so the calculation takes into account both the mouse position and the piece's size and grid placement.
Once the position is determined, the puzzle calculates the correct position for the piece by multiplying the column and row with tileWidth and tileHeight. I also set a boolean value called isInCorrectPosition to check whether the piece's current position matches its correct position. If the piece is in the correct location, it is marked as properly placed, and the piece is locked in place. At this point, the piece is no longer considered the selectedTile, and the interaction for that piece is complete.
The above describes how the whole puzzle part is accomplished and constructed on the Processing platform. As the whole mechanism works on the cooperation between Processing and Arduino, there are codes concerning the Arduino platform, also associated with the interaction process. On the Arduino platform, my main task is to control two servos by receiving values sent from the Processing platform. In the initial setup, the serial communication is configured with a baud rate of 9600, and the two servos are connected to pins 9 and 10. In the code, I define the two servos as myservo and myservo1.
When the platform receives the value processing_values[0] equal to 1, the code enters its first conditional block, indicating that both servos will move. Since I want to use the servos to control the opening and closing of a wooden box lid, although both servos rotate by the same angle, they should rotate in opposite directions. To achieve this, I added a for loop to repeat the motion. The first servo will rotate from 0 to 90 degrees, while the second servo, starting at 90 degrees, will rotate back to 0 degrees. With this setup, when the two servos are placed symmetrically, they can together control the opening and closing of the lid in sync.
If processing_values[0] is not equal to 1, the code enters the else block. In this case, the two servos rotate in the opposite directions, each by the same angle. The first servo will rotate from 90 to 0 degrees, and the second servo will rotate from 0 to 90 degrees. This behavior is also controlled by the conditional logic. By monitoring the values received from the Processing platform, I can easily control the servos’ movements. Once both servos complete their rotation, the break; statement stops the loop, breaking the servo movements and finishing the action.
Compared to the puzzle module, the code for the riddle button module is much simpler. The basic mechanism is that when the connected button is pressed, two servos rotate to specific positions and remain in those positions until the button is pressed again. The two servos are connected to pins 9 and 10, while the button is connected to pin 3. In the setup() function, the button pin is initialized as an input, and the two servos are set to their initial positions. The two servos are defined as myservo and myservo1, with the first servo starting at 0 degrees and the second servo starting at 100 degrees.
The main challenge I encountered while working on the riddle button module was ensuring that the servos would stop moving once they reached their new positions, rather than moving continuously when the button was pressed. This was solved by using a boolean flag. Once the servos are moved, the flag is set to true, which ensures that no further movement will occur until the button is pressed again. Even if the button is released, no other actions are performed. Additionally, I created the lastButtonState variable to track and detect when the button's state changes, which helps to ensure that the program reacts correctly to button presses.
In summary, the code for this module operates fairly simply and shares similarities with the servo control in the puzzle module. Both servos move to the same angle but in different directions. Once they reach their target positions, they remain locked in place until the next action is triggered by the user.
Before the user testing session, the external structure of the project was constructed using cardboard. This choice made it easier to visualize and build the overall design. However, after receiving feedback from players during the user testing session, it became clear that many people were dissatisfied with the use of cardboard as the external casing. Indeed, relying solely on cardboard gave the project a flimsy and rough appearance, and it was evident that cardboard lacked the strength to support the entire structure.
Following advice from my instructor, I decided to use the MakerCase software platform to design a custom box that would better suit the project's needs. After creating the box design on the platform, I could adjust the connections and export the design as an SVG file. This file was then used to laser-cut the parts for the box. In designing the wooden case, I had to ensure there was enough space for the computer’s ports and the HDMI display to be properly secured, so I took measurements to allow room for the cables and data connections. Fortunately, I had already planned for the openings in the box during the design phase, so the process of measurement during construction was straightforward. This allowed me to configure the case in a way that fit the overall circuit design.
To ensure the structure was more stable, I securely mounted the screen, breadboard, and Arduino microcontroller to prevent cables from coming loose. This approach not only strengthened the overall structure but also ensured that any future modifications could be made on a more solid foundation.
This project heavily involves the use of servos, and I employed a similar connection method for linking the servos to the drive mechanism throughout the design. Rather than using drive rods, I directly welded two long pins into the servo arms and then fixed the pins into the edges of the cardboard using nails. Additionally, I reinforced the connections with hot glue. For securing the servos, I used a method similar to that for the external structure, utilizing soft iron strips. The edges and servo bases were connected using hot glue as well.
Overall, the setup and operation of the servos were quite stable. However, during the project assembly, there were instances where servos accidentally detached after being secured, and in some cases, certain servos did not operate perfectly. Despite these occasional issues, I did not encounter any major challenges in terms of using the servos to control the overall project.
In the circuit design for this project, I aimed to keep the wiring as hidden as possible, ensuring minimal visibility of the internal connections from the outside. Similarly, I wanted to conceal all the servos used in the project to give the overall appearance a more streamlined and clean look. In addition to leaving space for circuit connectors when designing the laser-cut wooden box, I also created small boxes to house and secure the servos and breadboard on the exterior of the box.
For example, when designing the three buttons, I arranged them evenly in a horizontal row. This way, during the final polishing of the exterior, I could easily enclose the wiring in a box with three cutouts, keeping the circuit hidden while still allowing the buttons to be accessible. At the junctions where the wooden box meets the internal cardboard compartments, I also left openings to allow the wires connecting the buttons and servos to pass through to the interior of the box. Inside the box, I designed compartments to separate the wires connecting the screen, servos, and buttons from the control board that operates the lid.
As a result, from the outside, all the control board wiring and circuits are relatively well concealed. Except for a few harder-to-hide wires, the overall appearance of the project is neat, orderly, and visually cohesive.
In the rest of the project, I also made several minor design enhancements. For example, in the riddle section, I used a laser cutter to etch the riddle onto the cardboard. However, since the laser-etched text was not very visible on the cardboard, I decided to include a hint asking users to turn on a flashlight in order to better see the riddle on the switch. To further highlight the project’s instructions and make it more engaging for the players, I added some simple decorative drawings. This not only made the guiding information more prominent but also increased the overall interactivity and enjoyment for users.
Inside the box, I also included a little "Easter egg." I came across an open-source project on the Bilibili website by a tech creator, which was called "Don't Bother Me Box." In this project, when you push a switch, the box opens, but a mechanical arm inside extends and pushes the switch back to its original position, closing the box again. The creator shared the custom control board and all the necessary code files, which had already been uploaded to a data board. Additionally, they provided the 3D printed shell files, making it easier for others to replicate the project.
I found this Easter egg to be particularly fun and in line with the theme of my own project. Therefore, I purchased the pre-made data board and 3D printed shell files from a second-hand platform. After a simple assembly, I added it as a bonus feature to my final project presentation, adding an extra layer of creativity and surprise for the viewers.
In designing this project, my goal was to create an experience that would immerse users as much as possible, allowing them to enjoy the process while solving puzzles. Looking at the final result presented at the IMA show, I can say that my project largely met my expectations and was well-received by visitors. Initially, I wanted users to explore my entire puzzle-solving experience freely, unraveling various riddles and triggering different mechanisms as they progressed, ultimately earning their reward. Based on user feedback, it seems that the project indeed requires a good amount of mental effort and manual dexterity. Since the mechanisms I designed require deep user involvement, the project offers a high level of interaction.
However, this does not mean that my project is without areas for improvement. In fact, there are many aspects that I can optimize and refine. For instance, while the puzzles and riddles are moderately challenging, the overall mechanism setup still feels somewhat simple. If I had more time, I would have explored more diverse types of mechanisms. Similarly, rather than guiding the users myself as the developer, I would like the project to be more self-explanatory. I could also attempt to offer a more personalized exploration experience, so that each individual could gain unique insights and experiences from engaging with the project.
Of course, it's clear that there are some differences between my original vision and the final outcome, which is the result of a series of compromises made throughout the development process. During the creation of this project, I learned a great deal, perhaps the most important lesson being how to adapt and iterate based on the existing work. By doing this, I was able to troubleshoot and overcome challenges, constantly improving the project. As my project name "Chimera" suggests, it represents something seemingly elusive and unattainable, and our task is to continuously explore and approach it. This aligns with the experience of my project, where players must solve one riddle after another, and also reflects my personal journey during the design process. What I intended to present was a seemingly distant and unreachable result, but by continuously moving toward it, I could constantly learn and refine what I had learned. Ultimately, the final outcome may not look exactly as I originally envisioned, but the effort and exploration involved in creating this final project has been immensely rewarding for me.
Code for Puzzle Part on Processing
int cols = 4;
int rows = 3;
int tileWidth, tileHeight;
PImage img;
Tile[] tiles;
Tile selectedTile = null;
boolean puzzleCompleted = false;
float startTime;
import processing.serial.*;
Serial serialPort;
boolean serialDataSent = false;
int NUM_OF_VALUES_FROM_PROCESSING = 3;
int processing_values[] = new int[NUM_OF_VALUES_FROM_PROCESSING];
PImage backgroundImg;
void setup() {
size(1200, 900);
printArray(Serial.list());
serialPort = new Serial(this, "COM8", 9600);
img = loadImage("istockphoto-155142781-612x612.jpg");
backgroundImg = loadImage("微信图片_20241212140725.jpg");
tileWidth = img.width / cols;
tileHeight = img.height / rows;
tiles = new Tile[cols * rows];
int index = 0;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
tiles[index] = new Tile(col * tileWidth, row * tileHeight, img.get(col * tileWidth, row * tileHeight, tileWidth, tileHeight), index);
index++;
}
}
shuffleTiles();
startTime = millis();
}
void draw() {
background(255);
image(backgroundImg, 0, 0, width, height);
for (int i = 0; i < tiles.length; i++) {
tiles[i].display();
}
stroke(0);
noFill();
float borderX = 0;
float borderY = 0;
float borderWidth = cols * tileWidth;
float borderHeight = rows * tileHeight;
rect(borderX, borderY, borderWidth, borderHeight);
puzzleCompleted = true;
for (int i = 0; i < tiles.length; i++) {
if (!tiles[i].isInCorrectPosition()) {
puzzleCompleted = false;
break;
}
}
if (puzzleCompleted) {
fill(0);
textSize(32);
textAlign(CENTER, CENTER);
text("You Win!", width / 2, height / 2);
float elapsedTime = (millis() - startTime) / 1000.0;
textSize(20);
text("Time: " + elapsedTime + " seconds", width / 2, height / 2 + 40);
processing_values[0] = 1;
sendSerialData();
}
}
void mousePressed() {
for (int i = 0; i < tiles.length; i++) {
if (tiles[i].isMouseOver()) {
if (selectedTile == null && !tiles[i].isCorrectlyPlaced) {
selectedTile = tiles[i];
}
else if (selectedTile == tiles[i]) {
selectedTile = null;
}
else if (!tiles[i].isCorrectlyPlaced) {
selectedTile = tiles[i];
}
break;
}
}
}
void mouseDragged() {
if (selectedTile != null) {
selectedTile.x = mouseX - tileWidth / 2;
selectedTile.y = mouseY - tileHeight / 2;
}
}
void mouseReleased() {
if (selectedTile != null) {
int col = int((selectedTile.x + tileWidth / 2) / tileWidth);
int row = int((selectedTile.y + tileHeight / 2) / tileHeight);
Tile targetTile = getTileAt(col, row);
if (targetTile == null && !selectedTile.isInCorrectPosition()) {
boolean validPosition = false;
float randomX = 0, randomY = 0;
while (!validPosition) {
randomX = random(width - tileWidth);
randomY = random(height - tileHeight);
validPosition = true;
for (Tile t : tiles) {
if (dist(randomX, randomY, t.x, t.y) < tileWidth) {
validPosition = false;
break;
}
}
}
selectedTile.x = randomX;
selectedTile.y = randomY;
} else {
selectedTile.x = col * tileWidth;
selectedTile.y = row * tileHeight;
if (selectedTile.isInCorrectPosition()) {
selectedTile.isCorrectlyPlaced = true;
selectedTile = null;
}
}
}
}
void shuffleTiles() {
for (int i = 0; i < tiles.length; i++) {
Tile temp = tiles[i];
int randomIndex = int(random(tiles.length));
tiles[i] = tiles[randomIndex];
tiles[randomIndex] = temp;
boolean overlap = true;
float randomX = 0, randomY = 0;
while (overlap) {
randomX = random(width - tileWidth);
randomY = random(height - tileHeight);
overlap = false;
for (Tile t : tiles) {
if (dist(randomX, randomY, t.x, t.y) < tileWidth) {
overlap = true;
break;
}
}
}
tiles[i].x = randomX;
tiles[i].y = randomY;
}
}
Tile getTileAt(int col, int row) {
for (Tile t : tiles) {
int tileCol = t.index % cols;
int tileRow = t.index / cols;
if (tileCol == col && tileRow == row) {
return t;
}
}
return null;
}
class Tile {
float x, y;
PImage img;
int index;
boolean isCorrectlyPlaced = false;
Tile(float x, float y, PImage img, int index) {
this.x = x;
this.y = y;
this.img = img;
this.index = index;
}
void display() {
if (this == selectedTile) {
stroke(255, 0, 0);
strokeWeight(4);
} else {
noStroke();
}
image(img, x, y, tileWidth, tileHeight);
}
boolean isMouseOver() {
return (mouseX > x && mouseX < x + tileWidth && mouseY > y && mouseY < y + tileHeight);
}
boolean isInCorrectPosition() {
int correctX = (index % cols) * tileWidth;
int correctY = (index / cols) * tileHeight;
return (x == correctX && y == correctY);
}
}
void sendSerialData() {
String data = "";
for (int i=0; i<processing_values.length; i++) {
data += processing_values[i];
if (i < processing_values.length-1) {
data += ",";
}
else {
data += "\n";
}
}
serialPort.write(data);
print("To Arduino: " + data);
}
Code for Puzzle Part on Arduino
#define NUM_OF_VALUES_FROM_PROCESSING 2
int processing_values[NUM_OF_VALUES_FROM_PROCESSING];
#include <Servo.h>
Servo myservo;
Servo myservo1;
int pos = 0;
void getSerialData() {
static int tempValue = 0;
static int tempSign = 1;
static int valueIndex = 0;
while (Serial.available()) {
char c = Serial.read();
if (c >= '0' && c <= '9') {
tempValue = tempValue * 10 + (c - '0');
} else if (c == '-') {
tempSign = -1;
} else if (c == ',' || c == '\n') {
if (valueIndex < NUM_OF_VALUES_FROM_PROCESSING) {
processing_values[valueIndex] = tempValue * tempSign;
}
tempValue = 0;
tempSign = 1;
if (c == ',') {
valueIndex = valueIndex + 1;
} else {
valueIndex = 0;
}
}
}
}
void setup() {
Serial.begin(9600);
myservo1.attach(9);
myservo.attach(10);
}
void loop() {
getSerialData();
if (processing_values[0] == 1) {
for (pos = 0; pos <= 90; pos++) {
myservo.write(pos);
myservo1.write(90 - pos);
delay(100);
break;
}
} else {
for (pos = 90; pos >= 0; pos--) {
myservo.write(pos);
myservo1.write(90 - pos);
delay(100);
break;
}
}
}
Code for Button Part on Arduino
#include <Servo.h>
Servo myservo;
Servo myservo1;
const int buttonPin = 3;
int buttonState = 0;
int lastButtonState = 0;
bool servoMoved = false;
void setup() {
pinMode(buttonPin, INPUT);
Serial.begin(9600);
myservo1.attach(9);
myservo.attach(10);
myservo.write(0);
myservo1.write(100);
}
void loop() {
buttonState = digitalRead(buttonPin);
if (buttonState == LOW && lastButtonState == HIGH) {
if (!servoMoved) {
myservo.write(90);
myservo1.write(0);
servoMoved = true;
}
}
if (buttonState == HIGH) {
}
lastButtonState = buttonState;
delay(50);
}