Cliente GraphQL para Unity
¡Hola! Hoy quiero compartir mi experiencia desarrollando un cliente GraphQL para Unity. Este cliente fue parte de un proyecto que nació durante mis prácticas en Turku Game Lab en Finlandia, pero ya sabéis, a veces no hay tiempo para todo y este proyecto se quedó olvidado en mi portátil durante unos meses. Aunque todavía quiero implementar dicho proyecto, decidí segmentarlo para asegurarme de que todo funcione correctamente una vez que empiece a integrar todas las funcionalidades.
Cómo Empezó Todo
Estaba en una reunión con mi tutor, recién terminado un curso de Unity, y estábamos intercambiando ideas sobre cómo mostrar lo que he aprendido durante mi formación profesional. El detalle es que la mayoría de los estudiantes en prácticas del laboratorio venían de entornos de desarrollo de juegos o gráficos, mientras que yo era el bicho raro con mi FP de desarrollo multiplataforma, el tutor no tenía muy claro qué había aprendido en clase y cómo podíamos aprovechar la experiencia.
Fue entonces cuando se me ocurrió: ¿es posible aprovechar lo que sé en un motor de videojuegos? Aunque quizás no sepa sobre modelado 3D, programación de shaders o las complejidades de la implementación de mecánicas de juego, sí tenía conocimientos sobre bases de datos, APIs REST y arquitecturas cliente-servidor. Así que pensé: "¿No hay un montón de juegos online hoy en día?, ¿podría intentar hacer algo del estilo?"
La Idea: Un Juego de Preguntas en Red
La idea no es nueva ni mucho menos. En ése momento, estaba en Finlandia y para pasar el rato con mis amigos y compañeros de España, jugábamos a Jackbox y algunos juegos de navegador. Inspirado por eso, decidí crear un prototipo de un juego de preguntas en red. Y para esto, GraphQL parecía la opción perfecta. ¿Por qué? Bueno, imaginad poder crear preguntas flexibles y reutilizables que puedas mandar como consultas a una base de datos. Pensad en preguntas como "Nombra Pokémon que puedas encontrar en la Ruta X" o "¿Cuál es la habilidad oculta del Pokémon Y?" - Con GraphQL, estas operaciones solo requerirían una única petición y gestionar esos datos sería mucho más fácil.
Pero eso era solo una parte del puzle. La otra parte era crear un servidor para manejar la gestión de lobbies y salas, permitiendo a los jugadores competir con sus amigos. Era ambicioso, sí, pero me pareció la forma perfecta de mostrar tanto mi experiencia en programación como mis recién adquiridas habilidades en desarrollo de juegos.
El Cliente GraphQL
Entonces, ¿qué construí exactamente? En esencia, es un cliente GraphQL adaptado para Unity. Esto es lo que puede hacer:
- Ejecutar consultas y mutaciones GraphQL.
- Manejar respuestas.
- Cachear resultados para mejorar el rendimiento y seguir las buenas prácticas de uso de esta tecnología.
- Integrarse sin problemas con el sistema MonoBehaviour de Unity.
¿Y qué lo hace interesante? Un Query Builder que te permite construir consultas GraphQL de forma programática. Incluyendo argumentos de entrada y filtros, no fue fácil, pero ya hablaremos más de eso.
Herramientas Utilizadas
Para este proyecto, me sumergí en:
- Unity (¡por supuesto!)
- C#
- GraphQL (aunque era tecnología nueva para mí)
- NSubstitute (para mocking en nuestras pruebas - sí, también llegaremos a eso)
Entrando en el Desarrollo
Vamos a revisar algo de código y ver cómo funciona este cliente GraphQL . Aviso: esta implementación está lejos de ser un proyecto para versión 1.0, pero funciona. En el repositorio hay una escena a modo de demo, os dejó aquí una vista previa:
Y aquí como está configurada:
Como veis, sólo hemos necesitado una lista de nombres de Pokémon, pero esto lo podéis cambiar por un rango de números, o manejarlo como queráis.
Preparando el Escenario
Lo primero era decidir cómo estructurar este paquete. Sabía que quería algo que funcionara bien con el sistema MonoBehaviour de Unity, pero también necesitaba que fuera lo suficientemente flexible para manejar operaciones GraphQL complejas. Así que me decidí por una clase central GraphQLUnityClient
que haría las operaciones principales, con un envoltorio MonoBehaviour GraphQLClientBehaviour
para una fácil integración en las escenas de Unity.
El Query Builder
Hablemos del Query Builder. Esto... fue como intentar resolver un cubo de Rubik por primera vez. La idea era simple: crear una forma de construir consultas GraphQL de manera programática. Pero ya sabéis que la tokenización de cadenas es compleja y por mucho que me gustaría mejorar con Regex, no estaba teniendo mucho éxito.
Aquí tenéis un poco de lo que puede hacer:
var query = new QueryBuilder()
.Operation("GetPokemon")
.Variable("name", "Pikachu", "String!")
.BeginObject("pokemon(name: $name)")
.Field("id")
.Field("name")
.BeginObject("abilities")
.Field("name")
.EndObject()
.EndObject()
.Build();
Se ve bien, ¿verdad? Pero llegar ahí fue una aventura. Pasé días peleándome con la manipulación de cadenas, tratando de averiguar cómo anidar objetos correctamente, manejar variables y asegurarme de que todo estuviera formateado correctamente para GraphQL.
Seré sincero, al final pedí ayuda - sí, me apoyé en algo de IA para pulir detalles. Aunque saber cuándo pedir ayuda también es una habilidad, ¿no?
Pruebas: Mockear o No Mockear
Ahora, pasemos a las pruebas. Aquí es donde las cosas se pusieron interesantes. Después de las pruebas unitarias, estaba listo para crear un servidor local para hacer pruebas. Pero cuando vi los requisitos para desplegar una instancia local de la PokeAPI (nuestro endpoint GraphQL de prueba), me di cuenta de que podría ser excesivo para nuestros requisitos.
Así que, Plan B: mocking. Fue entonces cuando encontré NSubstitute, una biblioteca de mocking para .NET. Ahora, admito que normalmente no soy el mayor fan de las pruebas con mocks. Hay veces en las que acabas codificando demasiadas abstracciones y haces pruebas por por hacer pruebas, sin saber cuáles serían los requisitos y casos límite reales. Pero en este caso, creo que fue la decisión correcta.
Configurar NSubstitute en Unity fue... tedioso. El framework de pruebas de Unity no es precisamente conocido por llevarse bien con bibliotecas de terceros. Pero después de algunas visitas a foros y algunos malabares, lo conseguí.
Aquí tenéis un fragmento de cómo quedaron nuestras pruebas:
[Test]
public void SendQuery_SuccessfulQuery_ReturnsCorrectResponse()
{
// Arrange
var mockWebRequest = Substitute.For<IWebRequest>();
mockWebRequest.SendWebRequestAsync().Returns(Task.FromResult("{\"data\":{\"pokemon\":{\"name\":\"Pikachu\"}}}"));
var client = new GraphQLUnityClient("https://example.com/graphql", null, mockWebRequestFactory);
// Act
var response = await client.SendQueryAsync<PokemonResponse>(query);
// Assert
Assert.AreEqual("Pikachu", response.Data.Pokemon.Name);
}
No es perfecto, pero nos permitió probar el comportamiento de nuestro cliente sin necesidad de tener un servidor GraphQL completo en funcionamiento.
Desafíos y Peculiaridades de Unity
Vale, ya hemos cubierto lo básico de cómo construimos este cliente GraphQL. Ahora, vamos a ver en detalle algunos de los problemas que casi me hicieron descartar el proyecto e ir a por algo, cualquier cosa, más simple.
Desafío #1: El Query Builder
Mencioné antes que el Query Builder estaba un poco fuera de mi liga, pero dejadme que os lo explique. El problema principal no era solo unir algunos campos - era asegurarse de que el constructor pudiera manejar la complejidad de las consultas GraphQL. Estamos hablando de objetos anidados, variables, escalares personalizados, de todo.
Aquí tenéis un fragmento que me dio un par de dolores de cabeza:
public QueryBuilder BeginObject(string name)
{
if (_indent == 0)
{
if (_variableDefinitions.Count > 0)
{
_query.Append($"({string.Join(", ", _variableDefinitions)}) ");
}
_query.AppendLine("{");
}
else
{
if (!_isFirstField)
{
_query.AppendLine();
}
}
_query.AppendLine($"{new string(' ', _indent * 2)}{name} {{");
_indent++;
_isFirstField = true;
hasObject = true;
return this;
}
Parece bastante simple, ¿verdad? Pero asegurarse de que esto funcionara con todos los otros métodos, mantener el control de la indentación y manejar casos límite... digamos que me dio un toque de humildad. Pensaba que era más capaz de lo que realmente soy, después de todo, era yo el que gritaba "¡Regex!" cada vez que teníamos que hablar de validación de cadenas en clase, pero esto ni siquiera es Regex.
Desafío #2: Hacer que Unity y Async Sean Compatibles
Aquí es donde las cosas se pusieron interesantes. Las operaciones GraphQL son inherentemente asíncronas, lo cual no es gran cosa en un entorno C# estándar. Pero ¿Unity? Bueno, Unity tiene sus propias ideas sobre hilos y operaciones asíncronas.
El problema principal era que el sistema de networking de Unity, UnityWebRequest, no soporta de forma nativa el patrón async/await que hace que la programación asíncrona sea mucho más limpia en C# moderno. Pero no iba a dejar que eso me detuviera.
En lugar de recurrir a las corrutinas (que, seamos honestos, pueden volverse un lío difícil de depurar si las operaciones son complejas), decidí extender el WebRequest de Unity para permitir el uso de async/await. Así es como quedó:
using UnityEngine.Networking;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace GraphQL.Unity
{
public static class UnityWebRequestExtensions
{
public static TaskAwaiter GetAwaiter(this UnityWebRequestAsyncOperation asyncOp)
{
var tcs = new TaskCompletionSource<object>();
asyncOp.completed += obj => { tcs.SetResult(null); };
return ((Task)tcs.Task).GetAwaiter();
}
}
}
Este pequeño script nos permite usar async/await con el WebRequest de Unity, haciendo que nuestras operaciones GraphQL sean mucho más limpias y fáciles de manejar. Así es como podemos usarlo:
var getRequest = UnityWebRequest.Get("http://www.example.com/graphql");
await getRequest.SendWebRequest();
var result = getRequest.downloadHandler.text;
Al implementar esta extensión, comseguimos lo mejor de ambos mundos: el robusto sistema de networking de Unity y el patrón async/await limpio y fácil de leer del C# actual. Nos permitió realizar operaciones asíncronas sin bloquear el hilo principal o recurrir al infierno de los callbacks.
Admito que no puedo llevarme todo el mérito de esta idea. Encontré un enfoque similar en un gist de GitHub y lo adapté a nuestras necesidades.
Consideraciones Específicas de Unity
Una cosa que realmente destacó fue lo diferente que es el desarrollo en Unity del desarrollo C# estándar. En un entorno C# regular, podrías crear un singleton o una clase estática para tu cliente GraphQL. Pero en Unity, los MonoBehaviours mandan.
Tuvimos que envolver todo nuestro cliente en un MonoBehaviour:
public class GraphQLClientBehaviour : MonoBehaviour
{
[SerializeField] private string graphQLUrl = "https://api.example.com/graphql";
[SerializeField] private string authToken;
private GraphQLUnityClient _client;
public GraphQLUnityClient Client
{
get
{
if (_client == null)
{
_client = new GraphQLUnityClient(graphQLUrl, authToken, new UnityWebRequestFactory());
}
return _client;
}
}
// ... other methods ...
}
Esto nos permitió agregar fácilmente el cliente a escenas de juego y configurarlo a través del Inspector de Unity. Es un enfoque diferente para la arquitectura de aplicaciones, pero encaja bien con el sistema basado en componentes de Unity.
Mirando Hacia Adelante: ¿Qué Sigue para Nuestro Cliente GraphQL?
Ahora, no me malinterpretéis - me sentí realizado cuando la escena demo que usa el cliente finalmente funcionó. Pero no puedo llamar a esto una versión completa. Aquí hay algunas cosas en mi lista de tareas pendientes:
- Cachear el Esquema de GraphQL: Ahora mismo, estamos navegando a ciegas en cuanto a la estructura de la API GraphQL que estamos consultando. Al cachear el esquema, podríamos proporcionar una mejor comprobación de errores e incluso autocompletado en el Query Builder.
- Separar los Filtros en el Query Builder: Actualmente, nuestros filtros están acoplados con los campos de la consulta. Me gustaría separarlos, haciendo el Query Builder aún más flexible.
- Mejorar la Integración con el Editor de Unity: He visto otros proyectos de la comunidad con esta característica y era ideal, así que estoy deseando implementarla para nuestro cliente.
- Optimizaciones de Rendimiento: Aunque nuestro cliente funciona bien para la mayoría de los casos de uso, siempre hay espacio para la optimización. En particular, me gustaría investigar formas más eficientes de manejar grandes conjuntos de datos.
Lecciones Aprendidas
- Trabajar en Nuevos Entornos: Cuando empecé este proyecto, me sentí un poco como pez fuera del agua. Mi conocimiento de C# no es particularmente fuerte, menos aún cuando hablamos de Unity, pero poder hacer uso de lo que aprendí durante mi formación profesional me demostró que podría tener una oportunidad como desarrollador de videojuegos.
- Flexibilidad Ante Todo: Cuando retomé el proyecto original, recordé la importancia del código reutilizable. No dejaba de pensar en cómo el QueryBuilder podría usarse no solo para nuestra idea de juego de preguntas, sino para todo tipo de mecánicas de juego. ¿Necesitas obtener el inventario de un jugador? Consulta GraphQL. ¿Quieres actualizar las estadísticas de un personaje? Mutación GraphQL. Esto podría facilitar desarrollos en algunos casos.
- Pruebas en el Desarrollo de Juegos: Antes de este proyecto, no había tenido muchas oportunidades de realizar pruebas, así que fue muy positivo tener la oportunidad de hacerlo. Pero vaya, hay tantas cosas que puedes hacer - lidiar con MonoBehaviours, manejar operaciones asíncronas, mockear clases específicas de Unity - solo puedo imaginar cómo podría ser eso en juegos que tardan años en desarrollarse. Me dio una nueva apreciación por la importancia del código testeable, ¡incluso (especialmente!) en el desarrollo de juegos. Y me enseñó que a veces, necesitas ser creativo con tus enfoques de prueba. Puede que el mocking no sea ideal, pero cuando es eso o no hacer pruebas en absoluto, mockea con confianza.
Conclusión
Lo que empezó como una forma de mostrar mis habilidades se convirtió en algo más. Con suerte, tendré tiempo para seguir trabajando en ello. Pero quién sabe, podría moverlo a Godot si acabo moviendo mi proyecto de preguntas a ese motor. En cualquier caso, veo este proyecto como un paso en la dirección correcta, ya que me dio la oportunidad de recontextualizar lo que aprendí en la FP en un entorno de desarrollo de juegos.