Realizati o scena care sa contina:
Bonus:
todo: raycast pentru selectia obiectelor - point and click mers la ele
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, pot sa afisez informatii: de ex "Press E to use" { done = true; Interaction(); } } } }
La fel de bine aceste interactiuni pot fi detectate folosind sistemul de colizuni din Unity
Physics Events
/* Both objects have to have a Collider and one object has to have a Rigidbody for these Events to work */ private void OnCollisionEnter(Collision hit) { Debug.Log(gameObject.name + " just hit " + hit.gameObject.name); } private void OnCollisionStay(Collision hit) { Debug.Log(gameObject.name + " is hitting " + hit.gameObject.name); } private void OnCollisionExit(Collision hit) { Debug.Log(gameObject.name + " stopped hitting " + hit.gameObject.name); } /* Trigger must be checked on one of the Colliders */ private void OnTriggerEnter(Collider hit) { Debug.Log(gameObject.name + " just hit " + hit.name); } private void OnTriggerStay(Collider hit) { Debug.Log(gameObject.name + " is hitting " + hit.name); } private void OnTriggerExit(Collider hit) { Debug.Log(gameObject.name + " stopped hitting " + hit.name); } /* For 2D Colliders just add 2D to the Method name and the Parameter Type */ private void OnCollisionEnter2D(Collision2D hit) { } private void OnCollisionStay2D(Collision2D hit) { } private void OnCollisionExit2D(Collision2D hit) { } private void OnTriggerEnter2D(Collider2D hit) { } private void OnTriggerStay2D(Collider2D hit) { } private void OnTriggerExit2D(Collider2D hit) { }
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 ... PlayerManager.instance.score += value; //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); }
O problema in programarea interactiunilor este detectarea player-ului, in sensul de referinta. Astfel, avem mai multe variante:
public class PlayerManager : MonoBehaviour { public static PlayerManager instance; public GameObject player; void Awake() { instance = this; } }
Folosind varianta simpla cu singleton, putem lua pozitia player-ului de interes:
target = PlayerManager.instance.player.transform;
Astfel, putem efectua usor operatii care tin de player - de exemplu putem orienta inamicii sau un npc cu fata catre player, in momentul unei interactiuni.
//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); }
Astfel, pentru inamici putem defini un controller cu un radius de actiune, si un gizmos pentru vizualizare usoara a acestuia in editor.
public class EnemyController : MonoBehaviour { public float radius = 2; void OnDrawGizmosSelected() { Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, radius); } }
Diferenta este ca acesti agenti vor raspunde automat la anumite evenimente:
Pentru ca un inamic sa se miste spre player, atunci cand player-ul intra in raza de actiune putem folosi componenta de NavMeshAgent
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. }
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.
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 } }
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; }
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(); } } } }