|
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
|
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é.
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
|
|
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
|
|
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
|