This shows you the differences between two versions of the page.
pjv:laboratoare:07 [2019/01/16 12:05] alexandru.gradinaru |
pjv:laboratoare:07 [2019/10/02 12:57] (current) alexandru.gradinaru |
||
---|---|---|---|
Line 3: | Line 3: | ||
==== Agenti ==== | ==== Agenti ==== | ||
- | Pentru a programa inamici sau agenti NPC (Non-Playable Character) se poate folosi aceasi functionalitate de navigare automata (NavMesh) si componenta de tip NavMeshAgent, pentru navigatie, similar cu sectiunea de Navigare automata din laboratorul precedent. | + | Pentru a programa inamici sau agenti NPC (Non-Playable Character) se poate folosi aceeasi functionalitate de navigare automata (NavMesh) si componenta de tip NavMeshAgent, pentru navigatie, similar cu sectiunea de Navigare automata din laboratorul precedent. |
Diferenta este ca acesti agenti vor raspunde automat la anumite evenimente: | Diferenta este ca acesti agenti vor raspunde automat la anumite evenimente: | ||
Line 9: | Line 9: | ||
* NPC-urile interactoneaza la fel, bazate pe o raza de actiune sau efectiv interactiune directa (click) | * NPC-urile interactoneaza la fel, bazate pe o raza de actiune sau efectiv interactiune directa (click) | ||
- | ==== Inamici ==== | + | === Inamici === |
Astfel, pentru inamici putem defini un controller cu un radius de actiune, si un gizmos pentru vizualizare usoara a acestuia in editor. | Astfel, pentru inamici putem defini un controller cu un radius de actiune, si un gizmos pentru vizualizare usoara a acestuia in editor. | ||
Line 26: | Line 26: | ||
} | } | ||
</code> | </code> | ||
+ | |||
+ | {{ :pjv:laboratoare:enemy-radius.png?500 |}} | ||
+ | |||
Pentru ca un inamic sa se miste spre player, atunci cand player-ul intra in raza de actiune putem folosi componenta de NavMeshAgent | Pentru ca un inamic sa se miste spre player, atunci cand player-ul intra in raza de actiune putem folosi componenta de NavMeshAgent | ||
Line 37: | Line 40: | ||
{ | { | ||
//misca agentul pana la player | //misca agentul pana la player | ||
- | agent.SetDestination(hit.point); | + | agent.SetDestination(target.position); |
| | ||
//in momentul in care intra in raza de atac, ataca | //in momentul in care intra in raza de atac, ataca | ||
+ | } else { | ||
+ | // agentul se misca in treaba lui / patruleaza etc. | ||
} | } | ||
</code> | </code> | ||
Line 69: | Line 74: | ||
</code> | </code> | ||
- | <hidden> | + | O problema in activitatea agentilor este detectarea player-ului, in sensul de referinta. Astfel, avem mai multe variante: |
+ | * putem cauta un obiect dupa tag | ||
+ | * intr-o variabila target putem referentia direct player-ul (dar asta inseamna ca la fiecare agent trebuie mapat) | ||
+ | * putem folosi un singleton in care se tine referentiaza playerul si poate fi accesat de oriunde | ||
+ | |||
+ | <code> | ||
+ | public class PlayerManager : MonoBehaviour { | ||
+ | |||
+ | public static PlayerManager instance; | ||
+ | public GameObject player; | ||
+ | |||
+ | void Awake() | ||
+ | { | ||
+ | instance = this; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Folosind varianta simpla cu singleton, putem lua pozitia player-ului de inters, similar cu laboratorul precedent: | ||
+ | |||
+ | <code> | ||
+ | target = PlayerManager.instance.player.transform; | ||
+ | </code> | ||
+ | |||
+ | <code> | ||
+ | |||
+ | //Roteste cu 90 grade | ||
+ | void RotateN() { | ||
+ | Vector3 currentRotation = transform.rotation; | ||
+ | Vector3 wantedRotation = currentRotation * Quaternion.AngleAxis(-90, Vector3.up); | ||
+ | transform.rotation = Quaternion.Slerp(currentRotation, wantedRotation, Time.deltaTime * rotationSpeed); | ||
+ | } | ||
+ | //Roteste inamicul cu fata catre player | ||
+ | void FaceTarget () | ||
+ | { | ||
+ | Vector3 direction = (target.position - transform.position).normalized; | ||
+ | Quaternion lookRotation = Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z)); | ||
+ | transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * 5f); | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | === 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> | ||
+ | |||
+ | 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(); | ||
+ | } | ||
+ | | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | Apoi, in clasa NPC-ului, putem instantia un dialog personalizat pentru NPC-ul respectiv: | ||
+ | |||
+ | <code> | ||
+ | |||
+ | 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); | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | {{ :pjv:laboratoare:dialog.png?500 |}} | ||
+ | |||
+ | === Quest System === | ||
+ | |||
+ | In ceea ce priveste quest-urile, sunt foarte multe posibilitati de abordare, dar in general implica urmatoarele elemente: | ||
+ | * 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 Recompensa, intrucat exista multe tipuri in care se pot instantia aceste lucruri. | ||
+ | |||
+ | Exemple de tipuri de Obiective: | ||
+ | * Kill - kill a bunch of stuff | ||
+ | * Gather - gather stuff for me | ||
+ | * Deliver - deliver my live letter | ||
+ | * etc. | ||
+ | |||
+ | Exemple de tipuri de Recompense: | ||
+ | * items | ||
+ | * experience | ||
+ | * gold | ||
+ | * skill | ||
+ | * etc. | ||
+ | |||
+ | Exemplu de clasa generica de tip Quest | ||
+ | |||
+ | <code> | ||
+ | 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. | ||
+ | } | ||
+ | | ||
+ | | ||
+ | |||
+ | } | ||
+ | </code> | ||
+ | |||
+ | Apoi, un exemplu de un quest concret. | ||
+ | |||
+ | <code> | ||
+ | |||
+ | 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( ...)); | ||
+ | | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | Si un exemplu de Obiectiv | ||
+ | |||
+ | <code> | ||
+ | |||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | | ||
+ | | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | In ceea ce priveste cine gestioneaza questul, acesta este de obicei un NPC, deci putem extinde clasa NPC cu cateva lucuri specifice: | ||
+ | |||
+ | <code> | ||
+ | |||
+ | 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(); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | </code> | ||
+ | |||
+ | Gestiunea interfetei de quest pentru player, se poate face similar cu cea de inventar, prezentata in laboratorul precedent. | ||
+ | |||
+ | |||
+ | ==== Cerinte ==== | ||
+ | |||
+ | Realizarea unui joc 3D RPG | ||
+ | |||
- Adaugati unul sau mai multi NPC care: | - Adaugati unul sau mai multi NPC care: | ||
- stiu sa converseze (text) prin raspunsuri la intrebari standard | - stiu sa converseze (text) prin raspunsuri la intrebari standard | ||
- | - pot oferi un quest | + | - pot oferi un quest (quest-urile au obiective si recompense) |
- Adaugati unul sau mai multi inamici in scena scriptati astfel incat: | - Adaugati unul sau mai multi inamici in scena scriptati astfel incat: | ||
- sa fie animati | - sa fie animati | ||
- sa se plimbe intr-o proximitate | - sa se plimbe intr-o proximitate | ||
- | - la apropierea jucatorului, sa il atace | + | - la apropierea jucatorului, sa il atace |
+ | |||
+ | <hidden> | ||
+ | |||
+ | | ||
NPC | NPC | ||
Line 95: | Line 365: | ||
- | - | ||
- | - </hidden> | + | </hidden> |
<hidden> | <hidden> |