This shows you the differences between two versions of the page.
pjv:laboratoare:2022:02 [2022/10/19 12:07] alexandru.gradinaru [Animatii 2D in Unity] |
pjv:laboratoare:2022:02 [2022/11/09 11:41] (current) alexandru.gradinaru |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Animatii si interactiuni ===== | + | ===== Programarea animatiilor ===== |
==== Cerinte ===== | ==== Cerinte ===== | ||
<note important>Puteti porni de la scenele create in Lab 1</note> | <note important>Puteti porni de la scenele create in Lab 1</note> | ||
+ | |||
+ | <note tip>Resurse folosite: | ||
+ | * [[https://assetstore.unity.com/packages/3d/environments/fantasy/fantasy-forest-environment-free-demo-35361|Fantasy Forest Environment - Free Demo]] | ||
+ | </note> | ||
Realizati o scena de joc similara cu un joc de Duck Hunt 3D | Realizati o scena de joc similara cu un joc de Duck Hunt 3D | ||
Line 10: | Line 14: | ||
- un teren simplu si ceva vegetatie | - un teren simplu si ceva vegetatie | ||
- Playerul: | - Playerul: | ||
- | - este reprezentat de o camera statica in scena | + | - este reprezentat de o camera statica in scena (e suficient sa aveti doar camera, nu exista miscare din partea jucatorului) |
- are posiblitatea de a trage inspre inamici in 2 moduri: | - are posiblitatea de a trage inspre inamici in 2 moduri: | ||
- left click: lanseaza un proiectil fizic care mere inspre directia une a fost apasat click (cu anumite reguli: fie are o durata de viata, fie pana a nimerit ceva, fie adaugati o forta combinata cu reguli de distrugere etc.) | - left click: lanseaza un proiectil fizic care mere inspre directia une a fost apasat click (cu anumite reguli: fie are o durata de viata, fie pana a nimerit ceva, fie adaugati o forta combinata cu reguli de distrugere etc.) | ||
Line 235: | Line 239: | ||
transform.position = Vector3.MoveTowards(transform.position, target.position, step); | transform.position = Vector3.MoveTowards(transform.position, target.position, step); | ||
</code> | </code> | ||
+ | |||
+ | === Coliziuni === | ||
+ | |||
+ | Pentru detectia coliziunilor se poate folosi un collider2D. Pentru o interactiune usoara, se poate genera un Trigger la evenimentul de coliziune bifand caseta de ''Trigger'' din componenta. Dupa care se poate prelua evenimentul si obiectul cu care se face coliziunea. | ||
+ | <code> | ||
+ | void OnTriggerEnter(Collider otherObject) { | ||
+ | |||
+ | score += 10; | ||
+ | scoreText.text = score.ToString(); | ||
+ | Destroy(otherObject.gameObject); | ||
+ | |||
+ | } | ||
+ | |||
+ | void OnCollisionEnter(Collider otherObject) { | ||
+ | |||
+ | score += 10; | ||
+ | scoreText.text = score.ToString(); | ||
+ | Destroy(otherObject.gameObject); | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | ==== Raycasting ==== | ||
+ | |||
+ | In spatiul 3D testarea selectarii sau tintirii asupra unui obiect se face in general printr-o tehnica numita Raycasting, care presupune trasarea unei raze de la sursa (in general camera) la destinatie. Acest lucru este util atat pentru object picking (selectarea unui obiect in spatiul 3D cu mouse-ul spre exemplu) cat si pentru tintirea si lansarea de proiectile. | ||
+ | |||
+ | Desi in cazul unui FPS, lansarea de proiectile se poate face si fizic (deci instantiez un proiectil care are damage, si ii imprim o forta cu care sa se deplaseze spre o directie), in general ea se simuleaza prin trasarea unei raze si detectia coliziuniii cu aceasta, si apoi aplicarea de efecte astfel incat impactul sa para real (proiectilul in general nu este vizibil ochiului uman). | ||
+ | |||
+ | Pentru trasarea unei raze de la camera la o tinta se poate folosi functia de Raycast din pachetul de Physics oferit de Unity. Functia Raycast accepta o multitudine de variante de paramtri, in functie nevoia de utilizare, parametrii principali fiind punctul de start si directia. Un exemplu pentru o camera FPS si o tinta intr-un anumit range se poate folosi urmatoarea secventa de cod: | ||
+ | |||
+ | <code c#> | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | RaycastHit hit; | ||
+ | |||
+ | //daca am un obiect pe directia in range | ||
+ | if(Physics.Raycast(fpsCam.transform.position, fpsCam.transform.forward, out hit, range)) { | ||
+ | //accesez un script de damage pe targetul meu daca accesta exista | ||
+ | Enemy orc = hit.transform.GetComponent<Enemy>(); | ||
+ | if(orc!=null) { | ||
+ | orc.TakeDamage(ammount); //TakeDamage este o functie publica in scriptul de Enemy | ||
+ | | ||
+ | enemyHealthBar.SetActive(true); //pot afisa si Healthbar-ul inamicului la plasarea tintei pe acesta | ||
+ | } | ||
+ | | ||
+ | if(orc.rigidbody != null) { | ||
+ | orc.rigidbody.addForce(-hit.normal * impactForce); //adauga o forta la impact | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | In mod specific, pentru a prelua punctul in care s-a facut click cu mouse-ul, exista metode Unity dedicate | ||
+ | <code c#> | ||
+ | |||
+ | |||
+ | if(Input.GetMouseButtonDown(0)) //la apasarea click stanga | ||
+ | { | ||
+ | Ray mouseClickRay = camera.ScreenPointToRay(Input.mousePosition); //creaza o raza printr-un punct de pe ecran | ||
+ | RaycastHit hit; | ||
+ | | ||
+ | if(Physics.Raycast(mouseClickRay, out hit)) | ||
+ | { | ||
+ | // | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | Asa cum se observa si in exemplul de mai sus, Racycast-urile se pot folosi in multe moduri. Cateva exemple: | ||
+ | * aplicarea unei coliziuni | ||
+ | * selectia unui obiect | ||
+ | * tintirea unui obiect | ||
+ | * actionarea unei functii din obiectul tintit | ||
+ | * si altele | ||
+ | |||
+ | ==== Elemente de GUI in 3D ==== | ||
+ | |||
+ | Similar cu spatiul 2D, se pot folosi atat obiecte plasate in scena, cat si partea de Canvas, in ceea ce priveste interfata grafica. | ||
+ | |||
+ | |||
+ | ==== Crearea de animatii pentru obiecte 3D ==== | ||
+ | |||
+ | Crearea animatiilor se poate face folosind utilitarul de animatie (Window > Animation), similar ca in spatiul 2D. | ||
+ | Similar, se poate folosi Animator Controller pentru a gestiona starile (spre exemplu tras cu arma). | ||
+ | |||
+ | <code> | ||
+ | void Update() | ||
+ | { | ||
+ | if(Input.GetButtonDown("Fire1")) | ||
+ | { | ||
+ | //set animation state | ||
+ | animator.SetInteger("state", STATE_SHOOTING); | ||
+ | | ||
+ | //raycast & everything else .. | ||
+ | | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Daca vreti sa aveti actiunea de Fire incontinuu, se poate folosi punctia de Input, GetButton() | ||
+ | |||
+ | <code> | ||
+ | void Update() | ||
+ | { | ||
+ | if(Input.GetButton("Fire1") && Time.time >= nextFire) | ||
+ | { | ||
+ | nextFire = Time.time + 1f/fireRate; | ||
+ | | ||
+ | //set animation state | ||
+ | animator.SetInteger("state", STATE_SHOOTING); | ||
+ | | ||
+ | //raycast & everything else .. | ||
+ | } | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | ==== Animatia caracterelor ==== | ||
+ | |||
+ | Animatia caracterelor 3D, se face similar ca in 2D, fiind utilizata animatia bazata pe oase. Astfel, fiecare caracter 3D are definite oase (bones) care controleaza miscarea si animatia acestuia. | ||
+ | |||
+ | {{ :pjv:laboratoare:humanoid_bones.png?direct&600 |}} | ||
+ | |||
+ | Crearea animatiilor se poate face folosind utilitarul de animatie (Window > Animation), similar ca in spatiul 2D. Similar, se poate folosi Animator Controller pentru a gestiona starile. | ||
+ | |||
+ | Ca notiune suplimentara, se pot defini stari de tranzitie/interpolare intre anumite animatii. Un exemplu comun este trecerea sau ajustarea animatiei de walk si run in functie de viteza curenta a personajului, sau a animatie de alergare stanga - fata - dreapta (daca sunt definite animatii diferite). Acest lucru se poate realiza folosind ''BlendTrees'' (Create State > From New Blend Tree). | ||
+ | |||
+ | {{ :pjv:laboratoare:mecanimblendtreestatediagramcombined.png?direct&600 |}} | ||
+ | |||
+ | === Avatar Masks and Animation Layers === | ||
+ | |||
+ | In cazul in care vrem sa combinam animatii putem folosi masti si niveluri de animatie. | ||
+ | |||
+ | {{ https://www.theappguruz.com/app/uploads/2015/06/wave-animation-with-avatar-mask-1.png |}} | ||
+ | |||
+ | Mastile ne permit sa facem discard la o portiune de animatie astfel incat sa preluam animatia doar pentru anumite parti de schelet. | ||
+ | |||
+ | <note tip> | ||
+ | **Avatar Masks** | ||
+ | Masking allows you to discard some of the animation data within a clip, allowing the clip to animate only parts of the object or character rather than the entire thing. For example, if you had a character with a throwing animation. If you wanted to be able to use the throwing animation in conjunction with various other body movements such as running, crouching and jumping, you could create a mask for the throwing animation limiting it to just the right arm, upper body and head. This portion of the animation can then be played in a layer over the top of the base running or jumping animations. | ||
+ | </note> | ||
+ | |||
+ | {{ https://docs.unity3d.com/uploads/Main/AvatarMaskInspectorHumanoid.png |}} | ||
+ | |||
+ | In combinatie cu Layere de animatie in Unity putem combina diverse tipuri de animatii: de ex mers si tras in acelasi timp. | ||
+ | |||
+ | <note tip> | ||
+ | **Animation Layers** | ||
+ | Unity uses Animation Layers for managing complex state machines for different body parts. An example of this is if you have a lower-body layer for walking-jumping, and an upper-body layer for throwing objects / shooting. | ||
+ | </note> | ||
+ | |||
+ | {{ https://docs.unity3d.com/2018.2/Documentation/uploads/Main/AnimatorSyncedLayer.png |}} | ||
+ | |||
+ | ==== Wait for animation ==== | ||
+ | |||
+ | Exista mai multe variante: | ||
+ | * verificam starea animatiei | ||
+ | |||
+ | <code> | ||
+ | Destroy (gameObject, this.GetComponent<Animator>().GetCurrentAnimatorStateInfo(0).length + delay); | ||
+ | </code> | ||
+ | * folosim corutine | ||
+ | |||
+ | <code> | ||
+ | | ||
+ | Update() { | ||
+ | animation.Play("Die"); | ||
+ | waitForAnimation(); | ||
+ | } | ||
+ | | ||
+ | IEnumerator waitForAnimation() { | ||
+ | yield WaitForSeconds (animation["CrouchRun"].length); | ||
+ | | ||
+ | Destroy(gameObject) ; | ||
+ | | ||
+ | dead = Instantiate( deadNew, dead.position, dead.transform.rotation); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | |||
+ | ==== CheatSheet ==== | ||
+ | > **GameObject Manipulation** | ||
+ | |||
+ | <code c#> | ||
+ | /* Create a GameObject */ | ||
+ | Instantiate(GameObject prefab); | ||
+ | Instantiate(GameObject prefab, Transform parent); | ||
+ | Instantiate(GameObject prefab, Vector3 position, Quaternion rotation); | ||
+ | /* In Practice */ | ||
+ | Instantiate(bullet); | ||
+ | Instantiate(bullet, bulletSpawn.transform); | ||
+ | Instantiate(bullet, Vector3.zero, Quaternion.identity); | ||
+ | Instantiate(bullet, new Vector3(0, 0, 10), bullet.transform.rotation); | ||
+ | |||
+ | newobj = Instantiate(objTemplate) as ObjType; | ||
+ | |||
+ | //from pregab - prefab must be in Resources folder | ||
+ | newobj1 = Instantiate(Resources.Load("enemy")); | ||
+ | |||
+ | // Instantiate the projectile at the position and rotation of this transform | ||
+ | Rigidbody projectile; | ||
+ | Rigidbody clone; | ||
+ | clone = Instantiate(projectile, transform.position, transform.rotation); | ||
+ | |||
+ | enemyOrc = Instantiate(Orc) as Enemy; | ||
+ | |||
+ | /* Destroy a GameObject */ | ||
+ | Destroy(gameObject); | ||
+ | |||
+ | /* Finding GameObjects */ | ||
+ | GameObject myObj = GameObject.Find("NAME IN HIERARCHY"); | ||
+ | GameObject myObj = GameObject.FindWithTag("TAG"); | ||
+ | childObject=parentObject.GetChild("child_name"); | ||
+ | parentObject.GetChild("child_name").GetComponent<SpriteRenderer>().sprite = image; | ||
+ | |||
+ | /* Accessing Components */ | ||
+ | Example myComponent = GetComponent<Example>(); | ||
+ | AudioSource audioSource = GetComponent<AudioSource>(); | ||
+ | Rigidbody rgbd = GetComponent<Rigidbody>(); | ||
+ | GetComponent<SpriteRenderer>().sprite = image; //set image in child component | ||
+ | GetComponent<Text>().text = '123' //set text | ||
+ | |||
+ | /* Transforms - can be accessed using the `transform` attribute */ | ||
+ | Vector3 objectPosition = gameObject.transform.position; | ||
+ | gameObject.transform.position = new Vector3(posX, posY, posZ); | ||
+ | transform.Translate(Vector3.up * Time.deltaTime, Space.World); | ||
+ | transform.Rotate(Vector3.up * Time.deltaTime, Space.World); | ||
+ | |||
+ | /* Activate - can hide or how an element from the scene*/ | ||
+ | myObject.SetActive(false); // hide | ||
+ | myObject.SetActive(true); // show | ||
+ | |||
+ | GetComponent<BoxCollider>().SetActive(false); // hide component | ||
+ | |||
+ | </code> | ||
+ | |||
+ | > **Vector Quick Reference** | ||
+ | |||
+ | X = Left/Right Y = Up/Down Z = Forward/Back | ||
+ | |||
+ | <code c#> | ||
+ | Vector3.right /* (1, 0, 0) */ Vector2.right /* (1, 0) */ | ||
+ | Vector3.left /* (-1, 0, 0) */ Vector2.left /* (-1, 0) */ | ||
+ | Vector3.up /* (0, 1, 0) */ Vector2.up /* (0, 1) */ | ||
+ | Vector3.down /* (0, -1, 0) */ Vector2.down /* (0, -1) */ | ||
+ | Vector3.forward /* (0, 0, 1) */ | ||
+ | Vector3.back /* (0, 0, -1) */ | ||
+ | Vector3.zero /* (0, 0, 0) */ Vector2.zero /* (0, 0) */ | ||
+ | Vector3.one /* (1, 1, 1) */ Vector2.one /* (1, 1) */ | ||
+ | float length = myVector.magnitude /* Length of this Vector */ | ||
+ | myVector.normalized /* Keeps direction, but reduces length to 1 */ | ||
+ | |||
+ | </code> | ||
+ | |||
+ | > **Time Variables** | ||
+ | |||
+ | <code c#> | ||
+ | /* The time in seconds since the start of the game */ | ||
+ | float timeSinceStartOfGame = Time.time; | ||
+ | |||
+ | /* The scale at which the time is passing */ | ||
+ | float currentTimeScale = Time.timeScale; | ||
+ | /* Pause time */ | ||
+ | Time.timeScale = 0; | ||
+ | |||
+ | /* The time in seconds it took to complete the last frame */ | ||
+ | /* Use with Update() and LateUpdate() */ | ||
+ | float timePassedSinceLastFrame = Time.deltaTime; | ||
+ | |||
+ | /* The interval in seconds at which physics and fixed frame rate updates are performed */ | ||
+ | /* Use with FixedUpdate() */ | ||
+ | float physicsInterval = Time.fixedDeltaTime; | ||
+ | |||
+ | </code> | ||
+ | |||
+ | > **Random values** | ||
+ | |||
+ | <code c#> | ||
+ | Random.Range(-10.0f, 10.0f) | ||
+ | Random.Range(0, 8); | ||
+ | </code> | ||
+ | |||
+ | > **Physics Events** | ||
+ | |||
+ | <code c#> | ||
+ | /* Both objects have to have a Collider and one object has to have a Rigidbody for these Events to work */ | ||
+ | private void OnCollisionEnter(Collision hit) { Debug.Log(gameObject.name + " just hit " + hit.gameObject.name); } | ||
+ | private void OnCollisionStay(Collision hit) { Debug.Log(gameObject.name + " is hitting " + hit.gameObject.name); } | ||
+ | private void OnCollisionExit(Collision hit) { Debug.Log(gameObject.name + " stopped hitting " + hit.gameObject.name); } | ||
+ | |||
+ | /* Trigger must be checked on one of the Colliders */ | ||
+ | private void OnTriggerEnter(Collider hit) { Debug.Log(gameObject.name + " just hit " + hit.name); } | ||
+ | private void OnTriggerStay(Collider hit) { Debug.Log(gameObject.name + " is hitting " + hit.name); } | ||
+ | private void OnTriggerExit(Collider hit) { Debug.Log(gameObject.name + " stopped hitting " + hit.name); } | ||
+ | |||
+ | /* For 2D Colliders just add 2D to the Method name and the Parameter Type */ | ||
+ | private void OnCollisionEnter2D(Collision2D hit) { } | ||
+ | private void OnCollisionStay2D(Collision2D hit) { } | ||
+ | private void OnCollisionExit2D(Collision2D hit) { } | ||
+ | private void OnTriggerEnter2D(Collider2D hit) { } | ||
+ | private void OnTriggerStay2D(Collider2D hit) { } | ||
+ | private void OnTriggerExit2D(Collider2D hit) { } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | > **Coroutines** | ||
+ | |||
+ | <code c#> | ||
+ | /* Create a Coroutine */ | ||
+ | private IEnumerator CountSeconds(int count = 10) | ||
+ | { | ||
+ | for (int i = 0; i <= count; i++) { | ||
+ | Debug.Log(i + " second(s) have passed"); | ||
+ | yield return new WaitForSeconds(1.0f); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /* Call a Coroutine */ | ||
+ | StartCoroutine(CountSeconds()); | ||
+ | StartCoroutine(CountSeconds(10)); | ||
+ | |||
+ | /* Call a Coroutine that may need to be stopped */ | ||
+ | StartCoroutine("CountSeconds"); | ||
+ | StartCoroutine("CountSeconds", 10); | ||
+ | |||
+ | /* Stop a Coroutine */ | ||
+ | StopCoroutine("CountSeconds"); | ||
+ | StopAllCoroutines(); | ||
+ | |||
+ | /* Store and call a Coroutine from a variable */ | ||
+ | private IEnumerator countSecondsCoroutine; | ||
+ | |||
+ | countSecondsCoroutine = CountSeconds(); | ||
+ | StartCoroutine(countSecondsCoroutine); | ||
+ | |||
+ | /* Stop a stored Coroutine */ | ||
+ | StopCoroutine(countSecondsCoroutine); | ||
+ | |||
+ | /* Coroutine Return Types */ | ||
+ | yield return null; // Waits until the next Update() call | ||
+ | yield return new WaitForFixedUpdate(); // Waits until the next FixedUpdate() call | ||
+ | yield return new WaitForEndOfFrame(); // Waits until everything this frame has executed | ||
+ | yield return new WaitForSeconds(float seconds); // Waits for game time in seconds | ||
+ | yield return new WaitUntil(() => MY_CONDITION); // Waits until a custom condition is met | ||
+ | yield return new WWW("MY/WEB/REQUEST"); // Waits for a web request | ||
+ | yield return StartCoroutine("MY_COROUTINE"); // Waits until another Coroutine is completed | ||
+ | |||
+ | </code> | ||
+ | |||
+ | > **Input Quick Reference** | ||
+ | |||
+ | <code c#> | ||
+ | if (Input.GetKeyDown(KeyCode.Space)) { Debug.Log("Space key was Pressed"); } | ||
+ | if (Input.GetKeyUp(KeyCode.W)) { Debug.Log("W key was Released"); } | ||
+ | if (Input.GetKey(KeyCode.UpArrow)) { Debug.Log("Up Arrow key is being held down"); } | ||
+ | |||
+ | /* Button Input located under Edit >> Project Settings >> Input */ | ||
+ | if (Input.GetButtonDown("ButtonName")) { Debug.Log("Button was pressed"); } | ||
+ | if (Input.GetButtonUp("ButtonName")) { Debug.Log("Button was released"); } | ||
+ | if (Input.GetButton("ButtonName")) { Debug.Log("Button is being held down"); } | ||
+ | |||
+ | float translation = Input.GetAxis("Vertical") * speed; | ||
+ | float rotation = Input.GetAxis("Horizontal") * rotationSpeed; | ||
+ | |||
+ | // Make it move 10 meters per second instead of 10 meters per frame... | ||
+ | translation *= Time.deltaTime; | ||
+ | rotation *= Time.deltaTime; | ||
+ | |||
+ | // Move translation along the object's z-axis | ||
+ | transform.Translate(0, 0, translation); | ||
+ | |||
+ | // Rotate around our y-axis | ||
+ | transform.Rotate(0, rotation, 0); | ||
+ | |||
+ | /* special input events */ | ||
+ | // OnMouseDown is called when the user has pressed the mouse button while over the Collider. | ||
+ | // This event is sent to all scripts of the GameObject with Collider or GUI Element. Scripts of the parent or child objects do not receive this event. | ||
+ | // This function is not called on objects that belong to Ignore Raycast layer. | ||
+ | // This function is called on Colliders marked as Trigger if and only if Physics.queriesHitTriggers is true. | ||
+ | void OnMouseDown() | ||
+ | { | ||
+ | // Destroy the gameObject after clicking on it | ||
+ | Destroy(gameObject); | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ |