Skip to content

Commit

Permalink
Work on pymunk physics engine and example code. Add support for colli…
Browse files Browse the repository at this point in the history
…sion detection and removing sprites.
  • Loading branch information
pvcraven committed Apr 24, 2020
1 parent 54638bf commit 232a859
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 18 deletions.
85 changes: 79 additions & 6 deletions arcade/experimental/pymunk_demo_top_down.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
Top-down
"""
import math
import arcade
from typing import Optional
from arcade.experimental.pymunk_physics_engine import PymunkPhysicsEngine

SCREEN_TITLE = "PyMunk Top-Down"
Expand All @@ -18,18 +20,23 @@

# Physics force used to move the player. Higher number, faster accelerating.
PLAYER_MOVE_FORCE = 4000
BULLET_MOVE_FORCE = 2500


class MyWindow(arcade.Window):
""" Main Window """
def __init__(self, width, height, title):
""" Init """
super().__init__(width, height, title)

arcade.set_background_color(arcade.color.AMAZON)

self.player_list = None
self.wall_list = None
self.bullet_list = None
self.item_list = None
self.player_sprite = None
self.physics_engine = None
self.physics_engine: Optional[PymunkPhysicsEngine] = None

# Track the current state of what key is pressed
self.left_pressed = False
Expand All @@ -38,9 +45,11 @@ def __init__(self, width, height, title):
self.down_pressed = False

def setup(self):
""" Set up everything """
# Create the sprite lists
self.player_list = arcade.SpriteList()
self.wall_list = arcade.SpriteList()
self.bullet_list = arcade.SpriteList()
self.item_list = arcade.SpriteList()

# Set up the player
Expand Down Expand Up @@ -80,7 +89,7 @@ def setup(self):

# Add some movable boxes
for x in range(SPRITE_SIZE * 3, SPRITE_SIZE * 8, SPRITE_SIZE):
item = arcade.Sprite(":resources:images/tiles/boxCrate.png",
item = arcade.Sprite(":resources:images/space_shooter/meteorGrey_big1.png",
SPRITE_SCALING_PLAYER)
item.center_x = x
item.center_y = 400
Expand All @@ -103,6 +112,13 @@ def setup(self):
self.physics_engine = PymunkPhysicsEngine(damping=damping,
gravity=gravity)

def rock_hit_handler(arbiter, space, data):
""" Called for bullet/rock collision """
bullet_shape = arbiter.shapes[0]
bullet_sprite = self.physics_engine.get_sprite_for_shape(bullet_shape)
bullet_sprite.remove_from_sprite_lists()

self.physics_engine.add_collision_handler("bullet", "rock", rock_hit_handler)
# 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
Expand All @@ -117,6 +133,7 @@ def setup(self):
friction=0.6,
moment=PymunkPhysicsEngine.MOMENT_INF,
damping=0.01,
collision_type="player",
max_velocity=400)

# Create the walls.
Expand All @@ -128,13 +145,56 @@ def setup(self):
# Dynamic is default.
self.physics_engine.add_sprite_list(self.wall_list,
friction=0.6,
collision_type="wall",
body_type=PymunkPhysicsEngine.STATIC)

# Create some boxes to push around.
# Mass controls, well, the mass of an object. Defaults to 1.
self.physics_engine.add_sprite_list(self.item_list,
mass=1,
friction=0.6)
friction=0.6,
collision_type="rock")

def on_mouse_press(self, x, y, button, modifiers):
""" Called whenever the mouse button is clicked. """

bullet = arcade.SpriteSolidColor(5, 5, arcade.color.RED)
self.bullet_list.append(bullet)

# Position the bullet at the player's current location
start_x = self.player_sprite.center_x
start_y = self.player_sprite.center_y
bullet.position = self.player_sprite.position

# Get from the mouse the destination location for the bullet
# IMPORTANT! If you have a scrolling screen, you will also need
# to add in self.view_bottom and self.view_left.
dest_x = x
dest_y = y

# Do math to calculate how to get the bullet to the destination.
# Calculation the angle in radians between the start points
# and end points. This is the angle the bullet will travel.
x_diff = dest_x - start_x
y_diff = dest_y - start_y
angle = math.atan2(y_diff, x_diff)

force = [math.cos(angle), math.sin(angle)]
size = max(self.player_sprite.width, self.player_sprite.height) / 2

bullet.center_x += size * force[0]
bullet.center_y += size * force[1]

self.physics_engine.add_sprite(bullet,
mass=0.1,
damping=1.0,
friction=0.6)

# Taking into account the angle, calculate our force.
force[0] *= BULLET_MOVE_FORCE
force[1] *= BULLET_MOVE_FORCE

self.physics_engine.apply_force(bullet, force)

def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed. """
Expand All @@ -147,6 +207,18 @@ def on_key_press(self, key, modifiers):
self.left_pressed = True
elif key == arcade.key.RIGHT:
self.right_pressed = True
elif key == arcade.key.SPACE:
bullet = arcade.SpriteSolidColor(5, 5, arcade.color.RED)
bullet.position = self.player_sprite.position
bullet.center_x += 30
self.bullet_list.append(bullet)
self.physics_engine.add_sprite(bullet,
mass=0.1,
damping=1.0,
friction=0.6,
collision_type="bullet")
force = (3000, 0)
self.physics_engine.apply_force(bullet, force)

def on_key_release(self, key, modifiers):
"""Called when the user releases a key. """
Expand Down Expand Up @@ -180,19 +252,20 @@ def on_update(self, delta_time):
elif self.right_pressed and not self.left_pressed:
force = (PLAYER_MOVE_FORCE, 0)
self.physics_engine.apply_force(self.player_sprite, force)
else:
print("Pow")

# --- Move items in the physics engine
self.physics_engine.step()
self.physics_engine.resync_sprites()

def on_draw(self):
""" Draw everything """
arcade.start_render()
self.wall_list.draw()
self.bullet_list.draw()
self.item_list.draw()
self.player_list.draw()

def main():
""" Main method """
window = MyWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
window.setup()
arcade.run()
Expand Down
64 changes: 52 additions & 12 deletions arcade/experimental/pymunk_physics_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,30 @@ def __init__(self, gravity=(0, 0), damping: float = 1.0):
self.space = pymunk.Space()
self.space.gravity = gravity
self.space.damping = damping
self.collision_types = []
self.sprites = {}


def add_sprite(self,
sprite: Sprite,
mass: float=1,
friction: float=0.2,
mass: float = 1,
friction: float = 0.2,
moment=None,
body_type=DYNAMIC,
damping=None,
gravity=(0, 0),
max_velocity=None,
radius=0
radius: float = 0,
collision_type: str = None,
):
""" Add a sprite to the physics engine. """

if sprite in self.sprites:
print("Sprite already in added.")
print("Sprite already in space.")

if collision_type not in self.collision_types:
self.collision_types.append(collision_type)

collision_type_id = self.collision_types.index(collision_type)

if moment is None:
moment = pymunk.moment_for_box(mass, (sprite.width, sprite.height))
Expand All @@ -53,9 +60,10 @@ def add_sprite(self,
body.position = pymunk.Vec2d(sprite.center_x, sprite.center_y)

def velocity_callback(my_body, my_gravity, my_damping, dt):
""" Used for custom damping, gravity, and max_velocity. """
if damping is not None:

adj_damping = ((damping * 100) / 100) ** (dt)
adj_damping = ((damping * 100) / 100) ** dt
# print(damping, my_damping, adj_damping)
my_damping = adj_damping
if gravity is not None:
Expand All @@ -65,9 +73,9 @@ def velocity_callback(my_body, my_gravity, my_damping, dt):

if max_velocity:

l = my_body.velocity.length
if l > max_velocity:
scale = max_velocity / l
velocity = my_body.velocity.length
if velocity > max_velocity:
scale = max_velocity / velocity
body.velocity = body.velocity * scale

if damping is not None:
Expand All @@ -78,38 +86,70 @@ def velocity_callback(my_body, my_gravity, my_damping, dt):
scaled_poly = [[x * sprite.scale for x in z] for z in poly]

shape = pymunk.Poly(body, scaled_poly, radius=radius)
if collision_type:
shape.collision_type = collision_type_id
shape.friction = friction

physics_object = _PhysicsObject(body, shape)
self.sprites[sprite] = physics_object

self.space.add(body, shape)
sprite.register_physics_engine(self)

def add_sprite_list(self,
sprite_list: SpriteList,
mass: float = 1,
friction: float = 0.2,
moment=None,
body_type=DYNAMIC
body_type=DYNAMIC,
collision_type=None
):
""" Add all sprites in a sprite list to the physics engine. """

for sprite in sprite_list:
self.add_sprite(sprite, mass, friction, moment, body_type)
self.add_sprite(sprite, mass, friction, moment, body_type, collision_type=collision_type)

def remove_sprite(self, sprite: Sprite):
physics_object = self.sprites[sprite]
self.space.remove(physics_object.body)
self.space.remove(physics_object.shape)

def get_sprite_for_shape(self, shape) -> Sprite:
for sprite in self.sprites:
if self.sprites[sprite].shape is shape:
return sprite

def add_collision_handler(self, first_type, second_type, handler):
""" Add code to handle collisions between objects. """
if first_type not in self.collision_types:
self.collision_types.append(first_type)
first_type_id = self.collision_types.index(first_type)

if second_type not in self.collision_types:
self.collision_types.append(second_type)
second_type = self.collision_types.index(second_type)

h = self.space.add_collision_handler(first_type_id, second_type)
h.post_solve = handler

def resync_sprites(self):
""" Set visual sprites to be the same location as physics engine sprites. """
for sprite in self.sprites:
physics_object = self.sprites[sprite]
sprite.center_x = physics_object.body.position.x
sprite.center_y = physics_object.body.position.y
sprite.angle = math.degrees(physics_object.body.angle)

def step(self, delta_time = 1/60.0):
def step(self, delta_time=1 / 60.0):
""" Tell the physics engine to perform calculations. """
# Update physics
# Use a constant time step, don't use delta_time
# See "Game loop / moving time forward"
# http://www.pymunk.org/en/latest/overview.html#game-loop-moving-time-forward
self.space.step(delta_time)
self.resync_sprites()

def apply_force(self, sprite, force):
""" Apply force to a Sprite. """
physics_object = self.sprites[sprite]
physics_object.body.apply_force_at_local_point(force, (0, 0))

0 comments on commit 232a859

Please sign in to comment.