Python pygame-ce(GUI编程)模块最完整教程(8/8)

上一篇文章:

Python pygame(GUI编程)模块最完整教程(7)_Python-ZZY的博客-CSDN博客

总目录:

pygame/README.md · Python-ZZY/CSDN-articles - Gitee.com

23 进阶声音操作

参考资料:

https://pyga.me/docs/ref/sndarray.html

https://pyga.me/docs/ref/midi.html

MIDI格式 - 简书

23.1 通过MIDI输出声音

179f9725573f8cef2344fa148c587c79.jpeg

pygame.midi模块操作MIDI(乐器数字接口)。各种电子乐器与计算机通过MIDI进行交互。管理MIDI输入,可以从一些MIDI输入设备获取信息;将MIDI输出,则可以模拟某种乐器播放音符。

和pg.camera一样,midi不会自动导入到pygame中,所以需要额外的导入和初始化。

import pygame as pg
from pygame import midi

pg.init()
midi.init()

下面的程序演示了如何通过midi播放从低到高的音阶(相邻音符之间相差一个半音)。

import pygame as pg
from pygame import midi

pg.init()
midi.init()

port = midi.get_default_output_id() # 获取默认的MIDI输出
midi_out = midi.Output(port)

# 速度范围0 - 127,速度127时音符持续时间最长
velocity = 100

# 声调最高音符名为127
max_note = 127

try:
    for note in range(0, max_note + 1):
    
        print(note)
        midi_out.note_on(note, velocity) # 按下音符

        # 音符播放是非阻塞的,不会等一个音符播完再执行下面的代码
    
        pg.time.wait(800) # 等待一段时间后再执行代码
        midi_out.note_off(note) # 释放音符

finally: # 退出midi
    del midi_out
    midi.quit()

上面的代码首先通过get_default_output_id方法获取默认的MIDI设备,然后创建了一个Output对象,用于管理MIDI的输出。然后通过Output.note_on按下音符,等待800ms后释放音符。 

最后通过finally代码块,无论是否出现异常都要退出midi。不显式退出虽然不太可能出现问题,但是官方建议最好还是加上finally来确保midi退出。

note_on方法可接受3个参数。第一个是音符编码,范围是0-127,相邻音符之间相差一个半音,数字越大音调越高,可参考MIDI音符代码表。第二个参数表示音符的响度(不知道为什么官方用速度来表示),范围同样是0-127,数字越大声音响度越大。第三个参数channel可选,表示播放的声道。与note_on相对,还有note_off方法用于释放音符,参数与之相同。

23.2 模拟乐器音色

pg.midi.Output对象还有一个方法set_instrument,可以将音符音色设置为不同乐器的音色。默认的音色是钢琴。

设置音色需要指定音色编码,所有编码可参考MIDI 128种音色码表。音色编码的范围仍为0-127。

pg.midi.Output.set_instrument(instrument_id, channel=0) -> None

下面的示例将所有音色的声音都播放了一遍。

import pygame as pg
from pygame import midi

pg.init()
midi.init()

port = midi.get_default_output_id() # 获取默认的MIDI输出
midi_out = midi.Output(port)

velocity = 100

max_instrument = 127

try:
    for instrument in range(0, max_instrument + 1):
    
        print(instrument)
        midi_out.set_instrument(instrument) # 设置音色
        midi_out.note_on(80, velocity) # 按下音符
    
        pg.time.wait(800)
        midi_out.note_off(80) # 释放音符

finally:
    del midi_out
    midi.quit()

23.3 写入原始MIDI信息数据

midi.Output.write用于写入较多的MIDI数据,来控制音符的播放。

Output.write(data) -> None

data必须是一个列表,其中包含多个小列表,每个列表都代表一个MIDI事件(为便于理解以下称作“操作”)。操作列表由信息列表、时间戳组成。信息列表一般是3个值或1个值,第一个值表示状态位,后面的值表示数据位(可选,默认为0)。例如下面这个data列表:

port = midi.get_default_output_id()
midi_out = midi.Output(port, latency=1) # 设置延迟为1

program_change = 192
note_on = 144
note_off = 128

data = [
    [[program_change, 40, 0], 0],
    [[note_on, 80, 127], 0],
    [[note_off, 80, 127], 1000],
    ]

midi_out.write(data)

这个data列表包含3个MIDI操作,改变程序(也就是乐器音色),按下音符,松开音符。时间戳表示触发这个操作的时间。比如这个data就表示:在时间为0ms的时候,改变音色,按下音调为80,强度为127的音符;在时间为1000ms时(注意:不是距离上次操作经过了1000ms),松开音调为80,强度为127的音符。

需要注意的是:使用write的时候一般要把Output类的latency参数设为一个大于0的值。latency表示延迟,即写入数据之前会有一段延迟的时间,单位是ms。如果不设置延迟,那么时间戳是无效的,所有操作都会同时进行(个人觉得这个设置不太好)。因为我不需要太多延迟,所以将它设为1即可。如果延迟设为2000,则在write中的操作处理之前会延迟2000ms。延迟时间不会算进时间戳。

正如上面所说,每个操作包含操作信息,时间戳组成,那么前面的操作信息又代表什么呢?参见这篇文章:常见MIDI信息的状态位和数据位含义表。状态位的代码决定了后面数据位的含义。比如状态位144,就表示在频道1上按下音符;后面的数据位分别表示音符代码、响度代码。状态位128,则表示在频道1上释放音符;后面的数据位分别表示音符代码、响度代码。状态位192,则表示在频道1上改变音色;后面的数据位只有一个,表示乐器音色代码,由于只需要一个数据位,所以最后的数据位留空为0即可。

此外,还需要注意data列表是有顺序的,操作必须按照时间戳从小到大排列,不然有些操作无法正确执行。例如:

data = [
    [[program_change, 70, 0], 0],
    
    [[note_on, 80, 127], 0],
    [[note_on, 100, 127], 1000],
    
    [[note_off, 80, 127], 2000],
    [[note_off, 100, 127], 2000],
    ] 
'上面是正确的排列 (0 <= 1000 <= 2000 <= 2000)'

data = [
    [[program_change, 70, 0], 0],
    
    [[note_on, 80, 127], 0],
    [[note_off, 80, 127], 2000],
    
    [[note_on, 100, 127], 1000],
    [[note_off, 100, 127], 2000],
    ] 
'上面是错误的排列,时间戳没有从小到大排列'
'(0 <= 2000 !< 1000 <= 2000)'
'如果使用这种方法排列将执行不到[[note_on, 100, 127], 1000]这一段'

按照上面正确的代码播放,将达到这样的效果:首先改变音色,播放80音符;过1秒后播放100音符;再过1秒后同时结束两个音符的播放。 

23.4 midi模块索引 - 乐器数字接口

事件:midi在输入和输出时会分别产生MIDIIN和MIDIOUT两个事件。

init() -> None, quit() -> None

初始化/退出midi。

Input(device_id) -> None

Input(device_id, buffer_size) -> None

创建一个midi输入对象,device_id是输入设备的标识符,buffer_id是可缓冲的输入事件数量。

Input.close() -> None

尝试关闭midi输入(在Windows系统上可能出现问题)。

Input.poll() -> bool

判断是否有midi的输入数据

Input.read(num_events) -> midi_event_list

读取并返回未被处理的midi输入事件,num_events代表事件的数量。

Output(device_id) -> None

Output(device_id, latency=0) -> None

Output(device_id, buffer_size=256) -> None

Output(device_id, latency, buffer_size) -> None

创建midi输出对象。buffer_size是输出事件的缓冲数量,latency是输出的延迟时间。

Output.abort() -> None

中止midi传输(可能导致数据还没发完时就终止了)

Output.close() -> None

关闭midi输出对象(Windows上可能有问题)

Output.note_off(note, velocity=None, channel=0) -> None

松开音符,note是音符编码,velocity是速度,channel是频道。

Output.note_on(note, velocity, channel=0) -> None

按下音符,note是音符编码,velocity是速度,channel是频道。

Output.set_instrument(instrument_id, channel=0) -> None

设置乐器音色

Output.pitch_bend(value=0, channel=0) -> None

微调midi输出时的音高。value为正数表示升高,负数表示降低,范围是-8192到+8191,每4096代表一个半音。例如:-8192表示降低两个半音(即一个全音),4096表示升高一个半音。

Output.write(data) -> None

写入多个MIDI操作

Output.write_short(status) -> None

Output.write_short(status, data1=0, data2=0) -> None

写入单个的MIDI操作,status是状态位,data1, data2分别是两个数据位。不考虑时间戳。

Output.write_sys_ex(when, msg) -> None

写入全是系统信息的MIDI操作。

midi_output.write_sys_ex(0, '\xF0\x7D\x10\x11\x12\x13\xF7')

# 相当于:

midi_output.write_sys_ex(pg.midi.time(), # midi计时器的时间
                         [0xF0, 0x7D, 0x10, 0x11, 0x12, 0x13, 0xF7])

get_default_input_id() -> default_id

获取默认的MIDI输入设备

get_default_output_id() -> default_id

获取默认的MIDI输出设备

get_device_info(an_id) -> (interf, name, input, output, opened)

get_device_info(an_id) -> None

获取给定设备ID的信息。返回值一般是元组,ID超出范围会返回None。元组中各个值的含义:interf表示描述设备接口的字符串(例如"ALSA"),name表示设备名称(例如"Midi Through Port-0"),input表示该设备是否为输入设备(1或0),output表示该设备是否为输出设备(1或0),opened表示该设备是否为开启状态。

midis2events(midi_events, device_id) -> [Event, ...]

将MIDI事件转换为pygame事件。midi_events也就是传递给Output.write的data参数中的每个操作列表。

time() -> time

返回midi计时器得到的时间(从midi.init开始算)

frequency_to_midi(midi_note) -> midi_note

将频率转换为MIDI音符代码(会圆整到最接近的那个音符)

midi_to_ansi_note(midi_note) -> ansi_note

将MIDI音符代码转换为ansi音符格式的字符串

MidiException(errno) -> None

MIDI异常。当MIDI设备找不到,或者其他原因时可能报错。

23.5 sndarray模块索引 - Sound与numpy数组

pg.sndarray用于pg.mixer.Sound和numpy数组之间的转换,和pg.surfarray一样依赖于numpy库。

声音数据是由每秒数千个样本组成的,每个样本都是在特定时刻的波的振幅。例如,在22khz格式中,阵列的单元号5是5/22000秒后的波的振幅。

array(Sound) -> array

将声音对象复制并转换为numpy数组,更改数组不会改变声音对象本身。

samples(Sound) -> array

将声音对象之间转换为numpy数组,更改数组将改变声音对象本身。

make_sound(array) -> Sound

将声音样本数组转换为Sound对象。

24 pygame探索

参考资料:pygame — pygame-ce v2.4.0 documentation

24.1 pygame主模块索引

pygame主模块里面并没有什么特别重要的内容,因为学习pygame的时候,读者已经了解了主模块中常用的函数,接下来将整理一些函数的用法。

以下是pygame主模块中唯一可能用到的4个东西(剩下的都没什么用)。

init() -> (numpass, numfail)

初始化pygame所有子模块,并返回初始化成功的数量和失败的数量。

其实在pygame的每个子模块中都有init和quit方法,用于初始化和退出单个的子模块。pygame功能很多,有时候并不用于单纯的编写游戏,有时候也会用于播放音乐等等。如果只需要使用pygame.mixer模块,就无需初始化整个的pygame,只需要调用pygame.mixer.init()而非pygame.init()。

quit() -> None

退出pygame,与init方法相反。这个方法会关闭pygame窗口。调用pg.quit方法并不会退出整个python程序,只会退出pygame。有时候有必要在退出时顺便调用sys.exit来确保python程序完全退出。

register_quit(callable) -> None

注册一个函数。当调用pygame.quit时,会调用它。

error

pygame.error继承于Exception,表示一些pygame相关的异常。可用于异常捕获。

24.2 环境变量

部分pygame的行为可以通过环境变量来控制,即操作os.environ。例如:

import os
os.environ["环境变量名"] = "环境变量值"

下面是一些可能用到的pygame相关环境变量及用法。

PM_RECOMMENDED_INPUT_DEVICE

设置默认的MIDI输入设备

PM_RECOMMENDED_OUTPUT_DEVICE

设置默认的MIDI输出设备

PYGAME_DISPLAY

pygame的显示索引(即pg.display.set_mode的display参数),默认为"0"

PYGAME_FORCE_SCALE

强制set_mode使用缩放显示模式,是"default"或"photo"。如果设置了“photo”,则缩放使用最慢但质量最高的各向异性缩放算法(如果可用)。必须在调用pygame.display.set_mode()之前设置

PYGAME_BLEND_ALPHA_SDL2

这使得pygame对所有alpha混合使用SDL2 blitter。SDL2绘制器有时比默认的要快,但使用不同的公式,因此最终颜色可能不同。必须在调用pygame.init()初始化所有导入的pygame模块之前设置。

PYGAME_HIDE_SUPPORT_PROMPT

是否隐藏pygame启动时打印的那一段提示,设置为"1"表示隐藏。

PYGAME_CAMERA

设置摄像机的后端,如"opencv"。必须在pg.camera初始化之前调用。

SDL_IME_SHOW_UI

是否显示输入候选框。必须在pg.display.set_mode前面调用

SDL_VIDEO_CENTERED

是否将窗口设在屏幕中央。必须在pg.display.set_mode前面调用

SDL_VIDEO_WINDOW_POS

设置pygame窗口的位置(左上角),格式为"x, y"。必须在pg.display.set_mode前面调用

SDL_VIDEO_ALLOW_SCREENSAVER

默认情况下,pygame运行时会禁用屏保,如果设为"1"则表示允许显示屏幕保护系统。

SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS

默认情况下,当窗口不在焦点中时,游戏手柄这样的设备不会更新。然而,使用这个环境变量,即使窗口在后台,也有可能获得操纵杆更新。必须在调用pygame.init()之前设置。

24.3 _sdl2

pygame._sdl2模块中包含了一些实验性的SDL2功能,可能在未来有更改。_sdl2中还包含一些子模块。_sdl2不会被自动导入到pygame里面。

_sdl2包含以下几个模块:

模块名描述
controller操作控制器(如游戏手柄)
touch触屏输入
windowpygame多窗口创建(在pygame-ce 2.5.2已经变成了一个单独的模块pygame.Window)
video包括图像、纹理等

24.4 其他模块

typing

pygame-ce 2.5.2新增了pygame.typing模块,用于类型提示,提高代码的可读性。

  • pygame.typing.FileLike:文件类型格式
  • pygame.typing.SequenceLike:序列类型格式(列表、字典、字符串等)
  • pygame.typing.Point:表示一个点的坐标格式
  • pygame.typing.IntPoint:表示一个点的坐标格式,但是是严格的整数
  • pygame.typing.ColorLike:表示颜色格式
  • pygame.typing.RectLike:表示矩形格式

例如:

import pygame as pg

def blit_image_to_screen(surface: pg.Surface, pos: pg.typing.IntPoint):
    ...

这样可以使代码更加可读,对于没有此类习惯的编程者可能没什么用。

geometry

pygame.geometry — pygame-ce v2.5.3 documentation

实验性模块,用于支持一些其他的形状(圆形、线条) ,是对Rect的扩充。

25 性能提升

25.1 GPU

GPU即图形处理器,替代了部分CPU的工作,在游戏中使用GPU可以极大提升绘制性能(尤其是3D游戏)。GPU在处理缩放、旋转等功能时速度尤其快。

查看一个程序是否使用GPU,可通过任务管理器(Windows)。

6852a0910f27a4991b6cdb4cc22006f5.png

可以在右侧找到该程序的GPU使用情况。如上方的Python使用了GPU。

25.2 SCALED标志

要想在pygame中启用GPU,最快捷、无需大改的方法是在pg.display.set_mode中加上pg.SCALED标记。pg.SCALED原本用于适应分辨率,但是由于在缩放的过程中调用了GPU,所以可以加上这个标志。

screen = pg.display.set_mode((500, 400), flags=pg.SCALED)

注意:多个标记应使用按位或操作符"|" 进行连接。

例如:

screen = pg.display.set_mode((500, 400), flags=pg.SCALED | pg.RESIZABLE)

25.3 _sdl2.video

_sdl2.video中提供了一些方法直接使用GPU纹理。这种方式很麻烦,并且难以兼容不使用GPU的代码,甚至难以使用blit这种基本的方法。此外,_sdl2是实验性的模块,尤其是video模块,官方明确指出在未来会有大的改动(可能是pygame3往后的版本)。

以下是一段示例,来源于pygame.examples:

#!/usr/bin/env python
""" pygame.examples.sprite_texture

Experimental! Uses APIs which may disappear in the next release (_sdl2 is private).


Hardware accelerated Image objects with pygame.sprite.

_sdl2.video.Image is a backwards compatible way with to use Texture with
pygame.sprite groups.
"""
import os
import pygame


from pygame._sdl2 import Window, Texture, Image, Renderer


data_dir = os.path.join(os.path.split(os.path.abspath(__file__))[0], "data")


def load_img(file):
    return pygame.image.load(os.path.join(data_dir, file))


pygame.display.init()
pygame.key.set_repeat(10, 10)

win = Window("asdf", resizable=True)
renderer = Renderer(win)
tex = Texture.from_surface(renderer, load_img("alien1.gif"))


class Something(pygame.sprite.Sprite):
    def __init__(self, img):
        pygame.sprite.Sprite.__init__(self)

        self.rect = img.get_rect()
        self.image = img

        self.rect.w *= 5
        self.rect.h *= 5

        img.origin = self.rect.w / 2, self.rect.h / 2


sprite = Something(Image(tex, (0, 0, tex.width / 2, tex.height / 2)))
sprite.rect.x = 250
sprite.rect.y = 50

# sprite2 = Something(Image(sprite.image))
sprite2 = Something(Image(tex))
sprite2.rect.x = 250
sprite2.rect.y = 250
sprite2.rect.w /= 2
sprite2.rect.h /= 2

group = pygame.sprite.Group()
group.add(sprite2)
group.add(sprite)

import math

t = 0
running = True
clock = pygame.Clock()
renderer.draw_color = (255, 0, 0, 255)

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            elif event.key == pygame.K_LEFT:
                sprite.rect.x -= 5
            elif event.key == pygame.K_RIGHT:
                sprite.rect.x += 5
            elif event.key == pygame.K_DOWN:
                sprite.rect.y += 5
            elif event.key == pygame.K_UP:
                sprite.rect.y -= 5

    renderer.clear()
    t += 1

    img = sprite.image
    img.angle += 1
    img.flip_x = t % 50 < 25
    img.flip_y = t % 100 < 50
    img.color[0] = int(255.0 * (0.5 + math.sin(0.5 * t + 10.0) / 2.0))
    img.alpha = int(255.0 * (0.5 + math.sin(0.1 * t) / 2.0))
    # img.draw(dstrect=(x, y, 5 * img.srcrect['w'], 5 * img.srcrect['h']))

    group.draw(renderer)

    renderer.present()

    clock.tick(60)
    win.title = str(f"FPS: {clock.get_fps()}")

pygame.quit()

26 打包pygame

26.1 用pyinstaller打包pygame

Python pyinstaller打包exe最完整教程_Python-ZZY的博客-CSDN博客

26.2 在网页上运行pygame

python pygbag教程 —— 在网页上运行pygame程序(全网中文教程首发)_Python-ZZY的博客-CSDN博客

26.3 在Andriod上运行pygame

实战用Python+Pygame+Kivy(Buildozer)+Ubuntu开发安卓android手机端apk游戏及踩坑分享_pygame打包成apk-CSDN博客

同时给出一篇官方的教程以及机器翻译: pygame/android.txt · Python-ZZY/CSDN-articles - Gitee.com

26.4 在IOS上运行pygame(探索中)

pygame暂不支持在IOS上运行,但是基于pygame_sdl2(和pygame有区别)的renpy视觉小说引擎可以在IOS上运行。

27 关于pygame使用的一些总结

以下是作者使用pygame的一些总结。

27.1 FPS相关注意点

FPS的设定

FPS设定不宜过小(一般不能小于30),也不宜过大(尽量不要超过120以上)。过小的FPS值会导致动画画面不连续,画面看起来有些卡顿。过大的FPS值超出了计算机的计算能力,即使设置的很大,实际FPS也不会达到那么大,没有意义,反而会导致FPS不稳定。最佳的FPS设定一般在30-60左右(作者习惯用FPS=60)。

实际FPS不稳定

正常情况下,实际FPS值大小应该与设定FPS值差不多,相差不会超过2。

如果出现FPS在设定的FPS和一个比较小的数值之间摆动,实际FPS变化很不稳定,就会显得游戏画面很卡顿。如果实际FPS偏小,而设定的FPS并不是很大,这种情况一般是由于屏幕上单次渲染的东西太多,或者渲染太大的表面,CPU的计算能力达不到导致的。

缓解此问题有一个比较简便的方式,在通过set_mode设置窗口的时候添加标记pg.SCALED。这个参数会调用GPU,缓解CPU的压力。 

screen = pg.display.set_mode((800, 600), pg.SCALED)

但是,更重要的是对代码进行优化。主要注意下面几点:

绘制一张非常大的图片(比如游戏地图)时,不应该直接绘制一个地图整体。应先手动把它分割成几个小块图片,然后分别绘制。

载入不透明的背景图片时(一般是一个与屏幕大小差不多的图片),应通过Surface.convert()方法进行转换。有些图像的位深度不当,convert()方法会进行转换,缓解渲染的压力。

bg_image = pg.image.load("bg_image.png").convert()

如果出现实际FPS低于设定的FPS,但是数值比较稳定,例如设定FPS=60,但实际FPS在40上下波动,可能是载入背景图片时未进行convert()转换。此外,还需要注意:convert()会消除一张图片的透明通道,一般只在载入无需透明图片的时候有用。

如果FPS数值不断减小,那极有可能是程序中的一些数据在不断变多,可能是一些没用的程序数据没有被正确删除。例如飞机大战游戏中,飞机不断射出的子弹应在飞出屏幕后就把它删除,否则就会越来越多,拖慢程序。

27.2 物理引擎

通过一种叫做“物理引擎”的东西,我们可以在程序中模拟现实生活中的物体行为。例如一个球下落后的轨迹,两个物体相撞后会发生怎样的变化,这些可以用物理力学知识解释的现象,大都可以通过物理引擎进行模拟。

推荐的物理引擎:pymunk。教程如下:

Python 物理引擎pymunk最完整教程(上)

附录 easy_pygame

pygame过于简单,还称不上“游戏引擎”,但是可以对此进行加工。

作者自制的easy_pygame引擎包含一些特色功能:

  • 静态和动态的多状态精灵
  • 动作系统(比如:淡入淡出、移动、缩放、旋转、振动)
  • 图文混排渲染
  • 场景类(使用状态机控制);可支持动作系统的场景类
  • 完美碰撞检测函数
  • 存档功能
  • 多背景音乐管理
  • 相对位置转换成绝对位置后再获取文件路径,同时支持pyinstaller的资源内置
  • 封装了缓存算法的图片载入
  • 帧序列处理(包括单张图片上多帧、多图片)
  • pg.time.get_ticks()的时间计算问题优化
  • 一些基本的UI组件模型(只是基本模型,不提供任何渲染和事件处理),组件的排放引入了tkinter的pack, grid, place布局方式
  • 如有必要,后续推出以及一些游戏拓展库和一些便捷打包工具

该游戏引擎代码已发布至:easy_pygame: A game engine based on pygame

但是教程和文档还没有完成。

作者使用pygame(包括epg)制作的一些游戏已发布至Python-ZZY - itch.io;作者在资源页面发布的一些游戏是个人初学pygame时所做,可供新手参考。

关于easy_pygame,以及本教程需要完善的内容欢迎向作者提出。全教程完。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值