def collide(sprite1, sprite2, collision_objects=None, original_pos=None):
"""
solves a simple spatial collision problem for two Sprites (that have a rect property)
- defaults to SAT collision between two objects
- thanks to doc's at: http://www.sevenson.com.au/actionscript/sat/
- TODO: handle angles on objects
- TODO: handle velocities of sprites prior to collision to calculate correct normals
:param Sprite sprite1: sprite 1
:param Sprite sprite2: sprite 2 (the other sprite)
:param Union[None,Tuple[Collision]] collision_objects: the two always-recycled returnable Collision instances (aside from None); if None,
use our default ones
:param Union[Tuple[int],None] original_pos: the position of sprite1 before doing the move that lead to this collision-detection call
:return: a Collision object with all details of the collision between the two Sprites (None if there is no collision)
:rtype: Union[None,Collision]
"""
pass
python类Rect()的实例源码
def __init__(self,ishape,pos):
if not isinstance(ishape, tuple):
ishape = ishape,None
image,shape = ishape
if shape == None:
shape = pygame.Rect(0,0,image.get_width(),image.get_height())
if isinstance(shape, tuple): shape = pygame.Rect(shape)
self.image = image
self._image = self.image
self.shape = shape
self.rect = pygame.Rect(pos[0],pos[1],shape.w,shape.h)
self._rect = pygame.Rect(self.rect)
self.irect = pygame.Rect(pos[0]-self.shape.x,pos[1]-self.shape.y,
image.get_width(),image.get_height())
self._irect = pygame.Rect(self.irect)
self.groups = 0
self.agroups = 0
self.updated = 1
def setimage(self,ishape):
"""Set the image of the Sprite.
Arguments:
ishape -- an image, or an image, rectstyle. The rectstyle will
describe the shape of the image, used for collision detection.
"""
if not isinstance(ishape, tuple):
ishape = ishape,None
image,shape = ishape
if shape == None:
shape = pygame.Rect(0,0,image.get_width(),image.get_height())
if isinstance(shape, tuple):
shape = pygame.Rect(shape)
self.image = image
self.shape = shape
self.rect.w,self.rect.h = shape.w,shape.h
self.irect.w,self.irect.h = image.get_width(),image.get_height()
self.updated = 1
def run_codes(self,cdata,rect):
"""Run codes.
Arguments:
cdata -- a dict of code:(handler function, value)
rect -- a tile rect of the parts of the layer that should have
their codes run
"""
tw,th = self.tiles[0].image.get_width(),self.tiles[0].image.get_height()
x1,y1,w,h = rect
clayer = self.clayer
t = Tile()
for y in range(y1,y1+h):
for x in range(x1,x1+w):
n = clayer[y][x]
if n in cdata:
fnc,value = cdata[n]
t.tx,t.ty = x,y
t.rect = pygame.Rect(x*tw,y*th,tw,th)
fnc(self,t,value)
def get_vector_to_closest_pond(self):
x_offset = self.closest_pond.rect.centerx - self.rect.centerx
y_offset = self.closest_pond.rect.centery - self.rect.centery
vector_length = math.sqrt((x_offset * x_offset) + (y_offset * y_offset))
if x_offset == 0 or vector_length == 0:
x_vel = 0
else:
x_vel = x_offset / vector_length
if y_offset == 0 or vector_length == 0:
y_vel = 0
else:
y_vel = y_offset / vector_length
self.closest_pond_vector = x_vel, y_vel
def get_vector_to_closest_food(self):
x_offset = self.closest_food.rect.centerx - self.rect.centerx
y_offset = self.closest_food.rect.centery - self.rect.centery
vector_length = math.sqrt((x_offset * x_offset) + (y_offset * y_offset))
if x_offset == 0 or vector_length == 0:
x_vel = 0
else:
x_vel = x_offset / vector_length
if y_offset == 0 or vector_length == 0:
y_vel = 0
else:
y_vel = y_offset / vector_length
self.closest_food_vector = x_vel, y_vel
def render(self, color):
pygame.draw.rect(Renderer.SCREEN, color, self.rect, 0)
# Line to nearest entity
#pygame.draw.line(Renderer.SCREEN, (0, 255, 0), (self.rect.centerx, self.rect.centery), (self.closest_entity.rect.centerx, self.closest_entity.rect.centery), 1)
# Line to nearest food
#pygame.draw.line(Renderer.SCREEN, (255, 0, 0), (self.x + self.size / 2, self.y + self.size / 2), (self.closest_food.x + 3, self.closest_food.y + 3), 1)
# Line following movement vector
#start_line = self.x + self.size / 2, self.y + self.size / 2
#end_line = self.x + (self.size / 2) + (60 * self.closest_food_vector[0]), self.y + (self.size / 2) + (60 * self.closest_food_vector[1])
#moving_line_start = self.x + (self.size / 2), self.y + (self.size / 2)
#moving_line_end = moving_line_start[0] + (20 * self.dx), moving_line_start[1] + (20 * self.dy)
#pygame.draw.line(Renderer.SCREEN, (0, 255, 0), moving_line_start, moving_line_end, 1)
#pygame.draw.line(Renderer.SCREEN, (0, 255, 0), start_line, end_line, 1)
def _draw_agent(center_point, screen, base_size=30):
'''
Args:
center_point (tuple): (x,y)
screen (pygame.Surface)
Returns:
(pygame.rect)
'''
tri_bot_left = center_point[0] - base_size, center_point[1] + base_size
tri_bot_right = center_point[0] + base_size, center_point[1] + base_size
tri_top = center_point[0], center_point[1] - base_size
tri = [tri_bot_left, tri_top, tri_bot_right]
tri_color = (98, 140, 190)
return pygame.draw.polygon(screen, tri_color, tri)
def move(self, x, y, absolute=False):
"""
Moves us by x/y pixels (or to x,y if absolute=True).
:param Union[int,None] x: the amount in pixels to move in x-direction
:param Union[int,None] y: the amount in pixels to move in y-direction
:param bool absolute: whether x and y are given as absolute coordinates (default: False): in this case x/y=None means do not move in this dimension
"""
# absolute coordinates given
if absolute:
if x is not None:
self.rect.x = x
if y is not None:
self.rect.y = y
# do a minimum of 1 pix (if larger 0.0)
else:
if 0 < x < 1:
x = 1
self.rect.x += x
if 0 < y < 1:
y = 1
self.rect.y += y
# then we do the boundary checking
if self.x_max is not None and self.rect.x > self.x_max:
self.rect.x = self.x_max
elif self.x_min is not None and self.rect.x < self.x_min:
self.rect.x = self.x_min
if self.y_max is not None and self.rect.y > self.y_max:
self.rect.y = self.y_max
elif self.y_min is not None and self.rect.y < self.y_min:
self.rect.y = self.y_min
# @override(GameObject)
def render(self, display):
"""
Paints the Sprite with its current image onto the given Display object.
:param Display display: the Display object to render on (Display has a pygame.Surface, on which we blit our image)
"""
if self.image:
#print("render at x={}".format(self.rect.x + self.image_rect.x - display.offsets[0]))
display.surface.blit(self.image, (self.rect.x + self.image_rect.x - display.offsets[0], self.rect.y + self.image_rect.y - display.offsets[1]))
if DEBUG_FLAGS & DEBUG_RENDER_SPRITES_RECTS:
pygame.draw.rect(display.surface, DEBUG_RENDER_SPRITES_RECTS_COLOR,
pygame.Rect((self.rect.x - display.offsets[0], self.rect.y - display.offsets[1]),
(self.rect.w, self.rect.h)), 1)
def __init__(self, x, y, image_file, **kwargs):
ro = kwargs.pop("render_order", 0) # by default, make this Sprite render first
super().__init__(x, y, image_file=image_file, render_order=ro, **kwargs)
self.vx = kwargs.get("vx", 1)
self.vy = kwargs.get("vy", 1)
self.repeat_x = kwargs.get("repeat_x", True)
self.repeat_y = kwargs.get("repeat_y", True)
self.repeat_w = kwargs.get("repeat_w", self.rect.width)
self.repeat_h = kwargs.get("repeat_h", self.rect.height)
# don't collide with anything
self.type = Sprite.get_type("none")
self.collision_mask = 0
# @override(Sprite)
def render(self, display):
# debug rendering (no backgrounds) -> early out
if DEBUG_FLAGS & DEBUG_DONT_RENDER_TILED_TILE_LAYERS:
return
self.ignore_after_n_ticks = 100 # replenish counter so that the repeater never goes out of the Viewport's scope
view_x = display.offsets[0]
view_y = display.offsets[1]
offset_x = self.rect.x + view_x * self.vx
offset_y = self.rect.y + view_y * self.vy
if self.repeat_x:
start_x = math.floor(-offset_x % self.repeat_w)
if start_x > 0:
start_x -= self.repeat_w
else:
start_x = self.rect.x - view_x
if self.repeat_y:
start_y = math.floor(-offset_y % self.repeat_h)
if start_y > 0:
start_y -= self.repeat_h
else:
start_y = self.rect.y - view_y
scale = 1.0
cur_y = start_y
while cur_y < display.height / scale:
cur_x = start_x
while cur_x < display.width / scale:
#display.surface.blit(self.image, dest=(math.floor(cur_x + view_x), math.floor(cur_y + view_y)))
display.surface.blit(self.image, dest=(math.floor(cur_x), math.floor(cur_y)))
cur_x += self.repeat_w
if not self.repeat_x:
break
cur_y += self.repeat_h
if not self.repeat_y:
break
def add_sprite(self, sprite, group_name):
"""
Adds a new single Sprite to an existing or a new pygame.sprite.Group.
:param Sprite sprite: the Sprite to be added to this Stage (the Sprite's position is defined in its rect.x/y properties)
:param str group_name: the name of the group to which the GameObject should be added (group will not be created if it doesn't exist yet)
:return: the Sprite that was added
:rtype: Sprite
"""
# if the group doesn't exist yet, create it
if group_name not in self.sprite_groups:
self.sprite_groups[group_name] = pygame.sprite.Group()
sprite.stage = self # set the Stage of this GameObject
self.sprite_groups[group_name].add(sprite)
self.sprites.append(sprite)
sprite.sprite_groups.append(self.sprite_groups[group_name])
# add each single Sprite to the sorted (by render_order) to_render list and to the "all"-sprites list
# - note: the to_render list also contains entire TiledTileLayer objects
if sprite.do_render:
self.to_render.append(sprite)
self.to_render.sort(key=lambda x: x.render_order)
# trigger two events, one on the Stage with the object as target and one on the object with the Stage as target
self.trigger_event("added_to_stage", sprite)
sprite.trigger_event("added_to_stage", self)
return sprite
def __init__(self, pytmx_layer, pytmx_tiled_map, tile_sprite_handler):
"""
:param pytmx.pytmx.TiledTileLayer pytmx_layer: the underlying pytmx TiledTileLayer
:param pytmx.pytmx.TiledMap pytmx_tiled_map: the underlying pytmx TiledMap object (representing the tmx file)
:param callable tile_sprite_handler: the callable that returns an ndarray, populated with TileSprite objects for storage in this layer
"""
super().__init__(pytmx_layer, pytmx_tiled_map)
self.type_str = self.properties.get("type", "none")
self.type = 0
# get type mask of this layer from `type` property
for t in self.type_str.split(","):
self.type |= Sprite.get_type(t)
# an ndarray holding all single tiles (by x/y position) from this layer
# non-existing tiles are not(!) stored in this ndarray and return None at the respective x/y position
self.tile_sprites = tile_sprite_handler(self)
# update do_render indicator depending on some debug settings
self.do_render = (self.properties["do_render"] == "true" and not (DEBUG_FLAGS & DEBUG_DONT_RENDER_TILED_TILE_LAYERS)) or \
(self.type != Sprite.get_type("none") and (DEBUG_FLAGS & DEBUG_RENDER_COLLISION_TILES))
self.render_order = int(self.properties["render_order"])
# put this layer in one single Sprite that we can then blit on the display (with 'area=[some rect]' to avoid drawing the entire layer each time)
self.pygame_sprite = None
# we are rendering this layer, need to store entire image in this structure
if self.do_render:
self.pygame_sprite = self.build_sprite_surface()
def build_sprite_surface(self):
"""
Builds the image (pygame.Surface) for this tile layer based on all found tiles in the layer.
"""
surf = pygame.Surface((self.pytmx_layer.width * self.pytmx_tiled_map.tilewidth, self.pytmx_layer.height * self.pytmx_tiled_map.tileheight),
flags=pygame.SRCALPHA)
# rendered collision layer
if self.type != Sprite.get_type("none") and (DEBUG_FLAGS & DEBUG_RENDER_COLLISION_TILES):
# red for normal collisions, light-blue for touch collisions
color = DEBUG_RENDER_COLLISION_TILES_COLOR_DEFAULT if self.type & Sprite.get_type("default") else DEBUG_RENDER_COLLISION_TILES_COLOR_OTHER
for (x, y, image), (_, _, gid) in zip(self.pytmx_layer.tiles(), self.pytmx_layer.iter_data()):
surf.blit(image.convert_alpha(), (x * self.pytmx_tiled_map.tilewidth, y * self.pytmx_tiled_map.tileheight))
tile_props = self.pytmx_tiled_map.get_tile_properties_by_gid(gid) or {}
# normal collision tiles
if not tile_props.get("no_collision"):
pygame.draw.rect(surf, color, pygame.Rect((x * self.pytmx_tiled_map.tilewidth, y * self.pytmx_tiled_map.tileheight),
(self.pytmx_tiled_map.tilewidth, self.pytmx_tiled_map.tileheight)), 1)
# "normal" layer (and no debug rendering)
else:
for x, y, image in self.pytmx_layer.tiles():
surf.blit(image.convert_alpha(), (x * self.pytmx_tiled_map.tilewidth, y * self.pytmx_tiled_map.tileheight))
pygame_sprite = pygame.sprite.Sprite()
pygame_sprite.image = surf
pygame_sprite.rect = surf.get_rect()
return pygame_sprite
def render(self, display):
"""
Blits a part of our Sprite's image onto the Display's Surface using the Display's offset attributes.
:param Display display: the Display object to render on
"""
assert self.do_render, "ERROR: TiledTileLayer.render() called but self.do_render is False!"
assert not isinstance(self.pygame_sprite, Sprite), "ERROR: TiledTileLayer.render() called but self.pygame_sprite is not a Sprite!"
r = pygame.Rect(self.pygame_sprite.rect) # make a clone so we don't change the original Rect
# apply the display offsets (camera)
r.x += display.offsets[0]
r.y += display.offsets[1]
r.width = display.width
r.height = display.height
display.surface.blit(self.pygame_sprite.image, dest=(0, 0), area=r)
def get_y(self, x):
"""
Calculates the y value (in normal cartesian y-direction (positive values on up axis)) for a given x-value.
:param int x: the x-value (x=0 for left edge of tile x=tilewidth for right edge of tile)
:return: the calculated y-value
:rtype: int
"""
# y = mx + b
if self.slope is None or self.offset is None:
return 0
return self.slope * min(x, self.max_x) + self.offset * self.rect.height
def sloped_xy_pull(self, sprite):
"""
Applies a so-called xy-pull on a Sprite object moving in x-direction in this sloped tile.
An xy-pull is a change in the y-coordinate because of the x-movement (sliding up/down a slope while moving left/right).
:param Sprite sprite: the Sprite object that's moving on the slope
"""
if self.slope == 0 or not self.slope:
return
# the local x value for the Sprite on the tile's internal x-axis (0=left edge of tile)
x_local = max(0, (sprite.rect.left if self.slope < 0 else sprite.rect.right) - self.rect.left)
# the absolute y-position that we will force the sprite into
y = self.rect.bottom - self.get_y(x_local) - sprite.rect.height
sprite.move(None, y, True)
def invert(self):
"""
Inverts this Collision in place to yield the Collision for the case that the two Sprites are switched.
"""
# flip the sprites
tmp = self.sprite1
self.sprite1 = self.sprite2
self.sprite2 = tmp
# invert the normal and separate (leave distance negative, leave magnitude positive)
self.normal_x = -self.normal_x
self.normal_y = -self.normal_y
self.separate = [-self.separate[0], -self.separate[1]]
# the direction veloc
self.direction_veloc = -self.direction_veloc
return self
## OBSOLETE CLASS
#class PlatformerCollision(Collision):
# """
# A collision object that can be used by PlatformerPhysics to handle Collisions.
# """
#
# def __init__(self):
# super().__init__()
# self.impact = 0.0 # the impulse of the collision on some mass (used for pushing heavy objects)
# # OBSOLETE: these should all be no longer needed
# # self.slope = False # whether this is a collision with a sloped TileSprite of a TiledTileLayer
# (will also be False if obj1 collides with the Tile's rect, but obj1 is still in air (slope))
# # self.slope_y_pull = 0 # amount of y that Sprite has to move up (negative) or down (positive) because of the collision (with a slope)
# #self.slope_up_down = 0 # 0=no slope, -1=down slope, 1 = up slope
def get_highest_tile(tiles, direction, start_abs, end_abs):
"""
Returns the `highest` tile in a list (row or column) of sloped, full-collision or empty tiles.
:param list tiles: the list of tiles to check
:param str direction: the direction in which the list of tiles is arranged (x=row of tiles or y=column of tiles)
:param int start_abs: the absolute leftmost x-value from where to check
:param int end_abs: the absolute rightmost x-value from where to check
:return: a tuple consisting of a) the highest SlopedTileSprite found in the list and b) the height value measured on a cartesian y-axis (positive=up)
:rtype: Tuple[SlopedTileSprite,int]
"""
# start with leftmost tile (measure max height for the two x points: sprite's leftmost edge and tile's right edge)
best_tile = None # the highest tile in this row (if not height==0.0)
tile = tiles[0]
if tile:
max_y = max(tile.get_y(start_abs - tile.rect.left), tile.get_y(tile.rect.width))
best_tile = tile
else:
max_y = 0
# then do all center tiles
for slot in range(1, len(tiles) - 1):
tile = tiles[slot]
max_ = tile.max_y if tile else 0
if max_ > max_y:
max_y = max_
best_tile = tile
# then do the rightmost tile (max between tiles left edge and sprite's right edge)
tile = tiles[-1]
max_ = max(tile.get_y(end_abs - tile.rect.left), tile.get_y(0)) if tile else 0
if max_ > max_y:
max_y = max_
best_tile = tile
# TODO: store x-in and y-pull(push) in tile props (as temporary values)
return best_tile, max_y
def push_an_object(self, pusher, col):
"""
Pushes a pushable other GameObject (assuming that this other object also has a PlatformerPhysics Component).
:param pusher: the Sprite that's actively pushing against the other GameObject
:param col: the Collision object (that caused the push) returned by the collision detector method
"""
pushee = col.sprite2 # the object being pushed
orig_x = pushee.rect.x
pushee_phys = pushee.components["physics"]
# calculate the amount to move in x-direction based on vx_max and the collision-separation
move_x = - col.separate[0] * abs(pushee_phys.vx_max / col.direction_veloc)
# adjust x-speed based on vx_max
self.vx = math.copysign(pushee_phys.vx_max, col.direction_veloc)
# first move rock, then do a x-collision detection of the rock, then fix that collision (if any) -> only then move the pusher
pushee.move(move_x, 0)
# TODO: be careful not to overwrite the col object that's currently still being used by this method's caller
# right now it's being overridden by the below call -> it's not a problem yet because this collision object is only used further via the normal_x
# property, which should stay the same
self.collide_in_one_direction(pushee, "x", self.vx, (orig_x, pushee.rect.y))
# re-align pusher with edge of pushee
if self.vx < 0:
x_delta = pushee.rect.right - pusher.rect.left
else:
x_delta = pushee.rect.left - pusher.rect.right
# and we are done
pusher.move(x_delta, 0)
def follow(self, game_loop=None, first=False):
"""
Helper method to follow our self.obj_to_follow (should not be called by the API user).
Called when the Stage triggers Event 'post_tick' (passes GameLoop into it which is not used).
:param GameLoop game_loop: the GameLoop that's currently playing
:param bool first: whether this is the very first call to this function (if so, do a hard center on, otherwise a soft-center-on)
"""
follow_x = self.directions["x"](self.obj_to_follow) if callable(self.directions["x"]) else self.directions["x"]
follow_y = self.directions["y"](self.obj_to_follow) if callable(self.directions["y"]) else self.directions["y"]
func = self.center_on if first else self.soft_center_on
func(self.obj_to_follow.rect.centerx if follow_x else None, self.obj_to_follow.rect.centery if follow_y else None)
def collide(sprite1, sprite2, collision_objects=None, original_pos=None):
# use default CollisionObjects?
if not collision_objects:
collision_objects = SATCollision.default_collision_objects
# do AABB first for a likely early out
# TODO: right now, we only have pygame.Rect anyway, so these are AABBs
if (sprite1.rect.right < sprite2.rect.left or sprite1.rect.bottom < sprite2.rect.top or
sprite2.rect.right < sprite1.rect.left or sprite2.rect.right < sprite1.rect.left):
return None
test = SATCollision.try_collide(sprite1, sprite2, collision_objects[0], False)
if not test:
return None
test = SATCollision.try_collide(sprite2, sprite1, collision_objects[1], True)
if not test:
return None
# pick the best collision from the two
ret = collision_objects[1] if collision_objects[1].magnitude < collision_objects[0].magnitude else collision_objects[0]
if not ret.is_collided:
return None
# fill in some more values in the recycled Collision object before returning it
ret.separate[0] = - ret.distance * ret.normal_x
ret.separate[1] = - ret.distance * ret.normal_y
if not original_pos:
original_pos = (sprite1.rect.x, sprite1.rect.y)
ret.original_pos = original_pos
return ret
def hit(self,x,y,t,s):
tiles = self.tiles
tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
t.tx = x
t.ty = y
t.rect = Rect(x*tw,y*th,tw,th)
t._rect = t.rect
if hasattr(t,'hit'):
t.hit(self,t,s)
def loop(self):
"""Update and hit testing loop. Run this once per frame."""
self.loop_sprites() #sprites may move
self.loop_tilehits() #sprites move
self.loop_spritehits() #no sprites should move
for s in self.sprites:
s._rect = pygame.Rect(s.rect)
def __init__(self, ai_settings, screen, ship):
"""??????????"""
#super(Bullet, self).__init__() ???2.7?? ?? 3.0?? ??
super().__init__()
self.screen = screen
#class Foo(): ?????????
# def __init__(self, frob, frotz)
# self.frobnicate = frob
# self.frotz = frotz
#class Bar(Foo):
# def __init__(self, frob, frizzle)
# super().__init__(frob, 34)
# self.frazzle = frizzle
# ??0.0???????????????????.
self.rect = pygame.Rect(0, 0, ai_settings.bullet_width,
ai_settings.bullet_height)
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
# ???????????
self.y = float(self.rect.y)
self.color = ai_settings.bullet_color
self.speed_factor = ai_settings.bullet_speed_factor
#???? ????? ??? pygame.rect() ??????????
#?? ?????????? X Y ?? ?? ?? ????? ????? ?settings ????
#rect.centerx ??????????
#rect.top ????????????
def update(self):
"""??????."""
# ??????????. ???????? ???????
self.y -= self.speed_factor
# ?? rect ??. ????????
self.rect.y = self.y
def draw_bullet(self):
"""???????."""
pygame.draw.rect(self.screen, self.color, self.rect)
#?draw.recct() ???self.color????????rect ???????
def __init__(self, ai_settings, screen, ship):
"""??????????"""
#super(Bullet, self).__init__() ???2.7?? ?? 3.0?? ??
super().__init__()
self.screen = screen
#class Foo(): ?????????
# def __init__(self, frob, frotz)
# self.frobnicate = frob
# self.frotz = frotz
#class Bar(Foo):
# def __init__(self, frob, frizzle)
# super().__init__(frob, 34)
# self.frazzle = frizzle
# ??0.0???????????????????.
self.rect = pygame.Rect(0, 0, ai_settings.bullet_width,
ai_settings.bullet_height)
self.rect.centerx = ship.rect.centerx
self.rect.top = ship.rect.top
# ???????????
self.y = float(self.rect.y)
self.color = ai_settings.bullet_color
self.speed_factor = ai_settings.bullet_speed_factor
#???? ????? ??? pygame.rect() ??????????
#?? ?????????? X Y ?? ?? ?? ????? ????? ?settings ????
#rect.centerx ??????????
#rect.top ????????????
def update(self):
"""??????."""
# ??????????. ???????? ???????
self.y -= self.speed_factor
# ?? rect ??. ????????
self.rect.y = self.y