Architecture d'un système d'interaction unifié pour jeux de ferme 2D sous Unity

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 :

  1. Acquisition spatiale : Convertir la position de l'écran en coordonnées mondiales via Camera.main.ScreenToWorldPoint.
  2. 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.
  3. 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 CompositeCollider2D pour les objets fins (canaux). Utilisez TilemapCollider2D avec 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.y pour éviter qu'elles ne clignotent à travers les cultures.
  • Tactile multi-touch : Sur mobile, bloquez les interactions si Input.touchCount > 1 pour é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 float brut. 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.flipX par une inversion de transform.localScale pour garantir la cohérence de la carte de profondeur lors des miroirs.
  • Espace UI vs Monde : Forcez la conversion Camera.main.ScreenToWorldPoint au 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 Mode sur Clamp et gérez les boucles manuellement via normalizedTime pour éviter les sauts d'état brusques.
  • Espace de simulation des particules : Les particules d'arosage doivent être en Simulation Space: World pour suivre les mouvements du joueur sans se désolidariser du point d'émission.
  • UV Sampling : Configurez le Wrap Mode de la RenderTexture de profondeur sur Clamp pour 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.

Étiquettes: Unity 2D Architecture de jeux Shader RenderTexture State Machine

Publié le 12 juillet à 05h27