He estado repasando temas relacionados con los gráficos en la plataforma .NET y para probar algunas cosas se me ha ocurrido programar un juego al que jugaba cuando empecé a meterme en el mundo de la informática. El juego en cuestión es el de la serpiente (yo lo conocí como Nibbles). La idea es bastante sencilla: se trata de mover por la pantalla un elemento gráfico simulando una serpiente que debe pasar por encima de ciertos puntos donde se encuentra su alimento preferido. Cada vez que un alimento es engullido la serpiente aumenta su tamaño y aparece otro alimento en la pantalla. La serpiente no puede salirse de la pantalla ni chocar consigo misma.
Yo he utilizado C# para programarlo, aunque es muy parecido en Visual Basic o Visual C++. A continuación os muestro el código y también un par de imágenes de su ejecución.
using System;
using System.Timers;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
namespace nibblesCS01
{
public partial class fNibbles : Form
{
List<Punto> path;
private const int DESPLAZAMIENTO = 5;
private const int ANCHO_SERPIENTE = 6;
private const int ANCHO_INICIAL = 180;
private const int INCREMENTO_ANCHO = 20;
private const int ALIMENTOS_NECESARIOS = 12;
private int INTERVALO_TEMPORIZADOR = 100;
private List<Alimento> alimentos;
int alimentosComidos;
public fNibbles()
{
InitializeComponent();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
const int WM_KEYDOWN = 0x100;
const int WM_SYSKEYDOWN = 0x104;
Punto pto = new Punto();
if ((msg.Msg == WM_KEYDOWN) || (msg.Msg == WM_SYSKEYDOWN))
{
switch (keyData)
{
case Keys.Right:
if (path[path.Count - 1].d != 0)
{
pto.d = 0;
pto.x = path[path.Count - 1].x;
pto.y = path[path.Count - 1].y;
path[path.Count - 1].d = 0;
path.Add(pto);
}
break;
case Keys.Left:
if (path[path.Count - 1].d != 1)
{
pto.d = 1;
pto.x = path[path.Count - 1].x;
pto.y = path[path.Count - 1].y;
path[path.Count - 1].d = 1;
path.Add(pto);
}
break;
case Keys.Up:
if (path[path.Count - 1].d != 2)
{
pto.d = 2;
pto.x = path[path.Count - 1].x;
pto.y = path[path.Count - 1].y;
path[path.Count - 1].d = 2;
path.Add(pto);
}
break;
case Keys.Down:
if (path[path.Count - 1].d != 3)
{
pto.d = 3;
pto.x = path[path.Count - 1].x;
pto.y = path[path.Count - 1].y;
path[path.Count - 1].d = 3;
path.Add(pto);
}
break;
//Datos depuración
//case Keys.Enter:
// for (int i = 0; i < path.Count; i++)
// {
// lbl.Text += "(" + path[i].x + "," + path[i].y + "," + path[i].d +") ";
// }
// lbl.Text += " -- ";
// break;
//Opción pausar el juego
case Keys.P:
tTemp.Enabled = !tTemp.Enabled;
break;
}
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void fNibbles_Load(object sender, EventArgs e)
{
int xAlimento, yAlimento;
alimentosComidos = 0;
alimentos = new List<Alimento>();
//Inicializar la serpiente
path = new List<Punto>();
Punto pto = new Punto(0, 200, 0);
path.Add(pto);
pto = new Punto(ANCHO_INICIAL, 200, 0);
path.Add(pto);
Random r = new Random(DateTime.Now.Millisecond);
xAlimento = r.Next((this.Width - 5) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
yAlimento = r.Next((this.Height - 30) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
Alimento ptoAlimento = new Alimento(xAlimento, yAlimento, Color.Blue);
alimentos.Add(ptoAlimento);
lblAlimentosRestantes.Text = ALIMENTOS_NECESARIOS.ToString();
//Datos depuración
//lbl.Text = "[" + ptoAlimento.x + "," + ptoAlimento.y + "] ";
//Inicializar el temporizador
tTemp.Tick += new System.EventHandler(OnTimerEvent);
tTemp.Interval = INTERVALO_TEMPORIZADOR;
tTemp.Enabled = true;
}
//Calcula la longitud de la serpiente en función de los puntos que componen su silueta actual
private int longitudSerpiente()
{
int longitud;
longitud = 0;
for (int i = 1; i < path.Count; i++)
{
if (path[i - 1].x == path[i].x)
{
longitud += Math.Abs(path[i].y - path[i - 1].y);
}
else
{
longitud += Math.Abs(path[i].x - path[i-1].x);
}
}
return longitud;
}
//Muestra un punto en la pantalla por cada objeto de tipo alimento guardado en la lista
//de alimentos
public void MostrarAlimentos()
{
Graphics g = this.CreateGraphics();
foreach (Alimento al in alimentos)
{
g.FillEllipse(Brushes.Blue, al.x - 4, al.y - 4, 8, 8);
}
}
//Manejador del evento correspondiente al vencimiento del temporizador
//Desplaza la serpiente una posición y la redibuja
public void OnTimerEvent(object source, EventArgs e)
{
//Desplazar la serpiente una posición
if (DesplazarSerpiente())
{
//Limpiar la pantalla y redibujar la serpiente
MostrarSerpiente();
MostrarAlimentos();
}
}
//Dependiendo de la dirección del último punto añadido a la ruta, modificar el
//valor de dicho punto. El primer punto de la ruta debe chequearse para ver si
//coincide con el valor del segundo, y si es así, eliminar el primero de la ruta
private bool DesplazarSerpiente()
{
Punto pto0 = new Punto();
Punto pto1 = new Punto();
Punto ultimoPto = new Punto();
bool noDesplazarCola;
int xAlimento, yAlimento;
pto0 = path[0];
pto1 = path[1];
ultimoPto = path[path.Count - 1];
noDesplazarCola = false;
//Desplazamos la cabeza de la serpiente en la dirección que corresponda
switch (ultimoPto.d)
{
case 0: //dcha
ultimoPto.x += DESPLAZAMIENTO;
break;
case 1: //izda
ultimoPto.x -= DESPLAZAMIENTO;
break;
case 2: //arriba
ultimoPto.y -= DESPLAZAMIENTO;
break;
case 3: //abajo
ultimoPto.y += DESPLAZAMIENTO;
break;
}
//Comprobamos si la serpiente se sale de los márgenes o se muerde a sí misma
if (path[path.Count - 1].x > this.Width - 5 || path[path.Count - 1].y > this.Height - 55 || path[path.Count - 1].x < 0 || path[path.Count - 1].y < 0 ||
SerpienteSeMuerde())
{
tTemp.Enabled = false;
//this.Refresh();
MostrarMensaje("GAME OVER!!!", 240, 150, Brushes.DarkRed);
//MessageBox.Show("GAME OVER");
return false;
}
//Comprobamos si la serpiente se come un alimento
if (alimentos.Count > 0)
{
if (path[path.Count - 1].x == alimentos[0].x && path[path.Count - 1].y == alimentos[0].y)
{
//Come un alimento
alimentosComidos++;
//Actualizamos el contador de alimentos restantes
lblAlimentosRestantes.Text = (ALIMENTOS_NECESARIOS - alimentosComidos).ToString();
if (alimentosComidos == ALIMENTOS_NECESARIOS)
{
tTemp.Enabled = false;
MostrarMensaje("CONGRATULATIONS! YOU PASSED THIS LEVEL.", 100, 150, Brushes.DarkGreen);
return false;
}
else
{
alimentos.RemoveAt(0);
noDesplazarCola = true;
//Colocamos otro alimento en la pantalla
Random r = new Random(DateTime.Now.Millisecond);
xAlimento = r.Next((this.Width - 5) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
yAlimento = r.Next((this.Height - 55) / DESPLAZAMIENTO) * DESPLAZAMIENTO;
Alimento ptoAlimento = new Alimento(xAlimento, yAlimento, Color.Blue);
alimentos.Add(ptoAlimento);
//Datos depuración
//lbl.Text += "*" + path[0].x + "," + path[0].y + "* ";
//lbl.Text = "[" + ptoAlimento.x + "," + ptoAlimento.y + "] ";
}
}
}
if (noDesplazarCola == false)
{
//Desplazamos la cola de la serpiente en la dirección que indica el último punto
switch (pto0.d)
{
case 0: //derecha
if ((pto0.x + DESPLAZAMIENTO == pto1.x) && (pto0.y == pto1.y))
{
path.RemoveAt(0);
}
else
{
path[0].x += DESPLAZAMIENTO;
}
break;
case 1: //izda
if ((pto0.x - DESPLAZAMIENTO == pto1.x) && (pto0.y == pto1.y))
{
path.RemoveAt(0);
}
else
{
path[0].x -= DESPLAZAMIENTO;
}
break;
case 2: //arriba
if ((pto0.y - DESPLAZAMIENTO == pto1.y) && (pto0.x == pto1.x))
{
path.RemoveAt(0);
}
else
{
path[0].y -= DESPLAZAMIENTO;
}
break;
case 3: //abajo
if ((pto0.y + DESPLAZAMIENTO == pto1.y) && (pto0.x == pto1.x))
{
path.RemoveAt(0);
}
else
{
path[0].y += DESPLAZAMIENTO;
}
break;
}
}
return true;
}
//Comprobamos si la cabeza de la serpiente se cruza con el resto del cuerpo en algún punto
public bool SerpienteSeMuerde()
{
for (int i = 0; i < path.Count - 2; i++)
{
if (((path[i].x == path[path.Count - 1].x) && (path[path.Count - 1].y >= path[i].y) && (path[path.Count - 1].y <= path[i+1].y))||
((path[i].x == path[path.Count - 1].x) && (path[path.Count - 1].y <= path[i].y) && (path[path.Count - 1].y >= path[i + 1].y)))
{
return true;
}
if (((path[i].y == path[path.Count - 1].y) && (path[path.Count - 1].x >= path[i].x) && (path[path.Count - 1].x <= path[i+1].x)) ||
((path[i].y == path[path.Count - 1].y) && (path[path.Count - 1].x <= path[i].x) && (path[path.Count - 1].x >= path[i + 1].x)))
{
return true;
}
if ((path[path.Count - 1].x == path[i].x) && (path[path.Count - 1].y == path[i].y))
return true;
}
return false;
}
//Dibujar líneas punto a punto entre cada dos puntos contiguos almacenados en
//la ruta
public void MostrarSerpiente()
{
Graphics g = this.CreateGraphics();
Pen p = new Pen(Brushes.BurlyWood, ANCHO_SERPIENTE);
this.Refresh();
if (path.Count > 1)
{
for (int i = 1; i < path.Count; i++)
{
g.DrawLine(p, path[i - 1].x, path[i - 1].y, path[i].x, path[i].y);
}
}
//Datos para depuración
//lbl1.Text = "Xn=" + path[path.Count - 1].x;
//lbl2.Text = "Yn=" + path[path.Count - 1].y;
//lbl3.Text = "Xn_1=" + path[path.Count - 2].x;
//lbl4.Text = "Yn_1=" + path[path.Count - 2].y;
//lblLong.Text = longitudSerpiente().ToString();
}
private void fNibbles_KeyDown(object sender, KeyEventArgs e)
{
//SE PODRÍA UTILIZAR ESTE EVENTO EN LUGAR DE SOBREESCRIBIR EL MÉTODO ProcessCmdKey DEL
//FORMULARIO. AQUÍ NO SE UTILIZA ESTA OPCIÓN.
// //Comprobamos si se trata de una flecha
// if ((e.KeyCode == Keys.Up) || (e.KeyCode == Keys.Down) ||
// (e.KeyCode == Keys.Left) || (e.KeyCode == Keys.Right))
// {
// if (e.KeyCode == Keys.Right)
// path[path.Count - 1].d = 0;
// if (e.KeyCode == Keys.Left)
// path[path.Count - 1].d = 1;
// if (e.KeyCode == Keys.Up)
// path[path.Count - 1].d = 2;
// if (e.KeyCode == Keys.Down)
// path[path.Count - 1].d = 3;
// }
}
private void MostrarMensaje(String mensaje, int x, int y, Brush b)
{
Graphics g = this.CreateGraphics();
Font f = new Font("Arial", 14, FontStyle.Bold);
this.Refresh();
g.DrawString(mensaje, f, b , x, y);
}
}
//Clase que ayuda a definir la longitud y forma de la serpiente para
//su impresión en la pantalla
class Punto
{
public int x;
public int y;
public int d; //0(dcha), 1(arriba), 2(abajo), 3(izda)
public Punto() { }
public Punto(int _x, int _y, int _d)
{
x = _x;
y = _y;
d = _d;
}
}
//Esta clase define los alimentos que la serpiente debe ir "comiendo"
//para avanzar en el juego. En esta versión la propiedad c(color) no
//se utiliza.
class Alimento
{
public int x;
public int y;
public Color c;
public Alimento() { }
public Alimento(int _x, int _y, Color _c)
{
x = _x;
y = _y;
c = _c;
}
}
}

