Created
February 16, 2025 17:35
-
-
Save sdsalyer/4e71d051e3ca9593f35579261fb2bfa8 to your computer and use it in GitHub Desktop.
Python Arcade work-in-progress bubble game
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
""" | |
.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