SmallPT en .Net

Buenas, para quien tenga interés en el tema, he convertido el programita SmallPT de Path tracing a .Net.

He convertido también algunas de sus versiones, aunque no he conseguido que me funcione Parallel.For 😦 Se me queda colgado sin hacer nada….

En fin, ha sido un ejercicio interesante, y me ha servido para leer un poco más sobre Path Tracing.

 

Aquí tenéis el código: http://www.mallorcaaldia.com/FTP/SmallPT.rar

Anuncios
Publicado en Uncategorized | Deja un comentario

Creando un mini engine para Android con Xamarin. Parte 1: Vídeo, Cámara, OnPause, OnResume

Bueno, como comenté, estoy intentando realizar un mini-engine cross platform que me permita poder manejar la cámara y el vídeo con facilidad. MonoGame es una buena entrada, sin embargo, el tema del vídeo y la cámara dejan mucho que desear…

Así que mis requerimientos son:

Que pueda utilizar la cámara y el video de una forma sencilla

Que pueda dibujar cosas encima de la cámara y el vídeo en OpenGL.

Tampoco es tanto!!!! Jajaja, pero bueno, se las trae!!!

Como es para Android, he decidido dirigirme a la versión 2.2, puesto que tiene lo que necesito y todavía hay un 33% de dispositivos que la tienen. Es una pena, porqué la 4.0 me permite trabajar con el vídeo y la cámara de una forma más sencilla y potente. Pero ..un 33% es mucho!! http://developer.android.com/about/dashboards/index.html

Empecé con la cámara, y ya empezaron los problemas. Para resumirlo, os diré que cuando se rota el movil de landscape a Portrait y vicecersa…¡la aplicación se reinicia! Increíble, pero cierto. Se destruye la ‘Activity’ y se crea otra vez…. En fin, la solución ha sido que esté siempre en Portrait y ya me encargare yo de gestionarlo( aunque esto todavía no lo he analizado bien)

El siguiente problema era el vídeo y la cámara. Tengo que activar rápidamente una cosa u otra y tengo que poder dibujar encima con OpenGL. Esto me ha llevado muuchos problemas:

No puedo tener un View de la cámara y otro del vídeo al mismo tiempo aunque sólo utilice uno de ellos. Se ve que el video no se reproduce , puesto que necesita disponer de una surface para él solito sin compartir.

Por lo tanto, lo que hago es crear un View del tipo que necesito y después eliminarlo. De esta forma todo me funciona sin problemas.

El segundo problema era el de dibujar con OGL. Por suerte…¡se puede hacer! Sólo hay que añadir un View, decirle que es transparente y a trabajar con OGL 2.0 Eso sí, hay que programarse los shaders y la gestión de texturas!!! Pero como yo sólo pinto rectangulos texturizados, no es mucho problema…

Y ahora viene el gran cacao…¿qué pasa cuando se Pausa y se vuelve a la aplicación? Bueno, pues HAY que restaurar de nuevo TODOS los shaders y recargar las texturas!!!!

No es un proceso complicado, sin embargo, todavía había una pega más. Cuando tenía la cámara activada y pulsaba el botón del móvil para volver al inicio, la aplicación entra en Pause. Al Regresar, la aplicación recreaba los shaders y todo perfecto, se pintaba encima los rectángulos texturizados.

Sin embargo, esto NO funcionaba con el vídeo!!! ¿porqué?!!!! La lógica estaba bien, y si funciona con uno debería ir con el otro. En fin, al final lo he resuelto quitando el View de OGL en Pause y volviéndolo a añadir en Resume.

Espero que os sirva si tenéis que hacer cosas con Android!!!!

Publicado en MonoDroid | Etiquetado , , , , | Deja un comentario

PlayVideo con MonoDroid indicando un archivo

Bueno, sé que parece una tontería, pero NO he conseguido usando SetVideoPath, reproducir un video que guardo en el proyecto. Si buscamos por internet parece que tiene que ser posible, metiéndolo en la carpeta de assets y referenciando ‘file:///android_asset/TransData/video.mp4’, sin embargo, yo no lo he conseguido.

Así que busqué por internet y el ‘truco’ está en crear un MediaPlayer y decirle que lo reproduzca en el VideoView. Genial!! Parece sencillo, pero…¡que va!!!!!

Al hacer mediaplayer.SetDisplay(videoView.Holder), me saltaba la excepción de : Surface has been released

Qué locura!! Si yo sólo quiero reproducir un archivo de vídeo. En fin, al final, lo conseguí. Lo que ocurre es que al hacer SetDisplay, el sistema tiene que asegurarse de que la Surface de VideoView está creada. Esa es la gran pega.

IMPORTANTE: La carpeta TransData NO tiene que estar dentro de Assets. Tiene que estar al mismo nivel que Assets, Resources, Properties…. Y dentro de TransData tenemos que insertar el video, e indicarle en las propiedades que es un AndroidAsset y Copiar si es más reciente.

IMPORTANT: The Folder TransData DOES NOT HAVE TO BE LOCATED INTO THE ASSETS FOLDER. It must be at the same level as Assets, Resources, Propertires… And You must add the video file INTO de TransData folder, add set the properties to AndroidAsset and Copy if newer

Así que aquí os dejo el código:

</pre>
using System;
using Android.App;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
using Android.Media;

namespace PlayVideo
{
 [Activity (Label = "PlayVideo", MainLauncher = true)]
 public class Activity1 : Activity,MediaPlayer.IOnPreparedListener,ISurfaceHolderCallback
 {
 VideoView videoView;
 protected override void OnCreate (Bundle bundle)
 {
 base.OnCreate (bundle);

// Set our view from the "main" layout resource
 SetContentView (Resource.Layout.Main);

videoView = FindViewById<VideoView> (Resource.Id.SampleVideoView);

play ("TransData/video.mp4");
 }
 MediaPlayer player;
 void play(string fullPath)
 {
 ISurfaceHolder holder = videoView.Holder;
 holder.SetType (SurfaceType.PushBuffers);
 // Necesito saber cuando la superficie esta creada para poder asignar el Display al MediaPlayer
 holder.AddCallback (this);
 player = new MediaPlayer();
Android.Content.Res.AssetFileDescriptor afd = this.Assets.OpenFd(fullPath);
 if (afd != null)
 {
 player.SetDataSource(afd.FileDescriptor, afd.StartOffset, afd.Length);
 player.Prepare ();
 player.Start();
 }
 }

public void SurfaceCreated (ISurfaceHolder holder)
 {
 Console.WriteLine ("SurfaceCreated");
 player.SetDisplay(holder);
 }

public void SurfaceDestroyed (ISurfaceHolder holder)
 {
 Console.WriteLine ("SurfaceDestroyed");
 }

public void SurfaceChanged (ISurfaceHolder holder, Android.Graphics.Format format, int w, int h)
 {
 Console.WriteLine ("SurfaceChanged");
 }
 }

}
<pre>
Publicado en MonoDroid | Etiquetado , , , | Deja un comentario

Modificando la posición de un SoundEffect en XNA y Windows Phone 7

Hace poco tuve que hacer una aplicación para Windows Phone 7. Como conozco bastante XNA, supuse que no sería problema, pero algo que yo creía que en el siglo XXI estaba superado, la gestión del sonido, se convirtió en un verdadero problema para mi. Resulta que no encontré forma para poder modificar la posición en la que se escucha un sonido ( modificar el PlayPosition)
Yo necesitaba tener un audio bastante largo, y cambiar la posición para poder rebobinar y avanzar. Lo cierto es que me frustré muchísimo.
Por suerte, viendo código de otra gente y otros ejemplos ( OggSharp, para reproducir ogg por Stream en XNA), pude averiguar una forma en la que crear este sistema. Y esta es la clase que he creado y que aquí os presento: SoundEffectEvolved

La utilización es muy sencilla, para crear un sonido simplemente tenemos que llamar al constructor, indicando un stream ( NO se puede usar el Content).

// Creamos el sonido
sound = new SoundEffectEvolved(TitleContainer.OpenStream("test.wav"),false);
// Reproducimos desde el principio
sound.Play(0);

Y para modificar la posición…

sound.Position = 3;	// Nos ponemos en el segundo 3

¡Facilísimo!

Aquí teneis el ejemplo: Source code

Y aquí os copio el código de la clase:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Media;

namespace SoundEffectEvolved
{
    /// <summary>
    /// Reproduce un wav partiendo de un stream, pero pudiendo modificar la posición de reproducción en todo momento
    /// </summary>
    public class SoundEffectEvolved
    {
		/// <summary>
		/// Objeto XNA para reproducir sonido dinámico
		/// </summary>
        private DynamicSoundEffectInstance effect;

		/// <summary>
		/// Valor que indica la posición en la que tenemos que copiar el siguiente buffer de sonido
		/// </summary>
		int offset;

		/// <summary>
		/// Usado internamente para saber cuando hemos llegado al final del sample
		/// </summary>
		bool endOfSample;

		/// <summary>
		/// Coge/Establece el modo repeat
		/// </summary>
		public bool Repeat { get; set; }

		/// <summary>
		/// Sample rate
		/// </summary>
		int sampleRate;

		/// <summary>
		/// Nº de canales
		/// </summary>
		int channels;

		public float Volume
		{
			get { return effect.Volume; }
			set { effect.Volume = value; }
		}

		public float Pan
		{
			get { return effect.Pan; }
			set { effect.Pan = value; }
		}

		public float Pitch
		{
			get { return effect.Pitch; }
			set { effect.Pitch = value; }
		}

		/// <summary>
		/// Estado de la reproducción
		/// </summary>
		public MediaState MediaState { get; private set; }

		/// <summary>
		/// Datos del Wav en memoria
		/// </summary>
		byte[] byteArray;

		/// <summary>
		/// Coge/Establece la posición en segundos
		/// </summary>
		public float Position
		{
			get
			{
				// Falta pasar la posición a segundos
				return IntToSeconds(offset);
			}
			set
			{
				if (MediaState == MediaState.Stopped)
				{
					Play(value);
				}
				else
				{
					// Tengo que pasar los segundos a bytes

					offset = SecondsToInt(value);

				}
			}
		}

		/// <summary>
		/// Devuelve el tamaño del sample en segundos
		/// </summary>
		public float SampleLength { get { return IntToSeconds(sampleLength); } }

		/// <summary>
		/// Tamaño del buffer que subiré cada vez que el buffer de sonido esté lleno
		/// </summary>
		int bufferSize;

		/// <summary>
		/// Tamaño del sample en bytes
		/// </summary>
		int sampleLength;

		/// <summary>
		/// Convertimos segundos al offset necesario
		/// </summary>
		/// <param name="second"></param>
		/// <returns></returns>
		int SecondsToInt(float second)
		{
			float posInSeconds = second;
			float v = sampleRate * channels;

			v *= 2;	// Debo multiplicar por 2 porqué el audio siempre es de 16 bits

			int offset = (int)(v * posInSeconds);

			// No puede ser impar si es de 16 bits
			if ((offset & 1) != 0) offset++;

			return offset;
		}

		/// <summary>
		/// Convertimos de entero a Segundos
		/// </summary>
		/// <param name="pos"></param>
		/// <returns></returns>
		float IntToSeconds(int pos)
		{
			pos /= 2;	// Divido por dos porqué el audio siempre es de 16 bits

			return (float)pos / (sampleRate * channels);

		}

		/// <summary>
		/// Metodo que rellena el buffer de sonido
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void BufferNeeded(object sender, EventArgs e)
		{
			if (endOfSample)
			{
				MediaState = MediaState.Stopped;
				return;
			}

			// Podemos 'subir' tantos buffers como queramos, pero claro, la subida bloquea el sistema
			// El valor de bufferCount es importante, porqué determina el tamaño de memoria que vamos a mover cada vez
			// que se termine el buffer de sonido. Tenemos que poner un valor pequeño, para poder modificar el offset dinámicamente, pero 
			// un valor muy pequeño puede provocar 'ruidos' en el sonido, porqué no bastará el tamañó del buffer para reproducir el sonido limpiamente

			for (int i = 0; i < 2; i++)
			{
				// Como subimos 2 buffers, necesitamos dividr el tamaño del buffer size entre 2

				int bufferCount = bufferSize/2;

				bool reset = false;

				if (offset + bufferCount > sampleLength)
				{
					bufferCount = sampleLength - offset;
					reset = true;
				}

				effect.SubmitBuffer(byteArray, offset, bufferCount);

				offset += bufferCount;

				// Hemos superado el límite en este bloque
				if (reset)
				{
					offset = 0;

					// Si no tengo que repetir, marco como que se terminará el sample cuando se produzca otra vez el evento
					// BufferNeeded, y allí se indicará que se ha parado el sample
					if (!Repeat)
					{
						endOfSample = true;
						break;
					}
				}
			}
		}

		/// <summary>
		/// Lee un archivo Wav
		/// </summary>
		/// <param name="reader"></param>
		void ReadWav(BinaryReader reader)
		{
			int chunkID = reader.ReadInt32();
			int fileSize = reader.ReadInt32();
			int riffType = reader.ReadInt32();
			int fmtID = reader.ReadInt32();
			int fmtSize = reader.ReadInt32();
			int fmtCode = reader.ReadInt16();
			int channels = reader.ReadInt16();
			int sampleRate = reader.ReadInt32();
			int fmtAvgBPS = reader.ReadInt32();
			int fmtBlockAlign = reader.ReadInt16();
			int bitDepth = reader.ReadInt16();

			if (fmtSize == 18)
			{
				// Read any extra values
				int fmtExtraSize = reader.ReadInt16();
				reader.ReadBytes(fmtExtraSize);
			}

			// Read until whe find the 'data' chunck
			for (; ; )
			{
				byte[] bytes = reader.ReadBytes(4);

				if (bytes[0] == (byte)'d' && bytes[1] == (byte)'a' && bytes[2] == (byte)'t' && bytes[3] == (byte)'a') break;
			}

			int dataSize = reader.ReadInt32();

			sampleLength = dataSize;
			this.sampleRate = sampleRate;
			this.channels = channels;

			byteArray = reader.ReadBytes(dataSize);
		}

		/// <summary>
		/// Constructor por defecto
		/// </summary>
		/// <param name="stream">Stream</param>
		/// <param name="loop">Indica si queremos que reproduzca el sonido en Loop</param>
		public SoundEffectEvolved(Stream stream,bool loop)
        {

			using(BinaryReader reader=new BinaryReader(stream))
			{
				ReadWav(reader);
			}

			Repeat = loop;

			MediaState = MediaState.Stopped;
		}

		/// <summary>
		/// Creo los objetos necesarios para reproducir un DynamicSoundEffectInstance
		/// </summary>
		void CreateDynamicSoundEffect()
		{
			// DynamicSound siempre utiliza audio de 16 bits

			effect = new DynamicSoundEffectInstance(sampleRate, (channels > 1 ? AudioChannels.Stereo : AudioChannels.Mono));

			// Tamaño del buffer para que se actualice 10 veces por segundo
			bufferSize = effect.GetSampleSizeInBytes(TimeSpan.FromMilliseconds(100));

			effect.BufferNeeded += BufferNeeded;

		}

		/// <summary>
		/// Reproduce el sonido, especificando el segundo en el que queremos empezar
		/// </summary>
		/// <param name="startTimeInSeconds">Segundo en el que queremos empezar</param>
        public void Play(float startTimeInSeconds)
        {
            if (MediaState == MediaState.Playing)
            {
                return;
            }

            MediaState = MediaState.Playing;

			offset = SecondsToInt(startTimeInSeconds);

			endOfSample = false;

			if (effect != null)
			{
				effect.Stop();

				// Debido al bug de XNA, que no permite parar y reproducir el mismo sonido al momento, necesitamos crear otro effect

				effect.BufferNeeded -= BufferNeeded;

				effect.Dispose();
			}

			CreateDynamicSoundEffect();

            effect.Play();
        }

        public void Pause()
        {
            if (MediaState == MediaState.Paused)
            {
                return;
            }

            MediaState = MediaState.Paused;

            effect.Pause();
        }

        public void Resume()
        {
            if (MediaState != MediaState.Paused)
            {
                return;
            }

            MediaState = MediaState.Playing;

            effect.Resume();
        }

        public void Stop()
        {
            if (MediaState == MediaState.Stopped)
            {
                return;
            }

            MediaState = MediaState.Stopped;

            effect.Stop();
        }


    }
}
Publicado en XNA | Deja un comentario

Como utilizar el timer correctamente en los juegos al pausarlos

Buff, vaya título. Pero bueno, los que trabajais en esto sabéis que es importante gestionar correctamente el modo pausa. Yo me he encontrado con este problemilla hace poco, y aquí os explico la forma que he tenido de resolverlo.

Vamos a plantear un jueguecito pequeño, y veamos el problema:
Tengo un jugador, que a cogido super-energía. Pero la energía dura sólo 3 segundos, justo después se vuelve a transformar en un pobre y esmirriao jugador.
¿Cómo programamos esto? Bueno, ahí va.

if then
tiempoParaVolverASerYoMismo=currentTime+3000;
soySuperHombre =true;
end

El update sería algo así como

if soySuperHombre AND currentTime>=tiempoParaVolverASerYoMismo then
soySuperHombre =false;
[Activar Animacion de convertires de nuevo al estado normal]
end

Bien, esto va perfecto! Pero ahora, vamos a pensar lo que ocurriría si nos hicieran Pause justo después de que me convirtiera a Super-Hombre…
¡El timer sigue avanzando! Entonces al regresar de la pausa, no esperará esos 3 segunditos que yo quería que esperara!!!

Así que he escrito un pequeño programa en Corona, para que podáis probarlo y ver cómo se haría correctamente. Pensad que en el mundo del móvil, la pausa puede ocurrir porqué te llaman, así que HAY que gestionarla!!!

En el ejemplo hay 2 rectángulos que se mueven de izquierda a derecha. Ambos se desplazan de forma que tarden 10s en llegar a la derecha de la pantalla. Ambos tienen programados que a los 5s cambien de color. Si no hacemos nada, veremos que cambian a los 5s y a los 10 llegan al final. Perfecto.
Pero si pausamos antes de que lleguen a los 5s, veremos que uno de ellos cambiará de color aunque estemos en pausa. El otro habrá tardado exactamente 5s DE JUEGO EFECTIVO, en cambiar de color, que es la forma correcta.

local pauseButton

--Informa en pantalla del tiempo
local timerText
local timerTextBad

--sprites utilizados
local rectangleRight
local rectangleBad

local paused=false

--Métodos de timer
local rightTimeMethod={ current=0,delta=0,last=0 }
local badTimeMethod={ current=0,delta=0,last=0 }

--Tiempo en el que quiero que me cambie el color
local changeColorTimeForRight

--Tiempo en el que quiero que me cambie el color
local changeColorTimeForBad

--Método que te devuelve el incremento que hay que realizar en cada frame, dando el nº de segundos total y el espacio a recorrer
function AtXSecond(numsec,t)

	return (rightTimeMethod.delta/1000)*t/numsec

end

function AtXSecondBad(numsec,t)

	return (badTimeMethod.delta/1000)*t/numsec

end

function UpdateTime(event)

	--Método erróneo de uso de un sistema TimeBased

	badTimeMethod.current=event.time

	badTimeMethod.delta = event.time - badTimeMethod.last

	badTimeMethod.last = event.time

	--Si estoy en modo pause, no corre el tiempo
	if paused then

		--Indicamos que no hay tiempo transcurrido
		badTimeMethod.delta=0

	end

	--Método correcto!!

	rightTimeMethod.delta = event.time - rightTimeMethod.last

	rightTimeMethod.last = event.time

	--Si estoy en modo pause, no corre el tiempo
	if paused then

		--Indicamos que no hay tiempo transcurrido
		rightTimeMethod.delta=0

		--Aquí current no está cambiando y por tanto, al volver de pausa, tendrá el valor correcto
	else
		--Actualizamos el tiempo actual
		rightTimeMethod.current=rightTimeMethod.current+rightTimeMethod.delta
	end
end

--Método principal del Update
function Update(event)

	UpdateTime(event)

	--Hago que el rectángulo se mueva hacia la derecha y que me recorra todo el ancho en 10s
	rectangleRight.x=rectangleRight.x+AtXSecond(10,display.contentWidth)

	--Hago que el rectángulo se mueva hacia la derecha y que me recorra todo el ancho en 10s
	rectangleBad.x=rectangleBad.x+AtXSecondBad(10,display.contentWidth)

	timerText.text="Time ok:"..rightTimeMethod.current
	timerTextBad.text="Time Bad:"..badTimeMethod.current

	--Aquí le indico que cuando llegue a los 5 segundos, me cambie el color
	--La diferencia principal está aquí. Con el método erróneo, current se va actualizando y por tanto genera este enorme problema
	--Imagina que pausas el juego y tenías indicado que a los 10s el jugador cambiaba de forma. Si no lo controlas de esta forma te
	--encontrarás con este problema
	if rightTimeMethod.current>=changeColorTimeForRight then rectangleRight:setFillColor(255,255,0) end
	if badTimeMethod.current>=changeColorTimeForBad then rectangleBad:setFillColor(255,255,0) end
end

--Touch event al pulsar pause
function pausedEvent(event)

	--Queremos controlar el momento en el que suelta el botón
	if event.phase~="ended" then return end

	paused=not paused

	if paused then pauseButton.text="Press to continue" else pauseButton.text="Press to Pause" end
end

function main()

	--Añado los listeners

	Runtime:addEventListener("enterFrame", Update)

	--Añado variables informativas
	timerText = display.newText( "timer", 100, 10, "Helvetica", 16 )
	timerText:setReferencePoint(display.TopLeftReferencePoint)
	timerText:setTextColor(255, 255, 255)

	timerTextBad = display.newText( "timer", 100, 30, "Helvetica", 16 )
	timerTextBad:setReferencePoint(display.TopLeftReferencePoint)
	timerTextBad:setTextColor(255, 255, 255)

	--El boton de pause
	pauseButton = display.newText( "Press to Pause", 100, 150, "Helvetica", 16 )
	pauseButton:setReferencePoint(display.TopLeftReferencePoint)
	pauseButton:setTextColor(0, 0, 255)

	pauseButton:addEventListener("touch",pausedEvent)

	rectangleRight=display.newRect(0, 60, 20, 10 )
	rectangleRight:setFillColor(255,0,0,255)

	rectangleBad=display.newRect(0, 110, 20, 10 )
	rectangleBad:setFillColor(0,255,0,255)

	--Cogemos el valor inicial del temporizador
	local currentTime=system.getTimer()

	rightTimeMethod.current = currentTime

	rightTimeMethod.lastTime=currentTime

	badTimeMethod.current = currentTime

	badTimeMethod.lastTime=currentTime

	--Ahora le digo que quiero que me cambie el color a los 5 segundos

	changeColorTimeForRight=currentTime+5000
	changeColorTimeForBad=currentTime+5000
end

--Este es el punto de ejecución, por tanto, llamamos a main()
main()

Publicado en Corona | Deja un comentario

Ejemplo sencillo de física con Corona

Descargar código fuente

Un simple ejemplo para ver como usar las colisiones de física con corona. Una parte interesante, o al menos para mi, es la forma de resolver las colisiones sin utilizar clases. Al principio usaba el nombre e iba comprobando el nombre del objeto para saber si colisionaba con un tipo u otro. Pero se me ocurrió el onCollide:


module(..., package.seeall)

NOMBRE="star1"

--Se produce la colisión del lanzador con este objeto
function onCollide(o,event)

--Añado variables informativas
local myText = display.newText( "Collide star1", o.x, o.y, "Helvetica", 16 )
myText:setTextColor(0, 255, 255)

end

function create(x,y)

local o= display.newImage( "star1.png" )

o.name=NOMBRE

o.x=x
o.y=y

local wallCollisionFilter = { categoryBits = 255, maskBits = 255 }
local wallPhysicsProp = { density=1, filter=wallCollisionFilter,radius=9 }

physics.addBody( o,"static", wallPhysicsProp)

o.onCollide=onCollide

return o

end

Y el handler de la colisión se encarga de llamar al onCollide del sprite que corresponde.

function onCollision(event)

--Mostramos en el cuadro de texto los objetos de colisión
myText.text=event.object1.name.." / "..event.object2.name.."/"..event.phase

local phaseIsBegan=(event.phase == "began")
local phaseIsEnded=(event.phase == "ended")

local o=nil

--Quiero comprobar si colisiono con el jugador. Por tanto, debo averiguar si uno de los dos objetos de colisión es el jugador
if event.object2.name=="player" then

o=event.object1

elseif event.object1.name=="player" then

o= event.object2

end

if (o~=nil) then

--¿Tiene definido este método?
if o.onCollide~=nil then

o.onCollide(o,event)

return
end
else
--Esto significa que colisionan dos objetos, pero que uno de ellos no es el player
return
end

end

Es una forma sencilla, (semi) orientada a objetos, que no precisa de utilización de clases.

Publicado en Corona, Videojuegos | Deja un comentario

Sobre Undos y Redos

Me he propuesto hacer un editor que soporte Undo/Redo. Nunca lo he hecho hasta la fecha y para mi era un reto importante. Voy a contaros la forma que he tenido de resolverlo.

Primero empecé con el Undo. Sólo necesito una lista y una Action que ejecutar. Si por ejemplo quiero añadir un Tile al mapa, haría:

Action undo = new Action(() => { map.RemoveTile(tile); map.Invalidate(); });

De esta forma, al hacer el Undo, sólo tenía que ejecutar la Action y todo perfecto.

Pero entonces se me planteó el problema del Redo…¿Cómo puedo hacer el Redo? Bien, pues decidí que al añadir un comando a la lista de Undos, también le diría el Redo:

Action undo = new Action(() => { map.RemoveTile(tile); map.Invalidate(); });
Action redo = new Action(() => { map.AddTile(tile); });
undoredoList.Add(new UndoRedo(undo,redo));

Por ahora todo bien, vamos a ver cómo implementamos el Undo y el Redo:

public static void Undo()
{
// Cojo el Undo 
Accion accion = undoredoList[currentUndoIndex];

// Ejecuto su Action
accion.Undo();

// En la lista de Redo, guardo el índice de este Undo
redoList.Add(currentUndoIndex);

// Me coloco en el siguiente Undo
currentUndoIndex++;

}
public static void Redo()
{
// Cojo el Undo 
Accion accion = undoredoList[redoList[currentRedoIndex]];

// Ejecuto su Action
accion.Redo();

// Ahora debería actualizar la lista de Redo, eliminando el que ya he hecho, y aumentar la lista de Undo, para eliminar el Redo que acabo de hacer

}

Lo de la lista de Redo, Undo, aumentar la lista no me gustaba, así que pensé otra forma de hacerlo: En lugar de tener una lista con índices, mantener dos lista, una de undo y la otra de redo:

public static void AddTile(Map map, Tile tile)
{
    Action undo = new Action(() => { map.RemoveTile(tile); map.Invalidate(); });
    Action redo = new Action(() => { AddTile(map, tile); });

    AddCommand(new Accion("Add Tile", undo, redo));

    map.AddTile(tile);

    map.Invalidate();
}

public static void Undo()
{
   accion.Undo();

   // Quito esta acción de la lista de undos
   undoList.Remove(accion);

   // Añado el Redo a la lista
   redoList.Insert(0, accion);

    // Inserto el texto del Redo en la lista de strings que le aparece al usuario
    MainForm.CurrentInstance.iRedoList.Strings.Insert(0, accion.Text);
}

public static void Redo()
{
    if (redoList.Count == 0) return;

    Accion accion = redoList[0];

    accion.Redo();

    redoList.Remove(accion);
}

El Redo además no necesita informar al Undo, y aquí está la clave. No lo necesita hacer porqué en AddTile, el Action del Redo llama al mismo método y por tanto ya se encargará el propio método de volver a meterlo en la lista del undo

Grupos

El otro problema que me planteé fue que cuando por ejemplo, insertará 10 tiles de golpe con el ratón ( cuando pulsas el ratón con un tile y lo desplazas hasta que sueltas el botón), no quería que me creara 10 elementos en la lista de Undos que ve el usuario. De alguna forma tengo que poder ‘agrupar’ un conjunto de Undos y que aparezcan como uno solo.

Esto lo he resuelto con un IDGrupo. Si tengo que crear un nuevo grupo, lo que hago es darle un valor diferente de 0 y todos los Undos que se vayan insertando tendrán el mismo IDGrupo. Entonces al terminar, sólo tengo que insertar una linea en la lista que ve el usuario.

// Aquí vemos el ejemplo de cómo usar estos métodos
private void MapControl_MouseDown(object sender, MouseEventArgs e)
{
    if (Mapa.ClickMode == ClickModes.Add)
    {
        // Empezamos el grupo de Undos, para que todos los AddTiles que hayan aparezcan como una sola línea en el Undo
        CommandHub.StartUndoGroup();

        AddTile(e.X, e.Y);
    }

    if (Mapa.ClickMode == ClickModes.Selection)
    {
        StartSelection(e.Location);
    }
}

private void MapControl_MouseUp(object sender, MouseEventArgs e)
{
    if (Mapa.ClickMode == ClickModes.Selection)
    {
        EndSelection();

        Invalidate();
    }

    if (Mapa.ClickMode == ClickModes.Add)
    {
        // Fin del grupo de Undo
        CommandHub.EndUndoGroup();
    }
    

}

/// <summary>
/// Empieza un grupo de Undos. Un grupo es cuando por ejemplo pulso el botón izquierdo en el mapa y añado 100 tiles. En lugar de crear 100 strings, creo
/// uno que significan los 100.
/// </summary>
/// <param name="text"></param>
public static void StartUndoGroup()
{
    useUndoGroup = true;

    countStartUndoGroup = undoList.Count;

    // Necesito identificar cada grupo con un mismo ID
    IDGrupo++;
}

/// <summary>
/// Termina un grupo de Undos. Añade el string en la lista junto al nº de Undos que ha generado
/// </summary>
/// <param name="text"></param>
public static void EndUndoGroup()
{
    useUndoGroup = false;

    // Añado sólo un elemento a lista de Undos que ve el usuario
    MainForm.CurrentInstance.iUndoList.Strings.Insert(0, undoList[0].Text + " (" + (undoList.Count - countStartUndoGroup)+")");

    IDGrupo = 0;
}

Ahora sólo hace falta en el Undo y el Redo gestionar los grupos


    /// <summary>
    /// Clase que gestiona el Undo y el Redo. Importante que el Redo llame al método que vuelve a añadir a la lista de undos
    /// </summary>
    public class Accion
    {
        public Action Undo;
        public Action Redo;
        public string Text;
        public int IDGrupo;

        public Accion(string text,Action undo, Action redo)
        {
            Text = text;
            Undo = undo;
            Redo = redo;
            IDGrupo = CommandHub.IDGrupo;
        }
    }
/// <summary>
/// Realiza un undo
/// </summary>
public static void Undo()
{
    if (undoList.Count ==0) return;

    Accion accion = undoList[0];

    // Compruebo si es un grupo

    if (accion.IDGrupo != 0)
    {
        int idgrupo=accion.IDGrupo;
        int counter=0;

        while (undoList.Count>0 && undoList[0].IDGrupo == idgrupo)
        {
            accion = undoList[0];

            accion.Undo();

            // Quito esta acción de la lista de undos
            undoList.Remove(accion);

            // Añado el Redo a la lista
            redoList.Insert(0, accion);

            counter++;
        }

        // Inserto el texto del Redo en la lista de strings, pero como un grupo
        MainForm.CurrentInstance.iRedoList.Strings.Insert(0, accion.Text + " (" + counter+")");

    }
    else
    {
        accion.Undo();

        // Quito esta acción de la lista de undos
        undoList.Remove(accion);

        // Añado el Redo a la lista
        redoList.Insert(0, accion);

        // Inserto el texto del Redo en la lista de strings
        MainForm.CurrentInstance.iRedoList.Strings.Insert(0, accion.Text);

    }

    // Quito el texto del Undo de la lista de strings
    MainForm.CurrentInstance.iUndoList.Strings.RemoveAt(0);

}

/// <summary>
/// Realiza un redo. Así como creo la Action del Redo, el redo lo que hace es volverme a crear el Undo
/// </summary>
public static void Redo()
{
    if (redoList.Count == 0) return;

    Accion accion = redoList[0];

    // Compruebo si es un grupo

    if (accion.IDGrupo != 0)
    {
        int idgrupo = accion.IDGrupo;
        int counter = 0;

        while (redoList.Count > 0 && redoList[0].IDGrupo == idgrupo)
        {
            accion = redoList[0];

            accion.Redo();

            // Quito esta acción de la lista de redos
            redoList.Remove(accion);

            counter++;
        }

    }
    else
    {
        accion.Redo();

        redoList.Remove(accion);
    }
}

Publicado en Otros | Deja un comentario