在 pygame 中为平台游戏添加滚动功能

2024-12-06 08:40:00
admin
原创
131
摘要:问题描述:好的,我在下面附上了我的项目代码,我只是在用 pygame 制作平台游戏方面做了一些实验。我试图弄清楚如何做一些跟随玩家的非常简单的滚动,这样玩家就是相机的中心,它会弹跳/跟随他。有人能帮我吗?import pygame from pygame import * WIN_WIDTH = 800 W...

问题描述:

好的,我在下面附上了我的项目代码,我只是在用 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并重新实现它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_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)

这是我们最终滚动操作的一个小动画:

在此处输入图片描述

以下是完整代码。请注意,我做了一些更改:

  • 级别更大,并且有更多的平台

  • 使用 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 使用spygame.sprite.Sprite中组织的对象。Sprites的属性用于绘制对象以及对象之间的碰撞测试。没有内置功能可以在绘制之前将对象坐标转换为屏幕坐标。作为对 Pygame 开发人员的建议:在方法中为相机偏移量
提供一个可选参数会很好。pygame.sprite.Group`.rect`
pygame.sprite.Group.draw

有不同的方法:

  • 除了移动玩家,您还可以将场景中的任何物体朝相反方向移动。这是所有方法中最糟糕的。我强烈建议不要这样做。每次添加新物体时,您都需要确保它随着玩家的移动而移动。处理对象动画或浮点精度可能会变成一场噩梦。

  • 创建世界的虚拟屏幕大小,并在虚拟屏幕上绘制整个屏幕。在每一帧结束时,地图的一小部分会显示在屏幕上。

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

有两种可能性。您可以通过指定区域blit参数直接在屏幕上显示虚拟屏幕的区域:

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:

因为现在,您有一个静态背景,并且您控制的玩家被传输到他所在的位置,您有两个选项可以始终显示中间的角色。

  1. 如果您的地图足够小,您可以有一个大的图像 A,并根据玩家的位置派生一个矩形,该矩形的大小与屏幕大小相同。这样,玩家将始终位于中间。Rect.clamp(Rect) 或 Rect.clamp_ip(Rect) 将帮助您实现这一点。

  2. 另一种方法是使用不同的元组来表示屏幕上的位置。玩家在屏幕中心的位置将为一个常量值,而背景位置将是玩家位置的负值。

解决方案 4:

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

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

例如,您的屏幕可能从左上角的位置 (10,10) 开始显示地图 - 所有与显示相关的代码(在上述情况下为属性.rect)应从当前逻辑位置中减去屏幕偏移量 - (假设角色位于地图坐标 (12,15) - 因此,应将其绘制在 (12,15) - (10, 10) -> (2, 5) BLOCK_SIZE 处)在上面的示例中,BLOCK_SIZE 被硬编码为 32,32,因此您希望将其绘制在显示屏上的物理像素位置 (2 32, 5 * 32) 处)

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

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用