Développer un jeu de ferme 2D dans Unity nécessite de traiter des actions apparemment distinctes, comme couper un arbre ou planter une graine, comme un unique protocole de changement d'état. Couper représente une transition destructive (libération de ressources, fin de cycle de vie), tandis que planter initie un flux temporel (croissance, arrosage). Ces deux actions partagent des contraintes strictes : un positionnement spatial au pixel près, une indépendance face à l'interface utilisateur, des retours multicouches, et une sérialisation sans perte pour la sauvegarde.
Remplacer les événements UI par une détection spatiale pilotée par état
Utiliser des composants Button ou des RaycastTarget sur des sprites est une approche fragile. Elle échoue dès que la densité d'objets augmente ou que l'on vise le tactile. Une architecture robuste sépare la détection de l'intention :
- Acquisition spatiale : Convertir la position de l'écran en coordonnées mondiales via
Camera.main.ScreenToWorldPoint. - Filtrage géométrique : Évaluer la distance entre le point de contact et l'ancrage de l'objet. Un rayon d'action dynamique, dérivé de la taille du
SpriteRenderer, permet d'ajuster la tolérance. - Résolution d'état : Une table de transition détermine l'action selon l'outil équipé et l'état courant de la cible.
Pour les animations interactives (comme la coupe progressive), l'Animator doit permettre les interruptions. Désactivez Has Exit Time et pilotez la progression via un paramètre float. L'évolution des données métier doit rester couplée à la progression visuelle pour éviter toute désynchronisation.
Modèle de retour sur interaction à quatre niveaux
Une interaction réussie nécessite une validation cognitive immédiate à travers quatre couches :
| Couche | Implémentation technique | Objectif |
|---|---|---|
| Visuelle | Pulsation de SpriteRenderer.color (ex: alpha 1.0 à 0.7) |
Accusé de réception instantané |
| Sonore | AudioSource.PlayOneShot avec variation de hauteur aléatoire |
Éviter la lassitude auditive |
| Données | Mise à jour de l'inventaire (ex: Inventaire.Ajouter("Bois", 3)) |
Synchro de l'état brut du joueur |
| Monde | Modification de l'état global (ex: révéler un objet enfoui) | Impact physique sur l'environnement |
Gestion de l'oclusion : Cartographie de profondeur Z
Limites des Sorting Layers
Les Sorting Layer et Order in Layer d'Unity offrent un ordre de rendu discret, inadapté aux espaces continus. Il est impossibel de représenter correctement un joueur passant derrière un grand arbre (la tête dépasse, le corps est caché) ou de gérer dynamiquement des objets longs comme les canaux d'irrigation.
Implémentation d'une RenderTexture de profondeur
La solution consiste à générer une carte de profondeur basée sur l'axe Y (représentant l'axe Z en 2D). Une RenderTexture stocke la position Y de chaque objet. Les sprites lisent ensuite cette texture pour déterminer leur propre opacité.
Shader d'écriture (ZDepthWrite) :
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.depth = v.vertex.y; // Y agit comme profondeur spatiale
return o;
}
half4 frag(v2f i) : SV_Target {
return half4(i.depth, i.depth, i.depth, 1.0);
}
Shader de lecture (ZDepthRead) :
half4 frag(v2f i) : SV_Target {
float sceneDepth = tex2D(_ZDepthTex, i.uv).r;
float selfDepth = i.worldPos.y;
// L'objet est transparent s'il est derrière un objet ayant une plus grande valeur Y
float alpha = (selfDepth > sceneDepth) ? 0.0 : 1.0;
return half4(i.color.rgb, i.color.a * alpha);
}
Pour optimiser les performances, divisez la scène en couches de mise à jour : statique (calcul unique), semi-dynamique (rafraîchie toutes les 2 secondes) et dynamique (mise à jour par frame dans le champ de vision). Un LoD (Level of Detail) basé sur la distance réduit la précision des cultures éloignées.
Réutilisation du protocole et actions atomiques
Abstraction des sources d'interaction
L'implémentation d'une scierie automatisée ne nécessite pas de réécrire la logique de coupe. En abstrayant le déclencheur, on s'assure que seuls les paramètres d'entrée changent.
public interface IInteractif {
EtatObjet EtatCourant { get; }
float RayonAction { get; }
void ExecuterInteraction(ContexteInteraction ctx);
}
public class Arbre : MonoBehaviour, IInteractif {
public void ExecuterInteraction(ContexteInteraction ctx) {
if (ctx.Source == TypeSource.Joueur && ctx.Outil == TypeOutil.Hache) {
DemarrerAnimationCoupe();
} else if (ctx.Source == TypeSource.Batiment && ctx.Batiment == TypeBatiment.Scierie) {
TraiterAutomatiquement(); // Réutilise la logique interne sans animation
}
}
}
Décomposition de l'agriculture
Planter ne doit pas être une fonction monolithique. La division en actions atomiques indépendantes permet une flexibilité maximale (ex: arrosage automatique par la pluie). Les étapes incluent CreuserTrou, PlacerGraine, RecouvrirTerre, Arroser, et Pousser, chacune pilotée par un ScriptableObject définissant ses conditions et annulations.
Dérivés techniques : Ligne de vue et atténuation sonore
La carte de profondeur Z offre des extensions fonctionnelles gratuites. La ligne de vue (Line of Sight) se calcule en échantillonnant les pixels entre deux points :
public bool PeutVoir(Vector2 cible) {
Vector2 dir = (cible - positionJoueur).normalized;
float dist = Vector2.Distance(positionJoueur, cible);
for (float d = 0.2f; d < dist; d += 0.2f) {
Vector2 echantillon = positionJoueur + dir * d;
float profondeur = EvaluerProfondeurZ(echantillon);
if (profondeur < positionJoueur.y) return false;
}
return true;
}
Optimisations et pièges techniques courants
- Atlas et compression : La compression de texture (ETC1/ASTC) altère les limites des sprites. Désactivez la compression et les Mip Maps pour les sprites interactifs pour éviter un décalage fatal du point d'ancrage.
- Colliders de Tilemap : Évitez
CompositeCollider2Dpour les objets fins (canaux). UtilisezTilemapCollider2Davec des formes physiques personnalisées pour garantir des points de contact précis. - Hash d'Animator : Ne nommez pas les états avec des chaînes complexes susceptibles de varier entre les versions d'Unity. Préfixez-les avec des identifiants numériques stricts (ex:
001_EtatCoupe). - Particules de pluie : Ne basez pas leur profondeur sur la texture de la frame précédente. Calculez leur ordre de rendu via
_WorldSpaceCameraPos.y - worldPos.ypour éviter qu'elles ne clignotent à travers les cultures. - Tactile multi-touch : Sur mobile, bloquez les interactions si
Input.touchCount > 1pour éviter de déclencher une coupe lors d'un pincement pour zoomer. - Sérialisation des flottants : Ne sérialisez jamais la croissance d'une plante avec un
floatbrut. Convertissez en entier (millisecondes ou ticks) avant la sauvegarde pour éviter les boucles de progression infinies dues aux erreurs d'arrondi. - Flip des Sprites : Remplacez
SpriteRenderer.flipXpar une inversion detransform.localScalepour garantir la cohérence de la carte de profondeur lors des miroirs. - Espace UI vs Monde : Forcez la conversion
Camera.main.ScreenToWorldPointau début de chaque logique de clic pour éviter qu'un appui sur un bouton d'inventaire n'affecte les cultures en arrière-plan. - Animation Curve : Définissez le
Wrap ModesurClampet gérez les boucles manuellement vianormalizedTimepour éviter les sauts d'état brusques. - Espace de simulation des particules : Les particules d'arosage doivent être en
Simulation Space: Worldpour suivre les mouvements du joueur sans se désolidariser du point d'émission. - UV Sampling : Configurez le
Wrap Modede la RenderTexture de profondeur surClamppour empêcher l'apparition de lignes noires aux bords de l'écran. - Color Space : Vérifiez la cohérence entre les paramètres de
Color Space(Linear/Gamma) du projet et l'importation des textures (sRGB) pour que les variations d'opacité des retours visuels soient exactes.