sobota, 30 kwietnia 2016

Dane użytkownika

Jak już wspomniałem w jednym z poprzednim wpisów, gracze będą mieli możliwość wybrania zestawu żetonów, z których będą korzystać w trakcie gry. Każdy gracz będzie mógł utworzyć swój własny zestaw, więc informacje o zestawach muszą być gdzieś zapisywane. Może się to dziać na sprzęcie użytkownika, ale w obecnych czasach standardem jest zapisywanie danych na serwerze, dzięki czemu gracz może mieć do nich dostęp z dowolnego komputera. W poprzednim wpisie omówiłem w jaki sposób można zapisywać dane gracza do pliku, a w tym wpisie omówię w jaki sposób wygodnie z tych danych korzystać.

Format zapisu danych

Jeśli na serwerze zapisywana jest niewielka ilość danych, to troska o ich porządek nie jest zbyt konieczna. Przykładowo jeśli plik gracza zawierałby tylko 2 krótkie linijki tekstu, to odwoływanie się do nich byłoby proste. Problem zaczyna się wtedy, gdy plik tekstowy zawiera wiele linijek kodu i dodatkowo wielkość tego pliku nie jest znana. Wtedy trzeba starać się zapisywać dane w taki sposób, aby program mógł łatwo znaleźć potrzebne jemu dane.

W mojej grze w profilu gracza będzie można zapisać wiele zestawów żetonów, a każdy żeton będzie się składał z 4 stosów żetonów, przy czym każdy stos musi się składać z co najmniej 2 kart żetonów, o numerze będącym liczbą całkowitą. Program potrzebuje więc informacje o tym, z którego zestawu chce korzystać gracz, oraz jaka jest zawartość poszczególnych zestawów żetonów.
W pliku tekstowym gracza umieszczam więc następujący tekst:

Handset
1

set 0
0 1
1 2
3 4
5 6

set 1
0 2
1 3 4
81 32
45 16 35 134

Linia "Handset" będzie dla mojego programu punktem orientacyjnym, dzięki któremu program będzie wiedział, że linijkę niżej znajduje się numer zestawu żetonów z którego korzysta użytkownik. Niżej zaś znajdują się informacje o zestawach żetonów, gdzie pojedyncza linia oznacza jeden stos kart, a numery kart są oddzielone spacjami.

Wczytywanie informacji z pliku

Gdy użytkownik dołączy się do gry, serwer będzie musiał wczytać jego dane. Całą zawartość pliku łatwo można skopiować do tablicy stringów (ciągów znaków) za pomocą jednej linijki kodu:

 string [] Lines = File.ReadAllLines (@"C:/TokenBattle/" + Nickname + ".txt");

Jednak wczytanie tych danych do programu to nie wszystko, trzeba je jeszcze zinterpretować. Nasz skrypt musi teraz znaleźć linię "Handset", aby odczytać z niższej linii numer zestawu, z którego trzeba skorzystać. Wykorzystamy do tego następujący fragment kodu:

 Array.FindIndex (Lines, s => s.Equals ("Handset"))

Funkcja Array.FindIndex jest funkcją znajdującą się w bibliotece System, więc aby z niej skorzystać trzeba zadeklarować tą bibliotekę, lub poprzedzić tą funkcję nazwą tej biblioteki. Funkcja korzysta z 2 argumentów: pierwszym argumentem jest tablica, którą chcemy przeszukać, zaś drugi argument ma typ Predicate, który składa się z argumentu będącym aktualnie sprawdzanym elementem tablicy, oraz warunku który określa, czy ten element tablicy spełnia nasze kryteria. Jeśli Predicate zwróci wartość true, to funkcja zwraca numer indeksu tablicy, który spełnił nasze kryteria. W naszym przypadku Funkcja FindIndex wyszukuje w tablicy ciąg znaków, który brzmi "Handset". Teraz nasz program wie, gdzie znajduje się informacja o wykorzystywanym zestawie żetonów, ale musi tą informację przetworzyć, do czego posłuży nam taka linia kodu:

 int HandsetNumber = int.Parse (Lines [Array.FindIndex (Lines, s => s.Equals ("Handset")) + 1]);

Tworzy ona zmienną typu int, do której chce przypisać wartość typu string (ponieważ jest to linijka tekstu z tablicy stringów), więc musimy zmienić typ zmiennej za pomocą funkcji int.Parse. Dodatkowo nie chcemy się odwołać do linii "Handset", tylko do linii następnej, więc do numeru indeksu dodajemy 1.

Następnie w podobny sposób chcemy znaleźć indeks linijki tablicy, która rozpoczyna informację o szukanym zestawie żetonów:

 int LineNumber = Array.FindIndex (Lines, s => s.Equals ("set " + HandsetNumber.ToString ()));

Teraz chcemy przepisać dane do zmiennych innego skryptu:

 for (int x = 0; x < 4; x++) {
  string [] Stack = Lines [++LineNumber].Split (' ');
  SScript ().Player [p].CardQueue [x].NumberOfCards = Stack.Length;
  SScript ().Player [p].CardQueue [x].CardNumbers = new int [Stack.Length]; 
  int y = 0;
  foreach (string number in Stack) {
   SScript ().Player [p].CardQueue [x].CardNumbers [y] = int.Parse (number);
   y++;
  }
 }

Korzystam tutaj z pętli for, żeby przepisane były 4 linijki tekstu. Chcę jednak, aby były one rozpisane w formie zmiennych typu int, a nie dość, że linie są typu string, to jeszcze sam string składa się z kilku liczb, które trzeba rozdzielić. Robimy to za pomocą funkcji Split(), która rozdziela string wszędzie tam, gdzie spotka char (zmienną typu znak) podany w argumencie tej funkcji. Rozdzielanym stringiem jest string z tablicy Lines, gdzie numerem indeksu jest int LineNumber. Dwa plusy poprzedzające zmienną LineNumber oznaczają, że zostaje ona zwiększona o 1 zanim zostanie wykorzystana (dla porównania postawienie dwóch plusów po tej zmiennej sprawiłoby, że zostałaby zwiększana o 1 dopiero po jej wykorzystaniu). Nowo powstałe stringi przypisuję do tablicy stringów Stack. 

Fragment SScript ().Player [p].CardQueue [x] określa miejsce w którym chcę zapisać informację, ogólnie to struktura w strukturze w skrypcie. W tamtym miejscu znajduje się zmienna NumberOfCards, która ma zawierać informację o ilości kart żetonów na stosie, a informację tę można zdobyć sprawdzając liczbę elementów tablicy Stack, co można dokonać funkcją Length. Parę linijek niżej korzystam z pętli foreach, która wykonuje pętlę dla każdego elementu tablicy, którą podajemy jako argument (w moim przypadku jest to tablica Stack). Nazwę tego elementu podajemy w nawiasie tej pętli.


Tym sposobem program wczytał z pliku tekstowego potrzebne mu informacje.

sobota, 23 kwietnia 2016

System logowania i rejestracji

Środek semestru niestety przyniósł ze sobą testów, do których nauka zajęła mi sporo czasu, ale mam nadzieję, że teraz będę miał więcej czasu na pisanie.

Dzisiejszy wpis poświęcę tworzeniu systemu umożliwiającego rejestrowaniu się i logowaniu na serwerze. Dotychczas do gry mogła dołączać się dowolna liczba osób, ale gra nie oferowała możliwości ponownego dołączenia się do gry - nawet jeśli do gry dołączał ten sam gracz, to gra traktowała go jako zupełnie nowego gracza. Teoretycznie można było sprawdzać adres IP graczy, ale nie pozwalało to na rozróżnienie graczy, jeśli korzystali z tej samej sieci. Potrzebowałem więc jakiegoś prostego systemu rejestracji i logowania.

Interfejs

Unity oferuje nam sporo narzędzi ułatwiających tworzenie menu i interfejsu. Jednym z takich narzędzi jest GameObject który zwie się InputField. Uproszczając jest to pole tekstowe, które można dowolnie wypełniać, posiadające sporo wbudowanych funkcji. Aby je utworzyć wystarczy wchodzić w kolejno: GameObject -> UI -> InputField. W podobny sposób tworzymy Button, który służyć nam będzie jako przycisk.

Jeśli na scenie nie istnieje obiekt typu Canvas, to przy tworzeniu elementów UI zostaje utworzony, aby elementy UI zostały do niego podpięte. Canvas jest obiektem na którym umieszcza się elementy służące jako interfejs. W edytorze sceny widzimy go jako prostokąt, który reprezentuje ekran użytkownika, dzięki czemu możemy na nim łatwo rozmieścić wszystkie elementy interfejsu.

Potrzebujemy 6 pól tekstowych i 2 przyciski, które potem przypiszemy do prefabu. Dla czytelności można je przypiąć do pustych obiektów, których nazwa będzie nas informowała o tym, które elementu będą należeć do logowania, a które do rejestracji. EventSystem jest obiektem, który umożliwia poprawne funkcjonowanie UI:


Teraz tworzymy nowy skrypt i przypisujemy go do prefabu. Dodajemy do niego zmienne w których będziemy zawierać odnośniki do poszczególnych elementów interfejsu, aby mieć do nich łatwy dostęp:

 public GameObject LoginNickname;
 public GameObject LoginPassword;
 public GameObject LoginButton;
 public GameObject RegisterNickname;
 public GameObject RegisterEmail;
 public GameObject RegisterPassword;
 public GameObject RegisterConfirmPassword;
 public GameObject RegisterButton;

Następnie w inspektorze Unity przypisujemy do zmiennych odpowiadające im obiekty poprzez przeciągnięcie ich:

Alternatywnym rozwiązaniem jest wyszukanie tych obiektów z poziomu skryptu za pomocą funkcji find. Kwestia gustu.

Co dalej: dodajemy do skryptu zmienne typu string (ciągi znaków), które będą przechowywać informację o tekście znajdującym się w InputField. Aby tekst był automatycznie przekazywany do zmiennych musimy dodać własną funkcję, jak np.:

 public void ChangeLoginNickname () {
  LoginNicknameText = LoginNickname.GetComponent<InputField> ().text;
 }

Jednak funkcja ta będzie pozostawała bezużyteczna, jeśli jej nie wywołamy. Na szczęście w InputFieldach znajduje się komponent, który umożliwia automatycznie aktywowanie funkcji podczas edytowania tekstu. Aby skorzystać z tej właściwości musimy dodać do listy nowy element zawierający informację o obiekcie posiadający interesujący nas skrypt, oraz wskazać na funkcję z tego skryptu:

W podobny sposób postępujemy z innymi polami tekstowymi oraz przyciskami. Prócz tego w polach haseł jako ContentType ustawiamy Password, dzięki czemu wpisywany w nich ciąg znaków będzie przedstawiany na ekranie jako ciąg gwiazdek.

Logowanie się i rejestracja


Rolą przycisków jest przekazanie zawartości pól tekstowych na serwer i aby to zrobić musimy znaleźć nasz utworzony wcześniej PlayerPrefab i uruchomić na nim funkcję:

 PlayerScript PScript;

 void Start () {
  PScript = GameObject.Find ("MyPlayer").GetComponent<PlayerScript> ();
 }

 public void Register () {
  PScript.CmdRegister (RegisterNicknameText, RegisterEmailText, RegisterPasswordText);
 }

Zaś funkcja w PlayerPrefabie wygląda u mnie tak:

 [Command]
 public void CmdRegister (string Nickname, string Email, string Password) {
  if (!Directory.Exists (@"C:/TokenBattle")) {
   Directory.CreateDirectory (@"C:/TokenBattle");
  }
  if (!File.Exists (@"C:/TokenBattle/" + Nickname + ".txt")) {
   File.WriteAllText (@"C:/TokenBattle/"+Nickname+".txt",Nickname+"\r\n"+Email+"\r\n"+Password);
  }
 }

Musieliśmy zastosować tutaj atrybut Command, ponieważ funkcja jest wywoływana na kliencie i ma być wykonana na serwerze. Jej argumentami są 3 zmienne reprezentujące ciągi znaków. Pierwszą wykonywaną rzeczą jest sprawdzenie, czy na serwerze istnieje folder TokenBattle na dysku C i jeśli nie istnieje, to zostaje on utworzony. Standardowo przed wyrażeniem Directory trzeba napisać System.IO, ale żeby tego nie robić dodałem na początku klasy tą bibliotekę, dzięki czemu pisanie kodu staje się łatwiejsze, a bibliotekę można dodać za pomocą linii: using System.IO;

Kolejną rzeczą którą wykonuje ta funkcja jest sprawdzenie, czy w folderze TokenBattle znajduje się plik tekstowy o nazwie użytkownika, jeśli nie istnieje, to zostaje utworzony. Funkcja File.WriteAllText zawiera 2 argumenty: lokalizację pliku tekstowego, oraz ciąg znaków który ma być w niej umieszczony. Ciągi znaków możemy ze sobą łączyć za pomocą dodawania, a nową linię tworzymy umieszczając w ciągu znaków wyrażenie \r\n. Zazwyczaj korzystając ze stringów wystarczy użyć tylko \n, ale przy zapisie ich do pliku musimy wyjątkowo dodać \r.

Nasz skrypt potrzebuje jeszcze funkcji odpowiadającej za logowanie się na serwer:

 [Command]
 public void CmdLogin (string Nickname, string Password) {
  if (File.Exists (@"C:/TokenBattle/" + Nickname + ".txt")) {
   string [] Lines;
   Lines = File.ReadAllLines (@"C:/TokenBattle/" + Nickname + ".txt");
   if (Lines [2] == Password) {
    ConnectPlayer (Nickname);
    RpcSpawnCObject ();
    DownloadData ();
   }
  }
 }

W przeciwieństwie do poprzedniej funkcji ta nie potrzebuje adresu e-mail, wymaga ona tylko nazwy użytkownika i hasła. Najpierw skrypt sprawdza, czy na serwerze istnieje plik z danymi użytkownika - jeśli taki plik nie istnieje, to znaczy że użytkownik nie jest zarejestrowany. Następnie tworzona jest tablica ciągów znaków i przypisywana jest do niej cała zawartość pliku. Kolejną rzeczą jest sprawdzenie, czy podane przez użytkownika hasło jest zgodne z drugą linijką pliku tekstowego użytkownika, czyli hasła (linie są numerowane od zera). Jeśli wszystko się zgadza, to uruchamiane są 3 stworzone przeze mnie funkcje, które kolejno: przekazują informację o podłączeniu się gracza, tworzą na podłączonym kliencie obiekt z danymi i przekazują dane o partii z serwera na klienta.

Skrypt tworzący obiekt z danymi klienta wygląda następująco:

 [ClientRpc]
 public void RpcSpawnCObject () {
  if (name == "MyPlayer") {
   Instantiate (Resources.Load ("PreCObject")).name = "CObject";
   Destroy (GameObject.Find ("PreLogin"));
  }
 }

Sprawdza on, czy nazwa PlayerPrefabu brzmi "MyPlayer" (nazwa ta była ustawiana tylko na kliencie przy tworzeniu prefabu, więc na innych klientach będzie miała inną nazwę) i jeśli następuje zgodność, to tworzony jest nowy prefab i usuwane jest tło. Dotychczas obiekt klienta z danymi był tworzony tuż po podłączeniu się użytkownika na serwer, ale opóźniłem to aby pojawiał się dopiero po zalogowaniu.

System ten można dalej rozbudowywać w zależności od potrzeb. W przyszłości dodane zostaną fragmentu kodu sprawdzające zawartość pól tekstowy, czyli ich długość, rodzaj znaków, czy zgodność obu haseł. Prócz tego przydatne byłoby komunikaty informujące o błędach przy logowaniu/rejestracji, oraz różnego rodzaju systemy szyfrujące dane, ale w fazie testów tego nie potrzebuję, więc zostawię to na później.

poniedziałek, 11 kwietnia 2016

Dostęp gracza do kart żetonów

Tym razem skupi się na projektowaniu rozgrywki. Jednym z elementów wartych przemyślenia jest dostęp gracza do określonej puli żetonów. W całej grze będzie istniała całkiem spora liczba żetonów, ale w trakcie gry gracz będzie miał dostęp tylko do niektórych z nich. Naszym zadaniem jest wykreować zasady gry w taki sposób, aby liczba dostępnych w trakcie gry żetonów nie była zbyt przytłaczająca, ale jednocześnie oferowała graczom wybór.

Przy projektowaniu warto jest rozważać jak najwięcej pomysłów, by na podstawie ich wad i zalet określić najlepsze rozwiązanie.

Pomysł 1

W trakcie gry gracz ma stały dostęp do całej puli wybranych przez siebie żetonów. Jeśli pula żetonów będzie duża, to mogą pojawić się pewne problemy z rozmieszczeniem interface'u- karty reprezentujące informacje o żetonach będą musiały być bardzo małe, aby wszystkie zmieściły się na ekranie wraz z samą planszą, a same karty byłyby też niezbyt czytelne na niskich rozdzielczościach. Takie rozwiązanie stawiałoby graczy przed bardzo szerokim wyborem żetonów. Ilość pól na planszy przeważnie będzie dosyć spora, a użycie pojedynczego żetonu może mieć inny efekt w zależności od tego w jakim miejscu planszy położymy żeton, więc gracz wybierany żeton powinien analizować każdą kombinację żetonu i pola, co może być bardzo czasochłonne i przytłaczające. Zbyt duży wybór sprawia że szukanie przez gracza właściwego rozwiązania będzie zbyt rutynowe i nudne.

Pomysł 2

W trakcie gry gracz ma jednocześnie dostęp tylko do kilku żetonów z wybranej przez siebie puli żetonów. Aby cała pula żetonów była wykorzystana muszą one być w jakiś sposób zastępowane. Niech więc każdy użyty żeton będzie zastępowany innym losowym żetonem z puli. W takiej sytuacji gracz zawsze ma dostęp tylko do kilku żetonów, dzięki czemu nie jest przytłoczony wyborem, ale jego decyzje nadal mogą mieć wpływ na rozgrywkę. Pojawia się tutaj także nowy element strategiczny: użycie żetonu sprawia, że tymczasowo traci dostęp do jego ponownego użycia, więc gracz może starać się unikać używania żetonów które prawdopodobnie bardziej przydadzą się w następnych turach. Sama losowość żetonów sprawia, że gra staje się bardziej nieprzewidywalna, a więc bardziej emocjonująca. Niestety emocje wywołana losowością mogą być zarówno pozytywne jak i negatywne - gracza może frustrować fakt, że o wyniku meczu zadecydowały nie jego umiejętności, lecz szczęście.

Pomysł 3

W trakcie gry gracz ma dostęp do kilku stosów kart żetonów. Używać można tylko kart z wierzchu stosu i po użyciu przenoszona jest na spód stosu. W takim przypadku gracz nadal ma na raz dostęp tylko do kilku żetonów, a więc nie jest przytłoczony wyborem. Dodatkowo nadal nie może użyć tego samego żetonu kilka razy pod rząd, co urozmaica rozgrywkę. Dochodzi jednak jeszcze jeden element strategiczny: żetony na każdym stosie pojawią się cyklicznie, dzięki czemu przy uważnej grze będziemy wiedzieć jakie żetony będą dla nas dostępne w następnych turach. Możemy także dać graczowi większą swobodę przy tworzeniu swojego zestawu żetonów: teraz nie tylko będzie mógł wybrać żetony do swojej puli, lecz także będzie mógł je dowolnie rozmieścić na stosach, co nie tylko daje graczom większą swobodę, lecz także pozwoli wykazać się lepszym graczom. Przykładowo na kilku stosach można by umieścić karty żetonów sytuacyjnych, które w specyficznych sytuacjach byłyby mocne, a na pozostałych stosach można by umieścić żetony uniwersalne, na których można by polegać gdyby sytuacja nie sprzyjała żetonom sytuacyjnym.


Zaletą dwóch pierwszych pomysłów z pewnością jest prostota, ale całokształt trzeciego pomysłu bardziej przypadł mi do gustu, ponieważ daje graczom największe pole do popisu. Przy tworzeniu gier ważne jest, aby jak najwcześniej przemyśleć podstawy mechaniki i wybrać jeden pomysł którego będzie się trzymało. Zmiana podstaw mechaniki w środkowym etapie tworzenia gry byłaby kłopotliwa, ponieważ wymagałaby zmiany struktury dużej części programu, co mogłoby zabrać sporo czasu. Warto jest więc programować grę w taki sposób, aby w razie zmiany planów przekształcenie algorytmów nie zajmowało dużo czasów. Dobrym pomysłem jest też programowanie gry z myślą o dodatkowych trybach rozgrywki - wtedy łatwe będzie zarówno przekształcenie gry, jak i jej rozbudowanie.