Przyspieszony kurs C# pod Unity3d dla leniwych i opornych.

Poprzednie lekcje – Spis Treści

Tym razem podejmiemy się tematu ważnego, ale dość krótkiego. Omówimy sobie podstawową rzecz, na której będziemy operować w Unity niemal cały czas. Czyli czym są klasy i funkcję?

Dzisiaj temat tylko liźniemy, będzie krótko i bardzo teoretycznie. W następnej części temat sobie rozszerzymy.

Klasa

Aby dobrze zrozumieć pojęcie klasy, trzeba tak naprawdę zrozumieć trzy pojęcia: Klasa, Obiekt i Instancja.

Obiekt – Obiekty można kojarzyć z obiektami z prawdziwego świata. Mają one dwie charakterystyki: Stan oraz Zachowanie. Przykładowo: stanami człowieka będą: wiek, imię, nazwisko. Zaś jego zachowaniami, będzie spanie, chodzenie, bieganie, oddychanie. Innym przykładem może być telefon. Stanami będzie: waga, rozmiar, model procesora. Zachowania? Dzwonienie, odtwarzanie muzyki. W programowaniu mamy odpowiedniki tych dwóch parametrów. Stanami są atrybuty, a zachowaniami są funkcję.

Klasa – To jest najłatwiejsze pojęcie. Klasa będzie matrycą, mówiącą jak mamy opisać jakiś obiekt. Czyli jakie atrybuty i funkcję potrzebujemy. Np. klasa przeciwnika, będzie posiadała atrybuty mówiące, ile życia ma przeciwnik, jak się nazywa etc. Oraz np.: funkcję odpowiedzialne za patrolowanie czy atak na gracza.

Instancja – Instancja jest unikalną kopią danego obiektu. Czyli gdy mamy np. dwóch przeciwników, obaj reprezentują tą samą klasę, ale dwoma instancjami obiektu – bo mimo że opisuje ich ten sam zestaw atrybutów i klas, to są niezależni. Jeden może patrolować, a drugi z nami walczyć. Jeden może mieć 60HP, a drugi 80 HP. Z technicznego punktu widzenia, instancja charakteryzuje się tym, że rezerwuje obszar pamięci do przechowywania takiej instancji. W praktyce słowa instancja używa się dość rzadko, bo najczęściej wspominając konkretną instancję, używa się słowa obiekt.

No dobra, to jak zdefiniować sobie klasę?

using UnityEngine;
using System.Collections;

public class Lekcja_04 {

}

Powyżej mamy najłatwiejszą definicję klasy, jaką można sobie wyobrazić. Czyli: Podajemy identyfikator dostępności (o tym w późniejszej lekcji), słówko kluczowe class, oraz nazwę. Całą zawartość klasy (funkcję i atrybuty) umieszczamy sobie wewnątrz nawiasów klamrowych.

using UnityEngine;
using System.Collections;

public class Lekcja_04 : MonoBehaviour {

}

Jednak w Unity3d najczęściej pojawia się taka budowa. Wymusza to na nas Unity3d. Ten dwukropek oznacza dziedziczenie (to też omówimy później). I wszystkie klasy, które chcą korzystać z dobrodziejstw Unity (jak funkcja Start, Update itp.) muszą dziedziczyć po MonoBehaviour.

Najważniejszą jest kwestia wspomniana w pierwszej lekcji. Jeśli mamy skrypt który opisuje w całości jakąś klasę, to nazwa skryptu musi być identyczna jak nazwa klasy.

Atrybut

Jak wspominałem atrybuty określają obiekt. Brzmi poważnie, ale w praktyce są to zwykłe zmienne wewnątrz danej klasy. Definiujemy je sobie… tak jak robiliśmy to wcześniej:

using UnityEngine;
using System.Collections;

public class Lekcja_04 : MonoBehaviour {

	private int zm1 = 3;
	private string zm2;

	public float zm3;

}

Znów mamy atrybut dostępności, typ zmiennej, nazwę i ewentualną wartość początkową.

Funkcja

No i czas na funkcję. Mamy funkcję charakterystyczne dla Unity takie jak Start czy Update. Udostępnia nam je dziedziczenie po MonoBehaviour, przez co mają one określony swój wygląd i musimy się do niego stosować. Natomiast możemy sobie dopisać swoje funkcję:

using UnityEngine;
using System.Collections;

public class Lekcja_04 : MonoBehaviour {

	private int zm1 = 3;
	private string zm2;

	public float zm3;

	void Start () {
	
	}

	private int func1() {

	}

	public float func2(int attr1) {

	}
}

Budowa bardzo przypomina zmienne. Zaczynamy klasycznie od identyfikatora dostępności, potem podajemy typ zwracany (Uwaga! Jako typ zwracany, można podać void, co oznacza, że funkcja nie zwraca wartości. Jeśli jednak zdecydujemy się na jakiś typ zwracany, funkcja MUSI go zwrócić za pomocą polecenia return). W następnej kolejności mamy nazwę funkcji, a później w nawiasach okrągłych atrybuty.

Każdemu atrybutowi trzeba podać jego typ. Jeśli zdecydujemy się, że atrybut1 jest typu int, to później przy wywołaniu trzeba podać dokładnie taki typ. Możemy też podać domyślną wartość atrybutu. Jeśli w powyższym przykładzie, chcielibyśmy, aby attr1 miał domyślną wartość 3 możemy to zapisać tak:

public float func2(int attr1 = 3) {

}

Co to nam daje? Gdy wywołamy funkcję normalnie: func2(6)  to zmienna attr1 wewnątrz funkcji będzie miała wartość 6. Ale, gdy wywołamy ją: func2()  to wartość attr1 będzie wynosić 3. Gdybyśmy nie podali domyślnej wartości w drugim przypadku dostalibyśmy błąd.

Atrybutów możemy mieć tyle ile nam się podoba. Trzeba jednak pamiętać, że atrybuty posiadające domyślną wartość, muszą być na końcu. Czyli, taki zapis jest OK:  public float func2(int attr1, float attr2, string attr3 = “”, int attr4 = 3) , ale taki już nie: public float func2(int attr1 = 4, float attr2, string attr3, int attr4 = 3)  Czemu? Bo przy wywołaniu trudno pominąć pierwszy parametr. Dlatego im częściej jakiś parametr będzie przyjmował wartość domyślną, tym bardziej na końcu listy powinien być.

Klasa w klasie

Jest jeszcze jedna ciekawostka. Wewnątrz danej klasy, możemy sobie zdefiniować inną klasę.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {
	
	public class MojaKlasa
	{
		private int zm1 = 3;

		public float zm2;

		public MojaKlasa(int z1) {
			
		}

		public MojaKlasa(int z1, float z2) {

		}

		public int getZm1() {
			return zm1;
		}
	}
	
	void Start () {
		MojaKlasa mk = new MojaKlasa (4.3);		
	}
}

Cała definicja klasy wygląda tak jak omawialiśmy wcześniej, jednak w tym wypadku nie trzeba dziedziczyć po MonoBehaviour, bo takie klasy najczęściej służą do przechowywania bardziej złożonych zmiennych.

Najciekawszą rzeczą jest jednak funkcja: public MojaKlasa(int z1) oraz: public MojaKlasa(int z1, float z2) Cóż to jest? Jest to konstruktor. Czym jest konstruktor? Funkcją, która wykonuje się przy tworzeniu instnacji obiektu, czyli gdy wykonuje się ta linijka: MojaKlasa mk = new MojaKlasa ();
[stextbox id=”info” defcaption=”true”]Uważny czytelnik mógł teraz zauważyć, że gdybyśmy konstruktor zdefiniowali tak: public MojaKlasa(int z1, float z2 = null) otrzymalibyśmy taki sam efekt, co przy stworzeniu dwóch konstruktorów. Czyli można by utworzyć klasę podając: MojaKlasa mk = new MojaKlasa (5);  oraz: MojaKlasa mk = new MojaKlasa (5, 6.2);[/stextbox]
Konstruktory najczęściej służą do ustalenia startowych wartości niektórych zmiennych. W przypadku konstruktora, zasady są takie same jak w przypadku funkcji, z dwoma różnicami. Konstruktor zawsze musi być publiczny (public) oraz nazwa funkcji musi być identyczna jak nazwa klasy.

Może też zwracać uwagę fakt, że funkcja konstruktora pojawia się 2 razy. Możemy tak sobie definiować zarówno funkcję jak i konstruktory. Który zostanie wywołany, zależy od tego jakie parametry przy tworzeniu instancji poda programista później. W przykładzie powyżej, wywołany zostałby konstruktor z jednym parametrem.

Możliwa jest też taka opcja:

public class MojaKlasa
	{
		private int zm1 = 3;

		public float zm2;

		public MojaKlasa(float z2) {
			
		}

		public MojaKlasa(int z2) {

		}

		public int getZm1() {
			return zm1;
		}
	}

Jak widać konstruktory są niemal identyczne. Różnią się tylko typem atrybuty. I to właśnie fakt, jaki typ atrybuty podamy, zadecyduje, którego konstruktora użyć. Oczywiście nie ma problemu, żeby stworzyć konstruktor bezargumentowy. W ogóle istnienie konstruktora nie jest obowiązkowe.

Ważna uwaga dla klas dziedziczących po MonoBehaviour jest taka, że nie powinno się dla nich definiować konstruktorów. W najlepszym wypadku i tak zostaną nadpisane. Zamiast tego, należy używać funkcji Start i Awake.

Zadanie Domowe

Dzisiaj brak.

Pętle (For, While, Do-While), tablice (Array) i kolekcje (ArrayList, HashMap) <- Poprzedni odcinek

Następny odcinek -> Dziedziczenie, specyfikatory dostępu, rzutowanie typów