Interakce s předměty. Log #17 (Unity, C#)
18.03.2018

    Zbraň už mám vytvořenou, ale aby s ní hráč mohl ve hře pracovat - zvednout ji, vložit do inventáře, nebo z inventáře vyhodit, je potřeba jí tohle chování naskriptovat. Taky bude potřeba nastavit fyziku předmětů, aby dokázaly reagovat na prostředí. Jednoduše se z předmětu vytvoří prefab a veškeré vlastnosti se přidají tomuto prefabu. Pro načtení předmětu do hry se bude využívát právě tohoto prefabu, na který je uložena reference v mém ScriptableObjectu.

    Prvním krokem je tedy vytvoření ScriptableObjectu mé sekery. Ve složce Editor si vytvořím třídu WeaponAsset, která mi vytvoří menu prvek Weapon v kontextovém menu. Takže pokud v adresáři ObjectsData/Items/Weapons kliknu pravým tak pod Create > Item > Weapon vytvořím novou asset položku zbraně. Tu pojmenuji Axe a vyplním pole pro jméno, popis, a do políčka Item Icon přetáhnu moji ikonu vytvořenou v minulém logu a už jen nastavím ostatní vlastnosti. Políčko Item Prefab zatím nechám prázdné, na prefabu budu následně pracovat. Tady je ještě jednou jak vypadá má asset třída.

 

using UnityEditor;

public class WeaponAsset
{
    [MenuItem("Assets/Create/Item/Weapon")]
    public static void CreateAsset()
    {
        ScriptableObjectUtility.CreateAsset<Weapon>();
    }
}

 

    Ta mi prozatím postačí pro jakoukoliv zbraň. Teď se vrátím k prefabu, prefab je asset který ukládá objekt i s nastavením komponent, to budu rozhodně potřebovat u mých předmětů, každý totiž bude mít fyziku a skript pro interakci. Začnu tedy s tím jednodušším, s fyzikou. Co tím tedy mám na mysli? Fyzika předmětu se postará o to, že předmět může ležet na zemi, nebo jiném objektu a nebude propadávat či levitovat. Sekeře přidám komponentu Mesh Collider, která se postará o řešení kolizí s ostatními collidery. Jediné co je třeba nastavit, aby byl konvexní, tím zjednoduším nároky na výpočet collideru a samotný mesh collideru. Pak přidám komponentu Rigid Body, tahle se stará o to že předmět nebude levitovat, bude na něj tedy působit gravitace. Tady není zatím potřeba měnit žádné nastavení. Když teď hru pustím, má sekera spadne na zem, to vypadá dobře.

    Teď k samotné interakci. Na venek bude fungovat tak, že když na objekt hráč klikne, postava k němu nejprve přijde, pak předmět zvedne (bez animace) a ten se přidá do inventáře. Vnitřně, teda celé chování a skript, se už bude chovat složitěji. První věcí je teda rozeznat na co hráč kliknul a podle toho se zachovat, je totiž rozdíl jestli hráč kliknul na NPC, nepřítele, nebo na předmět. Když poznám že šlo o předmět, musím nejdříve hráče poslat k předmětu, pak ho teprev zvednout. Když předmět zvednu, znamená to, že musí zmizet ze scény, ale protože potřebuji pořád vědět co jsem zvednul, musím data předmětu někam uložit. K tomu použiji můj ScriptableObject kontejner. Data si budu teda uchovávat po celou dobu existence předmětu a když jej budu vytvářet zase tyto data využiji.

    Začnu tím, že vytvořím třídu Interactable. V ní přidám virtualní metodu Awake() a v této metodě jen nastavím vrstvu na interactable (v mém případě vrstva číslo 11) pomocí gameObject.layer = 11. A ještě přidám virtuální metodu Interact(GameObject) kterou nechám prázdnou. Virtuální proto, aby bylo možné je přepsat v třídách které budou z této třídy dědit. Pak ve třídě PlayerController přidám podmínku, když hráč klikne na objekt s interactable vrtsvou (číslo 11). Tu přiřazuji do veřejné proměnné LayerMask interactionMask v inspektoru komponenty PlayerController

 

using UnityEngine;

public class Interactable : MonoBehaviour
{
    protected virtual void Awake()
    {
        gameObject.layer = 11;
    }

    public virtual void Interact(GameObject character){}
}

 

    Dalším krokem je vytvoření třídy ItemInteraction, která se stará o samotnou interakci s předmětem a říká jak se má objekt po interakci zachovat. V případě předmětu ho prostě postava zvedne, tím se objekt předmětu zničí a přidá se do inventáře (to bude v následujícím logu). Musí ovšem dědit z třídy Interactable. Na začátku třídy tedy bude proměnná Item itemData, uchovávající ScriptableObject a tím data předmětu. A pak metoda Awake() s klíčovým slovem new a metoda Interact(GameObject) s klíčovým slovem pro přetěžování, override. Ve funkci Interact(GameObject) zatím jen zavolám funkci Destroy(GameObject) a v Awake() funkci base.Awake() pro zavolání funkce nadřazené třídy. 

 

using UnityEngine;

public class ItemInteraction : Interactable {

    public Item itemData;

    protected new void Awake()
    {
        base.Awake();
    }

    public override void Interact(GameObject character)
    {
        Destroy(gameObject);
    }
}

 

    Nyní přichází trošku komplikovanější část, jelikož musím vyřešit, aby hráč první přišel k předmětu a až pak s ním reagoval. Využiju událostí a delegáta, které mi řeknou, až hráč dorazí na hráčem zvolenou pozici. Instanci delegáta události budu přidávat jen v případě, že kliknu na interactable objekt. V případě že kliknu kamkoliv jinam, je potřeba instanci odebrat, to mi zabrání tomu, že když kliknu na předmět a pak kliknu jinam, tak se mi nevyvolá interakce až dorazí na místo. Takže abych věděl, že hráč dorazil na místo, vytvořím delegáta a událost ve třídě PlayerMotor, která se mi stará o pohyb postavy. Nad třídu tedy přidám řádek public delegate void DestinationReached(), tím deklaruji nového delegáta, ve třídě pak vytvořím událost s typem mého delegáta public event DestinationReached Reached. To je událost která se vyvolá na místě které musím ještě definovat. Ve třídě PlayerMotor je v Update() metodě podmínka, která je splněna v případě že hráč dorazil na pozici, tam vyvolám moji událost zavoláním funkce události, Reached(). Jakmile je událost vyvolána, tak se zavolají všechny funkce (posluchači), které jsem registroval za pomoci delegáta.

 

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;

public delegate void DestinationReached();

[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(PlayerController))]
public class PlayerMotor : MonoBehaviour
{
    public event DestinationReached Reached;

    NavMeshAgent agent;
    bool pending;
    Vector3 target;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
    }

    public void MoveToObject(Vector3 position)
    {
        GetComponent<CharacterAnimation>().StopAllAnimations();
        pending = true;
        agent.SetDestination(position);
    }

    void Update()
    {
        if (pending && agent.remainingDistance <= agent.stoppingDistance)
        {
            if (Reached != null) Reached();
            pending = false;
        }
    }
}

 

    Zpět ke třídě PlayerController, kde si referenci na komponentu Interactable kliknutého objekt uložím v podmínce pro moji interactionMask, lokálně do proměnné Interactable _object. Pak přidám instanci delegáta do seznamu delegátů pro moji událost, řeknu postavě ať jde k předmětu a nakonec si uložím instanci kliknutého objektu. Do podmínky pro movementMask jen přidám řádek kde delegáta odebírám, takže pokud kliknu jinam, interakce se nevyvolá. Teď přidám funkci Interact() nad funkci Update(), ve které vyvolám na můj kliknutý předmět funkci Interact(GameObject), a hned potom musím instanci delegáta odebrat.

 

using UnityEngine;
using UnityEngine.AI;
using UnityEngine.EventSystems;

[RequireComponent(typeof(NavMeshAgent))]
public class PlayerController : MonoBehaviour
{
    public LayerMask movementMask;      // The ground
    public LayerMask interactionMask;   // Everything we can interact with
    public float mouseClickDistance;
    PlayerMotor motor;      // Reference to our motor
    Camera cam;             // Reference to our camera
    RaycastHit hit;

    // Get references
    void Start()
    {
        motor = GetComponent<PlayerMotor>();
        cam = Camera.main;
    }

    void Interact()
    {
        hit.transform.GetComponent<Interactable>().Interact(gameObject);
        GetComponent<PlayerMotor>().Reached -= new DestinationReached(Interact);
    }

    // Update is called once per frame
    void Update()
    {
        // If we press left mouse
        if (Input.GetMouseButtonDown(0))
        {
            // Shoot out a ray
            Ray ray = cam.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;

            // If we hit walkable layer
            if (Physics.Raycast(ray, out hit, mouseClickDistance, movementMask))
            {
                GetComponent<PlayerMotor>().Reached -= new DestinationReached(Interact);
                motor.MoveToObject(hit.point);
            }

            // If we hit interactable layer
            if (Physics.Raycast(ray, out hit, mouseClickDistance, interactionMask))
            {
                Interactable _object = hit.transform.GetComponent<Interactable>();
                GetComponent<PlayerMotor>().Reached += new DestinationReached(Interact);
                motor.MoveToObject(_object.interactionPoint.position);
                this.hit = hit;
            }
        }
    }
}

 

    Tím je interakce hotová, jen přetáhnu skript ItemInteraction na můj předmět (prefab), do ScriptableObjectu do pole Item Prefab vložím moji sekeru (prefab) a do pole Item Data v komponentě Item Interaction přetáhnu můj ScriptableObject a hotový prefab uložím do adresáře Resources/Objects/Items/Weapons.

 

 

    Zbraň teď umím zvednout, ale ve skutečnosti mi jen zmizí ze scény. V příštím logu to napravím, přidám tento objekt na seznam do inventáře.