id Tech 4 : code

Tout permuter

=Notes générales

  • Le projet Game dans Visual Studio créera le fichier gamex86.dll.
  • Le projet DoomDLL créera l'exécutable principal du jeu, Doom3.exe. J'ignore pourquoi le mot DLL se trouve dans le nom d'un projet qui sera compilé en .exe.
  • Le projet d3xp dans la solution de Doom 3 serait pour l'extension Resurrection of Evil. Je crois que d3xp signifie Doom 3 expansion pack.Quand je fais une recherche dans tout le projet dans Visual C++, ce projet porte à confusion par rapport au projet Game que j'utilise principalement. On peut donc faire un clic droit dessus dans VC++, et sélectionner Décharger le projet. Cela ne le supprimera pas (il faut le faire explicitement dans l'explorateur Windows), mais la recherche dans toute la solution ignorera ce projet.
  • id Tech 4 utilise certaines fonctions de la bibliothèque standard du langage C (voir neo/idlib/precompiled.h), mais les conteneurs std de C++ ne sont généralement pas utilisés chez id (sauf pour les outils de création comme l'éditeur de niveau). (Source) (en)
  • Il y a plusieurs endroits dans le code où on trouve des typedef dans les déclarations de struct :

    typedef struct chose_s
    {
    ...
    } chose_t;

    Ceci sert à utiliser les structures sans avoir à toujours écrire struct devant le nom. Les suffixes (_s et _t) indiquent le type de l'objet auquel on fera référence.

    À certains endroits, le nom de la struct est omis ; les struct avec des typedef n'ont pas besoin de nom de base, à moins qu'elles ne doivent être déclarées d'avance (forward declaration). Ce sont surtout (peut-être exclusivement) les noms des typedef qui sont utilisés.

=Modifications

Compilation

  • Compilation avec Visual Studio 2010 Express

    Ces notes se basent considérablement sur les directives de compilation du site Riot's House of Stuff ; le site n'étant pas disponible à l'heure actuelle, les notes sont publiées ici. Note importante : les instructions dans ce fichier texte nécessitent le fichier win_nanoafx.h.

    Le but de la liste d'instructions suivante est surtout de clarifier les étapes en expliquant à quoi elles servent, au meilleur de mes connaissances.

    À la base, Doom 3 ne compile qu'avec la version complète de Visual Studio, car il utilise la bibliothèque propriétaire MFC qui n'est distribuée qu'avec Visual Studio Professional, c'est-à-dire la version payante. Il faut donc effectuer quelques modifications au code pour supprimer les dépendances problématiques.

    Une fois ces étapes achevées, les outils de développement de Doom 3 (éditeur de niveau, éditeur de GUI, etc.) seront omis à la compilation, et leurs commandes de console correspondantes ne seront pas reconnues. Évidemment, compiler ces outils ne sera pas nécessaire tant qu'on possède un exemplaire de Doom 3, qui les contient déjà.

    1. Si ce n'est pas déjà fait, récupérer le code de Doom 3 de la page GitHub officielle d'id Software.
    2. Selon le fichier README.txt (dans le dossier racine de Doom 3), le kit de développement DirectX de Microsoft doit être installé.
    3. Dans le dossier neo :
      1. Ouvrir _Release.props et enlever toutes les occurrences de nafxcw.lib;.
      2. Ouvrir _Debug.props et enlever toutes les occurrences de nafxcwd.lib;.

      Ces deux fichiers font partie de la bibliothèque MFC.

      1. Récupérer un fichier nommé win_nanoafx.h, anciennement disponible sur le site Riot's House of Stuff (qui n'est maintenant plus qu'un répertoire). Ce fichier contient les définitions de quelques classes (comme CComBSTR) qui sont utilisées par le moteur de jeu.
      2. Placer le fichier dans neo/sys/win32.
      3. Dans le même dossier, ouvrir win_shared.cpp, puis inclure win_nanoafx.h à la ligne 49 :

        #include "win_nanoafx.h"

    4. Ouvrir neo/sys/win32/win_taskkeyhook.cpp et mettre en commentaire la ligne 37 :

      #include <afxwin.h>

      En général, les entêtes C++ dont le nom contient « AFX » font partie de MFC.

    5. Ouvrir neo/idlib/precompiled.h et mettre en commentaire la ligne 61 :

      #include "../tools/comafx/StdAfx.h"

      StdAfx.h est le nom standard pour un fichier qui contient tous les entêtes qui seront compilés d'avance. Les entêtes précompilés servent à accélérer la compilation du projet durant le développement ; par contre, ce fichier particulier n'est d'aucune utilité car il ne fait référence qu'aux entêtes MFC.

    6. Ouvrir neo/framework/BuildDefines.h et mettre en commentaire la ligne 96 :

      #define ID_ALLOW_TOOLS

    7. Ouvrir neo/doom.sln, puis faire les changements suivants dans Visual C++ :
      1. Dans l'explorateur de solutions, supprimer ces dossiers (clic droit > Supprimer) :
        • dlls (non disponible)
        • exes (non disponible)
        • libs (non disponible)
      2. Propriétés du projet DoomDLL (Toutes les configurations) > Répertoires VC++ :
        1. Dans Répertoires Include, ajouter le dossier include de l'SDK DirectX :

          C:/Program Files (x86)/Microsoft DirectX SDK (June 2010)/Include

        2. Dans Répertoires de bibliothèques, ajouter le dossier de bibliothèques de l'SDK DirectX :

          C:/Program Files (x86)/Microsoft DirectX SDK (June 2010)/Lib/x86

      3. Supprimer tous les dossiers dans DoomDLL/Tools, SAUF Compilers.
      4. Supprimer le dossier DoomDLL/Sys/RC. (Ce dossier contient le code pour les outils de création.)

    Pour l'erreur suivante : « Couldn't load default.cfg »

    Le compilateur cherche dans le mauvais dossier pour trouver default.cfg. Le chemin où il cherche sera affiché quelques lignes plus haut dans la fenêtre de sortie.

    L'idéal est de copier le dossier base de Doom 3 dans le dossier racine du projet.

    J'ai déjà tenté de changer le répertoire des ressources via la méthode suivante :

    1. Propriétés du projet DoomDLL (toutes les configurations) > Débogage > Arguments de la commande
    2. Dans l'instruction +set fs_basepath, changer le chemin pour celui du dossier racine du jeu, c'est-à-dire celui qui contient le dossier base :
      • pour déboguer dans Visual Studio, ce sera le dossier racine du projet
      • pour démarrer le jeu de façon traditionnelle, ce sera le dossier où se trouvera le fichier Doom3.exe du mod (monmod/build/Win32/Release par défaut en mode Release)

    Par contre, il semble que la valeur de fs_basepath ne soit pas affectée par la ligne de commande, du moins pas à la compilation (ou peut-être même à l'exécution de DOOM3.exe...).

    Notes :

    • La variable fs_basepath, qui contient le chemin absolu vers le dossier des ressources du jeu, prend comme valeur par défaut le dossier courant (celui du projet et de l'exécutable). On peut vérifier sa valeur dans la console en tapant fs_basepath.
    • La première fois que j'ai rencontré ce problème, j'ai tout simplement changé le nom du dossier racine de doom3.gpl à doom, et tout a fonctionné...

    Pour l'erreur suivante : « error C2504: '_bstr_t' : classe de base non définie »

    Le nouveau fichier, win_nanoafx.h, fait référence à la classe _bstr_t, qui est définie dans comutil.h.

    Si on compile en mode « Dedicated Release » (comme c'est le cas par défaut), comutil.h ne sera pas inclus au projet par win_shared.cpp :

    #ifndef ID_DEDICATED
    #include <comdef.h>
    #include <comutil.h>
    #include <Wbemidl.h>

    #pragma comment (lib, "wbemuuid.lib")
    #endif

    On peut soit laisser tomber l'inclusion de win_nanoafx.h dans win_shared.cpp (ligne 49), soit compiler dans un autre mode.

Réglages généraux

  • Changer le nom de l'application

    Fichier : framework/Licensee.h

    Il suffit de trouver la constante GAME_NAME, puis modifier sa valeur.

  • Choisir le GUI du menu principal

    Fichier : neo/framework/Session.cpp, ligne 2907

    guiMainMenu = uiManager->FindGui( "guis/mainmenu.gui", true, false, true );

    Note : Il y a plus qu'un mainmenu.gui. Celui qui sera utilisé se trouve dans zpak003.pk4, dans le dossier guis.

  • Commandes du jeu

    1. Les commandes de clavier sont définies dans base/DoomConfig.cfg (ou, si ce fichier n'existe pas, base/default.cfg).
    2. Les impulsions sont définies dans neo/framework/UsercmdGen.h.
    3. Les impulsions sont vérifiées dans idPlayer::PerformImpulse().

Modifications précises

  • Créer une CVar

    1. Déclarer la CVar dans neo/game/gamesys/SysCvar.cpp
    2. Déclarer la CVar avec extern dans neo/game/gamesys/SysCvar.h
  • Ajouter une nouvelle propriété aux windowDef

    1. Fichier : neo/ui/Window.hDéclarer la variable correspondante dans la classe idWindow. La variable devra être une occurrence d'une classe héritant d'idWinVar
    2. Fichier : neo/ui/Window.cpp
      1. Initialiser la variable dans idWindow::CommonInit()
      2. L'ajouter à RegisterVars[] (~ ligne 62). Les autres variables montrent comment indiquer le type de la propriété
      3. Les mots-clés sont traités dans idWindow::GetWinVarByName()
  • Modifier les propriétés d'un matériau (texture)

    Pour modifier les propriétés d'un matériau dans le code, il faut utiliser des identifiants particuliers pour les valeurs modifiables.

    Par exemple, pour permettre au code C++ de modifier l'alpha (l'opacité) d'un matériau, il faudra donner la valeur parm3 à la propriété alpha :

    custom/mon_materiau
    {
        // Première passe
        {
            ...
     
            alpha parm3
        }
    }

    Les paramètres de matériaux utilisés par la classe idMaterial sont indiqués dans l'énumération expRegister_t. Malheureusement, les noms des éléments sont vagues, et je n'ai trouvé aucune documentation précise qui explique clairement à quoi chaque élément fait référence.

    Il semble que les quatre premiers paramètres sont les valeurs RVBA (0 = rouge, 1 = vert, 2 = bleu, 3 = alpha). parm3 signifie simplement le paramètre #3, alpha, c'est-à-dire l'opacité.

    Ainsi, pour modifier la transparence du matériau :

    1. on récupère le matériau à l'aide du gestionnaire de déclarations (decl manager)
    2. on modifie la valeur alpha avec idRenderSystemLocal::SetColor4()
    3. on applique la matériau (dans cet exemple, la texture fera le quart de l'écran, et sera affichée dans le coin supérieur gauche)

    // L'idéal serait de faire de monMateriau un membre de la classe et de l'initialiser dans le constructeur
    const idMaterial *monMateriau = declManager->FindMaterial( "custom/mon_materiau" );
     
    ...
     
    renderSystem->SetColor4( 1.0f, 1.0f, 1.0f, 0.5f ); // Rouge, vert, bleu, alpha (0,5)
    renderSystem->DrawStretchPic( 0, 0, SCREEN_WIDTH /2, SCREEN_HEIGHT /2, 0.0f, 0.0f, 1.0f, 1.0f, monMateriau );

=Récupération et échange d'informations

  • Afficher un message dans la console

    La plupart des classes, sinon toutes, utilisent un objet de type idCommon (neo/framework/Common.h). L'objet se nomme common et a une portée globale.

    La méthode Printf() de cet objet fonctionne comme la méthode standard std::printf(), sauf qu'elle imprime le texte dans la console de Doom.

    common->Printf( "Message" );

  • Créer un fichier et y insérer du texte

    idStr texte( "Du texte.\n" );
    fileSystem->WriteFile( "fichier.txt", texte.c_str(), texte.Length() );

    fichier.txt sera créé / modifié dans le dossier base.

    fileSystem est initialisé très tôt dans l'application, et peut donc servir presque partout.

    Pour inscrire plus d'une ligne :

    idFile *fichier = fileSystem->OpenFileAppend( "nom_du_fichier.txt" );
    fichier->Write( "Du texte.\n\n", 10 ); // 10 == nombre de caractères
    fichier->Write( "D'autre texte.\n\n", 15 );
    fileSystem->CloseFile( fichier );

  • Trouver le type d'une entité

    Fichier : game/Entity.cpp

    Fonction : idEntity::GetEntityDefName()

    Note : Lorsqu'on veut comparer le nom d'une entité avec une chaîne de caractère, la chaîne doit être au format idStr :

    if ( GetEntityDefName() == idStr( "func_securitycamera" ) ) // Fonctionne

    et non un const char* :

    if ( GetEntityDefName() == "func_securitycamera" ) // Ne fonctionne pas

  • Insérer une valeur du code C++ dans un GUI

    Les GUI sont issus de la classe idUserInterfaceLocal. Il suffit d'appeler une des fonctions prévues dans le code :

    • idUserInterfaceLocal::SetStateString()
    • idUserInterfaceLocal::SetStateBool()
    • idUserInterfaceLocal::SetStateInt()
    • idUserInterfaceLocal::SetStateFloat()

    Par exemple, pour insérer une clé nommée chose, avec la valeur affaire :

    // C++
    objetGUI->SetStateString( "chose", "affaire" );

    Une fois ceci fait, le gui référencé sera en mesure de récupérer la valeur comme suit avec la syntaxe gui::propriété :

    // fichier .gui
    windowDef Desktop
    {
        rect 0,0,640,480
        menugui 1
        backcolor 0, 0, 0, 0.5
        text "gui::chose" // affaire
    }

  • Suite d'événements pour le déplacement du joueur (astuce incomplète)

    J'ai noté ici des appels de fonction pertinents sur le déplacement du joueur à l'appui d'une touche. Incomplètes et d'origines variées, ces notes ne sont pas totalement cohérentes entre elles.

    La classe usercmd_t représente l'ensemble des commandes récentes du joueur. La classe idPlayer contient un objet de ce type, nommé usercmd.

    1. On récupère les instructions de l'utilisateur les plus récentes (un objet usercmd_t).-> idSessionLocal::RunGameTic() appelle idUsercmdGenLocal::GetDirectUsercmd() :

      cmd = usercmdGen->GetDirectUsercmd();

      -> GetDirectUsercmd() fait appel à idUsercmdGenLocal::MakeCurrent() :

      // create the usercmd
      MakeCurrent();

      Cette fonction est appelée à chaque image ; c'est elle qui met à jour usercmd.impulse dans la classe idPlayer.

      -> MakeCurrent() fait appel à idUsercmdGenLocal::KeyMove() :

      // get basic movement from keyboard

      KeyMove();

    2. RunGameTic() appelle ensuite idGameLocal::RunFrame() en lui passant l'objet usercmd_t

      gameReturn_t ret = game->RunFrame( &cmd );

    idPlayer::EvaluateControls() est aussi appelée à chaque image. Quand on appuie une touche d'impulsion, on exécute idPlayer::PerformImpulse().

    Les impulsions sont des commandes avec des significations particulières (sélectionner une arme, recharger, entres autres). Voir neo/framework/UsercmdGen.h, ligne ~53 pour avoir une meilleure idée des impulsions enregistrées.

=Problèmes de développement

  • Boîte de dialogue sous Visual C++ : Un ou plusieurs projets de la solution n'ont pas été correctement chargés.

    Ce message d'erreur fait surface à cause d'une sous-section dans le fichier neo/doom.sln.

    1. Faire une copie de doom.sln (au cas où).
    2. Ouvrir doom.sln dans un éditeur de texte.
    3. Supprimer la section NestedProjects :

      GlobalSection(NestedProjects) = preSolution
          {49BEC5C6-B964-417A-851E-808886B57400} = {347D107C-D787-4408-A60D-86FA45997F9B}
          {F46F5D4E-C1D4-4ADE-9FAA-5F0CE3AA07F1} = {347D107C-D787-4408-A60D-86FA45997F9B}
          {6EA6406F-3E65-47D9-8246-D6660A81606F} = {003B01AB-152D-45C8-BF45-E5A035042D7F}
          {49BEC5C6-B964-417A-851E-808886B57420} = {003B01AB-152D-45C8-BF45-E5A035042D7F}
          {49BEC5C6-B964-417A-851E-808886B57430} = {1E2B3940-65F8-4D8F-9EEE-85E94EBBC6DF}
          {49BEC5C6-B964-417A-851E-808886B574F1} = {1E2B3940-65F8-4D8F-9EEE-85E94EBBC6DF}
          {0BC6FCC9-C65E-4B1F-9A58-0B9399987C9F} = {1E2B3940-65F8-4D8F-9EEE-85E94EBBC6DF}
      EndGlobalSection