Dans ce tutorial
- Pourquoi les Quaternion sont utiles
- Faire que l'ennemi regarde le joueur avec réalisme
- Orienter du texte face à la caméra
- Combiner plusieurs Quaternions
- Orienter un billboard face à la caméra
- Ajouter des mouvements
- Plus de states
Le projet du tutoriel
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.
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).
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;
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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".
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 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.
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;
}
}
_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.
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é.
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)







December 7, 2012 - 1:03 pm
Excellent article, merci à l’auteur et au traducteur.