Differences

This shows you the differences between two versions of the page.

Link to this comparison view

pjv:laboratoare:2022:05 [2022/11/09 11:46]
alexandru.gradinaru
pjv:laboratoare:2022:05 [2022/11/23 13:23] (current)
alexandru.gradinaru
Line 1: Line 1:
-===== Programarea ​obiectelor ​=====+ 
 +===== Programarea ​interfetelor grafice ​=====
  
 ==== Cerinte ===== ==== Cerinte =====
 +
 +<note important>​Pentru simplitate puteti porni de la scena de MOBA creata in Lab3/Lab4, dar nu este obligatoriu</​note>​
 +
 +Realizati o scena care sa contina:
 +
 +  * Environment:​ teren cu copaci
 +  * Personaj
 +    * Controlabil
 +    * Are un healthbar afisat
 +    * Are cel putin 2 atribute (de ex strength si dexterity)
 +    * Are un inventar cu obiecte
 +      * accesibil pe tasta '​i'​
 +      * obiectele se pot adauga, utiliza sau elimina din inventar
 +      * obiectele sunt de cel putin 2 tipuri: ​
 +        - consumabil: de ex la apasarea pe el se poate recupera din viata personajului
 +        - echipabil: de ex la o apasare se echipeaza - cresc anumite atribute ale playerului (strength, dexterity etc.) - la o alta apasare se dezechipeaza - scad atributele definite pe item
 +  * Inamici care se misca pe harta
 +    * Inamicii trebuie sa aiba healthbar deasupa capului
 +  * Un minimap cu buline rosii pentru inamici si bulina albastra pentru player
  
  
 +Bonus:
 +  * NPC Dialog system ​
  
 ==== Documentatie video ===== ==== Documentatie video =====
Line 16: Line 38:
 <​html><​br></​html>​ <​html><​br></​html>​
 ==== Sumar documentatie ===== ==== Sumar documentatie =====
-==== 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. 
  
-<​code>​ +==== Elemente de GUI ====
-public class InteractionObject : MonoBehaviour {+
  
-  public float radius = 1f; +Se pot folosi atat obiecte plasate in scena, cat si partea de Canvas, in ceea ce priveste interfata grafica.
-  public Transform interactionPoint;​ +
-  Transform interactionObject;​ +
-  bool done = false;+
  
-  //metoda abstracta, speficica fiecarui tip de interactiuni +=== FPS Gun ===
-  public virtual void Interaction () +
-  { +
-  +
-  }+
  
-  void Update () +Astfel, spre exemplu pentru un FPS single player, in general nu avem nevoie de caracter, ci doar de maini, sau eventual doar o arma animata. 
-  { +Pentru a controla usor acest aspect se poate importa un obiect texturat pentru armacare se va atasa camerei 3D disponibile in scena si se va configura astfel incat sa arate cat mai natural.
-      float distance = Vector3.Distance(interactionObject.positioninteractionPoint.position);+
  
-      if (distance <= radius && !done) // avem interactiune cu obiectul, pot sa afisez informatii: de ex "Press E to use" +{{ :​pjv:​laboratoare:​gun.png?​direct&​750 |}}
-      ​{ +
-        done = true; +
-        Interaction();​ +
-      } +
-    ​} +
-  ​}+
  
-+Pentru a crea un crosshair sau o tinta se poata adauga un disc sau o imagine cu transparenta in centrul canvas-ului.  
-</​code>​+ 
 +{{ :​pjv:​laboratoare:​crosshair.png?​direct&​750 |}
 + 
 +Bineinteles,​ se poate crea un crosshair animat si folosind 4 dreptunghiuri simple. 
 + 
 +{{ :​pjv:​laboratoare:​crosshair2.png?​direct&​750 |}} 
 + 
 +=== Healthbar === 
 + 
 +Pentru reprezentarea healthbar-ului sau a altor elemente de tip bara, se poate folosi un element de UI (gameobject) de tip Slider, asa cum este reprezentat in imaginea de mai sus.
  
-Astfel, toate obiectele ce vor avea interactiuni,​ vor mosteni aceasta clasa. 
-Spre exemplu pentru un obiect de pickup putem avea urmatoarea secventa: 
 <​code>​ <​code>​
-public class PickupObject : InteractionObject { +GetComponent.<​Slider>​().value = myHealth;
- +
-  public override void Interaction() +
-  { +
-    base.Interaction(); // se apeleaza metoda parinte, in caz ca avem ceva generic +
-     +
-    //​mecanica +
-    ​... +
-     +
-    //distrugem obiectul +
-    Destroy(gameObject); +
-     +
-  } +
-}+
 </​code>​ </​code>​
  
-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.+De asemenea se mai pot folosi doua dreptunghiuri suprapuse: unul care reprezinta viata totala, ​si unul care reprezinta viata curentacel din urma fiind scalar relativ ​la valoare vietii personajului.
  
 +{{ :​pjv:​laboratoare:​2022:​healthbar.png?​200 |}}
 +
 +=== Minimap ===
 +
 +Pentru crearea unui minimap se foloseste de regula o camera suplimentara,​ plasata top-down deasupra scenei.
 +
 +{{ :​pjv:​laboratoare:​minimap1.png?​direct&​350 |}}
 +
 +Camera special creata va trebui sa afiseze scena intr-o textura. Pentru acest lucru vom folosi un asset de tip ''​Render texture''​ (//Assets -> Create -> Render Texture//) pe care o vom atasa ca Target in camera nou creata.
 +
 +{{ :​pjv:​laboratoare:​minimap3.png?​direct&​350 |}}
 +
 +Pasul urmator este sa afisam Minimap-ul. Pentru acest lucru, in canvas vom crea o imagine si vom lega textura in care salvam imaginile de la camera la aceasta.
 +
 +{{ :​pjv:​laboratoare:​minimap4.png?​direct&​350 |}}
 +
 +Rezultatul poate fi ibunatatit prin plasarea acestuia intr-un loc potrivit adaugarea unei masti sau a unei borduri si asa mai departe.
 +
 +{{ :​pjv:​laboratoare:​minimap7.png?​direct&​750 |}}
 +
 +=== Layers === 
 +
 +Pentru a defini sau actualiza Layere se paote merge la Edit -> Project Settings -> Tags and Layers.
 +Putem folosi Layere pentru a delimita diverse lucruri, de exemplu care obiecte se vad pe carema, pentru care se calculeaza coliziunea etc.
 +
 +Putem simplifica geometria din minimap folosind Layere si primitive simplificate.
 +
 +{{ https://​blog.theknightsofunity.com/​wp-content/​uploads/​2016/​04/​desktop.png |}}
 +
 +De exemplu se poata adauga un obiect copil pentru caracter sau obietele care vreau sa apara pe harta. Setam Layerul de Minimap pentru obiect.
 +
 +{{ https://​blog.theknightsofunity.com/​wp-content/​uploads/​2016/​04/​0f762c1f-bc89-4dd7-af2e-04b11abe7520.png |}}
 +
 +Adaugam si cativa inamici cu alta culoare
 +
 +{{ https://​blog.theknightsofunity.com/​wp-content/​uploads/​2016/​04/​c57d0c80-c9e3-42b4-9a0d-6efa9d7a68ca.png |}}
 +
 +Urmatorul pas este sa configuram camera principala astfel incat sa nu afiseze aceste primitive.
 +
 +{{ http://​blog.theknightsofunity.com/​wp-content/​uploads/​2016/​04/​5b252849-280c-4322-9115-7e20fb71de52.png |}}
 +
 +Apoi dezactivam toate celelalte layere si lasam doar Minimap pentru camera care face Render to Texture din setarea de Culling Mask
 +
 +{{ https://​blog.theknightsofunity.com/​wp-content/​uploads/​2016/​04/​da459b5f-c6ad-4217-a2e8-938f418535b7.png |}}
 +
 +Rezultatul este simplificat ca geometrie si urmeaza pozitiile personajelor
 +
 +{{ https://​blog.theknightsofunity.com/​wp-content/​uploads/​2016/​04/​3b117f6d-153f-4df6-a96c-51487c410209.png |}}
 +
 +==== Crearea de animatii pentru obiecte de UI====
 +
 +Crearea animatiilor se poate face folosind utilitarul de animatie (Window > Animation), similar ca si pentru restul animatiilor.
 +Similar, se poate folosi Animator Controller pentru a gestiona starile (spre exemplu tras cu arma).
 +
 +
 +=== GUI ===
 +
 +Pentru afisarea elementelor de interfata relative la spatiul scenei se poate folosi atat un canvas, cat si mai multe instante de canvas.
 +
 +{{ https://​www.stevestreeting.com/​images/​healthbarsdemo_thumb.png |}}
 +
 +Astfel, o varianta este sa atasati un canvas de tip WorldSpace la obiectele care au nevoie de elemente de interfata grafica (de exemplu health bar).
 +
 +
 +{{ https://​www.stevestreeting.com/​images/​healthbarobj.png |}}
 +{{ :​pjv:​laboratoare:​2020:​localcanvas.png?​direct&​750 |}}
 +{{ :​pjv:​laboratoare:​2020:​localcanvas-wspace.png |}}
 +
 +O alta varianta este prin scriptarea unui singur element de canvas referentiat si transpunerea lui relativ la obiectul curent (util de exemplu in cazul dialogului sau a elementelor care nu se pot afisa de mai multe ori in acelasi timp)
 <​code>​ <​code>​
-void OnDrawGizmosSelected ​()+var target : Transform;​ 
 +  
 +function Update ​()
 { {
-  Gizmos.color ​Color.white; +     var wantedPos ​Camera.main.WorldToViewportPoint ​(target.position)
-  Gizmos.DrawWireSphere(interactionPoint.position, radius); +     ​transform.position = wantedPos
-} +}
 </​code>​ </​code>​
 +=== Inventar ===
  
-{{ :​pjv:​laboratoare:​gizmosselected.png?​direct&​750 |}}+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
 +<​code>​
  
-=== Quest System ​===+[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;​ 
 +
 +</​code>​
  
-In ceea ce priveste quest-urilesunt foarte multe posibilitati de abordare, dar in general implica urmatoarele elemente: +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).
-  * obiectivele quest-ului +
-  * recompensele +
-  * cine gestioneaza quest-ul+
  
-Astfel, o abordare sugerata este sa se abstractizeze o clasa de tip Quest, una de tip Goal (obiectiv) si una de tip Recompensaintrucat exista multe tipuri in care se pot instantia aceste lucruri.+Mai departepentru un inventar vom avea nevoie de lista de obiecte gestionabile. Pentru acest lucru vom face un script ​de gestiune pentru inventar ​(e.g InventoryManagercare gestioneaza adaugarea, eliminarea ​si interogarea inventarului. Pentru o accesare mai usoara si mai facilaideal 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.
  
-Exemple de tipuri de Obiective: +<​code>​ 
-  * Kill - kill a bunch of stuff +public class InventoryManager ​MonoBehaviour {
-  * Gather - gather stuff for me +
-  * Deliver - deliver my live letter +
-  * etc.+
  
-Exemple de tipuri ​de Recompense: +  // singleton 
-  ​items +  public static InventoryManager instance; 
-  ​* experience +  void Awake() { 
-  ​* gold +    instance = this; 
-  ​* skill +  } 
-  ​* etc.+   
 +  //​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);​ 
 +  }
  
-Exemplu de clasa generica de tip Quest+
 +</​code>​
  
 +Fiind definit ca un singleton, putem accesa acum foarte usor gestionarea inventarului:​
 <​code>​ <​code>​
-public class Quest : MonoBehaviour {+Inventory.instance.Add(item);​ 
 +Inventory.instance.Remove(item);​ 
 +</​code>​
  
-  public List<GoalGoals = new List<​Goal>​();​ +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 ​List<​Reward>​ Rewards = new List<​Reward>​(); +<code
-  public ​bool completed;+  public ​delegate void OnInventoryChanged(); 
 +  public ​OnInventoryChanged onInventoryChangedCallback;
   ​   ​
-  ​public void CheckGoals() { + //​metode pentru gestionare 
-    ​completed = Goals.All(g => g.completed); //questul este gata cand toate obiectivele sunt complete+  ​public void Add(Item item) { 
 +    ..
 +    onInventoryChangedCallback.Invoke(); //notifica despre modificare
   }   }
   ​   ​
-  public void GiveReward() { +  public void Remove(Item item) { 
-    //in functie de tipul recompensei se adauga obiecte in inventar, sau se adauga experienta, skill points etc.+    ​... 
 +    onInventoryChangedCallback.Invoke(); ​//notifica despre ​ modificare
   }   }
 +</​code>​
 +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).
 +
 +{{ :​pjv:​laboratoare:​2020:​inventory.png |}}
 +{{ :​pjv:​laboratoare:​2020:​inventory-grid_layout.png |}}
 +
 +Pentru sloturile de inventar este indicat sa folositi un prefab sau un template.
 +
 +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:
 +
 +<​code>​
 +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(..)
 +  } 
  
 } }
 </​code>​ </​code>​
  
-Apoiun exemplu de un quest concret. +Bineintelesin inventar se pot pune diferse restrictii si interactiuni (cum ar fi dimensiunea maxima a inventarului)
 <​code>​ <​code>​
 +bool AddItem() {
  
-public class RandomSlayThingsQuest : Quest { +if(items.Count >space
- +//no more room 
-  void Start(+return false
-  { +
-    QuestName ​"​Random Slayer";​ +
-    Description = "Kill some time";​ +
-     +
-    Goals.Add(new KillGoal( ...)); +
-    Goals.Add(new KillGoal( ...)); +
-     +
-    Rewards.Add(new ItemReward( ...)); +
-    Rewards.Add(new ExperienceReward( ...));    ​ +
-   +
-  }+
  
 +else return true;
 } }
  
 +if(Inventory.instance.Add(item)) Destroy(gameObject);​
 </​code>​ </​code>​
  
-Si un exemplu ​de Obiectiv+Similar se poate face si gestiunea altor interfet: de quest pentru player, de echipament / arma, skilltree etc. 
 + 
 +=== NPC === 
 + 
 +In ceea ce priveste NPC-urile, acestea de obicei interactioneaza cu player-ul prin dialog: oferirea de informatii, gossip, quest-uri etc. 
 + 
 +Sistemele de dialog si de quest-uri pot fi foarte complexe, iar posibilitatile nelimitate. In continuare voi prezenta cateva lucruri de baza prezente in aceste sisteme. 
 + 
 +Din punct de vedere programatic,​ NPC-urile sunt tot un tip de obiect cu care se poate interactiona,​ astfe ca se poate folosi aceasi paradigma din laboratorul precedent, de InteractionObject. 
 + 
 +Primul punct important in interactiunea cu NPC-urile este sistemul de dialog. 
 + 
 +=== Dialog System === 
 + 
 +Pentru crearea unui sistem de dialog, se folosesc elemente de UI Canvas: Panel, Button, Image etc. 
 + 
 +{{ :​pjv:​laboratoare:​dialog-panel.png?​500 |}}  
 + 
 +Mai departe, putem face un DialogManager,​ tot sub format singleton, pentru a putea referentia elementele de UI mai usor, o singura data. In acest manager putem configura diverse linii de dialog, numele NPC-ului care sa apara in caseta de dialog etc. De asemenea, va trebui sa tinem minte si in ce moment, sau la care linie de dialog ne aflam.
  
 <​code>​ <​code>​
  
-public class KillGoal ​Goal {+public class DialogManager ​MonoBehaviour ​{
   ​   ​
-  ​bool completed+  ​public static DialogManager instance
-  ​int currentAmmount;+  ​public GameObject panel; 
 +  public string NPCName; 
 +  public List<​string>​ sentences = new List<​string>​();
   ​   ​
-  ​public KillGoal(int enemyId, int ammount) { +  ​Button continueButton;​ 
-    ...+  Text dialogeTextContainerNPCNameContainer;​  
 +   
 +  ​int lineIndex;​ 
 +     
 +  void Awake() 
 +  ​
 +    ​//get child components 
 +    dialogeTextContainer = ...  
 +    NPCNameContainer = ... 
 +    continueButton = ... 
 +    continueButton.onClick.AddListener(delegate { ContinueDialog();​ }); 
 +     
 +    instance = this;
   }   }
   ​   ​
-  public ​override ​void Init() { +  public void AddNewDialogue(string[] lines, string NPCName) 
-    //listen to enemy death event +  ​
-    ​EnemyManager.onDie +EnemyDied;+    //initializeaza dialogul curent din NPC 
 +    ​lineIndex=0; 
 +    ...
   }   }
   ​   ​
-  void EnemyDied(enemy) { +  ​public ​void showDialog() 
-    ​this.currentAmmount+++  ​
-    ​if(this.currentAmmount >ammount) { +    ​dialogeTextContainer.text = sentences[lineIndex]//afiseaza linia de dialog curenta 
-      this.completed = true; +    ​NPCNameContainer.textNPCName; 
-    ​}+    panel.SetActive(true)
 +    ​
   }   }
   ​   ​
 +  public void ContinueDialog()
 +  {
 +    lineIndex++;​
 +    showDialog();​
 +  }
   ​   ​
 } }
Line 179: Line 338:
 </​code>​ </​code>​
  
-In ceea ce priveste cine gestioneaza questulacesta este de obicei un NPC, deci putem extinde clasa NPC cu cateva lucuri specifice:+Apoiin clasa NPC-ului, putem instantia un dialog personalizat pentru ​NPC-ul respectiv:
  
 <​code>​ <​code>​
  
-public class QuestNPC : NPC { +public class NPC : InteractionObject ​{
- +
  
-  public ​bool assigned+  public ​string[] sentences//se pot configura liniile de dialog in editor 
-  public ​Quest quest;+  public ​string name;
   ​   ​
   public override void Interaction()   public override void Interaction()
Line 193: Line 351:
     base.Interaction();​ // se apeleaza metoda parinte     base.Interaction();​ // se apeleaza metoda parinte
  
-    ​if(!assigned) { +    ​DialogManager.Instance.AddNewDialog(sentences, name);
-      //dialog +
-      //assign +
-    } +
-     +
-    void CheckQuest() { +
-      if(quest.completed) { +
-        quest.GiveReward(); +
-      } +
-    }+
   }   }
-} 
  
 </​code>​ </​code>​
 +
 +{{ :​pjv:​laboratoare:​dialog.png?​500 |}}
pjv/laboratoare/2022/05.1667987180.txt.gz · Last modified: 2022/11/09 11:46 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