楔子
pyglet是一个纯python的库,因此安装它直接通过pip install pyglet
即可,很方便。当然开发游戏还有其它的库,比如pygame,但是那个相对来说会重一些,我们以后有机会再介绍。另外说一下我这里的环境,我目前使用的是python3.8.1,算是比较新的版本了,pyglet版本是1.5.0,操作系统是Windows,不过pyglet是跨平台的。
import pyglet
import sys
print(pyglet.version) # 1.5.0
print(sys.version.split(' ', 1)[0]) # 3.8.1
下面我们就来开始pyglet的愉快之旅吧。
创建一个窗口
老规矩,首先写一个游戏,那么肯定要有一个窗口吧,不然怎么显示内容呢?
import pyglet
# 调用pyglet.window.Window方法,传入宽和高,即可创建一个窗口
# 关于这个窗口,如果想象成一个坐标系的话,那么左下角就是(0, 0) 右上角就是(800, 600)
# 之所以说这个,后面会用到
game_window = pyglet.window.Window(800, 600)
if __name__ == '__main__':
# 调用pyglet.app.run()即可运行
pyglet.app.run()
"""
另外说一下,我们这里是初学pyglet
所以为了直观的显示调用的函数、类在哪个模块下,我们每一次都会从pyglet包来进行导入
当你熟悉了pyglet模块之后,在自己开发的时候,就可以通过类似于
from pyglet import window
window.Window()
这种from ... import ...的方式导入了,就不用每一次都从pyglet开始导入
"""
此刻我们就创建了一个黑乎乎的窗口,这个窗口就是通过pyglet.window.Window创建的,这是个类,这个类支持的参数如下。
width:窗口的宽度,默认是640像素,我们刚才指定的800
height:窗口的高度,默认是480像素,我们刚才指定的600
caption:窗口的标题,默认是sys.argv[0],我们看到刚才窗口的标题就是我们的文件路径
resizable:bool类型,表示窗口是否可以调整大小,就是你把鼠标放在窗口边缘,是否可以进行方向上的拉伸。默认是False,不可以。
style:用于指定窗口的边界风格,支持的选择如下:WINDOW_STYLE_DEFAULT(默认选项)、WINDOW_STYLE_DIALOG、WINDOW_STYLE_TOOL、WINDOW_STYLE_BORDERLESS,这几个都在Window这个类里面,可以通过pyglet.window.Window.WINDOW_STYLE_XXX指定
fullscreen:是否全屏,默认是False
visible:在窗口创建之后是否立刻显示,默认为True。如果你想在窗口显示之前修改某些属性,那么可以设置为False
vsync:这个参数比较难理解,如果为True,那么缓冲区翻转将会同步到主屏幕的帧回描上面,从而消除闪烁。默认是为True,我们不需要管它。
display:指定使用的显示设备,这个不需要管。
还剩下4个参数,基本上用不到,就不说了。
我们看到支持的参数虽然多,但是真正经常使用的也就是:width、height、caption、resizable
这几个参数。
import pyglet
game_window = pyglet.window.Window(
width=400,
height=300,
caption="古明地觉",
resizable=True
)
if __name__ == '__main__':
pyglet.app.run()
另外如果把鼠标放在窗口的边缘,那么还可以对窗口进行拉伸,将窗口变大或变小,这就是resizable参数发挥的作用。
给窗口添加点装饰
我们目前的窗口是黑乎乎的,什么也没有,我们是不是该给它们添加一些装饰呢?
添加文字
既然要添加,肯定要先创建一个文字,创建的方式通过pyglet.text.Label,这是一个类,支持的参数如下:
text:一个字符串,也就是你要显示的文本内容,默认是''
font_name:使用的字体,通过传递字符串指定,如果你传递了一个列表,里面指定了多个字体,那么只会使用第一个字体。默认为None
font_size:浮点型,字体的大小
bold:是否加粗,默认是False,不加粗
italic:是否为斜体,默认是False,不为斜体
color:字体的颜色,RGBA格式,三原色加上透明度。默认是(255, 255, 255, 255),即纯白色、不透明
x:文本内容的左下角的x坐标
y:文本内容的左下角的y坐标
width:显示的文本的宽度
height:显示的文本的高度
anchor_x:x坐标的锚点,参数为字符串:可以选择"left"、"center"、"right"。说说它和参数x的区别吧,anchor_x可以看成是对参数x的一个"弥补"吧,我们说参数x的坐标对应的是文本左下角的坐标,如果anchor_x指定为center,参数x指定的坐标就不再是左下角了,而是左下角所在的水平方向上的中间位置,当然指定为right,就变成右下角了。比如:我们想使文本居中,那么会把x和y的坐标改成窗口宽度和高度的一半,但是由于这两个坐标是针对左下角,所以显示出来会发现文本全部显示在窗口的右侧,这时候就可以通过将anchor_x和anchor_y全部指定为center使其居中。具体可以自己操作感受一下,如果觉得我描述的不好理解的话。
anchor_y:y坐标的锚点,参数为字符串:可以选择"bottom"、"baseline"、"center"、"top"。意义同anchor_x
align:水平方向的位置,可以为"left"、"center"、"right",只有当width参数指定了,参数align才会发挥作用
multiline:是否接受换行符,如果设置为True,那么你必须也要指定width参数,也就是宽度
import pyglet
game_window = pyglet.window.Window(
width=400,
height=300,
caption="古明地觉",
resizable=True
)
# 创建Label对象
label = pyglet.text.Label('Hello, world',
font_size=25, # 字体不指定,使用默认的,大小为25
x=game_window.width//2,
y=game_window.height//2,
anchor_x='center', anchor_y='center'
)
# 下面问题来了,我们要如何将字体显示在上面呢?
# 首先显示文本内容,可以通过label.draw()方法
# 但是我们直接写label.draw()是不行的,因为这样无法显示在窗口上面,显示不到窗口上面是无意义的
# 这里我们说一下,当pyglet创建窗口的时候,会调用窗口的on_draw方法,也就是Window这个类的on_draw方法
# 我们只有将label.draw()写到这个on_draw方法里面,才可以实现。
# 一种办法是继承Window这个类,然后重写里面的on_draw方法,用继承Window的类创建窗口,但是这个显然不科学
# 另一种就是通过反射的方式
def show_label():
# 将初始的窗口内容删除
game_window.clear()
# 添加文本,重新绘制窗口
label.draw()
# 重写on_draw方法,以后就会执行我们在show_label里面指定的代码
setattr(game_window, "on_draw", show_label)
if __name__ == '__main__':
pyglet.app.run()
这样文字就显示在上面了,但是我们是通过反射的方式,其实pyglet还提供了一种方法,通过装饰器的方式。
import pyglet
game_window = pyglet.window.Window(
width=400,
height=300,
caption="古明地觉",
resizable=True
)
label = pyglet.text.Label('Hello, world',
font_size=25,
x=game_window.width//2,
y=game_window.height//2,
anchor_x='center', anchor_y='center'
)
# 通过game_window.event进行装饰即可,但是函数名必须也要叫on_draw,我们后面都会使用这种方式
@game_window.event
def on_draw():
game_window.clear()
label.draw()
if __name__ == '__main__':
# 最后再来说一下这个run方法,我们之前简单提了一下。
"""
通过pyglet.app.run()将会进入pyglet的默认事件循环,并让pyglet响应所有的事件,比如:鼠标、键盘
将会在需要的时候被调用你的event handler
"""
pyglet.app.run()
结果是一样的,不再截图。
添加图片
我们如何添加一张图片呢?通过pyglet.resource.image,pyglet.resource会返回一个Loader对象,内部不同的方法用来加载不同的资源,pyglet.resource.image则是加载图片的,支持的参数如下:
name:文件路径
flip_x:是否水平翻转,默认为False
flip_y:是否垂直翻转,默认为False
rotate:按照逆时针的旋转角度,应该是90的整倍数
import pyglet
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
image = pyglet.resource.image("images/高木同学.png")
# 但是注意:图片的大小和窗口大小如果不一致,就会存在冲突
# 因为图片显示的方式是:假设窗口的宽为x、高为y。那么会从图片的左下角向右截取x个像素的部分、向上截取y个像素的部分,贴在窗口上面
# 如果图片比窗口小,那么肯定无法全部覆盖,窗口的右侧或者上方会存在之前的、没有覆盖到的黑色区域。
# 如果图片比窗口大,那么图片无法全部展示,只会展示图片的一部分,图片的右侧或者上方会有一部分细节展示不到
# 比如我们的窗口是800 600,但是当前图片的的大小是2400 1679
@game_window.event
def on_draw():
game_window.clear()
# 这行代码后面说
image.blit(0, 0)
if __name__ == '__main__':
pyglet.app.run()
这里图片的细节没有全部展示出来,这里是从图片左下角向右截取800像素、向上截取600像素的结果。那么如何解决这一问题呢?
import pyglet
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
image = pyglet.resource.image("images/高木同学.png")
# 是的image也有anchor_x和anchor_y,尽管参数里面没有,但是我们可以通过返回的image进行设置
image.anchor_x = 1000
image.anchor_y = 800
# 这两行代码表示什么含义呢?我们说图片是从左下角开始向右、向上截取的,直到宽为窗口的宽度、高为窗口的高度。
# 如果图片提前结束了,比如窗口高度是500,但是图片高度只有300,那么上方剩余的200就是默认的黑色区域
# 但是通过指定anchor_x=1000和anchor_y=800,那么将不再从图片的左下角进行截取了
# 而是会从向右1000个像素、向上800个像素的地方开始,再向右、向上进行截取
@game_window.event
def on_draw():
game_window.clear()
# 这行代码后面说
image.blit(0, 0)
if __name__ == '__main__':
pyglet.app.run()
但是说实话,这么做虽然可以让图片的关键部分展示出来,但这仍然不是我们期望的结果,我们还是希望图片完整的显示出来,那么最好的办法就是让图片的大小和窗口大小保持一致。
import pyglet
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
image = pyglet.resource.image("images/高木同学.png")
# 没错,image还有width和height,我们依旧可以通过返回的image进行设置
print(image.width, image.height) # 2400 1679
image.width = 800
image.height = 600
# 此时图片的大小和窗口的大小是一致的,那么从图片的左下角开始向右、向上所截取的部分,正好是图片的全部
# 因此就不需要anchor_x和anchor_y了
# 通过image.width=800和image.height=600,其实是对图片在水平和垂直方向上进行了缩放,但是原来的图片的比例显然不是800 / 600
# 但如果差别不大也无影响,但是如果窗口是800 100,那么图片肯定会变形。
# 这时候你可能要改变窗口大小了,或者换一张宽高比和窗口的宽高比更接近的图片
@game_window.event
def on_draw():
game_window.clear()
# 这个就跟label.draw()一样,肯定是要绘制在窗口上面的,blit是绘制图像
# 里面参数(0, 0)表示从图片的左下角开始绘制,是的,我们之前没有说,是因为我们这里指定了(0, 0),表示从左下角开始绘制
# 我们看到blit里面的参数其实还可以充当anchor_x和anchor_y的作用,但是一般我们都在blit里面传入0, 0
image.blit(0, 0)
if __name__ == '__main__':
pyglet.app.run()
同时添加文字和图片
import pyglet
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
label = pyglet.text.Label("高木同学",
font_size=25,
x=game_window.width//2,
y=game_window.height//2,
anchor_x='center', anchor_y='center',
color=(155, 255, 0, 255)
)
image = pyglet.resource.image("images/高木同学.png")
image.width = 800
image.height = 600
@game_window.event
def on_draw():
game_window.clear()
# 这是一个值得注意的点,绘制顺序是从上往下。
# 我们要先绘制图片,再绘制文字,让文字显示在图片上方。如果顺序反了,那么图片会把文字给挡住
image.blit(0, 0)
label.draw()
if __name__ == '__main__':
pyglet.app.run()
监控键盘和鼠标
监控键盘
监控键盘,无非是当某个键按下的时候触发相应事件,当某个键松开的时候触发某个事件。
import pyglet
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
# 这里我们只写一个字,当我们按下上下左右的时候,让这个字进行移动
label = pyglet.text.Label("木",
font_size=25,
x=game_window.width//2,
y=game_window.height//2,
anchor_x='center', anchor_y='center',
color=(155, 255, 0, 255)
)
image = pyglet.resource.image("images/高木同学.png")
image.width = 800
image.height = 600
@game_window.event
def on_draw():
game_window.clear()
image.blit(0, 0)
label.draw()
# 以上代码不变,显然我们需要把键盘注册成事件
# 函数是on_key_press,当我们按下键盘的某个键是会触发这个事件
@game_window.event
def on_key_press(symbol, modifiers):
# symbol参数指的是你按下的键,modifiers后面说
# 通过key可以模拟键盘的键,当然这个import我们应该放在上面的
from pyglet.window import key
"""
key.LEFT: ←
key.RIGHT: →
key.UP: ↑
key.DOWN: ↓
当然还有其它的键,比如回车:key.ENTER等等
"""
# 按下左键,"木"字左移10个像素,右键,右移10个像素,同理还有上键、下键等等
# 如果按下其它的键,那么退出。game_window.close()表示让窗口退出
if symbol == key.LEFT:
label.x -= 10
elif symbol == key.RIGHT:
label.x += 10
elif symbol == key.UP:
label.y += 10
elif symbol == key.DOWN:
label.y -= 10
else:
game_window.close()
if __name__ == '__main__':
pyglet.app.run()
此时我们通过上下左右键,就可以是"木"这个字进行移动,很简单。关于物体移动,并不是直接把物体移动了,而是先计算出移动之后的位置,然后重新渲染,并将物体放在新的位置上。
# 所以这段代码很关键,这个on_draw你可以理解为只有事件发生就会调用这个方法
@game_window.event
def on_draw():
# 当我们将"木"的坐标改变之后
# 将窗口清空
game_window.clear()
# 绘制图像
image.blit(0, 0)
# 绘制文字,此时再绘制就是我们移动后的位置了
# 所以移动本质上只是计算出了新的坐标,然后将"木"字展示在新的位置上
# 所以看起来就仿佛,"木"这个字在移动一样。
label.draw()
同理,on_key_press对应按下某个键,那么on_key_release则是松开某个键。
监控鼠标
监控鼠标和监控键盘类似,也有两个函数:on_mouse_press和on_mouse_release分别对应鼠标的按下和松开事件。
import pyglet
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
label = pyglet.text.Label("木",
font_size=25,
x=game_window.width//2,
y=game_window.height//2,
anchor_x='center', anchor_y='center',
color=(155, 255, 0, 255)
)
image = pyglet.resource.image("images/高木同学.png")
image.width = 800
image.height = 600
@game_window.event
def on_draw():
game_window.clear()
image.blit(0, 0)
label.draw()
@game_window.event
def on_mouse_press(x, y, symbol, modifiers):
# 参数x和y是你鼠标点击的位置的坐标
from pyglet.window import mouse
# mouse.LEFT左键,mouse.MIDDLE中间键,mouse.RIGHT右键
if symbol == mouse.LEFT:
label.x, label.y = x, y
else:
game_window.close()
if __name__ == '__main__':
pyglet.app.run()
此时我的鼠标点击在什么位置,"木"这个字就会移动到什么位置。
写个小游戏吧
对了,我想到了一个游戏。大致过程就是上面写着"你爱我吗?",然后下面写着两个选项:"是"和"否",当你点击"是",正常退出,点击"否",那么这个"否"字就跑到其它位置上。
import random
import pyglet
from pyglet.window import mouse
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
# 这里我们只写一个字,当我们按下上下左右的时候,让这个字进行移动
label = pyglet.text.Label("do you love me?",
font_size=25,
x=game_window.width // 2,
y=game_window.height // 1.2, # 我们要是文本靠近上方,那么y要适当增大
anchor_x='center', anchor_y='center',
color=(155, 255, 0, 255)
)
# 这里我们就不计算了,直接写上坐标
yes_label = pyglet.text.Label("yes", font_size=30, x=200, y=400,
anchor_x='center', anchor_y='center',
color=(155, 255, 0, 255))
no_label = pyglet.text.Label("no", font_size=30, x=600, y=400,
anchor_x='center', anchor_y='center',
color=(155, 255, 0, 255))
image = pyglet.resource.image("images/高木同学.png")
image.width = 800
image.height = 600
@game_window.event
def on_draw():
game_window.clear()
image.blit(0, 0)
# 绘制文字
label.draw()
yes_label.draw()
no_label.draw()
@game_window.event
def on_mouse_press(x, y, symbol, modifiers):
if symbol == mouse.LEFT:
"""
我们来计算鼠标是否点击在了文本上,首先对于Label对象来说,它还有一个content_width和content_height,表示这个文本整体所占的宽度和高度
而Label对象的x和y则表示这个文本的水平方向和竖直方向上的中间位置
那么我们就想到了:
对于yes_label,如果|x - yes_label.x| <= yes_label.content_width / 2并且|y - yes_label.y| <= yes_label.content_height / 2
也就是x和yes_label.x之差的绝对值小于等于文本整体宽度的一半、y和yes_label.y之差的绝对值小于等于文本整体高度的一半,那么我们就认为点击了yes
同理,对于no_label也是一样的,如果点击了,这个时候应该让no_label移动到别的位置上
但是注意不能和别的文字进行重叠
"""
if abs(x - yes_label.x) <= yes_label.content_width // 2 and abs(
y - yes_label.y) <= yes_label.content_height // 2:
game_window.close()
elif abs(x - no_label.x) <= no_label.content_width // 2 and abs(y - no_label.y) <= no_label.content_height // 2:
# 显然应该让no_label移动到其它的位置,但是文字不要越界
while True:
(pos_x, pos_y) = (
random.randint(no_label.content_width // 2, game_window.width - no_label.content_width // 2),
random.randint(no_label.content_height // 2, game_window.height - no_label.content_height // 2))
# 并且pos_x和pos_y不能和已有的文字重叠,由于都是中心位置,那么它们的距离肯定要大于各自文本的一半之和
# 为了更明显,我们再额外加上10像素
if (
abs(pos_x - yes_label.x) > yes_label.content_width // 2 + no_label.content_width // 2 + 10 and
abs(pos_y - yes_label.y) > yes_label.content_height / 2 + no_label.content_height // 2 + 10 and
abs(pos_x - label.x) > label.content_width // 2 + no_label.content_width // 2 + 10 and
abs(pos_y - label.y) > label.content_height // 2 + no_label.content_height // 2 + 10
):
# 分配成功结束循环,否则重新生成位置
no_label.x, no_label.y = pos_x, pos_y
break
if __name__ == '__main__':
pyglet.app.run()
启动之后界面如下,当我们点击no的时候,会发现no跑到了其它位置上了。
播放音乐
我们说加载资源使用pyglet.resource,加载图像:pyglet.resource.image,加载音乐:pyglet.resource.media,这里面接收两个参数:
name:文件路径
streaming:布尔类型,如果为True,那么会从磁盘一边加载一边播放,直到全部加载完。如果为False,那么会等到全部都加载到内存里面之后,再进行播放,默认是True。如果采用流式,那么对于比较长的曲目很有效,但如果是短小的声音、比如枪声、爆炸声,就不应该是用流式。应该在全部加载到内存里,并减少CPU性能损失。
import pyglet
game_window = pyglet.window.Window(
width=800,
height=600,
caption="古明地觉",
resizable=True
)
image = pyglet.resource.image("images/高木同学.png")
image.width = 800
image.height = 600
# 加载音乐,但是注意:如果你本地没有ffmpeg的话,那么音乐必须是wav格式的
# 关于如何将音频格式进行转化,可以看我的这篇博客:https://www.cnblogs.com/traditional/p/12391872.html
sound = pyglet.resource.media("sounds/Hanser,YUKIri - 遥控器(リモコン)(Cover 镜音).wav",
streaming=False)
# 播放音乐,就不需要放在on_draw里面了
# 我们需要调用pyglet.media.Player()实例化一个Player对象
play = pyglet.media.Player()
# 通过play.queue(sound)将要播放的音乐加入到队列当中
# 如果有多个音乐,那么组合成一个列表添加进去,play.queue([sound1, sound2, ...])
play.queue(sound)
# 依次播放队列里面的音乐
play.play()
@game_window.event
def on_draw():
game_window.clear()
image.blit(0, 0)
if __name__ == '__main__':
pyglet.app.run()
如果你发现音乐播放一遍之后停止了,那么你可以这么做:
sound = pyglet.resource.media(r"sounds/Hanser,YUKIri - 遥控器(リモコン)(Cover 镜音).wav")
play = pyglet.media.Player()
def repeat():
while True:
yield sound
play.queue(repeat())
# 这样就可以无限播放了
play.play()