前言:
下面是对本系列的一些说明:
首先游戏是一个外国人写的,原视频链接为https://www.youtube.com/watch?v=R9apl6B_ZgI
bilibili搬运视频的链接为:简介_哔哩哔哩_bilibili
至于游戏是不是星露谷物语风格,我也不知道,因为我也没玩过,只不过bilibili的视频标题就是这样的。
游戏的最终效果,可以点进B站的链接去看视频的P1简介部分
原视频是英文,机器翻译,可能会对一些人的学习造成难度
我跟着原视频写了一遍,在代码中添加了中文注释,对原版的代码出现bug的地方做了修改,并且根据自己的理解写了这篇中文版教程。
本系列,python自带的东西一概不会多说,也不会系统的将pygame里的所有API是如何使用的。但是只要是pygame里面的东西,第一次出现的时候都会详细说明是怎么使用的,比如这个函数的参数代表什么,返回值是什么。只要讲过一次,以后再出现就不会细讲了。本游戏是比较典型的2D游戏,所以一套下来pygame中常用到的东西基本都会讲到。
相比于pygame中的API,这次实战更重要的是能够获得2D游戏创作的一些套路,很多东西都是通用的,比如主角的移动,摄像机的设置,伪3D效果,碰撞检测。这些东西无论是在python中写,还是在其他的游戏开发引擎中写,大体思想都是相同的。学会了了这些思想,想要开发新的2D游戏,基本就不会遇到任何困难了。
完成这次实战,更加能够锻炼面向对象编程的能力,因为所有的代码基本都封装到类里面了。
本系列基本上是对代码逐行解读,并且会在每一节的最后,贴出本节修改过的文件里面的全部代码。
接下来是原作者给出的项目文件的链接,我们需要里面的图片素材,音频素材等等:
https://github.com/clear-code-projects/PyDew-Valley
下载完成后文件格式如下图所示
原作者在完成一部分功能后,会把截止到目前的所有代码保存到一个文件夹下,总共有23个文件夹,每个文件夹里面都有我们需要的完整素材,我们可以随便打开一个文件夹,里面名为code的文件夹是存放代码的地方,其他的文件夹都是我们用到的音频或者图片的素材,我们可以把除了code的文件夹都复制到我们的项目下,并且在我们的项目下新建一个名为code的文件夹,在这个文件夹里面写代码
接下来就是正文部分了,本节将讲述第一部分,窗口的创建与展示:
一.项目的架构:
项目的架构如下图所示,在主文件夹下存放了图中的5个文件夹,其存放的内容分别如箭头标注所示,其中code存放代码,我们每次创建.py文件都在该文件夹下创建。本次所需要编写的是main.py,settings.py和level.py三个文件,后续也会根据功能的增加不断添加新的代码,或者添加新的.py文件。
二.settings.py
再编写一个项目时通常会创建很多常量,我们希望把所有用到的常量都写在一个python文件中,如果要修改一些常量,直接到这个文件中去修改即可。
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
}
他们分别代表了窗口的高度和宽度,其值可以根据各自的情况更改。
至于其他的值代表的含义,以后用到的时候会说,这里可以先把所有的内容都先复制到自己的setting.py中
三.main.py
main.py的作用是给用户提供一个运行程序的接口,在运行main.py后,他会创建一个窗口并将它显示出来。
通常一个项目我们不希望它的main.py文件很长,因此我们并不在main.py中实现游戏的内容,而是希望再level.py中实现游戏的内容,因此在main.py文件中会调用level.py提供的接口,而后续的其他游戏功能的实现,都会写在level.py文件中,或者写在其他的.py文件中再由level.py去调用,总之,这次写完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()
1.导包部分
import pygame, sys
from settings import *
from level import Level
然后就是从settings(我们自己的settings.py)中导入所有的东西,这样我们就可以直接使用其中的常量了
然后就是从level.py中导入Level类,因为level.py还没写,所以先跳过
2.编写Game类
其中包含了两个函数,__init__函数是初始化的,每次创建一个game对象就会自动调用__init__函数
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()
pygame.init()是初始化pygame,这是pygame作者的硬性要求,在使用pygame之前写上这么一句就行了。只要在这里初始化过一次之后就不用再写了
pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))是创建一个窗口,其中的(SCREEN_WIDTH, SCREEN_HEIGHT)分别是窗口的宽度和高度,这窗口作为返回值被self.screen接收,以后我们要对窗口进行操作,对self.screen进行操作就行了
pygame.display.set_caption('农场')是设置标题,文字可以根据个人喜好随便写,下图箭头所指的位置就是一个窗口的标题
self.clock = pygame.time.Clock()是创建一个Clock对象,翻译过来可能称作时钟更为贴切,作者再time.py这个模块中写了很多关于时间的类,Clock这个类中包含了一些关于游戏时钟的方法,我们在下面会用到其中的一些方法,等用到的时候会再说。
self.level = Level()这个就是创建一个Level对象,但是level.py还没写,所以可以先跳过
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()
我们不希望游戏是窗口闪过一下就退出的,因此我们需要让这个程序一直保持运行,因此我们写了一个死循环While True:,这样我们就能一直运行这个程序除非我们主动关闭它。
这个循环被称为游戏的主循环,和pygame.init()必须写在开头类似,有一个语句是一定要写在主循环的结尾处的,就是pygame.display.update(),它的作用是更新窗口中的画面,就比如一开始主角的位置在(0,0)处,主角向右移动了1个像素,他的坐标变成了(1,0),但是此时游戏的画面中主角还是在原位置,只有执行了pygame.display.update()之后,游戏画面才会被更新,主角才会到他相应的位置。
由于一次循环调用一次pygame.display.update(),一次循环更新一次画面,因此一次循环我们称为一帧。
pygame.event.get()是一个检测运行中出现的事件的方法,它会将这一次循环(这一帧)中所有出现的事件都检测出来,并且以一个列表的形式返回。
而pygame中的事件大概有退出(pygame.QUIT),键盘按下,键盘松开等等,这里我们只用到退出事件,退出事件指的是用户点击窗口右上角的X。
pygame.event.get()返回这一帧发生的所有时间的列表,我们用for循环来遍历这个列表,如果发现列表中有一个事件是退出(if event.type == pygame.QUIT:),那我们就退出pygame(pygame.quit()),然后再推出这个python程序(sys.exit())
这里用到了刚刚导包的时候导入的sys,sys.exit()的作用是退出这个python程序,如果我们不写这句话,也可以退出,只不过是以程序错误的形式被迫退出的,如下图所示,如果不写sys.exit(),点击窗口右上角的X退出,下面的窗口会提示报错信息,进而中断整个程序
dt = self.clock.tick() / 1000,其中dt这个变量代表英文完整版翻译过来应该是变化的事件,它代表的是一帧(一次循环)所花费的事件,单位为秒
self.clock.tick()就是我们上面说的Clock类中的一个方法,它的返回值是这一帧距离上一帧经过了多少毫秒。再除以1000,得到的就是这一帧距离上一帧过去了多少秒了。
tick()这个方法还有一个作用就是控制游戏的帧率,如果括号内传入参数,这个方法就会控制游戏的帧率不超过这个参数的值,比如在这里写self.clock.tick(60),那么就会控制游戏的帧率不超过60,主要原理就是通过暂停来让游戏主循环进行的不那么快,一秒最多只能循环60次。
dt这个变量再后续会又用,等用到了再说,先知道他是代表一帧所花的时间就行
self.level.run(dt) 这个语句是调用上面创建的Level类的run函数,因为还没写level.py,先跳过。
3.主函数
上面编写的Game类是不会自己运行的,需要你在主函数中创建一个game对象,此时会自动调用__init__函数,然后再在主函数中调用game的run函数即可。
if __name__ == '__main__':
game = Game()
game.run()
四.level.py
我们在main.py创建了一个Level类,并且调用了他的run函数,因此我们要在level.py中写一个Level类,并实现他的run函数。相似的,Level类中的__init__函数是用来初始化的,run函数是用来运行游戏的。
在我们启动的main.py后,首先创建一个Game类的对象game,自动调用game的__init__函数,在这个__init__函数中,我们创建了一个Level类的对象level,自动调用了level的__init__函数,至此,游戏的初始化完成。我们调用game的run()函数,在game的run()函数中,我们写了一个游戏的主循环,每一次循环我们都调用了level的run函数。因此,我们也可以猜出来,Level类的run()函数,才是实现游戏的主要部分。
import pygame
from settings import *
class Level():
def __init__(self):
#得到屏幕的画面,得到的这个画面与main.py中的screen相同
self.display_surface = pygame.display.get_surface()
def run(self,dt):
#窗口的背景设为黑色
self.display_surface.fill('black')
本次的level部分实际上并没有实现什么东西,只不过写了一个大概的框架,留着以后填充。
1.导包部分
import pygame
from settings import *
settings作为记录本项目所有常量的文件,也是一定要导的
2.Level类
class Level():
def __init__(self):
#得到屏幕的画面,得到的这个画面与main.py中的screen相同
self.display_surface = pygame.display.get_surface()
def run(self,dt):
#窗口的背景设为黑色
self.display_surface.fill('black')
其中pygame.display.get_surface()是得到窗口,由于我们的窗口是在main.py中创建的,并且用了Game类中的self.screen来接受它,这就导致了如果我们想在Level类中对窗口进行操作,还得去调用main.py中的Game类中的screen对象,这需要我们在开头import main ,但是在main.py中我们import了level,又在level.py中import了mian,这在逻辑上是十分混乱的,我们还不如直接写到同一个文件中去。好在pygame提供了这样的一个方法,让我们能获取窗口,这里的display_surface与main.py中的screen指的是同一块窗口。
self.display_surface.fill('black')是将窗口填充为黑色,参数可以用字符串的形式来表示颜色,也可以用一个列表的形式,利用RGB来表示颜色,比如(0,0,0)代表黑色,(255,0,0)代表红色等等。
五.总结
本次,我们在settings.py中创建了本项目所需要的所有常量,并且在main.py中完成了游戏窗口的创建与显示,并且创建了Level对象并在游戏主循环中调用其run()函数。在level.py中,我们编写了Level类的框架,包括一个初始化__init__函数和一个run()函数,这两个函数等待我们的后续完善。