在 pygame 中为平台游戏添加滚动功能
- 2024-12-06 08:40:00
- admin 原创
- 131
问题描述:
好的,我在下面附上了我的项目代码,我只是在用 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:
因为现在,您有一个静态背景,并且您控制的玩家被传输到他所在的位置,您有两个选项可以始终显示中间的角色。
如果您的地图足够小,您可以有一个大的图像 A,并根据玩家的位置派生一个矩形,该矩形的大小与屏幕大小相同。这样,玩家将始终位于中间。Rect.clamp(Rect) 或 Rect.clamp_ip(Rect) 将帮助您实现这一点。
另一种方法是使用不同的元组来表示屏幕上的位置。玩家在屏幕中心的位置将为一个常量值,而背景位置将是玩家位置的负值。
解决方案 4:
唯一的方法是将地图中的逻辑位置与屏幕上的物理位置分开。
任何与在屏幕上实际绘制地图相关的代码 - 在您的情况下.rect
是精灵的所有属性 - 都必须基于屏幕实际使用的地图部分的偏移量来执行。
例如,您的屏幕可能从左上角的位置 (10,10) 开始显示地图 - 所有与显示相关的代码(在上述情况下为属性.rect
)应从当前逻辑位置中减去屏幕偏移量 - (假设角色位于地图坐标 (12,15) - 因此,应将其绘制在 (12,15) - (10, 10) -> (2, 5) BLOCK_SIZE 处)在上面的示例中,BLOCK_SIZE 被硬编码为 32,32,因此您希望将其绘制在显示屏上的物理像素位置 (2 32, 5 * 32) 处)
(提示:避免以这种方式进行硬编码,在代码开头将其作为常量声明)