Godot: Agregue un sistema de rasgos GDScript.

Creado en 18 oct. 2018  ·  93Comentarios  ·  Fuente: godotengine/godot

(Editar:
Para minimizar más problemas de problemas XY:
El problema que se aborda aquí es que el sistema Node-Scene / los lenguajes de secuencias de comandos de Godot aún no admiten la creación de implementaciones agrupadas y reutilizables que son 1) específicas de las características del nodo raíz y 2) que se pueden intercambiar y/o combinar. Los scripts con métodos estáticos o subnodos con scripts podrían usarse para el último bit, y en muchos casos esto funciona. Sin embargo, Godot generalmente prefiere que mantengas la lógica del comportamiento general de tu escena almacenada en el nodo raíz mientras usa los datos calculados por los nodos secundarios o les delega subtareas significativamente desviadas, por ejemplo, un KinematicBody2D no administra la animación, por lo que delega eso a un AnimationPlayer.

Tener un nodo raíz más delgado que usa nodos secundarios "componentes" para impulsar su comportamiento es un sistema débil en comparación. Tener un nodo raíz en gran parte vacío que simplemente delega todo su comportamiento a los nodos secundarios va en contra de este paradigma. Los nodos secundarios se convierten en extensiones de comportamiento del nodo raíz en lugar de objetos autosuficientes que realizan una tarea por derecho propio. Es muy torpe, y el diseño podría simplificarse/mejorarse permitiéndonos consolidar toda la lógica en el nodo raíz, pero también permitiéndonos dividir la lógica en diferentes partes componibles.

Supongo que el tema de este número es más sobre cómo lidiar con el problema anterior que sobre GDScript específicamente, pero creo que los rasgos de GDScript serían el enfoque más simple y directo para resolver el problema.
)

Para los no informados, los rasgos son esencialmente una forma de combinar dos clases en una sola (prácticamente un mecanismo de copiar/pegar), solo que, en lugar de copiar/pegar literalmente el texto de los archivos, todo lo que está haciendo es usar una declaración de palabra clave para vincular los dos archivos. (Editar: el truco es que, si bien un script solo puede heredar una clase, puede incluir múltiples rasgos)

Me imagino algo donde cualquier archivo GDScript se puede usar como rasgo para otro archivo GDScript, siempre que el tipo rasgado amplíe una clase heredada por el script fusionado, es decir, un GDScript que extiende Sprite no puede usar un GDScript de recursos como un rasgo, pero puede usar un Node2D GDScript. Me imagino una sintaxis similar a esta:

# 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")

Puedo ver dos formas de hacer esto:

  1. Analice previamente el script a través de RegEx para "^trait \proceso de recarga). Tendríamos que no admitir el anidamiento de rasgos o volver a examinar continuamente el código fuente generado después de cada iteración para ver si se han realizado más inserciones de rasgos.
  2. Analice el script normalmente, pero enséñele al analizador a reconocer la palabra clave, cargue el script al que se hace referencia, analice ESE script y luego agregue el contenido de su ClassNode al ClassNode generado del script actual (tomando efectivamente los resultados analizados de un script y agregándolo a los resultados analizados del otro script). Esto admitiría automáticamente el anidamiento de tipos con rasgos.

Por otro lado, es posible que las personas deseen que el rasgo GDScript tenga un nombre, pero es posible que NO deseen que el class_name de GDScript aparezca en CreateDialog (porque no está destinado a ser creado por sí solo). En este caso, puede que NO sea una buena idea hacer que ningún script lo admita; solo aquellos que están especialmente marcados (¿quizás escribiendo 'rasgo' en la parte superior del archivo?). De todos modos, cosas para pensar.

¿Pensamientos?

Editar: después de reflexionar un poco, creo que la opción 2 sería mucho mejor ya que 1) sabríamos DE QUÉ script proviene un segmento de script (para un mejor informe de errores) y 2) podríamos identificar los errores a medida que ocurren desde el los scripts incluidos deben analizarse en secuencia, en lugar de analizar todo al final. Esto reduciría el tiempo de procesamiento que agrega al proceso de análisis.

archived discussion feature proposal gdscript

Comentario más útil

@aaronfranke Traits , básicamente lo mismo que Mixins , tiene un caso de uso completamente diferente de las interfaces precisamente porque incluyen implementaciones de los métodos. Si una interfaz diera una implementación predeterminada, entonces ya no sería realmente una interfaz.

Los Traits/Mixins están presentes en PHP, Ruby, D, Rust, Haxe, Scala y muchos otros lenguajes (como se detalla en los Wikis vinculados), por lo que ya deberían estar muy familiarizados con las personas que tienen un amplio repertorio de familiaridad con el lenguaje de programación.

Si tuviéramos que implementar interfaces (a lo que tampoco me opongo, especialmente con la tipificación estática opcional), sería efectivamente solo una forma de especificar firmas de funciones y luego requerir que los scripts GDScript relevantes implementen esas firmas de funciones, con rasgos incluidos (si existían en ese momento).

Todos 93 comentarios

¿Cuál es el beneficio en lugar de: extends "res://move_right_trait.gd"

@MrJustreborn Porque puede tener múltiples rasgos en una clase, pero solo puede heredar un script.

Si entiendo correctamente, esto es básicamente lo que C# llama " interfaces ", pero ¿con métodos no abstractos? Podría ser mejor llamar a las interfaces de características en lugar de rasgos para que los programadores estén familiarizados.

@aaronfranke Traits , básicamente lo mismo que Mixins , tiene un caso de uso completamente diferente de las interfaces precisamente porque incluyen implementaciones de los métodos. Si una interfaz diera una implementación predeterminada, entonces ya no sería realmente una interfaz.

Los Traits/Mixins están presentes en PHP, Ruby, D, Rust, Haxe, Scala y muchos otros lenguajes (como se detalla en los Wikis vinculados), por lo que ya deberían estar muy familiarizados con las personas que tienen un amplio repertorio de familiaridad con el lenguaje de programación.

Si tuviéramos que implementar interfaces (a lo que tampoco me opongo, especialmente con la tipificación estática opcional), sería efectivamente solo una forma de especificar firmas de funciones y luego requerir que los scripts GDScript relevantes implementen esas firmas de funciones, con rasgos incluidos (si existían en ese momento).

¿Quizás una palabra clave como includes ?

extends Node2D
includes TraitClass

Aunque otros nombres como trait, mixin, has, etc. seguramente también están bien.

También me gusta la idea de tener alguna opción para excluir el tipo class_name del menú Agregar. Puede estar muy abarrotado de tipos pequeños que no funcionan por sí solos como nodos.

Incluso puede ser solo un tema principal en sí mismo.

(Accidentalmente borré mi comentario, ¡ups! Además, obligatorio "¿por qué no permites múltiples scripts, la unidad lo hace?" )

¿Cómo funcionará esto en VisualScript, si es que funciona?

Además, ¿podría ser beneficioso incluir una interfaz de inspector para rasgos, si se implementaran rasgos? Me imagino que algunos casos de uso para rasgos pueden incluir casos de usos en los que solo hay rasgos y ningún guión (al menos, ningún guión además de uno que incluye los archivos de rasgos). Sin embargo, al pensarlo más, me pregunto si el esfuerzo realizado para crear una interfaz de este tipo podría valer la pena, en comparación con solo hacer un script que incluya los archivos de rasgos.

@LikeLakers2

¿Cómo funcionará esto en VisualScript, si es que funciona?

Si se hace de la manera que sugerí, no sucedería para VisualScript en absoluto. Solo GDScript. Cualquier sistema de rasgos implementado para VisualScript se diseñaría de manera completamente diferente porque VisualScript no es un lenguaje analizado. Sin embargo, no excluye la posibilidad en absoluto (solo necesitaría implementarse de manera diferente). Además, ¿quizás deberíamos considerar obtener soporte de herencia de VisualScript primero? jajaja

Además, ¿podría ser beneficioso incluir una interfaz de inspector para rasgos, si se implementaran rasgos?

No tendría mucho sentido. Los rasgos simplemente imparten detalles al GDScript, entregándole las propiedades, constantes, señales y métodos definidos por el rasgo.

Imagino que algunos casos de uso para rasgos pueden incluir casos de usos en los que solo hay rasgos y ningún script (al menos, ningún script además de uno que incluye los archivos de rasgos).

Los rasgos, tal como se representan en otros idiomas, nunca se pueden usar de forma aislada, sino que deben incluirse en otra secuencia de comandos para que se puedan usar.

Me pregunto si el esfuerzo realizado para crear una interfaz de este tipo podría valer la pena.

Crear una interfaz de Inspector de alguna manera no tendría mucho sentido solo para GDScript. Agregar o eliminar un rasgo implicaría editar directamente el código fuente de la propiedad source_code del recurso Script, es decir, no es una propiedad en el Script mismo. Por tanto, o bien...

  1. el editor tendría que aprender cómo manejar específicamente la edición adecuada del código fuente para que los archivos GDScript hagan esto (propenso a errores), o...
  2. todos los scripts tendrían que admitir características para que GDScriptLanguage pueda proporcionar su propio proceso interno para agregar y eliminar características (pero no todos los idiomas SÍ admiten características, por lo que la propiedad no sería significativa en todos los casos).

¿Cuál es la necesidad de tal característica? ¿Hay algo que esto permita que no puedas hacer ahora? ¿O hace que algunas tareas sean significativamente más rápidas de manejar?

Prefiero mantener GDscript como un lenguaje simple que agregar características complejas que casi nunca se usan.

Resuelve el problema Child-Nodes-As-Script-Dependencies con el que este tipo tuvo un problema , pero no viene con el mismo tipo de equipaje que tenía MultiScript porque está restringido a un solo idioma. El módulo GDScript puede aislar la lógica sobre cómo los rasgos se relacionan entre sí y con el script principal, mientras que resolver las diferencias entre diferentes idiomas sería mucho más complicado.

Con la ausencia de múltiples importaciones/herencias múltiples, las dependencias de nodos secundarios como script son la única forma de evitar repetir MUCHO el código, y esto definitivamente resolvería el problema de una buena manera.

@groud @Zireael07 Quiero decir, el enfoque más radical entre idiomas sería 1) rediseñar completamente Object para usar un ScriptStack para fusionar scripts apilados en una sola representación de script, 2) reintroducir MultiScript y crear soporte de editor que convierte automáticamente la adición de scripts en multiscripts (o simplemente hacer que todos los scripts sean multiscript por simplicidad, en cuyo caso la implementación de MultiScript sería esencialmente nuestro ScriptStack), o 3) implementar una especie de sistema de características entre idiomas para el tipo de objeto que puede fusionarse en Guiones que amplían las referencias como rasgos, incorporando su contenido como un guión típico. Sin embargo, todas esas opciones son mucho más invasivas para el motor. Esto mantiene todo más simple.

No creo que el rasgo sea necesario. lo que más necesitamos es un ciclo rápido de liberación del motor. Quiero decir que necesitamos hacer que el motor sea más flexible para agregar nuevas funciones de la manera más simple, solo agregue nuevos archivos dll o más y el motor se integre automáticamente con él mismo, como el estilo de complementos en la mayoría de los IDE. por ejemplo, necesito desesperadamente que websocket funcione, no necesito esperar hasta que se lance 3.1. 3.1 demasiado roto en este momento con tantos errores. será genial si tenemos esta función. la nueva clase puede inyectarse automáticamente en GDScript desde .dll aleatorio o .so en alguna ruta. No sé cuánto esfuerzo hay que hacer en C++, pero espero que no sea demasiado difícil 😁

@fian46 Bueno, si alguien hubiera implementado websockets como un complemento GDNativo descargable, entonces sí, lo que describiste sería el flujo de trabajo. En su lugar, optaron por convertirlo en una función integrada disponible en el motor Vanilla. No hay nada que impida que las personas creen funciones de esa manera, por lo que su punto realmente no tiene relación con el tema de este número.

Vaya, no sé si GDNative existe 😂😂😂. El rasgo es increíble, pero ¿es más fácil crear una clase de rasgo falsa e instanciarla y luego llamar a la función como un script básico?

Si un script de Godot es una clase sin nombre, ¿por qué no instanciar "move_right_trait.gd" en "my_sprite.gd"?
Disculpa mi ignorancia si no entiendo el tema.

Entiendo el uso de rasgos en lenguajes con tipado más fuerte como Rust o (interfaces en) C++, pero en un lenguaje de tipeado esquivo, ¿no es un poco innecesario? La simple implementación de las mismas funciones debería permitirle lograr una interfaz uniforme entre sus tipos. Supongo que no estoy seguro de cuál es el problema exacto con la forma en que GDScript maneja las interfaces o cómo un sistema de rasgos realmente ayudaría.

¿No podría usar también preload("Some-other-behavior.gd") y almacenar los resultados en una variable para lograr básicamente el mismo efecto?

@fian46 @DriNeo Bueno, sí y no. La carga de scripts y el uso de clases de script ya se encargan de eso, pero el problema se extiende más allá de eso.

@ElYokai

Implementar las mismas funciones debería permitirle lograr una interfaz uniforme entre sus tipos

El problema no es lograr una interfaz uniforme, que es lo que tiene razón, la escritura de pato resuelve muy bien, sino más bien organizar (combinar/intercambiar) grupos de implementaciones relacionadas de manera eficiente.


En 3.1, con las clases de secuencia de comandos, puede definir funciones estáticas en una secuencia de comandos de referencia (o cualquier tipo en realidad) y luego usar esa secuencia de comandos como un espacio de nombres para acceder globalmente a esas funciones en 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!")

Sin embargo, cuando se trata de contenido no estático, especialmente cosas que usan funciones específicas de nodo, es problemático dividir la lógica de uno.

Por ejemplo, ¿qué pasa si tengo un KinematicBody2D y quiero tener un comportamiento de "Saltar" y un comportamiento de "Ejecutar"? Cada uno de estos comportamientos necesitaría acceso al manejo de entrada y a las funciones move_and_slide de KinematicBody2D. Idealmente, podría intercambiar la implementación de cada comportamiento de forma independiente y mantener todo el código de cada comportamiento en secuencias de comandos separadas.

Actualmente, todos los flujos de trabajo con los que estoy familiarizado para esto simplemente no son óptimos.

  1. Si mantiene todas las implementaciones en el mismo script y simplemente cambia las funciones que se están utilizando...

    1. cambiar los "comportamientos" puede implicar intercambiar varias funciones como un conjunto, por lo que no puede agrupar los cambios de implementación de manera efectiva.

    2. Todas las funciones para cada comportamiento (X * Y) se encuentran en su script único, por lo que puede inflarse muy rápidamente.

  2. Puede simplemente reemplazar todo el script, pero eso significa que debe crear un nuevo script para cada combinación de comportamientos más cualquier lógica que use esos comportamientos.
  3. Si usa nodos secundarios como dependencias de secuencias de comandos, eso significa que tendría estos extraños nodos Node2D de "componentes" que toman a su padre y llaman al método move_and_slide PARA ello, lo cual es poco natural, relativamente hablando.

    • Debe suponer que su padre implementará ese método o hará la lógica para verificar que tiene el método. Y si realiza una verificación, entonces puede fallar en silencio y potencialmente tener un error silencioso en su juego o puede convertirlo en un script de herramienta innecesariamente solo para que pueda establecer una advertencia de configuración en el nodo para informarle visualmente en el editor. que hay un problema.

    • Tampoco obtiene la finalización adecuada del código para las operaciones previstas de los nodos, ya que se derivan de Node2D y el objetivo es impulsar el comportamiento de un padre KinematicBody2D.

Ahora, admitiré que la opción 3 es el flujo de trabajo más efectivo actualmente, y que sus problemas pueden resolverse en gran medida usando la tipificación estática práctica para GDScript en 3.1. Sin embargo, hay una cuestión más fundamental en juego.

El sistema de escenas de nodos de Godot generalmente tiene la forma de usuarios que crean nodos o escenas que realizan un trabajo en particular, en su propio sistema cerrado. Puede instanciar esos nodos/escenas dentro de otra escena y hacer que calculen datos que luego usa la escena principal (tal es el caso de la relación Area2D y CollisionShape2D).

Sin embargo, el uso del motor Vanilla y la recomendación general de mejores prácticas es mantener el comportamiento de su escena bloqueado en el nodo raíz y/o su secuencia de comandos. Casi nunca tiene nodos de "componentes de comportamiento" que realmente le digan a la raíz qué hacer (y cuando está allí, se siente muy torpe). Los nodos AnimationPlayer/Tween son las únicas excepciones discutibles en las que puedo pensar, pero incluso sus operaciones están dirigidas por la raíz (efectivamente, les está delegando el control temporalmente). (Editar: incluso en este caso, la animación y la interpolación no son el trabajo de KinematicBody2D, por lo que tiene sentido que esas tareas se deleguen. Sin embargo, el movimiento, como correr y saltar, es su responsabilidad) Es más simple y más natural permitir una implementación de rasgos para organizar el código, ya que mantiene las relaciones entre los nodos estrictamente de datos arriba/comportamiento abajo y mantiene el código más aislado en sus propios archivos de script.

Eh, marcarte a ti mismo como 'implementando una interfaz/rasgo' también debería cumplir con una prueba * is * , lo cual es conveniente para probar la funcionalidad de algo.

@OvermindDL1 Quiero decir, di un ejemplo de hacer una prueba como esa, pero usé in en su lugar porque quería distinguir entre herencia y uso de rasgos.

Supongo que entré en un problema XY aquí, mi error. Acababa de salir de otros 2 números (n.º 23052, n.º 15996) que abordaban este tema de una forma u otra y pensé en presentar una propuesta, pero en realidad no brindé todo el contexto.

@groud esta solución resolverá uno de los problemas planteados contra #19486.

@willnationsdev gran idea, ¡estoy deseando que llegue!

Desde mi comprensión limitada, lo que este sistema de rasgos quiere lograr es habilitar algo similar al flujo de trabajo que se muestra en este video: https://www.youtube.com/watch?v=raQ3iHhE_Kk
(Tenga en cuenta que me refiero al _workflow_ que se muestra, no a la función utilizada)

En el vídeo se compara con otros tipos de flujos de trabajo, con sus ventajas y desventajas.

Al menos que yo sepa, este tipo de flujo de trabajo es imposible en GDScript en este momento, debido a cómo funciona la herencia.

@AfterRebelion Los primeros minutos de ese video donde aísla la modularidad, la capacidad de edición y la capacidad de depuración del código base (y los detalles relacionados de esos atributos) es de hecho la búsqueda de tener esta función.

Al menos que yo sepa, este tipo de flujo de trabajo es imposible en GDScript en este momento, debido a cómo funciona la herencia.

Esto no es del todo cierto, porque Godot en realidad lo hace muy bien en lo que respecta a las jerarquías de nodos y el diseño de escenas. Las escenas son modulares por naturaleza, las propiedades se pueden exportar (e incluso animar) directamente desde el editor sin tener que lidiar con el código, y todo se puede probar/depurar de forma aislada porque las escenas se pueden ejecutar de forma independiente.

La dificultad es cuando la lógica que normalmente se subcontrataría a un nodo secundario debe ser ejecutada por el nodo raíz porque la lógica se basa en las características heredadas del nodo raíz. En estos casos, la única forma de usar la composición es hacer que los niños comiencen a decirle al padre qué hacer en lugar de que se ocupen de sus propios asuntos mientras el padre los usa.

Esto no es un problema en Unity porque GameObject no tiene ninguna herencia real que los usuarios puedan aprovechar. En Unreal, podría ser un poco (?) De un problema ya que tienen jerarquías internas basadas en nodos/componentes similares para Actores.

De acuerdo, juguemos un poco al abogado del diablo ( @MysteryGM , puede que te diviertas con esto). Pasé un tiempo pensando en cómo podría escribir un sistema de este tipo en Unreal y eso me está dando una nueva perspectiva al respecto. Lo siento por las personas que pensaban que esto sería una buena idea / estaban entusiasmados con esto:

La introducción de un sistema de características agrega una capa de complejidad a GDScript como lenguaje que puede dificultar su mantenimiento.

Además de eso, los rasgos como característica hacen que sea más difícil aislar de dónde pueden provenir realmente las variables, constantes, señales, métodos e incluso subclases . Si el script de su Nodo de repente tiene 12 rasgos diferentes, entonces no necesariamente sabrá de dónde viene todo. Si ve una referencia a algo, entonces tendría que buscar en 12 archivos diferentes para saber dónde se encuentra un elemento en la base de código.

En realidad, esto reduce la capacidad de depuración de GDScript como lenguaje, ya que cualquier problema dado puede requerir que elija un promedio de 2 o 3 ubicaciones diferentes en la base del código. Si alguna de esas ubicaciones es difícil de encontrar porque dicen que están ubicadas en un script pero en realidad están ubicadas en otro lugar, y si la legibilidad del código no establece claramente qué cosa es responsable de los datos/lógica, entonces esos 2 o 3 los pasos se multiplican en un conjunto de pasos arbitrariamente grande y altamente estresante.

El tamaño y el alcance cada vez mayores de un proyecto magnifican aún más estos efectos negativos y hacen que el uso de rasgos sea una cualidad bastante insostenible.


Pero, ¿qué se puede hacer para resolver el problema? No queremos nodos secundarios de "lógica de componentes" que le digan a las raíces de la escena qué hacer, pero tampoco podemos confiar en la herencia o cambiar scripts completos para resolver nuestro problema.

Bueno, ¿qué haría cualquier motor que no sea ECS en esta situación? La composición sigue siendo la respuesta, pero en este caso un Nodo completo no es razonable cuando se toma a escala/complica la dinámica de la jerarquía de propiedad. En su lugar, uno puede simplemente definir objetos de implementación que no son de nodo que abstraen la implementación concreta de un comportamiento, pero que siguen siendo propiedad del nodo raíz. Esto se puede hacer con un script 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(...)

Si las exportaciones funcionaran de tal manera que pudiéramos editarlas en el Inspector como enumeraciones para las clases que derivan un tipo particular como podemos hacer para las nuevas instancias de recursos, sería genial. La única forma de hacerlo ahora sería corregir las exportaciones de secuencias de comandos de recursos y luego hacer que su secuencia de comandos de implementación amplíe los recursos (por ningún otro motivo). Sin embargo, hacer que amplíen Resource sería una buena idea si la implementación en sí requiere parámetros que le gustaría poder definir desde el Inspector. :-)

Ahora, lo que haría ESTO más fácil sería tener un sistema de fragmentos o un sistema de macros para que la creación de esas secciones de código declarativo reutilizadas sea más fácil para los desarrolladores.

De todos modos, sí, creo que identifiqué problemas evidentes con un sistema de rasgos y un mejor enfoque para resolver el problema. ¡Hurra por los problemas XY! /s

Editar:

Por lo tanto, el flujo de trabajo del ejemplo anterior implicaría configurar el script de implementación y luego usar la instancia del script en tiempo de ejecución para definir el comportamiento. Pero, ¿qué sucede si la implementación en sí requiere parámetros que le gustaría establecer de forma estática desde el Inspector? Aquí hay una versión basada en un recurso en su lugar.

# 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(...)

Relacionado: #22660

@AfterRebelion

Tenga en cuenta que me refiero al flujo de trabajo que se muestra, no a la función utilizada

Es irónico que, después de que aclaras esto y estoy de acuerdo con el flujo de trabajo óptimo, y luego no estoy de acuerdo con partes posteriores del comentario, lo sigo diciendo básicamente cómo la "función utilizada" en ese video es en realidad la forma ideal de abordar este problema en Godot de todos modos . Ja ja.

# 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
...

Wow, no sabía que la exportación fuera tan poderosa. Se esperaba que solo pudiera interactuar con primitivas y otras estructuras de datos.

Esto hace que mi comentario anterior sea inválido.
Como dijiste, si se implementa algún tipo de macro para facilitar su implementación, sería la mejor manera de implementar ese flujo de trabajo sin la necesidad de MultiScript. Tal vez no sea tan versátil como Unity, porque aún necesitaría declarar todos los scripts posibles de antemano, pero sigue siendo una buena opción.

@AfterRebelion

Como dijiste, si se implementa algún tipo de macro para facilitar su implementación, sería la mejor manera de implementar ese flujo de trabajo sin la necesidad de MultiScript.

Bueno, el enfoque basado en Resource que mencioné en el mismo comentario, combinado con un mejor soporte del editor de #22660, haría que la calidad fuera comparable a lo que Unity puede hacer.

Tal vez no sea tan versátil como Unity, porque aún necesitaría declarar todos los scripts posibles de antemano, pero sigue siendo una buena opción.

Bueno, si corrigen las sugerencias de tipo de matriz en 3.2, entonces puede definir una matriz exportada para rutas de archivos que deben extender un script y agregar efectivamente la suya. Esto incluso podría hacerse a través de un complemento en 3.1 usando la clase EditorInspectorPlugin para agregar contenido personalizado al Inspector para ciertos recursos o nodos.

Quiero decir, si quisiera un sistema similar a "Unity", entonces realmente TENDRÁ subnodos que le digan a la raíz qué hacer y solo necesitaría buscarlos haciendo referencia a su nombre, sin declararlos manualmente o agregarlos del script del nodo raíz. El método Resource es generalmente mucho más efectivo y mantiene una base de código más limpia.

Debido a que el sistema de rasgos ejercería una presión excesiva sobre la usabilidad del código GDScript, según las razones que describí, seguiré adelante y cerraré este problema. Las trampas de su adición superan con creces cualquier beneficio comparativamente escaso que podamos recibir de dicho sistema, y ​​esos mismos beneficios se pueden implementar de diferentes maneras con más claridad y facilidad de uso.

Bueno, me perdí esta discusión mientras estaba fuera. Todavía no lo he leído todo, pero tuve la idea de agregar rasgos a GDScript para resolver el problema de reutilización de código, que me parece mucho más elegante y claro que la herencia múltiple falsa de adjuntar un script a un tipo derivado. Aunque lo que haría sería crear archivos de rasgos específicos, sin permitir que ningún archivo de clase sea un rasgo. No creo que sea tan malo.

Pero estoy abierto a sugerencias para resolver el problema principal, que es reutilizar código en varios tipos de nodos.

@vnen , la solución que se me ocurrió, que es el último elemento, es externalizar las secciones reutilizables a los scripts de recursos.

  • Todavía se pueden exponer y editar sus propiedades en el Inspector, como si fueran variables miembro en un nodo.

  • Conservan un rastro claro de dónde provienen los datos y la lógica, algo que podría verse comprometido fácilmente si se incluyen muchos rasgos en un script.

  • No agregan ninguna complejidad indebida a GDScript como lenguaje. Por ejemplo, si existieran, tendríamos que resolver cómo se fusionarían las cualidades compartidas (propiedades, constantes, señales) en el script principal (o, si no se fusionan, obligar a los usuarios a lidiar con los conflictos de dependencia).

  • Los scripts de recursos son mejores ya que se pueden asignar y cambiar desde el Inspector. Los diseñadores, escritores, etc. podrán cambiar los objetos de implementación directamente desde el editor.

@willnationsdev Ya veo (aunque el nombre "scripts de recursos" suena extraño, ya que todos los scripts son recursos). El principal problema con esta solución es que no resuelve algo que la gente espera con el enfoque de herencia: agregar variables y señales exportadas al nodo raíz de la escena (en particular, cuando se instancia la escena en otro lugar). Todavía puede editar las variables exportadas desde los subrecursos, pero se vuelve menos práctico (no puede saber de un vistazo qué propiedades puede editar).

El otro problema es que requiere que el código repetitivo se repita mucho. También debe asegurarse de que los scripts de recursos estén realmente configurados antes de llamar a las funciones.

La ventaja es que no necesitamos nada, ya está disponible. Supongo que esto debería documentarse y las personas que usan el método actual "adjuntar secuencia de comandos al nodo derivado" podrían comentar sobre la solución para ver qué tan viable es.

@vnen

pero se vuelve menos práctico (no se puede saber de un vistazo qué propiedades se pueden editar).

¿Puedes dar más detalles sobre lo que quieres decir aquí? No veo cómo podría haber una falta sustancial de claridad en cuanto a qué propiedades son accesibles, especialmente si se fusionó algo como #22660.

  1. Instalo la escena, quiero saber cómo puedo editarla y, por lo tanto, miro el script del nodo raíz de esa escena.
  2. Dentro del guión, veo...

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

    • use FourWayMoveTrait

  3. Suponiendo que tenemos un medio para hacer clic en el seguimiento del identificador (¡que debería ser realmente una función 3.1.1!) para abrir el script, luego terminamos abriendo el script asociado y podemos ver sus propiedades.

Me parece el mismo número de pasos, a menos que me esté perdiendo algo.

Además, diría que las propiedades que se pueden editar son aún más claras con los recursos, ya que, si una implementación tiene propiedades específicas, entonces el hecho de que los datos estén relacionados con la implementación es más claro si tiene que acceder a ellos con el prefacio instancia de implementación, es decir, move_impl.<property> .

El otro problema es que requiere que el código repetitivo se repita mucho. También debe asegurarse de que los scripts de recursos estén realmente configurados antes de llamar a las funciones.

Esto es cierto, sin embargo, sigo pensando que los beneficios de ser una exportación con una inicialización superan el costo del verbo agregado:

("Compañero de equipo de alto nivel", HLT, como diseñadores, escritores, artistas, etc.)

  • Uno puede asignar los valores directamente desde el Inspector, en lugar de tener que abrir el script, ubicar la línea correcta para cambiar y luego cambiarla (ya se mencionó, pero lleva a...).

  • Se puede especificar que el contenido exportado tiene un requisito de tipo base. El Inspector puede entonces proporcionar una lista enumerada de implementaciones permitidas automáticamente. Luego, los HLT pueden asignar con seguridad solo derivaciones de ese tipo. Esto ayuda a aislarlos de la alternativa de tener que conocer las ramificaciones de todos los guiones de rasgos diferentes que flotan por ahí. También tendríamos que modificar el autocompletado en GDScript para admitir la búsqueda de archivos de rasgos con nombre y sin nombre en respuesta a ver la palabra clave use .

  • Se puede serializar una configuración de una implementación como un archivo *.tres. Luego, los HLT pueden arrastrarlos y soltarlos desde el panel del sistema de archivos o incluso crear los suyos propios en el Inspector. Si uno quisiera hacer lo mismo con los rasgos, tendría que crear un rasgo derivado que proporcione un constructor personalizado para anular el predeterminado. Luego usarían ese rasgo como una "preconfiguración" a través de un constructor imperativamente codificado.

    1. Más débil porque es imperativo en lugar de declarativo.
    2. Más débil porque el constructor debe definirse explícitamente en el script.
    3. Si el rasgo no tiene nombre, entonces el usuario necesitará saber dónde se encuentra el rasgo para usarlo correctamente en lugar del rasgo base predeterminado. Si se nombra el rasgo, obstruye el espacio de nombres global innecesariamente.
    4. Si cambian la secuencia de comandos para decir use FourWayMoveTrait en lugar de use MoveTrait , ya no hay ninguna indicación duradera de que la secuencia de comandos sea compatible con la base MoveTrait . Permite la confusión HLT en cuanto a si el FourWayMoveTrait puede incluso cambiar a un MoveTrait diferente sin romper las cosas.
    5. Si un HLT estuviera creando una nueva implementación de rasgo de esta manera, no necesariamente conocería todas las propiedades que se pueden/deben establecer a partir del rasgo base. Este no es un problema con los recursos creados en el Inspector.
  • Uno puede incluso tener múltiples Recursos del mismo tipo (si hubiera una razón para ello). Un rasgo no admitiría esto, sino que desencadenaría conflictos de análisis.

  • Se pueden cambiar las configuraciones y/o sus valores individuales sin salir de la ventana gráfica 2D/3D. Esto es mucho más cómodo para los HLT (sé que muchos se molestan cuando tienen que mirar el código).

Dicho todo esto, estoy de acuerdo en que el repetitivo es molesto. Sin embargo, para lidiar con eso, prefiero agregar un sistema de macros o fragmentos para simplificarlo. Un sistema de fragmentos sería una buena idea, ya que podría admitir cualquier idioma que se pueda editar en ScriptEditor, en lugar de solo GDScript solo.

Acerca de:

pero se vuelve menos práctico (no se puede saber de un vistazo qué propiedades se pueden editar).

Estaba hablando del inspector, así que me refiero al "HLT" aquí. Personas que no mirarán el código. Con rasgos podríamos agregar nuevas propiedades exportadas al script. Con las secuencias de comandos de recursos, solo puede exportar variables al recurso en sí, por lo que no se mostrarán en el inspector a menos que edite el subrecurso.

Entiendo tu argumento pero va más allá del problema original: evita repetir código (sin herencia). Los rasgos son para programadores. En muchos casos, solo hay una implementación que desea reutilizar y no desea exponerla en el inspector. Por supuesto, aún puede usar los scripts de recursos sin exportar simplemente asignándolos directamente en el código, pero aún no resuelve el problema de reutilizar las variables y señales exportadas de la implementación común, que es una de las razones principales por las que las personas intentan usar la herencia.

Es decir, actualmente las personas intentan usar la herencia de un script genérico no solo para funciones, sino también para propiedades exportadas (que generalmente están relacionadas con esas funciones) y señales (que luego se pueden conectar con la interfaz de usuario).

Este es el problema difícil de resolver. Hay algunas formas de hacerlo solo con GDScript, pero nuevamente requieren que se copie el código repetitivo.

Solo puedo imaginar el ida y vuelta que tendría que tener lugar para que los scripts externos se comportaran como si estuvieran escritos directamente en el script principal.

Sería bueno tener si el cielo y la tierra no tienen que moverse para lograrlo. X)

@vnen Veo lo que estás diciendo ahora. Bueno, dado que parece que todavía puede haber algo de vida en este número, lo reabriré la próxima vez que tenga la oportunidad.

¿Alguna noticia sobre la reapertura de esto? Ahora que se anunció GodotCon 2019, y Godot Sprint es una cosa, tal vez valga la pena hablar de esto allí.

@AfterRebelion Me acababa de olvidar volver y volver a abrirlo. Gracias por recordarme. XDD

@willnationsdev ¡Me gustó lo que leí sobre EditorInspectorPlugin! pregunta rápida, esto significa que puedo crear un inspector personalizado para un tipo de datos... por ejemplo... agregar un botón al inspector.
(ha pasado bastante tiempo desde que quise hacer esto, para tener una forma de activar eventos con fines de depuración con un botón en el inspector, podría hacer que el botón presione ejecutar algún script)

@xDGameStudios Sí, eso y mucho más es posible. Cualquier Control personalizado se puede agregar al Inspector, ya sea en la parte superior, en la parte inferior, encima de una propiedad o debajo de una categoría.

@willnationsdev No sé si puedo contactarte por mensaje privado!! Pero me gustaría saber más sobre EditorInspectorPlugin (como un código de muestra). algo en las líneas de un tipo de recurso personalizado (por ejemplo) MyResource que tiene una propiedad exportar "nombre" y un botón de inspección que imprime el "nombre" variable si lo presiono (en el editor o durante la depuración)! Falta mucha documentación en este asunto... ¡Yo mismo escribiría la documentación si supiera cómo usar esto! :D gracias

También me gustaría saber más al respecto. X)

Entonces, ¿es esto lo mismo que un script de carga automática con subclases que contienen funciones estáticas?

por ejemplo, su caso se convertiría en Traits.MoveRightTrait.move_right()

o incluso más simple, tener diferentes scripts de carga automática para diferentes clases de rasgos:

Movement.move_right()

No, los rasgos son una característica específica del idioma equivalente a copiar y pegar el código fuente de un script en otro script (algo así como una combinación de archivos). Entonces, si tengo un rasgo con move_right() , y luego declaro que mi segundo script usa ese rasgo, entonces también puede usar move_right() , incluso si no es estático e incluso si accede propiedades en otras partes de la clase. Solo daría como resultado un error de análisis si la propiedad no existiera en el segundo script.

Descubrí que tengo código duplicado (funciones más pequeñas, por ejemplo, hacer un arco) o nodos superfluos porque no puedo tener herencia múltiple.

Esto sería increíble, me encuentro teniendo que hacer un script con exactamente la misma funcionalidad solo para usar en diferentes tipos de nodos que causan diferentes extend s, así que básicamente solo para una línea de código. Por cierto, si alguien sabe cómo hacer eso con el sistema actual, hágamelo saber.

Si tengo funcionalidad para una secuencia de comandos con extends Node , ¿hay alguna forma de adjuntar el mismo comportamiento a otro tipo de nodo sin tener que duplicar el archivo fuente y reemplazarlo con extend apropiado?

¿Algún progreso en eso? Sigo teniendo que duplicar código o agregar nodos, como dije antes. Sé que no se hará en 3.1, pero ¿tal vez apuntar a 3.2?

Oh, no he estado trabajando en esto en absoluto. De hecho, estoy más avanzado en la implementación de mi método de extensión que en esto. Sin embargo, necesito hablar con vnen sobre ambos, ya que me gustaría trabajar con él en los mejores detalles de sintaxis/implementación.

Editar: si alguien más quiere intentar implementar rasgos, puede participar. Solo necesito coordinar con los desarrolladores.

"implementación del método de extensión"?

@Zireael07 #15586
Permite a las personas escribir scripts que pueden agregar nuevas funciones "incorporadas" para las clases de motores. Mi interpretación de la sintaxis sería algo como esto:

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

Luego, en otro lugar, podría simplemente hacer:

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

En secreto, haría una llamada a la secuencia de comandos de extensión constantemente cargada y llamaría a la función estática del mismo nombre que está vinculada a la clase Array, pasando la instancia como el primer parámetro.

Tuve algunas conversaciones con @vnen y @jahd2602 sobre esto. Una cosa que me viene a la mente es la solución de Jai al polimorfismo: importar el espacio de nombres de una propiedad.

De esta forma, podrías hacer algo como lo siguiente:

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()"

El punto es que la palabra clave using importa el espacio de nombres de una propiedad, de modo que b.foo() es realmente solo azúcar sintáctico para b.a.foo() .

Y luego, asegúrese de que b is A == true y que B se puedan usar en situaciones tipeadas que también acepten A.

Esto también tiene la ventaja de que las cosas no tienen que declararse como rasgos, esto funcionaría para cualquier cosa que no tenga nombres de calidad comunes.

Un problema es que esto no encaja bien con el sistema de herencia actual. Si tanto A como B son Node2D y hacemos una función en A: func baz(): print(self.position) , ¿qué posición se imprimirá cuando llamemos a b.baz() ?
Una solución podría ser que la persona que llama determine self . Llamar a b.foo() llamaría a foo() con b como propio y bafoo() llamaría a foo() con a como propio.

Si tuviéramos métodos independientes como Python (donde x.f(y) es azúcar para f(x,y) ), esto podría ser realmente fácil de implementar.

Otra idea no relacionada:

Concéntrese solo en funciones independientes, estilo JavaScript.

Si adoptamos la convención x.f(y) == f(x,y) para funciones estáticas, fácilmente podríamos tener lo siguiente:

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))

Esto tendría un impacto mínimo en el sistema de clases, ya que en realidad solo afecta a los métodos. Sin embargo, si realmente quisiéramos que esto fuera flexible, podríamos usar JavaScript completo y simplemente permitir que las definiciones de funciones sean valores asignables y llamables.

@jabcross Suena bien, me gusta el concepto de algún tipo de espacio de nombres opcional, y la idea de la función es interesante.

Con respecto al espacio de nombres, me pregunto por qué no simplemente using A , las otras cosas declarativas parecen extrañas.

Curioso también cómo tendría que resolverse con la herencia múltiple. Supongo que la opción A obliga a ambos scripts a heredar el mismo tipo, por lo que ambos se extienden sobre la misma clase, sin ninguna combinación especial.

Opción B, tal vez palabras clave adicionales de GDScript para especificar una clase de rasgo y de qué clase le gustaría tener sugerencias. Sería la misma idea, pero solo pasos adicionales para parecer más explícito.

En la parte superior de A.gd:

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

Ohhh, me gusta mucho el concepto de importación de espacios de nombres. Resuelve no solo el problema de los rasgos, sino también potencialmente el concepto de "métodos de extensión" para agregar contenido a los tipos de motores.

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 Si luego también agregamos lambas y/o permitimos que los objetos implementen un operador de llamada (y tuviéramos un tipo callable para valores compatibles), entonces podríamos comenzar a agregar un enfoque más funcional al código GDScript (que Creo que sería una gran idea). De acuerdo, caminando más hacia el territorio # 18698 de @vnen en ese momento, pero...

Tenemos que considerar que GDScript sigue siendo un lenguaje dinámico y algunas de esas sugerencias requieren una verificación de tiempo de ejecución para despachar correctamente las llamadas, agregando una penalización de rendimiento (incluso para las personas que no usan las funciones, ya que necesita verificar todas las llamadas a funciones y tal vez la búsqueda de propiedades también). Por eso también no estoy seguro de si agregar extensiones es una buena idea (especialmente porque son esencialmente azúcar de sintaxis).

Prefiero el sistema de rasgos puros, donde los rasgos no son clases sino una cosa propia. De esta manera, pueden resolverse por completo en tiempo de compilación y proporcionar mensajes de error en casos de conflictos de nombres. Creo que esto resolvería el problema sin sobrecarga de tiempo de ejecución adicional.

@vnen Ahhh, no me di cuenta de eso sobre el costo del tiempo de ejecución. Y si eso se aplica a cualquier implementación de método de extensión, supongo que tampoco sería ideal.

Entonces, si hiciéramos un sistema de rasgos puro, ¿estaba pensando simplemente en hacer trait TraitName en lugar de extends junto con using TraitName debajo de extensiones en otros scripts? ¿Y lo implementaría usted mismo o lo delegaría?

Entonces, si hiciéramos un sistema de rasgos puro, ¿estaba pensando simplemente en hacer trait TraitName en lugar de extends junto con using TraitName debajo de extensiones en otros scripts?

esa es mi idea Creo que es lo suficientemente simple y cubre casi todos los casos de uso para la reutilización de código. Incluso permitiría que la clase que usa el rasgo anule los métodos de rasgo (si es posible hacerlo en tiempo de compilación). Los rasgos también podrían extender otros rasgos.

¿Y lo implementaría usted mismo o lo delegaría?

No me importaría dejar la tarea a alguien más que esté a la altura. Estoy corto de tiempo de todos modos. Pero debemos acordar el diseño de antemano. Soy bastante flexible con los detalles, pero debería 1) no incurrir en verificaciones de tiempo de ejecución (creo que GDScript se presta bien a cosas que son imposibles de resolver en la compilación), 2) ser relativamente simple y 3) no agregar demasiado a tiempo de compilación.

@vnen Me gustan estas ideas. Me preguntaba cómo imagina que un rasgo podría hacer cosas como la finalización automática para las clases que lo incluirían, ¿o no sería posible?

Me preguntaba cómo imagina que un rasgo podría hacer cosas como la finalización automática para las clases que lo incluirían, ¿o no sería posible?

Un rasgo sería esencialmente una "importación" en mi opinión. Debería ser trivial mostrar la finalización, suponiendo que la finalización para los miembros funcione.

@vnen Me imagino que esencialmente analizaría en un ClassNode con un indicador trait establecido en él. Y luego, si hace una instrucción using , intentará fusionar todas las propiedades/métodos/señales/constantes/subclases en el script actual.

  1. Si un método colisiona, la implementación del script actual anularía el método base, como si estuviera anulando un método heredado.

    • Pero, ¿qué hacer si una clase base ya tiene el método "combinado"?

  2. Si una propiedad, una señal y una constante se superponen, verifique si es el mismo tipo/firma de señal. Si no hay discrepancia, considérelo simplemente como una "fusión" de la propiedad/señal/constante. De lo contrario, notifique al usuario sobre un conflicto de tipo/firma (un error de análisis).

¿Mala idea? ¿O deberíamos simplemente prohibir los conflictos sin métodos? ¿Qué pasa con las subclases? Tengo la sensación de que deberíamos hacer que esos sean conflictos.

@willnationsdev Suena como el "problema del diamante" (también conocido como "diamante mortal de la muerte"), una ambigüedad bien documentada con diferentes soluciones ya aplicadas en varios lenguajes de programación populares.

Lo cual me recuerda:
@vnen , ¿los rasgos podrán extender otros rasgos?

@ jahd2602 Ya sugirió eso como una posibilidad.

Los rasgos también podrían extender otros rasgos.

@ jahd2602 Según las soluciones de Perl/Python, parece que básicamente forman una "pila" de capas que contienen el contenido de cada clase, de modo que los conflictos del último rasgo utilizado descansan sobre las otras versiones y las sobrescriben. Eso suena como una muy buena solución para este escenario. A menos que tú o @vnen tengan pensamientos alternativos. Gracias por la descripción general vinculada de las soluciones jahd.

Unas cuantas preguntas.

Primero: ¿De qué manera debemos apoyar la declaración de uso?

Estoy pensando que la instrucción using debería requerir algún GDScript de valor constante.

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

Estoy pensando en todo lo anterior.

Segundo: ¿Cuál deberíamos decir que debería ser la sintaxis permitida para definir un rasgo y/o su nombre?

Es posible que alguien no quiera necesariamente usar una clase de secuencia de comandos para su rasgo, por lo que no creo que debamos imponer un requisito trait TraitName . Estoy pensando que trait debe estar en una línea.

Por lo tanto, si se trata de un script de herramientas, por supuesto, debería tener una herramienta en la parte superior. Luego, si es un rasgo, debe definir si es un rasgo en la siguiente línea. Opcionalmente, permita que alguien indique el nombre de la clase de script después de la declaración del rasgo en la misma línea y, si lo hace, no permita que usen también class_name . Si omiten el nombre del rasgo, entonces class_name <name> está bien. Luego, al extender otro tipo, podríamos insertar el extends después de la declaración del rasgo y/o en una línea propia después de la declaración del rasgo. Entonces, consideraría cada uno de estos válidos:

# 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

Tercero: ¿deberíamos, para fines de autocompletado y/o declaración de intención/requisitos, permitir que un rasgo defina un tipo base que debe extenderse?

Ya hemos discutido que los rasgos deberían apoyar la herencia de otros rasgos. Pero, ¿deberíamos permitir que TraitA extend Node , permitir que el script de TraitA obtenga el autocompletado de Node, pero luego también desencadenar un error de análisis si hacemos una declaración using TraitA cuando la clase actual no extiende Node? o cualquiera de sus tipos derivados?

Cuarto: en lugar de que los rasgos extiendan otros rasgos, ¿no podemos simplemente mantener la declaración extends reservada para extensiones de clase, permitir que un rasgo no necesite esta declaración en absoluto, pero en lugar de extender un rasgo base, permitir rasgos para simplemente tener sus propias declaraciones using que subimportan esos rasgos?

# 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".

Por supuesto, el beneficio aquí sería que podría agrupar múltiples rasgos bajo un solo nombre de rasgo usando múltiples declaraciones using , similar a los archivos de inclusión de C++ que incluyen varias otras clases.

Quinto: si tenemos un rasgo, y tiene un using o un extends para un método, y luego implementa el suyo propio, ¿qué hacemos cuando llama, dentro de esa función .<method_name> ?

cc @vnen

Primero: ¿De qué manera debemos apoyar la declaración de uso?

Estoy pensando que la instrucción using debería requerir algún GDScript de valor constante.

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

Estoy bien con todos esos. Pero para la ruta, uso una cadena directamente: using "res://my_trait.gd"

Segundo: ¿Cuál deberíamos decir que debería ser la sintaxis permitida para definir un rasgo y/o su nombre?

Es posible que alguien no quiera necesariamente usar una clase de secuencia de comandos para su rasgo, por lo que no creo que debamos imponer un requisito trait TraitName . Estoy pensando que trait debe estar en una línea.

Por lo tanto, si se trata de un script de herramientas, por supuesto, debería tener una herramienta en la parte superior. Luego, si es un rasgo, debe definir si es un rasgo en la siguiente línea. Opcionalmente, permita que alguien indique el nombre de la clase de secuencia de comandos después de la declaración de características en la misma línea y, si lo hace, no permita que usen también class_name . Si omiten el nombre del rasgo, entonces class_name <name> está bien. Luego, al extender otro tipo, podríamos insertar el extends después de la declaración del rasgo y/o en una línea propia después de la declaración del rasgo. Entonces, consideraría cada uno de estos válidos:

# 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 en un rasgo no debería hacer ninguna diferencia, ya que no se ejecutan directamente.

Estoy de acuerdo en que un rasgo no necesariamente tiene un nombre global. Usaría trait de manera similar a tool . Debe ser lo primero en el archivo de script (a excepción de los comentarios). Opcionalmente, la palabra clave debe ir seguida del nombre del rasgo. No usaría class_name para ellos, ya que no son clases.

Tercero: ¿deberíamos, para fines de autocompletado y/o declaración de intención/requisitos, permitir que un rasgo defina un tipo base que debe extenderse?

Sinceramente, no me gusta agregar funciones en el idioma por el bien del editor. Ahí es donde las anotaciones serían útiles.

Ahora, si queremos hacer que los rasgos se apliquen solo a un cierto tipo (y sus derivados), entonces está bien. De hecho, creo que esto es mejor por el bien de las verificaciones estáticas: permite que un rasgo use cosas de una clase mientras que la compilación puede verificar si se están usando con los tipos correctos y demás.

Cuarto: en lugar de que los rasgos extiendan otros rasgos, ¿no podemos simplemente mantener la declaración extends reservada para extensiones de clase, permitir que un rasgo no necesite esta declaración en absoluto, pero en lugar de extender un rasgo base, permitir rasgos para simplemente tener sus propias declaraciones using que subimportan _esos_ rasgos?

Bueno, eso es principalmente una cuestión de semántica. Cuando mencioné que los rasgos pueden extender a otro, realmente no quise usar la palabra clave extends . La principal diferencia es que con extends solo puede ampliar uno, con using puede incorporar muchos otros rasgos en uno. Estoy de acuerdo con using , mientras no haya ciclos, no es un problema.

Quinto: si tenemos un rasgo, y tiene un using o un extends para un método, y luego implementa el suyo propio, ¿qué hacemos cuando llama dentro de esa función .<method_name> ?

Esa es una pregunta difícil. Asumiría que un rasgo no tiene nada que ver con la herencia de clase. Entonces, la notación de puntos debe llamar al método en el rasgo principal, si hay uno, de lo contrario arroja un error. Un rasgo no debe ser consciente de las clases en las que se encuentra.

OTOH, un rasgo es casi como un "incluir", por lo que se aplicaría palabra por palabra a la clase, por lo tanto, llamando a la implementación principal. Pero, sinceramente, simplemente prohibiría la notación de puntos si el método no se encuentra en el rasgo principal.

¿Qué pasa con un rasgo que requiere que la clase tenga uno o más rasgos? Por ejemplo, un rasgo DoubleJumper que requiere tanto el rasgo Jumper , un rasgo Upgradable y una clase que hereda KinematicBody2D .

Rust, por ejemplo, le permite usar firmas de tipo como esas. Algo así como KinematicBody2D: Jumper, Upgradable . Pero como estamos usando : para anotar el tipo, podríamos usar KinematicBody2D & Jumper & Upgradable o algo así.

También está el problema del polimorfismo. ¿Qué pasa si la implementación del rasgo es diferente para cada clase, pero expone la misma interfaz?

Por ejemplo, queremos un método kill() en el rasgo Jumper , que usan tanto Enemy como Player . Queremos diferentes implementaciones para cada caso, manteniendo ambas compatibles con la misma firma de tipo Jumper . ¿Como hacer esto?

Para el polimorfismo, simplemente crearía un rasgo separado que incluye el rasgo con kill() y luego implementa su propia versión específica del método. El uso de rasgos que anulan los métodos de rasgos incluidos anteriormente es la forma en que lo manejaría.

Además, no creo que haya ningún plan (todavía) para hacer que una sugerencia de tipo tenga requisitos de rasgos. ¿Es algo que nos gustaría hacer?

crear un rasgo separado

¿No generaría eso un montón de archivos de rasgos únicos? Si pudiéramos hacer declaraciones de características anidadas (similares a la palabra clave class ), sería más conveniente. También podríamos anular los métodos directamente en la clase que usa el rasgo.

Realmente apreciaría un sistema de firma de tipo fuerte (tal vez con composición booleana y opcionales/no nulos). Los rasgos encajarían perfectamente.

No estoy seguro de si se discutió, pero creo que debería ser posible invocar una versión específica del rasgo de una función. Por ejemplo:

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()

que imprime: A , B , B .


Además, no estoy del todo seguro sobre el "costo sin tiempo de ejecución". ¿Cómo se manejarían los scripts cargados dinámicamente (no disponibles durante la exportación) con clases que usan rasgos que se definieron antes de la exportación? ¿Estoy malinterpretando algo? ¿O ese caso no se considera "tiempo de ejecución"?

No estoy seguro de si se discutió, pero creo que debería ser posible invocar una versión específica del rasgo de una función.

Ya estaba considerando esto, pero no estoy seguro de permitir que una clase use rasgos en conflicto (es decir, rasgos que definen el método con el mismo nombre). El orden de las sentencias using no debería marcar ninguna diferencia.

Además, no estoy del todo seguro sobre el "costo sin tiempo de ejecución". ¿Cómo se manejarían los scripts cargados dinámicamente (no disponibles durante la exportación) con clases que usan rasgos que se definieron antes de la exportación? ¿Estoy malinterpretando algo? ¿O ese caso no se considera "tiempo de ejecución"?

No se trata de exportar. Definitivamente afectará el tiempo de carga, ya que la compilación ocurre durante la carga (aunque no creo que sea muy significativo), pero no debería afectar cuando se ejecuta el script. Idealmente, los scripts deberían compilarse en la exportación, pero esa es otra discusión.

Hola a todos.

Soy nuevo en Godot y me he estado acostumbrando en los últimos días. Mientras trataba de descubrir las mejores prácticas para usar para hacer componentes reutilizables, me decidí por un patrón. Siempre haría que el nodo raíz de una subescena, que está destinado a ser instanciado en otra escena, exporte todas las propiedades que pretendo establecer desde el exterior. En la medida de lo posible, quería que el conocimiento de la estructura interna de la rama instanciada fuera innecesario para el resto de la escena.

Para que esto funcione, el nodo raíz tiene que "exportar" las propiedades y luego copiar los valores al elemento secundario apropiado en _ready. Entonces, por ejemplo, imagine un nodo Bomb con un Timer secundario. El nodo bomba raíz en la subescena exportaría "detonation_time" y luego haría $Timer.wait_time = detonation_time en _ready. Esto nos permite configurarlo muy bien en la interfaz de usuario de Godot cada vez que lo instanciamos sin tener que hacer que los niños sean editables y profundizar en el temporizador.

Sin embargo
1) Es una transformación muy mecánica, por lo que parece que el sistema podría admitir algo similar.
2) Probablemente agrega una ligera ineficiencia al establecer el valor apropiado directamente en el nodo secundario.

Antes de continuar, esto puede parecer tangencial a lo que se está discutiendo porque no implica permitir una especie de herencia "privada" (en la jerga de C++). Sin embargo, en realidad me gusta el sistema de construcción de comportamientos de Godot al componer elementos de escena en lugar de una ingeniería más similar a la herencia. Esas relaciones "escritas" no cambian y son estáticas. OTOH, la estructura de la escena es dinámica, incluso puedes cambiarla en tiempo de ejecución. La lógica del juego está tan sujeta a cambios durante el desarrollo que creo que el diseño de Godot encaja muy bien en el caso de uso.

Es cierto que los nodos secundarios se utilizan como extensiones de comportamiento de los nodos raíz, pero eso no hace que carezcan de autosuficiencia, en mi opinión. Un cronómetro es perfectamente autónomo y predecible en comportamiento, independientemente de lo que esté acostumbrado a cronometrar. Ya sea que use una cuchara para beber sopa o comer helado, cumple adecuadamente su función aunque actúe como una extensión de su mano. Veo los nodos raíz como maestros que coordinan los comportamientos de los nodos secundarios para que no tengan que saberse directamente unos de otros y, POR LO TANTO, puedan permanecer autónomos. Los nodos primarios/raíz son administradores vinculados al escritorio que delegan responsabilidades pero no realizan mucho trabajo directo. Como son delgados, es fácil crear uno nuevo para un comportamiento ligeramente diferente.

Sin embargo, creo que los nodos raíz también deberían actuar como la INTERFAZ principal para la funcionalidad de toda la rama instanciada. Todas las propiedades que se pueden modificar en la instancia deben ser "configurables" en el nodo raíz de la rama, incluso si el propietario final de la propiedad es un nodo secundario. A menos que me esté perdiendo algo, esto tiene que arreglarse manualmente en la versión actual de Godot. Sería bueno si esto pudiera automatizarse de alguna manera para combinar los beneficios de un sistema dinámico con secuencias de comandos más sencillas.

Una cosa en la que estoy pensando es en un sistema de "herencia dinámica", por así decirlo, disponible para las subclases de Node. Habría dos fuentes de propiedades/métodos en tal guión, los del guión que extiende y los que "surgieron" de los niños dentro de la estructura de la escena. Así que mi ejemplo con Bomb se convertiría en algo así como export lifted var $Timer.wait_time [= value?] as detonation_time dentro de la sección de variables miembro del script bomb.gd. Básicamente, el sistema generaría $Timer.wait_time = detonation_time en la devolución de llamada _ready y generaría el getter/setter que permitirá que $Bomb.detonation_time = 5 del padre del nodo Bomb resulte en la configuración de $Timer.wait_time = 5 .

En el ejemplo del OP con MoveRightTrait, el nodo al que está adjunto mysprite.gd tiene MoveRightTrait como un nodo secundario. Luego, en mysprite.gd tendríamos algo como lifted func $MoveRightTrait.move_right [as move_right] (quizás 'as' podría ser opcional cuando el nombre sea el mismo). Ahora, llamar a move_right en un objeto de secuencia de comandos creado a partir de mysprite.gd delegaría automáticamente al nodo secundario apropiado. ¿Quizás las señales podrían burbujear para que puedan adjuntarse a un nodo secundario desde la raíz? Tal vez los nodos completos podrían burbujearse con solo lifted $MoveRightTrait [as MvR] sin función, señal o var. En este caso, todos los métodos y propiedades en MoveRightTrait serían accesibles desde mysprite directamente como mysprite.move_right o a través de mysprite.MvR.move_right si se usa 'as MvR'.

Esa es una idea de cómo simplificar la creación de una INTERFAZ para una estructura de escena en la raíz de una rama instanciada, aumentando su característica de "caja negra" y obteniendo la conveniencia de secuencias de comandos junto con el poder del sistema de escena dinámica de Godot. Por supuesto, habría muchos detalles secundarios a considerar. Por ejemplo, a diferencia de las clases base, los nodos secundarios se pueden eliminar en tiempo de ejecución. ¿Cómo deberían comportarse las funciones y propiedades con burbujas/elevadas si se llama/se accede a ellas en ese caso de error? Si se vuelve a agregar un nodo con el NodePath correcto, ¿las propiedades levantadas comienzan a funcionar nuevamente? [SÍ, en mi opinión] También sería un error usar 'lifted' en clases que no se derivan de Node, ya que en ese caso nunca habría elementos secundarios para burbujear/eliminar. Además, los conflictos de nombres son posibles con "as {name}" duplicado o "lifted $Timer1 lift $Timer2" donde los nodos tienen propiedades/métodos con el mismo nombre. El intérprete de secuencias de comandos idealmente detectaría tales problemas lógicos.

Siento que esto nos daría mucho de lo que queremos, aunque en realidad es solo azúcar sintáctica lo que nos evita tener que escribir funciones de reenvío e inicializaciones. Además, debido a que es fundamentalmente simple conceptualmente, no debería ser tan difícil de implementar o explicar.

De todos modos, si llegaste hasta aquí, ¡gracias por leer!

Usé "levantado" en todas partes, pero eso es solo ilustrativo.
Algo como using var $Timer.wait_time as detonation_time o using $Timer obviamente es igual de bueno. En cualquier caso, puede pseudoheredar convenientemente de los nodos secundarios, creando un único punto de acceso coherente a la funcionalidad deseada en la raíz de la rama que se va a instanciar. El requisito de las piezas de funcionalidad reutilizables es que amplíen Node o una subclase del mismo para que puedan agregarse como elementos secundarios al componente más grande.

Otra forma de verlo es que la palabra clave "extiende" en un script que hereda de un nodo le brinda su relación "es-un" mientras usa la palabra clave "usando" o "elevado" en un script para "burbujear" un los miembros del nodo descendiente le brindan algo similar a "implementos" [hey, posible palabra clave] que existe en idiomas con herencia única pero múltiples "interfaces" (por ejemplo, Java). En la herencia múltiple sin restricciones (como c ++), las clases base forman un árbol [estático, escrito]. Por analogía, estoy proponiendo capas de sintaxis conveniente y eliminación repetitiva sobre los árboles de nodos existentes de Godot.

Si alguna vez se determina que esto es algo que vale la pena explorar, hay aspectos del espacio de diseño a considerar:
1) ¿Deberíamos permitir solo a los niños inmediatos en un "uso". IOW using $Timer pero no 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 the "using"-ed node instead of following a written in NodePath? For example #$ de find_node usando "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. usando var "Timer".wait_time como 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 usando MoveRightTrait como 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:` se convertiría en una prueba de la funcionalidad por convención.
4) El patrón de estado debe poder implementarse reemplazando un nodo "usando" con otro que tenga un comportamiento variable pero la misma interfaz [tipo de pato] y el mismo NodePath como se hace referencia en la declaración "usando". Con la forma en que funciona el árbol de escenas, esto funcionaría casi gratis. Sin embargo, el sistema tendría que rastrear las señales conectadas a través de un padre y restaurar las conexiones en el hijo reemplazado.

He estado trabajando con GDScript durante algún tiempo y tengo que estar de acuerdo, se necesita urgentemente algún tipo de rasgo/mixin y proxy/delegación. Es bastante molesto tener que configurar todo este modelo solo para conectar propiedades o exponer métodos de niños en la raíz de la escena.

O agregar niveles del árbol solo para simular componentes (se vuelve bastante engorroso con bastante rapidez, porque luego se rompen todas las rutas de los nodos con cada nuevo componente). Tal vez haya una mejor manera, algo así como meta/multi script que permita múltiples scripts en un nodo. Si tiene una solución idiomática, por favor comparta...

Lanzar C++ (GDNative) a la mezcla empeora aún más las cosas, porque _ready y _init se comportan de manera diferente allí (léase: la inicialización con valores predeterminados funciona a medias o no funciona en absoluto).

Esto es lo principal que tengo que solucionar en GDScript. A menudo necesito compartir la funcionalidad entre nodos sin estructurar toda mi estructura de herencia en torno a ella; por ejemplo, mi jugador y los comerciantes tienen un inventario, mi jugador, mis artículos y mis enemigos tienen estadísticas, mi jugador y mis enemigos tienen artículos equipados, etc.

Actualmente implemento estos 'componentes' compartidos como clases o nodos cargados en las 'entidades' que los requieren, pero es complicado (agrega muchas búsquedas de nodos, hace que escribir pato sea casi imposible, etc.) y los enfoques alternativos tienen sus propios inconvenientes, así que No he encontrado una mejor manera. Traits/mixins absolutamente salvarían mi vida.

Todo se reduce a poder compartir código entre objetos sin usar la herencia, lo que creo que es necesario y no es posible hacerlo limpiamente en Godot en este momento.

La forma en que entiendo los rasgos de Rust (https://doc.rust-lang.org/1.8.0/book/traits.html), es que son como clases de tipos de Haskell, donde requiere que se definan algunas funciones parametrizadas para el tipo está agregando un rasgo, y luego puede usar algunas funciones genéricas definidas sobre cualquier tipo que implemente un rasgo. ¿Son los rasgos de Rust algo diferente de lo que se propone aquí?

Este probablemente se migrará al por mayor, ya que se ha discutido extensamente aquí.

Este probablemente se migrará al por mayor, ya que se ha discutido extensamente aquí.

_En mi opinión, "mover" las propuestas no tiene sentido, es mejor cerrarlas y pedir que se vuelvan a abrir en Godot-Proposals si las personas expresan interés, y dejar que otras propuestas se implementen si es necesario. De todos modos..._

Me topé con este problema hace un año, pero solo ahora empiezo a comprender la utilidad potencial del sistema de rasgos.

Compartir mi flujo de trabajo actual con la esperanza de inspirar a alguien a comprender mejor el problema (y tal vez sugerir una mejor alternativa que no sea la implementación del sistema de rasgos).

1. Cree una herramienta para generar plantillas de componentes para cada tipo de nodo utilizado en el proyecto:

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

Ahora, lo que haría ESTO más fácil sería tener un sistema de fragmentos o un sistema de macros para que la creación de esas secciones de código declarativo reutilizadas sea más fácil para los desarrolladores.

Siguiendo tus pasos... 😅

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. Cree un método estático para reutilizarlo para inicializar componentes en el host (raíz, por ejemplo):

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')

Así es como se ve un componente (rasgo) una vez generado con el primer script:

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)

(Opcional) 3. Ampliar el componente (rasgo)

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

esa es mi idea Creo que es lo suficientemente simple y cubre casi todos los casos de uso para la reutilización de código. Incluso permitiría que la clase que usa el rasgo anule los métodos de rasgo (si es posible hacerlo en tiempo de compilación). Los rasgos también podrían extender otros rasgos.

Siguiendo tus pasos... 😅

class_name ComponentMotion2D extends ComponentNode2D

const MAX_SPEED = 100.0

var linear_velocity = Vector2()
var collision

export(Script) var impl
...

En realidad, los Script exportados se utilizan en estos componentes para impulsar el comportamiento de tipos de nodos raíz/host específicos por componente. Aquí, ComponentMotion2D tendrá principalmente dos scripts:

  • motion_kinematic_body_2d.gd
  • motion_rigid_body_2d.gd

Así que los niños todavía manejan el comportamiento host / root aquí. La terminología host proviene de mí usando máquinas de estado, y aquí es donde quizás los rasgos no encajarían perfectamente, porque los estados están mejor organizados como nodos en mi opinión.

Los componentes mismos están "conectados" a la raíz haciéndolos miembros onready , lo que reduce efectivamente el código repetitivo (con el costo de tener que hacer referencia a ellos como object.motion )

extends KinematicBody2D

onready var motion = $motion # ComponentMotion2D

No estoy seguro de si esto ayudaría a resolver el problema, pero C# tiene una cosa llamada métodos de extensión que amplían la funcionalidad de un tipo de clase.

Básicamente, la función debe ser estática, y el primer parámetro es obligatorio y debe ser self . Se vería así como una definición:

extensión.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)

Luego, cuando desee utilizar el método distance , simplemente haga esto:

jugador.gd

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

Lo conozco, pero eso no ayuda a resolver el problema. Jeje, gracias de todos modos.

Ya se propusieron métodos de extensión @TheColorRed , pero no creo que sean factibles en un lenguaje dinámico. Tampoco creo que resuelvan el problema de raíz que inicialmente inició esta discusión.


En otra nota, probablemente abriré muchas de las propuestas para GDScript como GIP (esto incluido, si a @willnationsdev no le importa).

Todavía creo que los rasgos tienen más sentido para compartir código horizontalmente en un lenguaje OO (sin herencia múltiple, pero no quiero ir por ese camino).

No creo que sean factibles en un lenguaje dinámico.

¿Sin embargo, GDS es dinámico? Los métodos de extensión podrían limitarse solo a instancias escritas y funcionarían exactamente igual que en otros idiomas: solo un azúcar sintáctico durante la compilación que reemplaza la llamada al método con la llamada al método estático (función). Honestamente, preferiría proxenetas (métodos externos) antes que prototipos ala JS u otras formas dinámicas de adjuntar métodos a clases o incluso solo instancias.

Sea lo que sea que decidamos hacer, espero que no decidamos llamarlo "proxenetas".

¿Sin embargo, GDS es dinámico?

Hay muchas definiciones de "dinámico" en este contexto. Para ser claros: es posible que el tipo de variable no se conozca en el momento de la compilación, por lo que la verificación de la extensión del método debe realizarse en tiempo de ejecución (lo que perjudicará el rendimiento de una forma u otra).

Los métodos de extensión podrían limitarse solo a instancias tipeadas

Si comenzamos a hacer esto, también podríamos hacer que GDScript solo se escriba. Pero esa es otra discusión que no entiendo aquí.

El punto es: las cosas no deberían comenzar o dejar de funcionar porque el usuario agregó tipos a un script. Casi siempre es confuso cuando sucede.

Nuevamente, no creo que resuelva el problema de todos modos. Estamos tratando de adjuntar el mismo código a varios tipos, mientras que un método de extensión lo agregará solo a un tipo.

Honestamente, preferiría proxenetas (métodos externos) antes que prototipos ala JS u otras formas dinámicas de adjuntar métodos a clases o incluso solo instancias.

Nadie propuso (todavía) adjuntar métodos dinámicamente (en tiempo de ejecución) y tampoco quiero eso. Los rasgos se aplicarían estáticamente en el momento de la compilación.

Originalmente hice un comentario sobre Haxe y su biblioteca de macros mixin, pero luego me di cuenta de que la mayoría de los usuarios no usarán un lenguaje de terceros de todos modos.

Recientemente me encontré con la necesidad de esto.

Tengo algunos objetos con los que el usuario puede interactuar pero no pueden compartir el mismo padre, pero necesitan grupos similares de API

por ejemplo, tengo algunas clases que no pueden heredar del mismo padre, pero usan un conjunto similar de API:
Almacén: Finanzas, Eliminación, MouseInteraction + otros
Vehículo: Finanzas, Eliminación, MouseInteraction + otros
VehicleTerminal: Finanzas, Eliminación, MouseInteraction + otros

Para las finanzas, he usado la composición, ya que requiere la menor cantidad de código de placa de caldera, ya que get_finances_component() es una API suficiente, ya que realmente no se preocupa en absoluto por los objetos del juego.

Los demás:
MouseInteraction and Delection Acabo de tener que copiar y pegar, ya que necesita saber sobre los objetos del juego, algunas composiciones no funcionan aquí a menos que haya hecho una delegación extraña:

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

pero eso realmente no me permite anular cómo funciona la eliminación donde, si fuera una clase heredada, podría tener la capacidad de volver a escribir parte del código de eliminación si es necesario.

MouseInteraction and Delection Acabo de tener que copiar y pegar, ya que necesita saber sobre los objetos del juego, algunas composiciones no funcionan aquí a menos que haya hecho una delegación extraña

Actualmente accedo a los componentes a través onready nodos. Estoy haciendo algo similar:

# character.gd

var input = $input # input component

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

Así que esto:

character.input.enabled = true

se convierte en esto:

character.focused = true

Como @Calinou señaló amablemente, mi problema https://github.com/godotengine/godot-proposals/issues/758 está estrechamente relacionado con esto. ¿Qué os parece la propuesta de poder añadir un rasgo a un grupo? Esto podría reducir drásticamente la necesidad de scripts y otros gastos generales.

Simplemente sería genial tener una forma de inyectar código compartible en las clases, y si tienen valores exportados, estos aparecen en el inspector, y tienen los métodos y propiedades disponibles y detectados al completar el código.

Las propuestas de características y mejoras para Godot Engine ahora se están discutiendo y revisando en un rastreador de problemas dedicado de Propuestas de mejora de Godot (GIP) ( godotengine/godot-proposals ). El rastreador GIP tiene una plantilla de problemas detallada diseñada para que las propuestas incluyan toda la información relevante para iniciar una discusión productiva y ayudar a la comunidad a evaluar la validez de la propuesta para el motor.

El rastreador principal ( godotengine/godot ) ahora está exclusivamente dedicado a los informes de errores y solicitudes de incorporación de cambios, lo que permite a los colaboradores concentrarse mejor en el trabajo de corrección de errores. Por lo tanto, ahora estamos cerrando todas las propuestas de funciones anteriores en el rastreador de problemas principal.

Si está interesado en esta propuesta de función, abra una nueva propuesta en el rastreador GIP siguiendo la plantilla de problema dada (después de verificar que aún no existe). Asegúrese de hacer referencia a este tema cerrado si incluye alguna discusión relevante (que también le animamos a resumir en la nueva propuesta). ¡Gracias por adelantado!

Nota: esta es una propuesta popular, si alguien la pasa a Godot Proposals, intente resumir la discusión también.

¿Fue útil esta página
0 / 5 - 0 calificaciones