Archivos de la categoría ‘Sin categoría’

Fundamentos de IA de juegos

Publicado: 7 septiembre, 2011 en Sin categoría

Por Geoff Howland

pathfinding.jpg

La IA (Inteligencia Artificial) de juegos es el objeto de mucha discusión recientemente, con una buena causa. Como los juegos avanzaron con rapidez en los ámbitos de gráficos y sonidos, esto viene a ser cada vez más evidente cuando las acciones de los jugadores controladores por el juego no están funcionando en una manera “inteligente”.

Más importante que la inteligencia de jugador controlado por el juego, es realmente la estupidez del jugador controlado por el juego. La mayoría de jugadores no están esperando parar cargar shooter en primera persona más reciente y enfrentarse contra el profesor Moriarty con una pistola de clavos. Ellos esperan que no estén jugando contra un paciente de lobotomía endogámica que está confundido por la complejidad de una esquina.

IA del comportamiento de las unidades

La IA de juegos no es necesariamente IA en el sentido estándar de la palabra. La IA de juegos para unidades es realmente un intento de programar cualidades de la vida real para proporcionar un desafío o una apariencia de realidad.

Un guardia en un juego que está parado en un lugar y nunca moviéndose puede, parecer muy poco realista. Sin embargo, si creas una rutina para hacer que el guardia mire a su alrededor en diferentes direcciones de tiempo en tiempo, o para que cambie su postura, puede empezar a parecer más vivo. Al crear una situación en la que otro guardia, que camina en una ruta previamente hecha, ocasionalmente se detiene delante del guardia que está en pie y parece hablar con él, la apariencia de realidad puede ser incrementada considerablemente.

Hay dos categorías de acciones en unidades de IA: Reaccionaria y espontánea.

Las unidades actúan en una manera reaccionaria cada vez que están respondiendo a un cambio en su entorno. Si un enemigo te ve y empezar a correr hacia ti y dispara, entonces ha actuado como una reacción al verte.

Las unidades actúan de una manera espontánea cuando realizan una acción que no se basa en algún cambio en su entorno. Una unidad que decide  moverse de su puesto de guardia de pie a un centinela caminante alrededor de la base, ha hecho una acción espontánea.

Mediante el uso de ambos tipos de comportamiento de unidades en tu juego, puedes crear la ilusión de que tienes unidades “inteligentes” que son autónomas, y no necesariamente máquinas simples.

IA reaccionaria

Las entradas reaccionarias siempre deben estar basadas en los sentidos de las unidades. Cuando se modela la IA después de los personajes humanos, necesitas tener en consideración su rango y distancia de visión, su sentido del oído y si es aplicable, el olfato.

La creación de niveles sin alertas es una buena manera de manejar la entrada de diferentes sentidos. Si una unidad ve a un enemigo directo en su mira, entonces debe cambiar a un modo de alerta que corresponde a la forma cómo reaccionaría al enfrentar a un enemigo. Si la unidad no ve al enemigo pero escucha sus pasos o un disparo, entonces la unidad debe ir a un correspondiente nivel de alerta que aplicaría al conocimiento no directo de la situación.

En el caso de una unidad que era un guardia, al oír un disparo haría el acto de guardia para investigar en el área el disparo que se efectuó. Al escuchar pasos puede hacer la guardia de pie en espera para emboscar a la unidad caminante. Todos estos diferentes tipos de acciones y alertas se pueden configurar en un sistema basado en reglas o en un sistema de lógica difusa para que así pueda interpretar cada sonido u observación por cada unidad y hacer que reaccionen de manera apropiada.

Una alerta general es también un factor importante en la apariencia de realidad e inteligencia en un juego. Si has estado corriendo alrededor disparando a una base llena de personas y sigues encontrando nuevos enemigos que son completamente desorientados al hecho de que no se ha dejado de dispar en los últimos diez minutos, esto va a parecer realmente fuera de lugar. Mediante la creación de un sistema de alerta para todas las unidades, y tal vez un plan de alerta, puedes fortalecer el aspecto de la realidad en el mundo de tu juego.

Un plan de alerta podría consistir en reglas que las unidades seguirían si hubo una alerta, a diferencia de las reglas para situaciones sin alerta. Por ejemplo, si hay una alerta que podría llevar a todas las unidades que se encuentran en áreas no críticas, a correr a la entrada de la base para ayudar en la defensa.

IA espontánea

La IA espontánea es extremadamente importante en la creación de un sentido de vida en los mundos de tus juegos. Si todos los que conoces están allí de pie esperando para que les hables o para que los mates, o peor aún, vagando alrededor sin rumbo, esto no va a ser muy convincente para el jugador.

Algunos métodos para dividir el problema de pie alrededor son para dar a cada una de las unidades un conjunto de objetivos sin alerta. Estos podrían incluir caminar preestablecidas, caminar a regiones preestablecidas aleatoriamente, parar al lado de otras unidades ocasionalmente cuando les pasen y caminar con otras unidades a destinos preestablecidos. He dicho preestablecido en todas estas situaciones, porque a menos que aparezcas con un algoritmo muy bueno para el establecimiento de destinos o rutas de tus unidades, tus unidades van a parecer como que están vagando sin rumbo.

Acciones de las unidades

¿Qué hace realmente a unidad del juego parecer inteligente en sus acciones? Si se mueve de una manera que el jugador también podría, o realizar una acción, como agacharse, en una situación que el jugador también podría, entonces la unidad puede parecer inteligente. No necesitas una gran cantidad de acciones para crear una ilusión de acciones inteligentes, sólo necesitas lo suficiente para cubrir cualquier situación básica en que tus unidades estarán involucradas.

La mayoría de áreas que cubras, si las cubres bien, la mayor posibilidad de que tus jugadores creerán que tus unidades están actuando “inteligentemente”. Colócate en el lugar de tus unidades ¿Qué harías en su situación? ¿Cómo responderías a las diversas formas de ataque o encuentros del enemigo? ¿Qué harías si nada pasó en lo absoluto?

Si respondes a estas preguntas, y correctamente las implementas para cada situación que tus unidades encontrarán, maximizarás tus posibilidades de tener unidades que parecen inteligentes, que es el primer paso para crear una buena IA, una IA de juego sólida.

Traducción: David Soria

Por Roberto Albornoz Figueroa (RCAF)
2 de octubre del 2006

Introducción

La mayoría de las personas que han estudiado o están cursando alguna carrera relacionada con computación e informática y han pasado por ramos relacionados con la programación, se han preguntado alguna vez: ¿Cómo se hace un videojuego?, ¿Qué tengo que saber? ¿Por donde empiezo? y muchas preguntas más. Muchos han intentado hacerlo sin resultado alguno, otros en cambio han podido crear sus primeros juegos con un poco de esfuerzo, pero aun así siempre quedan preguntas pendientes por responder y bastante por aprender.

Este artículo pretende responder algunas de esas preguntas y dar a conocer que entrar al mundo del desarrollo de videojuegos, no es algo tan complicado, pero se necesita siempre estar investigando, estudiando, leyendo sobre el tema, y lo más importante practicar mucho, ya se sabe que la práctica hace al maestro.

Existe gran cantidad de libros relacionados con el desarrollo de videojuegos, la gran mayoría se encuentra en idioma inglés, quizás para los que están empezando esto sea un impedimento, así que ya podemos contestar una de las preguntas ¿Qué tengo que saber?, debemos tener por lo menos algún conocimiento básico de ingles para entender documentos, artículos, libros, etc. para estar al día con la tecnología que se aplica en el mundo de los videojuegos. Al final de este artículo podremos ver un listado de algunos libros que son útiles para comenzar con el desarrollo de videojuegos.

Pac-man

Cuando se habla de desarrollo de juegos, se suele pensar que existe un programador, que debe ser capaz de hacer todo el juego, pero pensar esto es un gran error, ya que realizar un videojuego involucra aparte de programadores, otros personajes como diseñadores, músicos, escritores, etc. Este artículo se centrará en los programadores, pero hay que tener en cuenta que si se quiere producir un buen juego, además de la originalidad, debe tener buenos gráficos, buena música, jugabilidad, etc. Aunque todo lo que involucra este proceso puede ser construido por un solo programador, es más adecuado tener a un especialista en cada área.

Para comenzar en el desarrollo de videojuegos es preferible hacerlo con proyectos pequeños, con el fin de ser capaces de cumplir las metas que nos fijamos, nunca pensar en proyectos extremadamente grandes que se nos escapen de las manos, ya que lo mas probable es que nunca los terminemos, lo cual nos llevará a la frustración.

Mario 3

Es común que las personas que se dedican a esta área del desarrollo, comiencen con remakes de juegos como: Pong, Tetris, Arkanoid, Pacman, Spice Invaders, Sokoban, Snake, Mario Bross, etc. Es un muy buen ejercicio desarrollar algún juego antiguo con algunas modificaciones, aprenderemos bastante. Se pueden realizar videojuegos muy buenos y entretenidos basándonos en las mejores características de otros, solo basta dejar correr nuestra imaginación.

La selección de juegos nombrados anteriormente son algunos de los primeros que aparecieron en la historia de los videojuegos, como el famoso Pong. Todos estos juegos tienen algo en común, son juegos en 2D. Hoy en día la gran mayoría de los títulos que salen al mercado son juegos 3D como Doom, Quake, Call of Duty, Unreal Tournament, Need for Speed y un largo etcétera.

Muchos pretenderán comenzar a desarrollar un juego del tipo Doom, tienen que pensar que realizar uno de estos juegos de forma independiente (solo un programador) es una tarea titánica, además todos estos videojuegos fueron realizados por grandes equipos de personas (no solo programadores) y con presupuestos millonarios. Esto no quiere decir que no podamos crear uno de estos, pero hay que ir paso a paso. Cuando ya tengamos dominado los juegos 2D, pasaremos a crear algún proyecto 3D de un tamaño moderado o una buena idea es adaptar uno de nuestros juegos 2D a 3D.

nfs_mw.png

Herramientas necesarias

Ahora veremos que herramientas son necesarias para convertirse en un desarrollador de videojuegos. Antes que todo necesitamos manejar algún lenguaje de programación, entre mejor sea el nivel de conocimiento que tengamos de aquel lenguaje más rápido avanzaremos.

El lenguaje por excelencia usado en la historia del desarrollo de los videojuegos profesionales ha sido C y desde la última década el gran Dios es C++, debido a las ventajas que este posee, ya que hace uso del paradigma de la programación orientada a objetos, aunque también podemos usar características de la programación procedural de C.

Pero no solo basta con conocer un lenguaje de programación para que nuestras ideas de lo que queremos que realice el juego sean escritas en este lenguaje, sino que también necesitamos “algo” que nos transforme todo ese código que solo entienden los humanos a otro código que entienden las máquinas, estamos hablando del famoso compilador que realiza esta conversión a ceros y unos. Existen compiladores para cada tipo de lenguaje y estos pueden ser gratuitos o comerciales.

Ya dijimos que la mayoría de los proyectos son realizados en C/C++ pero hay otras alternativas, existen lenguajes compilados e interpretados. Ejemplo de un lenguaje compilado es C/C++, ejemplo de lenguajes interpretados (en vez de un compilador necesitan de un interprete para funcionar) son Python, Basic, etc.

Como ejemplo de compilador tenemos a MinGW (Minimalist GNU for Windows) el cual es un port del GCC de Linux. Este compilador es gratuito, pero no solo es el compilador, sino que también contiene otras herramientas que nos apoyan en el proceso de desarrollo, como es el linker, make, debugger, profiler, etc

Además del lenguaje y el compilador, necesitamos un editor de texto donde podamos escribir nuestras líneas de código, es aquí donde aparecen los Entornos de Desarrollo Integrado mas conocido como IDE (Integrated Development Environment). IDE’s hay para todos los gustos, pero existe una bastante cómoda de usar y con muy buenas características, estamos hablando de Code::Blocks. También puede ser usado Dev-C++ el cual ya trae en su distribución el compilador GCC, aunque desde hace mucho tiempo no es actualizado. Code::Blocks tiene varias características que lo hace superior a Dev-C++, una de ellas es el visor de funciones, variables, clases y la completación de código, que todo programador agradecerá. Otro Compilador/IDE bastante usado en estos tiempos es Microsoft Visual C++.

Conocimientos necesarios

Ya conocemos cuales son las herramientas básicas para comenzar, pero esto es solo la punta del iceberg, necesitamos otros conocimientos que probablemente en un principio para juegos sencillos no sean necesarios, pero lo serán a medida que el juego vaya aumentando su complejidad.

Para comenzar conviene saber sobre el manejo de estructuras de datos, esto es parte de los conocimientos básicos que debería tener cualquier programador.

Hoy en día disponemos de un conjunto de bibliotecas que nos facilitan el uso de estructuras de datos en nuestros programas, estamos hablando de la biblioteca STL (Standard Template Library), la cual ya es un standard de C++, y lo podemos encontrar en todos los compiladores actuales.

Para no entrar en detalle sobre esta biblioteca (lo cual debe ser discutido en otro artículo) solo diremos que STL es una colección de estructuras como listas, pilas, colas, vectores, etc. y algoritmos que manejan estas estructuras de forma transparente al programador. Conviene aprender a usarlas ya que nos ahorrarán mucho trabajo y nuestro código quedará más ordenado y elegante.

Siguiendo con los conocimientos, también debemos saber matemáticas, no tenemos que ser unos genios en esta área, pero ayuda bastante tener conocimientos de trigonometría y algebra lineal. Para comenzar no es tan necesario saber todo esto, pero mientras los proyectos se van haciendo más complejos, será necesario comenzar a revisar los viejos cuadernos y libros de matemáticas que tenemos guardados.

Si hay que saber matemáticas, no podíamos dejar afuera la física, pilar fundamental para ciertos juegos que simulan micromundos, donde intervienen fuerzas gravitacionales, velocidades, aceleración, masas, en general cualquier variable física. Básicamente en juegos de tipo plataformas como Mario Bross, se aplican ciertas cosas de cinemática básica en los personajes, como en el movimiento que realizan, cuando saltan, caen, etc. y por supuesto todo el tema relacionado con las colisiones y respuesta es parte de la física del juego. Existe una API que nos facilita el manejo de la física, se llama ODE (Open Dynamics Engine) la cual es Open Source. En un principio para construir nuestros primeros videojuegos la física será muy sencilla (si es que la tiene), y no será necesario hacer uso de complicadas bibliotecas.

Podemos hablar también del networking, que es todo lo relacionado con la comunicación de datos a través de redes (acá aparecen términos como sockets, TCP, UDP, etc.). Por ejemplo en todo juego multijugador debemos estar enviando datos de posiciones, puntajes y otras características de personajes, objetos, etc. a otras máquinas para que exista una interacción entre todos los jugadores con el fin de ver reflejado en nuestro PC lo que están haciendo los otros jugadores en sus propios computadores. Esto es usado hoy en día en todos los juegos multijugador, gran ejemplo de esto es Unreal Tournament. En esta área también disponemos de bibliotecas especializadas en el tema, un ejemplo es SDL_Net.

No podemos dejar afuera el scripting, que abarca todo lo relacionado con la generación de archivos scripts, donde cada uno de estos contiene variables, datos y código. Por ejemplo si en un juego necesitamos crear interfaces, o mantener posiciones de ciertos objetos, es más cómodo tener un archivo con esta información y que desde el programa principal se lea el archivo y se interprete, esto facilita mucho la modificación del juego, tanto en el proceso de desarrollo como en futuras actualizaciones, y nos permite liberarnos de estar compilando cada vez que hagamos un cambio en el código fuente. Existen lenguajes de scripting que nos facilitan la vida como Python y LUA.

También debemos aprender Inteligencia Artificial, ¿Qué sería de un juego donde los enemigos se comportan de forma poco “inteligente”?, simplemente no sería divertido jugar, y rápidamente aprenderíamos los movimientos y su comportamiento. Aquí es donde podemos aplicar conocimientos de IA en nuestros juegos, dándole un toque más real y mas entretenido.

Algunos métodos de la IA aplicadas al desarrollo de juegos son el PathFinding (Implementado a través del Algoritmo A* para la búsqueda del camino más corto entre dos puntos) aplicado en juegos como Starcraft, cuando por ejemplo queremos mover un personaje a cierto punto del mapa y este es capaz de llegar a ese punto esquivando todos los objetos que encuentre a su paso y usando el camino más corto. Otros métodos aplicados son los algoritmos genéticos, vida artificial, máquinas de estados finitos, árboles de búsqueda, también áreas extensas de la IA como las redes neuronales, etc.

Mediante scripts podemos generar el comportamiento que tendrá un personaje en un juego, a estos personajes se les llama NPC (Non Playing Character).

APIs Gráficas

Antes de terminar con los conocimientos no podemos dejar de hablar de algo importantísimo, el manejo de una API Gráfica. Una API (Application Programming Interface) o Interfaz de Programación de Aplicaciones, es un conjunto de funciones que realizan tareas específicas facilitando la vida al programador. Al hablar de API grafica nos referimos a un set de funciones para inicializar por ejemplo modos gráficos, realizar copiado de datos de la memoria del computador a la tarjeta de video (blitting), etc. Existen API‘s especificas para cada tipo de tarea que queramos realizar.

Actualmente las API‘s Gráficas más usadas son OpenGL y DirectX.

OpenGL

OpenGL (Open Graphics Library) es una API multiplataforma creada por Silicon Graphics en 1992, que maneja solo el aspecto gráfico de un sistema, dejando afuera el sonido, música, control de teclado, mouse, joysticks, gamepads, etc., los que deben ser controlados con otras API‘s especializadas. Existe una biblioteca multiplataforma para el manejo de audio tridimensional llamada OpenAL (Open Audio Library).

DirectX

DirectX es una API multimedia creada por Microsoft en 1995, que consta básicamente de Direct3D para la parte gráfica, DirectSound y DirectMusic para la parte de audio, y DirecInput para el control de teclados, joysticks, etc. También incluye DirectPlay para comunicación de datos en redes.

El uso de estas dos API‘s en un comienzo puede ser un poco complicado y engorroso, especialmente DirectX. Existen algunas alternativas como Allegro o SDL, las que son multiplataforma.

SDL Simple Directmedia Layer

SDL (Simple DirectMedia Layer) es una API gráfica para realizar operaciones de dibujado en 2D, gestionar efectos de sonido y música, y cargar imágenes.

Existen varias librerías que complementan SDL, algunas de ellas son:

  • SDL_image: Carga de imágenes en diferentes formatos: png, jpg, etc.
  • SDL_mixer: Carga de formatos de sonido como wav, mp3, ogg, etc.
  • SDL_net: Comunicación de datos en redes.
  • SDL_ttf: Uso de fuentes TrueType.
  • SDL_gfx: Dibujo de primitivas gráficas, escalar/rotar imágenes, control de framerate, Filtros de imagen MMX.

SDL permite básicamente crear aplicaciones en 2D, pero si queremos extender estas capacidades podemos usarla en conjunto con OpenGL, ya sea para crear nuevamente aplicaciones 2D que aprovechen las características de aceleración por hardware, que todas las tarjetas de video poseen hoy en día, como para crear aplicaciones completamente en 3D.

Ya vimos las herramientas y conocimientos necesarios para comenzar en el desarrollo de videojuegos, pero no podemos dejar de mencionar que también existen los Engines o Motores de juego, que están basados en algunas de las API‘s ya vistas, OpenGL o DirectX principalmente, que proveen al programador todas las funcionalidades necesarias para el desarrollo de un juego.

Básicamente un Engine esta formado por varios sistemas y subsistemas, por ejemplo un sistema gráfico para manejar objetos 2D o 3D, un sistema de control de entrada (teclado, mouse, etc.), sistema de texto, sistema de red, sistema de scripts, sistema de audio, etc. Todos los conocimientos necesarios que vimos antes ahora están aplicados en un Engine. API‘s Gráficas

Existen Engines gratuitos (algunos bastante buenos y complejos) y otros comerciales, por ejemplo podemos nombrar los siguientes: Irrlicht, Ogre 3D, Crystal Space 3D, PopCap, Torque, etc.

Para comenzar a desarrollar conviene utilizar algo más sencillo (pero no por eso menos potente). Por ejemplo podríamos usar la dupla SDL/OpenGL junto a otras librerías externas para construir nuestro propio Engine.

No solo es necesario disponer de las herramientas y conocimientos que ya vimos para el desarrollo de juegos, aun hay más, pero esto ya no es tarea del programador (aunque conviene también tener estos conocimientos en forma general por lo menos) estamos hablando del Diseño Gráfico de un juego. Alguien debe ser capaz de realizar las imágenes que aparecerán, ya sea en menús, pantallas de configuración y en el juego mismo.

Los típicos conocimientos que debe tener un diseñador grafico, son el uso de alguna herramienta de retoque fotográfico como Photoshop, Paint Shop Pro o Gimp, y herramientas para modelar objetos 3D tales como 3D Studio Max, Maya, Blender, entre otros.

Y no puede quedar afuera tampoco, algun personaje que componga la música o cree y/o edite los sonidos que aparecerán en el juego. Ejemplo de este tipo de software es Adobe Audition, Acid Pro, Reason, Fruity Loops, etc.

Estructura básica de un videojuego

Antes de terminar veremos básicamente como funciona por dentro un videojuego.

Primero hay que aclarar que un videojuego es un programa diferente a los programas convencionales.

Un videojuego debe funcionar en tiempo real, en todo momento mientras se está ejecutando, el juego debe estar realizando alguna tarea como: dibujar los objetos, actualizar coordenadas, calcular colisiones, etc., independiente de si el usuario hace algo. Obviamente debe también estar esperando que ocurra algun evento, ver si el usuario presiona alguna tecla, si mueve el mouse, o presiona algun botón de este y luego actuar en consecuencia. Todo esto y más ocurre en un ciclo o loop.

Básicamente la estructura de un videojuego consta de las siguientes partes:

  1. Inicialización
  2. Ciclo del videojuego
    1. Entrada
    2. Procesamiento
    3. Salida
  3. Finalización

Ahora veamos una explicación en más detalle de cada una de ellas:

1. Inicialización: aquí inicializaremos todo lo que será usado luego en el ciclo del videojuego. Por ejemplo aquí inicializaremos la librería gráfica, un modo gráfico, el sistema de sonido/música, de texto y cualquier otro tipo de sistema necesario. Además reservaremos memoria para los objetos que intervienen en el juego, creación de estructuras de datos, etc. Carga de sonidos, de imágenes y de recursos en general. También en este proceso se inicializarán las posiciones iniciales de los personajes, carga de puntajes desde un archivo, etc.

2. Ciclo del videojuego: el ciclo del videojuego es un loop que se estará repitiendo una y otra vez. Aquí es donde ocurre toda la acción del juego, y la única forma para poder salir de este ciclo es cuando el jugador pierde, llega al final del juego o sale del videojuego con alguna combinación de teclas o presionando algun botón del mouse, etc. El ciclo del juego consta básicamente de tres partes:

2.1. Entrada: en esta parte se obtiene desde algun dispositivo de entrada (teclado, mouse, joystick, etc.) todo lo que realiza el jugador, por ejemplo que tecla presionó/soltó del teclado, que botón del mouse presionó/soltó, si movió el mouse en alguna dirección, etc.

2.2. Procesamiento: aquí se procesa toda la información que se recibió en el punto anterior y se toman decisiones a partir de los datos de entrada. Es decir aquí está toda la lógica del juego. Se procesa la física, inteligencia artificial, comunicación de datos en red, etc.

2.3. Salida: en este punto se muestra toda la información ya procesada en el punto anterior, aquí es donde mostramos los gráficos en pantalla, reproducimos sonidos, etc.

3. Finalización: por último en esta parte se hace básicamente lo opuesto a lo que hicimos en la inicialización, es decir, eliminar de la memoria todos los recursos almacenados, ya sea imágenes, sonidos, música, etc. Cerrar todos los sistemas que se abrieron en la inicialización. Guardar datos de puntajes en un archivo, etc.

Si quisiéramos ver lo anterior en lenguaje C, la estructura básica se vería así:

int main(int argc, char ** argv)
{
    int salir = 0;

    while (! salir)
    {
        entrada();
        procesamiento();
        salida();
    }

    finalizacion();
    return 0;
}

Resumiendo vimos que los conocimientos básicos para desarrollar juegos son:

  • Lenguaje de programación.
  • Compilador/IDE.
  • Matemáticas.
  • Física.
  • Networking.
  • Scripting.
  • Inteligencia Artificial.
  • API Gráfica.
  • API para Sonido y Música.

Y la estructura básica de un videojuego la podemos ver en un simple diagrama de flujo:

Diagrama de flujo de la estructura básica

Recursos

API’s Gráficas y complementarias

IDE’s/Compiladores

Libros

Aquí tenemos algunos libros que nos ayudarán a adquirir más conocimientos y reforzar los que ya tenemos sobre el desarrollo de videojuegos. Todos estos ejemplares pueden ser adquiridos por medio de Amazon.

Por Roberto Albornoz Figueroa (RCAF)
10 de enero del 2007

Introducción

Una de las razones para comenzar a desarrollar primero videojuegos 2D, es que los conceptos involucrados son mucho más simples y fáciles de asimilar.

Además lo más probable es que varios de estos conceptos ya sean conocidos por quienes lleven un tiempo programando o trabajando con computadores.

Otra ventaja, es que obtendremos resultados más rápidos, a diferencia del desarrollo de videojuegos 3D, que involucra conocer tópicos más avanzados de matemáticas y de programación. Pero tampoco hay que asustarse, si queremos luego dar el próximo paso a las 3D, ya que hay que recordar siempre que todo lo podemos estudiar y aprender, nada es imposible.

En las próximas páginas conoceremos la terminología básica, que nos hará comprender de mejor forma los conceptos gráficos involucrados en un videojuego.

Conceptos básicos

Partiremos hablando del dispositivo externo más común y que todos ya conocemos, el Monitor. Este dispositivo de salida es uno de los más importantes que posee nuestro computador, ya que nos permite visualizar la información con la que estamos trabajando.

monitor.jpg

El monitor se conecta al computador a través de una Tarjeta de video. Esta tarjeta es un circuito integrado que nos permite transformar las señales digitales con las que trabaja, en señales análogas para que puedan ser visualizadas en el monitor. Este proceso es realizado a través de un conversor analógico-digital que toma la información que se encuentra en la memoria de video y luego la lleva al monitor. Este conversor se conoce como RAMDAC (Random Access Memory Digital Analog Converter).

tarjeta_video.png

La superficie de la pantalla de un monitor es fluorescente y esta compuesta por líneas horizontales. El hardware del monitor actualiza de arriba hacia abajo cada una de estas líneas. Para dibujar una imagen completa el monitor lo hace a una cierta velocidad, a esto se le conoce como Frecuencia de refrescado y se mide en Hz. Los valores frecuentes son de 60/70 Hz, incluso frecuencias mayores. Es preferible que supere los 70 Hz para que la vista no se canse tanto y no aparezcan los molestos parpadeos (flickering). El valor de la frecuencia depende de la resolución usada.

refrescado_monitor.png

El tiempo que existe entre dos refrescos de pantalla se conoce como Vertical retrace, y corresponde al momento en que el haz de electrones del monitor regresa a la parte superior de la pantalla.

El concepto más básico usado en la programación gráfica es el de píxel, significa Picture Element, es decir, un elemento de una imagen que corresponde a la unidad mínima que esta puede contener. También lo podríamos definir como un simple punto que es parte de una imagen determinada, que podemos mostrar en pantalla y que tiene asociado un color.

composicion_imagen.png

Esta imagen que está formada por un arreglo ordenado de pixeles en forma de grilla o cuadricula, se conoce con el nombre de Bitmap. Un bitmap posee dimensiones: ancho y alto (que se miden en número de pixeles) y además tiene asociado un formato, que puede ser alguno de los ya conocidos: bmp, png, jpg, gif, etc.

La diferencia entre formatos gráficos, viene dada por algunos parámetros que esperamos tener, como la calidad o tamaño del archivo. Por ejemplo si queremos mantener un equilibrio en tamaño/calidad, un formato que se ajusta a estos parámetros son los formatos jpg o png, no así el formato bmp, que casi no posee ningún tipo de compresión.

Una imagen tiene otras características, aparte de sus dimensiones, una de estas es la Profundidad de color. Esto corresponde al número de bits necesarios para poder representar un color, es conocido por las siglas BPP (Bits Per Pixel).

El número de bits que podemos utilizar es de 1, 8, 16, 24 o 32. Con este dato podemos saber cual será el número máximo de colores que puede representar una imagen en pantalla. El número de combinaciones se calcula mediante la siguiente fórmula:

Cantidad de colores = 2bpp

Ahora podemos resumir en una tabla cada una de estas combinaciones:

Número de bits Cantidad de colores Nombre
1 2 Monocromo
8 256 Indexado
16 65.536 Color de alta densidad (High Color)
24 16.777.216 Color real (True Color)
32 4.294.967.296 Color real (True Color)

El color se representa en pantalla utilizando el Modelo RGB (Red, Green, Blue), es decir, para formar un color necesitamos conocer la intensidad de estos colores primarios. Cada una de estas intensidades corresponde a un número que se encuentra en el rango: 0 a 255.

Cuando se utilizan 8 bits para mostrar un pixel, como ya sabemos, tenemos como máximo 256 colores, pero no se representan directamente utilizando las intensidades RGB, sino que se mantiene una Paleta o Colormap (un arreglo de tamaño igual a 256 bytes), donde cada posición apunta a un color con las 3 componentes (Rojo, Verde, Azul). Esta paleta puede ser modificada para implementar algunos efectos, ya que si cambiamos por ejemplo las componentes de un color, se verán modificados inmediatamente todos los pixeles que hagan referencia a dicho color. El modo de 8 bits, se conoce también como Modo indexado y ofrece un alto rendimiento, pero como ya sabemos con la desventaja de tener pocos colores disponibles.

Para profundidades más altas, esta paleta ya no existe, y esto nos permite tener un abanico más grande de colores. En la actualidad ya no usamos modos de video a 8 bits, normalmente usaremos 16 o 32, siendo el modo de 32 bits el más eficiente, ya que los procesadores de 32 bits trabajan con datos empaquetados en bloques de 4 bytes.

Todas las componentes que forman un color se empaquetan en un entero, donde se asigna una cantidad fija de bits para cada una de ellas.

El orden en que se empaquetan las componentes de un color, depende de la arquitectura en la que se esté trabajando, podemos usar el formato RGB (Big Endian) o BGR (Little Endian).

En arquitecturas x86 (Windows, Linux) se usa el formato Little Endian (el byte de menor peso se almacena en la dirección más baja de memoria y el byte de mayor peso en la más alta) y en PowerPC (Mac), Big Endian (el byte de mayor peso se almacena en la dirección más baja de memoria y el byte de menor peso en la dirección más alta).

La cantidad de bits usada para cada componente de un pixel, la podemos ver resumida en la siguiente tabla:

8 bits 16 bits 24 bits 32 bits
1:5:5:5 8:8:8 8:8:8:8
Se utiliza una paleta, donde cada posición apunta a un color que posee las intensidades RGB. El primer bit indica la transparencia, si es 0, la transparencia será total, si es 1 habrá opacidad total. 8 bits para cada corresponde al canal componente RGB El primer byte 8 bits para cada corresponde al canal componente RGB alpha (componente que almacena el grado de transparencia). Los últimos 3 bytes son las componentes RGB del color.
5:6:5
5 bits para componente Red, 6 bits para componente Green, 5 bits para componente Blue

Anteriormente mencionamos que una imagen tiene dimensiones y a esta dimensión (ancho y alto) también se le conoce como Resolución. La resolución nos permite conocer cuanto detalle existe en una imagen, esto quiere decir que a mayor resolución obtendremos mayor calidad. La resolución no solo se aplica a una imagen, sino que también a la pantalla o monitor. Existen algunas resoluciones estándar:

  • 320×200
  • 320×240
  • 640×480
  • 800×600
  • 1024×768
  • 1152×864
  • 1280×720
  • Etc.

El modo de video es la unión de la resolución con la profundidad de color y se denota como AnchoxAltoxBpp.

Por ejemplo podríamos trabajar con los siguientes modos de video:

  • 640x480x8 (640 pixeles de ancho x 480 pixeles de alto a 8 bits por pixel)
  • 640x480x16 (640 pixeles de ancho x 480 pixeles de alto a 16 bits por pixel)
  • 800x600x32 (800 pixeles de ancho x 600 pixeles de alto a 32 bits por pixel)
  • Etc.

La elección de un modo de video u otro, es un factor importante a la hora de comenzar a desarrollar un videojuego, ya que con estos datos sabremos cual será el flujo de información (medido en bytes) que existirá. Además las imágenes que se muestren en pantalla deberán tener algún tamaño determinado, que debe ajustarse a la resolución elegida.

Por ejemplo supongamos que estamos trabajando con el siguiente modo de video, 800x600x32 y queremos conocer cuando memoria ocupa una imagen con las mismas dimensiones que la resolución seleccionada, entonces el tamaño que ocupará será de:

Tamaño = 800 x 600 x 4 = 1.920.000 bytes = 1.83 Mb

Multiplicamos por 4, ya que 32/8 = 4 bytes, es decir para mostrar un solo color en pantalla o píxel necesitamos 4 bytes.

El resultado anterior no es un tamaño despreciable, imaginemos el tamaño que podríamos obtener a resoluciones mayores.

En los primeros juegos que aparecieron para PC, se utilizaban resoluciones muy bajas como 320×200 (también conocido como modo 13h) o 320×240 (modo x) y a 8 bits por píxel, o sea como ya sabemos disponíamos de un máximo de 256 colores.

A medida que pasaron los años se empezaron a utilizar resoluciones de 640×480 o 800×600 y a profundidades de color más altas, de 16 o 32 bits, todo esto gracias a que aumentaron las capacidades de las tarjetas de video.

Hoy en día, en la mayoría de los juegos tenemos la posibilidad de elegir el modo de video que mejor se adapte a nuestro equipo. Existen algunos casos típicos, como cuando queremos tener el mejor rendimiento en nuestro videojuego, bajamos la resolución al mínimo o a una resolución intermedia. Al contrario, cuando disponemos de un buen equipo y una buena tarjeta de video, lo más probable es que seleccionemos un modo de video más alto, para disfrutar de unos gráficos de mejor calidad.

Otro concepto importantísimo es el de Video RAM, básicamente es la memoria o buffer que se encuentra en la tarjeta de video. Toda tarjeta de video tiene como mínimo una memoria, un microprocesador, una caché y un bus de datos, que normalmente es AGP.

En la Video RAM se almacenan los datos que serán mostrados posteriormente en la pantalla, estos datos pueden ser texturas, vértices, etc.

Para el desarrollo de videojuegos 2D, esto se simplifica ya que solo tendremos en la memoria cada uno de los píxeles que forman la imagen.

En el caso de juegos 3D, en la memoria se pueden almacenar algunas rutinas o procedimientos que ejecutan algunas instrucciones para generar normalmente algún efecto, a estas rutinas se les conoce como Shaders.

Además, el microprocesador también conocido como GPU (Graphics Processing Unit), se comunica con la memoria a través de un bus interno, ejecuta instrucciones y actualiza el caché de instrucciones, de texturas y geometría. El proceso de llenar la memoria de video con datos se hace posible a la existencia de la GPU, el cual lo hace a través de un bus de puerto acelerado, conocido como AGP. Después el monitor lee periódicamente desde el buffer de video (Video RAM) los datos y refresca la pantalla, lo que nos permite ver una imagen en el monitor. No importa la velocidad con que se modifiquen los datos del buffer, el monitor no actualizará nada hasta el próximo refresco.

Técnicas 2D

Ahora continuaremos viendo otros conceptos, que llamaremos técnicas 2D. Estos términos son aplicados básicamente al desarrollo de aplicaciones 2D y algunos son la base para las 3D.

Sprites

Un sprite es cualquier objeto que aparece en nuestro videojuego. Normalmente tiene asociado algunos atributos, siendo los más básicos una imagen y una posición.

donkey_dance.jpg

Los sprites pueden ser estáticos o dinámicos. Al hablar de sprite estático, nos referimos a un objeto que no cambiará su posición durante el videojuego, por ejemplo la imagen de una llama, piedra, etc. En cambio un sprite dinámico, es aquel que tendrá algún tipo de movimiento, el cual puede ser realizado por el usuario o por el computador, por ejemplo el sprite de un jugador o de los enemigos.

Representar un sprite en memoria puede ser realizado de muchísimas formas, y puede depender mucho del gusto del programador y su experiencia. Normalmente se almacenan sus atributos en una estructura o clase.

Animación de sprites

Esto es bastante simple, aquí aparece el concepto de frame o cuadro, que corresponde a una de las imágenes que forman la animación completa de un sprite. Podemos pensar en una secuencia de imágenes que son dibujadas rápidamente, que engañan al ojo humano dando el efecto de un movimiento fluido. Es el mismo método que se utiliza para los dibujos animados.

Secuencia de imagenes que componen una animación

El número de frames dibujados en un periodo de tiempo se conoce como frame rate (tasa de frames), siendo un aspecto muy importante en la animación, ya que de esto depende la calidad de animación que obtendremos. Si cada uno de los frames se muestra muy rápido o muy lento, la ilusión del movimiento se perderá y el usuario verá cada uno de los frames como una imagen separada.

Una animación muy común puede ser la de un personaje caminando, saltando, o realizando cualquier otra acción.

Esto también debe ser representado de alguna forma inteligente en nuestro videojuego, utilizando algún tipo de estructura de datos adecuada, como un vector o lista.

Normalmente el sprite tendrá como atributo una lista de animaciones (las acciones del personaje, enemigo, etc.), donde cada animación corresponderá a otra lista con cada uno de los frames.

La totalidad de los frames que forman un personaje, suele almacenarse en un solo archivo de imagen, a esto se le llama sprite sheet, es decir, una imagen donde tenemos secuencias de frames para las diferentes acciones que puede realizar un personaje en un videojuego. La siguiente imagen lo ilustra claramente.

Sprite Sheet

Transparencias: Color keys

Cuando mostramos una imagen en pantalla, esta siempre se verá como un cuadrado o rectángulo, pero sabemos que dentro tiene la representación de un objeto, personaje, enemigo, ítem, etc. Al dibujar esta imagen queremos ver solo la forma que representa, para esto debemos elegir algún color transparente, pintar con ese color las zonas que no queremos que aparezcan y luego dibujar la imagen ignorando dicho color.

Normalmente se utiliza el color magenta (255, 0, 255), como color de transparencia, ya que es poco probable que se encuentre en una imagen.

Dependiendo de la API gráfica que utilicemos para desarrollar nuestro videojuego, tendremos una función que realice esta tarea, la de descartar un color determinado al dibujar la imagen.

sprite_sheet.jpg

Pero no solo usar un color como el magenta es la solución para no ver una imagen en su forma original, también podemos hacer uso de las propiedades del formato gráfico PNG (Portable Network Graphics), ya que este formato guarda un canal alpha con las partes transparentes de la imagen.

Transparencias: Máscara

Existe otro método para ignorar un color en una imagen, el uso de una máscara que se antepone al bitmap original, es decir, debemos tener otro bitmap que indica cuales son los pixeles transparentes. El color que indica la transparencia es el negro, y el color blanco la figura que queremos mostrar. Por lo tanto, al dibujar la imagen, recorreremos cada pixel de la máscara, y si este color es blanco, mostraremos el pixel del bitmap original que se encuentra en esa posición.

donkey_mascara.jpg

Transformaciones

Cuando dibujamos un sprite en pantalla, tenemos la posibilidad de aplicar algunas transformaciones, las más usadas son las siguientes:

Transformaciones: Traslación

Esta es la transformación más simple, corresponde a cambiar la posición del sprite para luego dibujarla en otro lugar de la pantalla, dando el efecto de movimiento, sin duda lo más usado en cualquier videojuego. Y se resume en asignar una nueva posición a las coordenadas (x, y) del sprite.

Por ejemplo si las variables x e y son las coordenadas de la esquina superior izquierda del sprite, y vx, vy las velocidades en cada eje, con una simple suma en cada componente cambiaremos la posición del sprite.

x = x + vx;
y = y + vy;

Transformaciones: Rotación

Otra transformación típica, que consiste en girar el sprite un número determinado de grados. Se usa para mostrar objetos vistos desde otro ángulo, por ejemplo en el videojuego Micro Machines, cuando doblamos cambiamos el ángulo del auto y luego este cambio lo vemos reflejado en la pantalla gracias a la rotación.

donkey_rotating.png

También lo podríamos usar para realizar efectos para una presentación, efectos durante el videojuego, etc. Además al rotar una imagen debemos saber con respecto a que punto lo haremos (pivote), por ejemplo en la siguiente imagen realizamos unas rotaciones con respecto al centro de la imagen.

Transformaciones: Escalado

Otra transformación muy usada en algunos videojuegos, consiste en escalar una imagen, es decir, cambiar su tamaño (normalmente de forma proporcional), ya sea un aumento o una disminución. Se puede usar para dar un efecto de profundidad, es decir, si el objeto está más alejado de nosotros lo dibujaremos más pequeño, en cambio si está muy cerca de nosotros, lo dibujaremos de un tamaño más grande.

Suele usarse también en conjunto con la rotación para realizar algunos efectos.

Al utilizar esta transformación, hablamos de scale factor, es decir, un valor que indica el porcentaje de escalado, donde el valor 1 corresponde al tamaño normal.

donkey_scaling.jpg

En la imagen anterior nos damos cuenta que al aumentar su tamaño, se produce un efecto no deseado en los bordes de la imagen, conocido como aliasing, es decir, las líneas o curvas que forman los bordes de la imagen se ven discontinuas (en forma de escalera). Para esto existe una solución, el antialiasing, una técnica que permite suavizar todos los bordes y así disminuir el efecto de escalera. En toda API gráfica encontraremos una función que realice esta tarea.

antialiasing.jpg

Transformaciones: Flipping

El flipping es un tipo de transformación especial para realizar algunos efectos.

Básicamente existen dos tipos, Vertical Flip que corresponde al efecto que se produce cuando se refleja un objeto en el agua y Horizontal Flip que corresponde al reflejo de un objeto en un espejo. Con la siguiente imagen quedará mucho más claro.

donkey_flipping.jpg

Composción alfa (Alpha blending)

La composición alfa o mezclado alpha es una técnica para crear transparencias en una imagen con respecto al fondo. Para lograr esto se agrega un cuarto canal a un color, llamado canal alpha.

Ahora los colores de una imagen se representarán como RGBA. Cada valor alfa de un pixel representa su grado de opacidad, donde un valor cero indica un 100% de transparencia, y a medida que aumentamos su valor se irá haciendo más opaco. El rango va desde 0 a 255.

Para crear un canal alpha en una imagen, tenemos dos opciones, una es almacenar esta información con algún software de edición de imágenes (Photoshop) y en un formato apropiado. (PNG, Portable Network Graphics), o hacerlo posteriormente en el mismo videojuego, utilizando alguna función de la API que nos permita setear un grado de transparencia para la imagen.

El funcionamiento de esta técnica es relativamente simple, ya que se realiza una media ponderada de cada pixel que forma la imagen y los pixeles que conforman el fondo.

Por ejemplo supongamos que tenemos un color de una imagen, el (100, 40, 50) con un valor alfa de 90, y dibujaremos este pixel sobre otro con el valor (70, 100, 80). Con estos datos el pixel resultante tomara un 35% del primer color y un 65% del segundo.

Entonces, multiplicaremos cada componente del pixel por el porcentaje que le corresponde y luego sumaremos cada unas de las componentes, obteniendo así el pixel resultante.

  • Pixel1: (100, 40, 50) / *0.35
  • Pixel2: (70, 100, 80) / *0.65
  • Pixel resultante: (100*0.35+70*0.65, 40*0.35+100*0.65, 50*0.35+80*0.65) = (81, 79, 70)

El cálculo anterior se debe hacer para cada pixel que forma la imagen, lo cual afecta al rendimiento, pero hoy en día todas las tarjetas de video realizan este tipo de operaciones vía hardware.

donkey_alpha_blending.jpg

Esta técnica se puede aplicar a un bitmap completo o solo a un grupo de pixeles de la imagen. Es utilizado para hacer una variedad de efectos en videojuegos, por ejemplo puede usarse para dar un carácter de fantasma a cierto personaje, para hacer un menú transparente, escribir texto sobre un rectángulo transparente, etc.

Screen buffer

El screen buffer es simplemente un área de la memoria de video, donde podemos dibujar algo que se mostrará en la pantalla. Si queremos mostrar un pixel, debemos tener un puntero a la dirección de memoria de video y calcular la posición (offset, desplazamiento) donde queremos dibujar el pixel y luego en esa posición copiar el byte o los bytes que representan el color.

Supongamos que estamos trabajando en el siguiente modo de video: 640x480x24, es decir, tenemos 640 pixeles de ancho, 480 pixeles de alto, y cada pixel ocupa 3 bytes, entonces:

int color;      // Aca almacenamos el color del pixel
int * buffer;   // Screen buffer
int x, y;       // Coordenadas del punto

// Calculamos el desplazamiento
int desp = y * 640 + x * 3;

// Colocamos un pixel en la posicion (x, y)
buffer[desp] = color;

En el ejemplo anterior no mostramos como se calcula el valor numérico del color, para hacerlo hay que hacer uso de operaciones a nivel de bits y desplazamientos, y esto depende de la profundidad de bits que se este usando. Supongamos que el color tiene la siguientes componentes (200, 150, 60) y estamos trabajando a 24 bits. Si recordamos la tabla de componentes RGB, para 24 bits tenemos que cada componente ocupa 8 bits, así que el cálculo es el siguiente:

int r = 200;
int g = 150;
int b = 60;

Color = (r << 16) | (g << 8) | b;

Es decir, gracias a los desplazamientos colocamos la componente en su posición correcta y luego sumamos todos estos valores con el operador OR a nivel de bits.

Para calcular la posición de memoria donde debe colocarse el pixel, la posición “y” es multiplicada por el ancho en pixeles de la resolución elegida, es decir, con esto nos movemos “y” líneas por la pantalla, luego en esa posición debemos movernos unos ciertos bytes hasta llegar a la posición “x” que buscamos. Para eso sumamos la posición de la coordenada “x” multiplicada por el tamaño del pixel en bytes, en este caso como estamos usando una profundidad de color de 24 bits, se debe multiplicar por 3. Luego asignamos el color al buffer justo en la posición que calculamos anteriormente.

En general esta es la forma elemental para dibujar un pixel en pantalla. Para dibujar una imagen completa debemos recorrer cada punto de la imagen, calcular la posición y asignar el color a dicha posición. Para nuestra suerte, todas las APIs gráficas traen funciones para crear un color, como lo hicimos anteriormente, y funciones para dibujar una imagen completa en pantalla.

Superficie

Este término es la base de la programación 2D. Básicamente es una zona de memoria que puede estar en la RAM o Video RAM y es usada para almacenar un bitmap. Si queremos ver esto en el ámbito de la programación, una Surface es una estructura o clase que tiene como mínimo las misma propiedades de un archivo de imagen, es decir, tendrá un arreglo lineal de datos que contiene cada uno de los pixeles que forma la imagen y además el ancho y alto del bitmap. Dependiendo de la biblioteca gráfica que se use, pueden existir más atributos.

Una Surface se podría confundir con el concepto de Sprite, pero en general un Sprite tiene como atributo al menos una Surface.

Blitting

El blitting es una operación para transferir un bloque de bytes (surface) de un sector de la memoria a otra. Viene del termino Blit (Block Image Transfer, Transferencia de Imagen por Bloque) y es una forma de renderizar (dibujar) sprites con o sin transparencias sobre un fondo. Esta técnica puede ser acelerada por hardware, lo cual ayuda a incrementar bastante el rendimiento, de hecho esta operación es una de las más críticas en cualquier videojuego y es la que más usaremos.

Gracias al blitting podemos armar una escena en un videojuego, por ejemplo en la siguiente imagen vemos que a partir de dos superficies, una con los sprites de los personajes y otra con un fondo, armamos a través de blits una tercera superficie que contiene la escena final.

No se ha encontrado nada.

En una operación de blitting, podemos especificar en que posición de la superficie destino queremos copiar la superficie origen, incluso que parte de la superficie origen queremos copiar. De esta forma podemos colocar en cualquier posición, superficies dentro de otras. La forma de especificar las superficies origen/destino y sus posiciones, depende de la API gráfica que se esté utilizando. Siempre se nos proveerá de una función de blitting, ya que todas las bibliotecas disponen de al menos una.

Doble búfer (double buffering)

Para entender esta técnica, antes que todo debemos saber que el monitor ya sea CRT (Tubo de rayos catódicos) o LCD (Pantalla de cristal líquido), no dibuja la imagen en la pantalla instantáneamente, sino que lo hace pixel a pixel, partiendo desde la esquina superior izquierda hasta la esquina inferior derecha. Esto se realiza a una velocidad bastante rápida que el ojo humano no percibe, y a este tiempo, como vimos al principio, se le conoce como frecuencia de refrescado.

Cuando queremos mostrar una imagen en la pantalla, como ya sabemos colocamos los pixeles de la imagen en la memoria de video y durante el tiempo de refrescado el monitor leerá la información que hay en la Video RAM y la mostrará en la pantalla.

Ahora imaginemos que queremos mover un objeto por la pantalla, los pasos son simples, primero dibujamos la imagen en una posición, luego la borramos, y la dibujamos en la nueva posición, pero aquí aparecen algunos problemas, si a la mitad del refrescado cambiamos la imagen en la memoria de video, el monitor ahora obtendrá otra información de la imagen y probablemente la parte superior e inferior de la imagen no correspondan, es decir, veremos imágenes superpuestas y además un molesto parpadeo. Una posible solución a esto, es esperar a que el monitor termine de refrescar la pantalla, para luego escribir en la Video RAM la nueva imagen. La mayoría de las APIs gráficas disponen de alguna función que espera el refrescado del monitor, evitando así el parpadeo y la superposición de imágenes, pero no del todo.

Para evitar totalmente el parpadeo, usamos esta técnica llamada Double Buffering, que consiste en tener dos áreas de memoria en la RAM (o Video RAM). Una de estas zonas se conoce como front-buffer, y corresponde a la que se muestra actualmente en pantalla y también tenemos el back-buffer, que es donde dibujamos los objetos que formarán la escena final. Estas áreas de memoria deben tener las mismas dimensiones del modo de video seleccionado.

Ahora tenemos dos opciones para esta técnica. Si el back-buffer se encuentre en la RAM, deberemos copiar todo el contenido al front-buffer, es decir, a la memoria de video para que veamos los cambios en el monitor. Para esto realizaremos un proceso de blitting, que dependiendo del modo de video usado, puede ser un poco costoso.

La otra posibilidad es que el back-buffer también se encuentre en la Video RAM, de ser así, no tendremos que realizar un blitting, sino que haremos algo más simple, intercambiaremos estas dos zonas de memoria, lo cual tiene un costo bastante bajo. A este proceso de intercambio del back-buffer con el front-buffer, se le conoce con el nombre de Page Flipping, y es lo que se suele usar hoy en día, ya que disponemos de más RAM en las tarjetas de video. Además la función de Page Flipping, está implementada en todas las APIs gráficas, así que solo deberemos llamarla en el momento adecuado.

Rectángulos sucios (dirty rectangles)

Esta técnica es una forma de optimizar la anterior. Con Double Buffering constantemente estamos volcando el contenido completo de una zona de memoria a otra, pero puede haber ocasiones donde no este sucediendo ningún cambio en pantalla, o tal vez solo haya cambiado una pequeña parte de esta. Es aquí donde aparece la técnica, Dirty Rectangles (rectángulos sucios) y su funcionamiento es bastante simple. Copiaremos a la Video RAM solo las áreas de la pantalla que han cambiado, por lo tanto, cada vez que algún sprite cambie su posición, copiaremos a la memoria de video el área de un rectángulo que rodee al sprite (incluyendo el área donde se encontraba antes) y la colocaremos justo en las mismas coordenadas en la Video RAM. Pero no siempre es recomendable utilizar esta técnica, ya que podemos tener demasiados sprites moviéndose por la pantalla, y ya no sería óptimo estar copiando cada rectángulo a la Video RAM, ya que sería lo mismo que copiar la pantalla completa, en un caso así conviene solo usar la técnica Double Buffering.

Recorte (clipping)

El recorte es un técnica bien sencilla, consiste en definir una área de corte para la pantalla, es decir, todo lo que se dibuje fuera de esta área será ignorado y no se mostrará en pantalla. Normalmente esta área de recorte coincide con la resolución elegida para el videojuego, pero puede ser cualquier otra.

Sistema de coordenadas 2D

Es importante saber que cuando dibujamos algo en pantalla, siempre tendremos que informar la posición en la cual dibujaremos el objeto. Por defecto la posición de un objeto se encuentra en la esquina superior izquierda, pero algunos prefieren usar como punto de anclaje el centro, y en general puede ser cualquier otro punto. Calcular el centro del punto es tan simple como sumar a la posición x, la mitad del ancho del objeto y a la posición y, la mitad de la altura del objeto.

Nosotros estamos acostumbrados a trabajar en el cuadrante I del plano cartesiano, donde las coordenadas del eje “x” crecen hacia la derecha y las coordenadas del eje “y” crecen hacia arriba, pero al trabajar con gráficos en el computador, esto es relativamente distinto, ya que el eje y se invierte, es decir, ahora el mundo está al revés, las coordenadas en el eje “y” crecerán hacia abajo.

En la siguiente imagen podemos ver el plano cartesiano en su forma normal versus el plano cartesiano utilizado en la programación gráfica.

Sincronización en los videojuegos

Lo ideal en cualquier videojuego, es que todos los objetos se muevan a la misma velocidad, independiente de la velocidad del computador donde se ejecute. Si no nos preocupamos por esto, y ejecutamos nuestro videojuego en un computador antiguo, por ejemplo, en un Pentium de 100 Mhz, probablemente el videojuego se vea muy lento, en cambio si lo ejecutamos en un computador con un procesador de última generación, un Pentium IV a 2.4 Ghz, se verá tan rápido que será imposible jugar.

Para solucionar este problema, disponemos de dos métodos.

Sincronización por cuadros por segundo (framerate)

El primer método, consiste en sincronizar el framerate, también conocido como FPS o Frames per Second (cuadros por segundo). Al hablar de FPS, nos referimos a la frecuencia con que se ejecuta el ciclo principal de un videojuego en un segundo. A mayor cantidad de FPS obtendremos una mayor fluidez en las animaciones.

En el cine se considera que una velocidad de 24 FPS es suficiente para que el ojo humano perciba una animación fluida, pero en los videojuegos esta cantidad es demasiado baja. Valores adecuados son sobre 60 o 100 FPS.

El método es bastante sencillo, y lo primero que debemos hacer cuando comienza el ciclo del videojuego, es obtener el tiempo transcurrido hasta ese momento, que normalmente se mide en milisegundos. Luego procesamos lo relacionado al videojuego, ya sea entrada, IA, lógica del juego, detección de colisiones, dibujo de gráficos, etc. y antes de terminar el ciclo, creamos otro loop en el cual vamos obteniendo el tiempo transcurrido hasta ese momento, calculamos la diferencia de tiempo y verificamos si es menor a los FPS que buscamos. De esta forma cada vez que ejecutemos el programa en una máquina diferente, el programa esperará el tiempo adecuado para obtener los FPS.

Utilizando este método, en computadores más rápidos se verá más fluido, pero si lo ejecutamos en una máquina con un procesador mucho más antiguo del que usamos para desarrollarlo, lo más probable es que se vea bastante lento.

Claro, no todo podía ser perfecto, pero es por eso que los videojuegos piden algunos requerimientos mínimos.

Ahora veamos el código para este método de sincronización:

int t1, t2;     // Almacena los tiempos inicial y final
int fps = 100;

while (! salir)
{
    t1 = GetTime(); // Obtenemos tiempo inicial

    // Hacemos algo ...

    do
    {
        t2 = GetTime(); // Obtenemos tiempo final
    } while ((t2 - t1) <= 1000/fps);

    // Si llegamos a este punto es porque ya paso el tiempo de espera
    // para que se cumpla el tiempo esperado
}

Sincronización por tiempo

El segundo método consiste en sincronizar en base al tiempo. En este método no importa el framerate que posea el videojuego, pero aun así los objetos se moverán a la misma velocidad en todas las máquinas.

Básicamente, lo que debemos hacer es calcular la posición de un objeto de una forma distinta, en el método anterior si queríamos mover un objeto 5 pixeles por frame haríamos lo siguiente:

x = x + 5;

Y para mantener siempre la misma velocidad en varios equipos, esperaríamos un tiempo determinado al final del ciclo del videojuego.

Lo que haremos ahora será obtener el tiempo que ha transcurrido hasta el momento de calcular la nueva posición del objeto, y multiplicar ese tiempo por la velocidad. Es decir:

x = x + vx*dt;

Ahora la variable vx, que almacena la velocidad, no será entera (int) sino que un numero flotante (float), ya que ahora lo multiplicaremos por un delta t de tiempo.

Imaginemos ahora que vx tiene el valor 0.005, y el ciclo demora 1 segundo en ejecutarse, el nuevo incremento será de:

x = x + 0.005*1000
x=x+5

Ahora el incremento es menor, claro, debe ser así porque en tan solo 10 milisegundos se mueve 0.05 pixeles, entonces cuando pase 1 segundo se habrá movido:

(1000/10)*0.05 = 100*0.05 = 5 pixeles

Justamente lo que esperábamos, ahora no importan los FPS que existan en el videojuego, basándonos en el tiempo, los objetos se moverán a la misma velocidad en cualquier equipo.

Veamos esto en un simple código:

int t_new, t_old;       // Almacena los tiempos actual y anterior
int dt;   // Diferencia de tiempos

t_new = GetTime();      // Tiempo actual

while(!salir)
{
    t_old = t_new;      // El tiempo anterior

    // Hacemos algo ...
    t_new = GetTime();  // Obtenemos el tiempo actual
    dt = t_new - t_old  // Calculamos la diferencia de tiempos
    x = x + vx*dt       // Calculamos la nueva posicion
    // Dibujamos objetos ...
}

Este método se usa bastante en videojuegos 3D, ya que el framerate varía mucho en cada ciclo, dependiendo de la cantidad de objetos que se deban renderizar.

Pero este método también tiene su desventaja, a pesar de que los objetos se mueven siempre a la misma velocidad, en un computador más lento, el desplazamiento no se verá fluidamente. Esto se aprecia en el ejemplo que ya vimos, en el caso extremo de demorarse 1 segundo cada ciclo, cada vez que se deba mover un objeto, este aparecerá 5 pixeles más a la derecha, produciéndose un salto muy notorio. En computadores aun más lentos, se comenzará a ver un parpadeo en el desplazamiento de los objetos.

Pero como ya dijimos nuestro videojuego siempre tendrá unos requerimientos mínimos y tendremos que seleccionar el método que más nos acomode. Pero claramente el primer método, sincronización por framerate, es el más simple y el más extendido.

Resumen

Hemos aprendido o repasado varios conceptos, que nos ayudarán a comprender mejor la programación gráfica que se usa en los videojuegos 2D.

Todos estos conceptos se pueden aplicar a cualquier biblioteca gráfica, muchas de las cosas que vimos ya vienen implementadas, como por ejemplo setear un modo de video, obtener un color, realizar operaciones de blitting, efectos de alpha blending, usar transparencias por color keys, transformaciones como rotación, escalado, traslación, page flipping, etc., por lo tanto no será necesario que escribamos código para esto, pero conviene conocer su funcionamiento.

Saludos

Publicado: 6 septiembre, 2011 en Sin categoría

Hola a todos los lectores. Desde ahora estaré publicando material. Espero os interese. ¡Feliz desarrollo!

Bienvenida

Publicado: 29 marzo, 2011 en Sin categoría

Bienvenidos a este blog dedicado a Python y pygame.