Go to Top

Dans ce tutorial

Le projet du tutoriel

Vous pouvez télécharger le projet ici.

Le projet est organisé de telle sorte que chaque scène montre une partie différente du tutoriel.

Chaque scène, script et objet utilisés sont situés sous Tutorial/Part1.

Vous verrez un indicateur lorsque le tutoriel change de scène.

Tutoriel Scene 1

Le tutoriel utilise un certain nombre d'assets gratuits de l'Asset Store.

Pourquoi les Quaternion sont utiles

Les quaternions sont utilisés pour décrire les rotations dans Unity, ils sont le format utilisé en interne plutôt que les matrices de transformation.

Il ya une confusion immédiate avec quaternions pour de nombreux utilisateurs - et cela est dû au fait que lorsque vous regardez un objet dans l'inspecteur, la rotation est représentée avec un Vector3 avec des rotations autour de X, Y et Z. Ce sont en fait les  eulerAngles de la rotation - et non le Quaternion lui-même. Cette confusion est aggravée par le fait que les quaternions ont des propriétés appelées x, y et z - mais ceux-ci n'ont rien à voir avec les valeurs spécifiées dans l'inspecteur (ou du moins très peu)!

Il ya plusieurs façons de faire un Quaternion - à partor d'un Vector3 d' angles, vous utilisez Quaternion.Euler soit avec les 3 axes de rotation comme paramètres ou comme un Vector3 décrivant la rotation. Pour faire une rotation qui regarde dans une direction, vous utilisez Quaternion.LookRotation (directionVector). Dans ce tutoriel, vous allez apprendre d'autres façons d'utiliser les quaternions et comment les combiner.

De nombreux débutants tombent dans le piège suivant:

transform.rotation = new Vector3(100,100,100);
transform.rotation = new Quaternion(100,100,100,1);

La première ligne renverra une erreur de conversion. La seconde passera le cap de la compilation mais pas celui de votre compréhension. Vous voulez que votre objet ait une rotation de (100,100,100) cependant dans l'inspecteur vous voyez des valeurs différentes. Pourtant vous avex passé (100,100,100,1) (c'est quoi ce 1...?). En fait vous n'avez pas donné une rotation mais plutôt un vecteur vers lequel l'objet regarde. Le 1 de fin est la quatrième dimension ou le domaine des nombres complexes/imaginaires. En simplifié, nous passons un vector3 cible que nous interpolons avec le 1 (cette valeur peut être 1 ou -1 mais apparait le plus souvent comme 1).

Ne pas manipuler les x, y, z et w d'un Quaternion si vous n'êtes pas sûr de ce que vous faites. Ces valeurs ne sont pas directement liées aux valeurs affichées dans l'Inspecteur.

Résumé sur eulerAngles et Quaternions:

EulerAngles:

Avantages:

  • Facile à comprendre et à visualiser puisqu'ils utilisent un vector 3D.
  • Ils sont calculés plus rapidement du simple fait qu'ils ne contiennent que 3 valeurs (4 pour le Quaternion).

Inconvénients:

  • Une rotation peut produire différent résultats. Différents logiciels utilisent des ordres différent (x,y,z) ou (y,x,z) ou autres. Le résultat est différent à chaque fois.
  • l'effet Gimbal Lock. Quand une rotation s'aligne avec une autre, elles se bloquent ensemble et une dimension est perdue.

Quaternions:

Avantages:

  • Evitent l'effet de Gimbal Lock
  • La rotation est basée sur un vecteur cible et non un ensemble de rotations indépendantes.

Inconvénients:

  • Ils sont légèrement plus lents que les eulerAngles du fait de la valeur en plus.
  • Un humain normalement constitué ne peut les comprendre et les maitriser entièrement en une seule vie.

Vous pouvez combiner des rotations multiples en utilisant l'opérateur * (qui vous sera utile plus tard dans ce tutoriel) et vous pouvez également modifier un Vector3 en multipliant par un quaternion, cela renvoie une version tournée du vecteur. L'ordre de la manipulation Quaternion est significatif.

var newVector = Quaternion.AngleAxis(90, Vector3.up) * Quaternion.LookRotation(someDirection) * someVector;
Vous n'additionez ou ne combinez pas deux Quaternions avec + (plus).  L'opérateur * (multiplication) agit comme un moyen de cumuler plusieurs Quaternions .
Une des raisons pour l'utilisation de Quaternion plutôt que Vector3 pour décrire les rotations, c'est que si un Vector3 peut décrire une rotation, il n'est pas possible de faire pivoter un objet en déplaçant les trois valeurs rotation de l'axe vers un ensemble de valeurs cibles. Cela est dû à un effet appelé Gimbal Lock qui affecte les EulerAngles, si vous êtes intéressés, vous pouvez en apprendre plus à ce sujet sur Wikipedia. Il se produit en raison de la nécessité de déplacer chaque valeur individuellement et dans certaines combinaisons ce qui crée une situation où la valeur cible ne peut pas être atteinte dans une dimension due aux mouvements dans les autres. Les Quaternions éliminent ce problème en faisant une rotation complète en une seule étape autour d'un axe unique - essentiellement en utilisant 4 dimensions d'espace!

EulerAngles sont faciles à représenter, mais sont beaucoup moins puissants que les Quaternions. Avec Unity, il est facile de se déplacer entre les deux représentations afin que vous puissiez choisir celle qui convient le mieux à chaque fois. Si vous restraindre les rotations, par exemple, vous pouvez trouver les eulerAngles d'un Quaternion, puis appliquez votre opération de restriction avec Mathf.Clamp puis tournez les angles de nouveau dans un quaternion en mettant à jour la variable eulerAngles ou en créant un nouveau Quaternion avec Quaternion.Euler (yourAngles).

Lorsque vous voulez combiner des rotations, comme une inclinaison de la tête et une rotation du corps, vous pouvez utiliser les deux approche, mais dès que les rotations ne sont plus autour de l'axes général (world axis), par exemple votre personnage peut aussi être penché en avant, alors vous préfèrerez les Quaternions. Il est très simple d'utiliser un Quaternion pour créer une simple rotation autour de chaque axe que vous souhaitez, puis les combiner.

Unity fournit également des axes très utiles transform.forward, transform.right et transform.up qui peuvent être utilisés en conjonction avec Quaternion.AngleAxis pour créer des rotations qui peuvent être appliquées en plus d'une rotation de l'objet existant.

transform.rotation = Quaternion.AngleAxis(degrees, transform.right) * transform.rotation;

Faire que l'ennemi regarde le joueur avec réalisme

Une chose bien commune que nous devons faire souvent est de faire quelque chose regarder dans une direction. Dans un jeu spatial en 3 dimensions où regarder sous tous les angles est possible, ce n'est pas un problème - mais lorsque les ennemis doivent se tenir debout sur le sol, les ennuis commencent.

Tutorial Scene 1

La manière la plus évidente pour qu'un ennemi regarde dans une direction est d'utiliser Transform.LookAt (somePosition). Dans notre premier exemple, nous avons un joueur qui est une capsule avec une caméra attachée dessus, elle se déplace en utilisant l' Input de base pour les mouvement FPS.

Il ya quatre ennemis dans notre scène. Nos ennemis sont assez stupides dans cette première scène, ils vont juste pointer vers le joueur, en utilisant BasicLookAtPlayer.cs

using UnityEngine;
using System.Collections;

public class BasicLookAtPlayer : MonoBehaviour {
	void Update () {
		transform.LookAt(Camera.main.transform.position);
	}
}

Comme vous pouvez le voir, on utilise la commande LookAt pour pointer l'ennemi vers la caméra principale, dans notre cas le joueur. Malheureusement, cela a un inconvénient assez important lorsque le joueur ou l'ennemi sont à différentes hauteurs! L'ennemi peut se retrouver couché sur le dos d'une manière improbable ou tout simplement à regarder vers le bas...

Rotation sur l'axe-y seulement

Nous devons faire tourner l'ennemi seulement autour de l'axe Y de sorte qu'il regarde en direction du joueur, sans tourner autour des axes x et z qui le feraient pencher de façon improbable.

Tutorial Scene 2

Heureusement, c'est assez simple à accomplir. Dasn cette scène, nous utilisons le script LookAtPlayerOnOneAxis.cs

using UnityEngine;
using System.Collections;

public class LookAtPlayerOnOneAxis : MonoBehaviour {

	void Update () {
		var newRotation = Quaternion.LookRotation(Camera.main.transform.position - transform.position).eulerAngles;
		newRotation.x = 0;
		newRotation.z = 0;
		transform.rotation = Quaternion.Euler(newRotation);
	}
}

Tout d'abord nous créons un nouveau Quaternion qui regarde depuis la position actuelle de l'ennemi vers la caméra principale (notre joueur). Quaternion.LookRotation prend une direction, que l'on peut obtenir facilement en faisant la soustraction de la position de la caméra moins la position de l'ennemi. Lorsque nous avons le Quaternion nous le changeons en une représentation de l'angle en Vector3 avec eulerAngles et nous le plaçons dans newRotation.

La rotation que nous avons créé est exactement la même que celle obtenue en utilisant la méthode LookAt dans la première scène.

Pour faire en sorte que cela ne soit qu'une rotation sur l'axe-y, nous avons simplement mis le x et z des éléments à 0 - puis nous changeons la valeur actualisée de nouveau en un Quaternion en utilisant Quaternion.Euler.

Faire la rotation en douceur

At the moment the enemies just snap to a rotation facing the player - that's not very realistic and will start to look strange when we start having them move around the scene.

Pour le moment, les ennemis se tournent brusquement vers le joueur - ce n'est pas très réaliste et ça peut sembler étrange quand nous commencerons à les déplacer autour de la scène.

Tutorial Scene 3

Nous pouvons utiliser Quaternion.Slerp pour interpoler entre la rotation actuelle du personnage et la rotation cible vers le joueur.

 using UnityEngine;
using System.Collections;

public class SmoothLookAtPlayerOnOneAxis : MonoBehaviour {

	void Update () {
		var newRotation = Quaternion.LookRotation(Camera.main.transform.position - transform.position).eulerAngles;
		newRotation.x = 0;
		newRotation.z = 0;
		transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(newRotation), Time.deltaTime);
	}
}

Encore une fois nous faisons la rotation à l'aide de LookRotation et un angle de zéro sur les axes x et z. Ensuite, nous utilisons Slerp pour interpoler entre la rotation actuelle et la cible.

Slerp prend trois paramètres. Le premier (1) est la rotation de départ, le seconde (2) est la rotation cible et le troisième est un nombre compris entre 0 et 1 indiquant le ratio de translation entre 1 et 2 . Une valeur de 0 ne change rien, une valeur de 1 termine la rotation  et une valeur de 0,5 reviendra à mi-chemin entre les deux.

Si vous voulez faire que la position entre le début et la fin ne soit pas une ligne droite,  alors vous pouvez appliquer une fonction qui "adoucit" la valeur. Une fonction d'"adoucissement" (...) a un paramètre compris entre 0 et 1 et renvoie une valeur comprise entre 0 et 1, mais la valeur retournée se trouve sur une courbe. Le résultat pratique de ceci est que le mouvement peut être fait pour être lent au début, puis plus rapide au milieu et enfin ralentir vers la fin - ou pratiquement tout ce que vous voulez - tant que vous avez une fonction pour cela.

Dans notre cas, nous voulons mettre à jour la rotation potentiel à chaque iframe, nous n'avons donc pas vraiment une bonne valeur de départ et de fin et une quantité connue de temps entre les deux. Pour atteindre notre objectif et faire que la rotation semble lisse, nous prenons la rotation actuelle et la mettons à jour pour se rapprocher de la rotation finale par un ratio à chaque frame. Pour ce faire, nous avons mis le troisième paramètre à Time.deltaTime.

L'effet de ceci est que le personnage va se mettre à tourner plus rapidement, puisque le ratio de départ représente une plus grande valeur - utiliser Time.deltaTime signifie que nous bougeons n% vers la rotation cible à chaque frame (où n% serait de 10% si Time.deltaTime était de 0,1 s). Le frame suivant, nous commençons plus près de la rotation cible (en supposant que le joueur ne soit pas en mouvement) et nous déplaços n% de la différence. Alors que nous approchons de la rotation cible notre vitesse de rotation va ralentir et nous donner une attrayante rotation en ralentissement.

Comme la rotation se rapproche de la cible, la différence sera très faible, mais nous sommes encore en approche de n%, de sorte qu'il y aura un temps très long avant que la rotation soit définitivement terminée. Ce principe est similaire à la limite en algèbre, la limite se rappoche d'une valeur (asymptote) sans jamais l'atteindre. Il peut être nécessaire de faire en sorte que le paramètre 3 croit avec le temps de manière à fermer la boucle.

Rotation en douceur avec une vitesse maximum

The problem of our last example is that  should the rotations be very different the character may start rotating very quickly, perhaps unrealistically.  We probably want a way of making a smooth rotation that is limited to a maximum number of degrees per second.

Le problème de notre dernier exemple est que, si les rotations sont très différentes, le personnage peut se mettre à tourner très rapidement, peut-être de manière irréaliste. Nous avons probablement besoin d'un moyen de créer une rotation en douceur qui est limiteé à un nombre maximum de degrés par seconde.

Tutorial Scene 4

Dans la quatrième scène nous utilisons le script RotateToPlayerWithAMaximumSpeed.cs

using UnityEngine;
using System.Collections;

public class RotateToPlayerWithAMaximumSpeed : MonoBehaviour {

	public float maximumRotateSpeed = 40;
	public float minimumTimeToReachTarget = 0.5f;
	Transform _transform;
	Transform _cameraTransform;
	float _velocity;

	void Start(){
		_transform = transform;
		_cameraTransform = Camera.main.transform;
	}

	void Update () {
		var newRotation = Quaternion.LookRotation(_cameraTransform.position - _transform.position).eulerAngles;
		var angles = _transform.rotation.eulerAngles;
		_transform.rotation = Quaternion.Euler(angles.x, Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed),
			angles.z);
	}
}

A partir de cet exemple, nous allons commencer à faire une incursion dans le monde de l'optimisation. Nous allons donc cacher nos Transforms pour la caméra et l'enemi.

L'utilisation de la Transform peut sembler être une simple utilisation de variable, mais non. Quand vous lancez la commande transform.position , le compilateur lance GetComponent<Transform>().position -cela peut alterer le rendement si vous lancez cet commande plusieurs fois en Update.

Donc, pour effectuer notre rotation avec une vitesse maximum nous appelons Mathf.SmoothDampAngle - cette fois nous allons également combiner les eulerAngles existants de la rotation avec une nouvelle valeur pour la rotation Y. Les fonctions SmoothDampXXXX (disponible sur Vector3 et Mathf) prennent une vitesse actuelle, une vitesse maximale et un temps prévu pour compléter le mouvement. La vitesse actuellet est seulement utilisée par la routine et nous devons fournir un float pour le contenir - nous ne l'assignons pas nous-mêmes.

Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed)

maximumRotateSpeed needs little explaining, but minimumTimeToReachTarget allows us to have a slower smoothed start and finish to our rotation.  When set to 0 the rotation will complete as fast as possible without exceeding the maximumSpeed, a larger value indicates that we are happy that the rotation be slower so that it will complete closing the continually updated rotation distance in the ideal time.  The practical upshot of that is that rotations will smoothly slow down as the target value is approached.

maximumRotateSpeed est assez clair cependant minimumTimeToReachTarget nous permet d'avoir un rythme plus lent et plus lissée en début et en fin de rotation. Lorsqu'elle est réglée sur 0, la rotation est complétée aussi vite que possible sans dépasser le maximumSpeed, une valeur plus élevée indique que nous voulons que la rotation soit plus lente afin qu'elle achève de fermer la distance de rotation continuellement mis à jour en temps idéal. La conséquence pratique en est que les rotations ralentissent en douceur quand la valeur cible est approchée.

	void Update () {
		var newRotation = Quaternion.LookRotation(_cameraTransform.position - _transform.position).eulerAngles;
		var angles = _transform.rotation.eulerAngles;
		_transform.rotation = Quaternion.Euler(angles.x, Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed),
			angles.z);
	}

L'Update fonctionne sur notre rotation cible, prend la rotation actuelle de l'ennemi et remplace l'axe de rotation Y avec le SmoothDampAngle pour faire tourner l'ennemi vers notre joueur. Un effet très utile.

Orienter du texte face à la caméra

Nos ennemis sont assez ennuyeux et nous allons vouloir leur donner un peu d' IA. La première chose que nous allons faire est de les faire dormir jusqu'à ce que le joueur soit proche d'eux - nous voulons indiquer leur sommeil avec un signe de ronflement Zzzzz sau-dessus d'eux quand ils sont dans cet état.

Tutorial Scene 5

Nous allons commencer par ajouter le commencement d'un Finite State Machine à chacun des enemis. Cela se trouve dans MovementStateMachine.

using UnityEngine;
using System.Collections;

public class MovementStateMachine : MonoBehaviour {

	public bool sleeping = true;
	public Transform sleepingPrefab;

	IEnumerator Start () {
		var t = transform;
		while(true){
			yield return new WaitForSeconds(Random.value * 3f + 2);
			if(sleeping)	{
				Instantiate(sleepingPrefab, t.position + Vector3.up * 3f, t.rotation);
			}
		}
	}
}

Nous avons un state machine très simple qui dit endormi ou non. Pas grand chose se passe quand il se réveille cependant. Et en fait il n'y a toujours rien pour le réveiller dans notre code. Mais cela suffit pour démontrer comment faire tourner un objet pour qu'il soit en face de la caméra

Quand le script se lance, il rentre dans une coroutine qui va créer une prefab au-dessus de la tête de l'enemi si le script est endormi à ce moment.

Voyez comment on peut cacher les composants dans des variables locales quand nous utilisons une coroutine.

La prefab est un simple TextMesh et MeshRenderer avec un script qui les fait monter au fil du temps, et notre script BasicLookAtPlayer de la scène 1.Le script de montée est aussi très simple.

using UnityEngine;
using System.Collections;

public class GentleRiseAndDestroy : MonoBehaviour {

	public float timeToLive = 2f;
	Transform _transform;

	void Start () {
		_transform = transform;
		Destroy(gameObject, timeToLive);
	}

	void Update () {
		_transform.position += Vector3.up * Time.deltaTime;
	}
}

Le script détruit l'objet au bout de quelques secondes, et fait l'objet s'élever lentement à chaque frame.

L'effet que nous recherchons est que le Zzzzzz apparaisse droit face la caméra, peu importe où il est - plutôt que de devoir se déplacer . Mais il y a encore quelques problèmes ici.

Tout d'abord le Zzzzzz apparaît à l'envers du fait que Unity veut que l'avant d'un TextMesh pointe dans la direction opposée de la caméra pour que le texte puisse être lu correctement de gauche à droite.

Unity offre quelque singularités quand il s'agit de la direction vers laquelle les objets pointent. Les plans générés par Unity ont face vers le haut au lieu de vers l'avant (Vector3.forward) alors que les TextMesh pointent vers l'avant.

When you get close to the enemies you see that there is another problem, the Zzzzzz aren't pointing outwards from the screen.  A lot of people are confused by this (I know I was) - we have a script that says LookAt the camera - why isn't it looking at it?

Lorsque vous vous approchez des ennemis vous voyez qu'il y a un problème, les Zzzzzz ne sont pas orientés vers l'extérieur de l'écran. Beaucoup de gens sont confus par cela (je l'étais) - nous avons un script qui dit LookAt (regarde) la caméra - pourquoi est-ce que cela ne regarde pas la caméra?

Modifier des vecteurs de Transform

Le problème avec LookAt, c'est que cela regarde vers l'emplacement de la caméra. Mais la caméra est située en un point fixe dans le monde, tandis que l'image qu'elle projette représente les choses vues sur son plan focal. Les objets regardent vers un seul point et, selon l'endroit où ils sont, ce n'est pas par rapport à l'emplacement où ils sont rendus à l'écran par la caméra.

Tutorial Scene 6

Il y a une astuce pour ce faire, mais elle n'est pas conseillé dans de nombreux cas et nous allons voir une autre façon dans la section suivante. Vous savez probablement que transform.forward représente la direction vers l'avant d'un objet de la même manière que Vector3.forward représente l'avant en coordonnées du monde. Ce que beaucoup de gens ne savent pas, c'est que vous pouvez également définir transform.forward et il va faire pivoter l'objet!

Ce que nous voulons, c'est notre TextMesh de pointer vers le plan focal de la caméra, et non y regarder. En fait, nous voulons que l'object adopte le forward de la caméra.

Rappelez-vous que les TextMeshes sont réfléchis. S'il s'agissait d'un modèle normal, nous voudrions qu'il adopter transform.forward pour lui permettre de faire face à la caméra.
using UnityEngine;
using System.Collections;

public class ReversedCameraDirection : MonoBehaviour {

	Transform _transform;
	Transform _cameraTransform;

	void Start () {
		_transform = transform;
		_cameraTransform = Camera.main.transform;
	}

	void Update () {
		_transform.forward = _cameraTransform.forward;
	}
}

Nous ajoutons ce script ReverseCameraDirection à la prefab Zzzzz Sleeping Text en place du script BasicLookAtPlayer et c'est parti.

Régler le vecteur transform.forward, .up ou .right, et semble juste très puissant et vous permet de penser en termes de vecteurs plutôt que rotations - mais attention, lorsque vous définissez l'une de ces valeurs, vous ne contrôlez pas les autres vecteurs  et vous trouverez parfois des objets retournés ou pivotés de manière inattendue..

Combiner plusieurs Quaternions

So our Zzzzzz themselves aren't very interesting and they could give us an opportunity to demonstrate how we can use Quaternion combinations.  Let's have the Zzzzzz slowly rotate as they drift upwards - what I mean is, have them face the camera but spin slowly around on the plane of the screen.

Donc, notre Zzzzzz  ne sont pas très intéressants et ils pourraient nous donner l'occasion de montrer comment on peut utiliser des combinaisons de quaternions. Nous allons faire que nos Zzzzzz tournent lentement pendant qu' ils dérivent vers le haut - ce que je veux dire, c'est les avoir face à la caméra, mais tournant lentement autour du plan de l'écran.

Tutorial Scene 7

Pour faire cette rotation, nous créons un nouveau script SpinAroundForward.

using UnityEngine;
using System.Collections;

public class SpinAroundForward : MonoBehaviour {

	Transform _transform;
	float _rotatingFactor;

	void Start(){
		_transform = transform;
		_rotatingFactor = 0.2f + (Random.value * 0.8f);
	}

	void Update () {
		_transform.rotation = _transform.rotation * Quaternion.AngleAxis(Time.deltaTime * 20 * _rotatingFactor, Vector3.forward); 
	}
}

Ce script crée un facteur de rotation au hasard dans la fonction Start et l'applique dans la fonction Update.

Vous pouvez voir comment nous combinons la rotation actuelle de l'objet avec une rotation autour de Vector3.forward. Quaternion.AngleAxis crée un quaternion qui est un nombre de degrés de rotation autour d'un axe donné. Nous utilisons l'opérateur * pour le combiner avec la rotation actuelle du Zzzzzz.

Maintenant, si vous exécutez ceci dans la scène tutoriel, vous verrez que le texte tourne, mais ne fait pas face à la caméra. Il y a une bonne raison à cela: notre script ReverseCameraDirection est désactivé! Si vous cliquez sur Sleeping Text 3 et activez le script ReverseCameraDirection vous verrez alors que le  Zzzzzz fait face à la caméra, mais ne tourne pas. Voici l'un des pièges de la définition d'un vecteur de transform directement - il mélange vos combinaisons de rotation. Nous avons besoin d'une approche différente si nous voulons atteindre notre objectif.

Tutorial Scene 8

En Scene 8 on remplace ReverseCameraDirection avec une version Quaternion de AdjustForwardToPointToCamera.

using UnityEngine;
using System.Collections;

public class AdjustForwardToPointToCamera : MonoBehaviour {

	Transform _transform;
	Transform _cameraTransform;

	void Start () {
		_transform = transform;
		_cameraTransform = Camera.main.transform;
	}

	void Update () {
		_transform.rotation = Quaternion.FromToRotation(_transform.forward, _cameraTransform.forward) * _transform.rotation;
	}
}

Quaternion.FromToRotation est très puissant quand vous avez des vecteurs qui ont besoin de réglage! Elle peut être utilisée pour faire pivoter un objet vers la surface de la chose sur lequel il repose  en réglant le vecteur up  de l'objet pour être la normale de la surface, par exemple. Dans notre cas, nous allons l'utiliser pour faire tourner la transform.forward de notre Zzzzzz pour être le même que transform.forward de la caméra.

Juste un rappel, n'oubliez pas que les TextMesh sont inversés - si c'était un objet normal nous réglerions le transform.forward de l'objet sur transform.forward de la caméra de sorte qu'il braque sur elle.

Nous combinons le Quaternion que nous avons créé à partir du forward en rotation vers l'angle correct avec la rotation actuelle (qui est également modifiée par notre script SpinAroundForward) et c'est tout, nous avons une caméra en face, en rotation, Zzzzzz. (Qui aurait cru cela!)

Faire tourner un plan pour faire face à la caméra

Ok, donc nous avons convenablement réglé nos TextMeshes face à la caméra - maintenant nous allons nous occuper des plans. Un plan créé avec Unity est un bon endroit pour mettre une texture 2D et la montrer au joueur. Faisons un plan 2D billboard qui fait toujours face à l'écran.

Pour nous donner une raison de faire cela, nous allons donner à nos ennemis une "humeur". Fondamentalement, ils vont commencer avec un niveau de bonheur et progressivement plus malheureux au fil du temps. Dans un jeu, nous pourrions trouver beaucoup d'autres raisons pour lesquelles cela pourrait être intéressant.

Le joueur va être capable de voir l'état d'esprit des ennemis quand ils regardent droit sur eux. Nous allons afficher une émoticônes qui  lorsque le joueur regarde directement vers eux - la couleur du smiley sera également une description plus précise de l'état d'esprit, vert, heureux er rouge, en colère.

Cela va introduire quelques scripts.

D'abord nous allons réfléchir à la façon de «regarder directement" un ennemi. Nous devons attacher un script au joueur qui permettra de tester ce qui est juste en face de lui.

using UnityEngine;
using System.Collections;

public class LookTrigger : MonoBehaviour {

	Transform _transform;

	void Start () {
		_transform = transform;
	}

	void Update () {
		RaycastHit hit;
		if(Physics.SphereCast(_transform.position + _transform.forward * 0.5f, 3f, _transform.forward, out hit, 400)){
			hit.collider.SendMessage("LookedAt", SendMessageOptions.DontRequireReceiver);
		}
	}
}

Nous envoyons un SphereCast à partir du joueur et, si elle heurte quelque chose, nous envoyons à l'objet touché un message indiquant qu'il a été "LookedAt".

Nous utilisons une SphereCast car elle offre un peu d'inexactitude quand on regarde les choses qui pourraient être très loin.

Ok, donc ça a été facile - maintenant nous avons besoin de l'ennemi pour avoir son humeur. Nous avons un nouveau script appelé EnemyMood.

using UnityEngine;
using System.Collections;

public class EnemyMood : MonoBehaviour {

	public MoodIndicator moodIndicatorPrefab;
	public float _mood;

	Transform _transform;
	MoodIndicator _currentIndicator;

	void Start () {
		_transform = transform;
		mood = Random.Range(30,99);
	}

	void Update () {
		mood -= Time.deltaTime/2;
	}

	void LookedAt(){
		if(_currentIndicator)
			return;

		_currentIndicator = Instantiate(moodIndicatorPrefab, _transform.position + Vector3.up * 3.5f,
			Quaternion.identity) as MoodIndicator;

		_currentIndicator.enemy = this;
	}
}

Nous commençons par initialiser une variable d'humeur avec une valeur comprise entre 30 et 99 , alors nous décrémentons lentement dans l'Update.

Nous avons ajouté une variable prefab pour le plan pour afficher l'humeur et donné à cette prefab un script MoodIndicator que nous allons examiner brièvement.

Lors de la réception d'un message EnemyMood LookedAt il vérifie s'il existe déjà un MoodIndicator actuel et retourne s'il existe.

Unity fera cette variable null/false quand le MoodIndicator est détruit.

S'il n'y avait aucun MoodIndicator, il en crée un et le place juste au-dessus de l'ennemi - alors il enseigne l'indicateur sur l'ennemi qu'il représente.

Le MoodIndicator gère l'affichage de plan et choisit le bon graphique à faire apparaître.

using UnityEngine;
using System.Collections;

public class MoodIndicator : MonoBehaviour {

	public Texture2D[] moodIndicators;
	public Color happyColor = Color.green;
	public Color angryColor = Color.red;
	public float fadeTime = 4f;
	public EnemyMood enemy;

	Transform _transform;
	Transform _cameraTransform;
	Material _material;
	Color _color;
	Quaternion _pointUpAtForward;

	void Start () {
		_pointUpAtForward = Quaternion.FromToRotation(Vector3.up, Vector3.forward);

		_material = renderer.material;

		//Définit le graphique
		_material.mainTexture = moodIndicators[
			Mathf.Clamp(
				Mathf.RoundToInt( enemy.mood/(100/moodIndicators.Length)),
				0,
				moodIndicators.Length-1)
			];

		//Calcule la couleur
		var moodRatio = enemy.mood/100;
		_material.color = _color = new Color(
			angryColor.r * (1 - moodRatio) + happyColor.r * moodRatio,
			angryColor.g * (1 - moodRatio) + happyColor.g * moodRatio,
			angryColor.b * (1 - moodRatio) + happyColor.b * moodRatio
		);

		Update();
	}

	void Awake(){
		_transform = transform;
		_cameraTransform = Camera.main.transform;
	}

	public void Update () {

		//Pointe le plan vers la caméra
		_transform.rotation = Quaternion.LookRotation(-_cameraTransform.forward, Vector3.up)
			* _pointUpAtForward;

		//Le graphique disparait
		_color.a -= Time.deltaTime/fadeTime;
		_material.color = _color;
	}
}

C'est notre premier vrai script alors nous allons le démanteler.

	public Texture2D[] moodIndicators;
	public Color happyColor = Color.green;
	public Color angryColor = Color.red;
	public float fadeTime = 4f;
	public EnemyMood enemy;

Nos variables publiques sont dans un tableau des images pour les utiliser comme indicateurs de l'humeur - ce sont des smileys pour triste, heureux, etc Il y en a 4 dans l'exemple. Nous disposons d'une couleur heureuse et une couleur en colère, alors nous voulons faire disparaître l'indicateur au fil du temps, nous avons donc une variable qui contient le nombre de secondes avant que le plan tout entier deviennent transparent. Enfin, nous avons l'ennemi qui contient cet indicateur. Tous ces éléments seront définis quand nous arrivons à la fonction Start.

	void Start () {
		_pointUpAtForward = Quaternion.FromToRotation(Vector3.up, Vector3.forward);

		_material = renderer.material;

		_material.mainTexture = moodIndicators[
			Mathf.Clamp(
				Mathf.RoundToInt( enemy.mood/(100/moodIndicators.Length)),
				0,
				moodIndicators.Length-1)
			];

		var moodRatio = enemy.mood/100;
		_material.color = _color = new Color(
			angryColor.r * (1 - moodRatio) + happyColor.r * moodRatio,
			angryColor.g * (1 - moodRatio) + happyColor.g * moodRatio,
			angryColor.b * (1 - moodRatio) + happyColor.b * moodRatio
		);

		Update();
	}

La première chose à faire est de créer un quaternion en cache qui permet de transformer un objet pour que son vecteur up pointe vers son vecteur forward. Rappelez-vous c'es ce que nous devions faire pour faire face à la caméra.

Ensuite, nous sélectionnons une texture qui représente l'état d'esprit en divisant l'humeur actuelle (un nombre entre 0 et 98) par un facteur qui sera mis dans la gamme de textures que nous avons fournis - nous restraignons les valeurs pour nous assurer de toujours obtenir une texture valide .

Ensuite, nous décidons d'une couleur pour le smiley en utilisant une proportion de colère et heureux selon l'humeur.

Enfin, nous appelons Update pour que le code de rotation soit appelé immédiatement.

La fonction Update se présente comme suit:

public void Update () {
		//Pointer le plan vers la caméra
		_transform.rotation = Quaternion.LookRotation(-_cameraTransform.forward, Vector3.up)
			* _pointUpAtForward;

		//Le graphique disparait
		_color.a -= Time.deltaTime/fadeTime;
		_material.color = _color;
	}

D'abord nous travaillons sur la rotation qui pointe vers la normal de l'objet vers la caméra en utilisant la fonction Quaternion.LookRotation, en passant le transform.forward de la caméra, puis nous combinons cela avec notre rotation en cache qui pointe le up de l'objet vers le forward . Notre plan est maintenant correctement aligné.

La deuxième partie de la routine  estompe l'alpha de l'image dans le temps.

C'est tout - si vous essayez, vous verrez les indicateurs surgissent lorsque vous centrez l'ennemi à l'écran. Ils se fâchent rapidement!

Ajouter des mouvements

Ok so it's time for our enemies to go and attack the player.  We are going to use Quaternions again, to set the target position of the enemy to be a different rotation around the player in an attempt to have them sneak up, and space out when attacking.  That at the moment is in the future - let's have a look at our new MovementStateMachine3.  Warning - it's grown up a bit!

Ok, donc il est temps pour nos ennemis d' attaquer le joueur. Nous allons utiliser les Quaternions à nouveau, pour faire en sorte que la position cible de l'ennemi soit une rotation différente autour du joueur  dans le but de les voir se faufiler et s'espacer quand ils attaquent. C'est pour le moment une futur action - nous allons jeter un oeil à notre nouveau MovementStateMachine3. Attention - il a un peu grandi!


MovementStateMachine3.cs

using UnityEngine;
using System.Collections;

public class MovementStateMachine3 : MonoBehaviour {

	public bool sleeping = true;
	public Transform sleepingPrefab;
	public float attackDistance = 5;
	public float sleepDistance = 30;
	public float speed = 2;
	public float health = 20;
	public float maximumAttackEffectRange = 1f;

	Transform _transform;
	Transform _player;
	public Transform target;

	public EnemyMood _mood;

	CharacterController _controller;
	AnimationState _attack;
	AnimationState _die;
	AnimationState _hit;
	Animation _animation;

	float _attackDistanceSquared;
	float _sleepDistanceSquared;
	float _attackRotation;
	float _maximumAttackEffectRangeSquared;
	float _angleToTarget;

	bool _busy;

	// Initialisation
	IEnumerator Start () {
		_transform = transform;
		_player = Camera.main.transform;

		_mood = GetComponent<EnemyMood>();

		_attackDistanceSquared = attackDistance * attackDistance;
		_sleepDistanceSquared = sleepDistance * sleepDistance;
		_maximumAttackEffectRangeSquared = maximumAttackEffectRange * maximumAttackEffectRange;

		_controller = GetComponent<CharacterController>();

		_animation = animation;
		_attack = _animation["attack"];
		_hit = _animation["gothit"];
		_die = _animation["die"];

		_attack.layer = 5;
		_hit.layer = 5;
		_die.layer = 5;

		_controller.Move(new Vector3(0,-20,0));

		while(true){
			yield return new WaitForSeconds(Random.value * 6f + 3);
			if(sleeping)	{
				var newPrefab = Instantiate(sleepingPrefab, _transform.position + Vector3.up * 3f, Quaternion.identity) as Transform;
				newPrefab.forward = Camera.main.transform.forward;
			}
		}
	}

	void Update(){
		//Verifie si quelque chose d'autre controle 
		if(_busy)
			return;

		if(sleeping){
			if((_transform.position - _player.position).sqrMagnitude < _attackDistanceSquared){
				sleeping = false;
				target = _player;
				//Where this enemy wants to stand to attack
				_attackRotation = Random.Range(60,310);
			}
		}else{
			//Si la cible est morte alors retourne en mode sleeping
			if(!target){
				sleeping = true;
				return;
			}

			var difference = (target.position - _transform.position);
			difference.y /= 6;
			var distanceSquared = difference.sqrMagnitude;

			//Trop loin, on oublie...
			if( distanceSquared > _sleepDistanceSquared){
				sleeping = true;
			}
			//Assez proche pour une attaque
			else if( distanceSquared < _maximumAttackEffectRangeSquared && _angleToTarget < 40f){
				StartCoroutine(Attack(target));
			}
			//Autrement, c'est le moment de se bouger
			else{
				//Décider la position cible
				var targetPosition = target.position + (Quaternion.AngleAxis(_attackRotation, Vector3.up) * target.forward * maximumAttackEffectRange * 0.8f);
				var basicMovement = (targetPosition - _transform.position).normalized * speed * Time.deltaTime;
				basicMovement.y = 0;
				//Mouvement seulement si en face
				_angleToTarget = Vector3.Angle(basicMovement, _transform.forward);
				if( _angleToTarget < 70f)
				{
					basicMovement.y = -20 * Time.deltaTime;
					_controller.Move(basicMovement);
				}
			}
		}
	}

	void OnTriggerEnter(Collider hit) {
		if(hit.transform == _transform)
			return;

		if(hit.transform == _player){
			StartCoroutine(Attack(_player));
		}else{
			var rival = hit.transform.GetComponent<EnemyMood>();
			if(rival){
				if(Random.value > _mood.mood/100)
					StartCoroutine("Attack",rival.transform);
			}
		}
	}

	IEnumerator Attack(Transform victim){
		sleeping = false;
		_busy = true;
		target = victim;
		_attack.enabled = true;
		_attack.time = 0;
		_attack.weight = 1;
		//Attendre la moitié de l'animation
		yield return StartCoroutine(WaitForAnimation(_attack, 0.5f));
		/Verifier si toujours proche
		if(victim && (victim.position - _transform.position).sqrMagnitude < _maximumAttackEffectRangeSquared){
			//Appliquer les dommages
			victim.SendMessage("TakeDamage", 1 + Random.value * 5, SendMessageOptions.DontRequireReceiver);
		}
		//Attendre la fin de l'animation
		yield return StartCoroutine(WaitForAnimation(_attack, 1f));
		_attack.weight = 0;
		_busy = false;
	}

	void TakeDamage(float amount){
		StopCoroutine("Attack");
		health -= amount;
		if(health < 0)
			StartCoroutine(Die());
		else
			StartCoroutine(Hit());
	}

	IEnumerator Die(){
		_busy = true;
		_animation.Stop();
		yield return StartCoroutine(PlayAnimation(_die));
		Destroy(gameObject);
	}

	IEnumerator Hit(){
		_busy = true;
		_animation.Stop();
		yield return StartCoroutine(PlayAnimation(_hit));
		_busy = false;
	}

	public static IEnumerator WaitForAnimation(AnimationState state, float ratio){
		state.wrapMode = WrapMode.ClampForever;
		state.enabled = true;
		state.speed = state.speed == 0 ? 1 : state.speed;
		while(state.normalizedTime < ratio-float.Epsilon){
			yield return null;
		}
	}

	public static IEnumerator PlayAnimation(AnimationState state){
		state.time = 0;
		state.weight = 1;
		state.speed = 1;
		state.enabled = true;
		var wait = WaitForAnimation(state, 1f);
		while(wait.MoveNext())
			yield return null;
		state.weight = 0;
	}
}

Ok, voyons les éléments intéressants - voici ce qui est nouveau dans la fonction Start:

       _mood = GetComponent<EnemyMood>();

		_attackDistanceSquared = attackDistance * attackDistance;
		_sleepDistanceSquared = sleepDistance * sleepDistance;
		_maximumAttackEffectRangeSquared = maximumAttackEffectRange * maximumAttackEffectRange;

		_controller = GetComponent<CharacterController>();

		_animation = animation;
		_attack = _animation["attack"];
		_hit = _animation["gothit"];
		_die = _animation["die"];

		_attack.layer = 5;
		_hit.layer = 5;
		_die.layer = 5;

Il est clair que nous faisons un tas de cache pour des raisons de performance - aussi toutes les distances sont au carré. Ainsi, nous pouvons éviter des racines carrées lorsque nous vérifions les distances.

Calculer la distance entre deux choses utilise le théorème de Pythagore et nécessite une racine carrée. Les racines carrées sont des fonctions récursives qui sont très coûteuses. Nous pouvons éviter de calculer la racine carrée  en utilisant le carré de la distance que nous voulons vérifier

On place plusieurs des animations sur un layer de haut niveau de sorte qu'elles remplacent l'animation par defaut (idle).

Donc la fonction Update est assez grande - elle commence comme ceci:.

                //Verifie si quelque chose d'autre controle 
		if(_busy)
			return;

Pour attaquer et d'être touché, nous allons permettre à quelque chose d'autre de contrôler le personnage - quand cela arrive _busy sera vrai et l'Update sera ignoré.

Ensuite, nous avons besoin de la logique pour quand l'ennemi est endormi.
                if(sleeping){
			if((_transform.position - _player.position).sqrMagnitude < _attackDistanceSquared){
				sleeping = false;
				target = _player;
				//Où l'ennemi veut se placer pour attaquer
				_attackRotation = Random.Range(60,310);
			}
		}

Quand l'ennemi est endormi, nous vérifions si le joueur est à l'intérieur de attackDistance, si il l'est - on éteint le mode sommeil (youpi!) et fixé la cible sur  le joueur. Nous allouons aussi une rotation particulière autour du joueur que nous voulons cibler (le sournois tente d'approcher par le côté ou derrière!) - Actuellement, c' est simplement stocké dans une variable, nous allons voir où elle est utilisé ensuite. C'est ce qui arrive quand l'ennemi ne dort pas:

			//Si la cible est morte alors retourne en mode sleeping
			if(!target){
				sleeping = true;
				return;
			}

			var difference = (target.position - _transform.position);
			difference.y /= 6;
			var distanceSquared = difference.sqrMagnitude;

			//Trop loin, on oublie...
			if( distanceSquared > _sleepDistanceSquared){
				sleeping = true;
			}
			//Assez proche pour une attaque?
			else if( distanceSquared < _maximumAttackEffectRangeSquared && _angleToTarget < 40f){
				StartCoroutine(Attack(target));
			}
			//Autrement, temps de se la bouger
			else{
				//Decider la position cible
				var targetPosition = target.position + (Quaternion.AngleAxis(_attackRotation, Vector3.up) * target.forward * maximumAttackEffectRange * 0.8f);
				var basicMovement = (targetPosition - _transform.position).normalized * speed * Time.deltaTime;
				basicMovement.y = 0;
				//Mouvement seulemeent si en face
				_angleToTarget = Vector3.Angle(basicMovement, _transform.forward);
				if( _angleToTarget < 70f){
					basicMovement.y = -20 * Time.deltaTime;
					_controller.Move(basicMovement);
				}
			}

Nous mettons l'ennemi à dormir si sa cible a disparu ou qu'elle est trop loin. Ensuite, si ce n'est pas le cas, si nous sommes à distance de frappe, nous commençons une coroutine pour effectuer une attaque. Si nous ne sommes pas endormis et nous ne sommes pas assez près pour attaquer alors nous envisageons de nous de déplacer vers la cible.

La première partie de ce travail est d'où nous voulons défendre. Il s'agit d'une combinaison de la position de la cible, l'angle que nous avons décidé sur le moment où nous l'avons ciblé et la distance de frappe (que nous voulons être à 80%). Nous utilisons un Quaternion basé sur l'angle pour modifier un vecteur basé sur la direction vers laquelle la cible est orientée et nous utilisons 80% de la distance de frappe pour modifier cette valeur. Tout cela est appelé targetPosition. Par exemple, si l'angle nous avons pris était de 180 et la distance de frappe est de 2 alors la position de la cible serait 1,6 unité du monde directement derrière la caméra.

Ensuite, nous travaillons sur l'angle entre l'endroit où nous sommes et où nous voulons aller - si il ya trop d'écart on ne bouge pas encore (on laisse le script de rotation définir l'angle). Si c'était ok, alors nous nous dirigeons vers la position cible après avoir ajouté un peu de pesanteur pour maintenir l'ennemi sur le terrain. Ouf!

Utiliser des coroutines pour attendre la fin d'une animation

Ok, donc nous savons que si nous sommes assez proche alors une coroutine d'attaque est lancée. Ce que nous voulons faire est de mettre en pause le comportement normal,  lancer une animation d'attaque et appliquer des dommages à moitié de l'animation. C'est ainsi que nous obtenons ce qui suit:

D'abord - pour faire la routine de plus utile, nous réveillons toujours un ennemi quand d'attaque est appelé - c'est parce que vous pouvez également déclencher une attaque avec deux ennemies qui se rentrent dedans!

	IEnumerator Attack(Transform victim){
		sleeping = false;
		_busy = true;
		target = victim;
		_attack.enabled = true;
		_attack.time = 0;
		_attack.weight = 1;
		//Attendre l amoitié de l'animation
		yield return StartCoroutine(WaitForAnimation(_attack, 0.5f));
		//Toujours assez proche?
		if(victim && (victim.position - _transform.position).sqrMagnitude < _maximumAttackEffectRangeSquared){
			//Appliquer les dommages
			victim.SendMessage("TakeDamage", 1 + Random.value * 5, SendMessageOptions.DontRequireReceiver);
		}
		//Attendre la fin de l'animation
		yield return StartCoroutine(WaitForAnimation(_attack, 1f));
		_attack.weight = 0;
		_busy = false;
	}

En réglant  _busy = true, nous mettons l'Update en attente alors que cette routine performe. _attack est l'animation d'attaque que nous activons, définissons le début et lui donner son facteur. L'animation commence alors à jouer. Ensuite, nous commençons une autre coroutine qui permettra de surveiller l'animation, dans ce cas, elle attendre qu'elle soit passée de 50%. Nous allons voir cette routine dans un instant.

Lorsque l'animation est passée de 50%, nous vérifions que la victime cible n'est pas déjà morte, et qu'elle est toujours à portée et si ces conditions sont remplies, nous envoyons un message TakeDamageavec une quantité aléatoire de dégâts à infliger.

Nous attendons ensuite que l'animation soit terminée avant de retirer notre état busy et désactiver l'animation.

Donc, nous allons jeter un oeil à la coroutine WaitForAnimation:

	public static IEnumerator WaitForAnimation(AnimationState state, float ratio){
		state.wrapMode = WrapMode.ClampForever;
		state.enabled = true;
		state.speed = state.speed == 0 ? 1 : state.speed;
		while(state.normalizedTime < ratio-float.Epsilon){
			yield return null;
		}
	}

Then the loop just waits for the .normalizedTime (the ratio of the clip) to be greater than or equal to the value we passed in (- a floating point fudge factor).

Comme vous pouvez le voir, c'est assez simple! Nous nous assurons d'abord que l'animation ne provoque pas  une boucle ou une réinitialisation, car cela pourrait signifier que nous avons manqué le point de fin. Nous nous assurons également qu'elle est activée et a une certaine vitesse, sinon cette routine ne finirait jamais.

Puis la boucle attend que .normalizedTime (le ratio du clip) soit supérieure ou égale à la valeur que nous avons passée. (- Un facteur arbitraire en float).

Plus de states

Donc maintenant nous avons attaqué et envoyé un message de dégâts, nous devrions examiner comment ce message est traité. Dans cette démo, le joueur est invincible (haha!), mais les ennemis peuvent infliger des dommages l'un l'autre.

Quand l'ennemi reçoit un message TakeDamage il doit réagir:

	void TakeDamage(float amount){
		StopCoroutine("Attack");
		health -= amount;
		if(health < 0)
			StartCoroutine(Die());
		else
			StartCoroutine(Hit());
	}

Tout d'abord nous stoppons toutes attaques en court, puis on soustrait la santé puis nous lançons une coroutine Hit ou Die.

	IEnumerator Die(){
		_busy = true;
		_animation.Stop();
		yield return StartCoroutine(PlayAnimation(_die));
		Destroy(gameObject);
	}

	IEnumerator Hit(){
		_busy = true;
		_animation.Stop();
		yield return StartCoroutine(PlayAnimation(_hit));
		_busy = false;
	}

Both coroutines play an animation to completion, Die destroys the object, Hit re-enables it.

And that's pretty much it for this tutorial.  You should check out RotateToFaceTarget which replaces our earlier rotation script, using the target variable rather than always facing the player - this let's enemies target each other.  The only other script of interest is WalkingAnimation:

Les deux coroutines jouent une animation entière, Die détruit l'objet,  Hit le réactive.

Et c'est à peu près tout pour ce tutoriel. Vous devriez vérifier RotateToFaceTarget qui remplace notre script de rotation, en utilisant la variable cible plutôt que de toujours faire face au joueur - cela permet aux ennemis de se cibler les uns les autres. Le seul autre script d'intérêt est WalkingAnimation:

using UnityEngine;
using System.Collections;

public class WalkingAnimation : MonoBehaviour {

	Transform _transform;
	Vector3 _lastPosition;
	AnimationState _walk;

	public float minimumDistance = 0.01f;

	void Start () {
		_transform = transform;
		_lastPosition = _transform.position;
		_walk = animation["walk"];
		_walk.layer = 2;
	}

	void Update () {
		var moved = (_transform.position - _lastPosition).magnitude;
		_lastPosition = _transform.position;
		if(moved < minimumDistance)		{
			_walk.weight = 0;
		}else{
			_walk.weight = moved * 100;
			_walk.enabled = true;
			_walk.speed = 1;
		}
	}
}

Ce script se fond dans l'animation de marche en fonction de la rapidité de l'ennemi en mouvement.

Conclusion

Nous espérons que vous avez trouvé ce tutoriel utile. S'il vous plaît n'hésitez pas à laisser vos commentaires - particulièrement utile serait les commentaires concernant de futures fonctionnalités que vous aimeriez voir expliquées.

Mike(whydoidoit)

, , , , , , , ,

One Response to "Quaternion – Rotation – Plan d’attaque"

  • Xstahef
    December 7, 2012 - 1:03 pm Reply

    Excellent article, merci à l’auteur et au traducteur.

Leave a Reply

%d bloggers like this: