You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.3 KiB

第11讲 | 如何设置图像的前后遮挡?

我们人的肉眼所观察到的世界是属于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进行第二次的贴图然后继续循环贴这样我们的围墙就开始连贯了起来。

我们来看一下贴上去的效果。

可以看到每隔一段贴一幅图另一段贴另一幅图这样一整段的围墙就贴完了。一共有十幅图片每一副图片的y值都向上减去30。

现在我们来总结一下贴这些连贯图片的重点:

  1. 将内容放入列表或者数组中。为了编程方便,将需要连续贴图的内容放入列表或者数组中就能够减少编程工作量;

  2. 计算好贴图的点,能让我们在连续贴图的过程中,只要控制位置变量就可以完成。

如果我们编写的是地图编辑器而地图编辑器生成的脚本代码除非写得非常智能一般来讲就是一连串的贴图代码这样就会有许许多多的blit的操作并不会将相同的元素加入循环或者列表那是因为脚本代码是电脑生成的没有更多的优化代码。

接下来,我们要将一个人物放上去。这个人物只是摆设,我们只是为了测试图像遮挡的情况。

player = 'human.png'
plr = pygame.image.load(player).convert_alpha()

然后我们在循环的围墙贴图的代码之后,放入人物。

screen.blit(plr, (62, 270))

我们将人物故意放在围墙的某一个位置,效果看起来是这样的。

这样看上去人物就站在围墙上面了。看起来他似乎有飞檐走壁的功夫然而事实上他应该几乎被围墙挡住但是这个时候问题就来了。虽然我们可以把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中进行绘制。效果就是人物被遮挡到了围墙外面。

这段代码起作用的地方是在[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值相同的话人物碰到一起会出现什么结果呢

欢迎留言说出你的看法。我在下一节的挑战中等你!