Archivo de la etiqueta: juegos

Gestión del tiempo en el desarrollo de videojuegos

Leo en CyberHades que ya hace un año de la reaparición del código fuente del Prince of Persia por parte de Jordan Mechner, que fue el videojuego que me hizo tocar por primera vez un ordenador :cry:. Esto de por sí ya merece una serie de entradas, tanto el juego y su desarrollo (no prometo poder resistirme a escribir sobre él en el futuro) como la odisea de la recuperación posterior de dicho código de formatos físicos maltratados por el tiempo (traducción más o menos libre de éste artículo de wired).

El motivo de esta entrada, es porque leo que el gran Fabien Sanglard ha publicado las tres primeras partes de una de sus clásicas reviews de código de Prince of Persia. Para quien no lo conozca, Sanglard es un tipo que gusta de leer el código fuente de videojuegos clásicos antes de almorzar. Lecturas muy recomendables que merecen otro post.

Esto me ha hecho volver a leer algunos de sus fantásticos posts, entre los cuales he dado con uno que me gustaría comentar, más que nada porque cuando desarrollas videojuegos (a cualquier nivel) antes o después vas a topar con este problema, la gestión del tiempo en el bucle principal.

Es habitual, sobretodo entre los clásicos (yo siempre lo he hecho así) utilizar el paradigma de actualización siguiente:

  1. Obtener el lapso de tiempo entre la última actualización del juego y el momento actual
  2. Actualizar el mundo en función de ese valor y el valor de las entradas
  3. Renderizar (pintar) el mundo

Sirva como ejemplo el bucle principal de Quake (1996, idSoftware):

WinMain
{
	while (1)
	{
		newtime = Sys_DoubleTime ();
		time = newtime - oldtime;
		Host_Frame (time)
		{
			setjmp
			Sys_SendKeyEvents
			IN_Commands
			Cbuf_Execute
			/* Network */
			CL_ReadPackets
			CL_SendCmd
			/* Prediction//Collision */
			CL_SetUpPlayerPrediction(false)
			CL_PredictMove
			CL_SetUpPlayerPrediction(true)
			CL_EmitEntities
			/* Rendition */
			SCR_UpdateScreen
		}
		oldtime = newtime;
	}
}

Como vemos, se obtiene el tiempo actual del sistema, y se obtiene la diferencia con el momento de actualización anterior. En función de eso se actualiza el estado del juego. Como digo, es la forma habitual de hacerlo y habitualmente funciona a la perfección, pero en algunos casos introduce una problemática adicional. ¿Que ocurre si ese lapso de tiempo se agranda?

Actualización temporal en videojuegos (Imagen de Fabien Sanglard)

Si el tiempo de actualización se hace más grande, significa que ocurrirán más cosas en el juego que nosotros tardaremos en saber. Si este efecto se lleva al extremo, podemos llegar a perder cosas importantes como la detección de colisiones. Un ejemplo:

Progresión de movimiento en una colisión (Imagen Fabien Sanglard)

En este ejemplo, si hacemos una actualización en el instante 0, y el tiempo de actualización se demora 12 o más milisegundos no habríamos detectado la colisión de los objetos A y B, por lo que el usuario habría visto que ambos objetos se atraviesan.

El problema en realidad no es que se alargue o no el tiempo de actualización, sino su variabilidad. Existen algoritmos que solucionan el problema, uno sencillo podría ser utilizar lapsos de tiempo constantes para actualizar.

    int  gameOn = 1
    int simulationTime = 0;

    while (1){

        int realTime = Gettime();

        while (simulationTime < realTime){
            simulationTime += 16; //Timeslice is ALWAYS 16ms.
            UpdateWorld(16);
        }

        RenderWorld();
    }

Actualización en periodos constantes (Imagen Fabien Sanglard)

Como vemos, de este modo separamos lo que es la actualización del renderizado. Pudiendo detectar situaciones que de otro modo se nos escaparían. Este paradigma es el que se utiliza en la industria del videojuego actual.

Así que como dice el señor Sanglard:

No website is as good as a good book. And no good book is as good as a disassembly output.

Por lo que os enlazo un blog y un libro sobre el tema:

Salud!