【pygame实现星露谷物语风格游戏】8.游戏地图的绘制与摄像机的创建

本文详细介绍了如何在2D游戏中实现游戏背景图片随主角移动的摄像机效果,包括创建精灵类、引入z轴概念以控制绘制顺序,以及重写精灵组的draw方法以实现摄像机跟随。作者还展示了如何设置主角和地板的z轴以及处理偏移量以保持主角在屏幕中心。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.目的

本次需要完成

把游戏从黑色的背景变成如下的背景图片

并且设置一个跟随主角的摄像机,使主角一直保持在中间

二.代码实现

1.将地板绘制进去

和player一样,我们需要创建一个地板类,并将其变成精灵,加入精灵组,这样,只需要调用精灵组的draw函数就能顺带把地板给绘制进来了

我们新建一个sprites.py文件,我们会将以后写的所有精灵类都写到这个文件里,由于player这个精灵占的篇幅比较大,所以把它单独写到了player.py里面

现在,我们只需要在sprites.py里书写一个叫Generic的精灵类即可

import pygame

from settings import *


class Generic(pygame.sprite.Sprite):

    def __init__(self,pos,surf,groups):

        super().__init__(groups)

        self.image = surf

        self.rect = self.image.get_rect(topleft = pos)

 

这些都是老生常谈了

然后把sprites.py中的所有东西都导入到level.py中

from sprites import *

接下来在level.py的setup函数中创建这个精灵类的对象即可

def setup(self):

    self.player = Player((640,360),self.all_sprites)

    Generic(

        pos = (0,0),

        surf = pygame.image.load('../graphics/world/ground.png').convert_alpha(),

        groups = self.all_sprites

    )

 

如此,就完成了Generic类的对象的创建,并且自动把它加入到了all_sprites这个精灵组里,随着精灵组的draw函数被调用,地板也被绘制了出来

2.z轴的引入

不过此时我们会发现,运行程序后只能看到游戏地板的图片,看不到主角了

因为我们先创建了player这个精灵,再创建了地板这个精灵,绘制的顺序也是这样的,这就导致了地板把我们的主角给覆盖掉了

因此我们需要引入z轴的概念。

这是一个2D游戏,这里所谓的Z轴就是告诉系统先画什么再画什么

这里就要提及settings里面的一个常量了

LAYERS这个常量是一个字典,里面的key值是需要绘制的事物的名称,value值是它的优先级,其中优先级的数字越大,表示越晚绘制。

直到目前,我们只创建了其中的两个,一个是"ground",也就是地板,它的优先级是1,另一个是"main",也就是主角,它的优先级是7

按照这个优先级,系统就直到,应该先绘制地板,再绘制主角,这样主角就会覆盖到地板上面了

所以我们分别给Player和Generic设置z这个变量

原作者在设置这两个类的z轴变量时,分别在player里直接赋值,而在Generic里面采取了传参赋值的方式,实在是很麻烦,如果是我就直接在Generic里面写self.z = LAYERS['ground']了。但是怕以后会用得到,还是按照原作者的方式写的。

这样我们还需要在创建这个对象的时候把z轴作为一个参数传递进去

3.重写精灵组的draw方法

这样,我我们在绘制精灵的时候,不仅要根据z轴的大小来决定先绘制谁,而且还要达到摄像机跟随主角的效果,作者提供的精灵组的draw方法显然是不能满足我们的需求的,因此我们需要重写它的draw方法

首先创建一个CameraGroup类继承自pygame提供的精灵组类,并且与此同时我们的all_sprites就不能是精灵组类的对象了,而是我们自己新写的CameraGroup类的对象。

接下来,我们获取游戏窗口,并且开始重写draw函数,实际上我们并不是重写draw函数,只是写了一个名为custon_draw的函数,并且在level类的run函数中不再调用draw

函数而是改为调用custon_draw函数而已

要实现按照z值的顺序绘制精灵实际上很简单,只需要用for循环遍历LAYERS里面的值,并且每次循环绘制出所有z值与当前所需要绘制的z值相同的精灵即可

最后,不要忘了把level的run函数里面调用的draw函数改为我们重新写的custon_draw函数

这样,我们再次运行程序,就会发现我们的主角跑到了地板的上面

接着,就是本节另一个重要的部分,摄像机的制作了

实际上,大部分的2D游戏,看似是主角在移动,实际上是背景图片在移动,主角一直牢牢锁定在屏幕的中间,如果要达到主角向右移动的效果,就让背景图片向左移动

这应用到了一些数学的知识,让我们先看看代码怎么写,再来详细讲一下里面的每一个变量代表了什么

代码增加的部分如图红框所示

self.offset是一个偏移量,代表了玩家 的实际坐标 距离屏幕的中心 的一个向量

就比如游戏一开始,玩家就在屏幕的正中间

图中绿色的方框代表我们的游戏窗口,红色的点代表玩家,此时玩家的坐标为(SCREEN_WIDTH/2,SCREEN_HEIGHT/2)

由self.offset.x = player.rect.centerx - SCREEN_WIDTH / 2         self.offset.y = player.rect.centery - SCREEN_HEIGHT / 2  可以得知,此时的self.offset = (0,0)

又由self.display_surface.blit(sprite.image, offset_rect)可知,我们在进行绘制精灵的时候,绘制的坐标并不是精灵实际的坐标,而是offset_rect这个变量

而offset_rect = sprite.rect.copy()     offset_rect.center -= self.offset可知,offset_rect这个变量是由精灵的实际坐标减去self.offset带来的

那么此时,offset_rect = player.rect.center - self.offset = (SCREEN_WIDTH/2,SCREEN_HEIGHT/2) - (0,0) =  (SCREEN_WIDTH/2,SCREEN_HEIGHT/2)

所以此时玩家还是绘制在屏幕的正中间

而当玩家进行移动后,跑到了如图所示的位置

其中红色的点是玩家的真是坐标(player.rect.center)

那么self.offset就是 红色的点 减去 窗口的中心点 也就是 图中黑色的线指向的这个矢量

那么offset_rect 就是玩家的真实位置 减去这个矢量self.offset,就又回到了 游戏窗口的中心点

而我们绘制玩家的时候,实际上是把它绘制到offset_rect上,所以,无论玩家的真实坐标在哪,我们始终把它绘制到游戏窗口的正中间

我们可以在代码中加入如下的内容,来验证我们的推算是否属实:

可以看到,游戏一开始,主角在屏幕中间,player_rect_center = (640,360)

此时的offset_rect = (554,298),因为player_rect_center是取的主角的图像的中心点的坐标,而offset_rect是取得精灵图像的左上角的坐标,所以实际上这两个绘制出来是在同一个地方的

然后主角进行移动,player_rect_center不断在变换,但是offset_rect始终是(554,298),所以游戏的主角一直被绘制在窗口的正中间

接下来是游戏的背景绘制

游戏一开始,玩家的实际位置在窗口的中间,如图所示,绿色为游戏窗口,黄色为游戏的背景图片,红色的点为玩家

然后玩家移动到如图所示的红点位置,self.offset就是图中黑色箭头的向量

然后玩家绘制的位置就是玩家图片的位置减去这个黑色的向量,就是图中紫色的点

同样的,游戏背景绘制的位置也是游戏原本的位置减去这个黑色的向量,也就是图中紫色方框的位置

这样绿色的游戏窗口展示的游戏的背景也会随之改变

达到了玩家固定在窗口中间不动,背景图片移动的摄像机效果

三.完整代码

sprites.py:

import pygame

from settings import *


class Generic(pygame.sprite.Sprite):

    def __init__(self,pos,surf,groups,z = LAYERS['main']):

        super().__init__(groups)

        self.image = surf

        self.rect = self.image.get_rect(topleft = pos)

        self.z = z

 

player.py:

其中player.py只是给Player类中增加了一个名为self.z的变量

import pygame

from settings import *

from support import *

from timer import Timer


class Player(pygame.sprite.Sprite):

    def __init__(self,pos,group):

        #这个参数可以传一个精灵组,这样就会自动把该精灵加入到该精灵组中

        #也可以为空,这样需要在外面手动调用精灵组的add函数来将这个精灵加入到精灵组中

        super().__init__(group)


        self.import_assets()

        self.status = 'down_idle'

        self.frame_index = 0


        #这里的变量名一定要叫image,因为这是它父类Sprite规定的

        self.image = self.animations[self.status][self.frame_index]



        #这个get_rect()也是父类中设置的方法

        #返回值是有很多,大概有x,y,centerx,centery,center,width,height这几类

        #大概就是image的x坐标,y坐标,中心的x坐标,中心的y坐标,中心点的坐标,宽度,高度等

        #参数可以不填,那么位置就默认是(0,0),也可以填一个列表,比如(100,100),那么初始的位置就是(100,100)

        #也可以是center = 一个坐标,这表示设置该图像的中心在这个坐标上

        #同样的这里的变量名也一定要叫rect,这是父类规定的

        self.rect = self.image.get_rect(center = pos)


        #z轴

        self.z = LAYERS['main']


        #创建一个二维的向量,参数不填默认是(0,0)

        self.direction = pygame.math.Vector2()#速度的方向

        self.pos = pygame.math.Vector2(self.rect.center)#位置

        self.speed = 200#速度


        self.timers = {

            'tool use':Timer(350,self.use_tool),

            'tool switch': Timer(200),

            'seed use': Timer(350, self.use_seed),

            'seed switch': Timer(200),

        }


        self.tools = ['hoe', 'axe', 'water']

        self.tool_index = 0

        self.selected_tool = self.tools[self.tool_index]


        self.seeds = ['corn', 'tomato']

        self.seed_index = 0

        self.selected_seed = self.seeds[self.seed_index]


    def use_tool(self):

        pass


    def use_seed(self):

        pass

    def import_assets(self):

        self.animations = {'up': [], 'down': [], 'left': [], 'right': [],

                           'right_idle': [], 'left_idle': [], 'up_idle': [], 'down_idle': [],

                           'right_hoe': [], 'left_hoe': [], 'up_hoe': [], 'down_hoe': [],

                           'right_axe': [], 'left_axe': [], 'up_axe': [], 'down_axe': [],

                           'right_water': [], 'left_water': [], 'up_water': [], 'down_water': []}


        for animation in self.animations.keys():

            full_path = '../graphics/character/' + animation

            self.animations[animation] = import_folder(full_path)


    def animate(self, dt):


        # 4 是比较合适的数字

        # 数字 决定做动作的快慢

        # 做一个动作需要 1/4秒

        # fream_index += dt 的话,要经过1s,才能变成下一个整数,做下一个动作

        # fream_index += 4*dt,那么增加的速度就快了四倍,经过1/4秒就能做下一个动作了

        self.frame_index += 4 * dt

        # print(self.frame_index)

        if self.frame_index >= len(self.animations[self.status]):

            self.frame_index = 0


        self.image = self.animations[self.status][int(self.frame_index)]


    def input(self):

        keys = pygame.key.get_pressed()

        #已经在使用工具的时候,停止对按键的检测

        if not self.timers['tool use'].active:

            if keys[pygame.K_UP]:

                self.direction.y = -1

                self.status = 'up'

            elif keys[pygame.K_DOWN]:

                self.direction.y = 1

                self.status = 'down'

            else:

                self.direction.y = 0#不要忘记加上这一句,不然按下键盘后再松开也不会停


            if keys[pygame.K_RIGHT]:

                self.direction.x = 1

                self.status = 'right'

            elif keys[pygame.K_LEFT]:

                self.direction.x = -1

                self.status = 'left'

            else:

                self.direction.x = 0


            if keys[pygame.K_SPACE]:

                #启动计时器

                self.timers['tool use'].activate()

                #实用工具的时候是不能移动的

                self.direction = pygame.math.Vector2()

                #要从第一张图片开始绘制

                self.frame_index = 0


            #按下左边的shift键更换工具

            if keys[pygame.K_LSHIFT] and not self.timers['tool switch'].active:

                self.timers['tool switch'].activate()

                self.tool_index += 1

                self.tool_index = self.tool_index if self.tool_index < len(self.tools) else 0

                self.selected_tool = self.tools[self.tool_index]


            #按下左边的ctrl键,使用种子

            if keys[pygame.K_RCTRL]:

                self.timers['seed use'].activate()

                self.direction = pygame.math.Vector2()

                self.frame_index = 0


            #按下e键,切换种子

            if keys[pygame.K_RSHIFT] and not self.timers['seed switch'].active:

                self.timers['seed switch'].activate()

                self.seed_index += 1

                self.seed_index = self.seed_index if self.seed_index < len(self.seeds) else 0

                self.selected_seed = self.seeds[self.seed_index]


    def get_status(self):


        # idle

        if self.direction.magnitude() == 0:

            # self.status += '_idle'

            # 上面这种方法不可取因为每一帧都会在字符串后面加上一个_idel

            # 所以status会变成 xxx_idle_idle_idle

            # 实际上当出现两个_idle的时候就已经报错了

            # 下面这种方法

            # split('_')是把一个字符串 以 '_' 为分节符分开

            # 他返回的是一个列表

            # 比如 a_b_c_d

            # 返回的就是[a,b,c,d]

            # 所以下面的【0】获取的就是_之前的内容

            self.status = self.status.split('_')[0] + '_idle'


        if self.timers['tool use'].active:

            self.status = self.status.split('_')[0] + '_' + self.selected_tool


    def update_timers(self):

        for timer in self.timers.values():

            timer.update()


    def move(self,dt):

        #向量归一化,比如一个n维向量为(x1,x2,x3...,xn)

        #那么向量归一化的操作就是等比例缩放x1到xn的大小让 根号下(x1的平方+x2的平方+...+xn的平方) 等于1

        #归一化的目的是如果同时按右键和上,那么direction就会变成(1,1),他的速度向量就是一个大小为根2,方向右上的向量了

        #magnitude()返回的是一个float类型的数据,他的大小为根号下(x1的平方+x2的平方+...+xn的平方)

        if self.direction.magnitude() > 0:#这表示如果有按键按下,如果向量里面全是0,会使归一化中的数学计算出现错误

            self.direction = self.direction.normalize()


        #位移 = 方向 * 速度 * 变化的时间()

        self.pos.x += self.direction.x * self.speed * dt

        self.rect.centerx = self.pos.x


        self.pos.y += self.direction.y * self.speed * dt

        self.rect.centery = self.pos.y


    def update(self,dt):

        self.input()

        self.get_status()

        self.update_timers()


        self.move(dt)

        self.animate(dt)

 

level.py:

import pygame

from settings import *

from player import Player

from overlay import Overlay

from sprites import *


class Level():

    def __init__(self):

        #得到屏幕的画面,得到的这个画面与main.py中的screen相同

        self.display_surface = pygame.display.get_surface()

        #创建精灵组

        self.all_sprites = CameraGroup()

        #调用setup方法

        self.setup()

        #创建工具和种子显示图层

        self.overlay = Overlay(self.player)


    def setup(self):

        self.player = Player((640,360),self.all_sprites)

        Generic(

            pos = (0,0),

            surf = pygame.image.load('../graphics/world/ground.png').convert_alpha(),

            groups = self.all_sprites,

            z = LAYERS['ground']

        )


    def run(self,dt):

        #窗口的背景设为黑色

        self.display_surface.fill('black')

        #调用精灵组的draw方法

        self.all_sprites.custom_draw(self.player)

        #调用精灵组的update方法

        self.all_sprites.update(dt)


        self.overlay.display()


class CameraGroup(pygame.sprite.Group):

    def __init__(self):

        super().__init__()

        #获取窗口

        self.display_surface = pygame.display.get_surface()

        #这是一个偏移量,代表的是玩家的实际位置与屏幕中间的矢量

        self.offset = pygame.math.Vector2()

    def custom_draw(self,player):

        self.offset.x = player.rect.centerx - SCREEN_WIDTH / 2

        self.offset.y = player.rect.centery - SCREEN_HEIGHT / 2


        for layer in LAYERS.values():#按照z轴从小到达绘制

            for sprite in self.sprites():

                if sprite.z == layer:#如果该精灵的z值等于当前要绘制的z值,才绘制

                    offset_rect = sprite.rect.copy()

                    offset_rect.center -= self.offset

                    #if sprite == player:

                        #print("player.rect.center为:(" +

                             # str(player.rect.centerx)+"," + str(player.rect.centery)+")")

                        #print("offset_rect为:(" + str(offset_rect.x)

                             # +"," +str(offset_rect.y)+")")

                    self.display_surface.blit(sprite.image,offset_rect)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

owooooow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值