Unity3d QuickTip – czyli szybkie porady, rozwiązania częstych problemów i sztuczki w Unity3d!
Dzisiejszy odcinek: Drzewko umiejętności postaci
Uwaga! Jest to poradnik typu QuickTip. Zatem skupia się on na osiągnięciu założonego celu. Zatem zakładamy że użytkownik zna na tyle program Unity3d, aby samodzielnie wykonać najprostsze czynności, jak np. dodanie modelu kostki do sceny czy dodanie modelowi jakiegoś komponentu. Jeżeli brakuje Ci tej podstawowej wiedzy, zapraszam do tutoriala:
Unity Tutorial – Podstawy
Teoria
Czy ktoś kiedyś widział grę RPG gdzie nie możemy rozwijać naszego bohatera? Musiałby to być spory ewenement. Mało tego w dzisiejszych czasach motyw ulepszania trafia się do prawie każdej gry. Ulepszamy broń, samochód, statek, bohatera. Ulepszamy wszystko. Istnieją nawet gry, których jedyną ideą jest dodawanie usprawnień do naszego statku, żeby dolecieć jak najdalej.
Najbardziej charakterystyczny system rozwoju, to drzewka rozwoju. Czyli, aby odblokować umiejętność X, musimy wcześniej odblokować umiejętność Y, a do tego zapłacić Z sztuk złota.
Dziś właśnie tego typu, proste drzewko sobie zaprezentujemy. Pewne umiejętności będą wymagały posiadania poprzednich, oraz pewnej liczby gotówki. Wybranie jakiejś umiejętności zwiększy jakiś współczynnik bohatera. Żeby tutaj nie komplikować i nie tworzyć specjalnie wymyślnego bohatera, po prostu wypiszemy sobie jego statystyki na ekranie, patrząc jak są modyfikowane w zależności od wybranych umiejętności.
Przygotowanie
Na samym początku zrobimy prosty skrypt (PlayerStats.cs), do przechowywania statystyk gracza. Jest on dość prosty, więc wrzucam w całości i szybko omówię.
using UnityEngine; public class PlayerStats : MonoBehaviour { private int maxHP; private int superPunch; private bool quickKill; private bool deathMachine; public int playerGold {get; set;} void Start() { maxHP = 100; superPunch = 0; quickKill = false; deathMachine = false; playerGold = 1000; } void OnGUI() { GUI.Label (new Rect (20, 0, 100, 20), "Max HP: " + maxHP); GUI.Label (new Rect (20, 30, 100, 20), "Attack: " + superPunch); GUI.Label (new Rect (20, 60, 100, 20), "QuickKill: " + quickKill); GUI.Label (new Rect (20, 90, 100, 20), "DeatchMachine: " + deathMachine); GUI.Label (new Rect (20, 120, 100, 20), "Gold: " + playerGold); } public void changePlayerStat(string name, float value) { switch(name) { case "maxHP": maxHP = (int)value; break; case "superPunch": superPunch = (int)value; break; case "quickKill": quickKill = value == 1; break; case "deathMachine": deathMachine = value == 1; break; } } }
Pierwsze linijki to deklaracje zmiennych. Wyróżnia się tylko jedna:
public int playerGold {get; set;}
Czym są te dwa magiczne słówka? Są to mutuatory, inaczej akcesory, albo popularne getery i setery. Brzmi strasznie, ale chodzi o to, że dzięki takiemu zapisowi nasza zmienna jest traktowana jak prywatna, ale posiadająca geter i seter. Ogólnie? Korzystamy z niej tak samo jak gdyby była publiczna, ale tak jest bezpieczniej.
W funkcji start tylko ustawiamy zmienne, w funkcji OnGUI wyświetlamy je sobie, żeby wiedzieć czy coś się dzieje. Istotna jest funkcja changePlayerStat, która pozwala modyfikować umiejętności i statystyki gracza. Próbowałem problem rozwiązać refleksjami, czy referencjami. Niestety Unity było bardzo oporne na takie zabiegi, stąd takie toporne i nieatrakcyjne rozwiązanie. Podajemy dwa parametry, nazwę zmiennej oraz wartość jaką ustawiamy.
Switch to odmiana ifa, która sprawdza po kolei czy podana w nawiasie zmienna, ma zadaną po słówku case (a przed dwukropkiem) wartość. Jeśli tak, wykonuje to co między dwukropkiem, a słówkiem break. Przez to, że chcemy zmieniać różne wartości (część statystyk to inty, część jest zmiennoprzecinkowa, a część jest, albo nie (bool)), musimy dokonać rzutowań na dobre typy, aby zapobiec błędom. O ile rzutowanie na inta jest proste, o tyle przy boolu, musimy nieco namieszać tworząc taki zapis:
deathMachine = value == 1;
Wstawiamy do zmiennej wartość zapytania value == 1. Kojarzysz pewnie ten zapis z ifa i słusznie. Jeśli podana wartość wyniesie 1, dostaniemy true, w przeciwnym wypadku false. Czyli to co chcieliśmy.
Obsługa drzewka umiejętności
Tutaj zaczynają się schody, ale dość małe. Tworzymy sobie kolejny skrypt SkillTree.cs. W tym momencie, możemy dodać oba skrypty (SkillTree.cs i PlayerStats.cs) do jakiegoś obiektu. Ja pracuje na nowym projekcie Unity, więc dodałem je do Main Camera.
Zaczynamy od deklaracji zmiennych:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class SkillTree : MonoBehaviour { private PlayerStats ps; private bool showTree; private List<MySkill> listOfSkills; }
Wyjątkowo podaję cały kod, ponieważ pojawia się nowy typ zmiennej List. Czyli… lista. Aby móc z niej korzystać, musimy dodać sobie odpowiednie biblioteki. Dlatego zaraz po domyślnych using, pojawiło się using System.Collections.Generic – bez tego, skrypt nie będzie wiedział czym jest List.
Samo list deklarujemy ciekawie, bo używamy symbolu diamentu (<>). Wewnątrz podajemy klasę (typ obiektu), jaki będzie w naszej liście. MySkill to klasa, którą utworzymy później. Reszta powinna być jasna. Żeby Unity nie truło, że MySkill nie istnieje, możesz sobie na szybko utworzyć plik MySkill.cs, a tym czasem dalej edytujemy SkillTree:
void Start () { ps = gameObject.GetComponent<PlayerStats> (); showTree = false; listOfSkills = new List<MySkill>(); }
W funkcji Start nie ma cudów, jedynie inicjujemy nasze zmienne. PlayerStat pobieramy sobie z obiektu (dlatego ważne, aby oba skrypty były dołączone do jednego obiektu).
void Update () { if (Input.GetKeyDown (KeyCode.C)) { showTree = !showTree; } }
Funkcja Update nie powala. Na kliknięcie na klawiaturze klawisza C, chowamy lub pokazujemy drzewko umiejętności, nad czym czuwa zmienna showTree. Zapis zastosowany wewnątrz ifa, powoduje ustawienie jako wartość zmiennej jej negacji. Czyli w miejsce true pojawi się false i odwrotnie – fajny trik warty zapamiętania dla zmiennych typu bool.
Teraz, weź łyk kawy, albo czegoś innego mocno pobudzającego, bo trafiamy na prawdopodobnie najbardziej poryty kawałek kodu:
void OnGUI() { if (showTree) { for(int i = 0 ; i < listOfSkills.Count ; i++) { MySkill skill = listOfSkills[i]; if(skill.isActive) { GUI.color = skill.btnOn; } else { GUI.color = skill.btnOff; } if (GUI.Button (skill.position, skill.label)) { if(skill.isAvailable() && (ps.playerGold >= skill.goldCost) && !skill.isActive) { ps.playerGold -= skill.goldCost; ps.changePlayerStat(skill.statToChange, skill.statValue); skill.isActive = true; } } } } }
Jest to wyświetlenie drzewka wraz z obsługą jego funkcjonalności. Jak pewnie wiesz (albo i nie), jeżeli wygenerowany przycisk wstawimy w funkcję if, to kod zawarty w ifie wykona się po kliknięciu w przycisk. Niestety, jeśli stworzymy 40 umiejętności, opisanie każdego ifa, przesuwanie wszystkiego itd. będzie bardzo pracochłonne i męczące, dlatego musiałem wymyślić coś bardziej praktycznego.
Pierwszy if, sprawdza tylko czy należy wyświetlić drzewko. Następnie mamy pętle, która wykonuje się dla wszystkich obiektów w liście umiejętności. Dla uproszczenia sobie kodu podstawiamy obecnie przerabiany skill pod zmienną (daje nam to tyle, że wszędzie zamiast pisać listOfSkills[i].cosTam(), piszemy skill.cosTam().
Następny if, sprawdza czy umiejętność jest już wykupiona. Jeśli tak, to zmieniamy kolor tekstu przycisku na ustawiony w nim jako btnOn, w przeciwnym wypadku zostaje kolor niewykupionej umiejętności czyli btnOff – oczywiście możesz zmieniać teksturę, tło obrazka, wszystko. Wybrałem kolor tekstu, bo można go łatwo zmienić, a chodziło mi tutaj o pokazanie opcji.
Sens całego przedsięwzięcia ujawnia się w ostatnim ifie.
if (GUI.Button (skill.position, skill.label)) { if(skill.isAvailable() && (ps.playerGold >= skill.goldCost) && !skill.isActive) { ps.playerGold -= skill.goldCost; ps.changePlayerStat(skill.statToChange, skill.statValue); skill.isActive = true; } }
Co tutaj mamy? If typowy dla przycisków, tworzący przycisk, ale jego pozycja (i wymiar), oraz opis zapisane są w obiekcie MySkill, przez co nie musimy tu nic ustawiać. Wewnątrz chcemy wykonać jakąś akcję przestawienia umiejętności.
Sprawdzamy kilka rzeczy dodatkowym ifem. Po pierwsze, czy możemy sobie wziąć taką umiejętność – czasami jest tak, że aby wziąć umiejętność HP II trzeba najpierw wykupić HP I. Właśnie za sprawdzenie takich warunków odpowiada funkcja isAvailable – omówimy ją przy okazji klasy MySkill. Drugi warunek, to fakt, że gracza stać na umiejętność (w złocie – założyłem, że wykup umiejętności może kosztować). Proste równanie, czy posiadane przez gracza złoto jest większe lub równe cenie umiejętności. No i ostatni warunek, czy umiejętność nie była już wykupiona (po co kupować dwa razy to samo?)
Wewnątrz ifa, wykonujemy kolejno: Zabieramy graczowi złoto, wykorzystujemy funkcję changePlayerStat aby zmienić wartości umiejętności – wszystko znów zapisane jest w obiekcie umiejętności. Na koniec ustawiamy umiejętność jako już wykupioną.
Wszystko fajnie, ale bez znajomości klasy MySkill wydaje się to bez sensu, dlatego przechodzimy do niej i ją tworzymy.
Klasa MySkill
using UnityEngine; using System.Collections; using System.Collections.Generic; public class MySkill { public Rect position { get; set;} public string label { get; set;} public string statToChange { get; set; } public float statValue { get; set; } public int goldCost { get; set; } public bool isActive { get; set; } public Color btnOn { get; set; } public Color btnOff { get; set; } public List<MySkill> requirements { get; set; } }
Ponownie wykorzystamy listę, więc znów dodajemy System.Collections.Generic. Dodatkowo zauważ, że klasa nie dziedziczy po MonoBehaviour. Dzieje się tak, bo klasy nie dodamy do GameObjectu, przez to dziedziczenie jest zbędne. Masa zmiennych, po co nam one?
- position – to pozycja i wielkość przycisku umiejętności w drzewku
- label – to opis umiejętności w drzewku
- statToChange – jaką statystykę ma zmieniać nasza umiejętność
- statValue – wartość zmiany
- goldCost – kosz umiejętności w złocie
- isActive – czy umiejętność jest już kupiona
- btnOn – kolor tekstu na wykupionej umiejętności
- btnOff – kolor tekstu na niewykupionej umiejętności
- requirements – list umiejętności, które gracz musi kupić, aby móc wykupić tę umiejętność
Wszystko jasne? To produkujemy pierwszą funkcję.
public MySkill(Rect p, string l, int gc, string sn, float sv) { position = p; label = l; goldCost = gc; isActive = false; requirements = new List<MySkill> (); btnOn = Color.red; btnOff = Color.yellow; statToChange = sn; statValue = sv; }
Charakterystyczna funkcja. Nie ma typu zwracanego, nazywa się tak samo jak klasa. Jest to konstruktor, czyli funkcja która się wykona w momencie utworzenia obiektu. Moment utworzenia obiektu to zapis:
MySkill skill = new MySkill();
Gdzie w nawiasie powinniśmy podać wszystkie parametry. Ja pominąłem część zmiennych, bo i tak ustawiłbym je identycznie dla każdej umiejętności (np. kolor tekstu), dlatego uznałem to za zbędne.
Dopisujemy jeszcze dwie funkcje:
public void addRequirement(MySkill skill) { requirements.Add (skill); }
Prościutka funkcja, która do listy wymaganych umiejętności dopisuje podaną w parametrze.
public bool isAvailable() { for(int i = 0 ; i < requirements.Count ; i++) { if(!requirements[i].isActive) { return false; } } return true; }
Na koniec funkcja, która sprawdza, czy zostały spełnione wymagania. Zasada jest cholernie prosta. Przelatujemy sobie całą listę wymaganych umiejętności, jeśli jakaś nie jest aktywna (nie została wykupiona), zwracamy false. Uznajemy tym samym, że gracz jeszcze nie jest godzien tej umiejętności. Jeżeli funkcja przeszła całą listę bez problemu, zwróci true.
No dobra, mamy prawie wszystko. Prawie, bo wypada dodać jakieś umiejętności. Wracamy do pliku SkillTree.cs
Tworzenie umiejętności
Do funkcji Start, dopisujemy następujący kod:
MySkill HP1 = new MySkill (new Rect (200, 20, 40, 40), "HP I", 100, "maxHP", 120); MySkill HP2 = new MySkill (new Rect (200, 80, 40, 40), "HP II", 200, "maxHP", 140); MySkill HP3 = new MySkill (new Rect (200, 140, 40, 40), "HP III", 200, "maxHP", 160); HP2.addRequirement (HP1); HP3.addRequirement (HP2); MySkill SP1 = new MySkill (new Rect (300, 20, 40, 40), "SP I", 100, "superPunch", 1); MySkill SP2 = new MySkill (new Rect (300, 80, 40, 40), "SP II", 100, "superPunch", 2); SP2.addRequirement (SP1); MySkill QK = new MySkill (new Rect (400, 20, 40, 40), "QK", 100, "quickKill", 1); MySkill DM = new MySkill (new Rect (350, 140, 40, 40), "DM", 100, "deathMachine", 1); DM.addRequirement (QK); DM.addRequirement (SP2); listOfSkills.Add (HP1); listOfSkills.Add (HP2); listOfSkills.Add (HP3); listOfSkills.Add (SP1); listOfSkills.Add (SP2); listOfSkills.Add (DM); listOfSkills.Add (QK);
Jak to działa? Tworzmy sobie najpierw umiejętność, podając wszystkie wymagane przez konstruktor parametry, czyli pozycję, opis, koszt, jaką statystykę ma zmienić dana umiejętność, oraz na jaką wartość (na jaką, nie o ile!)
Jeżeli chcemy, żeby jakiś skill był zależny od innego, to dodajemy danej umiejętności, drugą do listy wymaganych, przykładowo:
HP3.addRequirement (HP2);
Oznacza, że umiejętność HP3, wymaga umiejętności HP2. Na koniec dopisujemy umiejętności do listy.
Przy dopisaniu do listy, kolejność nie ma znaczenia. Ważne, żeby przed dodaniem umiejętności do listy, dodać jej wymagane umiejętności. Tyle! U mnie efekt wygląda tak:
Brakującą częścią całego układu, na pewno są linie pokazujące, która umiejętność jest zależna od której. Niestety nie ma prostego narzędzia tworzenia linii, dlatego sugerowałbym przygotowanie tekstury, z połączeniami, którą należałoby wstawić pod przyciski. Polecam też użyć zmiennych do wymiarów i pozycji przycisków, aby mieć przy pozycjonowaniu mniej zmieniania.
Kompletny projekt: SkillTree