Skip to content

Instantly share code, notes, and snippets.

@dfsklar
Last active January 3, 2021 23:35
Show Gist options
  • Save dfsklar/223ad0ae87f8ff0984c98a15f68e783b to your computer and use it in GitHub Desktop.
Save dfsklar/223ad0ae87f8ff0984c98a15f68e783b to your computer and use it in GitHub Desktop.
Seesaw program
"""
SEESAW version 4
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 See-Saw Example"
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 800
SCREEN_HALF_WIDTH = SCREEN_WIDTH / 2
GROUND_Y_LOC = 300
FULCRUM_HEIGHT_FROM_GROUND = 40
GROUND_ENABLED = True
SEESAW_WIDTH = 400
SEESAW_HALF_WIDTH = SEESAW_WIDTH / 2
FULCRUM_Y_LOC = GROUND_Y_LOC + FULCRUM_HEIGHT_FROM_GROUND
MAKE_FULCRUM_BE_DYNAMIC = False
SCREEN_UPDATE_FREQUENCY = 120.0 # Larger values make the ball movement *SLOWER*
# If automatic ball dropping is disabled, then only a mouse click generates a new ball for the scene to process.
AUTOMATIC_BALL_DROP_ENABLED = False
AUTOMATIC_BALL_DROP_DELAY = 1 # Smaller values make the balls drop more frequently
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 drop from the sky onto our seesaw.
"""
class CircleSprite(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 MyGame(arcade.Window):
"""
This is the first time you're seeing a python concept called "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 is provided by Sklar -- DO NOT MODIFY THIS:
def register_body_into_scene(self, body, *objs):
print('###############')
print(str(body.body_type))
# Register with the graphics part of the process
for obj in objs:
if type(obj) == pymunk.shapes.Segment:
self.list_of_segments.append(obj)
# Register with the physics engine
if not (body.body_type == pymunk.Body.STATIC):
self.space.add(body)
for shape in body.shapes:
self.space.add(shape)
else:
# Static bodies must not be registered, but their internal shapes must be.
for shape in body.shapes:
self.space.add(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 floor and the pegs must be set up here, but the balls are created/dropped dynamically
# after "time has begun" 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
self.space = pymunk.Space()
self.space.gravity = (GRAVITY_RIGHTWARD, GRAVITY_DOWNWARD)
self.list_of_segments = []
# FIRST PHYSICS BODY: The seesaw's fulcrum.
# We've tried both static and dynamic fulcrums. The dynamic case is interesting because it allows simulating
# playground equipment that is not properly installed!
if MAKE_FULCRUM_BE_DYNAMIC:
fulcrum = pymunk.Body(body_type=pymunk.Body.DYNAMIC)
else:
fulcrum = pymunk.Body(body_type=pymunk.Body.STATIC)
fulcrum.position = (SCREEN_HALF_WIDTH, GROUND_Y_LOC) # <<< world coordinate system
segment1 = pymunk.Segment(body=fulcrum, a=(0,FULCRUM_HEIGHT_FROM_GROUND), b=(-10,0), radius=1) # < LOCAL coords
segment2 = pymunk.Segment(body=fulcrum, a=(0,FULCRUM_HEIGHT_FROM_GROUND), b=( 10,0), radius=1)
segment3 = pymunk.Segment(body=fulcrum, a=(-10,0), b=(10,0), radius=1)
if MAKE_FULCRUM_BE_DYNAMIC:
# If we are specifying the fulcrum as being dynamic, we need to specify a mass value for each of its 3 sides.
for segment in (segment1, segment2, segment3):
segment.mass = 0.5
self.register_body_into_scene(fulcrum, segment1, segment2, segment3)
# SECOND PHYSICS BODY: The ground. This is static.
# We are not required to specify the mass (weight) of a static body.
if GROUND_ENABLED:
ground_body = pymunk.Body(body_type=pymunk.Body.STATIC)
ground_body.position = (SCREEN_HALF_WIDTH, GROUND_Y_LOC) # world
self.register_body_into_scene(ground_body,
pymunk.Segment(body=ground_body, a=(-SCREEN_HALF_WIDTH, 0), b=(SCREEN_HALF_WIDTH, 0), radius=1)) # local
# THIRD PHYSICS BODY: The seesaw plank. This is dynamic of course!
seesaw_body = pymunk.Body(body_type=pymunk.Body.DYNAMIC)
seesaw_body.position = (SCREEN_HALF_WIDTH, GROUND_Y_LOC)
# Because this is a DYNAMIC body that is directly involved with collisions with other dynamic objects (balls),
# we must specify the friction and mass for it.
thickness = 1
seesaw_plank = \
pymunk.Segment(seesaw_body,
a=( -SEESAW_HALF_WIDTH, FULCRUM_HEIGHT_FROM_GROUND),
b=( SEESAW_HALF_WIDTH, FULCRUM_HEIGHT_FROM_GROUND),
radius=thickness)
seesaw_plank.friction = 1
seesaw_plank.mass = 1
self.register_body_into_scene(seesaw_body, seesaw_plank)
# Set up the connection between the fulcrum point and the seesaw "plank"
rotation_center_joint = pymunk.PinJoint(a=seesaw_body, b=fulcrum, anchor_a=(0,FULCRUM_HEIGHT_FROM_GROUND), anchor_b=(0,FULCRUM_HEIGHT_FROM_GROUND))
self.space.add(rotation_center_joint)
# Let's start keeping a list of all of the dynamic balls in the scene.
self.ball_list = arcade.SpriteList()
if AUTOMATIC_BALL_DROP_ENABLED:
# How many "ticks" should the program wait before "dropping" the first ball into the playing area
self.ticks_to_next_ball = 10
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 = 0.5
radius = 15
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
self.register_body_into_scene(body, shape)
# The graphical representation of the balls must be done using Arcade sprites:
sprite = CircleSprite(":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()
if AUTOMATIC_BALL_DROP_ENABLED:
self.ticks_to_next_ball -= 1
if self.ticks_to_next_ball <= 0:
self.ticks_to_next_ball = AUTOMATIC_BALL_DROP_DELAY
x = random.randint(0, SCREEN_WIDTH)
y = SCREEN_HEIGHT
self.drop_ball_into_scene(x, y)
# 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:
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