Tutoriel/Créer un objet
Ce tutoriel décrit comment créer un objet pour être utilisé dans les niveaux. Cet objet sera une simple ampoule entourée d'un halo dont l'intensité oscille. Comme tous les objets utilisés dans les niveaux, celui-ci hérite de la classe base_item. Le sprite de l'ampoule et du halo seront récupérés directement dans le code, tandis que la variation d'intensité sera paramétrée depuis l'éditeur de niveaux. Celle-ci se traduira dans le jeu par un changement d'opacité du halo.
Sommaire |
Environnement de travail
Pour le développement de cet objet, nous allons nous placer dans l'environnement obtenu à l'issue du tutoriel « Préparer une personnalisation du jeu ». Nous allons adapter le résultat de l'extraction de l'archive custom-bear.tar.gz de manière à créer une bibliothèque d'objets pour le résultat de ce tutoriel.
Nous allons créer une nouvelle arborescence dans le répertoire ./lib/ afin d'y stocker les fichiers de ce tutoriel. Nous créons tout d'abord deux dossiers nommés ./lib/src/tutorial et ./lib/src/tutorial/code. Le premier contiendra les fichiers d'entête de nos classes et le second contiendra les fichiers d'implémentation. Ensuite, nous créons deux fichiers pour CMake : ./lib/src/CMakeLists.txt indique à CMake de traiter le sous-dossier ./lib/src/tutorial/. Son contenu est :
subdirs(tutorial)
Ensuite, nous ajoutons le fichier ./lib/src/tutorial/CMakeLists.txt pour compiler les fichiers de notre bibliothèque. Son contenu est le suivant :
cmake_minimum_required(VERSION 2.6) project(tutorial) include_directories( "${CMAKE_CURRENT_SOURCE_DIR}" ${BEAR_ENGINE_INCLUDE_DIRECTORY} ) link_directories( ${BEAR_ENGINE_LINK_DIRECTORY} ) file( GLOB TUTORIAL_SOURCE_FILES code/*.cpp ) add_library( tutorial SHARED ${TUTORIAL_SOURCE_FILES} ) target_link_libraries( tutorial bear_engine )
La commande include_directories dans ce fichier indique à CMake d'utiliser le répertoire courant pour rechercher les fichiers d'entête, ainsi que le dossier des entêtes du moteur de jeu. La commande link_directories de la ligne suivante lui indique le dossier où sont situées les bibliothèques de ce moteur. Vient ensuite la commande file qui permet ici de récupérer tous les fichiers d'extension .cpp dans le sous dossier code. Les noms de ces fichiers sont stockés dans la variable TUTORIAL_SOURCE_FILES pour être ensuite passés à la commande add_library. Cette dernière permet de créer une bibliothèque à partir du code des fichiers donnés en paramètres. Cett bibliothèque s'appellera libtutorial.so sous Linux. Enfin, la commande target_link_libraries indique à CMake de lier cette bibliothèque d'objets avec la bibliothèque principale du moteur de jeu.
Images de l'ampoule
Nous allons créer une petite ampoule et un halo en utilisant The Gimp. L'image obtenue après redimensionnement et composition est affichée ci-contre. Nous créons le fichier .spritepos ci-dessous pour facilement y récupérer la position et la taille des sprites.
halo: 0 0 64 64 light bulb: 64 0 31 52
Les tutoriels « Créer une animation » et « Script Scheme » abordent plus en détail la réalisation de graphismes.
Code de la classe ampoule
Comme tout objet évoluant dans un niveau, notre classe doit hériter de base_item. Nous y définissons des variables membres pour stocker les sprites, l'intensité et tous les autres éléments nécessaires à l'évolution et à l'affichage de l'objet. Nous nommerons notre ampoule « light_bulb ».
Trois fonctions virtuelles de base_item vont immédiatement nous intéresser. La fonction base_item::build() est appelée pour initialiser l'objet lorsqu'il est ajouté dans un calque de niveau. Nous l'utiliserons pour initialiser les variables membres de notre classe. La seconde fonction est base_item::progress(e). Elle est appelée régulièrement pour faire évoluer l'objet et prend en paramètre le temps écoulé depuis le dernier appel. Enfin, la fonction base_item::get_visual(v) est appelée lors de l'affichage pour récupérer dans son paramètre la représentation visuelle de l'objet.
La définition de notre classe se présente alors comme suit :
#include "engine/base_item.hpp" #include "engine/export.hpp" /** * \brief Une ampoule avec un halo dont l'intensité varie. * \author Julien Jorge */ class light_bulb: public bear::engine::base_item { DECLARE_BASE_ITEM(light_bulb); public: void build(); void progress( bear::universe::time_type elapsed_time ); void get_visual( std::list<bear::engine::scene_visual>& visuals ) const; private: /** \brief Intensité minimum du halo. */ double m_min_intensity; /** \brief Intensité maximum du halo. */ double m_max_intensity; /** \brief Temps écoulé depuis la création de l'objet. */ bear::universe::time_type m_elapsed_time; /** \brief The sprite of the bulb. */ bear::visual::sprite m_bulb; /** \brief The sprite of the halo. */ bear::visual::sprite m_halo; }; // class light_bulb
Le détail des fonction est décrit dans les sections suivantes. D'une manière générale, une bonne pratique consiste à commencer chacune de ces fonctions par un appel à son équivalent dans la classe mère, afin d'avoir une initialisation, une évolution et un affichage complet correspondant au sens de cette classe. La ligne mise en évidence est indispensable pour utiliser l'objet dans un niveau. Elle va de paire avec la ligne suivante, devant être ajoutée dans le fichier .cpp d'implémentation :
BASE_ITEM_EXPORT_NO_NAMESPACE( light_bulb )
Ces deux commandes sont déclarées dans le fichier engine/export.hpp.
Initialisation de l'ampoule et fonction membre build()
L'initialisation de l'objet consiste à récupérer les sprites de l'ampoule et du halo. Pour cela, le plus simple est d'utiliser la fonction level_globals::auto_sprite() pour récupérer un sprite dans une image à partir du nom donné dans le fichier .spritepos associé. Les autres variables membres sont initialisées dans le constructeur. L'intervalle de la variation d'intensité est arbitrairement choisi égal à [0,25 ; 0,90].
| Détails (m) | |
| |
L'appel à base_item::get_level_globals() ne peut être effectué sur une instance de base_item avant l'appel à la fonction build() ou pre_cache() de l'objet. En pratique, cela signifie que les sprites, les animations, les modèles ou les sons ne peuvent être initialisés dans le constructeur de l'objet. On retrouvera la même contrainte pour la fonction base_item::get_level(). |
Le code de ces fonctions est détaillé ci-dessous :
/** * \brief Constructeur. */ light_bulb::light_bulb() : m_elapsed_type(0), m_min_intensity(0.25), m_max_intensity(0.9) { } /** * \brief Initialise l'objet. */ void light_bulb::build() { // initialisation de la classe mère bear::engine::base_item::build(); m_bulb = get_level_globals().auto_sprite("gfx/light_bulb-spr.png", "light bulb"); m_halo = get_level_globals().auto_sprite("gfx/light_bulb-spr.png", "halo"); }
Une bonne pratique pour éviter que le chargement des images se fasse pendant le jeu et le ralentisse est de demander leur chargement dans la méthode base_item::pre_cache() comme suit :
/** * \brief Pré-chargement des ressources de l'objet. */ void light_bulb::pre_cache() { // Pré-chargement des ressources de la classe mère bear::engine::base_item::pre_cache(); get_level_globals().load_image("gfx/light_bulb-spr.png"); }
Évolution de l'ampoule, fonction membre progress()
L'évolution de l'ampoule se limite à faire varier l'intensité du halo. En pratique c'est l'opacité du halo qui va varier. La variation suit une sinusoïde fonction du temps écoulé. Le code de la fonction est détaillé ci-dessous :
/** * \brief Effectue une étape dans l'évolution de l'objet. * \param elapsed_time Temps écoulé depuis le dernier appel. */ void light_bulb::progress( bear::universe::time_type elapsed_time ) { // évolution de la classe mère bear::engine::base_item::progress(elapsed_time); // temps écoulé depuis la création de l'objet m_elapsed_time += elapsed_time; // mise à jour de l'opacité dans l'intervalle choisi m_halo.set_opacity ( m_min_intensity + (m_max_intensity - m_min_intensity) * std::sin(m_elapsed_time) ); }
Affichage de l'ampoule, fonction membre get_visual()
La méthode la plus simple pour afficher un sprite consiste à construire un objet de type bear::visual::scene_sprite. Cette classe est un scene_element représentant un sprite associé à une position. La position à indiquer est celle du coin en bas à gauche du sprite, relativement au repère du monde. L'ajout du scene_sprite à la liste passée en paramètre à la fonction se fait par l'intermédiaire d'un objet de type scene_visual, qui permet d'indiquer, quand cela est nécessaire, la position du scene_element dans la profondeur d'affichage. En dehors de cette information, les éléments seront affichés selon l'ordre de la liste.
Dans le code ci-dessous, les sprites sont centrés par rapport à l'objet et ajoutés sans indication de profondeur :
/** * \brief Récupère les visuels de l'objets. * \param visuals (sortie) Les visuels, positionnés dans le repère du monde. */ void light_bulb::get_visual ( std::list<bear::engine::scene_visual>& visuals ) const { // récupération des visuels de la classe mère bear::engine::base_item::get_visual(visuals); visuals.push_back ( bear::engine::scene_visual ( bear::visual::scene_sprite ( get_horizontal_middle() - m_bulb.width() / 2, get_vertical_middle() - m_bulb.height() / 2, m_bulb ) ) ); visuals.push_back ( bear::engine::scene_visual ( bear::visual::scene_sprite ( get_horizontal_middle() - m_halo.width() / 2, get_vertical_middle() - m_halo.height() / 2, m_halo ) ) ); }
Intégration à l'éditeur de niveaux
Le lien entre l'objet et l'éditeur de niveaux se fait en deux parties. D'une part l'éditeur à besoin d'un fichier de description d'objet pour connaître son héritage ainsi que le nom et les types de ses paramètres. D'autre part, des fonctions membres pour recevoir ces valeurs doivent être implémentées dans la classe de l'objet. Nous supposons ici que nous voulons pouvoir paramétrer l'intensité minimale et maximale du halo.
Fichier de description de l'ampoule
Les fichiers de description d'objet sont de simple fichiers XML dont le contenu est décrit par ce schéma. Ils doivent être placés dans un dossier à indiquer dans le dialogue de configuration des éditeurs, dans la première partie, nommée « Chemin des fichiers de classes d'objets » (voir aussi la section « Première fois » de la page de l'éditeur de niveaux).
La balise à la racine du fichier est item. Le nom de la classe doit être noté dans l'attribut class et une catégorie doit être indiqué dans l'attribut category :
<item class="light_bulb" category="tutoriel">Vient ensuite la liste des classes dont light_bulb hérite, ainsi qu'une description de l'objet :
<inherit> <class>bear::base_item</class> </inherit> <description>L'ampoule du tutoriel.</description>
Enfin, la liste des attributs de l'ampoule est donnée entre les balises fields, en utilisant une balise field par attribut. Cette dernière balise prend un attribut type indiquant le type du champ et un autre nommé name pour donner son nom. Par convention, le nom du champ est préfixé par le nom de la classe.
<fields> <field type="real" name="light_bulb.min_intensity"> <default_value>0.25</default_value> <description>L'intensité minimale du halo.</description> </field> <field type="real" name="light_bulb.max_intensity"> <default_value>0.90</default_value> <description>L'intensité maximale du halo.</description> </field> </fields> </item>
Traitement des paramètres par light_bulb
Le paramétrage de l'objet se fait alors par l'appel des méthodes virtuelles d'affectation de champ, telles que définies dans la section « Instanciation depuis l'éditeur de niveaux » de la page de base_item. Les champs traités par light_bulb sont de type réel, nous n'avons donc qu'une seul méthode à implémenter, détaillée ci-dessous :
/** * \brief Affecte une valeur à un champ de type « réel ». * \param name Le nom du champ. * \param value La valeur à affecter au champ. */ bool light_bulb::set_real_field( const std::string& name, double value ) { bool result(true); if ( name == "light_bulb.min_intensity" ) m_min_intensity = value; else if ( name == "light_bulb.max_intensity" ) m_max_intensity = value; else result = bear::engine::base_item::set_real_field(name, value); return result; }
Cette fonction teste simplement tous les noms de champs de type « réel », tels qu'ils ont été indiqués dans le fichier de description d'objet. La ligne mise en évidence est un appel à la fonction de la classe mère pour traiter le cas où le nom du champ ne correspond pas à un champ de light_bulb. La valeur retournée par la méthode indique si la valeur donnée a bien été affectée au champ désiré.
| Attention (m) | |
| |
Ces méthodes sont appelées entre la création de l'objet et son ajout au niveau. Il n'est donc pas possible d'y appeler les fonctions level_object::get_level_globals() ni level_object::get_level(). |
Test de l'objet
L'image ci-contre montre l'ampoule en action, dans le niveau construit dans le tutoriel « Créer un niveau ». Nous pouvons remarquer que le halo n'est pas parfaitement positionné par rapport à l'ampoule, ce qui demandera quelques ajustements dans la fonction light_bulb::get_visual().
Le code et le niveau sont disponibles dans l'archive tuto-item.tar.gz. Il suffit d'extraire l'archive directement à la racine du dossier créé par l'archive custom-bear.tar.gz obtenue à l'issue du tutoriel « Préparer une personnalisation du jeu » pour pouvoir tester le jeu en lançant l'une des commandes ci-dessous.
Avec le script fourni dans l'archive :
sh run-game.sh --start-level=level/tuto-item.cl
Avec le jeu Plee the Bear installé :
plee-the-bear --item-library=lib/bin/libtutorial.so --data-path=./data --set-game-var-uint=scenario/players_count=1 --start-level=level/tuto-item.cl
Cette dernière commande fonctionnera aussi si vous ne passez pas par l'archive custom-bear.tar.gz, modulo l'adaptation de la valeur du paramètre --data-path.
Pour aller plus loin
L'objet créé dans ce tutoriel évolue dans le niveau indépendamment des autres objets. Pour procéder à des interactions avec le reste, plusieurs pistes sont envisageables. Tout d'abord, la méthode base_item::collision() est appelée quand l'objet entre en collision avec un autre. D'autre part, la brique d'objet item_with_toggle permet de faire varier le comportement de l'objet selon un état binaire, défini par un activateur. Par exemple, en faisant hériter l'ampoule de item_with_toggle, il sera possible d'implémenter facilement un comportement d'ampoule allumée ou éteinte, comme dans le tutoriel sur les objets activables.
| Liste des Tutoriels (m) |
| Préparer une personnalisation du jeu · Créer un niveau · Créer un objet · Liens physiques · Création d'une animation · Script Scheme |
