简体   繁体   中英

How do I move the player smoothly in a tile based game?

In a tilebased rpg I am creating, I am trying to implement a function that moves the player between tiles smoothly. I have applied this in the player update and getkeys functions. When the player moves in any of the four directions, the program should calculate the next tile the player should land on, and until they land on that tile, the player should be moved smoothly between the two tiles.

However, the function I have created is not positioning the player correctly. The function is undershooting where the next tile should be, causing the player to move off the grid, which causes errors with collision.

import pygame as pg
import sys
vec = pg.math.Vector2

WHITE =     ( 255, 255, 255)
BLACK =     (   0,   0,   0)
RED =       ( 255,   0,   0)
YELLOW =    ( 255, 255,   0)
BLUE =      (   0,   0, 255)

WIDTH = 512 # 32 by 24 tiles
HEIGHT = 384
FPS = 60
TILESIZE = 32
PLAYER_SPEED = 3 * TILESIZE

MAP = ["1111111111111111",
       "1..............1",
       "1...........P..1",
       "1..1111........1",
       "1..1..1........1",
       "1..1111........1",
       "1..............1",
       "1........11111.1",
       "1........1...1.1",
       "1........11111.1",
       "1..............1",
       "1111111111111111"]


def collide_hit_rect(one, two):
    return one.hit_rect.colliderect(two.rect)

def player_collisions(sprite, group):
    hits_walls = pg.sprite.spritecollide(sprite, group, False, collide_hit_rect)
    if hits_walls:
        sprite.pos -= sprite.vel * TILESIZE




class Player(pg.sprite.Sprite):
    def __init__(self, game, x, y):
        self.groups = game.all_sprites
        pg.sprite.Sprite.__init__(self, self.groups)
        self.game = game
        self.walk_buffer = 200
        self.vel = vec(0, 0)
        self.pos = vec(x, y) *TILESIZE
        self.dirvec = vec(0, 0)
        self.last_pos = self.pos
        self.next_pos = vec(0, 0)
        
        self.current_frame = 0
        self.last_update = pg.time.get_ticks()
        self.walking = True
        self.between_tiles = False
        
        self.walking_sprites = [pg.Surface((TILESIZE, TILESIZE))]
        self.walking_sprites[0].fill(YELLOW)
        
        self.image = self.walking_sprites[0]
        self.rect = self.image.get_rect()
        self.hit_rect = self.rect
        self.hit_rect.bottom = self.rect.bottom


    def update(self):
        self.get_keys()
        self.rect = self.image.get_rect()
        self.rect.topleft = self.pos
        
        if self.pos == self.next_pos:
            self.between_tiles = False
            
        if self.between_tiles:
            self.pos += self.vel * self.game.dt
        
        self.hit_rect.topleft = self.pos
        player_collisions(self, self.game.walls)  # may change postion
        self.hit_rect.topleft = self.pos  # reset rectangle

        self.rect.midbottom = self.hit_rect.midbottom


    def get_keys(self):        
        self.dirvec = vec(0,0)
        now = pg.time.get_ticks()
        keys = pg.key.get_pressed()
        
        if now - self.last_update > self.walk_buffer:
            self.vel = vec(0,0)
            self.last_update = now
            if keys[pg.K_LEFT] or keys[pg.K_a]:
                self.dirvec.x = -1
                self.vel.x = -PLAYER_SPEED
            elif keys[pg.K_RIGHT] or keys[pg.K_d]:
                self.dirvec.x = 1
                self.vel.x = PLAYER_SPEED
            elif keys[pg.K_UP] or keys[pg.K_w]:
                self.dirvec.y = -1
                self.vel.y = -PLAYER_SPEED
            elif keys[pg.K_DOWN] or keys[pg.K_s]:
                self.dirvec.y = 1
                self.vel.y = PLAYER_SPEED
                

            if self.dirvec != vec(0,0):
                self.between_tiles = True
                self.walking = True

##                self.offset = self.vel * self.game.dt
                self.last_pos = self.pos
                self.next_pos = self.pos + self.dirvec * TILESIZE
                
            else:
                self.between_tiles = False
                self.walking = False





class Obstacle(pg.sprite.Sprite):
    def __init__(self, game, x, y):
        self.groups = game.walls
        pg.sprite.Sprite.__init__(self, self.groups)
        self.x = x * TILESIZE
        self.y = y * TILESIZE
        self.w = TILESIZE
        self.h = TILESIZE
        self.game = game
        self.image = pg.Surface((self.w,self.h))
        self.image.fill(BLACK)
        self.rect = self.image.get_rect()
        self.hit_rect = self.rect
        self.rect.x = self.x
        self.rect.y = self.y


class Game:
    def __init__(self):
        pg.init()
        self.screen = pg.display.set_mode((WIDTH, HEIGHT))
        pg.display.set_caption("Hello Stack Overflow")
        self.clock = pg.time.Clock()
        pg.key.set_repeat(500, 100)

    def new(self):
        self.all_sprites = pg.sprite.Group()
        self.walls = pg.sprite.Group()
        for row, tiles in enumerate(MAP):
            for col, tile in enumerate(tiles):
                if tile == "1":
                    Obstacle(self, col, row)
                elif tile == "P":
                    print("banana!")
                    self.player = Player(self, col, row)

    def quit(self):
        pg.quit()
        sys.exit()

    def run(self):
        # game loop - set self.playing = False to end the game
        self.playing = True
        while self.playing:
            self.dt = self.clock.tick(FPS) / 1000
            self.events()
            self.update()
            self.draw()


    def events(self):
        # catch all events here
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.quit()


    def update(self):
        self.player.update()


    def draw(self):
        self.screen.fill(WHITE)
        for wall in self.walls:
            self.screen.blit(wall.image, wall.rect)
        for sprite in self.all_sprites:
            self.screen.blit(sprite.image, sprite.rect)
    

        pg.display.flip()



# create the game object
g = Game()
while True:
    g.new()
    g.run()
    
pg.quit()




TL;DR update and getkeys functions are incorrectly calculating the position of the next tile the player should move too, causing them to fall off the tile grid and creating collsion errors

There are some issues.

Make sure that the motion status attributes are only changed when a key is pressed. Set a variable new_dir_vec when a key is pressed. Change the direction of movement and the status variables depending on the new direction of movement.

new_dir_vec = vec(0, 0)
if keys[pg.K_LEFT] or keys[pg.K_a]:
    new_dir_vec = vec(-1, 0)
# [...]

if new_dir_vec != vec(0,0):
    self.dirvec = new_dir_vec
    # [...]

The target position ( next_pos ) must be aligned with the grid. Calculate the index of the current cell and the target position:

current_index = self.rect.centerx // TILESIZE, self.rect.centery // TILESIZE
self.last_pos = vec(current_index) * TILESIZE
self.next_pos = self.last_pos + self.dirvec * TILESIZE

Complete method get_keys :

class Player(pg.sprite.Sprite):
    # [...]

    def get_keys(self):        
        now = pg.time.get_ticks()
        keys = pg.key.get_pressed()
        
        if now - self.last_update > self.walk_buffer:
            self.last_update = now
            
            new_dir_vec = vec(0, 0)
            if self.dirvec.y == 0:
                if keys[pg.K_LEFT] or keys[pg.K_a]:
                    new_dir_vec = vec(-1, 0)
                elif keys[pg.K_RIGHT] or keys[pg.K_d]:
                    new_dir_vec = vec(1, 0)
            if self.dirvec.x == 0:
                if keys[pg.K_UP] or keys[pg.K_w]:
                    new_dir_vec = vec(0, -1)
                elif keys[pg.K_DOWN] or keys[pg.K_s]:
                    new_dir_vec = vec(0, 1)
                
            if new_dir_vec != vec(0,0):
                self.dirvec = new_dir_vec
                self.vel = self.dirvec * PLAYER_SPEED
                self.between_tiles = True
                self.walking = True
                current_index = self.rect.centerx // TILESIZE, self.rect.centery // TILESIZE
                self.last_pos = vec(current_index) * TILESIZE
                self.next_pos = self.last_pos + self.dirvec * TILESIZE

Make sure the player doesn't step over the target. Compute the distance to the target ( delta = self.next_pos - self.pos ). If the next step is greater than the distance to the target, use the target position to determine the position ( self.pos = self.next_pos ):

delta = self.next_pos - self.pos
if delta.length() > (self.vel * self.game.dt).length():
    self.pos += self.vel * self.game.dt
else:
    self.pos = self.next_pos
    self.vel = vec(0, 0)
    # [...]

Complete method update :

class Player(pg.sprite.Sprite):
    # [...]

    def update(self):
        self.get_keys()
        self.rect = self.image.get_rect()
        self.rect.topleft = self.pos
        
        if self.pos != self.next_pos:
            
            delta = self.next_pos - self.pos
            if delta.length() > (self.vel * self.game.dt).length():
                self.pos += self.vel * self.game.dt
            else:
                self.pos = self.next_pos
                self.vel = vec(0, 0)
                self.dirvec = vec(0, 0)
                self.walking = False
                self.between_tiles = False
                    
        self.hit_rect.topleft = self.pos
        player_collisions(self, self.game.walls)  # may change postion
        self.hit_rect.topleft = self.pos  # reset rectangle

        self.rect.midbottom = self.hit_rect.midbottom

See also Move in grid .


Minimal example:

import pygame

TILESIZE = 32
WIDTH = TILESIZE * 16
HEIGHT = TILESIZE * 12
PLAYER_SPEED = 3 * TILESIZE

MAP = ["1111111111111111",
       "1..............1",
       "1...........P..1",
       "1..1111........1",
       "1..1..1........1",
       "1..1111........1",
       "1..............1",
       "1........11111.1",
       "1........1...1.1",
       "1........11111.1",
       "1..............1",
       "1111111111111111"]

class Player(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.walk_buffer = 50
        self.pos = pygame.math.Vector2(x, y) * TILESIZE
        self.dirvec = pygame.math.Vector2(0, 0)
        self.last_pos = self.pos
        self.next_pos = self.pos
        
        self.current_frame = 0
        self.last_update = pygame.time.get_ticks()
        self.between_tiles = False
        
        self.image = pygame.Surface((TILESIZE, TILESIZE))
        self.image.fill((255, 0, 0))
        self.rect = self.image.get_rect(topleft = (self.pos.x, self.pos.y))

    def update(self, dt, walls):
        self.get_keys()
        self.rect = self.image.get_rect(topleft = (self.pos.x, self.pos.y))
        
        if self.pos != self.next_pos:
            
            delta = self.next_pos - self.pos
            if delta.length() > (self.dirvec * PLAYER_SPEED * dt).length():
                self.pos += self.dirvec * PLAYER_SPEED * dt
            else:
                self.pos = self.next_pos
                self.dirvec = pygame.math.Vector2(0, 0)
                self.between_tiles = False
                    
        self.rect.topleft = self.pos
        if pygame.sprite.spritecollide(self, walls, False):
            self.pos = self.last_pos
            self.next_pos = self.last_pos
            self.dirvec = pygame.math.Vector2(0, 0)
            self.between_tiles = False
        self.rect.topleft = self.pos

    def get_keys(self):        
        now = pygame.time.get_ticks()
        keys = pygame.key.get_pressed()
        
        if now - self.last_update > self.walk_buffer:
            self.last_update = now
            
            new_dir_vec = pygame.math.Vector2(0, 0)
            if self.dirvec.y == 0:
                if keys[pygame.K_LEFT] or keys[pygame.K_a]:
                    new_dir_vec = pygame.math.Vector2(-1, 0)
                elif keys[pygame.K_RIGHT] or keys[pygame.K_d]:
                    new_dir_vec = pygame.math.Vector2(1, 0)
            if self.dirvec.x == 0:
                if keys[pygame.K_UP] or keys[pygame.K_w]:
                    new_dir_vec = pygame.math.Vector2(0, -1)
                elif keys[pygame.K_DOWN] or keys[pygame.K_s]:
                    new_dir_vec = pygame.math.Vector2(0, 1)
                
            if new_dir_vec != pygame.math.Vector2(0,0):
                self.dirvec = new_dir_vec
                self.between_tiles = True
                current_index = self.rect.centerx // TILESIZE, self.rect.centery // TILESIZE
                self.last_pos = pygame.math.Vector2(current_index) * TILESIZE
                self.next_pos = self.last_pos + self.dirvec * TILESIZE
                
class Obstacle(pygame.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = pygame.Surface((TILESIZE, TILESIZE))
        self.image.fill((0, 0, 0))
        self.rect = self.image.get_rect(topleft = (x * TILESIZE, y * TILESIZE))

pygame.init()
window = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()

all_sprites = pygame.sprite.Group()
walls = pygame.sprite.Group()
for row, tiles in enumerate(MAP):
    for col, tile in enumerate(tiles):
        if tile == "1":
            obstacle = Obstacle(col, row)
            walls.add(obstacle)
            all_sprites.add(obstacle)
        elif tile == "P":
            player = Player(col, row)
            all_sprites.add(player)

run = True
while run:
    dt = clock.tick(60) / 1000
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    
    player.update(dt, walls)
    
    window.fill((255, 255, 255))

    for x in range (0, window.get_width(), TILESIZE):
        pygame.draw.line(window, (127, 127, 127), (x, 0), (x, window.get_height()))
    for y in range (0, window.get_height(), TILESIZE):
        pygame.draw.line(window, (127, 127, 127), (0, y), (window.get_width(), y))

    walls.draw(window)
    for sprite in all_sprites:
        window.blit(sprite.image, sprite.rect)

    pygame.display.flip()
    
pygame.quit()
exit()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM