大家好,我是灰(微博@天外灰仙儿,gitee@overpara),今天给大家带来的是Godot4制作桌宠的第二期,将在第一期的基础上继续完善功能和动画。
上一期主要讲解了Godot4的下载和使用方法,以及对代码和一些主要节点的使用,第一期跟着做完也可以做出一个满足基础需要的桌宠,没有看过第一期的朋友可以翻翻我的微博或者空间相册看看。
不出意外的话也是最后一期桌宠教程,下一期会给大家带来一些不一样的小游戏教大家做,想要学习游戏制作并且喜欢我的教学风格的朋友可以点个微博关注或者加个qq好友哦~
本期的内容学完你会获得一个有久坐提醒、喝水提醒、保存提醒的桌宠,同时点击不同位置会有不同的随机对话、天气报告,支持多种动画的切换,支持逐帧/骨骼动画。
同样的,本教程的根本目的是让你初识游戏的制作流程、学会一款独立游戏引擎、了解面向对象编程、感受代码语言的逻辑魅力、掌握一些做游戏相关的英文,制作出来的桌宠是奖励自己好好学习的副产品~
正式开始之前还是先叠个甲,本人是工科出身,严重的工程思维——结果比原理更重要、实际比理论更重要,教程中难免会出现一些理论不准确的情况,但是只要做出来的东西能用那就是好的。
游戏版本:上一期是Godot的4.2.1版本,就在我做教程的过程中,Godot发布了4.2.2版本,所以本期用的是4.2.2版本,我个人感觉除了有的功能的汉化没了外没啥区别,所以这两个版本应该都可以用(主要是我喜欢用新不用旧)。
一、前情回顾
首先我们来梳理一下我们上一期制作的内容,这是我们的主场景节点树,其中包含了两个子场景:
我们同样编写了三个脚本代码,可喜可贺:
实现的功能包括:鼠标左键点击播放说话动画,进行随机对话,含天气报告功能;鼠标右键进行桌宠的拖动;快捷键实现桌宠的退出。
在上一期也有很多朋友反映运行的时候出现了桌宠背景不透明的问题(或者自己的透明,发给朋友玩玩的时候就不透明了),这是由于每个人的电脑显卡渲染机制不同的原因,这里我总结出了一套通用的解法(同样感谢反馈这个问题的诸多同学)。
首先在项目设置里把常规-显示-窗口-像素级透明,勾选上。
接着点击引擎右上角的Forward+改成兼容。
这样应该就可以适配绝大部分的显卡啦!
二、备份
在制作之前我还希望大家有版本管理的意识,在每一次升级之前先把自己之前的项目文件夹打个压缩包命个名再继续做(比如1_0版本)。这样如果你新版本做的出现了不可逆转的错误,或者你做一半后悔了,那你可以把之前的项目解压出来再重新导入到Godot里。
导入已存在的项目,可以在引擎里选择项目文件夹里的这个文件。或者直接选择你打包的压缩包。别人的源码也是这样打开。
然后在项目设置-应用-配置-名称里,可以把项目改个名字,比如加一个v2.0。
三、动画素材
这一章我们先把需要的动画先做出来,这里为了教程简单,我只用简单的单图来切换动画。
注:(如果在这一章你有拆件素材,你可以去搜搜怎么用Godot做骨骼动画,因为本文是游戏制作的教程不是动画教程,所以不会讲那么多关于动画的东西。骨骼动画和逐帧动画都是用AnimationPlayer实现的,所以如果你不想做单图/逐帧,只想做骨骼,那你在这章只要跟我把AnimationPlayer的动画轨道建好,动画名字起好,至于轨道里的动画是什么样的大家可以自己决定哦~)
其实在上一期教程里已经教大家怎么做动画了,那这一章节我就多讲一下关于动画节点的一些新知识。
首先介绍一下素材类型,第一种就是最朴素的单图素材,我们把单图给到Sprite2D节点的Texture属性,再把每一帧的Texture添加到关键帧(图和上一期是同一个角色,我们工作室出品的爱斯宝贝(成年体),上一期是爱斯的小孩形态):
说到这了,再重复一遍,不管是代码还是文件名,都要尽量用英文,不然导出可能会出问题。
第二种是游戏中最常用的逐帧动画素材,可以把每一帧规律地排列到同一张图上,然后用Sprite2D节点的Animation和Region属性来把一张图切割成多帧,再把需要的属性加入到关键帧,这个本文也会教(考虑到从网上下载的免费开源素材大多都是这种图)。
图为我们工作室出品的安娜:
第三种是骨骼动画,大家比较熟悉的虚拟主播的皮套就都是用骨骼制作的,还有大家玩的一些手游的动态立绘也都是骨骼动画,说实话比起Live2d cubism和spine来说,Godot的骨骼可以说是又麻烦又不出效果。虽然它支持从spine和blender导入骨骼,但是spine只支持到了Godot的4.1版本,blender貌似也只支持3d的骨骼。
不过若是你只想实现简单的flash动画效果,也可以自己尝试一下用Polygon2D、Skeleton2D和Bone2D节点实现一下。其中Polygon2D节点用来画出图片的纹理范围、网格以及骨骼的权重,Skeleton2D用来管理骨骼,Bone2D就是骨骼本身啦。这些节点的每一个属性都可以作为关键帧,以此来实现骨骼动画。
其实这期教程本来是打算以骨骼教程展开教学的,但是在手打了2000字教程后感觉几万字都教不完,并且这样会偏离教程的主旨……所以很抱歉,做骨骼动画的朋友还请自己找专门做动画的教程学一下,或者等等俺们工作室的老大出一期骨骼教程啦~
接下来我们正式添加动画,在这里我先讲单图素材做动画,讲完这个再讲逐帧动画素材怎么做动画。
首先选择Character场景,然后选择AnimationPlayer节点。
首先注意一下这个RESET动画,之前说过在给动画添加关键帧的时候,会在RESET里自动加一个关键帧属性的默认值。
比如你给图片的位置添加了一个关键帧,就会在RESET里自动把位置也加一个关键帧。
RESET里的关键帧每一个轨道只能在轨道开头有一个,这个RESET里的属性值代表了游戏刚启动的时候值为多少。所以当你遇到了游戏刚启动时的效果和你预想不一样的情况,就检查一下RESET里的关键帧属性有没有设置对。
点轨道右边的垃圾桶可以把它删了,因为我要换掉默认texture了,你如果是基于上一次做好的动画做的可以不删。
我们点这里可以新建(New)一个动画,先来做一个站立的默认动画。
起个名字就叫Idle(空闲)。
单图素材:
把texture里先换成准备好的idle动画的第一帧。
然后点旁边的小钥匙加入关键帧。
如法炮制,把每帧的图都加进去。这里我做的是站立一会儿快速眨眼两次的动画。
如果你每张图的尺寸和位置都不一样,还需要把Transform里的position和Scale也加入关键帧哦。
逐帧动画素材:
把图也是放到Texture里。
把Region(区域)的Enabled勾选上。
选择编辑区域。
选择栅格吸附。
这种素材都会把每一帧均匀分布排列,我们需要知道这个分布尺寸是多少,然后修改这里改变栅格的大小。因为我的图每帧占的尺寸是320x320,所以可以选32为格子大小(只要是倍数关系就行)。
然后我们框选出闲置动画的范围,最好不要框进来头顶的空白区域,后面每一行动画框出来的区域高度也要保持一致。
每一行动画高度保持一致是因为这样不会让不同的动画高度不一样,不框空白区域是防止上一行动画的脚漏到了下一行动画。
当然很多素材处理的很规整,不会出现漏像素的问题,这样就可以一口气选好几排动画啦~
框完之后点关闭,可以看到这三帧同时出现在了视图里。
我们展开检查器里的Animation。
其中Hframes是水平方向的帧数,我们这里就是3,Vframes是垂直方向的帧数,我们是1,Frame就是当前帧数(从0开始计数),Frame Coords就是当前在第几排第几列的帧。
然后调整好动画轨道的吸附秒数。
我们可以点Frame右边的小钥匙添加关键帧,每点一次会自动帮我们Frame+1,同时轨道上的指针也会自动按照吸附的秒数调到下一个位置,非常方便~
我们也要把hframes、vframes、region rect三个属性也加入到关键帧里,就放在动画一开始的地方。
好啦,两种方法都介绍完啦,下一步大家就可以一起看啦(我还是会以单图动画为主)~
四、完善动画
每次都是固定时间眨眼两次循环的话,可能会有点呆板,所以我想再做一个只眨眼一次的动画,闲置的时候这两个动画不停地随机播放~当然你也可以做其他的动画来填充闲置时间!
如果你希望和我一样在Idle动画的基础上做修改再来一个动画的话,还是点这个动画,然后点“复制为”。(不是的话你就继续点New吧!)
这里已经帮我们自动起好名字了,就叫Idle_2。
那既然如此,我们就回到Idle,把它重命名为Idle_1。
现在这里有四个动画了,我们选择Idle_2。
框选掉要删除的地方。
右键删除关键帧。
这样眨眼一次的动画也做完啦~
接下来,我们用这个方法来添加多个说话的动画,我们上一期只加了一个通用的说话动画,这次我希望可以对应不同的语气来显示不同的表情(如果你很能肝动画的话,甚至可以对应不同的嘴型来做)。
这次我添加了正常说话、开心说话、阴险说话、抱怨说话。
然后我还想做几个提醒动画,比如喝水提醒、久坐提醒、保存提醒。
这一章最后再介绍一种动画节点类型,这个节点适合fps(每秒帧数)固定的动画,并且这个动画是用来装饰用的,不需要怎么去修改的。
比如我这里想要添加一个可以闪烁的心心。
先右键根节点,选择添加子节点(或者Ctrl+A)。
选择AnimatedSprite2D节点。
改个名字,就叫Heart吧。
选择检查器里的Animation,找到这个Sprite Frames。
选新建。
新建完之后再点一下它,目的是给它展开。
这个时候引擎下方就会出现做动画的地方啦。
我们选择从精灵表中添加帧(如果你还是想用单图做,那就点它左边那个按钮来添加单图)。
找到我们的心,打开。
在这里设置一下行数和列数、每格子的大小、间距、偏移量。可以自己调一调试试,目的就是让这些格子把图都均匀地框起来。
然后用鼠标左键依次点击我们需要的格子,上面会出现序号。再点击下方的“添加x帧”。
这样帧动画就添加进去啦~
如果你需要复制一帧的话,可以选择要复制的帧,然后点击这个复制帧。
再在需要粘贴的位置的前一帧选择粘贴。
这样就好啦!
我箭头指的地方,从左到右依次是:开启自动播放、开启循环播放、FPS、选中帧持续几倍的时间。
然后我还希望只有在角色开心说话的时候,它才会显示。我们先来到Speaking_happy的动画,并把指针调到动画开始。
选择Heart节点,下面可能会自动跳到了Heart的动画轨道,我们点“动画”就可以回到AnimationPlayer的动画。
找到Heart的Visible属性,并添加一个关键帧。
把指针拉到动画的最后。
取消勾选,再添加一个关键帧。
返回RESET轨道,确认一下visible在默认情况下是不是非勾选状态。
如果不是的话,直接点击visible关键帧,右侧的检查器会变成这一帧的属性。
我们手动把Value取消勾选就好啦。
这样子动画就都做完啦~大家可以自己发挥想象力,做喜欢的动画!
五、划分区域&绑定对应的句子和动画
这一章我想实现点击不同的地方,会出现不同的对话和动画。
原来我们做的Area2D不用改,之后可以把它专门用来当鼠标右键拖动的范围。
这里我新建一个Area2D,改名叫Head_Area2D,同时画一个新的CollisionPolygon2D,把脑袋框进去。画的时候可以把别的Area2D隐藏掉(点节点树上的小眼睛)。
加下来我把身体和腿也加了Area2D。如果同一个Area2D想包含多个碰撞区也可以,就像我的Legs_Area2D一样。
接下来我们回到MainWindow的脚本里(也就是写台词的地方),把原来的sentences数组改成三部分,分别对应了头、身子、腿。这里我们把类型也声明成Array[Array],也就是数组里面再套个数组,这样做的目的是什么捏?马上就会解释!
再下一步,我们把之前写好的台词数组内容改成下面这样。可以发现整体是用方括号[]包起来的数组,这个数组又由一个个方括号[]包起来的数组组成,这就是所谓的二维数组。
我们之前想要调用第一句话的时候,可以使用sentences[0]来调用。但是现在是数组包着数组的情况,我们再想要调用第一句的话,就需要sentences_head[0][0],
意思就是要找sentences_head里的第一个元素,因为第一个元素就是一个数组,我们继续去找第一个元素(数组)的第一个元素(句子)。
不知道这一块我有没有说清楚,如果没有搞懂的同学可以再把上面那段话好好读几次。
大家可以看到,每个子数组的第一个元素都是台词,而第二个元素都对应着动画的名字,这样我们就可以用sentences_head[x][0]来给文本框加台词,用sentences_head[x][1]来调用动画啦(x是元素的脚标,也就是序号)!
那么接下来我就把点三个位置的台词都补全。
sentences_head[3][0]我还是空着,留着给天气用。
再把天气的数组位置改一下啦。
下面这一块应该会报错了,因为原来的数组已经不在啦。我们姑且先把_on_character_chat()一整个删掉吧,因为这个方法是Area2D传来的信号调用的,而我们现在只希望那个Area2D用于右键拖动。
还需要点击场景中的Character节点上的信号标志。
在信号里右键这个,点断开。因为这个连接的方法我们已经删掉啦。
现在我们回到Character场景。找到我们刚刚建好的Area2D们。
我们需要把这些节点都连接上input_event信号。
这个之前都讲过,就当带大家复习一下了。
这样这三个就算连完了。
我们还需要把之前的chat信号删了,改成对应每个部位的信号。
再找到我们之前写的鼠标左键触发chat信号的逻辑,可以删掉了~
然后我们把这段分别写到刚才新连接的信号方法里。对应上这三组信号。
保存一下场景,再回到MainWindow里,就可以看到Character节点新出现了三个信号啦。
我们再把这三个信号都连接到MainWindow里。
之前俺们是用数组自带的随机调用一个元素pick_random,在这里我们为了能更好地获得数组的脚标,就不用pick_random了,而是真的生成一个随机数。
这里randi_range是生成一个括号范围内的整数,因为我的sentences_body一共就3句话,对应了0、1、2,那就是(1, 2),存在一个临时变量index里(临时变量就是如果你在方法内部声明了一个变量,那当方法结束的时候这个变量就会被销毁掉)。
下面这一句就可以把随机数为脚标的台词给到文本框啦!
接下来我们把Character节点拽到MainWindow脚本里。还记得怎么拽吗?记得按住ctrl哦!
然后回到我们刚才的方法里,调用Character节点里的animation_player节点,播放台词对应的动画就好啦!
然后我们复制粘贴一下,把其他的信号方法也写好,注意这里红框圈出的地方要手动改成和信号对应的哦~
最后,别忘了再加上chat_text.play_chat(),也就是播放文字的动画啦。
大家可以运行一下,点击播放的动画和台词应该都设置好啦~
六、状态机的实现-上
我们做完了所有动画和台词,但是如何让这些动画切换还是一件麻烦事。
比如:
·怎么能保证空闲的时候在播放空闲动画?
·当后面做喝水、久坐等提醒的时候怎么保证多个提 醒的时间不冲突?
·当提醒的时候怎么保证不被其他动画打断?
·怎么保证说话的动画不被提醒打断?
包括很多同学希望能实现更多的功能,当功能过多的时候,代码就会非常庞大,可读性也会越来越差,后续加新的功能或者维护旧的功能就变得困难,这个时候就需要使用状态机啦!
状态机主要分为两点,就是状态和条件。比如我们人类,吃饭、睡觉、工作、娱乐、上厕所,这就是五种状态。当工作的时候困了,“困”就是一个条件,当满足了<困 and 老板不在>这两个条件,就可以切换到睡觉状态。
状态机一般可以通过三个方法来实现:
1、通过当前状态和条件来判断是否切换到其他状态。
2、当处于一个新状态时,首先要做的事情。
3、在当前状态下,要一直做的事情。
对于第一个方法可能非常好理解,但是第二个和第三个方法就经常被混淆。很多同学觉得,首先要做的事情和一直要做的事情没有区别。在这里我举几个例子大家就明白了:
·如果是洗澡的状态,我们首先要做的肯定是打开水 龙头,然后一直淋水就行了,打开水龙头不是要一 直做的。
·如果是抽烟的状态,我们首先要做的肯定是点烟, 然后一直抽就行了,点烟不是要一直做的。
·如果是喝水的状态,我们首先要做的肯定是打开瓶 盖,然后一直喝就行了,打开瓶盖不是一直要做的。
那回到我们的桌宠上来,对于我想要的功能来说,可以分为下面几个状态:
1、空闲
2、说话中
3、提醒中
我们来到Character场景里,在character脚本中新建一个enum(枚举)来存这三个状态。
其中IDLE为0,SPEAKING就会自动为1,WARN自动为2。后面会教大家怎么调用。
我们在下面写一个新方法,用来根据当前方法来判断下一个到什么状态。
这里我们箭头指向的就不是void了,而是int,因为我希望能返回(return)一个整数值用来表示接下来是哪个状态,如果返回的是-1那就代表状态不变。
那自然的,我们的第二个方法就是处于新状态的时候首先要做什么。第三个方法就是在当前状态持续做什么。
然后我们需要两个变量用来储存当前的状态和即将切换的下一个状态。类型就是我们刚才定义的枚举类型。current_state初始化就设为State.IDLE,next_state先设为-1。
我们来思考一下,这三个状态怎么切换。大家可以跟我有不一样的想法,我的想法是处于点击对话状态(SPEAKING)的时候不可以进行喝水、久坐等提醒(WARN),提醒的时候不可以通过点击打断提醒。对话和提醒结束后都会进入空闲(IDLE)状态,且只有在空闲(IDLE)状态才能进入对话(SPEAKING)和提醒(WARN)状态。
那我们先在get_next_state里写下如下代码。这段代码的意思是匹配(match)currrent_state的数值,去判断当前值等于IDLE、SPEAKING、WARN中的哪个,并去执行对应的代码。
当处于对话状态的时候,肯定会放一个对话的角色动画,当这个动画结束的时候我们就可以当做回到空闲状态啦。
这里我们用is_playing来判断一下动画是否在播放,这个方法会返回一个布尔(bool)类型的值,布尔类型就是只有true和false的一个枚举类型。在前面加一个感叹号代表反转,也就是说如果动画没在播放会返回一个false,我们加一个感叹号就会把这个false变为true。
聪明的你可能也已经想到了,其实if这个东西就是在判断条件是否为“true”的,如果条件为true就执行,条件为false就不执行。
在这里如果判断没有动画在播放,就返回(return)一下State.IDLE,注意一下,当一个方法中执行到了return语句,会直接结束整个方法。我们后面会多次用到return来结束方法这个技巧。
写到这里我们需要判断IDLE什么时候能到SPEAKING,那这里我们就定义一个标志位(flag),当这个标志位为true就进入SPEAKING,标志位为false就不做任何处理。
顺带说一下给变量设初值的时候一直用冒号等于,其实大家直接用等于号也是可以的,但是Godot里声明变量的时候不会限制变量的类型,我们加一个冒号相当于把变量类型强制限定为了冒号后面的类型。
在这里speakingFlag就会被强制限定为布尔值。这样数据处理起来方便快捷一些。
那我们把flag挂起来的地方,自然就是在点击到之前设置的几个Area2d的地方,点到了就都把flag挂起来!
我们可以直接把speakingFlag作为进入SPEAKING状态的判断条件,并且记得在SPEAKING能返回IDLE状态的时候把flag设为false。
接下来在判断WARN状态之前,我们先把提醒的动画和句子设一下吧。
因为我希望三种提醒分别用三个定时器来实现,并且互不冲突,所以在这里我就设三个标志位了。分别是喝水提醒drinkFlag、久坐提醒walkFlag、保存提醒saveFlag。
再新建三个Timer节点,用来给三个提醒设置计时。
在检查器里可以设置三个Timer的时间,可以把One Shot和Autostart勾上,这样计时器倒计时结束后会在我们需要的时候再打开,并且游戏开始的时候就会开始倒计时。
这三个时间的间隔最好分开一些,比如十分钟喝水提醒、半小时走路提醒、五分钟保存提醒,当然测试的时候时间可以先设的短一些。
设置好后,我们再把Timer的timeout信号连接到脚本里。
这里也很简单,把对应的flag都挂起来就行了。
我们回到MainWindow的脚本,把提醒的句子和动画也都定义一下。
为了让MainWindow能知道我们需要提醒了,我们在Character脚本里再加一个新的信号,可以叫chat_warn。
在状态切换这里,也可以把三个flag都放上去判断,or是“或”的意思,只要这三个flag有一个是挂起(true)的就直接进入WARN状态。
那我们在goto_new_state方法里,也可以先把match加上去,再在WARN状态时首先发出chat_warn的信号。
然后在MainWindow的脚本里连接一下character的chat_warn信号,我们可以判断一下character的哪个flag被挂起了,然后执行的内容可以参考前面点击对话的代码,也是给text赋值、播放角色动画、播放文字动画,只不过这里我们给每个代码块后面都加了一个return。
这样就可以让一个提醒处理完直接结束整个方法,结束后先返回到IDLE状态,再看需不需要进行另一个提醒,这样就可以防止两个提醒冲突的时候,前一个提醒被覆盖掉。
但是这些代码显然还是不够的,我们还没有重启计时器,也没有把flag重新置为false。
七、状态机的实现-下
我们先回到Character脚本,把三个Timer都拽进代码里。(本期内容经常两个脚本之间来回切换,可能比较烧脑,但是每个脚本分别干自己分内的事情,正是面向对象的魅力呀~)
然后再回到MainWindow里,把计时器启动,把flag都清零(顺便说一句,对于布尔值,true就是1,false就是0,毕竟本质就是个枚举嘛)。
但其实这里还有一个问题,思路比较灵活的朋友可能发现我并没有判断台词消失的时间,我们之前设置的时间是10s台词才会消失,而我判断的是角色动画停止(1s左右)的时间作为状态切换的时机。
这是因为我觉得角色播放空闲动画和台词没消失其实是不冲突的,这样既可以让大家看清台词,又可以让角色不至于在上一个动画最后一帧罚站。
但是对于提醒(WARN)来说,如果1s后就来了下一个提醒,可能看不清上一个提醒是啥,那我们在状态切换这里再加一个条件,就是判断文字是否已经消失了。
这个时候大家可能会注意到一个问题了,那就是我们所有节点沟通的纽带都是MainWindow这个场景,Character和ChatText这两个场景实际上是没有直接“沟通”过的。
如果我希望所有的场景都可以有一个自由沟通的桥梁怎么办呢?很简单,我们可以用自动加载场景。
首先我们新建一个脚本。
就起名叫GameManager,这里我新建了一个Autoload文件夹用来存放它。
在这里我们起一个类名,就叫GameManager。
在这里我们设一个布尔值,就叫text_showing,代表文字是否还存在。
在项目设置中找到这个自动加载。
点击这个路径,找到我们的GameManager.gd脚本。
给节点起一个名字(不可以和类名相同),然后点添加。完事儿把这个窗口关闭就行了
现在让我们去好久没去过的ChatText场景,在chat_text脚本里,定时器触发让对话消失的时候,把我们刚才设的值设为false。同时我觉得10s其实太长了,3s就够啦!
至于什么时候让它为true,我们可以在Character脚本里的goto_new_state里写,当它进入SPEAKING和WARN的时候,就可以首先让它们为true。
在get_next_state里,我们可以多加一个进入WARN的判断条件,就是需要没有文字显示的时候才能进入WARN。
再把WARN切换成IDLE的条件补上,我这里用的是跟SPEAKING一样的判断方法。
下一步我们还需要满足在提醒的时候不许玩家触发点击对话。
那我们可以在goto_new_state中,让玩家进入WARN状态时马上关闭所有Area2D的鼠标触发条件。
先把这三个碰撞区都拖进来。
再关掉三个区域的input_pickable属性。
在进入IDLE状态时,重新启用这个属性。
现在应该是功能都基本实现了,除了空闲状态的时候没有播放空闲动画。
这个时候我们别忘了,还有一个方法我们没用上捏。
我们还是先用match来把方法填充一下。
然后我们把两个待机动画的名字也放到一个新的数组里,我就起名叫idle_animations啦。
好,接下来要干什么,聪明的你应该都可以抢答啦。我们判断一下是否有动画在播放,如果没有,那就随机播放一个数组里动画。
最后,我希望状态机这三个方法可以一直运行,那下面我来介绍一个新方法,也是系统自带的方法。_physics_process,它是每一帧都会自动执行一次的方法,我们把它写在Character脚本里。
这个时候方法已经很多了,想要快速跳转到某一个方法可以点视图左下角这里。
那么每一帧开始前,我想要先确认一下是否要切换状态,如果需要的话这里get_next_state应该会返回一个State里的值给next_state,如果不需要的话应该会返回-1。
我们接着判断,如果返回的不是-1的话,说明需要切换状态了,那我们先把next_state赋值给curren_state,然后再执行goto_new_state。
最后我希望无论如何都要执行do_current_state,就把它写在if外面。
八、完善对话框
现在再来教大家做一下UI方面的东西,我想给角色说的话加一个对话框。
如果你希望对话框的大小能根据文字的大小和数量改变的话,你需要准备一个方形的对话框素材(且如果有小尾巴的话,最好放在角落里)。
我们先到MainWindow场景,右键ChatText场景,选择Reparent to New Node,如果你是4.2.2之前的版本的话应该显示的是“重设父节点为新节点”。
然后我们选择PanelContainer。
选择新建好的节点的检查器,选择Theme(主题)。
点击新建Theme。
再点一下出来的Theme。
在引擎下方会出现这个主题面板。
我们点击右上角这个加号。
找到PanelContainer,然后点添加类型。
找到这个彩色的像图片一样的东西。
点这个加号。
点这个<空>,然后点新建StyleBoxTexture。
点这个我们新建出来的东西。
检查器里会出现这个StyleBox,我们把我们准备好的素材拖拽到这个Texture里。
这个时候左上角应该是这样子,我不知道你们的是啥样,反正大概率不正常的各有千秋,尤其是把所有素材都放在同一张图里的朋友。
我们再选择一下ChatText节点。
把检查器里的Visible Ratio手动拉到1先,方便我们调试。
然后发现原来是这样子的~
我们回到刚才的StyleBox,选择这个Sub_Region,然后点编辑区域。
在左上角选择一种你喜欢的吸附模式,这个我前面都介绍过了,和Sprite2D的Region一样的。
我们先把需要的地方框起来再说。如果你想要微调的话,一定要点那个红色的点点来微调,不要点到线了。
然后我们切换到像素吸附或者栅格吸附下,去拖拽白色的虚线(这次不要点红点了),把四周不想缩放的元素都放在虚线外面,像我这样。尤其是圆角和小尾巴。调整完后点关闭。
调整完后如果还不正常(太大了或者太小了),可能是你素材本身尺寸不太正常,需要你用绘图软件自己改一改。
接下来我们可以手动再调整Texture Margins的值,这个就是我们刚才框的范围。
再找到这个Modulate-Color属性。
可以改变一下Alpha通道的值,让它变得透明一些(也可以改颜色,相当于挂了一个滤镜)。
然后我把PanelContainer缩放和移动成比较舒服的样子。
同时我再选中MainWindow场景里的ChatText。
在检查器里我把水平方向改成了左对齐(Left)。
在这里右键这个属性,点一下固定值。
当属性上出现这个图钉的时候,我们修改ChatText场景时,MainWindow里的ChatText子场景的这个属性就不会受到影响。
最后,我希望这个对话框在有字的时候再出现。那么再教大家一个小技巧,我们到ChatText的场景里,选择chat_text脚本。
框出来的地方是我新加的,get_parent是获取自己的父节点(也就是我们刚才做的PanelContainer),在游戏启动的时候隐藏自己的父节点,播放动画的时候显示父节点,隐藏文字的时候也隐藏自己的父节点。
最后我们运行一下,就像p1的动图一样的效果,没有任何问题啦~
最后的最后,非常感谢你能跟着做完整篇教程,大家也都给自己鼓鼓掌,能看完这么长的教程是一件非常了不起的事情!说明大家的耐心和学习能力都是一顶一的强!即便大家以后不会再打开Godot,我相信有这样学习和动手能力的大家也会在自己的行业内大放异彩的~