前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用pygame制作一个种菜游戏

使用pygame制作一个种菜游戏

作者头像
一只大鸽子
发布2022-12-06 14:41:08
9910
发布2022-12-06 14:41:08
举报
文章被收录于专栏:Python基础、进阶与实战

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]
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一只大鸽子 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PYDEW VALLEY 简介
  • s1-Setup
    • 程序入口 main.py
      • 游戏设置 settings.py
        • level.py
        • s2-basic player
        • s3-Importing the player graphics
        • s4-玩家动画
          • s5-使用工具
            • Timer类
          • s6-切换工具
          相关产品与服务
          云直播
          云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档