在 pygame 中为平台游戏添加滚动
问题描述
好的,所以我在下面包含了我的项目的代码,我只是在尝试使用 pygame 制作平台游戏.我试图弄清楚如何做一些非常简单的跟随玩家的滚动,所以玩家是相机的中心,它会反弹/跟随他.谁能帮帮我?
Ok so I included the code for my project below, I'm just doing some experimenting with pygame on making a platformer. I'm trying to figure out how to do some very simple scrolling that follows the player, so the player is the center of the camera and it bounces/follows him. Can anyone help me?
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()
解决方案
您需要在绘制实体时对实体的位置应用一个偏移.让我们称它为 offset 为 camera
,因为这是我们想要通过 this 实现的效果.
You need to apply an offset to the position of your entities when drawing them. Let's call that offset a camera
, since this is the effect we want to achieve with this.
首先,我们不能使用精灵组的draw
函数,因为精灵不需要知道它们的位置(rect
)是不是它们将要在屏幕上绘制的位置(最后,我们将继承 Group
类并重新实现它的 draw
以了解相机,但让我们慢慢开始吧).
First of all, we can't use the draw
function of the sprite group, since the sprites don't need to know that their position (rect
) is not the position they are going to be drawn on the screen (At the end, we'll subclass the Group
class and reimplement the it's draw
to be aware of the camera, but let's start slow).
让我们首先创建一个 Camera
类来保存我们想要应用到实体位置的偏移的状态:
Let's start by creating a Camera
class to hold the state of the offset we want to apply to the position of our entities:
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
来存储所有这些信息,但您可以轻松地使用一些字段.
We need to store the position of the camera, and the width and height of the level in pixels (since we want to stop scrolling at the edges of the level). I used a Rect
to store all these informations, but you could easily just use some fields.
使用 Rect
在 apply
函数中派上用场.这是我们重新计算屏幕上实体的位置以应用滚动的地方.
Using Rect
comes in handy in the apply
function. This is where we re-calculate the position of an entity on the screen to apply the scrolling.
主循环的每次迭代,我们都需要更新相机的位置,因此有 update
函数.它只是通过调用 camera_func
函数来改变状态,该函数将为我们完成所有艰苦的工作.我们稍后实施.
Once per iteration of the main loop, we need to update the position of the camera, hence there's the update
function. It just alters the state by calling the camera_func
function, which will do all the hard work for us. We implement it later.
让我们创建一个相机实例:
Let's create an instace of the camera:
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
函数),它可以跟随任意精灵,而不仅仅是玩家.您甚至可以在运行时更改它.
Our camera class is already very flexible and yet dead simple. It can use different kinds of scrolling (by providing different camera_func
functions), and it can follow any arbitary sprite, not just the player. You even can change this at runtime.
现在实现 camera_func
.一个简单的方法是让玩家(或我们想要跟随的任何实体)在屏幕上居中,实现起来很简单:
Now for the implementation of camera_func
. A simple approach is to just center the player (or whichever entity we want to follow) at the screen, and the implementation is straight forward:
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
的位置,并加上一半的屏幕大小.您可以像这样创建相机来尝试:
We just take the position of our target
, and add the half total screen size. You can try it by creating your camera like this:
camera = Camera(simple_camera, total_level_width, total_level_height)
到目前为止,一切都很好.但也许我们不想看到关卡外面的黑色背景?怎么样:
So far, so good. But maybe we don't want to see the black background outside the level? How about:
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
函数来确保我们不会滚动到 outside 级别.
Here we simply use the min
/max
functions to ensure we don't scroll outside out level.
通过这样创建您的相机来尝试:
Try it by creating your camera like this:
camera = Camera(complex_camera, total_level_width, total_level_height)
我们的最终滚动有一个小动画:
There's a little animation of our final scrolling in action:
这里又是完整的代码.注意我改变了一些东西:
Here's the complete code again. Note I changed some things:
- 关卡更大,平台更多
- 使用 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()
相关文章