【编程】零基础Pygame小游戏开发-07

欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】


素材文件,百度网盘下载链接 密码:y1cp

关于文件路径

计算机中的文件路径分为相对路径和绝对路径。比如说在你的桌面上有个项目文件夹dadishu,内部包含了一个imgs文件夹和一个main.py文件,文件夹内有张bg.png图片。

那么这张图片相对于main.py的相对路径就是./imgs/bg.png,而它实际的绝对路径大概是C:/Users/username/desktop/dadishu/imgs/bg.png

绝对路径不仅用起来很烦人,而且如果你把它写入了代码,那么以后整个项目文件夹移动到D盘之后代码就会出错,因为它还傻傻的认为自己在C盘,还回去C盘找文件。

下面这个函数可以非常好的解决这个问题,自动的把相对路径转换为绝对路径。你可以在单独的新文件中测试它。

import sys,os

def ipath(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    p = os.path.join(base_path, relative_path)
    return p

print(ipath('bg.mp3'))

这个ipath函数将输出一个完整的绝对路径。这在MacOS下将项目打包成App的时候非常关键,否则将导致运行错误。因为在运行代码的时候,绝对路径往往比相对路径更加可靠。

函数

我们经常需要把共同完成一个任务的很多行代码打包放在一起,称之为函数,然后在需要的地方整体使用这个函数,这不仅可以避免多余的重复代码,而且可以让整个代码逻辑看起来更清晰。
下面设定了一个生成随机位置的函数,它用来更新pos位置。

def randPos():  # 重置位置和时间
    global ratLoopStart, pos, posAll, freeze, times
    freeze = 0  # 解除冻结
    times = times + 1  # 次数增加
    ratLoopStart = time.time()  # 重置循环时间
    pos = posAll[random.randint(0, 4)]  # 更新位置

在这里的global ratLoopStart...表示在这个函数里面我们需要使用到的外部的变量数据,尽管这种使用外部数据的方法并不值得推荐,但对于我们初学小游戏来说也是可以的。

有了这个代码,我们随时执行randPos()就能改变地鼠的位置。

时间

我们之前使用time.sleep(0.04)这种阻碍计算机运行的方法来控制时间,但是它的问题很多。

首先,这个非常不精确,虽然每次都睡0.04秒,但是此外还有其他代码的运行耗时,所以实际上并不是我们期望的每隔0.04秒就刷新画面,肯定会多一些,但多多少呢?不知道,这也导致了我们游戏结束画面的倒计时完全不靠谱,无法按照真实的秒数计算。

其次,这个方法是浪费计算机速度的,本来它可以运行很多其他的事情,但我们却让他睡觉了,在这0.04秒中我们什么都不能做。

改进的策略就是去掉sleep,让计算机尽情的反复运行代码,刷新画面,有多快就刷多快吧,每秒钟100张画面(100帧)都没关系,刷新越快我们的游戏玩起来越流畅。

那怎么计时?time.time()可以获取到当前的时间,它返回1575167263.3102772这样的大数字,它表示从1970年到现在一共经过了多少秒,从小数部分.310...我们知道它实际比毫秒还精确很多,这足够我们用了。

当一局游戏结束的时候,我们记录下当前的gameOverStart=time.time(),就像我们在比赛开始按下秒表的按键。当然这个时候画面还是一直在被我们while 1不断的刷新,我们只要每次刷新的时候检测一下overTime=time.time()-gameOverStart的数值,结果overTime就是我们距离游戏过去了多少秒。然后我们在屏幕上显示int(10-overTime)这个数字就是倒计时,当倒计时结束也就是overTime>10的时候我们就进入下一局。

整体改进

首先我们梳理一下文件,把声音、图片和字体分别放到imgs、sounds、fonts三个文件夹中,看起来如下:

我们使用函数,并对时间计时方法进行优化,得到下面的完整代码,你可能需要认真的完整阅读并实验它,你可以下载顶部网盘中【打地鼠v12/v12】项目来测试:

# 改进计时系统的版本
import pygame
import sys, os, time, random
from pygame.locals import *  # 引入鼠标事件类型


def ipath(relative_path):  # 相对路径转绝对路径
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    p = os.path.join(base_path, relative_path)
    return p


pygame.init()  # 初始化
window = pygame.display.set_mode([600, 400])  # 设定窗口

sur = pygame.Surface([600, 400])  # 绘制背景容器
posAll = [[100, 150], [300, 150], [500, 150], [200, 300], [400, 300]]  # 六个位置
radius = 50  # 地鼠大小半径
tick = 0  # 计数器
pos = posAll[0]  # 外面记录圆的位置

score = 0  # 分数计数
pygame.font.init()  # 初始化文字
score_font = pygame.font.Font(ipath("fonts/MicrosoftYaqiHeiLight-2.ttf"), 30)  # 设定字体和字号
score_sur = score_font.render(str(score), False, (255, 0, 0))  # 生成计数表面

pygame.mouse.set_visible(False)  # 隐藏鼠标
mpos = [300, 200]  # 记录鼠标位置

times = 0  # 地鼠跳出的次数
times_max = 10  # 最多次数
ditu = pygame.image.load(ipath("imgs/dds-map.jpg"))  # 读取图片
rat1 = pygame.image.load(ipath("imgs/rat1.png"))  # 读取地鼠图片
rat2 = pygame.image.load(ipath("imgs/rat2.png"))  # 读取被砸地鼠图片
ham1 = pygame.image.load(ipath("imgs/hammer1.png"))  # 读取锤子图片
ham2 = pygame.image.load(ipath("imgs/hammer2.png"))  # 读取砸下锤子图片

pygame.mixer.music.load(ipath("sounds/bg.mp3"))  # 载入背景音乐
pygame.mixer.music.play(-1)  # 无限播放背景音乐
hitsound = pygame.mixer.Sound(ipath("sounds/hit.wav"))  # 载入击打声音
hurtsound = pygame.mixer.Sound(ipath("sounds/aiyo2.wav"))  # 载入地鼠叫声

ratLoopStart = time.time()  # 每次地鼠跳出循环的开始时间点
ratLoopMax = 3.0  # 地鼠循环每次出现最大时间长度,秒单位
ratLoopHide = 0.2  # 循环开始不显示地鼠时间,秒单位

hamsur = ham1  # 锤子图片表面
ratsur = rat1  # 地鼠图片表面
hitLoopStart = time.time()  # 锤击循环时间开始点
hitLoopMax = 0.1  # 锤击动画显示时间长度

freeze = 0  # 冻结状态,动画不变不得分。被击中后冻结一个极短时间。
gameOver = 0  # 游戏是否结束
gameOverStart = time.time()  # 结束画面开始时间
gameOverMax = 3  # 结束画面最长时间,单位秒


def ratLoopRest(score_add=0):  # 重置地鼠跳出循环,默认不加分score_add=0
    global score, ratsur, hurtsound, freeze, ratLoopStart
    if not freeze:  # 非冻结状态
        freeze = 1  # 冻结
        score = score + score_add  # 成绩增加
        if score_add > 0:  # 如果被击中
            ratsur = rat2  # 使用被击中的地鼠
            hurtsound.play()  # 被击中尖角
            # 将开始时间设置到ratLoopMax之前,等同立即结束当前循环
            ratLoopStart = time.time() - ratLoopMax


def randPos():  # 重置位置和时间
    global ratLoopStart, pos, posAll, freeze, times
    freeze = 0  # 解除冻结
    times = times + 1  # 次数增加
    ratLoopStart = time.time()  # 重置循环时间
    pos = posAll[random.randint(0, 4)]  # 更新位置


while 1:
    ts = time.time()  # 每次刷新的当前时间

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        elif event.type == MOUSEBUTTONDOWN and not gameOver and not freeze:  # 如果是鼠标按下事件
            hitLoopStart = time.time()  # 重置锤击循环
            hamsur = ham2  # 使用下落锤子
            hitsound.play()  # 播放击打声音
            mpos = pygame.mouse.get_pos()  # 获取鼠标位置
            dis = pygame.math.Vector2(mpos[0] - pos[0], mpos[1] - pos[1])  # 计算坐标差
            len = pygame.math.Vector2.length(dis)  # 计算距离
            if len < radius:
                ratLoopRest(1)
        elif event.type == MOUSEMOTION:  # 当鼠标移动的时候
            mpos = pygame.mouse.get_pos()  # 更新鼠标位置

    if times >= times_max:  # 地鼠跳出次数超过限定,显示结束画面
        if not gameOver:  # 如果不处于结束画面
            gameOverStart = time.time()  # 重置成绩画面计时
            gameOver = 1  # 进入结束画面状态

        sur.fill((0, 0, 0))  # 结束时候仍然用黑色清空画面
        pygame.mouse.set_visible(True)
        end_font = pygame.font.Font(
            ipath("fonts/MicrosoftYaqiHeiLight-2.ttf"), 48
        )  # 设定字体和字号
        end_sur = score_font.render(
            "你的分数是:{}/{}!".format(score, times_max), True, (255, 0, 0)
        )  # 生成计数表面
        sur.blit(end_sur, (100, 150))  # 显示结束画面

        cd = int(ts - gameOverStart)
        cd_sur = score_font.render(
            "重新开始倒计时{}".format(gameOverMax - cd), True, (255, 0, 0)
        )  # 生成计数表面
        sur.blit(cd_sur, (100, 200))  # 增加分数表面

        if cd > gameOverMax:  # 结束画面超过设定时间,重置游戏
            pygame.mouse.set_visible(False)
            times = -1
            score = 0
            gameOver = 0  # 再次进入游戏状态

    else:
        sur.blit(ditu, (0, 0))  # 添加背景图片
        score_sur = score_font.render(
            "分数:{}/{}!".format(score, times + 1), False, (255, 0, 0)
        )  # 重新生成分数文字表面
        sur.blit(score_sur, (200, 10))  # 增加分数表面

        if ts - ratLoopStart > ratLoopMax:  # 超过地鼠跳出循环时间
            ratLoopRest(0)

        if ts - ratLoopStart - hitLoopMax > ratLoopMax:  # 超过地鼠循环时间+点击时间
            randPos()

        if ts - hitLoopStart > hitLoopMax:  # 超过锤击动画循环
            hamsur = ham1  # 使用举起的锤头
            ratsur = rat1  # 使用正常地鼠
        else:
            hamsur = ham2  # 落下的锤头
            ratsur = rat2  # 使用击中地鼠

        if ts - ratLoopStart > ratLoopHide:  # 开始瞬间不显示地鼠
            sur.blit(ratsur, (pos[0] - 50, pos[1] - 70))  # 绘制地鼠

        sur.blit(hamsur, (mpos[0] - 50, mpos[1] - 100))  # 绘制锤头

    # 刷新画面
    window.blit(sur, (0, 0))
    pygame.display.flip()  # 刷新画面

有几个地方需要注意:

  • ratLoop是指地鼠出现到消失的循环,在这个循环的开始ratLoopHide =0.2秒地鼠不出现,也就是每次地鼠消失到出现中间有间隔。
  • 地鼠被击中后会进入hitLoop锤击循环时间,这个循环的目的是保持锤子落下和地鼠挨打动画一个很小的时间hitLoopMax = 0.1秒。
  • 因为地鼠被击中会有一个短暂的0.1秒停留,所以为了避免用户在这个时间重复击打地鼠得分,我们设置了freeze冻结状态,只有当freeze=0解冻的时候击打才可能得分,...and not freeze
  • 同样的gameOver状态用来表示进入结束画面,并有自己的计时系统gameOverStart, gameOverMax
  • ratLoopRest(1)被击中后立即重置并加分,ratLoopRest函数中freeze = 1立即冻结,避免重复击打加分。
  • 最后一大段各种if判断比较复杂,要仔细认真思考。

这次改进之后,游戏性能会有明显提升,原来的卡顿感没有了,而且计时系统也基本准确了。

但是现在每一局的难度是一样的,如何让游戏越来挑战越大?后面我们继续改进。

【编程】零基础Pygame小游戏开发(索引)

<未完待续>


欢迎关注我的专栏( つ•̀ω•́)つ【人工智能通识】


每个人的智能新时代

如果您发现文章错误,请不吝留言指正;
如果您觉得有用,请点喜欢;
如果您觉得很有用,欢迎转载~


END