21 agosto 2025

Los interfaces de C# y el inspector de Unity

Interface Contract
En C#, una interfaz es un contrato que define un conjunto de métodos, propiedades, eventos o indexadores que una clase o estructura debe implementar, sin que dicho contrato proporcione la implementación concreta. Es como un plano que especifica "qué" debe hacerse, pero no el "cómo". Cuando una clase se adhiere a una interfaz, se compromete públicamente a ofrecer, al menos, los métodos de dicha interfaz. Por tanto, dicha clase debe implementar el código de cada uno de los métodos del interfaz.

Las interfaces en C# ofrecen varias ventajas que las hacen fundamentales en el diseño de software. Las principales son:

  • Abstracción: Definen un contrato sin detalles de implementación, permitiendo centrarse en "qué" hace un objeto, no en "cómo" lo hace.
  • Flexibilidad y extensibilidad: Facilitan la adición de nuevas clases que implementen la misma interfaz sin modificar el código existente.
  • Herencia múltiple: Permiten que una clase implemente varias interfaces, superando la restricción de herencia única de clases en C#. Una clase sólo puede heredar de una clase madre, pero puede implementar varios interfaces simultáneamente.
  • Desacoplamiento: Reducen la dependencia entre componentes, promoviendo un diseño modular y facilitando la inyección de dependencias.
  • Mantenimiento y testing: Simplifican la sustitución de implementaciones (por ejemplo, en pruebas unitarias con mocks) al trabajar con contratos en lugar de clases concretas.
  • Consistencia: Garantizan que las clases que implementan la interfaz cumplan con un comportamiento estándar, mejorando la interoperabilidad.
  • Soporte a patrones de diseño: Son esenciales en patrones como Strategy, Factory o Repository, que dependen de abstracciones.

Todo esto se entiende mejor con ejemplos. En mi caso, he desarrollado una clase abstracta, denominada SteeringBehavior, que implementa la funcionalidad básica del movimiento de un agente inteligente. De esa clase heredan otras muchas hijas que implementan movimientos especializados. 

La clase abstracta SteeringBehavior
La clase abstracta SteeringBehavior

Algunas (no todas) de esas clases hijas necesitan que se les fije un blanco para poder calcular su movimiento, ya sea para alcanzar ese blanco, para perseguirlo, para mirar hacia él, etc. Ese blanco se define con un campo Target. 

Podría haber incluido ese campo dentro de la definición de las clases hijas, pero eso me habría restado flexibilidad. El problema habría venido al encadenar clases SteeringBehavior. Una clases SteeringBehavior ofrecen su funcionalidad componiendo las salidas de otras clases SteeringBehavior más sencillas. La flexibilidad de ese enfoque viene de poder construir en el inspector el árbol de dependencias de los diferentes SteeringBehavior.

Un agente, construido con la composición de varios SteeringBehavior
Un agente, construido con la composición de varios SteeringBehavior

Referencias en el inspector a los SteeringBehavior hijos
Referencias en el inspector a los SteeringBehavior hijos

Hay algunos SteeringBehavior para los que sabemos exactamente el tipo de los SteeringBehavior hijos que va a necesitar. En la captura anterior se puede ver que ActiveWallAvoiderSteeringBehavior necesita concretamente un hijo del tipo PassiveWallSteeringBehavior. Pero también hay casos en los que al SteeringBehavior padre le da igual el tipo concreto del hijo, siempre y cuando herede de SteeringBehavior y ofrezca un campo Target (al que pasarle su respectivo campo Target). En el caso de la captura, yo he pasado un SeekSteeringBehavior, pero podría haber pasado un ArriveSteeringBehavior o un PursuitSteeringBehavior, para obtener tipos de movimiento lgeramente diferentes. Si hubiera implementado el campo Target por herencia, habría perdido la flexibilidad de pasar cualquier tipo de SteeringBehavior con un campo Target, al verme obligado a especificar el tipo concreto, hijo de SteeringBehavior.

En vez de eso, lo que he hecho ha sido crear un interfaz.

El interfaz ITargeter
El interfaz ITargeter

Como se puede ver en la captura, el interfaz no puede ser más sencillo. Se declara con la palabra clave interface y, en este caso, se limita a señalar que las clases que se adhieran a este interfaz se comprometen a ofrecer una propiedad Target.

A una clase que se adhiriese a dicho interfaz le bastaría con implementar esa propiedad.

Una clase que implementa el interfaz ITargeter

Como se puede ver en la captura, la clase SeekSteeringBehavior hereda de SteeringBehavior y al mismo tiempo implementa el interfaz ITargeter.

Es posible que estés notando cierto solape entre las clases abstractas y los interfaces. Aunque ambas son mecanismos de abstracción, tienen diferencias importantes en su propósito y uso. Un interfaz define un contrato puro con firmas de métodos, propiedades, eventos o indexadores, sin ninguna implementación. Es ideal para especificar comportamientos que diferentes clases no relacionadas pueden compartir. Por su parte, una clase abstracta es una clase que no se puede instanciar directamente y puede contener tanto miembros abstractos (sin implementación) como miembros concretos (con implementación). Sirve como base para clases relacionadas que comparten lógica común. En general, si quisieras dotar de un comportamiento común a clases relacionadas por una madre, usarías clases abstractas (posiblemente haciendo abstracta a la clase madre); mientras que si el comportamiento común se diese en clases heterogéneas, lo habitual sería usar interfaces.

Mientras que una clase abstracta puede incluir métodos concretos, campos, constructores y lógica compartida que las clases derivadas pueden usar directamente; un interfaz no puede incluir implementación de métodos ni campos (solo a partir de C# 8.0 permite métodos por defecto, pero con restricciones, en general todo debe ser implementado por la clase que la adopta). Por otro lado, la clase abstracta permite modificadores de acceso (public, private, protected, etc.) para sus miembros; mientras que en en un interfaz todos los miembros son implícitamente públicos y no se pueden especificar modificadores de acceso. Todo ello sin olvidar la diferencia en cuanto la herencia: las clases abstractas sólo permiten herencia simple, mientras que los interfaces permiten herencia múltiple.

Unity añade una diferencia adicional, y que resulta determinante para este artículo, dado que permite mostrar en el inspector clases abstractas, pero no interfaces. En una de las capturas anteriores se veía el inspector de la clase ActiveWallAvoiderSteeringBehavior. Te pido que te fijes en su campo Steering Behavior. El código que declara ese campo y lo muestra en el inspector es el siguiente:

Declaración del campo steeringBehavior
Declaración del campo steeringBehavior

Fíjate en que steeringBehavior es del tipo SteeringBehavior (una clase abstracta) y en que el inspector no tiene problema en mostrarlo. Por tanto, podemos usar el inspector para meter en ese campo cualquier script que herede de SteeringBehavior.

Si quisiéramos que ese campo aceptase cualquier script que implementase ITargeter, deberíamos poder hacer [SerializeField] private ITargeter steeringBehavior. El problema es que aunque C# nos permite hacer algo así, Unity no permite exponer campos de tipo interfaz en el inspector.

Entonces, ¿cómo podemos hacer para asegurar que al campo steeringBehavior se le pase no sólo una clase que herede de SteeringBehavior, sino que además implemente el interfaz ITargeter? La respuesta es que Unity, no ofrece ninguna salvaguarda de serie, pero al menos podemos desarrollar la nuestra, tal y como te voy a explicar. 

Será más sencillo si te enseño primero lo que queremos conseguir y luego te explico cómo implementarlo. La solución que he encontrado ha sido diseñar mi propio PropertyAttribute para decorar con él los campos que quiero confirmar que cumplen uno o varios interfaces.

Atributo personalizado para confirmar el cumplimiento de interfaces
Atributo personalizado para confirmar el cumplimiento de interfaces

En la captura anterior se puede ver que he llamado a mi atributo InterfaceCompliant. En el ejemplo, el atributo verifica que el SteeringBehavior que se le pase al campo steeringBehavior cumple con el interfaz ITargeter.

Si el script que se le pasase al campo no cumpliese con el interfaz, el atributo se encargaría de mostrar un mensaje de alerta en el inspector.

Mensaje de alerta al pasar un script que no cumpliese con el interfaz
Mensaje de alerta al pasar un script que no cumpliese con el interfaz


Una vez que ya tenemos claro lo que queremos conseguir, vamos a ver cómo implementarlo. Como en el caso del resto de atributos, tenemos que crear una clase que herede de PropertyAttribute. Esta última es abstracta y nos obligará a implementar el método InterfaceCompliantAttribute, cuyos parámetros son los que se le pasarán al atributo cuando decore un campo.

Implementación de nuestro atributo personalizado
Implementación de nuestro atributo personalizado

Fíjate que el nombre de esta clase debe ser el de nuestro atributo, seguido del sufijo "Attribute". La clase debe ubicarse en dentro de la carpeta Scripts.

Esta clase sirve de modelo de datos del atributo. Guardaremos en ella, toda la información que necesitemos para hacer nuestra comprobación y mostrar el mensaje de advertencia si fuese necesario. En este caso, el atributo admite un número variable de parámetros (por eso la palabra clave params). Dichos parámetros son los tipos de nuestros interfaces y se pasarán en un arry que guardaremos en el campo InterfaceTypes.

Para representar al atributo personalizado en el inspectos, así como al campo que decora, utilizaremos un PropertyDrawer. Estas son clases que hereda, ¡oh, sorpresa!, de PropertyDrawer y que deben situarse en una carpeta Editor, al margen de la de Scripts. Podemos llamar a nuestra clase como queramos, lo importante es que la decoremos con un atributo CustomPropertyDrawer para señalar qué atributo es el que "dibuja" este PropertyDrawer en el inspector.

PropertyDrawer para dibujar nuestro atributo y el campo que decora
PropertyDrawer para dibujar nuestro atributo y el campo que decora

En nuestro caso, he empleado el atributo CustomPropertyDrawer para señalar que debe usarse este PropertyDrawer siempre que un campo aparezca decorado por un atributo InterfaceCompliantAttribute.

PropertyDrawer es otra clase abstracta que obliga a implementar un método denominado CreatePropertyGUI. Este método es al que llama el inspector cuando quiere dibujar en el inspector un campo decorado por este atributo. 

Implementación del método CreatePropertyGUI de InterfaceCompliantAttributeDrawer
Implementación del método CreatePropertyGUI de InterfaceCompliantAttributeDrawer

Una clase que herede de PropertyDrawer cuenta con una referencia al atributo que dibuja a través del campo attribute. Por eso, en la línea 22 se hace casting de él al tipo InterfaceCompliantAttributo que sabemos que es en realidad, para poder recuperar la lista de interfaces a comprobar en la línea 23.

Fíjate en que mientras el atributo se puede rescatar usando el campo attribute de PropertyDrawer, el campo decorado se pasa como un parámetro del método, property, encapsulado en un tipo Serialized Property (línea 19).

 Acto seguido, en la línea 26 generamos el contenedor visual en el que meteremos los widgets para representar nuestro campo en el inspector, así como, en su caso, el mensaje de advertencia. Por ahora, ese contenedor visual está vacío, pero en las líneas siguientes le iremos metiendo contenido. Conforme le vayamos añadiendo elementos al contenedor, estos se irán representando de arriba a abajo en el inspector.

Si miramos la captura en la que mostraba el mensaje de advertencia en el inspector, veremos que dicho mensaje iba antes del campo, así que por eso, el primer elemento que añadiremos al contenedor será dicho mensaje de advertencia, si fuese necesario (línea 28). Esta parte tiene bastante miga, así que la he sacado a su propio método (AddErrorBoxIfNeeded), para facilitar la lectura de CreatePropertyGUI y para permitirme reaprovechar ese método para volver a llamarlo en la línea 45. En cuanto acabe de explicar la implementación de CreatePropertyGUI veremos la de AddErrorIfNeeded.

Una vez que hemos añadido el mensaje de advertencia (si fuese necesario) al contenedor, hay que mostrar el campo decorado. Queremos mostrarlo con su aspecto por defecto en el inspector, así que nos limitamos a crear un PropertyField (línea 31). PropertyField detectará el tipo del campo encapsulado en property y los mostrará con el widget por defecto de Unity para ese tipo.

Atento ahora. Queremos que cada vez que metamos un nuevo script en el campo decorado, se vuelva a evaluar si el nuevo script cumple con los interfaces. Para eso, en la línea 43 le encargamos al contenedor que vigile el valor de property y que cada vez que este cambie vuelva a evaluar si mostrar o no el mensaje de advertencia (línea 45), redibujando el campo (línea 46). 

Hecho todo esto, añadimos el campo decorado al contenedor (línea 50) y retornamos este contenedor para que el inspector lo dibuje.

Como lo prometido es deuda, ahora vamos a analizar la implementación de AddErrorBoxIfNeeded.

Chequeo del campo decorado para ver si cumple con los interfaces

Para acceder al campo encapsulado dentro de un SerializedProperty hay que utilizar su campo objectReferenceValue (línea 70). 

Este campo es de tipo Object, así que luego habrá que hacer casting al tipo concreto que queremos comprobar. Eso lo hacemos en el método GetNotComplyingInterfaces de la línea 74. Ahora veremos su implementación, pero a alto nivel lo que hace ese método es coger una lista de interfaces (línea 75), y un objeto (línea 76), para luego hacer casting de ese objeto contra cada uno de los interfaces. Si alguno de esos castings resultase fallido, el tipo del interfaz que hubiese fallado se añadiría a una lista que se devolvería como salida del método (línea 74).

Dicha lista se pasa al método GenerateBox el cual dibuja el mensaje de advertencia, en caso de que la lista  tuviese algún elemento. En caso de estar vacía, el método no dibujaría nada. El mensaje dibujado se devolvería en un ErrorBox (línea 79) que se añadiría al contenedor (línea 85), tras borrar su contenido anterior (línea 82)

Por su parte, el chequeo de interfaces es muy sencillo.

Chequeo de interfaces sobre el script del campo
Chequeo de interfaces sobre el script del campo


A partir de la línea 120 se puede ver que, por cada uno de los interfaces de la lista (los pasados como parámetros del atributo), se comprueba si el script (checkedObject) que estamos chequeando lo implementa. Para ello, usamos el método de C# IsInstanceOfType (línea 123). Siempre he pensado que el método IsInstanceOfType es muy engañoso, porque leído de izquierda a derecha parece que quiere comprobar si "interfaceType-es-una-instancia-del-tipo-checkedObject", cuando, en realidad, lo que hace el método es comprobar si checkedObject es una instancia de interfaceType.

En caso de que checkedObject no implemente el interfaz, IsInstanceOfType devolverá falso y el tipo del interfaz se añadirá a la lista de interfaces no cumplidos (línea 125) que se devolverán como salida del método.

Por último, y por completitud, vamos a ver cómo se genera el mensaje de error en el método GenerateErrorBox.

Generación del mensaje de advertencia
Generación del mensaje de advertencia

Si el script a chequear cumpliese todos los interfaces, la lista que se le pasa como parámetro a GenerateErrorBox estaría vacía y el método se limitaría a devolver null (línea 90).

Sin embargo, si hubiese habido algún incumplimiento, se encadenarían los nombres de los interfaces no cumplidos, concatenándolos con comas entre ellos, como se puede ver en la líneas 93 a 95.

Esa lista de nombres de interfaces, concatenados con comas, se añadiría al final del mensaje de advertencia (línea 98) y con este se crearía un mensaje de error, que se devolvería para ser añadido al contenedor (línea 102).

Y con esto ya lo tienes todo. Es cierto que no vas a poder impedir que se metan en el campo decorado scripts que no cumplan con los interfaces, pero al menos saldrá un mensaje avisando del problema. Será muy difícil que no repares en el mensaje y soluciones el desliz. Por otro lado, todo este ejemplo ha servido para que veas cómo implementar un atributo personalizado con el que hacer uso de la flexibilidad del inspector de Unity. Espero que te haya resultado interesante.

09 agosto 2025

Luces y sombras de la iniciativa "Stop Killing Games"

La iniciativa Stop Killing Games es un movimiento de consumidores iniciado en 2024 por el youtuber Ross Scott, conocido por su canal Accursed Farms, en respuesta al cierre de los servidores de The Crew, un videojuego de Ubisoft que quedó injugable al requerir conexión constante a internet, incluso en su modo de un solo jugador. Esto generó indignación, ya que el título, aunque tenía componentes multijugador, también incluía un modo de un solo jugador que no tenía por qué depender de una conexión online. La iniciativa plantea un debate crucial sobre la propiedad digital en una era donde los juegos son cada vez más caros (con precios de hasta 80-100 euros) y dependen de servidores que pueden cerrarse. 

El logo de la iniciativa Stop Killing Games
El logo de la iniciativa Stop Killing Games

Ross Scott lanzó un video presentando la iniciativa y creó un sitio web (stopkillinggames.com) para recolectar firmas y promover peticiones gubernamentales en países como Francia, Reino Unido, Canadá, Australia y la Unión Europea.


Como respuesta a casos como este, la iniciativa exige preservar los videojuegos tras el fin de su soporte oficial, protegiendo los derechos de los consumidores y evitando que los juegos comprados se vuelvan inaccesibles por decisiones de las editoras. Su argumentación se basa en la oposición a que las editoras vendan los juegos como licencias temporales en lugar de productos permanentes. Frente a eso, defienden que los jugadores deberían tener derecho a disfrutar de lo que pagaron sin restricciones de tiempo. Además, reivindican los videojuegos como patrimonio cultural, comparándolos con películas o libros que no desaparecen tras su comercialización. 

Por eso, la iniciativa busca proteger legalmente a los jugadores ante cierres súbitos de servidores que dejen los juegos injugables. Para ello exigen que los juegos incorporen siempre un modo offline, con campañas de un solo jugador, para que no dependan de servidores activos; o bien que los juegos permitan a los usuarios crear servidores privados para mantener vivos los juegos cuando las empresas dejen de soportarlos.

Desde su arranque, la iniciativa ha sido apoyada por más de 1.4 millones de firmantes, medios de comunicación, y creadores de contenido como PewDiePie, MoistCr1TiKaL y Jacksepticeye. Incluso uno de los vicepresidentes del Parlamento Europeo (Nicolae "Nicu" Ştefănuță) mostró su apoyo a la iniciativa y unió su firma a las demás. No es determinante, porque el Parlamento Europeo es muy grande, pero si es la punta del iceberg de una tendencia cada vez más evidente de ponerle freno a ciertas prácticas abusivas.

Nicolae "Nicu" Ştefănuță
Nicolae "Nicu" Ştefănuță, uno de los vicepresidentes del Parlamento Europeo, apoyó publicamente la iniciativa

Frente a eso, se encuentran, como es lógico las principales empresas desarrolladoras. El lobby Video Games Europe, que representa a empresas como EA, Ubisoft y Activision, argumenta que las propuestas de la iniciativa harían el desarrollo de juegos "prohibitivamente caro" y defiende que los jugadores no son propietarios de los juegos, sino de licencias revocables. No es el único caso, otros influencers del mundo del desarrollo de videojuegos también han mostrado su oposición al movimiento de Ross Scott. El streamer Pirate Software (Jason Thor Hall) criticó la iniciativa, alegando que no es viable para todos los juegos y que podría dañar a los desarrolladores, lo que generó un enfrentamiento con Scott.

Está claro que las productoras defienden sus intereses, pero algunos de sus argumentos no carecen de base y merece la pena considerarlos para que iniciativas bien intencionadas no se acaben llevando por delante a los estudios indies o condicionando negativamente un mercado tan dinámico como el de los videojuegos.

Para empezar, la iniciativa peca de meter a todos los videojuegos en el mismo saco, cuando los hay de muy distintos tipos. La principal distinción es que hay juegos diseñados para funcionar íntegramente con los recursos locales de tu ordenador o consola (por ejemplo Half-Life: Alyx), mientras que otros juegos dependen de servidores online que son los que gestionan toda la lógica del juego y guardan el progreso de los jugadores (ejemplos evidentes Fortnite o World of Warcraft). 

Lo que parece olvidar esta iniciativa es que cuando un juego está diseñado de una manera, no es tan sencillo rediseñarlo de otra. Si un juego está construido para depender de servidores a veces es imposible rediseñarlo para que, llegado el caso, pueda redistribuirse localmente y funcionar con un servidor privado que se monte el usuario en casa. Los juegos online suelen contar con múltiples elementos licenciados en el extremo del servidor (motores físicos, frameworks para el mathmaking, etc). Esos elementos no son propiedad de los desarrolladores y, en muchos casos, no pueden redistribuirlos sin infringir los términos de sus licencias. Esto mismo se ve más claramente con el contenido audiovisual. Piensa en la música que te puedes comprar dentro de Fortnite, para amenizar las esperas en el lobby. Muchas de esas canciones son de artistas muy conocidos que han licenciado su obra para que esta sea escuchada dentro de Fortnite. Si esas canciones tuvieran que salir de los servidores centrales de Fortnite, para redistribuirse a copias locales del juego, el riesgo de divulgación incontrolada de esas canciones se dispararía, por lo que las productoras de los artistas querrían cubrirse las espaldas renegociando los derechos. También podría optarse por distribuir el juego sin ese contenido licenciado, pero eso ya iría en contra de la iniciativa, que defiende el derecho de los usuarios a quedarse con todo lo que hubieran pagado. Teniendo en cuenta lo anterior, si ya sería complicado redistribuir contenido audiovisual licenciado, quitar elementos funcionales sujetos a licencias puede resultar imposible al impedir la ejecución del juego. Por último, no hay que olvidar la potencia de cálculo. Guardar el estado y la evolución de un MMORPG requiere de mucho procesamiento, almacenamiento y de una red de datos poderosa. Los principales juegos de ese tipo contratan centros de proceso de datos completos. Replicar eso en algo que pudiera montar el usuario en su casa es prácticamente imposible.

CPD
Muchos juegos online se ejecutan en complejos centros de proceso de datos, imposibles de replicar en un domicilio

Luego está el paso del tiempo. A diferencia de una película, que sólo necesita un reproductor adecuado para poder verla, los videojuegos que dependen de servidores necesitan una labor continua, y experta, de mantenimiento que está más allá de las habilidades y conocimientos del común de los mortales. Esperar que cualquiera pudiera mantener en su casa los servidores de juegos tan complejos sería muy poco realista.

El mantenimiento de una arquitectura tan compleja está más allá de las posibilidades del ciudadano medio
El mantenimiento de una arquitectura tan compleja está más allá de las posibilidades del ciudadano medio

Por tanto, aunque la motivación que hay detrás de la iniciativa es perfectamente comprensible, hay muchos factores prácticos que no parecen haber tenido en cuenta y que se está discutiendo ampliamente en los foros de desarrollo de videojuegos. Al menos, esa discusión es sana y quizá alumbre un punto intermedio que sea asumible por todos. 

Algunos están proponiendo, por ejemplo, que los juegos cuenten con un etiquetado que advierta de la dependencia de servidores externos, de manera que el comprador pueda evaluar el riesgo y tomar una decisión informada.

El etiquetado, una posible solución
El etiquetado, una posible solución

Otros están proponiendo un marco de adhesión voluntaria para las desarrolladoras, en las que estás asuman a priori unos compromisos a la hora diseñar el juego, de manera que ofrezca una serie de garantías de preservación a largo plazo. Así, por ejemplo, se podría establecer una primera categoría de juegos que ofreciese modos de juego offline, aunque fuesen limitados, a base de bots, mapas específicos o conexiones locales con otros jugadores; una segunda categoría podría abarcar a juegos que llegasen a a acuerdos con entidades sin ánimo de lucro que se comprometiesen a conservar el juego con determinadas medidas de seguridad; una tercera categoría incluiría a todos aquellos juegos que pudieran ofrecer una instalación en un servidor local del jugador. Al igual que con la propuesta del etiquetado, la categoría se anunciaría junto con el juego, para que sus potenciales compradores pudieran evaluar el riesgo de su adquisición.

La categorización voluntaria puede ser otra solución
La categorización voluntaria puede ser otra solución

En todo caso, aunque la regulación que exige la iniciativa esté dirigida a proteger a los compradores, hay que diseñarla con mucho cuidado para que no suponga un freno a la iniciativa y la creatividad de un mercado tan pujante, económica y culturalmente, como el del videojuego. Determinadas condiciones, que pueden ser perfectamente factibles por el músculo de los grandes estudios, podrían ser inasumibles por los estudios pequeños e indies al disparar los costes y el riesgo legal. Se trata por tanto de proteger a los compradores, pero sin llevarse por delante a los pequeños estudios. Hay que preservar los juegos, pero de una forma que no limite seriamente la producción de nuevos juegos. En caso de asumir esta tarea, los legisladores tendrán que afrontar un equilibrio muy delicado que les va a exigir un conocimiento muy profundo de un mercado complejo y cambiante como es el de los videojuegos.

Equilibrio legislativo para esta iniciativa
Esta iniciativa exige una labor legislativa muy delicada