You are on page 1of 28

Cmo crear un juego Online (TCP) en Game Maker

Studio?

Por: Black_Cat usuario de ComunidadGM.org


Espero que este tutorial les sea de mucha ayuda. Decid lanzar una version 2
porque la anterior era muy genrica sobre qu eventos se deban utilizar para
manejarlo.
Esta vez me tom el doble de trabajo en hacerlo, porque quiero que sea lo ms
claro posible.
Esto de hacer un tutorial sobre juegos Online viene precisamente de que
yo quera hacer uno y no encontraba material en espaol o bien documentado.
As espero que lo aprovechen ahora que se los traigo :D
Aclaro que es bastante laborioso hacer un juego de este tipo, puesto que hay
que manejar el envo y recepcin de datos, en paralelo con las acciones que
requiera nuestro juego multijugador. Dije laborioso pero no imposible :)
Nota: Este tutorial se enfoca especficamente para juegos que utilicen el
protocolo TCP.
Antes de comenzar:
Es sumamente importante que conozcas algunos conceptos antes de
comenzar:
Que es TCP?
http://juegosluzplateada.blogspot.com.ar/2013/03/3-que-significa-tcp.html
Conceptos basicos: Buffers, sockets, puertos. (Ignoren la parte de la 39DLL)
.
http://juegosluzplateada.blogspot.com.ar/2013/03/1-presentacion-39dll-y-conceptos-basico.html
Bien, antes que nada, les aconsejo que antes de empezar, lean los apartados
en el manual de Game Maker que comprenden (o como mucho la lista de
funciones) :

DS LISTS
DS MAPS
BUFFERS

Aunque la iremos viendo de apoco, as no hay problema.


Hagamos un vistazo rpido de lo que tratan:

Las DS LIST, las listas, son bastantes tiles a la hora de almacenar


informacin. Como has de suponer en forma de lista, por ejemplo como

cuando ves una receta: 200 de manteca, 2 huevos , 1 taza de


leche...etc. Si, as, de esa forma es cmo se almacena.
Este tipo de estructura nos facilitar guardarlos en un orden, por
ejemplo, guardar los nombres de los jugadores que vayan entrando en
nuestra partida.
Por comodidad usaremos las funciones que ya vienen integradas en Game
Maker Studio para listas, en vez, de usar arrays (arreglos o vectores).
Los DS MAPS, los mapas, que tambin se les suele llamar diccionarios.
Este es un tipo de dato que guarda los datos como un diccionario. Por ejemplo:
Gato: animal peludo que dice Miau
Aqu Gato es la entrada y animal peludo que dice miau es el dato que
contiene la entrada Gato. Este tipo de dato es muy til e
imprescindible. Luego veremos porqu.
Buffers, es un concepto tambin esencial para enviar paquetes entre
nuestro servidor y los clientes conectados a este.
Por ejemplo si necesitamos enviar un puntaje o algunas coordenadas, es
necesario que se guarden y se enven. En la vida real equivaldra a mandar un
paquete a un familiar, colocas la direccin a donde va a enviarse, metes en el
paquete un par de galletas de la abuela y se lo envas. Exactamente as es
como lo implementaremos.
Creando el servidor
Para poder comunicarnos con otros clientes es necesario un paso a travs de
un servidor:
La informacin, o datos, no pueden ser transferidos de cliente a cliente. Es decir, primero
deben pasar a travs del Servidor para luego llegar a otro cliente. Observa entonces, que de
otra manera, habran lineas que cruzasen entre clientes .

http://juegosluzplateada.blogspot.com.ar/2013/03/3-que-significa-tcp.html
Es importante que hayas ledo las entradas del blog al principio del post, que
mencionan conceptos bsicos! Si no las has ledo, te doy otra oportunidad.
Bien, vamos a lo que en verdad nos interesa, para que toda la magia surja,
obviamente necesitamos al servidor, para ello contamos con la funcion:
network_create_server(type, port, max_client);
Esta funcin es utilizada para crear un nuevo servidor.
La funcion devolver un Identificador Unico (de ahora en mas ID), si se pudo
crear correctamente el servidor. Es importante que la guardemos en una
variable para luego usarla como referencia en otras funciones.
Ahora, en el caso de que no se pueda crear o haya ocurrido algo anormal
entonces devolver un valor menor a 0.
Parametros:
type: es el tipo de protocolo que vamos a usar para transferir datos, aqu

pueden ir 3 constantes incluidas ya en Game Maker:


network_socket_tcp : crea un socket web usando TCP
network_socket_udp: crea un socket web usando UDP
network_socket_bluethoot: crea un socket tipo bluethoot(Actualmente no
soportado).
Port: numero de puerto donde alojar al servidor.
Max_client: numero de clientes mximos permitidos en el juego.
Entonces, si queremos iniciar nuestro servidor, colocamos la siguiente
instruccin (Recuerde que el tutorial est enfocado unicamente al uso de TCP):
global.servidor = network_create_server(network_socket_tcp, 48050, 10);
Aqui decimos que queremos iniciar un servidor con protocolo TCP, que estar
alojado en el puerto numero 48050 y podr recibir un mximo de 10
conexiones simultaneas, o que es lo mismo, 10 jugadores.
Esto bien podra colocarse en algn botn. Para este ejemplo usamos un
evento create en un objeto limpio al que llamamos obj_servidor.

As almacenamos el resultado en nuestra variable global.servidor,


recordemos que dar un valor menor que 0 si ocurri un error al crear o un
numero identificador si sali todo bien.
Sera sensato verificar si hubo algn error o no.

NOTA: es importante saber que, si se crea un a conexin, sta debe


eliminarse en algun momento, sino quedar abierta y los hackers rusos de
Anonimau5 podran robar las fotos de nuestra gorda tia Jimenita.
Para ello disponemos de la funcion:
network_destroy(servidor_que_queremos_destruir);
donde servidor_que_queremos_destruir es el ID del servidor que
queremos eliminar.
Entonces agregamos, por ejemplo, al finalizar el juego, esta instruccin,
debidamente adaptada:

-La disponibilidad de nuestro puerto 48050


A la hora de crear el servidor puede que el puerto que quisieramos utilizar este
ocupado, por lo que a la primera no se iniciara, y no importase cuantas veces
abramos el servidor, si el puerto est ocupado, no se iniciar ni por lloriqueos
ni por berrinches... entonces vamos a tener que usar otro.
Esto es sencillo porque podemos pedir todos los que quieramos. Somos unos
nios malcriados y si no nos ceden el puerto que queremos pedimos otro
diferente, el caso es ganar nuestro caramelo.

Para pedir un puerto libre podemos usar un ciclo condicionado:


var puerto = 1024; // porque 1024 y no 48050? Lee las entradas al blog donde
explica los "puertos registrados"... ultima oportunidad si no leiste el blog
global.servidor = network_create_server(network_socket_tcp, puerto, 10);
while(global.servidor <0) //mientras no se pueda crear un servidor
{
puerto++; //pedimos otro puerto para ver si esta libre
global.servidor = network_create_server(network_socket_tcp, puerto,
10); //intentamos conectar
}
As tarde o temprano vamos a obtener un puerto libre.
Ya con esto podemos comenzar a conectar clientes. :)
El Cliente
Ahora llega el turno de los jugadores, o los clientes. Para crear uno contamos
con la funcin:
network_create_socket(type);
Esta funcin es utilizada para crear un nuevo cliente, que se comunicar con el
servidor para enviar y recibir paquetes. Debemos asignar el tipo de cliente que
queremos crear y esta funcin nos devolver un ID unico si se pudo crear el
cliente o un numero menor a 0 si hubo algun error.
Argumentos:
Type: es el tipo de cliente a crear, y puede ser alguna de estas constantes que
ya trae Game Maker:

network_socket_tcp : crea un socket web usando TCP


network_socket_udp: crea un socket web usando UDP
network_socket_bluethoot: crea un socket tipo bluethoot(Actualmente no
soportado).

Entonces si queremos crear un cliente usamos:


global.cliente = network_create_socket(network_socket_tcp);
En nuestro ejemplo creamos un objeto limpio y en el evento create colocamos
dicha instruccin.

Entonces verificamos si se pudo crear el cliente. Ante cualquier error, cerramos


el juego.
Recordemos que debemos destruir cualquier conexin que hagamos, para no
dejar puerto abiertos ni nada raro. Nadie quiere olvidar puertas abiertas en su
casa, o si?
Para ello usamos networ_destroy(conexion_que_queremos_destruir);

Bien, bien, bien :)


Ya hasta este punto tenemos un servidor y cuanto menos un cliente. Ahora,
hay que conectarlos para comenzar a pasar datos y recibirlos.
Entonces, conectaremos al cliente con el servidor y nunca al revs.
Para ello contamos con la siguiente funcion:
network_connect(socket, url, port);
Agumentos:
socket: la ID del socket que va a conectarse.
url: direccion IP o URL a donde queremos conectarnos. (Deben introducirse en
forma de cadena. Por ejemplo: 128.66.122.21 o comunidadgm.org [notese

las comillas necesarias] o bien se puede convertir a cadena con la funcion


string(...))
port: puerto donde esta alojado el servidor en la direccion url.
Como ya habamos creado nuestro cliente asi que en socket vamos a utilizarlo
con global.cliente.
La IP debe ser la direccin a donde queremos conectarnos, la IP de nuestro
amigo que hizo la partida. Y port el numero de puerto donde est alojada, el
cual est especificado en el momento en el que se cre el servidor.
Recordemos que se hizo esto con 48050.
Luego si no se pudo conectar, devuelve un valor menor que 0.
Nota: puedes utilizar 127.0.0.1 para conectar con varios clientes dentro del
mismo dispositivo.
Entonces, estoy en mi computadora, abro el servidor, que ya sabemos como
hacerlo, y deseo para probar si conecta, entonces puedo utilizar 127.0.0.1.
Nos debera quedar un cdigo como el siguiente:
network_connect(global.cliente, "127.0.0.1", 48050);
Nosotros vamos a colocarlo en el evento create de nuestro obj_cliente,
mientras verificamos el caso de que no podamos conectarnos :

Todo esto debe bastar para crear un servidor, un cliente y conectarlo. :D


Pero ahora vienen temas mas importantes si quieres que todo marche a la
perfeccin, as que si ests cansado, toma un vaso de tu bebida favorita, que
la cosa ahora se pone ms densa...

Eventos asncronos
- What the... hell? :S
- Tranquilo Brother, deja que te explique...
Los eventos asncronos son algunos de los eventos que pueden ser gatillados
dentro de una instancia y que pueden contener cdigos y acciones.
Gatillar se considera, lanzar o activar un evento, cuando ocurre algo en
especial o se cumplen determinadas condiciones o cuando se llama a una
funcin asncrona, por ejemplo: get_string_async(...).
Estos famosos eventos asncronos estn disponibles como eventos dentro de
un objeto. Vamos, donde estan create, step, alarm...

Nosotros vamos a utilizar 2 de esos, a saber:

Dialog
Networking

Es muuuuy importante saber que dentro de estos eventos se crea un ds_map


llamado async_load y es de uso exclusivo dentro de dichos eventos. Para

manipularlos es imprescindible saber usar las ds_maps, pero sino, vamos a


explicarte algunas funciones tiles.
Tambin es importante saber que como cada evento asncrono tiene su
async_load estos, tienen entradas diferentes dependiendo a que evento
pertenecen.
Ejemplo corto de como usar un evento asncrono.(OPCIONAL).
Rpidamente, creamos un nuevo proyecto en blanco, limpio. Creamos un
objeto sin nombre, y vamos al evento create.
All vamos a ejecutar una funcin asncrona para que veamos la funcionalidad
de esto, para ello usaremos get_string_async(...)
get_string_async(str,def);
Esta funcin nos pedir una cadena o un texto y devolver una respuesta en
forma de numero.
str: es el cartel que se mostrar como mensaje
def: es la cadena o el texto por defecto que estar puesto al mostrar el cartel.
As: la funcin muestra un cartel con el texto str con la respuesta def por
defecto.
Nos debera quedar algo as:

Esta funcin dispara el evento asncrono dialog. Y como sabemos, all, dentro
de ese evento se crea un ds_map, llamado async_load que tendr entradas
especiales.
En nuestro caso, es un cartel que pide nuestro nombre.
Entonces, vamos al evento asncrono dialog y comencemos:
Primero, nuestro mapa async_load, tiene 3 diferentes entradas (o palabras):

id: es el valor numrico que fue devuelto a la funcin que ejecut el


evento. En nuestro caso debera ser el mismo valor que contiene la
variable respuesta.
status: contiene un solo valor a la vez, de dos posibles. True si se ha
presionado el boton OK del cartel. O False si se ha presionado
Cancel. Se debe tener en cuenta que no todos las plataformas proveen
una opcin de cancelar.
result: contiene la cadena ingresada. O si no se ha ingresado nada,
entonces una cadena vacia: .

Bien, ya pedimos nuestro nombre en el evento create. Luego de que


ingresemos un texto, la funcin gatillar el evento asncrono dialog.
All se crea, especialmente nuestro mapa async_load, listo para operar.
As, vamos a nuestro evento. All vamos a pedir el valor de result que debera
ser una cadena ingresada por el usuario. Para pedir este valor, usamos la
funcion:
ds_map_find_value(map, key);
Esta funcin devuelve el valor contenido en key del mapa map. Recuerda que
al inicio del tutorial, dijimos que los mapas tambin se conocen como
diccionarios. Vuelvamos al ejemplo:
Diccionario de animales
Gato: animal que dice Miau
Si nosotros quisiramos obtener el valor de la key gato, de nuestro
mapa (o diccionario) llamado diccionario de animales, deberamos hacer:
var definicion;
definicion = ds_map_find_value(DiccionarioDeAnimales, gato);
guardamos el valor devuelto, y nuestra variable definicion debera contener
una cadena que dice: animal que dice miau.
Entonces, si quisieramos guardar y leer la cadena que ingresa el usuario a
travs de la funcion get_string_async(...) deberamos leer la key result.
Luego, verificamos por algn valor y si es el que esperbamos, operamos de
una u otra manera.
Dejo un ejemplo simple:

Luego, para leer los dems apartados como id o status se opera de la


misma manera. :)
--------Fin del corto tutorial opcional sobre como usar eventos asncronos-----

Bien, bien continuemos... hasta ahora solo podemos crear un servidor y


conectarnos remotamente. Este ejemplo no nos sirve si necesitamos, como
cliente, meter la IP de nuestro amigo que vive en Espaa, o que vive en Chile.
Es decir que colocar a secas 127.0.0.1 y compilar un cliente (o varios con
diferentes direcciones IP) no nos sirve.
Lo mismo para el puerto, puede que 48050 no est disponible siempre, y
cambie para el servidor, entonces compilarlo con dicho puerto no nos
conectara nunca.
Vamos a verlo por parte:
Para pedir una direccin vamos a usar la siguiente funcin:
get_string_async(str,def);
Esta funcin nos pedir una cadena o un texto y devolver una respuesta en
forma de numero.
str: es el cartel que se mostrar como mensaje
def: es la cadena o el texto por defecto que estar puesto al mostrar el cartel.

Recordemos que la direccin para network_connect(...) debe ser una cadena.


Entonces operamos de la siguiente manera, lo nico que debe estar en nuestro
evento create del objeto cliente es la creacion del socket cliente:
global.cliente = network_create_socket(network_socket_tcp); //Creamos al
cliente
if (global.cliente < 0)
game_end(); //Si no se puede crear el cliente, cerramos el juego
Adicionalmente vamos a necesitar algunas variables para almacenar la IP que
ingresar el usuario como as tambin el numero de puerto, adems de una
variable para verificar si la conexin se realiz con xito:
conexion = -1; // Variable para verificar si se conect exitosamente
IP_A_CONECTARSE = ""; // ip a donde queremos conectarnos
PUERTO_DEL_SERVIDOR = 0; // numero de puerto donde esta el servidor
rta_cadena = -1; //variable adicional para saber el motivo de gatillado
rta_entero = -1; // variable adicional para saber el motivo de gatillado
global.cliente = network_create_socket(network_socket_tcp); //Creamos al
cliente
if (global.cliente < 0)
game_end(); //Si no se puede crear el cliente, cerramos el juego
Para una mayor comodidad vamos a asignarle a nuestro obj_cliente un sprite
con un cartelito de cliente, como simulando un boton:

Luego agregamos la parte que pedimos una direccin para poder conectarnos.
Esto lo haremos desde un evento de mouse left key pressed, para que cada
vez que presionemos en nuestro boton obj_cliente, nos pida una IP, y un
numero de puerto.

Aqui pedimos una IP y guardamos el resultado numerico en rta_cadena


para saber el motivo por el cul se ejecuta nuestro evento asncrono dialog.
Guardamos los valores en rta_cadena y rta_numero porque ambas
funciones gatillan el mismo evento y es necesario csaber uando ocurre cada
uno. Si es por pedir una cadena o si es por pedir un numero.
As al hacer click se pide una cadena con get_string_async(...), e
inmediatamente esta funcin llama al evento asncrono dialog. All debemos
operar el resultado de async_load.
Entonces para saber que vinimos desde la funcion string_async, le
preguntamos a async_load si esto es asi, de la siguiente manera:
(Estamos ahora dentro del evento Dialog).
var llamada;
llamada = ds_map_find_value(async_load,"id");
if (llamada == rta_cadena)
.
Aqu obtenemos el ID de la funcin que llam a dialog y preguntamos si esa
funcion es la que pide la IP (o la cadena). Si es as, entonces leemos la IP que
ingres el usuario.
if (llamada == rta_cadena)
IP_A_CONECTARSE = ds_map_find_value(async_load, "result");
Eso bastara para obtener la IP...
Luego, el programa vuelve donde se hizo la llamada en el evento create y
ejecuta la instruccin get_integer_async(...) que vuelve a llamar al evento
dialog, entonces debemos hacer una distincin entre las llamadas.

Para ello, preguntamos si llamada es igual a la id de la funcin que nos trajo.


Es decir si llamada es igual al valor de get_integer_async(...).Entonces
preguntamos, dentro del evento dialog:
if (llamada == rta_entero) // Si llegamos por pedir un entero
Si llegamos por dicha funcin entonces deberamos almacenar el numero de
puerto:
if (llamada == rta_entero)
PUERTO_DEL_SERVIDOR = ds_map_find_value(async_load, "value");
ahora nuestro evento dialog debera lucir as:

es importante notar que cuando pedimos el valor ingresado por el


usuario en get_integer_async(...) se usa VALUE en vez de RESULT
como lo haramos con get_string_async(...).
Ya con eso, el programa, luego de ejecutar el evento asncrono dialog, vuelve
al evento create donde ejecuta network_connect(...) con los nuevos
parametros, si no se puede conectar entonces volver a pedir una IP y un
puerto.
Luego en caso de que nos conectemos nos moveremos al room de juego, para
ello solo bastara verificar en un evento step si la conexin se realiz con
exito:
if (conexin >= 0)
room_goto(room_juego);
Y eso es absolutamente todo lo que necesitas para conectar un cliente a un

servidor... largo no? XD


Ahora vamos a acomodarlo todo un poco...
Primero que nada, crearemos un objeto que llamaremos obj_boton_server. Y le
asignaremos un sprite de boton para el server.
Este nuevo objeto, obj_boton_server, deber llevar un evento de
mouse_key_pressed que al presionarlo, nos llevar a un room al que
crearemos y llamaremos room_servidor, donde tendr nicamente al
obj_server que ya habamos programado.
Luego, creamos un nuevo room llamado room_menu para poner los dos
botones creados:

obj_boton_server
obj_cliente

que ambos tienen un sprite para hacerlos simular un boton.


El juego debera verse de esta manera, estructuralmente
Yo eh pintado los rooms para identificarlos :

-Aceptando nuevos clientes y paquetes de datos.

Ahora veremos el segundo evento asncrono: Networking


Cada vez que el juego intercambia datos en una conexin se ejecuta el evento
asncrono Networking.
Es decir, cada vez que el servidor, por ejemplo, recibe a un cliente, o cuando se
desconecta, se ejecuta. As mismo sucede cuando se envan datos o reciben,
ya sea en un cliente o un servidor.
La manera de operar este evento es igual a la que forma en que se hizo con el
evento dialog, es decir, depuraremos para saber la razn de dicha ejecucin.
En nuestro evento dialog, tuvimos que depurar para saber si el evento se
ejecutaba por usar get_string_async(...) o si se ejecutaba por utilizar
get_integer_async(...).
En este caso debemos depurar para saber si se ejecut por recepcin de datos
o por si se ha conectado un nuevo cliente u otro motivo.
Es importante, saber que este mapa async_load (recordemos que dijimos
anteriormente, que todos los eventos asncronos contaban con uno) tiene
entradas especiales a determinadas acciones y otras comunes, entre un grupo
de acciones.
Entonces, como dijimos el evento asncrono networking se puede gatillar por
3 razones:
Se conect un cliente
Se desconect un cliente
Recibimos datos
Hay entradas o keys o palabras que son comunes para estos tres motivos de
gatillado y son:

type: esta entrada (o palabra) contendr alguno de los siguientes


valores, los cuales son constantes que trae Game maker, ya definidas.
Dichos valores reflejan el motivo por el cual se ejecuta el evento networking
y son:
network_type_conect: el evento fue gatillado por una conexin
entrante.
network_type_disconnect: el evento fue gatillado por una
desconexin.
network_type_data: el evento fue gatillado por datos entrantes.

id: el ID del socket que est recibiendo el evento.


En los casos en los que se reciben datos, id es el socket de quin envia
dichos datos, es decir, el numero de socket de quien envia el paquete de
informacion(id del cliente emisor).
ip: es la direccin IP del socket anterior (en formato de cadena).
port: es el nmero de puerto donde est alojado el cliente, dentro de la
IP anterior.

-Entradas adicionales para cada async_load dependiendo el motivo


Como dije anteriormente se agregan entradas (o keys o palabras) adicionales
dependiendo del motivo por el cual se gatilla el evento asncrono networking.
Si, se ejecuta por:

Conexin o desconexin: cuando se conecta o desconecta un cliente,


se agrega una key adicional a nuestro diccionario async_load y es:
socket: esta entrada contendr el nmero de socket que se conect o
desconect. til por ejemplo si queremos retirar un jugador de la lista
o agregarlo.

Por recepcin de datos: cuando se reciben datos se agregan dos keys


adicionales a nuestro diccionario async_load y son:
buffer: este es un identificador de buffer unico, el cual es generado
por el evento. Es un buffer dinmico, con alineacion a 1 byte y es
creado para almacenar el paquete
que enva el socket. A la final
del evento asincrono, este buffer es eliminado. (Explicaremos ms
de buffers en un momento).
size: contiene el tamao en bytes del buffer recibido.

Bien, hora de trabajar con la recepcin de clientes y datos.


Vamos al evento asncrono network, dentro de nuestro servidor(el
obj_servidor), puesto que queremos ver si se ha conectado algn cliente.
Cmo sabemos, primero debemos determinar la razn por la cual se podra
gatillar nuestro evento, para ello vamos a hacer la lectura de nuestro mapa
async_load para determinar el motivo y para ello vamos a usar una
conveniente estructura switch para ir filtrando o depurando.
Como sabemos, la entrada (o key) type contiene la razn o el motivo de la
ejecucin del evento networking, entonces sera lgico primero obtener el valor
y luego preguntar:
var MOTIVO;
MOTIVO = ds_map_find_value(async_load, "type");
switch (MOTIVO)
{
case network_type_connect:
//Se conect un cliente
break;
case network_type_disconnect:
//Se desconect un cliente
break;

case network_type_data:
//Recibimos datos
break;
}
As entonces podremos operar los datos recibidos, ms las entradas adicionales
por cada motivo, de manera conveniente para realizar algunas acciones, como
enlistar jugadores, quitarlos de una lista, reenviar datos, etc...
Ahora, vamos a suponer que se nos conecta un nuevo cliente a nuestro
servidor. Entonces, se activar el evento asncrono, se obtendr un valor y
almacenar en la variable MOTIVO, luego esta ser evaluada y entrar en el
caso de los clientes conectados, o sea, network_type_connect.
Nosotros vamos a enlistar a los jugadores que vayan llegando. Pero solo es con
fines del ejemplo, se puede hacer lo que al programador se le antoje.
Bien, para ello vamos a necesitar el uso de las ds_list funciones nativas de
Game Maker Studio para crear listas dinmicas. Para ello contamos con la
funcin:
ds_list_create();
la cual nos crea una lista dinmica y nos devuelve un ID nico que nos servir
para hacer referencia a dicha lista en caso de que la necesitemos para otras
funciones.
Convenientemente vamos a crear nuestra lista en el evento create, de la
siguiente forma:
lista_jugadores = ds_list_create();
Entonces as nos aseguramos de guardar el ID de nuestra lista en
lista_jugadores para luego poder referirnos a ella.
Ahora que tenemos una lista creada, bastara llenarla con los jugadores que
vayan entrando a la sesin. Entonces volvemos al evento asncrono donde
tenemos el switch, y dentro del case donde se verifica la conexin de un
cliente, usaramos una funcin para agregar un elemento a nuestra lista
lista_jugadores, la cual es:
ds_list_add(id_lista, valor);
Esta funcin nos permite agregar un valor a la lista, ya sea en formato de
numero o de cadena. Siempre se agrega al final.
Argumentos:

id_lista: ID de la lista en donde queremos agregar un nuevo valor.

valor: es el nuevo valor (un numero o una cadena) que queremos


agregar.

Ms concretamente, agrega a la lista id_lista el nuevo valor value al final.


Entonces dentro de nuestro caso para nuevos clientes conectados, deberamos
tener algo como esto:
case network_type_connect:
var cliente = ds_map_find_value( async_load , "socket" );
ds_list_add(lista_jugadores, cliente);
break;
Con esto decimos que, cada vez que se conecte un cliente, leeremos el socket
de dicho cliente y almacenaremos su nmero en nuestra lista lista_jugadores.
As iremos almacenando a todos los que vayan entrando a nuestra partida.
Si todo marcha bien, deberamos haber llegado a algo como esto:

Para ir vaciando la lista en caso de desconexin, deberamos hacer el mismo


proceso:
1-Obtener el socket

2-Modificar la lista
Para quitar un valor de la lista, contamos con la funcin:
ds_list_delete(id_list, pos);
con esta funcin borramos en la lista id_list el valor en la posicin pos.
Argumentos:
id: ID de la lista en donde queremos borrar un valor.
pos: posicin del valor que queremos quitar.
El nico problema que tendramos es que los jugadores se conectan en un
orden especfico, al momento de desconectarse no lo haran en el orden
inverso. Me refiero que el ltimo en entrar no sera necesariamente el primero
en salir. Por lo que no sabramos la posicin exacta de un valor en la lista.
Para lidiar con esto contamos con la siguiente funcin:
ds_list_find_index(id_list, val)
esta funcin nos devuelve la posicin de val dentro de la lista id_list. Si no
est dentro de la lista devolver -1
Argumentos:

id_list: lista en donde queremos operar


val: es el valor que buscaremos dentro de la lista

Entonces primero deberamos obtener el socket que se desconect, luego


debemos encontrar la posicin donde est dicho socket y finalmente eliminar el
socket de la lista.
case network_type_disconnect:
var SOCKET = ds_map_find_value( async_load , "socket" );
var POSICION = ds_list_find_index( lista_jugadores , SOCKET );
if (POSICION != -1)
ds_list_delete(lista_jugadores, POSICION);
break;
Ya con esto cubrimos la necesidad de tener un registro con nuestros clientes,
luego se pueden operar convenientemente si se desea. Por ejemplo dibujar una

lista con los jugadores conectados. O pedir sus nombres y tambin


almacenarlos. Todo depende lo que quiera hacer el programador.
Cubrimos tambin el hecho de que un jugador se desconecte, ya fuera por el
motivo que sea, se le cay internet, su madre le apag el modem para que
vaya a comprar las tortillas, o lo raptaron los extraterrestres. Game Maker nos
asegura que si se pierde la conexin, este enviar a nuestro servidor una seal
de desconexin.
Ahora viene la parte maaas emocionante :D
La transmisin de datos... sigue leyendo brother, que ahora ms que nada
necesitas ms esfuerzo.
- Buffers
-Qu es un buffer, se come?
-A decir verdad, no, no se comen... :(
Un buffer es un espacio de memoria que guarda datos temporalmente.
Es es mi definicin. Si quieres otra, por ah est el blog que recomend leer,
que contiene los conceptos estos que vinimos viendo desde hace rato :)
Bien, este es un tema importante e imprescindible puesto que con estos
buffers vamos a transmitir los datos, as que dar una explicacin breve.
Game Maker Studio tiene la posibilidad de crear buffers de diferentes tipos,
para luego llenarlos con informacin. Existen varios tipos, nosotros veremos
algunos:

Buffer fijo: una vez que se crea dicho buffer con un determinado
tamao, al llenarse no pueden agregarse mas datos.

Buffer dinmico: Una vez que se llena el buffer, si ha llegado a su


limite, ste se expandir hasta ocupar el espacio requerido

Buffer envolvente: una vez que se llega al limite del tamao de dicho
buffer, ste vuelve al principio para sobre escribir los datos que ya
hayan, hasta ocupar el espacio faltante.

Nosotros en este tutorial ocuparemos buffers dinamicos para evitarnos


mucho trabajo y algunos problemas.
Nota: Existe lo que se llama Alineacin de bytes, el cual no veremos y cuando
lo necesitemos, siempre usaremos el valor de 1, lo cual siempre nos funcionar
y ser suficiente.
Los buffers, como son espacios de memoria creados, los usaremos para colocar
valores y enviarlos como paquetes de datos a otros clientes, a travs del
servidor.

La estructura por convencin que vamos a utilizar en este tutorial para


redactar un buffer, ser:
-identificador de paquete
-contenido (o datos)
Es decir que para enviar un paquete, ste constar primero de un
identificador de paquete que ser un numero entero y luego de eso
podremos escribir los datos que queramos, ya sean cadenas, coordenadas,
nombres, etc.
Esta estructura es necesaria puesto que el servidor y los clientes
necesitarn saber que trae dentro del paquete y en qu orden.El Identificador
le ayudar a saber a cada parte, qu clase de paquete se espera recibir.
Un ejemplo sencillo de un paquete sera el siguiente:
1
x jugador
y jugador
donde 1 es el identificador de paquete, es decir, cuando el servidor reciba un
paquete con un nmero 1, sabr internamente que espera recibir un paquete
de coordenadas, en el orden de X primero y Y segundo.
Otro ejemplo sera el siguiente:
2
nombre jugador
donde 2 es el identificador de paquete, es decir, el servidor al recibir un
paquete con del numero 2 sabr que este contiene un nombre.
Es importante recalcar que los identificadores de paquetes deben ser
UNICOS. Esto es, no pueden haber dos paquetes con identificadores iguales,
sino el servidor o el cliente no sabrn que clase de paquetes le estn llegando
y se confundiran.
Entonces, para enviar datos al servidor (o a un cliente) es necesario crear un
buffer y llenarlo con datos. Para ello contamos con la siguiente funcin:
buffer_create(size, type, alignament);
Esta funcin crea un buffer y devuelve un ID para hacer referencia a dicho
buffer.
Argumentos:

size: el tamao del buffer en bytes


type: el tipo de buffer que queremos crear, pueden ser algunas de estas

constantes definidas en Game Maker:


buffer_fixed : buffer de tamao fijo
buffer_grow : buffer dinmicos
buffer_wrap: buffer envolvente

(Recuerde que ya los habamos explicado)

alignament: la alineacion de bytes (Nosotros dijimos que utilizaramos 1


en cualquier caso).

As para crear un paquete con datos, podramos colocar:


paquete = buffer_create(64,buffer_grow,1);
Con esto decimos que queremos crear un buffer, con un tamao de 64 bytes,
del tipo dinmico.
Una vez creado dicho paquete debemo llenarlo con los datos que quermos
enviar. Entonces comenzamos a escribir con la funcion:
buffer_write(buffer,type,value)
Esta funcin puede ser usada para escribir datos en nuestro buffer recin
creado. Los datos que escribas deben ir en concordancia con el tipo (type) que
se le ha pasado como parmetro. Es decir, NO intentes escribir una cadena si
esperas recibir un numero entero, deben coincidir.
Argumentos:

buffer: buffer donde vamos a escribir los datos.

type: tipo y tamao del dato que vamos a escribir. Game Maker Studio
cuenta con diversas constantes que reprensentan muchos tipos de datos,
voy a listar solo algunas para ms, busca en el manual en el apartado
de buffer_write. Algunas son:
buffer_u8 = un entero de 8 bits sin signo. Esto quiere decir que el
valor tiene que ir de 0 a 255
buffer_s8 = un entero de 8 bits con signo. Esto quiere decir que el
valor a escribir debe ir de -128 a 127 (el 0 se considera positivo)
buffer_u16 = Un entero de 16bits sin signo. El valor a escribir debe
ir de 0 a 65.535
buffer_s16 = Un entero de 16 bits con signo. El valor a escribir debe
ir de -32.768 a 32.767
buffer_string = Una cadena de cualquier tamao.

value: valor a escribir.

-Escribiendo y leyendo un buffer

Algo importante que se debe saber es que cuando se lee o se escribe dentro de
un buffer, hay como un indice dentro de este que indica la posicin donde se
termin de escribir (o leer).
Es como dejar un marcador dentro de un libro, comienzas el libro, lees 5
paginas y dejas el marcador all, luego la siguiente vez que vuelves a leer el
libro este marcador avanzar desde donde te quedaste.
Cada tipo de buffer tiene si tamao en bytes, dejo una tabla de los ya
mencionados, para ms, vase el manual.
TIPO DE DATO

ESPACIO EN BYTES

buffer_u8

buffer_s8

buffer_u16

buffer_s16

buffer_string

*N/A

*Las cadenas tienen un trato especial y es que al momento de terminar de


escribirlas se las agrega un 0, que significa NULO o fin de cadena.
Entonces, supongamos que tengo un buffer de 16 bits que est totalmente
vaco.
Este indice o marcador, se encontrar en la posicin 0.
Ahora bien, escribimos un dato del tamao y tipo buffer_u8, entonces, el
indice se mover hasta la posicin donde terminamos de escribir (o leer), asi
entonces, el indice estar en la posicin 1 de 16.
Supongamos que vuelvo a escribir un dato, esta vez del tamao buffer_s16,
entonces el indice se mover desde 1, unas 2 posiciones adicionales (porque es
lo que ocupan los buffers_u8 y buffers_s16), as entonces, el indice estar en
la posicion 3.
As ser consecutivamente mientras escribamos (o leamos) un dato dentro del
buffer.
Entonces, sera necesario contar con una funcin que nos permitiera mover
libremente dicho indice o marcador, para por ejemplo, leer el buffer desde el
incio.Por suerte contamos con:
buffer_seek(buffer, base, offset);
Esta funcion mueve el indice de buffer, desde la posicin base, offset
lugares adicionales.
Argumentos:

buffer: el buffer donde queremos mover el indice


base: posicin base desde donde nos moveremos. Tambien se aceptan

las siguientes constantes definidas dentro de Game Maker Studio:


buffer_seek_start = el comienzo del buffer
buffer_seek_relative = La posicin actual del indice
buffer_seek_end = el final del buffer

offset: lugares adicionales desde la posicin base.

Luego una funcin extra, que nos servir de ayuda ser:


buffer_tell(buffer);
Esta funcin devuelve el valor de donde se encuentra el indice de buffer
actualmente.
Argumentos:

buffer: buffer del cual queremos obtener la posicin de su indice de


escritura/lectura.

-Armando un buffer con datos o paquete de datos


Pfff, tanto lio... para un juego :-[
Primero vamos a crear un paquete, recordando nuestra convencin de como se
armaba uno:
1- identificador del paquetes
2- datos que queremos enviar
As por ejemplo, si quremos que nuestro cliente enve un paquete al servidor
(o viceversa), primero deberamos escribir un nmero para identificar el
paquete y seguido, los datos:
var paquete;
paquete = buffer_create(16, buffer_grow, 1); // creamos un paquete de 16
bytes
buffer_seek(paquete, buffer_seek_start, 0); //Movemos el indice de escritura
al inicio del paquete
buffer_write(paquete, buffer_u8, 10); // escribimos el identificador del paquete
buffer_write(paquete, buffer_s8, x);//Escribimos en paquete el valor de
nuestra x
buffer_write(paquete, buffer_s8, y); //escribimos en paquete el valor de
nuestra y

Ya con esto creamos nuestro paquete, que tiene la siguiente estructura:


10
coordenada X
coordenada Y
Siendo 10 el identificador de datos, que puede ser cualquier numero que
deseemos, siempre que recordemos que significa esto.
Es como si dijramos implicitamente: Este paquete contiene galletas
pero en vez de eso ponemos un 10, ( o cualquier numero). Entonces al
momento de recibirlos el servidor con solo ver el nmero 10 sabe que ah
dentro hay galletas, porque es la convencin que adoptamos para numerar a
los paquetes.
Bien, ahora con el paquete listo, debemos enviarlo al servidor... para ello
contamos con la siguiente funcion:
network_send_packet(socket, buffer, size);
Esta funcin sirve para enviar nuestro paquete buffer del tamao size por
medio de socket.
Si se envi bien el paquete entonces devolver el numero de bytes enviados
sino devolver un valor menor a 0.
Argumentos:

socket: socket que usaremos para enviar el paquete.


buffer: buffer o paquete que queremos enviar.
size: tamao de nuestro buffer (en bytes).

Bien, una vez sabiendo esto, nos quedara algo como:


var paquete;
paquete = buffer_create(16, buffer_grow, 1); // creamos el paquete
buffer_seek(paquete, buffer_seek_start, 0); //Movemos el indice de escritura
al inicio del paquete
buffer_write(paquete, buffer_u8, 10); // escribimos en paquete un valor que
va de 0 a 255 (notese que usamos buffer_u8), y el valor que escribimos alli es
10
buffer_write(paquete, buffer_s8, x);//Escribimos en paquete el valor de
nuestra x
buffer_write(paquete, buffer_s8, y); //escribimos en paquete el valor de
nuestra y

var tamanio = buffer_tell(paquete); //Esto nos devolver la posicin donde se


escribo el ultimo dato, por lo tanto, el tamao final del paquete.
network_send_packet(global.cliente, paquete, tamanio);// enviamos el
paquete a travs de nuestro socket cliente
Y ya con esto por fin, le pudimos enviar un dato al servidor! :D
Ah, pero nuestro servidor no sabe como interpretar cada paquete :O pues
ahora, vamos a trabajar ese aspecto.

Interpretando los paquetes de nuestros clientes


Para interpretar los paquetes de nuestros clientes, es necesario que vayamos a
nuestro evento asncrono en la parte que detecta los paquetes entrantes
dentro del switch que discriminas los motivos de ejeucin:
case network_type_data:
// ESTE APARTADO NOS INTERESA!!!
break;

Dentro del caso network_type_data, es necesario que que interpretemos


que clase de paquete nos lleg, para ello usaremos un switch anidado.

case network_type_data:
var paquete_recibido = ds_map_find_value(async_load,
buffer); //obtenemos el buffer que nos envia un cliente
buffer_seek(paquete_recibido, buffer_seek_start, 0); //nos
aseguramos de comenzar a leer desde el inicio.
Var ID_PAQUETE = buffer_read(paquete_recibido, buffer_u8);
//leemos el indice del paquete que nos enviaron
switch(ID_PAQUTE) //Evaluamos el indice de dicho paquete
{
case 10: //nos dice que recibimos un paquete de
coordenadas
//Leemos las coordenadas, como se ve, tengan en cuenta
que se leen en el orden en el cual estn definidas en el paquete del cliente.
player1_x = buffer_read(paquete_recibido, buffer_s8);
player1_y = buffer_read(paquete_recibido, buffer_s8);

break;
}
break;

Ya con esto el servidor puede procesar los datos que envi el cliente. En este
caso los guarda dentro de las variables player1_x y player1_y. Que pueden ser
interpretadas de cualquier forma que se desee dentro del programa.
Si quisiramos enviar datos del servidor al cliente, es exactamente el mismo
proceso:
-Crear un paquete
Colocarle una ID y los datos
-Enviarla al cliente
y finalmente, el cliente debera procesar los datos como crea conveniente.
Eso es todo... muuuuuuuuuuuy extenso, pero bueno! Es lo que se requiere
para hacer un juego en linea TCP.
Espero que les haya servido. Y si desean compartir el tutorial, bienvenido sea!
Los crditos son agradecidos.
Cualquier pregunta es bienvenida!
Black_Cat

You might also like