I. Prérequis▲
Commençons comme dans l'exemple Hello World : nous incluons les fichiers d'en-têtes d'Irrlicht et un fichier supplémentaire pour pouvoir demander à l'utilisateur un type de pilote en utilisant la console.
#include
<irrlicht.h>
#include
"driverChoice.h"
Définissons quel niveau de Quake 3 nous devons charger.
#define IRRLICHT_QUAKE3_ARENA
//#define ORIGINAL_QUAKE3_ARENA
//#define CUSTOM_QUAKE3_ARENA
//#define SHOW_SHADER_NAME
#ifdef ORIGINAL_QUAKE3_ARENA
#define QUAKE3_STORAGE_FORMAT addFolderFileArchive
#define QUAKE3_STORAGE_1
"/baseq3/"
#ifdef CUSTOM_QUAKE3_ARENA
#define QUAKE3_STORAGE_2
"/cf/"
#define QUAKE3_MAP_NAME
"maps/cf.bsp"
#else
#define QUAKE3_MAP_NAME
"maps/q3dm8.bsp"
#endif
#endif
#ifdef IRRLICHT_QUAKE3_ARENA
#define QUAKE3_STORAGE_FORMAT addFileArchive
#define QUAKE3_STORAGE_1
"../../media/map-20kdm2.pk3"
#define QUAKE3_MAP_NAME
"maps/20kdm2.bsp"
#endif
using
namespace
irr;
using
namespace
scene;
Encore une fois, pour être capable d'utiliser le fichier Irrlicht.DLL, nous devons le lier avec le Irrlicht.lib. Nous pouvons mettre cette option dans les configurations du projet, mais pour le faire plus simplement, nous utilisons un commentaire pragma lib :
#ifdef _MSC_VER
#pragma comment(lib,
"Irrlicht.lib"
)
#endif
Une classe pour produire une série de captures d'écran.
class
CScreenShotFactory : public
IEventReceiver
{
public
:
CScreenShotFactory( IrrlichtDevice *
device, const
c8 *
templateName, ISceneNode*
node )
:
Device(device), Number(0
), FilenameTemplate(templateName), Node(node)
{
FilenameTemplate.replace ( '/'
, '_'
);
FilenameTemplate.replace ( '
\\
'
, '_'
);
}
bool
OnEvent(const
SEvent&
event)
{
// Vérifie si l'utilisateur presse la touche F9.
if
((event.EventType ==
EET_KEY_INPUT_EVENT) &&
event.KeyInput.PressedDown)
{
if
(event.KeyInput.Key ==
KEY_F9)
{
video::
IImage*
image =
Device->
getVideoDriver()->
createScreenShot();
if
(image)
{
c8 buf[256
];
snprintf(buf, 256
, "%s_shot%04d.jpg"
,
FilenameTemplate.c_str(),
++
Number);
Device->
getVideoDriver()->
writeImageToFile(image, buf, 85
);
image->
drop();
}
}
else
if
(event.KeyInput.Key ==
KEY_F8)
{
if
(Node->
isDebugDataVisible())
Node->
setDebugDataVisible(scene::
EDS_OFF);
else
Node->
setDebugDataVisible(scene::
EDS_BBOX_ALL);
}
}
return
false
;
}
private
:
IrrlichtDevice *
Device;
u32 Number;
core::
stringc FilenameTemplate;
ISceneNode*
Node;
}
;
II. Chargement du niveau▲
OK, commençons.
int
IRRCALLCONV main(int
argc, char
*
argv[])
{
Comme dans l'exemple Hello World, nous créons un IrrlichtDevice avec createDevice(). Maintenant, la différence est que nous demandons à l'utilisateur de choisir quel pilote d'accélération matérielle utiliser. Le moteur logiciel sera trop lent pour dessiner une grande carte Quake 3, mais juste pour le fun, nous rendons cette décision possible aussi.
// Demande à l'utilisateur un pilote.
video::
E_DRIVER_TYPE driverType=
driverChoiceConsole();
if
(driverType==
video::
EDT_COUNT)
return
1
;
// Crée le moteur et quitte si sa création échoue.
const
core::
dimension2du videoDim(800
,600
);
IrrlichtDevice *
device =
createDevice(driverType, videoDim, 32
, false
);
if
(device ==
0
)
return
1
; // Ne peut pas créer le pilote sélectionné.
const
char
*
mapname=
0
;
if
(argc>
2
)
mapname =
argv[2
];
else
mapname =
QUAKE3_MAP_NAME;
Obtenons un pointeur sur le pilote vidéo et le gestionnaire de scène ainsi nous n'aurons plus à écrire tout le temps device->getVideoDriver() et device->getSceneManager().
video::
IVideoDriver*
driver =
device->
getVideoDriver();
scene::
ISceneManager*
smgr =
device->
getSceneManager();
gui::
IGUIEnvironment*
gui =
device->
getGUIEnvironment();
device->
getFileSystem()->
addFileArchive("../../media/"
);
Pour afficher la carte Quake 3, nous devons d'abord la charger. Les niveaux de Quake 3 sont contenus dans des fichiers .pk3, qui ne sont rien d'autre que des fichiers .zip. Donc nous ajoutons les fichiers .pk3 dans notre FileSystem. Après les avoir ajoutés, nous sommes capables de lire les fichiers de ces archives comme s'ils étaient directement stockés sur le disque dur.
if
(argc>
2
)
device->
getFileSystem()->
QUAKE3_STORAGE_FORMAT(argv[1
]);
else
device->
getFileSystem()->
QUAKE3_STORAGE_FORMAT(QUAKE3_STORAGE_1);
#ifdef QUAKE3_STORAGE_2
device->
getFileSystem()->
QUAKE3_STORAGE_FORMAT(QUAKE3_STORAGE_2);
#endif
// Contrôle de Z-Writing de Quake3 Shader.
smgr->
getParameters()->
setAttribute(scene::
ALLOW_ZWRITE_ON_TRANSPARENT, true
);
Maintenant nous pouvons charger le mesh en appelant getMesh(). Nous obtenons alors le pointeur sur un IAnimatedMesh. Comme vous le savez, les cartes Quake 3 ne sont pas vraiment animées, il y a seulement un très grand morceau de géométrie statique avec quelques matériels attachés. Comme le mesh IAnimatedMesh n'a qu'une seule image, nous obtenons donc la « première image » de « l'animation » qui est notre carte Quake et créons un nœud de l'octree(1) pour la scène en utilisant addOctreeSceneNode(). L'octree optimise un peu la scène en essayant de dessiner seulement la géométrie visible. Une alternative à l'octree pourrait être un IMeshSceneNode qui dessinera toujours entièrement la géométrie du mesh sans optimisation. Essayons-le, utilisez addMeshSceneNode() à la place d'addOctreeSceneNode() et comparez-les primitives(2) dessinées par le pilote vidéo. (Il y a une méthode getPrimitiveCountDrawn() dans la classe IVideoDriver.) Notez que cette optimisation avec l'octree est utile seulement quand on dessine des grands meshs constitués de nombreuses géométries.
scene::
IQ3LevelMesh*
const
mesh =
(scene::
IQ3LevelMesh*
) smgr->
getMesh(mapname);
Ajoutons le mesh de géométrie à la scène (polygones et chemins). Les meshs de géométrie sont optimisés pour être affichés plus rapidement.
scene::
ISceneNode*
node =
0
;
if
(mesh)
{
scene::
IMesh *
const
geometry =
mesh->
getMesh(quake3::
E_Q3_MESH_GEOMETRY);
node =
smgr->
addOctreeSceneNode(geometry, 0
, -
1
, 4096
);
}
// Crée un receveur d'événements pour faire des captures d'écran.
CScreenShotFactory screenshotFactory(device, mapname, node);
device->
setEventReceiver(&
screenshotFactory);
Maintenant, créons un nœud de scène pour chaque Shader. Les objets sont stockés dans le mesh de Quake scene::E_Q3_MESH_ITEMS et l'identifiant du Shader est stocké dans le MaterialParameters souvent noir ressemblant à des crânes et de la lave mobile… ou des tubes verts clignottants ?
if
( mesh )
{
// Le mesh supplémentaire peut être assez grand et non optimisé.
const
scene::
IMesh *
const
additional_mesh =
mesh->
getMesh(quake3::
E_Q3_MESH_ITEMS);
#ifdef SHOW_SHADER_NAME
gui::
IGUIFont *
font =
device->
getGUIEnvironment()->
getFont("../../media/fontlucida.png"
);
u32 count =
0
;
#endif
for
( u32 i =
0
; i!=
additional_mesh->
getMeshBufferCount(); ++
i )
{
const
IMeshBuffer*
meshBuffer =
additional_mesh->
getMeshBuffer(i);
const
video::
SMaterial&
material =
meshBuffer->
getMaterial();
// Le ShaderIndex est stocké dans les paramètres du matériel.
const
s32 shaderIndex =
(s32) material.MaterialTypeParam2;
// Le tampon du mesh peut être dessiné sans support supplémentaire, ou sans shader.
const
quake3::
IShader *
shader =
mesh->
getShader(shaderIndex);
if
(0
==
shader)
{
continue
;
}
// Nous pouvons afficher le shader vers la console dans son état original
// ou l'afficher d'une plus belle manière grâce à son analyse
// ... Cette ligne est commentée, car la console sera remplie...
// quake3::dumpShader ( Shader );
node =
smgr->
addQuake3SceneNode(meshBuffer, shader);
#ifdef SHOW_SHADER_NAME
count +=
1
;
core::
stringw name( node->
getName() );
node =
smgr->
addBillboardTextSceneNode(
font, name.c_str(), node,
core::
dimension2d<
f32>
(80.0
f, 8.0
f),
core::
vector3df(0
, 10
, 0
));
#endif
}
}
Maintenant nous avons juste besoin d'une caméra pour regarder le niveau de Quake 3. Nous voulons créer une caméra contrôlée par l'utilisateur. Il y a plusieurs caméras disponibles dans le moteur Irrlicht. Par exemple, la « MayaCamera » peut être contrôlée comme une caméra Maya : rotation en appuyant sur le bouton droit de la souris, zoom avec deux boutons appuyés, translation en appuyant le bouton gauche. Ceci peut être créé avec addCameraSceneNodeMaya(). Mais pour cet exemple, nous voulons créer une caméra qui se comporte comme celles des jeux de tir à la première personne (FPS(3)).
scene::
ICameraSceneNode*
camera =
smgr->
addCameraSceneNodeFPS();
III. Recherche d'une position de départ▲
Nous avons besoin d'une bonne position de départ dans le niveau. Nous pouvons demander au chargeur Quake 3 toutes les entrées avec class_name ayant pour valeur "info_player_deathmatch" nous choisissons un point de départ au hasard.
if
( mesh )
{
quake3::
tQ3EntityList &
entityList =
mesh->
getEntityList();
quake3::
IEntity search;
search.name =
"info_player_deathmatch"
;
s32 index =
entityList.binary_search(search);
if
(index >=
0
)
{
s32 notEndList;
do
{
const
quake3::
SVarGroup *
group =
entityList[index].getGroup(1
);
u32 parsepos =
0
;
const
core::
vector3df pos =
quake3::
getAsVector3df(group->
get("origin"
), parsepos);
parsepos =
0
;
const
f32 angle =
quake3::
getAsFloat(group->
get("angle"
), parsepos);
core::
vector3df target(0.
f, 0.
f, 1.
f);
target.rotateXZBy(angle);
camera->
setPosition(pos);
camera->
setTarget(pos +
target);
++
index;
notEndList =
( index <
(s32) entityList.size () &&
entityList[index].name ==
search.name &&
(device->
getTimer()->
getRealTime() >>
3
) &
1
);
notEndList =
index ==
2
;
}
while
( notEndList );
}
}
Le curseur de la souris n'a pas besoin d'être visible, nous la rendons donc invisible.
device->
getCursorControl()->
setVisible(false
);
// On charge le logo du moteur.
gui->
addImage(driver->
getTexture("irrlichtlogo2.png"
),
core::
position2d<
s32>
(10
, 10
));
// On montre le logo du moteur.
const
core::
position2di pos(videoDim.Width -
128
, videoDim.Height -
64
);
switch
( driverType )
{
case
video::
EDT_BURNINGSVIDEO:
gui->
addImage(driver->
getTexture("burninglogo.png"
), pos);
break
;
case
video::
EDT_OPENGL:
gui->
addImage(driver->
getTexture("opengllogo.png"
), pos);
break
;
case
video::
EDT_DIRECT3D8:
case
video::
EDT_DIRECT3D9:
gui->
addImage(driver->
getTexture("directxlogo.png"
), pos);
break
;
}
Nous avons tout fait, donc dessinons le tout. Nous écrivons aussi le nombre d'images par seconde ainsi que les primitives dans le titre de la fenêtre. La ligne 'if (device->isWindowActive())' est optionnelle, mais évite au moteur de capturer le curseur de la souris après un changement de tâche lorsque d'autres programmes sont actifs.
int
lastFPS =
-
1
;
while
(device->
run())
if
(device->
isWindowActive())
{
driver->
beginScene(true
, true
, video::
SColor(255
,20
,20
,40
));
smgr->
drawAll();
gui->
drawAll();
driver->
endScene();
int
fps =
driver->
getFPS();
//if (lastFPS != fps)
{
io::
IAttributes *
const
attr =
smgr->
getParameters();
core::
stringw str =
L"Q3 ["
;
str +=
driver->
getName();
str +=
"] FPS:"
;
str +=
fps;
str +=
" Cull:"
;
str +=
attr->
getAttributeAsInt("calls"
);
str +=
"/"
;
str +=
attr->
getAttributeAsInt("culled"
);
str +=
" Draw: "
;
str +=
attr->
getAttributeAsInt("drawn_solid"
);
str +=
"/"
;
str +=
attr->
getAttributeAsInt("drawn_transparent"
);
str +=
"/"
;
str +=
attr->
getAttributeAsInt("drawn_transparent_effect"
);
device->
setWindowCaption(str.c_str());
lastFPS =
fps;
}
}
À la fin, nous supprimons le moteur Irrlicht.
device->
drop();
return
0
;
}
IV. Conclusion▲
Vous pouvez désormais charger une carte Quake 3 en utilisant Irrlicht.
Dans le prochain tutoriel Hello World pour mobile, nous verrons comment montrer « Hello World » pour les mobiles Windows.
V. Remerciements▲
Merci à Nikolaus Gebhardt de nous permettre de traduire ce tutoriel.
Merci à LittleWhite pour sa relecture technique ainsi qu'à ClaudeLELOUP pour sa relecture orthographique.