This shows you the differences between two versions of the page.
pjv:laboratoare:07 [2018/10/11 01:49] 127.0.0.1 external edit |
pjv:laboratoare:07 [2019/10/02 12:57] (current) alexandru.gradinaru |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Laboratorul 07. ===== | + | ===== Mecanici avansate in 3D ===== |
+ | ==== Agenti ==== | ||
+ | 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: | ||
+ | * inamicii de obicei incep sa interactioneze atunci cand player-ul intra intr-o anumita raza de actiune | ||
+ | * NPC-urile interactoneaza la fel, bazate pe o raza de actiune sau efectiv interactiune directa (click) | ||
+ | |||
+ | === Inamici === | ||
+ | |||
+ | Astfel, pentru inamici putem defini un controller cu un radius de actiune, si un gizmos pentru vizualizare usoara a acestuia in editor. | ||
+ | |||
+ | <code> | ||
+ | public class EnemyController : MonoBehaviour { | ||
+ | |||
+ | public float radius = 2; | ||
+ | | ||
+ | void OnDrawGizmosSelected() { | ||
+ | Gizmos.color = Color.red; | ||
+ | Gizmos.DrawWireSphere(transform.position, radius); | ||
+ | } | ||
+ | |||
+ | |||
+ | } | ||
+ | </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 | ||
+ | |||
+ | <code> | ||
+ | |||
+ | void Update() { | ||
+ | //calculam distanta intre player si inamic | ||
+ | float distance = Vector3.Distance(target.position, transform.position); | ||
+ | |||
+ | if(distance <= radius) | ||
+ | { | ||
+ | //misca agentul pana la player | ||
+ | agent.SetDestination(target.position); | ||
+ | | ||
+ | //in momentul in care intra in raza de atac, ataca | ||
+ | |||
+ | } else { | ||
+ | // agentul se misca in treaba lui / patruleaza etc. | ||
+ | } | ||
+ | </code> | ||
+ | | ||
+ | Mai departe, se poate folosi o alta distanta, pentru a determina raza de atac. Un inamic poate avea atac melee (de aproape) sau de la o anumita distanta. | ||
+ | |||
+ | <code> | ||
+ | |||
+ | public float attackRadius = 1; | ||
+ | |||
+ | void Update() { | ||
+ | //calculam distanta intre player si inamic | ||
+ | float distance = Vector3.Distance(target.position, transform.position); | ||
+ | |||
+ | if(distance <= radius) | ||
+ | { | ||
+ | //misca agentul pana la player | ||
+ | agent.SetDestination(hit.point); | ||
+ | | ||
+ | //in momentul in care intra in raza de atac, ataca | ||
+ | if(distance <= attack) | ||
+ | { | ||
+ | //ataca | ||
+ | } | ||
+ | |||
+ | } | ||
+ | | ||
+ | </code> | ||
+ | |||
+ | 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: | ||
+ | - stiu sa converseze (text) prin raspunsuri la intrebari standard | ||
+ | - pot oferi un quest (quest-urile au obiective si recompense) | ||
+ | - Adaugati unul sau mai multi inamici in scena scriptati astfel incat: | ||
+ | - sa fie animati | ||
+ | - sa se plimbe intr-o proximitate | ||
+ | - la apropierea jucatorului, sa il atace | ||
+ | |||
+ | <hidden> | ||
+ | | ||
+ | | ||
+ | |||
+ | NPC | ||
+ | Agenti de la care se pot prelua/rezolva quest-uri | ||
+ | Sunt animati | ||
+ | Pot purta o conversatie cu mai multe fire de dialog si mai multe variante de raspuns | ||
+ | </hidden> | ||
+ | |||
+ | <hidden> | ||
+ | - poate schimba arma de atac echipata | ||
+ | - poate consulta quest-ul curent | ||
+ | - poate ataca | ||
+ | - are experienta si level-uri | ||
+ | - in functie de level poata adauga puncte intr-un skilltree | ||
+ | - poate folosi skill-uri/abilitati speciale | ||
+ | | ||
+ | |||
+ | - | ||
+ | </hidden> | ||
+ | |||
+ | <hidden> | ||
+ | (vezi demo UI de la 3dpub) | ||
+ | - coliziuni, proiectile | ||
+ | - interfata grafica pt un fps | ||
+ | - Managing weapons, Navmeshes, navmesh Agents, AI Auto-Attack | ||
+ | - animatie caractere atac, arma, coliziuni atac | ||
+ | |||
+ | 7: 3d RPG inventory system, quest system, skill system, pathfinding,Grid Snapping, Managing weapons, Navmeshes, navmesh Agents, AI Auto-Attack | ||
+ | |||
+ | QuestLog | ||
+ | |||
+ | playerul | ||
+ | are abilitati speciale care consuma mana/puncte de abilitate | ||
+ | poate prelua unul sau mai multe quest-uri | ||
+ | poate cara obiecte (iau un cub dintr-o parte si il pun in alta, ca sa sar pe el) | ||
+ | poate interactiona cu NPC (botsi) pentru preluare de quest-uri | ||
+ | are un scor in functie de numarul de quest-uri rezolvate | ||
+ | este animat (atac, miscare) | ||
+ | |||
+ | |||
+ | |||
+ | </hidden> |