Godot: 添加一个 GDScript Trait 系统。

创建于 2018-10-18  ·  93评论  ·  资料来源: godotengine/godot

(编辑:
为了最大限度地减少进一步的 XY 问题问题:
这里要解决的问题是 Godot 的节点场景系统/脚本语言还不支持创建可重用的分组实现,这些实现 1)特定于根节点的功能,2)可以交换和/或组合。 具有静态方法的脚本或具有脚本的子节点可用于后者,并且在许多情况下这是可行的。 但是,Godot 通常更喜欢您将场景整体行为的逻辑存储在根节点中,同时它使用由子节点计算的数据或将显着偏离的子任务委托给它们,例如 KinematicBody2D 不管理动画,因此它将其委托给 AnimationPlayer。

相比之下,使用“组件”子节点来驱动其行为的更薄的根节点是一个弱系统。 拥有一个基本上为空的根节点,只是将其所有行为委托给子节点,这与这种范式背道而驰。 子节点成为根节点的行为扩展,而不是独立完成任务的自给自足对象。 它非常笨重,并且可以通过允许我们整合根节点中的所有逻辑来简化/改进设计,但也允许我们将逻辑分解为不同的、可组合的块。

我想这个问题的主题更多是关于如何处理上述问题,而不是专门关于 GDScript,但我相信 GDScript Traits 将是解决问题的最简单和最直接的方法。
)

对于不知情的人来说,特征本质上是将两个类混合为一个的一种方式(几乎是一种复制/粘贴机制),只是,而不是从字面上复制/粘贴文件的文本,您所做的只是使用关键字语句来链接两个文件。 (编辑:诀窍是虽然脚本只能继承一个类,但它可以包含多个特征)

我在想象任何 GDScript 文件都可以用作另一个 GDScript 文件的特征,只要特征类型扩展了由合并到脚本继承的类,即 Sprite 扩展 GDScript 不能使用资源 GDScript作为特征,但它可以使用 Node2D GDScript。 我会想象一个类似于这样的语法:

# move_right_trait.gd
extends Node2D
class_name MoveRightTrait # not necessary, but just for clarity
func move_right():
    position.x += 1

# my_sprite.gd
extends Sprite
is MoveRightTrait # maybe add a 'use' or 'trait' keyword for this instead?
is "res://move_right_trait.gd" # alternative if class_name isn't used
func _physics_process():
    move_right() # MoveRightTrait's content has been merged into this script
    if MoveRightTrait in self:
        print("I have a MoveRightTrait")

我可以看到两种方法:

  1. 通过正则表达式为“^trait \”预解析脚本重新加载过程)。 我们必须不支持特征嵌套或在每次迭代后不断重新检查生成的源代码,以查看是否进行了更多特征插入。
  2. 正常解析脚本,但教解析器识别关键字,加载引用的脚本,解析THAT脚本,然后将其ClassNode的内容附加到当前脚本生成的ClassNode中(有效地获取一个脚本的解析结果并添加它到另一个脚本的解析结果)。 这将自动支持特征类型的嵌套。

另一方面,人们可能希望特性 GDScript 有一个名称,但可能不希望 GDScript 的 class_name 出现在 CreateDialog 中(因为它不是要自己创建的)。 在这种情况下,让任何脚本支持它实际上可能不是一个好主意; 只有那些特别标记的(也许通过在文件顶部写“特征”?)。 无论如何,要考虑的事情。

想法?

编辑:经过一番思考,我相信选项 2 会更好,因为 1)我们知道脚本段来自哪个脚本(以便更好地报告错误)和 2)我们能够识别自包含的脚本必须按顺序解析,而不是最后解析所有内容。 这将减少它添加到解析过程中的处理时间。

archived discussion feature proposal gdscript

最有用的评论

@aaronfranke TraitsMixins基本相同,具有与接口完全不同的用例,正是因为它们包含方法的实现。 如果一个接口给出了一个默认实现,那么它就不再一个接口了。

Traits/Mixins 存在于 PHP、Ruby、D、Rust、Haxe、Scala 和许多其他语言中(如链接的 Wiki 中详述),因此它们应该已经为对编程语言熟悉的人广泛熟悉。

如果我们要实现接口(我也不反对,尤其是可选的静态类型),它实际上只是一种指定函数签名的方式,然后要求相关的 GDScript 脚本实现这些函数签名,带有特征包括(如果那时已经存在)。

所有93条评论

有什么好处而不是: extends "res://move_right_trait.gd"

@MrJustreborn因为你可以在一个类中有多个特征,但你只能继承一个脚本。

如果我理解正确的话,这基本上就是 C# 所说的“接口”,但是使用非抽象方法? 调用特性接口而不是程序员熟悉的特征可能会更好。

@aaronfranke TraitsMixins基本相同,具有与接口完全不同的用例,正是因为它们包含方法的实现。 如果一个接口给出了一个默认实现,那么它就不再一个接口了。

Traits/Mixins 存在于 PHP、Ruby、D、Rust、Haxe、Scala 和许多其他语言中(如链接的 Wiki 中详述),因此它们应该已经为对编程语言熟悉的人广泛熟悉。

如果我们要实现接口(我也不反对,尤其是可选的静态类型),它实际上只是一种指定函数签名的方式,然后要求相关的 GDScript 脚本实现这些函数签名,带有特征包括(如果那时已经存在)。

也许像includes这样的关键字?

extends Node2D
includes TraitClass

尽管其他名称,如 trait、mixin、has 等,当然也可以。

我也喜欢从添加菜单中排除 class_name 类型的选项。 它可能会因无法单独作为节点运行的小型类型而变得非常混乱。

它甚至可能只是它自己的一个专题主题。

(不小心删除了我的评论,哎呀!另外,强制性的“你为什么不只允许多个脚本,统一做到这一点”

如果有的话,这将如何在 VisualScript 中工作?

此外,如果实现了特征,那么包含特征的检查器接口是否有益? 我想某些特征的用例可能包括只有特征而没有脚本的用例(至少,除了包含特征文件的脚本之外没有脚本)。 不过,仔细考虑后,我想知道与仅仅制作包含特征文件的脚本相比,制作这样一个界面的努力是否值得。

@LikeLakers2

如果有的话,这将如何在 VisualScript 中工作?

如果按照我建议的方式完成,VisualScript 根本不会发生这种情况。 仅 GDScript。 为 VisualScript 实现的任何特征系统都将完全不同地设计,因为 VisualScript 不是一种已解析的语言。 完全不排除这种可能性(只需要以不同的方式实施)。 另外,也许我们应该首先考虑获得 VisualScript 继承支持? 哈哈

此外,如果实现了特征,那么包含特征的检查器接口是否有益?

不会有太大意义。 trait 只是将细节传递给 GDScript,将其定义的属性、常量、信号和方法传递给它。

我想某些特征的用例可能包括只有特征而没有脚本的用例(至少,除了包含特征文件的脚本之外没有脚本)。

特征,因为它们用其他语言表示,永远不能单独使用,而必须包含在另一个脚本中才能使用。

我想知道为制作这样一个界面所付出的努力是否值得

以某种方式创建 Inspector 界面对于 GDScript 本身并没有多大意义。 添加或删除特征将涉及直接编辑脚本资源的source_code属性的源代码,即它不是脚本本身的属性。 因此,无论...

  1. 必须教编辑器如何专门处理正确编辑 GDScript 文件的源代码以执行此操作(容易出错),或者...
  2. 所有脚本都必须支持特征,以便 GDScriptLanguage 可以提供自己的内部过程来添加和删除特征(但并非所有语言都支持特征,因此该属性并非在所有情况下都有意义)。

对这样的功能有什么需求? 有什么是你现在不能做的吗? 或者它是否使某些任务的处理速度显着加快?

我宁愿让 GDscript 成为一种简单的语言,也不愿添加几乎从未使用过的复杂功能。

它解决了这个人遇到的 Child-Nodes-As-Script-Dependencies 问题,但没有任何与MultiScript 相同的包袱,因为它仅限于单一语言。 GDScript 模块可以隔离关于特征如何相互关联和主脚本的逻辑,而解决不同语言之间的差异会复杂得多。

由于没有多重导入/多重继承,子节点作为脚本依赖是避免重复代码的唯一方法,这肯定会很好地解决问题。

@groud @Zireael07我的意思是,更激进的跨语言方法是 1) 完全重新设计 Object 以使用 ScriptStack 将堆叠的脚本合并为单个脚本表示,2) 重新引入 MultiScript 并创建自动转换的编辑器支持将脚本添加到多脚本中(或者为了简单起见,直接将所有脚本设为多脚本,在这种情况下,MultiScript 的实现本质上就是我们的 ScriptStack),或者 3)为可以合并的 Object 类型实现一种跨语言特征系统将引用扩展脚本作为特征,像典型脚本一样合并它们的内容。 不过,所有这些选项都对引擎更具侵入性。 这使一切变得更简单。

我不认为特质是必要的。 我们最需要的是快速的引擎发布周期。 我的意思是我们需要让引擎更灵活地添加新功能,只需添加新的 dll 或 so 文件,引擎就会自动与其集成,就像大多数 IDE 中的插件样式一样。 例如,我真的非常需要 websocket 工作,我不需要等到 3.1 发布。 3.1 现在太坏了,有很多错误。 如果我们有这个功能,那就太好了。 新类可以从某个路径中的随机 .dll 或 .so 自动注入到 GDScript。 我不知道在 C++ 中要付出多少努力,但我希望这不会太难😁

@fian46好吧,如果有人将 websockets 实现为可下载的 GDNative 插件,那么是的,您所描述的就是工作流程。 相反,他们选择使其成为香草引擎中可用的集成功能。 没有什么可以阻止人们以这种方式创建功能,因此您的观点与本期主题无关。

哎呀,我不知道 GDNative 是否存在😂😂😂。 Trait 很棒,但是制作假的 trait 类和实例化它然后像基本脚本一样调用函数更容易吗?

如果 Godot 脚本是一个未命名的类,为什么不在 "my_sprite.gd" 中实例化 "move_right_trait.gd" 呢?
如果我不理解这个问题,请原谅我的无知。

我了解在诸如 Rust 或 C++(接口)等强类型语言中使用特征,但在闪避类型语言中这不是有点不必要吗? 简单地实现相同的功能应该允许您在您的类型之间实现统一的接口。 我想我有点不确定 GDScript 处理接口的方式的确切问题是什么,或者特征系统如何真正提供帮助。

难道你不能也使用 preload("Some-other-behavior.gd") 并将结果存储在一个变量中以达到基本相同的效果吗?

@fian46 @DriNeo好吧,是的,也不是。 加载脚本和使用脚本类已经解决了这个问题,但问题不止于此。

@TheYokai

实现相同的功能应该允许您在类型之间实现统一的接口

问题不在于实现一个统一的接口,你是对的,duck-typing 解决得很好,而是有效地组织(组合/交换)相关实现组。


在 3.1 中,使用脚本类,您可以在参考脚本(或任何类型)上定义静态函数,然后使用该脚本作为命名空间来全局访问 GDScript 中的这些函数。

extends Reference
class_name Game
static func print_text(p_text):
    print(p_text)
# can even add inner classes for sub-namespaces

extends Node
func _ready():
    Game.print_text("Hello World!")

但是,当涉及到非静态内容时,尤其是使用节点特定功能的内容时,划分一个人的逻辑很麻烦。

例如,如果我有一个 KinematicBody2D 并且我想要一个“跳跃”行为和一个“跑步”行为怎么办? 这些行为中的每一个都需要访问 KinematicBody2D 的输入处理和move_and_slide功能。 理想情况下,我将能够独立地交换每个行为的实现,并将每个行为的所有代码保存在单独的脚本中。

目前,我所熟悉的所有工作流程都不是最佳的。

  1. 如果您将所有实现保留在同一个脚本中,并且只是换掉正在使用的函数......

    1. 更改“行为”可能涉及将多个函数作为一组进行交换,因此您无法有效地对实现更改进行分组。

    2. 每个行为 (X * Y) 的所有函数都位于您的单个脚本中,因此它会很快变得臃肿。

  2. 您可以只替换整个脚本,但这意味着您必须为每个行为组合以及使用这些行为的任何逻辑创建一个新脚本。
  3. 如果您使用子节点作为脚本依赖项,那么这意味着您将拥有这些奇怪的“组件”Node2D 节点,它们会抓取它们的父节点并为其调用move_and_slide方法,相对而言,这有点不自然。

    • 您必须假设您的父母将实现该方法或执行逻辑以检查它是否具有该方法。 如果您进行检查,那么您可能会静默失败并可能在游戏中出现静默错误,或者您可以将其不必要地转换为工具脚本,这样您就可以在节点上设置配置警告,以便在编辑器中直观地告诉您有问题。

    • 您也没有为节点的预期操作获得正确的代码完成,因为它们从 Node2D 派生,并且重点是驱动 KinematicBody2D 父级的行为。

现在,我承认选项 3 是目前​​最有效的工作流程,并且它的问题可以通过在 3.1 中为 GDScript 使用方便的静态类型在很大程度上得到解决。 然而,还有一个更根本的问题在起作用。

Godot 的节点场景系统通常具有用户在自己的封闭系统中创建执行特定工作的节点或场景的形式。 您可以在另一个场景中实例化这些节点/场景,并让它们计算数据,然后由父场景使用(例如 Area2D 和 CollisionShape2D 关系的情况)。

但是,vanilla 引擎的用法和一般最佳实践建议是将场景的行为锁定到根节点和/或其脚本。 您几乎没有“行为组件”节点实际上告诉根要做什么(当它在那里时,感觉非常笨拙)。 AnimationPlayer/Tween 节点是我能想到的唯一有争议的例外,但即使它们的操作也是由根控制的(它实际上是临时将控制权委派给它们)。 (编辑:即使在这种情况下,动画和补间也不是 KinematicBody2D 的工作,因此委派这些任务是有道理的。但是,运动,如跑步和跳跃它的责任)允许更简单、更自然一个 trait 实现来组织代码,因为它严格保持节点之间的关系数据向上/行为向下,并将代码更加隔离到自己的脚本文件中。

嗯,将自己标记为“实现接口/特征”也应该完成* is *测试,这对于测试某些东西的功能很方便。

@OvermindDL1我的意思是,我举了一个这样的测试示例,但我使用in代替,因为我想区分继承和特征使用。

我想我在这里遇到了一个 XY 问题,我的错。 我刚刚从其他 2 个问题(#23052、#15996)以某种方式解决了这个主题,并认为我会提交一份提案,但我并没有真正提供所有背景信息。

@groud这个解决方案将解决针对#19486 提出的问题之一。

@willnationsdev好主意,我很期待!

据我有限的了解,这个 Trait 系统想要完成的事情是启用类似于此视频中显示的工作流程的功能: https ://www.youtube.com/watch?v=raQ3iHhE_Kk
(考虑到,我说的是显示的_workflow_,而不是使用的功能)

在视频中,它与其他类型的工作流程进行了比较,各有优缺点。

至少据我所知,目前在 GDScript 中这种工作流程是不可能的,因为继承是如何工作的。

@AfterRebelion视频的前几分钟,他隔离了代码库的模块化、可编辑性和可调试性(以及这些属性的相关细节),这确实是对拥有此功能的追求。

至少据我所知,目前在 GDScript 中这种工作流程是不可能的,因为继承是如何工作的。

这一点并不完全正确,因为 Godot 实际上在节点层次结构和设计场景方面做得很好。 场景本质上是模块化的,属性可以直接从编辑器中导出(甚至动画),而无需处理代码,并且所有内容都可以单独测试/调试,因为场景可以独立执行。

困难在于,通常外包给子节点的逻辑必须由根节点执行,因为该逻辑依赖于根节点的继承特征。 在这些情况下,使用组合的唯一方法是让孩子开始告诉父母该做什么,而不是让他们在父母使用它们时管好自己的事情。

这在 Unity 中不是问题,因为 GameObject 没有任何用户可以利用的真正继承。 在 Unreal 中,这可能有点(?)问题,因为它们具有相似的基于节点/组件的 Actor 内部层次结构。

好的,让我们在这里玩一下 Devil's Advocate( @MysteryGM ,你可能会从中得到乐趣)。 我花了一些时间思考如何在 Unreal 中编写这样一个系统,这让我对它有了新的认识。 对不起那些认为这是个好主意/对此感到兴奋的人:

引入 trait 系统给 GDScript 增加了一层复杂性,作为一种可能使其更难维护的语言。

最重要的是,特征作为一个特征使得更难隔离变量、常量、信号、方法甚至子类的实际来源。 如果您的 Node 脚本突然具有 12 个不同的特征,那么您不一定知道所有内容的来源。 如果您看到对某物的引用,那么您必须查看 12 个不同的文件才能知道某物在代码库中的位置。

这实际上会降低GDScript作为一种语言的可调试性,因为任何给定的问题都可能需要您在代码库中平均选择 2 或 3 个不同的位置。 如果这些位置中的任何一个很难找到,因为他们它们位于一个脚本中,但实际上位于其他地方 - 如果代码的可读性没有明确说明哪个东西负责数据/逻辑 - 那么那些 2 或 3步骤正在成倍增加,形成任意大的、高度紧张的步骤。

项目规模和范围的扩大进一步放大了这些负面影响,并使特征的使用成为一种相当站不住脚的品质。


但是可以做些什么来解决这个问题呢? 我们不希望子“组件逻辑”节点告诉场景根要做什么,但我们也不能依靠继承或更改整个脚本来解决我们的问题。

那么,在这种情况下,任何非 ECS 引擎会做什么? 组合仍然是答案,但在这种情况下,一个完整的节点在扩展/使所有权层次结构的动态复杂化时是不合理的。 相反,可以只定义非节点实现对象,抽象出一种行为的具体实现,但它们仍然归根节点所有。 这可以通过Reference脚本来完成。

# root.gd
extends KinematicBody2D

export(Script) var jump_impl_script = null setget set_jump_impl_script
var jump_impl
func set_jump_impl_script(p_script):
    jump_impl = p_script.new() if p_script else null

export(Script) var move_impl_script = null setget set_move_impl_script
var move_impl
func set_move_impl_script(p_script):
    move_impl = p_script.new() if p_script else null

func _physics_process():
    # use logic involving these...
    move_impl.move(...)
    jump_impl.jump(...)

如果导出以这样一种方式工作,我们可以在 Inspector 中将它们编辑为派生特定类型的类的枚举,就像我们可以为新的 Resource 实例一样,那会很酷。 现在唯一的方法是修复资源脚本导出,然后让你的实现脚本扩展资源(没有其他原因)。 但是,如果实现本身需要您希望能够从 Inspector 定义的参数,那么让它们扩展 Resource 将是一个好主意。 :-)

现在,让这更容易的是拥有一个片段系统或宏系统,以便开发人员更容易创建那些重用的声明性代码部分。

无论如何,是的,我想我发现了特征系统和解决问题的更好方法的明显问题。 万岁XY问题问题! /s

编辑:

因此,上述示例的工作流程将涉及设置实现脚本,然后在运行时使用脚本的实例来定义行为。 但是,如果实现本身需要您想从 Inspector 静态设置的参数怎么办? 这是一个基于资源的版本。

# root.gd
extends KinematicBody2D

# if you use a Resource script AND had a way of specifying that the assigned Resource 
# must extend that script, then the editor would automatically assign an instance of 
# that resource script to the var. No separate instancing or setter necessary.

export(Resource) var jump_impl = null # set jump duration, max height, tween easing via Inspector
export(Resource) var move_impl = null # similarly customize movement from Inspector

# can then create different Resources as different implementations. Because they are resources,
# one can edit them even outside of a scene!
func _physics_process():
    move_impl.move(...)
    jump_impl.jump(...)

相关:#22660

@AfterRebelion

考虑到,我说的是显示的工作流程,而不是使用的功能

具有讽刺意味的是,在您澄清这一点并且我同意最佳工作流程之后,然后不同意评论的后面部分,然后我通过基本上说该视频中的“使用的功能”实际上是解决问题的理想方式来跟进它无论如何,Godot 中的这个问题。 哈哈。

# root.gd
extends KinematicBody2D

export(Script) var jump_impl_script = null setget set_jump_impl_script
var jump_impl
func set_jump_impl_script(p_script):
jump_impl = p_script.new() if p_script else null
...

哇,不知道出口这么厉害。 预计它只能与原语和其他数据结构进行交互。

这使我之前的评论无效。
正如您所说,如果实现某种宏以使其实现更容易,那将是实现该工作流程而不需要 MultiScript 的最佳方式。 也许不如 Unity 多才多艺,因为您仍然需要事先声明所有可能的脚本,但仍然是一个不错的选择。

@AfterRebelion

正如您所说,如果实现某种宏以使其实现更容易,那将是实现该工作流程而不需要 MultiScript 的最佳方式。

好吧,我在同一条评论中提到的基于Resource的方法,再加上来自 #22660 的一些更好的编辑器支持,将使质量与 Unity 可以做的相当。

也许不如 Unity 多才多艺,因为您仍然需要事先声明所有可能的脚本,但仍然是一个不错的选择。

好吧,如果他们在 3.2 中修复了数组类型提示,那么您可以为文件路径定义一个导出数组,该数组必须扩展脚本并有效地添加您自己的。 这甚至可以通过 3.1 中的插件来完成,方法是使用EditorInspectorPlugin类将自定义内容添加到特定资源或节点的 Inspector。

我的意思是,如果你想要一个类似“Unity”的系统,那么你真的会有子节点来告诉根要做什么,你只需要通过引用它们的名称来获取它们,而不需要手动声明它们或添加它们从根节点的脚本。 Resource 方法通常更有效,并且保持更干净的代码库。

因为特征系统会对 GDScript 代码的可用性造成过大的压力,基于我概述的原因,我将继续关闭这个问题。 添加它的缺陷远远超过我们可能从这样一个系统中获得的任何相对微薄的好处,并且这些相同的好处可以以更清晰和可用性的不同方式实现。

好吧,我出去的时候错过了这次讨论。 还没有阅读所有内容,但我有向 GDScript 添加特征以解决代码重用问题的想法,我发现这比将脚本附加到派生类型的虚假多重继承更优雅和清晰。 虽然我要做的是制作特定的特征文件,但不允许任何类文件成为特征。 我不认为这会太糟糕。

但我愿意接受解决主要问题的建议,即在多个节点类型中重用代码。

@vnen我提出的最后一项解决方案是将可重用部分外包给资源脚本。

  • 它们仍然可以在 Inspector 中公开并编辑它们的属性,就像它们是节点上的成员变量一样。

  • 它们保留了数据和逻辑来自何处的清晰线索,如果将许多特征包含在一个脚本中,则很容易受到损害。

  • 它们不会给作为语言的 GDScript 添加任何过度的复杂性。 例如,如果它们存在,我们必须解决如何将共享质量(属性、常量、信号)合并到主脚本中(或者,如果不合并,则强制用户处理依赖冲突)。

  • 资源脚本更好,因为它们可以从 Inspector 分配和更改。 设计师、作家等将能够直接从编辑器更改实现对象。

@willnationsdev我看到了(尽管“资源脚本”这个名字听起来很奇怪,因为所有脚本都是资源)。 这个解决方案的主要问题是它没有解决人们对继承方法的期望:将导出的变量和信号添加到场景的根节点(特别是在其他地方实例化场景时)。 您仍然可以编辑从子资源中导出的变量,但它变得不太实用(您无法一眼看出您可以编辑哪些属性)。

另一个问题是它需要大量重复样板代码。 您还必须确保在调用函数之前实际设置了资源脚本。

优点是我们不需要任何东西,它已经可用。 我想这应该记录在案,使用当前“将脚本附加到派生节点”方法的人可以评论解决方案以查看它的可行性。

@vnen

但它变得不太实用(您无法一眼看出可以编辑哪些属性)。

你能详细说明你的意思吗? 我看不出哪些属性是可访问的,尤其是在合并 #22660 之类的内容时,怎么会存在明显的不明确性。

  1. 我实例化了场景,想知道如何编辑它,因此查看该场景的根节点的脚本。
  2. 在剧本里面,我看到...

    • export(MoveImpl) var move_impl = FourWayMoveImpl.new()

    • use FourWayMoveTrait

  3. 假设我们有一种点击跟踪标识符的方法(这应该是真正的 3.1.1 功能!)来打开脚本,然后我们最终打开关联的脚本并可以查看其属性。

对我来说似乎是相同数量的步骤,除非我错过了什么。

此外,我想说的资源实际上清楚哪些属性是可编辑的,因为如果一个实现具有特定于它的属性,那么如果您必须在访问它的开头使用实现实例,即move_impl.<property>

另一个问题是它需要大量重复样板代码。 您还必须确保在调用函数之前实际设置了资源脚本。

这是真的,但是,我仍然认为它是一个带有初始化的导出的好处超过了添加的语言的成本:

(“高级队友”,HLT,如设计师、作家、艺术家等)

  • 可以直接从 Inspector 分配值,而不必打开脚本,找到要更改的正确行,然后更改它(已经提到,但它会导致...)。

  • 可以指定导出的内容具有基本类型要求。 然后,Inspector 可以自动提供允许实现的枚举列表。 然后,HLT 可以安全地只分配该类型的派生。 这有助于将他们与需要了解所有不同特征脚本的后果的替代方案隔离开来。 我们还必须修改 GDScript 中的自动完成功能,以支持查找命名和未命名的特征文件,以响应看到use关键字。

  • 可以将实现的配置序列化为 *.tres 文件。 然后,HLT 可以从 FileSystem Dock 拖放它们,甚至可以在 Inspector 中创建自己的权限。 如果希望对特征做同样的事情,他们必须创建一个派生特征,该特征提供一个自定义构造函数来覆盖默认构造函数。 然后他们会通过命令式编码的构造函数将该特征用作“预配置”。

    1. 较弱,因为它是命令式的而不是声明式的。
    2. 较弱,因为构造函数必须在脚本中显式定义。
    3. 如果特征未命名,则用户需要知道特征的位置才能正确使用它,而不是默认的基本特征。 如果特征被命名,那么它会不必要地阻塞全局命名空间。
    4. 如果他们将脚本更改为use FourWayMoveTrait而不是use MoveTrait ,则不再有任何持久的迹象表明该脚本甚至与基本MoveTrait兼容。 它使 HLT 混淆FourWayMoveTrait是否甚至可以更改为不同的MoveTrait而不会破坏事物。
    5. 如果 HLT 以这种方式创建新的 trait 实现,他们不一定知道可以/需要从基本 trait 设置的所有属性。 这是在检查器中创建的资源的非问题。
  • 甚至可以拥有多个相同类型的资源(如果有原因的话)。 特征不支持这一点,而是会触发解析冲突。

  • 无需离开 2D/3D 视口即可更改配置和/或它们的单独值。 这对于 HLT 来说要舒服得多(我知道很多人在必须查看代码会彻底生气)。

尽管如此,我同意样板文件很烦人。 不过,为了解决这个问题,我宁愿添加一个宏或片段系统来简化它。 片段系统是个好主意,因为它可以支持任何可以在 ScriptEditor 中编辑的语言,而不仅仅是 GDScript。

关于:

但它变得不太实用(您无法一眼看出可以编辑哪些属性)。

我说的是检查员,所以我指的是这里的“HLT”。 不会查看代码的人。 使用特征,我们可以将新的导出属性添加到脚本中。 使用资源脚本,您只能将变量导出到资源本身,因此除非您编辑子资源,否则它们不会显示在检查器中。

我理解你的论点,但它超出了最初的问题:避免重复代码(没有继承)。 特征是为程序员准备的。 在许多情况下,您只想重用一个实现,并且您不想在检查器中公开它。 当然你仍然可以直接在代码中赋值来使用资源脚本而不需要导出,但是仍然不能解决从通用实现中重用导出的变量和信号的问题,这是人们试图这样做的主要原因之一使用继承。

也就是说,人们目前正在尝试将通用脚本的继承不仅用于函数,还用于导出的属性(通常与这些函数相关)和信号(然后可以与 UI 连接)。

这是很难解决的问题。 有一些方法可以单独使用 GDScript,但同样需要复制样板代码。

我只能想象为了让外部脚本表现得好像它们是直接写在主脚本本身中一样,必须反复进行。

如果天地不必为实现它而移动,那将是一件好事。 X)

@vnen我明白你现在在说什么。 好吧,既然这个问题似乎还有一些生命,我下次有机会时会重新打开它。

重开这个有消息吗? 既然宣布了 GodotCon 2019,并且 Godot Sprint 是一件事,也许这值得在那里讨论。

@AfterRebelion我刚刚忘记回来重新打开它。 谢谢你的提醒。 XD

@willnationsdev我喜欢我读到的关于 EditorInspectorPlugin 的内容! 快速提问,这意味着我可以为数据类型创建一个自定义检查器......例如......向检查器添加一个按钮。
(自从我想这样做以来已经有一段时间了,为了有一种方法来触发事件以使用检查器中的按钮进行调试,我可以让按钮按下执行一些脚本)

@xDGameStudios是的,这一切都是可能的。 任何自定义控件都可以添加到检查器中,可以在顶部、底部、属性上方或类别下方。

@willnationsdev不知道能不能私信联系你!! 但我想了解更多关于 EditorInspectorPlugin 的信息(如一些示例代码).. 自定义资源类型(例如) MyResource 的行中的某些内容,它具有属性导出“名称”和打印“名称”的检查器按钮如果我按下它(在编辑器中或调试期间),变量! 文档在这件事上缺乏大量时间......如果我知道如何使用它,我会自己编写文档! :D 谢谢

我也想了解更多。 X)

那么这与带有包含静态函数的子类的自动加载脚本相同吗?

例如,您的案例将变为Traits.MoveRightTrait.move_right()

甚至更简单,为不同的 trait 类提供不同的自动加载脚本:

Movement.move_right()

不,特征是一种特定于语言的功能,相当于将一个脚本的源代码复制粘贴到另一个脚本中(有点像文件合并)。 因此,如果我有move_right()的特征,然后我声明我的第二个脚本使用该特征,那么它也可以使用move_right() ,即使它不是静态的,即使它访问类中其他地方的属性。 如果该属性在第二个脚本中不存在,它只会导致解析错误。

我发现我要么有重复的代码(较小的函数,例如制作弧线),要么有多余的节点,因为我不能有多重继承。

这太棒了,我发现自己必须制作一个功能完全相同的脚本,只是在不同的节点类型上使用会导致不同的extend s,所以基本上只针对一行代码。 顺便说一句,如果有人知道如何使用当前系统做到这一点,请告诉我。

如果我有extends Node脚本的功能,有没有办法将相同的行为附加到另一个节点类型,而不必复制源文件并用适当的extend替换?

这方面有什么进展吗? 正如我之前所说,我一直不得不重复代码或必须添加节点。 我知道它不会在 3.1 中完成,但可能针对 3.2?

哦,我根本没有在做这个。 事实上,我在扩展方法的实现上比这更进一步。 不过,我需要与 vnen 讨论这两个问题,因为我想和他一起制定出最好的语法/实现细节。

编辑:如果其他人想尝试实施特征,欢迎他们加入。 只需要与开发人员协调。

“扩展方法实现”?

@Zireael07 #15586
它允许人们编写可以为引擎类添加新的“内置”功能的脚本。 我对语法的解释是这样的:

static Array func sum(p_self: Array):
    if not len(p_self):
        return 0
    var value = p_self[0]
    for i in range(1, len(p_self)):
        value += p_self[i]
    return value

然后,在其他地方,我可以这样做:

var arr = [1, 2, 3]
print(arr.sum()) # prints 6

它会偷偷调用不断加载的扩展脚本,并调用绑定到 Array 类的同名静态函数,并将实例作为第一个参数传入。

我已经与@vnen@jahd2602讨论过这个问题。 想到的一件事是 Jai 对多态性的解决方案:导入属性的命名空间。

这样,您可以执行以下操作:

class A:
    var a_prop: String = "Hello"
    func foo():
        print("A's a_prop: ", a_prop)
    func bar():
        print("A's bar()")

class B:
    using var a: A = A.new()
    var a_prop: String = "World" # Overriding A's a_prop

    func bar():  # Overriding A's bar()
        print("B's bar()")

func main():
    var b: B = B.new()
    b.foo() # output: "A's a_prop: World"
    b.bar() # output: "B's bar()"

关键是using关键字导入了属性的命名空间,因此b.foo()实际上只是b.a.foo()的语法糖。

然后,确保b is A == true和 B 可以在也接受 A 的类型化情况下使用。

这还有一个好处,那就是不必将事物声明为特征,这适用于任何没有共同质量名称的事物。

一个问题是这与当前的继承系统不能很好地结合。 如果 A 和 B 都是 Node2D 并且我们在 A 中创建一个函数: func baz(): print(self.position) ,当我们调用b.baz()时将打印哪个位置?
一种解决方案可能是让调用者确定self 。 调用 b.foo() 将调用 foo() 并将 b 作为自身,而 bafoo() 将调用 foo() 并将 a 作为自身。

如果我们有独立的方法,例如 Python(其中x.f(y)f(x,y)的糖),这可能非常容易实现。

另一个不相关的想法:

只关注独立函数,JavaScript 风格。

如果我们对静态函数采用x.f(y) == f(x,y)约定,我们可以很容易地得到以下内容:

class Jumper:
    static func jump(_self: KinematicBody2D):
        # jump implementation

class Runner:
    static func run(_self: KinematicBody2D, direction: Vector2):
        # run implementation

class Character:
    extends KinematicBody2D
    func run = Runner.run       # Example syntax
    func jump = Jumper.jump

func main():
    var character = Character.new()
    character.jump()
    character.run(Vector2(1,0))

这对类系统的影响很小,因为它实际上只影响方法。 但是,如果我们真的希望它灵活,我们可以使用完整的 JavaScript,只允许函数定义是可分配的、可调用的值。

@jabcross听起来不错,我确实喜欢某种可选命名空间的概念,而且函数的想法很有趣。

关于命名空间之一,我想知道为什么不简单地using A ,其他声明性的东西似乎无关紧要。

也很好奇它需要如何通过多重继承来解决。 我想选项 A 是强制两个脚本继承相同的类型,所以它们都只是在同一个类之上扩展,没有任何特殊的合并。

选项 B,也许是额外的 GDScript 关键字来指定一个特征类,以及你想从哪个类中获得提示。 这将是相同的想法,但只是额外的步骤看起来更明确。

在 A.gd 的顶部:

extends Trait as Node2D
is Trait as Node2D
is Trait extends B
extends B as Trait

哦,我真的很喜欢命名空间导入的概念。 它不仅解决了 trait 问题,还潜在地解决了将内容添加到引擎类型的“扩展方法”概念。

class_name ArrayExt
static func sum(_self: Array) -> int:
    var sum: int = 0
    for a_value in _self:
        sum += a_value
    return sum

using ArrayExt
func _ready():
    var a = [1, 2, 3]
    print(a.sum())

@jabcross如果我们随后还添加了lambas 和/或允许对象实现调用运算符(并且具有兼容值的callable类型),那么我们可以开始向 GDScript 代码添加更面向功能的方法(其中我认为这将是一个好主意)。 当然,那时更多地走进@vnen的#18698 领域,但是......

我们必须考虑到 GDScript 仍然是一种动态语言,其中一些建议需要运行时检查才能正确调度调用,这会增加性能损失(即使对于不使用这些功能的人来说,因为它需要检查所有函数调用也许还有属性查找)。 这也是为什么我不确定添加扩展是否是个好主意的原因(特别是因为它们本质上是语法糖)。

我更喜欢纯特征系统,其中特征不是类,而是它们自己的东西。 这样,它们可以在编译时完全解决,并在名称冲突的情况下提供错误消息。 我相信这将解决问题而无需额外的运行时开销。

@vnen Ahhh,没有意识到运行时成本。 如果这适用于任何扩展方法实现,那么我想这也不理想。

如果我们当时做了一个纯特征系统,你是否认为只是用trait TraitName代替extendsusing TraitName $ 配对在其他脚本中扩展? 您会自己实施,还是委托他人实施?

如果我们当时做了一个纯特征系统,你是否认为只是用trait TraitName代替extendsusing TraitName $ 配对在其他脚本中扩展?

这就是我的想法。 我相信它很简单,几乎涵盖了代码重用的所有用例。 我什至会允许使用 trait 的类覆盖 trait 方法(如果可以在编译时执行)。 特征还可以扩展其他特征。

您会自己实施,还是委托他人实施?

我不介意把任务交给能胜任的人。 反正我时间不够。 但是我们应该事先就设计达成一致。 我对细节非常灵活,但它应该 1)不会在运行时检查中产生(相信 GDScript 非常适合在编译时无法弄清楚的东西),2)相对简单,3)不要添加太多编译时间。

@vnen喜欢这些想法。 想知道你如何想象一个 trait 能够为包含它的类执行自动完成之类的事情,或者这不可能?

想知道你如何想象一个 trait 能够为包含它的类执行自动完成之类的事情,或者这不可能?

在我看来,特质本质上是一种“进口”。 假设成员的完成工作,显示完成应该是微不足道的。

@vnen我想你基本上会解析成一个带有trait标志的 ClassNode 。 然后,如果您执行using语句,它将尝试将所有属性/方法/信号/常量/子类合并到当前脚本中。

  1. 如果一个方法发生冲突,那么当前脚本的实现将覆盖基本方法,就像它覆盖一个继承的方法一样。

    • 但是如果基类已经有了“合并”方法怎么办?

  2. 如果属性、信号和常量重叠,请检查它是否是相同的类型/信号签名。 如果没有不匹配,则只需将其视为属性/信号/常数的“合并”。 否则,通知用户类型/签名冲突(解析错误)。

馊主意? 还是我们应该直接禁止非方法冲突? 子类呢? 我有一种感觉,我们应该让这些成为冲突。

@willnationsdev听起来像“钻石问题”(又名“致命的死亡钻石”),这是一个有据可查的歧义,不同的解决方案已经应用于各种流行的编程语言。

这使我想起:
@vnen特征是否能够扩展其他特征?

@jahd2602他已经建议这是一种可能性

特征还可以扩展其他特征。

@jahd2602基于 Perl/Python 解决方案,它们似乎基本上形成了一个“堆栈”层,其中包含每个类的内容,因此最后使用的特征的冲突位于其他版本之上并覆盖其他版本。 对于这种情况,这听起来是一个很好的解决方案。 除非您或@vnen有其他想法。 感谢您提供有关解决方案 jahd 的链接概述。

几个问题。

第一:我们应该以什么方式支持using语句?

我在想using语句应该需要一些常量值 GDScript。

using preload("res://my_trait.gd") # a preloaded expression
using ScriptClass.MyTrait # a const resource
using Autoload.MyTrait # a const resource
using MyTrait # a regular script class

以上我都在想。

第二:我们应该说定义特征和/或其名称的允许语法应该是什么?

有些人可能不一定想为他们的 trait 使用脚本类,所以我认为我们不应该强制执行trait TraitName要求。 我在想trait必须在一条线上。

所以,如果它是一个工具脚本,它当然应该在顶部有工具。 然后,如果它是一个特征,它必须在下一行定义它是否是一个特征。 可选地允许某人在同一行的 trait 声明之后声明脚本类名称,如果他们这样做,则不允许他们也使用class_name 。 如果他们省略了特征名称,那么class_name <name>就可以了。 然后,当扩展另一种类型时,我们可以在 trait 声明之后和/或在 trait 声明之后的一行中插入extends 。 所以,我认为这些都是有效的:

# Global name from trait keyword.
trait MyTrait extends BaseTrait

# Global name from class_name keyword, but is still a trait and also happens to be a tool script.
tool
trait
extends BaseTrait
class_name MyTrait

# A trait with no global name associated with it. Does not extend anything.
trait

第三:为了自动完成的目的和/或意图/要求的声明,我们是否应该允许特征定义它必须扩展的基本类型?

我们已经讨论过特征应该支持继承其他特征。 但是我们是否应该允许 TraitA 到extend Node ,允许 TraitA 脚本获取 Node 自动补全,但是如果我们在当前类不扩展 Node 时执行using TraitA语句也会触发解析错误或其任何派生类型?

第四:我们不能让特征扩展其他特征,而是简单地将extends语句保留用于类扩展,允许特征根本不需要此语句,而是扩展基本特征,允许特征只是有自己的using语句子导入这些特征?

# base_trait.gd
trait
func my_method():
    print("Hello")

# derived_trait.gd
trait
using preload("base_trait.gd")
func my_method():
   print("World") # overrides previous method, will only print "World".

当然,这里的好处是您可以通过使用多个using语句在单个特征名称下批处理多个特征,类似于包含其他几个类的 C++ 包含文件。

第五:如果我们有一个特征,并且它有一个usingextends用于方法,然后实现它自己的,当它调用时我们会做什么,在那个函数.<method_name>执行基本实现? 我们是否假设这些调用总是在类继承上下文中执行,并且特征层次在这里没有影响?

抄送@vnen

第一:我们应该以什么方式支持using语句?

我在想using语句应该需要一些常量值 GDScript。

using preload("res://my_trait.gd") # a preloaded expression
using ScriptClass.MyTrait # a const resource
using Autoload.MyTrait # a const resource
using MyTrait # a regular script class

我对所有这些都很好。 但是对于路径,我直接使用字符串: using "res://my_trait.gd"

第二:我们应该说定义特征和/或其名称的允许语法应该是什么?

有些人可能不一定想为他们的 trait 使用脚本类,所以我认为我们不应该强制执行trait TraitName要求。 我在想trait必须在一条线上。

所以,如果它是一个工具脚本,它当然应该在顶部有工具。 然后,如果它是一个特征,它必须在下一行定义它是否是一个特征。 可选地允许某人在同一行的 trait 声明之后声明脚本类名,如果他们这样做,则不允许他们也使用class_name 。 如果他们省略了特征名称,那么class_name <name>就可以了。 然后,当扩展另一种类型时,我们可以在 trait 声明之后和/或在 trait 声明之后的一行中插入extends 。 所以,我认为这些都是有效的:

# Global name from trait keyword.
trait MyTrait extends BaseTrait

# Global name from class_name keyword, but is still a trait and also happens to be a tool script.
tool
trait
extends BaseTrait
class_name MyTrait

# A trait with no global name associated with it. Does not extend anything.
trait

特征上的tool应该没有任何区别,因为它们不是直接执行的。

我同意特征不一定具有全局名称。 我会以与tool类似的方式使用trait #$ 。 它必须是脚本文件中的第一件事(注释除外)。 关键字后面应该可选地跟随特征名称。 我不会为他们使用class_name ,因为他们不是类。

第三:为了自动完成的目的和/或意图/要求的声明,我们是否应该允许特征定义它必须扩展的基本类型?

老实说,我不喜欢为了编辑器而在语言中添加功能。 这就是注释会派上用场的地方。

现在,如果我们想让特征只应用于某种类型(及其派生类),那就没问题了。 事实上,我认为这对于静态检查来说更好:它允许 trait 使用类中的东西,而编译器实际上可以检查它们是否与正确的类型一起使用等等。

第四:我们不能让特征扩展其他特征,而是简单地将extends语句保留用于类扩展,允许特征根本不需要此语句,而是扩展基本特征,允许特征来简单地拥有自己的using语句,其中子导入_那些_特征?

嗯,这主要是语义问题。 当我提到特征可以扩展另一个时,我并不是真的要使用extends关键字。 主要区别在于extends只能扩展一个,而using可以将许多其他特征嵌入到一个中。 我对using没问题,只要没有循环,就不是问题。

第五:如果我们有一个特征,并且它有一个usingextends用于方法,然后实现它自己的,当它调用时我们会做什么,在那个函数.<method_name>执行基本实现? 我们是否假设这些调用总是在类继承上下文中执行,并且特征层次在这里没有影响?

这是一个棘手的问题。 我会假设特征与类继承无关。 所以点符号应该调用父特征上的方法,如果有的话,否则抛出错误。 特质不应该知道它们所在的类。

OTOH,特征几乎就像“包含”,因此它将逐字应用于类,因此调用父实现。 但老实说,如果在父特征中找不到该方法,我会简单地禁止点符号。

要求类具有一个或多个其他特征的特征呢? 例如,一个特征DoubleJumper需要特征Jumper 、特征Upgradable和继承KinematicBody2D的类。

例如,Rust 允许您使用类似的类型签名。 类似KinematicBody2D: Jumper, Upgradable的东西。 但是由于我们使用:来注释类型,我们可以只使用KinematicBody2D & Jumper & Upgradable或其他东西。

还有多态性问题。 如果每个类的 trait 实现不同,但它公开了相同的接口怎么办?

例如,我们想要kill()特征中的方法JumperEnemyPlayer都使用该方法。 我们希望每种情况都有不同的实现,同时仍然保持与相同的Jumper类型签名兼容。 这个怎么做?

对于多态性,您只需创建一个单独的特征,其中包含带有kill()的特征,然后实现它自己的特定版本的方法。 使用覆盖以前包含的特征方法的特征是您处理它的方式。

另外,我认为(还)没有任何计划让类型提示具有特征要求。 这是我们想做的事情吗?

创建一个单独的特征

那不会生成一堆一次性的特征文件吗? 如果我们可以做嵌套的 trait 声明(类似于class关键字),那会更方便。 我们也可以直接在使用 trait 的类中重写方法。

我真的很欣赏一个强大的类型签名系统(可能具有布尔组合和可选/非空值)。 特质很适合。

我不确定它是否被讨论过,但我认为应该可以调用函数的特征特定版本。 例如:

trait A
func m():
  print("A")

trait B
func m():
  print("B")

class C
using A
using B

func c():
  A.m()
  B.m()
  m()

打印: ABB


我也不完全确定“无运行时成本”。 使用导出前定义的特征的类如何处理动态加载的脚本(导出期间不可用)? 我是不是误会了什么? 或者这种情况不被认为是“运行时”?

我不确定它是否被讨论过,但我认为应该可以调用函数的特征特定版本。

我已经在考虑这一点,但我不确定是否允许类使用冲突的特征(即定义具有相同名称的方法的特征)。 using语句的顺序应该没有区别。

我也不完全确定“无运行时成本”。 使用导出前定义的特征的类如何处理动态加载的脚本(导出期间不可用)? 我是不是误会了什么? 或者这种情况不被认为是“运行时”?

这不是关于出口。 它肯定会影响加载时间,因为编译发生在加载时(尽管我认为这不会很重要),但它不应该影响脚本执行的时间。 理想情况下,脚本应该在导出时编译,但这是另一个讨论。

大家好。

我是 Godot 的新手,过去几天已经习惯了。 当我试图找出用于制作可重用组件的最佳实践时,我决定了一种模式。 我总是将要在另一个场景中实例化的子场景的根节点导出我打算从外部设置的所有属性。 尽可能地,我想让场景的其余部分不需要了解实例化分支的内部结构。

为了完成这项工作,根节点必须“导出”属性,然后将值复制到 _ready 中的相应子节点。 因此,例如,想象一个带有子 Timer 的 Bomb 节点。 子场景中的根 Bomb 节点将导出“detonation_time”,然后在 _ready 中执行$Timer.wait_time = detonation_time 。 这允许我们在实例化它时在 Godot 的 UI 中很好地设置它,而不必让子项可编辑并深入到 Timer。

然而
1)这是一个非常机械的转换,所以系统似乎可以支持类似的东西
2) 与直接在子节点中设置适当的值相比,它可能会稍微降低效率。

在我继续之前,这似乎与正在讨论的内容相切,因为它不涉及允许某种“私有”继承(用 C++ 的说法)。 然而,我实际上喜欢 Godot 通过组合场景元素而不是更多类似继承的工程来构建行为的系统。 那些“写入”的关系是不变的和静态的。 OTOH,场景结构是动态的,您甚至可以在运行时更改它。 游戏逻辑在开发过程中非常容易发生变化,我认为 Godot 的设计非常适合用例。

确实,子节点被用作根节点的行为扩展,但这不会导致它们缺乏自给自足,IMO。 计时器是完全独立的,并且无论它习惯于什么时间,它的行为都是可预测的。 无论你是用勺子喝汤还是吃冰淇淋,它都能很好地发挥它的作用,即使它是你手的延伸。 我将根节点视为协调子节点行为的大师,因此它们不必直接了解彼此,因此能够保持自给自足。 父/根节点是桌面绑定的管理器,他们委派职责但不做太多直接工作。 由于它们很薄,因此很容易为稍微不同的行为创建一个新的。

但是,我认为根节点也应该作为整个实例分支功能的主要接口。 所有可以在实例中调整的属性都应该在分支的根节点中是“可设置的”,即使该属性的最终所有者是某个子节点也是如此。 除非我遗漏了什么,否则必须在当前版本的 Godot 中手动安排。 如果这可以以某种方式自动化以将动态系统的优点与更简单的脚本结合起来,那就太好了。

我正在考虑的一件事是“动态继承”系统,如果你愿意的话,它可用于 Node.js 的子类。 在这样的脚本中会有两个属性/方法来源,一个是它扩展的脚本,另一个是从场景结构中的子级“冒泡”的。 因此,我的 Bomb 示例将在 bomb.gd 脚本的成员变量部分中变成类似于export lifted var $Timer.wait_time [= value?] as detonation_time的内容。 系统本质上会在 _ready 回调中生成$Timer.wait_time = detonation_time并生成允许来自 Bomb 节点的父节点的$Bomb.detonation_time = 5导致设置$Timer.wait_time = 5的 getter/setter。

在带有 MoveRightTrait 的 OP 示例中,我们将 mysprite.gd 附加到的节点将 MoveRightTrait 作为子节点。 然后在 mysprite.gd 中我们会有类似lifted func $MoveRightTrait.move_right [as move_right]的东西(当名称相同时,'as' 可能是可选的)。 现在在从 mysprite.gd 创建的脚本对象上调用 move_right 将自动委托给适当的子节点。 也许信号可以冒泡,以便它们可以从根连接到子节点? 也许整个节点可以只用lifted $MoveRightTrait [as MvR]冒泡,而没有 func、signal 或 var。 在这种情况下,MoveRightTrait 上的所有方法和属性都可以直接从 mysprite 作为 mysprite.move_right 访问,如果使用“as MvR”,则可以通过 mysprite.MvR.move_right 访问。

这是如何在实例化分支的根目录中简化对场景结构的 INTERFACE 的创建,增加它们的“黑匣子”特征并获得脚本编写便利以及 Godot 动态场景系统的强大功能的一个想法。 当然,还有很多细节需要考虑。 例如,与基类不同,子节点可以在运行时删除。 如果在该错误情况下调用/访问,冒泡/提升的函数和属性应该如何表现? 如果添加了具有正确 NodePath 的节点,提升的属性是否会再次开始工作? [是的,IMO] 在不是从 Node 派生的类中使用“lifted”也是一个错误,因为在这种情况下永远不会有孩子可以冒泡/提升。 此外,重复的“as {name}”或“lifted $Timer1 lift $Timer2”可能会发生名称冲突,其中节点具有相同名称的属性/方法。 理想情况下,脚本解释器会检测到此类逻辑问题。

我觉得这会给我们很多我们想要的东西,尽管它实际上只是语法糖,使我们不必编写转发函数和初始化。 此外,因为它在概念上基本上很简单,所以实现或解释应该不难。

无论如何,如果你能走到这一步,感谢阅读!

我到处都用了“提升”,但这只是说明性的。
using var $Timer.wait_time as detonation_timeusing $Timer这样的东西显然也一样好。 在任何情况下,您都可以方便地从子节点进行伪继承,从而在要实例化的分支的根中创建对所需功能的一致的单点访问。 对可重用功能的要求是它们扩展 Node 或其子类,以便它们可以作为子类添加到更大的组件中。

另一种看待它的方式是,从 Node 继承的脚本上的“extends”关键字为您提供了“is-a”关系,同时在脚本上使用“using”或“lifted”关键字来“冒泡”a后代节点的成员为您提供了类似于“实现”[嘿,可能的关键字],存在于具有单一继承但多个“接口”的语言(例如 Java)中。 在不受限制的多重继承(如 c++)中,基类形成一个 [静态,写入] 树。 以此类推,我有点提议在 Godot 的现有节点树上分层方便的语法和样板消除。

如果确定这是值得探索的东西,则需要考虑设计空间的几个方面:
1)我们是否应该只允许“使用”中的直系子级。 IOW using $Timer但不是using $Bomb/Timer'? This would be simpler but would force us to write boilerplate in some cases. I say that a full NodePath ROOTED in the Node to which the script is attached should be legal [but NO references to parents/siblings allowed]. 2) Should there be an option that find_node 的the "using"-ed node instead of following a written in NodePath? For example using "Timer" with a string for the pattern would be slower but the forwarding architecture would continue to work if a referenced node's position in the sub-tree changes at run time. This could be used selectively for child nodes that we expect to move around beneath the root. Of course syntax would have to be worked out especially when using a particular member (eg. using var "Timer".wait_time as detonation_time is icky). 3) Should there be a way query for certain functionality [equivalent to asking if an interface is implemented or a child node is present]? Perhaps "using" entire nodes with aliases should allow testing the alias to be a query. So using MoveRightTrait as DirectionalMover in a script would result in node.DirectionalMover returning the child MoveRightTrait. This is logical because node.DirectionalMover.move_right() calls the method on the child MoveRightTrait. Other nodes without that statement would return null. So the statement if node.DirectionalMover:` 按照惯例将成为功能测试。
4) 状态模式应该可以通过将“using”-ed 节点替换为具有不同行为但与“using”语句中引用的相同接口 [duck typing] 和相同 NodePath 的另一个节点来实现。 使用场景树的工作方式,这几乎可以免费使用。 但是,系统必须跟踪通过父节点连接的信号并在被替换的子节点中恢复连接。

我已经使用 GDScript 有一段时间了,我必须同意,迫切需要某种 trait/mixin 和代理/委托功能。 设置所有这些样板只是为了在场景的根目录中连接属性或公开子项的方法是非常烦人的。

或者添加树的级别只是为了模拟组件(它很快就会变得相当麻烦,因为你会用每个新组件破坏所有节点路径)。 也许有更好的方法,比如元/多脚本允许一个节点上的多个脚本? 如果您有惯用的解决方案,请分享...

将 C++ (GDNative) 混合使用会使事情变得更糟,因为_ready_init在那里的行为不同(阅读:使用默认值进行初始化一半有效或根本无效)。

这是我必须在 GDScript 中解决的主要问题。 我经常需要跨节点共享功能,而不是围绕它构建我的整个继承结构——例如,我的玩家和店主有一个库存,我的玩家+物品+敌人有统计数据,我的玩家和敌人有装备的物品,等等。

目前,我将这些共享的“组件”实现为加载到需要它们的“实体”中的类或节点,但它很混乱(增加了对节点的大量搜索,几乎不可能进行鸭式打字等),并且替代方法有其自身的缺点,所以我还没有找到更好的方法。 特征/混合绝对会挽救我的生命。

它归结为能够在不使用继承的情况下跨对象共享代码,我认为这既是必要的,也是不可能在 Godot 中干净地完成的。

我理解 Rust 特征(https://doc.rust-lang.org/1.8.0/book/traits.html)的方式是,它们就像 Haskell 类型类,您需要为类型定义一些参数化函数您正在添加一个特征,然后您可以使用一些在任何实现特征的类型上定义的通用函数。 Rust 的特征与这里提出的不同吗?

这个可能会被大规模迁移,因为它在这里进行了广泛的讨论。

这个可能会被大规模迁移,因为它在这里进行了广泛的讨论。

_我发现提案的“移动”毫无意义,如果人们表示有兴趣,最好关闭它们并要求在 godot-proposals 中重新打开,并在需要时让其他提案实际实施。 无论如何..._

一年前我偶然发现了这个问题,但直到现在我才开始了解特征系统的潜在用途。

分享我目前的工作流程,希望能激发人们更好地理解这个问题(并可能提出一个更好的替代方案,而不是实现特征系统)。

1. 创建一个工具来为项目中使用的每个节点类型生成组件模板:

@willnationsdev https://github.com/godotengine/godot/issues/23101#issuecomment -431468744

现在,让这更容易的是拥有一个片段系统或宏系统,以便开发人员更容易创建那些重用的声明性代码部分。

走在你的脚步...😅

tool
extends EditorScript

const TYPES = [
    'Node',
    'Node2D',
]
const TYPES_PATH = 'types'
const TYPE_BASENAME_TEMPLATE = 'component_%s.gd'

const TEMPLATE = \
"""class_name Component{TYPE} extends {TYPE}

signal host_assigned(node)

export(bool) var enabled = true

export(NodePath) var host_path
var host

func _ready():
    ComponentCommon.init(self, host_path)"""

func _run():
    _update_scripts()


func _update_scripts():

    var base_dir = get_script().resource_path.get_base_dir()
    var dest = base_dir.plus_file(TYPES_PATH)

    for type in TYPES:
        var filename = TYPE_BASENAME_TEMPLATE % [type.to_lower()]
        var code = TEMPLATE.format({"TYPE" : type})
        var path = dest.plus_file(filename)

        print_debug("Writing component code for: " + path)

        var file = File.new()
        file.open(path, File.WRITE)
        file.store_line(code)
        file.close()

2. 创建一个静态方法,用于将组件初始化为宿主(例如根):

class_name ComponentCommon

static func init(p_component, p_host_path = NodePath()):

    assert(p_component is Node)

    # Try to assign
    if not p_host_path.is_empty():
        p_component.host = p_component.get_node(p_host_path)

    elif is_instance_valid(p_component.owner):
        p_component.host = p_component.owner

    elif is_instance_valid(p_component.get_parent()):
        p_component.host = p_component.get_parent()

    # Check
    if not is_instance_valid(p_component.host):
        push_warning(p_component.name.capitalize() + ": couldn't find a host, disabling.")
        p_component.enabled = false
    else:
        p_component.emit_signal('host_assigned')

这是使用第一个脚本生成的组件(特征)的样子:

class_name ComponentNode2D extends Node2D

signal host_assigned(node)

export(bool) var enabled = true

export(NodePath) var host_path
var host

func _ready():
    ComponentCommon.init(self, host_path)

(可选) 3. 扩展组件(trait)

@vnen https://github.com/godotengine/godot/issues/23101#issuecomment -471816901

这就是我的想法。 我相信它很简单,几乎涵盖了代码重用的所有用例。 我什至会允许使用 trait 的类覆盖 trait 方法(如果可以在编译时执行)。 特征还可以扩展其他特征。

走在你的脚步...😅

class_name ComponentMotion2D extends ComponentNode2D

const MAX_SPEED = 100.0

var linear_velocity = Vector2()
var collision

export(Script) var impl
...

实际上,在这些组件中使用导出的Script来驱动每个组件的特定主机/根节点类型的行为。 在这里, ComponentMotion2D将主要有两个脚本:

  • motion_kinematic_body_2d.gd
  • motion_rigid_body_2d.gd

所以孩子们仍然在这里驱动host / root行为。 host术语来自我使用状态机,这可能是 trait 不完美的地方,因为 imo 将状态更好地组织为节点。

通过使它们成为onready成员,组件本身被“硬连线”到根中,有效地减少了样板代码(实际上必须将它们引用为object.motion

extends KinematicBody2D

onready var motion = $motion # ComponentMotion2D

不确定这是否有助于解决问题,但 C# 有一个称为扩展方法的东西,它扩展了类类型的功能。

基本上该函数必须是静态的,并且第一个参数是必需的并且必须是self 。 它看起来像这样的定义:

扩展名.gd

# any script that uses this method must be an instance of `Node2D`
static func distance(self source: Node2D, target: Node2D):
    return source.global_position.distance_to(target.global_position)

# any script that uses this method must be an instance of `Rigidbody2D`
# a `Sprite` instance cannot use this method
static func distance(self source: Rigidbody2D, target: Node2D):
    return source.global_position.distance_to(target.global_position)

然后,当您想使用distance方法时,您只需执行以下操作:

播放器.gd

func _ready() -> void:
    print(self.distance($Enemy))
    print($BulletPoint.distance($Enemy))

我熟悉它,但这无助于解决问题。 呵呵,不过还是谢谢。

@TheColorRed扩展方法已经被提出,但我认为它们在动态语言中是不可行的。 我也不认为他们解决了最初开始讨论的根本问题。


另一方面,我可能会打开许多​​关于 GDScript 作为 GIP 的提案(如果@willnationsdev不介意,这包括在内)。

我仍然相信,在 OO 语言中横向共享代码最有意义(没有多重继承,但我不想那样做)。

我认为它们在动态语言中是不可行的

GDS 是动态的吗? 扩展方法可能仅限于类型化的实例,并且与其他语言中的工作方式完全相同 - 只是在编译期间将方法调用替换为静态方法(函数)调用的语法糖。 老实说,在原型 ala JS 或其他将方法附加到类甚至只是实例的动态方法之前,我更喜欢皮条客(扩展方法)。

无论我们决定做什么,我希望我们不要决定将其命名为“皮条客”。

GDS 是动态的吗?

在这种情况下,“动态”有很多定义。 需要明确的是:在编译时可能不知道变量类型,因此需要在运行时进行方法扩展检查(这会以某种方式损害性能)。

扩展方法可能仅限于类型化实例

如果我们开始这样做,我们不妨只输入 GDScript。 但这是另一个我不想在这里讨论的话题。

要点是:事情不应该因为用户向脚本添加类型而开始或停止工作。 当它发生时几乎总是令人困惑。

再说一次,我不认为它可以解决问题。 我们试图将相同的代码附加到多种类型,而扩展方法只会将它添加到一种类型。

老实说,在原型 ala JS 或其他将方法附加到类甚至只是实例的动态方法之前,我更喜欢皮条客(扩展方法)。

没有人提议(还)动态地(在运行时)附加方法,我也不想要。 特征将在编译时静态应用。

我最初对 Haxe 及其 mixin 宏库发表了评论,但后来我意识到大多数用户无论如何都不会使用第三方语言。

我最近遇到了这个需求。

我有一些用户可以与之交互但不能共享同一个父对象的对象,但它们需要类似的 API 组

例如,我有一些类不能从同一个父类继承,但使用一组类似的 API:
仓库:财务、删除、MouseInteraction + 其他
车辆:财务、删除、鼠标交互 + 其他
VehicleTerminal:财务、删除、MouseInteraction + 其他

对于我使用组合的财务,因为这需要最少的样板代码,因为 get_finances_component() 是一个足够的 API,因为它根本不关心游戏对象的。

其他:
鼠标交互和选择我只需要复制和粘贴,因为它需要了解游戏对象,除非我做了一些奇怪的委托,否则某些组合在这里不起作用:

Warehouse:
  func delete():
      get_delete_component().delete(self);

但这并不能真正让我覆盖删除的工作方式,如果它是一个继承的类,我可以在需要时重新编写一些删除代码。

鼠标交互和选择我只需要复制和粘贴,因为它需要了解游戏对象,除非我做了一些奇怪的委托,否则某些组合在这里不起作用

我目前通过onready节点访问组件。 我正在做类似的事情:

# character.gd

var input = $input # input component

func _set(property, value):
    if property == "focused": # override
        input.enabled = value
    return true

所以这:

character.input.enabled = true

变成这样:

character.focused = true

正如@Calinou亲切地指出我的问题https://github.com/godotengine/godot-proposals/issues/758与此密切相关。 您如何看待能够为组添加特征的提议? 这可能会极大地需要脚本和其他开销。

如果有一种方法可以将可共享的代码注入到类中,并且如果它们具有导出的值,那么这些值就会出现在检查器中,并且具有可用的方法和属性,并且可以通过代码完成进行检测。

Godot 引擎的功能和改进建议现在正在专用的Godot 改进建议 (GIP) ( godotengine/godot-proposals ) 问题跟踪器中进行讨论和审查。 GIP 跟踪器有一个详细的问题模板设计,以便提案包含所有相关信息,以启动富有成效的讨论并帮助社区评估引擎提案的有效性。

主要( godotengine/godot )跟踪器现在专门用于错误报告和拉取请求,使贡献者能够更好地专注于错误修复工作。 因此,我们现在正在关闭主要问题跟踪器上的所有旧功能提案。

如果您对此功能提案感兴趣,请按照给定的问题模板在 GIP 跟踪器上打开一个新提案(在检查它不存在之后)。 如果包含任何相关讨论(也鼓励您在新提案中总结),请务必参考此已关闭问题。 提前致谢!

注意:这是一个流行的提案,如果有人将其移至 Godot 提案,请尝试总结讨论。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

mefihl picture mefihl  ·  3评论

EdwardAngeles picture EdwardAngeles  ·  3评论

nunodonato picture nunodonato  ·  3评论

Spooner picture Spooner  ·  3评论

ivanskodje picture ivanskodje  ·  3评论