
"Driftwood" est un jeu de type playground en FPS, dans lequel le joueur incarne un astronaute chargé d'explorer les ruines d'une planète alien.
Genre: Playground, énigme
Moteur de jeu: Unity
Taille d'équipe: 6
Durée: Octobre 2024 - Mai 2025
Plateforme: PC
Moteur de jeu: Unity
Taille d'équipe: 6
Durée: Octobre 2024 - Mai 2025
Plateforme: PC
Résumé de la mécanique
Le joueur évolue sur une planète physique, il a la capacité de voler des comportements aux objets présents dans son environnement et de les attribuer à d'autres objets ou à lui-même.
Liste des comportements :



- Immuable, l'objet n'est plus affecté par les forces autour de lui, gravité comprise, et reste figé


- Impulsion, émet des ondes de choc à intervalle régulier


- Aimant, attire tout objet vers lui


- Fusée, projette l'objet dans une direction donnée à intervalle régulier


- Rebond, change la propriété physique de l'objet et le fait rebondir
Chaque objet peut porter jusqu'à deux comportements, ceux-ci auront alors des propriétés différentes.
Rôle
J’ai été responsable de la programmation de différents comportements, ainsi que de la mise en place et de la navigation de l’interface utilisateur. J’ai également conçu le système d’inventaire et le système de sauvegarde.
Comportement (exemple)
Fusion Aimant et Fusée
Pour cette combinaison, l'idée était que l'objet, quand la poussée est activée, laisse une trainée derrière lui qui attire tout ce qui passe dedans
--WIP--






Fusion Aimant et Rebond
Pour son comportement standard, les objets dans la zone magnétique sont attirés à chaque rebond de l'objet.
Ce comportement change légèrement lorsqu'il est porté par le joueur : sa zone est réduite et ne revient à sa position initiale que lorsqu'il entre en contact avec une surface.
Ce comportement change légèrement lorsqu'il est porté par le joueur : sa zone est réduite et ne revient à sa position initiale que lorsqu'il entre en contact avec une surface.
-- WIP --
Inversion du feedback des comportements appliqué sur le joueur
J’ai intégré l’inversion du feedback des comportements appliqués au joueur dans un code existant développé par un membre de l’équipe, ce qui m’a demandé de m’adapter à une structure déjà en place.
Système

Interaction
Dans notre jeu, le joueur a la possibilité de réaliser diverses actions avec son environnement. Pour le moment, seulement deux sont mises en place, le "porter d'objet" (Movable) et le "ramassage de collectables" (PickUp) mais, nous avons prévu d'ajouter d'autres types d'interaction plus tard.
Afin de faciliter l'ajout de nouveau type d'interaction, j'ai mis en place un système d'interaction basé sur une architecture modulaire par héritage.
Ainsi, chaque interaction est un enfant de InteractableObject et chacun a une fonction Interact qui exécute sa propre action. Cela nous permet d'ajouter ou de supprimer des interactions sans impacter le fonctionnement du système, qui lui exécute seulement la fonction Interact de l'objet qu'il a récupéré.
Ainsi, chaque interaction est un enfant de InteractableObject et chacun a une fonction Interact qui exécute sa propre action. Cela nous permet d'ajouter ou de supprimer des interactions sans impacter le fonctionnement du système, qui lui exécute seulement la fonction Interact de l'objet qu'il a récupéré.
Collectables
Nous disposons de deux types d'objets que le joueur peut ramasser :
- Les ressources dont le joueur aura besoin pour avancer dans son exploration et la réalisation de quêtes, ou qui servent uniquement de trophée.
- Des cassettes pour pouvoir changer la musique d'ambiance du jeu.
Ces deux types d’objets partagent de nombreuses caractéristiques (nom, icône, description...), j’ai donc conçu un système basé sur des ScriptableObjects, afin de faciliter leur gestion plus tard.
- Les ressources dont le joueur aura besoin pour avancer dans son exploration et la réalisation de quêtes, ou qui servent uniquement de trophée.
- Des cassettes pour pouvoir changer la musique d'ambiance du jeu.
Ces deux types d’objets partagent de nombreuses caractéristiques (nom, icône, description...), j’ai donc conçu un système basé sur des ScriptableObjects, afin de faciliter leur gestion plus tard.
Outils


Création d’outils de génération de données (ScriptableObject) pour les objets collectables (Souvenirs et Cassettes)
J’ai développé un outil interne permettant de générer plus efficacement les données liées à nos objets collectables. Cet outil inclut :
- un aperçu (preview) des sprites et modèles associés
- des listes déroulantes dynamiques récupérant automatiquement les noms des modèles dans les dossiers de stockage (sprites, modèles)
- la récupération des noms des pistes audio FMOD (fichiers .mp3)
- des listes déroulantes dynamiques récupérant automatiquement les noms des modèles dans les dossiers de stockage (sprites, modèles)
- la récupération des noms des pistes audio FMOD (fichiers .mp3)
L’objectif était de faciliter la création de nouveaux objets et leur intégration future dans le projet.
Pour nos tests, j'ai mis en place une fenêtre qui permet de moduler le contenu de l'inventaire (juste fonctionnel, pas visuel pour le moment)

Spawner LD
Afin de faciliter le placement de collectables et d'autres objets d'environnement sur des surfaces incurvées qui pouvaient être fastidieux, un spawner a été développé.
Chaque type d’objet dispose d’une prefab dédiée permettant son instanciation. Lorsqu’un objet doit être placé, sa référence ainsi que sa position sont transmises au PrefabSpawner.
Le PrefabSpawner émet alors un raycast vers le bas, récupère la normale du point d’impact, puis calcule l’orientation que doit avoir l’objet instancié pour s’aligner correctement avec la surface.







Fragments
Ressources récupérables en grande quantité quand un pot est brisé.
Chaque fois qu'un pot est détruit, il va instancier un nombre défini à l'avance de fragments. Ceux-ci sont propulsés aléatoirement autour du pot.
S'ils sont dans la zone de détection autour du joueur, ils vont commencer à avancer vers le joueur avec un MoveToward puis si leur distance avec lui est assez courte, ils vont être ramassé.
Chaque fois qu'un pot est détruit, il va instancier un nombre défini à l'avance de fragments. Ceux-ci sont propulsés aléatoirement autour du pot.
S'ils sont dans la zone de détection autour du joueur, ils vont commencer à avancer vers le joueur avec un MoveToward puis si leur distance avec lui est assez courte, ils vont être ramassé.
(WIP)
-> Pot, morceaux brisés : désactivation des colliders et passage en kinematic quand leur rigidBody est "arrêté" pendant un temps
-> Récupération et stockage du nombre de fragments non ramassés qui était dans la scène. Création d'une interaction permettant de récupérer ces fragments après être revenu en jeu.
Une gestion différente
La récupération des fragments n'étant pas directement une action voulue et faite par le joueur mais, plus une action imposée quand il passe à portée, j'ai fait le choix de gérer leur récupération en dehors de mon système d'interaction.
La récupération des fragments n'étant pas directement une action voulue et faite par le joueur mais, plus une action imposée quand il passe à portée, j'ai fait le choix de gérer leur récupération en dehors de mon système d'interaction.

Architecture des 3 inventaires --WIP--
Aide à la visée
La portée de détection des objets capables de recevoir un comportement étant infinie, il est très difficile de les cibler lorsqu’ils sont trop éloignés. J’ai donc mis en place un système d’assistance pour le joueur.
Dans un premier temps un raycast est tiré dans la direction avant de la caméra, si un objet pouvant recevoir un comportement est détecté, il est retourné.
Dans le cas contraire, un SphereCastAll est effectué afin de récupérer tous les objets recevant un comportement dans son rayon. Puis, on détermine lequel est le plus centré par rapport à la vue du joueur en utilisant un produit scalaire, puis on compare ce résultat à un seuil angulaire maximal pour valider la détection de l’objet.


Sans assistance

Avec assistance
Animation
Dans notre jeu, nous avons l'intention de placer des golems, chacun portant un type de comportement différent.
Pour leur donner un mouvement plus organique, notamment lors de leurs déplacements, j'ai choisi de gérer leur animation de manière procédurale.
Pour cela, j'ai utilisé le package Animation Rigging de Unity, en l’appliquant sur les modèles 3D riggés fournis par mon équipe. Ce package m’a permis d’ajouter des contraintes, notamment grâce à l’IK (Inverse Kinematics) qui permet de gérer la position d'un point final (un pied par exemple) et d'ajuster automatiquement les rigs intermédiaires. Cela permet de gérer dynamiquement des comportements comme le positionnement des pieds sur des surfaces irrégulières.
Position des jambes (basique WIP)
Un objet vide associé à chaque jambe est placé et est automatiquement placé sur la surface du sol sous lui par script. Cet objet définit la position par défaut que le pied de la jambe doit atteindre.
Lors du mouvement du golem, si la distance entre le pied et la position par défaut dépasse un seuil, on replace le pied à sa position par défaut. Le souci est que le golem lui peut toujours être en mouvement, par conséquent une fois le pied arrivé à sa position, il est en retrait par rapport au golem. Il faut donc que la position cible de la jambe soit en avance par rapport à sa position par défaut.
Pour cela, on calcule la vélocité du golem et sa direction de marche.
Pour cela, on calcule la vélocité du golem et sa direction de marche.
Position cible =
Position par défaut + (vitesse du golem réduite * direction vers position par défaut) + (vitesse du pot * facteurAjustement)
Position par défaut + (vitesse du golem réduite * direction vers position par défaut) + (vitesse du pot * facteurAjustement)






Rotation (designée WIP)
Lors d’un déplacement, le Golem évalue l’angle entre sa direction actuelle (vecteur forward local) et la direction vers la position cible.
- Si l’angle est inférieur à un seuil, le Golem suit sa trajectoire normalement
- Sinon, il s’arrête, fait une rotation sur lui-même pour s’aligner avec la direction cible, puis avance à nouveau.
- Sinon, il s’arrête, fait une rotation sur lui-même pour s’aligner avec la direction cible, puis avance à nouveau.
Définition du parcours (designée WIP)
Étant sur une planète, de forme sphérique, nous n’avons pas la possibilité d’utiliser le navmesh pour gérer la circulation de nos golems.
À l’aide d’un outil custom, plusieurs points de référence sont placés sur la surface de la planète.
Ces points servent à définir une zone que l’on “place” au-dessus de la planète. À l’intérieur de cette zone, un point aléatoire est sélectionné, puis projeté en direction de la planète.
Le point d’impact devient alors la position cible que le Golem devra atteindre.

UI




Wireframe
Inventaire : une liste de slots « vides » générés de façon dynamique à partir d’un nombre donné (tous les collectables n’ont pas encore été définis).
Inventaire : une liste de slots « vides » générés de façon dynamique à partir d’un nombre donné (tous les collectables n’ont pas encore été définis).
Les informations des collectables présents dans l’inventaire disposent d’un rendu 3D affiché dans la section « informations » quand on clique dessus.
Musique : la musique sélectionnée est placée au centre du viewport.
Chaque slot (qu'il soit un collectable ou une cassette) est une prefab de bouton, auquel sont transmises les données d’un ScriptableObject pour le remplir dynamiquement. Un événement OnClick est ensuite assigné afin de gérer les interactions.
Sauvegarde

Pour conserver la progression du joueur et mémoriser de multiples paramètres, tels que le comportement de chaque objet, le nombre de fragments et de collectables obtenus, ainsi que les objets ramassés ou non, j'ai décidé d’opter pour un système de sauvegarde au format JSON.
Juste avant que ma scène ne soit déchargé, un script de sauvegarde se lance, stocke toutes les données dont j'ai besoin dans une classe de données, puis elle est encodée en JSON.
À l'inverse au chargement de la scène, le fichier JSON est récupéré, et on va mettre à jour nos scriptable objects correspondants, qui seront par la suite utilisés par les autres scripts du projet.
Problème majeur rencontré : Sauvegarder les états de ma scène de façon bien distinct (Par exemple un pot qui a été cassé, doit resté cassé quand je recharge ma scène)
Solution création d'un ID unique en mode éditeur.
Juste avant que ma scène ne soit déchargé, un script de sauvegarde se lance, stocke toutes les données dont j'ai besoin dans une classe de données, puis elle est encodée en JSON.
À l'inverse au chargement de la scène, le fichier JSON est récupéré, et on va mettre à jour nos scriptable objects correspondants, qui seront par la suite utilisés par les autres scripts du projet.
Problème majeur rencontré : Sauvegarder les états de ma scène de façon bien distinct (Par exemple un pot qui a été cassé, doit resté cassé quand je recharge ma scène)
Solution création d'un ID unique en mode éditeur.
Identifiant unique
Afin de sauvegarder l'état des éléments de ma scène j'ai mis au point une génération d'un identifiant aléatoire en utilisant le Guid.NewGuid() afin de suivre et référencer de manière fiable mes objets à travers la scène et ma sauvegarde.
Lorsque le champs ID est vide, il en génère. Le problème de cette méthode, c'est que lorsque que mon objet est dupliqué dans la scène, l'ID l'est également ce qui pose problème.
Je dois donc gérer si l'ID généré est celui d'une duplication ou non. À terme j'ai élargi à "est-ce que mon ID est déjà dans la scène".
Lorsque le champs ID est vide, il en génère. Le problème de cette méthode, c'est que lorsque que mon objet est dupliqué dans la scène, l'ID l'est également ce qui pose problème.
Je dois donc gérer si l'ID généré est celui d'une duplication ou non. À terme j'ai élargi à "est-ce que mon ID est déjà dans la scène".
Pour cela j'ai 2 scripts qui fonctionnent de pair :
- UniqueID, placé sur chaque objet, non unique, que je veux récupérer pour ma sauvegarde.
- UniqueIDManagerEditor, qui va se lancer quand Unity se lance et à chaque ouverture de scène
- UniqueID, placé sur chaque objet, non unique, que je veux récupérer pour ma sauvegarde.
- UniqueIDManagerEditor, qui va se lancer quand Unity se lance et à chaque ouverture de scène

Script UniqueID
À chaque Awake et OnValidate, il va lancer une fonction CheckUniqueID qui vérifie deux choses :
- Si son champ d'ID est vide
- Si son ID existe déjà dans la scène (utilise une fonction de UniqueIDManagerEditor)
- Si son champ d'ID est vide
- Si son ID existe déjà dans la scène (utilise une fonction de UniqueIDManagerEditor)
Si l'un des cas est vrai, alors il va générer un nouvel identifiant, et mettre à jour l'identifiant du script qui correspond à l'objet que l'on veut sauvegarder (ici, les pots, pour sauvegarder l'état cassé ou non).
UniqueIDManagerEditor
Ce script statique est exécuté automatiquement lors de l'ouverture d'une scène ([InitializeOnLoad]). Il maintient une table de correspondance entre tous les ID et leurs objets associés, afin de :
- Stocker les ID au démarrage de la scène avec l'event EditorSceneManager.sceneOpened
- Garantir l'unicité des ID
- (Ré)générer les ID qui entre en conflit ou manquants
- Garantir l'unicité des ID
- (Ré)générer les ID qui entre en conflit ou manquants




