Tutorial de la localidad en la edición de misiones multiplayer o multijugador con Arma3.
Buenas, voy a intentar explicar un poco algunas cosas que me resultaron difíciles de comprender en la edición de misiones multiplayer del arma3. Esta información la he sacado de http://killzonekid.com, gracias!.
Obviamente este artículo va dirigido a la gente que ya se haya peleado con la edición de misiones y tenga conocimientos previos de la programación en arma3. Si editas en single player todo funcionará de maravilla y cuando exportes la misión a multijugador zas! no funciona nada. Intentaremos explicar por qué.
Hay que distinguir entre lo que es cliente, servidor, servidor dedicado, un cliente que se une a una partida, etc.. hay información sobre esto por ahí y entraré lo justo en esto. Si juegas en single player, el cliente, el servidor, el servidor dedicado, etc... es siempre todo tu mismo pc. Esto significa que mientras no definas las variables como privadas, pues todas las funciones que hagas pueden ser llamadas desde cualquier script.
Las variables locales que se escriben con '_', por ejemplo, _myFunction son validas dentro de un script y las variables globales por ej myFunction valen para todas las llamadas desde otros scripts y clases posteriores, etc.. Siempre y cuando no se hayan declarado como private.
_myFunction //disponible sólo en ese script };
myFunction //disponible en todos los scripts };
Cuando editamos en single player y jugamos posteriormente en single player no hay problema porque estas variables o funciones no hay que hacer que se ejecuten en cada pc, porque nuestro pc actúa como cliente y como servidor todo a la vez. Vamos que no vas a tener problemas y no vamos a hablar de editar en single player ¿pero por qué ésto no funciona en multiplayer? :
Cuando jugamos en multiplayer en arma3, cada pc conectado es diferente e independiente por completo en cuanto a ejecución de código, con lo cual, myFunction definida en mi PC no existirá para los demás ordenadores conectados, al menos que se haga algo con esto. Cuando jugamos en multiplayer a arma3, resulta que existe una maquina que es el servidor y luego cada pc de cada cliente. La variable myFunction creada en un cliente vale para cualquier llamada dentro de ese cliente (existe solo en ese ordenador), pero no se transmite a los demás ordenadores ni al server, con lo que lo que queremos lograr no va bien. Entonces ¿cómo se soluciona?.
Una de las maneras para solucionarlo es transmitiendo la variable a los otros ordenadores por ejemplo con publicVariable (más abajo veremos que usaremos publicVariable, o publicVariableClient o bien publicVariableServer, en función de a dónde queramos enviar la variable). Además también se transmitirá a otros pcs conectados a la partida después de haberse enviado la variable, es decir, a los JIP (Joining In Progress) clientes.
myFunction //disponible en un pc únicamente };
publicVariable "myFunction"; //Ahora myFunction está disponible en todos los ordenadores (server y clientes).
publicVariableClient "myFunction"; //corre desde el server. Ahora myFunction está disponible en un cliente.
publicVariableServer "myFunction"; //corre desde el cliente. Ahora myFunction está disponible en el server.
pero ojo: Mientras que publicVariable es compatible con JIP y persistente, publicVariableClient no lo es :(!!
¿qué pasa cuando inicias el server y te unes a el? Normalmente se tienen 3 ficheros en tu misión: mission.sqm, description.ext y init.sqf. Cuando los jugadores se unen al server por primera vez, se descargan los ficheros de la misión desde el server y se ejecutan en sus PC automáticamente. Pero antes de esto, el server también ejecuta esos archivos de la misión en su propio PC justo antes de que se ejecuten en el primero de los pc conectados. Luego, esos ficheros se van ejecutando en cada uno de los pcs conectados.
En el 1º, en el 2º, en el 3º, etc. Con lo cual si quieres tener myFunction en cada máquina, pues lo añades simplemente en el init.sqf por ejemplo. De esta manera tenemos la posibilidad de hacer una variable global. Pero si después necesitas cambiar el valor de esta variable global, necesitarás transmitir sus cambios con el comando publicVariable cada vez, para tener todos los PCs sincronizados.
Inciso: isServer y isDedicated, aclaraciones:
Los dos comandos indican si el entorno en los que se ejecutan los scripts es en el server o no. Si creas una misión multijugador y haces tu de host para que se conecten jugadores a tu partida, tu pc es el cliente y el servidor (con lo cual isServer será true en el pc donde se crea), pero no será un servidor dedicado separado, con lo cual isDedicated será falso en él. Después, cada usuario que se conecte es un cliente distinto. Como se puede fácilmente iniciar un servidor dedicado en tu PC y luego conectarte a el (busca esto en internet y verás que hay un ejecutable en la misma carpeta del juego con el cual puedes iniciar un servidor dedicado), lo mejor es que te focalices en esto. Lo mejor es que a la hora de hacer las pruebas. te centres en un entorno de servidor dedicado y punto (nada de probar en single player, ni haciendo de host en multiplayer, etc.. porque luego no funcionan las cosas y no se entiende, y es por lo que estamos explicando).
Ejemplo de uso del isDedicated dentro del init.sqf.
if (isDedicated) then {
//El código de aquí nos aseguramos de que sólo se ejecuta en el servidor dedicado.
} else {
//este código se ejecuta en cada cliente o pc conectado.
};
Como puedes ver, si no lo pruebas con un servidor dedicado, jamás entrará en el primer if: Esto se usa en scripts que sabes que corren tanto en el server como en los clientes conectados, por ejemplo en el init.sqf, mission.sqm y description.ext. Si tienes un script que se llama desde el init.sqf por ejemplo, el script correrá tanto en el server como en los clientes, y un script llamado desde ese otro script también se ejecutará en ambos, en servidor y en cada máquina, y así sucesivamente. Este es el motivo por el cual necesitamos el comando isDedicated, para que podamos controlar qué se ejecuta, dónde y cuando.
Ahora que sabemos cual es el principal problema, ¿cómo podemos ejecutar una función que haya sido sólo definida en el Server, desde un cliente?.
La única forma en la que podemos comunicarnos con el server es mediante una variable emitida o radiodifundida (broadcast en inglés) como dijimos antes. Cuando se envía una publicVariable desde un cliente, cada uno incluyendo al server la recibirá. Sabemos como emitir una variable con publicVariable, Pero ¿de que manera un cliente o el server pueden recibir esa variable emitida y ejecutar un código predefinido?. Pues esto lo puedes hacer ejecutando el comando addPublicVariableEventHandler en la maquina donde se tenga que recibir la variable enviada, en nuestro caso el server. El comando pasará el valor de la variable emitida al código que definas y lo ejecutará en el PC donde el eventhandler fue añadido. Tan simple como poner addPublicVariableEventHandler a esa variable emitida desde otro sitio en una maquina, y difundirla desde otra para lograr su ejecución remota. Así es como podemos comunicarnos con el server.
Ejemplo: Yo en mi cliente hago
broadcastVar = {
//disponible en un pc únicamente
};
publicVariable "broadcastVar"; //Ahora broadcastVar está disponible en todos los ordenadores (clientes).
Con esto envío al resto de ordenadores incluido el servidor la variable broadcastVar. Luego, en el server por ejemplo hago esto para recibir esa variable y hacer cosas con ella:
"broadcastVar" addPublicVariableEventHandler {
_broadcastVarName = _this select 0;
_broadcastVarValue = _this select 1; //hacer lo que se quiera con los datos aquí...
};
Pero qué pasa si necesitas recibir una respuesta de vuelta desde el server. Pues que puedes usar el mismo principio y añadir un eventhandler en tu cliente y de esta forma el server pueda hablar con tu cliente de la misma forma. En ese caso, podrías querer usar el comando publicVariable que es más adecuado para esta tarea ; publicVariableServer y publicVariableClient.
Vamos a poner un ejemplo de código usado en el init.sqf para entenderlo mejor. Desde un cliente A, vamos a enviar 2 números al servidor, el servidor los va a sumar y nos va a devolver la suma a ese cliente A (por ello desde el Server la enviaremos con publicVariableClient, porque la enviamos a un cliente).
if (isDedicated) then {
"packet" addPublicVariableEventHandler {
//Para el servidor esperamos una variable "packet" que es un array de 3 elementos
//el primer parámetro del array es el player y obtenemos su ID
_pcid = owner (_this select 1 select 0);
_number1 = _this select 1 select 1;
_number2 = _this select 1 select 2;
_thesum = _number1 + _number2;
packet = _thesum;
//enviamos la variable "packet" que ahora vale la suma de los dos números al cliente que nos interesa sólamente
_pcid publicVariableClient "packet";
//publicVariableClient porque lo queremos enviar al cliente y le enviamos el ID del player del cliente al que lo enviamos
};
} else {
"packet" addPublicVariableEventHandler {
//en el cliente esperamos una variable "packet" que es un numero _thesum = _this select 1; hint str _thesum;
};
};
Expliquemos paso a paso lo que ocurre aquí. Usamos el comando isDedicated para asegurarnos que los clientes y el servidor ejecutan sólo su propio código. Como estamos en el init.sqf, esto se ejecutará en clientes y servidor a la vez, por eso se hace esta separación con el isDedicated. Nosotros usaremos una variable llamada por ejemplo "packet" que será la que enviaremos de ida y vuelta. En el servidor la recibiremos como un array [player, number1, number2]. ¿Por qué necesitamos el parámetro player?, porque publicVariableClient necesita saber a qué PC enviarlo, y nosotros podemos obtener el ID del propietario de ese PC mediante el objecto player y el comando owner.
Después de extraer todos los datos desde el valor de la variable “packet”, el server hará la suma y emitirá la suma en la misma varibale “packet”. Lo podemos hacer así porque el public variable eventhandler no se activa en el equipo de transmisión. Sobre el cliente (en este caso únicamente sobre el cliente al que mandamos la variable), se espera la variable "packet" emitida como un numero, así que ahora "packet" es un número. Cuando llega, simplemente la mostramos con str hint.
Una vez hemos establecido los eventhandlers que esperan a la emisión de la variable "packet", podemos testear a emitir nuestra variable "packet" desde un cliente por ejemplo así:
packet = [player, 22, 44];
publicVariableServer "packet";
Con lo cual si todo está correcto, deberíamos ver en la partida de ese cliente únicamente un aviso de texto con el numero 66 dentro.
Si os fijáis hemos usado publicVariableServer porque lo que queremos es que la variable sólo la reciba el server. Si usamos publicVariable en su lugar, la variable además del server también la recibirán todos los clientes conectados también. Como desde los clientes no esperamos un array sino que esperamos un número, pues esto no irá bien. Además de que sólo nos interesaba enviarlo al Server.
LOCALIDAD:
Cada objeto en el juego tiene un propietario (menos los objetos embebidos en el mapa). Un propietario es un PC conectado. El servidor como ya sabemos es también un PC conectado, con lo que también puede ser un propietario. Cada propietario tiene una ID. El servidor por ejemplo tiene ID: 1, el primer humano conectado tiene el ID: 3, etc. Los objetos creados pertenecen al PC que los creó. Los objetos embebidos en el mapa pertenecen a todos y a nadie y tienen propietario ID: 0.
Algunos objetos pueden cambiar de propietario y regresar mientras que otros nunca cambian de propietario. Por ejemplo, el objeto player siempre pertenece al PC de la persona que lo controla. Un vehículo como por ejemplo un coche puede pertenecer a cualquier PC dependiendo de quién lo esté conduciendo. Si estás en el asiento del conductor de un coche, tu te adueñas de él y la propiedad del coche se transfiere a tu PC. Si sales fuera, aún continuas siendo el propietario al menos que algún otro se siente en el asiento del conductor o te desconectes del server. Si desconectas, la propiedad se transfiere al server. Sentándote en el asiento del pasajero no cambiará la actual propiedad del coche.
Saber la localidad de un objeto es muy importante en multiplayer, porque hay comandos que solo funcionarán si se ejecutan en el PC que es propietario del objeto. por ejemplo los comandos como setHit y setFuel deben ser ejecutados localmente, es decir, en el PC que sea el propietario del objeto. Los cambios actuales al objeto tendrán un efecto global y serán emitidos a cada PC. Con lo que si quieres cambiar la cantidad de gasolina de un vehículo, tienes que saber qué PC es el propietario de ese vehículo primero, luego ejecutar el comando setFuel en ese PC únicamente, y luego, en este caso en concreto, automáticamente propagará los cambios a través de internet actualizando los demas PCs conectados.
Por ello, algunos comandos de la BIS wiki tienen información de su localidad en arma3 en sus respectivas páginas.
Los argumentos del comando deben ser locales. Si el argumento es mi unidad player, sólo puedo enviar el comando en mi PC porque para los demás pcs mi objeto player es REMOTO así que allí no funcionará.
El comando acepta argumentos globales. si el argumento es un vehículo, no tengo que averiguar su pc propietario, lo puedo referenciar desde cualquier lugar y funcionará.
El efecto del comando es local, es decir, el efecto está limitado al PC que ejecuta el comando y no se transmitirá a los demás.
El efecto del comando es global, es decir, los cambios serán transmitidos y ocurrirán en cada PC, y a menudo también en los Pc JIP.
Echemos otro vistazo al comando setFuel. Los argumentos del comando (vehículo en este caso) deben ser locales, así que tenemos que ejecutar este comando en el PC del propietario del vehículo. El efecto del comando es global, con lo que cuando se actualice el nuevo nivel de gasolina, automáticamente los demás PCs verán ese nuevo nivel de gasolina en el vehículo.
Para saber si un objeto es local a un PC donde corres un script, usa el comando local. Devolverá true si el objeto pertenece a ese pc. Convirtiendo el objeto a un string también revelará la localidad del objeto.
Los objetos no locales tendrán el sufijo REMOTE. Para demostrarlo se hizo un hint representando mi jugador (player) en el server. Como player es siempre local a mi PC en el servidor sale con el sufijo REMOTE:
Arma3 también tiene un práctico eventhandler "local", el cual se disparará tan pronto como el objecto cambie su localidad. Por ejemplo cuando tu engendras (spawn) un vehículo en el servidor y luego entras en el asiento del conductor. La propiedad del vehículo cambiará del server a tu PC, el vehículo pasará a ser local a tu PC y el eventhandler se disparará. De todas formas, el procedimiento correcto para saber la propiedad de un objeto sería: Averiguar si el objeto te pertenece a ti ya, usando el comando local Si el objeto no es local, averiguar su propietario. Esto solo se puede hacer desde el Servidor PC. Solo el servidor sabe qué PC es propietario de qué cosa.
Enviar solicitud al servidor con el objeto en cuestión Chequear si el objeto es local al servidor, en caso de que el PC del servidor sea el dueño. Si el server no es el propietario, preguntar al server quien es el propietario ejecutando el comando owner en el server Ejecutar tu comando deseado en el Pc que es propietario del objeto Y así es como aparecería en el código si tuviéramos que hacer setFuel global:
//init.sqf
KK_fnc_setFuel = {
private ["_veh","_fuel"];
_veh = _this select 0;
_fuel = _this select 1;
if (local _veh) then {
_veh setFuel _fuel;
} else {
PVsetFuel = _this;
if (isDedicated) then {
(owner _veh) publicVariableClient "PVsetFuel";
} else {
publicVariableServer "PVsetFuel";
};
};
};
"PVsetFuel" addPublicVariableEventHandler {
(_this select 1) call KK_fnc_setFuel;
};
Hay otra manera, aunque no muy óptima porque incrementa el tráfico de red. Puedes tener todos los clientes incluido el servidor esperando a una variable (vehículo) transmitida y luego ver su localidad en cada PC. Sólo un Pc será su propietario y ejecutará el comando setFuel. Mientras que en el anterior ejemplo de codigo había un máximo de 2 llamadas extra echas al mismo tiempo para averiguar el propietario, en el siguiente ejemplo de código el máximo de llamadas extras podría ser igual al número de jugadores en el servidor (osea peor y menos óptimo).
//init.sqf KK_fnc_setFuel = {
private ["_veh","_fuel"];
_veh = _this select 0;
_fuel = _this select 1;
if (local _veh) then {
_veh setFuel _fuel;
} else {
PVsetFuel = _this; publicVariable "PVsetFuel";
};
};
"PVsetFuel" addPublicVariableEventHandler {
_veh = _this select 1 select 0;
_fuel = _this select 1 select 1;
if (local _veh) then {
_veh setFuel _fuel;
};
};
El segundo ejemplo correrá bien en ambos, servidor dedicado y servidor creado por un player. En ambos casos puedes llamar a KK_fnc_setFuel desde cualquier PC, por ejemplo:
[vehicles select 0, 0.5] call KK_fnc_setFuel;
Saludos y espero os ayude.