mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-10-22 18:03:45 +08:00
mod
This commit is contained in:
212
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第10讲 | 如何载入“飞机”和“敌人”?.md
Normal file
212
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第10讲 | 如何载入“飞机”和“敌人”?.md
Normal file
@@ -0,0 +1,212 @@
|
||||
<audio id="audio" title="第10讲 | 如何载入“飞机”和“敌人”?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a8/39/a8939945c3db1a6c8dbcf40f3376d739.mp3"></audio>
|
||||
|
||||
上周,我向你解释了如何载入背景图片,以及如何使用坐标值的变换来移动背景图片。今天,我们要载入主角“飞机”和一些“敌人”。
|
||||
|
||||
## 导入随机函数
|
||||
|
||||
我们选择Python做为开发语言。在开始之前,需要用到一个随机函数,这个随机函数会在之后的代码中用到,具体用作什么,我会在后面揭晓。
|
||||
|
||||
首先,需要将随机函数导入Python的随机函数库random。代码可以这么写:
|
||||
|
||||
```
|
||||
import random
|
||||
|
||||
```
|
||||
|
||||
然后,我们需要限定一个范围,来生成随机数,比如10到100的范围,代码可以这么写:
|
||||
|
||||
```
|
||||
random.randrange(10, 100)
|
||||
|
||||
```
|
||||
|
||||
这个函数会接受三个参数:开始、结束、递增数字。
|
||||
|
||||
<li>
|
||||
开始:开始随机的指定范围数值,包含在范围内。比如(10,100),就包含10。
|
||||
</li>
|
||||
<li>
|
||||
结束:开始随机的指定范围数值,不包含在范围内。比如(10,100),不包含100,最多到99。
|
||||
</li>
|
||||
<li>
|
||||
递增:指定递增数字。
|
||||
</li>
|
||||
|
||||
如果不填写递增值,则按照开始、结束的值随机分配。比如 (10,100) ,那就会在10~99之间随机分配任何一个数字。
|
||||
|
||||
## 载入主角飞机
|
||||
|
||||
我们说完了随机函数的准备工作,就可以开始载入飞机了。
|
||||
|
||||
我们假设主角的飞机是从下往上飞,那它的飞机头应该是朝着电脑屏幕上方,而敌人的飞机是从上往下出现,所以它的飞机头应该朝着电脑屏幕的下方。主角的飞机暂时固定在屏幕下方,敌人的飞机则一直不停从上往下飞。
|
||||
|
||||
飞机的图片是我从共享的图片网站上抓取下来,让美术帮我处理和加工了一下。其实就是将飞机从一整块背景图片上抠除下来,让飞机看起来拥有飞机本身的轮廓,而不是一幅“方块”的飞机图片,然后将其图片保存成png的格式。
|
||||
|
||||
我们来看这里的代码。和载入背景一样,我们需要先定义主角飞机的图片名和敌人飞机的图片名。
|
||||
|
||||
```
|
||||
plane = 'plane.png'
|
||||
enemy = 'enemy.png'
|
||||
|
||||
```
|
||||
|
||||
使用png格式的原因是,png格式包含alpha通道。我们可以将图片抠成透明图,这样将图片贴在背景上面就看不到任何黑色块。
|
||||
|
||||
我们先尝试贴一下主角的飞机。
|
||||
|
||||
```
|
||||
pln = pygame.image.load(plane).convert_alpha()
|
||||
screen.blit(pln, (40, 350))
|
||||
pygame.display.update()
|
||||
|
||||
```
|
||||
|
||||
我们定义一个叫pln的变量,载入plane图片,并且将alpha通道进行处理,然后在屏幕中绘制pln,最后我们使用update函数更新屏幕。
|
||||
|
||||
我们来看一下贴图的效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/ca/3b13e0a3ff4b7006ee4a1ddbaf8309ca.jpg" alt="">
|
||||
|
||||
我们已经将这幅图片贴了上去。
|
||||
|
||||
在载入的过程中,如果我不使用convert_alpha函数会怎样呢?我们也来做一下实验。
|
||||
|
||||
```
|
||||
pln = pygame.image.load(plane).convert()
|
||||
screen.blit(pln, (40, 350))
|
||||
pygame.display.update()
|
||||
|
||||
|
||||
```
|
||||
|
||||
我将 convert_alpha 改成了convert,来看一下效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/57/4ba2eb42b15c44056099e53489104d57.jpg" alt="">
|
||||
|
||||
看到了那个大大的黑色色块没有?这就是我们没有处理alpha通道导致的结果,导致了一个大大的抠图色块出现在屏幕,所以要记住:
|
||||
|
||||
<li>
|
||||
设计主角图的时候,要将图片抠下来;
|
||||
</li>
|
||||
<li>
|
||||
在贴图的时候,需要进行alpha混合的处理,否则贴上去的图会存在抠图黑块。
|
||||
</li>
|
||||
|
||||
## 载入敌人飞机
|
||||
|
||||
接下来,我们要从屏幕上方,贴一架敌人的飞机。
|
||||
|
||||
```
|
||||
enm = pygame.image.load(enemy).convert_alpha()
|
||||
screen.blit(enm, (30, 10))
|
||||
pygame.display.update()
|
||||
|
||||
```
|
||||
|
||||
我们将两架飞机前后代码整合起来,再来看一下效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/31/ee3059e4cb999d4a5f532c634e88d331.jpg" alt="">
|
||||
|
||||
这样,我们将两架飞机都贴在了屏幕上了。看起来是不是有点像样了呢?
|
||||
|
||||
敌方肯定不止一个飞机,那我们就需要贴更多的敌方飞机。这里我们就需要用到最开始提到的随机函数了。为什么使用随机函数呢?因为我们需要让敌方飞机的排列看起来很随机(笑)。
|
||||
|
||||
我们现在要加载相同的敌方飞机图片,加载三次。也就是说,我们会在屏幕上方的一个固定区域范围贴上三次敌人的飞机。我们需要准备三个随机 (x,y) 位置的数字,并且赋值给 blit 函数。
|
||||
|
||||
```
|
||||
ex1 = random.randrange(20, 600)
|
||||
ey1 = random.randrange(10, 50)
|
||||
ex2 = random.randrange(20, 600)
|
||||
ey2 = random.randrange(10, 50)
|
||||
ex3 = random.randrange(20, 600)
|
||||
ey3 = random.randrange(10, 50)
|
||||
screen.blit(enm, (ex1, ey1))
|
||||
screen.blit(enm, (ex2, ey2))
|
||||
screen.blit(enm, (ex3, ey3))
|
||||
|
||||
```
|
||||
|
||||
这样,我们就贴上了三幅飞机的图片。
|
||||
|
||||
我们再来看一下效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/7d/b932a985b390ccf359a4a56f25796c7d.jpg" alt="">
|
||||
|
||||
是不是看起来很有意思?但是这样并不能让飞机动起来,我们需要用到上一节里,移动背景图片的知识,来让敌人的飞机动起来。我们只需要将这三个y值在循环中设置成递增,就可以做到三架飞机的移动了。代码就像这样:
|
||||
|
||||
```
|
||||
screen.blit(enm, (ex1, ey1))
|
||||
screen.blit(enm, (ex2, ey2))
|
||||
screen.blit(enm, (ex3, ey3))
|
||||
ey1 +=1
|
||||
ey2 +=1
|
||||
ey3 +=1
|
||||
|
||||
```
|
||||
|
||||
这样我们就完成了敌人飞机不停往下飞的效果了。
|
||||
|
||||
后面的内容,也会像现在这样,代码很多,我带你再梳理一下逻辑。
|
||||
|
||||
**首先,我们需要使用Python程序库的随机函数来制作随机数。**
|
||||
|
||||
<li>
|
||||
通过这个随机函数,来随机载入敌人飞机的位置。当然如果有游戏策划的话,游戏可能会由某种固定的起始点来刷出敌人飞机,这里我们只用到随机函数来刷敌人飞机。
|
||||
</li>
|
||||
<li>
|
||||
如果想要做得更漂亮的话,我们可以将随机函数的值从屏幕最上方刷出来,这样看起来敌人就是从屏幕最上方飞下来的。比如我们可以设置y值为-10左右。
|
||||
</li>
|
||||
<li>
|
||||
如果想要做得更精细的话,我们可以通过程序得到图片的长和宽。通过图片的长和宽来计算刷出飞机的位置,我们可以使用屏幕大小来减去飞机长宽的大小来计算,比如屏幕长是640,图片的长是8。那么,我们在设置 x 轴位置的时候,就应该最大只设置到640-8这样的位置。这样就不至于我们在编程的时候,只刷出半架飞机,或者根本就看不到飞机。
|
||||
</li>
|
||||
|
||||
**其次,我们在载入敌人飞机的时候,需要贴三幅图片。**
|
||||
|
||||
<li>
|
||||
当然,我们可以优化这一系列的代码,比如我们可以将一系列blit放在一个函数里面。上述的代码只是一个针对教学用的代码,为的是让你更直观、明了地能看明白如何载入三幅敌人飞机的图片。我们优化了代码后,可以直接使用一段代码和一系列数组就可以完成这个操作。
|
||||
</li>
|
||||
<li>
|
||||
如果做了一幅alpha通道抠图的图片,我们在载入的时候,需要处理alpha通道的数据,让其图片达到“透明”的效果,而不是直接贴一幅有黑框的图片。
|
||||
</li>
|
||||
|
||||
**最后,事实上,我们要将这些内容更加完善,还有许多的工作要做。**
|
||||
|
||||
<li>
|
||||
这些工作我将在后续的内容中展开讲解。比如我们需要移动背景。这个我们上次已经说明了。在敌人飞机往下飞的过程中,我们需要考虑敌人飞机往下飞的速度,是不是要比屏幕移动的速度更快或者更慢,这样才能体现敌人飞机的等级高低,体现出游戏的难度是随着关卡的变化越来越难的。
|
||||
</li>
|
||||
<li>
|
||||
我们将游戏背景的图片blit函数放在游戏循环的最开始,而载入飞机的代码则放在稍后的部分,那么如果我们将游戏背景的图片放到飞机之后载入会发生什么事情呢?如果你一直在练习我在文中提供的代码,你应该可以知道,这个时候飞机的图片都会不见了,只能看到游戏背景。这是因为Pygame是按照blit代码的顺序来载入图片的,这部分内容我在后面的内容中讲解。
|
||||
</li>
|
||||
<li>
|
||||
我们可以将载入的图片资源放到一个资源包中,或者放在一个目录中,这样游戏的目录就不至于看起来乱七八糟,而是非常有序的。比如我们可以将所有和主角飞机相关的内容就放在飞机的目录下,和敌人相关的就放在敌人的目录下,背景和关卡就放在关卡的目录下,这样就看起来就整整齐齐。在编写代码的时候,从目录的名字不同,可以知道载入的是什么内容,比如:‘enemy/plane.png’。
|
||||
</li>
|
||||
|
||||
## 小结
|
||||
|
||||
好了,这节内容差不多了。我主要和你讲了三个内容:
|
||||
|
||||
<li>
|
||||
随机函数使用random.randrange来做,输入开始和结束值,就能随机出这一个范围的数字;
|
||||
</li>
|
||||
<li>
|
||||
让飞机移动起来,需要将x或者y的值进行加减变化;
|
||||
</li>
|
||||
<li>
|
||||
处理alpha混合半透明图片,需要使用conver_alpha函数。
|
||||
</li>
|
||||
|
||||
最后,给你留一个小思考题吧。
|
||||
|
||||
```
|
||||
while True:
|
||||
......
|
||||
ex1 = random.randrange(20, 600)
|
||||
ey1 = random.randrange(10, 50)
|
||||
screen.blit(enm, (ex1, ey1))
|
||||
pygame.display.update()
|
||||
|
||||
```
|
||||
|
||||
如果我们把这段代码的ex1, ey1变量放在游戏循环中(本来在循环外面),并且将ex1, ey1填入到敌人飞机的blit函数中,会出现什么样的结果呢?
|
||||
|
||||
欢迎留言说出你的看法。我在下一节的挑战中等你!
|
157
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第11讲 | 如何设置图像的前后遮挡?.md
Normal file
157
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第11讲 | 如何设置图像的前后遮挡?.md
Normal file
@@ -0,0 +1,157 @@
|
||||
<audio id="audio" title="第11讲 | 如何设置图像的前后遮挡?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e5/f3/e5aef9e29a582eb7076e70cf72eb3bf3.mp3"></audio>
|
||||
|
||||
我们人的肉眼所观察到的世界是属于3D世界,有远近大小之分。一个物件A被另一个物件B遮挡,物件A就会看不到,而在2D的世界里,都是平面的,没有实际的高度区分,就算做成了斜45度角,也是一种视觉呈现,并没有在计算机内形成高度差。
|
||||
|
||||
在一般的游戏引擎,或者像Pygame这样的游戏库中,基本都是“先绘制的图案先出来”,“后绘制的图案后出来”,而后绘制的图案一定遮挡前面绘制的图案。因为2D就是一个平面,从逻辑上讲,按照先后顺序绘制,没有任何问题。
|
||||
|
||||
但是如果我们现在做的游戏是斜45度角的游戏,类似《梦幻西游》视角的,那么人物和建筑物之间就存在遮挡的问题,如果处理不谨慎,就会出现人物浮在建筑物上,或者建筑物把人挡住了。
|
||||
|
||||
所以在一些2D引擎中,会有一个Z值的概念,Z值的概念就是在(X,Y)的基本2D位置上,加一个高度的概念。这个高度是一个伪概念,它模仿3D的Z值,只是作遮挡用。但是我们现在使用Pygame来编写游戏的话,并没有Z值的概念,所以我们需要想一些办法来解决遮挡的问题。
|
||||
|
||||
首先,我们从共享资源中抽取一段围墙的图片来进行摆放。
|
||||
|
||||
围墙分为两幅图片,都是往右上角延伸的。现在我们需要将这两段围墙连接起来。如果我们像以前的做法,一个图片一个blit的话,那是不行的。因为这样需要相当大的代码量,所以我们采取将围墙的代码放入一个list中的做法。
|
||||
|
||||
首先,我们要定义图片和载入图片。
|
||||
|
||||
```
|
||||
right_1 = 'right_1.png'
|
||||
right_2 = 'right_2.png'
|
||||
r_1 = pygame.image.load(right_1).convert_alpha()
|
||||
r_2 = pygame.image.load(right_2).convert_alpha()
|
||||
|
||||
```
|
||||
|
||||
然后,我们写一个循环,将围墙放入一个list中。我们想要将这两段围墙每隔一个放置不同的样式,就需要做一些判断。我们将数字除以2,如果能除尽,就摆放其中一个,否则就摆放另一个。
|
||||
|
||||
```
|
||||
total = 10
|
||||
wall = []
|
||||
while total > 0:
|
||||
if total % 2 == 0:
|
||||
wall.append(r_1)
|
||||
else:
|
||||
wall.append(r_2)
|
||||
total-=1
|
||||
|
||||
|
||||
```
|
||||
|
||||
这样,我们就将围墙的对象分割并且放入到了list里面,我们就可以在接下来的代码中使用这个list,来将围墙拼接出来。
|
||||
|
||||
在拼接之前,我们还要定义一系列的变量。现在我们已知这个图片的宽度是62,长度是195,所以我们需要增加的步长就是“每次拼接加62的宽度”。而围墙1和围墙2在拼接的过程中,是要往右上角倾斜的。经过测量,倾斜的高度是30,所以每增加一个围墙,就要往y轴减去30的高度,现在我们要定义初始化的x和y的起始位置,并且要定义增加步长的x值和y值,我们可以这么写:
|
||||
|
||||
```
|
||||
init_x = 0
|
||||
init_y = 300
|
||||
step_x = 62
|
||||
step_y = -30
|
||||
|
||||
```
|
||||
|
||||
我们要将这一系列变量放在循环中,因为每循环贴图一次,就需要重新初始化和计算步长,这样看上去就像把一系列墙一直贴在游戏中一样。
|
||||
|
||||
我们来看一下代码。
|
||||
|
||||
```
|
||||
for w in wall:
|
||||
screen.blit(w, (init_x, init_y))
|
||||
init_x += step_x
|
||||
init_y += step_y
|
||||
|
||||
```
|
||||
|
||||
这段代码的意思是,遍历wall这个list,取出下标并且赋值给w变量,每个w变量都是一个surface对象,这个对象通过screen.blit来贴上去,贴上去的位子使用初始x和初始y,然后初始x和初始y的位置又变化了,每次增加步长x和减去步长y,进行第二次的贴图,然后继续循环贴,这样我们的围墙就开始连贯了起来。
|
||||
|
||||
我们来看一下贴上去的效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/91/5e15bd7c6cff5c0ff0080090ae1cc391.jpg" alt="">
|
||||
|
||||
可以看到,每隔一段贴一幅图,另一段贴另一幅图,这样一整段的围墙就贴完了。一共有十幅图片,每一副图片的y值都向上减去30。
|
||||
|
||||
现在我们来总结一下贴这些连贯图片的重点:
|
||||
|
||||
<li>
|
||||
将内容放入列表或者数组中。为了编程方便,将需要连续贴图的内容放入列表或者数组中就能够减少编程工作量;
|
||||
</li>
|
||||
<li>
|
||||
计算好贴图的点,能让我们在连续贴图的过程中,只要控制位置变量就可以完成。
|
||||
</li>
|
||||
|
||||
如果我们编写的是地图编辑器,而地图编辑器生成的脚本代码,除非写得非常智能,一般来讲,就是一连串的贴图代码,这样就会有许许多多的blit的操作,并不会将相同的元素加入循环或者列表,那是因为脚本代码是电脑生成的,没有更多的优化代码。
|
||||
|
||||
接下来,我们要将一个人物放上去。这个人物只是摆设,我们只是为了测试图像遮挡的情况。
|
||||
|
||||
```
|
||||
player = 'human.png'
|
||||
plr = pygame.image.load(player).convert_alpha()
|
||||
|
||||
```
|
||||
|
||||
然后我们在循环的围墙贴图的代码之后,放入人物。
|
||||
|
||||
```
|
||||
screen.blit(plr, (62, 270))
|
||||
|
||||
```
|
||||
|
||||
我们将人物故意放在围墙的某一个位置,效果看起来是这样的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/a4/32e01951e1b5d28ec1cd0a616b9019a4.jpg" alt="">
|
||||
|
||||
这样看上去,人物就站在围墙上面了。看起来他似乎有飞檐走壁的功夫,然而事实上,他应该几乎被围墙挡住,但是这个时候问题就来了。虽然我们可以把blit的代码放在显示围墙的blit代码之下,让围墙遮挡住人物,但是当游戏在进行的时候,人物要往下走,这时候就需要显示在围墙之外,我们不可能在游戏运行的时候改变代码,这是不可能做到的。所以我们还需要改变代码。
|
||||
|
||||
事实上,在正式的游戏开发中,我们需要将人物的控制、NPC的控制等放在不同的线程中去做,而地图则是直接载入地图数据文件。在地图的数据文件中会告诉你,哪些坐标是有物件挡住的,不能走;哪些坐标有哪些物件,你需要走过去的时候被遮挡。但是在我们今天的内容中,为了你能看得更明白,我们将地图和人物的代码都放在游戏的大循环中去做。
|
||||
|
||||
我们使用代码来模拟Z值的作用,虽然在代码中没有体现Z值,但是通过代码你可以理解Z值的意义。
|
||||
|
||||
首先我们来定义一个函数,这个函数将blit代码抽取出来,然后判断传入的参数是不是list类型,如果是的话,就连续贴图,否则就贴一张图。
|
||||
|
||||
```
|
||||
def blit_squences(data, x, y):
|
||||
if isinstance(data, list):
|
||||
for d in data:
|
||||
screen.blit(d, (x, y))
|
||||
else:
|
||||
screen.blit(data, (x, y))
|
||||
|
||||
```
|
||||
|
||||
我们利用Python的isinstance函数,来判断传入的data是不是list类型。如果是的话,我们就遍历data,然后将data中的内容进行连续贴图。这是为了模拟我们除了贴人物,还要贴围墙。如果判断不是list类型的话,则直接贴上data。
|
||||
|
||||
然后,我们需要改变在游戏循环内的绘制图片代码。我们需要用blit_sequences函数来替代这块代码,然后我们在内部做一个判断,判断人物是不是和围墙的位置重叠了,如果是的话,就贴上人物和围墙。
|
||||
|
||||
```
|
||||
for w in wall:
|
||||
if init_y == 270:
|
||||
blit_squences([plr, w], init_x, init_y)
|
||||
else:
|
||||
blit_squences(w, init_x, init_y)
|
||||
init_x += step_x
|
||||
init_y += step_y
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,我们看到,我们使用了blit_sequences这个函数,替代了原本的surface.blit代码。在这段代码中,我们需要判断一个位置,这个位置是围墙的y值,如果人物走到了这个位置,那么我们就将人物和围墙对象放入到blit_sequences中进行绘制。效果就是,人物被遮挡到了围墙外面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/1f/5d4eb9552dce19be5a8b184fe347391f.jpg" alt="">
|
||||
|
||||
这段代码起作用的地方是在[plr, w]这部分。我告诉Pygame,要先绘制plr然后再绘制w,但是如果你换一个位置,就是先绘制w再绘制plr。
|
||||
|
||||
这一部分是示例代码,正式编写游戏的时候,其实是不太会这么写的。这是为了展示我们如何方便地切换绘制位置。其中,plr和w的list部分,事实上就是解释Z值所做的工作,如果plr的Z值高于w,那么就先绘制plr,否则就先绘制w。当然在正式编写类似的游戏的时候,我们需要考虑的是多线程,这些我们将在后续的内容中进行讲解。
|
||||
|
||||
一般的做法是,我们会在多线程中绘制人物,然后载入地图,我们会在人物走动的过程中,判断地图上的物件,然后进行Z值的调整,或许,Z值最高的是物件本身,比如围墙和建筑物的Z值是100,而人物的Z值一直保持在20,所以每次走到围墙和建筑物这里,总是先绘制人物,再绘制建筑物,这样就起到了遮挡的效果。
|
||||
|
||||
## 小结
|
||||
|
||||
这一节内容差不多了,我来总结一下。
|
||||
|
||||
我们其实就讲了一个内容。在做遮挡的时候,要考虑绘制顺序,先绘制的一定会被后绘制的遮挡。
|
||||
|
||||
如果做得比较成熟的话,利用Python,我们需要在外面包裹一层字典。每个物件载入的时候,都告知其Z值,然后在绘制的时候,判断Z值,安排绘制顺序。
|
||||
|
||||
现在给你留一个小问题。
|
||||
|
||||
如果在绘制的过程中,两个人物的Z值相同的话,人物碰到一起,会出现什么结果呢?
|
||||
|
||||
欢迎留言说出你的看法。我在下一节的挑战中等你!
|
245
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第12讲 | 如何设置精灵的变形、放大和缩小?.md
Normal file
245
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第12讲 | 如何设置精灵的变形、放大和缩小?.md
Normal file
@@ -0,0 +1,245 @@
|
||||
<audio id="audio" title="第12讲 | 如何设置精灵的变形、放大和缩小?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/2a/7e6a8426d16c7e043876f0acc5d7c42a.mp3"></audio>
|
||||
|
||||
上周四,我给你讲解了图片的遮挡问题。这一节我要和你讲精灵的变形、放大和缩小。如果之前没有做过游戏开发,你肯定会问,什么是精灵?
|
||||
|
||||
## 什么是精灵?
|
||||
|
||||
我先来解释一下什么是精灵。精灵当然不是我们传统意义上的什么树林里的精灵。精灵是一个游戏开发中的名词,英文叫Sprite。
|
||||
|
||||
>
|
||||
它多用于游戏中的人物和可移动物品,也可以用于显示鼠标指针和输入的文字。如果屏幕上的可移动物体的尺寸比一个精灵图要大,可由若干个精灵图缩放或者拼接而成。
|
||||
|
||||
|
||||
从**宏观**的概念讲,精灵就是一幅图片。比如我们之前中讲过的那些飞机图、背景图,这些都可以认为是精灵或者是从精灵中派生出来的。它就是一系列可以变化的图片。这些图片可以变形、放大、缩小,或者是一系列的动画帧等等。
|
||||
|
||||
从**编程**的角度讲,精灵是一种管理器。在一个精灵的管理器中,可能会有一系列的方法去操作精灵,比如添有加、删除操作,比如有图像的变形、放大、缩小操作,还有系列帧的显示操作等。
|
||||
|
||||
既然,精灵就是图片,那在“打飞机”中,飞机会随着画面的变化、操作的不同,而有变形、放大以及缩小的状态。我现在就来讲这些操作的实现,需要用到哪些函数,以及这背后都有什么技巧。
|
||||
|
||||
## 设置变形、放大和缩小需要用到哪些函数?
|
||||
|
||||
Pygame中的底层,使用的是SDL开发库,这个我们在之前的内容中已经讲过,因此,这些变形、放大缩小等操作,都有对应的SDL库。
|
||||
|
||||
我们要用到的还是之前的飞机图片,为了让你更明确的看清楚,我删除了背景,只呈现飞机的内容。
|
||||
|
||||
### 翻转函数flip
|
||||
|
||||
我们首先要用到的是**函数flip**。顾名思义,这个函数就是让你对图片进行翻转,你可以翻转成水平的或者垂直的。所以它拥有两个参数,一个是传入x,一个是传入y,并且都需要传入**布尔值**。如果传入x值为真,那就进行水平镜像翻转,如果y值为真,那就进行垂直镜像翻转,两个都为真,两方都进行翻转。这个函数会返回一个surface。
|
||||
|
||||
```
|
||||
pln_t = pygame.transform.flip(pln, 1, 1)
|
||||
screen.blit(pln_t, (40, 350))
|
||||
|
||||
```
|
||||
|
||||
我们看到的结果是这样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/96/2f/961af51b04e51d3ba802e44a1fd2382f.jpg" alt="">
|
||||
|
||||
原本飞机的头是朝上的,现在进行了水平和垂直的翻转。
|
||||
|
||||
### 缩放函数scale
|
||||
|
||||
我们再来看一下**缩放的函数scale**。scale的参数是这样:
|
||||
|
||||
```
|
||||
scale(Surface, (width, height), DestSurface =None)
|
||||
|
||||
```
|
||||
|
||||
其中第一个参数是绘制对象,第二个参数是缩放大小,第三个参数一般不太使用,指的是目标对象。
|
||||
|
||||
```
|
||||
pln_t = pygame.transform.scale(pln, (220,220))
|
||||
screen.blit(pln_t, (20, 150))
|
||||
|
||||
```
|
||||
|
||||
我们在代码中,将pln这个对象放大到220×220(飞机原本大小为195×62),然后看一下效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/66/78b91a5ea30d2eaa2c08fce1ca749b66.jpg" alt="">
|
||||
|
||||
你看,飞机变大了。我们再尝试修改一下代码。
|
||||
|
||||
```
|
||||
pln_t = pygame.transform.scale(pln, (20,20))
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1b/58/1bd5885a91a0462e852ddcbc15132358.jpg" alt="">
|
||||
|
||||
飞机就变小了。所以,**scale函数**的作用是,**只要你传入参数的width和height值大于原本精灵的长宽值,就变大,否则就变小。**
|
||||
|
||||
类似,我们还有一个**函数scale2x**,你只需要填入绘制对象即可,函数会帮你进行两倍扩大,不需要你计算原本的长宽值并且乘以2。
|
||||
|
||||
### 旋转函数rotate
|
||||
|
||||
我们再来看一下**rotate旋转函数**。它提供一个参数angle,也就是你需要旋转的角度,正负值都可以。
|
||||
|
||||
我们来看一下代码。
|
||||
|
||||
```
|
||||
pln_t = pygame.transform.rotate(pln, 20)
|
||||
|
||||
```
|
||||
|
||||
我们看到的效果就像这样。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/39/62704bbf6e240e17ade2a907a82d3939.jpg" alt="">
|
||||
|
||||
这样飞机就朝左侧旋转了20度。 相似的,也有整合的函数**rotozoom**。它该函数提供了旋转和扩大的功能。
|
||||
|
||||
如果代码这么写:
|
||||
|
||||
```
|
||||
pln_t = pygame.transform.rotozoom(pln, 20, 2)
|
||||
|
||||
```
|
||||
|
||||
我们能看到的效果就是这样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/17/f0/1785173b9a5e6e4f149b1c3d9797dbf0.jpg" alt="">
|
||||
|
||||
### 剪切函数chop
|
||||
|
||||
接下来的是**函数chop**,这个函数提供了图像剪切的功能。我们需要传入一个绘制对象以及一个rect矩形,这样就可以将输入的矩形的内容剪切出来。
|
||||
|
||||
```
|
||||
pln_t = pygame.transform.chop(pln, [20,150,25,155])
|
||||
screen.blit(pln_t, (20, 150))
|
||||
|
||||
```
|
||||
|
||||
我们看一下代码的内容,我们在blit的时候,将pln_t放置在(20,150)的位置上,所以我们在chop的时候,将剪裁[20,150,25,155]这样一个矩形进行裁切。
|
||||
|
||||
然后我们来看一下效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/63/d06607b46aa22f6321edf7a76ee7ab63.jpg" alt=""><br>
|
||||
<br>
|
||||
这么多函数,是不是容易记不住?我来给这一部分做个总结:
|
||||
|
||||
**对于精灵的所有放大、缩小或者变换的函数,都在pygame.transform模块里。它提供了一系列2D精灵的变换操作,包括旋转角度、缩小放大、镜像、描边、切割等功能,让你很方便地能够在游戏中随心所欲地对处理2D精灵。**
|
||||
|
||||
## Pygame中的Sprite
|
||||
|
||||
我们再来看一下Pygame本身,Pygame本身就提供有Sprite模块,Sprite模块提供了Sprite类,事实上,Pygame的精灵类最方便的功能就是将某些序列帧的图片,做成动画,并且保存在Sprite的组(group)里面。在Pygame里面,Sprite是一个轻量级的模块,我们需要做的是要将这个模块继承下来,并且重载某些方法。
|
||||
|
||||
### 类explode
|
||||
|
||||
我们现在有一副图片,效果是打击到某个点,开始爆开图案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/68/e5/68ee7fc3ef87a7fe6a471da3837626e5.jpg" alt="">
|
||||
|
||||
这幅图片一共三帧,是一个标准的精灵动画。那么我们需要做的,就是先将这幅图片导入到精灵类当中。我们做一个类explode:
|
||||
|
||||
```
|
||||
class explode(pygame.sprite.Sprite):
|
||||
|
||||
```
|
||||
|
||||
这个类继承自Sprite类,然后我们定义一个初始化函数,并且首先调用上层基类的初始化。
|
||||
|
||||
```
|
||||
def __init__(self, target, frame, single_w, single_h, pos=(0,0)):
|
||||
pygame.sprite.Sprite.__init__(self)
|
||||
|
||||
```
|
||||
|
||||
在这个类当中,我们看到了函数的定义内容,第一个参数**self**,我就不过多解释了;**target**是我们需要载入的目标图片;**frame**是我们需要告诉这个类,我们这个动画有几帧;**single_w, single_h** 代表了我们每一帧动画的长宽。在这里,我们的每一格动画是262×262。**pos**是我们告诉屏幕,将这个动画放置在屏幕的什么位置。
|
||||
|
||||
接下来,这是我编写的初始化代码:
|
||||
|
||||
```
|
||||
def __init__(self, target, frame, single_w, single_h, pos=(0,0)):
|
||||
|
||||
pygame.sprite.Sprite.__init__(self)
|
||||
|
||||
self.image = pygame.image.load(target).convert_alpha()
|
||||
|
||||
self.main_image = self.image
|
||||
|
||||
self.frame = frame
|
||||
|
||||
self.rect = self.image.get_rect()
|
||||
|
||||
self.count = 0
|
||||
|
||||
self.single_w, self.single_h = single_w, single_h
|
||||
|
||||
self.rect.topleft = pos
|
||||
|
||||
```
|
||||
|
||||
大部分代码你应该都能理解,但是有几个点,我要特殊说明一下。
|
||||
|
||||
第一个是**main_image**。这个是保存主image图片。我们在后续的切换帧的时候,需要在main_image中切割后面几帧,并且呈现在屏幕上,这样就会在视觉效果中呈现动画效果。**count**是每一帧的当前计数。在这里我们一共拥有三帧,这三帧我们记录在self.frame里,是总的帧数。
|
||||
|
||||
### 重载函数update
|
||||
|
||||
接下来,我们来看一下update代码。
|
||||
|
||||
```
|
||||
def update(self):
|
||||
|
||||
if self.count < self.frame-1:
|
||||
|
||||
self.count += 1
|
||||
|
||||
else:
|
||||
|
||||
self.count = 0
|
||||
|
||||
self.image = self.main_image.subsurface([self.count*self.single_w, 0, self.single_w,self.single_h])
|
||||
|
||||
```
|
||||
|
||||
**Update**是一个重载函数。事实上,在update函数里,需要判断帧数、当前循环的计数等等。但是,为了能让你能更直观容易地感受代码做了什么内容,所以我直接使用self.count来做帧数的计数。
|
||||
|
||||
进入函数后,我们使用self.count来和self.frame的总帧数进行对比。如果帧数不足以切换,那就加1,否则就置为0。判断结束后,我们就将image变成下一帧的内容。
|
||||
|
||||
其中,subsurface的意思是传入一个rect值,并且将这个值的surface对象复制给image对象,并且呈现出来。
|
||||
|
||||
这时候,我们需要将这些内容放入到group中。
|
||||
|
||||
```
|
||||
exp = explode('explode.png', 3, 262,262, (100,100))
|
||||
group = pygame.sprite.Group()
|
||||
group.add(exp)
|
||||
|
||||
```
|
||||
|
||||
首先,exp就是我们定义的explode类的对象,我们分别传入的内容是图片、帧数、单个帧的宽度、单个帧的高度,并且将这个精灵显示在屏幕的位置。
|
||||
|
||||
随后,我们定义一个叫作group的对象,并且将exp对象填入group中。随后,我们在大循环内,写一串代码。
|
||||
|
||||
```
|
||||
group.update()
|
||||
group.draw(screen)
|
||||
|
||||
```
|
||||
|
||||
这个update,调用的就是**exp.update函数**。draw就是在screen中绘制我们填入group中的内容。由于动画在文章中无法显示,所以我就不将图片放入到文章中来了。
|
||||
|
||||
在精灵类中,我们除了动画的呈现,还有碰撞效果的制作。这属于更为复杂的层面,后续的内容,我将会用简单的方式来呈现碰撞的实现。
|
||||
|
||||
当然,Sprite类还有更为高阶的用法,除了碰撞,还有Layer(层)的概念。group的添加精灵,事实上是没有次序概念的,所以哪个精灵在前,哪个在后是不清楚的,到了这个时候,你可以使用OrderUpdates、LayerUpdates这些类,其中LayerUpdates拥有众多方法可以调用,这样就会有分层的概念。
|
||||
|
||||
## 小结
|
||||
|
||||
这一节,你需要记住这几个重点。
|
||||
|
||||
<li>
|
||||
精灵的变形、缩放以及pygame中关于精灵类的一些简单的操作。
|
||||
</li>
|
||||
<li>
|
||||
你可以直观地感受到,精灵类和group类配合起来使用是一件很方便的事情,也就是说,我们忽略了blit的这些方法,直接在group中,进行update和draw就可以一次性做完很多的工作。
|
||||
</li>
|
||||
<li>
|
||||
如果我们单独编写精灵的序列帧动画函数,也不是不行,但是你可能需要编写相当多的代码来代替Sprite和group类的工作。
|
||||
</li>
|
||||
|
||||
现在留一个小问题给你。
|
||||
|
||||
结合精灵的变形、放大和缩小,再结合Pygame精灵类的内容,要在update重载函数里绘制动画帧效果,并且不停地放大、缩小,该怎么实现呢?
|
||||
|
||||
欢迎留言说出你的看法。我在下一节的挑战中等你!
|
167
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第13讲 | 如何设置淡入淡出和碰撞检测?.md
Normal file
167
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第13讲 | 如何设置淡入淡出和碰撞检测?.md
Normal file
@@ -0,0 +1,167 @@
|
||||
<audio id="audio" title="第13讲 | 如何设置淡入淡出和碰撞检测?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3a/2d/3ab5046253a5726e6f86fb57e0d8d32d.mp3"></audio>
|
||||
|
||||
我们在前一节,学习了精灵的变形、放大和缩小,并且学习了如何使用精灵类和组的概念来管理精灵,制成动画。今天,我将带你学习淡入淡出和碰撞热点的判断。
|
||||
|
||||
所谓的**淡入淡出**,就是英文的**fade-in**和**fade-out**。淡入淡出在电影、游戏、CG、操作系统、手机UI、应用等等各种地方随处可见。那究竟什么是淡入淡出呢?它在游戏中究竟如何实现呢?在我们的打飞机游戏中,什么时候会用到这个操作呢?
|
||||
|
||||
## 什么是淡入淡出?
|
||||
|
||||
不知道你有没有注意,在我们玩过的打飞机游戏中,当每一关游戏开始的时候,都会有个游戏画面逐渐出现的过程。短短几秒,从无到有,整个画面就呈现在你眼前了。同样,每一关结束的时候,也会有个画面逐渐消失的过程。
|
||||
|
||||
从**画面效果**讲,这个画面从有到逐渐屏幕变暗,直到消失,或者反过来,由暗逐渐变亮,到完全进入画面的过程,就叫做淡入淡出。从**声音**角度讲,也存在淡入淡出,比如音乐从无声到逐渐有声,或者从有声到逐渐无声。
|
||||
|
||||
**在Pygame中并不存在“画面的淡入淡出”这样的函数,需要我们自己去实现这样的功能。**
|
||||
|
||||
首先,如果我们想给这张图片进行淡入淡出的处理的话,就需要对它进行alpha混合处理。我们在前面谈到过alpha混合,你可以理解成半透明,但是alpha混合究竟是什么呢?
|
||||
|
||||
**alpha混合**就是将一部分被遮盖的图像进行半透明处理。在游戏引擎或者游戏库中,图像的alpha值是可以被修改的。每动态修改一次alpha值,就会让图像更透明或者更不透明。通过制作出alpha效果,我们可以在游戏中实现各种绚丽的效果。
|
||||
|
||||
一般来讲,底层图形接口的颜色为32位的值,包含RGB以及A(alpha),其中红色R、绿色G和蓝色B各为8位,alpha也为8位,所以合起来是32位的颜色值。
|
||||
|
||||
但是如果不存在A通道,那么就是24位的颜色值。每个颜色值都有256个级别的值,从程序角度是从0到255,而支持alpha通道的图片格式有png、tiff等。但是如果没有带alpha透明通道的图,我们也可以在程序中设置它的alpha值来做透明。
|
||||
|
||||
如果是Pygame,在load image函数的时候,不要处理alpha,也就是不要调用convert_alpha函数。具体为什么呢?我后面给你揭晓。
|
||||
|
||||
## 如何做出淡入淡出效果?
|
||||
|
||||
我们在没有背景图片载入的时候,做淡入淡出效果,就不是使用alpha通道了,而是需要用**fill函数**来填充背景色。
|
||||
|
||||
如果背景色是(0,0,0),也就是纯黑的话,那么就需要将(0,0,0)逐渐变成(255,255,255)来变成纯白,或者你自己定义一个RGB值来完成最终淡出后的背景色。
|
||||
|
||||
我们现在来看一下这段代码。
|
||||
|
||||
```
|
||||
pln = pygame.image.load(plane).convert()
|
||||
a=0
|
||||
while True:
|
||||
pln.set_alpha(a)
|
||||
screen.blit(pln, (20, 150))
|
||||
if a > 255:
|
||||
a=0
|
||||
screen.fill([a,a,a])
|
||||
a += 1
|
||||
|
||||
```
|
||||
|
||||
这段代码中,我们开始载入飞机图片。注意一下,我们没有用convert_alpha。如果我们用了convert_alpha,就会出现设置的alpha值没有任何作用。因为,在载入的时候,已经处理了alpha值了。
|
||||
|
||||
随后,我们定义一个变量a,这个a既作用在screen.fill上,将fill的RGB值进行变换,也作用在set_alpha这个函数里,这个函数将图片的surface进行alpha值的设置,最后blit出来,呈现在屏幕上。
|
||||
|
||||
我们呈现的效果就是这样。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/1c/2b6bc9023c281fa5ae7f44bbe51ef01c.jpg" alt="">
|
||||
|
||||
其他图片也可以做alpha混合,我们将最早的背景jpg图片传入,进行alpha半透明调整,效果是这样的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/ac/9c71cf0a87d6ebb9c2fd6e7f323d09ac.jpg" alt="">
|
||||
|
||||
## 如何设置碰撞检测?
|
||||
|
||||
说完了alpha混合,我们现在要来学习一下碰撞相关的内容。这个很好理解,飞机相撞了,就要用到碰撞。
|
||||
|
||||
事实上,在游戏中,碰撞属于物理引擎的一部分。特别是在3D游戏当中,物理引擎是独立于图形引擎的一个模块。程序员需要将图形引擎的对象填入到物理引擎中,计算出碰撞的结果,然后再返回给图形引擎,给出画面效果。做得精致的2D游戏也有独立的物理引擎,专门检测碰撞、计算重力等等。
|
||||
|
||||
但是在今天我们的课程中,我将使用浅显易懂,用你最能看懂的代码来解释碰撞是怎么回事。
|
||||
|
||||
**事实上,我们今天要讲到的碰撞是两个图片相交之间的碰撞检测,这并不算物理检测,而是图片检测。**
|
||||
|
||||
既然我们要检测的是图片,那么哪些前置信息是我们需要知道的呢?
|
||||
|
||||
首先,我们肯定要知道这两张需要碰撞图片的长宽,才能计算图片是否相交。在计算图片相交的时候,我们首先要知道它**所在位置的x轴的起点**,然后要知道它的**图片宽度**,然后我们要知道**图片位置的y起点**,以及它的**图片长度**,这样我们就得到了图片的长宽。
|
||||
|
||||
我们用上面的主角飞机图片和敌人飞机图片来做演示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/86/68/861154a6a7403ef7113c259f65425d68.jpg" alt="">
|
||||
|
||||
让两架飞机面对面,敌人的飞机从上往下飞,主角飞机从下往上飞。如果两架飞机碰到,我将在后台的命令行窗口显示一些字符串。
|
||||
|
||||
### 定义碰撞函数
|
||||
|
||||
接下来,我们来看一下,如何定义这个碰撞函数。
|
||||
|
||||
```
|
||||
def collide(a, axy, b, bxy):
|
||||
a_x1, a_x2 = axy[0], axy[0]+a.get_width()
|
||||
a_y1, a_y2 = axy[1], axy[1]+a.get_height()
|
||||
b_x1, b_x2 = bxy[0], bxy[0]+b.get_width()
|
||||
b_y1, b_y2 = bxy[1], bxy[1]+b.get_height()
|
||||
a1, a2 = range(a_x1, a_x2) , range(a_y1, a_y2)
|
||||
b1, b2 = range(b_x1, b_x2) , range(b_y1, b_y2)
|
||||
|
||||
ct = 0
|
||||
for a in a1:
|
||||
if a in b1:
|
||||
ct = 1
|
||||
break
|
||||
for a in a2:
|
||||
if a in b2:
|
||||
if ct == 1:
|
||||
return True
|
||||
return False
|
||||
|
||||
```
|
||||
|
||||
我们来仔细地看一下这段函数。
|
||||
|
||||
首先,**collide函数**拥有四个参数。第一个参数是第一幅图片的对象,第二个参数接收一个元组,接收第一幅图片所在的x轴和y轴,第三个参数是第二幅图片的对象,第四个参数接收一个元组,接收第二幅图片所在的x轴和y轴。
|
||||
|
||||
随后,代码进入一个**得到长宽**的过程。
|
||||
|
||||
a_x1获取a图片对象所在屏幕的x点的起始位置,这个位置由第二个参数的元组下标0提供,a_x2获取a图片对象所在屏幕的x点的终止位置(事实上是它的宽度),由于有x轴的起始坐标的关系,所以需要起始坐标加上图片宽度,才是它真实的x坐标结束点。
|
||||
|
||||
a_y2获取a图片对象所在屏幕的y点的起始位置,这个由第二个参数的元组下标1提供,a_y2获取a图片对象所在屏幕y点的终止位置,其实是它的长度,和前面的x轴一样,需要加上y轴所在屏幕的位置,才是真正的y轴的结束点。
|
||||
|
||||
和a图片是一个道理,b图片我就不作具体阐述了。
|
||||
|
||||
接下来,我们需要知道整个图片所在的屏幕点,那么我们就需要用到**range函数**。
|
||||
|
||||
Python的range函数,是自动形成的一串整数列表。它的函数原型是这样的。
|
||||
|
||||
```
|
||||
range(start, stop, [step])
|
||||
|
||||
```
|
||||
|
||||
其中步长step可以省略。因为默认是1,所以如果在range中输入了开始和结束,就会形成一个列表。如果省略了stop,就会从0开始计数,形成一串列表。比如range(5),那就会形成0,1,2,3,4。
|
||||
|
||||
我们在range中形成了一串列表,其中a1对应的是,a图片x值的起始点到终止点的列表,a2对应的是a图片y值的起始点到终止点的列表。接下来的b1和b2就不做阐述了,和a1是相同的代码逻辑。
|
||||
|
||||
### 碰撞的检测
|
||||
|
||||
随后,我们就需要进行碰撞的检测了。
|
||||
|
||||
首先,我们先要判断a图片x轴的列表数字里面,是不是存在b图片的x轴的数字。如果存在,那么就把计数加1,跳出循环。
|
||||
|
||||
接下来,我们再判断a图片的y轴的列表数字里面,是不是存在b图片的y轴的数字。如果存在,那么就返回为真(True),就说明碰撞检测成功了。如果计数等于0或者计数等于1但是并没有通过y轴的列表检测,那么就返回假(False)。
|
||||
|
||||
我们来看一下传入参数的代码。
|
||||
|
||||
```
|
||||
y1, y2 = 1, 1
|
||||
screen.blit(pln, (100, 300 + y1))
|
||||
screen.blit(enm, (100, 20 + y2))
|
||||
print collide(pln, (100,300+y1), enm, (100,20+y2))
|
||||
y1-=1
|
||||
y2+=1
|
||||
|
||||
```
|
||||
|
||||
我们在blit绘制的时候,y轴加入了一个变量,就是y1和y2。其中主角的飞机pln对象,y轴始终减1,敌人的飞机enm,始终加1,为的就是让两架飞机对向飞过来并且检测碰撞。
|
||||
|
||||
我们将pln和 enm以及它们所在的位置,分别传入collide函数,进行检测。我们将在命令行后台打印True或者False。如果是False就是没有碰撞,如果是True就是碰撞了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bb/35/bbc8171fa91b1cc49bede8ba38f2ea35.jpg" alt="">
|
||||
|
||||
当两架飞机碰到的时候,True就出现了,那是因为x轴和y轴都有不同程度的重叠。所以在collide函数里面,就返回了True。
|
||||
|
||||
另外,在Pygame里,精灵类当中也有碰撞检测的类可以提供使用,但是,**使用碰撞检测类可以用来进行球形的判断,而不能用于矩形图片之间的判断**。 这是更为高级和复杂的用法,在这里不做更深的阐述了。
|
||||
|
||||
## 小结
|
||||
|
||||
今天,我和你讲解了淡入淡出以及碰撞的热点检测。我们需要设置Alpha混合和背景填充,来实现淡入淡出,而普通图像碰撞的检测,则是通过判断图像x轴和y轴是否重叠来实现。
|
||||
|
||||
给你留个小问题吧。
|
||||
|
||||
如果给你一张图片,需要判断精准的碰撞,比如碰到机翼,或者碰到某一个非矩形的位置,你该如何判断碰撞结果?
|
||||
|
||||
欢迎留言说出你的看法。我在下一节的挑战中等你!
|
119
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第14讲 | 如何制作游戏资源包和保存机制?.md
Normal file
119
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第14讲 | 如何制作游戏资源包和保存机制?.md
Normal file
@@ -0,0 +1,119 @@
|
||||
<audio id="audio" title="第14讲 | 如何制作游戏资源包和保存机制?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/26/c5/26abded8e11ef1dde21c5177417b77c5.mp3"></audio>
|
||||
|
||||
我们要做一款打飞机游戏,里面有飞机图片、背景图片、飞机音效、碰撞音效等等非常多的素材。如果将这些资源都放置在一个目录下,将会变得非常混乱。如果按照素材内容来划分目录,程序读取的效率就不高,所以我们需要将这些素材打包在一个资源包内,然后将每个素材都放置在一个虚拟目录内。
|
||||
|
||||
因此,今天我们就来如何制作讲解资源包。简单来说,所谓的资源包,就是将游戏的所有资源和素材,进行打包分类,并且进行资源整合,亦或将资源和素材进行压缩,减少游戏体积。
|
||||
|
||||
## 什么是资源包?
|
||||
|
||||
我总结了一下,可以从这三个角度来理解什么是资源包。
|
||||
|
||||
<li>
|
||||
资源包是一种将游戏的资源和素材进行分类、梳理,并且打包的一种包裹。
|
||||
</li>
|
||||
<li>
|
||||
资源包可以用来压缩游戏资源和素材,减少游戏体积。
|
||||
</li>
|
||||
<li>
|
||||
资源包里存在任何可能性,比如它可以包含图片文件、模型文件、音频文件、脚本文件等等,具体要看游戏开发人员的配置需求,来决定资源包里的内容。
|
||||
</li>
|
||||
|
||||
现在很多游戏公司都不会编写特殊的资源包格式。因为设计一种资源包格式,需要经过一系列复杂的动作,包括包头、包身和包尾。
|
||||
|
||||
关于这个格式的设计,一会儿我会给你仔细分析。因为,和我们自定义网络协议包一样,一个好的资源包,能够很方便进行解包、打包、删除文件、插入文件的操作,以及游戏的在线更新、补丁更新、资源包的解包、打包、删除、插入、更新文件等操作。
|
||||
|
||||
而一个好的资源包格式,不会占用主程序大量的时间。因为在游戏中,需要直接读取包文件里面的内容。
|
||||
|
||||
比如我们之前在Pygame中读取的图片文件,在包裹格式中,可能会这么写伪代码:
|
||||
|
||||
```
|
||||
load.image(‘package.pack/plane.png’)
|
||||
|
||||
```
|
||||
|
||||
其中package.pack就是包裹,plane.png是存在在包裹里面的其中一幅图片文件。这样,打了包裹后的文件,就不会污染目录。一般一个包裹文件中存在大量资源,而我们只要按照包裹路径读取就可以了。
|
||||
|
||||
如果不编写特殊的资源包格式,那应该怎么制作资源包呢?答案是,**使用现成的压缩软件库,进行打包压缩,直接在程序内使用**。比如我们最常用的zip文件、rar文件,都是可以拿来做资源包文件的。在Python中有内置zip模块,可以直接读取zip文件。我们可以直接在Pygame中结合zip模块进行编程。
|
||||
|
||||
## 资源包的格式
|
||||
|
||||
我们要讲解的是资源包的制作,我将会用一种较为通用和简单易懂的方法,解释资源包都包含哪些内容,同时让你理解资源包是怎么制作的。
|
||||
|
||||
首先,从编程的格式来理解资源包,你需要了解下列这些内容。
|
||||
|
||||
<li>
|
||||
**资源包头**,是一种标记,存放在包裹里最开始的几个字节,一般是2~4个字节。资源包头可以用来辨别这个资源包是哪个公司出品的。例如我后面准备举的一个例子,这里面就有INFO这样的标记,INFO可能是这家游戏公司的名字或者是缩写等等。
|
||||
</li>
|
||||
<li>
|
||||
**资源包版本**,这个不是必须的。如果考虑到一款游戏各个版本之间变化很大,未来可能会修改资源包的格式,那么这个时候就需要版本号。版本号一般会使用2个字节的short类型来存储,或者直接用十六进制编辑器能看明白的字符串,来代表版本号,比如用10表示1.0。所以,结合资源包头,我们现在所看到的结构是INFO10。
|
||||
</li>
|
||||
<li>
|
||||
**资源包是否进行压缩**,这个也不是必需的,但是有一些资源包会说明白,究竟是不是压缩资源包。如果是压缩就是1,不是压缩就是0。至于压缩格式,一般在编程的时候,就会指定清楚,不需要特别说明在资源包内。
|
||||
</li>
|
||||
<li>
|
||||
**资源包的目录结构以及素材名文件名偏移量**,资源包内的目录结构都是虚拟的,所以你可以定义在资源包内类似于/game/res这样的目录结构。但是事实上,这只是方便程序调用,事实上目录是不存在的,这是一种只存在在包裹内的虚拟目录。
|
||||
</li>
|
||||
|
||||
然后,我们需要规定素材的**文件名**和**偏移量**。比如/game/res/background.jpg。这是告诉我们在/game/res虚拟目录下,拥有background.jpg这个文件。随后需要告诉程序偏移量是多少,一般是存储4个字节的整型数字。
|
||||
|
||||
到目前为止,资源包的格式看起来可能是这样的:
|
||||
|
||||
```
|
||||
INFO100/game/res/background.jpg,[四个字节的偏移量]
|
||||
|
||||
```
|
||||
|
||||
在这里,我们看到,偏移量之前多加了一个逗号“,”。这是一个**分隔符**,也就是告诉程序,这一段在哪里结束。
|
||||
|
||||
随后是四个字节的偏移量。所谓的**偏移量**,就是告诉程序,你要到这个包裹的第几个字节去寻找这个文件的具体内容。
|
||||
|
||||
<li>
|
||||
**资源包的素材本体**。每个本体都可能是一个二进制文件、文本文件或其他任何文件。这些文件的文件名在资源包的素材文件名中都被定义好了。在资源包的素材本体中,我们可能会碰到各种各样的二进制字符,那么我们怎么知道这些素材是从哪里开始哪里结束的呢?
|
||||
</li>
|
||||
<li>
|
||||
**资源包的素材长度**,规定素材的长度有两种方法,**一种方法**是在定义资源包的目录结构以及素材偏移量的时候,再加上一个素材长度,也是四个字节的整型数字。这种方法的好处是,不需要添加某个分隔符告诉程序,这个素材的本体到这里结束。**第二种方法**是在本体结束的位置添加分隔符,比如一个逗号或者分隔符号|。这种方法的好处是,不需要知道文件长度是多少。但是坏处是,分割符号可能会和素材本体重叠。
|
||||
</li>
|
||||
|
||||
比如素材的本体是个二进制文件,分隔符比如是!@#$,素材的本体里面也存在!@#$这样的内容,这样的情况下,就会出现读取中断,因为程序以为素材内的!@#$就是结束符号,事实上这只是素材本身的内容而已。
|
||||
|
||||
- **资源包结束符**,这个也不是必须的。我们要结束资源包,必须在资源包的结尾添加结束符,这个结束符是告诉程序,资源包已经结束了。
|
||||
|
||||
我们来看一个完整的资源包,大概是什么样子的。
|
||||
|
||||
```
|
||||
[资源包头][版本号][是否压缩][资源包目录/素材文件名A][文件A偏移量][文件A长度]…[资源包目录/素材文件名N][文件N偏移量][文件N长度][素材A本体]….[素材N本体][结束符]
|
||||
|
||||
```
|
||||
|
||||
了解了资源包的格式内容,我们可以很方便地利用Python或者C语言等来编写相应格式的资源包。
|
||||
|
||||
我来给这部分做一个总结:
|
||||
|
||||
资源包的存在,有两个目的,一是让游戏目录干净整洁,不然看上去都是乱七八糟的图片和各种配置,二是让游戏程序能更快地从内存中读取游戏资源制作的包裹文件,加速游戏的运行效率。这个包裹文件中含有虚拟目录、资源、资源位置、资源名字等等信息。我们不需要从文件目录中去读取单一文件,只需要从内存中载入的资源包中取出某个文件即可。
|
||||
|
||||
## 如何制作游戏的保存机制?
|
||||
|
||||
每一个游戏几乎都有保存和载入的机制。首先你需要知道,只有保存了数据,我们才能载入数据。那么游戏的保存机制是怎么做的呢?
|
||||
|
||||
事实上,游戏的保存和游戏的地图编辑器中保存地图的原理,可以说是异曲同工。如果一个游戏中,有地图、坐标、人物、装备、分数,这些都需要被记录下来,那么我们不可能将地图、坐标、人物、装备、分数等全部转换成二进制文件记录下来。那应该怎么做呢?
|
||||
|
||||
首先,如果是记录地图,有地图1或者地图2,我们只需要记录地图的ID就好了。假如是地图2,坐标是(x,y)。人物只需要记录人物的ID,再关联到人物。一个游戏中,玩家建立了一个人物角色,就会将这个人物角色进行保存,不至于丢失人物角色。所以,在读取游戏的时候,需要先读取人物角色,再读取保存的游戏内容。
|
||||
|
||||
至于分数就很好记录了,记录分数其实就是记录数字,所以记录起来会很方便。
|
||||
|
||||
那么装备呢?如果是装备,一般会将装备的所有内容记录下来,如果做得精致的游戏,还会将地图中那些掉落的装备和死去的NPC进行记录。
|
||||
|
||||
还有一种做法是,将游戏保存的文件直接导出成一个脚本文件,以后每次读取数据就只需要使用程序读取脚本就可以了。
|
||||
|
||||
## 小结
|
||||
|
||||
今天我讲解了资源包的制作以及游戏进度的保存,你需要你记住这些内容。
|
||||
|
||||
- 制作资源包的目的是为了厘清游戏素材以及游戏素材的存放结构。资源包的结构与压缩包的结构比较相似,但是为了更贴合游戏程序读取,会对虚拟目录和素材文件名等,做一些修改。
|
||||
- 另外,为了方便保存游戏进度,我们可以做成游戏脚本,第二次打开游戏直接载入保存的脚本即可。
|
||||
|
||||
给你留一个小思考题吧。
|
||||
|
||||
在《GTA》中,汽车会有不同程度的损毁,当你保存完游戏重新进入的时候,汽车又复原了,请问这是为什么呢?
|
||||
|
||||
欢迎留言说出你的看法。我在下一节的挑战中等你!
|
159
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第15讲 | 如何载入背景音乐和音效?.md
Normal file
159
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第15讲 | 如何载入背景音乐和音效?.md
Normal file
@@ -0,0 +1,159 @@
|
||||
<audio id="audio" title="第15讲 | 如何载入背景音乐和音效?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d6/68/d6924e7807e48a266c6d8535c4ecd868.mp3"></audio>
|
||||
|
||||
好的音乐总是伴随着游戏一起,一直被玩家所记忆。在游戏中播放音乐和音效并不是什么困难的事情,但是究竟什么时候播放什么音效,具体怎么实现,这恐怕就需要一些技巧了。比如,我今天要讲的,我们可以和某些函数捆绑在一起实现。
|
||||
|
||||
Pygame支持mp3、ogg、wav音频和音效的播放。音乐的模块都在pygame.mixer中,这里面包括音乐和音效。
|
||||
|
||||
我们在使用音频部分模块的时候,需要先初始化一次。
|
||||
|
||||
```
|
||||
pygame.mixer.init()
|
||||
|
||||
```
|
||||
|
||||
这个初始化应该在pygame.init()的初始化之后。
|
||||
|
||||
我们来看一下具体的函数,这些函数,存在在pygame.mixer.Sound模块下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/29/4d/299c0650d736f939189c49b32eb2b54d.jpg" alt="">
|
||||
|
||||
我们再来看一下Pygame.mixer.music音乐模块。我们可以尝试一下载入音频并且播放。
|
||||
|
||||
```
|
||||
pygame.mixer.music.load('bgm.mp3')
|
||||
pygame.mixer.music.set_volume(0.5)
|
||||
pygame.mixer.music.play()
|
||||
s1 = pygame.mixer.Sound('a.wav')
|
||||
s1.set_volume(0.5)
|
||||
s2 = pygame.mixer.Sound('b.wav')
|
||||
s2.set_volume(0.5)
|
||||
|
||||
```
|
||||
|
||||
我来解释一下这段代码。
|
||||
|
||||
刚开始,我们载入了一个名叫bgm的mp3文件,告诉程序需要载入这个文件,然后调整音量到0.5,随后就是play,也就是播放,播放是在程序的后台播放,然后程序会接着跑到下面的代码行。
|
||||
|
||||
随后,我们使用Sound模块,Sound模块初始化会载入a.wav,然后返回一个对象,这个对象设置音量为0.5,随后再初始化一次,载入b.wav,然后设置音量为0.5。
|
||||
|
||||
到这里为止,我们已经将所有的初始化、设置都在游戏的循环外做好了。
|
||||
|
||||
随后,我们需要结合前几节的内容,在循环里面,对飞机碰撞进行声音的操作,比如出现爆炸声的时候,播放什么声音;碰撞结束,播放另一种的声音。
|
||||
|
||||
```
|
||||
if True == collide(pln, (100,300+y1), enm, (100,20+y2)):
|
||||
s1.play()
|
||||
else:
|
||||
s2.play()
|
||||
for event in pygame.event.get():
|
||||
if event.type == QUIT:
|
||||
pygame.quit()
|
||||
if event.type == KEYDOWN:
|
||||
if event.key == K_p:
|
||||
pygame.mixer.music.pause()
|
||||
if event.key == K_r:
|
||||
pygame.mixer.music.unpause()
|
||||
|
||||
```
|
||||
|
||||
首先,我们使用**collide函数**。这在前面几章有过详细的说明。
|
||||
|
||||
这是一段检测飞机碰撞的代码,如果飞机碰撞了的话,就会返回True,如果返回True的话,我们就播放s1音频,否则就播放s2音频。当然,这个s2音频可能会一直在播放(因为一直没有碰撞)。
|
||||
|
||||
随后就是**事件监测**,如果检测到K_p,就是按下键盘p,就让音乐停止,使用pause函数;如果按下r键,就恢复播放。
|
||||
|
||||
我们在Pygame上的操作已经基本结束了,但是,音频和音效的内容并没有结束。
|
||||
|
||||
在游戏编程中,我们需要嵌入音频和音效,特别是在没有Pygame的时候,如果有一些游戏引擎没有提供音频库的话,我们就需要自己使用第三方的音频库。虽然可以使用耳熟能详的ffmpeg,但是感觉有点大材小用了,所以我们需要一个专门的音频库。
|
||||
|
||||
在这里,我推荐**BASS音频库**。你可以去 [http://www.un4seen.com](http://www.un4seen.com) 下载开发库。这个音频库是不开源的,如果你只是自己开发游戏玩玩,非商业目的,就可以使用。如果是商业使用,那就需要购买证书。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/e2/9022635f73854d8b464a188c585ee6e2.jpg" alt="">
|
||||
|
||||
在这个页面上,我们点击download按钮,就会下载最新版本的开发库。解压缩下来,会出现对应几个语言的开发目录。
|
||||
|
||||
其中bass.dll文件是动态链接库,要使用的话,可以在c文件夹下,使用lib库和bass.h进行头文件包含进行编程。
|
||||
|
||||
我们来看一下,如何使用C/C++语言加入Bass引擎的代码。
|
||||
|
||||
```
|
||||
BASS_Init(-1, 44100, 0, hwnd, NULL);
|
||||
HSTREAM s = BASS_StreamCreateFile(false, "a.mp3", 0, 0, 0);
|
||||
BASS_ChannelPlay(s, false);
|
||||
BASS_StreamFree(s)
|
||||
|
||||
```
|
||||
|
||||
首先,我们将 BASS 库初始化,初始化的参数是:设备、输出比率、标志位(比如8位音质、立体声、3D等等)、Windows句柄。你也可以输入0。最后一个是clsid,就是用于初始化DirectSound的类的ID,一般会使用NULL。
|
||||
|
||||
随后,开始从文件建立一个流,BASS_StreamCreateFile函数,返回一个HSTREAM。HSTREAM其实是一个DWORD类型。
|
||||
|
||||
这个函数里的参数,我也解释一下。
|
||||
|
||||
<li>
|
||||
第一个参数是内存。如果传入true的话,就将这个流保存在内存中;否则的话,就不保存在内存中。
|
||||
</li>
|
||||
<li>
|
||||
第二个参数是音频文件名。这个参数和第一个参数会联动。当第一个参数保存在内存中的时候,就填入内存地址,否则就填入文件名。
|
||||
</li>
|
||||
<li>
|
||||
第三个参数是偏移量,也就是文件从哪里开始播放。当然这个参数只在第一个参数为false,不保存在内存的情况下起作用。
|
||||
</li>
|
||||
<li>
|
||||
第四个参数是长度,如果填入0,就是所有长度。
|
||||
</li>
|
||||
<li>
|
||||
最后一个是标志位,填入的是创建模式,比如是循环播放方式,还是软件解码模式等等。
|
||||
</li>
|
||||
|
||||
接下来就是开始播放,第一个填入的是刚才返回的流的句柄,第二个参数是是否重新开始播放。最后一个就是播放完后进行回收资源,删除句柄。
|
||||
|
||||
```
|
||||
float v; DWORD r;
|
||||
BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, 100);
|
||||
v = BASS_GetVolume();
|
||||
v = 200;
|
||||
BASS_SetVolume(v);
|
||||
r = BASS_ChannelIsActive(s);
|
||||
if(r == BASS_ACTIVE_PAUSED)
|
||||
...
|
||||
else if(r == BASS_ACTIVE_PLAYING)
|
||||
...
|
||||
else if(r == BASS_ACTIVE_STOPPED)
|
||||
...
|
||||
else if (r == BASS_ACTIVE_STALLED)
|
||||
..
|
||||
|
||||
```
|
||||
|
||||
接下来就是调整音量以及获取播放的状态功能。
|
||||
|
||||
其中BASS_SetConfig中,第一个参数是选项,第二个参数是调整音量的值,BASS_CONFIG_GVOL_STREAM的意义是全局的流的音量。
|
||||
|
||||
随后我们就开始取得音量,BASS_GetVolume是获取系统的音量,并不是流的音量,第五行代码就是设置系统音量。
|
||||
|
||||
接下来,我们就要获取播放的状态。在BASS_ChannelIsActive的函数内填入流的句柄,随后获取返回值,然后使用返回值进行比较,其中BASS_ACTIVE_PAUSED,就是播放状态暂停,BASS_ACTIVE_PLAYING是正在播放中或者录音状态,BASS_ACTIVE_STOPPED是停止状态,或者流句柄并不是有效的,BASS_ACTIVE_STALLED是停滞状态。
|
||||
|
||||
一般的原因是,播放的状态缺少样本数据,流的播放停滞了,如果数据足够播放的话,就会自动恢复。
|
||||
|
||||
BASS库还有许许多多的函数和功能,就不在这里过多阐述了。
|
||||
|
||||
## 小结
|
||||
|
||||
我来总结一下。今天我们讲解了Pygame中音频和音效的播放。你应该记住这些东西。
|
||||
|
||||
<li>
|
||||
在Pygame中,播放音乐是不需要进行多线程控制的。它本身就会在后台进行播放。
|
||||
</li>
|
||||
<li>
|
||||
所有的音乐和音效都在pygame.mixer模块中,如果载入的是音乐,就使用music模块;如果载入的是音效,就使用Sound模块。
|
||||
</li>
|
||||
<li>
|
||||
随后我们介绍了BASS音频库。这几乎是最专业的音频库了。由于是C接口,所以通用多种语言,你可以使用.NET或者VB等语言来应用。当然如果要进行后台播放、多个频道播放等功能,你需要编写多线程的代码,并没有Pygame那么轻松,这里面很多事情需要自己去做。
|
||||
</li>
|
||||
|
||||
现在给你留一个小问题。
|
||||
|
||||
在pygame.mixer.music模块中,如何播放一首音乐后立刻播放另外一首音乐?
|
||||
|
||||
欢迎留言说出你的看法。我在下一节的挑战中等你!
|
153
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第6讲 | 从0开始整理开发流程.md
Normal file
153
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第6讲 | 从0开始整理开发流程.md
Normal file
@@ -0,0 +1,153 @@
|
||||
<audio id="audio" title="第6讲 | 从0开始整理开发流程" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0b/83/0b7bfbe4ae23cf73e4a527cbf658ab83.mp3"></audio>
|
||||
|
||||
在第一模块的基础知识中,我已经讲过开发一款游戏需要的一些背景知识。对于2D游戏和3D游戏的区别、从程序到美术、从2D、伪3D到3D等方面,我都进行了逐一地阐述。除此之外,对于任何类型的游戏开发都非常重要的三个概念:游戏引擎、底层绘图接口、地图编辑器,我也进行了一些我个人的解读。
|
||||
|
||||
有了这些背景知识,这一节,我来带你整理一下整个游戏开发流程以及流程中所需要的工具。
|
||||
|
||||
## 1.选择开发环境
|
||||
|
||||
我们默认是在Windows环境下进行开发,至于是什么版本的Windows不需要做更多地阐述,你愿意用什么版本就用什么版本,因为几乎所有流行的Windows环境都能进行编程工作。至于我为什么选择Windows环境进行开发,那是因为:
|
||||
|
||||
<li>首先,在Windows环境下,拥有比较方便的**调试工具**。不管是Python脚本语言还是C/C++语言,都可以使用图形界面进行调试;
|
||||
</li>
|
||||
<li>其次,Windows下的**IDE开发环境**也比其他平台更多,你拥有更多的工具可供选择。另外,在开发游戏的时候,你可以选择OpenGL、DirectX或者SDL等图形库进行编程。作为游戏开发,DirectX几乎是不可或缺的标准,而我在第四节讲述底层绘图接口的时候说过,它是由微软提供的游戏编程接口,在Windows下提供了更为方便的底层调用。
|
||||
</li>
|
||||
<li>除了Windows外,Linux平台的**图形显卡驱动**几乎是不完善的,无法发挥显卡的最大优势。苹果平台又一家独大,开发人员只能为其定制专一的代码,开发难度比之Windows就大了不少。
|
||||
</li>
|
||||
|
||||
## 2.下载脚本工具
|
||||
|
||||
在开发过程中,我们需要用到Python、Lua或者Ruby等脚本工具。我们可以直接用Python或者Ruby开发简单的游戏模块的Demo。**由于脚本语言代码的简单和高可读性,所以由脚本语言入手,进行早期示例的代码剖析,是一个不错的选择。**
|
||||
|
||||
Python我们可以从python.org下载,Lua我们可以从lua.org下载,相应地,Ruby也可以在ruby-lang.org下载。为了考虑兼容性,Python建议使用2.7.x版本。Lua下载最新的版本即可。Windows下Python的源代码编译并不方便,所以建议下载MSI安装包,直接安装即可。因为之后我要使用Pygame进行示范,所以建议你使用32位的Python版本。
|
||||
|
||||
## 3.选择编程语言版本
|
||||
|
||||
在开发的过程中,一定会用到C/C++语言。
|
||||
|
||||
如果你要使用VC++的话,会涉及购买、安装和配置等情况。为了使这个专栏的内容尽量简洁、可用,我建议使用Windows下,移植版本的GCC和G++进行编译(也就是MinGW移植版),GCC版本为4.2.x或者以上版本。有人说这些版本太老了,我认为,**初学阶段,版本越新,意味着你需要知道的编译器内容和编译器开关就越多,**因此建议你选择较为稳定的4.2.x或以上版本。
|
||||
|
||||
对于C++而言,我们也不需要用到最新的C++标准,比如C++11等。对于C语言,我们默认使用C89或者C99都是可以的。简洁、高效、显而易见,是我一向遵从的原则。
|
||||
|
||||
## 4.下载编译器
|
||||
|
||||
关于C/C++,你可以去MinGW官网下载4.2.x版本。当然如果你希望使用其他更新的版本也不是不行,你可以直接下载安装器,来获取编译器的版本。下载地址是这个:[https://sourceforge.net/projects/mingw/files/Installer/](https://sourceforge.net/projects/mingw/files/Installer/)
|
||||
|
||||
你也可以按照你的需求定制下载。如果要成为完整的编译器,必须下载这些内容:
|
||||
|
||||
<li>MinGW (C/C++) Compiler
|
||||
</li>
|
||||
<li>Binutils
|
||||
</li>
|
||||
<li>Windows32 API
|
||||
</li>
|
||||
<li>MinGW Runtime Libraries
|
||||
</li>
|
||||
<li>GNU Debugger (GDB)
|
||||
</li>
|
||||
<li>GNU Make
|
||||
</li>
|
||||
|
||||
一般来讲,使用安装器下载的编译器都是最新版本的,如果你需要下载特定的版本号,你可以在这个网址 [https://sourceforge.net/projects/mingw/files](https://sourceforge.net/projects/mingw/files) 下,找到相应的编译工具目录和对应的版本号。
|
||||
|
||||
这样,C/C++编译器就下载完成了。如果你是自己下载特定版本号的话,需要将所有包解压缩在一个指定的目录下,解压缩出来的目录结构一般有这几个常用子目录:bin、include、lib、share、 local、etc、var。
|
||||
|
||||
## 5.选择C/C++和Python的IDE
|
||||
|
||||
接下来,我们需要一套IDE来帮助我们进行C/C++和Python的开发。
|
||||
|
||||
**C/C++方面,我选择使用免费的MinGW Studio来完成。**MinGW Studio的界面绝大部分模仿了经典的VC6的IDE界面。虽然相对于更时髦的收费编译器来说,MinGW Studio没有特别智能的代码提示,但是可以方便我们完成程序的调试。
|
||||
|
||||
我们可以通过搜索引擎搜索到并且顺利地下载MinGW Studio。有一些IDE是自带C/C++编译器的,这种包也没有问题。如果你对C/C++这部分设置比较熟悉,你也可以自由选择其他IDE,比如DevCpp、CodeLite、CodeBlocks等等。
|
||||
|
||||
**至于Python方面,我们可以使用Wing IDE。**这是一个付费项目。也可以使用国内程序员编写的Ulipad,另一个付费软件Komodo,用来做Python、Ruby的IDE都很合适。至于Wing IDE,我们可以在wingware.com下载最新版本。
|
||||
|
||||
## 6.带你一起测试编译器的运作
|
||||
|
||||
首先,我们需要先测试编译器是否运作顺利,所以我选择Lua来进行编译。在将来,需要使用Lua的时候,必须将之编译为**静态库**或者**可执行文件**。
|
||||
|
||||
我们打开MinGW Studio,界面是这样的:
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/e9/13/e9ff8b431d455f4a03cef636dd838e13.jpg">
|
||||
|
||||
我们可以在Edit->Options->Compiler选项里设置具体的编译器路径,以便让IDE找到编译器来开始工作。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/d8/e0/d8affea68aed61a67f25a8bd04d766e0.jpg">
|
||||
|
||||
一般编译器的路径都会自带bin目录,所以设置的目录必须在bin目录的上级目录。比如我们设置的MinGW编译器路径为C:\MinGW,那么bin目录就是C:\MinGW\bin,所以在IDE的设置下,只需要设置为C:\MinGW就可以了。
|
||||
|
||||
我们将下载到的Lua5.x.x.tar.gz解压缩到某个目录。在我写文章的时候,Lua的最新版本是5.3.4。在这个目录下,并没有我们所需要的MinGW Studio的项目文件,所以我们需要手工建立一个。我们在File->New->Projects选项下,建立一个Win32 Static Library,也就是Windows静态库,将名字设为lua。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/01/84/016bd92e192c42b2b53aa076c6277f84.jpg">
|
||||
|
||||
然后我们将文件添加到项目里面,在项目虚拟目录里面,点击鼠标右键。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/0b/29/0bf22ba55c20fd93198d335260842629.jpg">
|
||||
|
||||
在弹出的选择文件对话框里,选中刚才解压缩出来的Lua目录,选择src目录下的所有或椎为.c的文件,随后,我们将 lua.c 排除在外(选中,右键,选择移除出项目)。因为我们制作静态库的时候,可以不用这个文件。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/90/d2/90a6974f6a57c8530f52dda064878ad2.jpg">
|
||||
|
||||
我们可以点击Build->Build选项来进行编译,编译完成后,如果编译成功的话,我们会在Debug目录下看到一个.a文件。如果你的项目名叫lua,那么制作出来的静态库就是liblua.a,所以个文件就是我们以后要用到**Lua静态库**。
|
||||
|
||||
如果你有一定的编程经验的话,可能已经看到,我们现在编译出来的是Debug,是调试版本,我们暂且不去管它。这个在后面我们会进行详细地探讨,目前我们只需要知道这一系列的使用方式和方法就可以了。
|
||||
|
||||
我们已经将Lua编译完毕了,后续的文章中我会教你使用Lua静态库。
|
||||
|
||||
接下来,我们尝试使用Python语言。你可以使用任何一个上述推荐的专业IDE来编写Python代码。实际上,Python的IDE不需要过多的配置。因为安装在Windows机器上后,Python的路径会被注册到系统。通常IDE会自动找到Python执行文件,并且,IDE的Shell窗口将会正确地找到Python并看到其互动窗口,就像这张图的内容:
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/05/82/057511edf04d2d71904b44f1462c2182.jpg">
|
||||
|
||||
现在,我们尝试在IDE中编写一小段Python测试代码,然后跑一下。程序运行结果就是打印一个 test字符串。
|
||||
|
||||
```
|
||||
import os, sys
|
||||
if __name__ == '__main__':
|
||||
print 'test'
|
||||
|
||||
```
|
||||
|
||||
最后,将该文件保存后缀为.py的文件,这就是Python源代码。
|
||||
|
||||
## 7.专为Python开发的游戏库Pygame
|
||||
|
||||
在这里,为你介绍一个专门为Python开发的游戏库Pygame。至于为什么选择Pygame,我在第四节讲底层绘图接口的时候已经解释了一些。这里再说一下。
|
||||
|
||||
Pygame包装了SDL的实现。在编写2D游戏方面,它的表现可以用三个词来形容:**成熟,稳定,简单**。它把一些细枝末节隐藏在了Python语法背后,当然也有Ruby语言封装的RubyGame,但是很多人对于这种魔幻的语言并不是特别了解,所以选择简洁的Python语法+SDL库封装是最合适的选择。
|
||||
|
||||
今后我们会编写游戏的示例Demo,一些轻量级的、游戏的某一部分的说明和介绍,我会使用Pygame进行简单的阐述。Windows版本我们点击这个网址下载这个版本的源代码。 [http://www.pygame.org/ftp/pygame-1.9.1release.zip](http://www.pygame.org/ftp/pygame-1.9.1release.zip) 如果你不愿意下载源代码,也可以根据自己对应的Python版本号下载对应的二进制包,支持Python 2.4 到3.2的版本。
|
||||
|
||||
```
|
||||
pygame-1.9.1.win32-py2.7.msi 3.1MB
|
||||
pygame-1.9.1release.win32-py2.4.exe 3MB
|
||||
pygame-1.9.1release.win32-py2.5.exe 3MB
|
||||
pygame-1.9.1.win32-py2.5.msi 3MB
|
||||
pygame-1.9.1.win32-py2.6.msi 3MB
|
||||
pygame-1.9.2a0.win32-py2.7.msi 6.4MB
|
||||
pygame-1.9.1.win32-py3.1.msi 3MB
|
||||
pygame-1.9.2a0.win32-py3.2.msi 6.4MB
|
||||
|
||||
```
|
||||
|
||||
如果你安装的是64位Windows和64位Python,注意Pygame版本和Python都需要是32位的,才能完美兼容和使用。
|
||||
|
||||
## 小结
|
||||
|
||||
好了,这节内容差不多了。我来总结一下。在这一节中:
|
||||
|
||||
<li>我先从各操作系统下的调试工具、IDE开发环境、显卡驱动等三个方面,分析了为什么选择在Windows环境下进行开发;
|
||||
</li>
|
||||
<li>然后,我还带你梳理了一遍开发所需要的语言和工具,并且提供了下载的网址和安装的方法;
|
||||
</li>
|
||||
<li>之后,我还带你测试了Lua脚本语言在编译器中的编译并且生成了静态库文件。
|
||||
</li>
|
||||
<li>最后给你介绍了Pygame,今后将会用到这个Python下的2D游戏开发引擎。
|
||||
</li>
|
||||
|
||||
最后,给你留一个思考题吧。
|
||||
|
||||
你可以结合之前几节的内容,思考一下,Pygame绑定SDL绘图接口是如何实现的?
|
||||
|
||||
欢迎留言说出你的看法,我在下一节的挑战中等你!
|
||||
|
||||
|
227
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第7讲 | 如何建立一个Windows窗体?.md
Normal file
227
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第7讲 | 如何建立一个Windows窗体?.md
Normal file
@@ -0,0 +1,227 @@
|
||||
|
||||
今天,我要跟你分享开发Windows游戏的第一步,建立窗体。
|
||||
|
||||
上一节,我讲解Python和C++的编译器,以及它们各自对应的IDE该如何选择,并且测试了C/C++的运行,编译了一个Lua静态库。准备工作基本上算是完成了。
|
||||
|
||||
如果你有一些编程功底,应该知道建立Windows的窗体所需的一些基础知识。如果你经验稍丰富一些,还应该知道Delphi、C++Builder、C#等等。这些工具都可以帮助你非常方便地做出一个空白窗体,但是这些窗体并没有游戏的绘图系统,所以它们只是“建立了一个标准窗体”而已。因此,虽然建立窗体是我们这一节的内容,但**我们要探讨的是,在窗体背后,Windows系统做了什么。**
|
||||
|
||||
## Windows窗体由哪些部分构成?
|
||||
|
||||
我们常规意义上的Windows窗体,由下列几个部分组成。
|
||||
|
||||
<li>**标题栏**:窗口上方的鼠标拖动条区域。标题栏的左边有控制菜单的图标,中间显示的是程序的标题。
|
||||
</li>
|
||||
<li>**菜单栏**:位于标题栏的下面,包含很多菜单,涉及的程序所负责的功能不一样,菜单的内容也不一样。比如有些有文件菜单,有些就没有,有一些窗体甚至根本就没有菜单栏。
|
||||
</li>
|
||||
<li>**工具栏**:位于菜单栏的下方,工具栏会以图形按钮的形式给出用户最常使用的一些命令。比如,新建、复制、粘贴、另存为等。
|
||||
</li>
|
||||
<li>**工作区域**:窗体的中间区域。一般窗体的输入输出都在这里面进行,如果你接触过Windows窗体编程,就知道在这个工作区域能做很多的事情,比如子窗体显示、层叠,在工作区域的子窗体内进行文字编辑等等。你可以理解成,游戏的图形图像就在此处显示。
|
||||
</li>
|
||||
<li>**状态栏**:位于窗体的底部,显示运行程序的当前状态。通过它,用户可以了解到程序运行的情况。比如的,如果我们开发出的窗体程序是个编辑器的话,我按了一下Insert键,那么状态栏就会显示Ins缩写;或者点击到哪个编辑区域,会在状态栏出现第几行第几列这样的标注。
|
||||
</li>
|
||||
<li>**滚动条**:如果窗体中显示的内容过多,不管横向还是纵向,当前可见的部分不够显示时,窗体就会出现滚动条,分为水平滚动条与垂直滚动条两种。
|
||||
</li>
|
||||
<li>**窗体缩放按钮**:窗体的缩放按钮在右上角,在窗体编程中属于System类目。这些缩放按钮依次为最小化、最大化和关闭按钮。
|
||||
</li>
|
||||
|
||||
我们来看一张标准的Windows窗体截图,这个软件名是Notepad++。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/cc/af/cc1d248bd1c76405ad73792112c33faf.jpg">
|
||||
|
||||
这是MSDN上对于窗体结构的说明:
|
||||
|
||||
```
|
||||
typedef struct tagWNDCLASSEX {
|
||||
UINT cbSize; //结构体大小,等于 sizeof(WNDCLASSEX)
|
||||
UINT style; //窗体的风格
|
||||
WNDPROC lpfnWndProc; //窗体函数指针
|
||||
int cbClsExtra; //附加在窗体类后的字节数,初始化是零
|
||||
int cbWndExtra; //附加在窗体实例化的附加字节数。系统初始化是零,如果一个应用程序使用WNDCLASSEX注册一个通过在资源中使用CLASS指令建立的对话框时,必须把这个成员设成DLGWINDOWEXTRA。
|
||||
HINSTANCE hInstance; //该对象的实例句柄
|
||||
HICON hIcon; //该对象的图标句柄
|
||||
HCURSOR hCursor; //该对象的光标句柄
|
||||
HBRUSH hbrBackground; //该对象的背景刷子
|
||||
LPCTSTR lpszMenuName; //菜单指针
|
||||
LPCTSTR lpszClassName; //类名指针
|
||||
HICON hIconSm; //与窗体关联的小图标,如果这个值为NULL,那么就把hIcon转换为大小比较合适的小图标
|
||||
} WNDCLASSEX, *PWNDCLASSEX;
|
||||
|
||||
```
|
||||
|
||||
## 使用C/C++编写Windows窗体
|
||||
|
||||
接下来,我将使用C/C++IDE来编写代码,完成一个默认窗体的开发,并让它运行起来。
|
||||
|
||||
```
|
||||
#include <windows.h>
|
||||
LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM, LPARAM);
|
||||
char szClassName[ ] = "WindowsApp";
|
||||
int WINAPI WinMain(HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nFunsterStil)
|
||||
|
||||
{
|
||||
HWND hwnd; /* 指向我们窗体的句柄 */
|
||||
MSG messages; /* 保存发往应用的消息 */
|
||||
WNDCLASSEX wincl; /* 前面详细介绍过的WNDCLASSEX结构的对象 */
|
||||
wincl.hInstance = hThisInstance;
|
||||
wincl.lpszClassName = szClassName;
|
||||
wincl.lpfnWndProc = WindowProcedure;
|
||||
wincl.style = CS_DBLCLKS;
|
||||
wincl.cbSize = sizeof(WNDCLASSEX);
|
||||
|
||||
```
|
||||
|
||||
上述代码开始给WNDCLASSEX结构对象赋值。
|
||||
|
||||
```
|
||||
/* 使用默认图标以及鼠标指针 */
|
||||
wincl.hIcon = LoadIcon(NULL, IDI_APPLICATION);
|
||||
wincl.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
|
||||
wincl.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
wincl.lpszMenuName = NULL; /* 没有菜单栏 */
|
||||
wincl.cbClsExtra = 0; /* 没有多余的字节跟在窗体类的后面 */
|
||||
wincl.cbWndExtra = 0;
|
||||
wincl.hbrBackground = (HBRUSH) GetStockObject(LTGRAY_BRUSH);
|
||||
if(!RegisterClassEx(&wincl)) return 0;
|
||||
|
||||
```
|
||||
|
||||
代码在窗口过程调用函数的时候,将地址赋值给lpfnWndProc,然后呼叫RegisterClassEx(&wincl)注册窗口类,系统就拥有了窗口过程函数的地址。如果注册失败,则返回0。
|
||||
|
||||
```
|
||||
hwnd = CreateWindowEx( 0, /* 扩展风格为0*/
|
||||
szClassName, /* 类名 */
|
||||
"Windows App", /* 窗体抬头标题 */
|
||||
WS_OVERLAPPEDWINDOW, /* 默认窗体 */
|
||||
CW_USEDEFAULT, /* 让操作系统决定窗体对应Windows的X位置在哪里 */
|
||||
CW_USEDEFAULT, /* 让操作系统决定窗体对应Windows的Y位置在哪里 */
|
||||
544, /* 程序宽度 */
|
||||
375, /* 程序高度 */
|
||||
HWND_DESKTOP, /* 父窗体的句柄,父窗体定义为Windows桌面,HWND_DESKTOP 是系统定义的最顶层的托管的窗体 */
|
||||
NULL, /* 没有菜单 */
|
||||
hThisInstance, /* 程序实例化句柄 */
|
||||
NULL /* 指向窗体的创建数据为空 */
|
||||
);
|
||||
ShowWindow(hwnd, nFunsterStil);
|
||||
/* 要显示窗体,使用的是ShowWindow函数 */
|
||||
while(GetMessage(&messages, NULL, 0, 0))
|
||||
{
|
||||
TranslateMessage(&messages);
|
||||
DispatchMessage(&messages);
|
||||
}
|
||||
return messages.wParam;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
建立并显示窗体,在循环内将虚拟键消息转换为字符串消息,随后调度一个消息给窗体程序。
|
||||
|
||||
```
|
||||
LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message) /* 指向消息的句柄 */
|
||||
{
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
default:
|
||||
return DefWindowProc(hwnd, message, wParam, lParam);
|
||||
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
最后是消息处理。当窗体程序接收到某些操作的时候,比如键盘、鼠标等等,就会呼叫 DispatchMessage(&messages);函数将消息回调给系统,系统通过注册的窗口类得到函数指针并且通过函数指针调用函数对消息进行处理。
|
||||
|
||||
还有一个经常用到的函数就是MoveWindow,就是移动已经建立的窗体。MoveWindow函数用来改变窗口的位置和尺寸,如果窗体本身就按照计算机的屏幕对齐左上角,对于窗体内的子窗体,就对齐父窗体的左上角。
|
||||
|
||||
```
|
||||
BOOL MoveWindow( HWND hWnd,/* 窗体句柄 */
|
||||
int x, /* 窗体左上角起点x轴 */
|
||||
int y, /* 窗体左上角起点y轴 */
|
||||
int nWidth, /* 窗体宽度 */
|
||||
int nHeight, /* 窗体高度 */
|
||||
BOOL bRepaint = TRUE /* 是否重新绘制,如果是true系统会发送WM_PAINT到窗体,然后呼叫UpdateWindow函数进行重新绘制,如果是false则不重新绘制*/
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
MoveWindow会给窗体发送WM_WINDOWPOSCHANGING,WM_WINDOWPOSCHANGED,WM_MOVE,WM_SIZE和WM_NCCALCSIZE消息。
|
||||
|
||||
类似的功能还有SetWindowPos,SetWindowPos功能更强大,可以设置更多的参数。
|
||||
|
||||
这是基本的使用C/C++绘制Windows窗体的流程,也是标准的Windows窗体的创建和显示。在后续的分享中,我也会使用GDI或者GDI+来绘制一些的内容。
|
||||
|
||||
## 使用Python编写Windows窗体
|
||||
|
||||
说完了C/C++系统编程编写的Windows窗体,接下来来看一下,如何使用Python来编写Windows窗体。
|
||||
|
||||
Python的Windows窗体编程一般会使用默认的Tinker库。不过用别的窗体库也可一建立一个窗体,比如Python版本的QT库或者wxPython。
|
||||
|
||||
现在来看一下,使用默认的Tinker来建立一个窗体。
|
||||
|
||||
```
|
||||
import Tkinter
|
||||
|
||||
def my_window(w, h):
|
||||
ws = root.winfo_screenwidth()
|
||||
hs = root.winfo_screenheight()
|
||||
x = (ws/2) - (w/2)
|
||||
y = (hs/2) - (h/2)
|
||||
root.geometry("%dx%d+%d+%d" % (w, h, x, y))
|
||||
|
||||
root = Tkinter.Tk(className='python windows app')
|
||||
my_window(100, 100)
|
||||
root.mainloop()
|
||||
|
||||
```
|
||||
|
||||
运行的结果是这样的。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/65/b8/657a175b08898385f555f7613d1a55b8.jpg">
|
||||
|
||||
我们可以看到左上角有一个Tk的标识,这是Tinker的默认图标。目前,我们只是建立了一个Windows的窗体,并不能直接编写游戏。除此之外,我们还必须要知道这些建立窗体的具体的细节。
|
||||
|
||||
不过,就像前面的文章所说,OpenGL并不附带任何关联窗体的编程,所以如果你使用的是OpenGL的接口来编写代码,稍微修改一下,这些窗体就能成为游戏屏幕窗体。
|
||||
|
||||
**游戏所有的内容都是在一个循环内完成的,即我们所有的绘图、线程、操作、刷新,都在一个大循环内完成**,类似我们在前面看到的代码。
|
||||
|
||||
```
|
||||
while(GetMessage(&messages, NULL, 0, 0))
|
||||
{
|
||||
TranslateMessage(&messages);
|
||||
DispatchMessage(&messages);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
以及使用Python编写的代码的窗体中,也会看到一个循环函数:
|
||||
|
||||
```
|
||||
root.mainloop()
|
||||
|
||||
```
|
||||
|
||||
在这个while循环中,消息的派发都在此完成。游戏也一样,我们所有游戏内的代码几乎都在循环内完成。你可以想象**一个循环完成一个大的绘制过程,第二个循环刷新前一次绘制过程,最终类似电影一样,完成整个动画的绘制以及不间断的操作。**
|
||||
|
||||
在建立Windows窗体的时候,程序会从入口函数WinMain开始运行,定义和初始化窗体类,然后将窗体类实例化,随后进行消息循环获取消息,然后将消息发送给消息处理函数,最后做出相应的操作。
|
||||
|
||||
## 小结
|
||||
|
||||
总结一下今天所说的内容,我们编写了一个标准的Windows窗体,在编写的过程中:
|
||||
|
||||
<li>窗体的结构是在建立窗体之前就定义下来的;
|
||||
</li>
|
||||
<li>所有长时间运行的程序,包括游戏,包括Windows本身都是一个大循环。我们在这个循环里做我们想做的事情,直到循环结束;
|
||||
</li>
|
||||
<li>如果使用脚本语言的方式编写窗体,就不需要关心那么多的东西,只需要定义坐标、位置和窗体名称即可。
|
||||
</li>
|
||||
|
||||
最后,给你留一道小思考题吧。
|
||||
|
||||
你经常会看到有一些游戏是需要全屏才能进行的。既然我们在这里建立了一个窗体,那请问你,全屏是怎么做到的呢?
|
||||
|
||||
欢迎留言说出你的看法,我在下一节的挑战中等你!
|
||||
|
||||
|
193
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第8讲 | 如何区分图形和图像?.md
Normal file
193
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第8讲 | 如何区分图形和图像?.md
Normal file
@@ -0,0 +1,193 @@
|
||||
<audio id="audio" title="第8讲 | 如何区分图形和图像?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9b/dc/9b6b806d82b95fb8990a72584daadadc.mp3"></audio>
|
||||
|
||||
据我所知,很多人可能都分不清图形和图像这两个概念:一种情况是你可能会觉得区分图形和图像这两个概念并没有什么实质的用途,于是就没关心过;另一种情况是,你可能朦胧中对图形和图像的区别有一些了解,但是不够深入或者不够透彻,也说不出一个所以然。没关系,今天我就来深入浅出地给你讲一下,图形和图像背后的那些事儿。
|
||||
|
||||
既然我们是做游戏开发,那首先我们得知道,从专业地角度讲,区分图形和图像对我们的开发工作有什么帮助。简单地说,**搞清楚了游戏开发中绘制、载入、保存的究竟是图形还是图像,你会对接口函数的使用有一个更深入的认识。**
|
||||
|
||||
比如,如果是图形接口,可能它的接口函数是:
|
||||
|
||||
```
|
||||
Surface* DrawSomething(int start_x, int start_y, int finish_x, int finish_y);
|
||||
|
||||
```
|
||||
|
||||
如果是图像接口,它的接口函数函数看起来可能是这个样子:
|
||||
|
||||
```
|
||||
Surface* LoadFromFile(const string& filename);
|
||||
|
||||
```
|
||||
|
||||
## 如何区分图形和图像?
|
||||
|
||||
从广义上说,所有我们人肉眼能看到的对象,都是图形。从狭义上说,图形是我们所看到的一种点、线、面的描述对象。
|
||||
|
||||
**图像,是由数据组成的任意像素点的描述对象。**比如我们所看到的照片。在电脑中,图形的显示过程是有一定顺序(比如从左往右)的,而图像则是按照像素点进行显示的。电脑对于图形的编辑、修改更为简单方便,因为单一的图形具有特殊的属性(比如圆圈的直径、颜色等等,因为这些在这个图形建立的时候就固定了下来)。
|
||||
|
||||
对于图像进行编辑则非常困难,软件需要用一些特殊的算法来计算图像的色块、区域、描边等等,来安排图像该如何进行编辑,有一些甚至还需要用到深度学习的方法来辨别图像的显示区域、显示的内容等等,所以图像的修改比之图形的修改要困难。
|
||||
|
||||
那么你可能就会问了,既然前面说,任何眼睛看到的对象,都是图形,那么我觉得图形也是一种图像,这么说对不对呢?如果按照载体来说,图形也是一种图像,这种说法是对的。因为一张JPG图片可能存储的是一幅照片,也可能存储一幅三角形的图形。虽然本质不一样,但是由于存储的形式是以图像的形式存储的,在电脑看来,这个三角形就是一幅图像。但是如果你在游戏中使用函数画出了一个三角形,那就是图形了。
|
||||
|
||||
所以,严格来说,**图形其实是图像的一种抽象表现形式**。一般来讲,图形的轮廓并不复杂,比如一个圆圈、一个方块、一个三角形、一条线、某些几何图形、工程上面使用的图纸和CAD等,这些都属于图形。图形的色彩也并不是很丰富。而图像一般都有复杂的轮廓、非常多的细节和颜色(当然也有纯单一的颜色,比如黑白照片)。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/fb/bc/fb2b9c4192fd7147c3346dc0da7423bc.jpg">
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/7d/0b/7d00b8af46c9455a24f5a6a3f77e650b.jpg">
|
||||
|
||||
所以,准确地说,图形和图像有不同的模式。当然,从计算机最底层的程序(显卡处理)来看,绘制图形和图像所经过的硬件处理几乎是一样的。一般显卡会经过这些流程进行图形、图像计算(2D)、显存,用来存取图形图像内容,GPU计算图像图像内容并渲染,最后输出到显示器。
|
||||
|
||||
从**图像的呈现方式**讲,只有通过图像的方式去呈现“图形”这个对象,才能看到图形,而在开发游戏的过程中,图形和图像的编程方式是截然不同的。比如我们要画线,那么可能会使用到一个叫DrawLine的函数。该函数里面需要输入线条的起始坐标,这就是图形的绘制方式。而在接下来的过程中,我将教你如何绘制图形和图像,并呈现出来。
|
||||
|
||||
## 跟我一起绘制图形和图形
|
||||
|
||||
现在,我们先用Pygame游戏库来建立一个窗体,然后开始绘制图形、载入图像。
|
||||
|
||||
在第五节的时候,我们已经讲过Pygame的安装和配置。在第六节的时候,我们讲过如何建立一个Windows窗体。现在从上到下,我们一起看一下这段代码。
|
||||
|
||||
```
|
||||
import pygame
|
||||
pygame.init()
|
||||
caption=pygame.display.set_caption('Python App')
|
||||
screen=pygame.display.set_mode([320,200]) #窗口大小为320*200
|
||||
while True:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
pygame.display.update()
|
||||
screen.fill([255,255,255]) #用白色填充窗体
|
||||
sys.exit()
|
||||
|
||||
```
|
||||
|
||||
在这段代码中,首先,我们需要告诉Python我们要引入Pygame。然后Pygame进行初始化(init)。在这个初始化的函数里,Pygame会初始化屏幕、声音、事件、按钮等一系列需要初始化的东西。随后,我们利用Pygame的display对象的set_caption函数来设置窗体的文字,将这个设置后的对象返回给caption变量。随后,再使用set_mode函数设置窗口大小,将窗口大小设置为320x200分辨率,将返回对象赋值给screen变量,最后screen拿到窗口句柄后,使用fill函数设置填充窗体的颜色,在这里填充的颜色是白色。
|
||||
|
||||
我们可以看到,使用Pygame游戏库来建立一个Windows窗体比前面我们提到的任何一种方式都快。那是因为**Pygame封装了建立窗体的代码和图形显示模块**。
|
||||
|
||||
我们在前面提到,**一个游戏是在一个大循环下形成的**,所以这里我们要补上一个大循环以确保这个程序不会立刻退出。
|
||||
|
||||
```
|
||||
while True:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
pygame.display.update()
|
||||
screen.fill([255,255,255]) #用白色填充窗体
|
||||
sys.exit()
|
||||
|
||||
```
|
||||
|
||||
这段代码的意思是,当条件为真(True)的时候(条件总是为真),进行一个循环。事实上这是个死循环,如果没有下面的退出代码的话。那么在这个循环里,从Pygame的event事件列表中取出event事件,然后进行判断,如果event的类型是退出类型(点击右上角的X关闭按钮),那么Pygame就退出,这个quit 函数就直接退出while大循环了。最终系统也退出sys.exit。
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/85/7c/85a8383c033ff7ec997e4e7ad9d1dd7c.jpg">
|
||||
|
||||
现在我们要在窗体上放上一个矩形和圆。我们先使用rect函数来画一个矩形:
|
||||
|
||||
```
|
||||
pygame.draw.rect(screen,[255,0,0],[150,10,0,40],0)
|
||||
|
||||
```
|
||||
|
||||
其中,draw中rect的定义为:rect(目标画布,颜色,位置,宽度)。
|
||||
|
||||
我们也可以用类似的方法来画一个圆:
|
||||
|
||||
```
|
||||
pygame.draw.circle(screen,[0,0,0],[top,left],20,1)
|
||||
|
||||
```
|
||||
|
||||
然后我们使用pygame.draw.circle()用来画圆形。circle函数具有5个参数:
|
||||
|
||||
<li>目标画布,在这里是screen
|
||||
</li>
|
||||
<li>颜色
|
||||
</li>
|
||||
<li>由左侧点和顶部点组成的圆形初始位置
|
||||
</li>
|
||||
<li>直径
|
||||
</li>
|
||||
<li>宽度
|
||||
</li>
|
||||
|
||||
现在我们将所有的代码合并起来看一下:
|
||||
|
||||
```
|
||||
import pygame
|
||||
pygame.init()
|
||||
caption=pygame.display.set_caption('Python App')
|
||||
screen=pygame.display.set_mode([320,200]) #窗口大小为640*480
|
||||
while True:
|
||||
for event in pygame.event.get():
|
||||
if event.type==pygame.QUIT:
|
||||
pygame.quit()
|
||||
pygame.draw.rect(screen,[255,0,0],[150,10,20,40],0)
|
||||
pygame.draw.circle(screen,[0,0,0],[20,50],20,1)
|
||||
pygame.display.update()
|
||||
screen.fill([255,255,255])#用白色填充窗口
|
||||
sys.exit()
|
||||
|
||||
```
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/41/4a/41ed7da2761a57bf68d990a660f7014a.jpg">
|
||||
|
||||
所以我们很容易就能看出来,**在Pygame游戏开发库里面,画一个图形是很容易的事情,你不需要知道太多的细节,只要将位置和颜色或者内容填充进去就可以了。**
|
||||
|
||||
我们可以在Pygame中使用Pygame.image.load来加载图像文件,这个函数支持各种图片格式。我们使用这个方法来加载一副PNG图片:
|
||||
|
||||
```
|
||||
obj = pygame.image.load("test.png").convert_alpha()
|
||||
|
||||
```
|
||||
|
||||
使用convert_alpha函数是因为这个函数会使用透明方法来绘制,所以我们在加载一个拥有alpha通道的图片的时候(比如TGA、PNG)的时候,可以使用这个方式。
|
||||
|
||||
然后使用blit方法将图像绘制出来:
|
||||
|
||||
```
|
||||
screen.blit(obj, (20,10))
|
||||
|
||||
```
|
||||
|
||||
或许你会问,blit是什么函数,我在这里简单介绍一下,blit这个函数会以各种函数形式出现在图形引擎的函数里面,比如FastBlit等等。这个函数具体负责将图像从某一个平面复制到另一个平面,或者将图像从内存复制到屏幕。简而言之,这个函数的功能就是将图像“绘制”在游戏窗体的屏幕上。
|
||||
|
||||
现在继续来看看blit函数。blit函数的第一个参数是加载完成的返回对象,第二个参数是绘制的坐标位置。最后我们需要update(更新)整个游戏窗体的绘制内容。
|
||||
|
||||
我们把载入图像的代码整合到刚才的代码中一块儿看一下。
|
||||
|
||||
```
|
||||
import pygame
|
||||
pygame.init()
|
||||
caption=pygame.display.set_caption('Python App')
|
||||
screen=pygame.display.set_mode([320,200]) #窗口大小为640*480
|
||||
|
||||
obj = pygame.image.load("test.png").convert_alpha()
|
||||
|
||||
while True:
|
||||
|
||||
for event in pygame.event.get():
|
||||
if event.type==pygame.QUIT:
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
screen.blit(obj, (20,10))
|
||||
pygame.display.update()
|
||||
screen.fill([255,255,255])#用白色填充窗口
|
||||
|
||||
```
|
||||
|
||||
最后呈现的效果是这样的:
|
||||
|
||||
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/9e/97/9e8f1747c6ddcdb3302de41d64a69c97.jpg">
|
||||
|
||||
## 小结
|
||||
|
||||
这一节,我带你学习了图形和图像的区别,使用Pygame绘制了最基础的图形,最后我们通过代码载入一副PNG图像并在屏幕上绘制出来。
|
||||
|
||||
给你留一个小练习吧。
|
||||
|
||||
请你结合上述代码,在游戏执行的大循环内,在游戏的窗体里面,绘制出一个从左到右移动的矩形、圆形或者图像。
|
||||
|
||||
之后,针对一些实操性强的内容,我都会适时给你留一些必要的练习。希望你每次都能动手去练习一下。同时,也欢迎你留言,说出你在练习中的疑惑和成果。温故而知新,相信你会有更多的收获!
|
||||
|
||||
我在下一节的挑战中等你!
|
||||
|
||||
|
186
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第9讲 | 如何绘制游戏背景?.md
Normal file
186
极客时间专栏/从0开始学游戏开发/第二章:客户端开发/第9讲 | 如何绘制游戏背景?.md
Normal file
@@ -0,0 +1,186 @@
|
||||
<audio id="audio" title="第9讲 | 如何绘制游戏背景?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/b4/0cc1687b5dcd6be8665c3980679f5ab4.mp3"></audio>
|
||||
|
||||
我在之前的文章中描述了各种基础知识,然后梳理了开发流程,并带你创建了一个窗体,现在我们要做的就是朝这个窗体里添加东西。
|
||||
|
||||
我会随着进度逐渐提升难度。就现阶段来讲,我们涉及的只是一些基础知识,并且只**需要将大部分的关注点放在我们要做的游戏内容上,并不需要关注过多的底层逻辑代码**。
|
||||
|
||||
做事情都有先后顺序,做游戏开发自然也是。为什么要学习先绘制游戏背景而不是别的什么,很简单,因为只有先绘制了游戏背景,才能进行后续的游戏图像遮挡、图形图像的显示等等操作。
|
||||
|
||||
不管你有没有玩过《超级玛丽》《魂斗罗》《雷电》之类的游戏,但一定对其画面不陌生。和我们要开始做的打飞机游戏一样,这种类型的2D游戏,其背景不是左右卷轴,就是上下卷轴。**所谓左右卷轴,就是游戏画面是横向的、左右运动的,而上下卷轴就是游戏画面是竖直对的、上下运动的。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/b1/2cd9958a20b5aca5650e9d6a99dec0b1.jpg" alt="">
|
||||
|
||||
像《雷电》这样的经典飞机游戏,就是属于上下卷轴的。上下卷轴的飞机游戏有一个特点,就是它是在空中,从凌驾于飞机之上的视角,往地面俯瞰的。因为是俯视角,所以我们可以很方便地看到游戏的整体地图,包括地面上的敌人、空中的敌人等等,层次感会很强。
|
||||
|
||||
因此,可以确定,我们要做的打飞机,也是一个上下卷轴的游戏。这样,我们就可以着手将需要的图片添加进去了。
|
||||
|
||||
我们要使用Pygame,先读取一个图片,让该图片成为游戏背景并载入进去。当下阶段,我们的图片从哪儿获得并不重要,因为在一个完整的游戏开发团队里面,都有专业的美术团队负责作图,但是现在我们没有,所以我就自己贴一幅图来代替正式的游戏背景。所以你现在只需要知道背景是如何贴上去的就好了。
|
||||
|
||||
和前面的文章说过的一样,我们需要先载入Pygame模块,并且定义一个变量background。我们将一幅名为lake,jpg的图片文件赋值给backgroud变量。
|
||||
|
||||
```
|
||||
import pygame
|
||||
background = 'lake.jpg'
|
||||
|
||||
```
|
||||
|
||||
然后,我们先把Pygame的所有组件都初始化。接下来,我们调用display类里的set_mode函数来对屏幕进行一个初始化。
|
||||
|
||||
```
|
||||
pygame.init()
|
||||
screen = pygame.display.set_mode((640, 480), 0, 32)
|
||||
pygame.display.set_caption("pygame game")
|
||||
|
||||
```
|
||||
|
||||
这里一共有三个参数,第一个参数是**分辨率**,比如我这里编写的是640x480的分辨率;第二个参数是**flag**,flag的参数我放在下面这个表里了;第三个参数是**32**,32代表的是颜色深度,这里是32位的意思。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/67/daecb1449c14d6f4c9b55922011bd667.jpg" alt="">
|
||||
|
||||
在设置完了窗体模式之后,后面的一段代码就是设置窗体的抬头文字,这里显示的是pygame game。
|
||||
|
||||
随后,我们要载入背景的图片。
|
||||
|
||||
```
|
||||
bg = pygame.image.load(background).convert()
|
||||
|
||||
```
|
||||
|
||||
我在前面的文章中也说过,这句话的意义是,载入backgroud图片。但是pygame.image.load这个函数返回的是一个surface,而.convert函数是来自于surface对象。你可以参考下面的代码来理解。
|
||||
|
||||
```
|
||||
surface_temp = pygame.image.load(background)
|
||||
bg = surface_temp.convert()
|
||||
|
||||
```
|
||||
|
||||
其次,bg这个变量也是一个surface,而convert函数的作用是改变一副图片的像素格式。convert有四个相同名字的重载函数。如果就像我们的代码里所示,convert没有任何参数,则表示直接返回一个surface对象。
|
||||
|
||||
好了,现在我们设置完了背景bg的surface,我们按照上面的文章,开始写一个大循环,并且在循环里面进行检测鼠标事件是不是退出操作,这是最基本的一项检测。
|
||||
|
||||
```
|
||||
while True:
|
||||
for event in pygame.event.get():
|
||||
if event.type == QUIT:
|
||||
pygame.quit()
|
||||
|
||||
```
|
||||
|
||||
和前面的文章一样,我们从event里取出事件列表,然后把每一个event的类型进行对比,如果发现有QUIT事件(鼠标点击X关闭按钮后),就直接退出游戏。完成这一步之后,就可以开始使用blit函数进行绘制屏幕的操作。
|
||||
|
||||
```
|
||||
screen.blit(bg, (0,0))
|
||||
|
||||
```
|
||||
|
||||
这句话的意思是,使用blit将bg在以游戏屏幕x,y轴为(0,0)的坐标位置在screen对象上绘制背景图像。然后我们需要update刷新屏幕,添加下面这行代码。
|
||||
|
||||
```
|
||||
pygame.display.update()
|
||||
|
||||
```
|
||||
|
||||
upadate这个函数是 pygame.display.flip 函数的优化版。因为pygame,display.flip是更新整块屏幕,所以如果加载的资源多,效率并不是很高,而update如果传递一个矩形值得参数的话,它会只更新这块矩形的内容,所以效率会比较高,但是不传递参数的话,默认还是会更新整块屏幕,但是这个函数不能用在set_mode的时候设置为OpenGL的模式下。
|
||||
|
||||
好了,我们该做的事情基本都做完了,现在我们来运行一下,看看效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/95/5c96858d8059e1729ce1e14d1c93fc95.jpg" alt="">
|
||||
|
||||
好了,背景是贴上去了。现在问题来了,要想让背景动起来该怎么做呢?如果在blit的时候,改变坐标是不是就可以移动背景图的位置了呢?你再开动脑筋想想,该怎么做才能让背景移动起来?
|
||||
|
||||
对的,我们只需要写一个循环,就可以将背景移动起来。
|
||||
|
||||
我们来修改一下大循环开始的代码。
|
||||
|
||||
```
|
||||
y_move = 0
|
||||
while True:
|
||||
for event in pygame.event.get():
|
||||
if event.type == QUIT:
|
||||
pygame.quit()
|
||||
screen.blit(bg, (0,y_move))
|
||||
y_move-=1
|
||||
|
||||
```
|
||||
|
||||
我们在大循环开始之前,在这段代码里定义了一个y值移动的变量,而我们每循环一次,blit就绘制一次屏幕,y值都会被减去1,所以我们每次看到的图片,都会不停往上移动,我们来看一下效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/81/135f2931b3a3168192ec9a5cc0a1ba81.jpg" alt="">
|
||||
|
||||
发现问题了没有,在移动的过程中,下方的图案居然没有被刷新,直接黏在了屏幕上,看起来是不是很恶心的样子?
|
||||
|
||||
我们应该怎么做才能达到正常的效果呢?也就是说,请你思考一下,应该怎样做,我们才可以将这个令人头疼的图像在移动的时候变得正常呢?
|
||||
|
||||
我们先来回顾一下,我们在循环里面做了哪些步骤:
|
||||
|
||||
<li>检测退出事件;
|
||||
</li>
|
||||
<li>在屏幕上绘制bg对象,坐标初始为(0, y);
|
||||
</li>
|
||||
<li>飞机每移动一格,坐标y减1;
|
||||
</li>
|
||||
<li>更新屏幕。
|
||||
</li>
|
||||
|
||||
看起来似乎没有什么问题,我再来带你梳理一下。
|
||||
|
||||
首先我们初始化的时候,屏幕是黑屏一块,没有任何图像,然后我们进入大循环,将bg对象绘制到屏幕上的时候,你觉得这时候我们的眼睛看到绘制的图像了吗?
|
||||
|
||||
如果你说是的话,那就大错特错了,因为**这个blit的动作,仅仅是绘制,而不是显示**。请记住这个区别:**绘制不等于显示**。
|
||||
|
||||
那你可能就要问了,既然绘制了,为什么不显示呢?要什么时候才能显示呢?答案是,要在update一次屏幕的时候,才会显示,这就是“更新”的作用。就像电影是一帧一帧的,如果没有下一帧更新,电影就会永远定格在某一秒。
|
||||
|
||||
所以问题逐渐就暴露出来了,我们再来重新梳理一下流程:
|
||||
|
||||
<li>检测退出事件;
|
||||
</li>
|
||||
<li>在屏幕上绘制bg对象,坐标初始为(0, y)(注意是绘制,不是显示);
|
||||
</li>
|
||||
<li>飞机每移动一格,坐标y就减1;
|
||||
</li>
|
||||
<li>更新屏幕,将第二步绘制的bg对象呈现在屏幕上,严谨地说,应该是将在update函数之前所有的绘制操作都更新一次并呈现在屏幕上)。
|
||||
</li>
|
||||
|
||||
好了,问题很清楚了,update函数只是将屏幕更新了一次,并未进行填充颜色或者“擦除”背景的操作,也就是**我们在移动y值的时候,整个屏幕不停地更新,然而没有擦除**。那么应该怎么将移动后的画面进行清理呢?
|
||||
|
||||
我们在update代码之后填入下面的代码。
|
||||
|
||||
```
|
||||
screen.fill([0,0,0])
|
||||
|
||||
```
|
||||
|
||||
fill操作拥有三个参数,其中第一个参数是**填充颜色;**第二个参数是**填充某一块区域**(如果不填入第二个参数,就会填充整个屏幕);第三个参数是**blit操作的特殊参数**,我们暂时可以不用管它。
|
||||
|
||||
所以,我们在代码里填充了黑色到整个屏幕,这样一来我们的屏幕操作变成这样:
|
||||
|
||||
<li>检测退出事件;
|
||||
</li>
|
||||
<li>在屏幕上绘制bg对象,坐标初始为(0, y);
|
||||
</li>
|
||||
<li>坐标的y减1;
|
||||
</li>
|
||||
<li>更新屏幕;
|
||||
</li>
|
||||
<li>填充屏幕区域为黑色。
|
||||
</li>
|
||||
|
||||
我们再运行一下看一下效果。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/66/4982b8ea20800fba341143140eeb8a66.jpg" alt="">
|
||||
|
||||
嗯,这下看起来正常了,屏幕不断往上移,并且没有拖着尾巴一样的图案了。
|
||||
|
||||
## 小结
|
||||
|
||||
我们在写2D游戏的时候要注意一点,就是:
|
||||
|
||||
我们要**想象游戏的每一帧就像电影的每一帧。每一帧做的事情,如果下一帧不去做,那么永远不会更新屏幕内容**。
|
||||
|
||||
所以,update的功能是更新调用update之前的所有动作,这些动作可以有绘制图像操作,也可以有音乐播放,也可以有动画每一帧的操作等等。只要update一次,屏幕的画面就会往前行进一次。
|
||||
|
||||
给你留个小思考题吧,我们在fill屏幕的时候,怎么做才能让填充的颜色不停变幻呢?
|
||||
|
||||
欢迎留言说出你的看法。我在下一节的挑战中等你!
|
||||
|
||||
|
Reference in New Issue
Block a user