Bonjour, Habr!
Un autre article vous attend sous la coupe, qui racontera comment je me suis fixé l'objectif de programmer le jeu, basé sur la traduction de l'article sur le Habr appelé Patterns de conception de niveau pour les jeux 2D .
L'article contient beaucoup de texte (régulier et source) et de nombreuses images.
Avant de commencer mon premier article, apprenons à vous connaître. Je m'appelle Denis. Je travaille en tant qu'administrateur système avec une expérience totale de 7 ans. Ce n’est pas à moi de vous dire qu’un administrateur système est un type de personnel informatique qui se déploie soigneusement une fois, puis envisage le scintillement de divers caractères sur un moniteur. Au fil du temps, je suis arrivé à la conclusion qu'il était temps d'élargir les frontières de la connaissance et de passer à la programmation. Sans entrer dans les détails, j'ai essayé de faire des projets en C ++ et Python. Mais après une année d'étude, je suis arrivé à la conclusion que la programmation de mon logiciel d'application et de système n'est pas la mienne. Pour diverses raisons.
Après réflexion, je me suis posé la question: qu'est-ce que j'aime vraiment faire avec les différents types d'équipements informatiques? Ma question à moi-même m'a jeté loin dans l'enfance, à savoir dans les happy hours passés pour PS1, PS2, Railroad Tycoon 3 pour PC ... Eh bien, vous comprenez. Jeu vidéo!
Par le nombre de supports de formation divers, le choix s'est porté sur Unity (ne pas réinventer la roue?). Après un mois de lecture et de visualisation de divers matériaux, j'ai décidé de lancer le premier jeu pour enfants très simple sur le marché du jeu. Pour surmonter la peur pour ainsi dire. Après tout, publier des applications sur le marché du jeu n'est pas effrayant, non?
Après quelques mois, j'ai sorti le jeu de plateforme déjà plus complexe. Puis il y a eu une pause (après tout, le travail doit être travaillé, après tout).
Il y a environ deux semaines, j'ai vu une traduction d'un article sur un hub appelé Level Design Patterns for 2D games ( https://habr.com/en/post/456152/ ) et je me suis dit - pourquoi pas? L'article a un tableau simple et clair avec une liste de ce qui devrait être dans le jeu pour qu'il soit intéressant. J'ai gentiment copié le tableau pour moi dans OneNote et marqué chaque motif avec la balise Cases (qui peut être marquée comme terminée).
Qu'est-ce que je veux obtenir en conséquence? Votre critique. Comme j'aime me dire - si vous voulez apprendre à nager, plongez tête baissée. Si vous pensez que j'ai bien fait quelque chose - écrivez-moi à ce sujet dans un commentaire. Si vous pensez que j'ai mal fait quelque chose, écrivez-y doublement.
Je vais commencer mon long programme pour programmer un autre jeu de plateforme.
Avatar
Une entité contrôlée par les joueurs dans un jeu. Par exemple, Mario et Luigi dans Super Mario Bros (Nintendo, 1985).
Il existe plusieurs sous-tâches qui doivent être mises en œuvre pour donner vie au héros. À savoir:
• ( ) • • • •
Pour implémenter l'animation, nous devons convertir un sprite unique en un sprite multiple. Cela se fait incroyablement simplement. Ajoutez l'image-objet au dossier du projet et recherchez-le dans l'Explorateur Explorateur Unity Explorer. Ensuite, en cliquant sur l'image-objet dans la fenêtre d'inspection, change la valeur de la propriété SptiteMode de Unique à Multiple .

Cliquez sur Appliquer , puis sur SpriteEditor .
Dans la fenêtre de l' éditeur de sprites , vous devez sélectionner chaque image de la future animation avec la souris, comme indiqué dans la figure ci-dessous.
De plus, Unity offre la possibilité de surligner automatiquement les limites des objets dans l'image-objet. Pour ce faire, dans la fenêtre de l' éditeur de sprites , vous devez cliquer sur le bouton Trancher . Dans le menu déroulant, vous devriez avoir Type => Automatique, Pivot => Centre . Il vous suffit de cliquer sur le bouton Slice . Après cela, tous les objets à l'intérieur du sprite seront sélectionnés automatiquement.

Faisons cette opération pour toutes les autres animations. Ensuite, vous devrez configurer les états d'animation et leur commutation. Cela se fait en deux étapes. La première action, le code de programme.
Créez un objet de jeu vide. Pour ce faire, cliquez avec le bouton droit sur l'onglet Hiérarchie et sélectionnez Créer vide dans le menu déroulant.

Un objet de jeu vide, qui est créé sur la scène, n'a par défaut qu'un seul composant - Transformer . Ce composant détermine la position de l'objet sur la scène, l'angle d'inclinaison et son échelle.
Vous pouvez rencontrer le mot transformer dans deux sens différents:
Pour créer votre propre composant, cliquez sur le bouton Ajouter un composant dans l'onglet de l'inspecteur d'objets. Ensuite, une boîte de recherche apparaît parmi les composants standard. Il suffit de commencer à taper le nom du futur script (ou composant déjà implémenté), s'il n'y a pas de noms appropriés, Unity vous proposera de créer un nouveau composant. J'ai appelé ce composant HeroScript.cs .

Tout d'abord, nous décrivons les champs qui stockeront des informations sur la composante visuelle et physique de Lucas:
Cap de spoiler private Animator animator;
Ensuite, les champs qui répondront au mouvement du personnage:
Cap de spoiler Vector3 localScale; bool facingRight = true; [SerializeField] private float dirX, dirY;
Un début a été fait, excellent. Ensuite, une énumération sera décrite et une propriété sera écrite qui sera chargée de changer l'état de l'animation. Cette énumération doit être écrite en dehors de la classe:
Cap de spoiler public enum CharState { idle,
Nous implémentons une propriété qui recevra et définira un nouvel état d'animation:
Cap de spoiler public CharState State { get {
La partie logicielle est terminée. Nous avons maintenant une énumération et une propriété qui seront associées au changement d'animation. Ensuite, la deuxième étape. Dans l'éditeur Unity, vous devez lier l'état de l'animation et indiquer à quelles valeurs int elles doivent être modifiées.
Pour ce faire, vous devez associer les sprites multiples créés précédemment à un objet de jeu vide. Tout ce que vous avez à faire est de sélectionner les cadres dans l'Explorateur Unity et de les faire glisser sur un objet de jeu vide, auquel nous avons précédemment fixé le script.

Faites-le avec chaque animation suivante. De plus, dans l'explorateur avec des animations, vous trouverez l'apparence d'un objet avec un diagramme et un bouton Lecture . Double-cliquez dessus pour ouvrir l'onglet Animateur . A l'intérieur, vous verrez plusieurs blocs avec animations et initialement, seuls les états d' Entrée et le premier jeu d'animation qui étaient connectés sont connectés. AnyState et les autres animations seront affichés sous forme de carrés gris normaux. Pour tout lier, vous devez cliquer sur le statut d' AnyState et sélectionner le seul menu déroulant Effectuer une transaction et le lier au bloc gris. Cette opération doit être effectuée pour chaque condition. À la fin, vous devriez obtenir quelque chose comme ce que vous voyez dans la capture d'écran ci-dessous.

Ensuite, vous devez indiquer explicitement quel état doit être exactement afin de démarrer l'animation nécessaire. Faites attention à la capture d'écran, à savoir sa partie gauche. Onglet Paramètres . Une variable de type int State y est créée. Ensuite, faites attention au côté droit. Tout d'abord, à partir de la transition de l'animation, vous devez décocher la case Can Transaction To Self . Cette opération vous évitera des transitions d'animation étranges et parfois complètement incompréhensibles à elle-même et à la section Conditions , où nous avons indiqué que cette transition d'animation avait la valeur 3 de la variable State . Après cela, Unity saura quelle animation exécuter.
Tout est fait pour le mouvement des personnages animés. Continuons.
La prochaine étape consiste à apprendre à Lucas à se déplacer sur la scène. C'est entièrement de la programmation. Pour déplacer le personnage autour de la scène, vous aurez besoin de boutons, en cliquant sur lequel Lucas va aller et venir. Pour ce faire, dans l'onglet Assets Store , nous devons importer des actifs standard, mais pas tous, uniquement quelques composants supplémentaires, à savoir:
• CrossPlatformInput
• Rédacteur
• Environnement
Après l'importation de l'actif, la fenêtre principale d'Unity doit être modifiée et un onglet supplémentaire Mobile Input apparaît. Nous l'activons.
Créons de nouveaux éléments d' interface utilisateur sur la scène - boutons de contrôle. Créez 4 boutons dans chaque direction. Haut, bas, avant et arrière. Dans le composant Image, nous attribuons aux boutons une image qui correspondra à l'image, ce qui signifie la capacité de se déplacer. Cela devrait ressembler à la capture d'écran ci-dessous:

À chaque bouton, ajoutez un composant AxisTouchButton . Ce script n'a que 4 champs. Le champ axisName signifie à quel nom répondre lorsqu'il est appelé. Le champ axisValue est responsable de la direction dans laquelle Lucas se déplacera. Le champ responseSpeed est responsable de la vitesse à laquelle Lucas développera sa vitesse. Le champ returnToCentreSpeed est responsable de la vitesse à laquelle le bouton revient au centre. Pour le bouton Suivant, laissez-le tel quel. Pour le bouton de retour, modifiez la valeur de axisValue à -1 pour que Lucas recule. Pour les boutons Haut et Bas, changez axisName en Vertical . Pour le bouton AxeValeur du bouton Haut, définissez la valeur sur 1, pour Bas -1.
Ensuite, modifiez HeroScript.cs . Ajouter un espace de noms à la directive using
using UnityStandardAssets.CrossPlatformInput;
Dans la méthode Start standard, ajoutez le code suivant:
Cap de spoiler void Start() { localScale = transform.localScale; animator = GetComponent<Animator>();
Nous créons une méthode qui sera chargée de déplacer le héros:
Cap de spoiler public void MoveHero() { dirX = CrossPlatformInputManager.GetAxis ("Horizontal") * moveSpeed * Time.deltaTime; dirY = CrossPlatformInputManager.GetAxis ("Vertical") * moveSpeed * Time.deltaTime; transform.position = new Vector2 (transform.position.x + dirX, transform.position.y + dirY); }
Comme vous pouvez le voir, tout est simple. Les champs dirX et DirY enregistrent des informations sur la direction de l'axe ( horizontal et vertical ) multipliées par la vitesse (qui devra être spécifiée dans l'éditeur) et multipliées par le temps de trajet depuis la dernière image.
transform.position écrit la nouvelle position dans le composant Transform de notre objet de jeu.
Sur le plan pratique du problème, vous pouvez exécuter la scène et voir comment Lucas tombe dans l'abîme, car aucun objet en dessous ne peut empêcher cela. Lucas est toujours dans l'animation Idle et ne se retourne pas lorsque nous le renvoyons. Pour cela, le script doit être modifié. Créez une méthode qui détermine dans quelle direction Lucas regarde:
Cap de spoiler void CheckWhereToFace () { if (dirX > 0) { facingRight = true; State = CharState.Walk; } if (dirX < 0) { facingRight = false; State = CharState.Walk; } if (dirX == 0) { State = CharState.idle; } if (dirY < 0) { State = CharState.Walk; } if (dirY > 0) { State = CharState.Walk; } if (((facingRight) && (localScale.x < 0)) || ((!facingRight) && (localScale.x > 0))) localScale.x *= -1; transform.localScale = localScale;
Cette partie du code n'est pas non plus difficile. La méthode décrit que si dirX > 0 (si nous allons à droite), alors nous faisons tourner le sprite dans cette direction et démarrons l'animation de marche. Si moins de 0, faites pivoter Lucas de 180 degrés et démarrez l'animation. Si dirX est nul, Lucas est debout et vous devez démarrer l'animation d'attente.
Pourquoi est-il préférable d'utiliser une opération avec Scale dans ce cas que d'utiliser flipX = true ? À l'avenir, je décrirai la capacité de prendre n'importe quel objet en main et, naturellement, Lucas peut se retourner en tenant quelque chose dans ses mains. Si j'utilisais le reflet habituel, l'objet que je tiendrais dans mes mains resterait sur le côté droit (par exemple) lorsque Lucas regarde vers la gauche et vice versa. Un zoom avant déplacera l'objet qui tient Lucas dans la même direction où Lucas lui-même s'est tourné.
Nous plaçons la fonction CheckWhereToFace () dans la fonction Update () , pour sa surveillance image par image.
Super. Les 2 premiers points de 5 sont complétés. Passons aux besoins de Lucas. Disons que Lucas aura 3 types de besoins qui doivent être satisfaits pour rester en vie. C'est le niveau de vie, le niveau de faim et le niveau de soif. Vous devez créer pour cela un panneau simple et compréhensible avec un indicateur de chaque élément. Pour créer un tel panneau, cliquez avec le bouton droit et sélectionnez UI => Panel .
Marquons-le approximativement comme indiqué ci-dessous

Le panneau se compose de trois images (Image) de chaque besoin (à gauche). À droite, le panneau lui-même. Sur le premier calque (nous le dirons ainsi), il y a un indicateur de couleur (Image) qui n'a pas de transparence, un objet Image est copié en dessous, qui est transparent. Cette image est à moitié transparente à l'original. En outre, Image, qui n'a pas de transparence, a la propriété Image Type = Filled . Cette fonctionnalité nous permettra de simuler une diminution de l'ampleur de l'échelle des besoins.


Définissez de nouvelles variables statiques:
Cap de spoiler [SerializeField] public static float Health = 100, Eat = 100, Water = 100, _Eat = 0.05f, _Water = 0.1f;
Dans ce cas, j'utilise des champs statiques. Cela est fait pour que ces champs soient uniques à toute la classe. De plus, cela nous permettra d'accéder directement à ces champs par nom de classe. Nous écrivons quelques fonctions simples:
Cap de spoiler private float fEat(float x) { Eat = Eat - x * Time.deltaTime; iEat.fillAmount = Eat / 100f;
Ensuite, nous écrivons une méthode qui recueille des informations sur le désir de manger et de boire:
Cap de spoiler private void Needs() { if (fEat(_Eat) < 0) { Debug.Log(Eat); } else if (fEat(0) == 0) { StartCoroutine(ifDie()); } if (fWater(_Water) < 0) { Debug.Log(Water); } else if (fWater(0) == 0) { StartCoroutine(ifDie()); }
La fonction Needs () est placée dans la fonction Update () et chaque trame est appelée. En conséquence, dans les lignes
if (fEat(_Eat) < 0)
une fonction est appelée qui passe en paramètre la quantité à retirer des variables Eat et Water . Si le résultat de la fonction n'est pas 0, alors Lucas n'est pas encore mort de soif ou de faim. Si Lucas meurt de faim ou de blessures mortelles, alors nous faisons de la coroutine
StartCoroutine(ifDie());
qui démarre l'animation de la mort et redémarre le niveau:
Cap de spoiler IEnumerator ifDie() { State = CharState.Die; yield return new WaitForSeconds(2); SceneManager.LoadScene("WoodDay", LoadSceneMode.Single); }
Carrelage dur
Un objet de jeu qui ne permet pas au joueur de le traverser. Exemple: le genre dans Super Mario Bros (Nintendo, 1985).
Pour réaliser la terre et empêcher Lucas de la traverser, vous devez connecter les composants BoxCollider2D et Rigidbody2D à Lucas. En outre, vous avez besoin d'un sprite de la terre sur lequel le composant BoxCollider2D sera situé . Le composant BoxCollider2D implémente les collisionneurs et leur comportement de collision. A ce stade, nous n'avons besoin de rien d'autre que d'empêcher l'échec de Lucas underground. Tout ce que nous pouvons éventuellement modifier est les bordures du collisionneur. Dans mon cas, le sprite au sol a une surface en herbe et pour qu'il ne semble pas que l'herbe soit capable de supporter le poids de Lucas, je vais éditer les bordures du composant.

Maintenant, un processus de marquage de niveau passionnant. Pour plus de commodité, vous pouvez exporter ce cube de terrain dans un préfabriqué. Un préfabriqué est un conteneur d'un objet de jeu, lors de la modification duquel vous pouvez appliquer automatiquement des modifications à tous les objets de jeu créés à partir de ce préfabriqué. Ensuite, clonez ce préfabriqué avec CTRL + D (après l'avoir sélectionné dans l'onglet de hiérarchie) et placez-le sur la scène.

Écran
La partie du niveau / monde du jeu qui est actuellement visible par le joueur.
Installez une caméra qui suivra le joueur sur le point d'afficher une partie de la scène. Ensuite, il y aura un script très simple à implémenter:
Cap de spoiler public GameObject objectToFollow; public float speed = 2.0f; void Update () { CamFoll(); } private void CamFoll() { float interpolation = speed * Time.deltaTime; Vector3 position = this.transform.position; position.y = Mathf.Lerp(this.transform.position.y, objectToFollow.transform.position.y, interpolation); position.x = Mathf.Lerp(this.transform.position.x, objectToFollow.transform.position.x, interpolation); this.transform.position = position; }
Dans le champ objectToFollow du type GameObject , un objet sera affecté à surveiller, et dans le champ speed, la vitesse à laquelle il est nécessaire de se déplacer en douceur derrière le GameObject affecté.
Les informations sur la vitesse de déplacement depuis la dernière image sont enregistrées dans le champ d'interpolation. Ensuite, la méthode Lerp sera utilisée, ce qui assurera un mouvement fluide de la caméra derrière Lucas lorsqu'elle se déplace le long des axes X et U. Malheureusement, je ne peux pas expliquer le fonctionnement de la ligne
position.y = Mathf.Lerp(this.transform.position.y, objectToFollow.transform.position.y, interpolation);
en termes de mathématiques. Par conséquent, je dirai plus simple - cette méthode allongera le temps d'exécution de toute action. Dans notre cas, il s'agit du mouvement de la caméra derrière l'objet.
Danger
Cap de spoilerEntités qui empêchent le joueur de terminer sa tâche. Exemple: pointes de 1001 pointes (Nicalis et 8bits Fanatics, 2014).
Commençons par ajouter quelque chose qui non seulement empêchera Lukas de traverser la scène jusqu'à la fin, mais affectera le nombre de ses vies et la possibilité de mourir (pour un, nous mettrons en œuvre le cinquième sous-problème pour mettre en œuvre l'histoire de la vie de Lucas - le héros peut être tué ou peut mourir).
Dans ce cas, nous répartissons les pointes sur la scène qui seront cachées derrière la végétation et seule l'attention du joueur aidera à passer.
Créez un GameObject vide et connectez-y les composants SpriteRenderer et PolygonCollider2D . Dans le composant SpriteRenderer , nous connectons l'image-objet du bouton barbelé ou tout autre objet comme souhaité. Attribuez également tag = Thorn à la pointe.
Ensuite, sur le Lucas GameObject, nous créons un script qui sera responsable de ce qui lui arrivera si Lucas entre en collision avec d'autres collisionneurs. Dans mon cas, je l'ai appelé ColliderReaction.cs
Cap de spoiler private Rigidbody2D rb2d; void Start() { rb2d = GetComponent<Rigidbody2D>(); } public void OnTriggerEnter2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Thorn": { rb2d.AddForce(transform.up * 4, ForceMode2D.Impulse); HeroScript.Health = HeroScript.Health - 5; } break; } }
L'essence du script est aussi simple que 2x2. Lorsqu'une balise Thorn entre en collision avec un objet de jeu, l'instruction Switch se compare aux candidats que nous avons spécifiés. Dans notre cas, pour l'instant, c'est Thorn . Tout d'abord, Lucas vomit, puis nous nous tournons vers une variable statique et prenons 5 unités de vie à Lucas. Pour l'avenir, je peux dire qu'il est logique de décrire la même chose pour un conflit avec des ennemis:
Cap de spoiler case "Enemy": { rb2d.AddForce(transform.up * 2, ForceMode2D.Impulse); HeroScript.Health = HeroScript.Health - 10; } break;
Ensuite, je propose de tuer deux oiseaux avec une pierre.
Élément collecté et règle.
Un objet de jeu que les joueurs peuvent ramasser.
Nous proposons la règle que si Lucas veut aller entre les îles et grimper, alors vous devez collecter un arbre afin de construire des ponts et des escaliers.
Selon des méthodes déjà adoptées, nous allons créer un arbre et des escaliers.
Nous allons connecter un script à l'arborescence qui sera responsable du nombre de journaux que vous pourrez supprimer si vous commencez à le couper. Étant donné que seule l'animation de l'attaque a été proposée dans l'ensemble de sprites, nous l'utiliserons lorsque nous couperons l'arbre (coûts de production).
Le script qui se trouve sur l'arborescence:
Cap de spoiler [SerializeField] private Transform inst;
Lorsque le niveau commence, nous écrivons une valeur aléatoire dans fireWood :
Cap de spoiler void Awake() { fireWood = Random.Range(4,10); }
Décrit une méthode avec un paramètre qui sera responsable du nombre de journaux qui tomberont en une seule fois:
Cap de spoiler public int fireWoodCounter(int x) { for (int i = 0; i < fireWood; i++) { fireWood = fireWood - x; InstantiateFireWood(); } return fireWood; }
Une méthode qui créera des clones de journaux sur scène.
void privé InstantiateFireWood ():
Cap de spoiler { Instantiate(FireWoodPref, inst.position, inst.rotation); }

Créons un journal et connectons-y un script avec le code suivant:
Cap de spoiler public void OnTriggerEnter2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Player": { if (InventoryOnHero.woodCount > 10) { Debug.Log(" !"); } else { InventoryOnHero.woodCount = InventoryOnHero.woodCount + 1; Destroy(this.gameObject); } } break; } }
Ensuite, nous créerons également une classe qui sera responsable de l'inventaire.
Vérifiez d'abord s'il y a de la place dans le sac. Sinon, alors l'erreur et le journal restent à mentir, s'il y a de l'espace, alors nous réapprovisionnons l'inventaire d'une unité et détruisons le journal.
Ensuite, vous devez faire quelque chose avec ces ressources. Comme mentionné ci-dessus, nous offrons au joueur la possibilité de construire des ponts et des escaliers.
Pour créer un pont, nous avons besoin de 2 préfabriqués avec la moitié gauche et droite du pont. BoxCollider2D . , , - , .
:
Cap de spoiler [SerializeField] private Transform inst1, inst2;
:
Cap de spoiler public void BuildBridge() { if (InventoryOnHero.woodCount == 0) { Debug.LogWarning (" !"); } if (InventoryOnHero.woodCount > 0) { BridgeCount = BridgeCount - 1; InventoryOnHero.woodCount = InventoryOnHero.woodCount - 1; } switch (BridgeCount) { case 5: Inst1(); break; case 0: Inst2(); break; default: Debug.LogWarning("- "); break; } }
, , . , 10 , 12 8.
, , , , . , 1 , 1 . , 5, , . 0, . , .
.
, ColliderReaction.cs :
Cap de spoiler void OnTriggerStay2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Ladder": { rb2d.gravityScale = 0; } break; } } void OnTriggerExit2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Ladder": { rb2d.gravityScale = 1; } break; } }
OnTriggerStay2D , . , 0. , . OnTriggerExit2D , .
, .
19 , . , , , , , .
GO, SpriteRenderer , BoxCollider2D , Rigidbody2D . , — , . , ru.stackoverflow.com.

Trees .
, . , -, , Raycast 2 (4 ). , , , ( ). ( ), . , . , , . , . , , ( , ).
, . , - .
:
Cap de spoiler [SerializeField] private GameObject area; private bool m1 = true, m2;
Update() , . , 3 , . 3, , .

, .
Cap de spoiler private void fSunFlower() { canBullet = canBullet - minus * Time.deltaTime; if (canBullet <= 0 && SR.flipX == false) { GameObject newArrow = Instantiate(sunFlowerBullet) as GameObject; newArrow.transform.position = transform.position; Rigidbody2D rb = newArrow.GetComponent<Rigidbody2D>(); rb.velocity = sunFlowerTrans.transform.forward * -sunFlowerBulletSpeed; canBullet = 2; } if (canBullet <= 0 && SR.flipX == true) { GameObject newArrow = Instantiate(sunFlowerBullet) as GameObject; newArrow.transform.position = transform.position; Rigidbody2D rb = newArrow.GetComponent<Rigidbody2D>(); rb.velocity = sunFlowerTrans.transform.forward * sunFlowerBulletSpeed; canBullet = 2; }
canBullet = canBullet - minus * Time.deltaTime;
, .
Cap de spoiler if (canBullet <= 0 && SR.flipX == false) { GameObject newArrow = Instantiate(sunFlowerBullet) }
, , , , :
Cap de spoiler public int Damage(int x) { Health = Health - x; return Health; }
, , :
Cap de spoiler public void ifDie() { if (Damage(0) <= 0) { Destroy(this.gameObject); } }
0, .
, :
Cap de spoiler if (bGreenMonster) { fGreenMonster(); } if (bSunFlower) { fSunFlower(); }
, .

.
, ?
, .
:

:
Cap de spoiler [SerializeField] private Transform Hero;
, :
Cap de spoiler private void AttackBtn() { if (CrossPlatformInputManager.GetButtonDown("Attack")) { GameObject.Find("Hero").GetComponent<HeroScript>().State = CharState.AttackA; Collider2D[] Trees = Physics2D.OverlapCircleAll(Hero.position, distWhatHeroSee, Tree); for (int i = 0; i < Trees.Length; i++) { Trees[i].GetComponent<TreeControl>().fireWoodCounter(1); Debug.Log("Trees Collider"); HeroScript.Water = HeroScript.Water - 0.7f; }
GameObject.Find("Hero").GetComponent<HeroScript>().State = CharState.AttackA;
, .
, :
Cap de spoiler Collider2D[] Trees = Physics2D.OverlapCircleAll(Hero.position, distWhatHeroSee, Tree); for (int i = 0; i < Trees.Length; i++) { Trees[i].GetComponent<TreeControl>().fireWoodCounter(1); Debug.Log("Trees Collider"); HeroScript.Water = HeroScript.Water - 0.7f; }
Trees , . , , , . .
, . Simple as that!
, - :

, — .
, . , , , .
2 , .
Bonne chance
.
https://opengameart.org/ , :