【pygame实现星露谷物语风格游戏】11.砍树

一.目标

实现树上随机生成苹果,玩家在手持斧头并且面对树木的时候按下空格可以砍树,砍树可以砍掉苹果。砍五下树后树会死亡,变成树桩

二.代码实现

首先在Tree类里面进行修改

这里的APPLE_POS是一个常量,它的形式如下图所示

他是一个字典,key值是树的名字,按照大树和小树来进行明明,value值是一系列的坐标,这个坐标是相对的,是苹果相对于果树的左上角的坐标

接下来是create_fruit函数

def create_fruit(self):

    for pos in self.apple_pos:

       if randint(0,10) < 2:

          #x坐标 = 偏移坐标 + 树的左边缘坐标

          x = pos[0] + self.rect.left

          #y坐标 = 偏移坐标 + 树的顶部边缘坐标

          y = pos[1] + self.rect.top

          Generic(

             pos = (x,y),

             surf = self.apple_surf,

             #把它加入苹果精灵组和all_sprites精灵组

             #all_sprites是传入的参数的self.group的第一个元素,所以是【0】

             groups = [self.apple_sprites,self.groups()[0]],

             z = LAYERS['fruit'])

 

对于我们获取的常量中的一系列坐标,每一个坐标都有十分之二的概率会生成苹果,因为我们从[0,10)之间取随机数,如果这个随机数小于2,就在该位置创建一个苹果。

因为我们用到了randint,所以还需要在这里导入一下

我们一会还得用到random里面的choice方法,所以在这里把全部的东西都导入进来了,而不是只导入了randint

from random import *

这样我们就可以在树上看到苹果了

但是,经过测试发现,打开游戏大概率树上不会显示苹果,问题出在这里

实际上,self.groups()是pygame的作者给我们提供的一个函数,它会返回这个精灵包含在哪个精灵组里面,返回值是一个列表的形式

但是这个方法返回的第0号索引,并不一定是all_sprites这个精灵组,而只有all_sprites这个精灵组才被我们调用draw方法,所以苹果有时候会绘制不出来

原作者之所以笃定0号索引一定是all_sprites,是因为我们在创建树木的时候,传递的group参数是一个列表,作者实际上是想获取这个参数的0号索引

但是原作者在写Tree这个类的时候,忘记了用self.groups来接收group这个参数,刚好self.groups()这个方法与self.groups重名,pygame给自动补全了,所以原作者以为他用self.groups接受了这个参数了,刚好self.groups()这个方法的返回值也是一个列表,阴差阳错下没有报错,又刚好原作者在录视频的时候启动那一次游戏,groups()函数返回的列表刚好把all_sprites放到了0号索引的位置(pygame的官方文档并没有提及返回的列表里的精灵组按什么顺序排序,但根据实际表现来看,很有可能是随机排序的),导致那一次启动苹果能够正常显示出来,这事也就过去了

总之,我们在tree这个类中接收了group这个参数就可以了

self.groups = groups

并且把self.groups()改成self.groups就行了

接着就是砍树功能的实现,大体思路就是玩家那边砍树会调用tree这边的函数,相应砍树的结果,先不管player那边怎么写,tree这边的思路就是给树设置一个生命值,写一个damage函数,每次玩家砍树都会调用tree的damage函数,在damage函数里对树的生命值减一,同时如果树上还有苹果,就去掉一个苹果。如果树的生命值小于等于0了,就把树变成死亡状态,改变贴图,碰撞箱等等

首先准备一些变量,那个计时器目前没用到,以后会用到,先创建一个吧。当然别忘了把Timer import进来

接着就是damage函数

def damage(self):


    #生命值减一

    self.health -= 1


    #看看生命值是否小于0了,如果小于0就把该改的东西都改一下就行可

    if self.health <= 0:

       self.image = self.stump_surf

       self.rect = self.image.get_rect(midbottom = self.rect.midbottom)

       self.hitbox = self.rect.copy().inflate(-10,-self.rect.height * 0.6)

       self.alive = False


    #如果树上还有苹果,随机选一个去除

    if len(self.apple_sprites.sprites()) > 0:

       random_apple = choice(self.apple_sprites.sprites())

       random_apple.kill()#Sprite.kill()就是将该精灵删除

 

其中涉及到了一个新的API:

sprite.kill() 就是删除这个精灵

接着,就需要修改player那边的代码了

逻辑是,首先判断玩家在使用斧子的时候面前是否有树。只需要找到玩家挥出斧子后,斧子会停留在哪个位置,比如下图中的蓝色点的位置

获取该位置的坐标,并且判断是否与树木的碰撞箱重合,如果碰撞箱重合了,就说明斧子砍到了树上,就调用树的damage函数

首先创建一个tree精灵组,把所有的树都加入到这个精灵组里面,再把这个精灵组当作参数传递到Player类里

接下来来到player.py里,接收这个参数

接下来就是改写use_tool函数,先判断使用的是什么工具,这里先把浇水和锄头给pass掉

遍历所有的树木,如果有一个树木的碰撞箱与我们上面说的蓝点的位置发生了重合,说明正在砍的就是这棵树,就调用这棵树的damage函数

代码中的self.target_pos,正是我们上面所说的蓝点的坐标,我们还没有获取他,接下来让我们写获取它的坐标的代码:

实际上很简单,就是玩家的中心点坐标加上一个偏移量

由于玩家面朝上下左右,这个偏移量是不一样的,所以把这个偏移量以字典的形式存到了settings.py里了,其形式如下图所示

由于玩家在不断移动,所以我们每帧都要重新获取这个坐标,所以在update函数里调用这个函数

至此,我们就完成了砍树功能

三.完整代码

sprites.py

import pygame

from settings import *

from random import *

from timer import Timer


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

       self.hitbox = self.rect.copy().inflate(-self.rect.width * 0.2, -self.rect.height * 0.75)



class Water(Generic):

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

       #传入的参数,fream是一个列表,存放了水流不同动作的五张图片

       self.frames = frames

       #fream_index是正在播放第几张图片

       self.frame_index = 0


       super().__init__(

             pos = pos,

             surf = self.frames[self.frame_index],

             groups = groups,

             z = LAYERS['water'])


    def animate(self,dt):

       self.frame_index += 5 * dt

       if self.frame_index >= len(self.frames):

          self.frame_index = 0

       self.image = self.frames[int(self.frame_index)]


    def update(self,dt):

       self.animate(dt)


class WildFlower(Generic):

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

       super().__init__(pos, surf, groups)

       self.hitbox = self.rect.copy().inflate(-20,-self.rect.height)


class Tree(Generic):

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

       super().__init__(pos, surf, groups)


       #创建苹果用到的变量

       #导入苹果的图像素材

       self.apple_surf = pygame.image.load('../graphics/fruit/apple.png')

       #苹果的坐标,是一个常量

       self.apple_pos = APPLE_POS[name]

       #创建苹果精灵组,这个精灵组会在砍树的时候用到

       self.apple_sprites = pygame.sprite.Group()

       self.groups = groups

       self.create_fruit()


       #为砍树用到的变量

       self.health = 5#树的生命值

       self.alive = True#树是否存活

       stump_path = f'../graphics/stumps/{"small" if name == "Small" else "large"}.png'#树状的图片路径

       self.stump_surf = pygame.image.load(stump_path).convert_alpha()#把树状图片导入进来

       self.invul_timer = Timer(200)#创建一个计时器


    def create_fruit(self):

       for pos in self.apple_pos:

          if randint(0,10) < 2:

             #x坐标 = 偏移坐标 + 树的左边缘坐标

             x = pos[0] + self.rect.left

             #y坐标 = 偏移坐标 + 树的顶部边缘坐标

             y = pos[1] + self.rect.top

             Generic(

                pos = (x,y),

                surf = self.apple_surf,

                #把它加入苹果精灵组和all_sprites精灵组

                #all_sprites是传入的参数的self.group的第一个元素,所以是【0】

                groups = [self.apple_sprites,self.groups[0]],

                z = LAYERS['fruit'])


    def damage(self):


       #生命值减一

       self.health -= 1


       #看看生命值是否小于0了,如果小于0就把该改的东西都改一下就行可

       if self.health <= 0:

          self.image = self.stump_surf

          self.rect = self.image.get_rect(midbottom = self.rect.midbottom)

          self.hitbox = self.rect.copy().inflate(-10,-self.rect.height * 0.6)

          self.alive = False


       #如果树上还有苹果,随机选一个去除

       if len(self.apple_sprites.sprites()) > 0:

          random_apple = choice(self.apple_sprites.sprites())

          random_apple.kill()#Sprite.kill()就是将该精灵删除

 

player.py

import pygame

from settings import *

from support import *

from timer import Timer


class Player(pygame.sprite.Sprite):

    def __init__(self,pos,group,collision_sprites,tree_sprites):

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

        #也可以为空,这样需要在外面手动调用精灵组的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.hitbox = self.rect.copy().inflate((-126,-70))

        self.collision_sprites = collision_sprites


        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]


        #接收树木精灵组

        self.tree_sprites = tree_sprites



    def use_tool(self):

        if self.selected_tool == 'hoe':

            pass


        if self.selected_tool == 'axe':

            for tree in self.tree_sprites.sprites():

                if tree.rect.collidepoint(self.target_pos):

                    tree.damage()


        if self.selected_tool == 'water':

            pass


    def get_target_pos(self):

        # 获取工具的作用区域坐标

        self.target_pos = self.rect.center + PLAYER_TOOL_OFFSET[self.status.split('_')[0]]


    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 collision(self,direction):

        # 遍历碰撞箱精灵组的所有精灵

        for sprite in self.collision_sprites.sprites():

            # 如果该精灵有一个叫hitbox的属性

            #实际上collision_sprites精灵组内的所有精灵都应该有hitbox这个属性,这句话属实多余

            if hasattr(sprite,'hitbox'):

                #如果该精灵的hitbox与玩家的hitbox有重叠

                if sprite.hitbox.colliderect(self.hitbox):

                    #如果此时正在水平方向移动

                    if direction == 'horizontal':

                        if self.direction.x > 0:  #玩家正在向右移动

                            self.hitbox.right = sprite.hitbox.left

                        if self.direction.x < 0:  # 玩家正在向左移动

                            self.hitbox.left = sprite.hitbox.right

                        self.rect.centerx = self.hitbox.centerx

                        self.pos.x = self.hitbox.centerx

                    #如果此时正在竖直方向移动

                    if direction == 'vertical':

                        if self.direction.y > 0:  # 玩家正在向下移动

                            self.hitbox.bottom = sprite.hitbox.top

                        if self.direction.y < 0:  # 玩家正在向上移动

                            self.hitbox.top = sprite.hitbox.bottom

                        self.rect.centery = self.hitbox.centery

                        self.pos.y = self.hitbox.centery



    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.hitbox.centerx = round(self.pos.x)

        self.rect.centerx = self.hitbox.centerx

        self.collision('horizontal')


        #竖直方向

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

        self.hitbox.centery = round(self.pos.y)

        self.rect.centery = self.hitbox.centery

        self.collision('vertical')


    def update(self,dt):

        self.input()

        self.get_status()

        self.update_timers()

        self.get_target_pos()


        self.move(dt)

        self.animate(dt)

 

level.py

import pygame

from settings import *

from player import Player

from overlay import Overlay

from sprites import *

from pytmx.util_pygame import load_pygame

from support import *


class Level():

    def __init__(self):

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

        self.display_surface = pygame.display.get_surface()


        #创建精灵组

        self.all_sprites = CameraGroup()

        #具有碰撞箱的精灵组

        self.collision_sprites = pygame.sprite.Group()

        #树木精灵组

        self.tree_sprites = pygame.sprite.Group()


        #调用setup方法

        self.setup()

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

        self.overlay = Overlay(self.player)


    def setup(self):


        #载入.tmx文件

        tmx_data = load_pygame('../data/map.tmx')


        #绘制房子与栅栏,他们都属于Generic类

        for layer in ['HouseFloor', 'HouseFurnitureBottom']:

            for x, y, surf in tmx_data.get_layer_by_name(layer).tiles():

                Generic((x * TILE_SIZE, y * TILE_SIZE), surf, self.all_sprites, LAYERS['house bottom'])


        for layer in ['HouseWalls', 'HouseFurnitureTop','Fence']:

            for x, y, surf in tmx_data.get_layer_by_name(layer).tiles():

                Generic((x * TILE_SIZE, y * TILE_SIZE), surf, [self.all_sprites, self.collision_sprites])




        #水流

        water_frames = import_folder('../graphics/water')

        for x, y, surf in tmx_data.get_layer_by_name('Water').tiles():

            Water((x * TILE_SIZE, y * TILE_SIZE), water_frames, self.all_sprites)


        #树木

        for obj in tmx_data.get_layer_by_name('Trees'):

            Tree((obj.x, obj.y), obj.image,

                 [self.all_sprites, self.collision_sprites,self.tree_sprites], obj.name)

        #野花

        for obj in tmx_data.get_layer_by_name('Decoration'):

            WildFlower((obj.x, obj.y), obj.image,[self.all_sprites, self.collision_sprites])


        #空气墙

        for x, y, surf in tmx_data.get_layer_by_name('Collision').tiles():

            Generic((x * TILE_SIZE, y * TILE_SIZE), pygame.Surface((TILE_SIZE, TILE_SIZE)), self.collision_sprites)


        #玩家

        for obj in tmx_data.get_layer_by_name('Player'):

            if obj.name == 'Start':

                self.player = Player(

                    pos=(obj.x, obj.y),

                    group=self.all_sprites,

                    collision_sprites=self.collision_sprites,

                    tree_sprites=self.tree_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 sorted(self.sprites(),key = lambda sprite: sprite.rect.centery):

                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、付费专栏及课程。

余额充值