Skip to content

Instantly share code, notes, and snippets.

@luluco250
Created February 2, 2022 20:58
Show Gist options
  • Save luluco250/259824e562fb9a951518f118787fea52 to your computer and use it in GitHub Desktop.
Save luluco250/259824e562fb9a951518f118787fea52 to your computer and use it in GitHub Desktop.
Simple single-player pong game in PyGame.
#!/usr/bin/env python3
import sys
import pygame as pg
import enum
import math
import random
# Global game constants.
GAME_TITLE = "Pong"
TITLE_UPDATE_INTERVAL_MS = 200
FPS_LIMIT = 120
SCREEN_WIDTH = 640
SCREEN_HEIGHT = 480
PADDLE_WIDTH = 150
PADDLE_HEIGHT = 25
BALL_WIDTH = 25
BALL_HEIGHT = 25
START_ANGLE_RANGE = 135
START_SPEED = 300
START_DELAY_MS = 1000
SPEED_INCREMENT = 50
HITS_BEFORE_INCREMENT = 3
SCORE_FONT_SIZE = 72
SCORE_OFFSET_X = 25
SCORE_OFFSET_Y = 25
BACKGROUND_COLOR = 0, 0, 0
FOREGROUND_COLOR = 255, 255, 255
def clamp(x, min_val, max_val):
"""
Returns min_val if x < min_val, max_val if x > max_val else x.
"""
return max(min_val, min(max_val, x))
def magnitude(x, y):
return math.sqrt(x * x + y * y)
def angle_between(x1, y1, x2, y2):
return math.degrees(math.atan2(y2 - y1, x2 - x1))
def length_angle_to_vector(length, degrees):
radians = math.radians(degrees)
return pg.Vector2(math.cos(radians), math.sin(radians)) * length
def vector_to_length_angle(vector):
length = magnitude(vector.x, vector.y)
radians = math.atan2(vector.y, vector.x)
return length, math.degrees(radians)
def start_ball_position():
return pg.Vector2(
SCREEN_WIDTH / 2 - BALL_WIDTH / 2,
# Start a little bit closer to the top instead of dead center.
SCREEN_HEIGHT / 3 - BALL_HEIGHT / 2,
)
def start_ball_velocity():
half_range = START_ANGLE_RANGE / 2
return length_angle_to_vector(
START_SPEED,
random.uniform(90 - half_range, 90 + half_range),
)
def update_score(font, score):
return font.render(str(score), True, FOREGROUND_COLOR)
def main():
### Initialzation. ###
pg.init()
pg.display.set_caption(GAME_TITLE)
screen = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1)
paddle = pg.Rect(
0,
SCREEN_HEIGHT - PADDLE_HEIGHT * 3,
PADDLE_WIDTH,
PADDLE_HEIGHT,
)
# The ball starts in the middle of the screen.
# We'll use a separate variable for the position because PyGame's Rect
# forces the position to be in integer, which breaks physics.
ball_pos = start_ball_position()
ball_vel = start_ball_velocity()
ball = pg.Rect(*ball_pos, BALL_WIDTH, BALL_HEIGHT)
clock = pg.time.Clock()
hits = 0
hitting = False
start_time = pg.time.get_ticks()
score_font = pg.font.Font(None, SCORE_FONT_SIZE)
score = 0
score_img = update_score(score_font, score)
title_time = 0
### Game loop. ###
while True:
# Get the time in seconds (with decimals) since the last frame.
# This can be used to calculate the framerate as well as correcting
# physics calculations to be at a fixed rate regardless of FPS.
delta_time = clock.tick(FPS_LIMIT) * 0.001
now = pg.time.get_ticks()
# Statistics in the title because why not?
if now - title_time > TITLE_UPDATE_INTERVAL_MS:
pg.display.set_caption(
GAME_TITLE +
f" {delta_time:.3f} Frametime" +
f" {clock.get_fps():.0f} FPS"
)
title_time = now
### Event handling. ###
for e in pg.event.get():
if e.type == pg.QUIT:
return 0
### Game logic. ###
# Move paddle to mouse position but keep it inside the screen.
paddle.left = clamp(
pg.mouse.get_pos()[0] - paddle.width / 2,
0,
SCREEN_WIDTH - paddle.width,
)
# Collide the ball with the paddle.
# 1. We'll get the magnitude of the current ball velocity (the
# length of the vector).
# 2. We'll get the angle between the paddle and the ball.
# 3. We'll flip the angle by adding 180° to it.
# 4. We'll set the new velocity by the magnitude and angle we just
# calculated.
if ball.colliderect(paddle):
# Check if we already hit the paddle in the last frame and skip
# the collision code if so (until we stop hitting it).
# This both prevents extra unnecessary processing as well as
# avoiding an exponential speed increase due to the hits variable
# incrementing.
# Oh and it avoids incrementing the score all the time too.
if not hitting:
vel = magnitude(*ball_vel)
# Increment velocity after a certain number of hits.
hits += 1
if hits >= HITS_BEFORE_INCREMENT:
vel += SPEED_INCREMENT
hits = 0
ball_x = ball_pos.x + BALL_WIDTH / 2
ball_y = ball_pos.y + BALL_HEIGHT / 2
paddle_x = paddle.left + PADDLE_WIDTH / 2
paddle_y = paddle.top + PADDLE_HEIGHT / 2
angle = angle_between(ball_x, ball_y, paddle_x, paddle_y) + 180
ball_vel = length_angle_to_vector(vel, angle)
hitting = True
score += 1
score_img = update_score(score_font, score)
else:
hitting = False
# Horizontal collision.
# - Sets the X velocity positive if colliding with the left wall.
# - Sets the X velocity negative if colliding with the right wall.
ball_vel.x = (
abs(ball_vel.x) if ball_pos.x < 0 else
-abs(ball_vel.x) if ball_pos.x + BALL_WIDTH > SCREEN_WIDTH else
ball_vel.x
)
# Sets the Y velocity positive if colliding with the top wall.
ball_vel.y = abs(ball_vel.y) if ball_pos.y < 0 else ball_vel.y
# Check if colliding with the bottom wall, if so we'll reset the game.
if ball_pos.y + BALL_HEIGHT > SCREEN_HEIGHT:
ball_pos = start_ball_position()
ball_vel = start_ball_velocity()
score = 0
score_img = update_score(score_font, score)
hits = 0
start_time = now
# We'll only start moving the ball after a certain time has passed since
# the last reset.
if now - start_time > START_DELAY_MS:
# Move the ball by its velocity.
# This is where we use the delta_time to scale the physics so that
# it doesn't move faster with high FPS or slower with low FPS.
ball_pos.x += ball_vel.x * delta_time
ball_pos.y += ball_vel.y * delta_time
# Update the position of the rect we'll draw with the one used in the
# physics calculations.
ball.left, ball.top = ball_pos.x, ball_pos.y
### Rendering. ###
# Clear the screen.
screen.fill(BACKGROUND_COLOR)
# Draw the score text.
screen.blit(score_img, (SCORE_OFFSET_X, SCORE_OFFSET_Y))
# Draw the paddle.
pg.draw.rect(screen, FOREGROUND_COLOR, paddle)
# Draw the ball.
pg.draw.rect(screen, FOREGROUND_COLOR, ball)
# Present the screen.
pg.display.flip()
return 0
if __name__ == "__main__":
sys.exit(main() or 0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment