Objetivos

◾ Crear un proyecto de postal navideña para concurso

◾ Ejemplo GL_Program, con audio y gráficos

◾ Diferentes archivos involucrados

📀 Descarga los ejemplos y recursos utilizados aquí

Scripts

💾 Archivo: main.pi

class Main implements GL_Program
{
	function Start()
	{
		AddScriptPackage("./data/scripts/");

		SetBackColor(RGB(0,0,0));
		SetBgColor(RGB(0,0,0));
		SetResolution(256, 256);
		SetViewScale(2);
		SetVSync(true);
		Set2DFilter(false);
		
		_music = NewMusic("ost");
		_music.SetLoop(true);
		_music.Load("data/audio/music.ogg");
		
		NewObject("background", "background");
		NewObject("title", "title");
		NewObject("santa", "santa");
		_snow = NewObject("snow", "snow");
		_snow.factor = 3;
		_snow.SetAlpha(100);
		_snow = NewObject("snow", "snow");
		NewObject("text", "text");
		
		_music.Play();		
		_music.SetVolume(200);
		::Start();
	}
}

Punto de inicio de nuestra demo navideña. En este GL_Program creamos los objetos que vamos a usar y de los que a continuación repasaremos sus scripts. Todos estos scripts, los de los objetos GL, los hemos ubicado en «./data/scripts».

💾 Archivo: data/scripts/background.pi

class Background implements GL_Sprite
{
	properties:
		u = 0.0f;
		uSpeed = 0.0f;
		
	virtual _operator_new(_name, _creator)
	{
		SetFlags(FLAG_UVWRAP);
		AddFrame("data/graphics/bg1.jpg", GetGL().GetResX(), GetGL().GetResY());
						
		
	}
	
	virtual Move()
	{
		SetUV(u, 0, 1, 1);		
		u += GetFTime() * uSpeed;
	}
}

El background lo único que hace es poner este gráfico en pantalla:

Y simular un scroll infinito bien a derecha o bien a izquierda. Para eso usamos el truco de las coordenadas de textura y el WRAPPING. Por defecto las texturas usan un wrap de tipo CLAMP o sea, las coordenadas de textura siempre quedan entre 0 y 1.

Si activamos el UVWRAP podemos usar valores mayores que 1 o menores que 0. Esto nos permite hacer repeticiones de la textura cuando se pinta o simular desplazamientos en la misma, cosa que usamos precisamente en el background. Movemos el U para simular el movimiento del scroll. Mira que cosa mas curiosa:

Por lo tanto, solo con ir moviendo la coordenada «U», simulamos el efecto de scroll. Para ello usamos en cada iteración del bucle de sistema, este código:

SetUV(u, 0, 1, 1);		
u += GetFTime() * uSpeed;

La función GL_Sprite::SetUV(x, y, w, h) establece las coordenadas de textura en todos los frames asociados al sprite.

La siguiente línea de código incrementa la propiedad «u» con el valor de «uSpeed». La velocidad queremos que sea distinta ya que nuestro Santa va a andar, correr, etc. y necesitamos crear diferentes efectos. El GetFTime() es una función que devuelve un factor relativo al framerate en marcha. Lo cierto es que no es necesario usarlo porque usamos sincronización con el retrazo vertical del monitor (VSync), sin embargo, es interesante enseñar su uso en el caso de que el VSync no estuviese activo. Usar GetFTime permite dejar el código preparado para que el código funcione igual en cualquier máquina con cualquier velocidad y framerate.

💾 Archivo: data/scripts/title.pi

class Title implements GL_Sprite
{
	properties:
		angle = 0;
		
	virtual _operator_new(_name, _creator)
	{
		AddFrame("data/graphics/title.png", GetGL().GetResX()*0.7, GetGL().GetResY()*0.5);
		SetPos((GetGL().GetResX()-GetWF())/2, 10);
	}
	
	virtual Move()
	{
		IncPos(0, sin(angle)*0.5f);
		angle += 0.1f * GetFTime();
	}
}

El título no es más que este gráfico centrado en pantalla:

Levitando levemente, de ahí que usemos la función «sin» y del mismo modo que antes, aunque como he dicho no es necesario, usemos GetFTime para el incremento de la propiedad «angle» que contendrá el ángulo en radianes.

💾 Archivo: data/scripts/snow.pi

class Snow implements GL_Sprite
{
	properties:
		u = 0;
		angle = 0;
		factor = 2;
		speed = 0.01f;
		
	virtual _operator_new(_name, _creator)
	{
		SetFlags(FLAG_UVWRAP);
		AddFrame("data/graphics/copos.png", GetGL().GetResX(), GetGL().GetResY());
	}
	
	virtual Move()
	{
		_bg = GetCreator().FindObject("background");
		u += (_bg.uSpeed + speed) * GetFTime();
		SetUV(u+sin(angle)*speed, -u, factor, factor);
		angle += GetFTime() * 0.1f;
	}
}

El efecto de la nieve cayendo usa también el UVWRAP como hemos hecho con el background. En este caso vamos a usar dos planos de nieve para crear cierta profundidad. Para ello no solo usaremos las coordenadas (u, v) para desplazar la textura, también vamos a cambiar el ancho y alto para repetir el dibujado de la textura y crear un mosaico, observa este ejemplo:

El caso x1 sería utilizando un ancho y alto de (1, 1). Dibujaría la textura tal cual, con el mismo aspecto y tamaño originales. Sin embargo, si usamos un ancho y alto de (2, 2), lo que haremos es dibujar 2 x 2 veces la misma textura ocupando el mismo área de representación. Este truco es el que usaremos para los copos de nuestra demo. La idea es simular más sensación de copos y que los que van por delante, se vean más grandes que los de atrás. Además, para reforzar este efecto de lejanía, los copos de atrás tendrán un valor de transparencia mayor. Observa las líneas de código en main.pi que hacen referencia a esto:

_snow.factor = 3;
_snow.SetAlpha(100);

💾 Archivo: data/scripts/text.pi

class Text implements GL_Text
{
	properties:
		str = "";
		min = 0;
		x = 0;
		
	virtual _operator_new(_name, _create)
	{
		SetFontName("data/graphics/font.ttf");
		SetFontSize(16);
		x = GetGL().GetResX();
		SetPos(x, 235);
		SetWidth(GetGL().GetResX()*2);
		
		str = "";
		if (GetEngine().GetLocaleLanguage() == "es")
		{
			str += "¡¡FELIZ NAVIDAD 2021!! ";
			str += "Demo presentada para el concurso organizado de postales programadas para MSXVR!! ";
			str += "En este caso, hecha con VR-SCRIPT usando GL. ";
			str += "¡Anímate a participar! que de paso te vendrá bien para aprender ";
			str += "a programar en tu súper máquina! ¡Viva el MSX! y el ¡MSXVR! ";
			str += "Créditos.... Programación: ALhONE's - Música: Francisco Ramirez [SoundCloud] - 03-jinge-bells-8bit-version - Santa's Sprite Animations: GameArt2D.com";
			str += "  ¡¡Gracias!!   ";
		}
		else
		{
			str += "MERRY CHRISTMAS 2021!! ";
			str += "Demo presented for the organized contest of postcards programmed for MSXVR !! ";
			str += "In this case, made with VR-SCRIPT using GL. ";
			str += "Go ahead and participate! By the way, it will be good for you to learn ";
			str += "to program on your super machine! Long live the MSX! and the MSXVR! ";
			str += "Credits .... Programming: ALhONE's - Music: Francisco Ramirez [SoundCloud] - 03-jinge-bells-8bit-version - Santa's Sprite Animations: GameArt2D.com";
			str += "  Thank you !!  ";		
		}
	}
	
	virtual Start()
	{
		SetAlpha(200);
	}
	
	virtual Move()
	{
		SetText(String_SubStr(str, min, 30));
		x -= GetFTime() * 1.0f;
		SetX(x);
		
		_s = GetTextSize(ascii(str[min]));
		
		if (int(x) == -_s[0])
		{
			x = 0;
			min++;
			if (min >= strlen(str))
			{
				x = GetGL().GetResX();
				min = 0;
			}
		}
	}
}

El texto con scroll infinito lo vamos a simular con un GL_Text. Además lo haremos extrayendo solo la parte de la cadena de texto que se va a visualizar. En nuestro caso, consideraremos 30 caracteres como el mínimo necesario para ocupar toda la pantalla en horizontal. La idea es ir pintando el texto poco a poco hacia la izquierda (coordenada X) y mirar si el carácter inicial se oculta por ese lado. Para saber el tamaño del carácter usamos la función GL_Text::GetTextSize(_text).

Esta función devuelve una lista con dos valores. El primer componente es el tamaño en horizontal y el segundo en vertical. Pues bien, en el caso que ese carácter desaparezca por la izquierda, será el momento de eliminarlo de nuestro string y movernos una posición. Esto es lo que hacemos con la propiedad min. Cuando esta propiedad supere el tamaño de la cadena de texto, volveremos a comenzar.

💾 Archivo: data/scripts/santa.pi

class Santa implements GL_Sprite
{
	properties:
		timer = 0;
		wait = 0;
		jumpY = 0;
		accY = 0;
		iter = 0;
		sndJojojo = 0;
		sndJump = 0;
		sndSlide = 0;
		bg = null;
		
	virtual _operator_new(_name, _creator)
	{
		_ani = AddAnimationWithPrefix("walk", "data/graphics/Walk (%d).png", 1, 13, 100, 64, -1, 0.7f);
		_ani = AddAnimationWithPrefix("run", "data/graphics/Run (%d).png", 1, 11, 100, 64, -1, 1.0f);
		_ani = AddAnimationWithPrefix("slide", "data/graphics/Slide (%d).png", 1, 11, 100, 64, -1, 1.0f);
		_ani = AddAnimationWithPrefix("jump", "data/graphics/Jump (%d).png", 1, 16, 100, 64, -1, 1.0f);
		_ani = AddAnimationWithPrefix("idle", "data/graphics/Idle (%d).png", 1, 16, 100, 64, -1, 1.0f);
		SetAnimation(_ani);
		SetPos(10, 164);
		
		sndJojojo = _creator.NewObject("snd0", "gl_sound");
		sndJojojo.Load("data/audio/jojojo.ogg");
		sndJump = _creator.NewObject("snd1", "gl_sound");
		sndJump.Load("data/audio/jumpend.ogg");
		sndSlide = _creator.NewObject("snd2", "gl_sound");
		sndSlide.Load("data/audio/slide.ogg");
	}
	
	virtual Start()
	{
		bg = GetCreator().FindObject("background");
		Idle();
	}
	
	function Idle()
	{
		timer = GetTime();
		wait = Rand(3000, 4000);
		bg.uSpeed = 0;
		if (Rand(0, 100) > 50)
			sndJojojo.Play();
		SetAnimation("idle");
		_change("idle");
	}
	
	state "idle"
	{
		if ((GetTime() - timer) >= wait)
		{
			if (Rand(0, 100) > 50)
				Walk();
			else
				Run();
		}
	}
	
	function Walk()
	{
		timer = GetTime();
		wait = Rand(4000, 8000);
		bg.uSpeed = GetHFlip() ? -0.001f : 0.001f;
		SetAnimation("walk");
		_change("walk");
	}
	
	state "walk"
	{
		if ((GetTime() - timer) >= wait)
		{
			Idle();
		}
	}
	
	function Run()
	{
		timer = GetTime();
		wait = Rand(6000, 8000);
		bg.uSpeed = GetHFlip() ? -0.003f : 0.003f;
		SetAnimation("run");
		_change("run");
	}
	
	state "run"
	{
		if ((GetTime() - timer) >= wait)
		{
			if (Rand(0, 100) > 80)
				Walk();
			else
			{
				iter++;
				if ((iter & 1) == 0)
					Jump();
				else
					Slide();
			}
		}
	}	
		
	function Slide()
	{
		bg.uSpeed = 0.000f;
		SetAnimation("slide");
		sndSlide.Play();
		_change("slide");
	}
	
	state "slide"
	{
		_top = GetHFlip() ? 10 : GetGL().GetResX()-100;
		IncPosTo(1, 0, _top);
		if (GetX() == _top)
		{
			SetHFlip(!GetHFlip());
			Idle();
		}
	}			

	function Jump()
	{
		bg.uSpeed = GetHFlip() ? -0.004f : 0.004f;
		SetAnimation("jump");
		accY = 0.0f;
		jumpY = -5.0f;
		_change("jump");
	}
	
	state "jump"
	{
		IncPos(0, jumpY);
		jumpY += accY * GetFTime();
		accY += 0.01f * GetFTime();
		if (GetY() >= 164)
		{
			sndJump.Play();
			SetY(164);
			Idle();
		}		
	}
}

Nuestro santa es un sprite que tiene 5 acciones: Caminar, Correr, Deslizarse, Saltar y permanecer quieto. Las distintas acciones cambian aleatoriamente de unas a otras creando un comportamiento caótico de nuestro personaje.

El personaje además cambiará de dirección una vez finaliza la secuencia de deslizarse por el suelo, de manera que con esto le damos más variedad con el cambio de sentido del scroll.

En este ejemplo usamos estados, que nos permiten tener lógica asociada a los mismos y visualmente nos permite identificar esta lógica con una estética agradable. Para cada estado se ha creado un método (función). Cada función inicializa al personaje, variables y el estado en curso.

Además de esto, se usan distintos sonidos que se irán lanzando a medida que sean requeridos. Uno es un «jojojo» que se aplica de forma aleatoria cada vez que entramos en un estado de Idle (quieto). Luego hay otro que se aplica cuando activamos el estado de deslizarse y otro que se aplica al finalizar un salto.

Observad como modificamos la velocidad de scroll sobre el objeto background según andamos, corremos, etc. usando el acceso al objeto «background» y su propiedad «uSpeed».

Mediante el Horizontal Flipping (HFlip) y la velocidad negativa, creamos el efecto de ir hacia la derecha o la izquierda.


Conclusión

Una vez más espero que hayas aprendido más cosas y que este nuevo ejemplo sirva para reforzar conceptos. Pronto serán las primeras Navidades del MSXVR y ya que han propuesto un concurso, que menos que participar, aunque sea de modo simbólico. Por cierto, la entrada en el foro a la información del concurso está aquí.

¡Saludos!

es_ESSpanish