python - Sprite 向左移动的速度比向右移动的快

标签 python pygame pytmx

我认为我遇到了舍入问题,导致我的 Sprite 在向左移动时移动得更快/跳得更远。

我的 Sprite 更新方法是调用 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

最佳答案

我撕掉了除了英雄和 QuestGame 类之外的所有内容,并且可以看到不正确的移动,因此问题不是由 pyscroll 引起的(除非还有更多问题) )。

移动问题的原因是您将英雄更新方法中的self._position设置为矩形的topleft坐标。

self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]

pygame.Rect 只能存储您分配给它们的整数和截断 float ,因此您不应该使用它们来更新英雄的实际位置。这是一个小演示:

>>> 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 是准确的位置,如果英雄与墙壁或其他障碍物发生碰撞(因为矩形用于碰撞检测)。

将上述两行移动到loop的墙壁碰撞中的if body_sensor.colliderect(wall.rect):子句中,它应该可以正常工作。

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

关于python - Sprite 向左移动的速度比向右移动的快,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48046883/

相关文章:

python - gzip.open().read() 的大小参数

python - Pygame从外部字体文件加载字体

python - Pygame 使用 PyTMX 加载 Tilemap

python - pygame中的碰撞问题

python - 解决pygame binascii.Error : Incorrect padding?

python - 如何在 pygame 中使图 block map 滚动?

python - 在 Linux 中将文件作为参数传递

python - Bokeh 服务器 : Customize row ratio

python - Argparse 可选标准输入参数