Skip to content

Instantly share code, notes, and snippets.

@dfsklar
Created January 21, 2021 22:30
Show Gist options
  • Save dfsklar/5d9bee04f050ab4adf28e6ea949defe8 to your computer and use it in GitHub Desktop.
Save dfsklar/5d9bee04f050ab4adf28e6ea949defe8 to your computer and use it in GitHub Desktop.
arcmunk_simplemotor.py
"""
SIMPLE MOTOR version 1
"""
import arcade
import pymunk
import random
import timeit
import math
SCREEN_TITLE = "Pymunk Motor 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 = 150
SEESAW_HALF_WIDTH = SEESAW_WIDTH / 2
FULCRUM_Y_LOC = GROUND_Y_LOC + FULCRUM_HEIGHT_FROM_GROUND
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_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 "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.
"""
def add_linesegment_to_scene(self, new_segment):
x = new_segment
return
#self.space.add(new_segment)
#self.list_of_segments.append(new_segment)
def register_body_into_scene(self, body, *objs):
if not (body.body_type == pymunk.Body.STATIC):
print("Adding a dynamic body")
self.space.add(body)
for shape in body.shapes:
self.space.add(shape)
self.list_of_segments.append(shape)
else:
print("Adding a static body")
for shape in body.shapes:
self.space.add(shape)
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 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)
# Let's start keeping a list of all of the dynamic balls in the scene.
self.ball_list = arcade.SpriteList()
self.list_of_segments = []
# FIRST PHYSICS BODY: The seesaw's fulcrum.
# It is a *static* body -- it will never move.
# We are not required to specify the mass (weight) of a static body.
fulcrum = pymunk.Body(body_type=pymunk.Body.STATIC)
fulcrum.position = (SCREEN_HALF_WIDTH, GROUND_Y_LOC)
# Remember the triangular fulcrum shape we used in the see-saw example?
# Well, in this case, we do NOT want the fulcrum to have any physics shape at all because we'd
# like the plank to be able to completely rotate 360 degrees without "hitting" anything that might interfere.
# So this fulcrum's "body" is "empty" -- we do not place any segments in it.
self.register_body_into_scene(fulcrum)
# 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.
sprite = CircleSprite(":resources:images/items/gold_1.png", \
fulcrum.position[0], fulcrum.position[1] + FULCRUM_HEIGHT_FROM_GROUND, 5)
self.ball_list.append(sprite)
# PHYSICS BODY: The seesaw plank. This is dynamic of course!
# Any dynamic body needs to have a mass; the best approach is to set the mass information on its shapes, not on the body itself.
seesaw_body = pymunk.Body(body_type=pymunk.Body.DYNAMIC)
seesaw_body.position = (SCREEN_HALF_WIDTH, GROUND_Y_LOC)
# Segments inside the body use coordinate system relative to the body's position ??
thickness = 1
seesaw_shape = \
pymunk.Segment(seesaw_body,
a=( -SEESAW_HALF_WIDTH, FULCRUM_HEIGHT_FROM_GROUND),
b=( SEESAW_HALF_WIDTH, FULCRUM_HEIGHT_FROM_GROUND),
radius=thickness)
seesaw_shape.friction = 1
seesaw_shape.mass = 4
chamber_width = 38
self.register_body_into_scene(seesaw_body, seesaw_shape,
pymunk.Segment(seesaw_body,
a=( -SEESAW_HALF_WIDTH, FULCRUM_HEIGHT_FROM_GROUND),
b=( -SEESAW_HALF_WIDTH-10, FULCRUM_HEIGHT_FROM_GROUND+chamber_width),
radius=thickness),
pymunk.Segment(seesaw_body,
a=( -SEESAW_HALF_WIDTH+120, FULCRUM_HEIGHT_FROM_GROUND+chamber_width),
b=( -SEESAW_HALF_WIDTH-10, FULCRUM_HEIGHT_FROM_GROUND+chamber_width),
radius=thickness),
pymunk.Segment(seesaw_body,
a=( SEESAW_HALF_WIDTH, FULCRUM_HEIGHT_FROM_GROUND),
b=( SEESAW_HALF_WIDTH+10, FULCRUM_HEIGHT_FROM_GROUND+chamber_width),
radius=thickness))
# Set up the connection between the fulcrum point and the seesaw "plank"
self.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(self.rotation_center_joint)
# Now, motorize that connection
motor = pymunk.SimpleMotor(fulcrum, seesaw_body, 2)
self.space.add(motor)
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 on_draw(self):
"""
Render the screen.
"""
# This command has to happen before we start drawing
arcade.start_render()
draw_start_time = timeit.default_timer()
self.ball_list.draw()
for line in self.list_of_segments:
body = line.body
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, arcade.color.WHITE, 2)
def on_mouse_press(self, x, y, which_button, which_modifiers):
self.drop_ball_into_scene(x, y)
def drop_ball_into_scene(self, x, y):
mass = 0.5
radius = 15
#inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0))
body = pymunk.Body(body_type=pymunk.Body.DYNAMIC) # mass, inertia)
body.position = (x, y)
shape = pymunk.Circle(body, radius)
shape.friction = 0.3
shape.mass = mass
self.space.add(body, shape)
sprite = CircleSprite_for_pymunk_shape(":resources:images/items/gold_1.png", shape)
self.ball_list.append(sprite)
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):
try:
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()
except:
pass
# 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
def main():
MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
arcade.run()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment