Después de un tiempo sin nuevas entregas, os dejo con la cuarta parte del capítulo 6, que es más larga de lo habitual para compensar con la tercera. Esta vez introducimos conceptos muy importantes como pueden ser las funciones, el diseño descendente o la programación estructurada.

Para empezar a entender la utilidad de las funciones veremos un caso directo en nuestro programa. Cuando hemos diseñado el menú tan sólo nos hemos preocupado de la faceta visual, no le hemos dotado de ninguna utilidad ya que tal y como está ahora el usuario no puede navegar entre las opciones del menú. Para navegar entre ellas, deberemos comprobar la pulsación arriba o abajo y incrementar o decrementar el elemento del array Juego.OpcionMenu. Nos encontramos con que esta tarea será necesaria en varios menús, y si generalizamos aún más, en cualquier tarea que implique moverse o modificar el valor de algo en función de la pulsación. Entonces, la cuestión es, ¿deberíamos repetir el código cada vez que lo necesitemos o existe alguna forma de evitarlo?

Ahí es donde aparece el concepto de función, y en general el de subprograma. Antes de pasar a crear e implementar una función, debemos aclarar aún más este concepto, porque hasta el momento lo que nos ha llevado a pensar en ellas es un caso muy limitado que nos presenta sólo una de sus utilidades (la reutilización de código sin caer en repeticiones), cuando su importancia ni mucho menos se limita a eso, sino que es uno de los pilares de la programación estructurada y muy importante para el diseño descendente.

Después de haber programado nuestra escena de entrada y de parte del menú, llevamos suficientes lineas de código a la espalda como para habernos dado cuenta de ciertos hechos, que en parte hemos comentado. La más evidente es que el orden de las instrucciones y su ejecución (flujo) es secuencial, las primeras se ejecutan primero y así sucesivamente, esta es la estructura o el esquema secuencial. Y que a la hora de controlar el orden de ejecución, encontramos condicionales (p.e:if) o estructuras iterativas (p.e:while), es decir el esquema condicional y el esquema iterativo respectivamente. La existencia de estas tres estructuras nos lleva a uno de los teoremas fundamentales de la programación, el de Böhm y Jacopini:

Todo algoritmo propio puede ser expresado en términos de sólo tres tipos de estructura: secuencial, condicional y repetitiva.

No vamos a entrar a definir lo que es un algoritmo, sino que nos centraremos en lo que implica, que es que mediante estas tres estructuras podemos resolver cualquier problema que se nos presente a la hora de programar. Pero aún así nos encontramos con la imposición de cómo resolver el problema, y para eso existe una metodología para el diseño de programas llamada diseño descendente o método del refinamiento sucesivo, íntimamente relacionado con un paradigma de programación, la programación estructurada.

A grandes rasgos, el método del diseño descendente se basa en dividir un problema dado en problemas menores y de mayor facilidad de resolución. Cada vez que se simplifique un problema, en otro menor aplicamos un nivel de refinamiento, no pudiendo mezclar tareas de diferente complejidad en un mismo nivel.

Por ejemplo, si queremos buscar un punto en un texto, podemos dividir esa tarea en subtareas para acometerlo. Primero desplazaríamos la mirada al texto, leeríamos, y una vez identificado el punto almacenaríamos su posición o en nuestra cabeza o apuntándolo. Pues si extrapolamos al diseño de un programa que debe buscar un punto en un string: leeríamos el string carácter a carácter, encontraríamos o no el punto, almacenaríamos su posición en RAM o en MS y posiblemente lo mostraríamos en pantalla.

La programación estructurada es un paradigma de programación que mezcla que los tres esquemas iterativos junto con la posibilidad de componer un programa con subprogramas menores, reutilizables y de aplicación generica a un mismo problema. Esos subprogramas suelen implementarse como funciones o como procedimientos dependiendo del lenguaje (en lua serán sólo funciones). El carácter génerico lo conseguimos mediante el paso de parametros, que en la practica son variables de ámbito local que en lugar de declararse e inicializarse dentro del subprograma obtienen sus valores en la llamada del mismo.

La relación entre el diseño descendente y la programación estructurada, es que el diseño descendente nos permite diseñar los programas, su estructura, y la programación estructurada nos permite implementarlos (llevarlos a cabo). Así vemos que ciertas tareas en el diseño descendente serán apropiadas para convertirse en funciones (por ejemplo leer un carácter de un texto dado), otras deberán ser aún más refinadas (por ejemplo sumar un vector) y otras las podremos implementar directamente con las herramientas que nos proporciona el lenguaje o con funciones ya programadas (sumar 2+2, o comprobar si el carácter leido es igual a punto).

Tras esta pequeña introducción ya debemos ser conscientes de que estamos trabajando dentro del paradigma de la programación estructurada, pero tampoco nos vamos a preocupar demasiado por eso. Son aspectos técnicos de programación a los que sería lo correcto ceñirse, sobretodo porque son las herramientas que nos permitirán realizar programas más complejos y que nos simplificarán la tarea de diseñar software, pero que en general no comentaremos pese a que se estén aplicando. También se dan casos en que me he tomado pequeñas licencias ya que es un curso introductorio y a veces el diseño correcto pasa por complicar más el problema a nivel técnico, y la intención es explicar de una forma sencilla y global el diseño de un videojuego.

Los últimos párrafos han sido muy teóricos, pese a que eran prescindibles a este nivel de complejidad, considero que es muy importante introducir estos conceptos para entender las herramientas de las que disponemos y como funcionan hasta cierto punto. Por eso, ahora que ya hemos introducido el diseño descendente junto con la programación estructurada, podemos introducir que mecanismo ofrece Lua y en otros muchos lenguajes para la implementación de subprogramas: las funciones y los procedimientos.

Los procedimientos son una serie de tareas que se ejecutan en sucesión (secuencialmente) al ser invocados y que pueden recibir parámetros. Algunos lenguajes permiten que la función reciba datos y envie datos por parámetro, esto quiere decir que los parámetros pueden ser leídos, modificados o ambos.

La funciones son instrucciones que se ejecutan secuencialmente al ser llamadas, que pueden recibir datos por parámetros, y que además devuelven un valor que normalmente se especifica mediante la palabra clave return. Esto hace que las funciones representen valores, mientras los procedimientos sólo realizan una serie de tareas, así debemos concebir la llamada a una función como el valor que devuelve allá donde la usemos. Por ejemplo, si una función devuelve siempre un entero de valor 2, debemos pensar que la llamada a la función representa un valor 2.

Por tanto tanto funciones como procedimientos son conjuntos de instrucciones que se realizan secuencialmente para solventar un problema mayor o ejecutar un trabajo, de forma que este quede resuelto en una llamada a la misma, en lugar de realizar todo el conjunto de instrucciones reiteradamente cada vez que se necesite llevar a cabo la tarea. La diferencia entre ambas está en que los procedimientos realizan esas tareas, y las funciones además devuelven un valor de forma que dicha función representa dicho valor dentro del código. Una vez que acaba su ejecución el flujo del programa vuelve al punto donde fue llamado el subprograma.

declaración de una función en Lua empieza con la palabra clave function, seguida del nombre de la función y los argumentos, acabando con un end. El nombre y los argumentos se conocen como firma de la función y será lo que nos permita llamarla en otra parte del código. Como hemos comentado el valor de retorno se especificará con la palabra return, una vez que se devuelve el valor la función para su ejecución y vuelve al lugar desde que fue llamada. Lua no distingue entre procedimiento y función, sino que para declarar un procedimiento bastará con crear una función sin sentencia return, es decir que no devuelva ningún valor. Lua tampoco nos permitirá modificar el valor de las variables introducidas como parámetros, sino que se realizará una copia a una variable local de ámbito de visibilidad la función.

Un pequeño Ejemplo

Antes de continuar haremos un paréntesis en el curso, y nos olvidaremos por un momento del juego que estamos desarrollando para crear un pequeño programa que nos permita ver la aplicación del diseño descendente, la programación estructurada y la programación de funciones. El programa sumará dos vectores cualquiera, para no distraernos con problemas ajenos a los conceptos que queremos poner en practica los declararemos y no serán introducidos por el usuario. Los vectores será considerados dentro del espacio euclidiano en 3D, es decir que tendrán 3 componentes que expresaremos como un array de enteros.

En primer lugar vamos a pensar en los pasos que seguimos al sumar dos vectores nosotros mismos, en el papel o mentalmente. Primero vemos los valores de los vectores, es decir los leemos, lo que en el programa se traducirá en la inicialización de las variables. En caso de que fueran introducidos por el usuario, estos serían introducidos por él. Después realizamos la suma, pero nos damos cuenta que es una tarea compleja, y nosotros mismos en nuestra mente la subdividimos o refinamos en sumar componente a componente del vector. Por último escribimos el resultado en el papel, análogamente en nuestro programa lo visualizaremos en pantalla. Por tanto ya hemos analizado el problema, y lo podemos expresar en forma algorítmica:

Algoritmo Sumar Vector Es
                  Inicializar;
                  SumaVector;
                  Visualizar;
Fin

Como hemos comentado, la tarea de sumar vector debe ser refinada, puesto que no la podemos realizar directamente con una instrucción. Para ello escribiremos el algoritmo de la misma:

Algorimo SumaVector ES
                 SumarPrimerasComponentes;
                 SumarSegundasComponentes;
                 SumarTercerasComponentes;
Fin

Ya no debemos refinar nada más, ya que las componentes de un vector son enteros, y el lenguaje nos permite realizar suma de enteros, es decir, el lenguaje nos ofrece una solución directa al problema. Es interesante mencionar, que a veces estas soluciones directas nos las ofrecerán las librerías o paquetes, de forma que aunque estas tareas esten refinadas nos llegan en forma de función cuya forma de funcionar (implementación) nos es indiferente ya que las veremos como cajas negras que realizan una tarea especifica.

Una vez creado el algoritmo, debemos decidir que refinamientos son apropiados para constituirse en subprograma. En esta fase estamos relacionando directamente el diseño descendente con la programación, y aplicamos el diseño a la arquitectura e implementación de nuestro programa. La unica tarea refinada es SumaVector. La suma de vectores es una tarea habitual y genérica, sirve para todos los vectores, por tanto nos será muy útil que sea un subprograma. Existen varios criterios para decidir que refinamientos crear como subprogramas, pero el de reutilización es quizás el más absoluto. Crear un subprograma de la suma de vectores nos permitirá disponer de una herramienta para sumar cualquier vector en cualquier programa, es decir, nos proporcionará la posibilidad de reutilización que nos ahorrará tiempo y trabajo.

El lenguaje en el que se expresan los algoritmos se llama pseudo-código y consiste en usar el lenguaje normal de forma que posteriormente puedan ser traducidas a código. Esta forma de diseñar un programa nos ayuda a entender mejor nuestro código, nos facilita la resolución del programa y reduce el tiempo de programación, ya que la programación dejará de ser un proceso completamente creativo para acercarse más a algo así como una traducción.

Por último programaremos el algoritmo. En este ejemplo la función Suma Vector estará en el mismo fichero Lua, pero en posteriores explicaremos como ubicarlas en otros ficheros para mejorar el orden y la legibilidad del código. Esto nos lleva a mencionar algo que parece evidente: antes de llamar a una función, esta debe haber sido declarada.

--Declaracion de la funcion encargada de sumar vectores
function SumaVector(Vector1, Vector2)
local V = {0,0,0}--Vector que contendrá el resultado
	for i = 1, 3 do--Iteramos sobre las 3 componentes
              V[i] = Vector1[i] + Vector2[i]
	end
return V
end

-- Este programa no tendrá bucle principal ya que solo hace la suma, la visualiza en pantalla y espera

--Incializacion
V1 = {5,3,9}--Vector 1 declarado y incializado, cambia los valores para ver el resultado
V2 = {89,1,7}--Vector 2 declarado y incializado, cambia los valores para ver el resultado
VR = {}--Vector para almacenar el resultado
Blanco = Color.new(255,255,255)
--Suma de Vectores
VR = SumaVector(V1,V2)--Le asignamos a VR el valor devuelto por SumaVector(como funcion representa un valor que puede ser asignado)

--Visualizacion
screen:print(50,50,"El Resultado es:", Blanco)
screen:print(70,60, "(" ..VR[1].. "," ..VR[2].. ","..VR[3].. ")" , Blanco)
screen.flip()
screen.waitVblankStart(10000)