Tanto los videojuegos más sencillos que puede ejecutar un teléfono móvil, como los más avanzados y espectaculares videojuegos para consolas de última generación, incluyen en su desarrollo altas dosis de código fuente. ¿Qué es la programación? ¿Por qué es necesaria? ¿Qué formas de programación existen?
Nunca está de más entender a grandes rasgos cómo funciona un sistema electrónico como un ordenador, una consola, o un smartphone, antes de aprender programación. Todos ellos se basan en lo mismo, el cerebro de todo dispositivo o Terminator: el microprocesador. Un circuito integrado cuyo cometido es ejecutar instrucciones sencillas de forma secuencial, eso sí, a una velocidad pasmosa.
Del transistor al microprocesador
El microprocesador (y toda la electrónica digital) se basa en un componente inventado a finales de los años cuarenta: el transistor. Simplificando mucho para el caso que nos ocupa, el transistor no es más que una especie de interruptor controlado por corriente eléctrica, similar en funcionamiento a las antiguas válvulas de vacío, pero mucho más pequeño, resistente y de menor consumo. Entre otras funciones, el transistor permite que se pueda controlar la activación de un circuito mediante otro.
Usando transistores podemos componer las llamadas puertas lógicas. Estas puertas son circuitos sencillos que realizan operaciones lógicas básicas sobre señales digitales (de un bit). Abro un paréntesis para comentar el porqué del código binario: Todo sistema electrónico utiliza dos valores en la señal eléctrica para operar: nivel bajo o 0 (normalmente 0 voltios) y nivel alto o 1 (que corresponde con el voltaje de alimentación del dispositivo).
Las operaciones más comunes que se hacen con puertas lógicas son: inversión o NOT (si a la entrada hay un 1, la puerta produce un 0), multiplicación lógica o AND (con dos entradas, si a la entrada hay 1 y 1, produce 1, en cualquier otro caso da 0) y suma lógica u OR (si como entrada hay 0 y 0 produce 0, y en cualquier otro caso produce 1). Hay unas pocas más (XOR, XAND, etc.), y conviene conocerlas, porque también se usan en programación.
Si conectamos varias puertas lógicas, podemos obtener distintos tipos de biestables, que mantienen un estado hasta que una señal les haga cambiar. Y con varios biestables podemos formar contadores, registros, etc. Todo lo anterior se usará para formar circuitos síncronos: circuitos en los que todos los biestables van conectados a una misma señal (señal de reloj) y sólo cambiarán de estado cuando se produzca un cambio en esta señal, que es periódica con una frecuencia determinada. ¿Os suenan «los gigahercios» de un procesador? Pues vienen de aquí, es la frecuencia de ésta señal de reloj (1 Hz es un ciclo por segundo, 1 GHz son mil millones de ciclos por segundo). Cabe decir que no todos los circuitos digitales son síncronos, pero sí lo son todos los sistemas informáticos. El motivo principal es que los circuitos síncronos son más estables: al producirse todos los cambios a la vez y espaciados en el tiempo, los retardos de transmisión entre componentes no afectan al valor resultante.
Como todo está basado en transistores, no os extrañe que se hable de no-sé-cuantos trillones de transistores cuando se presenta un procesador nuevo. También son la razón principal del sobrecalentamiento que se produce en los dispositivos: cuando un transistor cambia de estado, se produce un pequeño pico de corriente eléctrica, imaginaos esos trillones de transistores cambiando de estado millones de veces por segundo, todos ellos a la vez (ya que es un sistema síncrono), y concentrados en un chip del tamaño de la uña del dedo meñique… Total, que debido a esto cuanto mayor sea la frecuencia de reloj, mayor es el calentamiento que sufrirá el circuito, y de ahí los peligros del llamado overclocking, que consiste en aumentar la velocidad del reloj para mejorar el rendimiento.
Así que, una vez tenemos las bases de la electrónica síncrona, podemos combinar todos estos elementos para hacer un procesador. Necesitamos como mínimo los siguientes elementos que pueden lograrse combinando puertas lógicas y biestables:
- Un banco de memoria: un registro muy grande en el que se almacenan datos en forma de bits agrupados, y al que se accede mediante las llamadas direcciones de memoria, de cuyo tamaño dependerán del tamaño del banco de memoria. Por ejemplo, si cada dirección de memoria es de 8 bits, como máximo tendremos 2^8 direcciones posibles (256). Si en cada dirección hay 8 bits de datos, habrá 256 x 8: 2048 bits en total (2K).
- Una unidad aritmético-lógica (ALU) que realizará operaciones matemáticas, mediante varios registros en los que se puede almacenar un valor.
- La lógica de control y decodificador que se encarga de obtener las instrucciones, interpretarlas y ejecutarlas.
La cosa va más o menos así: en la memoria hay almacenado un programa, que es un conjunto de instrucciones codificadas. Cada ciclo de la señal de reloj, una de esas instrucciones pasa por la lógica de control, y ésta decidirá que hacer. Por ejemplo: tomar el valor de una dirección de memoria y guardarlo en un registro de la ALU. En la siguiente instrucción, puede ordenarse al procesador sumar ese registro con otro, y almacenar el resultado en otra posición de memoria. Este tipo de instrucciones son lo que a fin de cuentas compone el programa, en nuestro caso un videojuego.
Del ensamblador a los lenguajes de alto nivel
Cada una de las instrucciones que ejecuta el microprocesador está formada por un código binario de una determinada longitud. Para simplificar la tarea de trabajar con el código binario, los números binarios se agrupan en conjuntos de 4 bits, formando un dígito hexadecimal que comprende un valor entre 0 y 16, y se representan con los números del 0 al 9 y las letras de la A a la F. Dos dígitos hexadecimales forman un byte (8 bits). Por ejemplo, la instrucción para realizar la operación AND en Motorola 6800 es 10100100, A4 en hexadecimal.
El llamado lenguaje ensamblador, no es más que una simplificación en base a nemotécnicos (normalmente de tres letras) de estos códigos, de forma que sean inteligibles por un ser humano sin poderes sobrenaturales. Es más fácil leer AND, MOV, etc. que una ristra de unos y ceros o números hexadecimales. Dado que cada microprocesador tiene un juego de instrucciones diferente, también tiene un lenguaje de ensamblador distinto, aunque muchas de las instrucciones serán comunes para todos los micros (en nemotécnico, no en código máquina).
Aún así, la tarea de programar en ensamblador es un tanto farragosa como os podéis imaginar, por lo que se hicieron necesarios los lenguajes de alto nivel, como C, C++, BASIC, Ada, etc. Estos lenguajes adaptan el código máquina a un lenguaje más natural y sencillo de entender para las personas. Utilizan un compilador que es un programa que se encarga de analizar el código de alto nivel escrito y traducirlo a código máquina. Uno de los grandes avances es que permiten la generación y utilización de librerías o código precompilado, de forma que se pueda utilizar código genérico en otros programas. Sin embargo, el código compilado nunca podrá ser usado por otro microprocesador que no tenga un juego de instrucciones compatible. Así pues, el procesador Cell de la PlayStation 3 no puede ejecutar un programa compilado para el procesador Xenon de XBox360. Sí se puede utilizar un compilador para generar un programa ejecutable por otra plataforma con el mismo código fuente, pero habrá que adaptar algunas partes y utilizar librerías específicas compiladas para esa plataforma.
Por último están los lenguajes interpretados, que en lugar de traducirse a código máquina por un compilador, utilizan un intérprete que ejecutará el programa. Esto significa que hay un programa que se ocupa de leer el código, analizarlo y ejecutarlo en tiempo real, lo que es, naturalmente, más lento, pero tiene ventajas fundamentales como la independencia de la plataforma, ya que lo que cambia de una plataforma a otra es el programa intérprete, pero el código se mantiene. Un ejemplo de lenguaje interpretado es Java, o C#. Otro caso similar son los lenguajes de script que utilizan algunos motores gráficos, como LUA, TCL o UnrealScript que también son lenguajes interpretados. En este caso, el motor hace de intérprete. Esto es útil, porque así no es necesario recompilar todo el motor cada vez que hagamos un pequeño cambio en el programa, y si el motor está preparado para funcionar en distintas plataformas, tampoco hará falta adaptar el código.
En resumen
Dado que todo aparato capaz de ejecutar videojuegos está basado en microprocesadores, es necesario un programa. Como ya hemos visto, los microprocesadores ejecutan instrucciones muy sencillas, una detrás de otra. Sólo los procesadores de varios núcleos o sistemas con varios procesadores son capaces de ejecutar varias instrucciones a la vez, aunque con algunas limitaciones y bastantes quebraderos de cabeza. En cualquier caso, la mayoría de tareas de un videojuego ocurre de forma secuencial. El que nos de la sensación de que ocurren cosas a la vez es gracias a que el programa está basado en un bucle que actualiza el juego muchas veces por segundo, ocupándose de cada elemento uno a uno, de forma que el usuario no aprecie los cambios. Si la máquina no es capaz de ejecutar el programa a la velocidad adecuada, es cuando suceden «las bajadas de frame rate», y el usuario percibirá ralentizaciones y saltos. Pero todo esto es materia de otro artículo.
Aunque nunca viene mal, no es necesaria mucha más base que la que os comento aquí para aprender programación. Sin embargo, si os ha interesado este artículo y queréis profundizar más en la electrónica digital y la programación de microprocesadores, existen multitud de simuladores y procesadores didácticos para trastear y comprender mejor cómo funcionan. En cambio, no le recomiendo a nadie meterse ya en el mundo de la electrónica analógica y el hardware a no ser que este artículo le haya abierto los ojos a una nueva vocación o tenga una extrema curiosidad…