❤ 3Tyroine cortez Nemau Créer son propre système de quêtes
Ce tutoriel était à l'origine destiné à être en deux parties. La deuxième partie n'a jamais été créée, mais cette partie est largement suffisante pour créer les bases de son système.
Introduction
Dans l'excellent tutoriel sur le Shifumi vous avez été confrontés à la création de votre première scène et de vos premières fenêtres (je pars du principe que vous avez déjà lu cet article pour ne pas faire de la redondance d'information sur ce superbe site !).
C'est un bon début, mais cet article en apprend bien peu sur la création d'un véritable système. C'est pour ça que j'ai décidé (moi... Raho... a priori inconnu de vos personnes) de rédiger cet article qui vous apprendra à réaliser un véritable système qui s'ancrera correctement dans votre projet. Pour ce faire, j'ai choisi de raconter comment réaliser son propre système de quêtes ! Pourquoi un système de quêtes ? Parce que, au long de mes pérégrinations sur la toile, j'ai remarqué qu'il s'agissait d'un script que beaucoup de gens demandaient et que généralement, c'est lors du paramétrage du journal des quêtes que le bât blesse. Plutôt que de me lancer dans l'écriture d'un script de la mort qui tue qui s'adapte aux envies du maker, j'ai préféré écrire un article pour apprendre aux makers de la mort qui tuent (en l’occurrence, vous) comment réaliser leur propre système ! Attention... c'est parti !
Architecture du RGSS
Avant de démarrer l'implémentation de notre système, nous allons d'abord voir comment fonctionne le RGSS, sa structure générale, dans les grandes lignes. Il s'agit d'une partie un peu théorique mais qui est tout de même nécessaire pour bien comprendre ce que l'on fait. Et c'est en respectant cette structure que l'on va pouvoir faire des scripts super compatibles avec le reste du monde.
Pourquoi respecter l'architecture du RGSS ?
Dans l'absolu, pourquoi s'ennuyer à respecter une architecture ? La question semble légitime et je vais tâcher d'y répondre, pour plusieurs raisons :
• Le code sera plus lisible (généralement, dans sa structure), donc plus facile à maintenir.
• On maximise la compatibilité avec les autres scripts.
• En appliquant cette structure, le RGSS deviendra limpide !
Le troisième argument est un peu spéculatif, mais les deux premiers sont relativement importants. En effet, respecter cette structure est avant tout une manière de rendre son compte, structurellement, lisible et donc relativement bien ancré dans le RGSS. Car implémenter un script dans RPG Maker, c'est intégrer une application dans un écosystème déjà codé. Que ce soit sur RPG Maker ou plus tard, si ce tutoriel vous découvre une vocation de programmeur et que ça en devient votre métier, vous serez amené à respecter des structures de code. Alors autant commencer tout de suite !
Un système représenté en trois couches
Même si ce type de représentation ne s'applique pas à 100% des scripts, généralement un système est représenté par trois couches :
1.) Le modèle.
Spoiler (cliquez pour afficher) Le modèle est la couche de représentation statique. En gros, dans le cadre de RPG Maker, il s'agit des données telles que représentées dans la base de données. En effet, chaque champ de la base de données correspond à une instance de RPG::Quelquechose. Il s'agit d'une représentation statique car ses valeurs ne changeront pas au cours du jeu.
Pour voir un exemple de classe dite statique, rendez-vous dans l'aide de RPG Maker (F1 dans l'éditeur), allez dans l'index et choisissez n'importe quelle classe commençant par RPG::, par exemple RPG::Actor qui correspond à la représentation d'un acteur (héros) dans la base de données. Dans cette représentation, pas d'informations sur l'expérience car l'expérience est destinée à changer au fil du jeu. Ici, comme nous sommes dans la couche de représentation statique, on se contente de décrire un héros de manière brute.
2.) Le contrôleur.
Spoiler (cliquez pour afficher) Le contrôleur est la couche de représentation dynamique, elle va représenter dans le jeu une donnée statique. Pour reprendre l'exemple des héros, leur représentation statique est décrite dans la base de données au moyen de RPG::Actor, et leur couche de représentation dynamique est décrite par la classe Game_Actor (dans l'éditeur de script). Son rôle est de construire et de représenter un acteur, au fil de la partie. Ses valeurs initiales seront décrites au moyen de la couche de représentation statique mais contrairement à cette dernière, ses instances évolueront.
3.) La vue.
Spoiler (cliquez pour afficher) Maintenant que nous avons une représentation statique et une représentation dynamique, il nous faut une couche de représentation graphique. C'est le rôle de la vue. Elle aura pour rôle de représenter ces données dynamiques. En référence à nos héros, il existe plusieurs vues, il s'agit des scènes qui afficheront des données relatives à nos héros.
Voici, généralement, la représentation des systèmes complexes. Elle respecte plus ou moins ce schéma :
Les experts de la programmation (mais qui ne connaissent pas spécialement le RGSS) sont sûrement familiers avec ce genre d'architecture dite MVC (modèle-vue-contrôleur), et s'outrageront peut-être de la forme de mon schéma, cependant, je ne l'ai volontairement pas complexifié (en lui indiquant la notion d'injection de vue, ce qui ne serait pas très pertinent dans cet article). Retenons donc simplement que la vue, l'affichage, affiche des données dynamiques qui sont décrites sur bases du schéma établi dans la couche de représentation statique, à savoir la base de données.
Maintenant que nous avons une représentation imagée de ces trois couches, nous pouvons amener un autre intérêt à ce type d'architecture : il est facile de modifier la représentation de notre système sans changer "tout le script". Nous avons donc une indépendance sensible entre nos trois couches.
Est-ce que TOUS les scripts respectent cette architecture à trois couches?
Très bonne question. Non.
Cette architecture n'est à priori utile que lorsque l'on doit avoir une base de données. Si je voulais écrire un script qui calcule des ombres mouvantes, déployer le modèle à trois couches, ce serait un peu stupide.
Nous avons maintenant compris (enfin je l'espère) un exemple d'architecture particulièrement redondant dans le RGSS, nous allons maintenant le mettre en pratique en réalisant un système de quêtes. Go Go !
Réflexion sur le système
Avant de commencer, il est important de raisonner ce que fera notre système. Pourquoi? Et bien simplement parce que cela nous évitera de réécrire du code. Nous allons optimiser notre travail ! Alors nous allons essayer de voir ce que sera une quête. S4suk3 vous a parlé de la programmation orienté objet, de ce devoir de caractériser ce que l'on va manipuler. Nous allons donc caractériser une quête. Je vous propose cette structure :
A.) Id
L'Id permettra de représenter la quête au moyen d'un numéro, c'est tout de même plus confortable que devoir gérer des noms. L'Id sera donc l'identifiant de la quête (merci Cap'tain Obvious).
B.) Name, Description
Il s'agira du nom de la quête et de sa description. Par exemple "Sauver une souris", "Sauver la souris de la mère Michèle, prise en joug par le chat du père Lustucru (ahha WTF comment je réécris l'histoire).
C.) Exp, Gold
Ces deux champs correspondront à l'expérience et à l'argent que rapportera une quête réussie (et donc achevée).
D.) Items, Weapons, Armors
Ces trois champs correspondront à des listes d'entiers, qui seront les IDs des objets, armes, armures donnés en cas de réussite d'une quête. Pour faire gagner plusieurs fois le même objet, il suffira de répéter son ID dans la liste.
Maintenant que nous avons "le design" d'une quête dans sa représentation statique, nous allons pouvoir nous y mettre sérieusement, alors c'est parti pour la représentation de données statiques !
Mise en place de la couche statique
C'est ici que les romains s'empoignèrent. Dans un système du RGSS, on peut profiter de la base de données pour représenter nos quêtes. Malheureusement, ici, on ne peut pas étendre cette base de données (dans l'absolu c'est envisageable mais bon...). Alors comment allons-nous représenter nos quêtes ?
Une solution serait d'écrire un logiciel qui créerait notre base de données de quêtes mais ce tutoriel deviendrait alors beaucoup trop grand (et vous ne connaissez pas encore ma flemme légendaire... mais moi je la connais...).
Nous allons donc utiliser un script qui nous permettra de représenter des données statiquement.
L'Extend Database
L'Extend Database est un petit script très mignon qui permet de créer sa propre base de données statique très facilement.
Pour l'installer, rendons-nous sur la page du script, ici, récupérons le script et installons-le dans notre éditeur de script. En-dessous, créons un emplacement Mapping dans lequel nous écrirons notre mapping de base de données :
C'est dans "Mapping" que nous écrirons notre structure de quête et que nous insérerons nos quêtes. Pour la structure il suffit de nous référer à ce que nous avions établi comme schéma dans la rubrique Réflexion sur le Système.
Définition statique des quêtes
Une fois que vous aurez relu la page sur l'Extend Database, la définition d'une table pour les quêtes devra vous sembler enfantin. Il suffit de faire :
1
2
3
4
5
6
7
8
9
10
11
| # Définition de la structure statique des quêtes
class Quests < Database::Table
integer :id
string :name
string :description
integer :exp
integer :gold
polymorphic :items
polymorphic :weapons
polymorphic :armors
end |
Les trois derniers champs sont polymorphiques car il s'agira de listes d'entiers (et que ce système préfère fourrer tous les champs dont le type n'est pas connu dans un champ typé "polymorphic"... lol). Concrètement, polymorphic sert à stocker toutes les données dont le type n'est pas connu. Comme, malheureusement, les listes d'entiers ne sont pas proposées dans le système de type de l'Extend Database, nous utilisons un type générique (qui ne subira aucune conversion à l'insertion).
Maintenant que nous avons une structure statique, insérer des quêtes devient un jeu d'enfant. Je vous propose ces quêtes pour essayer notre script :
1
2
3
4
| Quests.insert(0, "Notre première Quête", "Sauver le monde", 10, 11, [],[],[])
Quests.insert(1, "Notre seconde Quête", "Sauver le monde", 20, 12, [1],[2],[3])
Quests.insert(2, "Notre troisième Quête", "Sauver le monde", 30, 13, [1,2],[1],[4,5,6])
Quests.insert(3, "Notre quatrième Quête", "Sauver le monde", 40, 14, [],[],[]) |
Si vous n'êtes pas à l'aise avec les 3 derniers champs, voici un peu d'aide qui devrait clarifier la situation :
• La première quête n'offre rien comme objets/armes/armures.
• La seconde offre l'objet 1, l'arme 2, l'armure 3.
• La troisième offre l'objet 1 et 2, l'arme 1 et les armures 4, 5 et 6
• La quatrième n'offre rien comme objets/armes/armures.
Au moyen de ce script nous avons très vite écrit la structure statique de nos quêtes et l'insertion d'une quête est assez facile.
L'insertion des quêtes peut se faire n'importe où dans l'éditeur de script, après la définition de la table. Cependant, j'ai pris l'habitude de créer un emplacement réservé dans mon éditeur de script pour localiser toutes mes insertions.
Conclusion sur la couche statique
Nous en avons DÉJÀ fini avec la couche statique. Il aurait été possible de le faire de plein d'autres manières mais je trouvais ça cool de faire la promotion du script de mon pote Nuki qui, il faut le reconnaître, facilite grandement l'écriture de cette partie.
Nous pouvons passer à la suite, la couche dynamique !
Mise en place de la couche dynamique
Cette partie sera, hélas, un peu plus longue que la précédente et abordera plus de concepts. En effet, c'est ici que nous verrons concrètement comment intégrer correctement un système dans le RGSS. N'hésitez donc pas à relire cette partie plusieurs fois pour être sûr de bien la comprendre. N'hésitez pas non plus à poser des questions dans ce superbe module de commentaires. Que ce soit moi ou un autre, nous nous ferons un plaisir de répondre !
Nous avons brillamment programmé (au moyen de la base de données étendue de Nuki, c'est vrai, mais ça reste une réussite !) notre couche statique, il va falloir s'attaquer à la couche dynamique. Je vous propose de créer un nouvel emplacement script, à la suite des deux précédents, que nous nommerons Quests Dynamic (ahahha ce nom !) et dans lequel nous coderons la logique dynamique de notre système :
Nous pouvons d'ores et déjà y écrire ce code Ruby :
1
2
3
4
| # Description dynamique d'une quête
class Game_Quest
end |
Et oui, nous avons créé une classe. Nous allons, avant de continuer, revoir certains points théoriques.
Qu'est ce qu'une classe
Le Ruby est un langage orienté objet. Dans le tutoriel sur le Shifumi, S4suk3 décrit la programmation orientée objet comme ceci :
Citation: Derrière cette appellation compliquée : "Orienté Objet", se cache un concept relativement facile à décrire. En effet, l'orienté objet consiste à dire que tout est descriptible. Donc que tous les éléments que je vais être amener à utiliser sont basés sur un "modèle" et le jeu du programmeur est de construire ces modèles et de les remplir. Le moule, le modèle, que l'on utilise pour décrire des éléments est appelé une classe. Dans la programmation orientée objet (plus spécifiquement orientée classe, mais ne nous arrêtons pas sur ce détail), on est amené à utiliser des classes déjà existantes et à créer nos propres classes, pour décrire des éléments.
Dans le jeu de la création de classe, il sera possible de créer des liens entre des classes : on appelle ces liens l'héritage. On va donc écrire une classe qui part avec toutes les caractéristiques d'une autre. C'est assez cool parce que ça évite de devoir réécrire inutilement du code. |
Concrètement, une classe est une sorte de moule qui contient toutes les informations nécessaires pour créer des données qui répondent à la description que la classe impose. Dans notre exemple, nous allons écrire la classe Game_Quest qui, en fonction d'un Identifiant, créera une structure dynamique sur base de sa représentation statique. Nous lui ajouterons des méthodes, que l'on pourrait traduire en "actions" pour finir une quête.
Qu'est ce qu'un constructeur
Il s'agit d'une méthode particulière d'une classe, c'est la méthode qui sera appelée lorsque l'on instanciera un objet. Par exemple :
1
| une_quete = Game_Quest.new(5) |
Dans cet exemple, je place dans la variable "une_quete" un objet Game_Quest qui sera construit en fonction de la donnée statique 5. Le .new est le constructeur, c'est lui qui, en fonction des arguments, va construire une instance. Dans notre classe Game_Quest nous allons ajouter le constructeur qui s'appelle toujours initialize :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Description dynamique d'une quête
class Game_Quest
# Constructeur
def initialize(id)
quest = Database.Quests[id]
@id = id
@name = quest.name
@description = quest.description
@exp = quest.exp
@gold = quest.gold
@items = quest.items
@weapons = quest.weapons
@armors = quest.armors
end
end |
Ici, notre constructeur prend un argument, un ID, il correspond au 5 dans notre exemple précédent. Sur base de cet ID, il va charger la donnée statique de la quête référencée par son ID, c'est la ligne quest = Database.Quests[id]. Ensuite nous allons attribuer les données de la quête à des attributs (pour rappel, un attribut dans une classe est une variable dont le nom commence par "@" et est donc accessible de partout dans les méthodes de la classe), les valeurs de la quête.
Cependant, actuellement, notre classe Game_Quest est identique à sa représentation statique. Il lui manque un attribut, celui qui définit si la quête est finie ou pas. Par défaut, quand une quête est démarrée (donc quand elle est construite), elle n'est pas finie. On peut donc modifier notre constructeur de la sorte :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Description dynamique d'une quête
class Game_Quest
# Constructeur
def initialize(id)
quest = Database.Quests[id]
@id = id
@name = quest.name
@description = quest.description
@exp = quest.exp
@gold = quest.gold
@items = quest.items
@weapons = quest.weapons
@armors = quest.armors
@finished = false
end
end |
Maintenant que nous avons développé (de manière complète) le constructeur de notre classe, nous allons pouvoir implémenter une méthode pour finir une quête. Le constructeur était le déclencheur d'une quête.
Implémenter la méthode pour terminer la quête
Maintenant que nous sommes capables de démarrer une quête (nous nous occuperons de l'intégrer dans "le jeu" dans une rubrique suivante) au moyen du constructeur. Typiquement, une quête est démarrée quand son @finished vaut false. Nous allons écrire une méthode qui permet de terminer une quête.
Car pour rappel, il s'agit ici de l'implémentation de la couche dynamique. Donc lorsque nous créerons un objet Game_Quest, c'est que nous lancerons une quête et donc, une fois lancée, elle ne sera pas finie (logique). Si une quête n'est pas lancée, alors elle n'est pas instanciée.
La première étape est de donner à l'attribut @finished la valeur true. Ensuite, il faudra distribuer l'expérience, l'or et donner les objets :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| # Description dynamique d'une quête
class Game_Quest
# Constructeur
def initialize(id)
quest = Database.Quests[id]
@id = id
@name = quest.name
@description = quest.description
@exp = quest.exp
@gold = quest.gold
@items = quest.items
@weapons = quest.weapons
@armors = quest.armors
@finished = false
end
# Termine une quête
def finish
@finished = true
# Donne l'or à l'équipe
$game_party.gain_gold(@gold)
# Attribue l'expérience à chaque membre de l'équipe
$game_party.members.each do |actor|
actor.gain_exp(@exp)
end
# Donne les objets
@items.each do |id|
$game_party.gain_item($data_items[id], 1)
end
# Donne les armes
@weapons.each do |id|
$game_party.gain_item($data_weapons[id], 1)
end
# Donne les armures
@armors.each do |id|
$game_party.gain_item($data_armors[id], 1)
end
end
end |
Voyons ensemble ce que fait cette méthode. Premièrement, elle change l'attribut @finished pour qu'il soit égal à true, donc que la quête soit finie. Ensuite, nous donnons les récompenses. Pour donner l'or, il existe dans Game_Party une méthode qui permet de donner de l'or, nous l'utilisons !
Il existe une autre méthode qui permet de récupérer un tableau avec tous les membres de l'équipe, il s'agit de members.
1
2
3
4
| # Attribuer l'expérience à chaque membre de l'équipe
$game_party.members.each do |actor|
actor.gain_exp(@exp)
end |
Cette partie est un petit peu complexe. Je vais vous l'expliquer dans les grandes lignes. Le rôle de la méthode each est de parcourir tous les éléments d'une structure complexe énumérable, dans cet exemple, il s'agit d'un tableau. Et de lui appliquer une fonction. Cette procédure, l'énumération de tous les éléments, s'appelle l'itération. Nous déclarons une variable entre pipes ( | ), et pour chaque élément de la structure de données cette variable prendra successivement sa valeur. Donc ici, nous appelons la méthode give_exp sur chaque élément du tableau $game_party.members qui est rempli d'instances de Game_Actor et nous leur donnons leur expérience en récompense ! Facile !
Spoiler (cliquez pour afficher) L'argument de each est un petit peu particulier, il s'agit d'un block, soit une fonction anonyme, mais ne nous arrêtons pas sur ce genre de détail
Le même procédé est utilisé pour donner à l'équipe les objets (objets/armes/armures). Cette fois on traverse les listes définies dans notre couche statique. Il serait possible d'optimiser cette fonction pour ne pas donner deux fois "une fois" les objets répétés dans la liste mais c'est du détail.
Comment trouver les méthodes qui nous sauvent ?
Dans la section précédente, nous avons utilisé plein de petites méthodes qui nous résolvent des problèmes. Pour donner l'or, pour donner l'expérience, pour donner les armes, comment découvre-t-on ces méthodes ?
Malheureusement, il n'y a pas de manière magique. La première chose est de bien comprendre le RGSS et de le lire. C'est en pratiquant que l'on devient pratiqueur.
Donc quand vous aurez fini la lecture de ce très didactique tutoriel, je vous invite à lire le RGSS en l'appréhendant avec ce que vous aurez appris (sans oublier de raisonner en fonction du système à trois couches) et vous trouverez quels sont les outils que vous pouvez déjà utiliser pour concevoir vos propres scripts ! Dingue non.
L'encapsulation
En programmation orientée objet, l'encapsulation est une idée, qui consiste à protéger les attributs d'un objet (les variables qui commencent par @). Donc rendre ses attributs inaccessibles de l'extérieur. Donc pour pouvoir lire ou modifier ces attributs, il faudra créer des méthodes qui se chargeront de renvoyer la valeur d'un attribut ou d'en modifier sa valeur.
Mais quel est l'intérêt !? Pourquoi devrais-je m'ennuyer à écrire chacune des méthodes une par une ?
L'utilisation d'une méthode permet de vérifier les modifications d'attributs, vérifier qu'elles sont cohérentes. On représente souvent les objets comme des boîtes noires ayant leurs propres modes de fonctionnement et dont le développeur ne connaît que les actions spécifiques. En utilisant des méthodes de lecture et de modification, on renforce cette notion de boîte noire en séparant la spécification du comportement d'un objet et son paramétrage pratique.
Dans le futur, en développant notre système, nous aurons besoin d'afficher les attributs d'une quête (dans le journal des quêtes par exemple), donc pour ça, il faudra que nous puissions y accéder. Une solution triviale serait d'utiliser la définition statique... mais... ce ne serait pas très convivial. Une autre solution serait d'écrire toutes les méthodes qui se chargeront de renvoyer l'attribut. Un exemple pour name :
1
2
3
| def name
return @name
end |
L'instruction return a pour rôle d'interrompre une fonction et de renvoyer ce qu'on lui a passé en argument. Donc la création de cette méthode permettrait de récupérer le nom d'une quête. Cependant, ces méthodes peuvent être générées. Au moyen de la méthode attr_reader. Par exemple :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| # Description dynamique d'une quête
class Game_Quest
# Accesseurs
attr_reader :id
attr_reader :name
attr_reader :description
attr_reader :exp
attr_reader :gold
attr_reader :items
attr_reader :weapons
attr_reader :armors
# Constructeur
def initialize(id)
quest = Database.Quests[id]
@id = id
@name = quest.name
@description = quest.description
@exp = quest.exp
@gold = quest.gold
@items = quest.items
@weapons = quest.weapons
@armors = quest.armors
@finished = false
end
# Renvoie l'état d'une quête
def finished?
return @finished
end
# Termine une quête
def finish
@finished = true
# Donne l'or à l'équipe
$game_party.gain_gold(@gold)
# Attribue l'expérience à chaque membre de l'équipe
$game_party.members.each do |actor|
actor.gain_exp(@exp)
end
# Donne les objets
@items.each do |id|
$game_party.gain_item($data_items[id], 1)
end
# Donne les armes
@weapons.each do |id|
$game_party.gain_item($data_weapons[id], 1)
end
# Donne les armures
@armors.each do |id|
$game_party.gain_item($data_armors[id], 1)
end
end
end |
Ici, j'ai généré les méthodes d'accès à mes attributs, vous pouvez vous rendre compte que je n'ai pas généré celle de son état (@finished) parce que par convention, les méthodes qui renvoient vrai ou faux (un booléen), en ruby, se finissent par un ?, c'est pour cette raison que j'ai écrit manuellement la fonction finished?.
En gros, en faisant ça, il est possible d'accéder aux attributs de l'extérieur, par exemple :
1
2
3
4
5
| quest = Game_Quest.new(1)
print quest.name # affichera notre seconde quête
print quest.finished? # affichera false
quest.finish
print quest.finished? # affichera true |
attr_reader va générer les méthodes qui renvoient les attributs et il ne faudra donc pas les écrire manuellement. Il existe deux autres méthodes de ce style : attr_writer, qui permet de générer les méthodes de modification des attributs, et attr_accessor, qui combine les deux et qui génère les méthodes de lecture et de modification. Dans notre exemple, nous n'avons besoin que de la lecture des attributs, donc nous ne nous étendrons pas sur ces deux autres méthodes.
C'en est fini de ce survol trèèès sommaire de l'encapsulation, nous allons pouvoir maintenant spécifier quelque chose.
Trouver une quête via son ID
Cette rubrique doit vous paraître un peu étrange étant donné que nous avions déjà trouvé une quête au moyen de "Database.Quests[id]", cependant, cette manière que j'ai utilisée pour introduire le cours n'est pas bonne !
Pourquoi donc
En effet, lors de nos essais, cela fonctionnait parfaitement, alors pourquoi vous demanderais-je de changer ?
Eh bien c'est très simple, cette manière de récupérer une quête utilise l'ordre d'insertion et non le champ ID référencé dans notre table. Donc si nous supprimons un enregistrement en plein milieu de notre liste de définition des quêtes, tout l'ordre serait perturbé. De même qu'il aurait été obligatoire de respecter un ordre croissant démarrant de zéro.
Nous allons devoir effectuer une requête simple ! Je vous montre le code :
1
| Database.Quests.find{|quest| quest.id == ID_DESIRE} |
Comme vous pouvez le constater, ce code ressemble assez fort à ceux que nous avions utilisé pour distribuer les récompenses. Typiquement, voici ce que fait cette ligne :
Citation: Prend tous les éléments de Database.Quests et les passe tous en revue sous le nom de quest (ce nom est libre, j'aurais pu mettre "lapin" à la place) et trouve celui dont l'id est égal à ID_DESIRE |
Pour votre culture générale, la partie {|quest| quest.id == ID_DESIRE} est appelé un prédicat, dans le jargon de ruby, c'est un block. On aurait pu remplacer les {} par do et end (donc :
1
| Database.Quests.find do |lapin| lapin.id == ID_Desire end |
)
Par convention j'ai l'habitude d'utiliser des accolades pour les prédicats qui ne prennent qu'une ligne et les do/end pour les plus grosses questions.
Nous verrons plus tard que nous pourrons nous servir de cette méthode find pour faire d'autres requêtes, avec plus de critères pris en compte, mais nous n'y sommes pas encore.
Comme nous savons comment récupérer correctement des quêtes en fonction de leur ID, nous pouvons réécrire notre classe et cette fois nous pouvons effectuer des insertions de manière totalement libre sans se soucier de l'ordre :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| # Description dynamique d'une quête
class Game_Quest
# Accesseurs
attr_reader :id
attr_reader :name
attr_reader :description
attr_reader :exp
attr_reader :gold
attr_reader :items
attr_reader :weapons
attr_reader :armors
# Constructeur
def initialize(id)
quest = Database.Quests.find{|quest| quest.id == id}
# le id que je teste en égalité avec quest.id est celui
# passé en argument au constructeur.
@id = id
@name = quest.name
@description = quest.description
@exp = quest.exp
@gold = quest.gold
@items = quest.items
@weapons = quest.weapons
@armors = quest.armors
@finished = false
end
# Renvoie l'état d'une quête
def finished?
return @finished
end
# Termine une quête
def finish
@finished = true
# Donne l'or à l'équipe
$game_party.gain_gold(@gold)
# Attribue l'expérience à chaque membre de l'équipe
$game_party.members.each do |actor|
actor.gain_exp(@exp)
end
# Donne les objets
@items.each do |id|
$game_party.gain_item($data_items[id], 1)
end
# Donne les armes
@weapons.each do |id|
$game_party.gain_item($data_weapons[id], 1)
end
# Donne les armures
@armors.each do |id|
$game_party.gain_item($data_armors[id], 1)
end
end
end |
Cette méthode find existe pour les objets énumérables de Ruby, donc on peut l'utiliser pour les tableaux, les dictionnaires etc. Nous allons voir dans la rubrique suivante comment s'en servir pour interroger la base de données.
Effectuer des requêtes
En récupérant une quête au moyen de son ID, nous avons déjà fait des requêtes. Nous allons voir qu'il est possible d'aller plus loin que ça.
Cette partie est un petit plus car nous n'en n'aurons pas réellement besoin pour la suite de ce tutoriel. Cependant, je suis intimement convaincu qu'il s'agit d'un apport, et sans cette fonctionnalité, l'Extend Database est un peu useless.
Nous avions donc énoncé que
1
| Database.Quests.find{|quest| quest.id == 10} |
est une requête, qui va chercher le premier enregistrement dont l'ID est égal à 10. Nous aurions pu évaluer un autre prédicat. Par exemple :
1
| Database.Quests.find{|quest| quest.name == "je suis un nooom"} |
Cette requête va chercher le premier enregistrement dont le nom est "je suis un nooom". == est un opérateur, il va vérifier l'égalité (en mathématiques, on utilise = pour représenter l'égalité, attention, en Ruby, le signe égal simple assigne une valeur. Donc x = 9 n'interrogera pas l'égalité entre x et 9 mais assignera à x la valeur 9 !). Il existe d'autres opérateurs que le ==. En voici quelques-uns :
x == y
Renvoie true si x est égal à y et false si x est différent de y.
x != y
Renvoie true si x est différent de y et false si x est égal à y.
x > y
Renvoie true si x est plus grand que y et false sinon.
x < y
Renvoie true si x est plus petit que y et false sinon.
x >= y
Renvoie true si x est plus grand que ou égal à y et false sinon.
x <= y
Renvoie true si x est plus petit que ou égal à y et false sinon.
Il est aussi possible de coupler les questions, au moyen des opérateurs logiques or et and (que l'on peut aussi écrire || et &&).
x and y
Renvoie true si les deux valeurs, x et y sont vraies.
x or y
Renvoie true si au moins une des deux valeurs, x ou y est vraie.
Il est évidemment possible de poser des questions plus grandes au moyen de parenthèses comme par exemple :
1
| (x and (y or z)) and (a or c) |
Apprendre à lire des expressions booléennes est un exercice intéressant et utile qui vous permettra d'aller plus loin dans la création de vos scripts. Cependant, ce n'est pas l'objectif de ce tutoriel, donc je vais clôturer cette partie sur cet encart, qui n'était là que pour vous expliquer pourquoi on récupère une quête de cette manière.
J'ajoute tout de même un petit exemple de requête sur notre table de quêtes :
1
| Database.Quests.find{|quest| quest.id < 10 and quest.id > 20} |
Je vous invite à essayer de comprendre le rôle de cette requête (rien de très dur) et vous serez prêt à exploiter la base de données vous-même pour vos propres systèmes !
Intégration dans le jeu
A ce stade-ci, nous avons déjà une grosse partie de l'application qui est réalisée, nous allons donc pouvoir nous intéresser à l'intégration dans le jeu. C'est-à-dire, à faire en sorte que le joueur puisse accepter et finir des quêtes. C'est donc la dernière ligne droite de cette partie-ci du tutoriel.
Les classes sont ouvertes, le Monkeypatching
Une chose qu'il est très important de savoir en Ruby, c'est qu'il s'agit d'un langage interprété et donc que ses classes sont ouvertes. Ce qui veut dire qu'à tout moment, je peux étendre une classe et lui rajouter des méthodes. Donc si par exemple je fais ça dans un script :
1
2
3
4
5
| class Fixnum
def successeur
return self + 1
end
end |
(pour information, la classe Fixnum est la classe qui décrit les entiers (les nombres de l'ensemble Z)) eh bien je pourrai faire : un_nombre.successeur qui me renverra ce nombre + 1. Donc 8.successeur vaudra 9 !
Lorsque l'on étend une classe, il n'est pas nécessaire de re-spécifier ses liens d'héritage, ça n'a même pas de sens, donc dans ce cours, nous ne le ferons JAMAIS !
Grâce à cette "feature" du langage Ruby, nous allons donc pouvoir étendre une classe du RGSS qui se chargera de gérer l'accumulation des quêtes et leur logique. On appelle cette pratique le Monkeypatching.
Le Monkeypatching des méthodes
Comme, nous l'avons vu, il est possible d'ajouter des méthodes, une question naturelle devrait vous venir à l'esprit :
Que se passe-t-il si j'ajoute une méthode qui a le même nom qu'une méthode déjà existante ?
Eh bien c'est une catastrophe, en effet, la méthode ancienne sera écrasée par la nouvelle ! Par exemple si je fais ça :
1
2
3
4
5
| class Fixnum
def (-)(autre_entier)
return self + autre_entier
end
end |
J'aurai écrasé la méthode "-" et l'aurai transformé en "+", ça permet de faire des petites blagues très drôles mais ce n'est pas très intelligent.
Cependant, c'est en écrasant des méthodes que nous allons pouvoir leur faire faire plus que ce pour quoi elles étaient prévues !
Les alias
Admettons que je veuille modifier le comportement d'une méthode ? Lui faire faire quelque chose en plus ?
"Fastoche, premièrement, je vais copier le code de la méthode source, ensuite, je la monkeypatch et je colle le contenu de la méthode source, puis j'ajoute ce que je veux !"
C'est une manière de faire qui marcherait, mais qui créerait son lot d'incompatibilités. Admettons que vous fassiez ça, tous les scripts qui monkeypatcheraient ces méthodes ne seraient plus compatibles et ce que nous voulons faire, c'est justement un script qui minimise les incompatibilités !
Pour cela, nous allons utiliser des alias.
Un alias est une manière de donner un autre nom à une méthode, comme ça, lorsque l'on écrasera une méthode, l'ancienne version sera toujours accessible via le nom que l'on lui aura donné via l'aliasisation ! Je vous montre !
Pour notre système de quêtes, nous allons modifier la classe Game_Party pour lui ajouter un attribut qui contiendra la liste des quêtes, qui sera un tableau ! Il suffit juste de rajouter une ligne dans son constructeur, je vous montre puis je vous explique :
Dans un nouveau script (en-dessous des précédents), j'ai créé un espace "Intégration des quêtes" dans lequel je vais étendre Game_Party :
1
2
3
4
5
6
7
8
| class Game_Party
attr_accessor :quests
alias quest_initialize initialize
def initialize
quest_initialize
@quests = []
end
end |
Concrètement, j'étend la classe Game_Party (sans re-spécifier son lien d'héritage parce que je suis modernement cool). Je rends l'attribut quests lisible et ensuite, je crée un alias sur le constructeur.
A ce stade, on peut accéder à la méthode initialize aussi au moyen de la méthode quest_initialize. Puis j'écrase la méthode initialize, j'appelle l'ancienne version (l'alias pointe sur la méthode au moment où l'alias est effectué, donc avant que l'on l'écrase) et j'ajoute mon attribut "quests" qui est un tableau ! Facile non ?
Game_Party correspond à l'état de jeu, c'est donc cette classe qui stockera les membres de l'équipe, les inventaires, l'équipement, c'est donc naturellement dans cette classe que j'ai décidé de stocker mes quêtes lancées (en cours ou finies). Maintenant que nous avons un attribut quests, nous allons pouvoir créer des méthodes pour Game_Party qui nous seront utiles !
Intégration de méthodes pour manipuler les quêtes
La classe Game_Party décrit une équipe dans sa totalité. Plus haut dans le tutoriel, nous avons vu qu'il était possible d'y accéder au moyen de la variable $game_party, nous allons donc créer ce que l'on appelle des méthodes d'interface.
La première que nous allons faire est la méthode qui permet de lancer une quête et que nous allons appeler launch_quest. Elle prendra en argument l'ID de la quête à lancer. Je vous donne le code et je vous explique :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Game_Party
attr_accessor :quests
# Modification du constructeur
alias quest_initialize initialize
def initialize
quest_initialize
@quests = []
end
# Lance une quête
def launch_quest(id)
if !@quests.find{|quest| quest.id == id}
@quests << Game_Quest.new(id)
end
end
end |
La première étape :
1
| if !@quests.find{|quest| quest.id == id} |
C'est pour n'ajouter une quête que si elle n'est pas déjà en cours, sinon, on aurait des doublons dans la liste de quêtes.
1
| @quests << Game_Quest.new(id) |
Cette ligne ajoute dans le tableau @quests la nouvelle quête (uniquement dans le cas ou elle n'était pas déjà présente).
Dans un appel de script, il est donc possible de faire simplement "$game_party.launch_quest(ID)" pour démarrer la quête définie par l'ID passé en argument.
Maintenant il va falloir finir une quête ! Pour finir une quête, il faut d'abord que la quête soit lancée (donc qu'elle existe dans la liste de quêtes) et qu'elle ne soit pas finie (pour éviter de gagner plusieurs fois les récompenses). Donc nous allons créer deux méthodes. Une première pour accéder à notre quête (qui nous permettra de modifier launch_quest, un tout petit peu) et une méthode pour finir une quête :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| class Game_Party
attr_accessor :quests
# Modification du constructeur
alias quest_initialize initialize
def initialize
quest_initialize
@quests = []
end
# Renvoie une quête en fonction de son ID
def quest(id)
return @quests.find{|quest| quest.id == id}
end
# Lance une quête
def launch_quest(id)
if !quest(id)
@quests << Game_Quest.new(id)
end
end
# Finit une quête
def finish_quest(id)
if quest(id) and !quest(id).finished?
quest(id).finish
end
end
end |
Et voilà ! Nous avons tous les éléments pour que notre système de quêtes soit complet. Je vous montre, dans des appels de scripts :
Démarrer une quête : $game_party.launch_quest(ID)
Terminer une quête : $game_party.finish_quest(ID)
Une quête est en cours : $game_party.quest(ID) && !$game_party.quest(ID).finished? (Cette ligne renvoie un booléen)
Une quête est finie : $game_party.quest(ID) && $game_party.quest(ID).finished? (Cette ligne renvoie un booléen)
Les deux dernières commandes sont un peu moches donc nous allons modifier Game_Party pour rendre ces deux dernières commandes plus faciles à lire :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| class Game_Party
attr_accessor :quests
# Modification du constructeur
alias quest_initialize initialize
def initialize
quest_initialize
@quests = []
end
# Renvoie une quête en fonction de son ID
def quest(id)
return @quests.find{|quest| quest.id == id}
end
# Lance une quête
def launch_quest(id)
if !quest(id)
@quests << Game_Quest.new(id)
end
end
# Finit une quête
def finish_quest(id)
if quest_launched?(id)
quest(id).finish
end
end
# Vérifie si une quête est en cours
def quest_launched?(id)
return quest(id) and !quest(id).finished?
end
# Vérifie si une quête est finie
def quest_finished?(id)
return quest(id) and quest(id).finished?
end
end |
Voici ce que ça donne pour les formules appelées en booléen :
Quête en cours : $game_party.quest_launched?(ID)
Une quête est finie : $game_party.quest_finished?(ID)
Et avec ces commandes, vous avez toutes les cartes en main pour intégrer votre système de quête au moyen d'appels de scripts simples. C'est ici que je conclus cette première partie !
Conclusion
Nous en avons fini avec la gestion logique de notre système ! Dans la partie 2 nous réaliserons étape par étape le journal des quêtes et nous l'intégrerons dans le menu. Cette partie fut assez importante car nous avons appris la construction d'un système qui maximise les compatibilités et qui n'écrase rien de manière arbitraire et non contrôlée. Si vous avez des questions, le système de commentaires est là. Je remercie Nuki qui m'a bien motivé sur la fin (il a même écrit une partie) et Hiino qui relira ma piteuse orthographe (fait Smiley) !
Bien à vous ! Raho !
Source :
- Raho et xvw, "Créer son propre système de quêtes [Partie 1/2]", BilouCorp, écrit le 3 octobre 2013 [consulté le 16 juin 2021], https://www.biloucorp.com/creer-son-propre-systeme-de-quetes-partie-12-17
|