Outils pour utilisateurs

Outils du site


dindomoteur_portail_du_moteur_de_metroidvania_orleanais

Ceci est une ancienne révision du document !


DindoMoteur : Portail du moteur de metroidvania orléanais

Cette page est la page principale du DindoMoteur, une proposition de création et documentation d'un moteur de Metroidvania à l'aide du moteur de jeu libre Godot.

Cette page est divisée en trois sections :

  • Une présentation des enjeux du projet.
  • La documentation des réglages généraux du moteur.
  • La centralisation des sous-articles liés au projet.

Enjeux du projet

Contexte

Cela fait maintenant 5 ans que j'utilise régulièrement le moteur de jeu vidéo libre Godot.

Celui-ci a accompagné ma découverte du développement informatique, et il prend de l'ampleur, rejoignant Blender dans le registre des outils libres qui agrègent assez de contributeurs et de d'utilisateurs pour devenir des logiciels équivalents aux alternatives propriétaires.

Au cours de mes recherches, j'ai fini par me rendre compte que la plupart des jeux vidéos ayant eu un impact culturel ne s'appuient en général pas seulement sur un moteur généraliste. Ces moteurs, comme Godot, Unity, Unreal Engine ou encore RPG Maker, sont des frameworks dédiés à la création de jeu vidéo mettant en place des outils graphiques et un langage de programmation qui facilite la mise-en-place de certains outils nécessaires au développement du logiciel.

Si ce sujet vous intéresse, vous devriez essayer de coder un petit jeu vidéo via la librairie python Pygame, puis en développer un second en utilisant Godot. Vous vous rendrez alors compte qu'un grand nombre des fonctionnalités à recoder soi-même en Pygame (par exemple les hitbox) sont maintenant pleinement intégrés dans Godot, qui, de surcroît, intègre un framework graphique.

Ces moteurs généralistes, donc, mettent en place l'ensemble des outils nécessaires à coder rapidement un jeu vidéo, mais leur aspect généraliste ne les rend pas capables de créer autre chose qu'un clone de jeu vidéo existant. Ils agrègent d'une certaine manière les algorithmes redondants du monde du jeu vidéo une fois qu'un nombre assez grand de jeux vidéos les proposent.

Par exemple, le jeu Wolfenstein 3D était le premier jeu vidéo en trois dimensions. À son époque, ce sont les développeurs qui ont codé le moteur de 3D qu'il utilise, puis, devant le succès et la prouesse de l'algorithme, de nombreux autres développeurs l'ont copié. Maintenant, un moteur comme Godot intègre nativement cet algorithme.

Or, ce qui fait le charme de nombreux jeux, c'est le caractère unique du gameplay qu'ils proposent. Ils ne peuvent se contenter d'utiliser le moteur généraliste : il est nécessaire de recréer un sous moteur spécialisé à l'aide du moteur généraliste.

À titre d'exemple, on peut penser à l'algorithme de téléportation du jeu Portal, le système d'énigme de Baba Is You, ou encore l'environnement modulable de Minecraft.

En réalité, ces exemples sont des exemples assez avancés qui mettent facilement en valeur la nécessité de respécialiser un jeu par rapport à un concept généraliste. L'utilisation des menus comme espaces de jeu et non plus comme simple espace de documentation et de paramétrages dans les RPG, par exemple dans les premiers Pokémon, est déjà une mise-en-place d'un gameplay particulier au sein d'une architecture classique.

Objectif

Mon objectif est de construire, de manière incrémentale, un moteur de jeu vidéo libre et documenté en français qui permette de cloner des jeux d'aventures de type metroidvania, comme Hollow Knight ou Dead Cells.

En prenant soin de documenter le projet, le but est de pouvoir disposer d'un moteur intégrant déjà les paramètres classiques, modifiables aisément (si possible via des interfaces graphiques). Une fois ce sous-moteur généraliste en place, le but est de pouvoir facilement re-spécialiser le gameplay en ajoutant du code dédié.

Une fois le moteur en place, il devient possible de le mettre à disposition de designer et d'artistes pour réaliser des versions concrètes de jeux vidéos à chaque point d'étape. Dans cette optique, chaque jeu vidéo associé au moteur correspond à une spécialisation du moteur.

Godot : kézako ?

Godot est donc un moteur de jeu vidéo libre.

Il intègre à la fois une interface graphique de programmation et un langage nommé gdscript, basé sur la syntaxe python. Il reste également possible de coder en C++ directement.

Le langage manipulé est orienté objet, et c'est ce qui fait la force de Godot : il intègre de nombreuses classes communément utilisées dans la programmation : sprites, animations, gestion de collision, parallaxes, entrées utilisateurs, etc… Grâce au système d'héritage, il est alors possible de créer et instancier ses propres objets, qui, dans la sémantique propre à Godot, sont nommés des scènes.

Dans la philosophie du moteur, une scène correspond en général à un objet (au sens humain) particulier. Un exemple typique est l'idée qu'une scène Village contiendra une scène Maison, qui contiendra une scène Table, etc.

Concernant la gestion des coordonnées, c'est le système vectoriel qui est utilisé, comme à l'accoutumée dans les jeux vidéos.

Les objets étant organisés selon un système d'arbre, les coordonnées d'une scène sont modifiées par l'ensemble des parents ce qui, dans l'exemple précédent, permet de bouger la Table en même temps que la Maison.

Pour plus d'infos sur Godot, RTFM.

Base de travail

Godot intègre une librairie de ressources (Asset Library) qui permet de voir comment utiliser le moteur de manière efficiente à l'aide de projets didactiques.

Dans notre cas, c'est le 2D Platformer Demo (KinematicBody) qui servira de base de travail pour le développement du sous-moteur.

Celui-ci intègre déjà les fonctionnalités de base d'un metroidvania très simple : personnage contrôlable, plateformes mouvantes, prototype d'AI, objets à ramasser…

Nous allons donc commencer par décortiquer cet exemple et en reconstruire les fonctionnalités principales dans un nouveau projet.

Mais avant ça…

Réglages généraux du moteur

Cette section discute des réglages généraux du moteur, qui sont de fait partagés par l'ensemble des sous-modules qui constituent du projet. Pour des raisons de clarté, ces sous-modules sont ensuite discutés dans des pages dédiées du wiki.

Organisation du répertoire

Dans sa documentation, Godot conseille d'organiser les scènes et ressources de telle manière à ce que celles-ci soient facilement transférables d'un dossier à l'autre.

Un des problèmes inhérents à l'organisation nodale décentralisée est le fait que certaines relation d’interdépendance concernent des objets qui ne sont pas localisés au même endroit : il devient donc difficile de copier-coller certaines scènes dans d'autres projets, et modifier localement une ressource peut avoir des conséquences sur un autre endroit du programme. Il est donc nécessaire de séparer le plus possible les objets les uns des autres et de permettre des mécanismes de sécurité afin de palier au manque d'une ressource externe.

Également, il devient difficile de se retrouver dans l'arborescence de fichier dans certains cas. la documentation de Godot propose donc de regrouper les fichiers par thématiques : un répertoire Personnage, un répertoire Lieu, un répertoire Dialogue, etc.

Dans le cas du Platformer, le répertoire principal est lui-même divisé en deux répertoires : src et assets. src contient les éléments propres à Godot : les scènes et les scripts. assets contient les données du jeu, comme les images, les sons, etc.

Comme évoqué précédemment, le dossier src est lui-même sous-divisé en répertoire thématiques : Actors, Level, Main, etc .

Nous garderons le même principe d'arborescence pour le moteur.

Mise en place des systèmes globaux

Avant même de créer la première scène, il s'agit de mettre en place certains mécanismes généraux pour le moteur de jeu. Cela concerne :

  • Le nom des entrées utilisateurs et les boutons qui y sont associés.
  • La résolution de base ( 1920 x 1080 ) et le système de mise à l'échelle utilisé pour les moniteurs plus petits.
  • La création d'un script global pour centraliser un certain nombre de paramètres de jeu (en faisant attention au risque d'interdépendance des nœuds ultérieurs).

Mise en place des entrées utilisateurs

Le moteur étant particulièrement chauvin, je commence par utiliser l'interface d'association des entrées utilisateurs ( Projet > Paramètres du projet… > Contrôles ) pour associer les touches du clavier aux mots-clefs suivants :

  • q : “gauche”
  • d : “droite”
  • s : “bas”
  • z : “haut”
  • espace : “espace”

Même s'il est conseillé d'associer des mot-clefs par action plutôt que par touche, les différentes utilisations de la touche espace possibles m'amènent à garder une balise généraliste pour ce cas particulier.

Paramétrage de la fenêtre

J'utilise maintenant Projet > Paramètre du projet… > Général > Window afin d'assigner la résolution de la fenêtre principal.

De manière générale, les jeux sont maintenant affichés en plein écran avec une résolution de 1920 par 1080 pixels. Dans notre cas nous voulons néanmoins rester sur un affichage fenêtré afin de faciliter le développement (même si cela peut permettre de ne pas oublier d'arrêter le programme une fois que les vérifications ont été faites).

Pour les questions de redimensionnement, je modifie, dans l'onglet Stretch, les paramètres suivants :

  • Mode : 2d
  • Aspect : keep

Il faudra peut-être revoir ultérieurement ces réglages.

Création de la scène Main

Afin de pouvoir tester les réglages graphiques, il est nécessaire d'avoir une scène principale dans Godot.

Je créé donc une scène Main.tscn dans le répertoire src/Main/ .

Afin de rester polyvalent, cette scène sera de type Node, qui est la classe maîtresse dans Godot.

Mise en place d'un nœud global

Je vais maintenant utiliser le système d'AutoLoad de Godot afin de mettre en place un nœud global.

Il est possible dans Godot d'instancier les scènes indépendamment les unes des autres, ce qui est parfois problématique au cas où celles-ci auraient besoin d'autres scène pour fonctionner correctement.

Ce nœud global sera chargé automatiquement à chaque lancement du programme ce qui lui permettra de centraliser l'ensemble des ressources nécessaires à chaque scène pour fonctionner. Dans certains cas, il pourra également simuler l'existence de certaines scènes pour résoudre des problèmes de dépendance.

Pour autant, il serait préférable que chaque scène soit dotée d'un mécanisme tel que :

  • si la scène est lancée seule, un mécanisme est mis-en-place pour bloquer les méthodes nécessitant l'accès à des nœuds externes et au nœud global.
  • si le nœud global est activé, l'absence de ressources externe peut être substituée par ce nœud global.
  • si la ressource externe est chargée également, celle-ci a la priorité sur les méthodes décrite ci-dessus.

Pour mettre en place le nœud global, je créé un script global.gd situé dans le répertoire src/Main/ puis utilise le menu Projet > Paramètres du projet… > AutoLoad pour lui assigner le nom Global.

Le mot Global permet maintenant d'y accéder depuis n'importe quelle scène ( exemple : print( Global.attribut ) ).

Ajout du paramètre de gravité

Au lieu d'utiliser l'API de Godot, nous centralisons la gravité via le noeud Global :

...
var gravite = 100
...
func _ready():
	...
	ProjectSettings.set_setting( "physics/2d/default_gravity", gravite )
...

Script actuel du nœud global ( global.gd )

extends Node

var gravite = 100

func _ready():
	ProjectSettings.set_setting( "physics/2d/default_gravity", gravite )

Définition d'une unité de référence

Certaines classes de Godot ont besoin d'une unité spatiale de référence pour fonctionner, notamment les tilemaps. Cette unité aura un impact sur d'autres nœuds, par exemple le joueur dont la capacité de saut devra faire sens en fonction du tilemap.

De manière presque arbitraire, cette unité de référence sera la puissance de deux ( 32 / 64 / 128 / 256 / 512 ).

Veuillez noter que la possibilité de zoomer sur des éléments du jeu via l'utilisation du nœud Camera2D change la manière dont on devra penser le système de résolution par rapport à la taille effective des éléments graphiques en terme de collision. Le système de mipmaps se chargera de gérer ces mises-à-l'échelles.

Mise-en-place des collisions

Les masques de collision sont redéfinis pour faciliter le travail, dans les Paramètres du projet, via le menu Layer Names > 2d Physics :

  • 1 : joueur
  • 2 : ennemis
  • 3 : objets
  • 4 : terrain
  • 5 : terrain2

À ce moment de la rédaction de la documentation, il est fort probable que ces calques soient temporaires. Il y a par exemple de grandes chances qu'un calque dédié à la Camera2D soit nécessaire (notamment pour bloquer le déplacement de la caméra lors d'une chute du fatale du Joueur).

Sous-articles du moteur

Cette section présente les différents sous-articles du projet, qui documentent indépendamment les différents sous-modules du moteur :

[EN CONSTRUCTION]

Dans les versions précédente de Godot, le PlatFormer n'utilisait pas de système d'héritage pour mutualiser l'attraction terrestre entre le joueur et les ennemis. C'est maintenant le cas, et le script Player hérite d'Actor, qui lui-même de Kinematic Body 2D. Nous comprenons l'intérêt de ce système, même si les scripts en deviennent moins intuitifs.

Nous commençons par mettre en place un système similaire, en créant un répertoire src/Personnage/ et un script Personnage.gd .

C'est grave docteur ?

À ce stade, nous avons donc le script Personnage.gd , auquel nous avons simplement implémenté la gravité :

class_name Personnage
extends KinematicBody2D

export var masse = 60
onready var gravite = ProjectSettings.get( "physics/2d/default_gravity" )

const NORMAL_SOL = Vector2.UP

var _velocite = Vector2.ZERO

# la fonction _physics_process héritée est appelée
# après la fonction _physics_process parente
func _physics_process( delta ):
	_velocite.y += gravite * masse * delta

Ainsi que le script Joueur.gd :

class_name Joueur
extends Personnage

const DETECTION_DISTANCE_SOL = 20.0

func _physics_process( _delta ):
	var accrocheVectorielle = Vector2.DOWN * DETECTION_DISTANCE_SOL
	
	_velocite = move_and_slide_with_snap(
		_velocite,
		accrocheVectorielle, # Accroche au sol
		NORMAL_SOL, # Direction du sol ( Vector2.UP )
		true, # Ne glisse pas sur le sol en cas d'inactivité ?
		4, # Nombre de collisions traitées par cycle
		0.9, # Angle maximum du sol ( en radians )
		false # Inertie infinie ?
	)

Le personnage, emporté par son propre poids, est attiré vers le sol et s'y accroche à son contact.

Mouvements latéraux

Après cela, nous pouvons déplacer le personnage latéralement grâce au clavier :

Personnage.gd

class_name Personnage
extends KinematicBody2D

var direction = Vector2.ZERO

export var masse = 60
export var vitesse = 300

onready var gravite = ProjectSettings.get( "physics/2d/default_gravity" )

const NORMAL_SOL = Vector2.UP

var _velocite = Vector2.ZERO

# la fonction _physics_process héritée est appelée
# après la fonction _physics_process parente
func _physics_process( delta ):
	_velocite.y += gravite * masse * delta

Joueur.gd

class_name Joueur
extends Personnage

# Distance d'accrochage aux formes de collisions :
const DETECTION_DISTANCE_SOL = 20.0

func _physics_process( _delta ):
	var accrocheVectorielle = Vector2.DOWN * DETECTION_DISTANCE_SOL
	
	bouge()
	
	_velocite.x = direction.x * vitesse
	
	_velocite = move_and_slide_with_snap(
		_velocite,
		accrocheVectorielle, # Accroche au sol
		NORMAL_SOL, # Direction du sol ( Vector2.UP )
		true, # Ne glisse pas sur le sol en cas d'inactivité ?
		4, # Nombre de collisions traitées par cycle
		0.9, # Angle maximum du sol ( en radians )
		false # Inertie infinie ?
	)

func bouge():
	if Input.is_action_just_pressed("gauche"):
		direction.x = -1
	elif Input.is_action_just_pressed("droite"):
		direction.x = 1
	
	if Input.is_action_just_released("gauche"):
		if not Input.is_action_pressed("droite"):
			direction.x = 0
		else:
			direction.x = 1
	
	if Input.is_action_just_released("droite"):
		if not Input.is_action_pressed("gauche"):
			direction.x = 0
		else:
			direction.x = -1

Il n'y a pas d'inertie pour l'instant, le personnage commence à se mouvoir à sa vitesse maximale et s'arrête d'un coup.

Système de saut

On peut maintenant le faire sauter de plate-forme en plate-forme grâce à la touche espace :

Personnage.gd

class_name Personnage
extends KinematicBody2D

var direction = Vector2.ZERO

export var masse = 350
export var vitesse = 300
export var forceSaut = 200

var forceSautActuelle = 0

onready var gravite = ProjectSettings.get( "physics/2d/default_gravity" )

const NORMAL_SOL = Vector2.UP

var _velocite = Vector2.ZERO

# la fonction _physics_process héritée est appelée
# après la fonction _physics_process parente
func _physics_process( delta ):
	_velocite.y += gravite * masse * delta

Joueur.gd

class_name Joueur
extends Personnage

# Distance d'accrochage aux formes de collisions :
const DETECTION_DISTANCE_SOL = 20.0

onready var detectionSol = $detectionSol

func _ready():
	pass

func _physics_process( _delta ):
	var accrocheVectorielle = Vector2.DOWN * DETECTION_DISTANCE_SOL
	var surLeSol = detectionSol.is_colliding()
	
	bouge()
	
	if surLeSol:
		if Input.is_action_just_pressed("espace"):
			forceSautActuelle = forceSaut
	
	_velocite.y -= forceSautActuelle
	if forceSautActuelle > 0:
		forceSautActuelle = forceSautActuelle - gravite
		if forceSautActuelle < 0:
			forceSautActuelle = 0
	
	_velocite.x = direction.x * vitesse
	
	_velocite = move_and_slide_with_snap(
		_velocite,
		Vector2.ZERO, # Accroche au sol
		NORMAL_SOL, # Direction du sol ( Vector2.UP )
		true, # Ne glisse pas sur le sol en cas d'inactivité ?
		4, # Nombre de collisions traitées par cycle
		0.9, # Angle maximum du sol ( en radians )
		false # Inertie infinie ?
	)

func bouge():
	if Input.is_action_just_pressed("gauche"):
		direction.x = -1
	elif Input.is_action_just_pressed("droite"):
		direction.x = 1
	
	if Input.is_action_just_released("gauche"):
		if not Input.is_action_pressed("droite"):
			direction.x = 0
		else:
			direction.x = 1
	
	if Input.is_action_just_released("droite"):
		if not Input.is_action_pressed("gauche"):
			direction.x = 0
		else:
			direction.x = -1

Avec cette version, il est possible de modifier sa direction pendant le saut, ce qui n'est pas réaliste mais plutôt agréable à jouer ! Pour changer le dynamisme des sauts, il faut modifier intelligemment la masse du Joueur et sa force de saut.

dindomoteur_portail_du_moteur_de_metroidvania_orleanais.1647784877.txt.gz · Dernière modification : 2022/03/20 14:01 de Simon Deplat