Вопрос-Ответ

Add scrolling to a platformer in pygame

Добавить прокрутку в платформер в pygame

Итак, я включил код для своего проекта ниже, я просто немного поэкспериментировал с pygame при создании платформера. Я пытаюсь понять, как выполнить очень простую прокрутку, которая следует за игроком, чтобы игрок был в центре камеры, и она отскакивала / следовала за ним. Кто-нибудь может мне помочь?

import pygame
from pygame import *

WIN_WIDTH = 800
WIN_HEIGHT = 640
HALF_WIDTH = int(WIN_WIDTH / 2)
HALF_HEIGHT = int(WIN_HEIGHT / 2)

DISPLAY = (WIN_WIDTH, WIN_HEIGHT)
DEPTH = 32
FLAGS = 0
CAMERA_SLACK = 30

def main():
global cameraX, cameraY
pygame.init()
screen = pygame.display.set_mode(DISPLAY, FLAGS, DEPTH)
pygame.display.set_caption("Use arrows to move!")
timer = pygame.time.Clock()

up = down = left = right = running = False
bg = Surface((32,32))
bg.convert()
bg.fill(Color("#000000"))
entities = pygame.sprite.Group()
player = Player(32, 32)
platforms = []

x = y = 0
level = [
"PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",
"P P",
"P P",
"P P",
"P P",
"P P",
"P P",
"P P",
"P PPPPPPPPPPP P",
"P P",
"P P",
"P P",
"P P",
"PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",]
# build the level
for row in level:
for col in row:
if col == "P":
p = Platform(x, y)
platforms.append(p)
entities.add(p)
if col == "E":
e = ExitBlock(x, y)
platforms.append(e)
entities.add(e)
x += 32
y += 32
x = 0

entities.add(player)

while 1:
timer.tick(60)

for e in pygame.event.get():
if e.type == QUIT: raise SystemExit, "QUIT"
if e.type == KEYDOWN and e.key == K_ESCAPE:
raise SystemExit, "ESCAPE"
if e.type == KEYDOWN and e.key == K_UP:
up = True
if e.type == KEYDOWN and e.key == K_DOWN:
down = True
if e.type == KEYDOWN and e.key == K_LEFT:
left = True
if e.type == KEYDOWN and e.key == K_RIGHT:
right = True
if e.type == KEYDOWN and e.key == K_SPACE:
running = True

if e.type == KEYUP and e.key == K_UP:
up = False
if e.type == KEYUP and e.key == K_DOWN:
down = False
if e.type == KEYUP and e.key == K_RIGHT:
right = False
if e.type == KEYUP and e.key == K_LEFT:
left = False
if e.type == KEYUP and e.key == K_RIGHT:
right = False

# draw background
for y in range(32):
for x in range(32):
screen.blit(bg, (x * 32, y * 32))

# update player, draw everything else
player.update(up, down, left, right, running, platforms)
entities.draw(screen)

pygame.display.update()

class Entity(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)

class Player(Entity):
def __init__(self, x, y):
Entity.__init__(self)
self.xvel = 0
self.yvel = 0
self.onGround = False
self.image = Surface((32,32))
self.image.fill(Color("#0000FF"))
self.image.convert()
self.rect = Rect(x, y, 32, 32)

def update(self, up, down, left, right, running, platforms):
if up:
# only jump if on the ground
if self.onGround: self.yvel -= 10
if down:
pass
if running:
self.xvel = 12
if left:
self.xvel = -8
if right:
self.xvel = 8
if not self.onGround:
# only accelerate with gravity if in the air
self.yvel += 0.3
# max falling speed
if self.yvel > 100: self.yvel = 100
if not(left or right):
self.xvel = 0
# increment in x direction
self.rect.left += self.xvel
# do x-axis collisions
self.collide(self.xvel, 0, platforms)
# increment in y direction
self.rect.top += self.yvel
# assuming we're in the air
self.onGround = False;
# do y-axis collisions
self.collide(0, self.yvel, platforms)

def collide(self, xvel, yvel, platforms):
for p in platforms:
if pygame.sprite.collide_rect(self, p):
if isinstance(p, ExitBlock):
pygame.event.post(pygame.event.Event(QUIT))
if xvel > 0:
self.rect.right = p.rect.left
print "collide right"
if xvel < 0:
self.rect.left = p.rect.right
print "collide left"
if yvel > 0:
self.rect.bottom = p.rect.top
self.onGround = True
self.yvel = 0
if yvel < 0:
self.rect.top = p.rect.bottom


class Platform(Entity):
def __init__(self, x, y):
Entity.__init__(self)
self.image = Surface((32, 32))
self.image.convert()
self.image.fill(Color("#DDDDDD"))
self.rect = Rect(x, y, 32, 32)

def update(self):
pass

class ExitBlock(Platform):
def __init__(self, x, y):
Platform.__init__(self, x, y)
self.image.fill(Color("#0033FF"))

if __name__ == "__main__":
main()
Переведено автоматически
Ответ 1

Вам нужно применить смещение к положению ваших объектов при их рисовании. Давайте назовем это смещение a camera, поскольку именно такого эффекта мы хотим добиться с помощью этого.

Прежде всего, мы не можем использовать draw функцию группы спрайтов, поскольку спрайтам не нужно знать, что их позиция (rect) отличается от позиции, в которой они будут нарисованы на экране (В конце мы создадим подкласс Group класса и переопределим it's draw, чтобы они знали о камере, но давайте начнем медленно).


Давайте начнем с создания Camera класса для хранения состояния смещения, которое мы хотим применить к положению наших объектов:

class Camera(object):
def __init__(self, camera_func, width, height):
self.camera_func = camera_func
self.state = Rect(0, 0, width, height)

def apply(self, target):
return target.rect.move(self.state.topleft)

def update(self, target):
self.state = self.camera_func(self.state, target.rect)

здесь следует отметить несколько моментов:

Нам нужно сохранить положение камеры, а также ширину и высоту уровня в пикселях (поскольку мы хотим остановить прокрутку по краям уровня). Я использовал Rect для хранения всей этой информации, но вы могли бы легко использовать просто некоторые поля.

Использование Rect удобно в apply функции. Здесь мы повторно вычисляем положение объекта на экране, чтобы применить прокрутку.

Один раз за итерацию основного цикла нам нужно обновить положение камеры, отсюда и функция update. Это просто изменяет состояние путем вызова camera_func функции, которая сделает всю тяжелую работу за нас. Мы реализуем это позже.

Давайте создадим изображение камеры:

for row in level:
...

total_level_width = len(level[0])*32 # calculate size of level in pixels
total_level_height = len(level)*32 # maybe make 32 an constant
camera = Camera(*to_be_implemented*, total_level_width, total_level_height)

entities.add(player)
...

и измените наш основной цикл:

# draw background
for y in range(32):
...

camera.update(player) # camera follows player. Note that we could also follow any other sprite

# update player, draw everything else
player.update(up, down, left, right, running, platforms)
for e in entities:
# apply the offset to each entity.
# call this for everything that should scroll,
# which is basically everything other than GUI/HUD/UI
screen.blit(e.image, camera.apply(e))

pygame.display.update()

Наш класс camera уже очень гибкий и в то же время предельно простой. Он может использовать разные виды прокрутки (предоставляя разные camera_func функции), и он может следовать за любым произвольным спрайтом, а не только за игроком. Вы даже можете изменить это во время выполнения.

Теперь о реализации camera_func. Простой подход заключается в том, чтобы просто расположить игрока (или любую другую сущность, за которой мы хотим следить) в центре экрана, и реализация будет простой:

def simple_camera(camera, target_rect):
l, t, _, _ = target_rect # l = left, t = top
_, _, w, h = camera # w = width, h = height
return Rect(-l+HALF_WIDTH, -t+HALF_HEIGHT, w, h)

Мы просто берем положение нашего target и добавляем половину общего размера экрана. Вы можете попробовать, создав свою камеру следующим образом:

camera = Camera(simple_camera, total_level_width, total_level_height)

Пока все хорошо. Но, может быть, мы не хотим видеть черный фон снаружи уровня? Как насчет:

def complex_camera(camera, target_rect):
# we want to center target_rect
x = -target_rect.center[0] + WIN_WIDTH/2
y = -target_rect.center[1] + WIN_HEIGHT/2
# move the camera. Let's use some vectors so we can easily substract/multiply
camera.topleft += (pygame.Vector2((x, y)) - pygame.Vector2(camera.topleft)) * 0.06 # add some smoothness coolnes
# set max/min x/y so we don't see stuff outside the world
camera.x = max(-(camera.width-WIN_WIDTH), min(0, camera.x))
camera.y = max(-(camera.height-WIN_HEIGHT), min(0, camera.y))

return camera

Здесь мы просто используем функции min/max, чтобы гарантировать, что мы не прокручиваем вне уровня out.

Попробуйте, создав свою камеру следующим образом:

camera = Camera(complex_camera, total_level_width, total_level_height)

Вот небольшая анимация нашей финальной прокрутки в действии:

введите описание изображения здесь

Вот снова полный код. Обратите внимание, что я кое-что изменил:


  • уровень стал больше, и появилось еще несколько платформ

  • используйте python 3

  • используйте группу спрайтов для управления камерой

  • переработан дублирующий код

  • поскольку Vector2 / 3 теперь стабилен, используйте их для упрощения математики

  • избавьтесь от этого уродливого кода обработки событий и используйте pygame.key.get_pressed вместо этого


 #! /usr/bin/python

import pygame
from pygame import *

SCREEN_SIZE = pygame.Rect((0, 0, 800, 640))
TILE_SIZE = 32
GRAVITY = pygame.Vector2((0, 0.3))

class CameraAwareLayeredUpdates(pygame.sprite.LayeredUpdates):
def __init__(self, target, world_size):
super().__init__()
self.target = target
self.cam = pygame.Vector2(0, 0)
self.world_size = world_size
if self.target:
self.add(target)

def update(self, *args):
super().update(*args)
if self.target:
x = -self.target.rect.center[0] + SCREEN_SIZE.width/2
y = -self.target.rect.center[1] + SCREEN_SIZE.height/2
self.cam += (pygame.Vector2((x, y)) - self.cam) * 0.05
self.cam.x = max(-(self.world_size.width-SCREEN_SIZE.width), min(0, self.cam.x))
self.cam.y = max(-(self.world_size.height-SCREEN_SIZE.height), min(0, self.cam.y))

def draw(self, surface):
spritedict = self.spritedict
surface_blit = surface.blit
dirty = self.lostsprites
self.lostsprites = []
dirty_append = dirty.append
init_rect = self._init_rect
for spr in self.sprites():
rec = spritedict[spr]
newrect = surface_blit(spr.image, spr.rect.move(self.cam))
if rec is init_rect:
dirty_append(newrect)
else:
if newrect.colliderect(rec):
dirty_append(newrect.union(rec))
else:
dirty_append(newrect)
dirty_append(rec)
spritedict[spr] = newrect
return dirty

def main():
pygame.init()
screen = pygame.display.set_mode(SCREEN_SIZE.size)
pygame.display.set_caption("Use arrows to move!")
timer = pygame.time.Clock()

level = [
"PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",
"P P",
"P P",
"P P",
"P PPPPPPPPPPP P",
"P P",
"P P",
"P P",
"P PPPPPPPP P",
"P P",
"P PPPPPPP P",
"P PPPPPP P",
"P P",
"P PPPPPPP P",
"P P",
"P PPPPPP P",
"P P",
"P PPPPPPPPPPP P",
"P P",
"P PPPPPPPPPPP P",
"P P",
"P P",
"P P",
"P P",
"PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",]


platforms = pygame.sprite.Group()
player = Player(platforms, (TILE_SIZE, TILE_SIZE))
level_width = len(level[0])*TILE_SIZE
level_height = len(level)*TILE_SIZE
entities = CameraAwareLayeredUpdates(player, pygame.Rect(0, 0, level_width, level_height))

# build the level
x = y = 0
for row in level:
for col in row:
if col == "P":
Platform((x, y), platforms, entities)
if col == "E":
ExitBlock((x, y), platforms, entities)
x += TILE_SIZE
y += TILE_SIZE
x = 0

while 1:

for e in pygame.event.get():
if e.type == QUIT:
return
if e.type == KEYDOWN and e.key == K_ESCAPE:
return

entities.update()

screen.fill((0, 0, 0))
entities.draw(screen)
pygame.display.update()
timer.tick(60)

class Entity(pygame.sprite.Sprite):
def __init__(self, color, pos, *groups):
super().__init__(*groups)
self.image = Surface((TILE_SIZE, TILE_SIZE))
self.image.fill(color)
self.rect = self.image.get_rect(topleft=pos)

class Player(Entity):
def __init__(self, platforms, pos, *groups):
super().__init__(Color("#0000FF"), pos)
self.vel = pygame.Vector2((0, 0))
self.onGround = False
self.platforms = platforms
self.speed = 8
self.jump_strength = 10

def update(self):
pressed = pygame.key.get_pressed()
up = pressed[K_UP]
left = pressed[K_LEFT]
right = pressed[K_RIGHT]
running = pressed[K_SPACE]

if up:
# only jump if on the ground
if self.onGround: self.vel.y = -self.jump_strength
if left:
self.vel.x = -self.speed
if right:
self.vel.x = self.speed
if running:
self.vel.x *= 1.5
if not self.onGround:
# only accelerate with gravity if in the air
self.vel += GRAVITY
# max falling speed
if self.vel.y > 100: self.vel.y = 100
print(self.vel.y)
if not(left or right):
self.vel.x = 0
# increment in x direction
self.rect.left += self.vel.x
# do x-axis collisions
self.collide(self.vel.x, 0, self.platforms)
# increment in y direction
self.rect.top += self.vel.y
# assuming we're in the air
self.onGround = False;
# do y-axis collisions
self.collide(0, self.vel.y, self.platforms)

def collide(self, xvel, yvel, platforms):
for p in platforms:
if pygame.sprite.collide_rect(self, p):
if isinstance(p, ExitBlock):
pygame.event.post(pygame.event.Event(QUIT))
if xvel > 0:
self.rect.right = p.rect.left
if xvel < 0:
self.rect.left = p.rect.right
if yvel > 0:
self.rect.bottom = p.rect.top
self.onGround = True
self.vel.y = 0
if yvel < 0:
self.rect.top = p.rect.bottom

class Platform(Entity):
def __init__(self, pos, *groups):
super().__init__(Color("#DDDDDD"), pos, *groups)

class ExitBlock(Entity):
def __init__(self, pos, *groups):
super().__init__(Color("#0033FF"), pos, *groups)

if __name__ == "__main__":
main()
Ответ 2

К сожалению, Pygame не имеет встроенного решения этой проблемы. Pygame использует pygame.sprite.Sprite объекты, организованные в pygame.sprite.Groups. Атрибут .rect спрайтов используется для рисования объектов, а также для проверки столкновений между объектами. Нет встроенной функции, которая могла бы преобразовывать координаты объекта в координаты экрана перед рисованием.
В качестве предложения для разработчиков Pygame: было бы неплохо иметь необязательный аргумент для смещения камеры в методе pygame.sprite.Group.draw.

Существуют разные подходы:


  • Вместо перемещения игрока вы можете перемещать любой объект в сцене в противоположном направлении. Это худший из всех подходов. Я настоятельно рекомендую этого не делать. Каждый раз, когда вы добавляете новый объект, вам нужно убедиться, что он перемещается вместе с игроком. Работа с анимацией объекта или точностью с плавающей запятой может оказаться кошмаром.



  • Создайте виртуальный размер экрана мира и нарисуйте весь экран на виртуальном экране. В конце каждого кадра на экране отображается часть карты.


    virtual_screen = pygame.Surface((map_width, map_height))

    Есть 2 возможности. Вы можете blit создать область виртуального экрана прямо на экране, указав аргумент area:


    camera_area = pygame.Rect(camera_x, camera_y, camera_width, camera_height)
    screen.blit(virtual_screen, (0, 0), camera_area)

    Другая возможность заключается в определении подповерхности, которая напрямую связана с исходной поверхностью, с помощьюsubsurface метода:


    camera_area = pygame.Rect(camera_x, camera_y, camera_width, camera_height)
    camera_subsurf = source_surf.subsurface(camera_area)

    screen.blit(camera_subsurf, (0, 0))

    Недостатком этого подхода является то, что он может занимать очень много памяти. Если виртуальный экран огромен, игра будет лагать. Это решение подходит только в том случае, если размер игровой области не намного больше экрана. Как правило, если игровая зона более чем в два раза превышает размер экрана, вам не следует идти этим путем (я говорю о удвоенном размере области, а не о удвоенной длине ее ширины и высоты).



  • Для больших игровых площадок единственный подход, который можно использовать, - это добавить смещение к объектам перед рисованием:


    offset_x = -camera_x
    offset_y = -camera_y
    for object in objects:
    screen.blit(object.image, (object.rect.x + offset_x, object.rect.y + offset_y))

    К сожалению, pygame.sprite.Group.draw в данном случае нельзя использовать напрямую. Этот подход подробно описан в ответе с высокой оценкой.

    В качестве альтернативы вы можете переместить все спрайты перед их рисованием:


    all_sprites = pygame.sprite.Group()

    for sprite in all_sprites:
    all_sprites.rect.move_ip(-camera_x, -camera_y)
    all_sprites.draw(screen)
    for sprite in all_sprites:
    all_sprites.rect.move_ip(camera_x, camera_y)


В конце комментарий о грязных механизмах и частичном обновлении экрана: как только игрок перемещается, весь экран становится грязным и нуждается в обновлении. Поэтому сомнительно, стоит ли вкладывать ресурсы в механизмы частичного обновления. Выполнение этих алгоритмов также требует времени. В высокодинамичных сценах результатом работы алгоритма является обновление всего.

Ответ 3

Поскольку, как вы уже знаете, у вас статичный фон, а игрок, которым вы управляете, находится в той позиции, в которой он находится, у вас есть 2 варианта, чтобы всегда показывать персонажа посередине.


  1. Если ваша карта достаточно мала, вы можете создать большое изображение A и вывести прямоугольник, основанный на позиции игрока, который будет размером экрана. Таким образом, игрок всегда будет в середине. В этом вам помогут Rect.clamp (Rect) или Rect.clamp_ip(Rect).


  2. Другой подход заключается в использовании другого кортежа для определения положения на экране. У игрока будет постоянное значение в центре экрана, в то время как положение фона будет отрицательным от положения игрока.


Ответ 4

Единственный способ сделать это - отделить логические позиции на карте от физических позиций на экране .

Любой код, связанный с фактическим отображением вашей карты на экране - в вашем случае все .rect атрибуты ваших спрайтов - должны выполняться на основе смещения той части вашей карты, которую фактически использует экран.

Например, на вашем экране может отображаться ваша карта, начинающаяся с позиции (10,10) в левом верхнем углу - весь код, связанный с их отображением (который в приведенном выше случае является .rectатрибутами), должен вычитать смещение экрана из текущей логической позиции - (скажем, символ находится в координатах карты (12,15) - таким образом, он должен быть нарисован в (12,15) - (10, 10) -> (2, 5) * РАЗМЕР БЛОКА) В вашем примере выше РАЗМЕР БЛОКА жестко задан равным 32,32, поэтому вы хотите нарисовать его в физическом положении пикселя (2 * 32, 5 * 32) на дисплее)

(подсказка: избегайте такого жесткого кодирования, сделайте это постоянным объявлением в начале вашего кода)

python pygame