这个名字是我编的。现在我们的主角要么死要么活,而没有一个量化的HP。当然还是那句话,具体如何取决于设计。不过这里我们试着来让主角没那么容易死。
要实现生命值系统,我们不仅需要修改玩家的相关代码,还需要在UI上有所表示。我们先来修改玩家的脚本。
HP一般来说是一个整数——即使UI以血条示人,实际的数据可能也不过是是当前HP和满血的一个比值。因此我们除了需要一个变量来表示玩家当前的HP,还需要一个变量来确定玩家的HP最大值(满血状态)。
定义这两个变量,为了方便调整HP最大值,我们把它暴露出来:
现在玩家受伤时不能直接让它die了。我们提供一个方法来减少它玩家的HP。
现在我们可以把die的条件调整为只有在HP小于等于0时才die,因为受到伤害可能会大于当前的剩余HP。不过为了避免一些问题,我们还是把玩家的生命值限制为非负数。这里可以用到一个简单的函数叫clamp,它可以把一个值限定到一个范围内,如果不超出这个范围就原样返回,如果小于最小值就返回最小值,超过最大值就返回最大值。
另一方面,作为敌人的负鼠不能直接调用die了,而是调用这个方法给玩家伤害。我们可以定义一个damage属性方便调整伤害:
我们希望在UI上体现玩家的HP状况。我们用素材中的樱桃来代表HP。樱桃看起来不那么对称,如果你不能接受也可以选择用宝石素材。
新建一个Control场景开始做HP的UI,可以叫它HpGauge。
假设我们想让HP的指示物横向排布,有多少HP就显示几个。由于涉及多个元素,我们自然会想到用一个容器来装它们。这里就用之前介绍过的BoxContainer,由于我们需要横向排布,所以这里选用HBoxContainer(H代表horizontal)。图片素材用TextureRect(“纹理矩形”)展示。
HP指示器很多时候出现在画面左上方,所以我们把它锚定到左上角,然后稍微拉开一点距离。
我们给这个Container加上一个TextureRect作为子节点以便查看效果。现在作为”一点HP“的素材显示在了容器中,但是它的默认设置并不是我们想要的。根据你对Container初始大小设定的值,此时HP的纹理的样子可能已经变得奇怪了:
此时需要调整TextureRect的设置。注意这里谈的尺寸涉及到两个东西。一个是TextureRect本身的大小(即TextureReact所确定的矩形的大小),另一个是TextureRect内部的纹理的大小。控制TextureRect的大小和纹理本身的尺寸主要依赖Expand Mode和Stretch Mode两个属性。
由于Container中的控件大小在某些时候会受容器控制,所以如果想看Expand Mode的效果可以单独放一个TextureRect到场景中。
Expand Mode会控制如何确定TextureRect的最小尺寸。其默认值为Keep Size,即保持所选纹理的尺寸作为最小尺寸。Ignore Size即忽略纹理的尺寸,TextureRect的尺寸任意。
带Fit(适应)的选项按照文档所说(当然实际上也是)主要是放在容器中比较有用。我们在稍后放到容器中再说。
Strech Mode控制TextureRect内部的纹理如何拉伸。默认的Scale就是我们刚才看到的效果,它会让纹理跟着TextureRect进行缩放。Tile可能比较少用,它会让纹理铺满整个空间。Keep就是保持纹理尺寸不动,如果此时Expand Mode为Ignore Size,那么就可以出现这种情况:
Keep Centered顾名思义就是始终保持纹理在矩形中间且不修改纹理本身的尺寸。带Aspect(宽高比)的会保持纹理的宽高比。Keep Aspect Centered就是说保持宽高比(进行拉伸)的同时保持在中心,这几种拉伸模式可能会比较常用:
最左侧为Keep Centered,Rect拉大了,但是纹理本身保存尺寸不变且居中。中间为Keep Aspect,纹理会在保持宽高比的前提下缩放,但是位置不动。右侧就是既按宽高比缩放又保持居中。
Keep Aspect Covered也会对纹理进行拉伸,但是超过矩形的部分会被遮住。
接下来我们放到容器中再来看这两个属性。在容器加入一个TextureRect,各属性保持默认。前面也提到过,当控件位于容器中时,它的尺寸和位置就不再完全受自己控制。放入容器后,检视面板的Layout菜单下面会多出来一批Container Sizing属性:
这是控件在容器中才能(通过编辑器)进行调整的属性。这些属性会告诉容纳它的父级容器应当如何安排它的大小。对于HBoxContainer来说,它主要控制水平方向上的排布(用Web的话来说就是”主轴“是横轴),因此这里Horizontal选项都不会起作用。当然,Contaienr Sizing部分的各属性对于不同类型的容器来说效果也不同。
不过对于HBoxContainer来说垂直方向上仍有可以调整的余地。默认的Fill决定了它会始终填满容器的高度:
无论Expand Mode为何,Fill会使得高度始终和容器保持一致。这里的两个橙色方框是我同时选中HBoxContainer和其中的TextureRect显示出来便于对比大小的,并不是什么神奇功能。要同时选中场景树中的节点,按下Control再选择即可,和各种操作文件系统和列表的软件的常用快捷键一样,Shift可以自动连续选择一系列相邻节点。
Shrink会让容器中的控件在对应方向上的尺寸缩小为最小尺寸。例如在设置了TextureRect的Expand Mode为Keep Size的情况下,将垂直方向上的sizing调整为Shrink Center,HBoxContainer中的TextureRect的高度就会和纹理本身保持一致而不是填满容器的高度,且同时把它放在垂直方向上的中间:
不过如果设置了TextureRect的Stretch Mode为保持宽高比且居中,其实我们也可以看到纹理保持在中间。只不过此时TextureRect本身的高度是和容器一样高的:
Shirink Begin和End类似,只不过位置不一样。不赘述。
如果我们不想纹理被缩放拉伸而走样,我们可能希望将Strech Mode和Expand Mode调整为保持纹理尺寸的模式,Expand Mode设置为Keep Size没毛病,Stretch Mode设置成Keep Centered为佳(便于保持图片在容器垂直方向上的中间)。
Keep Aspect Centered实际上在这里效果一样,只不过不是它本来的目的。由于在HBox容器中水平方向是HBoxContainer的“主轴”(这些容器和Web里的flex容器有点像,借用一下主轴的说法),所以其子节点的宽度交给了它控制,所以其实也不会缩放。
此外Container Sizing中还有个Expand属性没有提到。这个选项的意思就是说这个控件要不要展开以占据剩下所有的空间:
这两个樱桃除了左边的勾选了Expand之外其余设置相同。
如果容器中多个控件启用了展开,那么所有想展开的控件会平分剩余空间:
最后,如果你希望樱桃能够随着容器的大小变化而发生缩放的话,我们首先要修改TextureRect的Expand Mode,以使得它不再以纹理本身大小为最低要求。带Fit的选项就会根据设置动态调整TextureRect的最小尺寸,例如文档提到对于水平布局比较好用的Fit Width(宽度适应)系列选项会无视纹理的高度,让自己的宽度等于自己的高度。带Proportional的就是会保持纹理的比例进行调整。此时Stretch Mode如果设置为默认的Scale,我们就可以看到纹理本身也就会跟着TextureRect进行缩放,当然保险起见我们也可以设置为限制最多的Keep Aspect Centered:
总之,控件的大小、位置是锚点、容器、自身设置共同作用下确定的。这些设置会产生很多种组合,在何时需要哪种设置还需要我们根据需要来确定。
现在的问题是如何根据具体的HP值来调整容器中的东西的个数。毫无疑问我们要写代码。这里可以像之前那样,把里面的TextureRect单独做成一个场景,然后在需要的时候实例化出来加入到容器中。不过这里我就用更复杂的办法来做一下,当然控制容器的代码都是类似的,只不过不做成单独的场景的话像上面提到的TextureRect的各种属性我们得用代码调整。
给HpGauge新建一个脚本。这里我们肯定要引用一下HBoxContainer。写代码前先想象一下我们会怎么使用它。我们的玩家受伤时我们想要通过一个方法来告诉HP计量器HP发生了变化你应该做出改变。
因此定义这样一个方法,接受一个表示新的hp值的整数型参数。HP有多少我们就显示几个东西。这里我用笨办法做,所以我暴露一个材质属性作为要显示的东西的材质。
首先需要考虑更新后的hp和当前的hp的大小关系。如果新的HP大于当前的HP,那么我们需要显示更多的指示物,反之我们要减少指示物。
通过容器的引用我们可以获知其中有多少个子节点,以此确定目前在应用新的HP值之前HP为多少。如果新的HP多于当前的HP,那么我们就要加入新的TextureRect;否则,我们要移除从右往左最后几个樱桃:
看起来稍微有点复杂,我们慢慢来看。首先调用container的get_children方法获得一个包含了container的子节点的数组,也就是我们现在的樱桃。len函数返回数组长度,也就是有几个樱桃、几点HP——当然这里我们可以省略这个步骤,让玩家直接把受伤害之前的HP传进来,你可以作为练习换个思路写这里的代码。
delta是更新后的hp和当前hp的差值。如果以后要做加血的道具,那么我们的hp也可能会增加,所以这里都要考虑到(实际上初始化的时候也需要)。如果差值大于0(HP增加了),那么我们就需要创建几个新的TextureRect。
首先用TextureRect类型的构造函数来构造新节点,然后用代码设置前面讲过的expand_mode和stretch_mode。具体的名称可以查看文档。然后把材质属性给它。最后把它作为子节点加入到容器中。
如果差值小于0(HP减少了),我们就要删掉几个HP指示物。这里代码只有两行,但是看起来也有点复杂。数组的slice方法会对数组进行切片——很形象,就是从数组中拿一部分出来。很遗憾的是GDScript没有实现Python那样的切片语法糖。所以只能调用这个方法。
sliece方法的第一个参数是从哪里开始切(包含这个索引本身),-1就是数组的最后一个元素。第二个参数是切到哪里(不包含这个索引),注意这里的delta是负值,所以是+ delta,从右往左切!这里的第三个参数在我们这种从右往左切的情况下必须指定。第三个参数是所谓步长(step),也就是每次“前进”多少,默认值1的意思是每次索引加1,也就是从左往右切!这里要往左切所以要把-1放到第三个参数上。最后在循环内部,销毁对应的节点即可。
在程序中频繁地构造和销毁对象可能会对性能有一定影响。由于这里的HP指示物本身没有什么内部状态可言(它只是单纯地显示图片),所以我们可以重复利用创建过的TextureRect。
我们创建一个数组用于保存可用的(没有显示在画面中的)TextureRect。在需要更新HP的UI时,如果我们有足够多的可用TextureRect节点,那么我们直接把它拿出来放到容器中。pop_back会弹出(移除并返回)数组最后一个元素。
如果不够多,我们首先新建缺少的。然后再把已经创建的全部拿出来。
在需要减少HP指示器时,我们首先把它从容器中拿出来。注意,remove_child不会销毁节点,它只是让它离开场景树。从容器中拿掉之后,我们再把它放到texture_rects备用。
这样一来就不需要频繁地销毁和构造新的节点了。如你所见,这样做的代码都要多写不少,实际上你还可以选择直接隐藏部分节点。
但是必须要强调的是,过早优化是(开发)效率的大敌,就这点消耗来说,对于现今的设备来说都是小菜一碟。在开发初期或者需要快速完成开发的时候这种“优化”实际上可能很浪费时间,我们必须时刻做出权衡。
游戏运行时的UI会和游戏场景出现在同一场景中。为了不影响其它场景,我们把UI放在一个CanvasLayer(还记得它吗)中,然后给它一个纹理资源。
现在我们需要在玩家hp发生变化时让UI也随之变化。我们已经不止一次用到了信号,轻车熟路。我们定义一个hp发生变化的信号,然后我们在主场景中让hp控件连接它:
由于update_hp的函数签名刚好和hp_changed对应,所以这里直接连接即可。但是最后不要忘了,给hp计量器进行初始化!另一方面如果你也写了复活的代码,也要在重生时进行这些初始化!
评论区
共 条评论热门最新