Tutorial usando funciones UDP de net.dll para Fenix
por Titonus
2. Tipos o modelos de conexión entre los usuarios (servidores y clientes)
3. Tipos de datos para los paquetes UDP
4. Declaración de los paquetes UDP (variables) y acceso a sus campos
5. Inicio y apagado de la librería
6. Determinar y asignar paquetes UDP a las variables antes declaradas
7. Estableciendo conexiones (servidor)
8. Estableciendo conexiones (cliente)
9. Enviando y recibiendo paquetes UDP
Este pequeño tutorial pretende explicar de una forma práctica el uso de algunas de las funciones UDP de la librería net.dll (creada por MaQ) usando Fenix, de modo que tareas básicas y necesarias para cualquier juego o proyecto en red como: establecer la conexión con el servidor, enviar datos, recibir datos, etc... mediante pequeños bloques de código puedan ser realizadas, pero intentaré no entrar en ningún tipo o modelo concreto de conexión entre los usuarios (entendiendo como usuarios a los servidores y clientes) para comunicarse, ya que si bien una de las virtudes de esta librería es poder idear y desarrollar casi cualquier manera de comunicación entre dichos usuarios ofreciendo la posibilidad de un control total, es a su vez uno de sus principales inconvenientes para poder empezar a programar con ella, al tener que diseñar, controlar y programar todo su 'esqueleto' desde cero.
Sería recomendable conocer y dominar el concepto de puntero aunque estos no los vayamos a usar a fondo. A continuación y antes de entrar en materia doy unas pequeñas ideas en cuanto a los diversos tipos o modelos de conexión entre los usuarios; por último decir que todo lo que aquí se recoge está basado principalmente en los pequeños ejemplos que incluye la librería y que lo citado no es ni mucho menos la mejor manera de hacer las cosas ni las más adecuadas para todos los proyectos y juegos, y por ello recomiendo investigar y experimentar con la librería como mejor forma de entenderla y manejarla adecuadamente según sean las necesidades.
Para acabar ya con la introducción tan sólo decir que es muy posible que este tutorial tenga errores, ya sean gramáticos, teóricos o sobre el uso de la librería. Cualquier duda adicional sobre las funciones o comandos de la librería puede ser consultado en un fichero de texto que se incluye en la misma.
2. Tipos o modelos de conexión entre los usuarios (servidores y clientes)
Antes de entrar en el título de este punto y empezar a diseñar el algoritmo de red es saber si el juego es en tiempo real o por turnos.
Una primera conclusión de esto quizás nos lleve a pensar de una forma que es errónea o no es exacta del todo, intuyendo que los del primer tipo son más díficiles de llevar a cabo que los del otro tipo, por el simple hecho de que en tiempo real estamos enviando y recibiendo (generalmente independientemente de las acciones de los usuarios) constantemente información o datos y por turnos los datos serán transmitidos a los demás cuando por ejemplo un usuario haya acabado de realizar cierta acción y la información de éste sea necesaria para que el resto pueda llevar a cabo alguna acción en su turno.
Una forma de ver que el modo de transmisión de datos en tiempo real es un caso 'particular' del de por turnos es que la 'orden' de envío o recepción de datos de cada usuario es automática y no depende de estos, sin embargo y normalmente en el otro tipo (por turnos) es necesario que el usuario indique un fin o marca de que no desea realizar acciones hasta la próxima 'vez' u 'orden' para que se ponga en marcha la transmisión de datos.
Tomemos como ejemplo que se pueda romper este esquema de turnos ya que si un usuario desea abandonar (y puede, al haber sido diseñado así en un hipotético juego) la partida en el turno de otro usuario, en ese momento intervendría una transmisión de datos (en tiempo real) hacia los demás indicando que el usuario ha salido y todo ello aunque no tenga su 'orden' o su 'vez' para enviar información suya. Con ello quiero dar a entender que hay mil y una formas de transferir los datos y mezcla entre envió por turnos y en tiempo real, puesto que al fin y al cabo depende del propio diseño del juego.
Una vez comprendido esto pasemos a los tipos o modelos de conexión entre usuarios.
La manera más básica de una partida en red, ya sea en LAN (Local Area Network o lo que es lo mismo Red Local) o Internet, es pensar en dos ordenadores conectados, donde uno de ellos es el que crea la partida y el otro el que se une, lo que se denomina un servidor y un cliente, aquí el flujo de datos es muy sencillo ya que el servidor recibe los datos del cliente y envía sus datos, cosa que el cliente realiza de la misma forma enviando sus datos y recibiendo los del servidor.
Como cité en la introducción no voy a entrar a menudo en los métodos o maneras de como se van uniendo los clientes a la partida, pero dado que estamos en el caso base lo haré ya que puede servir a la hora de entender el bloque de código para establecer una conexión. La situación básicamente es la siguiente: el servidor se inicia abriendo un puerto para 'escuchar peticiones', en el momento que el cliente inicia (abre un puerto para 'enviar peticiones') e introduce la dirección IP o Host (son maneras de identificar a un ordenador) junto con el puerto del servidor al que manda una señal, el servidor entonces abrirá otro puerto, para 'enviar peticiones' y mandará información al cliente 'indicando que se ha conectado' y empezará la partida. El cliente también abrirá otro puerto para 'escuchar las peticiones' del servidor, manteniendo ambos, servidor y cliente, dos puertos abiertos, uno para 'escucha' y otro para 'envío', y entonces comenzará un búcle de transmisión de datos hasta que se finalize la partida, alguno de los dos desconecte o se produzca cualquier error técnico.
Cada vez que indique que abrimos un puerto para establecer un vínculo con otra máquina abriremos un 'socket' (para entenderlo facilmente, diremos que un 'socket' está un nivel superior a los canales por los que puede viajar la información) y usaremos un único canal del mismo para evitar posibles confusiones y líos a la hora de enviar y recibir datos entre los usuarios. En un 'socket' podemos abrir diferentes puertos, y cada 'socket' puede tener varios canales, pero cada canal abierto en un 'socket' tendrá un único puerto asociado.
Supongamos ahora un servidor y más de dos clientes que se conectan, en este caso lo más lógico es que el servidor reciba información de todos los demás y distribuya ésta (además de la suya) a cada uno de los clientes, los cuales tán solo enviarán su información al servidor y recibirán el resto menos la suya propia antes enviada. Otra cosa que pueda ser primordial que determinará en cierto sentido el diseño del servidor en este caso será si para que empieze la partida se han de conectar, antes de iniciar la transmisión de datos, un número determinado de clientes o en cambio estos pueden conectarse en cualquier momento del juego. Una consecuencia que se deriva de todo lo dicho es que por norma o regla general es que el pc o máquina que ejecuta el servidor normalmente habrá de disponer de unas condiciones óptimas para poder ofrecer un buen rendimiento en el reparto de información (procesador rápido, buena cantidad de memoria ram, ancho de banda grande según las condiciones...).
Hemos estado suponiendo que en los dos anteriores casos el servidor (realmente son un único caso) también interviene en el desarrollo del juego o actúa como otro jugador. Pues ahora supongamos que no, y entonces estamos hablando de un 'servidor dedicado', por lo tanto su única función al no intervenir ya como jugador será la de mediar en el reparto de información entre los jugadores. Si podemos decir que una desventaja de este tipo es que en la máquina donde se ejecuta no se puede 'jugar', la ventaja consiste en que podemos obtener un rendimiento mayor o más rapidez en el reparto de información entre los usuarios al no tener que gastar recursos en permitir a un jugador poder realizar acciones (interactuar), ya sea por ejemplo no teniendo que cargar/mostrar gráficos, calcular ecuaciones, colisiones, cargar sonidos y músicas, etc...
Como último caso falta hablar del 'servidor maestro' cuyo trabajo consiste en conocer que servidores (tanto dedicados o no) están ejecutándose o llevan actualmente una partida en curso, para así poder ofrecer a los clientes (cuando lo soliciten) una lista de servidores a los que poder conectarse para poder jugar. Si se desea implementar en el juego esta posibilidad habrá que hacer diversas modificaciones en el inicio de aquellos servidores que deseen autentificarse (accesibles a los usuarios que no sepan su dirección IP o Host y busquen por medio de la lista que ofrece el 'servidor maestro') en el 'servidor maestro' (pueden ser varios) para que le remitan información si están funcionando (cuantos jugadores tiene, reglas del juego...) o ya han cesado su actividad. Hoy en día estos 'servidores maestros' como por ejemplo: Battle.Net de la compañia Blizzard o el sistema WON conocido por el juego Half-Life, además de ofrecer las listas de servidores disponibles a los usuarios para poder jugar disponen de salas de chats y otras posibilidades. Como se puede deducir de estos dos ejemplos citados, los 'servidores maestros' no son creados por los usuarios finales del juego y son administrados por los responsables del producto.
Podría seguir citando otros casos o tipos de conexión entre usuarios pero a partir de los que he citado creo que se pueden construir la infinidad de casos existentes.
Para finalizar este apartado mencionar dos incisos:
Al principio de este punto hablaba sobre un tema que en cierto modo pueda corresponder a comunicaciones síncronas y/o asíncronas bajo el camuflaje de flujo de datos 'en tiempo real' y 'por turnos'. La finalidad de este tutorial no requiere conocimiento sobre estos dos conceptos, pero en otros tutoriales mucho más elaborados y detallados sobre comunicaciones en red podreís encontrar mas información al respecto para poder entender con mejor claridad como se realiza la transmision de datos desde un punto o nivel mas bajo.
Con respecto a usar las funciones UDP en este tutorial en vez de TCP, básicamente se trata por razones de su comportamiento, aunque ambos tienen sus ventajas y desventajas y pueden ser perfectamente aplicables, los paquetes TCP cuando se envian han de ser confirmados por la máquina receptora de que han llegado, esto permite la seguridad de que hasta que no lleguen no se haga el próximo envío lo que implica lentitud en la transmisión de datos, por el contrario los paquetes UDP simplemente se envían, aligerando la transmisión aunque se puede perder información al haber paquetes que no lleguen al destinatario, pero pudiendose 'arreglar' enviando varias veces el mismo paquete de información.
3. Tipos de datos para los paquetes UDP
Los tipos de datos que expongo a continuación son los genéricos utilizados en los ejemplos de la DLL y no es recomendable modificar nada ya que pueden producirse errores en tiempo de ejecución al usar las funciones UDP.
...
// Tipos de datos a usar para los paquetes UDP
type _IPaddress
int host; // Host o IP de tipo entero
word port; // Puerto de tipo palabra (word)
end;
type _UDPpacket
int channel; // Canal en el que se envía el paquete de tipo entero
byte pointer data; // Información a enviar o recibir que es un puntero
de tipo byte
int len; // Longitud de tipo entero del paquete
int maxlen; // Longitud máxima de tipo entero del paquete
int status; // Estado del paquete
_IPaddress address; // Dirección (tanto Host/IP como puerto de tipo
_IPaddress antes especificado)
end;
...
4. Declaración de los paquetes UDP (variables) y acceso a sus campos
Todos los paquetes UDP corresponderán a punteros de tipo _UDPpacket definidos en el apartado anterior, pueden ser definidos en cualquier sección de declaracion de variables (global, private...).
...
[Sección de declaración de variables]
// Punteros para los paquetes UDP
_UDPpacket pointer p; // Puntero 'p' de tipo _UDPpacket
_UDPpacket pointer q; // Puntero 'q' de tipo _UDPpacket
...
Al ser punteros de tipo _UDPpacket disponemos de todos los campos de dicho tipo, aunque los que usaremos de forma general en este tutorial serán básicamente los correspondientes a la información (byte pointer data), longitud (int len) y dirección (_IPaddress address).
...
p.len=50; // Fijamos la longitud a 50 bytes del paquete UDP 'p'
...
La longitud a fijar en cada paquete dependerá de la información que estos vayan a albergar, ya que si por ejemplo el puntero 'p' almacena un array de muy pocas posiciones de tipo byte y el puntero 'q' almacena un array bidimensional de tipo entero, entonces la longitud de 'q' deberá ser mucho mayor que la de 'p'. Normalmente deberemos fijar una longitud que permita que todos los valores que almacene el paquete puedan contener los valores posibles del juego, cuanto mayor por tanto sea la longitud mayor rango de valores podrá almacenar.
Según se vaya programando y probando pueda ser que veamos que algunos valores del paquete UDP llegan en la misma magnitud que la que maneja el emisor del paquete pero otros no y por tanto no se visualizen correctamente en el receptor, con lo cual esto nos indicará que la longitud del mismo ha de ser mucho mayor. Es recomendable que tanto el emisor y receptor manejen ambos paquetes con la misma logintud al igual que intentar optimizar el tamaño de los paquetes ideando fórmulas que nos permitan comprimir los valores para evitar grandes longitudes, por ejemplo si en un juego la variable 'angle' (mide los ángulos en miligrados) varía de 1000 en 1000, a la hora de 'meterlo' en el paquete podemos dividirlo entre 1000 y cuando llegue al receptor, dicho valor comprimido en el paquete recibido lo multiplicamos por 1000 para volver a su original y mostrarlo adecuadamente.
5. Inicio y apagado de la librería
Antes de todo deberemos tener en nuestro directorio de trabajo la librería (net.dll y SDL_net.dll en Windows) y en el código de Fenix haber incluido: ' import "net" '. Para utilizar las funciones de red de la librería debemos iniciarla mediante:
...
net_init(); // Inicializamos funciones de red
...
Normalmente antes de iniciar la librería (y al cerrar o finalizar el programa también) recomiendo cerrar todos los sockets posiblemente utilizados por el programa en una ejecución anterior (para evitar futuros problemas y/o cuelges) usando la siguiente función:
...
net_udp_close(i); // Cerramos el posible socket UDP abierto, donde 'i' corresponde a un determinado socket
...
Cuando vaya a salirse del juego o finalizarse apagaremos la librería mediante:
...
net_quit(); // Finalizamos las funciones de red
...
Es muy útil también antes de utilizar la anterior instrucción liberar de memoria los paquetes UDP utilizados, para ello bastará usar lo siguiente:
...
net_udp_freepacket(i); // Vaciamos y liberamos memoria del paquete UDP 'i'
...
6. Determinar y asignar paquetes UDP a las variables antes declaradas
Para establecer conexiones y/o realizar envios y recepciones de datos hemos de ver antes como determinar y asignar los distintos paquetes UDP que utlizemos para ello.
El número de paquetes que determinaremos dependerá por lo general del número posible de jugadores de la partida, también debemos tener en cuenta la longitud que poseerá cada uno de estos. Para realizar esto bastará utilizar la función siguiente:
...
net_udp_allocpacket(i,tamaño); // Determinamos un paquete UDP 'i' de longitud 'tamaño'
...
Una vez tengamos determinados todos los paquetes a utilizar, para poder acceder a sus campos y manejarlos debemos asignarles a cada uno una de las variables antes declaradas, una nota muy importante es que no es necesario declarar tantas variables como paquetes vayamos a usar o hayamos determinado, con unas cuantas variables (punteros de tipo _UDPpacket) nos bastará. Para poder por tanto manejar dichos paquetes tendremos que usar la siguiente igualdad 'consiguiendo' el paquete que queramos manipular:
...
p=net_udp_getpacket(i); // Donde 'p' es una variable (puntero de tipo _UDPpacket) antes declarada e 'i' es un paquete UDP que ha sido determinado
...
7. Estableciendo conexiones (servidor)
Iniciando el servidor deberemos poner el mismo en 'modo escucha' o abrir un socket, canal y puerto para poder recibir paquetes que lleguen a su dirección IP o Host, de tal modo que podamos saber que otras máquinas (clientes) intentan 'conectarse', para ello bastará con lo siguiente:
...
net_udp_open(0,puerto_escucha); // Abrimos el socket
0 para escucha en el puerto 'puerto_escucha'
net_udp_bind(0,1,direccion_local,puerto_escucha); // Asignamos la dirección
'direccion_local' al socket 0 en el canal 1 con el puerto 'puerto_escucha'
...
Analizemos lo anterior. La primera función solamente abre el socket 0 en un determinado puerto (puerto_escucha). La segunda función asigna, como se indica, la dirección (direccion_local) al socket 0 en el canal 1 (ya dijimos que ibamos a usar siempre el primer canal, que es el 1) con el puerto (puerto_escucha). Bien, en esta segunda función en 'direccion_local' deberemos introducir: "127.0.0.1" o "localhost" y el nº entero usado en 'puerto_escucha' que en ambas funciones ha de ser el mismo debido a que estamos abriendo un socket en 'modo escucha'. El valor de 'puerto_escucha' debería variar entre 0 y 65536, eligiendo el que se desee pero siempre usando el mismo en los clientes que quieran conectarse, excluyendo el 0 puesto que lo vamos a usar para establecer sockets para envio a otras máquinas.
Bien, ahora deberemos establecer un algoritmo (cuya localización en el 'proceso servidor' del juego variará según como se desee hacer) que permita establecer 'modos de envio' o inicializar otros sockets para enviar información posteriormente desde el servidor a los clientes que se están conectando. Añadamos a lo anterior este bloque de código:
...
p=net_udp_getpacket(i); // Donde 'p' es una variable (puntero de tipo _UDPpacket) antes declarado y el paquete UDP 'i' ya ha sido determinado
// Código para aceptar conexión de jugadores (clientes)
[Aquí habrá una condición que si
se cumple permitará la conexión de algún jugador/cliente]
[Aquí habrá una condición que sabrá si llega el paquete UDP 'i' al socket 0 del servidor, si es así necesitará saber si la información que contiene es la de una petición de un cliente para conectarse]
// Hay una petición para establecer una nueva conexión con un determinado cliente
net_udp_open(conectados+1,0); // Abrimos el socket 'conectados+1' en el puerto 0 para poder enviar datos a ese cliente
net_udp_bind2(conectados+1,1,p.address.host,puerto_clientes+conectados); // Asignamos la dirección 'p.address.host' al socket 'conectados+1' en el canal 1 con el puerto 'puerto_clientes+conectados'
conectados++; // Incrementamos el número de clientes conectados[Sentencia/s que informa (envía) al cliente de que ha conectado y su número de jugador]
[Fin de la condición]
[Fin de la condición]
...
Al igual que antes, pasemos a comentar algo más en profundidad lo anterior. Primero asignamos un paquete UDP 'i' a una variable (de tipo _UDPpacket) ya declarada, y por medio de un par de condiciones, de esta variable y del uso de funciones y comandos para recibir datos (lo veremos en un apartado posterior), y suponiendo el caso en el que se cumplan dichas condiciones, procederemos a establecer un 'modo de envío' con el cliente que solicita conectarse. La variable 'conectados' como se puede suponer lleva el control de todos los jugadores ya unidos a la partida, gracías a ella abrimos un socket no utilizado anteriormente en el puerto 0 (para establecer conexiones 'de envío' usaremos siempre el puerto 0). La función 'net_udp_bind2' al igual que la de mismo nombre (sin el 2) antes usada, asigna la dirección del cliente ('p.address.host' que es de tipo entero y no un string) al socket que le corresponde al cliente en el canal 1 con el puerto que también le corresponderá al cliente (donde 'puertos_clientes' puede ser por ejemplo el siguiente número del puerto que usa el servidor para 'escuchar'). En la condición donde vemos si llega o no el paquete UDP 'i' interviene la variable puntero p (de tipo _UDPpacket), donde después como se ha dicho usaremos el campo 'address.host' que incluye la dirección IP o Host del cliente que intenta conectarse, para establecer un 'vínculo' para enviarle información. Con esto incremetamos la cantidad de jugadores conectados y mediante algunas funciones enviamos (más adelante veremos como enviar paquetes UDP) al cliente que se ha conectado y su nº de jugador.
Y con esto tendremos hecho la parte más importante para que el servidor pueda manejar las conexiones de forma básica, ya que se habrán de añadir otros algoritmos para cerrar sockets de jugadores que hayan desconectado, perdido la conexión, etc... pero que pueden ser realizados usando un método similar a éste.
A continuación observaremos de una forma similar como establece las conexiones un cliente.
8. Estableciendo conexiones (cliente)
Si en el anterior apartado el servidor comenzaba abriendo un 'modo de escucha', aquí deberemos establecer primero un 'modo de envío' para transmitir al servidor un paquete UDP que será una petición para conectarnos a su partida, el siguiente código nos servirá para tal propósito:
...
net_udp_open(0,0); // Abrimos el socket 0 en el puerto
0 para poder enviar una petición al servidor para conectarnos
net_udp_bind(0,1,direccion_servidor,puerto_servidor); // Asignamos la dirección
del servidor al socket 0 en el canal 1 con el puerto del servidor (puerto_servidor)
...
Con lo anterior todavía no habríamos enviado un paquete UDP con dicha petición para entrar en la partida, por eso necesitamos 'preparar' un paquete y enviarlo a través del 'modo de envío' que hemos establecido pero una vez que lo enviemos y suponiendo que seamos aceptados por el servidor, éste nos enviará la confirmación de que hemos sido aceptados pero no sabremos a que socket y puerto (el canal siempre es 1) mandará dicho paquete, con lo que deberemos de establecer todos los 'modos de escucha' posibles a los que el servidor nos puede mandar la información. Una vez que 'consigamos' ese paquete podremos establecer un único 'modo de escucha' ya que donde es recibido dicho paquete (el socket y el puerto, el canal siempre es el 1) llegarán los sucesivos paquetes UDP. El bloque de código de todo lo dicho hasta ahora y unido a lo anterior será el siguiente:
...
p=net_udp_getpacket(i); // Donde 'p' es una variable (puntero de tipo _UDPpacket) antes declarado y el paquete UDP 'i' ya ha sido determinado
[Sentencia/s que envía al servidor la petición (paquete UDP) de unirnos a la partida]
// Código que prepara todos los posibles 'modos de escucha' para recibir el paquete UDP de confirmación por parte del servidor
[Aquí habrá un bucle que establece todos los posibles 'modos de escucha' con el siguiente código dentro del bucle]
net_udp_open(socket,puerto_cliente); // Abrimos un socket
'socket' para escucha en el puerto 'puerto_cliente'
net_udp_bind(socket,1,direccion_local,puerto_cliente); // Asignamos la dirección
'direccion_local' al socket 'socket' en el canal 1 con el puerto 'puerto_cliente'
[Fin del bucle]
...
[Aquí habrá un bucle y una condición que sabrá si llega un paquete UDP 'i', que confirmará la conexión por parte del servidor, a uno de los 'modos de escucha' antes establecidos]
// Hemos recibido un paquete que nos confirma que hemos conectado
[Aquí cerraremos por medio de un bucle todos los 'modos de escucha' antes realizados]
net_udp_close(t);// Cerramos el socket 't', donde 't' es una variable de tipo entero cualquiera que usa el bucle
[Fin del bucle]
// Ahora procedemos a establecer el 'modo de escucha' definitivo que sabemos cual es gracias a las variables que controlan el bucle principal
net_udp_open(s,puerto_cliente+s-1); // Abrimos el socket 's' para escucha en el puerto 'puerto_cliente+s-1'
net_udp_bind(s,1,direccion_local,puerto_cliente+s-1); // Asignamos la dirección 'direccion_local' al socket 's' en el canal 1 con el puerto 'puerto_cliente+s-1'
[Fin del bucle y condición]
...
Aclaremos algunos puntos del anterior bloque de código no citados antes. Fijemosnos en las variables que intervienen en el primer bucle, concretamente en 'socket' y 'puerto_cliente'. Ambas variables irán siendo incrementadas según 'avanze' el bucle, en la primera ejecución del mismo la variable 'socket' empezará en 1 hasta el máximo nº de sockets posibles a los que el servidor pueda enviar el paquete; la segunda por su parte comenzará en el siguiente número a 'puerto_servidor' y llegará hasta el nº máximo de máquinas que puedan intervenir. En el siguiente bucle y condición, tenemos por su parte en un bucle interior la variable 't' que irá incrementándose también hasta el nº de 'modos de escucha' que hayamos establecido antes. Por último tenemos la variable 's' y 'puerto_cliente+s-1', la primera determinará cual ha sido el socket al cual el servidor envió el paquete al cliente que solicitó entrar en la partida y formaba parte de uno de los 'modos de escucha' y que nos permite en ese instante crear el 'modo de escucha' al que le llegarán siempre los paquetes que le envíe el servidor, y por el otro lado 'puerto_cliente+s-1' será el puerto por el cual y de igual forma que antes el servidor envió el paquete UDP.
No se ha dicho que 'tipo' de información deben incluir los paquetes para que se sepa que son peticiones de entrada por parte de un cliente o mensajes de que se ha aceptado la conexión por parte de un servidor, puesto que lo que incluyan forma parte del diseño del juego y es elección de cada uno, aunque como 'norma' dicha información deberá ser totalmente distinguible de aquella que forme parte del juego en sí, por ejemplo del tipo: vida de un determinado jugador, posición en pantalla de éste, etc...
Hasta aquí ya tenemos una forma básica para que un cliente puede conectarse y entrar a una partida de un servidor, otros métodos que impliquen establecer conexiones para el cliente pueden ser similares al explicado.
Como penúltimo punto veamos las funciones a usar para poder enviar y recibir paquetes UDP.
9. Enviando y recibiendo paquetes UDP
Cuando enviamos a un cierto usuario (servidor o cliente) un paquete UDP al cual podemos acceder por medio de una variable puntero (de tipo _UDPpacket), la información que éste albergará la debemos de tener almacenada previamente en otra variable (que no tiene porque ser un puntero) que dicho usuario maneje, pero a su vez, puede que dicha variable obtuviera su información de otra variable puntero (de tipo _UDPpacket) al recibir un paquete UDP de otro usuario.
Es esta la idea que manipularemos en el envío y recibo de paquetes UDP y como en ella se indica deberemos de tener una variable 'general' que guardará la información del usuario relevante para la dinámica del juego. Poniendo un ejemplo como el siguiente:
...
// Estructura (variable) para el envío/recepción de datos
struct InformacionUsuario;
int posicion_x;
int posicion_y;
int grafico;
string nombre;
// ...
end;
...
El campo de la variable puntero (de tipo _UDPpacket) donde está alojada la información del paquete UDP enviado o recibido es data (de tipo puntero byte). Ahora veamos el bloque de código para enviar un paquete UDP:
...
p.len=lonbyte;// Longitud 'lonbyte' del paquete de datos
a enviar
memcopy(p.data,&InformacionUsuario,lonbyte);// Copiamos la longitud 'lonbyte'
de bytes de memoria desde la posición de la variable 'InformacionUsuario'
a la posición a la que hace referencia el puntero 'p' (de tipo _UDPpacket)
net_udp_send(socket,1,'i');// Enviamos el paquete 'i' al socket 'socket' y
al canal 1
...
La secuencia para enviar un paquete UDP consiste, como se puede observar, en utilizar una variable (puntero de tipo _UDPpacket) en este caso 'p', la cual ya ha sido asignada a un paquete UDP 'i' antes determinado, luego hay que 'definir' la longitud 'lonbyte' de dicho paquete y antes de enviarlo al socket 'socket' y al canal 1 debemos de copiar la información de dicho usuario desde la variable 'general' ('InformacionUsuario') al campo data de la variable 'p' mediante el uso de la función 'memcopy'. Los datos que contiene la variable 'general', o bien han sido 'rellenados' por un usuario (normalmente será el cliente el que haga esto) o copiados desde otra variable 'p' a dicha variable 'general' al haber sido recibido un paquete UDP de un usuario (será muy común de un servidor esta acción).
Para poder realizar la última acción comentada deberemos de recibir un paquete de una manera como la siguiente:
...
If (net_udp_recv(socket,i)>0) // Condición
que detecta si se recibe un paquete 'i' en el socket 'socket'
memcopy(&InformacionUsuario,p.data,lonbyte);// Copiamos la longitud 'lonbyte'
de bytes de memoria desde la posición a la que hace referencia el puntero
'p' (de tipo _UDPpacket) a la posición de la variable 'InformacionUsuario'
// ...
End;
...
Con esto por tanto y siempre que llegue o sea recibido un paquete UDP 'i' en el socket 'socket' tendremos disponible toda la información del mismo en la variable 'InformacionUsuario' para poder ser utilizada. Al igual que antes, la variable 'p' (puntero de tipo _UDPpacket) habrá sido antes asignada a un paquete 'i' ya determinado.
Para acabar con este punto y el tutorial, y suponiendo ya establecidas las conexiones entre un servidor y algunos clientes y que el servidor también pueda actuar como un jugador más, definamos una posible dinámica de envío y recepción de paquetes UDP en el desarrollo de una partida.
En principio el servidor 'rellenará' en un paquete toda su información (necesaria para los demás) y la enviará. Cuando la haya enviado a todos los clientes, entonces enviará un paquete al primer cliente informándole para que le envíe su información. Cuando llegue al servidor dicha información, el servidor procederá a enviar la información de ese cliente al resto de jugadores (menos al que envió dicha información), una vez hecho esto, enviará un paquete a otro cliente como al primero indicándole que le envie su información, y esto se repetirá hasta que todos los clientes hayan 'compartido' sus datos, y por tanto de nuevo el servidor actualizará y 'rellenará' su información para enviarla y repetir el proceso. Según se vaya obteniendo información de cada jugador, cada usuario podrá manipularla, ya sea mostrándola en pantalla o realizando otras tareas con ella.
Como ya se dijo anteriormente el mejor método de flujo de datos dependerá de como sea el diseño del juego, siendo el mejor el que permita mayor rapidez y fluidez en la transmisión de datos.
Si habéis leído todos los puntos del tutorial puede que queden ciertas 'lagunas' por resolver o ciertos aspectos que debierán ser matizados aún más, pero esto es debido a la gran complejidad que plantea el diseño y creación de un juego o proyecto en red, que hace que sea muy dificil aunar los diferentes conceptos e ideas que plantea.
De todos modos el fin de este tutorial, como dije en la introducción, no es más que intentar 'guiar' en el uso de algunas de las funciones UDP de la librería de una forma práctica. A todo aquel que lo lea espero que le ayude o sirva de algo.