sábado, 1 de febrero de 2025

Entre entusiastas y detractores




El uso de la llamada Inteligencia Artificial se está generalizando en el mundo de las marcas y aplicaciones. El tiempo de las versiones Beta a dado paso, masivamente, a las versiones oficiales y no existe hoy en día, una marca que se precie, que no haya incorporado esta tecnología a su buque insignia.

Yo como aficionado a las tecnologías no paro de sorprenderme de la rapidez en su avance y la curiosidad me llama.

Entre entusiastas y detractores no es un artículo que pretende ahondar en la brecha abierta por el uso de estas tecnologías, sino más bien en su buen uso, para el desarrollo de nuevas ideas, provocando el cambio en los modelos de aprendizaje, derribando fronteras y procurando un cambio progresivo y beneficioso al conjunto de la sociedad.

Uno de los grandes miedos hacia esta, podría ser, la dependencia que se crea con su uso generalizado y para ello, nada mejor que el debate abierto y permanente. Una mente lucida, nunca deja en manos de otro, lo que considera importante, al menos sin una correcta supervisión. Y esta, ¿la dejaremos también en manos de la que podiamos llamar "IA vigilante"?.

Para el ejemplo que os traigo he usado Replit, un IDE en la nube que incorpora dos poderosas herramientas basadas en Inteligencia Artificial, Agent y Assistant. No se trata de contar las bonanzas de la aplicación, que sin duda las tiene. Tampoco tengo claro, si lo que hay detrás es unicamente la IA, o un equipo mixto (Humano/Máquina > 1 o Humano/Máquina < 1), así que me gustaría pensar que el merito se debe a su gran equipo humano de desarrollo.

La idea, que no es original pero para mí novedosa y bastante técnica, trata de crear un "input remote" o mando a distancia para un videojuego desarrollado con Unity, utilizando para ello un dispositivo móvil. Como siempre, esto es solo un prototipo de desarrollo, sin animo de lucro, con el foco puesto en el aprendizaje. Todas las herramientas usadas, son solo para uso particular y poder dar una salida a mi inquietud y curiosidad. A continuación os muestro un enlace de YouTube, donde se puede ver en que consiste facilmente Vídeo InputRemote

Las explicaciones y el código que he usado en este desarrollo lo podeis consultar libremente en https://github.com/jmcaneda/MobileJoystickControllerDef

Para llevarla a cabo, he tomado uno de "mis pequejuegos", el que mejor podría adaptarse, desarrollado en Unity.

Vamos a comenzar por a describir el apartado del videojuego con el motor Unity y el código que tiene mas relación con esta idea.

Unity

En la Build Settings

  • Utilizo como plataforma WebGL, que como sabéis significa Web Graphics Library, es una API (interfaz de programación de aplicaciones) utilizada para renderizar gráficos 3D y 2D en navegadores web sin necesidad de plugins y permite a los desarrolladores crear gráficos interactivos y animaciones utilizando JavaScript y el estándar de gráficos OpenGL ES. La mayoria de los navegadores web ya incorporan este framework por defecto.

En Player Settings. Estos parámetros han sido consultados en el manual de Unity https://docs.unity3d.com/2022.3/Documentation/Manual/webgl-deploying.html

  • Compression Format: a Gzip.
  • Data Caching: a check.
  • Decompression  Fallback: a check.

¿Qué nos proporciona la Build?. Las carpetas Build, TemplateData y el archivo index.html. Si comprimimos el conjunto en un archivo .zip y lo subimos a la Plataforma de videojuegos Indie itchio, indicándole que se pueden ejecutar en un navegador web, ya podriamos disfrutar del mismo, utilizando como mando del videojuego, el propio teclado.

Pero..., ¿dónde colocamos todo este código para que podamos ejecutar nuestra aplicación correctamente?. Pues colocamos el resultado de la Build en Unity en la carpeta static del servidor, tal cual, sin modificar nada. ¿Quiere decir esto que si en vez del juego utilizado MaribelProject, lo cambio por otra Build, respetando su ubicación, funcionaría el resto?. La respuesta es si. Podemos reutilizar todo el código y solo, crear nuestro juego y podremos manejar los inputs, desde cualquier dispositivo móvil. Esto es lo fantástico!!!.

Archivos de la Build en Unity


static/Build/
 ├── Builds.data
 ├── Builds.framework.js
 ├── Builds.loader.js
 └── Builds.wasm
 			
static/TemplateData/
├── style.css
└── [assets visuales]
          
index.html

Del código C#, el que mayor interés tiene para la cuestión que estamos tratando es PlayerController.cs, de este extraeremos la parte que nos interesa resaltar. Son la declaración de clases de los diferentes tipos de datos que manejaremos en los diferentes eventos.

void HandleJoystickInput(string jsonData)
En este evento se recibirán los datos x e y del Joystick para el desplazamiento del personaje.
void HandleButtonInput(string jsonData)
En este evento se recibirán los datos state y action, acerca de las teclas pulsadas para el desplazamiento del personaje y visión de cámara.
void HandleInputMethod(string jsonData)
En este evento se recibirá el dato state, sobre el método de input elegido, a off, desde teclado, a on, desde dispositivo móvil.

      
      
      void HandleJoystickInput(string jsonData)
      {
          if (GameManager.instancia.inputRemote)
          {
              JoystickData data = JsonUtility.FromJson(jsonData);
              movement = new Vector3(data.x, 0, data.y) * moveSpeed;

              animator.SetFloat("Horizontal", data.x);
              animator.SetFloat("Vertical", data.y);
          }
      }

      void HandleButtonInput(string jsonData)
      {
          if (GameManager.instancia.inputRemote)
          {
              ButtonData data = JsonUtility.FromJson(jsonData);
              if (data.state == "pressed")
              {
                  switch (data.action)
                  {
                      case "up":
                          {
                              movement = Vector3.forward * moveSpeed;
                              animator.SetFloat("Horizontal", movement.x);
                              animator.SetFloat("Vertical", movement.y);
                              break;
                          }
                      case "down":
                          {
                              movement = Vector3.back * moveSpeed;
                              animator.SetFloat("Horizontal", movement.x);
                              animator.SetFloat("Vertical", movement.y);
                              break;
                          }
                      case "left":
                          {
                              movement = Vector3.left * moveSpeed;
                              animator.SetFloat("Horizontal", movement.x);
                              animator.SetFloat("Vertical", movement.y);
                              break;
                          }
                      case "right":
                          {
                              movement = Vector3.right * moveSpeed;
                              animator.SetFloat("Horizontal", movement.x);
                              animator.SetFloat("Vertical", movement.y);
                              break;
                          }
                      case "A":
                          {
                              break;
                          }
                      case "B":
                          {
                              // Iniciar el desplazamiento a la siguiente esquina
                              GameManager.instancia.indiceEsquinaActual = (GameManager.instancia.indiceEsquinaActual + 1) % GameManager.instancia.esquinas.Length;
                              GameManager.instancia.offset = GameManager.instancia.esquinas[GameManager.instancia.indiceEsquinaActual];
                              break;
                          }
                  }

              }
              else if (data.state == "released")
              {
                  if (data.action != "A" && data.action != "B")
                  {
                      movement = Vector3.zero;
                  }
              }
          }
      }
      
      void HandleInputMethod(string jsonData)
      {
          InputMethodData data = JsonUtility.FromJson(jsonData);
          switch (data.state)
          {
              case "on": GameManager.instancia.inputRemote = true; break;
              case "off": GameManager.instancia.inputRemote = false; break;
          }
      }
      
      [System.Serializable]
      public class JoystickData
      {
          public float x;
          public float y;
      }

      [System.Serializable]
      public class ButtonData
      {
          public string action;
          public string state;
      }

      [System.Serializable]
      public class InputMethodData
      {
          public string state;
      }
    
    

Objeto importante en Unity

En Unity, debe existir el objeto MaribelController y tener como componente, entre otros el script PlayerController.cs

Python y JavaScript

Esta es la parte en la que la asistencia de la IA de Replit, no con pocos intentos en la fase de depuración, facilitó las claves para encaminar este desarrollo, a buen término.

Este proyecto implementa una comunicación bidireccional en tiempo real,       entre un controlador móvil y un juego Unity WebGL, utilizando WebSocket como protocolo de comunicación.


Componentes Principales


Servidor WebSocket (app.py)


Implementado con Flask-SocketIO
Gestiona conexiones y desconexiones
Retransmite eventos entre el controlador y Unity

Cliente Controlador (controller.js)


Implementa la interfaz de usuario del controlador
Captura eventos táctiles y de botones
Emite eventos WebSocket al servidor

Cliente Unity (websocket.js)


Recibe eventos del servidor
Comunica con el juego Unity via SendMessage
Gestiona la conexión WebSocket del lado del juego


Vamos a verlo sobre un diagrama de bloques.



[Dispositivo Móvil] --> (WebSocket) --> [Servidor Flask] --> (WebSocket) --> [Unity WebGL]
     ^                                                                            |
     |                                    Feedback                                |
     +--------------------------------------------------------------------------- +
 			


Partes del código que cabe resaltar



game.html, este es el verdadero contenedor de Unity. Tomamos todo lo que nos interesa del código index.html, de la build de Unity y lo reescribimos en este.


websocket.js, recibe los eventos del servidor y los comunica a Unity vía SendMessage


handleGameControl(data) {
        if (!this.gameInstance) {
            console.warn('Game instance not ready, retrying connection...');
            setTimeout(() => this.socket.connect(), 1000);
            return;
        }

        // Send the control data to Unity
        switch(data.type) {
            case 'joystick':
                this.gameInstance.SendMessage('MaribelController', 'HandleJoystickInput', 
                    JSON.stringify(data.data));
                break;
            case 'button':
                this.gameInstance.SendMessage('MaribelController', 'HandleButtonInput',
                    JSON.stringify(data.data));
                break;
            case 'inputMethod':
                this.gameInstance.SendMessage('MaribelController', 'HandleInputMethod',
                    JSON.stringify(data.data));
                break;
        }
}
        

Importante, se puede observar que SendMessage busca un objeto llamado MaribelController y los métodos HandleJoystickInputHandleButtonInputHandleInputMethod, incluidos en el componente PlayerController.cs


Error en la versión de producción (Deployments)

Cuando ya disponía de todo el código probado en desarrollo, al pasarlo a producción en Replit (Deployments), se producía un error, que pese a estar documentado en parte en los manuales de Unity, no lo estaba para su uso con servidores Flask. Paso a describirlo, junto con la solución adoptada.

"WebAssembly streaming compilation failed! This can happen for example if "Content-Encoding" HTTP header is incorrectly enabled on the server for file /static/Build/Builds.wasm, but the file is not pre-compressed on disk (or vice versa)".

En app.py podrás ver como se ha resuelto este tema y la clave, en el siguiente código.

    @app.route('/static/Build/')
    def serve_build(filename):
        try:
            response = send_from_directory('static/Build', filename)
            if filename.endswith('.wasm'):
                response.headers['Content-Type'] = 'application/wasm'
                response.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'
                response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
                response.headers['Cross-Origin-Resource-Policy'] = 'cross-origin'
                response.headers['Cache-Control'] = 'no-cache'
                response.headers['Accept-Ranges'] = 'bytes'
                # Asegurarse de que no haya compresión
                response.direct_passthrough = True
                if 'Content-Encoding' in response.headers:
                    del response.headers['Content-Encoding']
                if 'Content-Length' in response.headers:
                    del response.headers['Content-Length']
            return response
        except Exception as e:
            app.logger.error(f'Error serving {filename}: {str(e)}')
            return str(e), 500
      

Técnica empleada y herramientas adicionales🔧

Asset Store 🏬

He utilizado los siguientes packages de Asset Store, casi siempre en su versión free o con mínima inversión.



Links de interés 🔗

He utilizado los sonidos de nuestro querido "Chiquito de la calzada".

El juego original, sin input remote, en WebGL y teclado, podéis probarlo sin problemas. Este es un videojuego regalo y me sirve como base para el desarrollo de este artículo. Gracias Maribel.

Elementos necesarios 🔑

  • La app requiere un dispositivo móvil, con capacidad para el escaneo de código QR y navegador web, compatible o habilitado para JavaScript (la mayoría ya lo tienen configurado por defecto).
  • La aplicación comenzará con el linkado desde el navegador web, compatible con WebGL (la mayoría ya lo son por defecto). Recomiendo que se realice desde equipo de sobremesa o portátil.
  • Desde el dispositivo móvil escanearemos el código QR y dispondremos el mismo en apaisado. Es importante que veamos el "connected" en verde, esto es indicativo que podemos hacer uso del dispositivo.
  • El foco debe estar puesto en el navegador, esto lo podemos conseguir posicionando el ratón en el mismo. Cambiaremos el input mode, en el dispositivo móvil, de Off a On y ya está, podremos manejar a nuestro personaje "Maribel", en su recorrido por las calles, escuchando los mensajes y procurando no ser convertida por los "UrbanZombies", de dos maneras: como Joystick o por medio de los botones de desplazamiento. El botón "B", esta programado para el cambio de posición de la cámara.
  • Requiere de una conexión a internet.

_

C.A.C. (Soho Málaga), supuso para nosotros (los de entonces), un lugar de encuentro.


0 comments:

Publicar un comentario