Skip to content

Instantly share code, notes, and snippets.

@dfsklar
Last active January 23, 2021 01:17
Show Gist options
  • Save dfsklar/08f75bf79eb09c5fc786e37770160afc to your computer and use it in GitHub Desktop.
Save dfsklar/08f75bf79eb09c5fc786e37770160afc to your computer and use it in GitHub Desktop.
arcmunk_bouncing_v_funnel.py
"""
BOUNCING "V" program version 1
This is the version that is the "starter" for the coding class exercises.
For commentary and an introduction to world/local coordinate systems:
https://docs.google.com/document/d/14PU6DO_4kwF27xfv4P_2hXptYvCZkTm0tff5BC7V0PQ
"""
import arcade
import pymunk
import random
import timeit
import math
SCREEN_TITLE = "Pymunk Bouncing 'V' Example"
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 800
SCREEN_HALF_WIDTH = SCREEN_WIDTH / 2
ROTATION_SPEED = 2.9 # Increase the number to make the motor spin the attached body faster
GROUND_Y_LOC = 100
MOTOR_PIVOT_POSITION = (150,600)
SCREEN_UPDATE_FREQUENCY = 120.0 # Larger values make the ball movement *SLOWER*
GRAVITY_DOWNWARD = -900
GRAVITY_RIGHTWARD = 0
"""
The arcade system automatically provides you with a Python "class" called "arcade.Sprite".
You use a sprite to draw any object that is not just "background".
For example, if your platformer game has a hero character and coins that the hero is supposed to collect,
the hero and coins are sprites, but the scenery (e.g. clouds and forests in the background) are not sprites.
Here we extend the Sprite class to create a new class called CircleSprite that provides the feature of having
a bitmap image "paint" a circular sprite.
We use this for the display of the balls that we can drop into the scene.
"""
class CircleSprite_for_pymunk_shape(arcade.Sprite):
def __init__(self, image_file_location, pymunk_shape):
super().__init__(image_file_location, center_x=pymunk_shape.body.position.x, center_y=pymunk_shape.body.position.y)
self.width = pymunk_shape.radius * 2
self.height = pymunk_shape.radius * 2
setattr(self, 'pymunk_shape', pymunk_shape)
class CircleSprite(arcade.Sprite):
def __init__(self, image_file_location, centerX, centerY, radius):
super().__init__(image_file_location, center_x=centerX, center_y=centerY)
self.width = radius * 2
self.height = radius * 2
setattr(self, 'arcade_shape', ( centerX, centerY, radius ))
class MyGame(arcade.Window):
"""
This is the first time you're seeing a python concept called a "class".
Think of it as a giant storage box that contains not only data-storage variables but also has "behavior"
in the form of built-in functions.
So it is "data PLUS behavior" all wrapped up in one giant box.
The most important thing to note about using classes is that you must use "self." to refer to a
function or data-storage box that is part of the class.
You'll see a lot of references to "self." below.
"""
# This function is provided by Sklar -- DO NOT MODIFY THIS:
def register_body_into_scene(self, body, *objs):
print('###############\nWe are now registering a body into the physics universe.')
print('Body type is: ' + 'STATIC' if body.body_type == pymunk.Body.STATIC else 'DYNAMIC')
# First register the BODY with the physics universe
self.space.add(body)
for shape in body.shapes:
# Next register the SHAPE (for example, a segment) with the physics universe.
self.space.add(shape)
# Next, for segment shapes, register with the graphics system to ensure they are drawn.
if type(shape) == pymunk.shapes.Segment:
self.list_of_segments.append(shape)
# Every arcade program must have an INITIALIZATION method that sets up the physics engine and creates
# the initial set of physics objects and "sprites".
# In this case, the static ramps/walls must be set up here, but the balls are created/dropped dynamically
# based on user click activity, so the balls are not created here.
def __init__(self, width, height, title):
super().__init__(width, height, title)
arcade.set_background_color(arcade.color.DARK_SLATE_GRAY)
self.time = 0
# Set up the physics "universe" -- an empty space initially having only gravity in place.
self.space = pymunk.Space()
self.space.gravity = (GRAVITY_RIGHTWARD, GRAVITY_DOWNWARD)
# The graphics system needs a list of all line-segments that need to be drawn for each animation frame.
# The function "register_body_into_scene" that I've provided automatically handles populating this list.
self.list_of_segments = []
# Similarly, we need to keep a list of the circular balls that need to be drawn.
self.ball_list = arcade.SpriteList()
# PHYSICS BODY: The "V" shape funnel -- this is a STATIC physics body.
#
v_body = pymunk.Body(body_type=pymunk.Body.STATIC)
v_body.position = (SCREEN_HALF_WIDTH, GROUND_Y_LOC) # <<< WORLD coordinate system
segment1 = pymunk.Segment(body=v_body, a=(50,300), b=(12,0), radius=1) # < LOCAL coordinate system
segment2 = pymunk.Segment(body=v_body, a=(-50,300), b=(-12,0), radius=1)
# For STATIC segments/shapes, you do NOT need to specify mass.
# BUT: you must specify elasticity if you want other things to be able to "bounce" off.
for a_segment in (segment1, segment2):
a_segment.elasticity = 0.8
self.register_body_into_scene(v_body)
# PHYSICS BODY: The entrance ramp. This is also STATIC.
ground_body = pymunk.Body(body_type=pymunk.Body.STATIC)
ground_body.position = (0, 700) # WORLD coordinates
self.register_body_into_scene(ground_body,
pymunk.Segment(body=ground_body, a=(0, -100), b=(400, -300), radius=1)) # LOCAL coordinates
# PHYSICS BODY: The "centerpoint" for the motorized box-with-hole
motorcenter_body = pymunk.Body(body_type=pymunk.Body.STATIC)
# Right now, I'm placing this motor at a random location. You will want to give it a different position.
motorcenter_body.position = MOTOR_PIVOT_POSITION
# Most of the time, we want a physics body to have at least one line segment. This lets it act
# as a ramp, or a seesaw plank, etc.
# But in this case, the motor is a static thing that really is just a "pivot point" sitting out in space.
# It's not made up of any line segments at all.
# So we are going to register the "motor center" as a body with NO children segments:
self.register_body_into_scene(motorcenter_body)
# But it would be nice to give the pivot point an appearance even though it's not in the physics engine.
# We can thus register an Arcade circular sprite just to give it an appearance.
# We're treating it like a ball, but this ball is static and thus does not "fall".
sprite = CircleSprite(":resources:images/pinball/bumper.png", \
motorcenter_body.position[0],
motorcenter_body.position[1], 8)
self.ball_list.append(sprite)
# PHYSICS BODY: The plank that the motor keeps rotating. This is dynamic of course!
# Currently, this is a simple line segment, but you might want to think about making it
# a "bucket" of some sort, providing a way to launch balls in a "spaced-out" interval way.
motor_controlled_body = pymunk.Body(body_type=pymunk.Body.DYNAMIC)
motor_controlled_body.position = MOTOR_PIVOT_POSITION
thickness = 0.5
motor_controlled_shape = \
pymunk.Segment(motor_controlled_body,
a=( -60, 0),
b=( 60, 0),
radius=thickness)
motor_controlled_shape.friction = 1
motor_controlled_shape.mass = 4
self.register_body_into_scene(motor_controlled_body, motor_controlled_shape)
# Set up the connection between the pivot point and the seesaw "plank"
self.rotation_center_joint = pymunk.PinJoint( \
a=motor_controlled_body, b=motorcenter_body, \
anchor_a=(0,0), anchor_b=(0,0))
self.space.add(self.rotation_center_joint)
# Now, motorize that connection
motor = pymunk.SimpleMotor(motorcenter_body, motor_controlled_body, ROTATION_SPEED)
self.space.add(motor)
def draw_all_linesegments(self):
for line in self.list_of_segments:
body = line.body
if body.body_type == pymunk.Body.STATIC:
color = arcade.color.GRAY
else:
color = arcade.color.WHITE
pv1 = body.position + line.a.rotated(body.angle)
pv2 = body.position + line.b.rotated(body.angle)
arcade.draw_line(pv1.x, pv1.y, pv2.x, pv2.y, color, 2)
# DO NOT MODIFY THIS:
def on_draw(self):
"""
Draw the screen, showing the current locations/orientations of all objects in the physics universe.
"""
# This command has to happen before we start drawing
arcade.start_render()
draw_start_time = timeit.default_timer()
self.draw_all_linesegments()
self.ball_list.draw()
def on_mouse_press(self, x, y, which_button, which_modifiers):
shift_key_was_pressed = (which_modifiers == 1) # You might want to behave differently if SHIFT key pressed!
self.drop_ball_into_scene(x, y)
def drop_ball_into_scene(self, x, y):
mass = 1.0
radius = 11
friction = 0.3
body = pymunk.Body(body_type=pymunk.Body.DYNAMIC) # mass, inertia)
body.position = (x, y)
shape = pymunk.Circle(body, radius)
shape.mass = mass
shape.friction = friction
shape.elasticity = 1.0
self.register_body_into_scene(body, shape)
# The graphical representation of the balls must be done using Arcade sprites:
sprite = CircleSprite_for_pymunk_shape(":resources:images/items/gold_1.png", shape)
self.ball_list.append(sprite)
# DO NOT MODIFY THIS:
def on_update(self, delta_time):
start_time = timeit.default_timer()
# Check for balls that fall off the screen.
for ball in []: #self.ball_list:
# If the ball's Y coordinate is below the screen (Y is negative):
if ball.pymunk_shape.body.position.y < 0:
# Remove this ball from physics engine
self.space.remove(ball.pymunk_shape, ball.pymunk_shape.body)
# Remove this ball from the list of sprites used for the graphics engine
ball.remove_from_sprite_lists()
# Update physics
# See "Game loop / moving time forward"
# http://www.pymunk.org/en/latest/overview.html#game-loop-moving-time-forward
self.space.step(1 / SCREEN_UPDATE_FREQUENCY)
# Keep in mind: The "physics engine" has its own non-graphics representation of where every ball is located.
# The physics engine does not automatically update the "sprites" that represent the balls graphically.
# It is your job to visit every ball and "copy" the physics location into the sprite's location.
for ball in self.ball_list:
if hasattr(ball, 'pymunk_shape'):
ball.center_x = ball.pymunk_shape.body.position.x
ball.center_y = ball.pymunk_shape.body.position.y
ball.angle = math.degrees(ball.pymunk_shape.body.angle)
self.time = timeit.default_timer() - start_time
# OK HERE WE GO! All of the above is simply "teaching" the python system about our game.
# But the next two lines actually "create" the game and then "launches" the Arcade scheduling system.
MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
arcade.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment