ACCUEIL CONTACT BANNIERES LIENS PLAN
francais anglais
MUSIQUE
CREATION 2D/3D
INFORMATIQUE
TELECHARGEMENTS




PC-INFOGAME.COM : Making-of : Programmation

Une base de données au coeur du projet

Les jeux vidéos manipulent une grande quantité de données ne serait-ce que pour charger les modèles, gérer les statistiques des entités ... Il est donc indispensable d'avoir recours aux bases de données pour stocker ces informations. Le coeur de Singularity est une base de données Access exportée sous forme de fichiers texte avec délimiteurs. Il n'existe pas directement de module pour gérer les bases Access et le SQL sous Dark Basic Pro. Les tables sont donc chargées dans des tableaux que l'on exploite ensuite dans le programme.

Construire rigoureusement une base de données n'est pas toujours aisée. Comment éviter au maximum la redondance tout en conservant une certaine unité dans les informations ? C'est le défi du programmeur... La base de données de Singularity est composée de 42 tables, ce qui représente 270 champs ! Voyons comment est structurée la base de données :

NOM NOMBRE FONTION
MEDIAS 5 Gestion des ressources (3D, 2D, sons).
BD 20 Représentent les données fixes du jeu. Elles ne sont pas modifiées dans le jeu. Il s'agit notamment des statistiques des entités, des armes mais aussi les messages, textes, positions de l'interface...
INFO 6 Ces tables stockent les données fluctuantes du jeu qui varient selon le joueur (vie, munitions, état du jeu...).
INIT 11 Il s'agit de tables de chargement qui configurent le jeu uniquement à chaque de début de partie. Elles font la liaison entre les medias et les données fixes.


Ce découpage strict des données possède de nombreux avantages :
  • Il n'y a pas de confusion entre les ressources et les données pures.
  • Les données stables et fluctuantes sont séparées. Ainsi sauvegarder une partie revient à conserver uniquement les tables fluctuantes (INFO). Publier un patch , c'est uniquement mettre à jour les tables fixes (BD) sans risque d'altérer le profil du joueur.
Le schéma relationnel de la base de données est disponible ci-dessous. L'intégralité des relations n'a pas été affichée pour plus de lisibilité.


Cliquez sur l'image pour l'afficher en plein écran




Intégrer la base de données dans le moteur de Dark Basic Pro

Le choix s'est porté sur l'exportation de la base de données sous forme de fichiers texte. Une simple macro dans Access permet de mettre à jour rapidement les fichiers du jeu. Mais comment traiter ces données sans requêtes SQL ? Tout d'abord, chaque table est entièrement chargée dans un tableau classique composé de structures (=types utilisateur). Voici par exemple, le transfert en mémoire du fichier concernant les entités :

REM DEFINITION DE LA BASE DE DONNEES : INFO_ENTITES
REM -----------------------------------------------

Function BASE_DE_DONNEES_INFO_entites(Ligne$ as string)
  local ID, Vie, ID_Projectile, Munitions as integer

  ID               =Val(Extraction(Ligne$,1))
  Vie              =Val(Extraction(Ligne$,2))
  ID_Projectile =Val(Extraction(Ligne$,3))
  Munitions     =Val(Extraction(Ligne$,4))

  Array insert at bottom BD_Info_Entites()
  BD_Info_Entites().ID=ID
  BD_Info_Entites().Vie=Vie
  BD_Info_Entites().ID_Projectile=ID_Projectile
  BD_Info_Entites().Munitions=Munitions
  BD_Info_Entites().Action=""
  BD_Info_Entites().Animation=""
  BD_Info_Entites().Delai=0

EndFunction


Une fois que toutes les tables sont chargées, un autre problème se pose. Comment accéder rapidement à un enregistrement (ligne de la table) alors qu'il y a plusieurs centaines de lignes ? Une simple boucle de recherche serait trop lourde à gérer étant donné le nombre d'informations conséquent à récolter à chaque instant de jeu. Une contrainte sur le remplissage des tables a donc été fixée. Chaque enregistrement est donc identifié (clé primaire) par un numéro indiquant exactement son indice dans le tableau correspondant. Ainsi si l'on veut connaître la vie de l'entité identifiée par le numéro 23, il suffit de se positionner à l'indice 23 du tableau des entités. On peut donc qualifier cette manoeuvre d'accès direct. La navigation dans la base de données est maintenant assez aisée tout en sachant que la jointure entre tables a été évité au maximum.



L'organisation du projet sous Dark Basic Pro

Une certaine rigueur au niveau des fichiers, des noms, de la manière de programmer est nécessaire pour faciliter les tâches de maintenance. Dans la mesure possible, toute constante est définie dans une variable en début de programme. Ceci permet de modifier rapidement des données à un et un seul endroit du code. Voici un extrait des constantes utilisées :

        Constantes.Camera.Vitesse_Depl=10
        Constantes.Camera.Vitesse_Angle=0.3
        Constantes.Camera.Angle_X_Min=60
        Constantes.Camera.Angle_X_Max=300
        Joueur.Camera.Active=true

        Joueur.Divers.Saut_Delai=1000
        Joueur.Divers.Saut_Hauteur=200

        Constantes.Portee.ObjAnim=500
        Constantes.Portee.Actions=600
        Constantes.Portee.Objets=500

        Constantes.Sons.Portee=5000
        Constantes.Sons.ID_Musique=-1
        Constantes.Sons.Indice_Temp=1

        Constantes.Entites.Delai_Suppression=10000

        Systeme.Chrono.FPS_Base=50

        Systeme.Environnement.Champ_Vision_Pres=1
        Systeme.Environnement.Champ_Vision_Loin=3000
        Systeme.Environnement.Lumiere_Ambiante=60

        Systeme.Environnement.Gravite_Physique_X=0
        Systeme.Environnement.Gravite_Entites=30
        Systeme.Environnement.Rayon_AI=80

        Systeme.Touches.Avancer=200
        Systeme.Touches.Reculer=208
        Systeme.Touches.Gauche=203
        Systeme.Touches.Droite=205


Le projet a été divisé en fichiers thématiques contenant chacun un ensemble de fonctions. La programmation orientée objets n'étant pas possible, il a fallu adopter une approche fonctionnelle en modules. 28 modules thématiques ont été créés :

ACTIONS, AI, ANIMATIONS, AUDIO, BILLBOARDING, CAMERA, 
CHAINES, CHARGEMENT, COLLISIONS, DECLARATION, DIALOGUE, 
DIVERS, ECRAN, ENTITES_AI, EVENEMENTS, FONTE, INTERACTIONS, 
LISTBOX, MATH, OBJETS, OBJETS_ANIMES, PERIPHERIQUES, PHYSIQUE, 
PROJECTILES, SCRIPTS, SOURCE, SPRITES, SYSTEME


Seul le module SCRIPTS est entièrement dépendant du jeu puisqu'il s'occupe du déroulement scénaristique du jeu. Les autres modules ont été crées et testés séparemment. Ils sont entièrement réutilisables et leur mise à jour est très aisée. Voici par exemple un extrait du module lié aux mathématiques.

REM DISTANCE ENTRE DEUX POINTS
REM --------------------------

Function MATH_Distance_2_Points(X1#, Y1#, Z1#, X2#, Y2#, Z2# as float, Dimension as integer)
  local X#, Y#, Z#, Distance# as float

  If Dimension=3
   vecteur=make vector3(1)
   X#=X1#-X2#
   Y#=Y1#-Y2#
   Z#=Z1#-Z2#
   set vector3 1,X#,Y#,Z#
   Distance#=length vector3(1)
  EndIf

  If Dimension=2
   vecteur=make vector2(1)
   X#=X1#-X2#
   Z#=Z1#-Z2#
   set vector2 1,X#,Z#
   Distance#=length vector2(1)
  EndIf

EndFunction Distance#




La boucle principale du jeu

Dans un jeu, une boucle principale "infinie" gère en permanence le déroulement de la partie. Plus la durée d'exécution de celle-ci est rapide, plus le jeu est fluide. L'optimisation des traitements a effectué est donc primordiale dans un jeu. Voici le contenu de la boucle principale de Singularity. A noter que seuls des appels de fonctions de mise à jour de chaque module est effectué. On regroupe ainsi les traitements sous un même thème.

Do
       SYSTEME_Temps_MAJ()

       ACTIONS_Clavier()
       ACTIONS_Souris()

       OBJETS_ANIMES_MAJ()

       SCRIPTS_triggers()
       TRIGGERS_MAJ()
       
       RunCollisionPRO()          `COLLISIONS

       PERIPHERIQUES_MAJ()
       CAMERA_MAJ()               `CAMERA

       COLLISIONS_MAJ()
       PROJECTILES_MAJ()
       ENTITES_AI_MAJ()

       ECRAN_Interface()
       ECRAN_Informations()

       AUDIO_MAJ()                 `AUDIO
       BILLBOARDS_MAJ()
       ANIMATIONS_MAJ()

       AI update                    `IA
       phy update                   `PHYSIQUE

       SCRIPTS_Shader_Rendu()
       sync                             `RENDU

Loop




Le système de collision

Une bonne gestion des collisions est primordiale dans un jeu en 3D de type FPS. Je me suis tourné vers l'utilisation d'un plugin NuclearGloryCollision pour gagner du temps dans un domaine que je ne maîtrisais pas. Cet outil permet de gérer facilement des collisions de deux types :
  • Ellipsoïde : le modèle 3D est représenté grossièrement par une ellipsoïde. Les collisions sont ainsi plus aisées à calculer car le modèle ne possède pas une forme complexe. Néanmoins la précision est collisions est évidemment moindre. Ceci a été utilisé pour toutes les entités y compris le héros et les projectiles.
  • Mesh (maillage) : la forme exacte de l'objet est pris en compte. Il s'agit d'une détection de collision au polygone près. Ceci est beaucoup plus gourmand qu'une simple ellipsoïde mais la précision est accrue. On réserve généralement ce mode de collision au décor.
Pour distinguer les différents éléments entrés dans le moteur, différents types de groupes ont été définis, chacun pouvant intéragir l'un avec l'autre :

TYPE_DECOR      
TYPE_OBJANIM    
TYPE_ENTITE     
TYPE_JOUEUR     
TYPE_ACTION     
TYPE_ARME       
TYPE_OBJET      
TYPE_PROJECTILE 
TYPE_PHYSIQUE   
TYPE_BILLBOARD  


La collision des projectiles est réelle ce qui signifie qu'un impact a lieu lorsque le projectile et sa cible sont entrés en contact. Il ne s'agit pas d'une collision calculée par la méthode du "raycasting" qui consiste à simuler une collision entre un point de départ et d'arrivé à un instant donné.



Le moteur physique

La gestion de la physique est entièrement assurée par le plugin DarkPhysics. Il s'agit d'un moteur de collision plus riche que le précédent. Il permet de gérer des formes de collision plus adaptées (cube, sphère, terrain, maillage...). Pour éviter d'encombrer la mémoire avec des objets déjà dans le plugin de collision NuclearGlory, seul le décor et les objets dit "physiques" sont chargés.

Un problème se pose quand au fonctionnement simultané du moteur physique et du système de collision. Comment les projectiles d'armes appartenant uniquement au système de collision pourront intéragir et déplacer les objets physiques ? Lors d'une collision entre un projectile et une objet, on applique manuellement une force à cet objet. Le moteur se charge ensuite de gérer les déplacements correspondants. Voici la routine chargée de faire communiquer les deux plugins. On récupère dans un premier temps les coordonnées de collisions avec lesquelles on forme un vecteur représentant la trajectoire du projectile. Après normalisation, on peut calculer la force à appliquer sur chaque axe pour refléter l'impact.

ID_Collision=CollisionHitObjPro(ID,Index_Collision)
Collision=CollisionHitTypePro(ID,Index_Collision)

Collision_X#=CollisionHitPointPro(ID,Index_Collision,1)
Collision_Y#=CollisionHitPointPro(ID,Index_Collision,2)
Collision_Z#=CollisionHitPointPro(ID,Index_Collision,3)

Pos_X#=CollisionHitPosPro(ID, Index_Collision, 1)
Pos_Y#=CollisionHitPosPro(ID, Index_Collision, 2)
Pos_Z#=CollisionHitPosPro(ID, Index_Collision, 3)

If Collision=TYPE_PHYSIQUE

   Force_Physique#=BD_Projectiles(Index).Force_Physique
   temp = make vector3(1)
   set vector3 1,Collision_X#-Source_X#,Collision_Y#-Source_Y#, Collision_Z#-Source_Z#
   normalize vector3 1,1
   
   phy add rigid body force ID_Collision,x vector3(1)*Force_Physique#,y vector3(1)*Force_Physique#,
                                       z vector3(1)*Force_Physique#, Collision_X#, Collision_Y#, Collision_Z#,1
EndIf


Physique




L'intelligence artificielle

Un nouveau plugin, Dark AI, a été utilisé pour faciliter la mise en place d'une intelligence artificielle. Une bonne IA repose entièrement sur un pathfinding efficace (=recherche du plus court chemin entre deux points). Le plugin utilise l'algorithme A* pour calculer une trajectoire. Son point fort est sa manière de déterminer les contours du décor et les points de passage. En effet, il calcule les collisions entre le décor et un plan horizontal pour générer les frontières du niveau et les waypoints. Ensuite toute une panoplie d'actions peut être développée (actions paramétrées pour les entités, attitudes et comportements). La faiblesse du plugin repose toutefois sur une seule gestion de l'environnement en deux dimensions. Pour définir plusieurs étages ou niveaux, il est nécessaire d'indiquer manuellement à quel plan de jeu doit se trouver l'entité (système de container).



Le billboarding ou la simulation de la 3D avec de la 2D

La méthode du billboarding est encore très couramment utilisée dans les jeux vidéos (Command And Conquer 3 par exemple). Elle consiste à afficher un élément en 2D que l'on oriente constamment en direction de la caméra. On simule ainsi un effet de volume puisque l'objet pivote en même temps que la caméra. Il est ainsi moins gourmand de gérer une texture animée en 2D que son équivalent en 3D. L'illusion est toutefois moins forte lorsque l'on est trop près de l'objet en question. C'est pourquoi on réserve généralement cette technique aux éléments éloignés qui n'ont pas besoin d'un modèle en haute résolution (arbres au lointain...). Les explosions de projectiles, le feu sont très souvent simulés par la méthode du billboarding.

Voyons sa mise en place sous Dark Basic Pro. Il y a 4 étapes fondamentales :
  • Création d'un plain (=objet 3D sans épaisseur)
  • Application d'une texture avec effet de transparence (ghost)
  • Orientation du plain vers la caméra
  • Animation de la texture si nécessaire
A noter que l'orientation de l'objet vers la caméra est facultative si l'on désire simuler des objets stagnants (pluie, pas...).

  `Création
	make object plain ID_Objet, Largeur, Hauteur
	position object ID_Objet, Pos_X#, Pos_Y#, Pos_Z#
	rotate object ID_Objet, Angle_X#, Angle_Y#, Angle_Z#
	ghost object on ID_Objet, 0
  
  `Boucle principale
        texture object ID_Objet,ID_Image
	Point Object ID_Objet, Camera.Pos_X, object position Y(ID_Objet), Joueur.Camera.Pos_Z


Billboarding




Des sons dans un environnement en 3D

Pour augmenter le réalisme d'un jeu, les bruitages ont un rôle primordial. Pour que l'immersion soit totale, le volume des sons et la répartition sur les enceintes est entièrement paramétrée. Sous Dark Basic Pro, le volume du son est déterminée selon un rapport entre la position du joueur et celle du son. Un pourcentage ainsi calculé indique le volume du son.

Function AUDIO_Son_Volume(ID_Son as integer, X#, Z# as float)
  local Volume_Min, Volume_Max as integer
  local Distance#, Result# as float

  Distance#=MATH_Distance_2_points(Joueur.Camera.Pos_X, -1, Joueur.Camera.Pos_Z, X#, -1, Z#,2)

  If Distance#<=Constantes.Sons.Portee
    Volume_Min=BD_Sons(ID_Son).Volume_Min
    Volume_Max=BD_Sons(ID_Son).Volume_Max

    Result#=1-(Distance#/Constantes.Sons.Portee)
    Result#=Result#*(Volume_Max-Volume_Min)+Volume_Min
  Else
    Result#=0
  EndIf

EndFunction Result#


Le calcul du pan, c'est-à-dire la répartition du son sur l'enceinte gauche ou droite est plus complexe. En effet, il s'agit en premier lieu de déterminer si le son a été déclenché à la gauche ou la droite du joueur. Pour cela un calcul trigonométrique permet de déterminer l'angle entre le son et le joueur. Un rapport sous forme de pourcentage est ensuite calculé pour répartir le son sur chaque enceinte.

Function MATH_AngleY(X#, Z# as float)
  local Angle# as float

  If X#>Joueur.Camera.Pos_X and Z#>Joueur.Camera.Pos_Z then 
		Angle#=abs(atan((X#-Joueur.Camera.Pos_X)/(Z#-Joueur.Camera.Pos_Z))) 
  If X#>Joueur.Camera.Pos_X and Z#<Joueur.Camera.Pos_Z then
		Angle#=abs(atan((Z#-Joueur.Camera.Pos_Z)/(X#-Joueur.Camera.Pos_X)))+90  
  If X#<Joueur.Camera.Pos_X and Z#<Joueur.Camera.Pos_Z then 
		Angle#=abs(atan((X#-Joueur.Camera.Pos_X)/(Z#-Joueur.Camera.Pos_Z)))+180
  If X#<Joueur.Camera.Pos_X and Z#>Joueur.Camera.Pos_Z then 
		Angle#=abs(atan((Z#-Joueur.Camera.Pos_Z)/(X#-Joueur.Camera.Pos_X)))+270

  Angle#=wrapvalue(Angle#-Joueur.Camera.Angle_Y)

EndFunction Angle#

Function AUDIO_Son_Pan(X#, Z# as float)
  local temp#, Result#, Angle# as float

  Angle#=MATH_AngleY(X#, Z#)

  `Enceinte gauche
  If Angle#<=360 and Angle#>=180
    temp#=abs(Angle#-270)
    Result#=(temp#/90)-1
  Else
  `Enceinte droite
    temp#=abs(Angle#-90)
    Result#=(90-temp#)/90
  EndIf

EndFunction Result#


Avec Dark Basic Pro, il est impossible de charger dynamiquement de nouvelles ressources. Le multi-threading n'étant pas disponible. Dans le cas des sons, ceci signifie qu'il faut charger à l'avance un nombre prédéfini de bruitages. Autrement dit, il faut par exemple charger 5 bruits de tirs de pistolet. Mais comment faire lorsque plus de 5 entités tirent en même temps ? L'astuce a consister à affirmer que l'on ne ferait pas de distinction sonore majeure entre 5, 6 ou 7 sons simultanés. Dès que le nombre maximal de son est atteint et qu'un nouvel individu déclenche un nouveau son, celui ayant le volume le plus faible est stoppé pour être réaffecter au nouvel individu. Un son plus faible est donc remplacé par un son plus fort, l'illusion du nombre est donc parfaite !



La vitesse d'exécution du jeu - Timer Based Movement

Lancer un jeu consiste à exécuter constamment une boucle principale "infinie" qui effectue tous les traitements. La vitesse d'exécution de cette boucle dépend entièrement de l'ordinateur qui l'exécute (=sa configuration). Autrement dit, sur un ordinateur puissant la boucle s'exécutera plus vite que sur un ordinateur normal. Ceci pose un problème majeur : celui de la vitesse du jeu en pleine partie. Le personnage de déplacera plus vite, les animations seront accélérées... bref, il est nécessaire de mettre en place un mécanisme harmonisant la vitesse d'exécution sur tous les machines. Le nombre de boucles exécutées par seconde est ce qu'on appelle couramment les FPS : le nombre d'images par seconde.

L'astuce consiste à calculer un coefficient d'accélération ou de ralentissement pour compenser la vitesse des mouvements dans le jeu. En effet, sur un ordinateur normal, le coefficient sera de 100%. Sur un PC deux fois plus rapide, le coefficient ne sera que de 50% pour réduire de moitié la vitesse des mouvements. Ce coefficient est déterminé selon la durée d'exécution d'une boucle principale. Il est appliqué aussi bien aux mouvements des entités, de la caméra qu'aux animations. Cette méthode a toutefois tendance à provoquer des sursauts lorsque le nombre d'images par seconde est très bas puisque la compensation de mouvement est très importante.

Function SYSTEME_Temps_MAJ()

     Systeme.Chrono.Temps=Timer()
     Systeme.Chrono.Coefficient=Systeme.Chrono.FPS_Base*
	                            (Systeme.Chrono.Temps-Systeme.Chrono.Temps_Precedent)/1000.0
     Systeme.Chrono.Temps_Precedent=Systeme.Chrono.Temps

     `Formule :
     `  FPS_Reel = (Temps_Actuel-Temps_Precedent) / 1000.0
     `  Coef = FPS_base / FPS_Reel
     `  ==> Coef = [ FPS_Base*(Temps_Actuel-Temps_Precedent)] / 1000.0

EndFunction




L'interface et la gestion des résolutions

La gestion d'une interface de jeu est plus complexe qu'il n'y parait. En effet, la principale préoccupation naît lorsqu'il s'agit de développer un jeu peut importe la résolution de l'écran. Comment positionner les éléments ? Comment adapter leurs dimensions ?

Dans Singularity, le positionnement des éléments est relatif. Ceci signifie qu'il est calculé en fonction de la taille de l'écran, de la position d'un autre élément ou par une valeur absolue selon la formule suivante :

POSITION (calculée au chargement) = % ECRAN [+/- LARGEUR D'UN ELEMENT] [+/- VALEUR FIXE]

De cette manière il est très aisée de positionner un objet quelque soit la résolution et même d'écrire du texte dans une zone graphique prévue à cet effet. La gestion de la taille des images n'a pas été envisagée car elle n'était pas nécessaire à ce stade. Différentes résolutions interfaces auraient pu être utilisées plutôt que d'agrossir ou réduire les éléments.

Le joueur doit aussi intéragir avec l'interface. Un bouton reste un simple élément graphique qui ne possède pas les propriétés qu'on pourrait lui connaître lors de la création d'un logiciel. Il est de notre ressort d'implémenter son comportement. Pour simuler différents états (cliqué, non cliqué...), des sprites animés ont été utilisés. Le passage d'une frame à l'autre ayant lieu à chaque clic ou appui.

La principale difficulté était d'identifier clairement un clic. En effet, lors d'un même clic, Dark Basic Pro déclenche à intervalle régulier l'événement "clic". Il est donc impossible de savoir précisément quand débute ou termine le clic. Une fonction personnalisée a donc été nécessaire :

Function PERIPHERIQUES_MAJ_Souris()
  `Systeme.Souris.Clic_Actuel  <= renvoie le clic continu
  `Systeme.Souris.Clic_Unique <= renvoie un clic unique

  Systeme.Souris.Clic_Precedent=Systeme.Souris.Clic_Actuel
  Systeme.Souris.Clic_Actuel=MouseClick()

  Systeme.Souris.Clic_Unique=0
  If Systeme.Souris.Clic_Actuel<>0 and Systeme.Souris.Clic_Actuel<>Systeme.Souris.Clic_Precedent
    Systeme.Souris.Clic_Unique=Systeme.Souris.Clic_Actuel
  EndIf

EndFunction

- COPYRIGHT 2008-2011 - ALL RIGHTS RESERVED - WEBMASTER : UDUN (GIRARD CYRIL) - DESIGN PAR : ANTYOZ