''' Starting point for the PaddleBall game. Created on Jun 21, 2010 @author: Matt Boutell ''' import pygame from pygame.locals import * from time import time, sleep from random import random, randrange, triangular import math SCREEN_WIDTH = 300 SCREEN_HEIGHT = 600 PADDLE_OFFSET = 100 PADDLE_SPEED = 2 BALL_OFFSET = 100 BALL_MOVEMENT_INTERVAL = 0.03 BALL_CREATION_INTERVAL = 4 BALL_MOVEMENTS_PER_CREATION = BALL_CREATION_INTERVAL / BALL_MOVEMENT_INTERVAL NUM_STARS = 50 pygame.init() screen = pygame.display.set_mode([SCREEN_WIDTH, SCREEN_HEIGHT]) # Helper functions def distance(p1, p2): dx = p1[0]-p2[0] dy = p1[1]-p2[1] return math.sqrt(dx*dx + dy*dy) def dot(v1, v2): return v1[0] * v2[0] + v1[1] * v2[1] def scalarMultiply(v, k): return [v[0] * k, v[1] * k] def vectorAdd(v1, v2): return [v1[0] + v2[0], v1[1] + v2[1]] class Paddle: """ A paddle that is controllable by the user """ WIDTH = 60 HEIGHT = 20 def __init__(self, x, y): self.x = x self.y = y self.vx = 0 self.bbox = [] def draw(self, surface): self.bbox = pygame.draw.rect(surface, [128, 128, 0], [self.x,self.y,Paddle.WIDTH, Paddle.HEIGHT]) pygame.draw.rect(surface, [255, 255, 128], [self.x,self.y,Paddle.WIDTH, Paddle.HEIGHT], 3) def setMoving(self, vx): self.vx = vx def move(self): self.x += self.vx if self.x < 0: self.x = 0 elif self.x > SCREEN_WIDTH - Paddle.WIDTH: self.x = SCREEN_WIDTH - Paddle.WIDTH class Star: def __init__(self): self.x = random() * SCREEN_WIDTH self.y = triangular(0, SCREEN_HEIGHT, 0.1 * SCREEN_HEIGHT) self.radius = 1.5 + random() * 1.5 def draw(self, surface): pygame.draw.circle(surface, [255, 255, 255], [int(self.x), int(self.y)], int(self.radius)) class Ball: """ A single moving ball """ RADIUS = 15 INIT_X, INIT_Y = SCREEN_WIDTH/2, BALL_OFFSET def __init__(self): self.x = Ball.INIT_X self.y = Ball.INIT_Y self.vx = random() * 4 - 2 self.vy = random() * 2 + 2 self.bbox = [] self.radius = randrange(10,20) self.mass = math.pi * self.radius**2 # constant density self.color = [randrange(32, 256), randrange(32, 256), randrange(32, 256)] def draw(self, surface): self.bbox = pygame.draw.circle(surface, self.color, [int(self.x), int(self.y)], self.radius) def move(self): self.x += self.vx self.y += self.vy # Bounces off side walls if self.x - self.radius < 0 or self.x + self.radius > SCREEN_WIDTH: self.vx = -self.vx # Bounces off top if self.y - self.radius < 50: self.vy = -self.vy # Acceleration due to gravity self.vy += 0.02 def moveBackwards(self): self.x -= self.vx self.y -= self.vy # Bounces off side walls if self.x - self.radius < 0 or self.x + self.radius > SCREEN_WIDTH: self.vx = -self.vx # Bounces off top if self.y - self.radius < 50: self.vy = -self.vy # CONSIDER: delete? # Acceleration due to gravity self.vy -= 0.02 def stop(self): self.vx, self.vy = 0, 0 def isCollide(self, other): return distance([self.x, self.y], [other.x, other.y]) < self.radius + other.radius def elasticCollide(self,other): """2D elastic collision, using only normal and tangent vectors. Much simpler than alternatives I've seen, since no trig. From http://www.vobarian.com/collisions/2dcollisions2.pdf """ x1 = self.x y1 = self.y x2 = other.x y2 = other.y v1 = [self.vx, self.vy] v2 = [other.vx, other.vy] m1 = self.mass m2 = other.mass # 1. Find normal and tangent unit vectors n = [x2-x1, y2-y1] nMag = math.sqrt(n[0]**2 + n[1]**2) n = [n[0]/nMag, n[1]/nMag] t = [-n[1], n[0]] # 2. Project velocities onto normal and tangent vectors v1n = dot(v1, n) v1t = dot(v1, t) v2n = dot(v2, n) v2t = dot(v2, t) # 3. Find new normal velocity scalars v1nNew = (v1n * (m1 - m2) + 2 * m2 * v2n)/(m1 + m2) v2nNew = (v2n * (m2 - m1) + 2 * m1 * v1n)/(m1 + m2) # 4. Find new velocity vector (normal component) v1n = scalarMultiply(n, v1nNew) v2n = scalarMultiply(n, v2nNew) # 5. Repeat for tangent velocities # No force is applied in tangent direction, so scalars remain the same v1t = scalarMultiply(t, v1t) v2t = scalarMultiply(t, v2t) # 6. Combine normal and tangent vectors v1 = vectorAdd(v1n, v1t) v2 = vectorAdd(v2n, v2t) # 7. Copy back to the balls as their new velocities self.vx = v1[0] self.vy = v1[1] other.vx = v2[0] other.vy = v2[1] class Planet(Ball): def __init__(self): self.hasRing = False if random() < 0.2: self.hasRing = True Ball.__init__(self) def draw(self, surface): Ball.draw(self, surface) if self.hasRing: start = [int(self.x - 1.5 * self.radius), int(self.y + 0.5 * self.radius)] end = [int(self.x + 1.5 * self.radius), int(self.y - 0.5 * self.radius)] pygame.draw.line(surface, [255, 255, 255], start, end, 2) def checkPaddleCollisions(paddle, balls): ballLocations = [b.bbox for b in balls] whichCollided = paddle.bbox.collidelist(ballLocations) score = 0 if whichCollided > -1: # Attempted hack # while balls[whichCollided].y >= paddle.y: # balls[whichCollided].moveBackwards() # balls[whichCollided].vy *= -1 # bounce # balls[whichCollided].vy += 0.1 # friction # original balls[whichCollided].vy *= -1 # bounce balls[whichCollided].y -= 2 # move up past paddle if overlap slightly balls[whichCollided].vy += 0.1 # friction score = len(balls) return score def checkBallCollisions(balls): for i in range(len(balls)): for j in range(i+1, len(balls)): collide = balls[i].isCollide(balls[j]) if collide: balls[i].moveBackwards() balls[j].moveBackwards() balls[i].elasticCollide(balls[j]) def createStars(): stars = [] for i in range(NUM_STARS): stars.append(Star()) return stars def eventLoop(): message = "PaddleBall!" startTime = time() ballCreationCounter = 0 paddle = Paddle(SCREEN_WIDTH/2 - Paddle.WIDTH/2, SCREEN_HEIGHT - PADDLE_OFFSET) balls = [Planet()] stars = createStars() score = 0 paused = False keyCount = 0 while True: events = pygame.event.get() for event in events: if event.type == QUIT: return if event.type == KEYDOWN and event.key == K_LEFT: paddle.setMoving(-PADDLE_SPEED) keyCount += 1 if event.type == KEYDOWN and event.key == K_RIGHT: paddle.setMoving(PADDLE_SPEED) keyCount += 1 if event.type == KEYDOWN and event.key == K_SPACE: paused = not paused if event.type == KEYUP: keyCount -= 1 if keyCount == 0: paddle.setMoving(0) if not paused: screen.fill([0,0,0]) for star in stars: star.draw(screen) paddle.move() paddle.draw(screen) elapsedTime = time() - startTime # Move the balls at regular intervals if elapsedTime > BALL_MOVEMENT_INTERVAL: ballCreationCounter += 1 if ballCreationCounter > BALL_MOVEMENTS_PER_CREATION: balls.append(Planet()) ballCreationCounter = 0 #print("moving") for ball in balls: ball.move() startTime = time() for ball in balls: ball.draw(screen) # Check for collisions and increment the score by the number of balls out there. score += checkPaddleCollisions(paddle, balls) checkBallCollisions(balls) # Remove balls that go off the screen #for i in range(len(balls): for ball in balls: if ball.y > SCREEN_HEIGHT: balls.remove(ball) message = 'Score: {0}'.format(str(score)) # Draw the score font = pygame.font.SysFont("Century Gothic", 24, bold=True, italic=False) scoreText = font.render(message, True, [255, 255, 0]) screen.blit(scoreText, [20, 20]) pygame.display.update() # end game if no more balls if len(balls) < 1: message = 'Game Over: {0}'.format(str(score)) font = pygame.font.SysFont("Century Gothic", 36, bold=True, italic=False) scoreText = font.render(message, True, [255, 255, 0]) screen.blit(scoreText, [0, SCREEN_WIDTH/2]) pygame.display.update() sleep(3) return eventLoop()