ATopeCode
Blog sobre Desarrollo de Software

martes, 16 de agosto de 2016

Programando juegos retro en Unity 3D

Si eres desarrollador de videojuegos de la 'vieja' escuela, has usado XNA, MONOGAME, SDL... y quieres programar videojuegos 2D en  Unity 3D, el primer inconveniente que encontrarás será que Unity es un motor 3D, aún cuando el proyecto sea 2D.
Cuando creas un proyecto 2D, Unity lo que hace es crear un proyecto 3D pero con cámara ortográfica, además de añadir varios componentes para trabajo en 2D, pero el motor del juego sigue siendo 3D.

Esto lo que produce es que nuestros 'sprites' o 'texturas' no se visualicen en pantalla en la posiciones y tamaños que nosotros queramos. Y tampoco tenemos la opción de seleccionar una resolución por defecto (320x200, 640x480...) para nuestro juego y adaptar el tamaño de los sprites a dicha resolución.



Otro incoveniente que encuentro, si eres un programador de la vieja escuela es que ahora se acabó el fijar los FPS del juego.  Es decir, antes, al final de cada ciclo de juego, se espera 'x' tiempo hasta que dicho ciclo durase el tiempo que le correspondía según los FPS para los que se diseñaba.
Esto evitaba que en ordenadores muy rápidos el juego fuera a toda leche. Lo que se consigue es que el juego vaya a la misma velocidad en todos los equipos, lógicamente, si el ordenador es muy lento no hay nada que hacer y el juego se verá relentizado.
Hoy en día se ha cambiado esta técnica por no utilizar la 'espera' al final de cada ciclo de juego. Lo que se hace es que el juego va a su máxima velocidad en todos los equipos y el movimiento de los 'Sprites' y demás cálculos se hacen en función del 'deltaTime' o tiempo (segundos) transcurrido desde el último ciclo de juego hasta el actual. Si sabes que tu sprite se moverá 'x' puntos en pantalla cada 1 segundo, solo tienes que multiplicar 'x * deltaTime' para saber el desplazamiento que tendrá en cada ciclo de juego.

Al utilizar el 'deltaTime' en vez de ajustar los Frames del juego provoca que el juego se vea igual de rápido en ordenadores potentes, pero en ordenadores que no dan la talla el movimiento se hace a saltos, como pasa más tiempo entre cada ciclo de juego, la distancia recorrida en cada ciclo es mayor y el sprite pasa de estar en una posición a otra bastante alejada sin que se vea el recorrido intermedio. Este problema casi no es apreciable en juegos 3D, pero en un juego 2D si que se nota.
Por eso para mi gusto, perfiero seguir programando con unos FPS fijos.
Si el juego va lento pues va lento, como pasó toda la vida, o puede que vaya bien y que en determinado momento se relentice por que hay más sprites en pantalla por ejemplo, pero se sabe que es por eso y que la máquina no dá para más. En el otro caso el juego parece que va bien pero notas algo raro y el jugador puede pensar que el juego es así siempre, en vez de darse cuenta que su equipo no es suficientemente potente.

Con la clase 'PixelPerfectCamera' se pueden seleccionar los FPS (Frames por segundo) para nuestro juego.
Si empezamos a ver que el juego va lento entonces hay dos opciones, o bajamos los FPS del juego (será más lento y menos suave en los movimientos) o nos toca revisar el código y optimizarlo para ahorrar milisegundos en cada ciclo de juego.

Es la principal diferencia entre programar juegos o aplicaciones, en los juegos hay que pensar que cada acción posiblemente vaya a repetirse en cada ciclo del juego y según como esté programada puede afectar al funcionamiento final del programa.


PixelPerfectCamer.cs: 

Enlace a repositorio en GitHub.


Una clase 'Behavior' para añadir al gameObject de la 'Camara' de la Escena Unity.
Dándole valor a los campos del Behavior desde la escena unity se consigue obtener una resolución de pantalla que coincide con la resolución virtual del juego, es decir, si decidimos desarrollar nuestro juego en una resolución 320x200 el resultado en pantalla del juego será equivalente a cuando antiguamente se cambiaba la resolución de la tarjeta gráfica a 320x200.

El resultado visual es el mismo, lo que pasa es que nuestro dispositivo (pc, móvil...) mantiene su resolución pero la imagen del juego es 'escalada' en cada frame por Unity para que ocupe toda la pantalla.

Se obtiene 'PixelPerfect', es decir, se hace coincidir el tamaño de un punto o pixel de nuestros Sprites con el tamaño de 1 punto o pixel de nuestra 'resolución virtual' en pantalla. Para que se entienda mejor, si nuestra resolución virtual es de 320x200 y usamos en nuestro juego un sprite de ancho 320, al centrarlo en pantalla ocupará todo el ancho de la misma.

NOTA.- Para todas las texturas utilizadas en el juego, el campo 'PixelsPerUnits' debe conservar su valor por defecto '100'.

Con esta idea en cabeza el diseño de juegos se hace como se hacía antiguamente, se diseña el juego para una resolución de pantalla específica (320x200, 640x480 ...) que será nuestra 'resolución virtual' para poder saber de que tamaño deben ser los Sprites del juego o Tiles.

Además provee varios métodos para conversión de posiciones o coordenadas, ya que las posiciones de Unity son de tipo 'float' y las de nuestra resolución virtual son tipo 'int'.
Si queremos hacer referencia a la posición (10,20) de nuestra 'resolución virtual' 320x200 por ejemplo, la posición 10 en el eje 'x' de unity no coincidirá con el punto/pixel nº10 de nuestra resolución virtual. Por defecto en Unity la posición 10 de nuestra resolución será la posición 0.10 y viceversa.
Utilizando los método de conversión de posiciones, en este caso:

Vector3 posicion = new Vector3(10,20,0);
Vector3 posicionUnity = Position2DToWorld(posicion);

Así conseguimos convertir las posiciones y desarrollar nuestro juego con una metodología 'PixelPerfect'.

-La clase 'PixelPerfectCamera.cs' se encarga de adaptar el tamaño de la cámara para que el juego se vea en pantalla según la 'resolución virtual' que hemos indicado y establece los FPS del juego.

-La clase 'AspectUtility.cs' la saqué de un foro de Unity y la adapté para que utilice los campos que indican la resolución virtual en la clase 'PixelPerfectCamera.cs'. Lo que hace esta clase es evitar que la imagen de nuestro juego se deforme en pantalla manteniendo el aspect ratio de nuestra resolución virtual. Para ello modifica el 'viewPort' de nuestra cámara principal a un tamaño que conserve una relación de aspecto (aspectRatio) igual a nuestra resolución virtual, así nuestro juego no se verá en toda la pantalla, sino solo en un rectángulo cuyas proporciones ancho*alto sean iguales a nuestra resolución virtual. Cuando el motor de Unity muestre nuestro juego en pantalla, lo escalará a las dimensiones del 'viewPort' evitando que se deforme (más ancho o alta) la imagen.
Por último esta clase crea otra cámara con un 'viewPort' que ocupa toda la pantalla y con color de relleno negro. La coloca detrás de la cámara principal en la escena unity y lo que consigue es el efecto de los 2 rectángulos negros en el borde de la pantalla que cubren la superficie sin imagen cuando vemos juegos o películas que no se visualizan en toda la superficie de la pantalla.


Clase PixelPerfectCamera.cs:
using UnityEngine;
using System.Collections;

public class PixelPerfectCamera : MonoBehaviour 
{ 
 public static int gameFps = 50; //Frames por segundo del juego.
 public static float texturesSize = 100f; //Es el valor de la propiedad 'Pixels to Units' de las imagenes que cargo en Unity.
 public static int myWidthResolution = 320;
 public static int myHeightResolution = 200;
 public int GameFps;
 public int virtualWidthResolution; //Variable para mostrar el valor en el 'Inspector' de Unity.
 public int virtualHeightResolution; //Variable para mostrar el valor en el 'Inspector' de Unity.
 public float virtualAspectRatio; //Variable para mostrar el valor en el 'Inspector' de Unity.
 public float orthograpicCameraSize;

 public static float unitsPerPixel;

 public PixelPerfectCamera()
 {
        virtualWidthResolution = myWidthResolution;
        virtualHeightResolution = myHeightResolution;

        unitsPerPixel = 1f / texturesSize;
 }

 void Awake()
 {
  //FrameRate (Frames por segundo del juego):
  QualitySettings.vSyncCount = 0;
  Application.targetFrameRate = gameFps;
  GameFps = gameFps;
 }

 // Use this for initialization
 void Start ()
 {
        virtualWidthResolution = myWidthResolution;
        virtualHeightResolution = myHeightResolution;

  //El tamaño de la camara ortografica es el mismo que el del FrameBuffer donde se dibuja la escena y luego
  //este se dibuja en memoria de video (pantalla). En vez de cambiar la resolucion del dispositivo destino,
  //Unity lo que se hace es "escalar" el frameBuffer (altoxancho o resolucion) a la resolucion actual del dispositivo.
  //Asi la imagen del juego siempre ocupara toda la pantalla, el problema es que dependiendo de la relacion de aspecto
  //(ancho/alto) de la resolucion destino y la resolucion con la que se programa el juego, si hay mucha diferencia entre
  //ambas, la imagen final en pantalla puede verse distorsionada respecto al ancho y alto. Para solucionar esto se
  //utliza el script 'AspectRatio' que cambia el tamaño del 'ViewPort' (region de pantalla sobre la que se va a copiar o
  //visualizar el frameBuffer (camera) añadiendo lineas negras a los lados o arriba abajo para simular que el dispositivo
  //destino tiene una resolucion con un aspect ratio igual al de la resolucion con la que se programo el juego (tamaño de camara).
  //Asi al escalar la imagen del frameBuffer se evita la deformacion, ya que la relacion entre el ancho y el alto de ambas
  //resoluciones es la misma, solo varian sus valores.


  //Se establece el tamaño de la camara ortografica (alto) y su ancho (aspectRatio) para que coincida con el nº de pixeles
  //de la resolucion con la que se programa el juego (resolucion virtual).
  //El escalado de la resolucion virtual (backBuffer) a la resolucion actual de la pantalla se hace de forma automatica
  //por Unity al ejecutar el juego. En Escritorio se escala al tamaño total de la resolucion actual de la pantalla, en Windows 8
  //se escala manteniendo la proporcion de aspectRatio entre la resolucion virtual y la de la pantalla (cambia automaticamente 
  //el tamaño del viewport en pantalla).
  Camera.main.orthographicSize = (myHeightResolution / 2f) * unitsPerPixel;
  Camera.main.aspect = (float)myWidthResolution / myHeightResolution;

  orthograpicCameraSize = Camera.main.orthographicSize;
  virtualAspectRatio = Camera.main.aspect;
 }

 void Update()
 {

 }
 
 /// 
 /// Convierte una posicion 2D segun la 'resolucion virtual' del juego en una posicion de la 'Camara' o 'World'.
 /// 
 /// Posicion de Camara o Mundo.
 /// Posicion 2D segun resolucion virtual.
 public static Vector3 Position2DToWorld(Vector3 position2D)
 {
  //Las posiciones 2D de la 'resolucion virtual' del juego tienen el punto (0,0) en el centro de la pantalla y son
  //enteras, cada coordenada de apunta a un punto/pixel de pantalla. Son las coordenadas comunes a todos los objetos
        //del juego para definir su posicion en la 'resolucion virtual' en la que se programa el juego, y asi después el motor
        //Unity escala toda la imagen renderizada en cada frame de juego (FrameBuffer o BackBuffer) a las dimensiones de la
        //'resolución real' de la pantalla del dispositivo donde se está ejecutando el juego.
  position2D.x *= unitsPerPixel;
  position2D.y *= unitsPerPixel;

  return position2D;
 }

 /// 
 /// Convierte una posicion del 'Mundo' o 'Camara' en una posicion 2D de la 'resolucion virtual' del juego.
 /// 
 /// Posicion 2D segun resolucion virtual.
 /// Posicion de Camara o Mundo.
 public static Vector3 WorldTo2DPosition(Vector3 positionWorld)
 {
  //Las posiciones del 'Mundo' o 'Camara' tiene el punto (0,0) en el centro de pantalla pero son decimales.
  //Su valor varia en funcion del valor 'Pixels to Units' de cada Imagen cargada y del tamaño (size) de la 'Camara'
  //de la Escena activa.
        //Las utilizan los 'GameObjects' y 'Sprites' en su componente 'transform' para indicar su posicion en pantalla.
  positionWorld.x *= texturesSize;
  positionWorld.y *= texturesSize;

  /*Es lo mismo:
  positionWorld.x /= unitsPerPixel;
  positionWorld.y /= unitsPerPixel;*/

  return positionWorld;
 }

 /// 
 /// Convierte una posicion de 'Pantalla' (raton) en una posicion 2D de la 'resolucion virtual' del juego.
 /// 
 /// Posicion 2D segun resolucion virtual.
 /// Posicion de Pantalla.
 public static Vector3 ScreenTo2DPosition(Vector3 positionScreen)
 {
  //Las posiciones de 'Pantalla' tienen el punto (0,0) en la esquina inferior-izquierda de la pantalla.
  //Son enteras y utilizan la resolucion actual de la pantalla (no la virtual del juego).
        //Las utiliza el raton y la pantalla táctil (Input.mousePosition, Input.Touch) para indicar la posicion en pantalla.

  positionScreen = Camera.main.ScreenToWorldPoint (positionScreen);
  positionScreen = WorldTo2DPosition (positionScreen);

  return positionScreen;
 }

 /// 
 /// Convierte una posicion 2D de la 'resolucion virtual' del juego en una posicion de 'Pantalla' (raton).
 /// 
 /// Posicion de Pantalla.
 /// Posicion 2D segun resolucion virtual.
 public static Vector3 Position2DToScreen(Vector3 position2D)
 {
  position2D = Position2DToWorld (position2D);
  position2D = Camera.main.WorldToScreenPoint (position2D);

  return position2D;
 }

    /// 
    /// Este metodo convierte una posicion 2D de la 'resolucion virtual' del juego en una posicion de 'ViewPort'
    /// (GuiText, GuiTexture).
    /// 
    /// The D to view port.
    /// Position2 d.
    public static Vector3 Position2DToViewPort(Vector3 position2D)
    {
        //Las coordenadas ViewPort tienen el punto (0,0) en la esquina inferior izquierda de la pantalla y van de 0 a 1 porque
        //son relativas al tamaño del ViewPort de la Camara (Camera.Rect).
        //Las utilizan los gameObjects 'GuiText' y 'GuiTexture' en su componente 'transform' para indicar la posicion en pantalla.
        //A partir de la versión 4.6 de Unity se cambió la gestión de UI y los objetos GuiText y GuiTexture ya no se pueden
        //añadir desde el inspector de Unity (pero si por código) y se crearon los contenedores (Canvas...) para posicionar
        //los nuevos elementos de interfaz de usuario.

        float xViewPort, yViewPort;
        xViewPort=(float)position2D.x / PixelPerfectCamera.myWidthResolution;
        yViewPort=(float)position2D.y / PixelPerfectCamera.myHeightResolution;
        
        //Para que el punto (0,0) de 2D esté en el centro de la pantalla en coordenadas ViewPort:
        xViewPort+=0.5f;
        yViewPort+=0.5f;
        
        Vector3 posicionViewPort = new Vector3(xViewPort, yViewPort, position2D.z);
        return posicionViewPort;
    }

    public static Vector3 ViewPortTo2DPosition(Vector3 positionViewPort)
    {
        int x2D, y2D;

        x2D = (int)positionViewPort.x * PixelPerfectCamera.myWidthResolution;
        y2D = (int)positionViewPort.y * PixelPerfectCamera.myHeightResolution;

        //Para que la posicion 2D tenga el punto (0,0) en el centro de la pantalla:
        x2D -= PixelPerfectCamera.myWidthResolution / 2;
        y2D -= PixelPerfectCamera.myHeightResolution / 2;

        Vector3 position2D = new Vector3(x2D, y2D, positionViewPort.z);
        return position2D;
    }

    public static Vector3 Position2DToGUIRect(Vector3 position2D)
    {
        //Cuando se utilizan los métodos estáticos de la clase 'GUI' para dibujar controles de interfaz de usuario (Texture2D, Label...)
        //la posición se define mediante una estructura de tipo 'Rect', que toman como punto (0,0) la esquina superior-izquierda
        //de la pantalla y son siempre enteros positivos (2D normal de toda la vida). El problema es que este sistema de coordenadas
        //funciona sobre la resolución real del dispositivo sobre el que se ejecuta el juego, y no tiene en cuenta el tamaño del ViewPort
        //de la cámara. Para ello hay que realizar unos cálculos adicionales.

        Vector3 guiRectPosition = new Vector3();
        Vector3 ptoSupIzq = new Vector3(-(PixelPerfectCamera.myWidthResolution / 2), (PixelPerfectCamera.myHeightResolution / 2), position2D.z);

        //Cambio de eje de coordenadas.
        guiRectPosition.x = position2D.x - ptoSupIzq.x;
        guiRectPosition.y = ptoSupIzq.y - position2D.y;
        guiRectPosition.z = position2D.z;
        
        //Teniendo en cuenta la relación entre la resolución real y la virtual.
        guiRectPosition.x = guiRectPosition.x * ((float)Screen.width / PixelPerfectCamera.myWidthResolution);
        guiRectPosition.y = guiRectPosition.y * ((float)Screen.height / PixelPerfectCamera.myHeightResolution);
        
        //Teniendo en cuenta el tamaño del ViewPort de la cámara (AspectRatio). Se suman a cada coordenada el ancho/alto de los posibles
        //rectángulos negros que se usan para limitar el ViewPort en pantalla.
        guiRectPosition.x = guiRectPosition.x + (Camera.main.rect.x * Screen.width);
        guiRectPosition.y = guiRectPosition.y + (Camera.main.rect.y * Screen.height);

        return guiRectPosition;
    }

    public static Vector3 GUIRectTo2DPosition(Vector3 guiRectPosition)
    {
        Vector3 pos2D = new Vector3();
        Vector2 ptoCentroGuiRect = new Vector2((PixelPerfectCamera.myWidthResolution / 2), (PixelPerfectCamera.myHeightResolution / 2));

        //Teniendo en cuenta el ViewPort de la Camara (Aspect Ratio).
        //Nota.- Hay que realizar antes el cálculo para el ViewPort en vez del cambio de resolución porque en Unity el tamaño
        //de los rectángulos que limitan el aspectRatio de la Pantalla miden su tamaño en puntos de la 'resolución real', no de
        //la 'resolución virtual' del juego.
        guiRectPosition.x = guiRectPosition.x - (Camera.main.rect.x * Screen.width);
        guiRectPosition.y = guiRectPosition.y - (Camera.main.rect.y * Screen.height);

        //Teniendo en cuenta la relación entre la resolución real y la virtual.
        guiRectPosition.x = guiRectPosition.x * ((float)PixelPerfectCamera.myWidthResolution / Screen.width);
        guiRectPosition.y = guiRectPosition.y * ((float)PixelPerfectCamera.myHeightResolution / Screen.height);

        //Cambio de eje de coordenadas.
        pos2D.x = guiRectPosition.x - ptoCentroGuiRect.x;
        pos2D.y = ptoCentroGuiRect.y - guiRectPosition.y;
        pos2D.z = guiRectPosition.z;

        return pos2D;
    }

}

Clase AspectUtility.cs:
// Más información sobre este archivo y su uso:
// http://wiki.unity3d.com/index.php?title=AspectRatioEnforcer

using UnityEngine;

public class AspectUtility : MonoBehaviour 
{
 
 public float _wantedAspectRatio; //Variable para mostrar el valor en el 'Inspector' de Unity.
 public float _currentAspectRatio; //Variable para mostrar el valor en el 'Inspector' de Unity.
 static private float wantedAspectRatio;
 static private Camera cam;
 static private Camera backgroundCam;
 public float viewPortWidth = 1f, viewPortHeight = 1f; //Variables para mostrar el valor en el 'Inspector' de Unity.
 
 public void Awake () {
  cam = GetComponent();
  if (!cam) {
   cam = Camera.main;
  }
  if (!cam) {
   Debug.LogError ("No camera available");
   return;
  }

  //Se usa la resolucion establecida en la clase 'PixelPerfectCamera' para calcular el Aspect Ratio deseado.
  //La resolucion establecida en 'PixelPerfectCamera' es con la que yo programo el juego (resolucion virtual).
  //Luego esta la resolucion real, que es la que tiene la tarjeta de video del dispositivo donde se ejecuta
  //el juego en ese momento.
  _wantedAspectRatio = (float) PixelPerfectCamera.myWidthResolution / PixelPerfectCamera.myHeightResolution;
  wantedAspectRatio = _wantedAspectRatio;
  _currentAspectRatio = (float)Screen.width / Screen.height;
  SetCamera();

  viewPortWidth = cam.rect.width;
  viewPortHeight = cam.rect.height;
 }
 
 public static void SetCamera () 
 {
  float currentAspectRatio = (float)Screen.width / Screen.height;
  // If the current aspect ratio is already approximately equal to the desired aspect ratio,
  // use a full-screen Rect (in case it was set to something else previously)
  if ((int)(currentAspectRatio * 100) / 100.0f == (int)(wantedAspectRatio * 100) / 100.0f) 
  {
   cam.rect = new Rect(0.0f, 0.0f, 1.0f, 1.0f);
   if (backgroundCam) 
   {
    Destroy(backgroundCam.gameObject);
   }
   return;
  }

  // Pillarbox
  if (currentAspectRatio > wantedAspectRatio) 
  {//La 'resolucion real' de la pantalla es mas ancha que la 'resolucion virtual' del juego. Se cambia el tamaño
   //del ViewPort para que su ancho sea mas pequeño y tenga el mismo aspect ratio (ancho/alto) que la 'resolucion virtual'
   //del juego, asi cuando Unity escale la imagen a mostrar en pantalla, como la escala dentro del ViewPort, la relacion
   //ancho/alto (aspectRatio) se mantiene y los graficos no estaran deformados.
   float inset = 1.0f - wantedAspectRatio/currentAspectRatio;
   cam.rect = new Rect(inset/2, 0.0f, 1.0f-inset, 1.0f);
  }

  // Letterbox
  else 
  {//La 'resolucion real' de la pantalla es mas alta que la 'resolucion virtual' del juego. Se cambia el tamaño
   //del ViewPort para que su alto sea mas pequeño y tenga el mismo aspect ratio (ancho/alto) que la 'resolucion virtual'
   //del juego, asi cuando Unity escale la imagen a mostrar en pantalla, como la escala dentro del ViewPort, la relacion
   //ancho/alto (aspectRatio) se mantiene y los graficos no estaran deformados.
   float inset = 1.0f - currentAspectRatio/wantedAspectRatio;
   cam.rect = new Rect(0.0f, inset/2, 1.0f, 1.0f-inset);
  }

  if (!backgroundCam) 
  {
   // Make a new camera behind the normal camera which displays black; otherwise the unused space is undefined
   backgroundCam = new GameObject("BackgroundCam", typeof(Camera)).GetComponent();
   backgroundCam.depth = int.MinValue;
   backgroundCam.clearFlags = CameraClearFlags.SolidColor;
   backgroundCam.backgroundColor = Color.black;
   backgroundCam.cullingMask = 0; //No dibuja ningún 'Layer'. Los 'gameObjects' se asignan a un 'Layer' y cada camara dibuja los gameObjects de determinados 'Layers' (por defecto de todos).
  }
 }
 
 public static int screenHeight 
 {
  get 
  {
   return (int)(Screen.height * cam.rect.height);
  }
 }
 
 public static int screenWidth 
 {
  get 
  {
   return (int)(Screen.width * cam.rect.width);
  }
 }
 
 public static int xOffset 
 {
  get 
  {
   return (int)(Screen.width * cam.rect.x);
  }
 }
 
 public static int yOffset 
 {
  get 
  {
   return (int)(Screen.height * cam.rect.y);
  }
 }
 
 public static Rect screenRect 
 {
  get 
  {
   return new Rect(cam.rect.x * Screen.width, cam.rect.y * Screen.height, cam.rect.width * Screen.width, cam.rect.height * Screen.height);
  }
 }
 
 public static Vector3 mousePosition 
 {
  get 
  {
   Vector3 mousePos = Input.mousePosition;
   mousePos.y -= (int)(cam.rect.y * Screen.height);
   mousePos.x -= (int)(cam.rect.x * Screen.width);
   return mousePos;
  }
 }
 
 public static Vector2 guiMousePosition 
 {
  get 
  {
   Vector2 mousePos = Input.mousePosition;
   mousePos.y = Mathf.Clamp(mousePos.y, cam.rect.y * Screen.height, cam.rect.y * Screen.height + cam.rect.height * Screen.height);
   mousePos.x = Mathf.Clamp(mousePos.x, cam.rect.x * Screen.width, cam.rect.x * Screen.width + cam.rect.width * Screen.width);
   return mousePos;
  }
 }
}

No hay comentarios:

Publicar un comentario