Implémenter un deuxième tir

Écrit le 28/10/2004 par Kurim
Dernière mise à jour : 06/02/2006

Introduction

Dans ce tutorial, on va ajouter un deuxième bouton de tir. Pour faire simple, on va se limiter à ajouter un burst fire au pistol de base. Ce bouton tirera les mêmes munitions que le tir primaire, mais en quantité différente. Une fois que le système est codé, il vous sera très facile d'ajouter cette fonction pour n'importe quel autre arme. Il vous est également possible de coder d'autre comportement que le tir de plusieurs munitions, ce qui pourra faire d'ailleurs l'objet d'autres tutoriaux.

Ce qu'il faut savoir

Les armes dans Doom 3 fonctionnent de façon particulière, entre le code et les scripts. Une classe d'arme, idWeapon, gère le comportement générique des armes, à savoir le déploiement, le rangement, le reload, et un certain nombre de fonction de ce type. Puis les differentes armes utilisent un fichier script, qui se trouve dans script/weapon_xxx.script. Il y a aussi le fichier script/weapon_base.script qui lui définit l'objet de base "arme". Ce fichier n'est pas très important en lui même, il sert juste à déclarer de nouvelles variables. Pour commencer, il faut donc que vous copiez les fichiers suivants du dossier /base vers le dossier de votre mod :
/script/weapon_base.script
/script/weapon_pistol.script
/script/weapon_shotgun.script
/guis/mainmenu.gui (pour ajouter le bouton au menu de config.)
/strings/english.lang (et toutes les langues que vous utilisez)

Dans le fichier weapon_base

Ajouter, dans la déclaration de l'objet "weapon_base", ceci :

boolean WEAPON_ATTACK2;

(en dessous de WEAPON_ATTACK par exemple)

Cette variable est modifiée dans le code, et peut être interrogé dans le script, pour connaître à tout instant l'état de l'arme. Ici, elle va nous permettre de savoir si on est en train de tirer avec le second tir. Il va falloir également l'ajouter dans le code.

Modifications dans le code

Dans weapon.h, ces variables d'état sont définis vers la ligne 135. Ajoutez-y, à cet endroit, la variable :

idScriptBool            WEAPON_ATTACK2;

Le type, idScriptBool, est un type particulier qui permet le lien avec la variable de même nom du script.

Maintenant, on va implémenter une nouvelle touche dans le code. Ce ne sera pas une touche dite "impulse", mais bien une touche d'état, car la laisser enfoncée pendant un certain temps permet de tirer plusieurs fois d'affilée. Nous allons utiliser un slot de bouton qui ne sert pas dans Doom 3. Allez dans UsercmdGen.h, en début de fichier, vous pouvez apercevoir :

 // usercmd_t->button bits
const int BUTTON_ATTACK            = BIT(0);
const int BUTTON_RUN            = BIT(1);
const int BUTTON_ZOOM            = BIT(2);
const int BUTTON_SCORES            = BIT(3);
const int BUTTON_MLOOK            = BIT(4);
const int BUTTON_5                = BIT(5);
const int BUTTON_6                = BIT(6);
const int BUTTON_7                = BIT(7);

Ici, on voit que les boutons 5, 6 et 7 ne sont pas utilisés. On va renommer le BUTTON_5 en :

 const int BUTTON_ATTACK2            = BIT(5);

Remarque : le nom de la touche à binder in-game sera quand même _button5, faites-y attention.

Maintenant, il faut coder le comportement à associer à ce bouton. Ce bouton servira uniquement à tirer, on ne se soucie pas des autres comportements qu'a par exemple le bouton d'attaque normal. Dans player.cpp, ligne 3845, il doit y avoir ceci :

     // check for attack
    AI_WEAPON_FIRED = false;
    if ( !influenceActive ) {
        if ( ( usercmd.buttons & BUTTON_ATTACK ) && !weaponGone ) {
            FireWeapon();
        } else if ( oldButtons & BUTTON_ATTACK ) {
            AI_ATTACK_HELD = false;
            weapon.GetEntity()->EndAttack();
        }
    }

c'est ici que se fait le lien entre le bouton et l'arme. Modifiez de facon à gerer 2 cas de figure :

     // check for attack
    AI_WEAPON_FIRED = false;
    if ( !influenceActive ) {
        if ( ( usercmd.buttons & BUTTON_ATTACK ) && !weaponGone ) {
            FireWeapon( 1 );
        } else if ( oldButtons & BUTTON_ATTACK ) {
            AI_ATTACK_HELD = false;
            weapon.GetEntity()->EndAttack();
        } else if ( ( usercmd.buttons & BUTTON_ATTACK2 ) && !weaponGone ) {
            FireWeapon( 2 );
        } else if ( oldButtons & BUTTON_ATTACK2 ) {
            AI_ATTACK_HELD = false;
            weapon.GetEntity()->EndAttack();
        }
    }

Ici, on a modifié la structure de la fonction FireWeapon(). On va la mettre a jour dans la déclaration de la classe, cherchez ça dans Player.h :

     void                    FireWeapon( void );

et changez-le en ça :

     void                    FireWeapon( int mode );

Voilà, vous l'aurez compris, le mode 1 sera pour le tir normal, et le mode 2 pour le deuxième tir que nous sommes en train d'ajouter. Si vous allez voir maintenant à l'intérieur de cette fonction FireWeapon, vous verrez qu'elle exécute ceci :

            weapon.GetEntity()->BeginAttack();

On va modifier cette ligne de la même façon :

            weapon.GetEntity()->BeginAttack( mode );

Puis dans Weapon.h, on met a jour la déclaration de la fonction BeginAttack comme ceci :

     void                    BeginAttack( int mode );

Maintenant, mettez à jour les fonctions BeginAttack et EndAttack dans Weapon.cpp, de sorte qu'elles puissent gérer 2 tirs, avec la variable WEAPON_ATTACK2 crée tout à l'heure :

 /*
================
idWeapon::BeginAttack
================
*/
void idWeapon::BeginAttack( int mode ) {
    idScriptBool *mode_attack;

    if ( status != WP_OUTOFAMMO ) {
        lastAttack = gameLocal.time;
    }

    if ( !isLinked ) {
        return;
    }

    if (mode == 2)
    {
        WEAPON_ATTACK = false;
        mode_attack = &WEAPON_ATTACK2;
    }
    else
    {
        WEAPON_ATTACK2 = false;
        mode_attack = &WEAPON_ATTACK;
}

    if ( !(*mode_attack) ) {
        if ( sndHum ) {
            StopSound( SND_CHANNEL_BODY, false );
        }
    }

    *mode_attack = true;
}

 /*
================
idWeapon::EndAttack
================
*/
void idWeapon::EndAttack( void ) {
    if ( !WEAPON_ATTACK.IsLinked() ) {
        return;
    }
    if ( WEAPON_ATTACK ) {
        WEAPON_ATTACK = false;
        if ( sndHum ) {
            StartSoundShader( sndHum, SND_CHANNEL_BODY, 0, false, NULL );
        }
    }
    if ( WEAPON_ATTACK2 ) {
        WEAPON_ATTACK2 = false;
        if ( sndHum ) {
            StartSoundShader( sndHum, SND_CHANNEL_BODY, 0, false, NULL );
        }
    }
}

Voilà, avec ce code, on devrait avoir les variables WEAPON_ATTACK et WEAPON_ATTACK2 qui valent vraie quand le joueur tir respectivement avec le tir primaire et secondaire, et de plus on ne devrait pas avoir les deux variables qui valent vraie en même temps. Le reste va maintenant se jouer dans les fichiers scripts, mais d'abord il faut être sur que WEAPON_ATTACK2 est bien linkée avec la variable du script. Pour ça, un petit Find In Files sur WEAPON_ATTACK nous montre tous les endroits où il faut rajouter la variable :

gamesysGameTypeInfo.h, ligne 6555, ajoutez la ligne du milieu :

    { "idScriptBool", "WEAPON_ATTACK", (int)(&((idWeapon *)0)->WEAPON_ATTACK), sizeof( ((idWeapon *)0)->WEAPON_ATTACK ) },
    { "idScriptBool", "WEAPON_ATTACK2", (int)(&((idWeapon *)0)->WEAPON_ATTACK2), sizeof( ((idWeapon *)0)->WEAPON_ATTACK2 ) },
    { "idScriptBool", "WEAPON_RELOAD", (int)(&((idWeapon *)0)->WEAPON_RELOAD), sizeof( ((idWeapon *)0)->WEAPON_RELOAD ) },

Fonction idWeapon::EnterCinematic, dans Weapon.cpp, ajoutez la ligne du milieu :

        WEAPON_ATTACK        = false;
        WEAPON_ATTACK2        = false;
        WEAPON_RELOAD        = false;

Fonction idWeapon::Clear ajoutez la ligne du milieu :

    WEAPON_ATTACK.Unlink();
    WEAPON_ATTACK2.Unlink();
    WEAPON_RELOAD.Unlink();


Fonction idWeapon::Restore :

    WEAPON_ATTACK.LinkTo(        scriptObject, "WEAPON_ATTACK" );
    WEAPON_ATTACK2.LinkTo(        scriptObject, "WEAPON_ATTACK2" );
    WEAPON_RELOAD.LinkTo(        scriptObject, "WEAPON_RELOAD" ); 

Fonction idWeapon::GetWeaponDef, vers la fin de la fonction :

    WEAPON_ATTACK.LinkTo(        scriptObject, "WEAPON_ATTACK" );
    WEAPON_ATTACK2.LinkTo(        scriptObject, "WEAPON_ATTACK2" );
    WEAPON_RELOAD.LinkTo(        scriptObject, "WEAPON_RELOAD" ); 

Création du deuxième tir de chaque arme

On va faire un exemple avec le pistol, pour lui faire un burst fire comme dans CS. Ouvrez weapon_pistol.script. C'est ici qu'est codé le comportement particulier de chaque arme. Ca commence par une série de défines ; ajoutez ceux-ci :

#define PISTOL_FIRERATE2            0.6
#define PISTOL_NUMAMMO2        4

Puis, dans la déclaration de l'objet weapon_pistol, ajoutez cette fonction :

     void        Fire2();

Ensuite, déscendez un peu dans le fichier pour trouver la fonction Idle. Dedans, vous avez cette portion de code :

        if ( ( currentTime >= next_attack ) && WEAPON_ATTACK ) {
            if ( ammoClip > 0 ) {
                weaponState( "Fire", PISTOL_IDLE_TO_FIRE );
            } else if ( ammoAvailable() > 0 ) {
                if ( autoReload() ) {
                    netReload();
                    weaponState( "Reload", PISTOL_IDLE_TO_RELOAD );
                }
            }
        }

ajoutez juste en dessous :

         if ( ( currentTime >= next_attack ) && WEAPON_ATTACK2 ) {
            if ( ammoClip >= PISTOL_NUMPROJECTILES2 ) {
                weaponState( "Fire2", PISTOL_IDLE_TO_FIRE );
            } else if ( ammoAvailable() > 0 ) {
                if ( autoReload() ) {
                    netReload();
                    weaponState( "Reload", PISTOL_IDLE_TO_RELOAD );
                }
            }
        }

Puis, un peu plus bas, ajoutez votre fonction Fire2, inspiré de la fonction Fire normal :

[code void weapon_pistol::Fire2() {
float ammoClip;
float i;

next_attack = sys.getTime() + PISTOL_FIRERATE2;

ammoClip = ammoInClip();
if ( ammoClip == PISTOL_LOWAMMO ) {
startSound( "snd_lowammo", SND_CHANNEL_ITEM, true );
}

for( i = 0; i < PISTOL_NUMAMMO2; i++ ) {
launchProjectiles( PISTOL_NUMPROJECTILES, spread, 0, 1.0, 1.0 );
}
playAnim( ANIMCHANNEL_ALL, "fire" );
waitUntil( animDone( ANIMCHANNEL_ALL, PISTOL_FIRE_TO_IDLE ) );
weaponState( "Idle", PISTOL_FIRE_TO_IDLE );
} ____CODE_END____

Voilà, il faut savoir que toutes les fonctions que vous appelez depuis ces fichiers (comme par exemple launchProjectiles), sont dans la classe idWeapon du code, avec le nom en Event_xxx où xxx est la nom de la fonction dans les fichiers scripts. Vous pouvez donc à partir de la modifier davantage le comportement des armes. Vous pouvez avec ça faire par exemple un double shotgun en tir secondaire, ou encore faire un système de charge pour le plasmagun... c'est déjà ca :p

Pour binder votre touche, et l'ajouter dans les menus, regardez dans le tutorial sur l'ajout d'une touche impulse, c'est la meme démarche, sauf que la commande à utiliser pour le bind est _button5.