En Verse, es posible crear una clase que amplíe la definición de otra, añadiendo o modificando los campos y métodos de la otra clase. Esto se suele llamar definir una subclase o herencia, ya que una clase hereda las definiciones de la otra.
Veamos el dispositivo Diseñador de clase como un ejemplo de definición de subclase. Con el dispositivo Diseñador de clase, es posible crear clases de personajes para personajes de jugadores, que permiten definir los atributos e inventarios específicos de una clase de personaje, como un tanque o un personaje de DPS (daño por segundo).
![]() |
![]() |
| Clase de personaje DPS creada con el dispositivo Diseñador de clase | Clase de personaje de tanque creada con el dispositivo Diseñador de clase |
En Verse, es posible crear una clase tank y una clase dps así:
tank := class():
StartingShields : int
MaxShields : int
AllowOvershield : logic
DamageReduction : int
dps := class():
StartingShields : int
MaxShields : int
AllowOvershield : logic
MovementMultiplier : float
Puesto que algunos de los campos de las dos clases son iguales, es posible reducir la duplicación con una superclase que contenga las propiedades y los comportamientos compartidos de las clases. Llamaremos a esta superclase player_character y convertiremos a tank y dps en subclases de player_character:
player_character := class():
StartingShields : int
MaxShields : int
AllowOvershield : logic
dps := class(player_character):
MovementMultiplier : float
tank := class(player_character):
DamageReduction : int
Como las clases tank y dps son subclases de player_character, heredan automáticamente los campos y métodos de la clase player_character, por lo que solo tienes que especificar lo que es diferente en esta clase con respecto a la superclase.
Por ejemplo, la clase dps solo añade el campo MovementMultiplier, y la clase tank solo añade el campo DamageReduction. Esta configuración es útil para cambiar los comportamientos compartidos por las dos clases más adelante, ya que solo será necesario hacerlo en la superclase.
Con Verse, puedes añadir más cambios para diferenciar las clases tank y dps añadiendo métodos a las subclases.
Un efecto útil del uso de subclases es que es posible utilizar la relación entre una superclase y sus subclases. Gracias a la herencia, una instancia de tank es un player_character especializado y una instancia de dps es un player_character especializado, lo cual se denomina relación. Como tank y dps son subclases de la misma superclase y se distinguen de su superclase compartida, tank no tiene una relación con dps.
Especificador override
Para crear instancias de clases con valores iniciales, una práctica habitual es tener una función que las genere. Por ejemplo:
CreateDPSPlayerCharacter() : dps =
return dps{StartingShields := 0, MaxShields := 0, AllowOvershield := false, MovementMultiplier := 1.9}
CreateTankPlayerCharacter() : tank =
return tank{StartingShields := 100, MaxShields := 200, AllowOvershield := true, DamageReduction := 50}
Las funciones CreateTankPlayerCharacter() y CreateDPSPlayerCharacter() crean las instancias con los valores iniciales adecuados. Como alternativa, puedes anular los campos de la superclase y asignarles valores iniciales para no tener que proporcionar tantos valores iniciales al crear una instancia.
Por ejemplo, la clase tank de la sección anterior podría tener este aspecto con anulaciones en los campos:
tank := class(player_character):
StartingShields<override> : int = 100
MaxShields<override> : int = 200
AllowOvershield<override> : logic = true
DamageReduction : int = 50
CreateTankPlayerCharacter() : tank =
return tank{}
También puedes anular métodos en la subclase, lo que significa que puedes utilizar el método override en cualquier lugar donde se pueda utilizar el método overriden. Es decir:
- El método debe aceptar al menos cualquier argumento aceptado por el método anulado, por lo que el tipo de parámetro debe ser un supertipo del tipo de parámetro de la función anulada.
- El método no debe devolver un valor que el método anulado no pudiera tener, por lo que el tipo de retorno debe ser un subtipo del tipo de retorno del método anulado.
- El método no debe tener más efectos que el método anulado, por lo que el especificador de efecto debe ser un subtipo del especificador de efecto del método anulado.
Super
De forma parecida a Self, es posible usar (super:) para acceder a las implementaciones de campos y métodos de la superclase. Para poder utilizar (super:), es necesario que el campo o método esté implementado en la definición de la superclase.
pet := class():
Sound : string
Speak() : void =
Log(Sound)
cat := class(pet):
Sound<override> : string = "Miau"
Speak<override>() : void =
(super:)Speak() # "Miau" aparece en el registro de salida.
Log("Purr") # "Purr" aparece en el registro de salida
Expresiones de bloque en el cuerpo de una subclase
Todas las expresiones de block presentes dentro del cuerpo de una subclase se ejecutan después de las expresiones de block especificadas en el cuerpo de la superclase. Por ejemplo, en el código siguiente, cuando se crea la instancia de la clase cat llamada MrSnuffles, primero se ejecuta Speak(), y después Purr().
pet := class():
Speak() : void =
...
block:
Speak()
cat := class(pet):
Purr() : void =
...
block:
Purr()
MrSnuffles := cat{}
Especificador abstract
Cuando una clase o un método de una clase tiene el especificador abstract, no es posible crear una instancia de ella. Las clases abstractas están pensadas para ser utilizadas como una superclase con una implementación parcial o como una interfaz común. Esto es útil cuando no tiene sentido tener instancias de una superclase pero tampoco se desea duplicar propiedades y comportamientos en clases similares.
En el siguiente ejemplo, como «pet» (mascota) es un concepto abstracto, una instancia de la clase «pet» no es lo suficientemente específica, pero un gato («cat») o un perro («dog») como mascota sí tienen sentido, por lo que esas subclases no se marcan como abstractas.
pet := class<abstract>():
Speak() : void
cat := class(pet):
Speak() : void =
...
dog := class(pet):
Speak() : void =
...

