The entire user experience with YouCannotMemorizeBruh in authentic situations is captured in detail in the final film documentation. The title screen, which provides users with detailed gaming instructions and a synopsis of the game, is clearly demonstrated at the outset. Players are set up for the interactive experience with this introduction. After then, the film cuts to the gaming section, when viewers interact with the joystick, button, and slider to mimic the on-screen actions.
The documentation describes a range of user experiences, such as initial learning phase confusion, sequence execution success, and responses to the visual and aural feedback. The game design's innate iterative learning process is highlighted by the inclusion of a variety of player tries. For instance, after repeated practice, sequences that first overwhelmed some players became doable, indicating a developing understanding of the gameplay fundamentals. The game's capacity to test players while maintaining accessibility is confirmed by this learning curve.
The film makes extensive use of key components like sequence generation, responsive LEDs, and visual animations. By striking a balance between clarity, engagement, and an accurate depiction of user interaction, the documentation successfully conveys both the player's experience and the technical complexity of the system. It ends by highlighting the game's enjoyment value and the places where user input prompted additional improvements.
Real Photo of the Product
The idea behind YouCannotMemorizeBruh is to combine a digital gaming experience with physical interaction. Our goal was to create a game that uses sensor-based inputs and memory-based challenges, drawing influence from Simon Says and Bop It. The final objective was to develop a responsive and immersive system that could assess cognitive abilities in an entertaining and engaging way.
Our initial focus was on comprehending the workings of current memory-based games and figuring out how to include physical input devices to improve player engagement. We highlighted the importance of diversity and ease of use in user controls based on this research. The following elements were selected because to their interaction and accessibility:
Joystick: A directional input that introduces spatial complexity.
Button: A simple, binary input for immediate actions.
Slider: A gradual input requiring precision, encouraging tactile engagement.
Modular sensor combinations that could be switched out according to player choice were a feature of early versions. However, it became evident during testing that mechanical instability was introduced by modularity. Consistent sensor security was not achieved by magnets, and gameplay was interrupted by connectivity problems. We therefore improved the system to concentrate on dependable, fixed inputs.
Two key areas for improvement were identified via user testing:
Gameplay Instructions: Players were unclear on whether the game focused on reaction speed or memory replication. To address this, we implemented a step-by-step introductory screen that clarified the objective and rules.
Premature Input Registration: Users interacted with the sensors before the sequence had finished playing. This was addressed by refining the input validation logic, ensuring inputs were only processed after the full sequence display.
Through this iterative process, we struck a balance between simplicity in hardware design and complexity in gameplay, enhancing both usability and challenge for the players.
Video of Initial 3D Model Design that Failed.
YouCannotMemorizeBruh was created through an iterative process that combined software development, electronics, and physical construction. The physical components of the game were made from acrylic sheets (плаcтины), which were laser-cut to provide accurate and long-lasting controller enclosures. The smooth integration of sensors and LEDs was made possible by this method, which also guaranteed a neat, polished hardware finish.
Controller Fabrication
Bradley, my colleague, and I laboured carefully to fabricate the controller cases. We laser-cut exact shapes, such as squares, rectangles, and parallelepipeds, using acrylic sheets to match the game's cube-themed design. The black-and-white colour palette, which we chose to complement the game's austere aesthetic and improve its visual appeal, also adhered to this uniformity.
To ensure structural stability, we used specialised acrylic glue to join the parts after laser cutting. I meticulously sanded the surfaces to smooth the edges and create a polished finish that looked like expertly created stone in order to enhance the appearance. The controllers now have a more firm grip and are more comfortable to handle thanks to this polishing procedure, which also increased the tactile sensation.
The game's aesthetic was further enhanced by the careful creation of delicate grooves and textures that created the appearance of polished stone. The ergonomic design of the controllers made sure they fit well in the user's hand, enhancing utility while maintaining the theme.
Hardware Implementation:
To provide a seamless user experience, we incorporated the following components:
Sensors:
Joystick: Captures up/down inputs, adding variety to the game sequence.
Button: Provides binary inputs for immediate and simple interactions.
Slider: Detects analog input, requiring players to engage precision and control.
LED Matrix: For visual feedback, such as animations for success (green) and failure (red). The LEDs synchronize with game events, providing real-time responses.
Microcontroller: The central system is the Arduino Uno, which manages serial connection and input detection through a USB cable that connects straight to the Processing interface. The smooth communication between the digital game logic and the physical sensors was made possible by this connected connection, which guaranteed steady and dependable data transfer between the microcontroller and the computer.
Before User Testing
Software Architecture:
The software integrates two core platforms—Arduino and Processing—to manage input, logic, and feedback. Data flows through a structured serial communication protocol:
Arduino Code:
Input data is captured using the analogRead() and digitalRead() functions.
The controller's internal LED matrix reacts to user actions. It displays a pre-made figure, like a checkmark, and turns green when participants enter the right sequence. The matrix displays a red blinking cross if the sequence is off. While a blue colour indicates the beginning of the game, the matrix stays white during the resting phase.
These animations were implemented using timing logic and custom patterns on the matrix.
Processing Code:
Manages game logic, sequence generation, and validation of user inputs.
For animations, I utilized prebuilt examples from Processing as a foundation for cube-based visual effects. I adapted these examples to synchronize animations with the game’s sequence events, enhancing the visual cohesion between the hardware and software components.
Challenges and Solutions:
I ran into a number of obstacles during the development process that called for innovative problem-solving and iterative improvement. Integrating the LED matrix replies with the Arduino and making sure they synchronised seamlessly with Processing events was one of the main challenges. Debugging this component required carefully controlling the time of communications and organising the serial data exchange to prevent delays or conflicts. To make sure LED animations matched user actions and game conditions precisely, I used the millis() method and flags to implement exact timing logic.
Adapting prebuilt animations from Processing examples presented another difficulty. Although the examples served as a helpful starting point, some adjustments were necessary to bring them into line with the game logic. For example, I introduced responsive scaling and colour changes that were linked to sequence advancement and user feedback by optimising cube animations to transition dynamically based on player input. The game's visual appeal was improved while maintaining computing efficiency thanks to this adaption procedure.
We had difficulties with the manufacture of the acrylic controller on the hardware side, especially when it came to guaranteeing precise laser cutting and attaining a polished surface. Early attempts produced components that lacked the needed visual quality or didn't fit precisely. I fixed this by meticulously sanding and polishing the surfaces, adding delicate ridges and textures that enhanced the controllers' look and feel.
There were also difficulties in guaranteeing consistency in input detection. For instance, minor variations in the joystick and slider readings led to unexpected game behaviours. In order to reduce this and guarantee accurate sensor data, I added smoothing routines and threshold modifications to the Arduino code.
I improved my programming and fabrication abilities through these difficulties, striking a balance between technical intricacy and user-centred design. In the end, their attempts to solve problems produced a polished, unified product that smoothly combines digital and physical interactions.
By combining digital feedback with physical inputs, YouCannotMemorizeBruh was able to develop a memory-based game that was both immersive and challenging. The iterative procedure enabled us to improve both the software and the hardware to produce a flawless experience in spite of technological difficulties.
The practical fabrication process was a fulfilling experience that gave me the chance to experiment with cutting-edge methods including surface finishing, acrylic bonding, and laser cutting. Together with the programming difficulties—like incorporating responsive LEDs and modifying animations—this project offered a thorough grasp of creating interactive systems.
The majority of user comments during the IMA Show was positive. The dynamic graphics, aural feedback, and escalating challenge were all well received by the players. As evidence of the game's replay value and competitive appeal, many players tried to beat their scores. Future enhancements might involve re-examining modular sensor integration and including sophisticated animations or voice-activated inputs to further boost user engagement.
The significance of striking a balance between technological viability and user-focused design was emphasised by this project. Every stage, from testing to prototyping, added insightful information about the creation of interactive systems.
Someone Playing Our Game on the IMA Show
Processing
import processing.serial.*;
import processing.sound.*;
import java.util.ArrayList;
SoundFile bgMusicMenu;
SoundFile bgMusicGame;
SoundFile loseSound;
Amplitude amplitudeAnalyzer;
Serial myPort;
int score = 0;
int recordScore = 0;
String difficulty = "Arcade";
boolean isWhiteBackground = true;
boolean gameStarted = false;
boolean showSuccessAnimation = false;
boolean showLoseScreen = false;
int successAnimationStartTime;
int loseScreenStartTime;
int roundStartTime; // Time when the round starts
int sequenceSpeed = 1000; // Speed of sequence display (in milliseconds)
ArrayList<String> leaderboardNames = new ArrayList<>(); // List of names
ArrayList<Integer> leaderboardScores = new ArrayList<>(); // List of scores
ArrayList<Float> leaderboardTimes = new ArrayList<>(); // List of times
int gameStartTime; // Start time of the game
float totalGameTime; // Total game time in seconds
boolean isEnteringName = false; // Flag for name input
String playerName = ""; // Name entered by the player
float fastestTime = Float.MAX_VALUE; // Fastest time record (in seconds)
PFont font;
int countdown = 3;
boolean isCountingDown = false;
int countdownStartTime;
int sequenceIndex = -1;
boolean showingSequence = false;
int sequenceStartTime;
int currentLevel = 1;
ArrayList<String> programSequence = new ArrayList<>();
ArrayList<String> userSequence = new ArrayList<>();
boolean waitingForUserInput = false;
float textScale = 0.5;
int animationDuration = 500;
int animationStartTime;
float a;
float offset = PI / 24.0;
int num = 12;
void setup() {
fullScreen(P3D);
background(255);
textAlign(CENTER, CENTER);
font = createFont("Courier", 24);
textFont(font);
bgMusicMenu = new SoundFile(this, "Paradigm.mp3");
bgMusicGame = new SoundFile(this, "music2.mp3");
loseSound = new SoundFile(this, "lose.mp3");
bgMusicMenu.loop();
amplitudeAnalyzer = new Amplitude(this);
amplitudeAnalyzer.input(bgMusicMenu);
String portName = Serial.list()[1];
myPort = new Serial(this, portName, 9600);
myPort.bufferUntil('\n');
}
void draw() {
background(255);
if (!gameStarted) {
showInstructionsWithCubeAndWave();
return;
}
if (isCountingDown) {
handleCountdown();
return;
}
if (showLoseScreen) {
displayLoseScreen();
return;
}
if (showSuccessAnimation) {
displaySuccessAnimation();
return;
}
if (showingSequence) {
handleSequenceDisplay();
return;
}
if (waitingForUserInput) {
displayGameScreen();
return;
}
}
void keyPressed() {
if (isEnteringName) {
if (key == ENTER || key == RETURN) {
// Add player to leaderboard
leaderboardNames.add(playerName);
leaderboardScores.add(score);
leaderboardTimes.add(totalGameTime);
// Sort leaderboard by score first, then time
sortLeaderboard();
// Reset name input
isEnteringName = false;
playerName = "";
resetGame(); // Restart game
} else if (key == BACKSPACE && playerName.length() > 0) {
playerName = playerName.substring(0, playerName.length() - 1); // Remove last character
} else if (key != BACKSPACE) {
playerName += key; // Append character to name
}
return;
}
if (key == 'C' || key == 'c') {
isWhiteBackground = !isWhiteBackground;
}
if (!gameStarted) {
if (key == 'R' || key == 'r') {
resetGame();
} else {
startCountdown();
}
}
}
void sortLeaderboard() {
for (int i = 0; i < leaderboardScores.size() - 1; i++) {
for (int j = i + 1; j < leaderboardScores.size(); j++) {
if (leaderboardScores.get(i) < leaderboardScores.get(j) ||
(leaderboardScores.get(i).equals(leaderboardScores.get(j)) && leaderboardTimes.get(i) > leaderboardTimes.get(j))) {
// Swap scores
int tempScore = leaderboardScores.get(i);
leaderboardScores.set(i, leaderboardScores.get(j));
leaderboardScores.set(j, tempScore);
// Swap times
float tempTime = leaderboardTimes.get(i);
leaderboardTimes.set(i, leaderboardTimes.get(j));
leaderboardTimes.set(j, tempTime);
// Swap names
String tempName = leaderboardNames.get(i);
leaderboardNames.set(i, leaderboardNames.get(j));
leaderboardNames.set(j, tempName);
}
}
}
}
void serialEvent(Serial myPort) {
String input = myPort.readStringUntil('\n').trim();
// Ignore input if the game is showing the sequence
if (showingSequence) {
return; // Do nothing if the sequence is being displayed
}
if (waitingForUserInput) {
userSequence.add(input);
if (userSequence.size() == programSequence.size()) {
if (userSequence.equals(programSequence)) {
score += currentLevel * 10;
if (score > recordScore) {
recordScore = score;
}
currentLevel++;
userSequence.clear();
addNextActionToSequence();
startSuccessAnimation();
myPort.write("WIN\n"); // Send WIN command to NeoPixel
} else {
startLoseScreen();
myPort.write("LOSE\n"); // Send LOSE command to NeoPixel
}
}
}
}
void resetGame() {
gameStarted = false;
isCountingDown = false;
showingSequence = false;
waitingForUserInput = false;
programSequence.clear();
userSequence.clear();
currentLevel = 1;
score = 0;
bgMusicGame.stop();
bgMusicMenu.loop();
}
void startCountdown() {
gameStarted = true;
isCountingDown = true;
countdown = 3;
countdownStartTime = millis();
gameStartTime = millis(); // Start total game time
bgMusicMenu.stop();
bgMusicGame.loop();
}
void handleCountdown() {
int elapsedTime = millis() - countdownStartTime;
if (elapsedTime > 1000) {
countdown--;
countdownStartTime = millis();
if (countdown == 0) {
isCountingDown = false;
startSequence();
myPort.write("START\n"); // Notify Arduino of game start
}
}
background(255);
fill(0);
textSize(60);
text(countdown, width / 2, height / 2);
}
void startSequence() {
if (programSequence.isEmpty()) {
generateSequence(currentLevel);
}
sequenceIndex = 0;
showingSequence = true;
sequenceStartTime = millis();
animationStartTime = millis();
textScale = 0.5;
roundStartTime = millis(); // Start the round timer
}
void handleSequenceDisplay() {
if (sequenceIndex < programSequence.size()) {
int elapsedTime = millis() - animationStartTime;
if (elapsedTime < sequenceSpeed) {
textScale = map(elapsedTime, 0, sequenceSpeed, 0.5, 1.5);
} else {
textScale = 1.0;
}
if (elapsedTime >= sequenceSpeed) { // Ensure exact timing
sequenceIndex++;
animationStartTime = millis(); // Reset for next sequence item
}
if (sequenceIndex < programSequence.size()) {
fill(0);
textSize(40 * textScale);
text(programSequence.get(sequenceIndex), width / 2, height / 2);
}
} else {
showingSequence = false;
waitingForUserInput = true; // Transition to input phase immediately
animationStartTime = 0; // Reset animation timing to avoid carryover
}
}
void displayGameScreen() {
// Calculate total elapsed time in seconds
totalGameTime = (millis() - (gameStartTime + 4000)) / 1000.0;
fill(0);
textSize(24);
text("Repeat the sequence!", width / 2, height / 3);
text("Your Input: " + userSequence.toString(), width / 2, height / 2);
text("Score: " + score, width / 2, height / 2 + 50);
text("Record Score: " + recordScore, width / 2, height / 2 + 100);
text("Total Time: " + totalGameTime + "s", width / 2, height / 2 + 150); // Show total elapsed time
text("Press R to return to the main menu.", width / 2, height - 50);
}
void generateSequence(int level) {
String[] actions = {"SLIDER", "BUTTON", "JOYSTICK UP", "JOYSTICK DOWN"};
programSequence.clear();
for (int i = 0; i < level; i++) {
programSequence.add(actions[(int) random(actions.length)]);
}
}
void addNextActionToSequence() {
String[] actions = {"SLIDER", "BUTTON", "JOYSTICK UP", "JOYSTICK DOWN"};
programSequence.add(actions[(int) random(actions.length)]);
}
void displaySuccessAnimation() {
int elapsedTime = millis() - successAnimationStartTime;
if (elapsedTime < 1000) {
background(0, 0, 26);
translate(width / 2, height / 2);
for (int i = 0; i < num; i++) {
float gray = map(i, 0, num - 1, 0, 255);
pushMatrix();
fill(gray);
rotateY(a + offset * i);
rotateX(a / 2 + offset * i);
box(200);
popMatrix();
}
a += 0.01;
} else {
showSuccessAnimation = false;
startSequence();
}
}
void startLoseScreen() {
showLoseScreen = true;
loseScreenStartTime = millis();
totalGameTime = (millis() - (gameStartTime + 4000)) / 1000.0; // Calculate total game time in seconds
bgMusicGame.stop();
loseSound.play();
}
void startSuccessAnimation() {
showSuccessAnimation = true;
successAnimationStartTime = millis();
totalGameTime = (millis() - (gameStartTime + 4000)) / 1000.0; // Calculate total game time in seconds
}
void displayLoseScreen() {
int elapsedTime = millis() - loseScreenStartTime;
if (elapsedTime < 7000 && !isEnteringName) { // Allow name input for 7 seconds
background(255, 0, 0);
fill(255);
textSize(60);
text("YOU LOSE", width / 2, height / 2);
textSize(24);
text("Final Score: " + score, width / 2, height / 2 + 50);
text("Total Game Time: " + totalGameTime + "s", width / 2, height / 2 + 100); // Show total game time
// Check if the player achieved a record
if (score >= recordScore && totalGameTime <= fastestTime) {
isEnteringName = true; // Enable name input
}
} else if (isEnteringName) {
background(0);
fill(255);
textSize(24);
text("New Record! Enter Your Name:", width / 2, height / 2 - 50);
text(playerName + "_", width / 2, height / 2);
} else {
showLoseScreen = false;
resetGame();
}
}
void showInstructionsWithCubeAndWave() {
drawRotatingCube(width / 2.0, height / 5.0, 80.0);
fill(0);
textSize(24);
text("Welcome to YouCannotMemorizeBruh", width / 2, height / 3);
text("Follow the sequence", width / 2, height / 2);
text("Press any key to start", width / 2, height / 2 + 60);
textSize(20);
text("Leaderboard:", width / 2, height / 2 + 200);
for (int i = 0; i < leaderboardNames.size(); i++) {
text((i + 1) + ". " + leaderboardNames.get(i) + " - " + leaderboardScores.get(i) + "pts - " + leaderboardTimes.get(i) + "s",
width / 2, height / 2 + 230 + i * 30);
}
}
void drawRotatingCube(float x, float y, float size) {
pushMatrix();
translate(x, y, -200);
rotateX(radians(frameCount * 0.5));
rotateY(radians(frameCount * 0.7));
fill(0);
stroke(255);
box(size);
popMatrix();
}
ARDUINO
#include <Adafruit_NeoPixel.h>
#include <Bounce2.h>
// NeoPixel settings
#define LED_PIN 12
#define NUM_LEDS 64 // 8x8 matrix
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// Input pins
#define BUTTON_PIN A3
#define JOYSTICK_X_PIN A0
#define JOYSTICK_Y_PIN A1
#define SLIDER_PIN A2
// Thresholds and debounce
#define JOYSTICK_THRESHOLD 10
Bounce buttonDebouncer = Bounce();
// Tracking states
int lastJoystickX = -1;
int lastJoystickY = -1;
int lastSliderValue = -1;
// Color presets
uint32_t red = strip.Color(255, 0, 0);
uint32_t green = strip.Color(0, 255, 0);
uint32_t white = strip.Color(255, 255, 255);
uint32_t off = strip.Color(0, 0, 0);
String command = ""; // For commands received via serial
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
buttonDebouncer.attach(BUTTON_PIN);
buttonDebouncer.interval(10);
strip.begin();
strip.show(); // Initialize all LEDs to "off"
}
void loop() {
// Handle serial commands
handleSerialCommands();
// Button handling
buttonDebouncer.update();
if (buttonDebouncer.fell()) {
Serial.println("BUTTON");
}
// Joystick handling
handleJoystick();
// Slider handling
handleSlider();
delay(50);
}
void handleSerialCommands() {
if (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
processCommand(command);
command = ""; // Clear the command buffer
} else {
command += c;
}
}
}
void processCommand(String cmd) {
if (cmd == "START") {
animateResting();
} else if (cmd == "WIN") {
animateWin();
} else if (cmd == "LOSE") {
animateLose();
}
}
void animateResting() {
strip.fill(white, 0, NUM_LEDS);
strip.show();
}
void animateWin() {
uint8_t checkmark[8][8] = {
{1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 1, 1, 0, 0, 1},
{1, 0, 0, 1, 1, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1}
};
for (int flash = 0; flash < 5; flash++) {
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
int index = row * 8 + col;
strip.setPixelColor(index, checkmark[row][col] ? green : off);
}
}
strip.show();
delay(200);
strip.fill(off, 0, NUM_LEDS);
strip.show();
delay(200);
}
}
void animateLose() {
uint8_t cross[8][8] = {
{1, 0, 0, 0, 0, 0, 0, 1},
{0, 1, 0, 0, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 1, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 0, 1, 1, 0, 0, 0},
{0, 0, 1, 0, 0, 1, 0, 0},
{0, 1, 0, 0, 0, 0, 1, 0},
{1, 0, 0, 0, 0, 0, 0, 1}
};
for (int flash = 0; flash < 5; flash++) {
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
int index = row * 8 + col;
strip.setPixelColor(index, cross[row][col] ? red : off);
}
}
strip.show();
delay(200);
strip.fill(off, 0, NUM_LEDS);
strip.show();
delay(200);
}
}
void handleJoystick() {
int joystickX = analogRead(JOYSTICK_X_PIN);
int joystickY = analogRead(JOYSTICK_Y_PIN);
if (joystickX < JOYSTICK_THRESHOLD && lastJoystickX != 0) {
Serial.println("JOYSTICK UP");
lastJoystickX = 0;
} else if (joystickX > 1023 - JOYSTICK_THRESHOLD && lastJoystickX != 1) {
Serial.println("JOYSTICK DOWN");
lastJoystickX = 1;
} else if (joystickX >= JOYSTICK_THRESHOLD && joystickX <= 1023 - JOYSTICK_THRESHOLD) {
lastJoystickX = -1;
}
if (joystickY < JOYSTICK_THRESHOLD && lastJoystickY != 0) {
Serial.println("JOYSTICK RIGHT");
lastJoystickY = 0;
} else if (joystickY > 1023 - JOYSTICK_THRESHOLD && lastJoystickY != 1) {
Serial.println("JOYSTICK LEFT");
lastJoystickY = 1;
} else if (joystickY >= JOYSTICK_THRESHOLD && joystickY <= 1023 - JOYSTICK_THRESHOLD) {
lastJoystickY = -1;
}
}
void handleSlider() {
int sliderValue = analogRead(SLIDER_PIN);
if (sliderValue < JOYSTICK_THRESHOLD && lastSliderValue != 0) {
Serial.println("SLIDER");
lastSliderValue = 0;
} else if (sliderValue > 1023 - JOYSTICK_THRESHOLD && lastSliderValue != 1) {
Serial.println("SLIDER");
lastSliderValue = 1;
} else if (sliderValue >= JOYSTICK_THRESHOLD && sliderValue <= 1023 - JOYSTICK_THRESHOLD) {
lastSliderValue = -1;
}
}