Unity3d FPS Tutorial, czyli tworzymy własną grę FPS od podstaw z wykorzystaniem silnika Unity3d.

Temat: Przeładowanie broni i amunicja

Spis treści

Jeżeli nie używałeś do tej pory Unity3d:

#0 – Podstawy Podstaw

FPS Tutorial:

#1 – Tworzenie nowego projektu i narzędzie terenu

#2 – Sterowanie postacią

#3 – Życie, pancerz i wytrzymałość postaci

#4 – Regeneracja życia i energii. Efekty trafienia

#5 – Kamera z bronią i strzelanie

– Przeładowanie i amunicja

#7 – Zbieranie przedmiotów

#8 – Druga broń

#9 – Rzut granatem i seria z karabinu

#10 – Celowanie i dziury po kulach

#11 – Przeciwnik z prostym AI, sztuczna inteligencja

#12 – Animacja postaci przeciwnika. Animator

#13 – Menu główne gry. GUI

#14 – Ostatnie szlify i budujemy projekt

Teoria

W ostatniej części daliśmy graczowi możliwość strzelania, oraz wyświetliliśmy broń. Jednak aby nie zrobić z gry kiepskiego filmu akcji klasy B, musimy ograniczyć liczbę kul, a co za tym idzie, umożliwić graczowi przeładowanie broni. Oczywiście, warto go też poinformować o posiadanym stanie amunicji.

Paczka assetów, do dzisiejszego odcinka:

Tutorial_06_assets

Dodajemy liczbę kul i magazynek

Aby obsłużyć posiadanie amunicji, musimy w ogóle dodać ją do broni. Dlatego otwieramy skrypt shooting.cs, nad którym pracowaliśmy w poprzednim odcinku. Zaczynamy od dodania kilku parametrów:

public Texture2D crosshairTexture;
public AudioClip pistolShot;
public int maxAmmo = 200;
public int clipSize = 10;

private int currentAmmo = 30;
private int currentClip;
private Rect position;   
private float range = 10.0f;
private GameObject pistolSparks; 
private Vector3 fwd;
private RaycastHit hit;

Łatwo się domyślić, co będziemy trzymać w tych zmiennych. Po pierwsze maksymalną liczbę kul, jaką może broń posiadać. Rozmiar magazynka, oraz ile kul obecnie posiadamy i ile jest w magazynku. Musimy teraz zmienić nieco system strzelania:

if(Input.GetButtonDown("Fire1") && currentClip > 0) {
    currentClip--;

Dodajemy kolejny warunek, który mówi, że musimy posiadać pociski w magazynku, aby móc wystrzelić. Po drugie, w momencie wystrzału, odejmujemy kule z magazynka.

Jak pewnie dało się zauważyć, wszystkie zmienne, które zadeklarowaliśmy posiadają zdefiniowane wartości, za wyjątkiem obecnego stanu magazynka. Stało się tak, ponieważ chcemy, żeby magazynek domyślnie był wypełniony maksymalną liczbą kul. Jeżeli wprowadzilibyśmy wartość na sztywno, trzeba by było o tym pamiętać przy każdej zmianie maksymalnego rozmiaru magazynka. Dzięki prostej sztuczce, możemy o tym zapomnieć. W funkcji Start dopisujemy:

currentClip = clipSize;

Właściwie tyle. Samo odejmowanie kul i ich posiadanie, mamy załatwione.

Wyświetlamy stan magazynka

Nie możemy jednak tego sprawdzić, jeśli nigdzie nie wyświetlamy liczby pocisków. Na początek dodajemy kolejną zmienną:

public Texture2D crosshairTexture;
public AudioClip pistolShot;
public int maxAmmo = 200;
public int clipSize = 10;
public GUIText ammoText;

private int currentAmmo = 30;
private int currentClip;
private Rect position;   
private float range = 10.0f;
private GameObject pistolSparks; 
private Vector3 fwd;
private RaycastHit hit;

Wracamy na chwilę do Unity i dodajmy do sceny obiekt GUIText. Dla wersji: 4.5.x:  [GameObject -> Create Other -> GUI Text]. Dla wersji 4.6.x: Tworzymy pusty GameObject: [Ctrl+Shift+N], a następnie dodajemy mu komponent GUIText [Component -> Rendering -> GUI Text]

Jedyne co ja z nim zrobiłem, to usunięcie parametru Text, zwiększenie czcionki (Font Size) na 30, oraz ustawienie Anchor na middle center, aby tekst się ładnie wyświetlał. Sam obiekt nazwałem GT_ammo. Pozwoli to łatwiejsze lokalizowanie obiektów danego typu, dzięki przedrostkowi GT od GUIText. Moje ustawienia obiektu wyglądają tak:

Ustawienia obiektu GT_ammo
Ustawienia obiektu GT_ammo

Wracamy do kodu i dodajemy kod, który nam wyświetli stan magazynka:

void OnGUI()
{
	GUI.DrawTexture(position, crosshairTexture);
	ammoText.pixelOffset = new Vector2(-Screen.width / 2 + 100, -Screen.height / 2 + 30);
	ammoText.text = currentClip + " / " + currentAmmo;
}

Po pierwsze, zmieniamy położenie tekstu. Domyślnie GUIText, wyświetla się centralnie na środku ekranu, a my chcemy go w lewym dolnym. Dlatego od jego położenia odejmujemy połowę szerokości ekranu, po tym zabiegu, widzieć będziemy jedynie połowę napisu. Dlatego, musimy dodać parę pikseli, które odpowiadają wysokości i szerokości tekstu. Po tym zabiegu, napis powinien się ładnie ułożyć. Drugi ruch, to dodanie samego tekstu. Stringi łączymy plusem, wyświetlając amunicję w magazynku, slash i posiadaną amunicję zapasową.

Dodajemy przeładowanie

Mamy już system strzelania i widzimy stan amunicji. Ale, po wystrzeleniu wszystkich pocisków, zostajemy bez amunicji. Dlatego, czas zrobić przeładowanie. Znów przyda się parę zmiennych:

public Texture2D crosshairTexture;
public AudioClip pistolShot;
public AudioClip reloadSound;
public int maxAmmo = 200;
public int clipSize = 10;
public GUIText ammoText;
public float reloadTime = 2.0f;

private int currentAmmo = 30;
private int currentClip;
private Rect position;   
private float range = 10.0f;
private GameObject pistolSparks; 
private Vector3 fwd;
private RaycastHit hit;
private bool isReloading = false;

private float timer = 0.0f;

Po pierwsze, przyda się dźwięk przeładowania. Po drugie, jakiś czas przeładowania. Dodatkowo, robimy sobie flagę, która sprawdza czy jesteśmy w trakcie przeładowania, oraz prosty timer. Uprzedzając pytania: Nie możemy użyć współprogramu, ponieważ, pracujemy głównie na funkcji Update, w której nie powinno wykorzystywać się funkcji StartCorutine, która to odpowiada, za uruchomienie współprogramów.

Czas zareagować na przeładowanie. Dlatego, w funkcji Update dodajemy:

if(((Input.GetButtonDown("Fire1") && currentClip == 0) || Input.GetButtonDown("Reload")) && currentClip < clipSize) {
	if(currentAmmo > 0) {
		audio.clip = reloadSound;
		audio.Play();
		isReloading = true;
	}
}

Warunek wydaje się skomplikowany, ale po prawdzie jest banalny. W pierwszej części mamy dwa warunki oddzielne spójnikiem LUB (||), po lewej mamy pierwszy wariant przeładowania, czyli gdy naciskamy strzał, a stan magazynka jest równy 0. Drugi wariant, to naciśnięcie przycisku Reload.  Teoretycznie, taki warunek mógłby wystarczyć. Ale po co mamy robić przeładowanie, gdy magazynek jest pełny? Stąd pojawia się ten dodatkowy zapis, mówiący o tym, że omówioną wcześniej część uruchamiamy, tylko wtedy, gdy w magazynku mamy mniej amunicji, niż maksymalnie możemy mieć. Dodatkowo, sprawdzamy czy w ogóle, posiadamy jakiś zapas amunicji. Bez sensu uruchamiać przeładowanie, gdy nie mamy czym przeładować.

Sam kod jest prosty. Podmieniamy klip dźwiękowy na klip przeładowania i go uruchamiamy, po czym zmieniamy zmienną naszą flagę isReloading na true. Dzięki temu, uruchomi się kolejny kod, znajdujący się w funkcji Update.

if(isReloading) {
	timer += Time.deltaTime;
	if(timer >= reloadTime) {
		int needAmmo = clipSize - currentClip;
		
		if(currentAmmo >= needAmmo) {
			currentClip = clipSize;
			currentAmmo -= needAmmo;
		} else {
			currentClip += currentAmmo;
			currentAmmo = 0;
		}
		
		audio.clip = pistolShot;
		isReloading = false;
		timer = 0.0f;
	}
}

Jeżeli postać ma przeładować, to zaczynamy liczyć czas, bo nie chcemy, żeby przeładowanie trwało ułamek sekundy (teraz wiadomo po co był nam timer). Jeżeli timer przekroczył czas przeładowania (który, warto ustawić na tak długi czas, ile trwa nasz dźwięk przeładowania) to zaczynamy wykonywać kod.

W zmiennej pomocniczej, sprawdzamy ile amunicji musimy załadować. Czyli od maksymalnego rozmiaru magazynka, odejmujemy jego aktualny stan.

Teraz sprawdzamy, czy w zapasie posiadamy tyle amunicji. Jeśli tak, to ładujemy aktualny stan magazynka, pełną wartością, a od zapasu odejmujemy tyle, ile załadowaliśmy. W przeciwnym wypadku, dodajemy do aktualnego stanu magazynka to co zostało, a licznik posiadanego zapasu zerujemy. Jest to chyba oczywiste.

Na koniec, ustawiamy audio.clip ponownie na wystrzał (zmienialiśmy go przy rozpoczęciu przeładowania), zerujemy timer i ustawiamy flagę przeładowania na false. Dzięki temu dajemy znać, że skończyliśmy przeładowanie, a timer mamy gotowy do działania przy następnym przeładowaniu.

Została jeszcze jedna zmiana w kodzie. Nie chcemy aby gracz mógł strzelać, w czasie przeładowania. Dodajemy więc do ifa wystrzału kolejny warunek.

if(Input.GetButtonDown("Fire1") && currentClip > 0 && !isReloading) {

Jeżeli nie przeładowujemy, można strzelać.

Teraz czas wrócić do Unity. Musimy tu załatwić dwie sprawy. Po pierwsze, uzupełnić wszystkie zmienne publiczne, które dodaliśmy w ostatnich krokach. W tym zmienną GT_Text oraz reloadSound. Druga sprawa, to przycisk Reload, który nie jest domyślnie zdefiniowany. Musimy to zrobić sami. Dlatego wybieramy [Edit -> Project Settings -> Input]. W panelu Inspector rozwijamy listę Axies. Zwiększamy Size o 1 i zmieniamy ostatni wpis, który pojawi się na samym dole listy. Zmieniamy tylko parametry Name i Positive Button, odpowiednio na Reload i r.

Ustawienia Inputów
Ustawienia Inputów

Od tej pory, powinniśmy być w stanie przeładować.

Wyświetlamy komunikaty o potrzebie przeładowania

W wielu grach, gdy skończy się nam amunicja, pojawia się komunikat o potrzebie przeładowania. My dodaliśmy opcję przeładowania strzałem, więc jest to element nieco zbędny, jednak może być pomocny dla gracza, więc jednak go zrobimy.

Będąc w Unity, dodajemy drugi GUIText. Dla wersji: 4.5.x:  [GameObject -> Create Other -> GUI Text]. Dla wersji 4.6.x: Tworzymy pusty GameObject: [Ctrl+Shift+N], a następnie dodajemy mu komponent GUIText [Component -> Rendering -> GUI Text]

Ja swojego nazwałem GT_reload. Ponownie zmieniamy rozmiar czcionki (Font Size), położenie napisu (Anchor) na middle center, wpisujemy treść komunikatu, u mnie brzmi ona “Press R or Fire to Reload”, oraz wyłączamy component, aby nie pojawił się domyślnie w grze.

Ustawienia obiektu GT_reload
Ustawienia obiektu GT_reload

Teraz czas na wrócenie do skryptu.

Najpierw musimy dodać ostatnią dzisiaj zmienną pomocniczą.

public Texture2D crosshairTexture;
public AudioClip pistolShot;
public AudioClip reloadSound;
public int maxAmmo = 200;
public int clipSize = 10;
public GUIText ammoText;
public GUIText reloadText;
public float reloadTime = 3.0f;

private int currentAmmo = 30;
private int currentClip;
private Rect position;   
private float range = 10.0f;
private GameObject pistolSparks; 
private Vector3 fwd;
private RaycastHit hit;
private bool isReloading = false;

private float timer = 0.0f;

Możemy teraz szybko przeskoczyć do Unity i uzupełnić zmienną, utworzonym wcześniej obiektem. Wracamy znowu do skryptu, by dokończyć nasze dzieło:

void OnGUI()
{
	GUI.DrawTexture(position, crosshairTexture);
	ammoText.pixelOffset = new Vector2(-Screen.width / 2 + 100, -Screen.height / 2 + 30);
	ammoText.text = currentClip + " / " + currentAmmo;

	if(currentClip == 0) {
		reloadText.enabled = true;
	} else {
		reloadText.enabled = false;
	}
}

Dodajemy prostego ifa. Jeśli obecny stan magazynka to 0, to wyświetlamy komunikat. W przeciwnym razie, zostawiamy go wyłączonego.

Podsumowanie

Zdaje sobie sprawę z tego i wy również musicie, że kod, który póki co posiadamy, nie jest perfekcyjny. Jednak chce, abyście mieli dokładny wgląd w proces powstawania gry, dlatego, wszelkie poprawki i usprawnienia kodu, będą się pojawiać później. Zastosujemy sobie bardziej zaawansowane techniki programowania, czy proste sztuczki na ułatwienie wielu rzeczy. Póki co tyle.

Na dzisiaj tyle. Nasza broń ma już ograniczony magazynek, oraz może zostać przeładowana. W kolejnej części zajmiemy się zbieraniem przedmiotów z ziemi.

Poprzednia część <- #5 – Kamera z bronią i strzelanie

Następna część -> #7 – Zbieranie przedmiotów