I wrote a program in pygame that basically acts as a physics engine for a ball. You can hit the ball around and your strokes are counted, as well as an extra stroke for going out of bounds. Recently, I added a few things like air resistance, and rewrote my movement physics to make it easier to bounce. I'm wondering if the physics approach is good. Also, is there any way to convert everything into SI units? Right now, my air drag is an arbitrary value.
import math
import pygame as pg
class Colors:
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
GOLD = (255, 215, 0)
GRAY = (100, 100, 100)
NIGHT = (20, 24, 82)
DAY = (135, 206, 235)
MOON = (245, 243, 206)
SMOKE = (96, 96, 96)
class Constants:
SCREEN_WIDTH = 1500
SCREEN_HEIGHT = 800
WINDOW_COLOR = Colors.NIGHT
TICKRATE = 60
GAME_SPEED = .35
LINE_COLOR = Colors.GOLD
ALINE_COLOR = Colors.GOLD
X_BOUNDS_BARRIER = 1
Y_BOUNDS_BARRIER = 1
BOUNCE_FUZZ = 0
START_X = int(.5 * SCREEN_WIDTH)
START_Y = int(.99 * SCREEN_HEIGHT)
AIR_DRAG = .3
GRAVITY = 9.80665
class Fonts:
pg.font.init()
strokeFont = pg.font.SysFont("monospace", 50)
STROKECOLOR = Colors.YELLOW
powerFont = pg.font.SysFont("arial", 15, bold=True)
POWERCOLOR = Colors.GREEN
angleFont = pg.font.SysFont("arial", 15, bold=True)
ANGLECOLOR = Colors.GREEN
penaltyFont = pg.font.SysFont("georgia", 40, bold=True)
PENALTYCOLOR = Colors.RED
toggleBoundsFont = pg.font.SysFont("geneva", 20)
TOGGLEBOUNDSCOLOR = Colors.RED
resistMultiplierFont = pg.font.SysFont("courier new", 13)
RESISTMULTIPLIERCOLOR = Colors.RED
powerMultiplierFont = pg.font.SysFont("courier new", 13)
POWERMULTIPLIERCOLOR = Colors.RED
class Ball(object):
def __init__(self, x, y, dx = 0, dy = 0, bounce = .8, radius = 10, color=Colors.SMOKE, outlinecolor=Colors.RED, density=1):
self.color = color
self.outlinecolor = outlinecolor
self.x = x
self.y = y
self.dx = dx
self.dy = dy
self.ax = 0
self.ay = Constants.GRAVITY
self.dt = Constants.GAME_SPEED
self.bounce = bounce
self.radius = radius
self.mass = 4/3 * math.pi * self.radius**3 * density
def show(self, window):
pg.draw.circle(window, self.outlinecolor, (int(self.x), int(self.y)), self.radius)
pg.draw.circle(window, self.color, (int(self.x), int(self.y)), self.radius - int(.4 * self.radius))
def update(self, update_frame):
update_frame += 1
self.vx += self.ax * self.dt
self.vy += self.ay * self.dt
if resist_multiplier:
drag = 6*math.pi * self.radius * resist_multiplier * Constants.AIR_DRAG
air_resist_x = -drag * self.vx / self.mass
air_resist_y = -drag * self.vy / self.mass
self.vx += air_resist_x/self.dt
self.vy += air_resist_y/self.dt
self.x += self.vx * self.dt
self.y += self.vy * self.dt
bounced, stop, shoot = False, False, True
# Top & Bottom
if self.y + self.radius > Constants.SCREEN_HEIGHT:
self.y = Constants.SCREEN_HEIGHT - self.radius
self.vy = -self.vy
bounced = True
print(' Bounce!')
if self.y - self.radius < Constants.Y_BOUNDS_BARRIER:
self.y = Constants.Y_BOUNDS_BARRIER + self.radius
self.vy = -self.vy
bounced = True
print(' Bounce!')
# Speed/Resistance Rectangles
if (self.x >= .875*Constants.SCREEN_WIDTH + self.radius) and (self.y + self.radius >= .98*Constants.SCREEN_HEIGHT):
self.x = .88*Constants.SCREEN_WIDTH + self.radius
self.y = .98*Constants.SCREEN_HEIGHT - self.radius
self.x = .87*Constants.SCREEN_WIDTH + self.radius
self.vy, self.vx = -self.vy, -2 * abs(self.vx)
bounced = True
if (self.x <= .1175*Constants.SCREEN_WIDTH + self.radius) and (self.y + self.radius >= .98*Constants.SCREEN_HEIGHT):
self.x = .118*Constants.SCREEN_WIDTH + self.radius
self.y = .98*Constants.SCREEN_HEIGHT - self.radius
self.x = .119*Constants.SCREEN_WIDTH + self.radius
self.vy, self.vx = -self.vy, 2 * abs(self.vx)
bounced = True
if x_bounded:
if (self.x - self.radius < Constants.X_BOUNDS_BARRIER):
self.x = Constants.X_BOUNDS_BARRIER + self.radius
self.vx = -self.vx
bounced = True
if (self.x + self.radius > Constants.SCREEN_WIDTH - Constants.X_BOUNDS_BARRIER):
self.x = Constants.SCREEN_WIDTH - Constants.X_BOUNDS_BARRIER - self.radius
self.vx = -self.vx
bounced = True
if self.vx > 1000:
self.vx = 1000
self.y = Constants.SCREEN_HEIGHT/4
if bounced:
self.vx *= self.bounce
self.vy *= self.bounce
print(f'\n Update Frame: {update_frame}',
' x-pos: %spx' % round(self.x),
' y-pos: %spx' % round(self.y),
' x-vel: %spx/u' % round(self.vx),
' y-vel: %spx/u' % round(self.vy),
sep='\n', end='\n\n')
return update_frame, shoot, stop
@staticmethod
def quadrant(x, y, xm, ym):
if ym < y and xm > x:
return 1
elif ym < y and xm < x:
return 2
elif ym > y and xm < x:
return 3
elif ym > y and xm > x:
return 4
else:
return False
def draw_window():
clock.tick(Constants.TICKRATE)
window.fill(Constants.WINDOW_COLOR)
resist_multiplier_text = 'Air Resistance: {:2.2f} m/s'.format(resist_multiplier)
resist_multiplier_label = Fonts.resistMultiplierFont.render(resist_multiplier_text, 1, Fonts.RESISTMULTIPLIERCOLOR)
pg.draw.rect(window, Colors.BLACK, (.8875*Constants.SCREEN_WIDTH, .98*Constants.SCREEN_HEIGHT, Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT))
pg.draw.arrow(window, Colors.MOON, Colors.GREEN, (.8875*Constants.SCREEN_WIDTH, .99*Constants.SCREEN_HEIGHT), (.88*Constants.SCREEN_WIDTH, .99*Constants.SCREEN_HEIGHT), 3, 3)
pg.draw.arrow(window, Colors.MOON, Colors.GREEN, (Constants.SCREEN_WIDTH, .975*Constants.SCREEN_HEIGHT), (.88*Constants.SCREEN_WIDTH, .975*Constants.SCREEN_HEIGHT), 3)
window.blit(resist_multiplier_label, (.8925*Constants.SCREEN_WIDTH, .98*Constants.SCREEN_HEIGHT))
power_multiplier_text = f'Swing Strength: {int(power_multiplier*100)}%'
power_multiplier_label = Fonts.powerMultiplierFont.render(power_multiplier_text, 1, Fonts.POWERMULTIPLIERCOLOR)
pg.draw.rect(window, Colors.BLACK, (0, .98*Constants.SCREEN_HEIGHT, .1125*Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT))
pg.draw.arrow(window, Colors.MOON, Colors.GREEN, (.1125*Constants.SCREEN_WIDTH, .99*Constants.SCREEN_HEIGHT), (.12*Constants.SCREEN_WIDTH, .99*Constants.SCREEN_HEIGHT), 3, 3)
pg.draw.arrow(window, Colors.MOON, Colors.GREEN, (0, .975*Constants.SCREEN_HEIGHT), (.12*Constants.SCREEN_WIDTH, .975*Constants.SCREEN_HEIGHT), 3)
window.blit(power_multiplier_label, (.005*Constants.SCREEN_WIDTH, .98*Constants.SCREEN_HEIGHT))
if not shoot:
pg.draw.arrow(window, Constants.ALINE_COLOR, Constants.ALINE_COLOR, aline[0], aline[1], 5)
pg.draw.arrow(window, Constants.LINE_COLOR, Constants.LINE_COLOR, line[0], line[1], 5)
stroke_text = 'Strokes: %s' % strokes
stroke_label = Fonts.strokeFont.render(stroke_text, 1, Fonts.STROKECOLOR)
if not strokes:
window.blit(stroke_label, (Constants.SCREEN_WIDTH - .21 * Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT - .985 * Constants.SCREEN_HEIGHT))
else:
window.blit(stroke_label, (Constants.SCREEN_WIDTH - (.21+.02*math.floor(math.log10(strokes))) * Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT - .985 * Constants.SCREEN_HEIGHT))
power_text = 'Shot Strength: %sN' % power_display
power_label = Fonts.powerFont.render(power_text, 1, Fonts.POWERCOLOR)
if not shoot: window.blit(power_label, (cursor_pos[0] + .008 * Constants.SCREEN_WIDTH, cursor_pos[1]))
angle_text = 'Angle: %s°' % angle_display
angle_label = Fonts.angleFont.render(angle_text, 1, Fonts.ANGLECOLOR)
if not shoot: window.blit(angle_label, (ball.x - .06 * Constants.SCREEN_WIDTH, ball.y - .01 * Constants.SCREEN_HEIGHT))
if penalty:
penalty_text = f'Out of Bounds! +1 Stroke'
penalty_label = Fonts.penaltyFont.render(penalty_text, 1, Fonts.PENALTYCOLOR)
penalty_rect = penalty_label.get_rect(center=(Constants.SCREEN_WIDTH/2, .225*Constants.SCREEN_HEIGHT))
window.blit(penalty_label, penalty_rect)
toggle_bounds_text = "Use [b] to toggle bounds"
toggle_bounds_label = Fonts.toggleBoundsFont.render(toggle_bounds_text, 1, Fonts.TOGGLEBOUNDSCOLOR)
toggle_bounds_rect = toggle_bounds_label.get_rect(center=(Constants.SCREEN_WIDTH/2, .275*Constants.SCREEN_HEIGHT))
window.blit(toggle_bounds_label, toggle_bounds_rect)
ball.show(window)
pg.display.flip()
def angle(cursor_pos):
x, y, xm, ym = ball.x, ball.y, cursor_pos[0], cursor_pos[1]
if x-xm:
angle = math.atan((y - ym) / (x - xm))
elif y > ym:
angle = math.pi/2
else:
angle = 3*math.pi/2
q = ball.quadrant(x,y,xm,ym)
if q: angle = math.pi*math.floor(q/2) - angle
if round(angle*deg) == 360:
angle = 0
if x > xm and not round(angle*deg):
angle = math.pi
return angle
def arrow(screen, lcolor, tricolor, start, end, trirad, thickness=2):
pg.draw.line(screen, lcolor, start, end, thickness)
rotation = (math.atan2(start[1] - end[1], end[0] - start[0])) + math.pi/2
pg.draw.polygon(screen, tricolor, ((end[0] + trirad * math.sin(rotation),
end[1] + trirad * math.cos(rotation)),
(end[0] + trirad * math.sin(rotation - 120*rad),
end[1] + trirad * math.cos(rotation - 120*rad)),
(end[0] + trirad * math.sin(rotation + 120*rad),
end[1] + trirad * math.cos(rotation + 120*rad))))
setattr(pg.draw, 'arrow', arrow)
def distance(x, y):
return math.sqrt(x**2 + y**2)
def update_values(quit, rkey, skey, shoot, xb, yb, strokes, x_bounded):
for event in pg.event.get():
if event.type == pg.QUIT:
quit = True
if event.type == pg.KEYDOWN:
if event.key == pg.K_ESCAPE:
quit = True
if event.key == pg.K_RIGHT:
if rkey != max(resist_dict):
rkey += 1
if event.key == pg.K_LEFT:
if rkey != min(resist_dict):
rkey -= 1
if event.key == pg.K_UP:
if skey != max(strength_dict):
skey += 1
if event.key == pg.K_DOWN:
if skey != min(strength_dict):
skey -= 1
if event.key == pg.K_b:
x_bounded = not x_bounded
if event.key == pg.K_q:
rkey = min(resist_dict)
skey = max(strength_dict)
x_bounded = True
if event.key == pg.K_e:
rkey = max(resist_dict)
skey = max(strength_dict)
x_bounded = False
if event.type == pg.MOUSEBUTTONDOWN:
if not shoot:
shoot, stop = True, False
strokes, xb, yb = hit_ball(strokes)
return quit, rkey, skey, shoot, xb, yb, strokes, x_bounded
def hit_ball(strokes):
x, y = ball.x, ball.y
xb, yb = ball.x, ball.y
power = power_multiplier/4 * distance(line_ball_x, line_ball_y)
print('\n\nBall Hit!')
print('\npower: %sN' % round(power, 2))
ang = angle(cursor_pos)
print('angle: %s°' % round(ang * deg, 2))
print('cos(a): %s' % round(math.cos(ang), 2)), print('sin(a): %s' % round(math.sin(ang), 2))
ball.vx, ball.vy = power * math.cos(ang), -power * math.sin(ang)
strokes += 1
return strokes, xb, yb
def initialize():
pg.init()
pg.display.set_caption('Golf')
window = pg.display.set_mode((Constants.SCREEN_WIDTH, Constants.SCREEN_HEIGHT))
pg.event.set_grab(True)
pg.mouse.set_cursor((8, 8), (0, 0), (0, 0, 0, 0, 0, 0, 0, 0), (0, 0, 0, 0, 0, 0, 0, 0))
return window
rad, deg = math.pi/180, 180/math.pi
x, y, power, ang, strokes = [0]*5
xb, yb = None, None
shoot, penalty, stop, quit, x_bounded = [False]*5
p_ticks, update_frame = 0, 0
ball = Ball(Constants.START_X, Constants.START_Y)
clock = pg.time.Clock()
strength_dict = {0: .01, 1: .02, 2: .04, 3: .08, 4: .16, 5: .25, 6: .50, 7: .75, 8: 1}; skey = 6
resist_dict = {0: 0, 1: .01, 2: .02, 3: .03, 4: .04, 5: .05, 6: .1, 7: .2, 8: .3, 9: .4, 10: .5, 11: .6, 12: .7,
13: .8, 14: .9, 15: 1, 16: 1.25, 17: 1.5, 18: 1.75, 19: 2, 20: 2.5, 21: 3, 22: 3.5, 23: 4, 24: 4.5,
25: 5}; rkey = 7
if __name__ == '__main__':
window = initialize()
while not quit:
power_multiplier = strength_dict[skey]
resist_multiplier = resist_dict[rkey]
seconds = (pg.time.get_ticks()-p_ticks)/1000
if seconds > 1.2: penalty = False
cursor_pos = pg.mouse.get_pos()
line = [(ball.x, ball.y), cursor_pos]
line_ball_x, line_ball_y = cursor_pos[0] - ball.x, cursor_pos[1] - ball.y
aline = [(ball.x, ball.y), (ball.x + .015 * Constants.SCREEN_WIDTH, ball.y)]
if not shoot:
power_display = round(
distance(line_ball_x, line_ball_y) * power_multiplier/5)
angle_display = round(angle(cursor_pos) * deg)
else:
if stop or (abs(ball.vy) < 5 and abs(ball.vx) < 1 and abs(ball.y - (Constants.START_Y - 2)) <= Constants.BOUNCE_FUZZ):
shoot = False
#ball.y = Constants.START_Y
print('\nThe ball has come to a rest!')
update_frame = 0
else:
update_frame, shoot, stop = ball.update(update_frame)
if not Constants.X_BOUNDS_BARRIER < ball.x < Constants.SCREEN_WIDTH:
shoot = False
print(f'\nOut of Bounds! Pos: {round(ball.x), round(ball.y)}')
penalty = True
p_ticks = pg.time.get_ticks()
strokes += 1
if Constants.X_BOUNDS_BARRIER < xb < Constants.SCREEN_WIDTH:
ball.x = xb
else:
ball.x = Constants.START_X
ball.y = yb
quit, rkey, skey, shoot, xb, yb, strokes, x_bounded = update_values(quit, rkey, skey, shoot, xb, yb, strokes, x_bounded)
draw_window()
print("\nShutting down...")
pg.quit()
monospace
andcourier new
? Either be generic or specific. \$\endgroup\$