49

好的,所以我在下面包含了我的项目的代码,我只是在尝试用 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()
4

4 回答 4

124

绘制实体时,您需要对实体的位置应用偏移。让我们称它为偏移量a camera,因为这是我们想要实现的效果。

首先,我们不能使用draw精灵组的功能,因为精灵不需要知道它们的位置(rect)不是它们要在屏幕上绘制的位置(最后,我们'将继承Group该类并重新实现它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)

这里需要注意一些事项:

我们需要存储相机的位置,以及以像素为单位的关卡的宽度和高度(因为我们想在关卡的边缘停止滚动)。我使用 aRect来存储所有这些信息,但您可以轻松地使用一些字段。

using在函数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_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函数来确保我们不会向外滚动。

通过像这样创建您的相机来尝试它:

camera = Camera(complex_camera, total_level_width, total_level_height)

我们的最终滚动有一个小动画:

在此处输入图像描述

这是完整的代码。注意我改变了一些东西:

  • 级别更大,拥有更多平台
  • 使用蟒蛇 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()
于 2013-01-16T11:19:42.770 回答
0

既然你知道,你有一个静态背景,你控制的玩家在他所在的位置上被blitted,你有两个选择总是在中间显示角色。

  1. 如果你的地图足够小,你可以有一个大的 img A,并根据玩家的位置推导出一个矩形,这将是屏幕的大小。这样,玩家将始终处于中间位置。一个 Rect.clamp(Rect) 或 Rect.clamp_ip(Rect) 将帮助你。

  2. 另一种方法是在屏幕上使用不同的元组作为位置。玩家将在屏幕中央有一个恒定值,而背景位置将是玩家位置的负值。

于 2013-01-16T09:06:54.523 回答
0

做到这一点的唯一方法是将地图中的逻辑位置与屏幕上的物理位置分开。

任何与在屏幕上实际绘制地图相关的代码(在您的情况下.rect是精灵的所有属性)都必须基于屏幕实际使用的地图部分的偏移量。

例如,您的屏幕可能会从左上角的位置 (10,10) 开始显示您的地图 - 所有显示相关的代码(在上述情况下是.rect属性)应该从当前逻辑位置减去屏幕偏移量 - (说字符在地图坐标(12,15)处 - 所以,它应该被绘制在 (12,15) - (10, 10) -> (2, 5) * BLOCK_SIZE) 在你上面的例子中 BLOCK_SIZE 被硬编码为 32 ,32,所以你想在显示器上的物理像素位置(2 * 32、5 * 32)绘制它)

(提示:避免以这种方式对事物进行硬编码,将其作为代码开头的常量声明)

于 2013-01-16T11:06:55.810 回答
0

不幸的是,Pygame 没有针对这个问题的内置解决方案。Pygame 使用pygame.sprite.Sprite组织在pygame.sprite.Groups中的对象。Sprites.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)
    

最后是关于脏机制和部分屏幕更新的评论:玩家一移动,整个屏幕都是脏的,需要更新。因此,您是否应该在部分更新机制上投入资源是值得怀疑的。这些算法也需要时间来运行。在高度动态的场景中,算法的结果是全部更新。

于 2021-05-15T07:23:16.807 回答