查看: 434|回复: 0

[讨论] 用主谓宾的思想来看待Godot的GDScript?(如何正确操作对象)

[复制链接]

62

主题

452

回帖

8

精华

版主

☯ 博 丽 不 是 灵 梦 ☯

经验
7151
硬币
1212 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

发表于 2024-7-24 23:34:58 | 显示全部楼层 |阅读模式
本帖最后由 囿里有条小咸鱼 于 2024-7-28 22:55 编辑

RT,本教程仅仅是从自然人类语言上的主谓宾角度出发,来讨论如何正确选择、操作对象。本篇教程以Godot的GDScript为例,基本上也适用于大部分面向对象编程语言。
在看本帖之前,建议先去学习godot相关的基本知识,包括节点、脚本等。
请注意:本贴中的术语不是借用就是自编,不一定具有专业性,仅供参考。
1L会不定期更新。



我们先来用通俗的语言解释下什么是主谓宾:
  • 主语:动作的发出者,话题的陈述者
  • 谓语:狭义上的谓语仅仅包括表动作和状态的语言结构;广义上的谓语也包括宾语在内
  • 宾语:谓语动作的对象

那么,我们就按照这个思路来分析下面的代码:
  1. var ref := get_node("MyNode")
  2. ref.queue_free()
复制代码
上面的代码中,我们用ref这个变量存储了对名叫MyNode的节点引用,然后让ref所指向的节点销毁。这里我们先不考虑ref为什么叫指向节点MyNode。我们先从第二行看起,这里我们把它翻译过来就是“ref执行销毁动作”,那么ref就是主语,而“销毁”这个动作,也就是调用queue_free()这个函数就是谓语。这样一来,我们通过<对象.调用函数>这一套模板实现了“某对象执行某动作”这一语句。

我们将调用函数的对象称为执行主语(Subject),被调用的函数叫做执行谓语(也叫动作或行为,Action 或 Behavior)

那么,如果我们所调用的函数需要一个参数传递进去呢?
  1. # 假设ref是Node2D类型的对象
  2. var target := get_node("Target") # 假设这个节点也是Node2D类型的对象
  3. ref.look_at(target.global_position)
复制代码
这里我们调用了ref的look_at()函数,该函数需要传入一个Vector2类型的参数,用于确定ref需要看向的位置,这里我们用target变量存储对节点Target的引用,然后在look_at()当中,我们将target传入,显然target并非Vector2类型的参数,因此会报错,而由于我们需要的是一个位置,因此我们需要获取target的位置属性,这里look_at()要求传入的位置是一个全局位置,因此我们需要获取target的global_position属性。这段代码的第三行我们便可以翻译为“ref执行看向动作,看向target的全局坐标”,这里target的全局坐标,也就是target.global_position就是整个动作的对象,我们可姑且将函数参数称为执行宾语(Object)。需要注意的是,这里的宾语实际上是target的global_position这个属性,而非target本身,但target是global_position这个属性的拥有者,相当于对global_position起到了修饰限定的作用,因此我们将target称作执行宾语的修饰语(或执行宾语定语,Attribute)

实际上,有些情况下,在执行主语后面加上"."之后,也会让该执行主语转变成定语,如下面这个例子:
  1. get_parent().get_node("MyChild").queue_free()
复制代码
我们先可以只看前面这部分:
  1. get_parent().get_node("MyChild")
复制代码
我们还可以再往前推一下:
  1. get_parent()
复制代码
这里get_parent()的执行主语为self,这里的self默认情况下可以省略,因此这一小段翻译过来就是“self执行获取父节点动作”,get_parent()在这里就变成了执行谓语,但是get_parent()会返回一个节点,此时如果我们再在后面加上一个get_node(xxx),也就相当于让父节点执行“获取节点”的操作,此时调用get_parent()函数实际上是要的返回结果,而这个返回结果就变成了执行主语,get_node(xxx)则变成了执行主语get_parent()的动作。因此,获取xx节点的操作也就发生在get_parent()身上而非self身上,因为此时self此时发生了语义转化(Semantic Transformation),从执行主语变成了执行主语的定语。而实际上的执行主语则因为语义偏移(Semantic Shift)而转到了“.”后面的语句上。

我们再回头看一下queue_free()那段代码,想要确定执行主语到底是谁很简单:从头看到尾,如果调用的函数会返回一个对象,且调用函数后面还接有一个函数调用,那么就接着往后看,直到看到有不返回值的函数为止。一般情况下,该不返回值函数就是执行谓语,其前面最近的那个对象(不论是通过变量存储引用还是通过函数调用获取返回值)即为执行主语,执行主语前面的均为执行定语。顺着这个思路,我们从get_parent()开始看起,get_parent()会返回一个节点,我们看后面有没有会返回值的函数,get_node(xxx)会返回值,因此我们再往后看,此时我们发现queue_free()不会返回值,因此它就是执行谓语,其前面紧邻的get_node(xxx)对象就是执行主语,该动作会从该对象身上发出。在前面的所有成分均为其执行定语。

执行主语和执行宾语十分重要,试想一下:如果你选错了一个执行主语:
  1. # 本来我是想让父节点的子节点(兄弟节点)来执行一些动作的
  2. get_parent().get_node("SomeNode").do_action()
  3. # 结果你写成了
  4. get_node("SomeNode").do_action()
复制代码
前面提过,默认的执行主语是self,如果是第一条,那么真正的执行主语是父节点的SomeNode子节点,但是如果省略了get_parent(),那么真正执行主语反而变成了self下方的SomeNode节点,有些情况下,如果没有对应节点,调用get_node()会报错。因此,确定好执行主语非常重要。除此之外,执行宾语也很重要,假设上述do_action()方法需要传递一个float类型的参数,如果你的执行宾语,一个是A.position,是Vector2类型;一个是B.position,是float类型,如果此时你把B.position的定语改为了A.,那么就算属性名是一样的,但是由于定语发生了变化,导致实际获取的position并不是float类型,也就是执行宾语也发生了变化,那么就会触发报错。

那么,赋值语句改如何判断执行主语和执行动作呢?
  1. var a = b
复制代码
实际上,赋值语句相当于调用对象的set()函数,因此可以转化为:
  1. set("a", b)
复制代码
实际上就是执行主语还是self,但是执行宾语变成了两个:一个是叫a的属性(变量),一个是数值b。但与此同时,也发生了一种潜在的语义偏移,此时a就拥有了b的值,变成了可用执行主语(Usable Subject),此时,a是可以作为后文的执行主语使用的,这一点在a为非数值类型时尤为明显。

好了,那么本贴就暂时告一段落,关于本贴的目的,其实很简单:就是提醒大家,在编写代码的时候,大家务必要确定好自己代码的执行主语和执行宾语,这样才能确保代码如期运行
>❀ To the Best You ❀<
您需要登录后才可以回帖 登录 | 创建账户

本版积分规则