Fairground shooting games use light guns: the "gun" sends a beam, and a sensor in the target sees it. You can build the same thing with a torch and the photoresistor. The target lights up a green LED for a short, random time. Hit it with the torch beam while it is green and you score a point; hit it while it is dark and you lose one. After ten targets the game shows your score. Please only use a normal torch or a phone light, and never shine it in anyone's eyes.
What you'll learn
- How to calibrate a light sensor to the light in the room
- How to detect a sudden jump in brightness
- How to make a game with random timing
- How to keep score and finish a game after a set number of rounds
What you need
- UNO R3 board and USB cable (UNO R3 Starter Kit)
- Photoresistor module (45-in-1 Sensor Kit)
- Active buzzer module (45-in-1 Sensor Kit)
- Green LED and 1 resistor, 220 Ω (UNO R3 Starter Kit)
- 1 push button (UNO R3 Starter Kit)
- A torch or a phone with a torch
- The printed parts from the 3D printing box below (target), or the cardboard version described there
- Breadboard and jumper wires (UNO R3 Starter Kit)
Wiring

| From | To |
|---|---|
| Photoresistor module S | UNO pin A0 |
| Photoresistor module + and − | 5V and GND |
| UNO pin 5 | 220 Ω resistor, then the green LED's long leg |
| Green LED short leg | GND |
| Active buzzer module S | UNO pin 8 |
| Active buzzer module + and − | 5V and GND |
| Start button, one leg | UNO pin 2 |
| Start button, the diagonally opposite leg | GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
The sketch
const int LIGHT_PIN = A0;
const int TARGET_LED = 5;
const int BUZZER_PIN = 8;
const int START_PIN = 2;
const int ROUNDS = 10;
const int JUMP = 120; // how much brighter counts as a hit
const bool BRIGHTER_IS_HIGHER = false; // see "Check your sensor"
int roomLight = 0;
int light() {
int v = analogRead(LIGHT_PIN);
return BRIGHTER_IS_HIGHER ? v : 1023 - v;
}
void beep(int ms) {
digitalWrite(BUZZER_PIN, HIGH);
delay(ms);
digitalWrite(BUZZER_PIN, LOW);
}
void calibrate() {
long sum = 0;
for (int i = 0; i < 50; i++) {
sum += light();
delay(10);
}
roomLight = sum / 50;
Serial.print("Room light: ");
Serial.println(roomLight);
}
bool hit() {
return light() > roomLight + JUMP;
}
void playGame() {
int score = 0;
for (int round = 1; round <= ROUNDS; round++) {
delay(random(800, 2500)); // wait a random time
if (hit()) { // shining too early costs a point
score--;
beep(400);
Serial.println("Too early! -1");
while (hit()) delay(10);
continue;
}
digitalWrite(TARGET_LED, HIGH);
unsigned long upTime = random(700, 1600);
unsigned long start = millis();
bool scored = false;
while (millis() - start < upTime) {
if (hit()) { scored = true; break; }
}
digitalWrite(TARGET_LED, LOW);
if (scored) {
score++;
beep(60); delay(60); beep(60);
Serial.print("Hit in ");
Serial.print(millis() - start);
Serial.println(" ms");
} else {
Serial.println("Missed");
}
while (hit()) delay(10); // wait until the beam is away
}
Serial.print("Game over! Score: ");
Serial.print(score);
Serial.print(" of ");
Serial.println(ROUNDS);
for (int i = 0; i < score; i++) { // blink once for every point
digitalWrite(TARGET_LED, HIGH); delay(200);
digitalWrite(TARGET_LED, LOW); delay(200);
}
}
void setup() {
Serial.begin(9600);
pinMode(TARGET_LED, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(START_PIN, INPUT_PULLUP);
Serial.println("Press the button to start");
}
void loop() {
if (digitalRead(START_PIN) == LOW) {
randomSeed(micros());
calibrate();
beep(300);
playGame();
}
}
Check your sensor
- Open the Serial Monitor at 9600 baud and press the button. It prints the room light.
- Shine the torch at the target from where you will play. If hits are never counted, change
BRIGHTER_IS_HIGHERtotrue. - If the target counts hits without the torch, make
JUMPbigger; if it misses real hits from far away, make it smaller or dim the room.
How it works
A light sensor on its own can't tell "torch" from "bright room". So the game first calibrates: it measures the normal room light 50 times and takes the average. A hit is any reading that is much brighter than that, by at least JUMP.
Random waiting times stop players from guessing the rhythm, and the "too early" rule stops them from simply holding the torch on the target. while (hit()) delay(10); waits until the beam has moved away, so one long flash counts only once.
Try this next
- Make the target stay up for a shorter time every round, so the game gets harder.
- Add a second target with a second photoresistor and light a random one.
- Show the score on the 1602 LCD.
Troubleshooting
-
Every round says "Too early": the room light changed after calibrating, or
JUMPis too small. - Hits are never counted: see "Check your sensor", and aim at the centre hole.
- The buzzer doesn't sound: it must be the active buzzer module, with S on pin 8.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.