Marek Winiarski

Unity QuickTip #52.2 – Inwentarz z wykorzystaniem Scriptable Objects

Unity QuickTip – czyli szybkie porady, rozwiązania częstych problemów i sztuczki w Unity!

Dzisiejszy odcinek:  Scriptable Objects, część druga.

Jest to druga część poradnika dotyczącego Scriptable Objects. W tej części zobaczycie jak można wykorzystać to narzędzie w bardziej życiowym przykładzie. Będzie tu znacznie mniej omawiania teorii, więc aby dobrze zrozumieć czym są Scriptable Objects, polecam przeczytać najpierw część pierwszą poradnika. Tak jak to miało miejsce w przypadku poprzedniej części, ten odcinek powstał z gościnnym udziałem Marcina Sroczyńskiego, którego gorąco pozdrawiam i oddaje mu głos.

Wprowadzenie

W poprzednim artykule przedstawiłem krótko czym są ScriptableObjects. Dzisiaj chciałbym przedstawić bardziej konkretny przykład ich zastosowania. Stworzymy mini zestaw broni dla głównej postaci gry. Do dyspozycji gracza będą 3 bronie:

Wszystkie modele broni będą miały właściwości takie jak:

oraz dla broni dystansowych dodatkowo:

Skorzystamy też z prostego prefabu wroga żebyśmy mieli do kogo strzelać.

Przygotowanie

Zestaw wszystkich assetów potrzebnych do wykonania tego zadania można sobie pobrać:

Unity QucikTip 52.2 – Assety

W projekcie jest dodanych parę rzeczy dlatego pozwolę sobie poświęcić chwilę na omówienie tego co znajduje się wewnątrz paczki.

Scena z paczki

Assety

W folderze z Assetami mamy foldery:

Foldery, które pominąłem, są folderami związanymi z Anima2D, które importowane są automatycznie po dodaniu paczki z AssetSore’a.

Scena

Na scenie mamy 4 elementy. Kamerę, gracza, zombie – przeciwnika, oraz tło. Na kamerze nie mamy żadnych dodatkowych komponentów. Na tle znajduje się BoxCollider2D. Na przeciwniku mamy również BoxCollider2D oraz Rigidbody2D.

Do gracza dodane są komponenty BoxCollider2D, Rigidbody2D, Animator, PoseManager – związany z Anima2D oraz PlayerBonesHandler. Po rozwinięciu jego hierarchii widzimy bardzo dużo elementów. Połowa z nich to po prostu Sprite’y natomiast druga to rig postaci, zawierający komponenty kości. Mamy wykonany cały rig postaci w Anima2D, dzięki czemu mamy możliwość wykonywania bardzo fajnych animacji. Animacje dla gracza są już przygotowane, mamy chodzenie, oddychanie – Idle, oraz Atak dla każdego z 3 rodzajów broni.

Niestety w tym momencie nie mamy możliwości omawiania wykonywania animacji w Anima2D, natomiast jeżeli ktoś chciałby aby taki artykuł powstał nie ma problemu aby go przygotować, wystarczy napisać do Marka :)

Animacje

Animator

W zależności od tego jaki kontroler wstawimy do naszej postaci po uruchomieniu gry, będzie wykonywać się inna animacja – tj. z innym ustawieniem rąk. Ustawienia są już odpowiednio przystosowane do broni, które będziemy tam umieszczać.

Po kliknięciu przycisku Play możemy zaobserwować animację Idle gracza. Animacje chodzenia i ataku obsłużymy w dalszej części artykułu.

Obsługa Chodzenia

Na samym początku przygotujemy obsługę chodzenia. W folderze Scripts tworzymy skrypt PlayerController i od razu dodajemy go do postaci gracza na scenie (element Blondie w hierarchii).
Obsługujemy chodzenie gracza za pomocą przycisków A oraz D.

using UnityEngine;

public class PlayerController : MonoBehaviour {

    private float speed = 1.75f;

    void Update() {
        if (Input.GetKey(KeyCode.A)) {
            MoveLeft();
        } else if (Input.GetKey(KeyCode.D)) {
            MoveRight();
        }
    }

    private void MoveLeft() {
        this.transform.Translate(Vector3.left * Time.deltaTime * speed);
    }

    private void MoveRight() {
        this.transform.Translate(Vector3.right * Time.deltaTime * speed);
    }
}

Następnie obsłużymy animację chodzenia. Wystarczy że pobierzemy referencję do animatora znajdującego się na naszym graczu i ustawimy parametr Walk na true, podczas chodzenia oraz na false jeżeli żaden przycisk nie jest wciśnięty.

Zajmijmy się też od razu obróceniem gracza w zależności od tego w którą stronę się porusza.
Wystarczy, że zapiszemy początkową scalę obiektu i przy poruszania będziemy ustawiać współrzędną X na dodatnią jeżeli poruszamy się w prawą stronę oraz na ujemną jeżeli poruszamy się w lewą stronę.

using UnityEngine;

public class PlayerController : MonoBehaviour {

    private float speed = 1.75f;

    private Animator animator;

    private Vector3 startScale;

    void Start() {
        this.animator = this.GetComponent<Animator>();
        this.startScale = this.transform.localScale;
    }

    void Update() {
        if (Input.GetKey(KeyCode.A)) {
            MoveLeft();
        } else if (Input.GetKey(KeyCode.D)) {
            MoveRight();
        } else {
            StopMoving();
        }
    }

    private void MoveLeft() {
        this.transform.Translate(Vector3.left * Time.deltaTime * speed);
        this.animator.SetBool(Constants.WALK_PARAMETER, true);
        this.transform.localScale = new Vector3(
            -startScale.x,
            startScale.y,
            startScale.z
        );
    }

    private void MoveRight() {
        this.transform.Translate(Vector3.right * Time.deltaTime * speed);
        this.animator.SetBool(Constants.WALK_PARAMETER, true);
        this.transform.localScale = new Vector3(
            startScale.x,
            startScale.y,
            startScale.z
        );
    }

    private void StopMoving() {
        if (this.animator.GetBool(Constants.WALK_PARAMETER)) {
            this.animator.SetBool(Constants.WALK_PARAMETER, false);
        }
    }
}

Modele broni

Tak jak wspomniałem we wstępie wszystkie bronie będą miały część wspólnych właściwości, jednak nie wszystkie. Naturalnym wydaje się podział na bronie ręczne i dystansowe. Dlaczego taki podział? Możemy łatwo stwierdzić, że dla broni automatycznej będziemy potrzebować pól takich jak:

Dla broni ręcznych takie dane nie są nam potrzebne dlatego już na samym początku utworzymy klasę bazową dla broni ze wspólnymi polami. Z tej klasy dziedziczyć będą bronie automatyczne, które będą rozszerzać tę klasę o dodatkowe pola, oraz bronie ręczne, które nie będą rozszerzać bazowej funkcjonalności.

Tworzymy nowy skrypt z typem wyliczeniowym i nazywamy go WeaponType.cs. Wyróżnimy 2 typy: MELEE oraz, AUTOMATIC. W przyszłości moglibyśmy dodać nowe typy broni. Na myśl przychodzi na przykład broń rzucana typu noże, granaty etc.

public enum WeaponType {
    MELEE,
    AUTOMATIC
}

W folderze ScriptableObjects utwórzmy nowy skrypt i nazwijmy go Weapon. Istotne aby zamiast dziedziczyć z MonoBehaviour dziedziczył z klasy ScriptableObject.

using UnityEngine;

public class Weapon : ScriptableObject {

    public WeaponType weaponType;

    public GameObject weaponPrefab;
    public Vector3 weaponPosition;
    public Vector3 weaponRotation;
    public RuntimeAnimatorController animatorController;

    public float damage;

}

Tworzymy 2 nowe ScriptableObjects: MeleeWeapon, oraz AutomaticWeapon oba te skrypty muszą dziedziczyć z klasy Weapon. Do klasy MeleeWeapon nie dodajemy nowych pól, i dodajemy na górze atrybut:

[CreateAssetMenu(fileName = "MeleeWeapon", menuName = "Gameplay/MeleeWeapon", order = 1)]

Natomiast do klasy AutomaticWeapon dodajemy 3 pola:

oraz atrybut:

[CreateAssetMenu(fileName = "AutomaticWeapon", menuName = "Gameplay/AutomaticWeapon", order = 2)]
using UnityEngine;

[CreateAssetMenu(fileName = "MeleeWeapon", menuName = "Gameplay/MeleeWeapon", order = 1)]
public class MeleeWeapon : Weapon {

}
using UnityEngine;

[CreateAssetMenu(fileName = "AutomaticWeapon", menuName = "Gameplay/AutomaticWeapon", order = 2)]
public class AutomaticWeapon : Weapon {

    public GameObject bulletPrefab;
    public float range;
    public Vector3 gunPoint;

}

W ten sposób mamy stworzoną bazową klasę dla broni, a każdą z podklas możemy dowolnie rozszerzać wedle własnego uznania. Klasa broni ręcznej nie ma żadnych nowych atrybutów, ponieważ zwyczajnie nic ciekawego nie wpadło mi do głowy. Jedyne o czym pomyślałem to możliwość dodania specjalnego efektu, który tworzyłby się przy uderzeniu taką bronią, ale tym nie będziemy się dzisiaj zajmować. Do czego potrzebujemy konkretne pola wyjaśni się w dalszej części artykułu.

Nim utworzymy obiekty dla broni musimy sprawdzić w jakiej pozycji i rotacji będziemy musieli je umieścić w hierarchii gracza. Aby to zrobić uruchomimy grę i do komponentu Animator przenieśmy element BlondieBaseballAC. Postać powinna ustawić się w odpowiedniej pozycji. Bronie będziemy tworzyć jako dziecko elementu ArmL(Blondie > Root > Body > HandL > ArmL).

Przenieśmy teraz prefab broni Baseball jako dziecko elementu ArmL i ustawmy pozycję broni tak aby wyglądała ładnie. Dla mnie idealnie wydaje się pozycja (2.15, 2.65, 0) oraz rotacja (0, 0, 0).

To samo wykonujemy dla pozostałych broni. Zmieniamy animator w postaci odpowiednio na Glock lub M4 i ustawiamy pozycje.

Pozycje, które ja wybrałem:
Baseball pozycja (2.15, 2.65, 0) ; rotacja (0, 0, 0)

Ustawienia Kija

Glock: pozycja (2.2, 1.1, 0) ; rotacja (0, 0, 22)

Ustawienia pistoletu

Rotacja: pozycja (-0.7, 0.05, 0) ; rotacja (0, 0, -13).

Ustawienia karabinu

Teraz przejdźmy do folderu ScriptableObjects kliknijmy prawym przyciskiem myszy, wybierzmy opcję Create > Gameplay i tworzymy:

Do każdego modelu wprowadźmy typ broni, pozycję, rotację, którą przed chwilą sprawdziliśmy, prefab, oraz animatorController z którego korzystaliśmy. Pozostałe pola możemy na razie pozostawić z domyślnymi wartościami – zajmiemy się nimi później.

Gotowy model kija
Gotowy model pistoletu
Gotowy model karabinu

Tworzymy teraz pusty obiekt na scenie i nazywamy go WeaponModels. Do tego tworzymy skrypt WeaponModel.cs i dodajmy go do tego obiektu. W skrypcie tworzymy listę naszych broni.

using System.Collections.Generic;
using UnityEngine;

public class WeaponModel : MonoBehaviour {

    public List<Weapon> weaponModels;

}

Na scenie dodajemy do skryptu utworzone wcześniej modele broni. Z nich właśnie będziemy korzystać podczas tworzenia broni dla naszej postaci.

Uzupełniamy listę broni z poziomu edytora.

Teraz możemy zająć się stworzeniem broni dla gracza przy starcie gry. Tworzymy skrypt WeaponController.cs i dodajmy go jako komponent do naszego gracza. W samym skrypcie będziemy tworzyć instancje broni na podstawie przygotowanych modeli. Potrzebujemy referencję do skryptu z modelami (pamiętajmy o przeniesieniu referencji ze sceny), animatora na postaci gracza oraz skryptu zawierającego referencje do kości gracza.

using UnityEngine;

public class WeaponController : MonoBehaviour {

[SerializeField] public WeaponModel weaponModel;
    private PlayerBonesHandler playerBonesHandler;
    private Animator animator;

    void Start() {
        this.animator = this.GetComponent<Animator>();
        this.playerBonesHandler = this.GetComponent<PlayerBonesHandler>();
    }
}

Dodajemy 3 dodatkowe zmienne:

Tworzymy metodę SwitchWeapon, która zajmie się stworzeniem instancji broni (przyda nam też za chwilę podczas obsługi zmiany broni).

Sprawdzamy czy zmienna currentWeapon nie jest pusta. Jeżeli coś się w niej znajduje to znaczy że używamy już jakiejś broni i musimy ją najpierw usunąć przed wyekwipowaniem nowej. Tworzymy instancję nowej broni, wybierając jeden model z wcześniej utworzonej listy modeli.

Instancji ustawiamy rodzica jako ArmL. Przypisujemy pozycję, rotację broni, runtimeAnimatorController oraz ustawiamy skalę jako wektor jedynek. Pamiętajmy aby metodę SwitchWeapon wywołać w metodzie Start, oraz przypisać zmiennej gunIndex wartość od 0 do 2 (ponieważ mamy 3 bronie).

Po uruchomieniu gry w zależności od wartości zmiennej gunIndex stworzymy baseball, glock’a lub m4.

using UnityEngine;

public class WeaponController : MonoBehaviour {

    [SerializeField] public WeaponModel weaponModel;
    private PlayerBonesHandler playerBonesHandler;
    private Animator animator;

    private int gunIndex;
    private GameObject currentWeapon;
    private Weapon currentModel;

    void Start() {
        this.gunIndex = 1;
        this.animator = this.GetComponent<Animator>();
        this.playerBonesHandler = this.GetComponent<PlayerBonesHandler>();

        SwitchWeapon();
    }

    private void SwitchWeapon() {
        if (this.currentWeapon != null) {
            Destroy(this.currentWeapon);
        }

        currentModel = weaponModel.weaponModels[this.gunIndex];

        this.currentWeapon = Instantiate(currentModel.weaponPrefab);
        this.currentWeapon.transform.SetParent(this.playerBonesHandler.armL);
        this.currentWeapon.transform.localPosition = currentModel.weaponPosition;
        this.currentWeapon.transform.localRotation = Quaternion.Euler(currentModel.weaponRotation);
        this.currentWeapon.transform.localScale = Vector3.one;
        this.animator.runtimeAnimatorController = currentModel.animatorController;
    }
}

Ponieważ bardzo fajnie przygotowaliśmy metodę SwitchWeapon, możemy w łatwy sposób obsłużyć zmianę broni. Wystarczy że do skryptu WeaponController’a dodamy taką funkcję Update.

void Update() {
    if (Input.GetKeyDown(KeyCode.N)) {
        Debug.Log("NEXT WEAPON");
        this.gunIndex++;

        if (this.gunIndex > 2) {
            this.gunIndex = 0;
        }

        SwitchWeapon();

    } else if (Input.GetKeyDown(KeyCode.B)) {
        Debug.Log("PREVIOUS WEAPON");
        this.gunIndex--;

        if (this.gunIndex < 0) {
            this.gunIndex = 2;
        }

        SwitchWeapon();
    }
}

Zmiana broni odbywa się przy pomocy klawiszy N lub B. W zależności od tego czy wybieramy następną czy poprzednią broń odpowiednio inkrementujemy lub dekrementujemy zmienną. Pamiętamy o tym, że musimy obsłużyć przypadek wyjścia zmiennej poza zakres( gdy zmienna gunIndex, będzie mniejsza od 0 lub większa od 2). Po uruchomieniu rozgrywki powinniśmy mieć możliwość zmiany broni.

Pocisk

Pocisk broni utworzymy za pomocą komponentu Line Renderer. Linie będziemy przesuwać w kierunku wroga, jeżeli droga pokonana przez pocisk będzie dłuższa niż zasięg broni, będziemy niszczyć pocisk.

Na scenie tworzymy nowy obiekt, resetujemy Transform i dodajemy do niego komponent LineRenderer. W Assetach tworzymy też nowy folder o nazwie Materials i tworzymy w nim nowy materiał. Zmieńmy jego shader na Sprites/Default. Kolor polecam ustawić na biały, wtedy cała kontrola koloru odbędzie się przez komponent LineRenderer’a.

LineRenderer posiada wiele różnych opcji, nam potrzebne będzie 5:

Ustawienia LineRenderer’a

Tak przygotowany element możemy zapisać jako Prefab, a następnie przeciągnąć do modeli broni Glock oraz M4 w miejsce pola bulletPrefab.

Dodajemy prefab pocisku do pistoletu
Dodajemy prefab pocisku do karabinu

Strzelanie

Jak wcześniej wspomniałem będziemy potrzebować punkt startowy pocisku. Ze względu na różnorodność broni jakie możemy wprowadzić do gry (pistolety, karabiny, wyrzutnie rakiet), każda z broni będzie miała inny punkt w którym znajduje się lufa. Na szczęście dla nas to nie problem. Ze względu na tę potrzebę dodaliśmy pole gunPoint do skryptu AutomaticWeapon. Teraz wystarczy sprawdzić w jakich współrzędnych powinien stworzyć się pocisk.

Uruchamiamy grę, zmieniamy broń na Glock i jako dziecko broni, tworzymy pusty obiekt. Ustawiamy go tak, aby był na końcu lufy – to będzie nasz gunPoint. Operację powtarzamy dla broni M4.

Szukamy najlepszej pozycji dla tworzenia pocisku w pistolecie.
Szukamy najlepszej pozycji do tworzenia pocisku w karabinie.

Wybrane przeze mnie pozycje to (2.3, 0.6) dla Glocka oraz (5, 1) dla M4. Teraz przypisujemy te współrzędne do modeli broni.

Ustawiamy współrzędne tworzenia pocisku dla pistoletu
Ustawiamy współrzędne tworzenia pocisku dla karabinu

Do WeaponController’a w dodajemy metodę Shoot oraz w funkcji Update jej wywołanie po naciśnięciu LPM:

if (Input.GetMouseButtonDown(0)) {
    Shoot();
}

W metodzie shoot sprawdzamy jakiego typu broń trzymamy w ręce, ponieważ dla broni ręcznej nie będziemy tworzyć pocisku.

if (currentModel.weaponType == WeaponType.AUTOMATIC) {

} else {

}

Zostały tylko stworzenie instancji pocisku i przypisane pozycji w jakiej powinien się znaleźć.

Będziemy używać informacji z naszego modelu currentModel. O czym musimy jednak pamiętać to fakt, że model przechowujemy jako Weapon. Jeżeli chcemy korzystać z pól broni automatycznej musimy ten obiekt rzutować na obiekt tego typu, w innym wypadku pola bulletPrefab, czy gunPoint nie będą dostępne.

Pociski tworzymy nie przypisując żadnego rodzica. Przy tworzeniu pocisku pobieramy z niego komponent LindeRenderer i obliczamy jego długość – bulletLength. Następnie zapisujemy stronę w jaką jesteśmy zwróceni – bulletDirection. Obliczamy bulletOffset, który jest połową długości pocisku pomnożoną przez kierunek pocisku.

Aby obliczyć miejsce utworzenia pocisku używamy metodę TransformPoint. Być może nie wszyscy mieli okazję z tej funkcji skorzystać dlatego już tłumaczę czym ona się zajmuje. Funkcja TransformPoint pozwala przekonwertować punkt ze współrzędnych lokalnych obiektu do współrzędnych globalnych. W modelach broni zapisane mamy faktycznie lokalne pozycje końca lufy – ten punkt przekazujemy jako parametr funkcji. Funkcję wywołujemy na rzecz obiektu currentWeapon(obecna używana broń).

Podsumowując, sprawdzając pozycję lufy tworzyliśmy obiekt w lokalnych współrzędnych broni.
Wywołując metodę TransformPoint na rzecz broni (przekazując jako parametr te właśnie lokalne współrzędne) otrzymamy współrzędne globalne w jakich powinien pojawić się pocisk.

Na sam koniec ustawiamy parametr animatora na true, aby wykonać animację strzału lub uderzenia.

private void Shoot() {
    if (currentModel.weaponType == WeaponType.AUTOMATIC) {
        var bullet = Instantiate(((AutomaticWeapon)currentModel).bulletPrefab);

        var lineRenderer = bullet.GetComponent<LineRenderer>();

        var bulletDirection = (int)Mathf.Sign(this.transform.localScale.x);
        var bulletLength = lineRenderer.GetPosition(1).x - lineRenderer.GetPosition(0).x;
        var bulletOffset = bulletDirection * (bulletLength / 2.0f);

        var spawnPoint = this.currentWeapon.transform.TransformPoint(((AutomaticWeapon)currentModel).gunPoint);

        bullet.transform.position = new Vector3(
            spawnPoint.x + bulletOffset,
            spawnPoint.y,
            spawnPoint.z
        );
    } else {

    }

    this.animator.SetBool(Constants.SLASH_PARAMETER, true);
}

Aktualny skrypt WeaponController.

using UnityEngine;

public class WeaponController : MonoBehaviour {

    [SerializeField] public WeaponModel weaponModel;
    private PlayerBonesHandler playerBonesHandler;
    private Animator animator;

    private int gunIndex;
    private GameObject currentWeapon;
    private Weapon currentModel;

    void Start() {
        this.gunIndex = 0;
        this.animator = this.GetComponent<Animator>();
        this.playerBonesHandler = this.GetComponent<PlayerBonesHandler>();

        SwitchWeapon();
    }

    void Update() {
        if (Input.GetMouseButtonDown(0)) {
            Shoot();
        }

        if (Input.GetKeyDown(KeyCode.N)) {
            Debug.Log("NEXT WEAPON");
            this.gunIndex++;
            if (this.gunIndex > 2) {
                this.gunIndex = 0;
            }
            SwitchWeapon();
        } else if (Input.GetKeyDown(KeyCode.B)) {
            Debug.Log("PREVIOUS WEAPON");
            this.gunIndex--;
            if (this.gunIndex < 0) {
                this.gunIndex = 2;
            }
            SwitchWeapon();
        }
    }

    private void SwitchWeapon() {
        if (this.currentWeapon != null) {
            Destroy(this.currentWeapon);
        }

        currentModel = weaponModel.weaponModels[this.gunIndex];

        this.currentWeapon = Instantiate(currentModel.weaponPrefab);
        this.currentWeapon.transform.SetParent(this.playerBonesHandler.armL);
        this.currentWeapon.transform.localPosition = currentModel.weaponPosition;
        this.currentWeapon.transform.localRotation = Quaternion.Euler(currentModel.weaponRotation);
        this.currentWeapon.transform.localScale = Vector3.one;
        this.animator.runtimeAnimatorController = currentModel.animatorController;
    }

    private void Shoot() {
        if (currentModel.weaponType == WeaponType.AUTOMATIC) {
            var bullet = Instantiate(((AutomaticWeapon)currentModel).bulletPrefab);

            var lineRenderer = bullet.GetComponent<LineRenderer>();
            var bulletLength = lineRenderer.GetPosition(1).x - lineRenderer.GetPosition(0).x;

            var bulletDirection = (int)Mathf.Sign(this.transform.localScale.x);
            var bulletOffset = bulletDirection * (bulletLength / 2.0f);

            var spawnPoint = this.currentWeapon.transform.TransformPoint(((AutomaticWeapon)currentModel).gunPoint);

            bullet.transform.position = new Vector3(
                spawnPoint.x + bulletOffset,
                spawnPoint.y,
                spawnPoint.z
            );
        } else {

        }
        this.animator.SetBool(Constants.SLASH_PARAMETER, true);
    }
}

 

Pocisk z pistoletu
Pocisk z karabinu

Po naciśnięciu LMP powinien pojawić się pocisk.

Obsługa pocisku

Tworzymy nowy skrypt BulletHandler i dodajmy go do prefabu pocisku. Do samego pocisku dodajmy też BoxCollider2D z checkboxem IsTrigger ustawioną na true.

Ustawienia pocisku

W nowym skrypcie tworzymy zmienne typu float:

Osoby, które będą chciałby dalej bawić się po przeczytaniu całego artykułu mogą zmienić zmienną speed na publiczną i dodać nowe pole do modelu broni np. bulletSpeed, wówczas każda broń może mieć inną prędkość pocisku.

W funkcji Start zapisujemy początkową współrzędną X pocisku. W funkcji Update przesuwamy pocisk dopóki różnica między obecną, a startową pozycją jest mniejsza od zasięgu pocisku (range) – pamiętamy o wartości bezwzględnej(aby zachować działanie dla pocisków lecących w obie strony). Jeżeli pocisk przekroczy wartość range, wówczas zostanie zniszczony.

using UnityEngine;

public class BulletHandler : MonoBehaviour {

    [HideInInspector]
    public float range;
    [HideInInspector]
    public float damage;
    [HideInInspector]
    public int direction;

    private float speed = 50.0f;
    private float startPositionX;

    void Start() {
        this.startPositionX = this.transform.position.x;
    }

    void Update () {
        if (Mathf.Abs(this.transform.position.x - this.startPositionX) < range) {
            this.transform.Translate(Vector3.right * direction * speed * Time.deltaTime);
        } else {
            Destroy(this.gameObject);
        }
    }
}

W skrypcie WeaponController w metodzie Shoot wewnątrz instrukcji if dodajemy linijki:

bullet.GetComponent<BulletHandler>().range = ((AutomaticWeapon)currentModel).range;
bullet.GetComponent<BulletHandler>().damage = ((AutomaticWeapon)currentModel).damage;
bullet.GetComponent<BulletHandler>().direction = (int)Mathf.Sign(this.transform.localScale.x);

Dzięki temu przy tworzeniu pocisku ustawimy jego zasięg, zadawane obrażenia oraz kierunek lotu.

private void Shoot() {
    if (currentModel.weaponType == WeaponType.AUTOMATIC) {
        var bullet = Instantiate(((AutomaticWeapon)currentModel).bulletPrefab);

        var lineRenderer = bullet.GetComponent<LineRenderer>();
        var bulletLength = lineRenderer.GetPosition(1).x - lineRenderer.GetPosition(0).x;

        var bulletDirection = (int)Mathf.Sign(this.transform.localScale.x);
        var bulletOffset = bulletDirection * (bulletLength / 2.0f);

        var spawnPoint = this.currentWeapon.transform.TransformPoint(((AutomaticWeapon)currentModel).gunPoint);

        bullet.transform.position = new Vector3(
            spawnPoint.x + bulletOffset,
            spawnPoint.y,
            spawnPoint.z
        );

        var bulletHandler = bullet.GetComponent<BulletHandler>();
        bulletHandler.range = ((AutomaticWeapon)currentModel).range;
        bulletHandler.damage = ((AutomaticWeapon)currentModel).damage;
        bulletHandler.direction = (int)Mathf.Sign(this.transform.localScale.x);
    } else {

    }
    this.animator.SetBool(Constants.SLASH_PARAMETER, true);
}

Na sam koniec musimy ustawić odpowiednie wartości zasięgu i obrażeń w modelach broni. Moje wartości:

Finalne ustawienia kija
Finalne ustawienia pistoletu
Finalne ustawienia karabinu

W tym momencie możemy przetestować latanie pocisków. Strzały z Glocka powinny być usuwane szybciej niż strzały z M4.

Przeciwnik

Na początku upewnijmy się, że Tag przeciwnika Frankie jest ustawiony jako “Enemy”, oraz że ma wstawiony komponent BoxCollider2D z checkboxem IsTrigger ustawionym na false. (Wszystko powinno być odpowiednio ustawione po pobraniu bazowego projektu)

Tworzymy skrypt EnemyController.cs i dodajemy go do obiektu Frankie na scenie. W skrypcie dodajemy tylko jedną metodę, która wypisze nam ilość zadanych obrażeń.

using UnityEngine;

public class EnemyController : MonoBehaviour {

    public void DealDamage(float damageAmount) {
        Debug.Log(damageAmount + " was dealt to Enemy");
    }
}

W skrypcie BulletHandler dodajemy funkcję OnTriggerEnter2D. W funkcji OnTriggerEnter2D sprawdzamy czy tag obiektu, który trafiliśmy to “Enemy” jeżeli tak, z komponentu EnemyController wywołujemy funkcję DealDamage z parametrem damage i usuwamy pocisk.

using UnityEngine;

public class BulletHandler : MonoBehaviour {

    [HideInInspector]
    public float range;
    [HideInInspector]
    public float damage;
    [HideInInspector]
    public int direction;

    private float speed = 50.0f;
    private float startPositionX;

    void Start() {
        this.startPositionX = this.transform.position.x;
    }

    void Update() {
        if (Mathf.Abs(this.transform.position.x - this.startPositionX) < this.range) {
            this.transform.Translate(Vector3.right * this.direction * this.speed * Time.deltaTime);
        } else {
            Destroy(this.gameObject);
        }
    }

    void OnTriggerEnter2D(Collider2D col) {
        if (col.gameObject.CompareTag("Enemy")) {
            col.gameObject.GetComponent<EnemyController>().DealDamage(this.damage);
            Destroy(this.gameObject);
        }
    }
}

Broń ręczna

Obsługę broni automatycznych mamy zakończoną, zostało nam jeszcze obsłużenie broni ręcznej.

Upewniamy się, że nasz wróg ma utworzoną i przypisaną warstwę (Layer) Enemy.

Sprawdzamy czy przeciwnik ma ustawioną odpowiednią warstwę.

Tworzymy nowy skrypt MeleeWeaponHandler.cs. Tworzymy w nim:

Funkcja ta pozwala nam sprawdzić, czy w zadanym promieniu od konkretnego punktu znajduje się obiekt na danej warstwie. Jeżeli trafimy na taki obiekt, zadajemy wrogowi obrażenia i dodajemy siłę aby go nieco przesunąć w kierunku uderzenia

Dokumentacja: Physics2D.OverlapCircle

using UnityEngine;

public class MeleeWeaponHandler : MonoBehaviour {

    [HideInInspector]
    public float damage;
    [HideInInspector]
    public int direction;

    public void CheckForHit() {
        var col = Physics2D.OverlapCircle(this.transform.position, 0.65f, 1 << LayerMask.NameToLayer("Enemy"));
        if (col != null) {
            col.gameObject.GetComponent<EnemyController>().DealDamage(this.damage);
            col.gameObject.GetComponent<Rigidbody2D>().AddForce(Vector2.right * this.direction * 2.0f, ForceMode2D.Impulse);
        }
    }
}

Do prefabu Baseball dodajemy utworzony skrypt oraz sprawdzamy czy mamy dodany BoxCollider2D z checkboxem IsTrigger na true.

W klasie WeaponController tworzymy koorutynę Delayhit. Koorutyna ma za zadanie poczekać na wykonanie animacji zamachu a dopiero następnie wykonać metodę CheckForHit.

private IEnumerator DelayHit() {
    yield return new WaitForSeconds(0.25f);

    var meleeWeaponHandler = this.currentWeapon.GetComponent<MeleeWeaponHandler>();
    meleeWeaponHandler.direction = (int)Mathf.Sign(this.transform.localScale.x);
    meleeWeaponHandler.damage = this.currentModel.damage;
    meleeWeaponHandler.CheckForHit();
}

Następnie wywołujemy koorutynę w metodzie Shoot.

if(...){
   (...)
} else {
    StartCoroutine(DelayHit());
}

W pierwszej linijce metody SwitchWeapon dodajmy też linijkę:

StopAllCoroutines();

Zabezpieczy ona nas przed błędami przy zmianie broni podczas uderzenia z baseball’a.

Gotowy skrypt WeaponController

using System.Collections;
using UnityEngine;

public class WeaponController : MonoBehaviour {

	[SerializeField] public WeaponModel weaponModel;
	private PlayerBonesHandler playerBonesHandler;
	private Animator animator;

	private int gunIndex;
	private GameObject currentWeapon;
	private Weapon currentModel;

	void Start() {
		this.gunIndex = 0;
		this.animator = this.GetComponent<Animator>();
		this.playerBonesHandler = this.GetComponent<PlayerBonesHandler>();

		SwitchWeapon();
	}

	void Update() {
		if (Input.GetMouseButtonDown(0)) {
			Shoot();
		}

		if (Input.GetKeyDown(KeyCode.N)) {
			Debug.Log("NEXT WEAPON");
			this.gunIndex++;
			if (this.gunIndex > 2) {
				this.gunIndex = 0;
			}
			SwitchWeapon();
		} else if (Input.GetKeyDown(KeyCode.B)) {
			Debug.Log("PREVIOUS WEAPON");
			this.gunIndex--;
			if (this.gunIndex < 0) {
				this.gunIndex = 2;
			}
			SwitchWeapon();
		}
	}

	private void SwitchWeapon() {
		StopAllCoroutines();
		if (this.currentWeapon != null) {
			Destroy(this.currentWeapon);
		}

		currentModel = weaponModel.weaponModels[this.gunIndex];

		this.currentWeapon = Instantiate(currentModel.weaponPrefab);
		this.currentWeapon.transform.SetParent(this.playerBonesHandler.armL);
		this.currentWeapon.transform.localPosition = currentModel.weaponPosition;
		this.currentWeapon.transform.localRotation = Quaternion.Euler(currentModel.weaponRotation);
		this.currentWeapon.transform.localScale = Vector3.one;
		this.animator.runtimeAnimatorController = currentModel.animatorController;
	}

	private void Shoot() {
		if (currentModel.weaponType == WeaponType.AUTOMATIC) {
			var bullet = Instantiate(((AutomaticWeapon)currentModel).bulletPrefab);

			var lineRenderer = bullet.GetComponent<LineRenderer>();
			var bulletLength = lineRenderer.GetPosition(1).x - lineRenderer.GetPosition(0).x;

			var bulletDirection = (int)Mathf.Sign(this.transform.localScale.x);
			var bulletOffset = bulletDirection * (bulletLength / 2.0f);

			var spawnPoint = this.currentWeapon.transform.TransformPoint(((AutomaticWeapon)currentModel).gunPoint);

			bullet.transform.position = new Vector3(
			spawnPoint.x + bulletOffset,
			spawnPoint.y,
			spawnPoint.z
			);

			var bulletHandler = bullet.GetComponent<BulletHandler>();
			bulletHandler.range = ((AutomaticWeapon)currentModel).range;
			bulletHandler.damage = ((AutomaticWeapon)currentModel).damage;
			bulletHandler.direction = (int)Mathf.Sign(this.transform.localScale.x);
		} else {
			StartCoroutine(DelayHit());
		}
		this.animator.SetBool(Constants.SLASH_PARAMETER, true);
	}

	private IEnumerator DelayHit() {
		yield return new WaitForSeconds(0.25f);

		var meleeWeaponHandler = this.currentWeapon.GetComponent<MeleeWeaponHandler>();
		meleeWeaponHandler.direction = (int)Mathf.Sign(this.transform.localScale.x);
		meleeWeaponHandler.damage = this.currentModel.damage;
		meleeWeaponHandler.CheckForHit();
	}
}

Podsumowanie

Jak widać w powyższym przykładzie ScriptableObject wprowadzają nam duży porządek w warstwie danych naszego projektu. Dzięki nim możemy dodawać nowe, usuwać stare, czy edytować obecne elementy bez żadnych problemów- wszystko jest jasne i czytelne.Ponadto w przypadku chęci dodania nowych funkcjonalności nic nie stoi na przeszkodzie żeby dodać nowe pola.

Scriptable Objects mogą mieć bardzo wiele zastosowań, jednak niewątpliwie największym udogodnieniem będą w grach z rozbudowanym systemem różnego rodzaju ekwipunku i prawdopodobnie tam będziemy spotykać się z nimi najczęściej.

Exit mobile version