.NET Tutorial 8. Añadiendo soporte On-Line: Cliente/Servidor (Parte I)

Una de las cosas que siempre me ha fascinado es la posibilidad de que dos o más ordenadores "hablen" entre sí. La tecnología avanza una barbaridad y hoy en día es relativamente "fácil" programarse una aplicación tipo Cliente/Servidor para poder comunicar dos o más ordenadores.

El tipo de cliente/servidor que os mostraré en esta entrega no es ni por asomo eficiente para un gran volumen de tráfico y de conexiones, pero creo que puede ser perfectamente válido para comunicar un par de decenas de clientes, lo cual nos deja un buen margen de maniobra si tenemos en mente un juego multijugador de "bajo" presupuesto y sin ninguna pretensión mas allá que alimentar nuestro ego 🙂

Bien, sabiendo esta limitación, repasemos algunos conceptos.

La comunicación la vamos a realizar mediante el protocolo TCP.

Existen multitud de protocolos: TCP, ICMP, IPX, SPX, UDP…Cada uno de ellos con sus caracteristicas particulares. En nuestro caso, el protoclo TCP asegura que lo que se envía, es lo que se va a recibir. Es decir, si yo envío "HOLA, SOY OLLYDBG", el propio protocolo TCP asegura que el "destinatario" va a recibir "HOLA, SOY OLLYDBG".

Otros protocolos como por ejemplo el UDP no aseguran que el "destinatario" reciba lo mismo que se envía. Si yo envío "HOLA, SOY OLLYDBG" vía UDP, puede ocurrir (o no) que el "destinatario" reciba "OLHA S,YOOL LYDGB".

Alguno/a puede estar pensado entonces: "Pues vaya mier**, ¿para que narices querría alguien usar UDP por ejemplo si lo que te llega puede no ser lo mismo que has envíado?".

Pues depende del tipo de aplicación. La inmensa mayoría de aplicaciones tipo "streaming" (VoIP, vídeo, etc) funcionan vía UDP, ya que al carecer de un "handshaking" es muchísimo más rápida. Además, UDP soporta "multicasting", esto quiere decir que podemos envíar un sólo paquete de datos para "n" clientes. Esto puede suponer un ahorro en el ancho de banda ("bandwidth") considerable en determinados escenarios. Los destinatarios (clientes) poseen algoritmos de correción de errores en caso de que los paquetes lleguen "desordenados". Además UDP no requiere una conexión "permamente".

Pero en fin, no pretendo calentaros mucho la cabeza y como nuestro proposito es hacer algo muy muy pero que muy sencillo dejaremos los tecnicismos y la teoría a un lado y nos centraremos más en el aspecto práctico.

SERVER "side"

En este tipo de aplicaciones tendremos dos tipos aplicaciones corriendo simultaneamente. Una aplicación actúa como "servidor" y otra (u otras) actúa/n como cliente.
Es posible crear una aplicación que actúe como servidor y cliente a la vez: la típica partida en LAN que nos hemos pegado jugando al  Diablo II, dónde un PC hacía de "servidor".
Este no va a ser nuestro caso, de momento.

Una de las formas más "fáciles" de probar este tipo de aplicaciones es hacer una especie de CHAT, y eso es precisamente lo que os mostraré hoy.

Por un lado tendremos que ejecutar nuestro servidor. El servidor se encargará de gestionar a todos los clientes que se conectan al chat. Además será el encargado de "reenviar" los mensajes que escribe cada cliente al resto de clientes. Si el cliente 1 escribe "Hola, como estás", este texto se envíará al servidor y el servidor se encargará de enviar dicho texto al resto de clientes, de tal forma, que todos los clientes vean "Hola, como estás".

Todo ello lo haremos de forma asíncrona.

¿Asíncrona? ¿Ein? ¿mande? WTF, no entiendo una palabra!!!! dO_ob

Sin entrar en demasiado detalle existen varias formas en las que un ordenador puede hacer una determinada tarea: de forma síncrona y de forma asíncrona.
Cuando realizamos una tarea de forma síncrona el ordenador estará "ocupado" mientras se realiza dicha tarea (nótese que he puesto ocupado entre comillas, ya que el tema del multithreading es tremendamente complejo y amplio que daría para 10 blogs como éste y aún me quedaría corto).
Sin embargo, una tarea asíncrona no "bloquea" al ordenador y éste puede hacer otras cosas al mismo tiempo.

Lo que no puede hacer nuestro servidor ni nuestros clientes es quedarse "bloqueado", esperando a enviar mensajes o a recibirlos.

El .NET Framework nos facilita una serie de métodos y clases para que nuestros sockets puedan trabajar de forma asíncrona.

A todo esto…ahora caigo que no he explicado que es un socket y resulta que esto es la "madre del cordero". 🙂

Un socket sería como un "enchufe" que permite la comunicación entre un programa cliente y un programa servidor a través de una red. También se definen como puntos finales de la conexión. Por decirlo de alguna forma es como una "pasarela" entre el cliente y el servidor. Es complicado de explicar. Aquí hay mas información por si tienes interés.

Bueno, estabamos hablando de los métodos asíncronos de los sockets.
Pues cómo decía, disponemos de varios métodos asíncronos para manejar los sockets. Veamos unos cuantos:

.BeginAccept

Mediante el método BeginAccept podremos ejecutar un determinado procedimiento cuando un cliente se conecta al servidor. ¿Cómo?. Asi:

serverSocket.BeginAccept(New AsyncCallback(AddressOf OnAccept), Nothing)

¿Que hace esa línea?

Cuando un determinado cliente se conecta a nuestro servidor hay que aceptar e "inicializar" el socket. Cuando esto ocurre se ejecutará en un hilo independiente el procedimiento OnAccept.

Si os fijais, el procedimiento OnAccept (que lo podriamos haber llamado pepe, por ejemplo, pero claro, el procedimiento pepe , no sé, no sé…:D ) está definido cómo un AsyncCallback. Esto quiere decir que el procedimiento OnAccept se ejecutará de forma asíncrona, en un hilo independiente, de está forma no se bloquea a serverSocket y éste puede seguir envíando y/o recibiendo cosas.

Los procedimientos asíncronos se definen todos así:

Private Sub OnAccept(ByVal ar As IAsyncResult)

Además, cuando se ejecuta un procedimiento asíncrono, hay que "finalizarlo". Es decir, si el procedimiento OnAccept se "dispara" por un .BeginAccept, tendremos que finalizarlo con un…..EndAccept 😉

.BeginReceive

Este método asíncrono provoca que se ejecute un determinado prodecimiento cuando el socket recibe "algo". Veámos cómo se usa:

clientSocket.BeginReceive(byteData, 0, byteData.Length, SocketFlags.None, New AsyncCallback(AddressOf OnReceive), clientSocket)

Al igual que antes, tenemos un AsyncCallback del procedimiento OnReceive. Es decir que cuando el socket reciba "algo" lo que se ejecutará será el procedimiento OnReceive, dónde si habeis estado antentos tiene que haber un .EndReceive

Para finalizar tenemos:

.BeginSend

y se usa así:

clientSocket.BeginSend(message, 0, message.Length, SocketFlags.None, New AsyncCallback(AddressOf OnSend), clientSocket)

Cómo podreis imaginar, .BeginSend se utiliza para enviar la información al cliente por el socket creado. y de nuevo, en el procedimiento OnSend tendremos un .EndSend

Resumiendo:

BeginAccept <—> EndAccept
BeginReceive <—> EndReceive
BeginSend <—> EndSend

Cómo veis  hemos usado estos tres métodos asíncronos que lo que hacen es ejecutar los procedimientos que les hemos dicho en sus respectivos AsyncCallbacks.

CLIENT "side"

Si más o menos has entendido como funciona la parte del servidor, verás que la parte del cliente es prácticamente igual.

En el cliente tendremos un nuevo método asíncrono, el que nos conecta con el servidor:

.BeginConnect

Verás que se usa como el resto de métodos:

clientSocket.BeginConnect(ipEndPoint, New AsyncCallback(AddressOf OnConnect), Nothing)

Y de nuevo en el procedimiento OnConnect tendremos el respectivo EndConnect:

Private Sub OnConnect(ByVal ar As IAsyncResult)
clientSocket.
EndConnect(ar)

En el cliente también verás que volvermos a tener un método para enviar (BeginSend) "cosas" al servidor y  otro método para recibir (BeginReceive) lo que el servidor envía al cliente. Funcionan de igual forma que en el servidor, es decir, con sus respectivos AsyncCallbacks.

Actualizando controles en una aplicación "gráfica"

Cuando se utilizan procedimientos asíncronos y queremos actualizar un control de un formulario, por ejemplo para mostrar los mensajes de clientes, no podremos hacerlo de forma "directa". Tenemos que utilizar lo que se conoce como delegados ("delegates").

Un delegado es por definición un "puntero seguro" (Thread-safe) . Se utilizan delegados precisamente para asegurarnos que el contenido del control (un listbox, un label, etc) se actualizán en un "hilo seguro", ya que como los procedimientos asíncronos son eso, asíncronos, no se asegura la correcta actualización del control.

Por eso vereis que tanto en el servidor cómo en el cliente hay "delegates", que se utilizan precisamente para eso 😉

Esto no es necesario en las aplicaciones en modo "consola". Hasta cierto punto el servidor se puede hacer en modo "consola", pero el cliente (que es lo que "vemos"), pues…cómo que no 🙂


Consideraciones finales

Aquí podeis ver una captura de pantalla de tres clientes conectados al servidor. Los clientes se estan ejecutando en una máquina virtual (VirtualBox), mientras que el servidor está en un Windows Vista: 


(haz click para agrandar)

Obviamente al ejecutar el servidor lo primero que va a ocurrir es que "salte" el firewall de Windows (u otro firewall que tengas instalado). Si quieres que este "invento" funcione tendrás que dejar pasar al servidor a través del firewall, sino, los clientes no podrán conectarse al servidor.

Puedes probar el servidor desde fuera de tu red. Para ello tienes que proceder como cualquier otro programa tipo "emule" o similares. Tendrás que abrir el puerto del servidor (por defecto es el puerto 3333) en tu router y luego redirigir el tráfico que llega al router por el puerto 3333 a la IP del pc dónde se está ejecutando el servidor. De esta forma puedes dar tu dirección IP pública a un amigo que tenga el cliente para que se conecte a tu servidor.

 

Sin duda alguna esta entrada ha sido la mas "técnica" hasta el momento. Hay multitud de conceptos por asimilar.

Aprovecho para adelantar que es probable que en las próximas entradas vaya un poco más "a saco". Me refiero a no tener que tirarme casi 6 horas en escribir una entrada. Proporcionaré el código fuente y poco más. Y sí teneis alguna duda, el botón de comentarios no muerde xDD

 

Saludos.
mov eax,ollydbg; Int 13h 

 

Descargar proyecto .NET Tutorial 8
(110 KB. Visual Studio 2008)

.NET Tutorial 7. Manipulando pixels por código: Efecto BlurMotion

Vamos a ver como es posible manipular un determinado bitmap para crear "efectos" curiosos.

Lo primero que tendremos que saber es que existen varias formas de manejar la información de un bitmap, en función del formato de los pixels que lo forman.

No es lo mismo acceder / manipular un bitmap de 32 bpp (bits por pixels), que uno de 24 bpp, o incluso uno de 8 bpp.

Por ejemplo, en un bitmap de 32 bpp, cada pixel está representado por los siguientes valores:

Un valor entre 0 y 255 para la componente azul
Un valor entre 0 y 255 para la componente verde
Un valor entre 0 y 255 para la componente roja
Un valor entre 0 y 255 para la componente alpha

Sin embargo, en un bitmap de 8bpp, cada pixel está representado por:

Una valor entre 0 y 255 que pertenece a una paleta de colores (paleta con 256 posibles valores)

Teneis que volver a pensar en "Arrays" (Arrays, WTF!!!). Cualquier bitmap, independientemente del formato de pixel que tiene, se almacena en memoria como un array unidimensional.

Para verlo más claro:

Esta imagen representa un array de bits para una imagen de 24 bpp.
Cada uno de los elementos del array representa el valor de cada componente.

El elemento 0 del array, representa el valor de la componente azul del pixel que está es la posición x=0, y=0 (0,0). El elemento 1 del array, representa el valor de la componente verde del pixel (0,0), el elemento 2 del array representa el valor de la componente roja del pixel (0,0), el elemento 3 del array representa el el valor de la componente azul del pixel (1,0),……

En una imagen de 24 bpp, cada pixel tiene 3 componentes: la azul, la verde y la roja. Cada una de estas componentes tienen 256 posibles colores (valores estre 0 y 255). Por lo tanto, en una imagen de 24 bpp, cualquier pixel puede tener 256 posibles valores de "azules", 256 posibles valores de "verdes" y 256 posibles valores de "rojos". Esto da un total de 256 * 256 * 256 = 16.777.216 posibles combinaciones. O dicho de otra forma, existen 2^24 (dos elevado a la 24) posibles valores para cualquier pixel en un formato de 24 bpp.

En una imagen de 8 bpp la cosa funciona de forma diferente:

En este caso, existe una "paleta" de 256 posibles valores. Recordar, es una bitmap de 8 bits por pixel, por lo tanto, cada pixel tiene 2^8 (dos elevado a a la 8) posibles valores.

Cada pixel tiene un valor. Dicho valor se corresponderá con el color establecido en la paleta de colores.

En la imagen anterior, los 3 primeros pixels tienen el valor 64. Esto quiere decir que dichos pixels se pintarán con el color "número 64" de la paleta. El penúltimo pixel por ejemplo tiene el valor 205. Esto quiere decir que aquel pixel se pintará con el color "número 205" de la paleta.

Cómo veis, es bastante distinto usar 24/32 bpp que 8 bpp.

Acceder directamente a los pixels de un bitmap

Tal y como se ha dicho anteriormente, la información del bitmap se guarda en un array unidimensional. Esto puede suponer un "problema", ya que los pixels se dibujan en un plano de dos dimensiones, con coordenadas X,Y.

Ejemplo práctico 1: un bitmap de 100×100 pixels y de 8 bpp se almacena en un array de (100 * 100) -1 elementos. Es decir, en miArray ( 9999 )

Ejemplo práctico 2: un bitmap de 100×100 pixels y de 24 bpp se almacena en un array de ( (100 * 100 ) -1 ) * 3 elementos. Es decir, en miArray ( 29997 )  (¿Por qué * 3?: Estate atento, ya hemos visto que cada pixel en un formato de 24 bpp necesita 3 valores, uno para la componente azul, otro para la componente verde y otro para la componente roja)

Pues bien, cómo acceder al elemento correcto. Miremos la siguiente imagen:

Mediante los métodos y propiedades que veremos luego, sabremos la posicion del scan0 y el valor del stride.

El scan0 nos indica  la posición de memoria que contiene el primer elemento del array. Por decirlo de alguna forma, es un "puntero" a la posición de memoria del primer elemento del array que forma el bitmap

El stride es el "ancho" (nótese que he puesto ancho entre-comillado) del bitmap. Este "ancho" lo gestiona el propio GDI.

Pues bien, según el formato o bpp del bitmap accederemos al mismo de la siguiente forma:

8bpp:
offset = (Y * stride) + X
miArray ( offset ) = 0 – 255

24bpp:
offset = (Y * stride) + (X * 3)
miArray ( offset ) = 0 -255 componente AZUL
offset = (Y * stride) + (X * 3) + 1
miArray ( offset ) = 0 -255 componente VERDE
offset = (Y * stride) + (X * 3) + 2
miArray ( offset ) = 0 -255 componente ROJA

Ejemplo 1 para "torpes":
En un bitmap de 8bpp, dibujar un pixel en la posición (23,10) con el color 134 de su paleta:
offset = (10 * stride) + 23
(supongamos aqui, que stride es un cierto valor, por ejemplo, 64), Por lo tanto:
offset = (10 * 64) + 23
offset = 663
miArray ( 663 ) = 134

Ejemplo 2 para "supertorpes":
En un bitmap de 24bpp, dibujar un pixel en la posición (23,10) y que sea de color amarillo:

Aclaración: El color amarillo se consigue como composición de: 255 rojo, 255 verde y 0 azul.

offset = (10 * stride) + (23 * 3)
(supongamos aqui, que stride es un cierto valor, por ejemplo, 64), Por lo tanto:
offset = (10 * 64) + (23 *3)
offset = 709
miArray (709) = 0

offset = (10 * stride) + (23 * 3) + 1
(supongamos aqui, que stride es un cierto valor, por ejemplo, 64), Por lo tanto:
offset = (10 * 64) + (23 *3)  + 1
offset = 710
miArray (710) = 255

offset = (10 * stride) + (23 * 3) + 2
(supongamos aqui, que stride es un cierto valor, por ejemplo, 64), Por lo tanto:
offset = (10 * 64) + (23 *3)  + 2
offset = 711
miArray (711) = 255

Cómo veis, en una imagen de 24bpp, para "pintar" un sólo pixel, necesitamos 3 posiciones en el array.

Si llegado a este punto de esta entrada no has entendido nada de nada de todo lo que hemos explicado, te sugiero dos cosas:

  • Relee de nuevo la entrada y si no entiendes algo usa los comentarios de esta página
  • Si aún releyendo todo sigues sin entenderlo, te recomiendo que te dediques al punto de cruz o a otro hobby, ya que está claro que la programación no es, ni será lo tuyo 😉

Bien, una vez vista la teoría, pasemos a la práctica. 😉
Tedremos que tener en cuenta un par de cosas, a saber:

  • "Bloquear" el bitmap mientras se manipula
  • Obtener la posición o dirección de memoria donde está almacenado el bitmap
  • Copiar los datos desde esa posicion de memoria a un array unidimensional
  • "pintar cosas"
  • Copiar los datos del array unidimensional a la posición del bitmap
  • "Desbloquear" el bitmap

El resultado será el siguiente: 

Aquí tenemos 2 bitmaps, el de la izquierda es de 8 bpp, mientras que el de la derecha es de 24 bpp.
Pues bien, el código es más o menos asi:

Dim rect8bpp As New Rectangle(0, 0, bmp8bpp.Width, bmp8bpp.Height)
‘ Bloquear los bits del bitmap
Dim bmpData8bpp As System.Drawing.Imaging.BitmapData = bmp8bpp.LockBits(rect8bpp, _
Drawing.Imaging.ImageLockMode.ReadWrite, bmp8bpp.PixelFormat)
‘ Dirección de la memoria de la primera linea
Dim ptr8bpp As IntPtr = bmpData8bpp.Scan0
Dim scanline8bpp As Integer = bmpData8bpp.Stride
‘ Array con los datos del bitmap
Dim bytes8bpp As Integer = bmpData8bpp.Stride * bmp8bpp.Height
Dim rgbValues8bpp(bytes8bpp – 1) As Byte
‘ copiar los valores del bitmap a nuestro array
System.Runtime.InteropServices.Marshal.Copy(ptr8bpp, rgbValues8bpp, 0, bytes8bpp)
‘ "pintar" unos cuantos pixels
‘ offset para 8bpp: (Y*scanline) + X = entrada color paleta

For y = 120 To 133
For x = 0 To 255
OffSet = (y * scanline8bpp) + x
rgbValues8bpp(OffSet) = x
Next
Next
‘ Copiar el array al bitmap
System.Runtime.InteropServices.Marshal.Copy(rgbValues8bpp, 0, ptr8bpp, bytes8bpp)
‘ Desbloquear los bits del bitmap
bmp8bpp.UnlockBits(bmpData8bpp)

En el bitmap de 8bpp hemos creado una paleta que va desde el negro "puro" hasta el "rojo puro" con el siguiente procedimiento:

Dim pal As System.Drawing.Imaging.ColorPalette = bmp8bpp.Palette
Dim i As Integer
‘255 niveles de "rojo"
For i = 0 To 255
pal.Entries(i) = Color.FromArgb(i, 0, 0)
Next
bmp8bpp.Palette = pal

Para el caso del bitmap de 24bpp, se hace algo similar (bloquear el bitmap, copiar, "pintar cosas", volver a copiar y desbloquear).

Bien, llegados a este punto, más de uno/a y más de dos estará diciendo "Hmmm…qué interesante" (todo ello con cara de WTF, pero que me estás contando colega, tanto rollo para dibujar dos lineas de "colorines"!!!)

Aix…pequeño padawan, cuanto te queda por aprender, a veces, los árboles nos impiden ver el bosque xDDDDDD.

¿Y si te enseño esto?:

¿o esto otro?

(Nota: aclarar que estos videos se ven fatal, están realizados en una máquina virtual (VirtualBox) y la velocidad y suavidad no es ni por asomo la misma que ejecutando el exe original)

Os recuerdo que en esos dos videos lo que veis se genera en tiempo real, no es ninguna "animación" prefabricada con "fotochop" o similares.

¿Cómo demonios conseguir hacer esos efectos? (jejeje, me alegra de haber captado tu atención de nuevo 😉

La respuesta es más simple de lo que parece (y de paso decir que es uno de los efectos mas viejos y manidos de las antiguas "demos" e "intros"):

La idea es tener una paleta de colores "adecuada" y dividir el valor de cada pixel por  el valor de sus pixels "vecinos". Sí, parece raro de entender, pero es simple de cojo***…digo, simple de narices:

  • Dibujo un pixel en la posición (X,Y) con el valor 255 de la paleta (en nuestro caso, amarillo puro)
  • Miro el valor de los siguientes pixels valor = (x-1,y) + (x+1,y) + (x,y-1) + (x,y+1)
  • Este valor lo divido entre 4 y lo pinto en la posición (X,Y). (X,Y) = valor /4
  • Esto lo hago con todos los pixels de la escena. De tal forma, que en cada nueva iteración, los valores de los pixels van cambiando, dando la sensación del "fuego".

En realidad, el mecanismo es más simple que un chupete, pero los resultados, son bastantes "chulos".

Bueno, hasta aquí la entrada de hoy. Espero vuestros comentarios.

 

Saludos.
mov eax,ollydbg; Int 13h

 

Descargar proyecto .NET Tutorial 7
(70 KB. Visual Studio 2008)

Teclas para Rotaciones3D:
Pulsa z,x,y para incrementar la velocidad de giro
Manten pulsada la tecla MAYS para decrementar la velocidad de giro

 

TetrisNET (Parte III)

Con esta tercera entrega estaremos a punto de finalizar nuestro "tetris", cómo podeis apreciar en el siguiente vídeo:

Y cómo el movimiento se demuestra andando, empecemos 😉

En las entregas anteriores vimos como crear y mover las piezas. Ahora tenemos que introducir un nuevo elemento: La rejilla de juegos (¿rejilla?….¿juegos?….tengo que de dejar de ver TRON 😉

En nuestro caso, nuestra "rejilla" será algo cómo esto:

Esta rejilla estará representada por la clase clsBloquesColocados.Esta clase contendrá un array bidimensional de 20 x 10 elementos. Cadauno de estos elementos pertenece a la siguiente estructura:

Public Structure sBloqueColocado
Dim HayBloque As Boolean
Dim TipoBloque As eTiposBloque
End Structure

Esto quiere decir que en todo momento sabremos que en ciertaposición de la rejilla hay o no un "bloque" (recordad que los bloquesson los que forman cada una de los siete tipos de piezas del juego) yen caso de que sí haya bloque sabremos de que "tipo" de bloque setrata.

La colisión

La parte "difícil" es saber cuando una determinada pieza se hacolocado. Veamos unas cuantas imagenes para confeccionar nuestroalgoritmo de colisión.

Supongamos que tenemos la siguiente situación:

En este caso, la pieza cyan es la que se está moviendo, mientras que la pieza morada ya está posicionada.
Pues bien, ¿Cómo demonios sabemos que la pieza cyan debe "parar" de caer?

Cuando posicionamos una pieza, se actualiza el objeto de tipo clsBloquesColocados.

La pieza cyan es un objeto de tipo clsBloques, mientras que la "pieza" morada pertenece a clsBloquesColocados.

Dentro de la clase clsBloques tenemos un par de propiedades interesantes: FilaCeldaTOPizquierda y ColumnaCeldaTOPizquierda.

Estas dos propiedades nos dicen en todo momento cual es la posicióndel bloque superior izquierdo dentro de nuestra rejilla de juego.Además en la clase clsBloques disponemos de las propiedades Filas y Columnas,con lo cual, combinando estas cuatro propiedades podremos saber laposición de cualquier bloque de la pieza en la rejilla de juego. Puesbien, en lenguaje normal podriamos hacer esto:

"Mira si justo debajo de la pieza que está cayendo hay algún bloque".

¿Cómo sabemos cual o dondo es "justo debajo"?

Fácil, "Justo debajo" = clsBloque.FilaCeldaTOPizquierda – clsBloque.Fila

¿Cómo sabemos si "hay algún bloque"?

Pues más fácil todavía: 

Mira desde la columna clsBloque.ColumnaTOPizquierda hasta clsBloque.ColumnaTOPizquierda + clsBloque.Columna si en la clase clsBloquesColocados el valor de la celda es "True".

Veamos, veamos…:

"En la fila de abajo de la pieza que esta cayendo SI que hay un bloque ya posicionado, por lo tanto, detener la pieza"

Y en está situación (igual que antes, la pieza cyan está cayendo mientras que la pieza morada ya está colocada:

En este caso, hay "dos" bloques que "Estan en la fila de abajo de la pieza que está cayendo":

A priori podriamos pensar que ya lo tenemos, pero ops!!!…..que pasa con esta situación:

"Mira en la fila de abajo si hay algún bloque ya colocado":

Pues sí, lo hay, pero obviamente, la pieza morada "puede" (y debe) seguir cayendo, por lo menos, una fila más.

Está claro que nuestro algoritmo hace aguas, ya que está situaciónno debería ocurrir. ¿Cómo solucionarlo?. De nuevo la respuesta latenemos delante:

"Mira en la fila de abajo si hay un bloque ya colocado". "Además mira encima de dicho bloque/s si la pieza que está cayendo tiene un "hueco" ("hueco" = clsBloque.SubCelda (fila, columna) = False )

Vamos a ver:

La celda "negra" representa el "bloque de la fila de abajo ya colocado". La celda "azul" representa el bloque de la ficha que está cayendo, y como vemos, en la posición del bloque "azul", la pieza morada NO tiene bloque, por lo tanto, sigue cayendo.

No me convences….a ver, a ver…:

En este caso vemos que encima de un "bloque" ya posicionado, SI que hay un bloque de la pieza que está cayendo. Por lo tanto, la pieza morada si que se tiene que detener.

Pues bien, todo este algoritmo está implementado en la funcion HayColision. Esta función devolverá True si la pieza ya "no puede caer" o False "si la pieza puede seguir cayendo".

Cómo vereis en el código, esta función básicamente consta de dosbucles FOR anidados que realizan las comprobaciones que hemos descritoarriba. No tiene mucho misterio.

La verificación de Lineas

Una vez que se ha detectado una colisión se actualiza el objeto de la clase clsBloquesColocados mediante el procedimiento ColocarPieza.Este procedimiento actualiza nuestra rejilla de juego, ya que sabemosla posición de la pieza que acaba de colisionar y de que tipo es.

La verificación de si se ha completado una o varias lineas estremendamente simple. Lo único que tendremos que mirar es nuestrarejilla de juego y verificar que "para una fila X, TODAS las columnas de dicha fila tienen la propiedad HayBloque = True".

Para "borrar" las lineas completadas, rellenamos un objeto temporal,únicamente con aquellas filas que "no estan completas". De todo esto seencarga el procedimiento CheckLineas.

Validación de los movimientos

Suponed que tenemos la siguiente situación:

En este caso, la pieza morada está "cayendo", mientras que la pieza verde ya está posicionada. En esta situación, NO podremos mover la pieza morada hacia la izquierda.

Disponemos de una función llamada MovimientoValido a la cual se le pasa un objeto de tipo clsBloque. Esta función verifica que en ningún bloque de la pieza, ya exista un bloque colocado.

En el ejemplo anterior, al mover la pieza morada hacia la izquierda tendriamos la siguiente situación:

Cómo veis, hay un bloque de la pieza morada que estaría en la misma posición que un bloque colocado, por lo tanto, el movimiento NO es válido.

La función MovimientoValido seutiliza también para ver si una determinada pieza se puede rotar o no,ya que hay situaciones donde es imposible rotar una pieza, por ejemploaqui:

En este caso, la barra estácayendo y aunque queramos "rotarla", no podemos, ya que no "coje" si larotamos. Pues de esto también se encarga la función MovimientoValido.

Notas finales

Se ha añadido una nueva tecla, en este caso el "cursor abajo", que acelera la caída de la pieza. El resto de teclas permanece igual que en la versión anterior.

Cosillas que quedan por hacer:

  • Dar "forma" a los menús del juego: Selección de niveles, opciones de juego, etc.
  • Añadir un efecto de partículas "simple" para cuando desaparezcan líneas.
  • Dar forma al interfaz del juego: puntos, combos, vidas, etc.
  • Añadir efectos sonoros y música en general.

Cosillas que pueden mejorar mucho el juego:

  • Añadir una opción para subir la puntuación a un página php, asp, etc a un servidor y poder de esta forma ver las puntuaciones "on-line"
  • Añadir una opción para jugar contra otro/otros jugadores mediante una conexión TCP/IP
  • Añadir varias modalidades de juego: Sprint, pieza única, etc.

 

Como habeis podido comprobar esta entrada ha sido con "fundamento".Espero que los conceptos que hemos visto aquí os hayan sido de ayuda.Recordad que aunque estemos haciendo un "tetris", alguna de las cosasque hemos visto a lo largo de estas tres entregas os pueden ser útilesa la hora de "enfocar" un determinado problema.

 

Saludos.
mov eax,ollydbg; Int 13h

 

Descargar proyecto .TetrisNET Parte III
(90 KB. Visual Studio 2008)

TetrisNET (Parte II)

Vamos a empezar a darle "forma" a nuestro tetris. En este entrega haremos esto:

Aquí ya podremos "mover" las piezas mientras caen. Básicamente tendremos dos objetos que son del tipo clsBloques. uno de ellos, BloqueA representa la pieza Activa (la que estamos moviendo), mientras que el otro objeto, BloqueNext, representa la "próxima" pieza. En posteriores entregas veremos que apenas modificando nada en el código podemos tener más de "una pieza próxima".

Los controles de momento son los siguientes:

Cursor Derecho: Mueve la pieza hacia la derecha
Cursor Izquierdo: Mueve la pieza hacia la izquierda
Cursor Arriba: Rota la pieza hacia la derecha (sentido horario)
Z: Rota la pieza hacia la izquierda (sentido anti-horario)
X: Rota la pieza hacia la derecha (sentido horario)
Barra Espaciadora: Genera una nueva pieza

Si observais el código, hay un pequeño "apaño" con las teclas "cursor arriba", "Z" y "X".  Me explico: Cuando pulsamos una tecla se genera el Evento KeyDown. Este evento se ejecuta c o n t i n u a m e n t e mientras la una tecla está pulsada.Por lo tanto, si dejamos el dedo "pegado" en la tecla "Z", es posible que la pieza se "rote" 10 veces seguidas, cosa que no queremos. Por lo tanto, tendremos un "Flag" en el evento opuesto, es decir, en el evento KeyUp, que nos indicará que ya hemos "soltado" la tecla.

Si os fijais con el "cursor derecho" y con el "cursor izquierdo" no hacemos este "apaño". Aquí si que nos interesa que mientras tengamos pulsados los cursores izquierdo o derecho, la pieza se mueva hacia la izquierda o derecha sin necesidad de soltar la tecla.

El código es bástante simple, quizás más simple incluso que la primera parte:

  • Se incializa BloqueA y BloqueNext
  • Se carga un Timer que actuará como "nivel de dificultad". Cuando más bajo sea su Interval, mas rápido caeran las piezas.
  • Se pulsan las teclas y se actúa en consecuencia (mover, rotar, etc)
  • Dentro del timer, se mira si la pieza ha llegado al final de la pantalla, en caso afirmativo se incia una nueva pieza

Cómo veis esta entrega es algo "escueta", ya que básicamente lo que hemos hecho ha sido "un lavado de cara" de la primera parte.

Si teneis alguna duda, ya sabeis, el botón de Comentario no muerde 😉

 

Saludos.
mov eax,ollydbg; Int 13h  

 

Descargar proyecto .TetrisNET Parte II
(85 KB. Visual Studio 2008)

TetrisNET (Parte I)

En esta serie de tutoriales aprenderemos cómo hacer un "tetris" desde cero.
Mi idea es ir poco a poco pero con buena letra. De poco o nada sirve coger un proyecto con un mogollón de clases, estructuras, módulos si no se entiende la base.

Echaremos mano otra vez de los arrays (WTF!!!). Tal y cómo vimos en el Tutorial 5, los arrays puede que sean la piedra angular de la programación. Cuanto antes os acostumbreis a ellos, antes podreis dar "el siguiente paso".

En nuestro caso construiremos una clase, clsBloques, que se encargará del manejo de las piezas del tetris.
En esta clase tendremos una array bidimensional que representarán a cualquier pieza:

En el caso de la figura anterior, el array mSubCeldas de boolenaos, que está dentro de la clase identifica a los "bloques" de la pieza. Este array bidimensional representa las (filas, columnas) que indican la forma de una pieza. En este caso, el array es de dimensiones (2,3)

Esto significa que en la fila 1, columna 1 (11), esa pieza "no tiene" bloque.
En la fila 1, columna 2 (12), si "que hay bloque", por lo tanto, en (1,2) tendremos un True.
En la fila 1, columna 3 (13) la pieza "no tiene" bloque, por lo tanto, en (1,3) tendremos un False
En la fila 2, columna 1 (21) la pieza si que tiene bloque
Lo mismo ocurre para la fila 2, colunma 2 (22) y para la fila 2, columna 3 (23).

La idea de hacerlo así, es que en cualquier momento podemos crear "piezas raras" (piezas con formas no estándar del tetris tradicional) sin necesidad de tocar el código.

Cuando se "rota" una pieza (ya bien sea rotar a la derecha: en el sentido horario, o rotar a la izquierda: en el sentido antihorario) lo que tenemos que hacer es modificar el array bidimensional, he intercambiar las filas por las columnas. En este caso se ha rotado hacia la derecha (sentido horario), por lo que tenemos un array de (3,2) (3 filas, 2 columnas):

Si comparamos estas dos imagenes:

Antes e rotar:              Después de rotar:
      

Lo que antes de rotar estaba en 11, pasa a 12
Lo que antes de rotar estaba en 12, pasa a 22
Lo que antes de rotar estaba en 13, pasa a 32
Lo que antes de rotar estaba en 21, pasa a 11
Lo que antes de rotar estaba en 22, pasa a 21
Lo que antes de rotar estaba en 23, pasa a 31

Vereis que en código hay dos procedimiento, RotacionIzquierda y RotacionDerecha que basicamente lo que hacen es crear un bloque "temporal" del mismo tipo que la pieza que se está rotando y mediante dos bucles FOR anidados actualiza la matriz de booleanos mSubCeldas.

En la próxima entrega veremos la utilidad del array mSubCeldas, pero a grandes rasgos hay que tener en cuenta esto:

Nuestra piezas las "colocaremos" en un array de 20 filas x 10 columnas, tal y como vemos en la figura anterior. Cada una de estas celdas valdrá "TRUE" o "FALSE" si en dicha celda hay un "bloque" de cualquier pieza. De esta forma es mucho mas fácil controlar las lineas que se hacen y demás cosas que ya veremos en próximas entregas.

En esta primera entrega veremos como se inicializa la clase clsBloques,  cómo establecer una ficha y cómo "rotarla":

Cómo podreis comprobar, no hay mucho misterio en esta entrega, ya que es una primera toma de contacto.

Desde aquí quiero agradecer a toni12 por la amabilidad de dibujar los sprites de las piezas. Thx a lot, eres un crack 😉

 

Saludos.
mov eax,ollydbg; Int 13h 

 

Descargar proyecto .TetrisNET Parte I
(71 KB. Visual Studio 2008)