Created
February 2, 2022 20:58
-
-
Save luluco250/259824e562fb9a951518f118787fea52 to your computer and use it in GitHub Desktop.
Simple single-player pong game in PyGame.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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