Table of Contents

Programarea obiectelor

Cerinte

Pentru simplitate, puteti porni de la scena de MOBA creata in Lab3, dar nu este obligatoriu

Realizati o scena care sa contina:

Bonus:

Documentatie video


Documentatie extinsa text


Sumar documentatie

todo: raycast pentru selectia obiectelor - point and click mers la ele

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.

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);
}	

Referinta globala la player

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);
	}

Inamici

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;
}

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