
Big Tuto SFML 2 / Action-RPG 2 / Expert : Legends of Meruvia
Chapitre 2 : Création et gestion de cinématiques
Tutoriel présenté par : Stéphane Barthélémy (Stephantasy)
Date d'écriture : 10 août 2016
Date de révision : -
Introduction
Quel jeu vidéo ne contient pas de scène cinématique ?
Elles sont souvent essentielles pour renforcer l’immersion du joueur dans l’histoire ou pour introduire des éléments ou personnages-clés du jeu. Nous ne parlerons pas ici de cinématiques précalculées (en 3D, et qui sont en fait des vidéos), telles celles qu’on trouve dans les introductions de la série Final Fantasy par exemple, mais plutôt de cinématiques utilisant le moteur du jeu. ![]()
Je vais vous présenter une fonctionnalité que j’ai développée lors de la création de mon jeu vidéo. Après avoir programmé ma première cinématique, et l’avoir modifiée deux fois, je me suis aperçu que cela allait rapidement devenir une vraie galère !
En effet, à chaque modification mineure, il faut recompiler tout le code.
De plus, à moins d’être un scénariste hors-pair, la réalisation d’un scénario nécessite souvent d’être ajusté et peaufiné; il faut souvent modifier les positionnements et les timings pour arriver à un résultat concluant. ![]()
Bref, programmer le scénario d’une cinématique directement dans le code peut rapidement mener à la création d’une belle usine à gaz !
Voyons ensemble comment faire autrement. ![]()
Préambule
Le gestionnaire de cinématiques peut apparaitre comme un outil merveilleux, nous permettant de créer des scénarios aisément et n’importe quand, puis voir notre jeu les jouer sans le moindre ajout de code !
Cependant, c’est loin d’être une fonctionnalité magique et cela peut apporter son lot de complications dans le développement de notre jeu. ![]()
En effet, l’implémentation de cet outil dans le jeu risque d’avoir un fort impact. Pour un maximum d’efficacité, le moteur du jeu doit offrir le plus d’accesseurs et de modificateurs possible. Parfois, il faudra même revoir l’organisation du code afin de permettre des fonctionnalités particulières qu’on aimerait dans nos cinématiques. ![]()
Implanter cet outil trop tôt dans le développement de notre moteur nous obligera peut-être à devoir le modifier souvent. L’implanter trop tard risque de nous obliger à modifier notre moteur.
Dans l’optique de garder tout cela le plus simple possible, nous nous contenterons de contrôles simples dans un premier temps. ![]()
Présentation
L’objectif est donc de créer un outil qui va nous permettre d’introduire des cinématiques dans notre jeu. Nous allons faire en sorte de pouvoir créer et modifier un scénario sans avoir à modifier la moindre ligne de code ! ![]()
Pour cela, nous allons avoir besoin de ce que j’appellerai : un gestionnaire de cinématiques. Il s’agit d’un outil permettant de jouer un scénario que l’on aura créé à l’aide de l’éditeur de scénarios.
Cet éditeur va nous permettre de produire un fichier texte qui sera lu et interprété par notre gestionnaire.

I- Le gestionnaire
Le gestionnaire est le cœur de notre solution, il est l’interface entre le scénario et le jeu.
I-1. Fonctionnement
Son fonctionnement est très simple :
1. Il lit le fichier contenant le scénario.
2. Il interprète chaque étape du scénario et indique au moteur de jeu ce qu’il faut faire.
Par exemple, si on voulait déplacer le personnage principal vers la droite jusqu’au milieu de la Map, notre fichier contiendrait une ligne de ce genre :
[1, Player, Déplacer vers la droite, Milieu de la Map]
Notre gestionnaire exécuterait alors une tâche pouvant ressembler à ceci :
Tant que (position joueur < milieu Map)
{
Déplacer joueur vers la droite
}
Comme vous pouvez le constater, la mécanique est simple et elle se répètera pour les diverses tâches qu’on va vouloir inclure dans nos cinématiques (chargement de Map, variation du volume de la musique, jouer un son, etc.). ![]()
I-2. La classe Cinematic
Étant relativement indépendante, la classe de notre gestionnaire, nommée « Cinematic », offre peu de contrôle. Seuls, son constructeur (pour créer l’objet), deux méthodes de réinitialisation et sa méthode principale sont publics. Tout le reste ne regarde qu’elle et est donc privé.
Circulez, il n’y a rien à voir ! ![]()
I-3. Définition de la classe (en-tête)
Fichier : Créer un nouveau fichier cinematic.h et y copier :
|
//Legends of Meruvia - C++ / SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
//Coded by: Stéphane Barthélémy
#pragma once
#ifndef CINEMATIC_H
#define CINEMATIC_H
#include <SFML/System/Clock.hpp>
#include <SFML/Window.hpp>
#include <sstream>
#include <string>
#include <vector>
#include <algorithm>
class Map;
class Player;
class Input;
class Sounds;
class Cinematic
{
public:
//Constructeur
Cinematic(Input&, Map&, Player&, Sounds&);
//Fonctions
bool isCinematic();
void setCinematicReplay(int number);
void setCinematicReset(void);
private:
Input &_input;
Map &_map;
Player &_player;
Sounds &_sounds;
// Enumérateurs
enum ActionType { // Types d'actions possibles dans un scénario
Action_End = -1, // Fin du scénario
Action_Wait, // Attente
Action_MoveSprite, // Action sur un Sprite
Action_LoadMap, // Charge une Map
Action_PlayMusic, // Joue une musique
Action_SetVolumeMusic, // Contrôle du volume de la musique
Action_PlaySound, // Joue un son
Action_Skip = 99 // Skip la cinématique
};
enum Conditions { // Conditions d'exécution d'une action
Condition_GoOn, // On continue de charger les actions
Condition_Final // On exécute les actions chargées
};
enum SpriteControl { // Contrôles d'un Sprite
SpriteMoveRight, // Déplacement vers la droite
SpriteMoveLeft, // Déplacement vers la gauche
SpriteMoveUp, // Déplacement vers le haut
SpriteMoveDown, // Déplacement vers le bas
SpriteAttack, // Attaque
SpriteLookRight, // Regarde à droite
SpriteLookLeft, // Regarde à guache
SpriteLookUp, // Regarde en haut
SpriteLookDown, // Regarde en bas
};
// Structure d'une action dans un scénario
struct Actions
{
int step; // Étape
int actionType; // Type de l'action
std::string param1; // Paramètre 1
std::string param2; // Paramètre 2
std::string param3; // Paramètre 3
std::string param4; // Paramètre 4
int condition; // Condition
};
// Liste des actions a exécuter
struct ExecActions
{
int step; // Numéro de l'étape
bool isConditionMeet; // État de la condition de l'étape
};
// Constantes
const std::string FILE_NAME = "scenarios/scenario_";
// Liste des paramètres utilisés
int MUSIC_VOLUME_INSTANT = 0; // Modification du volume instantanément
int MUSIC_VOLUME_INCREASE = 0; // Modification du volume progressivement
// Variables
int _currentCinematic; // Numéro de la cinématique jouée
bool _IsRunning; // Cinématique en cours
std::vector<int> _cinematicPlayed; // Cinématique déjà jouées
std::vector<Actions> _actions; // Liste des actions du scénario de la cinématique
std::vector<ExecActions> _execActions; // Actions à exécuter
int _activeStep; // Etape en cours de traitement
int _previousStep; // Dernière étape exécutée
bool _skipAsked; // On a demandé à passer la cinématique
sf::Clock clock; // Clock
sf::Time _actualTime; // Wait : Temps écoulé
sf::Time _memoTime; // Wait : Mémorisation du temps pour l'action "Wait"
// Fonctions
void initialize();
void play();
bool load(void);
void end(void);
bool isAlreadyPlayed(void);
void setAlreadyPlayed(void);
void readActions(void);
void executeActions(void);
};
#endif
|
I-4. La classe cinematic
Fichier : Créer un nouveau fichier cinematic.cpp et y copier :
|
//Legends of Meruvia - C++ / SFML 2
//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger
//Coded by: Stéphane Barthélémy
#include "Cinematic.h"
#include "player.h"
#include "map.h"
#include "input.h"
#include "sounds.h"
#include "magic.h"
#include "menus.h"
// Constructeur
Cinematic::Cinematic(Input &input, Map &map, Player &player, Sounds &sounds, Magic *magic, Menus &menus) :
_input(input), _map(map), _player(player), _sounds(sounds), _magic(magic), _menus(menus)
{
_currentCinematic = 0;
_IsRunning = false;
}
//Renvoie si une cinématique est jouée
bool Cinematic::isCinematic()
{
// Si une cinématique est associée à la Map et que la condition de l'exécuter est à 1, on lance la lecture du scénario
if ((_map.getCinematics() > 0 && _map.getCinematicsCondition() > 0) || _IsRunning) {
// La 1ère fois, on l'initialise
if (_map.getCinematicsCondition() > 0) {
initialize();
}
// On joue la cinématique
play();
return true;
}
return false;
}
// Initialisation
void Cinematic::initialize(){
// Numéro de la cinématique
_currentCinematic = _map.getCinematics();
// Reset du déclencheur de cinématique afin d'éviter de la relancer sans cesse
_map.resetCinematicsCondition();
// Est-ce que cette cinématique a déjà été jouée ? Si oui, on quitte.
if (isAlreadyPlayed())
{
end();
return;
}
// Initialisation
_activeStep = 0;
_previousStep = -1;
_skipAsked = false;
_player.setState(0);
// Gestion du temps écoulé
clock.restart();
_memoTime = clock.getElapsedTime();
// On vide la liste des actions
_actions.clear();
// On charge le fichier de la cinématique
if (load())
{
// On indique qu'une cinématique est en cours
_IsRunning = true;
}
else
{
end();
}
}
// Contrôle si la cinématique a déjà été jouée
bool Cinematic::isAlreadyPlayed(void)
{
for (size_t i = 0, size = _cinematicPlayed.size(); i < size; ++i) {
if (_cinematicPlayed[i] == _currentCinematic) {
return true;
}
}
return false;
}
// On mémorise le fait fait que cette cinématique a été jouée
void Cinematic::setAlreadyPlayed(void)
{
_cinematicPlayed.push_back(_currentCinematic);
}
// On efface la cinématique de la liste des "déjà jouées"
void Cinematic::setCinematicReplay(int number)
{
_cinematicPlayed.erase(std::remove(_cinematicPlayed.begin(), _cinematicPlayed.end(), number), _cinematicPlayed.end());
}
// On efface la liste des cinématiques déjà jouées
void Cinematic::setCinematicReset(void)
{
_cinematicPlayed.clear();
}
// Chargement du fichier
bool Cinematic::load(void)
{
bool result = false;
try
{
// On rempli notre vector avec les données du fichier
std::string path = FILE_NAME + std::to_string(_currentCinematic) + ".txt";
std::ifstream file(path);
std::string line;
while (std::getline(file, line))
{
std::stringstream linestream(line);
Actions temp = { -1, -1, "-1", "-1", "-1", "-1", -1 };
linestream >> temp.step >> temp.actionType >> temp.param1 >> temp.param2 >> temp.param3 >> temp.param4 >> temp.condition;
_actions.push_back(temp);
}
// On contrôle qu'il y ait des champs
if (_actions.size() > 0)
{
// On indique la fin du scénario
_actions.push_back({ -1, -1, "-1", "-1", "-1", "-1", -1 });
result = true;
}
}
catch (...) {
result = false;
}
return result;
}
// Lecture des actions du scénario
void Cinematic::readActions()
{
while (_activeStep != _previousStep) {
switch (_actions[_activeStep].actionType)
{
// fin du scenario
case Action_End:
{
end();
}break;
//wait
case Action_Wait:
{
// On charge l'action a exécuter et on indique qu'elle n'est pas remplie
ExecActions tmp = { _activeStep, false }; // Toujours "StopCond" ! (puisqu'on veut attendre...)
_execActions.push_back(tmp);
// On mémorise le temps actuel
_memoTime = clock.getElapsedTime();
// Sur une condition finale, on passe à l'execution des tâches
_previousStep = _activeStep;
}break;
// Contrôle Sprite
case Action_SpriteControl:
{
// On charge l'action a exécuter et on indique qu'elle n'est pas remplie
ExecActions tmp = { _activeStep, false };
_execActions.push_back(tmp);
// Sur une condition finale, on passe à l'execution des tâches
if (_actions[_activeStep].condition == Condition_Final)
{
_previousStep = _activeStep;
}
// Sinon on continue de charger les actions
else {
_activeStep++;
}
}
break;
// Map
case Action_LoadMap:
{
// On charge l'action a exécuter et on indique qu'elle n'est pas remplie
ExecActions tmp = { _activeStep, false };
_execActions.push_back(tmp);
if (_actions[_activeStep].condition == Condition_Final) {
_previousStep = _activeStep;
}
else {
_activeStep++;
}
}break;
// Musique
case Action_PlayMusic:
{
// On charge l'action a exécuter et on indique qu'elle n'est pas remplie
ExecActions tmp = { _activeStep, false };
_execActions.push_back(tmp);
if (_actions[_activeStep].condition == Condition_Final) {
_previousStep = _activeStep;
}
else {
_activeStep++;
}
}break;
// Volume musique
case Action_VolumeMusic:
{
// On charge l'action a exécuter et on indique qu'elle n'est pas remplie
ExecActions tmp = { _activeStep, false };
_execActions.push_back(tmp);
if (_actions[_activeStep].condition == Condition_Final) {
_previousStep = _activeStep;
}
else {
_activeStep++;
}
}break;
// Joue un son
case Action_PlaySound:
{
// On charge l'action a exécuter et on indique qu'elle n'est pas remplie
ExecActions tmp = { _activeStep, false };
_execActions.push_back(tmp);
if (_actions[_activeStep].condition == Condition_Final) {
_previousStep = _activeStep;
}
else {
_activeStep++;
}
}break;
// Skip
case Action_Skip:
{
// Si un skip a été demandé, on reset les actions et on execute les actions du Skip
if (_skipAsked) {
_activeStep++;
}
// Sinon, c'est qu'on a atteint la ligne "Skip" du fichier,
// donc on active simplement la fin de la cinématique en sautant les étapes du Skip
else {
while (_actions[_activeStep].actionType != Action_End) {
_activeStep++;
}
}
}break;
}
}
}
// Exécution des actions du scénario
void Cinematic::executeActions()
{
bool allActionsDone = true;
for (size_t i = 0, size = _execActions.size(); i < size ; ++i)
{
// Si la condition n'est pas encore remplie
if (!_execActions[i].isConditionMeet)
{
switch (_actions[_execActions[i].step].actionType)
{
// Wait
case Action_Wait:
{
// On compte le temps qui passe
_actualTime = clock.getElapsedTime();
if ((_actualTime - _memoTime) >= sf::milliseconds(std::stoi(_actions[_execActions[i].step].param1)))
{
// La condition est remplie lorsque le temps écoulé dépasse le temps prévue
_execActions[i].isConditionMeet = true;
}
}break;
// Contrôle Sprite
case Action_SpriteControl:
{
// Contrôle du joueur
if (_actions[_execActions[i].step].param1 == "Player")
{
// Action demandée
switch ((SpriteControl)std::stoi(_actions[_execActions[i].step].param2))
{
case SpriteMoveRight:
{
// Si le joueur a atteint sa destination, c'est fini
if (_player.getX() >= std::stoi(_actions[_execActions[i].step].param4))
{
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}
// Sinon on le déplace
else
{
// Est-ce qu'on cours ?
if (std::stoi(_actions[_execActions[i].step].param3) == 1){
_input.setButton(Input::ButtonName::run, true);
}
_input.setButton(Input::ButtonName::right, true);
_player.update(_input, _map, _magic, _menus);
}
}break;
case SpriteMoveLeft:
{
// Si le joueur a atteint sa destination, c'est fini
if (_player.getX() <= std::stoi(_actions[_execActions[i].step].param4))
{
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}
// Sinon on le déplace
else
{
// Est-ce qu'on cours ?
if (std::stoi(_actions[_execActions[i].step].param3) == 1) {
_input.setButton(Input::ButtonName::run, true);
}
_input.setButton(Input::ButtonName::left, true);
_player.update(_input, _map, _magic, _menus);
}
}break;
case SpriteMoveUp:
{
// Si le joueur a atteint sa destination, c'est fini
if (_player.getY() <= std::stoi(_actions[_execActions[i].step].param4))
{
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}
// Sinon on le déplace
else
{
// Est-ce qu'on cours ?
if (std::stoi(_actions[_execActions[i].step].param3) == 1) {
_input.setButton(Input::ButtonName::run, true);
}
_input.setButton(Input::ButtonName::up, true);
_player.update(_input, _map, _magic, _menus);
}
}break;
case SpriteMoveDown:
{
// Si le joueur a atteint sa destination, c'est fini
if (_player.getY() >= std::stoi(_actions[_execActions[i].step].param4))
{
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}
// Sinon on le déplace
else
{
// Est-ce qu'on cours ?
if (std::stoi(_actions[_execActions[i].step].param3) == 1) {
_input.setButton(Input::ButtonName::run, true);
}
_input.setButton(Input::ButtonName::down, true);
_player.update(_input, _map, _magic, _menus);
}
}break;
case SpriteAttack:
{
_input.setButton(Input::ButtonName::attack, true);
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}break;
case SpriteLookRight:
{
_player.setDirection(_player.getDirectionRight());
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}break;
case SpriteLookLeft:
{
_player.setDirection(_player.getDirectionLeft());
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}break;
case SpriteLookUp:
{
_player.setDirection(_player.getDirectionUp());
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}break;
case SpriteLookDown:
{
_player.setDirection(_player.getDirectionDown());
_player.update(_input, _map, _magic, _menus);
_execActions[i].isConditionMeet = true;
}break;
}
}
}break;
// On charge une Map
case Action_LoadMap:
{
_map.setLevel(std::stoi(_actions[_execActions[i].step].param1));
_map.changeLevel();
_player.initialize(_map);
_player.update(_input, _map, _magic, _menus);
// On valide la condition
_execActions[i].isConditionMeet = true;
}break;
// On charge une musique
case Action_PlayMusic:
{
// TODO (ajouter la possiblité de charger un musique dans Sounds)
// On valide la condition
_execActions[i].isConditionMeet = true;
}break;
//Volume musique
case Action_VolumeMusic:
{
// Augmentation du volume
if (std::stoi(_actions[_execActions[i].step].param1) == MUSIC_VOLUME_INCREASE)
{
float newVolume = 0.0;
// Volume instantané...
if (std::stoi(_actions[_execActions[i].step].param2) == MUSIC_VOLUME_INSTANT) {
newVolume = std::stoi(_actions[_execActions[i].step].param3);
}
// ...ou progressif
else
{
newVolume = _sounds.getMusicVolume()+1.0;
}
// Si le nouveau volume à atteint la valeur demandée ou la valeur maximale permise
if (newVolume >= std::stoi(_actions[_execActions[i].step].param3) || newVolume >= _sounds.getMusicVolumeMax())
{
if (newVolume >= _sounds.getMusicVolumeMax())
{
_sounds.setMusicVolume(_sounds.getMusicVolumeMax());
}
else {
_sounds.setMusicVolume(std::stoi(_actions[_execActions[i].step].param3));
}
// On valide la condition
_execActions[i].isConditionMeet = true;
}
// On applique le nouveau volume
else {
_sounds.setMusicVolume(newVolume);
}
}
// Diminution du Volume
else
{
int newVolume = 0;
// Volume instantané...
if (std::stoi(_actions[_execActions[i].step].param2) == MUSIC_VOLUME_INSTANT) {
newVolume = std::stoi(_actions[_execActions[i].step].param3);
}
// ...ou progressif
else
{
newVolume = _sounds.getMusicVolume() - 1.0;
}
// Si le nouveau volume à atteint la valeur demandée ou la valeur maximale permise
if (newVolume <= std::stoi(_actions[_execActions[i].step].param3) || newVolume <= 0)
{
if (newVolume <= 0)
{
_sounds.setMusicVolume(0.0);
}
else {
_sounds.setMusicVolume(std::stoi(_actions[_execActions[i].step].param3));
}
// On valide la condition
_execActions[i].isConditionMeet = true;
}
// On applique le nouveau volume
else {
_sounds.setMusicVolume(newVolume);
}
}
}break;
// On joue un son
case Action_PlaySound:
{
_sounds.PlaySoundFx(std::stoi(_actions[_execActions[i].step].param1));
// On valide la condition
_execActions[i].isConditionMeet = true;
}break;
}
}
// On contrôle l'avancement des actions
allActionsDone &= _execActions[i].isConditionMeet;
}
// Si toutes les actions ont été réalisées, on passe à l'étape suivante
if (allActionsDone)
_activeStep++;
}
// On joue la cinématique
void Cinematic::play()
{
/* L'exécution d'un scénario s'effectue en 2 étapes :
1. Lecture des actions jusqu'à la rencontre d'une condition d'attente
2. Exécution des actions jusqu'à ce qu'elles aient toutes été jouées.
=> Ce cycle ce poursuit tant qu'il y a des actions disponibles dans le scénario.
*/
// Sur demande de Skip, on passe directement à la partie Skip programmée
if (_input.getButton().enter)
{
// Si le skip a déjà été activé, on quitte
if (!_skipAsked) {
_skipAsked = true;
// On charge les étapes du Skip
int cpt = 0;
while (_actions[cpt].actionType != Action_Skip) {
cpt++;
// S'il n'y a pas de skip de prévu, on ignore la commande
// sinon on ne peut pas être certain que la cinématique se termine correctement
if (_actions[cpt].actionType == Action_End) {
cpt = 0;
break;
}
}
// On passe aux actions "Skip" (si elles existent)
if (cpt > 0) {
_previousStep = cpt - 1;
_activeStep = cpt;
}
}
}
// On reset les Inputs
std::vector<int> inputList;
inputList.push_back(Input::ButtonName::up);
inputList.push_back(Input::ButtonName::down);
inputList.push_back(Input::ButtonName::right);
inputList.push_back(Input::ButtonName::left);
inputList.push_back(Input::ButtonName::attack);
inputList.push_back(Input::ButtonName::run);
inputList.push_back(Input::ButtonName::enter);
inputList.push_back(Input::ButtonName::magic);
_input.clearInput(inputList);
// Étape 1
if (_activeStep != _previousStep)
{
_execActions.clear();
readActions();
}
// Étape 2
else
{
executeActions();
}
}
// Fin de la cinématique
void Cinematic::end(void)
{
setAlreadyPlayed();
_currentCinematic = 0;
_IsRunning = false;
_activeStep = _previousStep = -1;
}
|
I-5. Le constructeur
Le constructeur prend en paramètre les principaux objets du jeu, soit : Input, Map, Player, Sounds, Magic et Menus. On stocke une référence de chacun d’entre eux dans des attributs privés afin de ne pas avoir à les passer en paramètre à toutes les fonctions. ![]()
I-6. La méthode isCinematic()
La méthode « isCinematic() » se charge de contrôler s’il y a une cinématique à jouer. Si c’est le cas, elle prend la main et la garde jusqu’à la fin du scénario.
C’est la classe « Map » qui nous informe qu’il y a une cinématique à jouer. Cette classe possède deux membres intitulés « cinematics » et « cinematics_condition », déjà présents dans la classe (pas besoin de les ajouter donc - et on peut modifier leurs valeurs via le level editor
).
Le premier désigne le numéro du scénario à jouer et le second nous indique s’il faut jouer la cinématique. L’attribut « cinematics_condition » sert donc de déclencheur et pourrait être mis à 1 après qu’une série d’évènements se soit déroulée (par exemple, après avoir fini une quête en particulier).
Donc, pour qu’une cinématique soit jouée, il faut :
- que la Map fasse référence à un scénario* : cinematics > 0
- que l’on reçoive l’ordre d’exécuter le scénario : cinematics_condition > 0
*Vous noterez qu’en l’état actuel des choses, on ne peut pas associer plus d’une cinématique à une Map.
I-7. Autres méthodes
« setCinematicReplay() » permet d’ôter une cinématique de la liste des « déjà jouées » et donc de pouvoir la rejouer. Cela peut se produire lorsque le joueur meurt, par exemple.
« setCinematicReset() » permet d’effacer la liste des cinématiques déjà jouées. Cela est utile lorsque l’on souhaite charger une nouvelle partie ou une autre sauvegarde.
I-8. Fonctionnement : initialisation
Voici comment se déroule le déclenchement d’une cinématique.
On arrive dans une nouvelle Map. Celle-ci nous indique que cinematics == 2 et que cinematics_condition == 1. Cela se traduit par « autorisation de jouer le scénario numéro 2 ».
On commence par initialiser notre gestionnaire :
1. Initialisation de variables
2. Remise à 0 de cinematics_condition
3. On s’assure que cette cinématique n’a pas déjà été jouée
4. Chargement du fichier « scenario_2.txt »
L’initialisation est terminée, nous passons à l’exécution.
I-9. Fonctionnement : Play
Ca y est, on joue notre scénario ! C’est le rôle de la fonction « play() ». Voici comment cette dernière fonctionne. ![]()
Avant toute chose, on contrôle à tout moment si l’action « Skip » est demandée (affectée à la touche [ENTRÉE]). Mais nous reviendrons sur cette fonctionnalité plus tard. ![]()
Ensuite, on remet à zéro les « Input » afin que le joueur ne puisse pas interférer dans le bon déroulement de la cinématique. ![]()
Finalement, on exécute le scénario. Cela se passe en deux temps :
1. Lecture d’une ou de plusieurs actions.
2. Exécution d’une ou de plusieurs actions.
La lecture des actions se fait tant que la condition de l’action ne lui dit pas d’arrêter. La condition vaut 0 ou 1, signifiant respectivement « Continue » et « Arrête-toi » (Condition_GoOn et Condition_Final dans le code).
Une fois la lecture interrompue par la condition « Condition_Final », on passe à l’exécution des actions chargées. A son tour, l’exécution dure tant que toutes les actions n’ont pas rempli leur propre condition (nous les appellerons « conditions d’actions1 »).
Une fois que toutes les actions ont rempli leur condition, on retourne à la lecture des actions suivantes et cela se répète tant qu’il y a des actions dans le scénario. ![]()
Enfin, lorsqu’il n’y a plus d’action à lire, on indique qu’il n’y a plus de cinématique en cours de lecture et on mémorise le fait que cette cinématique vient d’être jouée (voir et revoir la même scène devient rapidement irritant !
).
Le jeu reprend ses droits.
En résumé, le gestionnaire traite les actions de la façon suivante :
1. S’il y a des actions à charger => étape 2 ; sinon FIN
2. Chargement des actions tant que la condition est à 0
3. Dès qu’une action a une condition à 1 => fin du chargement, on passe à l’exécution
4. Exécution de toutes les actions chargées
5. Toutes les actions chargées ont été exécutées => retour à l’étape 1
Note :
Les conditions d’actions dépendent de l’action. En effet, chaque action a une condition qui lui est propre.
Par exemple, si l’action est « Déplace le joueur vers la droite », sa condition sera une destination (une position du joueur en x). Pour une variation du volume de la musique, la condition sera un niveau sonore (exprimé de 0 à 100 avec la SFML).
I-10. Skip
Cette option permet de passer une cinématique. Bien que cela soit fort dommage (surtout quand on voit le mal qu’on se donne pour les créer
), on peut offrir cette possibilité. ![]()
En effet, on a le choix, car le Skip, pour être effectif, doit être prévu. Et c’est très important !
Sinon imaginez la pagaille qu’il peut y avoir à interrompre une cinématique n’importe quand.
Considérez le cas suivant :
1. Début de la cinématique
2. Le joueur doit se déplacer au milieu de la Map
3. Déplacement du joueur vers le milieu
4. Il discute avec le méchant
5. À la fin du dialogue, le méchant ouvre une trappe sous les pieds du joueur
6. Le joueur tombe et on charge une nouvelle Map (une grotte sombre infestée de vermines !)
7. Fin du scénario
Bon, maintenant, ça fois trois fois que je meurs dans la grotte et que le jeu me renvoie à la surface. Forcément, cela fait également trois fois que je me farcis la même cinématique et je décide de la passer. Voici alors ce qu’il se passe :
1. Début de la cinématique
2. Le joueur doit se déplacer au milieu de la Map
3. Déplacement du joueur vers… Je presse [ENTRÉE] => Skip !
4. Fin de la cinématique
« …Euh, la cinématique est terminée, mais le jeu n’a pas chargé la Map de la grotte. Je fais quoi maintenant ?!
»
Vous comprenez alors qu’on ne peut pas passer n’importe quand, ni n’importe comment une cinématique !
Ainsi, dans notre scénario, il va falloir prévoir ce qui se passe lorsqu’on Skip notre cinématique. Nous verrons cela en détails dans la partie dédiée à l’éditeur (au chapitre suivant
).
II- Intégration au moteur du jeu
L’intégration est très simple. On demande à la fonction « isCinematic() » s’il y a une cinématique à jouer. S’il n’y en a pas, le déroulement du jeu se poursuit normalement. S’il y en a une, on n’appelle plus la fonction « update() » dans le Main(), car la classe « Cinématic » se charge alors de mettre à jour le jeu. ![]()
Ce qui donne :
Fichier : main.cpp : Modifier le code :
|
// S'il y a une cinématique en cours, on ne met à jour que le gestionnaire de cinématique if(!Cinematic.isCinematic() ) { //Updates update(input, map, player, monster, sounds); } |
Une fois qu’il est lancé, le gestionnaire s’occupe de jouer le scénario jusqu’à la fin ou jusqu’à ce qu’un « Skip » soit demandé. Puis, il rend la main une fois qu’il a terminé. ![]()
N'oubliez pas non plus d'instancier la classe cinematic ! ![]()
Voilà le code complet du main(), mis à jour :
Fichier : main.cpp : Modifier le code :
|
int main(int argc, char *argv[])
{
// Création d'une fenêtre en SFML
RenderWindow window(VideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32),
"Meruvia - Big Tuto A-RPG 2 / Expert - Chap 02 - www.meruvia.net");
//On active la synchro verticale
window.setVerticalSyncEnabled(true);
//Instanciation des classes
Input input;
Map map;
Player player;
Sounds sounds;
Menus menus;
//On instancie autant de classes que de monstres gérables
Monster monster[MONSTRES_MAX];
//On instancie autant de classes que de projectiles magiques gérables
Magic magic[MAGIC_MAX];
//On instancie autant de classes que d'explosions gérables
Explosions explosion[EXPLOSIONS_MAX];
// Cnématique [SB - Ajout]
Cinematic Cinematic(input, map, player, sounds, magic, menus);
//On lance la musique en boucle
sounds.PlayMusic(true);
//On commence au premier niveau
map.setLevel(1);
map.changeLevel();
//On initialise le player
player.initialize(map);
player.setGold(100);
//On commence par le menu start
menus.setOnMenu(true, START);
// Boucle infinie, principale, du jeu
while (window.isOpen())
{
// Gestion des inputs
input.gestionInputs(window);
// S'il y a une cinématique en cours, on ne met à jour que le gestionnaire de cinématique [SB - Ajout]
if(!Cinematic.isCinematic() )
{
//Updates
update(input, map, player, monster, sounds, magic, menus, explosion);
}
// Dessin - draw
draw(window, map, player, monster, magic, menus, explosion);
window.display();
}
// On quitte
return 0;
}
|
Il faudra aussi rajouter les fonctions resetCinematicsCondition(void), getCinematics() et getCinematicsCondition() à notre classe Map :
Fichier : map.h : Rajouter les prototypes :
|
int getCinematics(void) const;
int getCinematicsCondition(void) const;
void resetCinematicsCondition(void);
|
Fichier : map.cpp : Rajouter les fonctions:
|
int Map::getCinematics(void) const { return cinematics; }
int Map::getCinematicsCondition(void) const { return cinematics_condition; }
void Map::resetCinematicsCondition(void) { cinematics_condition = 0; }
|

English
Français 