Designed by
Shuhua Li (sl2737), Yangyang Peng (yp373)
Most people had played the various arcades while the video game machines were not that popular at that time. One of the most popular arcades would be the light gun game. We use the light gun connected to the arcade to shoot the flights, UFOs and zombies on the screen to get higher scores. We decide to make this light gun game machine to pursue an arcade-like system realizing the basic functions for our teenage memories.
The goal of the project is to design a light gun UFOs shooting game using OpenCV, PyGame.This light gun game includes two parts of the work.
The hardware components of the system include the button for the trigger, laser point and the Pi camera. The button acts like a real light gun trigger, when we pull the trigger, we will press the button at the same time which let a beam appear on the screen shooting the aiming object. The laser pointer will flash of a reasonable time period when we pull the trigger for us to see the aiming point clearly. The Pi camera will work for realizing the targeting function which supports the main purpose of the project.
The button connects to the GPIO OUTPUT pin. Every time when we press the trigger, the program will create a beam object, it will add a new beam in the beam object group and project a new beam on the screen with the update of the PyGame event. The external button is connected to the GPIO pin 17. First, we refer to the pin map below.
Next, we connect the circuit on the protoboard refer to the theoretical circuits as on the left and test the functions with some testing programs:
The laser pointer connects to the GPIO OUTPUT pin, which can easily be controlled by the PWM signal. We set the PWM signal as LOW for default. When we press the button, the program will set the PWM signal as HIGH. Besides, a timer will start to count time at the same time. After the beam update on the screen. There will be an if-statement to check whether the time surpasses 0.6s, if so, it will set the PWM signal as LOW again. Through the process above, we realize the laser pointer flash function when we pull the trigger.
The laser pointer is called KY-008 type, it can be directly power through an RPi output pin as it only draws 30 mA from the processor that has outputs rated for 40 mA.
The output pin shows as below. We only need to connect the ground pin and the 5Vdc supply pin to the GPIO output pin on the RPi. The laser pointer basically works as a LED light. We first run a blink program tested its function, then assemble it to the RPi.
Fig.3 Lasor Pointer |
Fig.4 Pointer datasheet |
Fig.5 Pointer info table |
Fig.6 Pi Camera |
Fig.7 Pi Camera Configuration |
cap = cv2.VideoCapture(0)
cap.set(3, cameraw)
cap.set(4, camerad)
Convert them into HSV image which will benefit us to analyze.
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, lower_blue, upper_blue)
mask = cv2.morphologyEx(mask,cv2.MORPH_OPEN,kernelOpen)
Use the cvFindCountours function to calculate the edges in the picture. We put parameter cv_retr_tree which detect all the edges in the picture establishing only two layers of edges.
(cnts, _) = cv2.findContours(mask.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:10]
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.12 * peri, True)
if cv2.contourArea(c) < 5000:
break
if (cv2.isContourConvex(approx)) and len(approx) == 4:
screenCnt = approx
break
cv2.imshow("mask", cv2.resize(mask, (320,240)))
cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 3)
cv2.imshow("image", cv2.resize(image, (320,240)))
if cv2.waitKey(1) & 0xFF == ord('q'):
break
Calculate the central coordinate of the camera and get the corresponding edge point coordinates through the edge array we get above. We then utilize a perspective transform to convert the coordinate system and get the coordinate of the camera central point on the screen. Finally, we set an if statement to limit the shooting area of the aiming coordinate and project it on the screen with picture we preset.
pts = screenCnt.reshape(4,2)
s = pts.sum(axis = 1)
rect[0] = (rect[0] * 2 + pts[np.argmin(s)]) / 3
rect[2] = (rect[2] * 2 + pts[np.argmax(s)]) / 3
diff = np.diff(pts, axis = 1)
rect[1] = (rect[1] * 2 + pts[np.argmin(diff)]) / 3
rect[3] = (rect[3] * 2 + pts[np.argmax(diff)]) / 3
pcenter = np.float32([cameraw / 2,camerad / 2]) #camera center
dst = np.float32([[30,30],[width-60,30],[width-60,height-60],[30,height-60]])
M = cv2.getPerspectiveTransform(rect, dst)
ptarget = cv2.perspectiveTransform(pcenter.reshape(-1, 1, 2), M)
ptarget = ptarget.reshape(2)
if (ptarget[0] > 150)and(ptarget[0] < width-150)and(ptarget[1] > 150)and(ptarget[1] < height-150):
brect.center = np.rint(ptarget)
insight = True
else:
insight = False
The code above realize the aiming/targeting function.
We use pygame library to write the main functionality of our game.
The pygame library provides great features for game design. We use the predefined class pygame.sprite to create our own sprite class. The sprites are the movable objects on the screen. After we implement the update method of our sprite class, we can use sprite group to easily control the movement of multiple sprites. In our game, we use sprite class to describe UFOs, light beams, stars in background, explosion animations, and blinking texts, almost everything movable in the game!
The flowchart of main loop in game is showed below.
In an active game, the program will generate new UFOs and display them on the screen. The maximum limit of number of UFO increases when the player crashes more UFOs.
These UFO are moving on the screen in 3 states.
The UFO will change its color in every 10 frames, which makes the game more colorful!
We also want to add some effects to simulate a scene in the space. We display 500 stars on the screen which can move as if player is approaching them. These stars will appear randomly in the window with a accelerating speed towards the border. Once it disappears after it hits border, the game will initialize them again so that there are always 500 stars displayed on screen in every frame.
Once a UFO is destroyed, we immediately create an explosion sprite object at the position where the UFO is killed. The explosion will update its appearance every several frames so that player can see the explosion animation.
Fig.8 Arcade module |
Fig.9 Light gun case |
sl2737@cornell.edu
Game design, main program development, testing.
yp373@cornell.edu
Hardware design and assemble, human-machine interaction, testing, web page design.
# 5725 Final Project
# Space Protector - Light Gun Game Arcade
# Author: Shuhua Li (sl2737), Yangyang Peng (yp373)
# Release date: Dec 7, 2018
import numpy as np
import random
import time
import math
import pygame
import cv2
import RPi.GPIO as GPIO
# GPIO setup
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.IN, pull_up_down = GPIO.PUD_UP)
GPIO.setup(27, GPIO.OUT)
GPIO.output(27, GPIO.LOW)
WHITE = 255, 255, 255
BLACK = 0, 0, 0
BLUE = 0, 0, 255
GREEN = 0, 255, 0
RED = 255, 0, 0
ALIEN_RELOAD = 12
# Sprite class for beams
class Beam(pygame.sprite.Sprite):
def __init__(self, beam_origin_pos, beam_target_pos):
pygame.sprite.Sprite.__init__(self)
self.origin_pos = np.array(beam_origin_pos)
self.target_pos = np.array(beam_target_pos)
self.rect = pygame.Rect(0,0,1,1)
self.v = 6.0
self.a = 1.0
def update(self):
self.pos0 = np.rint((6.0 / self.v) * self.origin_pos + (1.0 - 6.0 / self.v) * self.target_pos)
self.pos1 = np.rint((3.0 / self.v) * self.origin_pos + (1.0 - 3.0 / self.v) * self.target_pos)
self.width = int(100.0 / self.v)
self.v += self.a
self.a += 2.0
if self.v > 50:
self.rect = pygame.Rect(self.target_pos, (1,1))
if self.v > 300:
self.kill()
# Sprite class for Aliens
class Alien(pygame.sprite.Sprite):
animcycle = 6
images = []
def __init__(self, ufo_origin_pos, ufo_target_pos, ufo_stay_pos):
pygame.sprite.Sprite.__init__(self)
self.origin_pos = np.array(ufo_origin_pos)
self.target_pos = np.array(ufo_target_pos)
self.stay_pos = np.array(ufo_stay_pos)
self.state = 0
self.frame = 0
self.image = self.images[0]
self.rect = self.image.get_rect()
self.rect.center = self.origin_pos
self.v = np.zeros(2)
self.v[0] = random.random() * 10 + 10
self.v[1] = random.random() * 20 - 10
self.hp = 100
def update(self):
global width
self.frame += 1
self.image = self.images[self.frame//self.animcycle%3]
# Move the UFO according to its state
if self.state == 0:
self.v = (self.v + (self.stay_pos - self.rect.center) / 100) * 0.95
#self.v = self.v / np.linalg.norm(self.v) * 20
if self.frame > 15:
self.state = 1
if self.state == 1:
self.v = self.v * 0.92 + (self.stay_pos - self.rect.center) / 20
if self.frame > 80:
self.state = 2
if self.state == 2:
self.v = self.v + (self.target_pos - self.rect.center) / 500
#print(self.rect.center)
self.rect.center = self.rect.center + self.v
if self.rect.center[0] > width + 100:
self.kill()
# Sprite class for explosion effects
class Explosion(pygame.sprite.Sprite):
defaultlife = 12
animcycle = 3
images = []
def __init__(self, actor):
pygame.sprite.Sprite.__init__(self)
self.image = self.images[0]
self.rect = self.image.get_rect(center=actor.rect.center)
self.life = self.defaultlife
def update(self):
self.life = self.life - 1
self.image = self.images[self.life//self.animcycle%2]
if self.life <= 0: self.kill()
# Sprite class for background stars
class Star(pygame.sprite.Sprite):
def __init__(self, origin_pos):
pygame.sprite.Sprite.__init__(self)
self.pos = np.array(origin_pos).astype(float)
direction = random.randrange(1000)
vm = random.random()*.6+.4
self.v = np.array([math.sin(direction) * vm, math.cos(direction) * vm])
for i in range(int(random.randint(0, 100))):
self.v *= 1.03
self.pos += self.v
def update(self):
global width, height
self.v *= 1.03
self.pos += self.v
if self.pos[0] < 0 or self.pos[0] > width or self.pos[1] < 0 or self.pos[1] > height:
self.__init__((width / 2, height / 2))
# Sprite class for blinking texts
class BlinkText(pygame.sprite.Sprite):
INTERVAL = 10
def __init__(self, display_text):
pygame.sprite.Sprite.__init__(self)
self.surface = font1.render(display_text, True, WHITE)
self.rect = self.surface.get_rect(center = (width / 2, height / 2 + 200))
self.blink = False
self.frame = self.INTERVAL
def update(self):
self.frame -= 1
if self.frame < 0:
self.blink = not self.blink
self.frame = self.INTERVAL
# Function to draw the blue border for openCV recognization
def drawBorder(screen):
pygame.draw.rect(screen, WHITE, pygame.Rect(0, 0, width, 70))
pygame.draw.rect(screen, WHITE, pygame.Rect(0, 0, 70, height))
pygame.draw.rect(screen, WHITE, pygame.Rect(width-70, 0, 70, height))
pygame.draw.rect(screen, WHITE, pygame.Rect(0, height-70, width, 70))
pygame.draw.rect(screen, BLUE, pygame.Rect(10, 10, width - 20, 50))
pygame.draw.rect(screen, BLUE, pygame.Rect(10, 10, 50, height - 20))
pygame.draw.rect(screen, BLUE, pygame.Rect(width-60, 10, 50, height - 20))
pygame.draw.rect(screen, BLUE, pygame.Rect(10, height-60, width - 20, 50))
# Initialize openCV parameters
rect = np.zeros((4, 2), dtype = "float32")
cameraw,camerad = 640, 480
cap = cv2.VideoCapture(0)
cap.set(3, cameraw)
cap.set(4, camerad)
lower_blue = np.array([90,80,160])
upper_blue = np.array([130,255,255])
kernelOpen=np.ones((3,3))
# Initialize pygame
pygame.init()
pygame.mouse.set_visible(False)
# Window mode
size = width, height = 1024, 768
screen = pygame.display.set_mode(size)
# Fullscreen mode
#size = width, height = 1920, 1080
#screen = pygame.display.set_mode(size, pygame.FULLSCREEN)
# Load images, fonts and sound effects
scope = pygame.image.load("sniper.png")
brect = scope.get_rect()
Alien.images.append(pygame.image.load("alien1.png"))
Alien.images.append(pygame.image.load("alien2.png"))
Alien.images.append(pygame.image.load("alien3.png"))
aliens = pygame.sprite.Group()
alienreload = ALIEN_RELOAD
icon = pygame.transform.scale(Alien.images[0], (32, 32))
pygame.display.set_icon(icon)
pygame.display.set_caption('Space Protector')
img = pygame.image.load('explosion1.gif')
Explosion.images = [img, pygame.transform.flip(img, 1, 1)]
font1 = pygame.font.Font("ARCADECLASSIC.TTF", 32)
font2 = pygame.font.Font("ARCADECLASSIC.TTF", 48)
font_title = pygame.font.Font("ARCADECLASSIC.TTF", 96)
if pygame.mixer and not pygame.mixer.get_init():
print ('Warning, no sound')
pygame.mixer = None
boom_sound = pygame.mixer.Sound('boom.wav')
hit_sound = pygame.mixer.Sound('hit.wav')
laser_sound = pygame.mixer.Sound('laser.wav')
laser_sound.set_volume(0.2)
if pygame.mixer:
pygame.mixer.music.load('house_lo.wav')
pygame.mixer.music.play(-1)
# Initialize global parameters used in game
ptarget = np.float32([width / 2, height / 2])
maxalien = 3
score = 0
starttime = time.time()
timestamp = 0
insight = False
game_running = True
ingame = 0
clock = pygame.time.Clock()
# Initialize sprite groups
beams = pygame.sprite.Group()
stars = pygame.sprite.Group()
explosions = pygame.sprite.Group()
hintstr = "PRESS BUTTON TO START"
hinttxt = BlinkText(hintstr)
# Initialize the background stars
for i in range(100):
stars.add(Star((width / 2, height / 2)))
# Main loop
while game_running:
# Use openCV to find the aiming position
ret, image = cap.read()
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, lower_blue, upper_blue)
mask = cv2.morphologyEx(mask,cv2.MORPH_OPEN,kernelOpen)
(cnts, _) = cv2.findContours(mask.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:10]
screenCnt = None
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.12 * peri, True)
if cv2.contourArea(c) < 5000:
break
if (cv2.isContourConvex(approx)) and len(approx) == 4:
screenCnt = approx
break
cv2.imshow("mask", cv2.resize(mask, (320,240)))
cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 3)
cv2.imshow("image", cv2.resize(image, (320,240)))
if cv2.waitKey(1) & 0xFF == ord('q'):
break
if screenCnt != None:
pts = screenCnt.reshape(4,2)
s = pts.sum(axis = 1)
rect[0] = (rect[0] * 2 + pts[np.argmin(s)]) / 3
rect[2] = (rect[2] * 2 + pts[np.argmax(s)]) / 3
diff = np.diff(pts, axis = 1)
rect[1] = (rect[1] * 2 + pts[np.argmin(diff)]) / 3
rect[3] = (rect[3] * 2 + pts[np.argmax(diff)]) / 3
pcenter = np.float32([cameraw / 2,camerad / 2])
dst = np.float32([[30,30],[width-60,30],[width-60,height-60],[30,height-60]])
M = cv2.getPerspectiveTransform(rect, dst)
ptarget = cv2.perspectiveTransform(pcenter.reshape(-1, 1, 2), M)
ptarget = ptarget.reshape(2)
if (ptarget[0] > 150)and(ptarget[0] < width-150)and(ptarget[1] > 150)and(ptarget[1] < height-150):
brect.center = np.rint(ptarget)
insight = True
else:
insight = False
else:
insight = False
clock.tick(20)
# Processing the inputs (keyboard, mouse and external buttons)
if time.time()-timestamp > 0.6:
GPIO.output(27, GPIO.LOW)
for event in pygame.event.get():
if (event.type is pygame.KEYDOWN):
presskey = pygame.key.get_pressed()
if presskey[pygame.K_ESCAPE]:
game_running = False
if presskey[pygame.K_SPACE]:
if ingame == 0:
ingame = 1
maxalien = 3
score = 0
hinttxt.kill()
starttime = time.time()
elif ingame == 1:
laser_sound.play()
beam = Beam([(width + brect.center[0]) / 3, height - 120],brect.center)
beams.add(beam)
else:
if time.time() - starttime > 3:
ingame = 0
hinttxt.kill()
hintstr = "PRESS BUTTON TO START"
hinttxt = BlinkText(hintstr)
elif (event.type is pygame.MOUSEMOTION):
pos = pygame.mouse.get_pos()
insight = True
brect.center = pos
screen.fill(BLACK)
stars.update()
for star in stars.sprites():
screen.set_at(star.pos.astype(int), WHITE)
# If game is playing
if ingame == 1:
if(not GPIO.input(17)):
laser_sound.play()
beam = Beam([width/2, height -120],brect.center)
beams.add(beam)
GPIO.output(27, GPIO.HIGH)
timestamp = time.time()
# Create new alien
if alienreload:
alienreload = alienreload - 1
elif len(aliens) < maxalien and random.random() < 0.5:
origin_y = int(random.random() * 400) + 200
target_y = int(random.random() * 400) + 200
stay_x = int(random.random() * 400) + 200
stay_y = int(random.random() * 400) + 200
alien = Alien([-100, origin_y], [width + 100, target_y], [stay_x, stay_y])
aliens.add(alien)
alienreload = ALIEN_RELOAD
aliens.update()
beams.update()
explosions.update()
aliens.draw(screen)
explosions.draw(screen)
for beam in beams.sprites():
pygame.draw.line(screen, GREEN, beam.pos0, beam.pos1, beam.width)
for alien in pygame.sprite.groupcollide(aliens, beams, 0, 1):
alien.hp -= 25
if alien.hp <= 0:
score += 100
if score % 500 == 0:
maxalien += 1
alien.kill()
boom_sound.play()
explosions.add(Explosion(alien))
else:
hit_sound.play()
for alien in aliens.sprites():
if alien.hp < 100 and alien.hp > 0:
hpbar = pygame.Rect(alien.rect)
hpbar.top = hpbar.top - 10
hpbar.height = 8
hpremain = pygame.Rect(hpbar)
hpremain.width = hpbar.width * alien.hp / 100
pygame.draw.rect(screen, RED, hpbar)
pygame.draw.rect(screen, GREEN, hpremain)
remaintime = 60 - (time.time() - starttime)
remaintimestr = "REMAIN " + str(int(remaintime)).zfill(3)
scorestr = "SCORE " + str(score).zfill(5)
remaintime_surface = font1.render(remaintimestr, True, WHITE)
remaintime_rect = remaintime_surface.get_rect(center = (180, 100))
screen.blit(remaintime_surface, remaintime_rect)
score_surface = font1.render(scorestr, True, WHITE)
score_rect = score_surface.get_rect(center = (width - 180, 100))
screen.blit(score_surface, score_rect)
if remaintime <= 0:
ingame = 2
starttime = time.time()
beams.empty()
aliens.empty()
explosions.empty()
hinttxt.kill()
hintstr = "PRESS BUTTON TO RETURN"
hinttxt = BlinkText(hintstr)
elif ingame == 0:
titlestr = "SPACE PROTECTOR"
title_surface = font_title.render(titlestr, True, WHITE)
title_rect = title_surface.get_rect(center = (width / 2, height / 2 - 100))
screen.blit(title_surface, title_rect)
hinttxt.update()
if(not GPIO.input(17)):
ingame = 1
maxalien = 3
score = 0
hinttxt.kill()
starttime = time.time()
if hinttxt.blink:
screen.blit(hinttxt.surface, hinttxt.rect)
elif ingame == 2:
titlestr = "GAME OVER"
title_surface = font_title.render(titlestr, True, WHITE)
title_rect = title_surface.get_rect(center = (width / 2, height / 2 - 100))
screen.blit(title_surface, title_rect)
score_surface = font2.render(scorestr, True, WHITE)
score_rect = score_surface.get_rect(center = (width / 2, height / 2 + 100))
screen.blit(score_surface, score_rect)
hinttxt.update()
if(not GPIO.input(17)):
if time.time() - starttime > 3:
ingame = 0
hinttxt.kill()
hintstr = "PRESS BUTTON TO START"
hinttxt = BlinkText(hintstr)
if time.time() - starttime > 3 and hinttxt.blink:
screen.blit(hinttxt.surface, hinttxt.rect)
if insight:
screen.blit(scope, brect)
# Finally, draw the blue border on the window
drawBorder(screen)
# Display the new frame onto the Screen
pygame.display.update()
# Release gpio pins and camera before exit
GPIO.cleanup()
cap.release()
cv2.destroyAllWindows()