Ok, dans cette article, je vais faire le tour sur un des plus gros éléments que j’ai construits pour l’exercice deux. Le boulet de canon. Lors de l’impact j’attribue un Bullet Time et Shader Flou pour donner un sensation d’impact et d’électrocution du joueur.
Le boulet de canon
Pour l’élément 3D, j’ai créé un sphère 3D avec deux bandes qui donne un peu de relief. Rien de bien complexe. Ensuite j’ai ajouté quelques effets de particules et une lumière pour donner l’effet électrique.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
public class CanonBall : MonoBehaviour { private GameObject gameControler; private SlowMotion slowmotion; //Needed to call our slow motion script. It should always be on the game controler private PlayerHealth playerHealth; public int damage = 34; private Vector3 colisionPoint; private Rigidbody canonBallRB; public float force = 50f; public GameObject explosion; private GameObject explosionHolder; public float explosionForce = 50f; public float explosionRadius = 10f; public float explosionUpForce = 1f; private AudioSource electrocuteAudio; private ParticleSystem lightningParticles; //Particles when electrocuted, this is a child gameobject on the player private SuperBlur.SuperBlur superBlur; //blur when electrocuted, this should be attached to the camera private CameraShake cameraShake; //cameraShake when electrocuted private bool destroyingCanonBall = false; // Use this for initialization void Start () { gameControler = GameObject.FindGameObjectWithTag("GameController"); slowmotion = gameControler.GetComponent<SlowMotion>(); electrocuteAudio = GetComponent<AudioSource>(); canonBallRB = gameObject.GetComponent<Rigidbody>(); } // Update is called once per frame void Update () { //if the canonball is being destroyed, initiate the 1 second shrink if (destroyingCanonBall) { transform.localScale = Vector3.Lerp(transform.localScale, new Vector3(0, 0, 0), Time.deltaTime); //we might be able to get a shader working to make the ball slowly transparant. } } private void OnTriggerEnter(Collider col) { if(col.tag == "Player") { //Debug.Log("collision with player"); canonBallRB.isKinematic = true; canonBallRB.velocity = Vector3.zero; //the ball is exploding. Stop it moving lightningParticles = col.transform.Find("LightningParticles").GetComponent<ParticleSystem>(); lightningParticles.Emit(10); cameraShake = col.GetComponentInChildren<CameraShake>(); slowmotion.runSlowMotion(); //This gets the diffrence between the canonball and the contact point. giving us the direction to shoot the player towards colisionPoint = col.transform.position - transform.position; StartCoroutine(cameraShake.ShakePosition()); StartCoroutine(lateReaction(col, canonBallRB, damage)); //electricute here } } private IEnumerator lateReaction(Collider col, Rigidbody canonBallRB, int damage) { electrocuteAudio.Play(); superBlur = col.GetComponentInChildren<SuperBlur.SuperBlur>(); superBlur.enabled = true; yield return new WaitForSecondsRealtime(0.7f); col.GetComponent<playerReactions>().throwBack(colisionPoint, force); //play explosion canonBallRB.AddExplosionForce(explosionForce, gameObject.transform.position, explosionRadius); playerHealth = col.GetComponent<PlayerHealth>(); playerHealth.looseHealth(damage); electrocuteAudio.Stop(); superBlur.enabled = false; Destroy(gameObject); explosionHolder = Instantiate(explosion, transform.position, transform.rotation); Destroy(explosionHolder, 1f); } public IEnumerator destroyBallAfterTime(float lifeTime) { Destroy(gameObject, lifeTime); //initiate the destroy in n seconds yield return new WaitForSeconds(lifeTime-1); //at n-1 initiate the shrink animation destroyingCanonBall = true; } } |
Il y a divers appels et événements dans ce script. On va essayer d’analyser point par point.
Détruire après un temps donnée
Quand le boulet de canon apparait, par défaut il va rester sur le terrain jusqu’a la destruction. Ceci n’est pas bon pour les performances sachant qu’il y a la possibilité d’en créer des centaines. Il faut donc penser à un moyen de les détruire. Dans notre zone en dessous du terrain, il y a un bout de code qui détruit tout élément qui le touche. Mais si nos boulent ne tombent pas du terrain ?
Il y a donc un CoRoutine public qui peut être appelé lors de la création du boulet de canon avec comme variable la durée de vie. Je préfère le gérer directement par une fonction publique pour permettre une durée de vie différente en fonction de l’endroit. Par exemple, je préfère avoir une durée de vie plus longue pour des boulets qui tombent sur une rampe alors qu’un canon qui tire dans le vide ne nécessite pas d’avoir une durée de vie au-delà de deux secondes.
1 2 3 4 5 6 7 |
public IEnumerator destroyBallAfterTime(float lifeTime) { Destroy(gameObject, lifeTime); //initiate the destroy in n seconds yield return new WaitForSeconds(lifeTime-1); //at n-1 initiate the shrink animation destroyingCanonBall = true; } |
La petite subtilité est le fait qu’on change le bool “destroyingCanonBall”. Et dans l’Update, dès qu’on voit ce bool a True, on réduit la taille de la boule pour faire un effet de disparition. C’est mieux que de le faire disparaître brutalement.
Quand le joueur touche une boule
Ici c’est décomposé dans deux parties. Le premier est le “OnTriggerEnter()” qui se déclenche au contact. Puis on exécute un CoRoutine “lateReaction()” car il fallait implémenter des pauses pour avoir l’effet de flou et de ralentis.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
private void OnTriggerEnter(Collider col) { if(col.tag == "Player") { //Debug.Log("collision with player"); canonBallRB.isKinematic = true; canonBallRB.velocity = Vector3.zero; //the ball is exploding. Stop it moving lightningParticles = col.transform.Find("LightningParticles").GetComponent<ParticleSystem>(); lightningParticles.Emit(10); slowmotion.runSlowMotion(); //This gets the diffrence between the canonball and the contact point. giving us the direction to shoot the player towards colisionPoint = col.transform.position - transform.position; cameraShake = col.GetComponentInChildren<CameraShake>(); StartCoroutine(cameraShake.ShakePosition()); StartCoroutine(lateReaction(col, canonBallRB, damage)); //electricute here } } private IEnumerator lateReaction(Collider col, Rigidbody canonBallRB, int damage) { electrocuteAudio.Play(); superBlur = col.GetComponentInChildren<SuperBlur.SuperBlur>(); superBlur.enabled = true; yield return new WaitForSecondsRealtime(0.7f); col.GetComponent<playerReactions>().throwBack(colisionPoint, force); //play explosion canonBallRB.AddExplosionForce(explosionForce, gameObject.transform.position, explosionRadius); playerHealth = col.GetComponent<PlayerHealth>(); playerHealth.looseHealth(damage); electrocuteAudio.Stop(); superBlur.enabled = false; Destroy(gameObject); explosionHolder = Instantiate(explosion, transform.position, transform.rotation); Destroy(explosionHolder, 1f); } |
Le premier chose qu’on fait est d’immobiliser le boulet pour ne pas qu’il roule pendant l’animation. On lance les effets graphiques d’éclairs, la camera qui secoue et l’effet de ralentis. Ces 3 éléments ont une durée déterminée et donc ne dépendent pas du CoRoutine pour mettre fin à l’animation.
Puis on lance la CoRoutine LateReaction qui va gérer la durée de tout le reste.
On retrouve donc la perte de vie qui appel le script sur le joueur, le son et les effets d’explosion qui démarrent après 0.7 secondes, l’effet de flou sur la camera et un nettoyage des éléments (sphère et particules d’explosion). Ça fait pas mal de code et je peux surement l’organiser mieux que ça. Mais ça fonctionne et donne l’effet voulu.
Stop, Bullet Time
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using UnityEngine; public class SlowMotion : MonoBehaviour { public float slowSpeed = 0.05f; public float slowTime = 2f; // 0.2 to 0.001 private void Update() { Time.timeScale += (1f / slowTime) * Time.unscaledDeltaTime; Time.timeScale = Mathf.Clamp(Time.timeScale, 0f, 1f); Time.fixedDeltaTime = Time.timeScale * 0.02f; //resetting the fixed delta time to the norm of 0.2 } public void runSlowMotion() { Time.timeScale = slowSpeed; Time.fixedDeltaTime = Time.timeScale * 0.02f; } } |
Je me suis largement inspiré de ce video pour réaliser l’effet :
En bref, dans la fonction runSlowMotion, on ralentit le temps interne d’unity Time.timeScale en lui attribuant une valeur basse. Cette baisse s’exécute en appelant le fonction public runSlowMotion().
Pour éviter de ne pas impacter le moteur physique et provoquer un effet saccadé, il faut manipuler le fixedDeltaTime pour qu’il soit toujours à 0.2F (sa valeur par défaut). Le FixedDeltaTime est le temps qui est utilisé par le moteur physique de Unity (qu’on peut utiliser dans le fixedUpdate(){}). Il faut qu’on force le temps à 0.02 par rapport au temps sinon il s’exécute trop rapidement et provoque des incohérences dans le rendu.
Ensuite, chaque frame (avec Update(){}), on augmente petit à petit la vitesse Time.TimeScale jusqu’à 1 et prenant soins de modifier le FixedDeltaTime en même temps.
Le Flou et le camera qui secoue
Bon j’envisageais d’écrire mon propre shader pour faire un effet de flou sur la camera mais en cherchant des sources, je suis tombé sur un excellent shader ici : https://github.com/PavelDoGreat/Super-Blur
Il suffit de coller le script SuperBlur.cs sur la camera, appliquer le matériel et le tour est joué. on applique l’effet en activant et désactivant le script (je pourrais aussi faire un vérification “if electrocuted” mais j’ai trop la flemme !!!)
Du coup je me rabats sur un petit script qui va secouer la camera
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class CameraShake : MonoBehaviour { public float duration = 2f; public float speed = 20f; public float magnitude = 2f; private Vector3 originalPosition; // Use this for initialization void Start () { originalPosition = transform.localPosition; } // Update is called once per frame void Update () { } public IEnumerator ShakePosition() { float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float x = (Mathf.PerlinNoise(Time.time * speed, 0f) * magnitude) - (magnitude / 2f); float y = (Mathf.PerlinNoise(0f, Time.time * speed) * magnitude) - (magnitude / 2f); transform.localPosition = new Vector3(originalPosition.x + x, originalPosition.y + y, originalPosition.z); yield return null; } transform.localPosition = originalPosition; } } |
Rien de bien sorcier ici, on a un CoRoutine public qui s’appelle ShakePosition(). Lors du démarrage, on enregistre la position du camera afin de le remettre après que la camera a bougé.
Pour l’effet, on modifie le Transform du camera avec un PerlinNoise pour avoir un mouvement fluide et on se base sur le temps du jeu pour définir les points dans la map Perlin pour donner un coté aléatoire.
Pingback: Déclencheurs dans Unity, Canons et Colonnes | StarbugStone's Dev Blog