Table of Contents

Elemente de 3D 3rd Person

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
}

GUI

Pentru afisarea elementelor de interfata relative la spatiul scenei se poate folosi atat un canvas, cat si mai multe instante de canvas.

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

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)

var target : Transform;
 
function Update ()
{
     var wantedPos = Camera.main.WorldToViewportPoint (target.position);
     transform.position = wantedPos;
}

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:

[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:

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:

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

}

Bineinteles, in inventar se pot pune diferse restrictii si interactiuni (cum ar fi dimensiunea maxima a inventarului)

bool AddItem() {

if(items.Count >= space)
//no more room
return false

else return true;
}

if(Inventory.instance.Add(item)) Destroy(gameObject);

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.

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.

public class DialogManager : MonoBehaviour {
  
  public static DialogManager instance;
  public GameObject panel;
  public string NPCName;
  public List<string> sentences = new List<string>();
  
  Button continueButton;
  Text dialogeTextContainer, NPCNameContainer; 
  
  int lineIndex;
    
  void Awake()
  {
    //get child components
    dialogeTextContainer = ... 
    NPCNameContainer = ...
    continueButton = ...
    continueButton.onClick.AddListener(delegate { ContinueDialog(); });
    
    instance = this;
  }
  
  public void AddNewDialogue(string[] lines, string NPCName)
  {
    //initializeaza dialogul curent din NPC
    lineIndex=0;
    ...
  }
  
  public void showDialog()
  {
    dialogeTextContainer.text = sentences[lineIndex]; //afiseaza linia de dialog curenta
    NPCNameContainer.text= NPCName;
    panel.SetActive(true);
    
  }
  
  public void ContinueDialog()
  {
    lineIndex++;
    showDialog();
  }
  
}

Apoi, in clasa NPC-ului, putem instantia un dialog personalizat pentru NPC-ul respectiv:

public class NPC : InteractionObject {

  public string[] sentences; //se pot configura liniile de dialog in editor
  public string name;
  
  public override void Interaction()
  {
    base.Interaction(); // se apeleaza metoda parinte

    DialogManager.Instance.AddNewDialog(sentences, name);
  }

Quest System

In ceea ce priveste quest-urile, sunt foarte multe posibilitati de abordare, dar in general implica urmatoarele elemente:

Astfel, o abordare sugerata este sa se abstractizeze o clasa de tip Quest, una de tip Goal (obiectiv) si una de tip Recompensa, intrucat exista multe tipuri in care se pot instantia aceste lucruri.

Exemple de tipuri de Obiective:

Exemple de tipuri de Recompense:

Exemplu de clasa generica de tip Quest

public class Quest : MonoBehaviour {

  public List<Goal> Goals = new List<Goal>();
  public List<Reward> Rewards = new List<Reward>();
  public bool completed;
  
  public void CheckGoals() {
    completed = Goals.All(g => g.completed); //questul este gata cand toate obiectivele sunt complete
  }
  
  public void GiveReward() {
    //in functie de tipul recompensei se adauga obiecte in inventar, sau se adauga experienta, skill points etc.
  }
  
  

}

Apoi, un exemplu de un quest concret.

public class RandomSlayThingsQuest : Quest {

  void Start()
  {
    QuestName = "Random Slayer";
    Description = "Kill some time";
    
    Goals.Add(new KillGoal( ...));
    Goals.Add(new KillGoal( ...));
    
    Rewards.Add(new ItemReward( ...));
    Rewards.Add(new ExperienceReward( ...));    
  
  }

}

Si un exemplu de Obiectiv

public class KillGoal : Goal {
  
  bool completed;
  int currentAmmount;
  
  public KillGoal(int enemyId, int ammount) {
    ...
  }
  
  public override void Init() {
    //listen to enemy death event
    EnemyManager.onDie += EnemyDied;
  }
  
  void EnemyDied(enemy) {
    this.currentAmmount++;
    if(this.currentAmmount >= ammount) {
      this.completed = true;
    }
  }
  
  
}

In ceea ce priveste cine gestioneaza questul, acesta este de obicei un NPC, deci putem extinde clasa NPC cu cateva lucuri specifice:

public class QuestNPC : NPC {
 

  public bool assigned;
  public Quest quest;
  
  public override void Interaction()
  {
    base.Interaction(); // se apeleaza metoda parinte

    if(!assigned) {
      //dialog
      //assign
    }
    
    void CheckQuest() {
      if(quest.completed) {
        quest.GiveReward();
      }
    }
  }
}

Cerinte

Realizarea unui joc 3D RPG 3rd Person 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
    5. poate consuma (de ex potiune de viata, mancare pentru viata) sau echipa elemente din inventar (sabie, torta, cutit, pistol etc)
    6. poate schimba arma de atac echipata (cel putin una melee si una ranges); in functie de asta va avea animatii diferite
    7. poate ataca automat un inamic atunci cand se da click pe el cu animatie
  3. Adaugati mai multe tipuri de obiecte interactionabile in scena (de ex de pickup, consumabile, arme)
  4. Adaugati unul sau mai multi NPC care:
    1. stiu sa converseze (text) prin raspunsuri la intrebari standard
    2. pot oferi un quest (quest-urile au obiective si recompense)
  5. Adaugati unul sau mai multi inamici in scena astfel incat:
    1. sa poata fi atacati atat melee (de cu sabie) cat si ranged (de ex arc / arma / proiectil)
    2. la apropierea jucatorului, intra in modul de atac
    3. au afisata o bara de viata deasupra lor in momentul in care intra in modul de atac
    4. sa fie animati
    5. sa se plimbe intr-o proximitate