【pygame实现星露谷物语风格游戏】21.交易商店的实现(完结)

一.目的

实现游戏交易商店,玩家可以出售农作物或者树木苹果来换取金币,金币可以用来购买种子

二.代码实现

我们之前给player创建的背包里面只有树木苹果和农作物,现在我们给player新增存储种子的背包和金币

既然有了种子的背包,那么我们的种子就不再是无限量的了,我们在种植的时候会先判断背包里的种子是否足够,再去种植,种植过后种子数目会减一

接下来,我们就要书写交易商店部分的代码了,打开tmx文件,发现player图层下有一片名为treder的区域,和之前的bed一样,我们在同样的地方把它当作Interaction类的精灵给创建出来

接下来,就是回到我们player.py里面,当时我们在写上床睡觉功能的时候,在input函数中给交易预留出来了写代码的位置

如果玩家在交易区域按下回车键,就会执行toggle_shop()函数,这个函数我们应该写道level.py里,接下来我们来完成这个函数

首先我们的思路是,在level.py中设置一个变量,来判断目前是否正在打开商店,如果正在打开商店,就会执行一些打开商店的代码,我们把这个变量称为shop_active

接下来,就是实现我们上面提及的toggle_shop函数,玩家在交易区域按下回车键,执行toggle_shop函数,代表玩家正在交易,所以,toggle_shop函数首先应该把shop_active由false改为true

我们只是在level.py中实现了这个函数,在player.py中没办法直接调用,所以我们把它当作参数传递进去

并且在player.py中接收这个参数即可

玩家执行打开商店的动作后,窗口需要弹出商店的菜单,接下来,我们来实现这个菜单,我们选择把这个东西写到一个新的文件中,称为menu.py,在里面写Menu类。我们先进行简单的初始化,并且在update函数中将其展示到屏幕上,由于我们还没对菜单进行界面设计,因此我们先用一个1000*1000的黑色方块来代替它

接下来把这个类import进level.py里

接着实例化对象出来

接下来来到level.py的run函数中,判断如果现在正处于打开商店的状态,就调用menu的update函数,来吧菜单展示出来,并且我们还需要对其他的一些动作进行限制,比如正在打开商店就停止所有精灵的update,或者下雨的时候打开商店就先不要生成下雨的动画

这样,我们运行游戏,走到商人旁边按下回车,会发现屏幕上弹出了窗口

接下来,我们还需要实现按键检测来关闭菜单,玩家按下回车后,关闭商店,关于按键检测的部分我们写在一个名为input的函数中,并且在update中调用input函数。

我们初步完成了菜单的打开如关闭,接下来,我们就要绘制如下图所示的菜单了

首先我们要搞清楚一些名称代表的含义

如上图所示,整个商店菜单所在的区域被称为主要的矩形(main_rect),而每一项商品都写在一块白色的背景矩形上,这个背景矩形被称为bg_rect,矩形与矩形之间的空隙叫做space,而矩形的黑色边框(这里只有wood展现出了黑色边框)被称为padding

接下来就在init里面设置一些参数,包括菜单的宽度,space,padding,还需要把商店内商品的名称给存到一个列表里,并且还要区分哪些是出售的,哪些是收购的

我们把str类型的文字加载进了options这个列表里,要想把文字显示在屏幕上,我们还需要让这些str类型的数据经过font字体的渲染才可以,我们之前已经在init里面创建了字体,

接下来我们可以调用字体的render方法,把文字渲染出来,其用法如下所示

渲染后的文字 = self.font.render(要渲染的文字,是否抗锯齿,字体的颜色)

其中第二个参数 是否抗锯齿 是一个bool类型的数据,如果是True,就表示渲染字体的时候需要抗锯齿,抗锯齿的效果是让字的边缘写出来更加平滑,少了很多毛刺

这样,我们就得到了选然后的文字,选然后的文字可以视为一张图片,我们可以用blit函数将其绘制到屏幕上了

接下来,我们创建一个setup函数,完成将option列表中的文字全都渲染出来,并把选然后的文字都存到一个列表中的操作

我们创建一个名为text_surfs的列表来存放这些选然后的文字

同时我们还创建了一个total_heights的变量,来记录菜单究竟需要多高

为了程序的拓展性,我们并没有把菜单的高度设为一个固定的值,而是根据商店内商品的数量而实时计算出来的,这样,我们可以在以后自行更加更多的商品,菜单的大小也会随之自动改变

total_height很容易计算,要把每一个选然后的文字的高度算入其中,文字与文字之间还要有空间(space),每个文字后续我们还要给他们设置边框(padding),每一个边框在竖直方向上都有上边框和下边框,把这些都算进去就好了

之后,我们还在setup里面设置了菜单放置的位置,我们把它放置在了屏幕的正中间,并且用了Rect数据类型的main_rect来存放它的所有位置信息

把文本导入,并且确定了整体的菜单栏在什么位置之后,我们就可以跑到update函数里面,把我们之前绘制的黑色框框删去,绘制我们的菜单了

我们把选然后的文字都存到了text_surfs这个列表里面了,接下来我们就一个个的把他们取出来分别绘制

分别绘制需要知道每一行文字需要放置的坐标,有了整体菜单栏的坐标和每一行文字的高度,通过简单的计算就能得到每一行文字放置的坐标,我们用top表示文字需要放置的地方的y坐标。此外,我们不仅需要在商店显示商品的名称,还需要展示该商品在玩家背包里的库存,因此我们用同样的方法把玩家背包里的某样商品的库存储存到列表里面,并且用amount来表示该商品在玩家背包里的库存。

接着把text_surf(商品名称),amount(该商品在玩家背包里的库存),top(该行文字应该绘制在什么地方)当作参数传递进一个名为show_entry的函数里面,接下来,我们需要在该函数内实现每一行文字的绘制

show_entry函数如下图所示,都是些关于坐标的简单计算

位移用到的新的东西就是pygame.draw.rect,看名字大概就能看出来,就是绘制一个矩形

第一个参数为在哪个屏幕上绘制

第二个参数为颜色

第三个参数为绘制哪一个矩形

第四个参数可以缺省,为边框的宽度,这里我们不给白色的背景框设置变量,因此为0

第五个参数为是否对四个棱角做圆滑处理,默认为0,也就是方方正正的矩形,值越大圆的越明显

运行游戏,来到商人旁边按下回车,我们商店的大致雏形就绘制好了

接下来,我们还需要把玩家的金币绘制到窗口下边的正中间,以便玩家随时查看自己的余额

跟菜单里的其他东西一样,先绘制白色背景,再绘制文字即可

在update函数中调用这个函数

打开游戏商店,就能看到下方出现了余额

接下来,我们来给商店的某一行菜单增加选中的效果,如下图所示,我们选中了wood这一行,这一行就会增加一个黑色的边框来表示选中

首先,我们需要一个参数index来表示我们正在选中的商品的标号,从上到下分别是从0到5,我们初始化为0

接下来,我们需要在show_entry中增加一个参数selected,是一个布尔值,表示当前的商品是否是被选中的,如果被选中,就给他绘制一个黑色的边框

下面调用函数的时候,参数的传递为,如果self.index等于当前商品在列表中的下标text_index,那么selected就是true

接下来,要实现,按下键盘上下键,选中的商品也会随之改变,很明显要写到input函数里面

检测到键盘上键被按下,self.index减一,反之加一。可以预想到如果只是这么写,会由于pygame键盘检测速度过快的缘故,按一下会被检测很多下,所以我们采取写player中使用工具相同的策略,设置一个时钟,按下按键计时200毫秒,计时结束之前不会再次检测按键

首先要导入Timer类

接着创建计时器

接着就是对input函数的修改

值得注意的是,我们把之前退出商店的回车键改成了ESC键,因为进入商店和退出商店都是回车键,那么我们在退出商店的时候按下回车,就会退出商店,那么menu中的update函数就不再执行了,那么menu中的计时器也就不起作用了,此时由于按键检测速度过快,按键重复检测到enter键按下,就会执行player那边的toggle_shop函数,再次打开商店,所以,打开商店和关闭商店不能是同一个键。

当然,也可以在关闭商店的时候启动player中的计时器,不过这样太麻烦了,还得把player 导入到 menu中,不如直接设置他们俩按键不同

此时,一直按住上键或者下键,index会超出我们的范围,所以我们还需要在input函数中给它约束一下,这样,如果index超过5,就跳到0重新开始,反之就跳到5

接下来,我们要实现的效果是,选中某个商品后,会在入下图所示的位置展示他是出售(sell)的还是收购(buy)的

我们要在窗口上打印文字,首先要将他渲染出来,还是在setup函数中完成文字的渲染

接下来,就是在show_entry中把相应的文字在合适的位置绘制出来

接下来就是实现商店的最后一步,也是本系列的最后一步

对于选中的商品,按下空格就会出售或者销售。明显,这部分也是写在input函数里面的

首先就是根据下表获取当前选择的商品的名称,再根据下标判断当前是出售商品还是购买的商品,这里离用到了我们之前创建的一个变量sell_border,他是出售商品与收购商品的分界线,下标在它之前的商品都是出售的,在它之后的都是收购的。

接着如果是出售商品,先看看玩家背包有没有剩余,如果有,就背包库存减一,玩家金钱余额增加相应的钱数

其中SALE_PRICES是settings.py中设置的常量,代表各个产品的单价

如果是购买商品,先获取单价,其中PURCHASE_PRICES也是settings.py中设置的常量,也是本系列使用到的最后一个常量了。

判断玩家是否买得起,如果买得起,玩家背包里的相应物品数目加一,玩家扣除相应的金钱

最后,为了游戏体验,可以把苹果,树木,小麦和马铃薯的初始数量都设为20

三.完整代码

以下是本节发生过改动的文件的完整代码

menu.py

import pygame

from settings import *

from timer import Timer


class Menu:

    def __init__(self, player, toggle_menu):

        #设置player

        self.player = player

        #设置关闭菜单的函数,实际上传进来的是level.py里面的toggle_shop函数

        self.toggle_menu = toggle_menu

        #获取游戏窗口

        self.display_surface = pygame.display.get_surface()

        #设置字体

        self.font = pygame.font.Font('../font/LycheeSoda.ttf', 30)


        #菜单设置

        self.width = 400#菜单宽度

        self.space = 10#菜单的space

        self.padding = 8#菜单的padding


        #文字列表,就是商店都卖什么,把卖得东西的名称放到一个列表里

        self.options = list(self.player.item_inventory.keys()) + list(self.player.seed_inventory.keys())

        #这些东西前四个是出售的,后两个是收购的,所以sell_border是一个分界的下标,如果下标小于等于sell_border就是出售,反之是收购

        self.sell_border = len(self.player.item_inventory) - 1

        #执行setup函数,把文字都渲染出来,并且计算一下总共需要多高的菜单

        self.setup()


        #当前正在选择第几号商品

        self.index = 0

        #200ms计时器

        self.timer = Timer(200)



    #将文字渲染,计算整体菜单的位置

    def setup(self):


        #文字列表,options是存放str的列表,而text_surfs是让这些str经过Font的渲染之后,生成的可以显示在屏幕上的文字的列表

        self.text_surfs = []

        #菜单的高度

        self.total_height = 0


        for item in self.options:

            #render的参数分别为:需要渲染的文字,是否抗锯齿,文字的颜色

            text_surf = self.font.render(item, False, 'Black')

            #把渲染好的文字加到列表中

            self.text_surfs.append(text_surf)

            #增加文本框的padding,一个文本框在竖直方向上有上下两个padding,所以padding要乘二

            self.total_height += text_surf.get_height() + (self.padding * 2)


        #最后别忘了加上space,每两个文本框之间有一个space,所以space的数目是文本框数目减一

        self.total_height += (len(self.text_surfs) - 1) * self.space

        #设置菜单的放置位置

        self.menu_top = SCREEN_HEIGHT / 2 - self.total_height / 2

        #把菜单的放置位置封装成一个rect数据类型

        self.main_rect = pygame.Rect(SCREEN_WIDTH / 2 - self.width / 2, self.menu_top, self.width, self.total_height)


        #渲染"buy"

        self.buy_text = self.font.render('buy', False, 'Black')

        #渲染"sell"

        self.sell_text = self.font.render('sell', False, 'Black')


    #显示玩家的金币余额

    def display_money(self):

        text_surf = self.font.render(f'${self.player.money}', False, 'Black')

        text_rect = text_surf.get_rect(midbottom=(SCREEN_WIDTH / 2, SCREEN_HEIGHT - 20))

        #在文字的外面扩张10像素,来当背景,第四个参数为边框的宽度,这里我们不给背景设置边框

        #第五个参数为半径,如果为0,绘制出来的就是纯正方形,半径越大越圆

        pygame.draw.rect(self.display_surface, 'White', text_rect.inflate(10, 10), 0, 4)

        self.display_surface.blit(text_surf, text_rect)


    #接收键盘事件

    def input(self):

        keys = pygame.key.get_pressed()


        #因为input在update函数中被调用了,所以timer也可以在这里更新,当然也可以把下面这一行写到update函数中

        self.timer.update()

        #按下ESC,退出商店

        if keys[pygame.K_ESCAPE]:

            self.toggle_menu()

        if not self.timer.active:

            #按上选择上一个商品

            if keys[pygame.K_UP]:

                self.index -= 1

                self.timer.activate()

            #按下选择下一个商品

            if keys[pygame.K_DOWN]:

                self.index += 1

                self.timer.activate()


            #按下空格,出售或者购买

            if keys[pygame.K_SPACE]:

                self.timer.activate()

                #根据下标index获取当前选择的商品的名称

                current_item = self.options[self.index]

                #如果下标小于等于sell_border,就是出售商品

                if self.index <= self.sell_border:

                    #先看看背包里有没有剩余

                    if self.player.item_inventory[current_item] > 0:

                        #背包中相应的物品减一

                        self.player.item_inventory[current_item] -= 1

                        #钱数相应的增加,其中SALE_PRICES是settings.py里面的常量

                        self.player.money += SALE_PRICES[current_item]

                #否则就是购买

                else:

                    #获取单价

                    seed_price = PURCHASE_PRICES[current_item]

                    #先判断能否买得起

                    if self.player.money >= seed_price:

                        #玩家背包内相应物品数量加一

                        self.player.seed_inventory[current_item] += 1

                        #玩家扣钱

                        self.player.money -= seed_price




        #让index值保持在0到5之间

        if self.index < 0:

            self.index = len(self.options) - 1

        if self.index > len(self.options) - 1:

            self.index = 0


    def show_entry(self, text_surf, amount, top,selected):

        #参数:text_surf为文本,amount为玩家背包中的库存,top为单个的文本放置的位置,为顶端的坐标

        # 绘制白色的文本框背景

        bg_rect = pygame.Rect(self.main_rect.left, top, self.width, text_surf.get_height() + (self.padding * 2))

        pygame.draw.rect(self.display_surface, 'White', bg_rect, 0, 4)


        # 把文本绘制出来

        text_rect = text_surf.get_rect(midleft=(self.main_rect.left + 20, bg_rect.centery))

        self.display_surface.blit(text_surf, text_rect)


        # 把玩家背包中的库存绘制出来

        amount_surf = self.font.render(str(amount), False, 'Black')

        amount_rect = amount_surf.get_rect(midright=(self.main_rect.right - 20, bg_rect.centery))

        self.display_surface.blit(amount_surf, amount_rect)


        if selected:

            #如果是被选中的一行,就给他绘制黑色的边框表示选中

            pygame.draw.rect(self.display_surface,'black',bg_rect,4,4)

            #显示是收购还是出售

            if self.index <= self.sell_border:

                pos_rect = self.sell_text.get_rect(midleft=(self.main_rect.left + 150, bg_rect.centery))

                self.display_surface.blit(self.sell_text, pos_rect)

            else:

                pos_rect = self.buy_text.get_rect(midleft=(self.main_rect.left + 150, bg_rect.centery))

                self.display_surface.blit(self.buy_text, pos_rect)



    def update(self):

        self.input()

        self.display_money()

        for text_index, text_surf in enumerate(self.text_surfs):

            #分别计算需要放置的坐标

            top = self.main_rect.top + text_index * (text_surf.get_height() + (self.padding * 2) + self.space)

            #玩家背包中的库存

            amount_list = list(self.player.item_inventory.values()) + list(self.player.seed_inventory.values())

            amount = amount_list[text_index]

            #调用show_entry函数把它们都绘制出来

            self.show_entry(text_surf, amount, top,self.index == text_index)

 

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 *

from transition import Transition

from soil import SoilLayer

from sky import Rain,Sky

from menu import Menu


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()

        #特殊区域精灵组

        self.interaction_sprites = pygame.sprite.Group()


        #调用setup方法

        self.setup()

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

        self.overlay = Overlay(self.player)

        #创建transition对象

        self.transition = Transition(self.reset,self.player)


        #实例化Rain对象

        self.rain = Rain(self.all_sprites)

        #一个bool值,表示是否正在下雨,有1/10的概率为true

        self.raining = randint(0,10) > 8

        self.soil_layer.raining = self.raining

        #实例化天空对象

        self.sky = Sky()


        #交易

        self.shop_active = False

        self.menu = Menu(self.player,self.toggle_shop)


    def setup(self):


        #土地管理类

        self.soil_layer = SoilLayer(self.all_sprites,self.collision_sprites)


        #载入.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(

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

                surf=obj.image,

                groups=[self.all_sprites, self.collision_sprites, self.tree_sprites],

                name=obj.name,

                player_add=self.player_add)

        #野花

        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,

                    interaction = self.interaction_sprites,

                    soil_layer = self.soil_layer,

                    toggle_shop = self.toggle_shop)


            if obj.name == 'Bed':

                Interaction((obj.x, obj.y), (obj.width, obj.height), self.interaction_sprites, obj.name)


            if obj.name == 'Trader':

                Interaction((obj.x, obj.y), (obj.width, obj.height), self.interaction_sprites, obj.name)


        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)


        #如果正在打开商店,就执行相应的函数

        if self.shop_active:

            self.menu.update()

        else: #并且我们希望打开商店的时候,精灵组的其他精灵停止一切动作,也就是update

            #调用精灵组的update方法

            self.all_sprites.update(dt)

            #收获植物

            self.plant_collision()


        self.overlay.display()


        #如果在睡觉,执行相应的函数

        if self.player.sleep:

            self.transition.play()


        #如果在下雨,就生成雨滴,但是正在打开商店,就不要生成雨滴了,因为雨滴会覆盖到商店菜单上

        if self.raining and not self.shop_active:

            self.rain.update()


        #天色变暗

        self.sky.display(dt)


        #print(self.shop_active)



    def player_add(self, item):

        #item是一个str类型的数据,代表要对哪一种物品加一

        self.player.item_inventory[item] += 1


    def toggle_shop(self):


        self.shop_active = not self.shop_active


    def reset(self):

        #更新植物状态

        self.soil_layer.update_plants()


        # 清除农田里的水渍

        self.soil_layer.remove_water()


        #每天重新随机一下是否要下雨

        self.raining = randint(0, 10) > 8

        self.soil_layer.raining = self.raining

        #如果新的一天下雨了,就自动灌溉所有被开垦过的土地

        if self.raining:

            self.soil_layer.water_all()


        #苹果重新长在树上

        for tree in self.tree_sprites.sprites():

            for apple in tree.apple_sprites.sprites():

                apple.kill()

            tree.create_fruit()


        #天色重新变量

        self.sky.start_color = [255, 255, 255]


    def plant_collision(self):

        #如果plant_sprite这个精灵组不为空

        if self.soil_layer.plant_sprites:

            #遍历精灵组中的所有精灵

            for plant in self.soil_layer.plant_sprites.sprites():

                #如果作物成熟了并且与玩家发生了碰撞

                if plant.harvestable and plant.rect.colliderect(self.player.hitbox):

                    #背包增加相应的作物

                    self.player_add(plant.plant_type)

                    #消除植物

                    plant.kill()

                    self.soil_layer.grid[plant.rect.centery // TILE_SIZE][plant.rect.centerx // TILE_SIZE].remove('P')

                    #绘制粒子效果

                    Particle(plant.rect.topleft, plant.image, self.all_sprites, z=LAYERS['main'])

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)

 

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,interaction,soil_layer,toggle_shop):

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

        #也可以为空,这样需要在外面手动调用精灵组的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.item_inventory = {

            'wood': 20,

            'apple': 20,

            'corn': 20,

            'tomato': 20

        }

        #玩家的种子背包

        self.seed_inventory = {

            'corn': 5,

            'tomato': 5

        }

        # 玩家的金币

        self.money = 200

        #接收树木精灵组

        self.tree_sprites = tree_sprites

        #接收特殊区域精灵组

        self.interaction = interaction

        #是否在睡觉

        self.sleep = False


        #泥土管理

        self.soil_layer = soil_layer


        #打开商店

        self.toggle_shop = toggle_shop



    def use_tool(self):

        if self.selected_tool == 'hoe':

            self.soil_layer.get_hit(self.target_pos)


        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':

            self.soil_layer.water(self.target_pos)


    def get_target_pos(self):

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

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


    def use_seed(self):

        #如果背包里还有种子才能种

        if self.seed_inventory[self.selected_seed] > 0:

            self.soil_layer.plant_seed(self.target_pos, self.selected_seed)

            #种子数量减一

            self.seed_inventory[self.selected_seed] -= 1

    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 and not self.sleep:

            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


            #按下右边的shift键,切换种子

            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]


            #按下回车键,进行睡觉

            if keys[pygame.K_RETURN]:

                #返回值是与第一个参数的精灵发生碰撞的精灵

                collided_interaction_sprite = pygame.sprite.spritecollide(self, self.interaction, False)

                #参数False表示,如果发生了重叠,不会kill掉第二个参数的精灵,如果是True,就Kill掉第二个参数的精灵

                if collided_interaction_sprite:

                    #交易的部分以后再说

                    if collided_interaction_sprite[0].name == 'Trader':

                        self.toggle_shop()

                    else:

                        #为了视觉效果设置成面朝床边

                        self.status = 'left_idle'

                        self.sleep = True


    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)

 

由于本系列完结,接下来贴出所有文件的完整代码:

main.py:

import pygame, sys

from settings import *

from level import Level



class Game:

    def __init__(self):

        #初始化

        pygame.init()

        #设置屏幕

        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

        #设置标题

        pygame.display.set_caption('农场')

        #创建时钟,为以后得到dt使用

        self.clock = pygame.time.Clock()

        #创建关卡对象Level,该类在level.py文件中

        self.level = Level()


    def run(self):

        while True:

            #按键检测

            for event in pygame.event.get():

                """如果按下的是右上角的红叉,就退出程序"""

                if event.type == pygame.QUIT:

                    #退出pygame

                    pygame.quit()

                    #退出python程序,不捕获异常,不加上这一句也能退出,但是会在中断报错

                    sys.exit()


            dt = self.clock.tick() / 1000

            self.level.run(dt)

            #更新画面,一定要写在while循环的最后

            pygame.display.update()



if __name__ == '__main__':

    game = Game()

    game.run()

 

overlay.py:

import pygame

from settings import *


class Overlay:

    def __init__(self,player):


        #获得屏幕

        self.display_surface = pygame.display.get_surface()

        #由于需要知道玩家正在手持什么工具或者种子,所以需要调用player里面的变量,所以把player当成参数传进来

        self.player = player

        #用于存放所有的工具的图片的字典,其key值是工具名,value值是图片

        self.tools_surf = {}

        #用于存放所有种子的图片的字典,其key值是种子名,value值是图片

        self.seeds_surf = {}


        #存放种子和工具的文件夹路径

        overlay_path = '../graphics/overlay/'


        #利用for循环分别把每一个工具和种子,利用image.load函数导入进来,然后再加入到字典中

        for tool in player.tools:

            #把图片加载进来

            img = pygame.image.load(overlay_path+'/'+tool+'.png').convert_alpha()

            self.tools_surf[tool] = img


        for seed in player.seeds:

            #把图片加载进来

            img = pygame.image.load(overlay_path+'/'+seed+'.png').convert_alpha()

            self.seeds_surf[seed] = img


    def display(self):


        #获取玩家正在使用的工具的图像

        tool_surf = self.tools_surf[self.player.selected_tool]

        #设置图像要显示的位置,其中midbottom 是图片下边的中间,OVERLAY_POSITIONS[]是在settings.py内设置的常量

        tool_rect = tool_surf.get_rect(midbottom = OVERLAY_POSITIONS['tool'])

        #绘制工具图像

        #.blit(要绘制的图像,要绘制的坐标)

        self.display_surface.blit(tool_surf,tool_rect)


        seed_surf = self.seeds_surf[self.player.selected_seed]

        seed_rect = seed_surf.get_rect(midbottom=OVERLAY_POSITIONS['seed'])

        self.display_surface.blit(seed_surf, seed_rect)

 

settings.py:

from pygame.math import Vector2

# screen

SCREEN_WIDTH = 1280

SCREEN_HEIGHT = 720

TILE_SIZE = 64


# overlay positions

OVERLAY_POSITIONS = {

    'tool' : (40, SCREEN_HEIGHT - 15),

    'seed': (70, SCREEN_HEIGHT - 5)}


PLAYER_TOOL_OFFSET = {

    'left': Vector2(-50,40),

    'right': Vector2(50,40),

    'up': Vector2(0,-10),

    'down': Vector2(0,50)

}


LAYERS = {

    'water': 0,

    'ground': 1,

    'soil': 2,

    'soil water': 3,

    'rain floor': 4,

    'house bottom': 5,

    'ground plant': 6,

    'main': 7,

    'house top': 8,

    'fruit': 9,

    'rain drops': 10

}


APPLE_POS = {

    'Small': [(18,17), (30,37), (12,50), (30,45), (20,30), (30,10)],

    'Large': [(30,24), (60,65), (50,50), (16,40),(45,50), (42,70)]

}


GROW_SPEED = {

    'corn': 1,

    'tomato': 0.7

}


SALE_PRICES = {

    'wood': 4,

    'apple': 2,

    'corn': 10,

    'tomato': 20

}

PURCHASE_PRICES = {

    'corn': 4,

    'tomato': 5

}

 

sky.py:

import pygame

from settings import *

from sprites import Generic

from random import *

from support import import_folder



#天空类,管理天色的

class Sky:

    def __init__(self):

        self.display_surface = pygame.display.get_surface()

        #创建一个与游戏窗口相同大小的图片,来当作蒙版

        self.full_surf = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))

        #开始的颜色,也就是天亮的颜色

        self.start_color = [255, 255, 255]

        #最终的颜色,也即是深夜的颜色

        self.end_color = (38, 101, 189)


    def display(self, dt):

        for index, value in enumerate(self.end_color):

            if self.start_color[index] > value:

                #R,G,B值分别每秒 减小 2

                self.start_color[index] -= 2 * dt

        #给图片填充颜色

        self.full_surf.fill(self.start_color)

        #以蒙版的形式绘制到窗口上

        self.display_surface.blit(self.full_surf, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)

#雨滴精灵

class Drop(Generic):

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

        #参数中moving表示是否是移动的雨滴,雨滴分为两种,已经落到地上的我们就不让他移动,在空中的我们给他一个向着左下的速度

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

        #雨滴这个精灵的存活时间

        self.lifetime = randint(400, 500)

        #计时器的开始时间

        self.start_time = pygame.time.get_ticks()


        #如果moving为true,表示这个雨滴是需要移动的

        self.moving = moving

        #精灵移动所需要的三个参数

        if self.moving:

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

            self.direction = pygame.math.Vector2(-2, 4)

            self.speed = randint(200, 250)


    def update(self, dt):

        #让雨滴动起来

        if self.moving:

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

            self.rect.topleft = (round(self.pos.x), round(self.pos.y))

        #过了存活时间,把这个精灵kill掉

        if pygame.time.get_ticks() - self.start_time >= self.lifetime:

            self.kill()


#掌管下雨的类

class Rain:

    def __init__(self,all_sprites):

        self.all_sprites = all_sprites

        #导入雨滴正在落下的图片,一共三张,以列表的形式返回

        self.rain_drops = import_folder('../graphics/rain/drops/')

        # 导入雨滴落到地板的图片,一共三张,以列表的形式返回

        self.rain_floor = import_folder('../graphics/rain/floor/')

        #整张地图的宽度和高度,我们需要这两个数据取随机数,在随机的位置生成雨滴

        self.floor_w, self.floor_h = pygame.image.load('../graphics/world/ground.png').get_size()


    #生成落到地板上的雨滴

    def create_floor(self):

        Drop(

            surf=choice(self.rain_floor),

            pos=(randint(0, self.floor_w), randint(0, self.floor_h)),

            moving=False,

            groups=self.all_sprites,

            z=LAYERS['rain floor'])


    #生成在空中的雨滴

    def create_drops(self):

        Drop(

            surf=choice(self.rain_drops),

            pos=(randint(0, self.floor_w), randint(0, self.floor_h)),

            moving=True,

            groups=self.all_sprites,

            z=LAYERS['rain drops'])


    def update(self):

        self.create_floor()

        self.create_drops()

 

soil.py:

import pygame

from settings import *

from pytmx.util_pygame import load_pygame

from support import *

from random import choice


#开垦过的土地

class SoilTile(pygame.sprite.Sprite):

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

       super().__init__(groups)

       self.image = surf

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

       self.z = LAYERS['soil']


#浇过水的土地

class WaterTile(pygame.sprite.Sprite):

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

       super().__init__(groups)

       self.image = surf

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

       self.z = LAYERS['soil water']


#植物

class Plant(pygame.sprite.Sprite):

    def __init__(self, plant_type, groups, soil, check_watered):

        super().__init__(groups)


        #种子的类型

        self.plant_type = plant_type

        #载入植物图片,里面存放的是植物不同年龄下的图片

        self.frames = import_folder(f'../graphics/fruit/{plant_type}')

        #种植该植物的那块土壤

        self.soil = soil

        #检查该土地是否浇水的函数

        self.check_watered = check_watered


        #植物的年龄,一开始是0,表示种子状态

        self.age = 0

        #植物的最大年龄

        #植物的最大年龄取决于我们准备了多少张不同年龄植物的图片,由于我们的年龄是从0开始计数的,所以还得减一

        self.max_age = len(self.frames) - 1

        #植物的生长速度

        self.grow_speed = GROW_SPEED[plant_type]

        #植物是否可以被收获了

        self.harvestable = False


        #设置植物的图片

        self.image = self.frames[self.age]

        #植物的坐标相对于土地的偏移量

        self.y_offset = -16 if plant_type == 'corn' else -8

        #植物的坐标

        self.rect = self.image.get_rect(midbottom=soil.rect.midbottom + pygame.math.Vector2(0, self.y_offset))

        #植物的图层

        self.z = LAYERS['ground plant']


    def grow(self):

        if self.check_watered(self.rect.center):

            self.age += self.grow_speed

            if int(self.age) > 0:

                self.z = LAYERS['main']

                self.hitbox = self.rect.copy().inflate(-26, -self.rect.height * 0.4)

            if self.age >= self.max_age:

                self.age = self.max_age

                self.harvestable = True

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

            self.rect = self.image.get_rect(midbottom=self.soil.rect.midbottom + pygame.math.Vector2(0, self.y_offset))


#管理所有土地的类

class SoilLayer:

    def __init__(self,all_sprites,collision_sprites):

        #all_sprites精灵组

        self.all_sprites = all_sprites

        #碰撞精灵组

        self.collision_sprites = collision_sprites

        #创建开垦过的土地精灵组

        self.soil_sprites = pygame.sprite.Group()

        #浇过水的土地精灵组

        self.water_sprites = pygame.sprite.Group()

        #植物精灵组

        self.plant_sprites = pygame.sprite.Group()


        #导入开垦过的土地的图片

        self.soil_surf = pygame.image.load('../graphics/soil/o.png')

        #获取浇水图片,一共有三张

        self.water_surfs = import_folder('../graphics/soil_water')


        self.create_soil_grid()

        self.create_hit_rects()


    def create_soil_grid(self):

        #导入地图的图片

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

        #地图的宽度,高度除以64,就是一共有多少块(64*64)的土地

        h_tiles, v_tiles = ground.get_width() // TILE_SIZE, ground.get_height() // TILE_SIZE

        #创建一个三维列表,最一开始,所有土地的属性都是空的

        self.grid = [[[] for col in range(h_tiles)] for row in range(v_tiles)]

        #如果是可以被耕种的土地块,就给他的列表添加一个标记'F'

        for x, y, _ in load_pygame('../data/map.tmx').get_layer_by_name('Farmable').tiles():

            self.grid[y][x].append('F')


    def create_hit_rects(self):

        #列表里存放的是所有能被耕种的土地,数据类型是pygame里提供的Rect数据类型,包含了坐标和宽高

        self.hit_rects = []

        for index_row, row in enumerate(self.grid):

            for index_col, cell in enumerate(row):

                #如果是能被耕种的

                if 'F' in cell:

                    x = index_col * TILE_SIZE

                    y = index_row * TILE_SIZE

                    rect = pygame.Rect(x, y, TILE_SIZE, TILE_SIZE)

                    self.hit_rects.append(rect)


    def get_hit(self, point):

        #point是传入的参数,代表的是玩家挥舞的锄头的落点的坐标

        for rect in self.hit_rects:

            #如果该可耕种的土地块与玩家的锄头坐标发生了重叠,说明玩家锄的就是这块地,就给他的属性再加上一个标记'X'

            if rect.collidepoint(point):

                #相反,这里得把pygame的坐标系转换成tmx文件中的坐标系

                x = rect.x // TILE_SIZE

                y = rect.y // TILE_SIZE


                if 'F' in self.grid[y][x]:

                    self.grid[y][x].append('X')

                    SoilTile(

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

                        surf=self.soil_surf,

                        groups=[self.all_sprites, self.soil_sprites])

                    if self.raining:

                        self.water(point)

                    #self.create_soil_tiles()


    def water(self, target_pos):

        for soil_sprite in self.soil_sprites.sprites():

            if soil_sprite.rect.collidepoint(target_pos):

                x = soil_sprite.rect.x // TILE_SIZE

                y = soil_sprite.rect.y // TILE_SIZE

                #给grid添加”W“标记

                self.grid[y][x].append('W')


                pos = soil_sprite.rect.topleft

                #随机选择三张之中的一张图片,来作为这个精灵的image

                surf = choice(self.water_surfs)

                WaterTile(pos, surf, [self.all_sprites, self.water_sprites])


    def remove_water(self):

        #删除所有的精灵

        for sprite in self.water_sprites.sprites():

            sprite.kill()

        #同时别忘了清理grid中的标记

        for row in self.grid:

            for cell in row:

                if 'W' in cell:

                    cell.remove('W')


    #给所有被开垦过的农田浇水

    def water_all(self):

        for index_row, row in enumerate(self.grid):

            for index_col, cell in enumerate(row):

                if 'X' in cell and 'W' not in cell:

                    cell.append('W')

                    x = index_col * TILE_SIZE

                    y = index_row * TILE_SIZE

                    WaterTile((x, y), choice(self.water_surfs), [self.all_sprites, self.water_sprites])


    #检擦某块土地是否浇水

    def check_watered(self, pos):

        x = pos[0] // TILE_SIZE

        y = pos[1] // TILE_SIZE

        cell = self.grid[y][x]

        is_watered = 'W' in cell

        return is_watered


    #种植植物

    def plant_seed(self, target_pos, seed):

        for soil_sprite in self.soil_sprites.sprites():

            if soil_sprite.rect.collidepoint(target_pos):


                x = soil_sprite.rect.x // TILE_SIZE

                y = soil_sprite.rect.y // TILE_SIZE

                #该土地并没有被种植植物

                if 'P' not in self.grid[y][x]:

                    self.grid[y][x].append('P')

                    Plant(seed, [self.all_sprites, self.plant_sprites, self.collision_sprites], soil_sprite,

                          self.check_watered)


    #更新植物的状态

    def update_plants(self):

        for plant in self.plant_sprites.sprites():

            plant.grow()


    """

    def create_soil_tiles(self):

        self.soil_sprites.empty()

        for index_row, row in enumerate(self.grid):

            for index_col, cell in enumerate(row):

                if 'X' in cell:

                    SoilTile(

                        pos=(index_col * TILE_SIZE, index_row * TILE_SIZE),

                        surf=self.soil_surf,

                        groups=[self.all_sprites, self.soil_sprites])"""

 

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 Interaction(Generic):

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

       surf = pygame.Surface(size)

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

       self.name = name


#野花

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 Particle(Generic):

    def __init__(self, pos, surf, groups, z, duration = 200):

       #其中duration是持续时间,默认为200ms

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

       self.start_time = pygame.time.get_ticks()

       self.duration = duration


       #将图片透明的地方设为黑色(1),不透明的地方设为白色,来获取掩码

       mask_surf = pygame.mask.from_surface(self.image)

       #根据掩码生成图片,该图片只有黑白两种颜色

       new_surf = mask_surf.to_surface()

       #set_colorkey是在绘制图片的时候只绘制指定的颜色

       new_surf.set_colorkey((0,0,0))

       self.image = new_surf


    def update(self,dt):

       #经过一定的时间之后,这个精灵会被删除

       current_time = pygame.time.get_ticks()

       if current_time - self.start_time > self.duration:

          self.kill()



#树木

class Tree(Generic):

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

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


       self.player_add = player_add


       #创建苹果用到的变量

       #导入苹果的图像素材

       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:

          if self.alive:

             Particle(self.rect.topleft, self.image, self.groups[0], LAYERS['fruit'], 300)

          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.player_add('wood')

          self.alive = False


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

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

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

          Particle(

             pos=random_apple.rect.topleft,

             surf=random_apple.image,

             groups=self.groups[0],

             z=LAYERS['fruit'],

             )

          self.player_add('apple')

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

 

support.py:

from os import walk

import pygame


def import_folder(path):

    surface_list = []


    for _,__,img_files in walk(path):

        for image in img_files:

            full_path = path + '/' + image

            image_surf = pygame.image.load(full_path).convert_alpha()

            surface_list.append(image_surf)


    return surface_list

 

timer.py:

import pygame


class Timer:

    def __init__(self,duration,func = None):

        self.duration = duration#计时器的持续时间

        self.func = func#计时结束后要执行的函数

        self.start_time = 0#开始时间,初始化为0

        self.active = False#bool,表示是否正在计时


    def activate(self):

        self.active = True

        # pygame.time.get_ticks()与pygame.time.Clock().get_ticks()不同

        # 这里返回的是 从程序开始 到现在的时间,单位是ms

        self.start_time = pygame.time.get_ticks()


    def deactivate(self):

        self.active = False

        self.start_time = 0


    def update(self):

        current_time = pygame.time.get_ticks()

        if current_time - self.start_time >=self.duration:

            if self.func and self.start_time !=0:

                self.func()

            self.deactivate()

 

transition.py:

import pygame

from settings import *



class Transition:

    def __init__(self, reset, player):


        #获取屏幕

        self.display_surface = pygame.display.get_surface()

        #获取reset这个方法

        self.reset = reset

        #由于要对玩家的sleep变量做改动,所以把player当参数传递接受进来

        self.player = player


        #获取一个与窗口等宽等高的image

        self.image = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))

        #颜色的深度

        self.color = 255

        #颜色的变化速率

        self.speed = -2


    def play(self):

        self.color += self.speed

        #当窗口变成纯黑的时候,开始反过来让数字变大,并且调用reset函数

        if self.color <= 0:

            self.speed *= -1

            self.color = 0

            self.reset()

        #当color大于255的时候,说明已经变成纯白的了,此时睡觉结束了,把一些变量恢复成原来的大小即可

        if self.color > 255:

            self.color = 255

            self.player.sleep = False

            self.speed = -2


        self.image.fill((self.color, self.color, self.color))

        #special_flags=pygame.BLEND_RGBA_MULT是一种绘制方式

        #使用了这种绘制方式后,当image是纯白色的,反而会变得透明,而它变得越来越黑的时候,会逐渐变得不透明

        #这样,就不会出现刚开始color很大的时候屏幕变白的效果了

        self.display_surface.blit(self.image, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)

 

四.总结

至此,游戏的所有基本功能都已实现

玩家可以砍树,砍苹果卖钱。通过金钱购买种子来种地,通过浇水,等待四天后植物成熟,收获植物再卖钱等等

通过这个系列,基本上2D游戏的大部分通用的制作思路都能学到,再想拓展其他功能,或者自己创建一个属于自己的2D游戏,基本上就是简单的堆代码的工作了。

正如开头所说,通过本次系列,最重要的是学习游戏制作的思路,至于pygame的API之类的,不重要。真要制作游戏还是建议使用游戏引擎。有了这些思想,再去学习其他的游戏引擎开发,基本上也就是学习一下API就可以掌握了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

owooooow

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

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

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

打赏作者

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

抵扣说明:

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

余额充值