Przyspieszony kurs C# pod Unity3d dla leniwych i opornych.

Poprzednie lekcje – Spis Treści

Tablice (Array)

W naszych zabawach z C# omawialiśmy zmienne i stałe. Ale co w przypadku, gdy chcemy przechować większą ilość podobnych danych? Np. zawartość inwentarza postaci? Z pomocą przychodzą nam tablice.

Tablicę w programowaniu, można rozumieć jak… tabelę. Po prostu.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	private int[] tab = new int[5];

	void Start () {
	}
}

Tyle wystarczy, żeby zadeklarować tablicę. Wygląda w sumie jak zwykła zmienna, z wyjątkiem dodania nawiasów kwadratowych przy typie, jaki przechowuje tablica. Tablica, nie może przechowywać różnych typów. Tzn. jeśli deklarujemy ją teraz jako int, nie dodamy do niej stringa. (Można to obejść, deklarując typ tablicy na object. Działa to dlatego, że int, double, string etc. wszystkie są objectami. Ale nie polecam tego działania, bo zajmuje to dużo więcej pamięci).

Przykład 1.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	private int[] tab = new int[5];

	void Start () {
		tab[0] = 4;
		tab[1] = 2;
		tab[2] = 6;
		tab[3] = 9;
		tab[4] = 1;

		Debug.Log ("tab[1] = " + tab[1]);
	}
}

Pierwsze na co trzeba zwrócić uwagę to indeskowanie (czyli określanie elementu w tablicy). Elementy tablicy liczymy od 0 (ZERA). Trzeba koniecznie o tym pamiętać!

Aby ustawić jakiś element tablicy, postępujemy jak ze zmienną, pamiętając by w nawiasach kwadratowych podać indeks elementu. Aby wypisać element, znów działamy jak ze zmienną, dodając indeks. Jeżeli chodzi o usuwanie elementów, to nie możemy tego dokonać bez zmiany rozmiaru tablicy, czyli bez redeklarowania jej. Tutaj zaleca się wyzerować element tablicy lub ustawić mu wartość null.

Można również zadeklarować tablicę, od razu wprowadzając do niej elementy:

private int[] tab = new int[5] {2, 3, 5, 6, 8};

Po prostu podajemy je sobie w nawiasach klamrowych za deklaracją tablicy. Jednak w tym wypadku, musimy podać maksymalną liczbę elementów. Tzn. jak tablicę określiliśmy na 5 elementową, to musimy ją całą wypełnić podając 5 elementów. Kiedy podajemy elementy tablicy, możemy sobie nieco skrócić kod, pomijając podanie rozmiaru tablicy:

private int[] tab = {2, 3, 5, 6, 8};

Jednak to nie wszystko. To co było powyżej to jedynie tablica jednowymiarowa. Możemy sobie stworzyć również tablicę 2 wymiarową (macierz). Da radę iść w więcej wymiarów, ale ogarnięcie tego jest potem praktycznie niemożliwe, więc raczej się tego nie stosuje. Macierzy 2 wymiarowej nie będę rozkładał na czynniki pierwsze, bo różnice między nią, a tablicą jednowymiarową, są bardzo proste do ogarnięcia:

Przykład 2.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	private int[,] tab2 = new int[2, 3];
	//private int[,] tab2 = { {1, 2}, {3, 4}, {5, 6} };
	private int[][] tab = new int[2][] { new int[] {1,2,3}, new int[] {4,5,6,7,8} };

	void Start () {

		tab2 [0, 0] = 1;
		tab2 [0, 1] = 2;
		tab2 [0, 2] = 3;
		tab2 [1, 0] = 4;
		tab2 [1, 1] = 5;
		tab2 [1, 2] = 6;

		Debug.Log ("tab[1][1] = " + tab[1][1] + " tab[2, 2] = " + tab2[0, 2]);
	}
}

Tablicę 2 wymiarową, możemy deklarować dwojako. Albo z przecinkiem (lub przecinkami) w nawiasie kwadratowym, sugerując rozmiar tablicy, albo z wykorzystaniem podwójnego nawiasu. W obu przypadkach możemy wymusić rozmiar tablicy, albo deklarować go na podstawie podanych elementów. Różnic zasadniczo nie ma. Jedynie trzeba być konsekwentnym. Tzn. wykorzystywać jeden typ zapisu. Ja preferuję ten z podwójnym nawiasem.

Kolekcje (List, HashMap)

Tablice są bardzo fajne, ale mają jedną wadę, którą może już zauważyłeś. Nie są dynamiczne. Tzn. gdy tworzymy tablicę podajemy jej rozmiar. Później tego rozmiaru nie zmienimy. Co to oznacza w praktyce? Że jeśli nie jesteś w stanie z góry określić ile miejsca potrzebujesz w tablicy jesteś w… no masz kłopot. Jak dasz za mało, zwyczajnie braknie miejsca. Gdy dasz za dużo, niepotrzebnie zblokujesz obszar pamięci, który można by lepiej wykorzystać. Efekt? Gra będzie pochłaniała masę RAMu i w efekcie będzie się ścinać. Na szczęście z pomocą przychodzą nam listy.

List (Lista)

Czym jest lista? Tworem bardzo zbliżonym do tablicy jednowymiarowej. Ma jednak opcję rozszerzania się.
[stextbox id=”info” defcaption=”true”]Dlaczego lista może się rozszerzać, a tablica nie? – Uwaga: ta treść jest bardzo techniczna. Nie martw się, jeśli jej nie rozumiesz. Nie przeszkodzi Ci to w korzystaniu z List, ale uznałem, że to fajna ciekawostka, którą warto znać.

Wynika to z budowy pamięci i sposobu zajmowania miejsca. Tablice zbudowane są tak, że zajmują sobie w pamięci pewien ciągły obszar. Gdy masz zmienną reprezentującą tablicę, tak naprawdę zna ona adres pamięci gdzie zaczyna się tablica. Później zna rozmiar (w bitach) typu przechowywanych danych (np. int zajmuje w pamięci 32 bity lub 64 bity (zależnie od systemu operacyjnego)) i sobie skacze o ten rozmiar szukając wskazanego elementu.

Lista zbudowana jest nieco inaczej. Otóż każdy element listy zna nie tylko swoją wartość, ale też zna adres swojego poprzednika i następnika (czyli elementu, który jest przed nim i po nim). Co to oznacza w praktyce? Że pamięć może być alokowana (takie mądre informatyczne pojęcie na zajęcie pamięci) w dowolnym miejscu. Gdy dodajemy nowy element do listy, wystarczy ostatniemu elementowi na liście powiedzieć gdzie leży nowy ostatni element (czyli ten, który dodajemy), a nowemu elementowi podać, gdzie leży jego poprzednik (czyli stary ostatni element). Usuwanie z listy jest również proste, gdyż wystarczy ze sobą połączyć (podając im adresy) poprzednik z następnikiem usuwanego elementu i zwolnić pamięć zajmowaną przez ten element.

Z budowy obu typów wynika jeszcze kwestia szybkości. Lista musi skakać po pamięci, kiedy tablica ma wszystko blisko. Więc tablica będzie odczytywać dane szybciej. Jednak ma to miejsce jedynie gdy tablica przechowuje typy proste (podstawowe) jak int, float, double, string, bool. Czemu? Bo ich rozmiary są niskie więc skoki po pamięci są mniejsze. Jednak gdy przechowujesz dane złożone (jak własna klasa), wtedy skoki są na tyle duże, że nie ma większej różnicy czy korzystasz z listy czy tablicy.[/stextbox]
No to zobaczmy, jak wyglądają listy:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Lekcja_03 : MonoBehaviour {

	private List<int> list = new List<int>();

	void Start () {

		list.Add (1);
		list.Add (5);
		list.Add (7);
		list.Add (3);

		Debug.Log ("list[2] = " + list[2]);

		list.Remove (7);

		Debug.Log ("list[2] = " + list[2]);

		Debug.Log ("list.Cointains(3) = " + list.Contains (3));
		Debug.Log ("list.Cointains(8) = " + list.Contains (8));
	}
}

Pierwsza warta uwagi rzecz, to fakt, że aby działały nam listy, musimy sobie zaimportować dodatkową bibliotekę System.Collections.Generic.

A co z samą obsługą list? Deklaracja wygląda dość prosto. W nawiasie trójkątnym podajemy typ jaki będzie obsługiwać lista. Nawias okrągły na końcu deklaracji, wynika ze składni języka. Musi być, jeśli tworzymy nowy obiekt.

Dodawanie elementów do listy, odbywa się przez polecenie Add (dodaj), usuwanie przez Remove (usuń). Proste, nie? Wypisywanie za to, odbywa się za pomocą indeksów, tak jak w tablicy. Podałem jeszcze fajną funkcję Contains, która sprawdza czy lista zawiera wskazany element. Bardzo przydatna rzecz. Przykład jest tak skonstruowany, aby pokazać jak zachowuje się lista po usunięciu elementu:

KursC_03_01

Jak widać, gdy usuniemy element 7, to wszystkie późniejsze elementy są cofane o 1. Tym sposobem na pozycji elementu o indeksie 2, ląduje element, który wcześniej miał indeks 3. Jak można się też domyślić po sposobie usuwania, w liście nie może być 2 identycznych elementów.

Dictionary (HashTable / Tablica Asocjacyjna / Map)

Tak, istnieje aż tyle określeń na ten twór. Ale czym on w sumie jest? Jest to bardzo przydatna rzecz, ponieważ daje nam tablicę jednoelementową, ale… z kluczami zamiast indeksów. Co to oznacza? Że zamiast odwoływać się do elementu tablicy: tab[3], odwołujemy się: tab.get(“NazwaNPC”). Po co? Dla większej przejrzystości i jasności kodu.

Przykład 3.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Lekcja_03 : MonoBehaviour {

	private Dictionary<string, int> tab = new Dictionary<string, int>();

	void Start () {

		tab.Add ("Jeden", 1);
		tab.Add ("Cos", 3);
		tab.Add ("Czyrak", 7);

		Debug.Log ("tab[Cos] = " + tab["Cos"]);

		tab.Remove ("Cos");

		Debug.Log ("list.CointainsKey(Czyrak) = " + tab.ContainsKey("Czyrak"));
		Debug.Log ("list.CointainsValue(7) = " + tab.ContainsValue(7));
	}
}

Z racji, że wszystko jest analogiczne do list, wyjaśnię tylko różnicę. W nawiasie trójkątnym podajemy dwa typy. Typ klucza i typ wartości. Klucz jest tym, po czym się odwołujemy, a wartość to wartość. Zmienia się też funkcja Contains. Zostaje rozbita na dwie: ContainsKey i ContainsValue. Jedna sprawdza czy istnieje zadany klucz, a druga czy istnieje zadana wartość.

Wszystko fajnie, ale nasuwa się pytanie, kiedy korzystać z list, a kiedy z tablic? Jeżeli korzystasz z typów złożonych – np. własny obiekt, gameObject etc. nie ma w sumie różnicy czego użyjesz. Jeśli za to, chcesz przechowywać typy proste (podstawowe) jak int, double, string, bool, float etc. wtedy zaleca się skorzystanie z tablicy zwykłej. Odczyt z niej jest do 6 razy szybszy (Jeżeli przeczytałeś ciekawostkę, wiesz dlaczego).

Przy tej całej zabawie z tablicami, przydatna może być umiejętność ich sortowania. Ale o tym jest inny tutorial.

Pętle (for, while, do-while)

Przy okazji tablic, warto omówić kwestię pętli. Czym jest pętla? Blokiem kodu, który wykonuje się n razy. Gdzie n my określamy. Po co nam to? Żeby kilka razy wykonać ten sam (albo podobny) kawałek kodu.

Wspominam pętle przy okazji tablic, ponieważ skorzystanie z pętli jest najłatwiejszą metodą wypisania zawartości całej tablicy. Ogólnie dysponujemy 4 rodzajami pętli. Robią dokładnie to samo, różnią się warunkiem zakończenia – czyli tym co je przerywa.

UWAGA. Należy uważać z warunkami przy pętlach, bo bardzo łatwo zrobić pętle nieskończoną, która zawiesi Unity.

For

Najpopularniejsza i najbardziej lubiana pętla.

Przykład 4.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	//private int[] tab = new int[5] {2, 3, 5, 6, 8};

	void Start () {
		//for (int i = 0 ; i < tab.Length ; i++) {
		for (int i = 0 ; i < 10 ; i++) {
			Debug.Log ("i = " + i);
		}

	}
}

Konstrukcja jest banalnie prosta. Pomiędzy średnikami mamy 3 “zasady” pętli. Pierwsza, to ustalenie jaka zmienna będzie iteratorem (dlatego najczęściej zmienną tą nazywa się i). Ustalamy też, od jakiej wartości ona startuje. Druga to określenie jaki jest warunek końca. Np. i < 10, oznacza, że pętla będzie się wykonywać, do póki i jest mniejsze od 10. Ostatnia część to określenie co robimy z iteratorem. Najczęściej pojawia się tam: i++ lub i– (tak, możemy liczyć w dół). Ale równie dobrze może być: i *= 2.

Jak widać, nasz iterator zmiania wartość po każdym obrocie. Dlatego, najczęściej operacje w pętli są zależne od jego wartości, tak jak w naszym przykładzie.

Ważne jest, że warunek sprawdzamy tu na początku. Więc gdy zadeklarujemy pętle tak: for(int i = 0 ; i > 3 ; i++)  to wykonujemy pętle do póki i jest większe od 3. Ale zakładamy, że na początku ma wartość 0. Zatem, pętla nie wykona żadnego obrotu.

W komentarzu podałem jeszcze jeden ciekawy zapis. Wykorzystałem tam tab.Length. Taki zapis zwraca nam rozmiar tablicy, co może być bardzo przydatne, gdy chcemy ją wypisać.

Foreach

Foreach czyli dosłownie dla każdego. Pętla stworzona do obsługi tablic. Czemu? Bo nie ma w niej iteratora. Jako parametr wywołania przyjmuje tablicę, a następnie przeprowadza nas przez każdy element. Więc przykład ze zwykłego fora, możemy przerobić tak:

Przykład 5.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	private int[] tab = new int[5] {2, 3, 5, 6, 8};

	void Start () {
		foreach (int var in tab) {
			Debug.Log (var);
		}
	}
}

var to nasza zmienna przechowująca bieżącą wartość, a po słówku “in” następuje podanie tablicy w której grzebiemy. Pętla zadziała też z listą czy innymi generycznymi zbiorami danych.

While

Pętla while różni się nieco od pętli for. Podstawowa różnica to właśnie warunek zakończenia.

Przykład 6.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	void Start () {
		int i = 0;
		while (i != 3) {
			Debug.Log (i);
			i++;
		}
	}
}

Pętla nie ma konkretnego iteratora (możemy go sobie sami wstawić, ale nie jest wymagany). Po prostu funkcja while, ma pewien warunek i będzie się wykonywać, do póki nie zostanie spełniony. Taki typowy przykład wykorzystania funkcji while to sczytywanie danych z pliku po znaku. Sczytujemy je, aż trafimy na znak końca pliku.

Tutaj trzeba uważać o tyle, że gdy damy warunek, który nie ma prawa być spełniony, to zawiesimy program.

Warto tutaj zauważyć dwie rzeczy. Po pierwsze, warunek znów sprawdzamy na początku. Więc pętla nie musi wykonać żadnego obrotu. Po drugie, zauważmy pewną zależność. Pętla for wykonywała się do póki i < 10, czyli do póki warunek był prawdą. Gdy i = 10, czyli warunek i < 10 stał się fałszem, wtedy praca pętli była kończona. W przypadku while jest dokładnie tak samo.

Do-While

Ostatnia pętla to do-while. Nieznacznie różniąca się od while:

Przykład 7.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	void Start () {
		int i = 3;
		do {
			Debug.Log (i);
			i++;
		} while (i != 3);
	}
}

Na czym polega różnica? Jest dokładnie jedna. Pętla do-while wykona się co najmniej 1 raz. Warunek pętli sprawdzamy na samym końcu. Reszta zasad zostaje jak w przypadku zwykłego while.

Break i Continue

Ostatnim ciekawym elementem pętli są dyrektywy break i continue. Co one robią?

Break

Przerywa działanie pętli. Powiedzmy, że mamy fora, pracującego od i = 0 do i = 20 i w środku mnożymy elementy jakiejś tablicy. Ale gdyby wartość wyrażenia przekroczyła powiedzmy 50, to chcemy przerwać dalsze mnożenie. Wtedy możemy się posłużyć poleceniem break w towarzystwie poznanego na poprzedniej lekcji ifa.

Przykład 8.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {
	
	void Start () {
		for (int i = 0; i < 10; i++) {
			if(i == 3) {
				break;
			}
			Debug.Log ("i = " + i);
		}
		
	}
}

Continue

Powiedzmy, że jest zbliżony do breaka. Tzn. przerywa działanie pętli, ale jedynie bieżącej iteracji. Czyli olewamy wszystko co znajduje się w pętli poniżej słówka continue i wykonujemy kolejny obrót pętli.

Przykład 9.

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {
	
	void Start () {
		for (int i = 0; i < 10; i++) {
			if(i == 3) {
				continue;
			}
			Debug.Log ("i = " + i);
		}
		
	}
}

Zadanie domowe

Do wykonania zadań może się przydać funkcja losująca (i wiedza), z poprzedniej lekcji.

  1. Stwórz listę, dodaj do niej 10 losowych elementów i wypisz całą zawartość korzystając  z 2 różnych pętli.
  2. Stwórz macierz 3 na 3 i wypisz zawartość 1 i 3 kolumny, wykorzystując dowolną pętle i polecenie continue.
  3. Stwórz macierz 5 na 5 i wypisz wszystkie elementy znajdujące się na przekątnej.
  4. Stwórz 15 elementową tablicę intów wypełnioną losowymi liczbami. Wypisz pierwsze 10 za pomocą pętli for, while i foreach. (wykorzystaj polecenie break).
  5. Stwórz tablicę z 10 elementami. Wypisz wszystkie elementy parzyste.
  6. Stwórz listę, złożoną z 15 losowych elementów. Usuń z niej wszystkie elementy nieparzyste. Wypisz całą tablicę po przekształceniu.

Odpowiedzi do zadań domowych:

Na wstępie zaznaczę, że podane niżej rozwiązania, są rozwiązaniami przykładowymi. W programowaniu zawsze jest wiele ścieżek do rozwiązania problemu, jedne są lepsze inne gorsze. Te poniżej nie są ani najlepszymi, ani najgorszymi, są rozwiązaniami, które akurat przyszły mi do głowy. Jeśli rozwiązałeś zadanie inaczej, ale działa, to rozwiązałeś je dobrze. Kody mogą się różnic optymalnością wykorzystania pamięci czy mocy procesora. Dałoby się to sprawdzić obliczając czas wykonania zadania przez oba programy, jednak przy tak trywialnych zadaniach, nie będzie to zauważalna różnica.

Zadanie 1:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Lekcja_03 : MonoBehaviour {

	private List<int> list = new List<int>();

	void Start()
	{
		for (int i = 0; i < 10; i++) {
			list.Add (Random.Range(0, 10));
		}

		foreach(int item in list) {
			Debug.Log(item);
		}

		for(int i = 0 ; i < 10 ; i++) {
			Debug.Log (list[i]);
		}

	}
}

Zadanie 2:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Lekcja_03 : MonoBehaviour {

	private int[][] tab = new int[3][] { new int[] {1,2,3}, new int[] {4,5,6}, new int[] {7,8,9} };

	void Start()
	{
		for(int i = 0 ; i < tab.Length ; i++) {
			for(int j = 0 ; j < tab.Length ; j++) {
				if(j == 1) {
					continue;
				}
				Debug.Log (tab[i][j]);
			}
		}
	}
}

Zadanie 3:

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	private int[][] tab = new int[5][] { new int[] {11,12,13,14,15}, new int[] {21,22,23,24,25}, new int[] {31,32,33,34,35}, new int[] {41,42,43,44,45}, new int[] {51,52,53,54,55} };

	void Start()
	{
		for(int i = 0 ; i < tab.Length ; i++) {
				Debug.Log (tab[i][i]);
		}
	}
}

Zadanie 4:

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	private int[] tab = new int[15];

	void Start()
	{
		for(int i = 0 ; i < tab.Length ; i++) {
			tab[i] = Random.Range(0, 10);
		}

		for (int i = 0; i < tab.Length; i++) {
			if(i == 10) {
				break;
			}
			Debug.Log(tab[i]);
		}

		int count = 0;
		foreach (int item in tab) {
			if(count == 10) {
				break;
			}
			Debug.Log(item);
			count++;
		}

		count = 0;
		while(count < tab.Length) {
			if(count == 10) {
				break;
			}
			Debug.Log(tab[count]);
			count++;
		}
	}
}

Zadanie 5:

using UnityEngine;
using System.Collections;

public class Lekcja_03 : MonoBehaviour {

	private int[] tab = new int[10];

	void Start()
	{
		for(int i = 0 ; i < tab.Length ; i++) {
			tab[i] = Random.Range(0, 10);
		}

		foreach (int item in tab) {
			if(item % 2 == 0) {
				Debug.Log(item);
			}
		}

	}
}

Zadanie 6:

Z racji, że nie możemy modyfikować elementów listy, na której operujemy wewnątrz pętli, jedynym rozwiązaniem jest tutaj przepisanie elementów, które chcemy zachować do tymczasowej listy.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Lekcja_03 : MonoBehaviour {

	private List<int> list = new List<int>();
	private List<int> tmpList = new List<int>();

	void Start()
	{
		for(int i = 0 ; i < 15 ; i++) {
			list.Add(Random.Range(0, 10));
		}

		foreach (int item in list) {
			if(item % 2 == 0) {
				tmpList.Add (item);
			}
		}
		list = tmpList;

		foreach (int item in list) {
			Debug.Log(item);
		}
	}
}

Funkcja warunkowa (If, Swith-Case), operatory logiczne <- Poprzedni odcinek

Następna Lekcja -> Klasa i funkcja