1. Pré-requis

Cet article explique comment débuter avec kjAPI. Il est du niveau débutant (mes amis professionnels y jetteront peut être aussi un oeil critique :o) ).

Les pré-requis sont les suivants :

  1. Avoir des connaissances en C++ (les bases devraient suffire)
  2. Une version de Visual Studio supérieure à 6.0 (le tutoriel utilise la version 7.1, mais les paramètres sont similaires pour les autres versions)
    (note : il est également possible de compiler avec Gcc)
  3. La version telechargeable de la kjAPI (http://dev.kjapi.com/)

2. Mise en place

Avant tout il faut installer la kjAPI. Ensuite il nous faut configurer l'environnement de développement :

a. Lancez Visual Studio
b. Ouvrez les options (menu Tools > Options) puis sélectionnez la catégorie Projects > VC++ Directories. Ajoutez le chemin des fichiers d'en-têtes de votre installation kjAPI (voir figure)

Image non disponible

c. De la même façon ajoutez les chemins vers les répertoires de bibliothèques.

Image non disponible

Ces réglages resteront valides pour tous les projets kjAPI. Il n'est donc pas nécessaire de les renouveler à chaque projet.

3. Création du projet

Vous allez créer un nouveau projet.
Allez dans File > New > Project puis choisissez Visual C++ projects > Win32 project.

Choisissez un nom de répertoire puis un nom de projet puis cliquez sur OK.

Image non disponible

Dans la fenêtre suivante, allez dans "Application settings" puis sélectionnez "Win32 application" et "Empty project".

Appuyez sur Terminer

Image non disponible

Il nous faut maintenant configurer les propriétés du projet. Il suffit de changer une seule option du fait que les bibliothèques kjAPI sont compilées pour le multithread.
Allez dans le menu Project > Properties puis dans la catégorie C/C++ > Code generation, changez l'option "runtime library".

(Faites la même chose pour la configuration release.)

Image non disponible

Le projet est maintenant prêt. Nous pouvons y ajouter le plus intéressant : les fichiers sources.

4. Initialisation

kjAPI fournit une classe très utile pour commencer un projet : la classe kjGameFramework.
Cette classe est une classe "abstraite". Il nous faut donc créer une classe personnalisée qui redéfinira le comportement des quelques fonctionnalités fournies par kjGameFramework.

Nous appellerons cette classe Application (je n'entrerai pas en détail sur les raisons du choix de ce nom :) )

Comme le projet ne contient aucun fichier source. Créez un fichier Application.h et ajoutez-le au projet. Dans ce fichier nous allons définir la classe Application.

 
Sélectionnez

#ifndef __APPLICATION_H__
#define __APPLICATION_H__

#include <kjGameFramework.h>

class Application : public kjGameFramework  
{
protected:
	virtual kjRenderer* OnCreateRenderer(const kjStr& RendererName);

public:
	Application();

	virtual bool Init(const char* lpstrGameName);
	virtual bool UpdateFrame();
	virtual void DoFrame();
	virtual void Release();
};

#endif

Comme vous le constatez la définition est plutôt simple. Nous redéfinissons 5 méthodes :

OnCreateRenderer :
Cette méthode permet à l'utilisateur de créer la fenêtre de l'application et de choisir le moteur de rendu qu'il souhaite. Elle est appelée automatiquement par kjGameFramework::Init().

Init :
Cette méthode permet d'initialiser l'application.

UpdateFrame :
Cette méthode est appelée a chaque "frame" avant la méthode de tracé. Elle permet de mettre à jour les états de l'application.

DoFrame :
Cette méthode est appelée à chaque frame pour le tracé de l'application

Release :
Cette méthode est appelée en fin d'application pour permettre la libération des ressources allouées.

Avant d'entrer en détail dans l'implémentation des ces méthodes attardons nous sur le point d'entrée de l'application.
L'application étant une application Win32 nous devons définir une fonction WinMain qui sera appelée par Windows lors du démarrage de l'application. C'est dans cette fonction que nous allons créer l'instance de l'application.
Cette fonction sera implémentée dans le fichier Application.cpp.
Comme ce fichier n'est pas présent dans le projet il faut donc le créer et l'ajouter à ce dernier.

Une fois fait ajoutez le code suivant :

 
Sélectionnez

#include "Application.h"

#include <windows.h>

//---------------------------------------------------------------------------------
/// Entry point of program
//---------------------------------------------------------------------------------
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	Application Game;

	Game.Init("kjAPI - Tutorial #1");
	Game.Run();
	Game.Release();

	return 0;
}

Comme vous le constatez le code parle de lui-même.
Une instance de la classe Application est créée sur la pile (elle sera donc détruite automatiquement à la sortie de la fonction WinMain).
La méthode Init() est appelée ensuite.
Puis vient l'appel de méthode Run() qui va entrer dans une boucle qui durera tant que l'utilisateur n'aura pas donné l'ordre de quitter l'application.
En interne cette méthode Run() appelle à tour de rôle la méthode UpdateFrame() et DoFrame(). Si UpdateFrame() renvoie true alors la boucle est interrompue et l'exécution redonne la main à la fonction WinMain pour la suite des événements.
Afin de libérer "proprement" les ressources allouées la méthode Release() est appelée.
Puis l'application se termine.

Maintenant que nous avons tout le code de démarrage de l'application il ne nous reste plus qu'à nous concentrer sur l'implémentation de chaque méthode de la classe Application.

Pour rendre le code moins ennuyeux et que l'effort soit récompensé, l'application aura pour but d'afficher un modèle en 3D et que l'utilisateur soit en mesure de tourner autour au moyen du clavier afin de l'observer.

Nous allons donc modifier légèrement la classe Application pour avoir tout ce qu'il nous faut.
Nous avons besoin d'une caméra de type RTS (qui orbite autour d'un point défini) ainsi que d'un modèle.
Pour la caméra nous allons utiliser la classe kjRTSCamera qui fournit tous les services désirés.
Pour le modèle nous avons besoin d'un pointeur sur la classe kjNode (qui permet de gérer des hiérarchies de modèles).

Voici le nouveau Application.h :

 
Sélectionnez

#ifndef __APPLICATION_H__
#define __APPLICATION_H__

#include <kjGameFramework.h>
#include <kjRTSCamera.h>

class kjNode;

class Application : public kjGameFramework  
{
private:
	kjRTSCamera m_Camera;
	kjNode*     m_pNode;

protected:
	virtual kjRenderer* OnCreateRenderer(const kjStr& RendererName);

public:
	Application();

	virtual bool Init(const char* lpstrGameName);
	virtual bool UpdateFrame();
	virtual void DoFrame();
	virtual void Release();
};

#endif

5. La méthode OnCreateRenderer()

La méthode OnCreateRenderer est simple : elle doit créer une instance de moteur de rendu et y attacher une fenêtre pour que l'affichage soit possible.
Pour cela elle utilise les variables membres de la classe kjGameFramework.

 
Sélectionnez

kjRenderer* Application::OnCreateRenderer(const kjStr& RendererName)
{
	kjRenderer *pRdr = NULL;

	pRdr = kjNew(kjDx9Renderer, KJ_MEMPOOL_GRAPHICS);
	pRdr->CreateAndAttachWindow("kjAPI - [DX9 renderer]",
	                            m_XRes, m_YRes, m_Depth, m_Stencil);

	return pRdr;
}

Le moteur de rendu est géré par la classe abstraite kjRenderer. Il est possible de créer deux types de moteur de rendu (OpenGL ou DirectX), la classe abstraite permettant d'avoir une seule et unique interface d'utilisation quelque soit le choix initial.
La création du moteur de rendu est une instanciation de la classe kjDx9Renderer.
Vous remarquerez que l'opérateur C++ new n'a pas été employé. En effet kjAPI utilise des macros définitions pour les allocations et désallocations, qui lui permettent de traquer plus facilement les fuites de mémoire et de gérer la mémoire à sa façon.

Il nous faut ensuite créer une fenêtre et l'associer au moteur de rendu. La création de la fenêtre est un simple appel a la methode CreateAndAttach de la class kjRenderer.
Les paramètres sont la taille en x et y de la fenêtre, la précision du buffer de profondeur et l'utilisation ou non du stencil buffer. Il faut également préciser si l'on veut un mode fenêtre ou plein ecran.
Ces valeurs sont définies par défaut et peuvent être modifiées au bon vouloir de chacun.

6. La méthode Init()

Cette méthode est assez importante car elle va nous permettre de créer les ressources nécessaires à l'application.
Dans notre cas nous avons besoin d'initialiser le clavier (pour les contrôles utilisateurs), la camèra pour avoir un oeil dans le monde 3D et le modèle lui-même.

 
Sélectionnez

bool Application::Init(const char* lpstrGameName)
{
	if (kjGameFramework::Init(lpstrGameName) == false)
	{
		kjAlert("Unable to init framework");
		return false;
	}

	// Init input manager
	GetkjCore()->CreateInputManager(false, GetRenderer->GetAttachedWindow());

	// Init camera
	m_Camera.Init(kjVector(0,1,0), 30, 45, 2);

	// Load file
	m_pNode = kjNode::LoadHierarchyFromFile("Datas/skeleton.kjh");
	if (m_pNode == NULL)
	{
		kjAlert("unable to load hierarchy file");
		return false;
	}

	GetRenderer->SetClearColor(0.3f,.4f,.5f);
	return true;
}

Avant toute chose, nous devons appeler la méthode Init() de la classe kjGameFramework qui fait le plus gros du travail d'initialisation de kjAPI. En effet elle initialise les bibliothèques nécessaires (kjCore, kjGraphics) et surtout elle appelle la méthode OnCreateRenderer() qui nous l'avons vu est vitale à l'affichage.

Une fois cet appel effectué avec succès, nous pouvons initialiser le clavier.
Pour cela nous devons initialiser le gestionnaire d'entrées (kjInputManager) qui initialisera à son tour clavier et souris.
Cette initialisation se fait en une ligne de code via la bibliothèque kjCore. Le 1er paramètre indique que nous n'avons pas besoin de DirectInput et le deuxième est le pointeur sur la fenêtre attachée au moteur de rendu.
Nous verrons comment accéder à l'InputManager et au clavier dans la méthode UpdateFrame().

Vient le tour de la camèra : de la même façon, une seule ligne de code suffit à initilialiser cet objet.
Le premier paramètre est la position du centre autour duquel la caméra tournera. Les deux paramètres suivant étant les angles (en degrés) de la caméra autour de ce centre (en coordonnées sphériques). Et enfin le dernier paramètre est la distance de la caméra par rapport au centre. (cf figure)

Image non disponible

Nous allons créer le modèle à partir d'un fichier kjh (fichier contenant une hiérarchie) (ce fichier a été créé grâce à l'exporteur 3DS Max de la kjAPI). Un fichier kjh a des liens vers d'autres fichiers de ressources (textures et géometrie).

Ces fichiers de données se trouvent en téléchargement ici (fichier zip, 225 Ko).

Pour charger le modèle en mémoire nous utilisons donc la méthode statique kjNode::LoadHierarchy(). Il suffit de spécifier le fichier à charger et la fonction s'occupe de tout (création de la géométrie, allocation des ressources graphiques, chargement des textures ....).
Avec la kjAPI il vous est également possible d'accéder à toutes les fonctions bas-niveau du moteur de rendu si vous preferez créer votre modèle de façon procédurale.
La méthode de chargement renvoie donc un pointeur sur une instance de classe kjNode ou NULL si une erreur s'est produite.

Une fois les opérations couronnées de succès, nous pouvons aussi définir la couleur de fond du moteur de rendu grâce a la méthode SetClearColor(). Les paramètres correspondent aux 3 composantes de couleurs (rouge, vert, bleu) dans l'intervalle [0, 1].

L'initialisation de notre application se termine ici et nous pouvons étudier de plus près la méthode UpdateFrame().

7. La méthode UpdateFrame()

Comme dit précédemment cette fonction est appelée à chaque frame pour permettre à l'application de mettre à jour l'état de celle ci.
Dans notre cas l'état est simple : c'est celui de la position de la camera autour du modèle en fonction des entrées clavier.
Regardons de plus près cette méthode :

 
Sélectionnez

bool Application::UpdateFrame()
{
	// return true means exit of application
	if (kjGameFramework::UpdateFrame() == true)
		return true;

	// Controls
	kjKeyboard* pKbd = GetkjCore()->GetInputManager()->GetKeyboard();
	float Turn = GetkjCore()->GetElapsed() * 180.f;
	if (pKbd->IsKeyDown(KJ_KBD_LEFT))  m_Camera.SetYAngle(m_Camera.GetYAngle() - Turn);
	if (pKbd->IsKeyDown(KJ_KBD_RIGHT)) m_Camera.SetYAngle(m_Camera.GetYAngle() + Turn);
	if (pKbd->IsKeyDown(KJ_KBD_UP))    m_Camera.SetXAngle(m_Camera.GetXAngle() + Turn * 0.5f);
	if (pKbd->IsKeyDown(KJ_KBD_DOWN))  m_Camera.SetXAngle(m_Camera.GetXAngle() - Turn * 0.5f);
	m_Camera.Update();

	// Update geometry (needed for morphed or skinned geometries)
	if (m_pNode)
		m_pNode->Update(GetkjCore()->GetElapsed());

	// return false because we dont need to quit yet 
	return false;
}

Pour commencer, il vous faut appeler la méthode UpdateFrame() de la classe kjGameFramework. Cet appel met à jour principalement la bibliothèque kjCore qui s'occupe de la gestion du temps de l'application et de la mise à jour des divers managers associés (comme l'InputManager par exemple).

Pour accéder au clavier, nous devons utiliser l'InputManager que nous avons créé dans la methode Init(). La méthode GetInputManager() de kjCore nous permet de récupérer un pointeur sur l'instance et d'appeler la méthode GetKeyboard() qui nous renverra l'instance de la classe kjKeyboard dont nous avons besoin.

La classe kjKeyboard possède un état de chaque touche. Une touche peut être appuyée ou relâchée. La classe conserve également un état qui permet de savoir si la touche vient d'être pressée ou vient d'être relâchée (ces états sont valides le temps d'une frame). Dans notre cas nous avons uniquement besoin de savoir si la touche est enfoncée ou non. La méthode IsKeyDown() rempli parfaitement le rôle. Il suffit de lui passer le code de touche à tester. Ici nous testons les touches flèchées (voir le fichier kjKeyboard.h pour la liste complète de tous les codes de touches disponibles).

Pour "animer" la camera en fonction des touches nous calculons d'abord le déplacement éventuel de la caméra en fonction du temps écoulé depuis la dernière frame. Ainsi la caméra tournera toujours à la même vitesse quelque soit la vitesse d'affichage.
Le temps écoulé depuis la dernière frame s'obtient grâce à la méthode GetElapsed() de la bibliothèque kjCore. La valeur retournée est en seconde.
En la multipliant par 180, nous pouvons déterminer de quel angle la caméra doit se pivoter (180 correspond donc à la vitesse angulaire de la camera soit 180 degrés par seconde).
La série d'instruction If permet de définir le mouvement en fonction du clavier. Ainsi la flèche de gauche décrémente l'angle Y de la caméra de la valeur calculée précédemment.

Une fois les paramètres de la caméra modifiés, il nous faut les valider à l'aide de la méthode Update() de la classe caméra. Cette fonction recalcule en interne la matrice associée à la caméra que nous utiliserons lors du DoFrame().

De la même façon il nous faut appeler la méthode Update() sur le modèle afin de mettre à jour les éventuels éléments dynamiques.

La méthode renvoie false pour indiquer que nous ne souhaitons pas quitter l'application.

8. La méthode DoFrame()

Cette méthode est plutôt simple à mettre en place dans notre cas.

 
Sélectionnez

void Application::DoFrame()
{
	kjGameFramework::DoFrame();

	// Set Matrices
	GetRenderer->SetCameraMatrix(m_Camera.GetViewMatrix());
	GetRenderer->SetWorldMatrix(kjMatrix::IDENTITY);

	// Draw model
	if (m_pNode)
		m_pNode->Draw();
}

La première étape consiste à appeler la méthode DoFrame() de la classe kjGameFramework (une impression de déjà vu ? :) ). Elle initialise la matrice de projection ainsi que le moteur de rendu pour qu'il se tienne prêt à tracer.

Comme nous utilisons notre propre caméra nous devons spécifier au moteur de rendu de l'utiliser. Cela se fait par l'envoi de la matrice de la caméra au moteur de rendu via la méthode SetCameraMatrix.
Nous nous assurons également que la matrice des objets (matrice monde) soit bien initialisée avec la matrice identité (kjMatrix::IDENTITY est une constante définie par la kjAPI).

Nous pouvons enfin tracer notre modèle grâce à sa méthode Draw(). kjAPI se charge de tous les appels graphiques pour vous.

9. La méthode Release()

Une fois l'application terminée, cette méthode est appelée afin que nous puissions faire le ménage.

 
Sélectionnez

void Application::Release()
{
	kjDelete(m_pNode);
	kjGameFramework::Release();
}

Dans notre cas, nous n'avons que le modèle a libérer. La kjAPI utilisant sa propre gestion de mémoire nous devons utiliser la macro définition kjDelete.
L'appel de la méthode Release() du kjGameFramework se fait (une fois n'est pas coutumes) après. Car il nous faut libérer nos ressources avant que kjAPI elle-même soit libérée.

10. Constructeur et dépendances

N'oubliez pas le constructeur de la classe Application qui se doit d'initialiser les variables tout simplement :

 
Sélectionnez

Application::Application():m_pNode(NULL)
{
}

L'utilisation de toutes ces classes n'est pas sans conséquences. Il nous faut donc inclure les fichiers d'en-têtes nécessaires.
Il faut donc ajouter au début de Application.cpp :

 
Sélectionnez

#include "Application.h"

#include <windows.h>
#include <kjNode.h>
#include <kjInputManager.h>
#include <kjKeyboard.h>
#include <kjDx9Renderer.h>
#include <kjWindowDx8Win32.h>

#pragma comment(lib,"kjCore.lib")
#pragma comment(lib,"kjGraphics.lib")
#pragma comment(lib,"kjGraphicsDx9.lib")

Les commandes du préprocesseur #pragma comment permettent de spécifier les bibliothèques à lier au programme. Le paramétrage de l'environnement de développement permet d'éviter de spécifier leur chemin complet.

11. Conclusion

J'espère que ce tutoriel aura permis à certains de découvrir la kjAPI et donné l'envie d'aller plus loin avec.
Il est possible de se passer de la classe kjGameFramework pour créer une application, permettant d'avoir plus de contrôle sur les opérations effectuées.
N'hésitez pas à me faire savoir quel type de tutoriel vous aimeriez voir dans ces colonnes.

Cédric

12. Téléchargements

Version PDF de ce tutoriel (12 pages, 188 Ko) : télécharger.

Modèle de squelette utilisé par le projet (fichier zip, 225 Ko) : télécharger.