轻松一下,做点简单的东西。(我原本是这样想的但是一看还是写了这么多字)
我在GDScript精要部分就用道具作为例子来讲解了很多编程概念,现在我们来试着真正地实现一下道具。
“道具”在不同的语境中有不同的表现形式。一个是道具这一概念本身的各种属性比如名字和描述,比如我们可能有一个道具栏,可以在其中看到这些信息。另一方面,道具可能会作为一个场景/节点出现在游戏中,比如那些可以掉落可以被玩家拾取的道具,它们有一些视觉上的表现,但是它们总是会代表一个抽象上的道具。
一般来说在一个游戏的某个具体的版本中,其中的道具是固定不变的。无论你的道具要出现在场景中还是道具栏中,无论它们要出现多少次,这些道具的概念上的、脱离具体表现形式的属性应当是不变的,并且应当是各种场景实例所共享的。为了方便后续的描述,我姑且称其为道具的“元数据”(metadata)。元数据就是描述数据的数据。
对于这种数据,我们可以用资源来表达。
资源
我们从启动Godot的一瞬间我们就已经在和各种资源打交道。各种能通过磁盘存取的东西都可以是资源。也就是说,我们前面一直在使用的场景、脚本也是资源。各种Sprite节点的Texture属性也是资源(注意它不能和图片文件划等号)。
正如文档所说,资源是数据的容器。资源自身并没有实际功能,但各种节点的功能需要通过资源来完成。
资源的一个特点是只会加载一次。一旦一个资源加载到了内存中,后续对此资源的访问都是指向同一个东西。
道具资源
作为游戏设计师,我们会不断设计出各种道具。这些道具有一些基本的属性需要我们设计。比如一般来说它们可能有一个名字,有一个代表它的图片,还有一段简短的描述。
要新建一个(或者说”种“)资源,我们需要进行一些和新建场景不一样的操作。前面提到资源都是可以存储到磁盘中的,因此创建资源时我们需要在文件系统面板中进行操作,在文件系统面板中的某个位置单击右键选择新建资源:
新建资源和新建脚本一样,需要为它选择一个基类。我们在这里可以看到各种各样的内置资源类型,比如各种Texture、各种Shape,以及GDScript脚本。我们这里要新建一种我们自己的资源,所以我们就选择一切资源的基类Resource。然后给它取名,比如叫cherry。
现在你的文件系统中会出现一个新的tres文件——但是目前光是这个文件并没有什么实际用处。资源和节点一样可以通过脚本来定义各种属性和方法来实现它的功能。
添加脚本
要为资源添加脚本不像给节点添加脚本那样在场景树中直接点一下就好了。我们首先需要在文件系统新建一个脚本文件:
由于我们是在给资源创建脚本,所以继承这一栏需要选择Resource。我们这个脚本将会为各种道具所用,所以我们叫它item_metadata。
现在我们要把这个脚本附加到刚才新建的cherry资源上。我们点击选中cherry资源,注意到右侧的检视面板:
在RefCounted一栏下可以看到Script属性,在这里我们可以直接把item_metadata脚本拖到这里,也可以点击箭头加载脚本。这样一来我们后续在脚本中编写的代码就会体现在cherry资源上了。
RefCounted是reference counted(引用计数)的意思。包括Resource在内的各种派生自RefCounted类都会记录有多少对象引用了自己,当这个数字降到0时就意味着没有人再使用它,它也就会被销毁,占用的内存也就会被释放,而不需要手动地操作。
定义道具的数据模型
现在我们在脚本中定义描述一个道具的元数据所需要的属性。现在我们能够想到的是,道具有它的名字、图片、描述、效果。我们定义如下属性:
我们为每一个属性都加上了export标记,这样我们就可以在资源上编辑它们。现在选中cherry资源,检视面板中应当出现了相应的条目。
直接从脚本创建资源
我们现在已经有了道具元数据的脚本,我们要创建新的道具资源,只需要新建资源然后把脚本像刚才那样给它就行。不过这样还是有点麻烦。
为了方便资源的创建以及在其它脚本中访问资源,我们可以在刚才的item_metadata脚本中给它一个类名:
现在在新建资源时可以直接通过搜索找到这个类,创建一个自动关联了此脚本的资源:
可拾取道具
新建一个场景,我们将会用它来表示玩家可以拾取的道具。
除了展示道具图标的Sprite2D之外,这里给它一个Area2D来检测碰到了玩家。
请将Area2D的碰撞属性配置为可以检测到玩家的层。此外为了避免不必要的麻烦,也可以将Area2D的Monitorable取消勾选,这样它就不会被其它Area检测到。
这个Pickup将成为各种道具的容器,道具本身的元数据就来自我们自己创建的各种ItemMetadata资源。我们在Pickup的脚本中export一个类型为ItemMetadata的属性,这样就可以在检视面板中填入某种具体的道具元数据,自然也可以通过代码按需设置可拾取道具所包含的具体道具元数据。
有了道具的元数据之后,稍后我们就可以根据具体的道具元数据来显示相关信息。这里我们通过道具的元数据拿到道具对应的图片,然后把它显示到Pickup的Sprite2D中:
我们设想,当发现玩家进入拾取范围时,我们就在UI上显示一些和道具有关的信息,同时让玩家知道有道具可以捡。
在玩家的脚本中,我们先增加一个变量用于表示身边的可拾取道具。
在处理输入的方法中,如果玩家按下了拾取的按键,且有道具可以捡,我们就捡起道具。现在我们暂时不管如何发挥道具的效果,只是简单地让它消失。这里记得在项目设置中添加叫pick的Input Action并绑定到你喜欢的按键。
在Pickup的脚本中,连接Area2D的信号,如果发现玩家进入了范围,那么我们就设置玩家的item_to_pick属性为这个道具。
“按F捡起”
有道具可捡时,我们显示一个简单的控件提示玩家可以捡起道具。我们新建一个UI控件场景,取名为PickupTip。然后添加节点如下:
前三个Label将分别用于展示道具的名称、描述、效果,最后一个提示“按下F捡起”。你可以根据自己的喜好来调整控件的布局。我就简单让VBoxContainer位于屏幕下半部分的某个地方。
给它添加一个脚本,添加对相关节点的引用备用:
随后我们会根据具体的道具的元数据来设置这些Label显示的内容。
我们还需要告知其它场景/节点什么时候显示这个UI。我们需要在Pickup中定义这样一个信号,告知外界有玩家进入了拾取范围:
这样我们就可以在主场景中编写相应代码来显示和隐藏拾起道具提示的UI。
我们之前把UI元素放在了单独的一个CanvasLayer中,为了方便起见,在主场景脚本中添加一个对它的引用。拾取提示不是随时都需要显示,所以我们在需要时才实例化它。这里又export一个变量来放拾取提示的场景。
现在的问题是,主场景如何连接上可拾取道具的信号呢?由于Godot的信号是各个对象的实例各自持有的,我们必须拿到一个具体的Pickup实例才能连接上它的信号。但是道具的生成可能会有多种方式,可能会预先放置在场景中,也可能在游戏中由其它节点生成。所以这里我们在主场景的脚本中连接上根节点的child_entered信号,它会在一个子节点进入场景树时发出,我们可以检查这个节点是不是Pickup,如果是我们就连接上它的player_entered信号:
具体的响应方法我们稍后补充。
在C#中,C#的事件可以是静态的,也就是说可以不和任何实例关联而仅和类关联。在这种情况下我们可以把这个信号/事件定义为静态的,这样任意Pickup实例发出的信号都是同一个信号,玩家到底碰到哪个道具了由信号的参数体现。需要响应该信号的对象也不需要引用具体的Pickup实例,只需要连接这个静态信号即可。但是目前GDScript还不支持静态的信号,所以我能想到比较好的方法也只能这样写。
在PickupTip的脚本中定义一个item属性用于存取其显示的道具的元数据。在这里我们单独定义item属性的setter(还记得这个东西吗),每当外部为item赋值时,我们都更新相关Label中的内容:
GDScript的属性有有一个比较好的特点是,可以不用单独定义一个后备变量用到属性的getter和setter中。直接在属性的getter和setter中存取属性自己不会发生无限递归,Godot会处理好这一点。
现在在主场景的脚本中,我们添加响应palyer_entered信号的方法:
如果需要显示拾取提示时这个UI场景尚未实例化,我们就实例化并加入UI的CanvasLayer,否则我们就令其可见。稍后我们会通过设置visible属性来控制拾取提示的显示和隐藏。当然无论如何,我们都要设置拾取提示的item属性为我们可以拾取的道具中所引用的道具元数据,以令其显示对应道具的信息。
隐藏拾取提示
刚才的工作只做了一半。当玩家离开道具的拾取范围时,我们要通过相关的信号来进行相关工作。玩家离开时,设置玩家可以拾取的道具为null,同时隐藏提示。代码很简单:
完全是同样的代码镜像了一下。
至此,我们的道具拾取基本上就完成了:
让道具发挥效用
按照设想,我们希望捡到樱桃之后HP+1。
不过正如在之前的GDScript精要中提到的那样,道具是一个很笼统的概念,它还可以细分为多种类别。比如贵重品(只能收集起来,可能不能交易和使用)、消耗品(可以使用,有一定的效果),装备也可以算作道具的一个类别。
这里为了简单起见,我们单独从ItemMetadata派生出一种HpItem,它包含使用后hp的变化量:
新建脚本时,继承的类选择之前的ItemMetadata。
由于这个新的资源类型是派生出来的,所以没太多需要增加的。我们只是增加一个hp量的属性。int类型可正可负,没毛病,有可能我们会需要做一些会减少hp的道具。
最后修改现有的cherry资源,这里直接在其检视面板中修改它关联的脚本为新的HpItem(选择Quick Load或者Load加载现有的脚本):
由于存在继承关系,之前的一些属性都还在,如果图片没了就重新给它加上去,除此之外只需要修改Hp Amount为1即可。
之前玩家捡起道具后除了准备销毁捡起的道具节点之外什么都没做,现在我们来把这个樱桃吃掉。当我们发现可以捡起的道具是HpItem时,我们就修改玩家的hp。这一次我们遵循前面PikcupTip的item属性的套路,把玩家的hp也改成能够执行额外工作的属性——让hp发生变化时即发出hp_changed信号:
由于之前的HP指示器UI本身就是连接到了hp_changed信号,所以不需要额外的修改。
现在再修改捡起道具的代码:
只要我们发现可以捡的道具代表的是HpItem,我们就可以修改玩家的hp。同时我们把新的hp的值限定到了0和HP最大值之间。当然这里具体怎样处理取决于你的设计。
完成!