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

Temat: Jak dodać postaci pasek życia, pancerza i wytrzymałości

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ą

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

#4 – Regeneracja życia i energii. Efekty trafienia.

#5 – Kamera z bronią i strzelanie

#6 – Przeładowanie broni 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

Skończyliśmy póki co etap klikania, a bierzemy się za pierwsze skrypty. Na początek, zaczniemy od prostej rzeczy. Najczęściej w grach FPS postać charakteryzują trzy atrybuty, czasami widoczne na ekranie. Punkty życia – obecnie przy modzie na samoodnawialne punkty życia, paska, ani konkretnej liczby punktów życia, nie ma na ekranie. My, póki co wyświetlimy pasek życia, aby dobrze wszystko widzieć. Druga rzecz to pancerz, który sam się nie odnawia i oczywiście tracimy go, zanim stracimy punkty życia. Ostatnia sprawa to wytrzymałość. Ona często też jest ukryta, ale jej efekt bardzo widoczny. Np. dłuższy sprint, sprawia, że postać się męczy i przestaje biec.

HUD w grze Far Cry
HUD w grze Far Cry

My, będziemy celować  w wygląd, zbliżony do tego z gry Far Cry. Oczywiście, jakość graficzna nie stanie na takim poziomie, bo to nie kurs grafiki. :)

Przygotowanie

Zanim weźmiemy się za pisanie kodu, musimy sobie przygotować tekstury dla naszych pasków. Mogą to być małe obrazki w jednolitym kolorze, bądź tekstury. Ja stworzyłem sobie 3 prostokąty 10x20px, w kolorze czerwonym (#bd2515), żółtym(#f5d91c) i niebieskim(#1e3ac7). Nazwałem je kolejno: hp, armour i stamina. Dodałem im filtr szumu, aby uzyskać ciekawszy efekt tekstury. Teraz wszystkie pliki importujemy do projektu, przeciągając myszką. Dla porządku wcześniej tworzymy folder “Textures”, gdzie będziemy trzymać wszystkie pliki tekstur, tam też wrzucamy nasze obrazki.

Wreszcie, możemy faktycznie przejść do kodowania. W folderze “Scripts” tworzymy nowy skrypt C#. W tym celu, klikamy prawym klawiszem myszy w obszarze panelu Project i wybieramy: Create -> C# Script. Nazywamy skrypt: PlayerStats.cs. Skrypt dołączamy od razu do First Person Controllera, przeciągając go na niego.

Wchodzimy w edycję skryptu, klikając na niego dwukrotnie.

Wyświetlenie statystyk

Aby wyświetlić statystyki, przede wszystkim, musimy posiadać informację, jaki jest obecny stan atrybutu, oraz jaka jest wartość maksymalna. Dlatego zaczynamy od definicji zmiennych:

private float maxHealth = 100;
private float currentHealth = 100;
private float maxArmour = 100;
private float currentArmour = 100;
private float maxStamina = 100;
private float currentStamina = 100;

Aby wyświetlić tekstury współczynników, potrzebujemy również w naszym skrypcie tekstur:

public Texture2D healthTexture;
public Texture2D armourTexture;
public Texture2D staminaTexture;

Teraz wracamy do Unity i przeciągamy tekstury na odpowiednie miejsca:

Ustawienie tekstur dla skryptu
Ustawienie tekstur dla skryptu

Możemy teraz wyświetlić nasze paski, wracamy do kodu. Od razu zadbamy o responsywność. Czyli, o fakt, żeby nasze paski skalowały się do rozdzielczości ekranu gracza. W tym celu deklarujemy jeszcze dwie zmienne pomocnicze:

private float barWidth;
private float barHeight;

Zmienne nie mają wartości, dlatego musimy o to zadbać. Wykorzystamy funkcję Awake, która uruchamia się w momencie gdy obiekt gracza zostanie powołany do życia. Szybciej niż funkcja Start.

void Awake()
{
	barHeight = Screen.height * 0.02f;
	barWidth = barHeight * 10.0f;

}

Kod jest prosty. Wysokość naszego paska, to 2% wysokości ekranu. Czyli np. dla full hd (1080px), nasz pasek będzie miał 20px wysokości. Kiedy dla 768px, będzie to ok. 15px. Wartość procentową, dobrałem metodą prób i błędów. Czyli testowałem, różne warianty, aż uzyskałem moim zdaniem najbardziej estetyczny. Szerokość za to uzależniamy od wysokości. Ma być to zwyczajnie dziesięciokrotność wysokości. Kierowałem się tu prostą zasadą. Domyślnie chciałem pasek w proporcji 100px na 10px. Czyli, żeby zachować proporcję, szerokość musi być dziesięciokrotnością wysokości.

Czas wyświetlić paski:

void OnGUI()
{
	GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
	                         Screen.height - barHeight - 10,
	                         currentHealth * barWidth / maxHealth,
	                         barHeight),
	                healthTexture);
	GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
	                         Screen.height - barHeight * 2 - 20,
	                         currentArmour * barWidth / maxArmour,
	                         barHeight),
	                armourTexture);
	GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
	                         Screen.height - barHeight * 3 - 30,
	                         currentStamina * barWidth / maxStamina,
	                         barHeight),
	                staminaTexture);
}

Uruchamiamy tutaj wszytko w funkcji OnGUI. Jest to funkcja, wyświetlające elementy interfejsu gracza (Graphic User Interface), czyli takie elementy jak nasze paski życia, czy później amunicja lub celownik.

Wykorzystujemy funkcję DrawTexture, która odpowiada za rysowanie tekstur. Ma ona dwa parametry. Pierwszy to obiekt typu Rect (za chwilę go omówię), drugi to Texture2D. Drugi jest oczywisty. Jest to tekstura, którą będziemy rysować. Obiekty typu Rect to nic innego jak prostokąt, który w Unity służy do ustalania pozycji obiektów w obszarze 2D. Ma cztery parametry. X, Y, W, H. Czyli… Swoją pozycję  w układzie współrzędnych: Osie X (poziomo) i Y (pionowo), oraz swoją szerokość (W – Width) i wysokość (H – Height).

Teraz wyjaśnijmy sobie skąd, tak skomplikowane obliczenia w każdej linijce.

Screen.width - barWidth - 10,

Układ współrzędnych w przypadku GUI ma swój początek (punkt (0, 0))  w lewym górnym rogu. Dlatego, aby wyświetlić paski w prawym dolnym, musimy wykonać pewne obliczenia. Od całkowitej szerokości ekranu odejmuje szerokość paska i dodatkowe 10 pikseli dla marginesu.

Screen.height - barHeight * 2 - 50,

Dla pozycji w pionie, stosujemy podobną sztuczkę. Od wysokości ekranu odejmujemy wysokość paska i dodatkowe 10 pikseli dla marginesu (dla pierwszego paska). Później dodatkowo mnożymy odjęcie wysokości paska oraz zwiększamy o 10 odejmowany margines. Po co? Bez tego, narysowalibyśmy paski na sobie. Dzięki temu zabiegowi, każdy będzie o swoją wysokość i 10 pikseli marginesu wyżej.

currentStamina * barWidth / maxStamina,
barHeight),

Ostatnie dwa parametry są raczej oczywiste. Pierwszy to proporcja. Nasza obecna liczba punktów życia/pancerza/wytrzymałości, mnożona przez maksymalną szerokość paska, podzielona przez maksymalną liczbę punktów życia. Taka proporcja oblicza długość paska, dla aktualnej liczby punktów. Mówiąc obrazowo: Gdy mamy 40% punktów życia, pasek punktów życia będzie zajmował 40% przestrzeni dla siebie przeznaczonej. Czyli gdyby maksymalnie miał mieć 100 pikseli, będzie miał 40. Ostatni parametr, to wysokość paska, która jest stała.

Otrzymywanie obrażeń

Mamy już nasze paski w tym pasek życia i pancerza. Ale co nam z nich, jeżeli są statycznym obrazkiem? W sumie nic. Ale dzięki zastosowaniu proporcji, obrazki powinny się ładnie skalować w zależności od punktów życia jakie mamy. Póki co nie mamy przeciwników, żeby sprawdzić czy to działa. Dlatego, stworzymy sobie prostą funkcję, która na przyciśnięcie przycisku zada nam obrażenia. Jednak trzeba pamiętać, że jeśli mamy pancerz, najpierw powinniśmy odejmować od niego.

Najprościej, będzie wykonać prostą funkcję odpowiedzialną za zadawanie obrażeń bohaterowi, w późniejszym etapie, gdy stworzymy już przeciwników, będziemy mogli ją łatwo wykorzystać.

void takeHit(float damage) 
{
	if(currentArmour > 0) {
		currentArmour -= damage;
		if(currentArmour < 0) {
			currentHealth += currentArmour;
			currentArmour = 0;
		}
	} else {
		currentHealth -= damage;
	}

	currentArmour = Mathf.Clamp(currentArmour, 0, maxArmour);
	currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
}

Funkcja o nazwie takeHit, nie zwracająca wartości, a przyjmująca parametr damage. Na początek w ifie, sprawdzamy czy posiadamy jeszcze pancerz. Jeśli nie, obrażenia odejmujemy od aktualnie posiadanego życia. Jeżeli mamy pancerz, to odejmujemy ilość pancerza, równą obrażeniom. Ale! Jeśli liczba punktów pancerza w tym momencie spada poniżej zera, oznacza to, że powinniśmy również odebrać część życia. Czynimy to w kolejnym ifie. W wewnętrznym ifie, używamy += zamiast -=, bo wartość currentArmour ma wartość ujemną. Dla pewności zerujemy wartość pancerza.

Ostatnie dwie linijki, zapobiegają dziwnemu zachowaniu się pasków. Funkcja Clamp z biblioteki Mathf, sprawia, że podana wartość, musi się mieścić w przedziale. Czyli np.:

currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);

Oznacza, że zmienna currentHealth, musi się zawierać w przedziale 0 do maxHealth – czyli 100. Co to oznacza? Że jeżeli życie postaci spadnie poniżej 0 i wyniesie np. -20, to my i tak otrzymamy wartość zero. Jakie to ma dla nas znaczenie?

Unikamy efektu rysowania w przeciwną stronę
Unikamy efektu rysowania w przeciwną stronę

Unikamy takiego efektu jak na obrazku. Gdyby nie to zaokrąglenie, jeżeli postać otrzymałaby za dużo obrażeń, jej pasek życia zacząłby rosnąć w przeciwną stronę.

OK. Czas przetestować jakoś naszą funkcję. W tym celu w funkcji Update wpisujemy sobie następujące linijki:

void Update()
{
	if(Input.GetKeyDown(KeyCode.P)) {
		takeHit(30);
	}
}

Jeżeli naciśnięty zostanie przycisk “P”, wykonujemy naszą funkcję z parametrem równym 30. Dzięki temu, testujemy odejmowanie pancerza, pancerza z życiem i samego życia. Uruchom teraz grę i przetestuj efekt, naciskając P.

Run Forest, run! – Unity v.4.6.x

Czas wykorzystać w jakiś sposób wytrzymałość. Proponuję dodać bieganie (domyślny First Person Controller nie oferuje takiej funkcji), które będzie męczyło naszą postać.

Dodajmy sobie kilka zmiennych:

public float walkSpeed = 10.0f;
public float runSpeed = 20.0f;
 
private CharacterController chCont;
private CharacterMotor chMotor;

Są to prędkość chodzenia i biegania, które są publiczne, aby dało się je zmodyfikować z poziomu Unity. Do tego mamy zmienne typu CharacterController i CharacterMotor. Są to elementy First Person Controllera odpowiadające za poruszanie się. Potrzebujemy się do nich odwołać, aby umożliwić bieganie. W funkcji Awake, dodajemy linijki:

chCont = GetComponent<CharacterController>();
chMotor = GetComponent<CharacterMotor>();

Funkcja GetComponent, pobiera nam komponent o wskazanej nazwie. Komponentami są wszystkie elementy wchodzące w skład obiektu.

Czas je wykorzystać.

void FixedUpdate () 
{
	float speed = walkSpeed;
	
	if(chCont.isGrounded && Input.GetKey(KeyCode.LeftShift)) {
		speed = runSpeed;
	}	
	
	chMotor.movement.maxForwardSpeed = speed;
}

Warte odnotowania jest to, że kod wywołujemy w funkcji FixedUpdate, zamiast Update. Nie będę się teraz zagłębiał w różnice między nimi, istotne jest to, że FixedUpdate wywołuje się z równą częstotliwością, niezależnie od ilości klatek na sekundę i odbywa się to przed wywołaniem funkcji Update.

Sam kod jest prosty. Pod zmienną tymczasową speed wstawiamy prędkość marszu. W ifie, wykorzystujemy jeden z komponentów, żeby sprawdzić czy postać dotyka ziemi (ciężko biegać w powietrzu), oraz sprawdzamy czy lewy shift jest naciśnięty. Jeśli tak, używamy prędkości biegu. Na koniec, podmieniamy zmienną maxForwardSpeed z komponentu CharacterMotor, aby umożliwić postaci, osiągnięcie większej prędkości.

Bieganie mamy, czas się zmęczyć.

void FixedUpdate () 
{	
	float speed = walkSpeed;
		
	if(chCont.isGrounded && Input.GetKey(KeyCode.LeftShift)) {
		speed = runSpeed;
		currentStamina -= 1;
		currentStamina = Mathf.Clamp(currentStamina, 0, maxStamina);
	}	
		
	chMotor.movement.maxForwardSpeed = speed;
}

Modyfikujemy kod, dodając zużycie staminy przy biegu, dochodzi do tego nasza znajoma funkcja zaokrąglająca. Jednak jest pewien problem. Kiedy stoimy, ale wciśniemy shift i tak się męczymy. Dlatego, musimy sprawdzić, czy postać w ogóle się porusza.

Dodajemy zmienną pomocniczą:

private Vector3 lastPosition;

Będziemy w niej trzymać informację, na temat ostatniej pozycji gracza.

Wracamy znów do FixedUpdate:

void FixedUpdate () 
{
	float speed = walkSpeed;
		
	if(chCont.isGrounded && Input.GetKey(KeyCode.LeftShift) && lastPosition != transform.position && currentStamina > 0) {
		lastPosition = transform.position;
		speed = runSpeed;
		currentStamina -= 1;
		currentStamina = Mathf.Clamp(currentStamina, 0, maxStamina);
	}	
		
	chMotor.movement.maxForwardSpeed = speed;
}

Zmiany są dwie. Po pierwsze gdy naciskamy shift, ustawiamy ostatnią pozycję na aktualną. Robimy to tylko w ifie, bo nie potrzebujemy tych danych cały czas, a jedynie w czasie biegu. Druga zmiana, to dodatkowy warunek w ifie. Sprawdzający czy ostatnia pozycja różni się od obecnej. Oznacza to, że się przemieszczamy.

Dodatkowo, dodajemy warunek sprawdzający czy nasza postać, ma w ogóle staminę, czyli siłę do biegu.

Zostało tylko zainicjować zmienną lastPosition. Robimy to w funkcji Awake:

void Awake()
{
	barHeight = Screen.height * 0.02f;
	barWidth = barHeight * 10.0f;

	chCont = GetComponent<CharacterController>();
	chMotor = GetComponent<CharacterMotor>();

	lastPosition = transform.position;
}

Bez kombinowania, wstawiamy do niej aktualną pozycję gracza.

Run Forest, run! – Unity v.5.0

Tutaj, jesteśmy w znacznie lepszej sytuacji, niż w wersji poprzedniej. Mianowicie domyślny kontroler postaci, posiada funkcję biegania. Nie musimy więc jej dodawać. Jedyne co trzeba zrobić, to obsłużyć zużycie staminy. Sam skrypt, podany dla wersji poprzedniej, też nie bardzo się zmieni. Ale lecimy po kolei:

Dodajemy sobie dwie zmienne:

private CharacterController chCont;
private UnityStandardAssets.Characters.FirstPerson.FirstPersonController fpsC;

Które wewnątrz funkcji Awake, od razu ustawiamy:

chCont = GetComponent<CharacterController>();
fpsC = gameObject.GetComponent<UnityStandardAssets.Characters.FirstPerson.FirstPersonController> ();

Jest to komponent CharacterController, który opisuje sytuację gracza, co przyda nam się za chwilę. oraz skrypt odpowiadający za poruszanie się postaci. Musimy użyć aż tak rozbudowanej ścieżki, aby uzyskać jednoznaczność odwołania.

Dodajemy samo zużycie staminy:

void FixedUpdate () 
{	
	if(chCont.isGrounded && Input.GetKey(KeyCode.LeftShift)) {
		currentStamina -= 1;
		currentStamina = Mathf.Clamp(currentStamina, 0, maxStamina);
	}	

	if (currentStamina > 0) {
		fpsC.CanRun = true;
	} else {
		fpsC.CanRun = false;
	}
}

Używamy funkcji FixedUpdate, czyli tej zależnej od fizyki gry, a nie od klatek – mamy zapewnione stałe odstępy między wywołaniami tej funkcji. Co mamy w ifie? Sam warunek to po pierwsze sprawdzenie, czy gracz dotyka ziemi – bo przecież nie męczy się od biegania w powietrzu. Po drugie, czy gracz naciska Lewy Shift, czyli klawisz, który odpowiada za bieganie w First Person Controllerze. Jeśli tak, to odejmujemy staminę i zaokrąglamy, tak aby nie stała się ujemna.

Drugi if, to zmiana zmiennej CanRun w FirstPersonController.cs, tak aby uniemożliwić bieganie gdy stamina jest < 0. Domyślnie tej zmiennej nie ma, ale dodamy ją sobie później, gdy skończymy z tym skryptem. Zasada jest prosta. Stamina > 0 = można biegać, w przeciwnym wypadku nie.

Jednak jest pewien problem. Kiedy stoimy, ale wciśniemy shift i tak się męczymy. Dlatego, musimy sprawdzić, czy postać w ogóle się porusza.

Dodajemy zmienną pomocniczą:

private Vector3 lastPosition;

Zmieniamy następnie funkcję FixedUpdate:

void FixedUpdate () 
{
	float speed = walkSpeed;
		
	if(chCont.isGrounded && Input.GetKey(KeyCode.LeftShift) && lastPosition != transform.position && currentStamina > 0) {
		lastPosition = transform.position;
		speed = runSpeed;
		currentStamina -= 1;
		currentStamina = Mathf.Clamp(currentStamina, 0, maxStamina);
		canRegenerate = 5.0f;
	}	
		
	if (currentStamina > 0) {
		fpsC.CanRun = true;
	} else {
		fpsC.CanRun = false;
	}
}

Zmiany są dwie. Po pierwsze gdy naciskamy shift, ustawiamy ostatnią pozycję na aktualną. Robimy to tylko w ifie, bo nie potrzebujemy tych danych cały czas, a jedynie w czasie biegu. Druga zmiana, to dodatkowy warunek w ifie. Sprawdzający czy ostatnia pozycja różni się od obecnej. Oznacza to, że się przemieszczamy. Na koniec musimy ustawić wartość startową dla naszej zmiennej lastPosition:

void Awake()
{
	barHeight = Screen.height * 0.02f;
	barWidth = barHeight * 10.0f;

	chCont = GetComponent<CharacterController>();
        fpsC = gameObject.GetComponent<UnityStandardAssets.Characters.FirstPerson.FirstPersonController> ();

	lastPosition = transform.position;
}

Wszystko powinno pięknie działać. Problemem natomiast jest fakt, że nasz poziom staminy nie wpływa na możliwość biegu gracza. Tutaj, niestety nie obejdzie się bez edycji pliku FirstPersonController.cs. Co tam należy zrobić? Dodajemy sobie jedną zmienną:

private bool m_CanRun = true;

A następnie kodzik:

public bool CanRun
{
	get { return m_CanRun; }
	set { m_CanRun = value; }
}

Co to nam daje? Mamy zmienną, która definiuje, czy możemy biegać, oraz dodajemy jej mutuatory (czyli popularny geter i seter), czyli pokrótce funkcję, które pozwalają pobrać i zmienić wartość tej zmiennej.

Jeszcze znajdujemy w kodzie taką linijkę:

speed = m_IsWalking ? m_WalkSpeed : m_RunSpeed;

I zamieniamy na:

speed = m_IsWalking ? m_WalkSpeed : m_CanRun ? m_RunSpeed : m_WalkSpeed;

Co to zmienia? W domyśle linijka sprawdza czy postać ma chodzić (IsWalking na true), jeśli tak, to dostajemy prędkość chodzenia, jeśli nie to prędkość biegu. – Ten cały dziwny zapis to jedno linijkowy if. Jego budowa jest następująca: [WARUNEK] ? [CO JEŚLI PRAWDA] : [CO JEŚLI FAŁSZ]. Stosowany najczęściej właśnie w takich przypadkach, gdzie warunek jest bardzo prosty i chcemy zwrócić jedną wartość.

Nasza podmiana, sprawiła, że w przypadku fałszu, czyli gdy postać ma biec, sprawdzamy jeszcze naszą dodatkową zmienną. Jeśli też jest prawdą, pozwalamy biec, w przeciwnym wypadku – idziemy.

Podsumowanie

W tej części, udało nam się stworzyć podstawowe GUI gracza, które jest responsywne. Do tego dodaliśmy funkcję biegania i męczenia się. Możemy też zadań naszej postaci obrażenia z uwzględnieniem pancerza.

W następnej części, zajmiemy się regeneracją współczynników. Poznamy współprogramy i proste sztuczki na oczekiwanie. Do tego dodamy kilka efektów, do otrzymywania obrażeń.

Cały kod PlayerStats.cs – Unity3d v.4.6.x:

using UnityEngine;
using System.Collections;

public class PlayerStats : MonoBehaviour {

	private float maxHealth = 100;
	private float currentHealth = 100;
	private float maxArmour = 100;
	private float currentArmour = 100;
	private float maxStamina = 100;
	private float currentStamina = 100;

	private float barWidth = 100;
	private float barHeight = 100;

	private float canHeal = 0.0f;
	private float canRegenerate = 0.0f;

	private CharacterController chCont;
	private CharacterMotor chMotor;
	private Vector3 lastPosition;

	public Texture2D healthTexture;
	public Texture2D armourTexture;
	public Texture2D staminaTexture;

	public float walkSpeed = 10.0f;
	public float runSpeed = 20.0f;

	public GUITexture hitTexture;


	void Awake()
	{
		barHeight = Screen.height * 0.02f;
		barWidth = barHeight * 10.0f;

		chCont = GetComponent<CharacterController>();
		chMotor = GetComponent<CharacterMotor>();

		lastPosition = transform.position;
	}
	
	void OnGUI()
	{
		GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
		                         Screen.height - barHeight - 10,
		                         currentHealth * barWidth / maxHealth,
		                         barHeight),
		                healthTexture);
		GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
		                         Screen.height - barHeight * 2 - 20,
		                         currentArmour * barWidth / maxArmour,
		                         barHeight),
		                armourTexture);
		GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
		                         Screen.height - barHeight * 3 - 30,
		                         currentStamina * barWidth / maxStamina,
		                         barHeight),
		                staminaTexture);
	}

	void Start()
	{
		Rect currentRes = new Rect(-Screen.width * 0.5f,
		                           -Screen.height * 0.5f,
		                           Screen.width,
		                           Screen.height);
		hitTexture.pixelInset = currentRes;
	}

	void Update()
	{
		if(Input.GetKeyDown(KeyCode.P)) {
			takeHit(30);
		}

		if(canHeal > 0.0f) {
			canHeal -= Time.deltaTime;
		}
		if(canRegenerate > 0.0f) {
			canRegenerate -= Time.deltaTime;
		}

		if(canHeal <= 0.0f && currentHealth < maxHealth) {
			regenerate(ref currentHealth, maxHealth);
		}
		if(canRegenerate <= 0.0f && currentStamina < maxStamina) {
			regenerate(ref currentStamina, maxStamina);
		}

	}

	void FixedUpdate () 
	{
		float speed = walkSpeed;
		
		if(chCont.isGrounded && Input.GetKey(KeyCode.LeftShift) && lastPosition != transform.position && currentStamina > 0) {
			lastPosition = transform.position;
			speed = runSpeed;
			currentStamina -= 1;
			currentStamina = Mathf.Clamp(currentStamina, 0, maxStamina);
			canRegenerate = 5.0f;
		}	
		
		chMotor.movement.maxForwardSpeed = speed;
	}

	void takeHit(float damage) 
	{

		Destroy(Instantiate(hitTexture), 0.15f);

		if(currentArmour > 0) {
			currentArmour = currentArmour - damage;
			if(currentArmour < 0) {
				currentHealth += currentArmour;
				currentArmour = 0;
			}
		} else {
			currentHealth -= damage;
		}

		if(currentHealth < maxHealth) {
			canHeal = 5.0f;
		}

		currentArmour = Mathf.Clamp(currentArmour, 0, maxArmour);
		currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
	}

	void regenerate(ref float currentStat, float maxStat)
	{
		currentStat += maxStat * 0.005f;
		Mathf.Clamp(currentStat, 0, maxStat);
	}

}

Cały kod PlayerStats.cs – Unity3d v.5.0.x:

using UnityEngine;
using System.Collections;

public class PlayerStats : MonoBehaviour {

	private float maxHealth = 100;
	private float currentHealth = 100;
	private float maxArmour = 100;
	private float currentArmour = 100;
	private float maxStamina = 100;
	private float currentStamina = 100;

	private float barWidth = 100;
	private float barHeight = 100;

	private float canHeal = 0.0f;
	private float canRegenerate = 0.0f;

	private CharacterController chCont;
	private UnityStandardAssets.Characters.FirstPerson.FirstPersonController fpsC;
	private Vector3 lastPosition;

	public Texture2D healthTexture;
	public Texture2D armourTexture;
	public Texture2D staminaTexture;

	public float walkSpeed = 10.0f;
	public float runSpeed = 20.0f;

	public GUITexture hitTexture;


	void Awake()
	{
		barHeight = Screen.height * 0.02f;
		barWidth = barHeight * 10.0f;

		chCont = GetComponent<CharacterController>();
		fpsC = gameObject.GetComponent<UnityStandardAssets.Characters.FirstPerson.FirstPersonController> ();

		lastPosition = transform.position;
	}
	
	void OnGUI()
	{
		GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
		                         Screen.height - barHeight - 10,
		                         currentHealth * barWidth / maxHealth,
		                         barHeight),
		                healthTexture);
		GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
		                         Screen.height - barHeight * 2 - 20,
		                         currentArmour * barWidth / maxArmour,
		                         barHeight),
		                armourTexture);
		GUI.DrawTexture(new Rect(Screen.width - barWidth - 10,
		                         Screen.height - barHeight * 3 - 30,
		                         currentStamina * barWidth / maxStamina,
		                         barHeight),
		                staminaTexture);
	}

	void Start()
	{
		Rect currentRes = new Rect(-Screen.width * 0.5f,
		                           -Screen.height * 0.5f,
		                           Screen.width,
		                           Screen.height);
		hitTexture.pixelInset = currentRes;
	}

	void Update()
	{
		if(Input.GetKeyDown(KeyCode.P)) {
			takeHit(30);
		}

		if(canHeal > 0.0f) {
			canHeal -= Time.deltaTime;
		}
		if(canRegenerate > 0.0f) {
			canRegenerate -= Time.deltaTime;
		}

		if(canHeal <= 0.0f && currentHealth < maxHealth) {
			regenerate(ref currentHealth, maxHealth);
		}
		if(canRegenerate <= 0.0f && currentStamina < maxStamina) {
			regenerate(ref currentStamina, maxStamina);
		}

	}

    void FixedUpdate () 
    {
	    float speed = walkSpeed;
		
	    if(chCont.isGrounded && Input.GetKey(KeyCode.LeftShift) && lastPosition != transform.position && currentStamina > 0) {
		    lastPosition = transform.position;
		    speed = runSpeed;
		    currentStamina -= 1;
		    currentStamina = Mathf.Clamp(currentStamina, 0, maxStamina);
		    canRegenerate = 5.0f;
	    }	
		
	    if (currentStamina > 0) {
		fpsC.CanRun = true;
	    } else {
		fpsC.CanRun = false;
	    }
    }

	void takeHit(float damage) 
	{

		Destroy(Instantiate(hitTexture), 0.15f);

		if(currentArmour > 0) {
			currentArmour = currentArmour - damage;
			if(currentArmour < 0) {
				currentHealth += currentArmour;
				currentArmour = 0;
			}
		} else {
			currentHealth -= damage;
		}

		if(currentHealth < maxHealth) {
			canHeal = 5.0f;
		}

		currentArmour = Mathf.Clamp(currentArmour, 0, maxArmour);
		currentHealth = Mathf.Clamp(currentHealth, 0, maxHealth);
	}

	void regenerate(ref float currentStat, float maxStat)
	{
		currentStat += maxStat * 0.005f;
		Mathf.Clamp(currentStat, 0, maxStat);
	}

}

 Poprzednia część <- #2 – Sterowanie Postacią

 Następna część -> #4  – Regeneracja życia i energii. Efekty trafienia