import arcade
from arcade.sprite_list import SpriteList, check_for_collision
from arcade import check_for_collision_with_list
from itertools import chain, combinations
from collections import defaultdict
from easing_functions import LinearInOut
from quest.helpers import Direction, SpriteListList, scale
from time import time
from math import sqrt
[docs]class QuestPhysicsEngine:
"""Base class for Quest Physics Engines
The engine is initialized with a :py:class:`QuestGame` instance, which
the engine uses to access sprites. Quest's physics engines make some assumptions
about the structure of a game in order to simplify logic. It is assumed that the
game has attributes `player_list`, `wall_list`, and `npc_list`. Players and NPCs will
be moved according to their `change_x` and `change_y` attributes; walls will not move.
When players or NPCs collide with walls, they are pushed back until they are no longer
colliding. When players or NPCs collide with each other, their `on_collision` methods
are called, but they are not automatically repelled. If you want players or NPCs to be
repelled from each other, see :py:class:`quest.examples.grandmas_soup.Grandma`.
Args:
game (QuestGame): The game to which the engine will be attached.
"""
def __init__(self, game):
self.game = game
self.player_list=game.player_list
self.wall_list=game.wall_list
self.npc_list=game.npc_list
all_sprite_lists = [self.player_list, self.wall_list, self.npc_list]
curr_map = self.game.get_current_map()
for layer in curr_map.layers:
all_sprite_lists.append(layer.sprite_list)
self.all_sprites = SpriteListList(all_sprite_lists)
[docs] def update(self, game):
"""waits for implementation from a more complex physics engine like py:class:`ContinuousPhysicsEngine`
"""
[docs] def player(self):
return self.player_list.sprite_list[0]
[docs]class ContinuousPhysicsEngine(QuestPhysicsEngine):
"""A continuous physics engine allows sprites to be at any point.
This implementation is simple but inefficient. It may be problematic with
more complex games. If we run into trouble, first try using spatial hashes
to resolve collisions.
"""
def __init__(self, game, **kwargs):
super().__init__(game, **kwargs)
self.non_wall_list = SpriteListList([self.player_list, self.npc_list])
[docs] def update(self):
"""Updates sprite positions and handles collisions.
"""
super().update(self.game)
self.update_sprite_positions()
self.resolve_collisions_with_walls()
self.resolve_collisions_between_nonwalls()
[docs] def update_sprite_positions(self):
"""Updates sprite positions using their `change_x` and `change_y` attributes.
"""
for moving_sprite in self.non_wall_list:
moving_sprite.center_x += moving_sprite.change_x
moving_sprite.center_y += moving_sprite.change_y
[docs] def resolve_collisions_with_walls(self):
"""Resolves collisions between every sprite and every wall.
"""
for moving_sprite in self.non_wall_list:
if moving_sprite.change_x == 0 and moving_sprite.change_y == 0:
continue
wall_collisions = check_for_collision_with_list(moving_sprite, self.wall_list)
for wall in wall_collisions:
moving_sprite.on_collision(wall, self.game)
wall.on_collision(moving_sprite, self.game)
self.resolve_sprite_wall_collision(moving_sprite, wall)
[docs] def resolve_sprite_wall_collision(self, sprite, wall):
"""Stops the sprite and backs it away from the wall until they are no longer colliding.
The distance by which the sprite backs up doubles until they are no longer colliding.
Note that this does not handle the case in which backing away from one wall means
it hits another wall (e.g. in a narrow passageway). This will be handled on the subsequent
update. We can write more complex code if it becomes necessary.
"""
sprite.stop()
repel_distance = 1
away = (wall.center_x - sprite.center_x, wall.center_y - sprite.center_y)
while check_for_collision(sprite, wall):
away_x, away_y = scale(away, repel_distance)
sprite.center_x = sprite.center_x - away_x
sprite.center_y = sprite.center_y - away_y
repel_distance *= 2
[docs] def resolve_collisions_between_nonwalls(self):
"""For every pair of nonwall sprites, resolves collisions.
"""
for sprite0, sprite1 in combinations(self.non_wall_list, 2):
if check_for_collision(sprite0, sprite1):
sprite0.on_collision(sprite1, self.game)
sprite1.on_collision(sprite0, self.game)
[docs]class DiscretePhysicsEngine(QuestPhysicsEngine):
"""A physics engine which snaps sprite movement to specific gridpoints.
Some games work better when sprites occupy specific tiles, rather than
having continuous positions. This can make it easier to think about
relationships between sprites (for example, to calculate which are
adjacent, or to plan a route). :py:class:`DiscretePhysicsEngine` handles
sprite movement in a discrete way, while animating sprites' transitions
from tile to tile.
Args:
player_sprite (arcade.Sprite):
dynamic_sprite_lists (bool): Whether new sprites might be added
to sprite lists during the game. Performance is better when
False. Default True.
"""
tile_transition_cutoff = 0.5
easing_class = LinearInOut
def __init__(self, game, grid_map_layer, diagonal=True, check_for_new_sprites=True, **kwargs):
super().__init__(game, **kwargs)
self.grid = grid_map_layer
self.diagonal = diagonal
self.easing = self.easing_class()
self.check_for_new_sprites = check_for_new_sprites
sprite_lists = [self.player_list, self.wall_list, self.npc_list]
self.all_nonbackground_sprites = SpriteListList(sprite_lists)
self.dynamic_sprites = SpriteListList([l for l in sprite_lists if not l.is_static])
self.ensure_sprite_metadata(all_sprites=True)
self.tile_positions = defaultdict(list)
for sprite in self.all_nonbackground_sprites:
self.tile_positions[sprite.current_tile].append(sprite)
[docs] def update(self):
super().update(self.game)
if self.check_for_new_sprites:
self.ensure_sprite_metadata()
for sprite in self.dynamic_sprites:
if (sprite.change_x or sprite.change_y) and not sprite.moving:
self.begin_move(sprite)
elif sprite.moving:
self.continue_move(sprite)
[docs] def begin_move(self, sprite):
direction = Direction.from_vector((sprite.change_x, sprite.change_y), self.diagonal)
ox, oy = sprite.origin_tile
vx, vy = direction.to_vector()
destination = (ox + vx, oy + vy)
if self.grid.position_in_grid(destination):
wall = self.get_wall(self.tile_positions[destination])
if wall:
self.player().on_collision(wall, self.game)
wall.on_collision(self.player(), self.game)
else:
sprite.moving = True
sprite.move_start = time()
sprite.t = 0
sprite.move_direction = direction
sprite.destination_tile = destination
[docs] def continue_move(self, sprite):
duration = 1 / sprite.speed
if sprite.move_direction.is_diagonal():
duration *= sqrt(2)
sprite.t = self.ease((time() - sprite.move_start) / duration)
if sprite.t >= self.tile_transition_cutoff:
self.tile_positions[sprite.current_tile].remove(sprite)
sprite.current_tile = sprite.destination_tile
for other_sprite in self.tile_positions[sprite.current_tile]:
sprite.on_collision(other_sprite, self.game)
other_sprite.on_collision(sprite, self.game)
self.tile_positions[sprite.current_tile].append(sprite)
if sprite.t >= 1.0:
self.end_move(sprite)
else:
p0 = self.grid.get_pixel_position(sprite.origin_tile)
p1 = self.grid.get_pixel_position(sprite.destination_tile)
sprite.center_x, sprite.center_y = self.interpolate(p0, p1, sprite.t)
[docs] def end_move(self, sprite):
sprite.moving = False
sprite.center_x, sprite.center_y = self.grid.get_pixel_position(sprite.destination_tile)
sprite.origin_tile = sprite.current_tile = sprite.destination_tile
[docs] def get_wall(self, sprite_list):
for sprite in sprite_list:
if sprite in self.wall_list:
return sprite
[docs] def interpolate(self, p0, p1, t):
(x0, y0), (x1, y1) = p0, p1
return x0 + (x1 - x0) * t, y0 + (y1 - y0) * t
[docs] def ease(self, x):
return self.easing.ease(x)