Vistas de página en total

martes, 13 de diciembre de 2016

Opciones para el desarrollo de aplicaciones para móviles

Este post me gustaría que fuera bastante dinámico, es decir, que se fuera actualizando con el tiempo, ya que estas tecnologías que voy a tratar se actualizan con bastante frecuencia y además yo no poseo la verdad absoluta sobre estos temas, ni mucho menos.

Consideraré las dos plataformas más extendidas actualmente: iOS y Android.

Desde mi punto de vista existen cinco formas de desarrollar aplicaciones para móviles:


  • Native-Native
  • Cross compiled
  • Web based
  • Canvas
  • Runtime javascript

Cada una tiene uno o más productos/frameworks alternativos, y cada una tiene sus ventajas y por supuesto sus inconvenientes. Veamos brevemente cada una de ellas.

Native-Native

El desarrollo se realiza de forma nativa en el propio lenguaje (o lenguajes) nativos de la plataforma.
En iOS se utiliza Objective-C o bien Swift como lenguajes de programación. En Android se utiliza Java como lenguaje de programación.
Ventajas:
  • Acceso total al API de cada plataforma, sin limitaciones
  • Mucha documentación, cursos, libros, videos, etc
  • Herramientas de desarrollo muy avanzadas (XCode para iOS, Android Studio para Android)
  • Máximo rendimiento de las aplicaciones
  • Interfaz de usuario propio de cada plataforma
Inconvenientes:
  • No hay una base de código única, necesitamos una base de código para cada plataforma. Hay que mantener dos versiones de la misma aplicación
  • Es bastante complejo el aprendizaje
  • Depuración lenta en emulador o simulador, ya que necesita compilación

Cross compiled

Se desarrolla en un único lenguaje de programación que es "traducido" a la plataforma nativa.
El ejemplo más popular es Xamarin (ahora de Microsoft), que utiliza el lenguaje C#.
Ventajas:
  • Un único lenguaje de programación para todas las plataformas
Inconvenientes:
  • En cada plataforma tenemos que hacer desarrollos específicos (aunque son con el mismo lenguaje de programación y con algunas parte de código común)
  • El rendimiento no es muy bueno, comparado con el desarrollo nativo
  • Depuración lenta en emulador o simulador, ya que necesita compilación

Web based

Se desarrolla la aplicación utilizando tecnologías propias de la web: HTML, Javascript y CSS.
La aplicación se ejecuta en una web view, es decir, la propia aplicación incluye un navegador en el cual se ejecuta la aplicación.
Los exponentes más conocidos de este tipo de solución son: PhoneGap, Apache Cordova.
Ventajas:
  • Se aprovechan los conocimientos de desarrollo web
  • Se pueden utilizar frameworks js o css: angular, bootstrap
  • Se utiliza el DOM
  • Interfaz de usuario web conocido por el usuario
  • Base de código única para todas las plataformas
  • Depuración ágil, ya que no necesita compilación
Inconvenientes:
  • Bajo rendimiento
  • No hay acceso a todas las funcionalidades de cada plataforma
  • Es muy difícil realizar interacciones complejas (componentes avanzados o bien interacciones como swipe etc)

Canvas

Esta solución es la utilizada por Corona SDK, aunque también la utilizan Unity y Unreal (dos plataformas para desarrollo de juegos 3D y 2D). Consiste en dibujar directamente sobre un canvas vacío todos los objetos y widgets de la aplicación, no utilizando los widgets de la plataforma. Se utiliza OpenGL. Así por ejemplo, no se utiliza el botón propio de la plataforma, sino que se dibuja el botón en el canvas.
Ventajas:
  • Muy adecuado para el desarrollo de juegos (aunque también muy válida para desarrollo de aplicaciones)
  • Base código única para todas las plataformas
  • Mismo lenguaje de programación para todas las plataformas
Inconvenientes:
  • La interfaz de usuario no tiene el aspecto propio de cada plataforma (el aspecto es el mismo en todas las plataformas)
  • Depuración rápida en simulador

Runtime Javascript

Ésta es probablemente la tecnología más novedosa y no debe confundirse con Web based, porque a pesar de que se utiliza javascript como lenguaje de programación, no tiene nada que ver con la web.
En este caso, la aplicación incluye un motor javascript incorporado (V8 en Android y Javascript Core en iOS). El motor ejecuta el código javascript directamente y hace llamadas a la plataforma nativa cuando es necesario. No utiliza el DOM, ya que no incluye una web view.
Los frameworks más conocidos son: React Native, Appcelerator Titanium y NativeScript.
Ventajas:
  • Base de código única para todas las plataformas
  • Lenguaje de programación único, muy conocido
  • Aspecto de la interfaz de usuario propio de cada plataforma (se utilizan los widgets nativos de cada plataforma)
  • Desarrollo muy ágil debido a que se recarga el javascript (no es necesario recompilar)
Inconvenientes:
  • Rendimiento aproximadamente un 10% peor que el desarrollo nativo
  • No hay entornos de desarrollo muy avanzados para estas soluciones

jueves, 29 de septiembre de 2016

Escalado adaptativo

Antes de nada, quiero avisar que esta entrada es un tema relativamente avanzado y que puede ser de poca utilidad, ya que voy a describir un modo de escalado complejo de utilizar y que no es muy utilizado en la práctica. Si quieres, puedes saltarte este post y pasar al siguiente.

Corona soporta varios modos de escalado para pasar lo que nosotros hemos "dibujado" en la pantalla virtual a la pantalla física del dispositivo:
  • letterbox
  • zoomEven
  • zoomStretch
  • none
Ya vimos en una entrada anterior qué modos son más convenientes tanto para juegos como para aplicaciones.
No hace mucho, Corona introdujo un nuevo modo, el modo adaptive, que es adecuado para aplicaciones (este modo no tiene mucho sentido para los juegos). Vamos a verlo un poco en detalle.

Escalado simple

La forma más inmediata de hacer escalado es definir un tamaño fijo de pantalla (ancho x alto) en pixels virtuales, y situar nuestros objetos, texto e imágenes en esa pantalla virtual. Después se especifica un modo de escalado, por ejemplo "letterbox", que indica a Corona cómo rellenar la pantalla del dispositivo físico a partir del contenido de la pantalla virtual. El tamaño de la pantalla virtual lo podemos obtener con el API de Corona:
  • display.contentWidth
  • display.contentHeight
Esto es lo que ya habíamos visto en anteriores posts.

Tamaño de la pantalla virtual cambiante

El escalado simple funciona bien en muchas situaciones (por ejemplo en juegos), pero no es la solución en absolutamente todos los casos. El escalado simple tiene algunas desventajas:
  • La relación de aspecto es fija (sea cual sea el dispositivo final), lo cual provoca franjas en los lados, como ya hemos visto
  • Puede ocurrir que el escalado al dispositivo real no sea un número entero. Por ejemplo, si la pantalla virtual tiene un ancho de 320 y el dispositivo real tiene un ancho de 480, entonces cada pixel de la pantalla virtual tiene que traducirse a 1.5 pixels de la pantalla física. Esto lo hace Corona bastante bien, pero en algunas imágenes pueden notarse ciertas imperfecciones.
  • Los widgets (botones, combos, etc) se verán escalados igual que el resto de los objetos. Lo cual provocará que en dispositivos como tablets veamos botones excesivamente grandes. Lo razonable sería que el tamaño de ciertos widgets fuera similar en todos los dispositivos, que no se escalaran por igual.
Estos problemas afectan fundamentalmente a las aplicaciones (no tanto a los juegos).
Una solución a estos problemas es que el tamaño de la pantalla virtual cambie en función del dispositivo. Esto es, que el tamaño de la pantalla virtual que definimos en config.lua tenga ancho y alto en función del tamaño del dispositivo. De esta forma se puede mantener la relación de aspecto del dispositivo (no franjas laterales) y escalar los pixels de forma perfecta (sin decimales).

Escalado adaptativo

Cambiar el tamaño de la pantalla virtual en función del dispositivo físico resuelve dos de los problemas mencionados anteriormente, pero no resuelve el problema del escalado proporcional de algunos widgets. Lo ideal sería que algunos widgets tuvieran el mismo tamaño "real" en todos los dispositivos.
El modo adaptativo de escalado resuelve todos estos problemas.

Principios

Cuando se selecciona el modo "adaptive" en config.lua, Corona calcula el ancho y el alto de la pantalla virtual mediante una serie de algoritmos. Los objetivos de estos algoritmos son:
  • Debe mantenerse la relación de aspecto (sin franjas laterales)
  • El escalado de pixels debe ser un número entero, o al menos, fracciones muy simples
  • La densidad de pixels debe ser aproximadamente la misma en todos los dispositivos (la densidad de pixels mide el número de pixels que hay en cada pulgada de pantalla, si la densidad es similar, el tamaño de los widgets será el mismo en todos los dispositivos).

Desventajas

La primera (y creo que única) gran desventaja del modo adaptativo es que, como el tamaño de la pantalla virtual no es fijo, se complica bastante el desarrollo (sobre todo el posicionamiento y tamaño de los objetos en la pantalla). No podremos utilizar valores fijos para la posición y el tamaño de los objetos. Cada tamaño y posición se tendrán que calcular en función del ancho y alto de la pantalla virtual.

Escalado simple vs adaptativo

La diferencia entre el resultado entre estos dos modos se puede ver con un ejemplo sencillo. No voy a poner el código de la aplicación para evitar complicarnos la vida. Voy a poner dibujos sencillos para entender el problema.
Imaginemos una pantalla con una lista de elementos y un botón abajo:



En el escalado simple, los objetos serán más grandes (más centímetros) en un dispositivo más grande, proporcionalmente al tamaño de la pantalla (en todos los modos de escalado simple: letterbox, zoomEven, zoomStrech). Veamos esta aplicación en un iPhone4 y en un iPhone6 plus:



Se observa que el alto de cada fila es mayor (más centímetros) en el dispositivo grande. Sin embargo,tenemos el mismo número de filas en ambos dispositivos.

En el modo adaptativo los objetos tienen el mismo tamaño (aproximadamente) en todos los tamaños de dispositivo. Esto significa que en dispositivos con pantalla más grande dispondremos de más espacio para meter más elementos. Por ejemplo, se podrán visualizar más elementos en la lista.



En este caso, el alto de la fila en centímetros es el mismo en ambos dispositivos, y por tanto, se muestran más filas en el dispositivo grande.

Creo que con esta explicación se puede entender el modo de escalado adaptativo, que se puede resumir en:


  • más complejo de programar (no se pueden utilizar coordenadas absolutas, ya que el tamaño de la pantalla virtual cambia en función del dispositivo)
  • la relación de aspecto de la pantalla virtual es la misma que la del dispositivo
  • el escalado se hace con números enteros (o fracciones muy simples)
  • no hay franjas laterales
  • si especificamos el tamaño de los widgets numéricamente (width=100), entonces el tamaño en centímetros es el mismo para todos los dispositivos

Y por último, no utilicéis el modo adaptativo para juegos, no merece la pena.





lunes, 19 de septiembre de 2016

Mi solución para juegos y aplicaciones

En las últimas entradas hemos estado viendo cómo resolver el problema de los distintos tipos de pantalla en los que nuestra aplicación se puede ejecutar. Hemos visto que Corona nos ofrece una pantalla virtual donde situamos nuestros objetos (con un tamaño definido por nosotros) y después Corona se encarga de escalar ese contenido a la pantalla real del dispositivo. Hemos visto que hay varios modos de escalado. Y además, sabemos que el API de Corona, en el objeto display tiene algunas constantes muy útiles. Con todo esto, y con la alineación de la pantalla virtual (que vamos a ver ahora mismo), es posible resolver casi cualquier problema. Nos centraremos en el modo "letterbox" que es el que yo utilizo habitualmente.
Empecemos con la alineación.

Alineación de la pantalla virtual

Como vimos, en modo "letterbox", el escalado puede producir bandas laterales (o superior/inferior) si las proporciones de la pantalla virtual no coinciden con las de la pantalla real. En estos casos Corona por defecto centra la pantalla virtual en la pantalla física, es decir, deja el mismo espacio arriba y abajo, o bien el mismo espacio a izquierda y derecha.
Sin embargo, este comportamiento por defecto se puede cambiar. Para ello, recurrimos al fichero config.lua:

application = {
    content = {
        width = 320,
        height = 480, 
        scale = "letterBox",
        xAlign = "center",
        yAlign = "center",
    },
}

Hemos añadido dos nuevos miembros a la conocida tabla application.content
xAlign indica la alineación horizontal
yAlign indica la alineación vertical
Con estos dos nuevos parámetros podemos cambiar la forma en que Corona sitúa la pantalla virtual dentro de la pantalla física.
El parámetro xAlign puede tomar los siguientes valores:
  • "left"
  • "center"
  • "right"
Y el parámetro yAlign puede tomar los siguientes valores:
  • "top"
  • "center"
  • "bottom"
De esta forma podemos controlar cómo se encaja la pantalla virtual en la física. En unos momento vamos a ver la utilidad de estos dos parámetros.

Solución para los juegos

Para los juegos, yo utilizo siempre el modo letterbox, para no perder información y para no deformar. Como van a aparecer bandas laterales en muchos casos, también utilizo xAlign = "center", yAlign="center" de esta forma me aseguro que el juego propiamente dicho aparece centrado en la pantalla.
¿Qué hago con las bandas laterales? Utilizo las constantes:

display.actualContentWidth
display.actualContentHeight

para poner un rectángulo o una imagen en el fondo que encaje bien con el resto del juego. Este rectángulo no aparecerá si las proporciones del dispositivo son iguales que las de la pantalla virtual.
El juego aparecerá escalado en función de la pantalla del dispositivo sin ninguna deformación.
Esta solución es muy sencilla de implementar y funciona en todos los casos.

Solución para las aplicaciones

Paradójicamente, el caso de las aplicaciones es algo más complejo que el de los juegos (aunque dije que el desarrollo de juegos es más difícil que el de aplicaciones).
En este caso, la aplicación tiene que mostrar una serie de elementos (widgets) en la pantalla y no siempre lo mejor es que salga centrada en pantalla. Por ejemplo, una aplicación que tenga una barra superior con algunas opciones de menú tiene que salir siempre arriba del todo, no vale que salga un poco más abajo por causa de las bandas del letterbox. Igualmente, si tenemos pestañas abajo, éstas tiene que salir abajo del todo.
Mi solución en este caso, es usar de nuevo "letterbox" como modo de escalado (no quiero deformaciones ni pérdida de información), pero en este caso alineo la pantalla virtual a la parte superior. Mi fichero config.lua sería algo así como:

application = {
    content = {
        width = 320,
        height = 480, 
        scale = "letterBox",
        xAlign = "center",
        yAlign = "top",
    },
}

De esta forma consigo que el contenido se visualice a partir de la parte superior de la pantalla. El problema entonces es cómo conseguir rellenar el espacio sobrante por abajo. Lo que hago es, utilizando las constantes:

display.actualContentWidth
display.actualContentHeight

puedo situar los elementos inferiores ajustados a la parte inferior de la pantalla (en función de estas constantes). Y el espacio que sobra en el centro, lo utilizo para los elementos centrales.
Como se puede intuir, la parte central no va a tener un tamaño fijo, sino que dependerá de las proporciones del dispositivo físico.
Normalmente la parte central suele ser una lista de elementos con scroll o algo similar, con lo cual, es fácilmente adaptable a un tamaño variable.
Ya veremos ejemplos de todo esto en futuras entradas.



viernes, 16 de septiembre de 2016

Modos de escalado en Corona SDK

Para resumir las últimas entradas, vimos que si pintamos objetos en pantalla utilizando los pixels del dispositivo (pixels reales), tenemos un problema por los diversos tamaños de pantalla y resoluciones que tienen los distintos dispositivos. También vimos que Corona permite definir una pantalla virtual con un tamaño determinado y definiendo un modo de escalado. Vimos el modo "letterbox" que lo que hace es centrar la pantalla virtual en la pantalla física aprovechando al máximo el espacio y escalando los objetos proporcionalmente y sin deformarlos. Cuando ponemos los objetos en la pantalla virtual nos olvidamos del tamaño real de la pantalla del móvil, para nosotros sólo existe esa pantalla virtual que tiene un tamaño determinado en pixels virtuales. Obviamente, dependiendo de las proporciones del dispositivo destino, en algunos casos, la pantalla virtual no se adapta 100% a la pantalla física, y pueden quedar zonas (arriba y abajo, o bien, a izquierda y derecha) que no estén cubiertas por la pantalla virtual.
Las constantes:

    display.contentWidth
    display.contentHeight

nos dan el tamaño de la pantalla virtual (en pixels virtuales).

Todo esto en cuanto al modo "letterbox", pero éste no es el único modo de escalado que tiene Corona. En esta entrada vamos a tratar este tema para conocerlos todos y ver cuál se adapta mejor a las necesidades de nuestra aplicación o juego.

Escalado

En Corona, como ya he dicho varias veces, trabajamos con una pantalla virtual de un determinado tamaño fijo, y nuestro programa siempre va a trabajar con pixels en las coordenadas de esa pantalla virtual.
Cuando la aplicación se ejecuta en un dispositivo, los pixels virtuales se expanden (o se contraen) para rellenar la pantalla del dispositivo. A esto se le llama escalado.
Nuestro programa no tiene porqué conocer el tamaño real del dispositivo, simplemente trabaja sobre la pantalla virtual, y Corona se encargará de escalar el contenido a la pantalla física. Corona escalará todos los objetos gráficos, el texto, las imágenes, etc.
Es muy habitual definir una pantalla virtual de 320x480. Se suele utilizar este tamaño porque casi todos los dispositivos iOS y muchos Android tienen la misma relación de aspecto.
Los modos de escalado que ofrece Corona son:
  • letterbox
  • zoomEven
  • zoomStretch
  • none

Modo letterbox

Este es el modo más utilizado y el que se suele utilizar en caso de duda.
El modo letterbox nunca deforma ni corta los objetos. Por tanto, es el modo más fácil de utilizar.
Todo lo que se sitúe dentro de la pantalla virtual se mostrará en la pantalla del dispositivo sin deformaciones y sin que los objetos se salgan de la pantalla.
En modo letterbox hay tres casos a considerar:



En el primer caso, la relación de aspecto de la pantalla virtual coincide con la del dispositivo, con lo cual no tenemos que hacer nada y el escalado será perfecto.
En el segundo caso, el dispositivo es algo alargado, y por tanto, como la pantalla virtual no se puede deformar, quedará un espacio arriba y abajo de la pantalla real que no se pueda rellenar con la pantalla virtual.
En el tercer caso, el dispositivo es más ancho y por tanto, al encajar la pantalla virtual en el dispositivo (sin deformar), quedará espacio a izquierda y derecha sin deformar.

En la práctica, rellenaremos estas zonas fuera de la pantalla virtual con contenido que no sea relevante, por ejemplo con un fondo. No podemos poner objetos que sean importantes porque en algunos dispositivos no se va a visualizar.

Podemos saber el espacio que tenemos fuera de la pantalla virtual con las constantes:

    display.actualContentWidth
    display.actualContentHeight

que nos indican el tamaño total que disponemos, en coordenadas virtuales.
Así por ejemplo, si queremos poner un fondo que ocupe la pantalla entera:

    local bg = display.newRect( 
            display.contentCenterX, display.contentCenterY,
            display.actualContentWidth, display.actualContentHeight)
    bg:setFillColor(0.5, 0.4, 0,7)

Y podemos poner encima otro rectángulo que ocupe la pantalla virtual completamente, que es donde deberíamos situar el contenido importante de nuestra aplicación:

    local content = display.newRect( 
            display.contentCenterX, display.contentCenterY,
            display.contentWidth, display.contentHeight)
    content:setFillColor(0.6, 0.5, 0,8)


Ahora, dependiendo del dispositivo, veremos un caso u otro de los mencionados anteriormente para letterbox.
El fichero config.lau contiene lo siguiente:

application = {
    content = {
        width = 320,
        height = 480,
        scale = "letterbox",
    },
}

Modo zoomEven

Este modo es el más utilizado, después de letterbox. En este modo, la pantalla virtual se expande lo máximo posible, y si es necesario habrá partes de la pantalla virtual que quedarán fuera del dispositivo y no se verán. El escalado es proporcional, sin deformación (escala igual en un eje que en el otro, igual que letterbox). Lo bueno que tiene este modo es que no hay barras laterales sin rellenar. Pero tiene el inconveniente de que pueden quedar zonas de la pantalla que no se muestran.


Para ver el modo zoomEven en acción, escribamos el siguiente fichero de config.lua:

application = {
    content = {
        width = 320,
        height = 480,
        scale = "zoomEven",
    },
}

Y el siguiente main.lua:

local bg = display.newRect( display.contentCenterX, display.contentCenterY, display.contentWidth, display.contentHeight )
bg:setFillColor( 1, 1, 1 )

local circle = display.newCircle( display.contentCenterX, display.contentCenterY, display.contentWidth/2 )
circle:setFillColor( 0.3, 0.6, 0.3 )

En blanco vemos la pantalla virtual y en el centro de ésta un círculo verde, que no se deforma en ningún dispositivo, pero que se puede salir fuera de la pantalla y no dibujarse completamente según el tamaño del dispositivo final.
Si el dispositivo es iPhone 4, entonces veremos el círculo completo, porque la relación de aspecto del iPhone 4 es la misma que la de nuestra pantalla virtual (tamaños distintos pero misma relación de aspecto). Sin embargo en otros dispositivos como el iPhone 5, más alargado, el círculo se cortará.

zoomStretch

Este modo escala, deformando si es necesario, la pantalla virtual a la pantalla física. Es decir, puede escalar de forma distinta en el eje X y en el eje Y.
No se utiliza mucho este modo porque la deformación que produce no suele ser aceptable. No se me ocurre ninguna aplicación en la que pudiera ser útil este modo. Pero es bueno saber que existe, por si acaso es necesario.

none

Este modo es equivalente a no tener ningún modo de escalado en config.lua

Resumen

  • letterbox: no deforma, puede haber barras laterales, no recorta
  • zoomEven: no deforma, no puede haber barras laterales, puede recortar
  • zoomStretch: puede deformar, no puede haber barras laterales, no recorta
¿Cuál es la mejor opción? En mi opinión, yo uso casi siempre letterbox, con una opción de alineamiento que explicaré en la próxima entrada. Pero eso no quita para que en algunos casos sean interesantes los otros modos.

miércoles, 14 de septiembre de 2016

Múltiples tamaños de pantalla en Corona SDK

Pixels reales vs pixels virtuales

En la entrada anterior vimos claramente el problema: Si no le decimos nada a Corona, las coordenadas y tamaños que especifiquemos para los objetos de pantalla se traducen directamente a pixels en la pantalla del móvil. O sea, las coordenadas x=300,y=400 se traducen a los pixels 300,400 de la pantalla. Igualmente con los tamaños: Un rectángulo con ancho=100 y alto=200 se traduce en la pantalla a un rectángulo de 100 pixels de ancho y 200 pixels de alto. Esto que parece lógico, hemos visto que causa problemas muy importantes cuando la aplicación tiene que ejecutarse en dispositivos con tamaños distintos de pantalla.

¿Cómo soluciona Corona SDK este problema? Pues mediante un mecanismo que se denomina "content scaling" (escalado de contenido). La idea, es no trabajar con los pixels reales directamente, sino sobre lo que podríamos denominar pixels virtuales. Es decir, definimos un tamaño de pantalla, por ejemplo, 320x480, y trabajamos sobre esa pantalla siempre. Corona se encargará después de convertir las coordenadas y tamaño de los objetos en pixels reales del dispositivo.

Por ejemplo, si definimos nuestra pantalla virtual en 320x480 y queremos un rectángulo centrado verticalmente y que ocupe todo el ancho de la pantalla, lo situaremos en x=160,y=240, y le definiremos un ancho de 320. Si el dispositivo tiene una resolución de 640x960, Corona se encargará de traducir las coordenadas y tamaño a esos pixels reales del dispositivo. En nuestro ejemplo, Corona situará el objeto en x=320,y=480, y el ancho será 640. En otro dispositivo con otra resolución, hará otra conversión a pixels reales, pero siempre podemos conseguir que el rectángulo salga centrado verticalmente y que ocupe todo el ancho.

Para que Corona pueda trabajar con una pantalla virtual, tenemos que crear un fichero nuevo, en el mismo directorio que el main.lua, denominado config.lua. El contenido de este fichero será:

application = {
    content = {
        width = 320,
        height = 480,
    },
}

Este fichero tiene una tabla, llamada application, y dentro de esta tabla hay otra tabla, llamada content que define el ancho y el algo de la pantalla virtual. En nuestro caso 320x480. Como veremos más adelante en la tabla application, podremos definir otros valores de configuración de nuestra aplicación, de momento nos quedamos con el tamaño de la pantalla virtual.

Probemos otra vez con el programa anterior, main.lua:

local rect = display.newRect( 160, 240, 320, 200 )
rect:setStrokeColor( 1,0,0 )
rect.strokeWidth = 5

Si ejecutamos ahora, veremos que el resultado es siempre el mismo, independientemente del dispositivo que elijamos en el simulador.

La proporción o relación de aspecto

Aunque parezca que ya hemos resuelto el problema, realmente no es así.
Vamos a ver qué pasaría si en lugar de un rectángulo, tenemos un círculo:

local circle = display.newCircle( 160, 240, 100 )
circle:setStrokeColor( 1,0,0 )
circle.strokeWidth = 5

Probamos primero en iPhone 4:



Todo bien, sale un círculo perfectamente centrado en la pantalla. Estamos trabajando con una pantalla virtual de 320x480, colocamos el círculo en x=160,y=240 (centro de la pantalla virtual) y Corona traslada correctamente estas coordenadas al centro de la pantalla del iPhone 4.
Pero cambiemos ahora el dispositivo a Nexus One, por ejemplo. Nos encontramos con una sorpresa:


Si nos fijamos bien, esto ya no es un círculo, ¡es una elipse! ¿qué ha pasado? El problema es que nuestra pantalla virtual, 320x480, tiene una relación de aspecto (ancho/alto) de 0.66 (320/480). El iPhone 4 tiene una relación de aspecto idéntica: 640/960=0.66, con lo cual, Corona no distorsiona el círculo al escalarlo en el iPhone 4. Sin embargo, la pantalla del Nexus es 480x800, y por tanto, la relación de aspecto es: 480/800=0.6, ligeramente distinta a las anteriores. Podríamos probar con otros dispositivos y veremos que la relación de aspecto cambia en cada uno.
TRUCO: para mostrar el tamaño de la pantalla virtual y el tamaño de la pantalla real, podemos utilizar el siguiente código al principio de main.lua:

-- dimensiones de la pantalla virtual
print( display.contentWidth, display.contentHeight, display.contentWidth/display.contentHeight )

-- dimensiones de la pantalla real
print( display.pixelWidth, display.pixelHeight, display.pixelWidth/display.pixelHeight )

Si vamos cambiando el dispositivo, podemos ir comprobando las distintas relaciones de aspecto de los distintos dispositivos.

Este problema lo podemos arreglar simplemente añadiendo una línea a la tabla application de config.lua:

application = {
    content = {
        width = 320,
        height = 480,
        scale = "letterbox",
    },
}

Si ahora probamos con distintos dispositivos veremos que siempre tenemos un círculo perfecto (no una elipse) centrado perfectamente en la pantalla.
El parámetro scale dice a Corona cómo tiene que escalar los objetos cuando traduce los pixels virtuales a pixels reales del dispositivo. Después veremos los distintos modos, de momento nos quedamos con que el modo de escalado "letterbox" no deforma los objetos.

Parece que ya hemos llegado al final del problema, y que con esto ya podemos hacer aplicaciones que funcionen en todo tipo de dispositivos. Pero, por desgracia, no es así. Estamos cerca de la solución, pero todavía queda algo. Parece que este problema no acaba nunca, y es que realmente estamos ante un problema que trae de cabeza a muchos desarrolladores de aplicaciones para múltiples dispositivos.

Letterbox y las bandas

Vamos a dibujar un rectángulo de color azul en nuestra aplicación. Ese rectángulo debería hacer de fondo de la aplicación y cubrir toda la pantalla. Añadamos estas líneas en main.lua justo antes de crear el círculo:

local background = display.newRect( 160, 240, 320, 480 )
background:setFillColor( 0,0,1 )

Probamos con iPhone 4.



Todo perfecto, el rectángulo azul cubre todo el fondo.
Ahora como siempre, cambiemos a otro dispositivo con otra relación de aspecto, por ejemplo el Nexus:



Aunque podría pasar desapercibido, han aparecido dos bandas negras, una arriba y otra abajo. Realmente no ha aparecido ninguna banda. El problema es que la pantalla virtual, en modo de escalado "letterbox" (que no deforma), no puede cubrir completamente la pantalla del Nexus, porque la relación de aspecto es distinta. Es decir, en modo "letterbox" la pantalla virtual sólo puede cubrir completamente la pantalla real si la relación de aspecto es la misma (como ocurre en el iPhone 4).

Hagamos números: En el iPhone 4, para convertir de pixels virtuales a pixels reales, Corona multiplica por 2 tanto el ancho como el alto (como estamos en modo "letterbox" se multiplican ambas dimensiones por el mismo valor, para no deformar). Al multiplicar por 2, el rectángulo de 320x480 se convierte en 640x960 pixels reales, y cubre la pantalla completa del iPhone 4.
En el caso del Nexus, Corona busca un factor multiplicativo para convertir de 320x480 a 480x800. Como no puede deformar, el valor es el mismo en ambas dimensiones. En este caso, 1.5, que hace que el ancho se ajuste completamente, ya que 320 multiplicado por 1.5 es  480. Sin embargo, al multiplicar 480 por 1.5 tenemos 720, que es ligeramente inferior a los 800 pixels reales del alto de pantalla del Nexus. De ahí que Corona deja sin rellenar una parte por arriba y por abajo.

¿De dónde sale es valor de 1.5 para el Nexus? No es complicado. Es el valor que permite rellenar completamente una de las dos dimensiones, ancho o alto, sin pasarse en la otra dimensión. Así, para el caso del Nexus:

    valor para rellenar el ancho: 480 / 320 = 1.5
    valor para rellenar el alto: 800 / 480 = 1.66

Se coge el valor más pequeño, o sea, 1.5
En el caso del iPhone 4, ambos valores salen 2, y por tanto, la pantalla virtual se corresponde exactamente con la pantalla real.

Todo esto, como he dicho, es para el modo de escalado "letterbox". En la siguiente entrada hablaré de otros modos de escalado.

Podríamos conseguir que las bandas laterales salgan a la izquierda y a la derecha si definimos una pantalla virtual muy "estilizada", por ejemplo:

application = {
    content = {
        width = 160,
        height = 480,
        scale = "letterbox",
    },
}

Y el main:

local background = display.newRect( 80, 240, 160, 480 )
background:setFillColor( 0,0,1 )


Cómo quitar las bandas

Hemos visto que el modo de escalado "letterbox" no deforma al escalar desde la pantalla virtual a la pantalla real. Además, si sobra espacio al escalar, deja el mismo espacio por arriba y por abajo (o bien por la izquierda y derecha en caso de que la dimensión "perjudicada" sea el ancho).

Para resolver este y otros problemas relacionados con los tamaños, es conveniente conocer una serie de constantes que nos proporciona Corona SDK a través de su API:

  • display.contentWidth y display.contentHeight: ancho y alto de la pantalla virtual
  • display.pixelWidth y display.pixelHeight: ancho y alto de la pantalla real
  • display.actualContentWidth y display.actualContentHeight: ancho y alto real de la pantalla, pero en pixels virtuales. Esto merece una explicación. En el caso de bandas arriba y abajo, display.actualContentWidth va a ser igual a display.contentWidth, sin embargo, display.actualContentHeight va a ser superior a display.contentHeight
Con estas constantes, ya podemos definir fácilmente un rectángulo que cubra toda la pantalla en cualquier dispositivo:

local background = display.newRect( display.contentWidth/2, display.contentHeight/2, display.actualContentWidth, display.actualContentHeight )

background:setFillColor( 0,0,1 )

Existen otras constantes en el API de Corona que nos ayudarán en el futuro:

  • display.contentCenterX y display.contentCenterY: coordenadas del centro de la pantalla en pixels virtuales
En la siguiente entrada, seguiré hablando de los modos de escalado y de otras técnicas para hacer aplicaciones multidispositivo.






El problema de múltiples pantallas y proporciones

El problema

Este tema de hoy es quizás unos de los más peliagudos y difíciles de entender en la programación de aplicaciones para móviles. En la siguiente entrada veremos cómo resolverlo en Corona SDK.

El problema de la programación para móviles es la (inmensa) diversidad de tamaños de pantalla en los dispositivos. Tenemos móviles desde 4 pulgadas hasta 6 pulgadas, y además tenemos las tablets, que pueden ir desde 7 pulgadas hasta las casi 13 pulgadas del iPad Pro de Apple. Además, para completar el panorama, las proporciones de la pantalla también cambian. Tenemos pantallas 4x3, 5x4, y casi cualquier combinación de parejas números.
Obviamente, como buenos programadores (y como interesados en distribuir/vender muchas unidades de nuestra aplicación), queremos que nuestra aplicación se ejecute igual de bien en el número máximo de dispositivos posible. Este es uno de los problemas más importantes que se dan en la programación móvil. Y hay varias soluciones:
  • layouts: que mueven y redimensionan dinámicamente los elementos en función del tamaño de la pantalla en base a una serie de reglas del estilo: este elemento se ajusta a la izquierda, este elemento va a la izquierda de este otro, etc. Esta programación con layouts es una solución muy interesante y que funciona muy bien "la primera vez", quiero decir, para la primera versión de nuestra aplicación. Pero el gran problema es cuando el cliente o nuestro jefe nos dice: "Tenemos que meter una lista debajo de ese botón", y es entonces cuando aparecen los sudores y temblores en el programador, pensando que va a tener que volver a redefinir todas las reglas de layouts para que todo quede bien por culpa de un simple elemento. Y lo peor llega cuando rematan la frase con: "Esto es fácil, verdad? Es sólo añadir un elemento más". Se puede desprender de estas líneas que aunque es bonita la idea de los layouts, no creo que sean la mejor idea (es mi opinión absolutamente personal). En cualquier caso, Apple utiliza layouts en los desarrollos para iOS y parece que a los programadores les gusta.
  • hacer proporcionales los elementos con respecto al tamaño de la pantalla: esto es más o menos lo que hace Corona, y que veremos en esta entrada.
  • llenar el código de if's y colocar los elementos en función del tamaño y proporciones de la pantalla, asegurándose que todo cuadra en todas las posibilidades. El lector comprenderá que esta opción, un tanto primitiva, no es muy adecuada en cuanto tengamos 10 o más elementos en una misma pantalla. Pero como siempre, para gustos los colores, hay gente muy muy paciente que es capaz de rellenar el código de if's preguntando por miles de posibles valores.

Hay otras soluciones, pero realmente la que me interesa explicar con detalle es la que se utiliza en las aplicaciones con Corona.

Ejemplo

Vamos a hacer un sencillo ejemplo donde podemos ver el problema, así de paso, ya vamos viendo el mundo de las apps.
Creamos un directorio en nuestro disco duro y también creamos un fichero nuevo: main.lua, que será el punto de arranque de nuestra aplicación. El contenido del main.lua será:

    local rect = display.newRect( 320, 480, 640, 320 )

Abrimos el simulador de Corona, seleccionamos la carpeta de nuestro proyecto y lo ejecutamos. Pero atención, vamos a ir al menú View > View As > iPhone 4. En la pantalla tendremos algo como lo siguiente:


Reconozco que el programa no es gran cosa, pero no se puede pedir mucho más por una única línea de código.
Aunque creo que se entiende bien, diré que estamos creando un rectángulo en la pantalla, en las coordenadas x=320, y=480, con ancho 640, y alto 320. Como podemos ver, el cuadrado sale centrado verticalmente en la pantalla del móvil y además ocupa todo el ancho de la pantalla. En Corona, como veremos más adelante, las coordenadas x,y de los objetos. sitúan el centro del objeto, por eso el cuadrado aparece centrado en la pantalla del iPhone 4 (que tiene un tamaño de pantalla de 640x960, y el centro es 320x480). En otros entornos, las coordenadas x,y sitúan la esquina superior izquierda del objeto, pero no así en Corona (ya veremos más adelante que este comportamiento se puede cambiar).
Hasta ahora todo muy bien. Pero vamos a ver qué ocurre si visualizamos nuestro programa en otro dispositivo, por ejemplo, en un iPhone 6. Vamos al menú View > View As > iPhone 6 y veremos algo como esto:


El resultado es parecido. Pero hay unas diferencias muy importantes: Sin haber cambiado coordenadas ni tamaño en nuestro programa, resulta que ahora el rectángulo ya no sale centrado verticalmente en la pantalla. ¿Por qué? Esto es fácil de responder: el tamaño del iPhone 6 es distinto, 750x1334, por tanto, la coordenada y=480 ya no es el centro vertical de la pantalla, y por tanto, el objeto no sale centrado verticalmente. Hay otro cambio, el rectángulo no ocupa todo el ancho de la pantalla. La justificación de esto también es obvia: El ancho de pantalla del iPhone 6 es 750, y el ancho de nuestro rectángulo es 640, por tanto no va a ocupar todo el ancho de la pantalla en el iPhone 6 (aunque sí en un iPhone 4).
Vamos a visualizar nuestro programa en un Nexus One (recordemos, View > View As > Nexus One). Aquí la situación todavía es peor, ya que la pantalla de este teléfono tiene 480x800. En este caso, no sólo no tenemos un rectángulo centrado verticalmente en la pantalla, sino que además, no cabe completamente en la pantalla. Esto es todavía peor, porque podemos perder contenido. Creo que no es difícil entender el resultado.

En el caso del Nexus One, estamos perdiendo 160 pixels del rectángulo (640 que es el ancho del rectángulo, menos 480 que es el ancho de pantalla del Nexus).
Si queremos comprobarlo, pongamos un borde alrededor del rectángulo y comprobemos que el borde derecho no se visualiza en el Nexus:

    rect:setStrokeColor( 1,0,0 )
    rect.strokeWidth = 5

En resumen, tenemos un problema. Hay que hacer algo para que cuando coloquemos objetos en la pantalla, no tengamos que preocuparnos del tamaño real del dispositivo en el que se ejecutará nuestra aplicación.






jueves, 8 de septiembre de 2016

Programación orientada a objetos en Lua

Lua, al igual que otros lenguajes no está orientado a objetos. Otros lenguajes como Java o C# tienen palabras y construcciones especiales para definir clases, crear objetos, herencia, etc. Pero Lua no.
Los conocedores de javascript con un nivel elevado saben que javascript tampoco está orientado a objetos, pero que se pueden realizar algunas "artimañas" para conseguir algo similar a la orientación a objetos. En Lua ocurre exactamente los mismo: utilizando las funciones, y las tablas podemos conseguir una funcionalidad de orientación a objetos muy similar a la de otros lenguajes.
Es importante notar que no existe una única forma de conseguir OOP en Lua, sino que cada desarrollador suele tener una arquitectura, al igual que ocurre en javascript. Yo mostraré la que yo suelo utilizar, que no tiene porqué ser la mejor forma, pero que es muy sencilla de entender y a mí me ha servido durante muchos años.

Clase base

En primer lugar, definiremos una clase base en el fichero BaseClass.lua:

function BaseClass( param1, param2 )
    local this = {}
    local privateAttr = param1
    this.publicAttr = param2
    local function privateMethod()
        return privateAttr
    end
    this.publicMethod = function()
        return privateMethod()
    end
    return this
end

En este ejemplo, hemos definido una función constructora, BaseClass(), de una clase que tiene dos atributos: privateAttr (que es privado) y publicAttr (que es público). También se han definido dos métodos: privateMethod (que es privado) y publicMethod (que es público).
En primer lugar, se crea una tabla (llamada this, aunque valdría cualquier otro nombre). Luego se crean atributos en la tabla this (que serán los atributos públicos) y también funciones en la tabla (que serán las funciones públicas). Los atributos y funciones que sean local se convertirán en atributos y funciones privados.
Por último retornamos la tabla this que será el objeto que acabamos de crear con los atributos y métodos públicos.
En este momento, lo normal es hacerse la pregunta: ¿Qué ocurre con las variables y funciones local? ¿Desaparecen al retornar? Pues no, esas variables y funciones siguen existiendo en el closure de las funciones que hemos metido dentro de la tabla this, y por tanto, las funciones públicas podrán seguir accediendo a ellas una vez retornado this.

Clase derivada

Ahora vamos a definir una clase que hereda de la clase base, en el fichero DerivedClass.lua :

require ("BaseClass")

function DerivedClass( param1, param2, param3 )
    local this = ClaseBase(param1, param2)
    this.attr1 = param1
    this.attr2 = param2
    local attr3 = param3
    local function pp()
        print( this.attr1, attr3, this.publicMethod() ) 
    end
    this.increment = function()
        this.attr1 = this.attr1 + 1
        this.attr2 = this.attr2 + 1
    end
    return this
end

En primer lugar tenemos que hacer require del módulo BaseClass para tener acceso a la función constructora de la clase base.
En este módulo definimos otra función constructora, DerivedClass(), similar a la de la clase base. Pero hay algo distinto. En la clase base creábamos una tabla this vacía, y ahora estamos llamando a la función constructora de la clase base para que nos retorne this. A partir de ese momento, vamos añadiendo nuevos atributos y métodos a this, tal y como hicimos antes.
Terminamos, al igual que antes, retornando la tabla this con los atributos de la clase base y los de la clase derivada.

Utilización

Por último, vamos a utilizar la clase DerivedClass en un fichero main.lua:

require ( "DerivedClass")

local obj = DerivedClass( 1, 2, 3 )
obj.increment()
print(obj.publicAttr)
print(obj.attr1, obj.attr2)

local obj2 = DerivedClass( 4, 5, 6 )
obj2.increment()
print(obj2.publicAttr)
print(obj2.attr1, obj2.attr2)

No es difícil entender este main.lua. Lo primero que tenemos que hacer es la sentencia require para llegar a la clase derivada (no hace falta el require de la clase base, porque eso ya está dentro del módulo de la clase derivada).
Luego creamos objetos de la clase derivada, llamando a la función constructora. Y por último accedemos a sus atributos y métodos públicos.

Otras opciones para OOP en Lua

Como ya he dicho, esta es la forma en que yo hago orientación a objetos en Lua, pero existen muchas otras alternativas. Debido a que Lua no es orientado a objetos, entonces tenemos que "simular" esta característica con construcciones propias del lenguaje. Esta que he descrito es mi forma de hacerlo y hasta ahora me ha funcionado muy bien. De OOP lo que más necesito es la encapsulación, métodos públicos y privados, y la herencia. Con mi método consigo todo lo que necesito de una forma sencilla y que funciona.
Sin embargo, como también he dicho existen otras alternativas. Quizás una de las más conocidas sea la que propone la web de Lua:


También en Corona labs tienen una propuesta:


Y por último, en la wiki de Lua hay otro tutorial también interesante:


Con alguna de estas opciones tendremos características más avanzadas de la orientación a objetos en Lua, pero ya digo que en mi opinión no las he necesitado nunca, y el problema es que utilizan características avanzadas de Lua que pueden complicar el software que desarrollamos.










martes, 6 de septiembre de 2016

Módulos

Cuando escribimos una aplicación con Corona SDK empezamos con un fichero llamado main.lua
Este fichero es el punto de entrada de nuestra aplicación y siempre será el punto de entrada por el que empieza la ejecución. Si quisiéramos podríamos escribir todo nuestro programa en un único fichero main.lua, lo cual es perfectamente posible, salvo que sería bastante difícil su mantenimiento si la aplicación tiene un tamaño moderado o grande. Otra limitación sería el propio editor o IDE, que probablemente tendría algunos problemas de rendimiento si main.lua tuviera muchas miles de líneas de código Lua. Desde luego, desde un punto de vista de ingeniería del software y de arquitectura, ésta no es la mejor idea (excepto para aplicaciones muy triviales o pruebas).

Los módulos Lua permiten que organicemos mejor nuestro programa. Un módulo es un fichero de código Lua que se incluye en main.lua mediante la sentencia require()
Por ejemplo, si tenemos un módulo en un fichero llamado utils.lua, en el main.lua tendremos que hacer:

    require( "utils" )

Obsérvese que no hay que añadir la extensión del fichero .lua

El contenido del fichero utils.lua podría ser:

    function add(a, b)
        return a + b
    end

Entonces, en main.lua, después de require(), podemos invocar a la función add().

En este ejemplo estamos declarando una función en el contexto global, lo cual no es una buena idea. Por eso, lo normal es que un módulo retorne una tabla con los datos y funciones. Por ejemplo:

    local t = {}

    t.add = function(a, b)
        return a + b
    end

    return t

De esta forma, en el main.lua tendremos que hacer lo siguiente:

    local utils = require( "utils" )
    print( utils.add( 5, 34 )

La sentencia require() devuelve el valor retornado por el fichero del módulo. En nuestro ejemplo, retorna una tabla que tiene una función.

Cualquier variable o función declarada en un modulo con local, queda en el ámbito local del módulo y no se puede acceder a ella desde otros sitios fuera del módulo.

Otro punto importante es que si ejecutamos la sentencia require() varias veces en nuestro programa, sólo se ejecutará el módulo la primera vez. Las siguientes veces simplemente detectan que ya se ha hecho require() de ese módulo y no lo vuelve a ejecutar, aunque sí que retorna el mismo valor que devolvió la primera vez.

Se podrían ejecutar todas las sentencias require() de todos los módulos en main.lua y asignar el resultado a variables globales, por ejemplo:

    utils = require( "utils" )
    constants = require( "constants" )

Las variables utils y constants son globales y por tanto podremos acceder a ellas desde cualquier punto de nuestro programa. Esta es una práctica razonable, pero se recomienda que cada módulo haga los require() que necesite y que asigne el resultado a variables locales:

    local utils = require( "utils" )
    local constants = require( "constants" )

En este caso, usaríamos en cada módulo otros módulos que se necesiten, y como no se ejecuta el módulo nada más que la primera vez, no hay problemas de rendimiento.

Por último, y para terminar el tema de los módulos Lua, quiero comentar que los ficheros de los módulos se pueden colocar en distintos subdirectorios (siempre debajo del directorio del proyecto donde debe estar obligatoriamente main.lua). Podríamos tener una estructura similar a la siguiente:


  • main.lua
    • utils
      • dateutils.lua
      • file.lua
    • scenes
      • splash_scene.lua
      • menu_scene.lua

Para acceder a estos módulos, se haría de la siguiente forma:

    require( "utils.dateutils" )

O sea, los subdirectorios se especifican con un punto y no hay que poner la extensión .lua






jueves, 1 de septiembre de 2016

Lua - Parte 5

Estructuras de control

Lua, como casi todos los lenguajes de programación, tiene una serie de estructuras de control que permiten iterar y hacer comprobaciones dentro del código:
  • if
  • for
  • while
  • repeat

IF

La sentencia if permite chequear una o más condiciones:

    if condición1 then
        sentencias1
    elseif condición2 then
        sentencias2
    elseif condición3 then
        sentencias3
    else
        sentencias4
    end

Las condiciones son expresiones cuyo resultado es un boolean, o sea, true o false. Si una expresión es nil se considera false, cualquier otro valor, se considera true. Ejemplos de condiciones:

    3 > 5    -- false
    5 > 1    -- true
    a        -- variable no existente -> nil -> false
    6        -- true
    0        -- true
    "hola"   -- true
    ""       -- true
    nil      -- false

Las partes elseif y else de la sentencia if son opcionales.

Lua no tiene la sentencia switch típica de otros lenguajes de programación, pero se puede sustituir muy fácilmente con la sentencia if.

FOR

Forma general de la sentencia for:

    for variable = start, end, step do
        sentencias
    end

La "variable" es el índice que va cambiando y que se puede utilizar dentro de las sentencias del bucle for. "start" es el valor de inicio y "end" es el valor final (incluidos ambos). "step" es opcional y permite pasar desde start hasta en end en pasos distintos de uno.

Ejemplo:

    for i = 3, 10 do
        print(i)
    end

Otro ejemplo, contando hacia atrás:

    for i = 100, 50, -1 do
        print(i)
    end

WHILE

La sentencia while permite ejecutar un bloque de sentencias mientras se cumpla una condición:

    while condición do
        sentencias
    end

REPEAT

La sentencia repeat permite ejecutar un bloque de sentencias mientras hasta que se cumpla una condición:

    repeat
        sentencias
    until condición

El bloque repeat se ejecuta siempre al menos una vez.

BREAK

La sentencia break permite salirse un bucle. Por ejemplo:

    for i = 100, 50, -1 do
        print(i)
        if i > 80 then
            break
        end
    end

Hay que tener en cuenta que Lua no tiene la sentencia continue, típica en otros lenguajes de programación para ir al principio del bucle.

Lua - Parte 4

Funciones

Todos hemos usado funciones de una u otra forma en muchos lenguajes de programación. En Lua también tenemos funciones y la idea es muy similar a la de los demás lenguajes. Sin embargo, hay algunos matices que es interesante considerar.

En Lua una función se puede definir de varias formas. La más sencilla y quizás la más utilizada es:

    function suma(a, b)
        return a + b
    end

De esta forma, estamos definiendo una función denominada "suma" de ámbito global (o sea, accesible desde cualquier sitio), que acepta dos argumentos, a y b, suma los dos argumentos y retorna el resultado de la suma. Creo que todo esto está bastante claro para todos aquellos que alguna vez han programado en algún lenguaje, no parece haber nada nuevo. Pero quiero destacar el hecho de que la función tiene ámbito global, es decir, que podemos invocarla desde cualquier punto de nuestro programa. Esto ocurre porque no hemos utilizado la palabra clave "local". Si hubiéramos querido definir una función con ámbito local:

    local function suma(a, b)
        return a + b
    end

En este caso, sólo podríamos invocarla desde el módulo en el que estamos (ya hablaremos de módulos en una entrada posterior, de momento adelanto que un módulo es un fichero Lua con funciones y otros elementos de código).

Otro tema interesante a destacar es que los parámetros, a y b, no declaran un tipo. Es decir, podemos invocar a la función "suma" con dos valores o varialbes cualquiera, de cualquier tipo. Lo normal será invocarla con dos números, pero nada nos impide utilizar otros tipos (eso sí, probablemente tendremos un error de ejecución, por ejemplo al invocar la función "suma" con dos strings).

Destacar también que la función no declara el tipo retornado. Retornará un valor numérico porque la sentencia "return" devuelve la suma de sus dos parámetros, pero realmente el lenguaje no obliga a retornar ningún tipo concreto. De hecho (reconozco que esto es algo enrevesado), la función podría retornar en un caso un número y en otros casos un string.

Por último no quiero olvidar que una función podría no retornar ningún valor. Para ello podemos utilizar la palabra clave "return" simplemente o bien llegar al final de la función sin retornar ningún valor.

Funciones dentro de una tabla

Como vimos al hablar de las tablas, una función puede ser un miembro de una tabla:

    local table = {
        add = suma,
    }

O bien:

    local table = {
        add = function(a, b) return a + b end,
    }

Retorno de múltiples resultados

Una función puede retornar más de un resultado:

    function statistics()
        return a, b, c
    end

Estos múltiples resultados se recogen de la siguiente forma:

    local x, y, z = statistics()

Número variable de argumentos

Una función no sólo puede retornar múltiples resultados, si no que también puede aceptar un número variable de argumentos:

    function guarda( ... )
        print( arg[1], arg[2], arg[3] )
    end

Los argumentos se recogen en la lista predefinida arg. El operador # nos permite conocer el número de argumentos que se han pasado a la función:

    function guarda( ... )
        for i = 1, #arg do   -- más adelante veremos los bucles
            print( arg[i] )
        end
    end

También podemos tener un número mínimo de argumentos a una función y los demás variables:

    function guarda( a, b, ... )
        print( a, b, arg[1], arg[2], arg[3] )
    end

Lo importante es que los tres puntos (argumentos variables) siempre tienen que ir al final.

Más sobre tablas y funciones

Ya he dicho varias veces que una función puede ser un miembro de una tabla. Esto es muy importante y se utiliza mucho en Lua. Una de las ventajas de esta construcción es que de esta forma no se crean funciones globales. Podríamos resolver también este problema con la palabra "local", pero en este caso el acceso a la función sería únicamente local, sin embargo, las funciones dentro de una tabla son accesibles desde cualquier punto del programa, pero de una forma que podría ser similar a los namespaces o packages de otros lenguajes (como C# o Java). Por ejemplo:

    myobject = {
        add = function(a, b) return a + b end,
    }

Para invocar a esta función haríamos:

    myobject.add(3, 5)

De esta forma estamos añadiendo un "namespace" a la función. Y no sobreescribiría a esta otra función que tiene el mismo nombre, pero está en otro "namespace":

    utils = {
        add = function(a, b) return a .. b end,
    }

Otra forma equivalente de añadir la función a la tabla sería:

    myobject = { }
    function myobject.add(a, b) return a + b end

Tablas, funciones y objetos

Lo que hemos visto sobre tablas y funciones hará pensar a algún lector en objetos y en programación orientada a objetos. Sin embargo, la forma que hemos visto de meter una función en una tabla no es realmente un objeto. El motivo principal es que la función no es "consciente" de en qué objeto (tabla) está, de hecho, no tiene acceso a otros datos de la tabla. Veámoslo con un ejemplo:

    myobject = {
        dato = 3,
    }
    function myobject.calcula(a) return dato + a end

En este caso tenemos una tabla con dos miembros: un número "dato" y una función "calcula".
Si intentamos invocar a la función "calcula":

    myobject.calcula(5)

Obtendremos un error similar al siguiente:

    attempt to perform arithmetic on global 'dato' (a nil value)

Esto se debe a que no existe una variable global llamada "dato" y por tanto su valor es nil, lo cual provoca un error al intentar sumarlo con el argumento "a". La variable "dato" de la tabla no es accesible a la función.

Podríamos resolverlo de la siguiente forma:

    myobject = {
        dato = 3,
    }
    function myobject.calcula(a) return myobject.dato + a end

Pero tenemos que reconocer que no es una forma muy elegante de acceder a la variable dato (ya que dependemos del nombre que demos a la tabla, en este caso, "myobject").

Otra solución, tampoco muy elegante por cierto, sería pasar como argumento a la función la propia tabla (reconozco que esto es todavía peor):

    myobject = {
        dato = 3,
    }
    function myobject.calcula(this, a) return this.dato + a end

    myobject.calcula(myobject, 5)

Aunque esta solución es incluso más horrible que la anterior, me permite introducir la solución que propone Lua para estos casos.

    myobject = {
        dato = 3,
    }
    function myobject:calcula(a) return self.dato + a end

Y para invocar a esta función:

    myobject:calcula(6)

Es una sintaxis un poco extraña, pero fácil de comprender. En primer lugar, para añadir la función a la tabla utilizamos dos puntos, en lugar de un punto como hemos hecho hasta ahora. También utilizamos dos puntos para invocar a la función. Los dos puntos indican a Lua que tenemos que necesitamos un parámetro intrínseco, denominado "self", y que es la propia tabla donde está la función. Este parámetro lo pone Lua (sin que hagamos nada y de forma similar al parámetro arg que Lua introduce en las funciones con número variable de argumentos).

En una entrada posterior trataré el tema de la programación orientada a objetos en Lua, donde veremos cómo podemos tener objetos reales en nuestros programas, a pesar de que Lua no es realmente un lenguaje orientado a objetos. Tendremos que hacer unas construcciones especiales, siguiendo un determinado patrón para conseguirlo.

Closure de una función

El closure de una función es un término que suele ser difícil de comprender de partida, salvo que se haya utilizado previamente en otros lenguajes como javascript.
En pocas palabras, el closure de una función son las variables y funciones que existen en su scope. Cuando una función está dentro de otra función, la función interna tiene acceso a todas las variables de la función externa. A esto se le conoce como lexical scope.
Estos dos conceptos que parecen a primera vista muy sencillos, se complican un poco, como ahora veremos.

    local function external(paramExt)        local varExt = paramExt + 1        local internal = function(paramInt)            local varInt = paramInt + varExt + paramExt        end        return internal    end

    local f = external(5)
    print( f(4) )

Este es un ejemplo bastante complejo, si no se tiene costumbre de manejar funciones como objetos. Voy a tratar de explicarlo despacio.
En primer lugar, tenemos una función llamada external que devuelve otra función llamada internal. Hasta aquí no hay nada extraño. Sin embargo, la función internal utiliza variables e incluso parámetros (varExt y paramExt) que pertenecen a la función external. Y nos podemos preguntar: ¿Cuánto valen esa variable y ese parámetro cuando mucho más abajo llamamos a la función f? Pues bien, esa variable y ese parámetro tienen los valores que tenían cuando llamamos a la función external. Es decir, paramExt vale 5 y varExt vale 6. Por tanto, la función f, que devuelve la suma de esos dos objetos más el parámetro paramInt, devuelve 5 + 6 + 4, o sea, 15.
La variable varExt y el parámetro paramExt son el closure de la función internal, y cuando se le invoque más adelante, tendrá acceso a esos valores de su closure. Cada vez que invocamos a la función external y recibimos una función como resultado, esa función tendrá asociado el closure correspondiente.