What are we gonna see?

◾ Proyecto GL_Program

◾ Ejemplo de videojuego (Monkey Demo)

◾ Diferentes archivos de script involucrados

📀 Descarga los ejemplos y recursos utilizados

Scripts Involucrados

Archivo¿Qué hace?
monkey.piScript de inicio, contiene GL_Program
scripts/hud.piGestiona el interfaz de usuario y el ratón
scripts/hud_text_button.piBotón interactivo con gestión de click de ratón
scripts/logo.piLogotipo animado
scripts/player.piPersonaje que controla el jugador con el ratón
scripts/stage.piEscenario donde el jugador se mueve e interactúa
scripts/task.piPlantilla/Interfaz de uso genérico
scripts/title.piPantalla donde aparece el título y se comienza a jugar

Secuencia tras la ejecución

💾 Archivo: monkey.pi

class Monkey implements GL_Program
{
	properties:
		hud = null;
		music = null;
		stage = null;
		
	function Start()
	{
		AddScriptPackage("./scripts/");
		
		SetBackColor(RGB(0,0,0));
		SetBgColor(RGB(0,0,0));
		SetResolution(320, 200);
		SetViewScale(-1);
		SetVSync(true);
		Set2DFilter(true);
		
		_music = NewMusic("ost");
		_music.SetLoop(true);
		music = _music;		
		
		hud = new HUD(this);
		
		_logo = new Logo(this);
		//_title = new Title(this);
		//stage = new Stage(this);
		
		::Start();
	}
}

Este archivo es el punto de entrada a nuestro programa. Hereda funcionalidad de «GL_Program» y comienza en la función «Start». Vamos a ir viendo paso a paso que hace:

AddScriptPackage("./scripts/");

Con esta función indicamos donde vamos a encontrar nuestros scripts. En el caso de un new, se buscará el nombre indicado, junto a la extensión .PI en la carpeta de scripts. Por ejemplo:

hud = new HUD(this);

Aquí crearemos una instancia de objeto usando el script «./scripts/hud.pi»

En las siguientes instrucciones:

SetBackColor(RGB(0,0,0)); 
SetBgColor(RGB(0,0,0)); 

Se indica el color de fondo completo (Back) y el de fondo de nuestra pantalla virtual (Bg-background). En esta imagen espero se entienda mejor este concepto:

El color azul correspondería al Back Color y el color rojo al «Bg». NOTA: Siendo 800×600 la resolución de pantalla establecida en el MSXVR.

La siguiente instrucción:

SetViewScale(-1);

Es la escala que establecemos para adaptar nuestra resolución de 320×200 a la de 800×600. Por defecto es 1. O sea, usaríamos 320×200, tal cual, centrada en la de 800×600. Si usamos valores mayores, escalaríamos este tamaño. El valor -1 permite adaptar lo mejor posible al máximo posible teniendo en cuenta el ancho. Mientras que si usamos -2, lo haríamos teniendo en cuenta el alto.

SetVSync(true);
Set2DFilter(true);

El SetVSync establece la sincronización con el retrazo vertical del monitor. O sea, que el cambio de página (del buffer de pintado) se hace cuando se recibe esta señal, de manera que evitamos que ese cambio se produzca en cualquier momento o situación del barrido del monitor.

El Set2DFilter indica si queremos usar el filtro 2D de suavizado de la imagen resultante. Si pusiésemos falso, el detalle de los pixeles sería más evidente, mas old-school.

_music = NewMusic("ost");
_music.SetLoop(true);
music = _music;	

Creamos un objeto «GL_Music» en la escena activa. La que crea un GL_Program por defecto. En nuestro caso usaremos dos canciones. Una que pondremos desde que sale el logotipo hasta la pantalla de título. Y luego otra que lanzaremos una vez entremos a jugar. Nos guardaremos en una propiedad «music» el controlador a la instancia de «GL_Music».

hud = new HUD(this);

La clase HUD representa el interfaz de usuario que vamos a usar en el juego. En este caso el puntero de ratón y la gestión sobre un tipo de objeto «Button» que comentaremos más adelante.

_logo = new Logo(this);
//_title = new Title(this);
//stage = new Stage(this);

La clase LOGO lanzará el logotipo de Lucas y que posteriormente lanzará el script de «Title». Comentar aquí que durante las pruebas, para poder ir probando los distintos módulos o partes, lanzaba directamente los scripts en pruebas, de ahí que haya dejado las líneas comentadas. Es un truco interesante, a la hora de plantear la secuencia de ejecución de código, el pensar en no tener que esperar un tiempo en cada ejecución que hagamos, de este modo podemos saltar a distintas partes de nuestra aplicación directamente.

::Start();

Y por último, la llama al «Start» del padre (GL_Program). Parte importante, ya que inicia los objetos creados y por tanto activa la escena en curso.

Nuestro siguiente script a comentar es «logo.pi». Es el que muestra el logotipo de Lucas y da paso a nuestra pantalla de título. Echemos un ojo…

💾 Archivo: scripts/logo.pi

class Logo implements Task
{
	function Init()
	{
		_spr = app.NewSprite("logo");
		_ani = _spr.AddAnimationWithPrefix("ani", "data/Logo%02d.png", 1, 36, 320, 200, -1, 0.25f);
		_spr.SetAnimation(_ani);
		_spr.SetPos(10, -10);
		_controller = _spr.CreateController();
		_controller.SetCustom(this, "OnController");
		
		_spr = app.NewSprite("fade");
		_spr.AddEmptyFrame(app.GetResX(), app.GetResY());
		_spr.SetColor(RGB(0,0,0));
		_spr.SetAlpha(0);
		
		app.music.Load("data/menu.ogg");
		app.music.Play();
		app.music.SetVolume(255);
	}
	
	function OnController(_controller)
	{
		_spr = _controller.GetTarget();
		if (_spr.GetAnimationLoops() >= 3)
		{
			_fade = app.GetActiveStage().FindObject("fade");
			_fade.IncAlpha(2);
			if (_fade.GetAlpha() == 255)
			{
				_title = new Title(app);
				delete this;
			}
		}
	}
}

Lo primero que vemos es que en este caso, nuestro script hereda de «Task». Este script «task.pi» lo podemos encontrar más abajo. Este interfaz, nos permite acceder a nuestro GL_Program a través de la propiedad «app» (propiedad que encontrarás en Task.pi)

En la función «Init» creamos todos los objetos GL que vamos a necesitar. Un sprite con la animación del logotipo , otro sprite que hará el truco de oscurecerse la pantalla (fade). También cargaremos la música «menu.ogg» y la reproduciremos.

En el controlador que asociamos al sprite «logo», lo que nos permite es poder interactuar en cada iteración, viendo si hemos reproducir 3 o más veces la animación y en tal caso, iremos haciendo aparecer (de transparente a opaco) usando el alpha, el sprite «fade», un sprite que es un rectángulo negro de tamaño de nuestra pantalla.

Una vez hemos hecho totalmente opaco el sprite «fade», o sea, el alpha es 255. Lanzamos nuestro script «Title» y borramos la instancia en la que estamos, la instancia del script «logo» actualmente en ejecución.

💾 Archivo: scripts/title.pi

class Title implements Task
{
	properties:
		but = null;
		fade = null;
		sndStart = null;
		
	function Init()
	{
		_bg0 = app.NewSprite("bg0");
		_bg0.AddFrame("data/bg0.png");
		_bg0.SetPriority(0);
	
		_bg1 = app.NewSprite("bg1");
		_bg1.AddFrame("data/bg1.png");
		_bg1.SetPriority(1);
		_controller = _bg1.CreateController();
		_controller.SetSpeedX(-0.25);
		_controller.SetCustom(this, "OnCloudsMove");

		_bg2 = app.NewSprite("bg2");
		_bg2.AddFrame("data/bg2.png");
		_bg2.SetPriority(2);
		
		_title = app.NewSprite("title");
		_title.AddFrame("data/title.png");
		_title.SetAlpha(0);
		_title.SetPriority(3);
		
		_timer = app.NewTimer("t1");
		_timer.SetFrequency(2000);
		_timer.SetUserCallback(this, "OnTitleMove_1");
		
		_fade = app.NewSprite("fade");
		_fade.AddEmptyFrame(app.GetResX(), app.GetResY());
		_fade.SetColor(RGB(0,0,0));
		_fade.SetPriority(4);
		_fx = _fade.CreateFX("fadeout");
		_fx.Start(2000);
		fade = _fade;
		
		_snd = app.NewSound("start");
		_snd.Load("data/start.ogg");
		sndStart = _snd;
		
		_but = app.hud.CreateTextButton(-1, 180, 0, 0, "¡Comienza la aventura!");
		_but.SetOnClick(this, "OnStartAdventure_1");
		_but.SetVisible(false);
		but = _but;
	}
	
	function OnStartAdventure_1()
	{
		_fx = fade.CreateFX("fadein");
		_fx.Start(2000);
		_fx.SetOnFinish(this, "OnStartAdventure_2");
		app.music.FadeOff(16);
		sndStart.Play();
		but.SetVisible(false);
	}
	
	function OnStartAdventure_2()
	{
		app.stage = new Stage(app);
		delete this;
	}
	
	function OnCloudsMove(_controller)
	{
		_controller.UpdatePos();
		_spr = _controller.GetTarget();
		if (_spr.GetX() < -_spr.GetWF())
			_spr.SetX(app.GetResX());
	}
	
	function OnTitleMove_1(_timer)
	{
		_timer.SetFrequency(20);
		_title = app.GetActiveStage().FindObject("title");
		_title.IncAlpha(30);
		if (_title.GetAlpha() == 255)
		{
			but.SetVisible(true);
			app.hud.cursor.SetVisible(true);
			_timer.SetUserCallback(this, "OnTitleMove_2");
		}
	}
	
	function OnTitleMove_2(_timer)
	{
		if (Rand(0, 100) > 99)
		{
			_title = app.GetActiveStage().FindObject("title");
			if (_title.GetFXCount() == 0)
			{
				_fx = _title.CreateFX("tint");
				_fx.Start(100, RGB(255,255,255));
			}
		}
	}
}

Nuestro script arranca por la función «Init» y en ella creamos los distintos objetos GL que vamos a necesitar:

  • El Background, que estará formado por 3 planos (fondo, nubes e isla)
  • El título de «Monkey Island»
  • Un botón «hud_text_button«, que se trata de un texto sobre el que podemos hacer click de ratón
  • Un sprite «fade» para simular el fundido de entrada y salida de esta pantalla
  • Un sonido para cuando se hace click sobre el botón
  • Un temporizador con el que vamos a gestionar en primera instancia, la transparencia del sprite del título. Y en segunda instancia, los brillos blancos aleatorios que se le aplican.

💾 Archivo: scripts/hud.pi

class HUD implements Task
{
	constants:
		EVENT_CLICK = 0;
		EVENT_MAX;
		
	properties:
		eventManager = null;
		cursor = null;
		stage = null;
		pad = null;
		controlList = [];
		bClick = 0;
		
	function Init()
	{
		eventManager = EventManager_New(EVENT_MAX);
	
		_stage = app.NewStage("hudstage");
		_stage.SetGlobal(true);
		_stage.SetPriority(1);
		//_stage.SetDebug(true);
		stage = _stage;
	
		_cursor = app.NewSprite("cursor", _stage);
		_cursor.AddFrame("data/pointer.png", 32, 32);
		_cursor.SetOffset(-15, -15);
		_cursor.SetPriority(1);
		cursor = _cursor;
		cursor.SetVisible(false);
		cursor.SetCollision(true);
		cursor.SetUserMove(this, "OnCursorMove");
		cursor.SetCollisionRect(16-2, 16-2, 4, 4);
		
		_pad = app.NewPad("pad", _stage);
		_pad.SetPreset("MOUSE");
		pad = _pad;
		
		_c = _cursor.CreateController();
		_c.SetPad(_pad);
	}
	
	function Final()
	{
		DestroyControls();
	}
	
	function CreateTextButton(_x, _y, _w, _h, _text)
	{
		_but = new HUD_Text_Button(app);
		_but.SetText(_text);
		if (_w || _h) _but.SetSize(_w, _h);
		_but.SetPos(_x, _y);
		controlList += [_but];
		return _but;
	}
	
	function DestroyControls()
	{
		for (i=0; i<sizeof(controlList); i++)
			delete controlList[i];
		controlList = [];
	}
	
	function OnCursorMove(_spr)
	{
		if (bClick == 0)
		{
			if (pad.IsButton(PAD_BUTTON_1))
			{
				bClick = 1;				
			}
		}
		else if (!pad.IsButton(PAD_BUTTON_1))
		{
			bClick = 0;
			EventManager_DoEvent(eventManager, EVENT_CLICK);
		}
	}
	
	function RegisterEvent(_eventID, _ctx, _event)
	{
		EventManager_AddEvent(eventManager, _eventID, _ctx, _event);
	}
}

La clase HUD en nuestro caso, va a gestionar todo el apartado de interfaz 2D y ratón. Las funciones nativas EventManager ofrecen un mecanismo para la gestión de eventos entre objetos. Nuestra clase HUD es muy simple y solo gestionamos los click.

El click se genera detectando una pulsación y posterior liberación del botón izquierdo del ratón (asociado al PAD_BUTTON_1 del control «pad»).

La imagen del cursor:

Tiene un tamaño de 32×32, el punto central está a 15 pixeles del (0, 0). Por ello usamos el SetOffset en el sprite. Para llevar el (0, 0) del cursor al (-15, -15) y así detectar los clicks justo en el centro del sprite.

💾 Archivo: scripts/hud_text_button.pi

class HUD_Text_Button implements Task
{
	properties:
		text = null;
		bClicked = 0;
		onClickCtx = null;
		onClickEvent = "";
		color = -1;
		
	function Init()
	{
		_text = app.NewText("hud_text_button", app.hud.stage);
		_text.SetFont("data/font.ttf", 8);
		_text.SetUserMove(this, "OnMove");
		_text.SetPriority(0);
		_text.SetCollision(true);
		_text.SetAlign(DT_CENTER|DT_VCENTER);
		app.hud.RegisterEvent(HUD.EVENT_CLICK, this, "OnClick");
		text = _text;
	}
	
	function SetText(_str)
	{
		text.SetText(_str);
		text.AdjustTextSize();
	}
	
	function SetColor(_c)
	{
		text.SetColor(_c);
		color = _c;
	}
	
	function SetBGcolor(_c)
	{
		text.SetBGColor(_c);
	}
	
	function SetPos(_x, _y)
	{
		if (_x < 0) _x = (app.GetResX() - text.GetWidth())/2;
		text.SetPos(_x, _y);
	}

	function SetSize(_w, _h)
	{
		text.SetSize(_w, _h);
	}
	
	function SetVisible(_v)
	{
		text.SetVisible(_v);
	}
	
	function SetOnClick(_ctx, _event)
	{
		onClickCtx = _ctx;
		onClickEvent = _event;
	}
	
	function OnClick()
	{
		if (app.hud.cursor.CheckCollisionWithSprite(text))
		{
			if (onClickCtx)
				onClickCtx._call(onClickEvent);
		}
	}
	
	function OnMove(_text)
	{
		if (app.hud.cursor.CheckCollisionWithSprite(_text))
		{
			_text.SetColor(RGB(255,255,0));
		}
		else
		{
			_text.SetColor(color);
		}
	}
}

Este script gestiona el funcionamiento del botón de texto que se crea desde el HUD y que muestra un texto que al pasar el ratón por encima, muestra un color distinto (amarillo) al que hayamos indicado en su creación.

Los sprites tienen la función «CheckCollisionWithSprite» que nos permite detectar si dos sprites se tocan. Con esta función detectaremos si nuestro cursor toca el botón para determinar tanto el cambio de color como el click.

Los sprites tienen una posición y un tamaño. Del mismo modo, un objeto tipo «gl_text», he realmente hereda parte de su comportamiento de un «gl_sprite», también lo tiene. En el momento de establecer el texto que queremos contenga el botón , la función «hud_button_text::SetText», usamos el método «gl_text::AdjustTextSize». De este modo, se ajusta el tamaño del sprite al tamaño del texto en pantalla y usando la fuente de letra que se haya establecido.

💾 Archivo: scripts/stage.pi

class Stage implements Task
{
	properties:
		bgCol = null;
		
	function Init()
	{
		_bg0 = app.NewSprite("bg0");
		_bg0.AddFrame("data/29.png");
		_bg0.SetPriority(0);
		
		_player = app.NewObject("player", "player", null, [app]);
		_player.SetPriority(1);
		_player.SetPos(10, 60);
				
		_bg1 = app.NewSprite("bg1");
		_bg1.AddFrame("data/29_fg.png");
		_bg1.SetPriority(10);
		
		_bgcol = app.NewSprite("bgCol");
		_bgcol.AddFrame("data/29_collision.png");
		_bgcol.SetVisible(true);
		_bgcol.SetAlpha(50);
		_bgcol.SetPriority(11);
		bgCol = _bgCol;
		
		_actions = [
			["Dar", "Abrir", "Mirar"], 
			["Empujar", "Cerrar", "Hablar a"], 
			["Usar", "Coger", "Tirar"]
		];
		
		for (i=0; i<3; i++)
		{
			for (j=0; j<3; j++)
			{
				_but = app.hud.CreateTextButton(5+i*66, 150+j*20, 60, 10, _actions[i][j]);
				_but.SetColor(RGB(255,0,255));
				_but.SetBGColor(RGB(55,0,55));
			}
		}
	
		app.music.Load("data/track1.ogg");
		app.music.Play();
		app.music.SetVolume(255);
	}
	
	function SetCameraX(_x)
	{
		app.GetActiveStage().SetCameraX(_x);
	}
	
	function GetCameraX()
	{
		return app.GetActiveStage().GetCameraX();
	}
	
	function GetWidth()
	{
		return bgCol.GetWidth();
	}
}

El «stage» visualmente son 2 planos (sprites): un background (bg0), por detrás de los actores. Un foreground (bg1), que irá por delante de los actores. Entre estos actores está nuestro «player». Nótese como se controla la prioridad del pintado de sprites con la función SetPriority. La prioridad más baja se pinta antes.

El sprite «bgCol» lo usaremos para almacenar la información de por donde ha de moverse el jugador. Este sprite incluye un fotograma donde con el color blanco indicamos que pixeles son caminables, frente a los de color negro que indican que el personaje no puede pisar.

La detección de esta colisión o la obtención de esta información la hace el player a la hora de moverse por el escenario. La función que realiza esta consulta es «player::CheckCol» que realmente llama a la función de «gl_sprite::GetFramePixel».

💾 Archivo: scripts/player.pi

class Player implements GL_Sprite
{
	properties:
		app = null;
		toX = 0;
		toY = 0;
		
	virtual _operator_new(_name, _creator, _params)
	{
		AddAnimationWithSheet("quiet", "data/Guy_Walking_Right-Sheet.png", 0, 0, 0, 0, 50, 50, -1, -1, -1, 0.25);
		AddAnimationWithSheet("walk-right", "data/Guy_Walking_Right-Sheet.png", 0, 5, 0, 0, 50, 50, -1, -1, -1, 0.25);
		AddAnimationWithSheet("walk-up", "data/Guy_Walking_Up-Sheet.png", 0, 5, 0, 0, 50, 50, -1, -1, -1, 0.25);
		AddAnimationWithSheet("walk-down", "data/Guy_Walking_Down-Sheet.png", 0, 5, 0, 0, 50, 50, -1, -1, -1, 0.25);

		SetUserMove(this, "OnMove");

		app = _params[0];
		app.hud.RegisterEvent(HUD.EVENT_CLICK, this, "OnClick");
	}
	
	virtual Start()
	{
		SetAnimation("quiet");
		app.hud.cursor.SetVisible(true);
	}
	
	function OnMove(_spr)
	{
		_scale = clamp(_spr.GetY()/150, 0.25, 1.0f);
		_spr.SetScale(_scale);
		
		_x = GetX() - app.GetResX()/2;
		_x = clamp(_x, 0, app.stage.GetWidth() - app.GetResX());
		app.stage.SetCameraX(_x);
	}
	
	state "goto"
	{
		_lx = GetX();
		_ly = GetY();
		
		_dx = abs(_lx - toX);
		_dy = abs(_ly - toY);
		
		if (_dx <= 10)
		{
			SetAnimation("quiet");
			return;
		}
		
		if (toX < _lx) _sx = -1;
		else _sx = 1;
		if (toY < _ly) _sy = -1;
		else _sy = 1;
		
		if (!GetCol(_lx+_sx, _ly+_sy))
		{
		}
		else if (!GetCol(_lx+_sx, _ly))
		{
			_sy = 0;
		}
		else if (!GetCol(_lx, _ly+_sy))
		{
			_sx = 0;
		}
		else if (!GetCol(_lx-_sx, _ly))
		{
			_sx = -_sx;
			_sy = 0;
		}
		else if (!GetCol(_lx-_sx, _ly+_sy))
		{
			_sx = -_sx;
		}
		else if (!GetCol(_lx-_sx, _ly-_sy))
		{
			_sx = -_sx;
			_sy = -_sy;
		}
		else if (!GetCol(_lx, _ly-_sy))
		{
			_sx = 0;
			_sy = -_sy;
		}
		else
		{
			_sx = 0;
			_sy = 0;
		}
	
		if (_sx || _sy)
		{	
			IncPos(_sx, _sy);
			if (_sy < 0 && _dy >= 10) SetAnimation("walk-up");
			else if (_sy > 0 && _dy >= 10) SetAnimation("walk-down");
			else 
			{
				SetAnimation("walk-right");
				SetHFlip(_sx < 0 && _dx >= 10);
			}
		}
		else
		{
			SetAnimation("quiet");
		}
	}
	
	function GetCol(_x, _y)
	{
		_c = app.stage.bgCol.GetFramePixel(0, _x+GetWF()/2, _y+GetHF());
		_c = Color_GetR(_c);
		return (_c < 32);
	}
	
	function Goto(_x, _y)
	{
		toX = _x;
		toY = _y;
		_change("goto");
	}
	
	function OnClick()
	{
		_x = app.stage.GetCameraX();
		Goto(_x + app.hud.cursor.GetXF(), app.hud.cursor.GetYF());
	}
}

El jugador carga las animaciones que va a emplear durante el juego. Su comportamiento es simplemente el de ir hacia donde hace click con el ratón. Realmente esta demo no incluye nada de lógica con respecto a plantear jugabilidad. Las acciones (botones) que se definen no se controlan y el personaje no interacciona con ningún elemento NPC, etc. del juego. La demo se limita a enseñar el paso previo a la creación de un juego estilo point & click. Del mismo modo, la rutina que gestiona el movimiento por el escenario es muy preliminar y no funciona correctamente en muchos casos con lo que el personaje se queda enganchado o rebotando en según que posiciones del mapa.

💾 Archivo: scripts/task.pi

class Task 
{
	properties:
		app = null;
		cc = null;
	
	function _operator_new(_app)
	{
		app = _app;
		cc = app.StartCreationContext();
		Init();
		app.EndCreationContext();
	}
	
	function _operator_delete()
	{
		delete cc;
		Final();
	}
	
	function Init()
	{
	}		
			
	function Final()
	{
	}
}

Esta clase se usa como interfaz común en otros scripts. Permite interaccionar con nuestro GL_Program a través de la propiedad «app» y usa un contexto de creación para gestionar el borrado de los objetos creados por el «task» una vez este se borra.

cc = app.StartCreationContext();

Todos los objetos creados en un GL_Program a partir de este punto (NewSprite, NewSound, NewObject, etc.) se asocian al contexto de creación que nos ha sido devuelto por la función y que en el ejemplo anterior guardaremos en una propiedad de nombre «cc».

app.EndCreationContext();

Finaliza el ámbito del contexto de creación. Observad la aplicación de esto, en el destructor de la clase (_operator_delete), cuando hacemos el borrado de la instancia guardad en la propiedad «cc», que es donde albergamos nuestro contexto de creación.


Conclusión

Espero que hayas pasado un rato entretenido con este tutorial y aprendido cosas nuevas. Ciertamente este ejemplo proporciona una idea muy preliminar de como plantear un juego estilo Point & Click, sin embargo, ha permitido introducir ideas y conocimientos que esperamos puedan servir a un futuro desarrollador de esa posible aventura Point & Click que sin duda a muchos nos gustaría disfrutar en nuestros MSXVR. ¡Mucho animo!

¡Nos vemos en el siguiente tutorial! ¡Saludos!

en_GBEnglish