Fonctionnement des messages

Écrit le 03/07/2003 par Bicou
Dernière mise à jour : 30/01/2006

Introduction

Voilà un petit tutorial pour vous faire comprendre le mécanisme des messages serveur -> client. Sachez d'abord que dans Half-Life le serveur et le client sont séparés, et que si vous voulez avoir des informations sur le joueur, sur des entités pour les utiliser du coté client (ex: la vie du joueur sur le HUD, les munitions, etc.), il faut envoyer un message serveur -> client.
On peut aussi envoyer un « message » dans l'autre sens (client -> serveur), mais c'est plus limité (on passe par l'équivalent d'une commande tapée dans la console).
Arrêtons-nous ici pour tout de suite passer aux explications pratiques.

Envoi d'un message serveur -> client

Tout d'abord, il faut assigner une variable au message. Pour cela, ouvrez player.cpp, et dans le début du fichier vous voyez une liste de variables de messages, ajoutez-y la vôtre :

int gmsgSetCurWeap = 0;
int gmsgSayText = 0;
int gmsgTextMsg = 0;
int gmsgSetFOV = 0;
int gmsgShowMenu = 0;
int gmsgGeigerRange = 0;
int gmsgTeamNames = 0;

int gmsgMonMessage = 0;

On appelle le message MonMessage. Il faudra conserver ce nom tout au long du tutorial, et si vous le modifiez, il faudra le modifier partout.
La variable est initialisée à 0. Il faut maintenant lui assigner sa valeur, pour cela un peu plus bas, dans la fonction LinkUserMessages :

    gmsgSetFOV = REG_USER_MSG( "SetFOV", 1 );
    gmsgShowMenu = REG_USER_MSG( "ShowMenu", -1 );
    gmsgShake = REG_USER_MSG("ScreenShake", sizeof(ScreenShake));
    gmsgFade = REG_USER_MSG("ScreenFade", sizeof(ScreenFade));
    gmsgAmmoX = REG_USER_MSG("AmmoX", 2);
    gmsgTeamNames = REG_USER_MSG( "TeamNames", -1 );

    gmsgMonMessage = REG_USER_MSG( "MonMessage", 15 );

On donne deux paramètres à la fonction : le nom du message, et sa taille en octets. Vous verrez dans la partie Fonctions WRITE_*** et taille du message comment déterminer la taille de votre message.
Ensuite, on va envoyer notre message. Pour cela, il faut utiliser une fonction pour indiquer au moteur que l'on va envoyer un message, ensuite envoyer notre message, et pour terminer indiquer au moteur qu'on a fini l'envoi du message.
Nous allons envoyer notre message depuis la fonction Spawn() du joueur, qui est lancée quand le joueur apparaît dans la map. Mais c'est un exemple, vous pouvez envoyer votre message depuis n'importe où du coté serveur (il suffit de redéclarer la variable gmsgMonMessage en extern)
C'est très simple (dans la fonction CBasePlayer::Spawn(), ligne ~3100, player.cpp, juste avant le } de fin de fonction) :

    MESSAGE_BEGIN( MSG_ONE, gmsgMonMessage, NULL, pev );
        WRITE_BYTE( pev->health );
        WRITE_BYTE( pev->armorvalue );
        WRITE_BYTE( pev->frags );
        WRITE_COORD( pev->origin.x );
        WRITE_COORD( pev->origin.y );
        WRITE_COORD( pev->origin.z );
    MESSAGE_END();
}

La fonction MESSAGE_BEGIN sert à indiquer au moteur qu'on va envoyer des variables.
Le premier paramètre est le destinataire du message. MSG_ONE indique qu'on envoie le message à 1 seul joueur (lequel est indiqué en 4e paramètre). On peut utiliser MSG_ALL pour l'envoyer à tous les joueurs (le 4e paramètre ne sert plus du coup).
Le second paramètre est le nom du message, c'est la variable qu'on a déclarée en haut de player.cpp (si le message n'est pas envoyé à partir de player.cpp, pensez à mettre extern int gmsgMonMessage en haut du fichier).
Le troisième paramètre est l'expéditeur du message. La plupart du temps on met NULL pour indiquer que c'est le serveur.
Le quatrième paramètre n'est utilisé que si vous avez mis MSG_ONE comme destinataire, auquel cas il faut mettre son pev (entvars_t*).

Viennent ensuite des séries de fonctions WRITE_*** qui envoient des variables. Voyez dans la partie Fonctions WRITE_*** et taille du message pour savoir quelle fonction utiliser et quelles fonctions sont utilisables.

Pour terminer l'envoi, on fait MESSAGE_END(). Voilà pour l'envoi du message.

Fonctions WRITE_*** et taille du message

Voici les fonctions qui permettent d'envoyer des données. Il faut utiliser une fonction différente en fonction des données que vous envoyez :

WRITE_BYTE Envoie un nombre (1 octet)
WRITE_CHAR Envoie un char (1 octet)
WRITE_SHORT Envoie un short (2 octets)
WRITE_LONG Envoie un long (4 octets)
WRITE_ANGLE Envoie un nombre à virgule flottante (nombre décimal), très peu utilisé (4 octets)
WRITE_COORD Envoie un float pour des coordonnées. Ils vont souvent par trois, pour envoyer les coordonnées dans les trois dimensions (4 octet par envoi)
WRITE_STRING Envoie un char* : une chaîne de caractères (la taille dépend de votre char*)
WRITE_ENTITY Pas utilisé

Avec la liste je vous ai donné la taille en octets de chaque envoi. Pour avoir la taille de votre message au total, ajoutez simplement toutes les tailles de chaque envoi. Si vous avez un STRING, mettez -1, et le moteur trouvera par lui-même (éviter). Cette taille totale est à mettre lors de l'initialisation de la variable du message. Dans notre exemple on avait :

gmsgMonMessage = REG_USER_MSG( "MonMessage", 15 )

En effet il y avait 3 bytes et 3 coord, soit 15 octets (NB: byte est le mot anglais de octet).

Réception du message côté client

Pour réceptionner le message, on utilise des fonctions du type MsgFun_NomDuMessage. Dans notre exemple, ce sera :

int MsgFunc_MonMessage( char *pszName, int iSize, void *pbuf );

*pszName contient le nom du message, soit MonMessage dans notre exemple.
iSize est la taille du message, en octets
*pbuf est une variable tampon, elle sert pas (pas à nous en tout cas).

Ensuite il faut commencer la lecture du message. Regardons le code :

int MsgFunc_MonMessage( char *pszName, int iSize, void *pbuf )
{
    //Variables qu'on doit recevoir :
    int iHealth, iArmor, iFrags;
    float flCoords[3];

    BEGIN_READ( iSize, pbuf ); //Début de la lecture

    iHealth = READ_BYTE();
    iArmor = READ_BYTE();
    iFrags = READ_BYTE();
    flCoords[0] = READ_COORD();
    flCoords[1] = READ_COORD();
    flCoords[2] = READ_COORD();

    return 1; //Message reçu
}

C'est très simple. Il faut d'abord déclarer des variables pour stocker ce qu'on va recevoir. Ensuite il faut commencer la lecture avec BEGIN_READ(), et viennent une série de READ_***, similaires aux WRITE_***. Pareil, à chaque fonction le type de variable. Il suffit de les remettre dans le même ordre côté serveur et côté client.

Cette fonction est habituellement intégrée dans deux types de classes. Le premier est une classe dérivée de CHudBase, il existe un tutorial à ce sujet ici. Le second est une classe dérivée de Panel, classe gérant les menus VGUI. Voir tutorial à ce sujet.

Les commandes client

Ce ne sont pas vraiment des messages à proprement parler. En fait c'est comme une commande que l'on taperait dans la console. Pour cela on utilise la fonction ClientCmd(). Exemple :

ClientCmd( "give weapon_mp5" );

Cela va donner le mp5 au joueur.

Mais on peut faire un peu mieux, en créant notre commande. Côté client, ça pose pas de problème :

ClientCmd( "macommande brousouf" );

J'ai mis ça, mais on peut mettre ce qu'on veut dans cette chaîne de caractères. Ce qui importe est côté serveur : fichier client.cpp, fonction ClientCommand() (ligne ~360). Vous voyez une série de else if. Il va falloir intercaler le vôtre entre deux, comme ceci :

    else if ( FStrEq(pcmd, "give" ) )
    {
        if ( g_flWeaponCheat != 0.0)
        {
            int iszItem = ALLOC_STRING( CMD_ARGV(1) );  // Make a copy of the classname
            pPlayer->GiveNamedItem( STRING(iszItem) );
        }
    }
    //Mon code
    else if( FStrEq( pcmd, "macommande ) )
    {
        //Affiche ce qu'on a reçu du client à la console.
        ALERT( at_console, "Le serveur a reçu le message "%s"n", CMD_ARGV(1) );
    }
    //Fin de mon code
    else if ( FStrEq(pcmd, "drop" ) )
    {
        // player is dropping an item.
        pPlayer->DropPlayerItem((char *)CMD_ARGV(1));
    }
    else if ( FStrEq(pcmd, "fov" ) )
    {
        if ( g_flWeaponCheat && CMD_ARGC() > 1)

Voilà, on a mis notre code. Le else if teste si la commande est « macommande ». Il existe deux fonctions : CMD_ARGC() pour avoir le nombre de mots dans la commande, et CMD_ARGV( int ) pour avoir le contenu du n-ième mot. CMD_ARGV( 0 ) sera la commande, CMD_ARGV( 1 ) sera le 1er mot après la commande, etc.
Vous pouvez faire ce que vous voulez de votre commande et du message. Sachez que si vous avez envoyé un int ou un float, pour le récupérer en tant que tel il faut faire atoi( CMD_ARGV( x ) ) ou atof( CMD_ARGV( x ) ).

Conclusion

J'espère que ce tutorial aura été clair, si vous avez des problèmes il y aura toujours quelqu'un sur les forums pour vous éclairer.