Tworzenie gier mobilnych w Flutter - szablon do tworzenia gier

Utworzony przez Łukasz Sujkowski, dnia 04.12.2022
 589    0
Tworzenie gier mobilnych w Flutter - szablon do tworzenia gier
Tak jak już wspomniałem w poprzednich artykułach, najnowsza aktualizacja technologii Flutter dostarczyła nam szablonu projektu do tworzenia gier. W tym artykule przedstawię krok po kroku jak z niego skorzystać, a następnie przeanalizuję jego strukturę.

W pierwszej kolejności musimy pobrać szablon projektu, w tym celu przechodzimy do repozytorium flutter/samples znajdujacego się pod linkiem https://github.com/flutter/samples, a następnie pobieramy je jako archiwum ZIP (zielony przycisk Code i w rozwijanym menu Download ZIP). Po rozpakowaniu powinien znajdować się tam katalog z szablonem o nazwie game_template. Kolejnym krokiem będzie utworzenie nowego projektu w Android Studio, jak to zrobić opisałem już w jednym z poprzednich artykułów. Przy zakładaniu projektu koniecznie zapamiętajcie zawartość pola Organization. Jeśli mamy już projekt musimy go odpowiednio wyczyścić, usuwamy pliki tak, aby uzyskać stan identyczny jak na poniższym zrzucie ekranu:

Struktura projektu po usunięciu niepotrzebnych plików

Następnie wklejamy do projektu całą zawartość katalogu game_template z pobranego archiwum, jeśli wystąpią jakieś konflikty, nadpisujemy pliki. Aby ułatwić sobie kopiowanie można kliknąć prawym przyciskiem myszy na dowolny plik w projekcie i wybrać Open In, a następnie Explorer (lub Finder na MacOS). Po tej operacji projekt powinien wyglądać następująco:

Struktura projektu po wklejeniu plików

Wszystko prawie gotowe do uruchomienia, jedyne co musimy jeszcze zrobić to poprawić nazwy z szablonowych na te które ustawiliśmy przy tworzeniu projektu. W tym celu wciskamy Ctrl + Shift + R, a następnie w całym projekcie zastępujemy frazę com.example.game_template, nasza nazwą pakietu - składa się ona z tego co wpisaliśmy w pole Organization przy tworzeniu projektu i nazwy projektu. W moim przypadku jest to com.unforge.games.tutorial. Po wprowadzeniu danych w odpowiednie pola, wciskamy Replace All:

Proces podmiany nazwy pakietu

Powtarzamy operację dla frazy game_template, którą zastępujemy nazwą projektu (w moim przypadku jest to tutorial). Teraz wystarczy wejść w plik pubspec.yaml i wykonać komendę pub get dostępną na pasku pod nazwą pliku. Jeśli wolimy zrobić to z poziomu konsoli, wpisujemy flutter pub get w głównym katalogu projektu. Gotowe, możemy uruchomić aplikację, jeśli wszystko jest okej, zobaczymy coś takiego:

Ekran główny szablonu

Zanim przejdziemy do analizowania kodu szablonu, otwórzmy plik pubspec.yaml i zerknijmy na sekcje dependencies.

Zawartość pliku pubspec.yaml

Jak widać domyślnie mamy zainstalowanych kilka pakietów. Pierwsze sześć pakietów jest niezbędne, kolejne są opcjonalne, zalecam jednak pozostawienie ich, póki nie zakończymy analizowania projektu:

  • audioplayers - pakiet umożliwia odtwarzanie wielu dźwięków jednocześnie.
  • cupertino_icons - pakiet dostarczający ikon.
  • go_router - rozszerza funkcje nawigowania po aplikacji.
  • logging - pakiet dostarczający funkcji logowania zdarzeń.
  • provider - pakiet do zarządzania stanem aplikacji.
  • shared_preferences - służy do zapisu prostych danych takich jak ustawienia.
  • firebase_core - integruje aplikacje z usługa Firebase.
  • firebase_crashlytics - integruję aplikację z usługa logowania i analizy błędów aplikacji.
  • game_services - integruję aplikacje z usługami Google Play (osiągnięcia, tablice wyników itd.).
  • google_mobile_ads - pakiet do obsługi reklam Google AdMob.
  • in_app_purchase - pakiet do obsługi mikropłatności Google Play.

Zależności omówione, przechodzimy do analizy kodu. Jak widać w katalogi lib znajdują się dwa pliki. Pierwszy firebase_options.dart jest plikiem generowany przez bibliotekę Firebase, drugi main.dart to nic innego jak główny plik naszej aplikacji i to właśnie od jego analizy zaczniemy. Po otwarciu pliku, przechodzimy do metody głównej main, jak widać zawiera ona wykomentowaną inicjalizację usługi FirebaseCrashlytics. Dalej mamy wywołanie metody guardedMain przejdźmy do niej (prawda jest taka, że skoro nie używamy Firebase, obecną metodę main moglibyśmy usunąć, a nazwę guardedMain zmienić na main). 

Konfiguracja loggera

Pierwszy blok kodu odpowiedzialny jest za konfigurację logowania komunikatów. Nadpisuje on również poziom komunikatów logowanych w środowisku produkcyjnym.

Ustawienie trybu ekranu

Kolejna istotna operacją tej metody jest ustawienie trybu wyświetlania na pełnoekranowy. W każdym momencie możemy wejść w SystemUiMode korzystając ze skrótu Ctrl + lewy przycisk myszy i sprawdzić inne dostępne opcje. Dalej znajdują się trzy zakomentowane bloki kodu, tyczą się one inicjalizacji kontrolerów kolejno reklam AdMob, usług i mikropłatności Google Play. Jeśli chcielibyśmy skorzystać z którejś z funkcjonalności, należałoby jest odkomentować. 

Implementacja polecenia runApp

Ostatnim poleceniem w metodzie guardedMain jest standardowy runApp, uruchamiający naszą aplikację na widgecie myApp do którego przekazujemy kilka parametrów, są to kolejno: LocalStorageSettingsPersistence obsługujący zapis ustawień użytkownika w lokalnej pamięci aplikacji,  LocalStoragePlayerProgressPersistence obsługujący zapis progresu gry i trzy kontrolery o których pisałem na początku, w naszym wypadku nie zostały one zainicjalizowane więc przekazany zostanie null.

Zaraz pod metodą guardedMain znajduje się definicja bezstanowego widgetu MyApp, w którym na samej górze znajduje się inicjalizacja zmiennej router. Przechowuje ona widget GoRouter odpowiedzialny za konfigurację nawigowania po aplikacji. Analizując ten element należy zwrócić uwagę na drzewiastą strukturę i wielokrotne wykorzystanie widgetu GoRoute który definiuje konkretne miejsce w aplikacji. Widget ten przyjmuje parametry:   

  • path - określający ścieżkę danego miejsca w aplikacji, może ona zawierać dynamiczne parametry tak jak na przykład w ‘session/:level’.
  • pageBuilder - będący funkcją budującą widok danego miejsca w aplikacji, normalnie powinna ona zwrócić widget, który chcemy wyświetlić, jednak w tym wypadku zwraca funkcję buildMyTransition generującą animowane przejście, a w niej widget który zostanie wyświetlony po tym przejściu.

Poniżej konfiguracji routera znajdują się zmienne elementów przekazanych do widgetu w metodzie guardedMain oraz konstruktor w którym elementy te zostały oznaczone jako wymagane. Dalej widzimy metodę build głównego widgetu aplikacji:

Metoda build zawierająca MultiProvider

Pierwszym widgetem w drzewie jest AppLifecycleObserver, odpowiada on za obsługę zmiany stanu naszej aplikacji. Przykładowo z jego wykorzystaniem możemy zareagować na minimalizację aplikacji. Widget ten jest utworzony w ramach szablonu (nie jest on domyślnym widgetem). Dalej widzimy MultiProvider pochodzący z pakietu provider. Najprościej mówiąc, umożliwia on dostęp do kontrolerów oraz ich stanów wymienionych w tablicy parametru providers, zagnieżdżonym w nim widgetach. Przykładowo na liście znajduje się AdsController, dzięki czemu w dowolnej chwili będziemy mogli wyświetlić reklamę. Idąc dalej, w parametrze child widzimy Builder który jest pozwala na wykonanie operacji, a po tym zwrócenie kolejnych widgetów drzewa. W tym przypadku Builder zwraca MaterialApp, jednak wygląda to troszkę inaczej niż w poprzednim artykule, bowiem tym razem korzystamy z wcześniej zdefiniowanego routera (zmienna _router). Umożliwi to przełączanie się między ekranami aplikacji, a tym który zostanie wyświetlony po uruchomieniu będzie zdefiniowany na samej górze routera MainMenuScreen:

Podstawowa ścieżka routera Go

Bazowy plik projektu za nami, teraz pora przyjrzeć się plikom znajdującym się w katalogu src:

Pliki ekranów w szablonie

Tutaj chciałbym zwrócić uwagę na zaznaczone przeze mnie katalogi. W nich znajdują się widgety wskazane w routerze, cała reszta to komponenty wykorzystywane przez te pięć głównych ekranów aplikacji. Zanim przejdziemy do analizowania konkretnych plików, zapoznajmy się z zawartością powyższych katalogów zaczynając od góry:

  • ads - zawiera AdsController służący do obsługi wyświetlania reklam oraz widget banneru reklamowego.
  • app_lifecycle - zawiera widget AppLifecycleObserver, o którym wspominałem wcześniej.
  • audio - zawiera widget AudioController, który implementuje wszystkie funkcje potrzebne do odtwarzania muzyki i dźwięków.
  • crashlytics - zawierający kod do obsługi usługi FirebaseCrashlytics.
  • game_internals - zawiera klasę LevelState reprezentującą stan rozgrywki danego poziomu.
  • games_services - zawiera GamesServicesController umożliwiający obsługę usług Google Play czyli osiągnięć i tabeli wyników.
  • in_app_purchase - zawiera InAppPurchaseController do obsługi produktów i zakupów w aplikacji.
  • player_progress - zawiera klasę PlayerProgress umożliwia zarządzanie stanem rozgrywki oraz zapis tych danych do pamięci.
  • style - znajdują się w nim pliki zawierające zmienne i widgety wpływające na wygląd gry.
  • main_menu - zawiera ekran menu głównego.
  • settings - zawiera ekran ustawień gry oraz kontroler odpowiedzialny za zapis tych ustawień.
  • level_selection - zawiera ekran wyboru poziomu.
  • play_session - zawiera ekran rozgrywki.
  • win_game - zawiera ekran wyświetlany po ukończeniu poziomu.

Jako, że wiele elementów ekranów tego szablonu będzie się powtarzało, w kolejnej części artykułu przeanalizuję tylko wybrane mechanizmy. Kolejność analizowanych plików będzie taka jak podczas rozgrywki, zaczniemy od menu głównego a skończymy na ekranie ukończonego poziomu. Przejdźmy do pliku lib/src/main_menu/main_menu.dart i metody build:

Metoda build głównego ekranu

Pierwsze cztery linijki metody rozpoczynają nasłuchiwanie stanu kontrolerów dostarczonych przez widget MultiProvider jeszcze w pliku main.dart, będą one wykorzystywane przy tworzeniu tego ekranu. Dalej mamy zwrócony widget Scaffold, a w nim korzystamy z ResponsiveScreen którego definicja znajduje się w folderze lib/src/styles. Menu znajduje się komponencie Column zwróconym w parametrze rectangularMenuArea.

Zasada działania routera - przycisk

Dalej widzimy kilka nieskomplikowanych przycisków. W przypadku przycisku z powyższego zrzutu ekranu, po kliknięciu wykonujemy operację GoRouter.of(context).go('/settings'), która powoduje przejście do ekranu SettingsScreen. Nieco ciekawiej wygląda przycisk wyciszenia dźwięku: 

Przycisk wyciszenia dźwięku

W tym przypadku widget ValueListenableBuilder nasłuchuje zmiany pola muted w kontrolerze SettingsController i na tej podstawie renderuje widget IconButton, w zależności od stanu zobaczymy inną ikonkę. Dodatkowo metoda onPressed wykonuje metodę toggleMutted tego samego kontrolera, co spowoduje zmianę stanu muted oraz wyciszy lub przywrócić dźwięk.

Otworzenie tabeli wyników

Zwróćmy też uwagę na wywołanie tablicy wyników, która pochodzi z pakietu game_services, w tym przypadku ekranu dostarcza nam pakiet dlatego jedyne co musimy zrobić to wywołać metodę showLeaderboard” Widget przycisku otacza lokalny widget _hideUntilReady, który powoduje, że przycisk zostanie wyświetlony dopiero gry kontroler GamesServicesController będzie gotowy. Przejdźmy do kolejnego pliku lib/src/settings/settings_screen.dart, w którym chciałbym zwrócić uwagę na jeden fragment kodu:

Przycisk zerowania progresu gracza

Powyższy kod dotyczy przycisku zerowania progresu gracza dostępnego w ustawieniach. Wygląda podobnie jak poprzednie, z jedną różnicą, po operacji wyzerowania wyświetlony jest snackBar, czyli dymek informacyjny wysuwający się z dołu ekranu. Kolejny fragment na który chciałbym zwrócić uwagę jest kod generujący listę poziomów znajdujący się w pliku lib/src/level_selection/level_selection_screen.dart:

Kod generujący listę poziomów

Pierwszym widgetem w powyższym fragmencie jest Expanded jest to widget wypełniający całą dostępną przestrzeń w kolumnie. W nim zagnieżdżony jest ListView, jest to nic innego jak widok listy. Pętlą for obsługujemy zawartość tablicy gameLevels, która pochodzi z pliku levels.dart znajdującego się w tym samym folderze. Dla każdego poziomu do listy dodajemy jedną pozycję za pomocą widgetu ListTile. Parametr enabled określa czy przycisk jest aktywny, w tym przypadku zależy to od najwyższego osiągniętego przez gracza poziomu. Dalej mamy akcję onTap wywoływaną przy wciśnięciu przycisku - w tym wypadku odtworzony zostanie dźwięk StxType.buttonTap, a następnie uruchomiony widget rozgrywki z odpowiednim numerem poziomu level.number przekazanym w parametrze. Ostatnie dwa parametry widgetu ListTile dotyczą wyświetlanej nazwy. Ekran wyboru poziomu przeanalizowany, przejdźmy teraz do ekranu rozgrywki znajdującego się w pliku lib/src/play_session/play_session_screen.dart:

Kod ekranu rozgrywki

Powyższy fragment to zawartość metody build, która zwraca MultiProvider odpowiedzialny za dostarczenie klasy stanu LevelState do widgetów w nim zagnieżdżonych. Przyjmuje ona dwa parametry: goal określający cel rozgrywki i onWin będący funkcja wywoływaną w momencie osiągnięcia celu. 

Fragment kodu rozgrywki

Kolejny fragment przedstawia sposób wykorzystania dostarczanej przez MultiProvider klasy. Korzystając z widgetu Consumer reagujemy na zmiany zachodzące w LevelState poprzez odświeżenie podrzędnych widgetów. W tym przypadku jest to Slider, który w parametrze value przyjmuje obecną wartość progresu. Dalej widzimy obsługę zdarzenia przesuwania slidera onChanged, oraz zdarzenia zakończenia przesuwania slidera onChangeEnd. Wywołują one metody zawarte w LevelState kolejno setProgress ustawiającą progres rozgrywki oraz evaluate weryfikującą czy cel został osiągnięty. Jeżeli metoda evaluate wykryje, że cel poziomu zostanie osiągnięty, wywoła funkcję przekazaną wcześniej parametrem onWin w tym przypadku była to funkcja _playerWon, której definicja znajduje się na samym dole tego pliku. Funkcja ta, wykonuje kilka czynności takich jak zapis wyniku, odtworzenie dźwięku, a następnie przechodzi do ekranu podsumowania rozgrywki. Znajduje się on w pliku lib/src/win_game/win_game_screen.dart, a jedyną jego funkcją jest wyświetlanie danych obiektu Score przekazanego parametrem z poprzedniego ekranu. 

Myślę, że na tym możemy zakończyć analizę kodu, gdyby była taka potrzeba wrócimy do pozostałych plików w kolejnych artykułach.

Komentarze

This site is protected by ReCaptcha and the Google Privacy Policy and Terms of Service apply.

Brak komentarzy...

Kategorie

Quixel  0
Blender  3
Unity  0
Flutter  5
Ogólne  3