Animatii si interactiuni 3D

Pentru navigarea automata a personajelor pana la un punct pe harta, indiferent ca e vorba de un personaj controlat sau un agent inteligent, avem nevoie in primul rand de generarea unor zone in care acestea sa se poata misca. Aceste zone poarta numele de Navigation Mesh sau NavMesh, prescurtat. In Unity se pot genera automat acest NavMesh folosind utilitarul de Navigation Bake: Window > AI > Navigation > Bake.

De mentionat este ca un navmesh se genereaza folosind elementele statice ale scenei. Pentru a marca un obiect ca fiind static, trebuie bifat in inspector casuta de Static.

Mai mult, meniul de Navigation, permite detalierea fiecarui obiect cu privire la navigatie, astfel incat se poate specifica daca un obiect e static sau daca e Walkable sau nu.

Crearea (Bake) NavMesh-ului este statica, in sensul ca odata creata ea nu se mai modifica la runtime. Astfel, daca avem obiecte mobile, care influienteaza NavMesh-ul, nu le putem declara statice si nu le putem include in procesul de Bake. Pentru acestea exista o componenta Nav Mesh Obstacle care altereaza dinamic NavMesh-ul.

Rezultatul este o zona in care agentii sau jucatorii pot naviga folosind functii puse la dispozitie de unity pentru deplasare.

Miscare la o destinatie point-and-click

Pentru a misca efectivul jucatorul, trebuie sa folosim sistemul de navigare al Unity. Pentru asta se poate folosi pachetul UnityEngine.AI, vom adauga o componenta de tip Nav Mesh Agent si vom initializa un agent.

using UnityEngine.AI;

void Start() {
   agent = GetComponent<NavMeshAgent>();
}

Pentru a controla jucatorul cu mouse-ul, avem nevoie de destinatia la care trebuie personajul sa navigheze. Acest lucru putem sa il aflam cu un RayCast.

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))
  {
    //misca player-ul pana la destinatie
    agent.SetDestination(hit.point); 
  }

}

In multe cazuri avem actiuni mai complexe la mouse click dreapta: cum ar fi atacarea unui inamic, sau preluarea unui obiect etc, caz un care obiectele se pot misca in scena. Pentru asta, trebuie implementata o functie astfel incat sa se poata actualiza pozitia targetului curent.

Transform target = null;

if(Input.GetMouseButtonDown(1)) //la apasarea click dreapta
{
  Ray mouseClickRay = camera.ScreenPointToRay(Input.mousePosition); //creaza o raza printr-un punct de pe ecran
  RaycastHit hit;
  
  if(Physics.Raycast(mouseClickRay, out hit))
  {
    target = hit.transform;
    StartCoroutine(FollowTarget()); //follow target pana la destinatie
  }

}

if(Input.GetMouseButtonDown(0)) //la apasarea click stanga
{
  target=null; //dezactiveaza target-ul
}

IEnumerator FollowTarget()
{
  while(target!=null) {
    agent.SetDestination(target); 
    yield return null;
  }
  yield return 0;
}

Pentru a avea o miscare completa, camera trebuie sa urmareasca jucatorul. Astfel, putem plasa camera ca si child al player-ului, sau putem actualiza pozitia folosind un script ca in exemplul de mai jos.

public Transform target;
public Vector3 offset;
public float height = 2f;
public float zoom=10f;

void LateUpdate() //se apeleaza imediat dupa update
{
  transform.position = target.position - offset * zoom; //actualizeaza pozitia
  transform.LookAt(target.position + Vector3.up * height); //seteaza directia camerei inspre player
}

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.

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).

Interactiunea cu obiectele

Interactiunea cu obiectele in spatiul 3D poate fi extrem de complexa, intrucat exista foarte multe forme de interactiuni: inamici, obiecte de pickup, deschidere de usi, activare de manivele etc. Fiecare dintre aceste interactiuni are specificul ei, dar abstractizand, putem deduce ca fiecare interactiune se intampla intr-o anumita raza si cu un anumit punct de interactiune. Pentru a defini usor aceste lucruri, putem crea o clasa generica denumita InteractionObject cu o metoda abstracta (virtuala) ce defineste interactiunea in detaliu.

public class InteractionObject : MonoBehaviour {

  public float radius = 1f;
  public Transform interactionPoint;
  Transform interactionObject;
  bool done = false;

  //metoda abstracta, speficica fiecarui tip de interactiuni
  public virtual void Interaction ()
  {
		
  }

  void Update ()
  {
      float distance = Vector3.Distance(interactionObject.position, interactionPoint.position);

      if (distance <= radius && !done) // avem interactiune cu obiectul
      {
        done = true;
        Interaction();
      }
    }
  }

}

Astfel, toate obiectele ce vor avea interactiuni, vor mosteni aceasta clasa. Spre exemplu pentru un obiect de pickup putem avea urmatoarea secventa:

public class PickupObject : InteractionObject {

  public override void Interaction()
  {
    base.Interaction(); // se apeleaza metoda parinte, in caz ca avem ceva generic
    
    //mecanica
    ...
    
    //distrugem obiectul
    Destroy(gameObject);
    
  }
}

Pentru a controla mai bine zona de actiune (radius) si punctul de interes pentru un obiect de interactiune (InteractionObject), se poate defini o functie de editor, atunci cand obiectul este selectat. In exemplul de mai jos, la selectarea obiectului se va afisa o sfera wireframe de culoare alba.

void OnDrawGizmosSelected ()
{
  Gizmos.color = Color.white;
  Gizmos.DrawWireSphere(interactionPoint.position, radius);
}	

Inventar

Pentru a crea un sistem de inventar avem nevoie in primul rand de date atasate fiecarui obiect, cum ar fi nume, icon, atribute etc. Putem realiza acest lucru usor prin obiecte scriptabile (Scriptable Objects). Obiectele Scriptable sunt containere de date ce nu trebuie sa fie atasate la un GameObject intr-o scena. Ele pot fi salvate ca asset-uri in bibliteca proiectului ca mai apoi sa poata fi utilizate.

Obiectele scriptabile se definesc prin crearea unui script ce mosteneste clasa ScriptableObject.

Pentru a instantia un obiect scriptabil avem doua variante:

  • prin script: SciptableObject.CreateInstance<MyScriptableObjectClass>()
  • din mediu: CreateAssetMenu - adauga o intrare noua in meniul de create asset
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item", order = 1)]
public class Item : ScriptableObject {
    new public string name = "New MyScriptableObject"; //suprascrie atributul name
    public string objectName = "New MyScriptableObject";
    public bool colorIsRandom = false;
    public Color thisColor = Color.white;
    public Sprite icon;
    public Vector3[] spawnPoints;
}

Intrucat exista atribute implicite pentru un obiect scriptabil (e.g. name), putem folosi variabile diferite (e.g. objectName) sau putem suprascrie definirea acestui atribut prin folosirea metodei new (new public string name).

Mai departe, pentru un inventar vom avea nevoie de o lista de obiecte gestionabile. Pentru acest lucru vom face un script de gestiune pentru inventar (e.g InventoryManager) care gestioneaza adaugarea, eliminarea si interogarea inventarului. Pentru o accesare mai usoara si mai facila, ideal ar fi ca acest inventorymanager sa fie un Singleton. Pentru a avea un singleton trebuie sa ne asiguram ca avem o singura instanta creata pentru acest script atunci cand e accesat.

public class InventoryManager : MonoBehaviour {

  // singleton
  public static InventoryManager instance;
  void Awake() {
    instance = this;
  }
  
  //lista de obiecte
  public List<Item> items = new List<Item>();
  
  //metode pentru gestionare
  public void Add(Item item) {
    items.Add(item);
  }
  
  public void Remove(Item item) {
    items.Remove(item);
  }

}

Fiind definit ca un singleton, putem accesa acum foarte usor gestionarea inventarului:

Inventory.instance.Add(item);
Inventory.instance.Remove(item);

Inca un element util in gestionarea inventarului este definirea unei metode de a notifica atunci cand s-a produs o modificare in inventar. Pentru acest lucru putem folosi Delegates. Un Delegate este un pointer la o metode. Aceasta ne permite sa tratam metoda ca o variabila și sa o folosim pentru un callback. Cand este apelata, acesta notifica toate metodele care fac referire la delegate. Astfel putem definit o variabila pentru evenimentul de schimbare.

  public delegate void OnInventoryChanged();
  public OnInventoryChanged onInventoryChangedCallback;
  
 //metode pentru gestionare
  public void Add(Item item) {
    ...
    onInventoryChangedCallback.Invoke(); //notifica despre modificare
  }
  
  public void Remove(Item item) {
    ...
    onInventoryChangedCallback.Invoke(); //notifica despre  modificare
  }

Urmatorul pas este crearea unei interfete grafice si legarea interfetei de functionalitatea InventoryManger-ului. Pentru interfata grafica, putem folosi, ca si pana acum, canvas-ul oferit de Unity, structurat astfel incat sa avem un panou general pentru inventar, si mai multe slot-uri pentru obiectele din acesta. Pe fiecare slot putem defini urmatoarele aspecte:

  • un gameobject
  • un buton de accesare : pentru folosirea obiectului, echipare etc.
  • un buton de eliminare : pentru eliminarea din inventar
  • o imagine: icon pentru obiectul in inventar
  • script de gestionare: pentru fiecare slot putem avea un script de gestionare care va actualiza interfata slotului respectiv (nume, icon etc).

Interfata grafica a inventarului are nevoie si de un script de gestionare. Astfel vom aveam un script care asculta (subscribe) evenimentul definit (delegate) la actualizarea inventarului, si actualizeaza fiecare slot din interfata grafica:

void Start() {
  inventory = Inventory.instance;
  inventory.onInventoryChangedCallback += UpdateUI; //definesc o metoda ca se apeleaza la aparitia unui eveniment delegat
  
  slots = GetComponentsInChildren<InventorySlot>(); //fiecare slot din inventar
}

void UpdateUI() {
  
  //actualizare fiecare slot
  for(i=0; i < slots.Length; i++)
  {
    if(i<iventory.items.Count) slots[i].AddItem(..)
    else slots[i].RemoveItem(..)
  } 

}

Cerinte

Realizarea unui joc 3D Top-Down

  1. Configurati scena pentru rulare 3D
  2. Adaugati in scena un caracter animat (puteti lua resurse din asset store sau alte locuri: Resurse utile) astfel incat:
    1. camera urmareste caracterul de sus
    2. se va deplasa prin click mouse-ul la o locatie pe harta;
    3. poate lua obiecte in inventar
    4. poate arunca obiecte din inventar
  3. Adaugati mai multe tipuri de obiecte interactionabile in scena
pjv/laboratoare/06.txt · Last modified: 2019/10/02 12:59 by alexandru.gradinaru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0