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:
- broń biała (kij baseballowy),
- pistolet (Glock),
- karabin maszynowy (M4).
Wszystkie modele broni będą miały właściwości takie jak:
- rodzaj (typ) broni,
- zadawane obrażenia,
- prefab broni,
- współrzędne w której należy utworzyć broń,
- rotację w jakiej należy utworzyć broń,
- odpowiedni animator controller.
oraz dla broni dystansowych dodatkowo:
- prefab pocisku,
- zasięg,
- współrzędne miejsca wystrzału pocisku
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ć:
W projekcie jest dodanych parę rzeczy dlatego pozwolę sobie poświęcić chwilę na omówienie tego co znajduje się wewnątrz paczki.
Assety
W folderze z Assetami mamy foldery:
- Animations – zawiera pozę dla gracza oraz animacje i kontrolery dla każdego rodzaju broni,
- Graphics – znajdziemy tutaj wszystkie grafiki dla postaci, tła (https://opengameart.org/content/bevouliin-free-game-background-for-game-developers) oraz broni,
- Prefabs – zapisane już prefaby dla wszysktich elementów z folderu Graphics,
- Scenes – zapisane sceny, aktualnie tylko jedna scena Gameplay,
- Scripts – folder, w którym będziemy umieszczać skrypty. Aktualnie znajduje się w nim skrypt Constants, który zawiera stałe dla Animatora oraz PlayerBonesHandler, który przetrzymuje referencję do kości naszej postaci.
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
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:
- prefab pocisku,
- zasięg,
- współrzędne miejsca wystrzału pocisku.
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:
- GameObject bulletPrefab,
- float range,
- Vector3 gunPoint,
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)
Glock: pozycja (2.2, 1.1, 0) ; rotacja (0, 0, 22)
Rotacja: pozycja (-0.7, 0.05, 0) ; rotacja (0, 0, -13).
Teraz przejdźmy do folderu ScriptableObjects kliknijmy prawym przyciskiem myszy, wybierzmy opcję Create > Gameplay i tworzymy:
- model typu MeleeWeapon i nazywamy go BaseballModel,
- 2 modele typu AutomaticWeapon: GlockModel oraz M4Model.
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.
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.
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:
- int gunIndex – indeks obecnie trzymanej broni,
- GameObject currentWeapon – obiekt obecnie trzymanej broni,
- Weapon currentModel – tu przechowamy obecny model broni.
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:
- Materials – tutaj przenosimy przed chwilą stworzony materiał,
- Positions – to u nas to będzie tablica 2 elementów, które określą początek i koniec pocisku, czyli niejako jego długość. Ja proponuję wartości X zakresu -0.5 dla punktu 0 oraz 0.5 dla punktu 1,
- Width – tę wartość ustawiamy dowolnie jak nam pasuje, ja wybrałem 0.05,
- Color – póki co możemy zostawić kolor biały,
- Use Wrold Space – ten checkbox musi być ustawiony na false. Jeżeli checkbox będzie ustawiony na true zignorowana zostanie pozycja obiektu.
Dokumentacja: https://docs.unity3d.com/ScriptReference/LineRenderer-useWorldSpace.html
Tak przygotowany element możemy zapisać jako Prefab, a następnie przeciągnąć do modeli broni Glock oraz M4 w miejsce pola bulletPrefab.
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.
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.
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); } }
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.
W nowym skrypcie tworzymy zmienne typu float:
- publiczną zmienną range – tę zmienną będziemy ustawiać podczas tworzenia pocisku na wartość z modelu broni,
- publiczną zmienną direction – zmienna będzie określać kierunek pocisku,
- publiczną zmienną damage – ilość zadawanych obrażeń,
- prywatną zmienną speed – tutaj możemy wstawić dowolną wartość jaka nam odpowiada, ja ustawiłem wartość 50,
- prywatną zmienną startPositionX – startowa współrzędna X naszego pocisku.
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:
- Baseball:
- damage: 20
- Glock:
- damage: 30
- zasięg: 5
- M4:
- damage: 50
- zasięg: 10
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.
Tworzymy nowy skrypt MeleeWeaponHandler.cs. Tworzymy w nim:
- publiczna zmienną direction,
- funkcję CheckForHit, w której skorzystamy z funkcji Physics2D.OverlapCircle.
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.