Week 10 — Output Devices
Group Assignment
- Measure the power consumption of an output device.
- Document your work on the group page and reflect on your individual page.
Individual Assignment
- Add an output device to your custom microcontroller board and program it.
Learning Outcomes
- Demonstrate workflows used for controlling output devices with a custom MCU board.
Overview
For this week, I tested three output devices on my board:
- Servo motor
- Buzzer
- I2C LCD display
Video Links
- Video 1 - Servo basic movement
- Video 2 - Servo advanced motion
- Video 3 - Buzzer sound tests
- Video 4 - I2C LCD game demo
- Video 5 - Space Invaders game demo
1. Servo Motor
I installed the ESP32Servo library in Arduino IDE and tested the servo on pin D10.
First I verified simple left-right movement, then moved to smoother and more expressive motion.
Step 1 was confirming that the hardware and library were working with a very simple motion test. After that, I moved to a second test to explore better movement quality and behavior design.
AI Request (Prompt)
I connected the servo motor to XIAO ESP32-C3, and it is connected to pin D10.
Give me some code on how to control a servo motor with some interesting motions.
Code 1 - Basic sweep
#include <ESP32Servo.h>
Servo myServo;
void setup() {
myServo.setPeriodHertz(50);
myServo.attach(D10, 500, 2400);
}
void loop() {
// Move 0 -> 180
for (int pos = 0; pos <= 180; pos += 2) {
myServo.write(pos);
delay(15);
}
// Move 180 -> 0
for (int pos = 180; pos >= 0; pos -= 2) {
myServo.write(pos);
delay(15);
}
}
Result 1
Servo motor moved left and right through the full 180 degree range.
After confirming the basic sweep worked, I wanted to make some more interesting motion for the servo motor. I asked AI to create a different type of movement based on the previous program, with smoother transitions and more expressive behavior.
AI Request (Prompt)
Based on the previous servo program, create a new version with more interesting motion.
Use the same board and pin (XIAO ESP32-C3, D10), and add smooth transitions,
speed variation, and a short wiggle-style movement.
Code 2 - Smooth and varied motion
#include <ESP32Servo.h>
Servo myServo;
void smoothMove(int startPos, int endPos, int stepDelay) {
if (startPos < endPos) {
for (int pos = startPos; pos <= endPos; pos++) {
myServo.write(pos);
delay(stepDelay);
}
} else {
for (int pos = startPos; pos >= endPos; pos--) {
myServo.write(pos);
delay(stepDelay);
}
}
}
void setup() {
myServo.setPeriodHertz(50);
myServo.attach(D10, 500, 2400);
myServo.write(90);
delay(1000);
}
void loop() {
smoothMove(90, 20, 12);
delay(300);
smoothMove(20, 160, 6);
delay(300);
smoothMove(160, 90, 10);
delay(500);
myServo.write(70); delay(120);
myServo.write(110); delay(120);
myServo.write(80); delay(120);
myServo.write(100); delay(120);
myServo.write(90);
delay(1000);
}
Result 2
Servo movement became smoother, faster in some segments, and included short "wiggle" actions.
2. Buzzer
I connected a buzzer to D10 and tested two interactions using a button: single-click for a short
blaster sound and double-click to start a melody.
In this step, I wanted to practice output behavior based on user input patterns. The goal was to build two clear interactions: short press for sound effect and multi-click behavior for melody.
ChatGPT Request
Write Arduino code for a buzzer connected to pin D10 and a button connected to pin D7 on a XIAO ESP32-C3.
When I press the button once, it should play a short sci-fi blaster shot sound.
When I double-click the button, it should start playing a Star Wars-style melody on the buzzer.
By default, there should be no sound.
Code - Button + buzzer interaction
const int buzzerPin = D10;
const int buttonPin = D7;
// ---------- Notes ----------
#define NOTE_C4 262
#define NOTE_D4 294
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_D5 587
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
// ---------- Melody ----------
int melody[] = {
NOTE_A4, NOTE_A4, NOTE_A4,
NOTE_F4, NOTE_C5,
NOTE_A4, NOTE_F4, NOTE_C5, NOTE_A4,
NOTE_E5, NOTE_E5, NOTE_E5,
NOTE_F5, NOTE_C5,
NOTE_GS4, NOTE_F4, NOTE_C5, NOTE_A4,
NOTE_A5, NOTE_A4, NOTE_A4,
NOTE_A5, NOTE_GS5, NOTE_G5, NOTE_FS5, NOTE_F5,
NOTE_FS5
};
int noteDurations[] = {
500, 500, 500,
350, 150,
500, 350, 150, 650,
500, 500, 500,
350, 150,
500, 350, 150, 650,
500, 350, 150,
500, 250, 250, 125, 125, 250,
250
};
// ---------- Button handling ----------
bool lastButtonReading = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 30;
int clickCount = 0;
unsigned long firstClickTime = 0;
const unsigned long doubleClickGap = 350;
// ---------- Music state ----------
bool musicPlaying = false;
int currentNote = 0;
unsigned long noteStartTime = 0;
unsigned long noteTotalTime = 0;
bool noteActive = false;
void setup() {
pinMode(buzzerPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
}
// Aggressive sci-fi shoot sound
void playShootSound() {
noTone(buzzerPin);
for (int burst = 0; burst < 3; burst++) {
for (int freq = 2200; freq >= 700; freq -= 120) {
tone(buzzerPin, freq);
delay(10);
}
noTone(buzzerPin);
delay(15);
}
noTone(buzzerPin);
}
// Start melody from beginning
void startMusic() {
musicPlaying = true;
currentNote = 0;
noteActive = false;
}
// Stop melody
void stopMusic() {
musicPlaying = false;
currentNote = 0;
noteActive = false;
noTone(buzzerPin);
}
// Non-blocking melody player
void updateMusic() {
if (!musicPlaying) return;
int totalNotes = sizeof(melody) / sizeof(melody[0]);
if (currentNote >= totalNotes) {
stopMusic();
return;
}
if (!noteActive) {
int duration = noteDurations[currentNote];
tone(buzzerPin, melody[currentNote]);
noteStartTime = millis();
noteTotalTime = duration;
noteActive = true;
return;
}
unsigned long elapsed = millis() - noteStartTime;
if (elapsed >= noteTotalTime) {
noTone(buzzerPin);
}
if (elapsed >= (unsigned long)(noteTotalTime * 1.3)) {
currentNote++;
noteActive = false;
}
}
// Detect click and single/double click logic
void updateButton() {
bool reading = digitalRead(buttonPin);
if (reading != lastButtonReading) {
lastDebounceTime = millis();
lastButtonReading = reading;
}
if ((millis() - lastDebounceTime) > debounceDelay) {
static bool lastStableState = HIGH;
if (reading != lastStableState) {
lastStableState = reading;
if (lastStableState == LOW) { // button pressed
clickCount++;
if (clickCount == 1) {
firstClickTime = millis();
} else if (clickCount == 2) {
// Double click detected
clickCount = 0;
noTone(buzzerPin);
startMusic();
}
}
}
}
// If only one click happened and time expired -> single click action
if (clickCount == 1 && (millis() - firstClickTime > doubleClickGap)) {
clickCount = 0;
if (!musicPlaying) {
playShootSound();
}
}
}
void loop() {
updateButton();
updateMusic();
}
Result 3
The buzzer successfully generated effects on button interaction, and this test helped me practice timing, debounce logic, and event-based behavior.
3. I2C LCD Display
For the I2C screen, I used four connections: VCC, GND, SDA, and
SCL. On my board, SDA = D4 and SCL = D5.
I split this into two parts: first, verify basic I2C communication and LCD output; second, reuse the working setup to test a simple interactive mini-game with a button.
I2C (Inter-Integrated Circuit) is a communication protocol that allows a microcontroller to exchange data with other devices using only two signal lines: SDA and SCL. This is why an I2C display usually has four pins in total: VCC, GND, SDA, and SCL. VCC provides power to the display, and GND provides the ground connection. The two other pins are used for communication. SDA stands for Serial Data, and it carries the actual information being sent between the microcontroller and the display. SCL stands for Serial Clock, and it provides the timing signal that tells the devices when to read or send each bit of data.
The communication works in a synchronized way. The SDA line carries the bits of information, while the SCL line provides the clock pulses. Each time the clock changes, the receiving device knows when to read the next bit from the data line. In this way, the display receives the information one bit at a time, reconstructs the bytes, and then processes them according to whether they are commands or display data.
AI Request (Prompt)
My I2C display is connected with VCC and GND.
SCL is connected to pin D5 and SDA is connected to pin D4.
Make a simple program for the screen just to check that it works.
Code - I2C LCD check program
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x20, 16, 2);
void setup() {
Wire.begin(D4, D5);
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Hello Yaro!");
lcd.setCursor(0, 1);
lcd.print("LCD works");
}
void loop() {
}
LCD screen is working, and now I can write and experiment with different code.
LCD Mini-Game (Button on D7)
I also tested a simple obstacle-jump style game idea on the LCD using button input from D7.
AI Request (Prompt)
Make a simple interaction game for a 16x2 I2C LCD and a button on D7.
Maybe something like a simple jump-over-obstacle game.
Use XIAO ESP32-C3 with SDA on D4 and SCL on D5.
Code - I2C LCD Button jump game
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x20, 16, 2); // change to 0x7C if needed
const int buttonPin = D7;
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 30;
int playerRow = 1; // 1 = ground, 0 = jump
bool isJumping = false;
unsigned long jumpStartTime = 0;
const unsigned long jumpDuration = 500;
int obstacleCol = 15;
int score = 0;
bool gameOver = false;
unsigned long lastStepTime = 0;
int gameSpeed = 250; // smaller = faster
void setup() {
pinMode(buttonPin, INPUT_PULLUP);
Wire.begin(D4, D5);
lcd.init();
lcd.backlight();
showStartScreen();
}
void loop() {
if (gameOver) {
if (buttonPressed()) {
resetGame();
}
return;
}
if (buttonPressed() && !isJumping) {
isJumping = true;
jumpStartTime = millis();
playerRow = 0;
}
if (isJumping && millis() - jumpStartTime > jumpDuration) {
isJumping = false;
playerRow = 1;
}
if (millis() - lastStepTime > gameSpeed) {
lastStepTime = millis();
updateGame();
drawGame();
}
}
bool buttonPressed() {
bool reading = digitalRead(buttonPin);
if (reading != lastButtonState) {
lastDebounceTime = millis();
lastButtonState = reading;
}
if ((millis() - lastDebounceTime) > debounceDelay) {
static bool stableState = HIGH;
if (reading != stableState) {
stableState = reading;
if (stableState == LOW) {
return true;
}
}
}
return false;
}
void showStartScreen() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Mini Jump Game");
lcd.setCursor(0, 1);
lcd.print("Press btn start");
while (!buttonPressed()) {
delay(10);
}
resetGame();
}
void resetGame() {
playerRow = 1;
isJumping = false;
obstacleCol = 15;
score = 0;
gameOver = false;
gameSpeed = 250;
lcd.clear();
drawGame();
}
void updateGame() {
obstacleCol--;
if (obstacleCol < 0) {
obstacleCol = 15;
score++;
if (gameSpeed > 120) {
gameSpeed -= 10; // gradually increase difficulty
}
}
if (obstacleCol == 1 && playerRow == 1) {
gameOver = true;
showGameOver();
}
}
void drawGame() {
lcd.clear();
// player
lcd.setCursor(1, playerRow);
lcd.print("P");
// obstacle
lcd.setCursor(obstacleCol, 1);
lcd.print("#");
// score
lcd.setCursor(10, 0);
lcd.print("S:");
lcd.print(score);
}
void showGameOver() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Game Over!");
lcd.setCursor(0, 1);
lcd.print("Score:");
lcd.print(score);
delay(1500);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Press button");
lcd.setCursor(0, 1);
lcd.print("to restart");
}
4. Space Invaders on QPad (RP2040 + OLED)
Next, I decided to build a QPad board based on Quentin Bolsee's QPad project. I used QPad Xiao because I have an RP2040 microcontroller.
QPad source: https://gitlab.cba.mit.edu/quentinbolsee/qpad-xiao
On Mods project (https://modsproject.org/) I found a mill-2D PCB with G-code output that fits my Lunyee 3018 CNC machine. It was also an experiment to use the new CNC milling machine and different end mills.
With my CNC machine, I do not have tiny bits (or enough experience yet) for drilling all holes with the same setup, so I drilled the holes using a drill press. For components layout, I soldered pin headers so the XIAO RP2040 and display can be connected on top without permanent soldering. I also soldered two resistors. After testing with hello world, the display worked correctly.
Tetris reference used for initial testing: qpad_tetris (Quentin Bolsee)
Based on the existing code, I created several requests to ChatGPT to build a Space Invaders game. After the first version, I used multiple additional prompts to improve behavior and visuals.
AI Request (Prompt) 1
Make a space invaders game based on the tetris code and device configuration.
Also make an animation preview screen with cool graphics for my small screem.
AI Request (Prompt) 2
The code is working, the platform is moving, but it's not enemies, no space invaders coming,
which I can shoot. Also, you can make platform looks more beautiful,
and space invaders also can have different shapes.
AI Request (Prompt) 3
Okay, good, it's working, but when I finish, it's no next level.
Make at least three levels with different types of invaders.
Also, the welcome screen can be different graphics, maybe some simple animation,
several frames of animation like Space Invaders, and put the text a bit higher
and maybe some characters and platform fighting.
AI Request (Prompt) 4
Okay good, it's a few adjustments:
- I want you to add some space and whether the invaders' text can be in the center
because the last word is out of the screen.
- You can add text by Yaro underneath. Maybe this text by Yaro can be on the left side;
then on the right side it can be a 3D animation of the space invader monster face.
- Also don't forget to make the space invaders monster and the platform interesting shapes.
- When the game is over, when next level, it can be the message "Level complete. Press B for the next level"
and put the invaders a little bit higher because they are too low and a little bit more dense
with each other in the vertical direction.
- Also when the invaders reach the bottom or hit the platform, it's game over.
In place press B to restart.
AI Request (Prompt) 5
It's one strange thing when I kill invaders from the bottom line,
but it's still some invaders on the top line. I still have game over
when probably bottom line is without any invaders, which is a bottom check.
Please to be sure that only invaders which are not eliminated can reach the bottom
and cause game over.
Code - Space Invaders V4
// SPACE INVADERS V4
// XIAO RP2040 + SSD1306 OLED + touch pads
// Updated with centered title, intro art, denser/higher invaders,
// level complete screen, game over rules, and improved sprite shapes.
#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
static const uint8_t PIN_LEFT = D1;
static const uint8_t PIN_RIGHT = D7;
static const uint8_t PIN_A = D9;
static const uint8_t PIN_B = D10;
#define N_TOUCH 4
#define THRESHOLD 6
int touch_pins[N_TOUCH] = { PIN_LEFT, PIN_RIGHT, PIN_A, PIN_B };
bool nowT[N_TOUCH] = {false, false, false, false};
bool pastT[N_TOUCH] = {false, false, false, false};
void update_touch() {
for (int i = 0; i < N_TOUCH; i++) {
int p = touch_pins[i];
pinMode(p, OUTPUT);
digitalWrite(p, LOW);
delayMicroseconds(20);
pinMode(p, INPUT_PULLUP);
int t = 0;
while (!digitalRead(p) && t < 200) t++;
pastT[i] = nowT[i];
nowT[i] = (t > THRESHOLD);
}
}
bool pressed(int i) { return nowT[i] && !pastT[i]; }
bool held(int i) { return nowT[i]; }
enum { T_LEFT = 0, T_RIGHT = 1, T_A = 2, T_B = 3 };
// ================= GAME CONSTANTS =================
#define EC 5
#define ER 3
#define MAX_BULLETS 4
const int PLAYER_Y = 58;
const int PLAYER_W = 12;
const int ENEMY_W = 10;
const int ENEMY_H = 8;
const int ENEMY_GAP_X = 5;
const int ENEMY_GAP_Y = 4; // denser vertically
const int ENEMY_START_X = 18;
const int ENEMY_START_Y = 10; // higher
// ================= GAME STATE =================
int px = 58;
int level = 1;
int ex = ENEMY_START_X;
int ey = ENEMY_START_Y;
int dir = 1;
int enemyStepDelay = 22;
int enemyFrame = 0;
unsigned long lastEnemyMove = 0;
bool enemy[ER][EC];
bool intro = true;
bool gameOver = false;
bool levelComplete = false;
int completedLevel = 1;
unsigned long introTick = 0;
struct Bullet {
int x;
int y;
bool a;
};
Bullet bullets[MAX_BULLETS];
// ================= HELPERS =================
void clearBullets() {
for (int i = 0; i < MAX_BULLETS; i++) bullets[i].a = false;
}
void initEnemies() {
for (int r = 0; r < ER; r++) {
for (int c = 0; c < EC; c++) {
enemy[r][c] = true;
}
}
ex = ENEMY_START_X;
ey = ENEMY_START_Y;
dir = 1;
clearBullets();
}
bool enemiesAlive() {
for (int r = 0; r < ER; r++) {
for (int c = 0; c < EC; c++) {
if (enemy[r][c]) return true;
}
}
return false;
}
void startGame() {
level = 1;
px = 58;
gameOver = false;
levelComplete = false;
initEnemies();
}
void startNextLevel() {
level++;
if (level > 3) level = 1;
levelComplete = false;
px = 58;
initEnemies();
}
// ================= PLAYER =================
void drawPlayer() {
// more interesting ship shape
u8g2.drawPixel(px + 5, PLAYER_Y - 3);
u8g2.drawBox(px + 4, PLAYER_Y - 2, 3, 1);
u8g2.drawBox(px + 3, PLAYER_Y - 1, 5, 1);
u8g2.drawBox(px + 2, PLAYER_Y, 7, 1);
u8g2.drawBox(px + 1, PLAYER_Y + 1, 9, 1);
u8g2.drawBox(px, PLAYER_Y + 2, 12, 1);
u8g2.drawPixel(px + 1, PLAYER_Y + 3);
u8g2.drawPixel(px + 10, PLAYER_Y + 3);
}
// ================= BULLETS =================
void fire() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullets[i].a) {
bullets[i].x = px + PLAYER_W / 2;
bullets[i].y = PLAYER_Y - 5;
bullets[i].a = true;
break;
}
}
}
void updateBullets() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (bullets[i].a) {
bullets[i].y -= 3;
if (bullets[i].y < 0) bullets[i].a = false;
}
}
}
void drawBullets() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (bullets[i].a) u8g2.drawVLine(bullets[i].x, bullets[i].y, 3);
}
}
// ================= ENEMIES =================
void drawEnemyType1(int x, int y, bool phase) {
u8g2.drawBox(x + 2, y, 6, 1);
u8g2.drawBox(x + 1, y + 1, 8, 1);
u8g2.drawBox(x, y + 2, 10, 1);
u8g2.drawPixel(x + 2, y + 3);
u8g2.drawPixel(x + 7, y + 3);
if (phase) {
u8g2.drawLine(x + 1, y + 4, x + 3, y + 6);
u8g2.drawLine(x + 8, y + 4, x + 6, y + 6);
} else {
u8g2.drawLine(x + 2, y + 4, x + 1, y + 6);
u8g2.drawLine(x + 7, y + 4, x + 8, y + 6);
}
}
void drawEnemyType2(int x, int y, bool phase) {
u8g2.drawFrame(x + 1, y + 1, 8, 4);
u8g2.drawPixel(x, y + 2);
u8g2.drawPixel(x + 9, y + 2);
u8g2.drawPixel(x + 3, y + 2);
u8g2.drawPixel(x + 6, y + 2);
if (phase) {
u8g2.drawLine(x + 2, y + 5, x + 1, y + 6);
u8g2.drawLine(x + 7, y + 5, x + 8, y + 6);
} else {
u8g2.drawLine(x + 3, y + 5, x + 3, y + 6);
u8g2.drawLine(x + 6, y + 5, x + 6, y + 6);
}
}
void drawEnemyType3(int x, int y, bool phase) {
u8g2.drawPixel(x + 4, y);
u8g2.drawBox(x + 2, y + 1, 5, 1);
u8g2.drawBox(x + 1, y + 2, 7, 1);
u8g2.drawBox(x, y + 3, 9, 1);
u8g2.drawPixel(x + 2, y + 4);
u8g2.drawPixel(x + 6, y + 4);
if (phase) {
u8g2.drawLine(x + 2, y + 5, x + 1, y + 6);
u8g2.drawLine(x + 6, y + 5, x + 7, y + 6);
} else {
u8g2.drawLine(x + 3, y + 5, x + 3, y + 6);
u8g2.drawLine(x + 5, y + 5, x + 5, y + 6);
}
}
void drawEnemy(int x, int y, int rowType) {
bool phase = ((millis() / 220) % 2) == 0;
if (level == 1) {
drawEnemyType1(x, y, phase);
} else if (level == 2) {
drawEnemyType2(x, y, phase);
} else {
// mix on level 3
if (rowType == 0) drawEnemyType1(x, y, phase);
else if (rowType == 1) drawEnemyType2(x, y, phase);
else drawEnemyType3(x, y, phase);
}
}
void drawEnemies() {
for (int r = 0; r < ER; r++) {
for (int c = 0; c < EC; c++) {
if (enemy[r][c]) {
int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
drawEnemy(x, y, r);
}
}
}
}
void updateEnemies() {
if (millis() - lastEnemyMove < (unsigned long)enemyStepDelay) return;
lastEnemyMove = millis();
enemyFrame++;
int speed = level;
ex += dir * speed;
// Find bounds using only alive invaders
int leftMost = 999;
int rightMost = -999;
int bottomMost = -999;
for (int r = 0; r < ER; r++) {
for (int c = 0; c < EC; c++) {
if (!enemy[r][c]) continue;
int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
if (x < leftMost) leftMost = x;
if (x + ENEMY_W > rightMost) rightMost = x + ENEMY_W;
if (y + ENEMY_H > bottomMost) bottomMost = y + ENEMY_H;
}
}
if (leftMost == 999) return; // no alive invaders
if (leftMost < 2 || rightMost > 126) {
dir = -dir;
ex += dir * speed;
ey += 3;
// recompute bottom after stepping down
bottomMost = -999;
for (int r = 0; r < ER; r++) {
for (int c = 0; c < EC; c++) {
if (!enemy[r][c]) continue;
int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
if (y + ENEMY_H > bottomMost) bottomMost = y + ENEMY_H;
}
}
}
// Game over only if a STILL-ALIVE invader reaches the base height
if (bottomMost >= PLAYER_Y - 2) {
gameOver = true;
}
// Game over only if a STILL-ALIVE invader touches the player area
for (int r = 0; r < ER; r++) {
for (int c = 0; c < EC; c++) {
if (!enemy[r][c]) continue;
int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
bool hitPlayerZone = !(x + ENEMY_W < px || x > px + PLAYER_W ||
y + ENEMY_H < PLAYER_Y - 4 || y > PLAYER_Y + 3);
if (hitPlayerZone) {
gameOver = true;
}
}
}
}
// ================= COLLISION =================
void checkHit() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullets[i].a) continue;
for (int r = 0; r < ER; r++) {
for (int c = 0; c < EC; c++) {
if (!enemy[r][c]) continue;
int x = ex + c * (ENEMY_W + ENEMY_GAP_X);
int y = ey + r * (ENEMY_H + ENEMY_GAP_Y);
if (bullets[i].x > x && bullets[i].x < x + ENEMY_W &&
bullets[i].y > y && bullets[i].y < y + ENEMY_H) {
enemy[r][c] = false;
bullets[i].a = false;
}
}
}
}
}
// ================= INTRO =================
void drawMonsterHead3D(int x, int y, int frame) {
// pseudo-3D face on right side
u8g2.drawRFrame(x, y, 28, 20, 3);
u8g2.drawRFrame(x + 2, y + 2, 24, 16, 2);
u8g2.drawDisc(x + 8, y + 8, 2, U8G2_DRAW_ALL);
u8g2.drawDisc(x + 20, y + 8, 2, U8G2_DRAW_ALL);
u8g2.drawLine(x + 6, y + 14, x + 22, y + 14);
u8g2.drawLine(x + 3, y + 3, x - 2, y - 2);
u8g2.drawLine(x + 25, y + 3, x + 30, y - 2);
if (frame % 20 < 10) {
u8g2.drawLine(x + 10, y + 15, x + 14, y + 17);
u8g2.drawLine(x + 18, y + 15, x + 14, y + 17);
} else {
u8g2.drawLine(x + 9, y + 16, x + 19, y + 16);
}
}
void drawIntro() {
u8g2.clearBuffer();
int frame = (millis() / 90) % 40;
// left side text
u8g2.setFont(u8g2_font_7x13B_tr);
u8g2.drawStr(8, 14, "SPACE");
u8g2.drawStr(8, 28, "INVADERS");
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.drawStr(8, 39, "by Yaro");
// right side monster face
drawMonsterHead3D(88, 8, frame);
// small battle scene lower area
int invX = 16 + (frame % 18);
drawEnemyType3(invX, 46, frame % 2 == 0);
drawEnemyType2(invX + 18, 46, frame % 2 != 0);
drawPlayer();
if (frame % 16 < 8) {
u8g2.drawVLine(px + 6, 44, 8);
}
u8g2.drawHLine(0, 54, 128);
u8g2.setFont(u8g2_font_6x12_tr);
u8g2.drawStr(28, 63, "PRESS B");
u8g2.sendBuffer();
}
// ================= SCREENS =================
void drawHUD() {
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.setCursor(0, 8);
u8g2.print("LVL:");
u8g2.print(level);
}
void drawLevelComplete() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x12_tr);
u8g2.drawStr(14, 20, "LEVEL COMPLETE");
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.drawStr(22, 34, "Press B for the");
u8g2.drawStr(32, 44, "next level");
u8g2.drawStr(48, 56, "L=");
u8g2.print(completedLevel);
u8g2.sendBuffer();
}
void drawGameOver() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_7x13B_tr);
u8g2.drawStr(22, 22, "GAME OVER");
u8g2.setFont(u8g2_font_5x8_tf);
u8g2.drawStr(18, 38, "Invaders reached");
u8g2.drawStr(35, 46, "the base");
u8g2.drawStr(18, 58, "Press B to restart");
u8g2.sendBuffer();
}
void render() {
u8g2.clearBuffer();
drawHUD();
drawEnemies();
drawBullets();
drawPlayer();
u8g2.sendBuffer();
}
// ================= SETUP =================
void setup() {
Serial.begin(115200);
Wire.begin();
u8g2.begin();
startGame();
}
// ================= LOOP =================
void loop() {
update_touch();
if (intro) {
drawIntro();
if (pressed(T_B)) {
intro = false;
startGame();
}
delay(30);
return;
}
if (gameOver) {
drawGameOver();
if (pressed(T_B)) {
startGame();
}
delay(30);
return;
}
if (levelComplete) {
drawLevelComplete();
if (pressed(T_B)) {
startNextLevel();
}
delay(30);
return;
}
if (held(T_LEFT)) px -= 2;
if (held(T_RIGHT)) px += 2;
if (px < 0) px = 0;
if (px > 116) px = 116;
if (pressed(T_A)) fire();
updateBullets();
updateEnemies();
checkHit();
if (!enemiesAlive()) {
completedLevel = level;
levelComplete = true;
}
render();
delay(30);
}
Reflection
- I practiced controlling different output devices from one custom board.
- I improved confidence with timing-based behavior and output patterns.
- I2C wiring and pin mapping became much clearer after this week.
- Using short iterative tests made debugging much faster.
References
Video References
- Video 1 - Servo basic movement
- Video 2 - Servo advanced motion
- Video 3 - Buzzer sound tests
- Video 4 - I2C LCD game demo
- Video 5 - Space Invaders game demo
Arduino Code References (.ino)
servo_basic_sweep.inoservo_smooth_motion.inobuzzer_button_interaction.inoi2c_lcd_check.inoi2c_lcd_mini_game.inospace_invaders_v4.ino