【pygame实现星露谷物语风格游戏】2.玩家的创建与移动

一.前言

上一节中,我们完成了main.py与settings.py的代码,实现了窗口的创建与显示,并且完成了level.py中Level类的框架,等待我们以后的完善。

这次我们要完成玩家的创建于移动。效果如下图所示

由于我们还没有给玩家赋予一个形象(这一点需要下一节来完成),因此采用一个绿色的方块来代表玩家。

我们可以按键盘上的上下左右键来控制玩家的移动,并且同时按住上或者下于左或者右可以斜着移动。

在进行代码编写之前,我们需要先了解一个pygame中的概念:精灵于精灵组

二.精灵与精灵组

精灵和精灵组实际上是pygame的作者给我们写的两个类,我们可以在自己写的类里面继承作者提供的精灵类,这样我们就可以在自己的类里调用精灵类里面的方法了,而不用我们自己写了。

精灵与精灵组的存在是为了解决如下的问题的:比如我们写一个飞机大战的游戏,我们的主角只有一个,我们可以给他设置他的外观,他的移动方式等等。但是一般敌机都是有很多很多个的,我们不可能一个个的写每一个敌机的外观,移动的规律等等。

因此我们把敌机写在一个类里面,比如A型号的敌机写成一个类A,B型号的敌机写成一个类B,等等等等,然后分别在每一种敌机的类里面设置他们的外观,移动规律等等,然后再跑到主函数里面创建这些敌机类的对象,然后再调用这些对象的绘制函数(在屏幕上把敌机绘制出来)和移动的函数。

但是这样写主函数也会很长,因为每一个类都需要我们创建对象,调用函数,创建对象,调用函数。

因此我们可以让这些类继承精灵类,继承自精灵类之后,我们就可以创建一个精灵组,来存放这些继承精灵类的类,然后对这些类进行统一的管理。

这也要求我们在写这些继承精灵类的类是,需要规范化,这一点在下面会提及。

1.精灵组提供的接口

精灵组的创建如下所示:

group = pygame.sprite.Group()

这样,我们就可以调用group的add函数来给这个精灵组加入精灵,也就是继承自精灵类的类的对象了

我们可以创建多个精灵组,让具有相似性质的精灵存在一个精灵里面,统一进行管理。

比如游戏中,到了白天,会出现太阳和很多白云,就把太阳和这些白云放到同一个精灵组里,到了白天,就调用该精灵组的draw函数,这样一行代码,就可以把组内的太阳和很多很多的云全都绘制到屏幕上了。到了晚上,会出现月亮和星星,就把月亮和星星放到另一个精灵组里面,到了晚上调用这个精灵组的draw函数即可。

精灵组的draw函数:

group.draw(screen)

该函数的作用是将该精灵组的所有精灵都绘制到窗口上,其中参数screen是要在哪一个窗口上绘制

实际上该函数是分别调用了精灵组中每一个精灵的draw函数,但是精灵类已经给我们提供了完善的draw函数,因此我们没有必要在自己的类里面重写draw函数了

精灵组的update函数:

group.update()

该函数的作用是调用精灵组中每一个精灵的update函数,每一个精灵的update函数都需要我们自己写,因为只有我们才知道我们想要我们的精灵干什么。

2.如何写一个精灵

很简单,只需要正常的写一个类,让它继承作者提供的精灵类,然后创建这个类的对象,我们就称这个对象是精灵,我们就可以把它加入到精灵组中,去执行draw函数,或者执行update函数了:

class A(pygame.sprite.Sprite):

    def __init__(self):

        pass

    def update(self):

        pass


#创建精灵组

group = pygame.sprite.Group()

#创建精灵

a = A()

#把精灵加入精灵组

group.add(a)

 

当然,因为我们需要继承Sprite类,因此我们在书写自己的类时,要相对规范,符合父类对我们的要求,具体如下:

在初始化函数__init__中,我们需要:

1.调用父类的__init__函数,这是python要求的,只管写就是了,不过值得注意的是,在调用父类的__init__函数的时候,我们可以选择传一个参数,这个参数是一个精灵组,这样,我们在创建该类的对象的时候,就会自动把该对象加入到那个精灵组中,不需要再调用精灵组的add函数了

class A(pygame.sprite.Sprite):

    def __init__(self,g):

        super().__init__(g)

    def update(self):

        pass


#创建精灵组

group = pygame.sprite.Group()

#创建精灵

a = A(group)

 

当然也可以选择不加这个参数

2.我们一定要有两个变量,名称分别为self.image和self.rect,这两个变量分别为精灵的外貌(也就是一张图片)和精灵的位置大小等参数。这两个变量的名字一定不要打错,因为父类中明明白白写的就是这两个变量,如果没有这两个变量,那么draw函数就没办法正常运行,因为绘制的时候我们需要知道精灵长什么样子和精灵在哪里。

一般来说,我们先用pygame的load函数导入一张图片,让这张图片当作精灵的外貌,把这张图片赋值给self.image,但是我们这里先不使用导入的图片,而是先用一张32X64的绿色方块图片来代表我们的精灵的外貌:

self.image = pygame.Surface((32,64))

self.image.fill('green')

 

其中pygame.Surface((32,64))是创建一张32*64的方块图片,fill('green')是把这个方块填充成绿色

self.rect则需要通过self.image得到,pygame给self.image提供了一个方法:

self.rect = self.image.get_rect()

self.image.get_rect()会返回这张图片的宽度和高度,顺带的,还会返回一些图片的位置信息,如果我们不传参数的话,它的位置会默认再(0,0)处,它的坐标系就不过多赘述了,就是左上角为(0,0),x轴是向右的,y轴是向下的

如果参数传递为一个坐标,比如(100,100),那么就会把这张图片左上角设置在(100,100)的地方

如果参数为一个这样的形式的东西:center = (100,100),那么就会把图片的中心设置在(100,100)的地方

它的返回值需要用self.rect来接收,它就代表了精灵的位置

它有如下的双属性:

self.rect.x:精灵左上角的x坐标

self.rect.y:精灵左上角的y坐标

self.rect.centerx:精灵中心点的x坐标

self.rect.centery:精灵中心点的y坐标

self.rect.center:精灵中心点的坐标

self.rect.width:精灵的宽度

self.rect.height:精灵的高度

后续我们可以在update函数中修改self.rect中的这些位置属性,来达到精灵移动的目的

于是,一个完整的精灵的__init__函数至少应该具有以下的东西:

class A(pygame.sprite.Sprite):

    def __init__(self):

        super().__init__()

        self.image = 一张图片

        self.rect = self.image.get_rect()

    def update(self):

        pass

 

而update函数,就没有这么多要求,按照自己的需求写即可。

三.在上一节代码的基础上编写代码

1.在level.py中改动的东西

我们想要写一个玩家类,这个玩家类继承精灵类,然后创建一个玩家类的对象,把玩家类的对象放入一个精灵组中,然后在run函数中,调用该精灵组的draw函数和update函数。

我们并不想在level.py中写Player类,为了代码的阅读性,我们创建一个文件叫player.py,并在这里书写Player类。不过现在,让我们先假装已经写完了,来对level.py进行修改。

首先要从player.py中把Player类导入进来,然后在__init__函数中创建一个精灵组,并且创建一个Player对象,不过,我们以后肯定会写很多个类,创建很多个对象,因此我们不希望把这些代码都堆到__init__函数中,所以我们把创建对象并把对象放入精灵组的代码写到一个叫setup的函数里,然后在run函数内,调用精灵组的draw函数和update函数。

代码更改的部分如下图方框所指所示

其中把精灵加入精灵组是利用了调用父类的__init__函数时传参的方法,因此并没有在这里采用精灵组的add函数,而是把精灵组当作一个参数传入到了Player类里了

在创建对象的时候,两个参数分别是精灵的初始位置,和精灵组

而update函数里面的参数,就是上节提到的dt,也就是每一帧消耗多少秒,这个在后面会用到

这也意味着,我们在写Player类的update函数时,也会用到一个叫dt的参数

2.player.py的书写

首先导入pygame和settings,settings作为存储本项目所有常量的文件,建议不管用不用得到都先导进去

import pygame

from settings import *

 

接着写Player类

class Player(pygame.sprite.Sprite):

    def __init__(self,pos,group):

       

        super().__init__(group)


        self.image = pygame.Surface((32,64))

        self.image.fill('green')


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


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

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

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

        self.speed = 200#速度

 

其中pos和group是传进来的参数,我们把图片的中心点的初始位置设置到pos处

重点是后面的三行,其他的都是已经讲过的知识了

pygame.math.Vector2()是创建一个二维的矢量

当然,也有pygame.math.Vector3(),是创建一个三维的矢量,但是作为一个2D游戏,只会用到二维矢量

它的物理意义就不再多讲了,这里只说一下它在python中的表现形式

这里以二维的为例,他就是一个有两个元素的列表,我们可以分别称它们为x和y:[x,y]

如果是三维的,就是一个有三个元素的列表,我们可以分别称他们为x,y,z:[x,y,z]

x和y分别是float类型,代表这个矢量在x轴方向上的映射值,和这个矢量在y轴方向的映射值,比如一个二维矢量【1,2】,代表如下图的一个矢量:

pygame.math.Vector2()就是创建二维矢量的一个方法,括号内不传参数代表这个矢量的初始值为【0,0】,如果想要设置初始值,就给他传相应的参数就行了。

这里,我们想用

self.direction这个二维矢量代表速度的方向,比如[0,0]代表没有速度,[1,0]代表速度向x轴的正方向,[-1,0]代表速度向x轴的负方向,等等。

这里可能会想为什么不直接设置两个int类型的变量x和y来代表速度的方向呢,因为我们接下来会用到作者给我们提供的一些方法,这个以后再说。

self.pos这个二维矢量代表玩家的位置,因此我们在创建的时候,需要给他初始化到玩家的位置上:self.pos = pygame.math.Vector2(self.rect.center)

self.speed代表速度,也就是每秒可以移动多少像素

接下来,我们想要达到的效果是,按下键盘,玩家移动,首先我们要检测玩家是否按下按钮,这里我们写到input函数里:

def input(self):

    keys = pygame.key.get_pressed()


    if keys[pygame.K_UP]:

        self.direction.y = -1

    elif keys[pygame.K_DOWN]:

        self.direction.y = 1

    else:

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


    if keys[pygame.K_RIGHT]:

        self.direction.x = 1

    elif keys[pygame.K_LEFT]:

        self.direction.x = -1

    else:

        self.direction.x = 0

 

pygame.key.get_pressed()返回的是一个字典,他会检测各个按键是否按下,如果按下,该键对应的值就会变成1,比如如果我们按下方向键的上键,那么keys[pygame.K_UP]就会变成1,否者就是0。pygame里每一个按键有他的名字,这里我们先只用上下左右键,分别名字叫pygame.K_UP,pygame.K_DOWN,pygame.K_RIGHT,pygame.K_LEFT

这样,如果某个按键按下,对应的方向的速度就会改变,如果松开,对应方向的速度就会归零

接着我们写玩家移动的函数

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

 

利用的原理很简单,位移 + 方向  速度  变化的时间

但是当我们同时按下上和右的时候,我们会发现玩家的移动速度会比直着走要快,因为此时的direction为[1,1],这个矢量的大小为根号2了,但是我们希望速度的方向这个矢量的大小始终是1,因此我们需要调用作者给我们提供的归一化方法:

self.direction.normalize(),它的作用就是等比例的缩放矢量中每一个方向的值,使这个矢量的大小(比如二维就是根号下x方+y方)保持为1

但是仅仅这样在移动的时候还是会报错,因为当我们没有按下任何按键的时候,direction为[0,0],这时候调用normalize函数,其中的数学运算是会发生错误的,因此我们还要加上这样的一个判断: if self.direction.magnitude() > 0:

magnitude()返回的是一个矢量的欧几里得距离,放到二维里也就是根号下x方+y方

这样我们可以在update函数中调用input函数和move函数了

def update(self, dt):

    self.input()

    self.move(dt)

 

基本上大部分2d游戏的移动控制的思路都是这样,如果实在不能理解说明欠缺其中的数学知识,文字的形式实在难以讲清楚,建议先把这段代码copy下来,直接进行后面的部分

三.完整代码:

这里只放发生过改变的文件的完整代码:

level.py:

import pygame

from settings import *

from player import Player


class Level:

    def __init__(self):


       # get the display surface

       self.display_surface = pygame.display.get_surface()


       # sprite groups

       self.all_sprites = pygame.sprite.Group()


       self.setup()


    def setup(self):

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


    def run(self,dt):

       self.display_surface.fill('black')

       self.all_sprites.draw(self.display_surface)

       self.all_sprites.update(dt)

 

player.py:

import pygame

from settings import *


class Player(pygame.sprite.Sprite):

    def __init__(self, pos, group):

       super().__init__(group)


       # general setup

       #创建一个矩形来代表玩家

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

       self.image = pygame.Surface((32,64))

       self.image.fill('green')

       #设置坐标

       #返回值有很多,既包含了距离也包含了大小

       #大概有x,y,centerx,centery,width,height这六个返回值

       #参数如果不写,默认为(0,0),也就是把他的位置设为左上角

       #这里参数为center = pos,也就是把他的中心放在了pos位置上

       #这里的变量一定要取名为rect,这是它的父类Sprite的规定

       #只有这样,才能正确调用 draw函数

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


       # movement attributes

       self.direction = pygame.math.Vector2()

       self.pos = pygame.math.Vector2(self.rect.center)

       self.speed = 200


    def input(self):

       keys = pygame.key.get_pressed()


       if keys[pygame.K_UP]:

          self.direction.y = -1

       elif keys[pygame.K_DOWN]:

          self.direction.y = 1

       else:

          self.direction.y = 0


       if keys[pygame.K_RIGHT]:

          self.direction.x = 1

       elif keys[pygame.K_LEFT]:

          self.direction.x = -1

       else:

          self.direction.x = 0


    def move(self,dt):


       # normalizing a vector

       #向量归一化,pygame提供了一个便捷的API

       if self.direction.magnitude() > 0:#只有按下按钮移动的时候才能调用,不然会报错

          #magnitude是返回向量的欧几里得距离

          #向量的存储格式是[x,y],归一化的作用是让x**2 + y**2 = 1

          #但是没有按下按钮的时候,【0,0】会使数学计算发生错误

          self.direction = self.direction.normalize()


       # horizontal movement

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

       #如果不*dt,就是一秒变化的距离,但实际上每一帧都在调用move函数,因此需要乘上一帧所用的时间,才是真正的位移

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

       #rect.centerx 是中心点的x坐标,如果是rect.x就是左上角的x坐标

       self.rect.centerx = self.pos.x


       # vertical movement

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

       self.rect.centery = self.pos.y



    def update(self, dt):

       self.input()

       self.move(dt)

 

四.总结

首先,我们知道,在主循环中,每次循环都会调用level.py中的run函数,而我们精灵组的draw函数和update函数写在了run函数中,因此每次循环(每一帧)都会调用精灵组draw函数和update函数,而我们精灵组中的player的update函数,调用了键盘检测函数input和移动函数move,因此每一帧,我们电脑都会检测是否有按键按下,并且改变精灵的坐标(rect.x,rect.y),并且将精灵在相应的坐标上绘制出来,也就达到了玩家移动的目的

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

owooooow

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

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

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

打赏作者

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

抵扣说明:

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

余额充值