Skip to content

Instantly share code, notes, and snippets.

@sdsalyer
Created February 16, 2025 17:35
Show Gist options
  • Save sdsalyer/4e71d051e3ca9593f35579261fb2bfa8 to your computer and use it in GitHub Desktop.
Save sdsalyer/4e71d051e3ca9593f35579261fb2bfa8 to your computer and use it in GitHub Desktop.
Python Arcade work-in-progress bubble game
"""
.oOo.oOo.
by b3rts
"""
from arcade.types import Color
from typing_extensions import override
import arcade
import random
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
WINDOW_TITLE = ".oOo.oOo."
RED_COLOR = Color(r=200, g=100, b=100, a=100)
GREEN_COLOR = Color(r=100, g=200, b=100, a=100)
WALL_COLOR = Color(r=100, g=100, b=100, a=100)
PLAYER_COLOR = Color(r=100, g=100, b=200, a=100)
PLAYER_INITIAL_RADIUS = 20
BIGN_BUBBLE_MAX = 10
SMOL_BUBBLE_MAX = 100
# --- Physics forces. Higher number, faster accelerating.
# Gravity
GRAVITY = 0
# Damping - Amount of speed lost per second
DEFAULT_DAMPING = 0.99
# Friction between objects
PLAYER_FRICTION = 0.0
WALL_FRICTION = 0.0
BUBBLE_FRICTION = 0.0
# TODO: Set mass = radius/100 ?
# Mass (defaults to 1)
PLAYER_MASS = 0.5
BUBBLE_MASS = 0.5
# Keep player from going too fast
PLAYER_MOVE_FORCE = 60
PLAYER_MAX_HORIZONTAL_SPEED = 300
PLAYER_MAX_VERTICAL_SPEED = PLAYER_MAX_HORIZONTAL_SPEED
BOUNDARIES = [
{
'x': 0,
'y': WINDOW_HEIGHT/2,
'width': 14,
'height': WINDOW_HEIGHT
},
{
'x': WINDOW_WIDTH/2,
'y': 0,
'width': WINDOW_WIDTH,
'height': 14
},
{
'x': WINDOW_WIDTH,
'y': WINDOW_HEIGHT/2,
'width': 14,
'height': WINDOW_HEIGHT
},
{
'x': WINDOW_WIDTH/2,
'y': WINDOW_HEIGHT,
'width': WINDOW_WIDTH,
'height': 14
},
]
# https://api.arcade.academy/en/latest/api_docs/api/sprites.html#arcade.SpriteCircle
class Bubble(arcade.SpriteCircle):
def __init__(self, center_x: int, center_y: int, radius: int, is_player: bool = False, color: Color = PLAYER_COLOR, soft: bool = True, **kwargs):
super().__init__(radius, color, soft, **kwargs)
self.center_x: int = center_x
self.center_y: int = center_y
self.radius: int = radius
self.angle: int = random.randrange(360)
# self.change_x: float = random.uniform(-0.75, +0.75)
# self.change_y: float = random.uniform(-0.75, +0.75)
self.color: Color = color
self.is_player: bool = is_player
self.width: int = radius
self.height: int = radius
def grow(self, rad: int):
self.radius += rad
self.width += rad * 2
self.height += rad * 2
def shrink(self, rad: int):
self.radius -= rad
self.width -= rad
self.height -= rad
class GameView(arcade.View):
"""
Main application class.
"""
def __init__(self):
super().__init__()
self.score: int = 0
self.fps_text: arcade.Text | None = None
self.score_text: arcade.Text | None = None
self.player_sprite: arcade.Sprite | None = None
self.player_list: arcade.SpriteList[arcade.Sprite] | None = None
self.bubble_list: arcade.SpriteList[arcade.Sprite] | None = None
self.wall_list: arcade.SpriteList[arcade.Sprite] | None = None
self.outlines: arcade.shape_list.ShapeElementList[arcade.shape_list.Shape] | None = None
self.physics_engine: arcade.PymunkPhysicsEngine | None = None
#self.space = pymunk.Space()
self.up_pressed: bool = False
self.down_pressed: bool = False
self.left_pressed: bool = False
self.right_pressed: bool = False
def setup(self):
self.score = 0
self.score_text = arcade.Text(
f"Score: {self.score}",
10, 20, arcade.color.WHITE, 14
)
self.fps_text = arcade.Text(
f"FPS: {arcade.get_fps(60):5.1f}",
10, WINDOW_HEIGHT - 20, arcade.color.ASH_GREY, 14
)
self.player_list = arcade.SpriteList()
self.bubble_list = arcade.SpriteList()
self.wall_list = arcade.SpriteList()
self.outlines = arcade.shape_list.ShapeElementList()
for wall in BOUNDARIES:
wall = arcade.SpriteSolidColor(wall['width'], wall['height'], wall['x'], wall['y'], WALL_COLOR)
self.wall_list.append(wall)
self.player_sprite = Bubble(
center_x=WINDOW_WIDTH/2 ,
center_y=WINDOW_HEIGHT/2,
radius = PLAYER_INITIAL_RADIUS,
is_player = True
)
self.player_sprite.angle = 0
print(f"Player size: {self.player_sprite.size}")
# Add to player sprite list
self.player_list.append(self.player_sprite)
# biggin bubbles
for i in range(BIGN_BUBBLE_MAX):
bubble = Bubble(
center_x = random.randrange(WINDOW_WIDTH),
center_y = random.randrange(WINDOW_HEIGHT),
radius = random.randint(20, 100),
soft = True,
color = Color(
r = random.randrange(60, 160, 20),
g = random.randrange(60, 160, 20),
b = random.randrange(60, 160, 20),
a = 200 #random.randrange(0, 255, 16))
)
)
# Add the coin to the lists
self.bubble_list.append(bubble)
# smol bubbles
for i in range(SMOL_BUBBLE_MAX):
bubble = Bubble(
center_x = random.randrange(WINDOW_WIDTH),
center_y = random.randrange(WINDOW_HEIGHT),
radius = random.randint(4, 20),
soft = True,
color = Color(
r = random.randrange(60, 160, 20),
g = random.randrange(60, 160, 20),
b = random.randrange(60, 160, 20),
a = 200 #random.randrange(0, 255, 16))
)
)
# Add the coin to the lists
self.bubble_list.append(bubble)
for bubble in self.bubble_list:
# Don't start on top of the player
if int(bubble.center_x - self.player_sprite.center_x)^2 < 100^2:
bubble.center_x = bubble.center_x * 3
if int(bubble.center_y - self.player_sprite.center_y)^2 < 100^2:
bubble.center_y = bubble.center_y * 3
# --- Pymunk Physics Engine Setup ---
# The default damping for every object controls the percent of velocity
# the object will keep each second. A value of 1.0 is no speed loss,
# 0.9 is 10% per second, 0.1 is 90% per second.
# For top-down games, this is basically the friction for moving objects.
# For platformers with gravity, this should probably be set to 1.0.
# Default value is 1.0 if not specified.
damping = DEFAULT_DAMPING
# Set the gravity. (0, 0) is good for outer space and top-down.
gravity = (0, -GRAVITY)
# Create the physics engine
self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping, gravity=gravity)
# handle specific collisions
def on_hit_bubble_bubble(bubble1_sprite: Bubble, bubble2_sprite: Bubble, _arbiter, _space, _data):
# Have we collected this?
print("--HIT--")
print(f"- bubble1 size: {bubble1_sprite.size}")
print(f"- bubble1 radius: {bubble1_sprite.radius}")
print(f"- bubble2 size: {bubble2_sprite.size}")
print(f"- bubble2 radius: {bubble2_sprite.radius}")
if bubble2_sprite.radius <= bubble1_sprite.radius:
print("--> GROW")
rad = int(bubble2_sprite.radius / 4)
bubble1_sprite.grow(rad)
#self.physics_engine.get_physics_object(bubble1_sprite).shape.radius += rad
if bubble1_sprite.is_player:
self.score += int(rad * random.uniform(1.0, 2.0) * 100)
print(f"--> bubble2 size: {bubble2_sprite.size}")
print(f"--> bubble2 radius: {bubble2_sprite.radius}")
print(f"--> bubble1 new size: {bubble1_sprite.size}")
print(f"--> bubble1 new radius: {bubble1_sprite.radius}")
bubble2_sprite.remove_from_sprite_lists()
elif bubble2_sprite.radius > bubble1_sprite.radius:
print("--> shrink")
rad = int(bubble2_sprite.radius / 4)
bubble1_sprite.shrink(rad)
if bubble1_sprite.is_player:
self.score -= int(rad * random.uniform(1.0, 2.0) * 100)
print(f"--> bubble2 size: {bubble2_sprite.size}")
print(f"--> bubble2 radius: {bubble2_sprite.radius}")
print(f"--> bubble1 new size: {bubble1_sprite.size}")
print(f"--> bubble1 new radius: {bubble1_sprite.radius}")
bubble2_sprite.remove_from_sprite_lists()
self.physics_engine.add_collision_handler("player", "bubble", post_handler=on_hit_bubble_bubble)
self.physics_engine.add_collision_handler("bubble", "bubble", post_handler=on_hit_bubble_bubble)
# Add the player.
# For the player, we set the damping to a lower value, which increases
# the damping rate. This prevents the character from traveling too far
# after the player lets off the movement keys.
# Setting the moment of inertia to PymunkPhysicsEngine.MOMENT_INF prevents it from
# rotating.
# Friction normally goes between 0 (no friction) and 1.0 (high friction)
# Friction is between two objects in contact. It is important to remember
# in top-down games that friction moving along the 'floor' is controlled
# by damping.
self.physics_engine.add_sprite(self.player_sprite,
friction=PLAYER_FRICTION,
mass=PLAYER_MASS,
radius=self.player_sprite.radius,
moment_of_inertia=arcade.PymunkPhysicsEngine.MOMENT_INF,
collision_type="player",
max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED,
elasticity=0.5)
# Create the walls.
# By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
# move.
# Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
# PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
# repositioned by code and don't respond to physics forces.
# Dynamic is default.
self.physics_engine.add_sprite_list(self.bubble_list,
friction=BUBBLE_FRICTION,
mass=BUBBLE_MASS,
collision_type="bubble",
body_type=arcade.PymunkPhysicsEngine.DYNAMIC,
elasticity=0.5)
self.physics_engine.add_sprite_list(self.wall_list,
friction=WALL_FRICTION,
collision_type="wall",
body_type=arcade.PymunkPhysicsEngine.STATIC,
elasticity=1.0)
# give bubbles an initial inertia
for bubble in self.bubble_list:
force = (10*random.uniform(-PLAYER_MOVE_FORCE, +PLAYER_MOVE_FORCE), 10*random.uniform(-PLAYER_MOVE_FORCE, +PLAYER_MOVE_FORCE))
self.physics_engine.apply_force(bubble, force)
# Don't show the mouse cursor
self.window.set_mouse_visible(False)
# Set the background color
self.background_color = arcade.color.EERIE_BLACK
def reset(self):
# Do changes needed to restart the game here if you want to support that
pass
@override
def on_draw(self):
"""Render the screen."""
# This command should happen before we start drawing. It will clear
# the screen to the background color, and erase what we drew last frame.
self.clear()
# Draw all the sprites.
self.wall_list.draw()
self.bubble_list.draw()
# for bubble in self.bubble_list:
# color = RED_COLOR if bubble.radius > self.player_sprite.radius else GREEN_COLOR
# arcade.draw_circle_outline(bubble.center_x, bubble.center_y, bubble.width/2, color)
self.player_list.draw()
# for player in self.player_list:
# arcade.draw_circle_outline(player.center_x, player.center_y, player.width/2, player.color)
self.outlines.draw()
# Put the text on the screen.
self.score_text.value = f"Score: {self.score}"
self.score_text.draw()
# Get & draw the FPS for the last 60 frames
if arcade.timings_enabled():
self.fps_text.value = f"FPS: {arcade.get_fps(60):5.1f}"
self.fps_text.draw()
@override
def on_update(self, delta_time: float):
"""
All the logic to move, and the game logic goes here.
Normally, you'll call update() on the sprite lists that
need it.
"""
if self.score <= 0:
self.score = 0
if self.player_sprite.radius < 2.0:
print("--GAME OVER--")
self.player_sprite.remove_from_sprite_lists()
if self.player_list:
# mass = 0.5
# radius = self.player_sprite.radius
# inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0))
# body = pymunk.Body(mass, inertia)
# x = self.player_sprite.center_x
# y = self.player_sprite.center_y
# body.position = x, y
# shape = pymunk.Circle(body, radius, pymunk.Vec2d(0, 0))
# shape.friction = 0.3
# self.space.add(body, shape)
# Update player forces based on keys pressed
if self.up_pressed and not self.down_pressed:
# Create a force to the left. Apply it.
force = (0, +PLAYER_MOVE_FORCE)
self.physics_engine.apply_force(self.player_sprite, force)
# Set friction to zero for the player while moving
self.physics_engine.set_friction(self.player_sprite, 0.0)
elif self.down_pressed and not self.up_pressed:
# Create a force to the right. Apply it.
force = (0, -PLAYER_MOVE_FORCE)
self.physics_engine.apply_force(self.player_sprite, force)
# Set friction to zero for the player while moving
self.physics_engine.set_friction(self.player_sprite, 0.0)
if self.left_pressed and not self.right_pressed:
# Create a force to the left. Apply it.
force = (-PLAYER_MOVE_FORCE, 0)
self.physics_engine.apply_force(self.player_sprite, force)
# Set friction to zero for the player while moving
self.physics_engine.set_friction(self.player_sprite, 0.0)
elif self.right_pressed and not self.left_pressed:
# Create a force to the right. Apply it.
force = (+PLAYER_MOVE_FORCE, 0)
self.physics_engine.apply_force(self.player_sprite, force)
# Set friction to zero for the player while moving
self.physics_engine.set_friction(self.player_sprite, 0.0)
else:
# Player's feet are not moving. Therefore up the friction so we stop.
self.physics_engine.set_friction(self.player_sprite, 0.0)
self.outlines.clear()
for bubble in self.bubble_list:
color = RED_COLOR if bubble.radius > self.player_sprite.radius else GREEN_COLOR
bubble.color = color
outline = arcade.shape_list.create_ellipse_outline(
center_x=bubble.center_x,
center_y=bubble.center_y,
width=bubble.width,
height=bubble.width,
color=color,
border_width=2)
self.outlines.append(outline)
for bubble in self.player_list:
color = bubble.color
outline = arcade.shape_list.create_ellipse_outline(
center_x=bubble.center_x,
center_y=bubble.center_y,
width=bubble.width,
height=bubble.width,
color=color,
border_width=4)
self.outlines.append(outline)
# Move items in the physics engine
self.physics_engine.step()
self.player_list.update()
self.bubble_list.update()
# Generate a list of all sprites that collided with the player.
# hit_list = arcade.check_for_collision_with_list(self.player_sprite, self.bubble_list)
# Loop through each colliding sprite, change it, and add to the score.
# for bubble in hit_list:
@override
def on_key_press(self, symbol: int, modifiers: int):
"""Called whenever a key on the keyboard is pressed."""
if symbol == arcade.key.SPACE:
if arcade.timings_enabled():
arcade.disable_timings()
else:
arcade.enable_timings()
if symbol == arcade.key.UP or symbol == arcade.key.W:
self.up_pressed = True
if symbol == arcade.key.DOWN or symbol == arcade.key.S:
self.down_pressed = True
if symbol == arcade.key.LEFT or symbol == arcade.key.A:
self.left_pressed = True
if symbol == arcade.key.RIGHT or symbol == arcade.key.D:
self.right_pressed = True
@override
def on_key_release(self, symbol: int, modifiers: int):
"""Called whenever the user lets off a previously pressed key."""
if symbol == arcade.key.UP or symbol == arcade.key.W:
self.up_pressed = False
if symbol == arcade.key.DOWN or symbol == arcade.key.S:
self.down_pressed = False
if symbol == arcade.key.LEFT or symbol == arcade.key.A:
self.left_pressed = False
if symbol == arcade.key.RIGHT or symbol == arcade.key.D:
self.right_pressed = False
def main():
"""Main function"""
# Create a window class. This is what actually shows up on screen
window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
# Create and setup the GameView
game = GameView()
game.setup()
# Show GameView on screen
window.show_view(game)
# Start the arcade game loop
arcade.run()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment