Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
328 views
in Technique[技术] by (71.8m points)

pygame - Puzzled by my sprite's unequal +/ - velocity

I have two sprites in my game. The zombie sprite works perfectly, moving in all directions at a velocity of 1.0. My player sprite however, despite moves more slowly in the positive x/y direction, despite all values in all directions being 2.25.

For the life of me I can't seem to see what is wrong here.

Full working code:

import pygame
import random
import sys
import itertools
import math
import time
from datetime import datetime
from librarymodified import *
from pygame.locals import *

# prints text using the supplied font
def print_text(font, x, y, text, color=(255,255,255)):
    imgText = font.render(text, True, color)
    DISPLAYSURF.blit(imgText, (x,y))

# MySprite class extends pygame.sprite.Sprite
class MySprite(pygame.sprite.Sprite):

    def __init__(self, target):
        pygame.sprite.Sprite.__init__(self) #extend the base Sprite class
        self.master_image = None
        self.frame = 0
        self.old_frame = -1
        self.frame_width = 1
        self.frame_height = 1
        self.first_frame = 0
        self.last_frame = 0
        self.columns = 1
        self.last_time = 0
        self.direction = 0
        self.times_hit = 0
        self.direction = 0
        self.velocity = Point(0.0,0.0)

    # times_hit property
    def _get_times_hit(self): return self.times_hit
    def _set_times_hit(self, hits): self.times_hit += hits
    number_hits_taken = property(_get_times_hit, _set_times_hit)

    #X property
    def _getx(self): return self.rect.x
    def _setx(self,value): self.rect.x = value
    X = property(_getx,_setx)

    #Y property
    def _gety(self): return self.rect.y
    def _sety(self,value): self.rect.y = value
    Y = property(_gety,_sety)

    #position property
    def _getpos(self): return self.rect.topleft
    def _setpos(self,pos): self.rect.topleft = pos
    position = property(_getpos,_setpos)


    def load(self, filename, width, height, columns):
        self.master_image = pygame.image.load(filename).convert_alpha()
        self.frame_width = width
        self.frame_height = height
        self.rect = Rect(0,0,width,height)
        self.columns = columns
        #try to auto-calculate total frames
        rect = self.master_image.get_rect()
        self.last_frame = (rect.width // width) * (rect.height // height) - 1

    def update(self, current_time, rate=30):
        #update animation frame number
        if current_time > self.last_time + rate:
            self.frame += 1
            if self.frame > self.last_frame:
                self.frame = self.first_frame
            self.last_time = current_time

        #build current frame only if it changed
        if self.frame != self.old_frame:
            frame_x = (self.frame % self.columns) * self.frame_width
            frame_y = (self.frame // self.columns) * self.frame_height
            rect = Rect(frame_x, frame_y, self.frame_width, self.frame_height)
            self.image = self.master_image.subsurface(rect)
            self.old_frame = self.frame

    def __str__(self):
        return str(self.frame) + "," + str(self.first_frame) + 
               "," + str(self.last_frame) + "," + str(self.frame_width) + 
               "," + str(self.frame_height) + "," + str(self.columns) + 
               "," + str(self.rect)

#Point class
class Point(object):
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    #X property
    def getx(self): return self.__x
    def setx(self, x): self.__x = x
    x = property(getx, setx)

    #Y property
    def gety(self): return self.__y
    def sety(self, y): self.__y = y
    y = property(gety, sety)

    def __str__(self):
        return "{X:" + "{:.0f}".format(self.__x) + 
            ",Y:" + "{:.0f}".format(self.__y) + "}"


def calc_velocity(direction, vel = 1.0):
    velocity = Point(0, 0)
    if direction == 0: # North
        velocity.y = -vel
    elif direction == 2: # East
        velocity.x = vel
    elif direction == 4: # south
        velocity.y = vel
    elif direction == 6: # west
        velocity.x = -vel
    return velocity

def reverse_direction(sprite):
    if sprite.direction == 0:
        sprite.direction = 4
    elif sprite.direction == 2:
        sprite.direction = 6
    elif sprite.direction == 4:
        sprite.direction = 0
    elif sprite.direction == 6:
        sprite.direction = 2




# main
pygame.init()

DISPLAYSURF = pygame.display.set_mode((800,600))
pygame.display.set_caption("Collision Detection")
font = pygame.font.SysFont(None, 36)
fpsclock = pygame.time.Clock()
fps = 30

# create sprite groups
zombie_group = pygame.sprite.Group()
player_group = pygame.sprite.Group()
health_group = pygame.sprite.Group()

# create player sprite
player = MySprite(DISPLAYSURF)
player.load("farmer walk.png", 96, 96, 8)
player.position = (80,80)
player.direction = 4
player_group.add(player)

# create zombie sprite
zombie_image = pygame.image.load("zombie walk.png").convert_alpha()
for i in range(1):
    zombie = MySprite(DISPLAYSURF)
    zombie.load("zombie walk.png", 96, 96, 8)
    zombie.position = (random.randint(0, 700), random.randint(0, 500))
    zombie.direction = random.randint(0,3) * 2
    zombie_group.add(zombie)

# create health sprite
health = MySprite(DISPLAYSURF)
health.load("health.png", 32, 32, 1)
health.position = (400, 300)
health_group.add(health)

game_over = False
player_moving = False
player_health = 100

# colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 0)
YELLOW = (255, 255, 0)

##DISPLAYSURF.fill(BLACK)
##pygame.mouse.set_visible(True)


# event loop
while True:
    ticks = pygame.time.get_ticks() # ms since pygame.init() called
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
        if event.type == MOUSEMOTION:
            mousex, mousey = event.pos

    # keyboard polling
    keys = pygame.key.get_pressed()
    if keys[K_ESCAPE]:
        pygame.quit()
        sys.exit()
    elif keys[K_UP] or keys[K_w]:
        player.direction = 0
        player_moving = True
    elif keys[K_RIGHT] or keys[K_d]:
        player.direction = 2
        player_moving = True
    elif keys[K_LEFT] or keys[K_a]:
        player.direction = 6
        player_moving = True
    elif keys[K_DOWN] or keys[K_s]:
        player.direction = 4
        player_moving = True
    else:
        player_moving = False

    # these things should not happen if game is over
    if not game_over:
        # update player sprite
        player_group.update(ticks, 50)
        # use player direction to calculate frame range
        player.first_frame = player.direction * player.columns
        player.last_frame = player.first_frame + player.columns-1
        if player.frame < player.first_frame:
            player.frame = player.first_frame

        if not player_moving:
            # stop animating when player is not moving
            player.frame = player.first_frame = player.last_frame
        else:
            # move player in that direction
            player.velocity = calc_velocity(player.direction, 1.5)
            player.velocity.x *= 1.5
            player.velocity.y *= 1.5

        # manually move player
        if player_moving:
            player.X += player.velocity.x
            player.Y += player.velocity.y
            if player.X <0: player.X = 0
            elif player.X > 700: player.X = 700
            if player.Y <0: player.Y = 0
            elif player.Y > 500: player.Y = 500


        # update zombie sprites
        zombie_group.update(ticks, 50)

        # manually update zombies
        for z in zombie_group:
            # set zombie animation range
            z.first_frame = z.direction * z.columns
            z.last_frame = z.first_frame + z.columns-1
            if z.frame < z.first_frame:
                z.frame = z.first_frame
            z.velocity = calc_velocity(z.direction)

            # keep zombie on screen
            z.X += z.velocity.x
            z.Y += z.velocity.y
            if z.X < 0 or z.X > 700 or z.Y < 0 or z.Y > 500:
                reverse_direction(z)

        # check for sprite collision
        attacker = 0
        attacker = pygame.sprite.spritecollideany(player, zombie_group)
        if attacker != None:
            # more precise check
            if pygame.sprite.collide_rect_ratio(0.5)(player, attacker):
                player_health -= 10
                if attacker.X < player.X: attacker.X -= 10
                elif attacker.X > player.X: attacker.X += 10
            else:
                attacker = None

        # update health drop
        health_group.update(ticks, 50)

        # check for collision with health
        if pygame.sprite.collide_rect_ratio(0.5)(player, health):
            player_health += 30
            if player_health >100: player_health = 100
            health.X = random.randint(0, 700)
            health.Y = random.randint(0, 500)

        # is player dead?
        if player_health <= 0:
            game_over = True

        # clear screen
        DISPLAYSURF.fill((50,50,100))

        # draw sprites
        player_group.draw(DISPLAYSURF)
        zombie_group.draw(DISPLAYSURF)
        health_group.draw(DISPLAYSURF)

        # draw energy bar
        pygame.draw.rect(DISPLAYSURF, WHITE, (299, 555, 203, 31), 2)
        pygame.draw.rect(DISPLAYSURF, GREEN, (301, 557, player_health * 2, 28))

        # print zombie and player velocities for purpose of testing
        print_text(font, 350, 460, "Zombie X vel: " +
                   str(zombie.velocity.x) + "
Y vel: " +
                   str(zombie.velocity.y))
        print_text(font, 350, 500, "Player X vel: " +
                   str(player.velocity.x) + "
Y vel: " +
                   str(player.velocity.y))

    if game_over:
        print_text(font, 300, 200, "G A M E   O V E R")

    pygame.display.update()
    fpsclock.tick(fps)
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Problem

The sprite group is drawing your sprite using the sprite's rect attribute. A pygame Rect object can only hold integers, so it'll truncate all floating point numbers.

Let's say you have a x = 5.

  • If you add 1.1: x += 1.1 <=> x = x + 1.1 <=> x = 5 + 1.1 <=> x = 6.1 which will be truncated to x = 6. It have increased by 1.
  • If you subtract 1.1: x -= 1.1 <=> x = x - 1.1 <=> x = 5 - 1.1 <=> x = 3.9 which will be truncated to x = 3. It have decreased by 2.

In other words: You'll move faster in the left direction than the right (the same principle applies to negative numbers). Here's an example demonstrating it:

import pygame
pygame.init()


class Player(pygame.sprite.Sprite):
    def __init__(self, group):
        super(Player, self).__init__(group)
        self.image = pygame.Surface((32, 32))
        self.rect = self.image.get_rect()


screen = pygame.display.set_mode((100, 100))
group = pygame.sprite.Group()
player = Player(group)
clock = pygame.time.Clock()

while True:
    clock.tick(10)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            quit()
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                x = player.rect.x + 1.1
                print("Actual x:", x)
                player.rect.x = player.rect.x + 1.1
                print("Truncated x:", player.rect.x)
            elif event.key == pygame.K_LEFT:
                x = player.rect.x - 1.1
                print("Actual x:", x)
                player.rect.x = player.rect.x - 1.1
                print("Truncated x:", player.rect.x)

    screen.fill((255, 255, 255))
    group.draw(screen)
    pygame.display.update()

Solution

Using floating point numbers for position is great; it makes it possible to move a sprite less than a pixel every frame (if your game updates 120 times per second and you want your sprite to move only 30 pixels per second).

However, you have to compensate for the fact that the rect objects cannot hold them. The most straightforward solution is to have an attribute position which keep track of the position of the sprite in floating point precision. Then at every update change the rect to the position of the attribute. Like this:

import pygame
pygame.init()


class Player(pygame.sprite.Sprite):
    def __init__(self, group):
        super(Player, self).__init__(group)
        self.image = pygame.Surface((32, 32))
        self.rect = self.image.get_rect()
        self.position = self.rect.x  # Or whatever point of the rect you want the position to be.


screen = pygame.display.set_mode((100, 100))
group = pygame.sprite.Group()
player = Player(group)
clock = pygame.time.Clock()

while True:
    clock.tick(10)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            quit()
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                player.position += 1.1
                player.rect.x = player.position
            elif event.key == pygame.K_LEFT:
                player.position -= 1.1
                player.rect.x = player.position

    screen.fill((255, 255, 255))
    group.draw(screen)
    pygame.display.update()

I've only showed how this movement works in the x-axis, but it's exactly the same on the y-axis.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...