使用pygame制作一个种菜游戏

2022-12-06 14:41:08 浏览数 (1)

PYDEW VALLEY 简介

该教程使用pygame制作一个类似星露谷物语(Stardew Valley)的种菜游戏。

当然,星露谷物语作者用了超过5年的时间制作,内容非常丰富。而这个只是一个简单的demo,跟着教程大概要十几个小时就可以实现。 麻雀虽小,五脏俱全,通过这个教程还是可以学到很多东西的,Python的常用语法;Pygame的精灵类、输入处理、镜头控制等。完成了这个教程,也就基本掌握了Pygame。

B站视频(搬运): https://www.bilibili.com/video/BV1ia411d7yW?p=1

github(代码 素材) : https://github.com/clear-code-projects/PyDew-Valley

油管(原作者) https://www.youtube.com/watch?v=T4IX36sP_0c

有兴趣也可以看看星露谷物语是如何一个人制作出该游戏的:B站搜索BV1zZ4y1q7Lv。

阅读本文前,最好了解PyGame基本概念。如果还不熟悉PyGame,可以阅读之前的PyGame入门。

由于视频内容过多(接近7小时),无法一一记录。本文基本上只是一个大纲,记录一些重要的内容方便理解。建议观看视频,并对照着从github下载的代码学习。

s1-Setup

从github下载好源码,我们或得到一堆.rar压缩包,每个压缩包对应一节内容。我们解压s1-setup.rar开始第一步。解压后项目结构如下:

先看code文件夹。 code文件夹保存了项目的源码,这里有3个文件:level.py,main.py,settings.py。从名称来看,大概能知道main.py是程序入口,settings.py和游戏设置有关,而level.py是什么还不清楚。下面让我们分别看看这3个文件。

(其余部分是游戏资源文件,存放一些音乐、数据、字体、图片等内容,先不用管)

程序入口 main.py

代码语言:javascript复制
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('Sprout land')
        self.clock = pygame.time.Clock()
        self.level = Level()

    def run(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
  
            dt = self.clock.tick() / 1000
            self.level.run(dt)
            pygame.display.update()

if __name__ == '__main__':
    game = Game()
    game.run()

可以看到main.py定义了一个Game类,然后在main中实例化一个Game,并调用其run()方法。

Game类中定义了两个方法: __init__:初始化游戏,设置游戏屏幕大小、标题等。 run() :定义游戏的基本循环,包含退出事件检测和游戏更新。

注释:这里用到的deltatime,参考 https://www.youtube.com/watch?v=rWtfClpWSb8&t=1s

游戏设置 settings.py

游戏的一些设置,比如游戏的屏幕尺寸,标题大小...

代码语言:javascript复制
from pygame.math import Vector2
# screen
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
TILE_SIZE = 64
...

level.py

最后来到level.py。看这个名称很难知道它是干什么的,查看源码可以发现,它定义了一个Level类。Level类定义了一个初始化方法__init__获取显示表面和精灵组, run方法对精灵组进行了更新。

level.py的作用是把游戏元素的更新和显示从Game中抽离出来,让程序结构清晰。

代码语言:javascript复制
from settings import *

class Level:
    def __init__(self):

        # get the display surface
        self.display_surface = pygame.display.get_surface()

        # sprite groups
        self.all_sprites = pygame.sprite.Group()

    def run(self,dt):
        self.display_surface.fill('black')
        self.all_sprites.draw(self.display_surface)
        self.all_sprites.update()

s2-basic player

对应s2-basic player。创建一个简单的角色:

在上一节的基础上,我们创建一个角色。 首先,新建文件player.py 然后在文件中,导入相关的包:

代码语言:javascript复制
import pygame
from settings import *

创建Player类,继承精灵类pygame.sprite.Sprite

代码语言:javascript复制
class Player(pygame.sprite.Sprite):

初始化方法:调用父类的初始化方法。 super().__init__(group) # 传入group,让该精灵类成为group中的成员。并设置imagerect属性(设置精灵的图像和位置)。

后面的directionposspeed属性是为了方便我们控制角色。

代码语言:javascript复制
def __init__(self, pos, group):
    super().__init__(group)

    # general setup
    self.image = pygame.Surface((32,64))
    self.image.fill('green')
    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

input方法: input方法检测键盘输入,更改玩家移动方向。

代码语言:javascript复制
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

move方法: 应用方向,修改玩家位置。

代码语言:javascript复制
def move(self,dt):

    # normalizing a vector 
    if self.direction.magnitude() > 0:
        self.direction = self.direction.normalize()

    # horizontal movement
    self.pos.x  = self.direction.x * self.speed * dt
    self.rect.centerx = self.pos.x

    # vertical movement
    self.pos.y  = self.direction.y * self.speed * dt
    self.rect.centery = self.pos.y

update方法:update每次更新时会自动被调用(因为我们继承了精灵类)。在update里调用定义好的input和move方法,来接受输入,移动玩家。

代码语言:javascript复制
def update(self, dt):
    self.input()
    self.move(dt)

这样,玩家(Player)就定义好了。接下来只需要将玩家放到Level中。

为了让逻辑更清醒,在Level类中定义setup函数来设置这些元素。(目前只有一个玩家)

代码语言:javascript复制
def setup(self):
    self.player = Player((640,360), self.all_sprites)

并在Level的初始化方法中调用setup

这样就完成了,运行main.py就能看到一个绿色方块,并且可以用上下左右键移动。

写到这里就感受到这种文件结构的好处:如果我们想添加一个东西,只需要新建一个类,并且在Level里添加一下就好了。其它部分不需要改动。

s3-Importing the player graphics

对应s3-import 。 项目里有一个graphics文件夹,打开graphics会看到里面是很多角色的贴图。这就是这节要做的事情--导入角色的图片。为了方便获得图片路径,创建support.py文件,在里面写读取图片路径的方法:

代码语言:javascript复制
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

这里用到了os模块的walk方法,这是一个目录遍历的方法,返回的是一个三元组(dirpath, dirnames, filenames) 获得路径后用image_surf = pygame.image.load(full_path).convert_alpha()获得图片对应的surf列表。

在Player类中,我们通过一个字典,保存角色的不同动作对应的surf:

代码语言:javascript复制
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)

然后在Player的初始化方法中设置:

代码语言:javascript复制
self.import_assets()
self.status = 'left_water'
self.frame_index = 0

# general setup
self.image = self.animations[self.status][self.frame_index]

s4-玩家动画

从图片到动画实际上很简单,实际上你只需要切换图片。定义animate,切换图片。并在update中调用。

代码语言:javascript复制
def animate(self,dt):
    self.frame_index  = 4 * dt
    if self.frame_index >= len(self.animations[self.status]):
        self.frame_index = 0

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

这样就有了动画,但是目前只有一种状态。所以我们要在input函数中根据不同的输入修改状态:

代码语言:javascript复制
if keys[pygame.K_UP]:
    self.direction.y = -1
    self.status = 'up'
elif keys[pygame.K_DOWN]:
    self.direction.y = 1
    self.status = 'down'
    ...

这样做又会带来一个问题:我们向上移动后,状态会一直保持up,相应地一直播放up动画(向上移动)。 所以我们增加一个get_status方法:如果玩家不再移动,就修改为_idle(空闲)状态

代码语言:javascript复制
def get_status(self):  
    # idle
    if self.direction.magnitude() == 0:
        self.status = self.status.split('_')[0]   '_idle'

你可能对self.status.split('_')[0] '_idle'感到奇怪。试想一下,如果我们已经是_idle状态,直接在后面 _idle就会变成up_idle_idle(一个不存在的状态)。所以我们用split()方法分割字符串,然后用[0]获取_最前面的单词。

s5-使用工具

现在我们想实现: 玩家按下空格后,使用工具。并且,玩家使用工具应该花费一些时间,这个期间内不能移动。 为此定义了一个Timer类,作为计时器。在玩家按下空格后,Timer激活(.activate()),玩家使用工具并且无法执行其它操作。

Timer类

代码语言:javascript复制
import pygame 

class Timer:
    def __init__(self,duration,func = None):
        self.duration = duration
        self.func = func
        self.start_time = 0
        self.active = False

    def activate(self):
        self.active = True
        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:
            self.deactivate()
            if self.func:
                self.func()

然后在Player中,添加tool_use动作。在_init__中,添加计时器和选择的工具。计时器和use_tool动作绑定。

代码语言:javascript复制
# timers 
self.timers = {
    'tool use': Timer(2000,self.use_tool)
}

# tools 
self.selected_tool = 'water'

并且定义使用工具的方法use_tool,目前还没实现,先用print代替。

代码语言:javascript复制
def use_tool(self):
    print(self.selected_tool)

input中,处理空格命令:

代码语言:javascript复制
# tool use
if not self.timers['tool use'].active:
    ...
    if keys[pygame.K_SPACE]:
        self.timers['tool use'].activate()
        self.direction = pygame.math.Vector2()
        self.frame_index = 0

修改玩家状态:

代码语言:javascript复制
def get_status(self): 
    # idle
    ...
    # tool use
    if self.timers['tool use'].active:
        self.status = self.status.split('_')[0]   '_'   self.selected_tool

创建更新所有计时器的方法update_timers,并在update中调用:

代码语言:javascript复制
def update_timers(self):
    for timer in self.timers.values():
        timer.update()
代码语言:javascript复制
    def update(self, dt):
        self.input()
        self.get_status()
        self.update_timers()
        ...

s6-切换工具

实现按下q切换工具。

用列表tools保存工具,用索引tool_index来指示现在使用的工具。

按下q切换tool_index。如果直接这样做,会发现按下q后一直切换,所以我们需要做一个时间限制。比如说200毫秒内只能切换一次。

为此,我们添加一个计时器:

代码语言:javascript复制
# timers 
self.timers = {
    'tool use': Timer(350,self.use_tool),
    'tool switch': Timer(200),
    ...
}

并在input中限制,只有该计时器持续时间(200 ms)结束后才能进行下一次切换工具:

代码语言:javascript复制
# change tool
if keys[pygame.K_q] 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]

和使用工具类似,添加使用种子。添加计算器、状态列表:

代码语言:javascript复制
# timers 
self.timers = {
    'tool use': Timer(350,self.use_tool),
    'tool switch': Timer(200),
    'seed use': Timer(350,self.use_seed),
    'seed switch': Timer(200),
}
# seeds 
self.seeds = ['corn', 'tomato']
self.seed_index = 0
self.selected_seed = self.seeds[self.seed_index]

在input()添加按键处理:

代码语言:javascript复制

# tool use
if keys[pygame.K_SPACE]:
    self.timers['tool use'].activate()
    self.direction = pygame.math.Vector2()
    self.frame_index = 0

# change tool
if keys[pygame.K_q] 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]

# seed use
if keys[pygame.K_LCTRL]:
    self.timers['seed use'].activate()
    self.direction = pygame.math.Vector2()
    self.frame_index = 0

# change seed 
if keys[pygame.K_e] 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]

0 人点赞