我认为我有一个舍入问题导致我的精灵在向左移动时移动得更快/跳得更远。
我的精灵更新方法是调用move,它为每个轴调用move_single_axis。在这里我正在进行一些碰撞检测,我依靠pygame的rect类来检测碰撞,并设置新的位置。
我认为这是问题,但我不确定如何绕过舍入问题,因为pygame的rect使用了整数。
以下是更新代码:
def update(self, dt, game):
self.calc_grav(game, dt)
self.animate(dt, game)
self._old_position = self._position[:]
self.move(dt, game)
self.rect.topleft = self._position
def move(self, dt, game):
# Move each axis separately. Note that this checks for collisions both times.
dx = self.velocity[0]
dy = self.velocity[1]
if dx != 0:
self.move_single_axis(dx, 0, dt)
if dy != 0:
self.move_single_axis(0, dy, dt)
def move_single_axis(self, dx, dy, dt):
#print("hero_destination: ({}, {})".format(dx *dt, dy *dt))
self._position[0] += dx * dt
self._position[1] += dy * dt
#print("Game walls: {}".format(game.walls))
self.rect.topleft = self._position
body_sensor = self.get_body_sensor()
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
#print(" -- Moving right; Hit the left side of the wall")
self.rect.right = wall.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
#print(" -- Moving left; Hit the right side of the wall")
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
if dy > 0: # Moving down; Hit the top side of the wall
#print(" -- Moving down; Hit the top side of the wall")
self.rect.bottom = wall.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
#print(" -- Moving up; Hit the bottom side of the wall")
self.rect.top = wall.rect.bottom
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
以下是整个来源(https://github.com/davidahines/python_sidescroller):
import os.path
import pygame
from pygame.locals import *
from pytmx.util_pygame import load_pygame
import pyscroll
import pyscroll.data
from pyscroll.group import PyscrollGroup
# define configuration variables here
RESOURCES_DIR = 'data'
HERO_JUMP_HEIGHT = 180
HERO_MOVE_SPEED = 200 # pixels per second
GRAVITY = 1000
MAP_FILENAME = 'maps/dungeon_0.tmx'
# simple wrapper to keep the screen resizeable
def init_screen(width, height):
screen = pygame.display.set_mode((width, height), pygame.RESIZABLE)
return screen
# make loading maps a little easier
def get_map(filename):
return os.path.join(RESOURCES_DIR, filename)
# make loading images a little easier
def load_image(filename):
return pygame.image.load(os.path.join(RESOURCES_DIR, filename))
class Hero(pygame.sprite.Sprite):
""" Our Hero
The Hero has three collision rects, one for the whole sprite "rect" and
"old_rect", and another to check collisions with walls, called "feet".
The position list is used because pygame rects are inaccurate for
positioning sprites; because the values they get are 'rounded down'
as integers, the sprite would move faster moving left or up.
Feet is 1/2 as wide as the normal rect, and 8 pixels tall. This size size
allows the top of the sprite to overlap walls. The feet rect is used for
collisions, while the 'rect' rect is used for drawing.
There is also an old_rect that is used to reposition the sprite if it
collides with level walls.
"""
def __init__(self, map_data_object):
pygame.sprite.Sprite.__init__(self)
self.STATE_STANDING = 0
self.STATE_WALKING = 1
self.STATE_JUMPING = 2
self.FRAME_DELAY_STANDING =1
self.FRAME_DELAY_WALKING = 1
self.FRAME_DELAY_JUMPING = 1
self.FACING_RIGHT = 0
self.FACING_LEFT = 1
self.MILLISECONDS_TO_SECONDS = 1000.0
self.COLLISION_BOX_OFFSET = 8
self.time_in_state = 0.0
self.current_walking_frame = 0
self.current_standing_frame = 0
self.current_jumping_frame = 0
self.load_sprites()
self.velocity = [0, 0]
self.state = self.STATE_STANDING
self.facing = self.FACING_RIGHT
self._position = [map_data_object.x, map_data_object.y]
self._old_position = self.position
self.rect = pygame.Rect(8, 0, self.image.get_rect().width - 8, self.image.get_rect().height)
def set_state(self, state):
if self.state != state:
self.state = state
self.time_in_state = 0.0
def load_sprites(self):
self.spritesheet = Spritesheet('data/art/platformer_template_g.png')
standing_images = self.spritesheet.images_at((
pygame.Rect(0, 0, 32, 32),
), colorkey= (0,255,81))
self.standing_images = []
for standing_image in standing_images:
self.standing_images.append(standing_image.convert_alpha())
self.image = self.standing_images[self.current_standing_frame]
@property
def position(self):
return list(self._position)
@position.setter
def position(self, value):
self._position = list(value)
def get_floor_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1]+2, self.rect.width -self.COLLISION_BOX_OFFSET, self.rect.height)
def get_ceiling_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1]-self.rect.height, self.rect.width, 2)
def get_body_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1], self.rect.width -self.COLLISION_BOX_OFFSET, self.rect.height)
def calc_grav(self, game, dt):
""" Calculate effect of gravity. """
floor_sensor = self.get_floor_sensor()
collidelist = floor_sensor.collidelist(game.walls)
hero_is_airborne = collidelist == -1
if hero_is_airborne:
if self.velocity[1] == 0:
self.velocity[1] = GRAVITY * dt
else:
self.velocity[1] += GRAVITY * dt
def update(self, dt, game):
self.calc_grav(game, dt)
self._old_position = self._position[:]
self.move(dt, game)
def move(self, dt, game):
# Move each axis separately. Note that this checks for collisions both times.
dx = self.velocity[0]
dy = self.velocity[1]
if dx != 0:
self.move_single_axis(dx, 0, dt)
if dy != 0:
self.move_single_axis(0, dy, dt)
self.rect.topleft = self._position
def move_single_axis(self, dx, dy, dt):
#print("hero_destination: ({}, {})".format(dx *dt, dy *dt))
self._position[0] += dx * dt
self._position[1] += dy * dt
#print("Game walls: {}".format(game.walls))
self.rect.topleft = self._position
body_sensor = self.get_body_sensor()
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
self.rect.right = wall.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
if dy > 0: # Moving down; Hit the top side of the wall
self.rect.bottom = wall.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
self.rect.top = wall.rect.bottom
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
class Wall(pygame.sprite.Sprite):
"""
A sprite extension for all the walls in the game
"""
def __init__(self, map_data_object):
pygame.sprite.Sprite.__init__(self)
self._position = [map_data_object.x, map_data_object.y]
self.rect = pygame.Rect(
map_data_object.x, map_data_object.y,
map_data_object.width, map_data_object.height)
@property
def position(self):
return list(self._position)
@position.setter
def position(self, value):
self._position = list(value)
class Spritesheet(object):
def __init__(self, filename):
try:
self.sheet = pygame.image.load(filename).convert()
except pygame.error:
print ('Unable to load spritesheet image: {}').format(filename)
raise SystemExit
# Load a specific image from a specific rectangle
def image_at(self, rectangle, colorkey = None):
"Loads image from x,y,x+offset,y+offset"
rect = pygame.Rect(rectangle)
image = pygame.Surface(rect.size).convert()
image.blit(self.sheet, (0, 0), rect)
if colorkey is not None:
if colorkey is -1:
colorkey = image.get_at((0,0))
image.set_colorkey(colorkey, pygame.RLEACCEL)
return image
# Load a whole bunch of images and return them as a list
def images_at(self, rects, colorkey = None):
"Loads multiple images, supply a list of coordinates"
return [self.image_at(rect, colorkey) for rect in rects]
class QuestGame(object):
""" This class is a basic game.
It also reads input and moves the Hero around the map.
Finally, it uses a pyscroll group to render the map and Hero.
This class will load data, create a pyscroll group, a hero object.
"""
filename = get_map(MAP_FILENAME)
def __init__(self):
# true while running
self.running = False
self.debug = False
# load data from pytmx
self.tmx_data = load_pygame(self.filename)
# setup level geometry with simple pygame rects, loaded from pytmx
self.walls = list()
self.npcs = list()
for map_object in self.tmx_data.objects:
if map_object.type == "wall":
self.walls.append(Wall(map_object))
elif map_object.type == "guard":
print("npc load failed: reimplement npc")
#self.npcs.append(Npc(map_object))
elif map_object.type == "hero":
self.hero = Hero(map_object)
# create new data source for pyscroll
map_data = pyscroll.data.TiledMapData(self.tmx_data)
# create new renderer (camera)
self.map_layer = pyscroll.BufferedRenderer(map_data, screen.get_size(), clamp_camera=True, tall_sprites=1)
self.map_layer.zoom = 2
self.group = PyscrollGroup(map_layer=self.map_layer, default_layer=3)
# add our hero to the group
self.group.add(self.hero)
def draw(self, surface):
# center the map/screen on our Hero
self.group.center(self.hero.rect.center)
# draw the map and all sprites
self.group.draw(surface)
if(self.debug):
floor_sensor_rect = self.hero.get_floor_sensor()
ox, oy = self.map_layer.get_center_offset()
new_rect = floor_sensor_rect.move(ox * 2, oy * 2)
pygame.draw.rect(surface, (255,0,0), new_rect)
def handle_input(self, dt):
""" Handle pygame input events
"""
poll = pygame.event.poll
event = poll()
while event:
if event.type == QUIT:
self.running = False
break
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
self.running = False
break
# this will be handled if the window is resized
elif event.type == VIDEORESIZE:
init_screen(event.w, event.h)
self.map_layer.set_size((event.w, event.h))
event = poll()
# using get_pressed is slightly less accurate than testing for events
# but is much easier to use.
pressed = pygame.key.get_pressed()
floor_sensor = self.hero.get_floor_sensor()
floor_collidelist = floor_sensor.collidelist(self.walls)
hero_is_airborne = floor_collidelist == -1
ceiling_sensor = self.hero.get_ceiling_sensor()
ceiling_collidelist = ceiling_sensor.collidelist(self.walls)
hero_touches_ceiling = ceiling_collidelist != -1
if pressed[K_l]:
print("airborne: {}".format(hero_is_airborne))
print("hero position: {}, {}".format(self.hero.position[0], self.hero.position[1]))
print("hero_touches_ceiling: {}".format(hero_touches_ceiling))
print("hero_is_airborne: {}".format(hero_is_airborne))
if hero_is_airborne == False:
#JUMP
if pressed[K_SPACE]:
self.hero.set_state(self.hero.STATE_JUMPING)
# stop the player animation
if pressed[K_LEFT] and pressed[K_RIGHT] == False:
# play the jump left animations
self.hero.velocity[0] = -HERO_MOVE_SPEED
elif pressed[K_RIGHT] and pressed[K_LEFT] == False:
self.hero.velocity[0] = HERO_MOVE_SPEED
self.hero.velocity[1]= -HERO_JUMP_HEIGHT
elif pressed[K_LEFT] and pressed[K_RIGHT] == False:
self.hero.set_state(self.hero.STATE_WALKING)
self.hero.velocity[0] = -HERO_MOVE_SPEED
elif pressed[K_RIGHT] and pressed[K_LEFT] == False:
self.hero.set_state(self.hero.STATE_WALKING)
self.hero.velocity[0] = HERO_MOVE_SPEED
else:
self.hero.state = self.hero.STATE_STANDING
self.hero.velocity[0] = 0
def update(self, dt):
""" Tasks that occur over time should be handled here
"""
self.group.update(dt, self)
def run(self):
""" Run the game loop
"""
clock = pygame.time.Clock()
self.running = True
from collections import deque
times = deque(maxlen=30)
try:
while self.running:
dt = clock.tick(60) / 1000.
times.append(clock.get_fps())
self.handle_input(dt)
self.update(dt)
self.draw(screen)
pygame.display.flip()
except KeyboardInterrupt:
self.running = False
if __name__ == "__main__":
pygame.init()
pygame.font.init()
screen = init_screen(800, 600)
pygame.display.set_caption('Test Game.')
try:
game = QuestGame()
game.run()
except:
pygame.quit()
raise
答案 0 :(得分:1)
除了英雄和QuestGame
课程之外我删除了所有内容并且可以看到错误的移动,因此问题不是由pyscroll
引起的(除非有更多问题)。
移动问题的原因是您将英雄的更新方法中的self._position
设置为rect的topleft
坐标。
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
pygame.Rect
只能存储整数并截断你分配给它们的浮点数,因此你不应该使用它们来更新英雄的实际位置。这是一个小小的示范:
>>> pos = 10
>>> rect = pygame.Rect(10, 0, 5, 5)
>>> pos -= 1.4 # Move left.
>>> rect.x = pos
>>> rect
<rect(8, 0, 5, 5)> # Truncated the actual position.
>>> pos = rect.x # Pos is now 8 so we moved 2 pixels.
>>> pos += 1.4 # Move right.
>>> rect.x = pos
>>> rect
<rect(9, 0, 5, 5)> # Truncated.
>>> pos = rect.x
>>> pos # Oops, we only moved 1 pixel to the right.
9
self._position
是确切的位置,如果英雄与墙壁或其他障碍物发生碰撞,则只应设置为其中一个rect
的坐标(因为矩形用于碰撞检测)。
将两条提到的行移动到if body_sensor.colliderect(wall.rect):
的墙碰撞中的loop
子句中,它应该可以正常工作。
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
self.rect.right = wall.rect.left
self._position[0] = self.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
self._position[0] = self.rect.left
if dy > 0: # Moving down; Hit the top side of the wall
self.rect.bottom = wall.rect.top
self._position[1] = self.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
self.rect.top = wall.rect.bottom
self._position[1] = self.rect.top