0%

简介

面试Python web相关岗位时,最容易被问到的问题,WSGI协议是什么?

这篇文章就简单解释一下WSGI并实现一个满足WSGI协议的服务。

阅读全文 »

时常在朋友圈看见好友们为人转发「水滴筹」,某某重病,很需要钱,我路过看见,是认识的人,都会小捐10~20块。

对重病来说,我这点钱形式意义大于实际意义,杯水车薪。

有时看见「水滴筹」会有点恍惚,自己父母也到了容易生各种病的年纪了,我是不是马上也要面临这样的问题了?

我是个爱面子的人,不到很困难的时候,是不会弄「水滴筹」求朋友转发集钱治病的,但谁不爱面子?现实就是,一场重病掏空普通中产家庭,这可能就是富人与中产之间最明显的差别,此时钱就是命。

实话说,我家没什么钱,我想象着自己父母重病,我能做什么?

那时我自己可能也成家了,父母重病,作为子女,我得拿出所有的钱给他们治病?那后面的房贷、小孩开销呢?而且钱也不都是我挣的,跟另一半该怎么说?

没有钱,自己的小家难以维持,不给钱治父母的病又不现实。

为了减少这种两难情况落到我头上的概率,我选择去了解商业保险。

注意,我不会给你推荐任何保险,只会说出我个人的看法感觉以及我与父母在保险这事上的博弈。

商业保险,给人的第一印象就是骗钱的。

无论跟父母说还是跟几个亲近的朋友讲,他们都会有这样的感觉,商业保险会给你设置很多细节,到你真的得病了,肯定会说你不满足这个条件不满足那个条件,最终不会给你赔钱。这玩意就是骗钱的。

面对这样的问题,我会简单的解释,商业保险是为了挣钱,我们买,只是买个不一定用的上的保障而已,主要的目的是减低自己未来生活所面对的风险。

这话说的很像一个保险推销人员。

对于朋友,我简单说一下,他们不听就罢了,但对于父母,他们就必须理解。

没错,我就TM想给保险公司赚这个钱,我不想以后你们得个重病,我挣扎都无法挣扎,所以我花钱,跟保险公司做对赌。

要说服对商业保险已有比较严重偏见的父母是很有难度的事情,母亲常跟我讲,我们已经买了200多的社保了,你给我和老爸买的保险就没必要了,那么贵。

此前,母亲在我面前说这话的频率并不高,直到她腰部出了问题,去住院后,想起我有这个保险,寻思着可以弄一下,省一笔钱,可结果是被拒保了。

原因其实也简单,因为这个病在投保前就存在了,所以你买的保险无法生效了。

但其实我在投保前,我母亲身体是很健康的,至少在电子病历上,问题出在治病时,母亲想要医生意识到自己的问题挺严重的,希望他认真的治一下,就特意说,腰痛了很久了,医生这样一记,电子病历就显示你这个病有很长时间了,而商业保险不会理会你主观倾诉,只看数据这些客观条件,它此时就会判断你在购买它之前已经患有这个病,是不能被报销的。

这事之后,父母愈发觉得这玩意骗钱,让我费了点脑子。最后使出大绝招,我的钱怎么花,花谁身上是我个人的事情,你们要做的就是提供个人信息配合一下我花钱,而且这个钱本质上来说不是为你们花的,是为我自己花的,避免你们生病了,我产生严重的财务危机。

此外,跟父母交流的一个技巧就是用他们熟悉的东西来举例子,比如我跟父亲说,你买那些乱七八糟的股票都知道要弄个对冲,买个车子,都要上这个保险那个保险,到最重要的身体上,你就觉得这个不行那个骗人,你觉得身体和股票车子这些哪个重要?

有这样的情况并不奇怪,我个人就是从怀疑商业保险被「洗脑」成相信商业保险的,想被「洗脑」?去看本专门讲保险的书就好了,了解其盈利原理,了解其大致种类,了解保险经纪人这个物种等等,大致的了解后,对商业保险妖魔化的感觉就会清淡很多。

但就算看完一些相关的书籍,我也不建议你自己去选保险,选择保险还是需要专业人士的,毕竟品种那么多,差异那么细。

另外一个误解就是觉得,商业保险我想买就能买。实话说,我想给我父亲上重疾已经没有较高性价比的产品给我选择了,最终只给他老人家上了百万医疗险,而我母亲年轻一些,可以选的产品比较多,我就将重疾和医疗险都上了。

总结而言,我买了一些保险,要连续交个10年左右,花个十几万,在这段时间,父母不幸患了病,如果满足保险要求,就有一定的缓冲。

你可能会问,那不满足呢?又花了保险钱,到头来还是要花那多钱治病?

如果这种最惨情况发生了,只能认。我对保险做的所有事情都是降低出现重大风险的概率,而无法杜绝它,我一直都明白,成年人的世界难有十全十美的方法,选个「较好的」就行了。

希望这篇文章可以让你思考一下保险,思考一下风险控制,我们现在也老大不小了,除了游戏外,是得掌握点其他东西了。

前言

通过前面九篇文章的编写,「跳跳兔」游戏基本已经被编写出来了,本节在此基础上进一步完善它,并添加上云彩背景。

添加云彩背景

添加云彩背景的大致步骤如下。

  • 1.编写云彩类
  • 2.载入云彩图片
  • 3.随机生成云彩
  • 4.云彩同步移动

一步步来编写,首先是创建云彩类,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# sprites.py

class Cloud(pg.sprite.Sprite):
def __init__(self, game):
self._layer = CLOUD_LAYER
self.groups = game.all_sprites, game.clouds
pg.sprite.Sprite.__init__(self, self.groups)
self.game = game
self.image = random.choice(self.game.cloud_images)
self.image.set_colorkey(BLACK)
self.rect = self.image.get_rect()
# 随机出现位置
scale = random.randrange(50, 101) / 100
self.image = pg.transform.scale(self.image, (int(self.rect.width * scale),
int(self.rect.height * scale)))
self.rect.x = random.randrange(WIDTH - self.rect.width)
self.rect.y = random.randrange(-500, -50)

def update(self):
# 云朵大于2倍高度,就被消除
if self.rect.top > HEIGHT * 2:
self.kill()

代码内容与此前内容非常类似,不再详细分析。

但你仔细观察,你会发现,Cloud类的__init__()方法中创建了 self._layer,并通过如下形式将其加入到相应的groups中。

1
2
self.groups = game.all_sprites, game.clouds
pg.sprite.Sprite.__init__(self, self.groups)

这是一个优化点,后文再讨论。

构建了Cloud类后,接着要做的就是载入图片、随机生成以及同步移动了,轻车熟路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# main.py/Game

def load_data(self): # 加载数据
# ... 省略无关代码
# 加载云彩图片
self.cloud_images = []
for i in range(1, 4):
self.cloud_images.append(pg.image.load(os.path.join(img_dir, 'cloud{}.png'.format(i))).convert())

def new(self):
self.score = 0
# ... 省略无关代码
# 创建云彩
for i in range(8):
c = Cloud(self)
c.rect.y += 500
self.run()

def update(self):
# 玩家到达游戏框 1/4 处时(注意,游戏框,头部为0,底部为游戏框长度,到到游戏框的1/4处,表示已经到达了顶部一部分了)
if self.player.rect.top <= HEIGHT / 4:
# 玩家位置移动(往下移动)
self.player.pos.y += max(abs(self.player.vel.y), 2)
# 随机生成新云朵
if random.randrange(100) < 10:
Cloud(self)
# 云彩同步移动
for cloud in self.clouds:
cloud.rect.y += max(abs(self.player.vel.y / 2), 2)
# 敌人位置同步往下移动
for mob in self.mobs:
mob.rect.y += max(abs(self.player.vel.y), 2)
# 平台在游戏框外时,将其注销,避免资源浪费
for plat in self.platforms:
# 平台移动位置(往下移动,移动的距离与玩家相同,这样玩家才能依旧站立在原本的平台上)
plat.rect.y += max(abs(self.player.vel.y), 2)
if plat.rect.top >= HEIGHT:
plat.kill()
# 分数增加 - 平台销毁,分数相加
self.score += 10

woo~,搞定,如果有疑惑,可以拉下github代码对照着看。

优化

云彩类添加完了,接着来进行一些优化。

首先,对碰撞检测的优化,如果你仔细观察,你会发现,玩家对象与敌人对象的本体还没有接触到,就触发了碰撞检测,游戏就结束了,造成这种现象的原因是,玩家也好、敌人也好,游戏中的任何元素都是一个对应大小的长方形,碰撞检测默认形式就是对这两个方块进行检测,此时两个元素本身可能没有接触,但对应的方块接触到了,所以触发了碰撞检测。

为了避免这种情况,可以利用pygame的蒙版机制mask,为Player、Mob都创建相应的蒙版,具体做法如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# sprites.py

class Player(pg.sprite.Sprite):
def animate(self):
# ... 省略无关代码
self.mask = pg.mask.from_surface(self.image) # 创建蒙版

class Mob(pg.sprite.Sprite):

def update(self):
# ... 省略无关代码
self.rect = self.image.get_rect()
self.mask = pg.mask.from_surface(self.image) # 创建蒙版
self.rect.center = center
# ... 省略无关代码

检测时,加上pygame.sprite.collide_mask回调则可

1
2
3
4
5
6
def update(self):
# ... 省略无关代码
# 碰撞检测 - 如果碰撞到了敌人,游戏结束
mob_hits = pg.sprite.spritecollide(self.player, self.mobs, False, pg.sprite.collide_mask)
if mob_hits:
self.playing = False

此外还有个需要优化的问题,就是元素图层关系,增加云彩对象后,图层关系的问题显得明显,云彩作为背景却会遮挡玩家对象、敌人对象、平台对象等,这是不合理的,不同元素应该在不同图层,从而合理的显示出来。

首先定义好不同元素要出现的图层。

1
2
3
4
5
6
7
8
# settings.py

# 不同元素在不同层
PLAYER_LAYER = 2 # 玩家
PLATFORM_LAYER = 1 # 平台
POW_LAYER = 1 # 道具
MOB_LAYER = 2 # 敌人
CLOUD_LAYER = 0 # 云

然后为不同的元素对象都添加上设置图层的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#Sprite.py

class Player(pg.sprite.Sprite):
def __init__(self, game):
self._layer = PLAYER_LAYER # 对应的图层
self.groups = game.all_sprites # 所在的组
pg.sprite.Sprite.__init__(self, self.groups) # 添加、实例化
# ... 省略无关代码

class Platform(pg.sprite.Sprite):
def __init__(self, game, x, y):
self._layer = PLATFORM_LAYER # 对应的图层
self.groups = game.all_sprites, game.platforms # 所在的组
pg.sprite.Sprite.__init__(self, self.groups) # 添加、实例化
# ... 省略无关代码

class Pow(pg.sprite.Sprite):
def __init__(self, game, plat):
self._layer = POW_LAYER
self.groups = game.all_sprites, game.powerups
pg.sprite.Sprite.__init__(self, self.groups)
# ... 省略无关代码

class Mob(pg.sprite.Sprite):
def __init__(self, game):
self._layer = MOB_LAYER
self.groups = game.all_sprites, game.mobs
pg.sprite.Sprite.__init__(self, self.groups)
# ... 省略无关代码

class Cloud(pg.sprite.Sprite):
def __init__(self, game):
self._layer = CLOUD_LAYER
self.groups = game.all_sprites, game.clouds
pg.sprite.Sprite.__init__(self, self.groups)
# ... 省略无关代码

优化后,再修改一个Game类的new()方法,使用pygame.sprite.LayeredUpdates()来实例化all_sprites对象。

LayeredUpdates(分层更新)是一个精灵组,它可以处理图层并顺序绘制元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# main.py

class Game:
def new(self):
self.score = 0
# self.all_sprites = pg.sprite.Group()
# 层次添加,避免元素重叠显示(如背景云遮挡住平台与玩家)
self.all_sprites = pg.sprite.LayeredUpdates()
self.platforms = pg.sprite.Group()
self.powerups = pg.sprite.Group() # 急速飞跃道具
self.mobs = pg.sprite.Group() # 敌人对象
self.clouds = pg.sprite.Group() # 云彩对象
self.player = Player(self)
self.all_sprites.add(self.player)
for plat in PLATFORM_LIST:
p = Platform(self, *plat)
# self.all_sprites.add(p)
# self.platforms.add(p)
self.mob_timer = 0
# 游戏的背景音乐
pg.mixer.music.load(os.path.join(self.snd_dir, 'Happy Tune.ogg'))
# 创建云彩
for i in range(8):
c = Cloud(self)
c.rect.y += 500
self.run()

最终效果如下。

「跳跳兔」至此开发完啦,Pygame系列的文章也暂时告一段落啦。

「跳跳兔」代码github:https://github.com/ayuLiao/jumprabbit

Pygame还有很多有趣的功能在「跳跳兔」游戏中并没有体现出来。

正如本系列第一篇文章所说,这些文章只是学习笔记,此外还有下面两个游戏的学习笔记,一个是打飞机、一个是RPG打僵尸游戏。

如果大家感兴趣,记得告诉我,我才有动力继续分享,后面按个人计划应该会以漫画形式分享算法、计算机基础方面的东西,希望喜欢。

最后,再次声明一下,学习内容来自:http://kidscancode.org/,游戏并不是自主原创的。

前言

在上一节中,我们对「跳跳兔」进行了简单的优化,然后为游戏中不同的状态添加不同的音乐。

这一节,为游戏添加道具与敌人,触碰到道具,玩家可以急速飞跃,触碰到敌人,玩家将会死亡,游戏结束

添加道具

先整理一下添加道具其整体思路。

  • 1.加载道具图片
  • 2.加载触发道具时的音效
  • 3.让道具随机出现到平台上
  • 4.碰撞检测 - 触碰到道具后会有的效果

想要的效果,道具随机出现在不同的平台上,玩家触碰到,会飞速向上飞跃。

一步步来实现。

首先构建一个新的类来表示这个道具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# sprites.py

class Pow(pg.sprite.Sprite):
def __init__(self, game, plat):
self.groups = game.all_sprites, game.powerups
pg.sprite.Sprite.__init__(self, self.groups)
self.game = game # 整个游戏对象
self.plat = plat # 平台对象
self.type = random.choice(['boost'])
# 加载道具图片
self.image = self.game.spritesheet.get_image(820, 1805, 71, 70)
self.image.set_colorkey(BLACK)
self.rect = self.image.get_rect()
# 道具出现在平台中间的上方
self.rect.centerx = self.plat.rect.centerx
self.rect.bottom = self.plat.rect.top - 5

代码关键部分有相应的注释,不再详细分析。

然后在平台实例化时,随机实例化道具对象。

1
2
3
4
5
6
7
class Platform(pg.sprite.Sprite):
def __init__(self, game, x, y):
#... 省略无关代码

# 随机在平台上初始化道具
if random.randrange(100) < POW_SPAWN_PCT:
Pow(self.game, self)

这样,道具实例化就完成了。

接着添加音效。

1
2
3
4
5
6
7
8
9
def load_data(self): # 加载数据
# ... 省略无关代码

# 加载音乐
self.snd_dir = os.path.join(self.dir, 'snd')
# 跳跃时音效
self.jump_sound = pg.mixer.Sound(os.path.join(self.snd_dir, 'Jump33.wav'))
# 使用道具时音效
self.boost_sound = pg.mixer.Sound(os.path.join(self.snd_dir, 'Boost16.wav'))

至此,只剩道具碰撞检测逻辑未完成了。来搞一搞

1
2
3
4
5
6
7
8
9
10
11
12
# main.py/Game

def update(self):
# ... 省略无关代码
# 碰撞检测 - 玩家碰撞到急速飞跃道具
pow_hits = pg.sprite.spritecollide(self.player, self.powerups, True)
for pow in pow_hits:
# 道具类型 - 不同道具可以实现不同的效果
if pow.type == 'boost':
self.boost_sound.play() # 播放相应的音效
self.player.vel.y = -BOOST_POWER # 快递移动的距离
self.player.jumping = False # 此时不为跳跃状态

此时如果直接运行,会发现,玩家移动时,道具也会同步移动,原因是这部分代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# main.py/Game

def update(self):
# 调用所有元素的update()方法
self.all_sprites.update()

# ... 省略无关代码

# 玩家到达游戏框 1/4 处时(注意,游戏框,头部为0,底部为游戏框长度,到到游戏框的1/4处,表示已经到达了顶部一部分了)
if self.player.rect.top <= HEIGHT / 4:
# 玩家位置移动(往下移动)
self.player.pos.y += abs(self.player.vel.y)
# 平台在游戏框外时,将其注销,避免资源浪费
for plat in self.platforms:
# 平台移动位置(往下移动,移动的距离与玩家相同,这样玩家才能依旧站立在原本的平台上)
plat.rect.y += abs(self.player.vel.y)
if plat.rect.top >= HEIGHT:
plat.kill()
# 分数增加 - 平台销毁,分数相加
self.score += 10

此时为了避免道具同步移动,在Pow类中创建update()方法,实现如下逻辑。

1
2
3
4
5
6
7
8
9
10
11
# 道具对象
class Pow(pg.sprite.Sprite):
def __init__(self, game, plat):
# ... 省略无关代码

def update(self):
# 更新时,避免道具一同移动
self.rect.bottom = self.plat.rect.top - 5
# 如果道具对应的平台已经被消除,那么道具也要被消除
if not self.game.platforms.has(self.plat):
self.kill() # 消除道具

最终效果如下。

添加敌人

在实现前,依旧先整理一下实现的整体逻辑。

  • 1.构建敌人类
  • 2.敌人移动效果
  • 3.敌人随机出现效果
  • 4.敌人碰撞检测
  • 5.敌人不与跳跳兔同步移动

首先来构建敌人类,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# sprites.py

class Mob(pg.sprite.Sprite):
def __init__(self, game):
self.groups = game.all_sprites, game.mobs
pg.sprite.Sprite.__init__(self, self.groups)
self.game = game
# 加载不同状态的图片
self.image_up = self.game.spritesheet.get_image(566, 510, 122, 139)
self.image_up.set_colorkey(BLACK)
self.image_down = self.game.spritesheet.get_image(568, 1534, 122, 135)
self.image_down.set_colorkey(BLACK)
self.image = self.image_up # 默认为向上的图片
self.rect = self.image.get_rect()
# x轴中心位置随机选择
self.rect.centerx = random.choice([-100, WIDTH + 100])
# 随机x轴速度
self.vx = random.randrange(1, 4)
if self.rect.centerx > WIDTH:
self.vx *= -1
# 随机敌人y轴位置
self.rect.y = random.randrange(HEIGHT / 2)
self.vy = 0 # y轴速度默认为0
self.dy = 0.5

__init__()方法中,为敌人添加了不同状态的两种图片并随机敌人初始centerx、x轴速度与y轴位置。

接着实现敌人移动效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# sprites.py

class Mob(pg.sprite.Sprite):

def __init__(self, game):
# ... 省略

def update(self):
self.rect.x += self.vx # x轴位置移动
self.vy += self.dy
# 实现上下抖动移动的效果
if self.vy > 3 or self.vy < -3:
self.dy *= -1
center = self.rect.center
# 上下移动方向不同,使用不同的图片
if self.dy < 0:
self.image = self.image_up
else:
self.image = self.image_down
self.rect = self.image.get_rect()
self.rect.center = center
# y轴具体的移动
self.rect.y += self.vy
# 超过屏幕范围,删除敌人
if self.rect.left > WIDTH + 100 or self.rect.right < -100:
self.kill()

看注释就可明白代码的含义,不再详细分析。

接着实现产生敌人与碰撞检测相关逻辑,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# main.py/Game

def new(self):
self.score = 0
# ... 省略无关代码
self.powerups = pg.sprite.Group() # 急速飞跃道具
self.mobs = pg.sprite.Group() # 敌人对象
# ... 省略无关代码
self.mob_timer = 0

def update(self):
self.all_sprites.update()
# 产生敌人
now = pg.time.get_ticks()
# 通过时间间隔来判断是否要产生敌人
if now - self.mob_timer > 5000 + random.choice([-1000, -500, 0, 500, 1000]):
self.mob_timer = now
Mob(self)
# 碰撞检测 - 如果碰撞到了敌人,游戏结束
mob_hits = pg.sprite.spritecollide(self.player, self.mobs, False)
if mob_hits:
self.playing = False

通过时间间隔的形式随机产生敌人,至于碰撞检测…一样的逻辑。

最后,为了避免敌人元素跟随跳跳兔一同移动,需要添加如下逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def update(self):
# 玩家到达游戏框 1/4 处时(注意,游戏框,头部为0,底部为游戏框长度,到到游戏框的1/4处,表示已经到达了顶部一部分了)
if self.player.rect.top <= HEIGHT / 4:
# 玩家位置移动(往下移动)
self.player.pos.y += abs(self.player.vel.y)
# 平台在游戏框外时,将其注销,避免资源浪费
for plat in self.platforms:
# 平台移动位置(往下移动,移动的距离与玩家相同,这样玩家才能依旧站立在原本的平台上)
plat.rect.y += abs(self.player.vel.y)
if plat.rect.top >= HEIGHT:
plat.kill()
# 分数增加 - 平台销毁,分数相加
self.score += 10
# 敌人位置同步往下移动
for mob in self.mobs:
mob.rect.y += max(abs(self.player.vel.y), 2)

最终效果如下。

在本节中,我们对「跳跳兔」做了道具与敌人的添加,增加了「跳跳兔游戏的趣味性」。

因为考虑到篇幅,文中没有给出完整的代码,但为了方便大家理解,我将相应的代码上传到了github,图片资源与音乐资源也在github上。

https://github.com/ayuLiao/jumprabbit

如果文章对你有帮助或你觉得有点意思,点击「在看」支持作者一波。

处理私事,拖更许久,抱歉抱歉。

前言

在第七节中,实现了玩家对象的动画效果以及平台的图片化。

本节将细节优化一下,然后加上相应的配音。

细节优化

如果你有仔细的试玩第七节实现的跳跳兔,会发现一些瑕疵,这里简单修改一下。

瑕疵1:跳跳兔会平移。

造成该现象的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# main.py/Game

def update(self):
self.all_sprites.update()
# # 玩家在界面中时(y>0),进行碰撞检测,检测玩家是否碰撞到平台
if self.player.vel.y > 0:
hits = pg.sprite.spritecollide(self.player, self.platforms, False)

# 会产生平移效果
if hits:
self.player.pos.y = hits[0].rect.top
self.player.vel.y = 0

...# 省略其他无关代码

在update()方法检查碰撞检测时,如果发生了碰撞,就直接将hits列表中第一个元素赋值给player玩家对象,从而造成了跳跳兔本身没有跳跃,只是碰到了平台周围,就瞬移到上一个平台的效果。

代码进行如下修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# main.py/Game

def update(self):
self.all_sprites.update()
# # 玩家在界面中时(y>0),进行碰撞检测,检测玩家是否碰撞到平台
if self.player.vel.y > 0:
hits = pg.sprite.spritecollide(self.player, self.platforms, False)

if hits:
lowest = hits[0]
for hit in hits:
if hit.rect.bottom > lowest.rect.bottom:
lowest = hit # 保存最小的值
# 避免平移效果 - 兔子最底部没有小于碰撞检测中的最小值,则不算跳跃到平台上
if self.player.pos.y < lowest.rect.centery:
self.player.pos.y = lowest.rect.top
self.player.vel.y = 0
self.player.jumping = False

...# 省略其他无关代码

发送碰撞时,要进一步判断,先获得碰撞时,碰撞对象hit底部的最小值,然后判断此时玩家对象的位置y轴坐标是否要小于lowest对象的中心位置,如果小于,则说明玩家对象在lowest之上,此时才移动玩家对象。

瑕疵2:跳跃太高,可玩性低

简单点击空格,跳跳兔就可以跳很高,让游戏变得简单无趣,这里加多些逻辑,实现短跳与长跳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# sprites.py/Player

# 短跳
def jump_cut(self):
if self.jumping:
if self.vel.y < -3:
self.vel.y = -3

def jump(self):
# 跳跃到其他平台 - 玩家对象x加减1,为了做碰撞检测,只有站立在平台上,才能实现跳跃
self.rect.y += 2
hits = pg.sprite.spritecollide(self, self.game.platforms, False)
self.rect.y -= 2
if hits and not self.jumping:
self.jumping = True
self.vel.y = -20

通过self.jumping判断当前是否在跳跃状态,如果在跳跃状态,就将y轴速度砍到3。此外jump()方法也做了修改,通过y轴来判断是否产生碰撞检测,如果发生了碰撞而且又是没有跳跃的状态,这说明跳跳兔在平台上,可以进行跳跃,将self.jumping跳跃标志设为True。

你可能会疑惑,不慌,结合下面代码一起看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# main.py/Game

# 事件处理
def events(self):
for event in pg.event.get():
# 关闭
if event.type == pg.QUIT:
if self.playing:
self.playing = False
self.running = False
# 跳跃
if event.type == pg.KEYDOWN:
if event.key == pg.K_SPACE:
self.player.jump()
# 按钮抬起,减小跳跃速度,从而实现,快速点击,短跳,长按,长跳的效果
if event.type == pg.KEYUP:
if event.key == pg.K_SPACE:
self.player.jump_cut()

简单而言,如果长按空格键进行跳跃,就只会有jump()方法的效果,跳跃快结束时,才松开空格,此时jump_cut()方法已经没有明显的作用了。

如果简单快击空格键,此时跳跃还未完成,jump_cut()又被调用,跳跳兔y轴速度会被减弱,从而实现短跳的效果。

增加背景音乐

在pygame中,操作音乐是非常简单的,无论多大的音乐文件都可以被pygame以流的形式读入并播放。

跳跳兔游戏在不同的状态要播放不同状态的音乐。

在Game/load_data()方法中加载音频文件夹。

1
2
3
4
5
6
7
8
9
# main.py/Game

def load_data(self): # 加载数据
...
# 加载精灵图片
self.spritesheet = Spritesheet(os.path.join(img_dir, SPRITESHEET))
# 加载音乐
self.snd_dir = os.path.join(self.dir, 'snd')
self.jump_sound = pg.mixer.Sound(os.path.join(self.snd_dir, 'Jump33.wav')) # 玩家对象跳跃时的音乐

新建游戏时,加载游戏初始音乐,此外游戏结束时,也使用同样的音乐,逻辑如下。

1
2
3
4
5
6
7
8
9
10
11
# main.py/Game

# 开始游戏的钩子函数
def show_start_screen(self):
# 加载音乐
pg.mixer.music.load(os.path.join(self.snd_dir, 'Yippee.ogg'))
# 重复播放
pg.mixer.music.play(loops=-1)
... # 其他逻辑,省略
# 退出音乐
pg.mixer.music.fadeout(500)

通过pygame.mixer.music.load()方法可以将音乐加载到磁盘中,然后调用play()方法播放,其中loops参数用于指定循环播放次数,-1表示一直循环播放,当需要退出音乐时,滴啊用fadeout()方法,退出音乐,fadeout(time)设置音乐淡出的时间,该方法会阻塞到音乐消失。

游戏结束方法逻辑与show_start_screen()一致,不再展示代码分析。

游戏的过程中以同样的方式加载音乐作为游戏的背景音乐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def new(self):
...
# 游戏初始时音乐
pg.mixer.music.load(os.path.join(self.snd_dir, 'Happy Tune.ogg'))
self.run()

def run(self):
# loops表示循环次数,-1表示音乐将无限播放下去
pg.mixer.music.play(loops=-1)
self.playing = True
while self.playing:
self.clock.tick(FPS)
self.events()
self.update()
self.draw()
# 退出后停止音乐,fadeout(time)设置音乐淡出的时间,该方法会阻塞到音乐消失
pg.mixer.music.fadeout(500)

最后,为跳跃动作加上相应的音乐。

1
2
3
4
5
6
7
8
9
10
11
# sprites.py/Player

def jump(self):
# 跳跃到其他平台 - 玩家对象x加减1,为了做碰撞检测,只有站立在平台上,才能实现跳跃
self.rect.y += 2
hits = pg.sprite.spritecollide(self, self.game.platforms, False)
self.rect.y -= 2
if hits and not self.jumping:
self.game.jump_sound.play() # 播放跳跃的声音
self.jumping = True
self.vel.y = -20

在本节中,我们对「跳跳兔」进行了简单的优化,然后为游戏中不同的状态添加不同的音乐。

因为考虑到篇幅,文中没有给出完整的代码,但为了方便大家理解,我将相应的代码上传到了github,图片资源与音乐资源也在github上。

https://github.com/ayuLiao/jumprabbit

如果文章对你有帮助或你觉得有点意思,点击「在看」支持作者一波。

前言

在第6节内容中,实现了游戏结束逻辑与玩家图片化,但跳跳兔只是一张简单的图片,显得比较呆板,本节会为跳跳兔添加上相应的动画效果,并将平台替换成相应的图片。

添加动画

跳跳兔在站立时,希望有上下蹲的动画,在走动时,希望有左右走动的动画,在跳跃时,希望有跳跃动画。

动画的本质就是不同图片间的切换,在pygame中要实现动画,只需要在不同帧使用不同的图片则可。

在Player的__init__()方法中定义多个变量用于记录不同的状态,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# sprites.py

class Player(pg.sprite.Sprite):
def __init__(self, game):
pg.sprite.Sprite.__init__(self)
self.game = game
# 不同的状态
self.walking = False
self.jumping = False
# 当前帧(用于判断当前要执行哪个动画)
self.current_frame = 0
self.last_update = 0
self.load_images() # 加载图片
self.image = self.standing_frames[0]
self.rect = self.image.get_rect()
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.pos = vec(WIDTH / 2, HEIGHT / 2)
self.vel = vec(0, 0) # 速度
self.acc = vec(0, 0) # 加速度

在__init__()方法中,定义了self.walking与self.jumping状态,用于表示玩家对象是在行走状态还是跳跃状态,接着定义了self.current_frame用于表示当前帧,定义了self.last_update用于记录上一次的时间点,随后,编调用了load_images()方法来载入图片,该方法代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# sprites.py/Player

def load_images(self):
# 站立状态
self.standing_frames = [self.game.spritesheet.get_image(614, 1063, 120, 191),
self.game.spritesheet.get_image(690, 406, 120, 201)]
for frame in self.standing_frames:
frame.set_colorkey(BLACK) # 将图像矩阵中除图像外周围的元素都设置为透明的

# 走动状态
self.walk_frames_r = [self.game.spritesheet.get_image(678, 860, 120, 201),
self.game.spritesheet.get_image(692, 1458, 120, 207)]
self.walk_frames_l = []
for frame in self.walk_frames_r:
frame.set_colorkey(BLACK)
# 水平翻转
self.walk_frames_l.append(pg.transform.flip(frame, True, False))

# 跳跃状态
self.jump_frame = self.game.spritesheet.get_image(382, 763, 150, 181)
self.jump_frame.set_colorkey(BLACK)

在load_images()方法中,为不同的状态载入了不同的图片,其中,走动状态的图片还做了水平翻转处理,这是因为原始的大图中,走动的图片只有一个方向的,而走动可以往左走也可以往右走,所以需要将图片水平翻转一下。

调用pygame.transform.flip(Surface, xbool, ybool)用于翻转,xbool => True 为水平翻转,ybool => True 为垂直翻转。

图片准备好后,动画效果的基本素材就准备好了,在Player类的update()方法中调用动画方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# sprites.py/Player

def update(self):
# 动画
self.animate()
...

def animate(self):
# 获得当前过了多少毫秒
now = pg.time.get_ticks()

if self.vel.x != 0: # 判断速度在x轴方向是否为0,从而判断玩家对象是否移动
self.walking = True
else:
self.walking = False
# 走动状态下的动画
if self.walking:
# 当前时间 - 上次时间 大于 180,即间隔时间大于180时
if now - self.last_update > 180:
self.last_update = now
# 当前帧 加一 与 walk_frames_l 长度取余,从而得到当前要做哪个东西
self.current_frame = (self.current_frame + 1) % len(self.walk_frames_l)
bottom = self.rect.bottom
# 向左走还是向右走
if self.vel.x > 0:
# 当前帧要做的动作
self.image = self.walk_frames_r[self.current_frame]
else:
self.image = self.walk_frames_l[self.current_frame]
self.rect = self.image.get_rect()
self.rect.bottom = bottom

# 站立状态下的动画
if not self.jumping and not self.walking:
if now - self.last_update > 350:
self.last_update = now
self.current_frame = (self.current_frame + 1) % len(self.standing_frames)
bottom = self.rect.bottom
self.image = self.standing_frames[self.current_frame]
self.rect = self.image.get_rect()
self.rect.bottom = bottom

在pygame 中的时间是以毫秒(千分之一秒)表示的,通过 pygame.time.get_ticks 函数可以获得 pygame.init 后经过的时间的毫秒数。

随后的逻辑通过注释可以比较简单的理解。判断当前时间与上一层记录时间的间隔,如果满足条件,则只需图片的切换逻辑,注意,时间都是毫秒级的。

切换图片的核心逻辑就是当前帧与图片列表长度取余,获得下标,通过下标去取列表中的图片。

走动时效果如下:

有个细节需要注意,在判断玩家对象是否是走动状态时,利用了速度变量的x轴是否为0来判断

1
2
3
4
if self.vel.x != 0: # 判断速度在x轴方向是否为0,从而判断玩家对象是否移动
self.walking = True
else:
self.walking = False

但self.vel.x通常不会为0,所以需要处理一下,修改一下update()方法中的逻辑,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def update(self):
# 动画
self.animate()
self.acc = vec(0, PLAYER_GRAV)
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.acc.x = -PLAYER_ACC
if keys[pg.K_RIGHT]:
self.acc.x = PLAYER_ACC

# 获得加速度
self.acc.x += self.vel.x * PLAYER_FRICTION
# 速度与加速度
self.vel += self.acc
# 如果速度小于0.1,则速度为0(比如这样设置,不然速度永远无法0)
if abs(self.vel.x) < 0.1:
self.vel.x = 0
self.pos += self.vel + 0.5 * self.acc
# wrap around the sides of the screen
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH

self.rect.midbottom = self.pos

如果self.vel.x的绝对值小于0.1,则让self.vel.x为0。

平台图片化

跳跳兔要跳跃到相应的平台上,现在平依旧是方块,这里以相同的方式将平台替换成相应的图片。

在Platform的__init__()中,实现载入图片的逻辑,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
class Platform(pg.sprite.Sprite):
def __init__(self, game, x, y):
pg.sprite.Sprite.__init__(self)
self.game = game
# 载入图片
images = [self.game.spritesheet.get_image(0, 288, 380, 94),
self.game.spritesheet.get_image(213, 1662, 201, 100)]
# 随机选择一种
self.image = random.choice(images)
self.image.set_colorkey(BLACK)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y

载入完图片,随机选择一个图片作为样式,需要注意,我们修改了__init__()的参数,此时该方法只需要获得(x,y)坐标以及game实例则可。

因为__init__()被修改了,所以实例化逻辑也要修改一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#setting.py
# 起始平台
PLATFORM_LIST = [(0, HEIGHT - 60),
(WIDTH / 2 - 50, HEIGHT * 3 / 4 - 50),
(125, HEIGHT - 350),
(350, 200),
(175, 100)]

# main.py/Game

def new(self):
self.score = 0
self.all_sprites = pg.sprite.Group()
self.platforms = pg.sprite.Group()
self.player = Player(self)
self.all_sprites.add(self.player)
for plat in PLATFORM_LIST:
p = Platform(self, *plat)
self.all_sprites.add(p)
self.platforms.add(p)
self.run()

def update(self):
# ...
# 判断平台数,产生新的平台
while len(self.platforms) < 6:
width = random.randrange(50, 100)
# 平台虽然是随机生成的,但会生成在某一个范围内
p = Platform(self, random.randrange(0, WIDTH - width),
random.randrange(-75, -30))
self.platforms.add(p)
self.all_sprites.add(p)

最终效果如下

在本节中,我们实现了玩家对象的动画效果以及平台的图片化。

因为考虑到篇幅,文中没有给出完整的代码,但为了方便大家理解,我将相应的代码上传到了github

https://github.com/ayuLiao/jumprabbit

如果文章对你有帮助或你觉得有点意思,点击「在看」支持作者一波。

HackPython改名为「懒编程」,纯粹是因为我有另外一个写杂文的号叫「懒写作」,此外,感觉「懒编程」比HackPython好记。

前言

在第5节内容中,实现了积分机制、玩家死亡逻辑以及游戏开始界面逻辑,本节继续完善游戏,来实现游戏结束逻辑与玩家图片化,不再使用方块。

游戏结束逻辑

回忆一下整体逻辑:

1
2
3
4
5
6
7
g = Game()
g.show_start_screen()
while g.running:
g.new()
g.show_go_screen()

pg.quit()

可以通过show_go_screen()方法实现游戏结束逻辑,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# main.py/Game

# 每轮游戏结束后,都会调用该方法
def show_go_screen(self):
# game over/continue
if not self.running: # 是否在运行
return
self.screen.fill(BGCOLOR) # 游戏框背景颜色填充
# 绘制文字
self.draw_text("GAME OVER", 48, WHITE, WIDTH / 2, HEIGHT / 4)
self.draw_text("Score: " + str(self.score), 22, WHITE, WIDTH / 2, HEIGHT / 2)
self.draw_text("Press a key to play again", 22, WHITE, WIDTH / 2, HEIGHT * 3 / 4)
# 判断分数
if self.score > self.highscore:
self.highscore = self.score
self.draw_text("NEW HIGH SCORE!", 22, WHITE, WIDTH / 2, HEIGHT / 2 + 40)
# 记录新的最高分到文件中 - 持久化
with open(os.path.join(self.dir, HS_FILE), 'w') as f:
f.write(str(self.score))
else:
self.draw_text("High Score: " + str(self.highscore), 22, WHITE, WIDTH / 2, HEIGHT / 2 + 40)
# 翻转
pg.display.flip()
# 等待敲击任意键,重新开始新的一轮游戏
self.wait_for_key()

show_go_screen()方法逻辑可以阅读详细的注释,不再赘述。

show_go_screen()通过文件的方式来记录最高的分数,所以在游戏一开始,就需要从文件中读取此前的分数,好为这部分逻辑做判断,逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Game:
def __init__(self):
# ...
# 加载最高分数
self.load_data()

def load_data(self):
self.dir = os.path.dirname(__file__)
filepath = os.path.join(self.dir, HS_FILE)
with open(filepath, 'r') as f:
try:
self.highscore = int(f.read())
except:
self.highscore = 0

玩家图片化

一个正常的游戏,肯定要有相应的图片素材的,图片素材是否精美也是影响他人是否要玩你游戏的重要因素。

通常,一个游戏,会有多张大图,多个同类元素都放在这张大的png图片中,而不是每个元素都是一个png元素,通过这种方式,让整个游戏包更小。

如下,一张完整的图

与其对应的就是图中不同元素其坐标位置(x,y)以及元素图片大小

写一段从完整图片中获取对应元素的逻辑,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# sprites.py

# 加载与解析精灵图片
class Spritesheet:
def __init__(self, filename):
# 主要要使用convert()进行优化, convert()方法会 改变图片的像素格式
# 这里加载了整张图片
self.spritesheet = pg.image.load(filename).convert()

# 从比较大的精灵表中,通过相应的xml位置,抓取中出需要的元素
def get_image(self, x, y, width, height):
# 创建Surface对象(画板对象)
image = pg.Surface((width, height))
# blit — 画一个图像到另一个
# 将整张图片中,对应位置(x,y)对应大小(width,height)中的图片画到画板中
image.blit(self.spritesheet, (0, 0), (x, y, width, height))
# pygame.transform.scale 缩放的大小
# 这里将图片缩放为原来的一半
image = pg.transform.scale(image, (width // 2, height // 2))
return image

在__init__()中,通过pygame.image.load()方法加载完整的图片,记得使用convert()方法进行优化,随后定义了get_image()方法,该方法的逻辑也很直接,先实例化Surface类,获得与图片大小相同的面板对象,然后,通过blit()方法将完整图片中对应位置与大小的元素剥离出来。

经过实践,原本图片元素太大,所以通过pygame.transform.scale()方法将图片元素缩小2倍。

编写完Spritesheet类后,在Game类的load_data()方法中实例化一下

1
2
3
4
5
6
7
8
9
10
11
12
13
# main.py/Game

def load_data(self): # 加载数据
self.dir = os.path.dirname(__file__)
filepath = os.path.join(self.dir, HS_FILE)
with open(filepath, 'r') as f:
try:
self.highscore = int(f.read())
except:
self.highscore = 0
img_dir = os.path.join(self.dir, 'img')
# 加载精灵图片
self.spritesheet = Spritesheet(os.path.join(img_dir, SPRITESHEET))

做完这些后,在Player类初始化时调用其中的get_image()方法就大功告成了。

1
2
3
4
5
6
7
8
9
10
11
12
class Player(pg.sprite.Sprite):
def __init__(self, game):
pg.sprite.Sprite.__init__(self)
self.game = game
# 加载 bunny1_ready状态的兔子图片, xml文件中给出的(x,y)与(width,height)
self.image = self.game.spritesheet.get_image(614, 1063, 120, 191)
self.image.set_colorkey(BLACK)
self.rect = self.image.get_rect()
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.pos = vec(WIDTH / 2, HEIGHT / 2)
self.vel = vec(0, 0) # 速度
self.acc = vec(0, 0) # 加速度

为了整体美观,修改了一下,游戏框整体的背景颜色,具体通过self.screen.fill(BGCOLOR)方法。

最后,效果如下:

结尾

在本节中,我们实现了游戏结束界面以及使用了玩家元素,后面,会进一步优化玩家元素,让玩家在左右移动时,是不同的图片,从而让整个游戏显得更加灵动。

因为考虑到篇幅,文中没有给出完整的代码,但为了方便大家理解,我将相应的代码上传到了github

https://github.com/ayuLiao/jumprabbit

如果文章对你有帮助或你觉得有点意思,点击「在看」支持作者一波。

前言

本节会增加计分机制,即跳跃了多少个平台,有多少积分,此外如果玩家掉落在游戏框外,玩家死亡,开始新的一局游戏。

增加积分机制

积分机制的原理其实很简单,通过pygame在游戏框中绘制相应的文字,当玩家跳跃到新平台时,积分发生相应的变更,en…逻辑太简单了,直接看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# main.py/Game

# 开始新游戏时
def new(self):
# 初始化积分
self.score = 0

def update(self):
# ...

# 玩家到达游戏框 1/4 处时(注意,游戏框,头部为0,底部为游戏框长度,到到游戏框的1/4处,表示已经到达了顶部一部分了)
if self.player.rect.top <= HEIGHT / 4:
# 玩家位置移动(往下移动)
self.player.pos.y += abs(self.player.vel.y)
# 平台在游戏框外时,将其注销,避免资源浪费
for plat in self.platforms:
# 平台移动位置(往下移动,移动的距离与玩家相同,这样玩家才能依旧站立在原本的平台上)
plat.rect.y += abs(self.player.vel.y)
if plat.rect.top >= HEIGHT:
plat.kill()
# 分数增加 - 平台销毁,分数相加
self.score += 10

在new()方法中,初始化了积分对象,然后在update()方法中更新。这里的计分方式不是玩家跳跃到一个新的平台就积分,而是旧的平台被销毁后,再计分。这就能避免玩家不向上跳跃,而一直在原地跳跃就能获得积分的情况。

接着就是绘制积分文字的逻辑,将其放置在draw()方法中实现.

1
2
3
4
5
6
7
8
9
10
# main.py/Game

def draw(self):
# 绘制
self.screen.fill(BLACK)
self.all_sprites.draw(self.screen)
# 绘制文字 - 具体的分数
self.draw_text(str(self.score), 22, WHITE, WIDTH / 2, 15)
# 翻转
pg.display.flip()

在draw()方法中,调用了self.draw_text()方法进行文字的显示,该方法代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# main.py/Game

def __init__(self):
# ...
# 设置绘制时使用的字体,你也可以直接使用系统本身的字体
self.font_name = pg.font.match_font(FONT_NAME)

# 绘制文字
def draw_text(self, text, size, color, x, y):
font = pg.font.Font(self.font_name, size) # 设置字体与大小
text_surface = font.render(text, True, color) # 设置颜色
text_rect = text_surface.get_rect() # 获得字体对象
text_rect.midtop = (x, y) # 定义位置
self.screen.blit(text_surface, text_rect) # 在屏幕中绘制字体

代码中给出了详细的注释,不再赘述。

增加了上面的代码,运行游戏,就会有相应的积分效果

玩家死亡

如果玩家这个方块位于游戏框范围外,该玩家就死亡了,将这个逻辑实现到Game类的update()方法中

1
2
3
4
5
6
7
8
9
10
#main.py/Game

def update(self):
# 死亡 - 玩家底部大于游戏框高度
if self.player.rect.bottom > HEIGHT:
for sprite in self.all_sprites:
sprite.rect.y -= max(self.player.vel.y, 10)
# 元素底部小于0 - 说明在游戏框外面,将其删除
if sprite.rect.bottom < 0:
sprite.kill()

一开始判断玩家对象方块的底部是否大于游戏框高度了,如果大于,说明玩家已经在游戏框最底部了,而且有一部分已经在游戏框外了,此时玩家死亡,游戏结束。

绘制游戏开始界面

你是否还记得第二节中给出的整体结构,其中包括了show_start_screen()方法与show_go_screen()方法,因为整体的运行逻辑如下:

1
2
3
4
5
6
7
g = Game()
g.show_start_screen() # 游戏开始前会执行的逻辑
while g.running:
g.new()
g.show_go_screen() # 一轮游戏结束后会执行的逻辑

pg.quit()

可以将游戏开始界面的绘制逻辑写到show_start_screen()方法中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
# main.py/Game

# 开始游戏的钩子函数
def show_start_screen(self):
self.screen.fill(BGCOLOR) # 填充颜色
self.draw_text(TITLE, 48, WHITE, WIDTH / 2, HEIGHT / 4)
# 绘制文字
self.draw_text("Left and right button move, space bar jump", 22, WHITE, WIDTH / 2, HEIGHT / 2)
self.draw_text("Press any key to start the game", 22, WHITE, WIDTH / 2, HEIGHT * 3 / 4)
# 画布翻转
pg.display.flip()
self.wait_for_key() # 等待用户敲击键盘中的仍以位置

在show_start_screen()方法中,一开会先填充了整个背景颜色,然后调用了draw_text()方法绘制文字,这里如果使用中文是无法显示的,pygame中要显示中文,需要指定对应的系统字体,如系统中的「宋体」、「黑体」等,最后调用了wait_for_key()方法进行等待,该方法逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
# main.py/Game

def wait_for_key(self):
waiting = True
while waiting:
self.clock.tick(FPS)
for event in pg.event.get():
if event.type == pg.QUIT: # 点击退出,结束等待循环
waiting = False
self.running = False
if event.type == pg.KEYUP: # 按下键盘,结束等待循环
waiting = False

wait_for_key()方法的逻辑就是一个无限循环,只有用户点击关闭按钮或敲击键盘中的任意键才能退出这个循环。

退出循环后,才能进入真正的游戏循环开始游戏。

结尾

在本节中,我们实现了计分逻辑、死亡逻辑以及游戏启动时简单欢迎界面逻辑。

因为考虑到篇幅,文中没有给出完整的代码,但为了方便大家理解,我将相应的代码上传到了github

https://github.com/ayuLiao/jumprabbit

如果文章对你有帮助或你觉得有点意思,点击「在看」支持作者一波。

简介

「跳跳兔」小游戏中的玩家当然要有跳跃能力,本节就来实现玩家类的跳跃以及整个游戏框界面的更新,当玩家跳跃时,游戏背景要往后移动,要有新的平台产生,让玩家可以继续跳跃,本节就来实现这样的功能。

赋予玩家跳跃功能

为了让界面有多个元素,在一开始初始化游戏框时,就初始化多个平台元素,效果如下:

代码如下,为了方便理解,没有展示无关代码,将多个文件的代码一同显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# settings.py

# 平台列表
PLATFORM_LIST = [(0, HEIGHT - 40, WIDTH, 40),
(WIDTH / 2 - 50, HEIGHT * 3 / 4, 100, 20),
(125, HEIGHT - 350, 100, 20),
(350, 200, 100, 20),
(175, 100, 50, 20)]

# main.py/Game

def new(self):
# start a new game
self.all_sprites = pg.sprite.Group()
self.platforms = pg.sprite.Group()
self.player = Player(self)
self.all_sprites.add(self.player)
# 实例化一系列平台,并添加到所有元素组与平台元素组中
for plat in PLATFORM_LIST:
p = Platform(*plat)
self.all_sprites.add(p)
self.platforms.add(p)
self.run()

上述逻辑就是在开始新游戏时,for迭代创建PLATFORM_LIST列表中给定位位置与大小的平台。

接着就来实现跳跃逻辑。

为了代码直观,将监控跳跃相关的逻辑写到events()方法中(该方法负责事件处理),我们希望点击空格键,玩家元素跳跃,其具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
# main.py/Game

def events(self):
for event in pg.event.get():
if event.type == pg.QUIT:
if self.playing:
self.playing = False
self.running = False
# 玩家跳跃
if event.type == pg.KEYDOWN:
if event.key == pg.K_SPACE:
self.player.jump() # 调用玩家跳跃方法

简单而言,就是监控键盘敲击事件,然后判断敲击的按键为空格键(K_SPACE),最后调用玩家对象的jump()方法实现跳跃。

看一下jump()方法的具体逻辑。

1
2
3
4
5
6
7
8
9
10
#sprites.py/Player

# 跳跃
def jump(self):
# 对x轴进行加一减一操作,玩家只有站立在平台上,才能跳跃成功。
self.rect.x += 1
hits = pg.sprite.spritecollide(self, self.game.platforms, False)
self.rect.x -= 1
if hits: # 碰撞检测成功,站立在了平台上
self.vel.y = -20 # 跳跃

对player玩家对象的x走了加一减一并进行碰撞检测的操作,通过这种方式,就可以判断当前玩家对象是否站立与平台上,只有在平台上,才能让其跳跃,在空中是不允许玩家跳跃的(无法二段跳).

跳跃的本质是让玩家对象向上移动,这里就是-20,之所以是负数,是因为最上传y轴坐标为0,最下方y轴坐标为游戏框的高度,-20就是让玩家对象向上移动,如下图:

但这里跟新的只是self.vel速度属性,真正移动玩家对象的方法依旧是update()方法,其逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#sprites.py/Player

def update(self):
# 有初始的加速度 - 玩家没有在平台上就会掉落
self.acc = vec(0, PLAYER_GRAV)
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.acc.x = -PLAYER_ACC
if keys[pg.K_RIGHT]:
self.acc.x = PLAYER_ACC

self.acc.x += self.vel.x * PLAYER_FRICTION
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH
# 赋值给底部 - 碰撞检测相关
self.rect.midbottom = self.pos

update()方法逻辑与上节内容相同,不再赘述。

至此,就完成了玩家的跳跃。

背景移动效果

但玩家对象光跳跃是不够的,整个游戏界面不移动,玩家跳两下就没有平台可以跳跃了,所以接着来实现游戏界面整体移动的效果。

在开始写之前,需要理清一个概念,要实现移动效果,并不是游戏界面整体向下移动了,而是游戏框中的元素整体向后移动的,具体而言就是玩家类向下移动了一部分具体,对应的平台元素也向下移动了一段距离。

要实现这个效果,可以通过Game游戏类的update()方法来实现,该方法主要用于更新游戏的整体状态,其具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# main.py/Game

def update(self):
self.all_sprites.update()
# # 玩家在界面中时(y>0),进行碰撞检测,检测玩家是否碰撞到平台
if self.player.vel.y > 0:
hits = pg.sprite.spritecollide(self.player, self.platforms, False)
if hits:
self.player.pos.y = hits[0].rect.top
self.player.vel.y = 0
# 玩家到达游戏框 1/4 处时(注意,游戏框,头部为0,底部为游戏框长度,到到游戏框的1/4处,表示已经到达了顶部一部分了)
if self.player.rect.top <= HEIGHT / 4:
# 玩家位置移动(往下移动)
self.player.pos.y += abs(self.player.vel.y)
# 平台在游戏框外时,将其注销,避免资源浪费
for plat in self.platforms:
# 平台移动位置(往下移动,移动的距离与玩家相同,这样玩家才能依旧站立在原本的平台上)
plat.rect.y += abs(self.player.vel.y)
if plat.rect.top >= HEIGHT:
plat.kill()

# 判断平台数,产生新的平台
while len(self.platforms) < 6:
width = random.randrange(50, 100)
# 随机生成平台
p = Platform(random.randrange(0, WIDTH - width),
random.randrange(-75, -30),
width, 20)
self.platforms.add(p)
self.all_sprites.add(p)

Game类的update()方法中,主要新增了两段逻辑。

当玩家到达游戏框的1/4时,此时玩家对象已经在高位,此时要做的就是移动玩家对象以及通过for迭代移动所有的平台对象,让玩家对象与平台对象向下移动相同的距离实现整个游戏框界面向下移动的效果。此外还会判断平台对象的顶部是否大于游戏框高度,如果大于,说明当前的平台对象依旧完全在游戏框外了,调用kill()方法将其注销,避免占用额外的内存。

接着通过while循环当前平台对象的个数,通过此前的逻辑,我们消除了在游戏框外的平台对象,为了让平台对象个数总是为6,这里会随机创建相应个数的平台对象作为补充,通过random来实现平台位置的随机,让游戏不至于太过无聊。

结尾

因为考虑到篇幅,文中没有给出完整的代码,但为了方便大家理解,我将相应的代码上传到了github

https://github.com/ayuLiao/jumprabbit

如果文章对你有帮助或你觉得有点意思,点击「在看」支持作者一波。

简介

pygame如何通过键盘来控制游戏中的元素呢?元素之间是如何进行碰撞检测的呢?

阅读完本节你就很清晰了,此外本文还会整理出pygame开发游戏的通用整体结构,该系列后续的内容都以这个结构来编写。

整体结构

随着游戏项目的复杂化,有必要整理一下代码,形成一个统一的风格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import pygame as pg

class Game:
# 初始化窗口、计数器
def __init__(self):
pass

# 开启新的游戏
def new(self):
pass

# 游戏中的大循环
def run(self):
pass

# 更新
def update(self):
pass

# 事件
def events(self):
pass

# 绘制界面
def draw(self):
pass

# 游戏启动 / 开始画面的钩子函数
def show_start_screen(self):
# game splash/start screen
pass

# 游戏结束 / 继续的钩子函数
def show_go_screen(self):
pass

# 入口逻辑
g = Game()
g.show_start_screen()
while g.running:
g.new()
g.show_go_screen()

pg.quit()

将不同的步骤封成不同的方法,这样让Game类会简单直观很多,不同方法的细节如下

__init__中,初始化各种基本对象,如pygame对象、计数器对象等待

1
2
3
4
5
6
7
8
9
10
# main.py/Game

def __init__(self):
# 初始化窗口、计数器
pg.init()
pg.mixer.init()
self.screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption(TITLE)
self.clock = pg.time.Clock()
self.running = True

在new()方法中,新游戏开始时,进行元素的初始化,并调用run()方法,运行游戏的主循环。

1
2
3
4
5
6
7
8
# main.py/Game

# 开启新的游戏
def new(self):
self.all_sprites = pg.sprite.Group()
self.player = Player()
self.all_sprites.add(self.player)
self.run()

run()方法就很简单了,主要就是调用相应的方法设置帧率、处理事件输入、更新状态与绘制渲染游戏等,代码如下

1
2
3
4
5
6
7
8
9
10
# main.py/Game

# 游戏中的大循环
def run(self):
self.playing = True
while self.playing:
self.clock.tick(FPS) # 设置帧率
self.events() # 事件输入处理
self.update() # 状态更新
self.draw() # 绘制

对应方法代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def update(self):
# 更新所有元素的状态
self.all_sprites.update()

def events(self):
# 获得事件输入
for event in pg.event.get():
# 判断是否是退出事件
if event.type == pg.QUIT:
if self.playing:
self.playing = False
self.running = False

def draw(self):
# 绘制与渲染游戏背景
self.screen.fill(BLACK)
self.all_sprites.draw(self.screen) # 绘制所有元素到界面中
pg.display.flip()

代码内容在前面文章中介绍过,不再赘述。

控制玩家元素

控制玩家元素其实非常简单,核心就是利用pg.key.get_pressed()方法获得键盘的输入,然后判断敲击的键位是不是需要的键位,如果是,则移动元素。

但还需要考虑的是如何移动元素?想此前简单的对元素x轴或y轴方向添加位移距离是不适合的,比如我就敲了一下左键,元素就一直往左移动,不会停下来,这不符合常规,应该要实现,只有一直按着左键,元素才能一直向左移动。

为了实现上述的效果,就需要引入摩擦系数,模仿现实世界,我们之所以走一步不会一直滑下去就是因为存在摩擦力,这里我们使用摩擦系数来模拟这个过程。

下面通过代码来实现一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import pygame as pg
from settings import *
vec = pg.math.Vector2 # 二维变量

# 在settings.py中,为了方便理解,贴一下
# PLAYER_ACC = 0.5 # 加速度
# PLAYER_FRICTION = -0.12 # 摩擦系数

class Player(pg.sprite.Sprite):
def __init__(self):
pg.sprite.Sprite.__init__(self)
# 创建一个黄色方块作为玩家元素
self.image = pg.Surface((30, 40))
self.image.fill(YELLOW)
self.rect = self.image.get_rect()
# 初始化在游戏框中心
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.pos = vec(WIDTH / 2, HEIGHT / 2) # 创建一个二维变量并将其初始化为游戏框的中心位置
self.vel = vec(0, 0) # 记录速度
self.acc = vec(0, 0) # 记录加速度

def update(self):
self.acc = vec(0, 0)
keys = pg.key.get_pressed() # 获得键盘输入
if keys[pg.K_LEFT]: # 按左
self.acc.x = -PLAYER_ACC # 加速度,正负号表示方向
if keys[pg.K_RIGHT]: # 按右
self.acc.x = PLAYER_ACC

# 计算加速度
# 当前加速度 = (上一帧的速度 * 摩擦系数) + 上一帧加速度
self.acc += self.vel * PLAYER_FRICTION
# 当前速度 = 当前加速度 + 上一帧的速度
self.vel += self.acc
# 有了当前速度与加速度,就可以计算移动距离了
self.pos += self.vel + 0.5 * self.acc
# 跑马灯效果
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH
# 移动玩家元素
self.rect.center = self.pos

__init__()方法中,除了初始化玩家元素为一个黄色长方形外,还利用pg.math.Vector2初始化了多个二维变量,pygame的math模块还支持三维变量的初始化。

初始化多个二维变量的目的在于计算当前帧的移动距离,其基本物理公式可以参考下图。

再看到update()方法,计算移动距离其实就是套用了上图红色框中的公式,只是时间t为1。时间t之所以可以为1,是因为我们每一帧都在这部分逻辑,将每一帧时间作为1则可。

其余的具体逻辑,请阅读详细的注释,注释比文字好理解。

最后,将计算出来的新位置self.pos赋值给玩家元素的中心点,从而实现玩家元素的移动,玩家中心点的理解可以参考下图。

碰撞检测

通过上两节的代码,已经可以实现通过键盘空中游戏框中黄色方块的效果,但感觉有点单调,是否可以让元素在某个平台上运行呢?

具体而言就是实现下面的效果,黄色的玩家元素在绿色的平台上站立移动,如果超出绿色小平台的范围,会自动的落下到下面的绿色大平台。

wow,跟「跳跳兔」游戏有点关系了。

首先,绿色平台本身也是一个元素,所以在开始新游戏时,需要初始化好这个元素,具体逻辑写到new()方法中则可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# main.py/Game

# 开始新的游戏
def new(self):
self.all_sprites = pg.sprite.Group() # 所有元素组
self.platforms = pg.sprite.Group() # 平台元素组
self.player = Player() # 实例化玩家对象
self.all_sprites.add(self.player) # 添加到所有元素组
p1 = Platform(0, HEIGHT - 40, WIDTH, 40) # 实例化平台元素, 绿色小平台
self.all_sprites.add(p1) # 添加到所有元素组
self.platforms.add(p1) # 添加到平台元素组
p2 = Platform(WIDTH / 2 - 50, HEIGHT * 3 / 4, 100, 20) # 绿色大平台
self.all_sprites.add(p2)
self.platforms.add(p2)
self.run() # 运行

# 为了方便,展示一下Platform类对应的代码
# sprites.py

class Platform(pg.sprite.Sprite):
def __init__(self, x, y, w, h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w, h))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y

代码中有详细的注释,你可能对创建一个平台元素组的做法带有疑惑,平台对象为什么既要添加到所有元素组中又有添加到平台元素组中?

这是为了方便做碰撞检测。

通常,碰撞检测的逻辑会写到游戏中元素状态更新后,判断更新后的状态是否发生了碰撞,所谓碰撞就是不同的两个元素某部分接触到了,在pygame中使用pg.sprite.spritecollide()方法就可以判断出来了。

看一下具体的代码

1
2
3
4
5
6
7
8
9
10
11
# main.py/Game

def update(self):
self.all_sprites.update()
# 碰撞检测,检测玩家对象self.player 与 平台元素组中的所有平台对象是否发生碰撞
hits = pg.sprite.spritecollide(self.player, self.platforms, False)
if hits: # 发生碰撞
# 玩家对象的y轴位置等于碰撞对象的顶部
self.player.pos.y = hits[0].rect.top
# 玩家对象y轴速度为0,即不再移动
self.player.vel.y = 0

看到碰撞检测的相关逻辑,玩家对象要与所有的平台对象进行碰撞检测,利用平台元素组就可以轻松搞定,这也是new()中平台对象既要添加到所有元素组中又有添加到平台元素组中的原因。

如果发生了碰撞,则玩家对象的y轴位置等于碰撞对象的顶部,玩家对象y轴速度为0,即不再移动。

为了让效果正常,我们还需要修改一下玩家类的update()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# spirtes.py/Player

def update(self):
self.acc = vec(0, 0.5) # x轴没有加速度,而y轴有初始加速度,让物体可以自由落下
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.acc.x = -PLAYER_ACC
if keys[pg.K_RIGHT]:
self.acc.x = PLAYER_ACC

self.acc.x += self.vel.x * PLAYER_FRICTION
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
if self.pos.x > WIDTH:
self.pos.x = 0
if self.pos.x < 0:
self.pos.x = WIDTH
# 移动时,以元素底部为基准 - 与碰撞检测那里相互呼应
self.rect.midbottom = self.pos

通过下面的图来辅助理解。

速度分为x轴方向与y轴方向,因为y轴方向需要做碰撞检测,如果self.pos赋值给玩家元素的中心,即center,那么就会出现穿透现象。(在碰撞检测时,操作的是pos与val属性,而此时又将pos赋值给玩家元素的center,必然会出现穿透现象。)

本节给出了比较多的内容,讨论元素的控制,基本运动的实现逻辑以及碰撞检测等,至此已经构建出了「跳跳兔」最基本的原型。

如果你觉得文章还不错,点击「在看」支持一下作者。

今天一日一题不弄了,花点时间准备录「动态规划」的视频。