Tutorial Corona SDK

Tutorial -1- Indice 1. Introducción al desarrollo con Corona SDK …………………………………… 3 Ejercicios tema 1………………………………………………

Views 209 Downloads 7 File size 2MB

Report DMCA / Copyright

DOWNLOAD FILE

Recommend stories

Citation preview

Tutorial

-1-

Indice 1. Introducción al desarrollo con Corona SDK …………………………………… 3 Ejercicios tema 1………………………………………………………………… 9 2. Empezar a programar con LUA …………………………………………………...13 Ejercicios tema 2 ………………………………………………………………..21 3. Formas, imágenes y texto ………………………………………………………….23 Ejercicios tema 3 ………………………………………………………………..35 4. Eventos e interacción ………………………………………………………….…...39 Ejercicios tema 4 ……………………………………………………………......47 5. Timers y animaciones …………………………………………………………...…50 Ejercicios tema 5 ………………………………………………………………..58 6. Motor Físico …………………………………………………………………………..61 Ejercicios tema 6 ………………………………………………………………..71 7. Hardware y multimedia ………………………………………………………….....74 Ejercicios tema 7 ………………………………………………………………..85 8. Gestión de mapas y GPS …………………………………………………………..90 Ejercicios tema 8 ………………………………………………………………..97 9. Diseñando la interfaz de usuario ……………………………………………..…100 Ejercicios tema 9 ………………………………………………………………105 10. Entrada y salida …………………………………………………………….….…108 Ejercicios tema 10 ……………………………………………………….….…116 11. Composer y widget ……………………………………………………………….120 Ejercicios tema 11 ………………………………………………………..……130 12. Motor físico – Parte 2 ……………………………………………………….…...135 Ejercicios tema 12 ………………………………………………………..……142

-2-

Tema 1 Introducción al desarrollo con Corona SDK • Corona es un kit de desarrollo de software que permite la creación de aplicaciones móviles para iPhone, iPad y dispositivos Android. • Su objetivo es el de ahorrar el mayor tiempo posible desarrollando proyectos móviles al compartir código entre los diferentes sistemas • Tiene una versión gratuita para desarrollar que se puede descargar desde el siguiente enlace:

https://developer.coronalabs.com/downloads/corona-sdk

• El proceso de desarrollo con Corona SDK es muy simple: • Diseñamos los elementos de audio, vídeo, imagen y animación con alguna herramienta destinada a ello (Photoshop, Premiere, Audacity...). • Rápido desarrollo de la aplicación haciendo uso del simulador multiplataforma para previsualizar nuestro proyecto en los diferentes dispositivos. • Publicación con muy pocos clicks del resultado de nuestro proyecto en la App Store y en Google Play • El lenguaje de programación empleado para el desarrollo con Corona SDK es LUA:

-3-

• LUA es un lenguaje de programación imperativo (se basa en estados de la aplicación que podemos ir modificando mediante sentencias), estructurado y bastante ligero • Sólo funciona embebido en un cliente anfitrión, este caso, el framework Corona • Ofrece un buen soporte para la programación orientada a objetos, programación funcional y programación orientada a datos

Corona SDK - LUA • A través del uso de funciones C, Lua puede ser aumentado para abarcar un amplio rango de diferentes dominios, creando así lenguajes de programación personalizados. • LUA es software libre y se proporciona sin garantías, tal y como especifica su manual de referencia en el sitio oficial www.lua.org.

Características de Corona • Posee un motor OpenGL-ES propio que nos permite realizar grandes manipulaciones gráficas de manera muy sencilla • Destaca por su desarrollo multiplataforma. Permite crear aplicaciones tanto para iOS (iPhone y iPad), como para Android. • Cuenta con controles nativos para el acceso al dispositivo y a los diferentes elementos hardware como cámara, acelerómetro, GPS... etc. • Las aplicaciones creadas con Corona se pueden optimizar para ocupar el menor espacio posible. A partir de 400ks.

-4-

Instalación de Corona SDK • Para obtener la versión gratuita del framework, con la que podremos crear aplicaciones de manera ilimitada, tendremos que crearnos una cuenta de desarrollador en la web oficial de Corona

http://www.coronalabs.com/

• Debemos descargarnos e instalar la aplicación correspondiente a nuestro sistema operativo (Mac OS X o Microsoft Windows)

-5-

• Con la instalación obtendremos una carpeta en nuestra máquina con las siguientes aplicaciones

-6-

• Contamos con tres aplicaciones para poder acceder al Simulador de Corona (Corona Simulator, Corona Terminal y debugger) • Las tres nos dan acceso al mismo simulador, sólo que las dos últimas se acompañan de herramientas para poder realizar la depuración de código a través del terminal. • El resto de directorios corresponden a herramientas, recursos y diferentes ejemplos donde podemos apreciar la diversidad de aplicaciones que podemos desarrollar con Corona SDK Corona SDK

Simulador Corona SDK • Desde la ventana principal de la aplicación simulador podremos acceder a todos nuestros proyectos, así como crear uno nuevo o ejecutar cualquiera de los ejemplos que nos vienen dentro del directorio SampleCode

• Si abrimos cualquier proyecto, ejecutaremos automáticamente la vista del simulador, la cual podemos cambiar para realizar las pruebas pertinentes en los diferentes dispositivos.

-7-

• Para editar el código de nuestro proyecto se usará el editor de texto que tengamos definido por defecto en nuestra máquina. Para cambiar esto tendremos que cambiar la aplicación asociada a los archivos de tipo .lua • Al salvar un fichero .lua correspondiente al proyecto activo en el simulador, éste nos preguntará si deseamos volver a cargar la aplicación para poder observar los cambios en el dispositivo. Esta opción se puede desactivar.

-8-

Ejercicios Tema 1 Ejercicio 1 Vamos a realizar la instalación completa de Corona SDK y comprobaremos su correcto funcionamiento en nuestra máquina, para así poder afrontar el resto de temas sin problemas. Para comenzar con la instalación, debemos acceder a la web oficial de Corona para darnos de alta como desarrolladores (https://developer.coronalabs.com/user/register)

Una vez estemos correctamente registrados e iniciemos sesión en la página, podemos descargarnos el sdk desde la propia web, distinguiendo el sistema operativo sobre el que vamos a trabajar (https://developer.coronalabs.com/downloads/corona-sdk)

-9-

Procedemos a instalar el ejecutable que hemos descargado de la web y tendremos acceso a la carpeta con las herramientas de Corona SDK, en el directorio donde hayamos especificado durante la instalación. Los ejecutables que se han instalado en nuestra máquina son los siguientes:

- 10 -

Si hacemos doble click sobre la aplicación Corona Simulator, abrimos la interfaz principal para desarrollar con Corona SDK

- 11 -

En el menú de la aplicación podemos acceder a File -> Open.. y si accedemos a la carpeta Sample Codes del directorio donde hemos instalado Corona SDK, podemos observar un gran número de ejemplos. Prueba a ejecutar algunos ejemplos que vienen por defecto con Corona SDK para visualizar cómo se ven reflejados en el simulador. Ampliación 1 Cambia el tipo de simulador en el que se ejecuta la aplicación accediendo al menú de Corona SDK en Window -> View As… Ejercicio 2 En vez de ejecutar la aplicación Corona Simulator, vamos a ejecutar la aplicación Corona Terminal. Prueba a seguir los mismos pasos que el ejercicio anterior y observa los mensajes que van apareciendo en el terminal. Para acceder al código que se está ejecutando sólo hay que abrir el menú de la aplicación en la ruta File -> Open project in Editor...

- 12 -

Tema 2 Empezar a programar con LUA LUA • Lua es un lenguaje de programación extensible el cual debe funcionar dentro de un programa contenedor o simplemente anfitrión. • Se pretende que Lua sea usado como un lenguaje de script potente y ligero para cualquier programa que lo necesite. • Los programas en Lua no son interpretados directamente, sino compilados a código bytecode, que es ejecutado por la máquina virtual de Lua. • El proceso de compilación es normalmente transparente al usuario y se realiza en tiempo de ejecución, pero puede hacerse con anticipación para aumentar el rendimiento y reducir el uso de la memoria al prescindir del compilador.

LUA -Variables • Los nombres de las variables pueden ser definidos con cualquier combinación de letras, dígitos y caracteres de subrayado (underscore) siempre y cuando no comiencen por un dígito. • En Lua hay diferencia entre mayúsculas y minúsculas a la hora de definir los nombres de las variables and - palabra reservada del sistema And y AND - nombres diferentes y válidos • Como convención, las nombres de variables que empiezan por un carácter de subrayado seguido por letras mayúsculas están reservadas para su uso como variables globales internas de Lua (ej: _VERSION) • Lua es un lenguaje dinámicamente tipado, es decir, las variables no tienen tipo, sólo tienen tipo los valores. No existen definiciones de tipo en el lenguaje ya que cada valor almacena su propio tipo. - 13 -

• Existen 8 tipos básicos en Lua: nil, boolean, number, string, function, userdata, thread y table • El tipo nil es el valor nulo, cuya principal función es la de ser diferente a cualquier otro valor • boolean es el tipo de los valores true (verdadero) o false (falso) • Tanto nil como false hacen que una condición sea falsa, cualquier otro valor la hace verdadera • number representa números reales en coma flotante y doble precisión • String representa una tira de caracteres. Lua trabaja con 8 bits. Los strings pueden contener cualquier carácter de 8 bits. • El tipo userdata se incluye para permitir guardar en diferentes espacios de memoria distintas funciones creadas en C. No tienen asociadas operaciones predefinidas en Lua, excepto la asignación y el test de identidad. • El tipo thread representa procesos de ejecución y es usado para implementar co-rutinas. Estas rutinas no deben ser confundidas con las propias del sistema operativo, ya que Lua permite la gestión de diferentes procesos incluso en aquellos sistemas operativos que no lo soporten. • El tipo table implementa arrays asociativos, es decir, arrays que no sólo pueden ser indexados con números, si no que pueden como clave cualquier valor diferente de nil. • Debido a que las funciones en Lua son valores de primera clase los campos de las tablas pueden contener funciones, por lo que las tablas pueden contener también métodos.

- 14 -

• Las variables pueden ser de tres tipos: locales, globales y campos de tabla • Por defecto todas las variables son de tipo global a no ser que sean declaradas explícitamente como locales, con la palabra reservada local. • Las variables locales tienen un ámbito (scope) definido. Se puede acceder a ellas desde dentro de las funciones definidas en su mismo ámbito.

LUA -Tablas • Las tablas son variables utilizadas para almacenar dentro otros valores. Sería el tipo equivalente a lo que conocemos como arrays en otros lenguajes de programación. • Para inicializar nuestras tablas tenemos dos posibilidades: miTabla = {} miTabla = { 1, 4, “Test”, true}

creamos una tabla vacía en este caso le asignamos directamente 4 elementos nada más crearla. Los índices son de tipo numérico

miTabla = { latitud = 30.05, longitud = -3.07, direccion = “C/ Prueba 123”} También asignamos directamente los valores, pero en este caso los índices no son de tipo numérico

- 15 -

• Para acceder a los valores almacenados en una tabla también podemos proceder de varias maneras: miTabla = { 1, 4, “Test”, true} miTabla [3]

accedemos al valor de la tabla situado en la posición 3, en este caso Test

miTabla = { latitud = 30.05, longitud = -3.07, direccion = “C/ Prueba 123”} miTabla.latitud

en este caso accedemos al valor de la tabla cuya clave es latitud. En este caso, 30.05

LUA -Tablas (funciones) • Podemos conocer el tamaño de una tabla con el operador # miTabla = { 1, 4, “Test”, true}

la sentencia #miTabla nos devolverá el tamaño de la tabla. En este caso 4

• Con la función table.sort ordenamos los valores de la tabla ignorando las claves. (La ordenación se basa en el algoritmo quicksort) tablaCiudades = {"Zaragoza", "Barcelona", "Madrid", "Valencia", "Almeria"} table.sort (tablaCiudades) for i=1, #tablaCiudades, 1 do print (tablaCiudades[i]) end

Nos muestra el siguiente resultado en el Terminal: Almeria Barcelona Madrid Valencia Zaragoza - 16 -

• La función table.concat recoge una tabla de strings o de números y devuelve otro string concatenando todos los valores de la tabla. tablaCiudades = {"Zaragoza", "Barcelona", "Madrid", "Valencia", "Almeria"} concatenada = table.concat (tablaCiudades) En la variable concatenada obtendremos el valor “ZaragozaBarcelonaMadridValenciaAlmeria”

• Con table.remove eliminamos el último elemento de la tabla y el valor es devuelto por la función ultimoValor = table.remove (tablaCiudades) En la variable ultimoValor obtendremos el valor “Almeria”

• Para recorrer una tabla en Lua, tenemos diferentes opciones. La primera y más sencilla es usando un bucle for a partir de la longitud de la tabla tablaCiudades = {"Zaragoza", "Barcelona", "Madrid", "Valencia", "Almeria"} Resultado en el Terminal:

for i=1, #tablaCiudades do print (tablaCiudades [i]) end

Zaragoza Barcelona Madrid Valencia Almeria

• Otra forma para recorrer la tabla, obteniendo el mismo resultado es utilizando la estructura for ... inpairs… Cada iteración del bucle, la variable ciudad toma el valor de la tabla correspondiente

for i, ciudad in ipairs(tablaCiudades) do print(ciudad) end

- 17 -

LUA - Control de flujo if [condicion] then else | elseif [condicion] • La sentencia if comprueba la condición y ejecuta la parte del then si esta condición se cumple, si no, se ejecuta la parte del else, la cual es opcional if a < 0 then a=0 end

Si el valor de la variable a es menor que cero, se le asigna el valor cero. Si es mayor que cero, no se hace nada

if a < 0 then a=0 else a = 20 end

Si el valor de la variable a es menor que cero, se le asigna el valor cero. Si es mayor que cero, se le asigna el valor 20

• Para evaluar distintas condiciones, utilizaremos la sentencia if anidada utilizando elseif if n == 1 then print (“N es 1”) elseif n == 2 then print (“N es 2”) elseif n == 3 then print (“N es 3”) end

Dependiendo del valor de N, obtendremos el mensaje correspondiente a través del Terminal

while [condicion] do • Se ejecuta el proceso incluido dentro del bucle hasta que la evaluación de la condición devuelva false n=2 while n New Project y completamos los datos que nos piden: -App Name: HolaMundo -Choose Template: Blank -Screen Size: Phone preset | Width: 320 | Height: 480 -Default Orientation: Upright Una vez creado el proyecto y seleccionado el directorio donde lo vamos a guardar, seleccionamos abrir el editor, para poder modificar nuestro código. En este momento, tendremos algo así, en nuestro fichero main.lua: ------------------------------------------------------------------------------------------- main.lua – ----------------------------------------------------------------------------------------- Your code here

Para terminar nuestro primer proyecto Lua, podemos agregar la siguiente línea de código: ------------------------------------------------------------------------------------------- main.lua – ----------------------------------------------------------------------------------------- Your code here print ("Hola mundo Lua")

- 21 -

Salvamos el fichero main.lua y automáticamente, el simulador detecta cambios y nos refresca la vista del modelo de móvil que tengamos seleccionado. Para poder observar el mensaje que hemos programado, debemos acceder al Terminal. Ejercicio 2 A partir de la siguiente tabla: local miTabla = {“Pajaro”, “Perro”, “Tiburón”, “Pulpo”} Crea un método que reciba como parámetro un número e imprima en el Terminal la posición de la tabla correspondiente a ese número Ejercicio 3 Crea un método que reciba como parámetro un número N y muestre por el Terminal los N primeros números primos.

- 22 -

Tema 3 Formas, Imágenes y Texto Formas Geométricas Básicas • Desde la API de Corona tenemos la posibilidad de acceder a una serie de métodos que nos permiten mostrar elementos geométricos en nuestros dispositivos. • Los elementos que podemos representar mediante métodos de Lua en el simulador son: líneas, rectángulos, círculos, elipses y rectángulos redondeados display.newLine ( x1, y1, x2, y2) • Con el método newLine podemos especificar una línea recta entre dos puntos • Los parámetros (x1, y1) indican el punto de inicio de la recta. Los parámetros (x2, y2) indican el punto final de la recta. • El color por defecto es el blanco

display.newLine( 120, 100, 300, 120)

• Podemos especificar el color y el tamaño de la recta con el método setColor y el atributo width • Los colores deben especificarse con valores específicos para el Rojo, Verde y Azul. El número que debe indicarse debe estar entre 0 y 255.

- 23 -

local recta = display.newLine( 100,50, 270,350 ) recta:setColor( 1, 0, 0 ) recta.width = 6

display.newRect (left, top, width, height) • Con este método especificamos la creación de un rectángulo en nuestra aplicación • Los parámetros left y top especifican la situación de la coordenada del centro de la figura. Los parámetros width (anchura) y height (altura) especifican el tamaño de nuestro rectángulo.

display.newRect ( 200, 250, 150, 50)

- 24 -

• Si almacenamos la creación de nuestro rectángulo en una variable, podemos modificar el relleno y el trazo.

local miRect = display.newRect(175, 250, 200, 100) miRect.strokeWidth = 6 miRect:setFillColor(0, 1, 0) miRect:setStrokeColor(1, 0, 0)

• Con el método strokeWidth especificamos el tamaño del trazo del rectángulo • Con setFillColor definimos el color de relleno del rectángulo • El método setStrokeColor define el color del trazo • Los colores se definen indicando los valores de Rojo, Verde y Azul con números entre 0 y 1. No obstante, si se define un único parámetro se trabajará con una escala de grises, siendo 1 el blanco y 0 el negro. display.newRoundedRect( left, top, width, height, cornerRadius ) • Funciona como el método anterior, sólo que en este le tenemos que especificar el radio de curvatura en las esquinas del rectángulo con el último parámetro.

local miRoundRect = display.newRoundedRect(135, 200, 150, 150, 20) miRoundRect.strokeWidth = 3 miRoundRect:setFillColor( 1, 1, 0) miRoundRect:setStrokeColor(0, 0, 1) - 25 -

display.newCircle( xCenter, yCenter, radius ) • Este método nos permite dibujar un círculo especificando el punto central del círculo (xCenter, yCenter) y el radio (radius)

local miCirc = display.newCircle( 150, 200, 100 )

Imágenes display.newImage( filename [,centerx, centery] ) • Con este método podemos situar en nuestra aplicación imágenes. Para ello debemos incluir las imágenes en el directorio de nuestra aplicación y a través del parámetro filename debemos especificarle la ruta exacta donde poder encontrarlas. • Mediante top y left, que son parámetros opcionales, podemos especificar las coordenadas donde vamos a situar la imagen. Si no especificamos coordenadas, el centro de la imagen se sitúa en la coordenadas (0,0) de la vista que la contenga.

- 26 -

display.newImage ("img/corona.jpg", 200, 250)

local imagen = display.newImage( "img/corona.jpg" ) imagen:translate( 80, 200 ) • Creamos una imagen y con translate le asignamos una nueva posición. imagen:setFillColor( 1, 0, 0 ) • Se le asigna un color de relleno a la imagen, siguiendo la asignación de valores para Rojo, Verde y Azul (valores entre 0 y 255). imagen.isVisible = true • Propiedad de la imagen para mostrarla (true) u ocultarla (false) dentro de la vista donde se encuentra imagen:removeSelf()

Cargando imágenes remotas - 27 -

display.loadRemoteImage ( url, method, listener, destFilename [, baseDir] [, x, y] ) • Accedemos, a través de este método a una imagen alojada en un servidor de internet para mostrarla en nuestra aplicación. • Mediante el parámetro url le especificamos la dirección donde se encuentra alojada la imagen. • Con el parámetro method indicamos cómo se va a realizar la petición sobre la imagen. Tiene dos valores posibles, GET (valor por defecto) y POST • En el parámetro listener tenemos que especificar el nombre de un método en el que vamos a poder tratar la respuesta de la petición al servidor • El método que especifiquemos en el parámetro listener se llamará una vez se complete la petición http para la descarga de la imagen. • En este método tendremos acceso a un objeto de tipo event dentro del cual podremos acceder a 3 propiedades diferentes: event.response - un string que contiene la ruta de destino del fichero creado. event.target - el nuevo objeto creado una vez se descarga la imagen. event.isError - un valor de tipo booleano que indica si se ha producido un error de red (true) o no (false). • En el parámetro destFilename indicamos cual será el nombre de la imagen descargada dentro del sistema de archivos propio de nuestro dispositivo. • El parámetro baseDir, que es opcional, nos sirve para indicar dónde vamos a guardar la nueva imagen dentro del sistema de archivos de nuestro dispositivo. • Podemos definir las coordenadas exactas donde vamos a situar nuestra imagen, con los parámetros opcionales x e y.

- 28 -

local function networkListener( event ) if ( event.isError ) then print ( "Error en la red - Fallo en la descarga" ) end end display.loadRemoteImage( "http://developer.anscamobile.com/demo/hello.png", "GET", networkListener, "helloCopy.png", system.DocumentsDirectory, 160,200)

• Estamos lanzando una petición GET sobre la url indicada para que nos devuelva la imagen. La respuesta de esta imagen la estamos tratando en el método networkListener y le indicamos que se guarde en el directorio de documentos por defecto con el nombre helloCopy.png. La situamos en el punto (160,200) dentro de nuestra vista.

Manipulando imágenes • Contamos con una serie de métodos que nos permiten modificar las propiedades más importantes de nuestras imágenes: - 29 -

local img = display.newImage ("img/corona.jpg") img.x = 200 img.y = 300

• Podemos modificar la posición de una imagen accediendo a sus atributos x ey • Con el atributo alpha, podemos especificar la transparencia de la imagen. El valor que se indica varía entre 0 y 1

local img = display.newImage ("img/corona.jpg", 200, 175) img.alpha = 0.6

• Podemos definir la escala en la que se muestra la imagen accediendo a los atributos xScale e yScale.

- 30 -

local img = display.newImage ("img/corona.jpg", 160,230) img.xScale = 3 img.yScale = 5

• Podemos modificar la rotación de la imagen creada modificando el atributo rotation. El valor se debe especificar en grados.

local img = display.newImage ("img/corona.jpg", 200, 175) img.rotation = 70

• Para modificar el tamaño de la imagen tenemos los atributos width (ancho) y height (alto)

- 31 -

local img = display.newImage ("img/corona.jpg", 190,175) img.width = 250 img.height = 50

Máscaras sobre imágenes graphics.newMask( filename ) • Con este método podemos crear máscaras a partir de una imagen, la cual definimos a través de filename, para aplicárselas a otras imágenes diferentes. • La imagen que actúa como máscara, tiene unas características concretas que debe cumplir: • Su altura y su anchura, deben ser números múltiplos de 4 • La imagen de máscara debe llevar un borde negro alrededor de,

Máscara

Imagen sobre la que se aplica la máscara

- 32 -

local img = display.newImage ("img/corona.jpg", 175,200) img.width = 190 img.height = 190 local mask = graphics.newMask ("img/circlemask.png") img:setMask (mask)

Manejando Texto display.newText( string, Xcenter, Ycenter, [width, height,] font, size ) • Con el método newText podemos mostrar una cadena de texto (string) situada en las coordenadas Xcenter, Ycenter, de la vista que lo contiene. • Como parámetros opcionales, podemos definir el tamaño del texto que vamos a mostrar con width y height • A través del parámetro font podemos definir la fuente que vamos a usar para el texto. Se puede especificar las fuentes del sistema por defecto con native.systemFont y native.systemFontBold • Para obtener una lista con todas las fuentes disponibles, podemos ejecutar el código siguiente, con la llamada al método native.getFontNames() local fuentes = native.getFontNames() for i=1, #fuentes, 1 do print (fuentes[i]) end

- 33 -

• Con el parámetro size, especificamos el tamaño del texto, definido en píxeles local myText = display.newText("Hello World!", 160, 100, native.systemFont, 36)

- 34 -

Ejercicios Tema 3 Ejercicio 1 A partir de lo aprendido en el tema 3, vamos a dibujar un rectángulo marrón en un nuevo proyecto de Corona

local rect = display.newRect (150,300,60,100) rect:setFillColor (0.56,0.33,0.15)

Ahora vamos a finalizar nuestra obra maestra dándole un toque de naturaleza al dibujo. Ya que tenemos el tronco, vamos a dibujar la copa del árbol. Para dibujar polígonos más complejos, Corona nos ofrece la posibilidad de utilizar las llamadas Polylines. Podemos dibujar una línea con los método ya conocidos y posteriormente, agregarle, todos los puntos que queramos (mediante un par de coordenadas) para completar todos los vértices del polígono que queramos crear. local copaArbol = display.newLine (120, 250, 90, 230) copaArbol:append(80,200, 100,150, 170,140, 220,200, 180,250, 120,250) copaArbol:setColor (0, 1, 0)

- 35 -

El polígono creado para la copa del árbol no se puede rellenar sin usar complejos algoritmos en los que no vamos a entrar de momento. Trata de completar el ejercicio para obtener un dibujo como el de la imagen

- 36 -

Ejercicio 2 Intenta realizar el siguiente dibujo. No utilices coordenadas absolutas para ello. Puedes usar las coordenadas (x, y) y las propiedades width y height de cada objeto para situar los demás. NOTA: con display.contentWidth y display.contentHeight puedes obtener el ancho y el alto del dispositivo

Ejercicio 3 En este último ejercicio vamos a insertar un gradiente por debajo de nuestro dibujo anterior ç

- 37 -

Para ello, podemos usar la función de la librería graphics newGradient. local g = graphics.newGradient( color1, color2 [ , direction] ) color1 y color2 son dos tablas que determinan el color de inicio y el color de fin (Ej: {1,1,1}) direction determina la dirección del gradiente (“up”, “down”, “left”, “right”) el gradiente creado se le puede añadir a un rectángulo a través de su función setColor

- 38 -

Tema 4 Eventos e interacción Detectando eventos • Los Eventos son la principal fuente de información desde la cual podemos recoger datos y así crear una interacción con el usuario de nuestra aplicación. • Existen muchas maneras para detectar los Eventos. Por ejemplo, cada uno de los elementos de nuestra aplicación puede ser tratado como un botón interactivo. Esto nos ofrece una gran flexibilidad a la hora de estructurar nuestras interfaces gráficas. • Durante este tema, aprenderemos cómo agregar diferentes eventos sobre los objetos mostrados en nuestra vista, así como la forma de manejar eventos de tipo global, como puede ser la orientación de nuestro dispositivo.

La función addEventListener Objeto:addEventListener ( eventName, listener) • Con esta función vamos a poder indicar qué evento vamos a registrar sobre un objeto en concreto y cómo vamos a proceder a tratarlo. • En este caso, Objeto se trataría de la entidad sobre la que vamos a recoger el evento en el que estamos interesados. Puede ser cualquier elemento que tengamos situado en la vista de nuestra aplicación. • Con el parámetro eventName podemos especificar el tipo de evento que vamos a recoger. • En listener debemos especificar el nombre de la función que vamos a ejecutar una vez tenga lugar el evento que estamos capturando. • Para dejar de escuchar un evento concreto sobre un objeto, debemos usar el método removeEventListener

- 39 -

Eventos Globales • Hablamos de Eventos Globales cuando hacemos referencia a ese tipo de eventos que no están directamente relacionados con ningún objeto en concreto. • Envían información a todos los listeners (métodos para capturar los eventos) que se hayan implementado para tratarlos. • A través de estos eventos podemos acceder a la información de la orientación de nuestro dispositivo, el estado de nuestra aplicación, el uso del acelerómetro y la brújula... • Al no asignarse sobre ningún objeto en concreto, tendremos que asignarlos a la clase Runtime enterFrame • Este evento se lanza cada vez que se actualice la vista de nuestra aplicación. Por defecto son 30fps, pero este valor se puede modificar si accedemos al archivo config.lua que se crea en la carpeta de nuestro proyecto. • Capturando este evento podríamos, por ejemplo modificar el contenido de nuestra pantalla en tiempo real en función de las acciones del usuario. function capturarEvento (event) print (event.name) print (event.time) end Runtime:addEventListener("enterFrame", capturarEvento )

System • Este evento se lanza para avisar a nuestra aplicación de eventos externos, como cuando la aplicación tiene que entrar en suspenso por la entrada de una llamada telefónica.

- 40 -

• Podemos acceder a la propiedad type del evento para saber qué se está lanzando. Los diferentes tipos para el evento system son: applicationStart - Ocurre cuando la aplicación se lanza y todo el código en main.lua se ha ejecutado. applicationExit - Se captura cuando el usuario cierra la aplicación. applicationSuspend - Ocurre cuando la aplicación debe suspenderse por la entrada de una llamada o porque el dispositivo entra en inactividad por falta de uso. Mientras que la aplicación está suspendida no recibe eventos de ningún tipo. applicationResume - Este evento se lanza cuando la aplicación vuelve a ejecución después de un estado de suspensión. • Con el siguiente ejemplo detectamos cuando arranca nuestra aplicación y mostramos un mensaje. Esto puede ser útil para configurar todos los parámetros iniciales de nuestra aplicación function capturarEvento (event) if (event.type == "applicationStart") then print ("Acaba de arrancar la aplicacion") end end Runtime:addEventListener("system", capturarEvento )

Orientation • Con este tipo de evento podemos detectar los cambios en la orientación de nuestro dispositivo. Sólo estará disponible para teléfonos que tengan soporte para acelerómetro. • Los diferentes tipos (event.type) de orientación a los que podemos acceder son: portrait, landscapeLeft, landscapeRight, portraitUpsideDown, faceUp, faceDown. • Si accedemos a la propiedad delta del evento, obtenemos la diferencia - 41 -

entre la orientación inicial y la final.

local label = display.newText( "portrait", 100, 200, nil, 30 ) local function onOrientationChange( event ) label.text = event.type local newAngle = label.rotation - event.delta transition.to( label, { time=150, rotation=newAngle } ) end Runtime:addEventListener( "orientation", onOrientationChange )

accelerometer • El evento de acelerómetro nos permite capturar movimientos repentinos de nuestro dispositivo y determinar su orientación en relación a la gravedad. • Este evento sólo puede ser tratado por dispositivos con soporte para acelerómetro. • Podemos capturar la aceleración del dispositivo de dos maneras, en relación a la gravedad, con los parámetros xGravity, yGravity y zGravity, o la aceleración instantánea con los parámetros del evento xInstant, yInstant y zInstant. • Si el parámetro del evento isShake es verdadero, nos indica que el usuario está agitando el dispositivo. local label = display.newText( "En reposo", 100, 200, nil, 30 ) local function detectaAcelerometro( event ) if (event.isShake == true) then label.text = "Agitado" end end Runtime:addEventListener( "accelerometer", detectaAcelerometro ) - 42 -

Heading • En este caso vamos a ser capaces de detectar los eventos generados por la brújula del dispositivo, si está disponible. Hay que tener en cuenta que el simulador de Corona no detecta este tipo de eventos • Podemos acceder a dos atributos del evento que nos devuelven la desviación en grados con respecto al norte geográfico (geographic) o al norte magnético (magnetic). Android sólo soporta el acceso al atributo magnetic. heading

local function detectaBrujula( event ) print (event.magnetic) end Runtime:addEventListener( "heading", detectaBrujula )

memoryWarning • Este evento sólo está disponible para iOS • Se ejecuta cuando recibimos un aviso por algún error determinado de memoria. Apple sugiere capturar este evento y en caso de ser lanzado, liberar el mayor número de memoria posible para subsanarlo • El evento no contiene ningún atributo. local function capturandoMemoriaBaja( event ) print( "Recibido aviso de memoria" ) end Runtime:addEventListener( "memoryWarning", capturandoMemoriaBaja )

- 43 -

Touch Events • Cuando el usuario toca la pantalla se genera un evento de toque sobre el objeto que ha sido pulsado. El evento puede ser interceptado y tratado por cualquier objeto disponible en nuestra vista. • Los eventos se propagan entre todos los objetos con capacidad para tratarlo hasta que uno de ellos lo capture y ejecute el código correspondiente para manejarlo. • La propagación del evento se realiza siguiendo la jerarquía de disposición de los objetos en nuestra vista, es decir, el primero en recibirlo será el que se encuentre encima de todos los demás. • Si el evento no ha sido capturado por ninguno de los objetos de nuestra vista, se propaga como un evento global para poder ser capturado por el objeto Runtime Touch • Detectamos cuando el usuario pulsa sobre el objeto al que le hemos asignado el listener. • Cuando se produce el toque, se lanza una secuencia de estados que podemos capturar para poder realizar diferentes acciones en cada una de las fases. • Las propiedades del evento a las que podemos acceder son las siguientes: event.x - es la posición x de las coordenadas del toque dentro de la pantalla event.y - es la posición y de las coordenadas del toque dentro de la pantalla event.xStart - se trata de la coordenada x donde ha comenzado el toque event.yStart - la coordenada y donde ha comenzado el toque event.phase - Las diferentes fases del toque touch

- 44 -

• Los diferentes estados a los que podemos acceder dentro del evento de toque son los siguientes: began - nos indica que un dedo empieza a tocar la pantalla del dispositivo moved - un dedo se desliza por la pantalla ended - un dedo deja de tocar la pantalla cancelled - el sistema cancela el seguimiento del toque

local img = display.newImage( "img/imagina.jpg", 100, 200) local function capturaEventoToque(event ) if (event.phase == "began") then print ("Comienza el toque") elseif (event.phase == "moved") then img.x = event.x img.y = event.y elseif (event.phase == "ended") then print ("Fin del toque") end return true end img:addEventListener( "touch", capturaEventoToque )

touch (multitouch) • Lo primero que debemos hacer para permitir los eventos con varios toques sobre la pantalla es decírselo a nuestra aplicación con la siguiente sentencia: system.activate( "multitouch" )

- 45 -

• Una vez tengamos esto activado, cuando se reciban múltiples toques, estos se trataran individualmente, recogiéndose en sus respectivos listeners. Lo único que tendrán en común, será su propiedad de timestamp.

Creación de eventos propios • En Corona podemos crear eventos propios y asignárselos para su captura sobre objetos de nuestra aplicación o sobre el propio objeto Runtime • En cualquiera de los dos casos, se debe asignar el evento sobre el objeto que lo captura, utilizando el siguiente método: object:dispatchEvent(event)

• El parámetro event debe ser una tabla donde se especifique el identificador del evento (name) y el objeto que será el encargado de recibir dicho evento (target)

-- Creamos un objeto para poder asignarle el evento local imagen = display.newImage("img/imagina.jpg",100,100 ) -- Configuramos el listener local myListener = function( event ) print( "Evento " .. event.name ) print( "Target tiene un ancho de: " .. event.target.contentWidth ) end imagen:addEventListener( "myEventType", myListener ) -- Creamos el evento y lo asignamos local event = { name="myEventType", target=imagen } imagen:dispatchEvent( event

- 46 -

Ejercicios Tema 4 Ejercicio 1 Vamos a crear un botón que nos permita mostrar una imagen. Para ello vamos a dibujar un botón en nuestra vista local rect = display.newRoundedRect( 160, 150, 100, 50, 20) rect:setFillColor( 1, 0, 0)

Necesitamos capturar el evento de toque sobre el botón que acabamos de crear: rect:addEventListener( "touch", listener )

Por último, tenemos que implementar el método que ejecutaremos al capturar el evento de toque, el cual nos insertará una imagen en nuestra vista

- 47 -

local function listener(event) if event.phase == "began" then local img = display.newImage ("corona.png", 0,0) img.width =200 img.height = 200 img.x = 150 img.y = 300 end return true end

Ejercicio 2 A partir del siguiente diseño que debes crear en tu proyecto:

- 48 -

Debes detectar los cambios de orientación del dispositivo, para organizar el contenido de la siguiente manera:

Puedes usar los métodos rotate y translate de los objetos para situarlos dependiendo de la orientación.

- 49 -

Tema 5 Timers y Animaciones Timer • Muchas veces puede ser de utilidad para nuestra aplicación ejecutar cierta función en el futuro en vez de inmediatamente. • Para ello tenemos la librería timer que nos provee de varios métodos para poder llevar a cabo esta tarea. timer.performWithDelay( delay, listener [, iterations] ) • Con el método performWithDelay de la librería timer podemos ejecutar una función, la cual especificaremos a través del parámetro listener con un retraso de los segundos que le asignemos mediante el parámetro delay. • El parámetro opcional iterations podemos usarlo para concretar el número de veces que queremos que se repita la ejecución de la función. • Por ejemplo, si ponemos que el valor de iterations es 3, se ejecutaría la función que hayamos especificado en listener 3 veces con un retraso de tantos segundos como hayamos puesto en el parámetro delay. local function holaMundo() print( "¡Hola Mundo!" ) end timer.performWithDelay( 5000, holaMundo, 5 )

• En este ejemplo vamos a ejecutar la función holaMundo 5 veces. Cada ejecución se lanzará pasados 5 segundos con respecto a la anterior. • Como el segundo parámetro del método especifica que se debe usar un objeto de tipo funcion, podemos utilizar una función anónima para especificar el código que queremos ejecutar cada X tiempo. • Una función anónima es aquella que no tiene nombre, es decir, que no queda ligada a ninguna variable. - 50 -

• El código a continuación realiza las mismas acciones que el primer ejemplo que hemos expuesto. timer.performWithDelay( 5000, function() print( "¡Hola Mundo!" ) end, 5 )

• El parámetro listener sólo acepta como valor la referencia a una función, es decir, su nombre, es por ello que, directamente no podemos pasarle parámetros cuando programamos su ejecución a través de timer. • Esto podemos solucionarlo juntando en un solo ejemplo las dos maneras que hemos aprendido para ejecutar el método performWithDelay local function holaMundo( arg1, arg2 ) local function holaMundo( arg1, arg2 ) print( "¡Hola Mundo!", arg1, arg2 ) end timer.performWithDelay( 5000, function() holaMundo( "primero", "segundo" ) end, 5 )

Timer - Otras funciones • A parte de poder lanzar funciones cada cierto tiempo, la librería timer nos ofrece otros métodos para poder interactuar con las ejecuciones futuras que realicemos: timer.pause ( timerId ) • Este método se encarga de pausar un timer (timerId) creado con performWithDelay. Devuelve un Number que representa el tiempo que le falta al timer timer.resume ( timerId ) • Con este método podemos volver a arrancar la ejecución de un timer - 51 -

(timerId) sobre el que anteriormente hubiéramos ejecutado el método pause timer.cancel ( timerId ) • Se cancela la ejecución de un timer (timerId) que hubiéramos creado con timer.performWithDelay()

Transitions • Una de las características más valoradas de Corona, es que todos los objetos que mostremos en nuestras vistas pueden ser animados. • Las animaciones se realizan mediante la generación de una secuencia de frames que van pasando suavemente de unos a otros. • El término tween hace referencia a cómo estas transiciones se van a generar. También es usado en ocasiones para indicar que la propiedad de un objeto va a ser modificada. • La librería Transitions nos permite crear animaciones con una única línea de código con la posibilidad de modificar una o más propiedades de alguno de los elementos mostrados en nuestras vistas. • La manera más sencilla para realizar esta acción es usando el método transition.to. • Mediante este método podemos especificar los cambios que queramos sobre las propiedades que nos interesen de cualquiera de los objetos que tengamos en nuestras vistas. Transitions transition.to (target, params) • El parámetro target hace referencia al objeto sobre el que vamos a aplicar la transición • En params especificamos los valores sobre los que se va a ejecutar la animación. Podemos usar las siguientes opciones no-animables:

- 52 -

- params.time - especifica el la duración de la transición enmilisegundos - params.transition - por defecto la transición es lineal - params.delay - indica el retardo hasta que comience la transición - params.delta - se trata de un valor booleano que indica si los parámetros animables son tomados como valores finales o como cambios sobre lo que ya existía. - params.onStart - función que se ejecuta cuando comienza la animación. - params.onComplete - función ejecutada cuando se completa la animación • El siguiente código nos muestra cómo realizar una simple animación. Mostramos un cuadrado en nuestra vista modificamos sus parámetros. local w,h = display.contentWidth, display.contentHeight local square = display.newRect( 0, 0, 100, 100 ) square:setFillColor( 1,1,1 ) local w,h = display.contentWidth, display.contentHeight transition.to( square, { time=1500, alpha=0, x=(w-100), y=(h-100), width=200, height=200 } ) transition.to( square, { time=500, delay=2500, alpha=1.0 } )

• Con este método facilitamos mucho el proceso para la creación de animaciones. Comparamos estos dos métodos de animar un rectángulo:

- 53 -

Movieclips • Movieclip es una librería externa que nos permite crear animaciones de tipo sprite (una serie de imágenes que ejecutadas en serie crean una animación) • Esta solución es una manera muy sencilla y poco pesada de desarrollar animaciones muy vistosas. Nos ayuda también para importar a Corona contenido ya existente en Flash. • Para usar la librería Movieclip, debemos descargarla desde el siguiente enlace:

Movieclips • Antes de poder usar la librería en nuestros proyectos, debemos importarla a nuestro código, de la siguiente manera: local movieclip = require (“movieclip”)

• Con esto declaramos que vamos a usar una librería llamada movieclip.lua, la cual debe estar localizada en el mismo directorio que el archivo .lua donde la estemos importando. movieclip:newAnim (frames) • Con este método creamos la animación a partir de un array de imágenes - 54 -

que serán, en el orden que ocupen, las encargadas de conformar nuestro movieclip. myAnim = movieclip.newAnim{ "img1.png", "img2.png", "img3.png", "img4.png" }

• Si no especificamos una ruta concreta dentro de nuestro proyecto, las imágenes contenidas en el array que da forma a la animación deben estar en la misma ruta que el fichero .lua que contenga el movieclip.

movieclip:play() • Da comienzo a la animación en el orden insertado en la creación del movieclip. • Si se ha pausado la animación en algún frame, llamando al método frame sin parámetros podremos reanudarla. movieclip:play { startFrame = X, endFrame = Y, loop = Z, remove = true } • De esta manera determinamos que comience la animación indicándole el frame de comienzo (startFrame), el frame de fin (endFrame), el número de veces que se debe ejecutar la animación (loop) y si se debe eliminar el movieclip a si mismo cuando se termine la animación movieclip:reverse () • Arranca la animación pero en sentido contrario al establecido en su creación. • Una vez alcance el primer frame, volverá automáticamente al primero y reanudará el movieclip. movieclip:reverse { startFrame = X, endFrame = Y, loop = Z, remove = true } • Al igual que con play podemos indicar el frame de comienzo, el de finalización, el número de vueltas que tendrá la animación y si se debe - 55 -

eliminar una vez terminada. movieclip:nextFrame () • Aplicado sobre una animación, detiene su ejecución y muestra el siguiente frame en el orden indicado en la creación del movieclip. movieclip:previousFrame () • Detiene cualquier movieclip sobre el cual se ejecute la función y muestra el frame anterior con el orden indicado en su creación. movieclip:stop () • Detiene la animación en el frame que se encuentre siguiendo el orden inicial. movieclip:stopAtFrame (frame) • Detiene la aplicación en el frame indicado. • Podemos usarlo seguido de una llamada al método play y conseguiremos que la animación pase automáticamente al frame que necesitemos según nuestras necesidades movieclip:setDrag • Convierte cualquier objeto definido con movieclip a un objeto con capacidades para ser arrastrado por nuestra vista. • Podemos utilizar una serie de parámetros para definir limitar el objeto. Definiendo la propiedad drag = true indicamos que el objeto puede ser arrastrado. De la misma manera, podemos usarlo con false para quitarle esa propiedad. • Con limitX y limitY definimos los límites en el eje de las x y en el eje de las y hasta donde vamos a poder arrastrar nuestra animación. • El parámetro bounds define los límites del objeto que nos permiten - 56 -

arrastrarlo. Está definido como {left, top, width, height} movieclip:setDrag • Los parámetros onPress, onDrag y onRelease, nos indican los métodos que se deben ejecutar una vez se capturen esos tres eventos (pulsar, arrastrar y dejar de pulsar) myAnim:setDrag{ drag=true, limitX=false, limitY=false, onPress=myPressFunction, onDrag=myDragFunction, onRelease=myReleaseFunction, Bounds={ 10, 10, 200, 50 } }

- 57 -

Ejercicios Tema 5 Ejercicio 1 En este primer ejercicio vamos a tratar con una imagen de tipo sprite para realizar diferentes operaciones aprendidas en este tema. Necesitamos, para crear nuestro sprite una imagen de este tipo,

La cual podemos dividir en imágenes simples y, al ponerlas en serie muestren algún tipo de animación. Lo primero que debemos hacer es descargarnos la librería movieclip.lua del siguiente link e incorporarla a nuestro proyecto: http://developer.anscamobile.com/demo/Movieclip.zip local movieclip = require ("movieclip")

Para poder usar la librería, el fichero movieclip.lua, contenido dentro del proyecto que nos hemos descargado, debe situarse en el mismo directorio donde tengamos el archivo main.lua del nuevo proyecto que estamos creando. El siguiente paso es crear nuestra animación a partir de las imágenes que hemos agregado también al mismo directorio del proyecto: local animacion = movieclip.newAnim {“sonic1.png”, “sonic2.png”, “sonic3.png”, “sonic4.png”, “sonic5.png”, “sonic6.png”, “sonic7.png”, “sonic8.png”, sonic9.png”}

Podemos situar nuestra animación y posteriormente iniciarla.

- 58 -

animacion.x = display.contentWidth / 2 animacion.y = display.contentHeight / 2 animacion:play()

En este momento hemos situado nuestra animación en el centro de la pantalla y hemos iniciado su reproducción

Nuestro siguiente paso en el ejercicio será crear una serie de botones, los cuales ejecutarán una serie de transiciones sobre la animación que hemos creado. local botonRojo = display.newRoundedRect (60,400,90,30,10) botonRojo:setFillColor (1,0,0) local botonVerde = display.newRoundedRect (160,400,90,30,10) botonVerde:setFillColor (0,1,0) local botonAzul = display.newRoundedRect (260,400,90,30,10) botonAzul:setFillColor (0,0,1) botonRojo:addEventListener ("touch", pulsarBotonRojo) botonVerde:addEventListener ("touch", pulsarBotonVerde) botonAzul:addEventListener ("touch", pulsarBotonAzul) - 59 -

Se debe rellenar los tres métodos que capturan los eventos, siguiendo las siguientes pautas: -Mientras que se mantenga pulsado el botón rojo, la animación debe desaparecer. Una vez se deje de pulsar, la animación debe volver a aparecer. -Si mantenemos pulsado el botón verde, la animación se debe desplazar hasta la parte superior de nuestra vista. Una vez dejemos de pulsar, la animación volverá a su ubicación original. -Pulsando el botón azul, la animación debe rotar sobre su eje. Cuando dejemos de pulsar, debe volver a su posición original. AMPLIACIÓN Para este ejercicio vamos a crear una lluvia de objetos. Cada X tiempo crearemos un objeto y simularemos la animación para que recorra la pantalla de arriba abajo, simulando una lluvia. Debemos usar para esto los timer y sus funciones relacionadas aprendidas en este tema. NOTA: Para poder hacer que cada objeto se genere en una posición aleatoria, podemos usar el método math.random

- 60 -

Tema 6 Motor Físico • Corona facilita el uso de propiedades físicas con los elementos que dispongamos en las distintas vistas de nuestra aplicación. • Cualquier objeto, imagen o sprite puede ser tratado como una “unidad física” y automáticamente empezar a interactuar con otros objetos dentro de la simulación. • Corona traduce automáticamente las unidades típicas de medición que usamos para colocar nuestros objetos (píxeles), a una unidad métrica de medición (metros). • Todas las posiciones se especifican en píxeles, pero internamente se traducen en metros con una conversión de 30 píxeles por metro.

Motor Físico (Configuración) • Con la siguiente línea declaramos la posibilidad de usar el motor físico en nuestro código: local physics = require "physics"

• El uso de las propiedades físicas en Corona se pueden habilitar y deshabilitar en función de las diferentes necesidades de la aplicación. Incluso se puede pausar la utilización del motor físico. • Los siguientes métodos sirven para manejar el uso del motor físico durante nuestra aplicación. physics.start () • El método start inicializa o restaura el modelo físico de la aplicación. Se debe llamar a este método antes que cualquier otra función, para poder contar con las propiedades físicas de los objetos.

- 61 -

physics.stop () • El método stop destruye el mundo físico de la aplicación. physics.pause () • Utilizamos este método para pausar el motor físico en cualquier punto de la ejecución de nuestra aplicación. physics.setGravity (x , y) • Nos permite modificar el valor de la gravedad especificando los valores en el eje x y en el y. • Para simular la gravedad terrestre podemos usar los siguientes valores: physics.setGravity (0,9.8)

physics.getGravity () • Devuelve el valor x e y del vector global de la gravedad, en m/s2 gx, gy = physics.getGravity ()

• Podemos definir la gravedad de nuestra aplicación basándonos en la inclinación del dispositivo con una simple función que determine este movimiento utilizando el acelerómetro. local function onTilt( event ) physics.setGravity( 10 * event.xGravity, - 10 * event.yGravity ) end Runtime:addEventListener( "accelerometer", onTilt )

- 62 -

physics.setScale (x) • Realizando una llamada a este método podemos definir el coeficiente de conversión entre las coordenadas en píxeles con las que definimos nuestros objetos y las coordenadas físicas simuladas por Corona. • El valor por defecto es 30, lo que significa que los valores físicos situados entre 0.1m y 10m estarían cubiertos entre las coordenadas 3 y 300 de nuestra vista, aproximadamente. physics.setDrawMode (text) • Selecciona uno de los 3 posibles modos para el motor físico. “debug”, “hybrid” y “normal” • El modo se puede cambiar en tiempo de ejecución. Es recomendable su uso en el simulador ya que se pueden apreciar los diferentes comportamientos de los objetos.

- 63 -

Detección de Colisiones • Las colisiones entre eventos físicos en nuestra aplicación, deben ser tratados mediante la captura de una serie de eventos que controlan esta característica. • Para colisiones genéricas, debemos capturar el evento “collision”. • Este evento incluye fases incluye diferentes fases, “began” y “ended”, las cuales hacen referencia al momento de inicio de la colisión y al momento de fin, respectivamente. • Aparte del evento general de colisión, disponemos de otros dos eventos para poder controlar la colisión: “preCollision” - evento lanzado en el momento anterior de comenzar la interacción. “postCollision” - evento lanzado justo cuando termina la interacción entre los objetos presentes en la colisión. Se trata del único evento dentro de la colisión donde se reporta con qué fuerza se ha realizado la misma. Detección de Colisiones - Eventos Globales • Cuando realizamos la detección a nivel global, el evento incluye event.object1 y event.object2, los cuales contienen las propiedades de los dos objetos incluidos en la colisión. • Como sabemos, Corona trata los objetos como tablas LUA, por lo que podremos agregar, a nuestros objetos las propiedades que nos interesen a la hora de tratarlos en el código. • Por ejemplo, podemos añadir un nombre a una imagen que queramos mostrar en nuestra vista: local img1 = display.newImage( "imagen.png", 100, 200 ) img1.miNombre = "Imagen1" - 64 -

local physics = require ("physics") physics.start() local suelo = display.newImage( "suelo.png", 175, 450 ) physics.addBody(suelo, "static") suelo.myName = "suelo" local bloque1 = display.newImage( "bloque.png", 100, 200 ) physics.addBody( bloque1, { density = 1.0, friction = 0.3, bounce = 0.2 } ) bloque1.myName = "bloque1" local bloque2 = display.newImage( "bloque.png", 100, 120 ) physics.addBody( bloque2, { density = 1.0, friction = 0.3, bounce = 0.2 } ) bloque2.myName = "bloque2" local function onCollision( event ) if ( event.phase == "began" ) then print( "began: " .. event.object1.myName .. " & " .. event.object2.myName ) elseif ( event.phase == "ended" ) then print( "ended: " .. event.object1.myName .. " & " .. event.object2.myName ) end end Runtime:addEventListener( "collision", onCollision )

- 65 -

Detección de Colisiones - Eventos Locales • En este caso podemos acceder a los objetos participantes en la colisión mediante self y event.other ... local bloque1 = display.newImage( "bloque.png", 100, 200 ) physics.addBody( bloque1, { density = 1.0, friction = 0.3, bounce = 0.2 } ) bloque1.myName = "bloque1" local bloque2 = display.newImage( "bloque.png", 100, 120 ) physics.addBody( bloque2, { density = 1.0, friction = 0.3, bounce = 0.2 } ) bloque2.myName = "bloque2" local function onCollision( event ) if ( event.phase == "began" ) then print( "began: " .. self.myName .. " & " .. event.other.myName ) elseif ( event.phase == "ended" ) then print( "ended: " .. self.myName .. " & " .. event.other.myName ) end end bloque1.collision = onLocalCollision bloque1:addEventListener("collision", bloque1) bloque2.collision = onLocalCollision bloque2:addEventListener("collision", bloque2)

Detección de Colisiones – Fuerzas - 66 -

• Cuando termina una colisión podemos obtener la fuerza directa entre los dos objetos, así como la fuerza lateral entre los mismos, la cual es una fuerza de fricción. • Podemos acceder a la fuerza directa y la lateral dentro del evento postCollision, recuperando las propiedades event.force y event.friction.

local function onPostCollision( event ) if ( event.force > 1.0 ) then print( "Fuerza Directa: " .. event.force .. " Fricción: " .. event.friction ) end end Runtime:addEventListener( "postCollision", onPostCollision )

Detección de Colisiones - Collision Categories • Podemos determinar qué objetos pueden colisionar entre si utilizando las categorías y las máscaras. • En este caso se deben especificar en binario utilizando su notación decimal (1, 2, 4, 8, 16, 32...) • Cada tipo de objeto que definamos en nuestra aplicación podrá tener un filtro diferente en el que definiremos los valores de categoryBits (categoría a la que pertenece) y maskBits (categorías con las que puede colisionar) collisionFilter = { categoryBits = 1, maskBits = 6 } -- Con este filtro, colisionará con los objetos que tengan categoryBits 4 o 2 local redBody = { density=0.2, friction=0, bounce=0.95, radius=43.0, filter=collisionFilter } redBalloon = display.newImage( "red_balloon.png" ) physics.addBody( redBalloon, redBody )

- 67 -

• Para poder determinar los valores de máscara que debemos usar para cada uno de los filtros, podemos usar la siguiente tabla de colisiones:

• En la primera línea determinamos la categoría que le asignamos a cada uno de los objetos de nuestra aplicación. En la segunda especificamos con qué otros objetos puede colisionar. • En la última columna obtenemos la suma de las casillas rellenas. El primer valor corresponde al categoryBits y el segundo al maskBits. Corona SDK Cuerpos Físicos • Una de las características principales del motor físico de Corona es que nosotros podemos representar todos los objetos que necesitemos y Corona se encargará de re presentarlos físicamente en nuestra aplicación y devolvernos las propiedades características de cada uno de ellos. • Cuando añadimos un objeto al motor físico usando physics.addBody, no obtenemos un objeto nuevo, si no que ampliamos las propiedades del mismo. • Podremos seguir accediendo a las propiedades del objeto antes del cambio pero, en este caso, el objeto estará sujeto a todas las propiedades físicas proporcionadas por el motor de Corona, como puede ser la gravedad. Cuerpos Físicos – physics.addBody

- 68 -

• Con esta función transformamos un objeto simple de Corona en una simulación física del mismo capaz de interactuar con el motor físico de la aplicación. • Los cuerpos físicos tienen 3 propiedades principales: density: es multiplicada por el área del objeto para determinar su masa. friction: indica la resistencia del objeto con respecto a los demás bounce: hace referencia a la capacidad de rebote del objeto. El valor por defecto es 0.2. Valores por encima de 1.0 tendrán comportamientos indeterminados

• Por defecto, este constructor asume que el objeto creado es rectangular. local bloque = display.newImage( "bloque.png", 100, 200 ) physics.addBody( bloque, { density = 1.0, friction = 0.3, bounce = 0.2 } )

• La tabla con las propiedades físicas puede ser declarada fuera y usada en varios componentes. local bloque1 = display.newImage( "bloqueImagina.png", 100, 200 ) local bloque2 = display.newImage( "bloqueImagina.png", 180, 280 ) local materialBloque = { density = 1.0, friction = 0.3, bounce = 0.2 } physics.addBody( bloque1, materialBloque ) physics.addBody( bloque2, materialBloque )

• Los cuerpos circulares requieren un parámetro adicional radius. Es muy útil para usarlo en objetos como piedras, balones… local roca = display.newImage( "boulder.png", 100, 200 ) physics.addBody( roca, { density = 1.0, friction = 0.3, bounce = 0.2, radius = 40 } )

- 69 -

• Para crear polígonos más complejos, debemos especificar cada uno de los vértices como un par de coordenadas mediante la propiedad shape. Estas coordenadas deben declararse siguiendo el orden de las agujas del reloj. • El máximo número de vértices permitidos para un polígono es de 8. Una vez creada la forma, esta puede ser usada para varios objetos. • Este sería el ejemplo para crear la forma física de un pentágono. local pentagon = display.newImage("pentagon.png") pentagon.x = 200 pentagon.y = 50 PentagonShape = { 0,-37, 37,-10, 23,34, -23,34, -37,-10 } physics.addBody( pentagon, { density=3.0, friction=0.8, bounce=0.3, shape=pentagonShape } )

• Podemos declarar un objeto que actúe como sensor. Esto quiere decir, que no interactua con los demás objetos de la vista, pero sí reciben y producen las diferentes colisiones con los demás. • Por ejemplo, podríamos crear un objeto el cual, al pasar por encima de él con uno de los personajes de nuestra aplicación, active la apertura de una caja situada en otra parte del juego. local rect = display.newRect( 50, 50, 100, 100 ) rect:setFillColor( 1, 1, 1, 1 ) rect.isVisible = true -- opcional physics.addBody( rect, "static", { isSensor = true } )

- 70 -

Ejercicios Tema 6 Ejercicio 1 Para este primer ejercicio vamos a crear una estructura mediante la cual podamos apreciar, sin salir de la aplicación, de los diferentes modos que tenemos a nuestra disposición dentro del motor físico para representar los diferentes objetos dentro de nuestra aplicación. Para nuestro proyecto vamos a necesitar 3 imágenes, las cuales vamos a dejar a merced de la simulación gravitatoria que nos ofrece el motor físico de Corona para observar su comportamiento. Nosotros vamos a utilizar las siguientes:

Lo primero que vamos a hacer es importar la librería para manejar objetos físicos en Corona e inicializarla: local physics = require ("physics") physics.start()

Una vez inicializada la librería, vamos a crear todos los objetos dentro de nuestra vista, las imágenes, el suelo, para que no se pierdan los objetos en el infinito y 3 botones que nos van a permitir alternar entre las diferentes opciones para mostrar estos objetos dentro del motor físico: local suelo = display.newRect (140,390,400,30) physics.addBody (suelo, "static") local logo = display.newImage ("logocorona.png", 180,-100) physics.addBody (logo, {bounce=0.3, friction= 0.4}) local icuadernos = display.newImage ("icuadernos.png", 100,-100) physics.addBody (icuadernos, {bounce=0.3, friction= 0.4}) - 71 -

local idiotify = display.newImage ("idiotify.png", 185,-100) physics.addBody (idiotify, {bounce=0.3, friction= 0.4})

-- Creamos los botones local rectanguloRojo = display.newRoundedRect (55,425,100,40, 20) rectanguloRojo:setFillColor (1, 0, 0) local rectanguloVerde = display.newRoundedRect (160,425,100,40, 20) rectanguloVerde:setFillColor (0, 1, 0) local rectanguloAzul = display.newRoundedRect (265,425,100,40, 20) rectanguloAzul:setFillColor (0, 0, 1) -- Añadimos estos botones al motor físico para poder verlos en los diferentes modos physics.addBody (rectanguloRojo, "static", { density=1.0}) physics.addBody (rectanguloVerde, "static", { density=1.0}) physics.addBody (rectanguloAzul, "static", { density=1.0}) -- Agregamos los eventos que controlarán cuando pulsemos un botón rectanguloRojo:addEventListener ("touch", pulsarBotonRojo) rectanguloVerde:addEventListener ("touch", pulsarBotonVerde) rectanguloAzul:addEventListener ("touch", pulsarBotonAzul)

Si ejecutamos nuestra aplicación observaremos nuestras 3 imágenes cayendo desde lo más alto de nuestra vista hasta el suelo que hemos colocado como tope. Nuestro siguiente paso será rellenar los métodos que controlan los eventos de pulsación de los botones: local function pulsarBotonRojo (event) if event.phase == "began" then print ("DEBUG") physics.setDrawMode ("debug") end end

Con este método controlariamos el boton Rojo. Rellena los otros 2 métodos para cambiar entre los modos restantes. Si utilizamos los modos “debug” o “hybrid” para observar nuestra aplicación - 72 -

podemos analizar los diferentes estados físicos de los objetos representados con diferentes colores: -Naranja: objetos físicos en movimiento -Gris: objetos en reposo -Verde: objetos estáticos, como el suelo o los botones. AMPLIACIÓN Modifica las imágenes del ejercicio y utiliza alguna que sea de tipo círcular, adaptando el objeto físico para que se adapte a la forma de la nueva imagen. Ejercicio 2 Modifica el ejercicio anterior para realizar un “filtro” de los objetos. Cada uno de los objetos cumplirá un comportamiento determinado: -El primer objeto no colisionará ni con el suelo ni con los botones, por lo que se perderá en el infinito de nuestra aplicación -El segundo objeto no colisionará con el suelo, pero sí con los botones. -El tercer objeto colisionará con el suelo En todo caso, muestra en la consola el estado de las colisiones y los objetos que participan en ellas.

- 73 -

Tema 7 Hardware y Multimedia Información del dispositivo • Corona nos ofrece una serie de librerías y métodos para poder acceder a la información del sistema donde se está ejecutando nuestra aplicación. • Puede sernos de gran utilidad para poder mostrar determinada información en función del modelo de dispositivo donde estemos ejecutando la aplicación. • Aparte podemos acceder a los diferentes componentes que tenga nuestro dispositivo disponible, como puede ser el acelerómetro, el sistema de ficheros interno, el sistema de localización... Información del dispositivo system.info (param) • Este método nos devuelve información sobre el sistema que está ejecutando nuestra aplicación. • Con el parámetro param indicamos cual es la información que vamos a querer recuperar. • Los valores que podemos obtener son los siguientes: “name” - devuelve el nombre del dispositivo. Por ejemplo para el iphone, sería el mismo nombre que podemos encontrar en el Itunes “model” - obtenemos el modelo del dispositivo (Ej. “Iphone”) “deviceId” - el identificador único del dispositivo “environment” - te indica dónde estás ejecutando la aplicación. Puede tomar valores “simulator” y “device” “platformName” - devuelve el nombre de la plataforma. Por ej, el sistema operativo. - 74 -

“platformVersion” - obtenemos la versión de la plataforma donde ejecutamos la aplicación. “version” - la versión de Corona “textureMemoryUsed” - nos devuelve el uso de memoria de la aplicación en bytes. “maxTextureSize” - obtenemos el máximo ancho y alto de textura que podemos usar en nuestra aplicación. system.getTimer () • Esta función nos devuelve, en milisegundos, el tiempo que ha pasado desde que se ha ejecutado la aplicación. system.openURL (url) • Nos permite abrir una url en el navegador del dispositivo. • Permite abrir urls formateadas en dispositivos iOS: - Mail: mailto:[email protected] - Teléfonos: tel:666888999 - Webs: http://viveahora.travel system.pathForFile (filename [ ,baseDirectory]) • Genera la ruta absoluta donde podemos encontrar el fichero filename. Se le puede especificar un directorio base a partir del cual generar esa ruta. system.vibrate () • Emite una vibración en nuestro dispositivo Directorios del sistema

- 75 -

• En todos los dispositivos tenemos una serie de directorios definidos por el sistema a los cuales podemos acceder con las siguientes constantes: system.DocumentsDirectory • Es el directorio que utilizamos para trabajar con ficheros que deben guardarse incluso cuando termine la sesión en la que estamos trabajando. system.TemporaryDirectory • Se trata de un directorio temporal. La persistencia de los ficheros guardados en este directorio, no se asegura que se mantenga una vez terminada la sesión. system.ResourceDirectory • Se trata del directorio donde tenemos empaquetados todos los contenidos de la aplicación. Nunca se deben guardar ficheros propios en este directorio.

Sistema de Audio • El sistema de audio de Corona nos ofrece la posibilidad de acceder a las características más avanzadas de la librería OpenAL (API multiplataforma de audio 3D empleada mayoritariamente en juegos) • El número máximo de canales que podemos usar es de 32. Es decir, podemos reproducir simultáneamente un máximo de 32 sonidos en nuestra aplicación. • El número máximo de canales posibles no implica que la aplicación funcione correctamente. Cuanto más ficheros de audio se reproduzcan a la vez, más carga tendrá la CPU, por lo tanto, el rendimiento de nuestra aplicación será menor. • Existen dos maneras de cargar el audio que vamos a reproducir. Tendremos que seleccionar la que más se aproxime a las necesidades de nuestra aplicación, en función del rendimiento y del uso de la Memoria. audio.loadSound (file)

- 76 -

• Con este primer método, cargamos el audio entero en memoria y así lo tenemos disponible a la hora de reproducirlo. Realiza un gran uso de memoria, por lo tanto es el método menos aconsejable para aplicaciones con problemas de rendimiento. audio.loadStream (file) • En este caso, el audio es preparado para su lectura en pequeños trozos, según sea requerido por la aplicación. Con este método conseguimos un mayor ahorro de memoria, pero no aseguramos la correcta reproducción del audio, ya que esta depende de los recursos disponibles en la CPU. • La ejecución múltiple de sonidos se debe hacer de diferente manera, dependiendo del método que utilicemos para cargar el audio. • Si cargamos el audio en memoria, podemos reproducirlo el número de veces que queramos a lo largo de nuestra aplicación con la misma instancia. local bomba = audio.loadSound ("bomb_wav.wav") audio.play (bomba) audio.play (bomba) audio.play (bomba) audio.play (bomba) audio.play (bomba) audio.play (bomba) audio.play (bomba)

• No se deben cargar varias instancias de un mismo sonido a través de loadSound. Sería una pérdida de memoria. • Por el contrario, si el método utilizado para la reproducción de nuestro sonido es loadStream, hay que tener en cuenta que una instancia sólo puede ser reproducida simultáneamente por cada canal. • Por lo tanto, si queremos lanzar varias reproducciones a la vez, utilizando loadStream, debemos crear varias instancias que manejen los diferentes sonidos. local musica1 = audio.loadStream ("musica1.wav") local musica2 = audio.loadStream ("musica2.wav") audio.play (musica1) audio.play (musica2)

- 77 -

• Al utilizar los streams, la carga en memoria es más reducida que de la manera que hemos visto anteriormente. audio.play (audioHandle [, options]) • Con el parámetro audioHandle definimos el archivo que vamos a reproducir, el cual tendremos que haber cargado anteriormente con audio.loadSound o audio.loadStream. • En el parámetro opcional options definimos las propiedades de reproducción que va a tener nuestro audio. Podemos reflejar los siguientes valores: channel - el número del canal donde vamos a querer reproducir nuestro audio. loops - el número de veces seguidas que se va a reproducir el audio. El audio se va a reproducir (loops+1) veces. duration - en este valor especificaremos, en milisegundos, el tiempo que queremos que dure la reproducción de nuestro audio. audio.play (audioHandle [, options]) fadein - especificado en milisegundos, nos asigna el valor de la rampa de volumen que se efectuará al principio de nuestra reproducción. onComplete - se trata de un listener, el cual se ejecutará una vez termine la reproducción del audio. Nos devuelve la siguiente tabla event. event.name - “soundCompletion” event.channel event.handle - el fichero de audio reproducido event.completed - este parámetro tendrá valor true si el audio se ha detenido porque ha llegado al final normalmente y false si se ha detenido por otras causas. - 78 -

audio.stop ([channel]) • Método para detener el canal especificado por parámetro. Si no se especifica ningún canal, se detienen todos.

audio.pause ([channel]) • Método para pausar los canales especificados como parámetro. Si no especificamos ningún canal, se pausarán todos los canales activos. audio.resume ([channel]) • Con este método volvemos a poner en reproducción el canal seleccionado. Si no se especifica ningún canal, afecta a todos los canales activos. audio.stopWithDelay (duration) • Detiene el audio que se está reproduciendo actualmente una vez pase el tiempo especificado en duration audio.setVolume (volumen [, options]) • Establece el volumen para la reproducción en activo. El valor del parámetro volume va desde 0.0 a 1.0. • En el parámetro options podemos especificar una tabla para especificar el canal al cual le vamos a cambiar el volumen. Ej. {channel=X} audio.seek (time [, audioHandle] [, { channel = c }]) • Desplaza la reproducción del audio activo hasta el punto, especificado en milisegundos que indiquemos. • Como parámetros opcionales podemos especificar el audio a parar y el canal sobre el que actuaremos - 79 -

Vídeo media.playVideo (path [, baseSource], showControls, listener) • Reproduce el vídeo situado en la ruta que especifiquemos en el parámetro path. • Mediante la constante baseSource podemos controlar como se interpreta la ruta del fichero que estamos reproduciendo. • El parámetro showControls es un booleano que especifica si se muestran (true) o no (false) los controles del vídeo mientras está en reproducción. • En el último parámetro especificamos el método que se va a ejecutar cuando termine la ejecución del vídeo. • Nota: Este API no está disponible en el simulador de Windows.

local function terminaVideo (event) print ("terminado") end media.playVideo ("videoEjemplo.mp4", true, terminaVideo)

- 80 -

Captura de imágenes • En casi todos los dispositivos de nueva generación tenemos disponible una cámara con la que se pueden capturar tanto imágenes como vídeos. • Esta captura de contenido multimedia puede ser usado dentro de nuestra aplicación y por eso, Corona nos ofrece un API para manejar todo lo relacionado con la cámara. • Este API no se puede utilizar en el simulador de Corona para Windows, así que la aplicación si se desarrolla sobre ese sistema operativo, debe ser probada directamente en el dispositivo. media.show (mediaSource, listener [, filetable]) • Con este método mostramos el interfaz específico, dependiendo de la plataforma en la que estemos ejecutando la aplicación, para acceder a la cámara o a la librería de imágenes. • Por defecto, la imagen se añade a la parte superior de nuestra vista. También tenemos la posibilidad de almacenarla en cualquier directorio. • Necesitamos establecer un listener para manejar el objeto que nos devuelva el método (la imagen). • En Android, para poder utilizar la cámara debemos establecer en nuestro fichero build.settings los permisos necesarios. settings = { android = { usesPermissions = { "android.permission.CAMERA", "android.permission.WRITE_EXTERNAL_STORAGE", }, }, } - 81 -

• Para el caso especifico de desarrollar para el iPad, necesitamos especificar dos parámetros adicionales. • A la llamada de media.show debemos agregar los valores de: origin - el rectángulo a partir del cual se va a mostrar el popover para seleccionar la foto. Lo más normal es usar el botón desde el cual lanzamos la selección de imágenes. (Ej: myButton.contentBounds)

permittedArrowDirections - un array opcional indicando las posibles direcciones hacia las cuales se extenderá el popover. Los valores admitidos son: {“up”, “down”, “left”, “right”, “any”} • Mediante el parámetro mediaSource de la llamada al método, vamos a especificar de dónde vamos a obtener la imagen. Puede contener alguno de los siguientes valores: media.PhotoLibrary media.Camera media.SavedPhotoAlbum • Debemos indicar el método que se va a hacer cargo de gestionar el final del evento de captura de imágenes a través del parámetro listener. • Este método recuperará el objeto event.target donde quedará almacenada la imagen que hayamos seleccionado para su uso. • Si no realizamos ninguna modificación sobre la imagen capturada, esta se mostrará en la parte superior izquierda de nuestra vista. • Por último nos queda el parámetro opciona filetable, mediante el cual podemos crear un array para especificar donde se va a salvar la imagen capturada.

- 82 -

• Si utilizamos este método, ya no recibiremos el objeto event.target en el método listener. • El formato de la tabla para el parámetro filetable es: { baseDir= , filename= [, type=]}

local function onComplete(event) local photo = event.target photo.x = 65 photo.y = 250 if photo then print( "photo w,h = " .. photo.width .. "," .. photo.height ) end end local button = display.newRect(120, 240, 80, 70) local function tomarPhoto( event ) --Comprobamos si el dispositivo tiene cámara if media.hasSource (media.Camera) then media.show( media.Camera, onComplete ) end end button:addEventListener("tap", tomarPhoto )

Uso del Acelerómetro • El acelerómetro es uno de los componentes disponible en la mayoría de los dispositivos de última generación capaz de detectar el movimiento del mismo y aplicarlo para su uso en las aplicaciones. • En Corona, el acelerómetro se trata como si fuese un evento global, el cual vamos a tratar a través de la clase Runtime. • Con la siguiente sentencia estamos indicando que vamos a capturar los - 83 -

eventos lanzados por el acelerómetro en el método indicado para ello. Runtime:addEventListener (“accelerometer”, metodoAccel)

• En el listener donde capturamos el evento, podemos acceder a diferentes valores: • Para poder obtener los diferentes valores según la posición del dispositivo, podemos acceder a las propiedades event.xGravity, event.yGravity, event.zGravity • Mediante el uso de event.isShake sabremos si el movimiento que ha realizado el dispositivo es una agitación. • Con event.deltaTime podremos acceder a los segundos que han transcurrido desde el último evento de acelerómetro.

- 84 -

Ejercicios Tema 7 Ejercicio 1 Vamos a practicar con la API para el manejo de sonidos que nos ofrece Corona, creando un pequeño reproductor de audio como el que podemos ver en la siguiente captura:

Lo primero que vamos a hacer es colocar los diferentes objetos que tenemos en nuestra vista: local contentWidth = display.contentWidth local contentHeight = display.contentHeight local texto = display.newText ("El audio está parado", contentWidth/2, contentHeight – 50) local botonPlay = display.newImage ("botonStart.png", 100, contentHeight – 150) botonPlay.width = 50 botonPlay.height = 50 local botonPause = display.newImage ("botonPause.png",160, contentHeight – 150) botonPause.width = 50 botonPause.height = 50 local botonStop = display.newImage ("botonStop.png", 220, contentHeight - 150) botonStop.width = 50 - 85 botonStop.height = 50

Si ejecutamos nuestra aplicación podremos observar los 3 botones y el texto que nos servirá de indicador para mostrar el estado de la canción. Lo siguiente que haremos será crear el audio que vamos a reproducir: local musica = audio.loadStream ("music.mp3")

Cargamos el audio que vamos a usar en la variable musica para poder acceder a ella a través de los diferentes eventos que capturaremos al interactuar con los botones. El siguiente paso es crear los eventos para los botones y, en función de la acción de cada uno se debe completar el código que falta: local function pulsarPlay (event) -- COMPLETAR -- Reproduce el audio -- Si el audio está pausado, lo continúa end local function pulsarPause (event) -- COMPLETAR -- Pausa la reproducción del audio end local function pulsarStop (event) -- COMPLETAR -- Detiene la reproducción del audio -- Coloca su reproducción al principio del audio end botonPlay:addEventListener ("tap", pulsarPlay) botonPause:addEventListener ("tap", pulsarPause) botonStop:addEventListener ("tap", pulsarStop)

Aparte, en cada uno de los métodos, debemos actualizar el estado del texto para indicarnos en qué situación está la reproducción. Para finalizar el ejercicio, vamos a especificar el tiempo que llevamos reproducido y el tiempo total del audio. Para ello debemos crear una variable - 86 -

para llevar la cuenta del tiempo reproducido, así como el texto donde pondremos los detalles: local tiempoTranscurrido = 0 local textoTiempo = display.newText ("Total reproducido ".. (tiempoTranscurrido/1000).. " / "..(audio.getDuration(musica)/1000), 70,320)

Usamos el método audio.getDuration que nos devuelve el tiempo total del audio que le pasemos por parámetro. Como el número devuelto es en milisegundos, lo dividimos entre 1000 para obtener los segundos correspondientes. El tiempo transcurrido lo podemos controlar a través de un timer que se ejecute cada segundo y podamos controlar dentro de lo diferentes eventos que hemos creado anteriormente. local reproducido reproducido = timer.performWithDelay (1000, function () tiempoTranscurrido = tiempoTranscurrido +1 textoTiempo.text = "Total reproducido "..tiempoTranscurrido.. " / ".. (audio.getDuration(musica)/1000) local isChannel1Playing = audio.isChannelPlaying( 1 ) if not isChannel1Playing then tiempoTranscurrido = 0 timer.pause (reproducido) end End , -1 ) timer.pause(reproducido)

A través de este timer, cada segundo, vamos incrementando la variable que controla el tiempo de nuestra aplicación, actualizamos el texto y comprobamos si el audio sigue reproduciéndose, es decir, no ha llegado al final para reiniciarlo. Por último, crea el código necesario para controlar el comportamiento del timer dentro de cada uno de los eventos (play, pause y stop) - 87 -

EJERCICIO 2 Vamos a mezclar en este ejercicio dos de los temas aprendidos hasta la fecha, la reproducción de audio vista en este tema y el manejo de colisiones del motor físico de Corona. Se debe crear una interfaz como la siguiente:

Todos los objetos de la interfaz se podrán arrastrar a lo largo de la vista, excepto el círculo rojo que será nuestro objeto master. Cada uno de los cuadrados representará un tipo diferente de audio y un canal diferente. Si uno de los rectángulos entra en contacto con el círculo, debe empezar a sonar en loop su sonido asociado. Una vez lo separemos debe parar de - 88 -

sonar.

NOTAS: -Recuerda que, a los objetos que creamos se le pueden añadir parámetros según nuestra conveniecia: rectangulo1.pistaAudio = audio1 rectangulo1.canal = 1 .… -Para el movimiento arrastrando puedes capturar el evento “touch” de los objetos.

EJERCICIO 3 Realizar una aplicación que nos permita capturar una imagen a través de la cámara del dispositivo. Una vez capturada, mostrarla en un tamaño reducido. Permitir, al tocar la imagen capturada, que esta, se amplie ocupando todo el ancho y alto de la pantalla.

- 89 -

Tema 8 Gestión de Mapas y GPS Mapas • La funcionalidad MapView, permite al desarrollador incorporar uno o más mapas de Google en su aplicación. • Permite a su vez realizar diferentes tipos de acciones utilizando la información proveniente del mapa y comparándola con diferentes parámetros del dispositivo, como puede ser su situación. Mapas • Se pueden marcar puntos de interés en el mapa e incluso utilizar herramientas de geocoding (traducción de coordenadas de latitud/ longitud en la dirección física más cercana. • MapView sólo está disponible para su uso en dispositivos y, por lo tanto, no funciona sobre el simulador. • Para poder probar esta característica se debe hacer sobre un dispositivo con sistema operativo iOS, sobre el simulador del XCode o sobre un dispositivo Android. Mapas • Para poder probarlo sobre Android, se deben definir los permisos de Internet y de localización el fichero build.settings de nuestro proyecto.

- 90 -

settings = { android = { usesPermissions = { -- Required by the MapView to fetch its contents from the Google Maps servers. "android.permission.INTERNET", -- Optional permission used to display current location via the GPS. "android.permission.ACCESS_FINE_LOCATION", -- Optional permission used to display current location via WiFi or cellular service. "android.permission.ACCESS_COARSE_LOCATION", }, usesFeatures = { -- If you set permissions "ACCESS_FINE_LOCATION" and "ACCESS_COARSE_LOCATION" above, -- then you may want to set up your app to not require location services as follows. -- Otherwise, devices that do not have location sevices (such as a GPS) will be unable -- to purchase this app in the app store. { name = "android.hardware.location", required = false }, { name = "android.hardware.location.gps", required = false }, { name = "android.hardware.location.network", required = false }, }, }, }

myMap = native.newMapView( Xcenter, Ycenter, width, height ) • Mediante esta instrucción, creamos una vista de mapa en nuestra aplicación situada centrada en las coordenadas especificadas (Xcenter, Ycenter) y con el tamaño indicado (width, height) • Este constructor nos devuelve un objeto Corona, el cual puede ser tratado como cualquier otro dentro de nuestra vista. • Con estas características podemos conseguir funcionalidades no permitidas dentro de las aplicaciones nativas de Apple, como mover el mapa o transformarlo en un objeto físico.

- 91 -

Atributos del Mapa myMap.mapType = “standard” | “satellite” | “hybrid” • Modificando el atributo mapType, podemos elegir entre las diferentes vistas de mapa que tenemos disponibles. • En este caso, contamos con tres opciones. La vista de mapa normal (standard), la imagen desde el satélite (satellite) o la mezcla de los dos estilos (hybrid). • El estilo por defecto es standard. myMap.isZoomEnabled = true | false • Modificando esta propiedad del mapa determinamos si es posible realizar las acciones gestuales (pinch) para ampliar y reducir la vista del mapa. • El valor por defecto es true. myMap.isScrollEnabled = true | false • Esta propiedad nos indica si le permitimos al usuario hacer scroll sobre la vista del mapa. • El valor por defecto es true. myMap.isLocationUpdating = true | false • Con esta opción indicamos si se muestra o no la posición actual del usuario. • Cuando activamos esta propiedad este valor se irá actualizando constantemente en el mapa según se vaya moviendo el dispositivo físicamente. - 92 -

• El valor por defecto es false.

isVisible = myMap.isLocationVisible • Se trata de una propiedad de solo lectura la cual nos indica si la posición del usuario se encuentra dentro de la región que estamos mostrando en ese momento en nuestra vista del mapa. • Este valor está basado en una aproximación, por lo que puede llevar a errores cuando la posición esté en los límites del mapa.

Funciones del Mapa latitude, longitude = myMap:getAddressLocation (“Dirección a buscar”) • Con esta función obtenemos las coordenadas (longitud y latitud) de la dirección que pasemos por parámetro. • Estas coordenadas se obtienen a través de una petición HTTP sobre Google maps. • Aparte de direcciones, podemos usar como parámetros sitios destacados y el servicio de Google se encargará de localizarlos (Torre Eiffel, Liberty Statue). • Esta información nos puede servir para colocar un marcador en el mapa, para volver a centrar el mapa en la localización deseada o para llevar a cabo cualquier función que incluya el uso de un par de coordenadas. myLocation = myMap:getUserLocation () • Esta sentencia contiene una tabla con la información detallada de la posición del usuario. • Los valores que podemos encontrar dentro de esta tabla son los siguientes: - 93 -

longitude | latitude | altitude | accuracy | time | speed | direction | isUpdating

• El último parámetro nos indica si en el instante que capturamos la localización del usuario, está se está actualizando. myMap:setRegion (latitude, longitude, latitudeSpan, longitudeSpan, isAnimated) • Movemos la parte visible de nuestro mapa a una nueva localización declarada por los parámetros latitude y longitude. • Con los valores de latitudeSpan y longitudeSpan definimos el nivel de zoom sobre el centro de nuestra región. • El último parámetro nos indica si la transición hacia la nueva región se realizará mediante una animación o de golpe (true / false). myMap:setCenter (latitude, longitude, isAnimated) • Mediante esta función movemos la región actual de nuestro mapa a una nueva cuyo centro es el par de coordenadas (latitude y longitude) que introducimos por parámetro. • El último parámetro es un valor booleano el cual nos indica si la transición a la nueva región se hace automática o mediante una animación. myMap:addMarker (latitude, longitude, { title = “Titulo”, subtitle = “Subtítulo”}) • Mediante el uso de esta función agregamos un marcados en las coordenadas indicadas. • La tabla que usamos como tercer parámetro, nos indica el título y el subtítulo del popup que saldrá al hacer click sobre el marcador en el mapa. myMap:removeAllMarkers() • Función para eliminar todos los marcadores que tengamos dentro de nuestra vista de mapa.

- 94 -

Geocoding • Un par de coordenadas de latitud/longitud pueden ser transformadas, en una dirección aproximada. • Para ello, internamente, nos conectaremos a los servidores de Google para transformar esta información. • Esta conexión no depende de nosotros, por lo que, el rendimiento de nuestra aplicación puede verse resentido dependiendo del estado de los servidores de Google. • Para utilizar esta funcionalidad dentro de los mapas, lo primero que debemos hacer es poner nuestra aplicación a escuchar el evento mapAddress. • Con el uso de la función myMap:nearestAddress (latitude, longitude, resultHandler) • obtendremos una llamada de tipo asíncrono sobre la función que hayamos definido en resultHandler. • Los parámetros devueltos por el evento son los siguientes: event.street - el nombre de la calle event.streetDetail - el número de la calle (u otros datos sobre la localización) event.city - la ciudad o el pueblo event.cityDetail - información adicional sobre la ciudad event.region - el estado o la provincia event.regionDetail - la región por debajo del estado o la provincia event.postalCode - el código postal event.country - el nombre del país event.countryCode - la abreviación estándar del país - 95 -

event.isError - indica si la conexión nos devuelve algún tipo de error

• Un ejemplo para la captura del evento sería el siguiente: local function mapAddressHandler( event ) if event.isError then native.showAlert("Error", "mapView Error: " .. event.errorMessage , {"OK"}) else native.showAlert("Correcto", "La localización está en: " .. event.city .. ", " .. event.country , {"OK"}) end end myMap:nearestAddress( 38.898748, -77.037684, mapAddressHandler )

- 96 -

Ejercicios Tema 8 Ejercicio 1 En este ejercicio vamos a utilizar los mapas nativos gracias a la funcionalidad MapView que nos ofrece Corona. En muy pocas lineas podremos utilizar mapas completamente funcionales en nuestra aplicación. Hay que señalar que Corona trata los mapas como otro objeto más de nuestra aplicación por lo que podremos aplicarle si queremos cualquier funcionalidad típica de tratamiento de imágenes como rotar el mapa o moverlo por la pantalla. Con la siguiente línea crearemos y mostraremos un mapa: local mapa = native.newMapView( 0, 0, 320, 420 )

Los parametros de la funcion newMapView son los puntos de posición del mapa y su tamaño. En nuestro caso lo colocariamos en el punto de origen (0,0) con un tamaño de (320,420) Para comprobar los distintos modos de vista que poseen nuestros mapas vamos a crear tres botones en la parte inferior de nuestra aplicación. local rectanguloRojo = display.newRoundedRect (60,425,80,40, 20) rectanguloRojo:setFillColor (1, 0, 0) local rectanguloVerde = display.newRoundedRect (160,425,80,40, 20) rectanguloVerde:setFillColor (0, 1, 0) local rectanguloAzul = display.newRoundedRect (260,425,80,40, 20) rectanguloAzul:setFillColor (0, 0, 1)

Una vez creados los botones, les añadiremos los manejadores para controlar los eventos de pulsado, y estos controladores cambiaran el tipo de vista del mapa. local function pulsarBotonRojo(event) if(event.phase == "began") then mapa.mapType = "standard" end end local function pulsarBotonAzul(event) if(event.phase == "began") then - 97 mapa.mapType = "satellite" end end

local function pulsarBotonVerde(event) if(event.phase == "began") then mapa.mapType = "hybrid" end end -- Agregamos los eventos que controlarán cuando pulsemos un botón rectanguloRojo:addEventListener ("touch", pulsarBotonRojo) rectanguloVerde:addEventListener ("touch", pulsarBotonVerde) rectanguloAzul:addEventListener ("touch", pulsarBotonAzul)

Ampliación 1 Como ampliación vamos a reutilizar los botones ya creados para comprobar otras de las funciones que nos ofrecen los mapas nativos. Uno de los botones deberá centrar el mapa en la región en la cual nos situemos. Otro deberá colocar una marca en el centro del mapa. El último botón deberá eliminar todas las marcas que hayamos colocado anteriormente Ejercicio 2 Otra opción para mostrar mapas es utilizar la Google Maps app. Para utilizar este servicio, es tan fácil como llamarlo de la siguiente manera: system.openURL("comgooglemaps://")

Otra de las principales cualidades de Google Maps es el conocido Street View al cual podriamos llamar de la siguiente manera: system.openURL("comgooglemaps://? center=40.765819,73.975866&zoom=14&views=traffic&mapmode=streetview") - 98 -

Para asegurarnos de que Google Maps no va a dejarnos colgados por cuestiones ajenas a nuestra aplicación, podemos asegurarnos de que google maps ha funcionado, y en caso de fallo redirigir a Apple Maps de la siguiente manera: local didOpenGoogleMaps =system.openURL("comgooglemaps://? daddr=San+Francisco, +CA&saddr=cupertino") if ( didOpenGoogleMaps == false ) then --defer to Apple Maps system.openURL("http://maps.apple.com/?daddr=San+Francisco, +CA&saddr=cupertino") end

Ampliación 2 Prueba a modificar las direcciones para crear tus propios recorridos y mostrarlos al pulsar un botón. Esto suele ser muy aplicado a la hora de querer mostrar al publico como llegar a ciertos eventos o oficinas.

- 99 -

Tema 9 Diseñando la interfaz de usuario Interfaces • Mediante podemos acceder a diferentes características nativas del dispositivo donde estemos ejecutando nuestra aplicación. • Podemos acceder con una simple línea de código a las alertas del dispositivo, campos para creación de formulario, vistas de carga... etc. Interfaces

Activity Indicator native.setActivityIndicator (state) • Mediante esta función indicamos si el indicador de actividad o indicador de carga está activo o no (true o false respectivamente). • Este indicador no se activará hasta que el bloque donde esté contenida la llamada al método se haya ejecutado por completo. • Todos los eventos de toque se anulan una vez mostremos el indicador ya que quedará por encima de todos los objetos contenidos en nuestra vista. • Para su mejor funcionamiento, se aconseja hacer las llamadas para mostrar/ocultar el indicador desde métodos diferentes.

native.setActivityIndicator( true ) -- oculta el indicador después de 2 segundos timer.performWithDelay( 2000, function() native.setActivityIndicator( false ) end, 1)

- 100 -

Alertas native.showAlert (title, message [, buttonLabels [,listener]]) • Mediante la llamada a este método mostramos una alerta, del tipo definido en el dispositivo, con el título y el mensaje pasados por parámetro (title, message). • Mientras se muestra la alerta, el programa sigue en ejecución en background, pero todas las interacciones del usuario quedan bloqueadas hasta que no se oculte la alerta. • buttonLabels se trata de una tabla donde definimos cada uno de los botones que aparecerán en la alerta. Se debe incluir al menos uno. • El primer botón definido estará resaltado y será tomado como el botón por defecto. • El máximo número de botones permitidos en una alerta es de 6. • El método que definamos como listener será ejecutado cuando el usuario pulse cualquiera de los botones de la alerta. • Dentro del evento capturado podemos acceder a diferentes propiedades. Una de ellas sería event.action, la cual nos devuelve lo sucedido con la alerta. • Los valores para event.action son, clicked, si el usuario ha pulsado alguno de los botones y cancelled, si la alerta se ha eliminado con el método native.cancelAlert(). local function onComplete( event ) if "clicked" == event.action then local i = event.index if 1 == i then -- No hacemos nada, se cierra la alerta elseif 2 == i then -- Abrimos una URL cuando se pulsa el segundo botón system.openURL( "http://viveahora.travel" ) end end - 101 -

end local alert = native.showAlert( "Vive Ahora", "Agencia de viajes", { "OK", "Más Info" }, onComplete )

native.cancelAlert (alert) • Cancelamos la alerta pasada por parámetro en este función. • Por ejemplo, podríamos ocultar una alerta, pasado un tiempo determinado.

Fuentes native.newFont (name [,size]) • Creamos un objeto de tipo fuente, el cual puede ser posteriormente asignado a un campo de texto nativo o a una caja de texto. • También se puede usar como parámetro en el momento de la creación de objetos de texto con display.newText(). • El parámetro name especifica el nombre de la fuente alojada en el dispositivo nativo. Se puede obtener una lista de todas las fuentes disponibles ejecutando el método native.getFontNames(). • Aparte podemos usar constantes como native.systemFont y native.systemFontBold.

Campo de Texto native.newTextField (Xcenter, Ycenter, width, height [,listener]) • Crea un campo de texto de una sola línea para introducir texto en las coordenadas específicas con el tamaño indicado. • Las propiedades que se pueden modificar de un campo de texto son: • object.align - puede ser cualquier de los valores “left”, “center” o “right”. • object.font - la fuente para el campo de texto - 102 -

• object.hasBackground - es true cuando el campo de texto tiene fondo y false si es transparente. • object.size - el tamaño del texto. • object.text - cadena que contiene el texto del campo. • En el parámetro listener podemos especificar la función para manejar cuando el usuario introduce texto en el campo. • También podemos manejar el campo de texto aparte, capturando sobre el objeto el evento “userInput”. local defaultField local function textListener( event ) if event.phase == "began" then elseif event.phase == "ended" then elseif event.phase == "ended" or event.phase == "submitted" then elseif event.phase == "editing" then end end defaultField = native.newTextField( 150, 150, 180, 30 ) defaultField:addEventListener( "userInput", textListener )

Campo de Texto Multilínea native.newTextBox (Xcenter, Ycenter, width, height) • Creamos un campo de texto multilínea con scroll para mostrar texto. • Tenemos disponibles las mismas propiedades y los mismos métodos que usamos para los campos de texto.

Web Popups native.showWebPopup (x, y, width, height, url [,options]) • Con este componente podemos incorporar a nuestra aplicación pequeñas vistas a páginas web. • Definimos la coordenada de nuestra vista donde se va a crear (x y) y - 103 -

el tamaño que ocupará (width, height) • En el parámetro url definimos la página web que vamos a cargar en el contenedor.

- 104 -

Ejercicios Tema 9 Ejercicio 1 Para este primer ejercicio, el primer paso va a ser crear una aplicación como la siguiente: Para ello creamos un fondo blanco, colocamos una imagen a gusto en el centro de la aplicación y un texto a pie de pantalla para indicar la acción que debe realizarse. El código para esto sería el siguiente: local contentWidth = display.contentWidth local contentHeight = display.contentHeight local fondo=display.newRect(contentWidth/2,contentHeight/2,380,480) fondo:setFillColor(1,1,1) local logo=display.newImage("imagina.jpg",contentWidth/2,contentHeight/2) local cuenta = display.newText("¡¡¡Pínchame!!!",contentWidth/2 , (contentHeight/2) + 90,nil,25) cuenta:setFillColor(0,0,0)

Una vez tengamos esto hecho pasaremos a asociarle a la imagen un método manejador del evento “touch” como hemos aprendido en temas anteriores y a dicha acción le diremos que ejecute una Alerta. Quedaría de la siguiente forma: local function pulsarLogo(event) native.showAlert("Alerta","¿Quieres que te redirijamos a la web?", {"Si","No"},yesPressed) end logo:addEventListener ("touch", pulsarLogo)

Como podemos ver, solo hemos dado opción para dos botones . El botón “No” , no hará nada, y el de “Sí” abrirá una página web en nuestro navegador predeterminado. El método listenerAlert deberá controlar que botón ha sido pulsado y actuar en consecuencia.

- 105 -

local function yesPressed(event) if("clicked"==event.action) then local i = event.index if (i == 2) then --nothing elseif (i == 1) then system.openURL( "http://www.imaginaformacion.com" ) end end end

A continuación, para añadir reforzar los conocimientos aprendidos, vamos a utilizar un Activity Indicator para crear un tiempo de espera. Para saber cuanto falta, utilizaremos el texto ya creado anteriormente donde indicaba pínchame para expresar en segundos el tiempo restante para poder interactuar. El código sería el siguiente: local cargando = display.newText("Cargando...",95,100,nil,25) cargando:setFillColor(0,0,0) native.setActivityIndicator(true) timer.performWithDelay(1000,function() cuenta.text="...2…" end) timer.performWithDelay(2000,function() cuenta.text="...1…" end) timer.performWithDelay(3000,function() native.setActivityIndicator(false) cargando.text="" cuenta.text="¡¡¡Pinchame!!!" end)

Ampliación 1 Añade un campo de texto debajo de la imagen donde coloques la dirección a cargar por el Popup, en caso de no haber escrito ningún texto, al pinchar en la imagen deberá aparecer otra alerta indicando que necesitas introducir una dirección web.

- 106 -

Ampliación 2 Crea una nueva aplicación con un campo de texto multilínea y un botón imprimir por consola. El funcionamiento de la aplicación será el siguiente. Al pulsar el botón imprimir se deberá comprobar que el campo de texto, tenga texto. En caso de no tener, se emitirá una alerta avisando al usuario. En caso de tener texto deberás imprimir el texto del campo por consola.

- 107 -

Tema 10 Entrada y Salida Ficheros • Todos los documentos relacionados con la aplicación se encuentran guardados en directorios internos de la propia aplicación a los cuales no se puede acceder desde otras aplicaciones. • En determinados directorios de la aplicación, tenemos permisos de lectura y escritura, por lo que podemos almacenar y recuperar datos de estos ficheros según convenga para nuestra aplicación. • Las rutas para cada uno de los ficheros son únicas dentro de nuestra aplicación. • Para la creación de rutas utilizamos la siguiente función: local path = system.pathForFile (filename [, baseDirectory]) • Nos devuelve la ruta del fichero especificado como primer parámetros. • Podemos definir en el segundo parámetro a partir de qué directorio base se creará la ruta solicitada.

• Por lo general, los ficheros deben estar almacenados en alguno de estos 3 directorios: • system.DocumentsDirectory - directorio usado para aquellos ficheros que deban permanecer almacenados entre las diferentes ejecuciones de la aplicación. • system.TemporaryDirectory - los ficheros almacenados en este directorio no aseguran seguir guardados en siguientes ejecuciones de la aplicación. • system.ResourceDirectory - es el directorio donde se encuentran todos los ficheros relacionados con la aplicación (imágenes, sonidos...). - 108 -

• Para la lectura de ficheros utilizaremos la librería io provista por Lua. • Esta librería nos permite abrir ficheros especificándole la ruta en la que se encuentran. • Ejemplo para leer un fichero situado en nuestro directorio de documentos. local path = system.pathForFile( "data.txt", system.DocumentsDirectory ) local file = io.open( path, "r" ) if file then -- nil if no file found local contents = file:read( "*a" ) print( "Contenido de " .. path .. "\n" .. contents ) io.close( file ) end

• Aseguraos de colocar el fichero de texto en el directorio correcto. • En el ejemplo anterior hemos obtenido la ruta del fichero el cual vamos a leer, especificando el nombre del fichero y el directorio base donde poder encontrarlo. • Mediante la función io.open procedemos a abrir el fichero para su lectura. • Esta función, aparte de recibir la ruta con el fichero, podemos indicarle el modo de apertura de ese fichero. Las opciones son: “r” - modo lectura. Es el definido por defecto “w” - modo escritura. “a” - escribe, pero añadiendo el contenido al ya existente en el fichero. “r+” - modo actualización. Todos los datos anteriores se guardan “w+” - modo actualización. Todos los datos anteriores son borrados “a+” - modo actualización. Todos los datos anteriores se guardan y sólo se puede escribir al final del fichero.

• Ejemplo para escribir en un fichero situado en nuestro directorio de documentos.

- 109 -

local path = system.pathForFile( "data.txt", system.DocumentsDirectory ) -- io.open opens a file at path. returns nil if no file found local file = io.open( path, "r" ) if file then -- read all contents of file into a string local contents = file:read( "*a" ) print( "Contenido de " .. path .. "\n" .. contents ) io.close( file ) else -- create file b/c it doesn't exist yet file = io.open( path, "w" ) local numbers = {1,2,3,4,5,6,7,8,9} file:write( "¡Aliméntame con datos \n", numbers[1], numbers[2], "\n" ) for _,v in ipairs( numbers ) do file:write( v, " " ) end file:write( "\n¡No quedan datos \n" ) io.close( file ) end

• En el ejemplo anterior hemos seguido los mismos pasos que para leer de fichero, solo que esta vez hemos utilizado el método write. • Con el método system.pathForFile seleccionamos el directorio donde se creará o modificará nuestro fichero. • Es muy importante no crear, modificar o escribir ficheros en el directorio ResourceDirectory pues cuando la aplicación se lanza se comprueba la integridad de la aplicación. En caso de fallo de integridad tu app no se lanzará a ejecución y será detectado como malware. • Para escribir un salto de linea se utilizará el carácter especial “\n” Operaciones de manipulación implícitas: • io.close([file]) equivalente a file:close( ), si no especificamos un fichero cerrará el fichero de salida por defecto - 110 -

• io.flush( ) equivalente a file:flush, libera los buses de escritura en un fichero • io.input([file]) llamado con el nombre de un fichero, abre dicho fichero en modo texto y lo asocia al manejador por defecto, llamado con el manejador de fichero simplemente asocia dicho manejador al definido por defecto. En caso de error esta función propaga el error en vez de devolver un código de error. • io.lines([filename]) abre el fichero en modo lectura y devuelve un iterador con el cual podremos recorrer todas las lineas de dicho fichero. A continuación un ejemplo: for line in io.lines(filename) do body end

• io.open(filename [, mode]) explicado anteriormente. • io.output([file]) similar a io.input pero opera sobre la salida. • io.popen(prog [,mode]) ejecuta el programa prog en un proceso separado y devuelve un manejador para leer o escribir los datos de dicho programa. Esta función es dependiente del sistema por lo que no esta disponible en todas las plataformas. • io.read(...) equivalente a io.input( ):read • io.tmpfile( ) devuelve un manejador para un fichero temporal que es abierto en modo modificar. • io.type(obj) comprueba si el objeto es un manejador valido. Devuelve el string “file” si es un manejador abierto , “closed file” si está cerrado y “nil” si no lo es. • io.write(...) equivalente a io.output( ):write. Operaciones de manipulación explícita: • file:close( ) cierra los ficheros. • file:flush( ) libera los buses de escritura en un fichero • file:lines( ) abre el fichero en modo lectura y devuelve un iterador con el cual podremos recorrer todas las lineas de dicho fichero. • file:read(...) lee del fichero de acuerdo a los formatos: • *n lee un número • *a lee el fichero entero desde la posición actual, devolviendo un string vacío al final del fichero. • *l lee la siguiente linea, devolviendo nil al final del fichero - 111 -

• file:seek([whence] [, offset]) modifica y obtiene la posicion del fichero mediante el string whence y el desplazamiento offset, siendo los posibles valores de whence: • set inicio del fichero • cur posicion actual • end final del fichero

• file:setvbuf(mode [, size]) modifica el modo de buffering para un fichero de salida. Hay tres modos de buffering: • no : sin buffering, el resultado de cualquier operación de salida se ejecuta inmediatamente. • full : buffering completo, el buffer se descarga cuando se llena o cuando se le pide mediante flush. • line : buffering por lineas, el buffer se descarga después de la entrada de una nueva linea. • file:write(...) escribe el valor de sus argumentos en el fichero. Los argumentos deben ser strings o números.

JSON • Las funciones de manejo de datos mediante JSON están integradas en Corona • Recuerda que para utilizar una libreria integrada en corona solo hace falta utilizar require “json” y almacenar su valor en una variable local. • JSON es muy similar a una estructura de tablas de Lua. Un ejemplo a continuación: {

}

"name": "Jack (\"Bee\") Nimble", "format": { "shape": "rect”, "width": 1920, "height": 1080, "interface": false, "framerate": 24 }

- 112 -

• Si tu JSON esta almacenado en un fichero .json que tienes en tu directorio, deberás leer dicho fichero y almacenarlo en una variable, para ello te aconsejamos que utilices una función como la que te mostraremos a continuación:

-- jsonFile() loads json file & returns contents as a string local jsonFile = function( filename, base ) -- set default base dir if none specified if not base then base = system.ResourceDirectory; end -- create a file path for corona i/o local path = system.pathForFile( filename, base ) -- will hold contents of file local contents

end

-- io.open opens a file at path. returns nil if no file found local file = io.open( path, "r" ) if file then -- read all contents of file into a string contents = file:read( "*a" ) io.close( file ) -- close the file after using it end return contents

• Si tu JSON esta almacenado en un string, necesitas decodificarlo, que es otra manera de decir, transformaré mi JSON en una tabla Lua. • Para ello utilizaremos una función llamada decode • Utilizando la función anterior podemos decodificar el valor de un archivo JSON a una tabla Lua con el siguiente código: local json = require "json" - 113 -

local t = json.decode( jsonFile( "sample.json" ) )

• Si tienes una tabla Lua y quieres transformarla en JSON solo tienes que utilizar la función inversa de decode, es decir, encode. A continuación un ejemplo: local jsonString = json.encode( myLuaTable )

• En caso de necesitar obtener los JSON de un ordenador remoto o un servicio web, el proceso será el mismo que el explicado, solo que obviamente habrá que descargarlos primero.

Base de datos • Corona incluye soporte en todas las plataformas para bases de datos SQLite. • La API que provee las funciones para utilizar bases de datos SQLite estan disponibles en luaforge.net/projects/luasqlite/ así como la documentación se puede leer en luasqlite.luaforge.net/lsqlite3.html. • Recuerda que al proveer el directorio a la funcion open( ) te asegures de utilizar system.pathForFile( ). Si intentas abrir directamente con el nombre de la base de datos, por ejemplo “base_de_datos.db” no funcionará correctamente. • A continuación os mostraremos un ejemplo de como abrir una base de datos guardada en memoria, crear una nueva tabla, añadir datos y finalmente mostrar dichos datos en pantalla. require "sqlite3" local db = sqlite3.open_memory() db:exec[[ CREATE TABLE test (id INTEGER PRIMARY KEY, content); INSERT INTO test VALUES (NULL, 'Hello World'); INSERT INTO test VALUES (NULL, 'Hello Lua'); INSERT INTO test VALUES (NULL, 'Hello Sqlite3') ]] print( "version " .. sqlite3.version() ) for row in db:nrows("SELECT * FROM test") do - 114 -

local t = display.newText(row.content, 150, 120 * row.id, null, 16) t:setTextColor( 1, 0, 1) end

• A continuación os mostraremos un ejemplo de como abrir una base de datos guardada en fichero y comprobar si una tabla existe antes de crearla. Importante acordarse de cerrar la conexión con la base de datos al salir

--Include sqlite require "sqlite3" --Open data.db. If the file doesn't exist it will be created local path = system.pathForFile("data.db", system.DocumentsDirectory) db = sqlite3.open(path) local function onSystemEvent(event) if(event.type == "applicationExit") then db:close() end end --Setup the table if it doesn't exist local tablesetup = [[CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, content, content2);]] print(tablesetup) db:exec( tablesetup ) --Add rows with a auto index in 'id'. You don't need to specify a set of values because we're populating all of them local testvalue = {} testvalue[1] = 'Hello' testvalue[2] = 'World' local tablefill =[[INSERT INTO test VALUES (NULL, ']]..testvalue[1]..[[',']]..testvalue[2]..[['); ]] local tablefill2 =[[INSERT INTO test VALUES (NULL, ']]..testvalue[2]..[[',']]..testvalue[1]..[['); ]] db:exec( tablefill ) db:exec( tablefill2 ) --setup the system listener to catch applicationExit Runtime:addEventListener( "system", onSystemEvent )

- 115 -

Ejercicios Tema 10 Ejercicio 1 Vamos a practicar lo aprendido en entrada/salida de ficheros. Para ello haremos ejercicios utilizando la librería io. Primero vamos a crear un fichero en el que escribir. Para ello debemos de crear un path en el cual le indicaremos el nombre del fichero de la siguiente manera: local path = system.pathForFile("miFichero.txt", system.DocumentsDirectory)

Luego vamos a escribir algo en dicho fichero: local file = io.open( path, "w" ) file:write( "Este es mi primer fichero.\n") io.close( file )

Si ahora comprobáis el fichero veréis que contiene lo que le hemos puesto. A continuación vamos a añadir más texto al fichero: file = io.open( path, "a" ) file:write( "Sigo escribiendo en mi fichero, cómo me gusta mi fichero.\n") io.close(file)

Y por último vamos a hacer que lea el texto del fichero, mostrándose el contenido por la Terminal: local file = io.open(path,"r") local contenido=file:read("*a") print(contenido)

¿Qué pasaría si en vez de usar el modo “a” usara el “w” la segunda vez que escribimos?Juega con los modos de acceso y comprueba todas las posibilidades.

- 116 -

Ampliación 1 Si solo quisiera leer las lineas pares hasta el final del texto, ¿cómo podría conseguirlo? Impleméntalo. Pista: utiliza la funciones line para recorrer el contenido. Ejercicio 2 En este ejercicio vamos a crear una tabla Lua y a partir de ella vamos a obtener un archivo JSON. Para ello lo primero es crear nuestra tabla , por ejemplo esta : local tablaTrabajadores= {{nombre="Paco",puesto="Limpiador",salario="500"}, {nombre="Fulano",puesto="Administrativo",salario="800"}, {nombre="Perico",puesto="Informático",salario="5000"}, {nombre="Mengano",puesto="Jefe de ventas",salario="1500"}}

Ahora deberemos transformar nuestra tabla lua a un valor JSON , y para ello utilizamos la función encode que nos proporciona Corona. local json = require "json" local jsonString = json.encode(tablaTrabajadores)

Por último guardamos el valor obtenido por la función en un fichero .json utilizando la librería io como hemos aprendido anteriormente local path = system.pathForFile("myJSON.json",system.DocumentsDirectory) local file = io.open(path,"w") file:write(jsonString) io.close(file)

Ampliacion 2 Ahora que ya sabemos crear ficheros .json a partir de tablas lua vamos a - 117 -

hacer el paso contrario. Utilizando un .json cualquiera, el que habéis creado antes o cualquiera que descarguéis de Internet, decodificarlo a una tabla lua y luego mostrarlo por consola. Ejercicio 3 En este ejercicio vamos a crear una base de datos y a insertar unas filas. Para ello primero creamos la base de datos y la tabla en caso de no existir. require "sqlite3" local path = system.pathForFile("miBaseDeDatos.db", system.DocumentsDirectory) db = sqlite3.open( path ) local function onSystemEvent( event ) if( event.type == "applicationExit" ) then db:close() end end local tablesetup = [[CREATE TABLE IF NOT EXISTS tablaTrabajadores (id INTEGER PRIMARY KEY, nombre, puesto, salario);]] print(tablesetup) db:exec( tablesetup )

Ahora introduciremos en la base de datos unas filas: local paco =[[INSERT INTO tablaTrabajadores VALUES (NULL, "Paco", "Limpiador","400" )]] local manolo =[[INSERT INTO tablaTrabajadores VALUES (NULL, "Manolo", "Becario","Las gracias" )]] db:exec( paco ) db:exec( manolo )

Por último las visualizaremos y las mostraremos por la Pantalla: local titulo = "Trabajadores" local tit = display.newText(titulo,160,60,null,40) tit:setFillColor(1,0,1) --print all the table contents for row in db:nrows("SELECT * FROM tablaTrabajadores") do local nombre = "Nombre :"..row.nombre local puesto = "Puesto : "..row.puesto local salario = "Salario :"..row.salario local t = display.newText(nombre, 160, row.id*100, null, 16) t:setFillColor(1,0,1) local t1 = display.newText(puesto, 160, 20 + row.id*100, null, 16) - 118 t1:setFillColor(1,0,1) local t2 = display.newText(salario, 160, 40 + row.id*100, null, 16) t2:setFillColor(1,0,1) end

Ampliación 3 En la siguiente ampliación deberéis eliminar solo a uno de los trabajadores y editar el otro trabajador modificando su puesto y su salario. Mostrar el contenido de la base de datos para comprobar que las instrucciones sobre la base de datos se han ejecutado correctamente.

- 119 -

Tema 11 Composer y Widget Composer • La API de Composer es una herramienta muy potente y flexible. • La estructura de los Composer se basa en crear varias Escenas y encadenarlas mediante funciones para lograr la estructura navegacional deseada. • Por defecto, las escenas de nuestro Composer no se eliminan cuando hay una transición de escena. Sin embargo, gracias a las librerías ofrecidas por Corona podemos eliminar las escenas en cada uno de los cambios. Composer • Para asegurarnos de que cada escena es eliminada en su transición, en el archivo principal, main.lua elegiremos la opción recycleOnSceneChange a true. • Para cambiar de una escena a otra hay que utilizar el método Composer.gotoScene( ) • Recordar que para utilizar la librería Composer hay que inicializarla local composer = require "composer" composer.recycleOnSceneChange = true

• Necesitaremos un fichero .lua para cada una de las escenas a mostrar. • Para crear la nueva escena deberemos importar la librería composer como hemos comentado anteriormente y crear la escena con el método composer.newScene( ) • Cada escena deberá implementar los métodos controladores de eventos para poder viajar entre escenas correctamente.

- 120 -

local composer = require "composer" local scene = composer.newScene() -- setup function: function scene:createScene( event ) local img = display.newImage( "image.png" ) self.view:insert( img ) end scene:addEventListener( "createScene" ) -- cleanup function: function scene:destroyScene( event ) print( "Called when scene is unloaded." ) end scene:addEventListener( "destroyScene" ) return scene

• Los diferentes eventos que se producen en las transiciones de escenas son los siguientes • create : cuando la escena aun no existe • show : al entrar la escena en pantalla, tiene dos fases: • will : cuando va a entrar • did : al entrar • hide : al salir la escena de la pantalla, tiene dos fases: • will : cuando va a salir • did : al salir • destroy : al destruir la pantalla • overlayBegan : cuando se llama a la escena mediante el metodo composer.showOverlay( ) • overlayEnded : cuando se esconde la pantalla mediante el metodo composer.hideOverlay( ) • Las funciones básicas para moverse entre escenas serían las siguientes • composer.gotoScene( sceneName [, options] ) la cual nos traslada a la escena cuyo nombre coincida con las opciones de transición elegidas - 121 -

• Las posibles transiciones pueden ser las siguientes: •fade •zoomOutIn •zoomOutInFade •zoomInOut •zoomInOutFade •Flip •FlipFadeOutIn •zoomOutInRotate •zoomOutInFadeRotate •zoomInOutRotate •zoomInOutFadeRotate •fromRight (over original scene) •fromLeft (over original scene) •fromTop (over original scene) •fromBottom (over original scene) •slideLeft (pushes original scene) •slideRight (pushes original scene) •slideDown (pushes original scene) •slideUp (pushes original scene) •crossFade

• Otras funciones básicas son: • composer.getPrevious( ) devuelve el nombre de la anterior escena • composer.getScene( sceneName ) devuelve la escena cuyo nombre coincida con el parametro sceneName • composer.getCurrentSceneName( ) devuelve el nombre de la escena actual - 122 -

Widget • Los widgets son elementos interactivos incluidos dentro de la librería Widget • Son elementos muy útiles que nos ayudarán a diseñar interfaces complejas de forma rápida y sencilla • A continuación expondremos algunos de los widgets más comunes y útiles Widget • Los primeros widgets a mostrar serán el on/off Switch, Radio Button y CheckBox, los cuales son especializaciones del widget genérico switch Widget

• Como siempre recordar que para utilizar una libreria primero hay que importarla • El código para implementar el controlador de un switch sería el siguiente local function onSwitchPress( event ) local switch = event.target local response = switch.id.." is on: "..tostring( switch.isOn ) print( response ) end

- 123 -

• Para crear un Checkbox sería el siguiente: local checkbox = widget.newSwitch { left = 60, top = 230, style = "checkbox", id = "My checkbox widget", initialSwitchState = false, onPress = onSwitchPress } --Text to show the on/off switch state checkbox.text = display.newText( tostring( checkbox.isOn ), 0, 0, native.systemFontBold, 18 ) checkbox.text.x = checkbox.x checkbox.text.y = checkbox.y - checkbox.text.contentHeight

• Para crear un Radio button sería el siguiente: local radioButton = widget.newSwitch { left = 150, top = 230, style = "radio", id = "My radio button widget", initialSwitchState = false, onPress = onSwitchPress } --Text to show the on/off switch state radioButton.text = display.newText( tostring( radioButton.isOn ), 0, 0, native.systemFontBold, 18 ) radioButton.text.x = radioButton.x radioButton.text.y = radioButton.y - radioButton.text.contentHeight

- 124 -

• Para crear un On/Off Switch sería el siguiente: local onOffSwitch = widget.newSwitch { left = 250, top = 230, id = "My on/off switch widget", initialSwitchState = true, onRelease = onSwitchPress }

• Para controlar manualmente cualquier switch utilizaremos el método setState switch:setState{ isOn = true, isAnimated = true, onComplete = function }

• Otro widget bastante recurrente es el button • Deberemos crear un manejador de los eventos para el botón como el que veremos a continuación local function onButtonEvent( event ) local phase = event.phase local target = event.target if ( "began" == phase ) then print( target.id .. " pressed" ) target:setLabel( "Pressed" ) --set a new label elseif ( "ended" == phase ) then print( target.id .. " released" ) target:setLabel( target.baseLabel ) --reset the label end return true end

- 125 -

• La implementación del botón quedaría de la siguiente manera local myButton = widget.newButton { left = 10, top = 80, label = "Default", labelAlign = "center", font = "Arial", fontSize = 18, labelColor = { default = {100,100,100}, over = {255,255,255}}, onEvent = onButtonEvent } myButton.baseLabel = "Default"

• El tab bar es otro elemento muy común en las aplicaciones móviles, su manejador seria parecido al siguiente local function onPress( event ) local pressedTab = event.target print( pressedTab._id.." pressed ") end

• La implementación de los diferentes botones del tab bar sería así local tabButtons = { { id = "Tab1", label = "Tab1", labelColor = { default = {0,0,0}, over = {1,1,1} }, onPress = onPress, selected = false }, { id = "Tab2", label = "Tab2", labelColor = { default = {0,0,0}, over = {1,1,1} }, onPress = onPress, selected = true }, --more tabs can follow } - 126 -

• Para colocar el tab bar en pantalla, podemos utilizar el siguiente código local tabBar = widget.newTabBar { left = 50, top = 150, width = 240, height = 60, buttons = tabButtons }

• Para seleccionar que tab debe estar seleccionado tenemos la función :setSelected([integer]) donde el parámetro indica el indice del botón que quedará seleccionado • El último widget que vamos a exponer es el Table View, pero hay muchos más widgets que os animamos a seguir estudiando en las APIs de Corona

• El widget Table View visualmente sería como podemos observar en la imagen de la izquierda. Una tabla con diferentes filas , las cuales podemos seleccionar y pinchar.

- 127 -

• Para utilizar un Table View necesitaremos crear un manejador tanto de la tabla como de las filas local widget = require( "widget" ) -- Listen for tableView events local function tableViewListener( event ) local phase = event.phase local row = event.target print( event.phase ) end -- Handle row rendering local function onRowRender( event ) local phase = event.phase local row = event.row

end

local rowTitle = display.newText( row, "Row " .. row.index, 0, 0, nil, 14 ) rowTitle.x = 160 rowTitle.y = 15 rowTitle:setTextColor( 0, 0, 0 )

-- Handle touches on the row local function onRowTouch( event ) local phase = event.phase if "press" == phase then print( "Touched row:", event.target.index ) end end

- 128 -

• A la hora de crear la tabla, primero deberemos crear la tabla como tal, y luego añadiremos las filas a dicha tabla. -- Create a tableView local tableView = widget.newTableView { top = 100, width = 320, height = 366, maskFile = "assets/mask-320x366.png", listener = tableViewListener, onRowRender = onRowRender, onRowTouch = onRowTouch, } -- Create 100 rows for i = 1, 100 do local isCategory = false local rowHeight = 40 local rowColor = { default = { 1, 1, 1 }, } local lineColor = { 0,0,0 } -- Make some rows categories if i == 25 or i == 50 or i == 75 then isCategory = true rowHeight = 24 rowColor = { default = { 0.59, 0.63, 0.71, 1 }, } end -- Insert the row into the tableView tableView:insertRow { isCategory = isCategory, rowHeight = rowHeight, rowColor = rowColor, lineColor = lineColor, } end

- 129 -

Ejercicios Tema 11 Ejercicio 1 En este ejercicio vamos a crear una aplicación mediante storyboards que deberá tener un aspecto similar al siguiente. Para ello utilizaremos además de los storyboards un tab bar con las imágenes que adjuntamos en el campus. Primero crearemos el main.lua indicando que utilizaremos storyboads y widgets. local storyboard = require "storyboard" local widget = require "widget" storyboard.purgeOnSceneChange = true storyboard.gotoScene( "scene1", "fade", 400 )

Ahora crearemos el tab bar justo después de indicar el cambio de escena, todo lo que vaya después de un cambio de escena se quedará en ejecución y no variará por dicho cambio.

-- lo que pongamos por debajo de la transición no será afectado por ella local tabButtons = { { width = 32, height = 32, defaultFile = "icon1.png", overFile = "icon1-down.png", label = "Botón 1", selected = true, - 130 -

}, {

width = 32, height = 32, defaultFile = "icon2.png", overFile = "icon2-down.png",

label = "Botón 2", },

} -- una vez creados los botones, creamos el tabBar y asociamos los botones local tabBar = widget.newTabBar { top = display.contentHeight - 50, width = display.contentWidth, backgroundFile = "tabbar.png", tabSelectedLeftFile = "tabBar_tabSelectedLeft.png", tabSelectedMiddleFile = "tabBar_tabSelectedMiddle.png", tabSelectedRightFile = "tabBar_tabSelectedRight.png", tabSelectedFrameWidth = 20, tabSelectedFrameHeight = 52, buttons = tabButtons }

Ahora que hemos creado el main, deberemos crear tantos ficheros como escenas queramos en nuestra aplicación. Para nuestro caso crearemos 3 escenas las cuales cambiarán al pinchar en la imagen. Solo mostraremos el código de una de ellas, pues visto el funcionamiento de una escena, vistas las demás. local composer = require( "storyboard" ) composer.recycleOnSceneChange = true local scene = composer.newScene() local image, fondo

Creamos el evento controlador de la imagen, para que al pinchar en ella vayamos a la escena 2 local function onSceneTouch( self, event ) if event.phase == "began" then composer.gotoScene( "scene2", "slideLeft", 800 ) - 131 -

end

return true end

Ahora añadimos los controladores para manejar los eventos producidos por los cambios de escenas function scene:create( event ) local screenGroup = self.view fondo = display.newRect(0,0,320,480) fondo:setFillColor(250,250,250) screenGroup:insert(fondo) image = display.newImage("corona.png",30,100) image.touch = onSceneTouch

screenGroup:insert( image )

text1 = display.newText( "Pulsa en la imagen para continuar", 20, 20, native.systemFontBold, 12 ) text1:setTextColor( 0 ) text1:setReferencePoint( display.CenterReferencePoint ) text1.x, text1.y = display.contentWidth * 0.5, 50 screenGroup:insert( text1 ) end function scene:show( event ) print( "1: enterScene event" ) image:addEventListener( "touch", image ) end function scene:destroy( event ) print( "Dile adiós a la escena 1" ) return scene

Por ultimo indicamos que controladores estan asociados a los eventos producidos scene:addEventListener( "show", scene ) scene:addEventListener( "create", scene ) scene:addEventListener( "destroy", scene ) return scene

- 132 -

Ampliación 1 En esta ampliación deberéis añadir el resto de controladores para los eventos lanzados por los cambios de escena, y sacar por consola el estado en el que se encuentra en cada momento, para así comprender mejor cuando se producen estos eventos. Prueba también a modificar las diferentes transiciones entre estados. Ejercicio 2 En este ejercicio vamos a crear una interfaz como la siguiente: Utilizando la librería widget podemos crear componentes visuales de forma muy rápida y sencilla con un buen acabado visual. Al pinchar en los diferentes botones cambiaremos el color de fondo de nuestra aplicación al color que indica la label de cada botón. Para crear un widget lo primero siempre es importar la librería como vemos en el código. local widget = require "widget"

Una vez importada la librería crearemos el fondo blanco de nuestra aplicación. local fondo = display.newRect(0,0,320,480)

Y para añadir cada uno de los botones necesitaremos el controlador del botón

- 133 -

local function onButtonEventAzul( event ) local phase = event.phase local target = event.target if ( "began" == phase ) then print( target.id .. " pressed" ) target:setLabel( "Pressed" ) --set a new label fondo:setFillColor(0,0,255) elseif ( "ended" == phase ) then print( target.id .. " released" ) target:setLabel( "Azul" ) --reset the label fondo:setFillColor(255) end return true end

Una vez creado el controlador podemos declarar el botón como vemos a continuación. local myButton = widget.newButton { left = 70, top = 80, label = "Azul", labelAlign = "center", font = "Arial", fontSize = 18, labelColor = { default = {0,0,0}, over = {0,0,255} }, onEvent = onButtonEventAzul }

Crea el resto de botones y comprueba su funcionalidad. Juega con las propiedades que ofrecen los widgets y prueba diferentes combinaciones. Ampliación 2 Crea en una nueva aplicación un Radio Button, un Check Box y un On/Off Switch y añádeles la funcionalidad que veas oportuno.

- 134 -

Tema 12 Motor Físico - Parte 2 Motor Físico - Parte 2 • En este tema, vamos a entrar en detalles más complejos sobre el motor físico de corona • Ya hemos visto como construir cuerpos sencillos y detectar sus colisiones, siendo capaces de tratarlas adecuadamente, pero Corona nos permite la construcción de cuerpos mas complejos utilizando varios cuerpos simples. • Al crear un cuerpo complejo a partir de cuerpos sencillos podemos definir características distintas para cada parte. Por ejemplo si construyéramos un lápiz, la goma seria un cuerpo distinto con un rebote mayor que el resto del cuerpo. • Además puesto que cualquier forma simple debe ser cóncava, cualquier forma convexa deberá formarse con múltiples formas simples • El constructor para crear un cuerpo complejo es el mismo que para crear un poligono, solo que con más de un cuerpo • physics.addBody(displayObject, [bodyType, ] , bodyElement1, bodyElement2,...) • Cada body element tendrá sus propias propiedades, junto con su forma definida para detectar las colisiones. local physics = require ("physics") physics.start() local car = display.newImage("big_red_car.png") roofShape = { -20,-10, 20,-10, 20,10, -20,10 } hoodShape = { 0,-35, 37,30, -37,30 } trunkShape = { 0,-37, 37,-10, 23,34, -23,34, -37,-10 } physics.addBody( car, "dynamic", { density= 3.0, friction=0.5, bounce=0.2, shape=roofShape }, { density= 6.0, friction=0.6, bounce=0.4, shape=hoodShape }, { density= 4.0, friction=0.5, bounce=0.4, shape=trunkShape } )

- 135 -

• Un sensor es un cuerpo simple o complejo que produce colisiones pero no interactúa con otros cuerpos. • Todos los objetos sean simples o complejos pueden ser sensores si tienen la propiedad isSensor a true. • Un ejemplo de sensores podría ser la meta en una carrera o las casillas de un ajedrez. local rect = display.newRect( 50, 50, 100, 100 ) rect:setFillColor( 1,1,1,1 ) rect.isVisible = false --optional physics.addBody( rect, { isSensor = true } )

• Los cuerpos pueden ser eliminados como cualquier otro objeto de display myBody:removeSelf() -- or – myBody.parent:remove( myBody )

• Tienen una lista de propiedades que pueden ser modificadas • body.isAwake indica si esta despierto y en simulación • body.isBodyActive indica si estan activos, la diferencia entre despierto y activo, es que un cuerpo dormido se despierta por una interacción pero uno inactivo no interacciona con nada • body.isBullet si es una “bala” se tratará como tal y se detectarán colisiones de forma continua evitando que por la velocidad del objeto eviten alguna colisión. • body.isSensor indica si el objeto es un sensor • body.isSleepingAllowed indica si tiene permitido entrar en estado dormido, no suele ser necesario pues el estado despierto dormido cambia debido a las interacciones con otros objetos • body.isFixedRotation indica si es capaz de girar sobre si mismo o verse afectado por este tipo de fuerzas

- 136 -

• body.angularVelocity indica con un valor numérico su velocidad angular • body.linearDamping indica con un valor numérico su valor de amortiguamiento lineal • body.angularDamping indica con un valor numérico su valor de amortiguamiento angular • body.bodyType indica con un string el tipo de cuerpo que simula. • “static” no se mueve ni es afectado por el resto de elementos • “dynamic” es afectado por la gravedad y las colisiones • “kinematic” son como los estáticos pero pueden ser movidos por fuerzas diferentes de la gravedad. Los objetos que se pueden arrastrar, suelen ser “kinematic” al menos durante el tiempo en el que se ejecute el arrastre • Algunos métodos muy útiles de los cuerpos son • body:applyForce(fuerzaX,fuerzaY,puntoX,puntoY) aplica una fuerza lineal sobre el punto que se indica. Muy útil si queremos simular acciones físicas • body:applyTorque(fuerzaRotacion) aplica una fuerza de rotacion sobre el centro del objeto • body:applyLinearImpulse(fuerzaX,fuerzaY,puntoX,puntoY) es como applyForce solo que aplica un solo impulso, es decir, es una fuerza momentánea. Podría usarse para simular una explosión • body:applyAngularImpulse(impulsoRotación) igual que applyTorque pero momentáneo. • Cuando hay colisiones globales con objetos complejos se devuelven valores adicionales enteros indicando cual es el elemento simple que ha recibido la colisión

- 137 -

event.element1 event.element2

• Cuando la colisión es local se crean dos campos adicionales event.selfElement event.otherElement

• Los joints o articulaciones son objetos que sirven como unión para objetos complejos a partir de cuerpos rígidos. Por ejemplo, con joints se podrían crear cadenas o marionetas. • Hay varios tipos básicos de joints, los mas sencillos son los pivot joint con los cuales puedes unir dos objetos en un mismo punto de unión. un ejemplo sería una cadena. • Los distance joint los cuales unen dos cuerpos mediante una distancia fija. • Los piston joint une dos cuerpos mediante un eje de movimiento, como los pistones del motor de un coche. • Los wheel joint combinan el piston y el pivot joint de manera que aunque siempre están a la misma distancia ambos cuerpos se mueven con libertad.

• El pivot joint como hemos explicado antes une dos cuerpos por un determinado punto. myJoint = physics.newJoint( "pivot", cuerpoA, cuerpoB, 200,300 )

• Por defecto, los joint motors tienen una rotación máxima así como una velocidad de motor. myJoint.isMotorEnabled – (boolean) myJoint.motorSpeed myJoint.motorTorque -- (get-only) - 138 -

myJoint.maxMotorTorque -- (set-only)

• Si se desea podemos restringir el ángulo de rotación para simular mejor determinados procesos. myJoint.isLimitEnabled = true – (boolean) myJoint:setRotationLimits( -45, 45 ) a1, a2 = myJoint:getRotationLimits() myJoint.jointAngle -- (get-only; value in degrees) myJoint.jointSpeed -- (get only; value in degrees per second)

• El distance joint une dos cuerpos a una distancia fija. myJoint = physics.newJoint( "distance", cuerpoA, cuerpoB, cuerpoA.x,cuerpoA.y, cuerpoB.x,cuerpoB.y )

• Tiene tres propiedades básicas • length es la distancia entre los puntos de altura • frequency es la frecuencia de amortiguación • dampingRatio es un valor entre 0 y 1 siendo 0 sin amortiguacion y 1 amortiguación crítica • El piston joint une dos cuerpos mediante un eje de movimiento restringido. myJoint = physics.newJoint( "piston", cuerpoA, cuerpoB, cuerpoA.x,cuerpoA.y, axisDistanceX,axisDistanceY )

• Pueden tener un motor como el pivot joint solo que este es lineal myJoint.isMotorEnabled – (boolean) myJoint.motorSpeed -- (linear speed, in units of pixels per second) myJoint.motorForce -- (get-only) - 139 -

myJoint.maxMotorForce -- (set-only)

• Se puede especificar también los rangos de movimiento lineal myJoint.isLimitEnabled = true – (boolean) myJoint:setLimits( 100, 200 ) p1, p2 = myJoint:getLimits() myJoint.jointTranslation -- (get-only; linear value in pixels) myJoint.jointSpeed -- (get only; value in pixels per second)

• El wheel joint combina el piston y el pivot joint. La principal diferencia con el piston joint es que los objetos pueden rotar. myJoint = physics.newJoint( "wheel", cuerpoA, cuerpoB, cuerpoA.x,cuerpoA.y, axisDistanceX,axisDistanceY )

• También dispone de unos motores lineales y rotacionales myJoint.isMotorEnabled – (boolean) myJoint.motorSpeed -- (linear speed, in units of pixels per second) myJoint.motorForce -- (get-only)

• Los límites de rango de movimiento se puede editar myJoint.isLimitEnabled = true – (boolean) myJoint:setLimits( 100, 200 ) p1, p2 = myJoint:getLimits() myJoint.jointTranslation -- (get-only; linear value in pixels) myJoint.jointSpeed -- (get only; value in pixels per second)

• El touch joint conecta un cuerpo simple con el punto actual de toque en la pantalla y añade una fuerza hacia dicho punto arrastrable. touchJount = physics.newJoint( "touch", cuerpo, cuerpo.x, cuerpo.y )

• Hay varios atributos a tener en cuenta, entre ellos la fuerza máxima, la frecuencia de amortiguación, y el ratio de amortiguación. myJoint.maxForce = 10000 force = myJoint.maxForce - 140 -

myJoint.frequency = 50 frequency = myJoint.frequency myJoint.dampingRatio = 0.2 damping = myJoint.dampingRatio

- 141 -

Ejercicios Tema 12 Ejercicio 1 Para este ejercicio vamos a crear un nunchaku el cual podremos mover pinchando sobre este. Para ello lo primero será iniciar el motor físico y crear las paredes de nuestra aplicación, quedando algo similar a la imagen. Para crear las paredes utilizaremos el siguiente código local physics = require("physics") physics.start() local suelo = display.newRect(0,420,320,460) physics.addBody(suelo,"static",{friction=0.6}) local paredIzq = display.newRect (-100, 0, 101, display.contentHeight) local paredDer = display.newRect (display.contentWidth, 0, 100, display.contentHeight) local techo = display.newRect (0, -100, display.contentWidth, 101) physics.addBody(paredIzq,"static", {bounce=0.1,friction=0.1}) physics.addBody(paredDer,"static", {bounce=0.1,friction=0.1}) physics.addBody(techo,"static",{bounce=0.1,friction=0.1})

Una vez ya tenemos creadas las paredes vamos a proceder a crear las cadenas y las uniones entre ellas, para lo cual el tipo de joint que mejor nos viene es el pivot joint.

- 142 -

local cadenaJoints={} local link ={} for i=1,5 do link[i] = display.newImage("link.png") link[i].x = 100 link[i].y = 56 + (i*24) physics.addBody(link[i],{density=2.0, friction=0.3, bounce=0}) if(i>1) then prevLink = link[i-1] cadenaJoints[#cadenaJoints + 1] = physics.newJoint("pivot",prevLink,link[i],100,46 + (i*24)) end end

Ahora crearemos los mangos de nuestro nunchaku, para ello utilizaremos el siguiente código local nunchaku={} nunchaku[1] = display.newRect(100,33,20,55) nunchaku[1]:setFillColor(0.6,0.1,0.1) physics.addBody(nunchaku[1],{density=1.0, friction=0.3,bounce=0.3}) cadenaJoints[#cadenaJoints+1] = physics.newJoint("pivot",link[1],nunchaku[1],100,55) nunchaku[2] = display.newRect(100,222,20,55) nunchaku[2]:setFillColor(0.6,0.1,0.1) physics.addBody(nunchaku[2],{density=1.0, friction=0.3,bounce=0.3}) cadenaJoints[#cadenaJoints+1] = physics.newJoint("pivot",link[5],nunchaku[2],100,200)

Con esto, tendremos creado nuestro nunchaku, pero no seremos capaces de arrastrarlo ni moverlo, para lograr el movimiento tendremos que crear dos touch joint, y para manejar los touch joint, necesitamos su controlador. El controlador quedaría de la siguiente manera local function dragBody(event) local body = event.target local phase = event.phase local stage = display.getCurrentStage() if (phase == "began") then - 143 -

stage:setFocus(body,event.id) body.isFocus = true if params and params.center then body.tempJoint = physics.newJoint("touch", body,body.x,body.y) else body.tempJoint = physics.newJoint("touch",body,event.x,event.y) end body.tempJoint.maxForce = 0.7*body.tempJoint.maxForce elseif body.isFocus then if (phase == "moved")then body.tempJoint:setTarget(event.x,event.y) elseif (phase == "ended" or phase == "cancelled") then stage:setFocus(body,nil) body.isFocus = false body.tempJoint:removeSelf() end end return true end

El touch joint tiene bastantes parámetros con los que jugar que harán que nuestro nunchaku se comporte de una manera distinta. Prueba a variar los parámetros utilizados o incluso añadir nuevos parámetros de los explicados en la teoría. Para finalizar lo único que nos queda es asociar el manejador del touch joint con los mangos de los nunchakus. Lo cual sería de la siguiente manera. nunchaku[1]:addEventListener("touch",dragBody) nunchaku[2]:addEventListener("touch",dragBody)

Ampliación 1 Haz una aplicación con la cual seas capaz de disparar un cañón o una bala mediante el pulsado de un botón. Para ello deberás utilizar los métodos body:applyLinearImpulse o body:applyForce y comprueba como varia su comportamiento.

- 144 -

Ampliación 2 Aprovechando la aplicación anterior coloca una diana estática en la pantalla y utilizando los widgets que creas oportunos elige la dirección y la fuerza que aplicarás a la bala para intentar acertar a la diana.

- 145 -