08 septiembre 2009

Laptops gratis (no es broma!)

Me ha vuelto a llegar este mensaje tres veces en los últimos 4 días. Parece mentira lo ingenuos que podemos llegar a ser. Reproduzco aquí el texto del mensaje y luego añadiré el par de comentarios obligados...




ERICSSON DE DISTRIBUCIÓN GRATUITA

Cordial saludo.

La sociedad Ericsson distribuye gratuitamente computadores portátiles con la intención de contrarrestar
a Nokia que ha hecho lo mismo como estrategia de mercadeo.
Ericsson tiene como objetivo fundamental aumentar su popularidad, por este motivo
Ericsson distribuye gratuitamente el nuevo computador portátil WAP.

Todo lo que hay que hacer es enviar este email a ocho de tus conocidos
y en aproximadamente tres semanas, recibirás un portátil Ericsson T18.

Si el mensaje es enviado a veinte personas o más puedes
correr con la suerte de recibir un portátil Ericsson R320.
Es importante tener claro
que no se trata de una broma. Compruébelo, desee gusto, regálese un computador portátil.
Buena suerte. Lic. Angela Gil Telf. 0412.380.70.28 / 0212.272.99.08
Por favor, avisame cuando recibas tu laptop!



De este tipo de mensajes me sorprenden dos cosas:
  1. ¿Alguien todavía se cree que una compañía como Sony/Ericsson, Nokia, Toshiba, Dell (o cualquier otra) va a regalar un portátil "por que sí"? Si regalaran alguno, sería al menos con el propósito de hacerse propaganda, de modo que no regalarían miles de ellos de forma anónima.
  2. Parece que la gente no entiende todavía eso de que el correo electrónico es privado. Ni Ericsson ni nadie puede saber si envías copia del mensaje a una persona, a 8, o a 3000, de modo que no podrían "premiarte" por hacerlo.
No seamos tan ingenuos...

Y por cierto... si reenviais un correo como este "por si acaso", al menos incluid a los destinatarios como "copia oculta" (CCO o BCC), no en "Para:" ni en "CC:" para preservar la privacidad de vuestros contactos.

07 septiembre 2009

Solo se gana lo que se da


¿Por qué pierdo tiempo en crear cursos para gente que no conozco?

Porque me parece absurdo que el conocimiento se pierda. Prefiero que los materiales que preparo para mis alumnos (presenciales) o en mis (pocos) ratos de hobby pueda ser útil a alguien más.

Dicho por Antonio Machado, y aunque la SGAE parezca opinar lo contrario:

En cuestiones de cultura y de saber, sólo se pierde lo que se guarda, sólo se gana lo que se da.
(Descubierto en http://nocturnosaltea.blogspot.com/2008/11/la-espuma-de-la-vida.html )

06 septiembre 2009

Incrustar imágenes en OpenOffice

Estaba creando versiones PDF de alguno de mis cursos, de una forma que debería ser sencilla: actualizo la web, copio y pego sobre OpenOffice.org (en concreto sobre Writer) y genero el PDF.

El problema es que me gusta conservar una copia también en formato "editable", en el propio formato ODT de OpenOffice... pero cuando copias y pegas una página Web sobre OpenOffice writer, las imágenes no quedan incluidas, sino vinculadas, de modo que siguen estando en la web, no en el documento, con los problemas de lentitud cada vez que paseas por el documento, y el riesgo de que otra persona intente usar ese documento y se encuentre que no hay imágenes.

Pero la solución es fácil: después de copiar y pegar la página web o el fragmento que interese, con sus imágenes y todo, hay que entrar al menú "Editar", y a la opción "Vínculos". Entonces seleccionamos todas las imágenes que nos interese incluir como parte del documento (en mi caso, todas las que contiene) y pulsamos el botón "Interrumpir".

Con eso dejan de ser vínculos y pasan a ser imágenes incrustadas en el documento, que ya podremos editar sin estar conectados a la web, o ceder a otras personas.

Es una posibilidad muy útil y que cuesta encontrar (incluso Googleando), así que aquí la dejo...

08 enero 2009

Remake (parcial) de Fruity Frank... 25 - Añadiendo efectos sonoros

Vamos a añadir sonidos al juego, para que tenga "algo más de vida". Para conseguirlo, crearemos una nueva clase "Sonido", que se apoyará en "Sdl_mixer", ocultando los detalles de esta librería.

Por ejemplo, el constructor se encargará de cargar un fichero de música:
    /// Constructor a partir de un nombre de fichero
public Sonido(string nombreFichero)
{
punteroInterno = SdlMixer.Mix_LoadMUS(nombreFichero);
}

Y tendremos funciones para reproducir un sonido una vez (por ejemplo, cuando muere un enemigo, o nuestro personaje, o cuando recogemos una fruta), así como para reproducir un sonido de forma continua (para la música de fondo):
    /// Reproducir una vez
public void Reproducir1()
{
SdlMixer.Mix_PlayMusic(punteroInterno, 1);
}

/// Reproducir continuo (musica de fondo)
public void ReproducirFondo()
{
SdlMixer.Mix_PlayMusic(punteroInterno, -1);
}

También necesitamos otra función que permita dejar de reproducir sonidos:
    /// Interrumpir toda la reproducción de sonido
public void Interrumpir()
{
SdlMixer.Mix_HaltMusic();
}


Así, cada nivel tendrá una música de fondo. Esta música se declara en la clase "Nivel" genérica:
    public class Nivel
{
protected Sonido miMusicaFondo;
...

En la clase "Nivel" prepararemos también las funciones para reproducir su música de fondo o para detenerla, ambas basadas en las posibilidades de la clase "Sonido":
    public void ReproducirMusica()
{
if (miMusicaFondo != null)
miMusicaFondo.ReproducirFondo();
}

public void PararMusica()
{
if (miMusicaFondo != null)
miMusicaFondo.Interrumpir();
}

Y esa música se cargará en el constructor de cada "clase hija" (porque cada nivel concreto tendrá su propia música de fondo):
    public class Nivel1: Nivel
{

const byte NUMENEMIGOS = 3;

public Nivel1()
{
byte i;

miMapa = new Mapa1();
miMusicaFondo = new Sonido("sonidos\\fruity-nivel1.mp3");
enemigos = new Enemigo[NUMENEMIGOS];
...

En el juego, tendremos una música adicional para el cambio de nivel (todavía no habrá sonido al recoger frutas ni al morir personajes), y entonces, cuando se cambio de nivel, deberemos parar la música del nivel anterior, reproducir el sonido de cambio de nivel, y a continuación comenzar a reproducir la música de fondo del nuevo nivel, así:
    public  void SiguienteNivel()
{
miNivel.PararMusica();
musicaNuevoNivel.Reproducir1();
...
if (numeroNivel % 3 == 1)
miNivel = new Nivel1();
...
miNivel.ReproducirMusica();



Sólo falta crear la carpeta sonidos, y guardar en ella los sonidos que nos interesan, que habremos capturado previamente del juego original, o bien habremos creado nosotros mismos. También deberemos modificar los ficheros "BAT" encargados de compilar todos los fuentes, para que incluyan la nueva clase "Sonido".

Como siempre, todo el fuente del proyecto está en: code.google.com/p/fruityfrank

01 enero 2009

Remake (parcial) de Fruity Frank... 24 - Enemigos más inteligentes

Hasta ahora nuestros enemigos se movían simplemente de lado a lado. Ahora vamos a intentar que el movimiento sea un poco más "inteligente", y más parecido al del Fruity Frank original.

Antes teníamos un único método "Mover", común a todos los elementos de la clase "Enemigo". Ahora debemos hacer que no todos los enemigos se comporten igual:

  • En primer lugar, no todos aparecerán en el instante inicial: cada uno tendrá un retardo algo mayor que los anteriores, para que aparezcan uno por uno.

  • Para seguir, la inteligencia de los movimientos que hará cada uno dependerá del tipo de enemigo del que se trate. Para más detalles:


    • Los "señores nariz" se moverán casi siempre por los huecos de la pantalla, y pocas veces romperán paredes.

    • Los "señores pepino" serán mucho más impacientes, y romperán paredes con más facilidad.


  • Ambos tipos de enemigos se mueven básicamente al azar, tendiendo a proseguir su marcha hasta que choquen con algo, momento en el que cambiarán de sentido. Más adelante habrá un tercer enemigo, la "fresa", cuyo movimiento será básicamente para perseguir al personaje.

  • Además, cada tipo enemigo aparece en un punto distinto de la pantalla:


    • Los "nariz" salen desde una zona central de la pantalla, que llamaremos su "nido".

    • Los "pepino" caen desde la parte superior.



Vayamos viendo cómo hacer todo esto...

Para que los enemigos puedan saber si se pueden mover a una cierta casilla hará falta que se puedan comunicar con el nivel que los contiene, así que en el constructor les indicaremos cual es ese nivel:
    public Nariz(Nivel n) {
miNivel = n;
...

Además, la función "EsPosibleMover" que ya existía estaba pensada para el personaje, que podía atravesar paredes y comer frutas, pero los enemigos no pueden comer frutas, y se mueven preferentemente por espacios huecos. Por eso, nos interesará una nueva función, que podría llamarse "MovilidadEnemigo", y que tendría 3 valores posibles:

  • Valdría 0 para indicar cuando no puede entrar a esa posición, porque haya una fruta, una manzana u otro enemigo.

  • Sería 1 cuando puede entrar con dificultad, rompiendo paredes, algo que los Nariz pocas veces harán, pero los Pepino sí podrán hacer con más frecuencia.

  • Será 2 cuando sea espacio hueco, por el que se pueden desplazar sin problema.

  • También habría que comprobar si en esa casilla ya hay otro enemigo (o si ya está entrando en ella), porque no se deberán solapar dos en la misma posición de pantalla, pero esto lo dejamos para un poco más adelante.


    /// Indica si es el enemigo puede moverse a cierta posicion de la pantalla
/// (2 = sí, hueco; 1 = a veces, pared; 0 = nunca, fruta).
public byte MovilidadEnemigo(short x, short y)
{
short xMapa = (short) ((x-xIniPantalla)/anchoCasilla);
short yMapa = (short) ((y-yIniPantalla)/altoCasilla);

if ((x < xIniPantalla) || (xMapa >= MAXCOLS) || // Si se sale, no puede
(y < yIniPantalla) || (yMapa >= MAXFILAS)) return 0;

char simbolo = GetPosicion(xMapa, yMapa);

if ((simbolo == ' ') // Si es hueco, sí puede
|| (simbolo == 'N')) // También si es el nido
return 2;

if ((simbolo == 'X') // Si es pared, puede con dificultad
|| (simbolo == 'Y') || (simbolo == 'Z') )
return 1;

// En el resto de casos, sería una fruta y no puede
return 0;
}

Así, la lógica del movimiento de los enemigos "Nariz" podría ser así:

  • Si se estaba moviendo, continuará en la misma dirección hasta que tope con un obstáculo.

  • Cuando encuentre un obstáculo, elegirá una nueva direccción al azar. Empezará a a moverse en esa dirección si no hay obstáculos; si hay una pared, se empezará a mover con una probabilidad baja (por ejemplo, un 25%). Si hay una obstáculo, o bien si es una pared pero el número al azar ha indicado que no debe atravesarla, se quedará parado, y en el siguiente fotograma de juego se volverá a elegir una dirección al azar y a repetir el proceso.


    public override void Mover()
{
// Si no está activo, espero el tiempo estipulado para que aparezca
if (!activo)
{
contadorHastaRetardo ++;
if (contadorHastaRetardo >= retardo)
activo = true;
return;
}

// Si está parado, busco nueva dirección
if (parado) {
calcularNuevaDireccion();
return;
}

// Si se puede mover en horizontal o vertical, avanza
if ((!parado) && (incrX > 0)) {
if (miNivel.MovilidadEnemigo(
(short) (x+miNivel.GetAnchoCasilla()), y) == 2)
x += incrX;
else
parado = true;
}
...

Y la rutina de mover el enemigo "Pepino", que sí puede atravesar paredes, de momento podría ser muy similar, con la diferencia de que podrá entrar en casillas de "Movilidad 1" (paredes) y que deberá borrar la casilla a la que entra:
    ...
// Si se puede mover en horizontal o vertical, avanza
if ((!parado) && (incrX > 0)) {
if (miNivel.MovilidadEnemigo(
(short) (x+miNivel.GetAnchoCasilla()), y) >= 1)
{
x += incrX;
miNivel.BorrarPosicionPantalla(x,y);
}
else
parado = true;
}
...

En cuanto a la posición inicial de cada enemigo, la definimos desde el constructor de cada nivel. Por ejemplo, para el Nivel 1, podría ser:
    public  Nivel1()
{
byte i;

miMapa = new Mapa1();
enemigos = new Enemigo[NUMENEMIGOS];

enemigos[0] = new Nariz(this);
enemigos[0].MoverA(miMapa.posXnido, miMapa.posYnido);
enemigos[0].SetRetardo(25); // 1 segundo despues del comienzo

enemigos[1] = new Nariz(this);
enemigos[1].MoverA(miMapa.posXnido, miMapa.posYnido);
enemigos[1].SetRetardo(75); // 3 segundos despues del comienzo

enemigos[2] = new Pepino(this);
enemigos[2].MoverA(
(short) (miMapa.GetXIni() + 5*miMapa.GetAnchoCasilla()),
miMapa.GetYIni());
enemigos[2].SetRetardo(150); // 6 segundos despues del comienzo
enemigos[2].SetVelocidad(0,4);
}


Como siempre, todo el fuente del proyecto está en: code.google.com/p/fruityfrank