Kompleksowy samouczek dotyczący płomienia (lub jak tworzyć gry z trzepotem)

Wprowadzenie

Cześć wszystkim! Jestem Luan i witam w tym pierwszym obszernym samouczku dotyczącym płomienia.

Flame to minimalistyczny silnik gry Flutter, który zapewnia kilka modułów do stworzenia gry opartej na kanwie.

W tym samouczku stworzymy bardzo prostą grę, w której pudełka spadną, a celem jest ich zniszczenie, zanim trafią one na dół ekranu.

Tak będzie wyglądać gra

Możesz sam sprawdzić grę, aby zobaczyć, co zamierzamy zainstalować ten pakiet APK lub zainstalować go ze Sklepu Play.

Pozwoli nam to objąć wszystkie funkcje dostarczone przez framework i zademonstrować, jak wykonywać najbardziej podstawowe operacje: renderowanie, duszki, audio, tekst, animacje i więcej.

Dlaczego wybraliśmy tę grę? Oprócz tego, że jest bardzo prosty, ale kompletny do eksploracji frameworka, jednym dobrym czynnikiem jest to, że mamy na to środki!

W tej grze użyjemy następujących zasobów: jednego duszka skrzyni, jednego duszka eksplozji (z animacją), dźwięku eksplozji, dźwięku „chybienia” (na wypadek, gdy pudełko nie zostanie uderzone), muzyki w tle, a także ładna czcionka do renderowania partytury.

Trudno jest znaleźć dobre zasoby komercyjnie dostępne w Internecie, ale znaleźliśmy wszystko (oprócz czcionki) w tej niesamowitej witrynie. Są naprawdę świetne, absolutnie zalecane.

Na razie arkusze sprite nie są obsługiwane, więc najpierw musimy przekonwertować wszystkie zasoby na odpowiednie formaty. Musimy także przekonwertować cały dźwięk do formatu MP3 (OGG jest również obsługiwany; WAV nie jest). Po tym wszystkim możesz pobrać pakiet zawierający wszystko, czego potrzebujesz pod względem zasobów.

Warto również wspomnieć, że wszystko opisane tutaj jest zatwierdzone, jako w pełni funkcjonalna pełna wersja, na GitHub. W razie wątpliwości zawsze możesz rzucić okiem. Ponadto przykład został utworzony po wykonaniu tych dokładnych kroków i przyjmowaniu częstych zmian jako migawek. W całym samouczku połączę określone zatwierdzenia, które przesuwają repozytorium do danego etapu. Umożliwi to tworzenie punktów kontrolnych i przeglądanie starego kodu, aby zobaczyć wszystko, co nie było jasne.

Ostatnia rzecz; możesz sprawdzić pełną dokumentację Flame tutaj. Jeśli masz jakieś pytania, sugestie, błędy, otwórz problem lub skontaktuj się ze mną.

Oprócz tego będziesz musiał zainstalować Flutter i Dart. Jeśli Ci odpowiada, możesz także skorzystać z IntelliJ IDEA, która jest bardzo dobrym miejscem do pisania kodu. Aby zainstalować te rzeczy, możesz sprawdzić mnóstwo samouczków, takich jak ten.

Podstawy

Na razie zakładam, że masz wszystko gotowe. Więc po prostu uruchom następujące i otwórz to!

Pierwszą rzeczą, którą musisz zrobić, to dodać zależność od płomienia. Przejdź do pliku pubspec.yaml i upewnij się, że listy kluczy zależności są aktywne:

Korzystamy z najnowszej wersji 0.5.0, ale możesz także wybrać nową, jeśli jest dostępna.

Teraz twój plik main.dart zawiera już wiele rzeczy: metodę „główną”, którą należy zachować; oraz kolejne wywołanie metody runApp. Ta metoda pobiera widżety i inne składniki Flutter, które są używane do tworzenia ekranów aplikacji. Ponieważ tworzymy grę, narysujemy wszystko na płótnie i nie będziemy używać tych elementów; więc rozbierz to wszystko.

Nasza główna metoda jest teraz pusta i dodamy dwie rzeczy; po pierwsze, trochę konfiguracji:

Import flame.dart daje dostęp do klasy statycznej Flame, która jest tylko uchwytem dla kilku innych przydatnych klas. Będziemy wykorzystywać więcej tego później. Na razie nazywamy dwie metody w tej klasie Flame.

Ten ostatni jest oczywisty, wyłącza część logowania z wtyczki audioplayers. Wkrótce dodamy do gry dźwięk, a jeśli to nie zadziała, to tutaj należy skomentować, aby rozwiązać problem. Ale w końcu się tam dostaniemy.

Pierwsza linia jest bardziej złożona. Zasadniczo niektóre kluczowe funkcje Fluttera są nieobecne, ponieważ nie używamy metody runApp. To wywołanie enableEvents robi pewne obejście, aby uzyskać to, co jest niezbędne dla każdej aplikacji, bez potrzeby używania widżetów.

Wreszcie musimy rozpocząć naszą grę. Aby to zrobić, dodamy jeszcze jedną klasę do listy importów, klasę gry. Ta klasa zapewnia abstrakcję niezbędną do stworzenia dowolnej gry: pętli gry. Musi być podklasowany, abyś musiał wdrożyć podstawy każdej gry: metodę aktualizacji, która jest wywoływana, gdy jest to wygodne i zajmuje czas od ostatniej aktualizacji, oraz metodę renderowania, która musi wiedzieć, jak narysować aktualny stan gry. Wewnętrzne funkcjonowanie pętli pozostaje do rozwiązania przez klasę gry (możesz oczywiście spojrzeć, to bardzo proste) i wystarczy zadzwonić od początku, cóż, od początku.

Punkt kontrolny: 599f809

W tej chwili renderowanie nic nie robi, więc jeśli go uruchomisz, powinien uruchomić się, ale dać czarny ekran. Słodkie! Mamy więc funkcjonalną aplikację bez żadnych widżetów i innych elementów oraz puste płótno, aby rozpocząć rysowanie naszej aplikacji.

Renderowanie kształtów

A jak się robi rysowanie? Narysujmy prosty prostokąt, aby zobaczyć go w akcji. Dodaj następujące metody do metody renderowania:

Tutaj, jak widać, definiujemy prostokąt na podstawie pozycji ekranu. Poniższy obraz pokazuje, jak reguły są zorientowane. Zasadniczo początek znajduje się w lewym górnym rogu, a oś rośnie w prawo i w dół.

Pamiętaj też, że większość metod rysowania wymaga Malowania. Farba to nie tylko jeden kolor, ale może to być Degradè lub inne tekstury. Zwykle chciałbyś mieć jednolity kolor lub przejść bezpośrednio do duszka. Dlatego właśnie ustawiliśmy kolor wewnątrz farby na wystąpienie koloru.

Kolor reprezentuje pojedynczy kolor ARGB; tworzysz go za pomocą liczby całkowitej, którą możesz zapisać w formacie szesnastkowym, aby ułatwić czytanie; jest w formacie A (alfa, przezroczystość, zwykle 0xFF), a następnie dwie cyfry R, ​​G i B w tej kolejności.

Istnieje również kolekcja nazwanych kolorów; jest jednak w pakiecie materiałowym. Tylko uważaj, aby zaimportować tylko moduł Kolory, aby nie przypadkowo użyć czegoś innego z pakietu materiałów.

Świetnie, teraz mamy kwadrat!

Punkt kontrolny: 4eff3bf

Wiemy również, jak działają linijki, ale nie znamy wymiarów ekranu! Jak narysujemy coś w pozostałych trzech rogach bez tych informacji? Nie obawiaj się, ponieważ Flame ma metodę pobierania rzeczywistego wymiaru ekranu (to dlatego, że wokół tego jest udokumentowany problem.

Zasadniczo metoda asynchroniczna

Pamiętaj, że słowa kluczowego „czekaj”, podobnie jak w JavaScript, można używać tylko w funkcji asynchronicznej, więc upewnij się, że wykonujesz swoją główną asynchronię (Flutter nie będzie tego obchodził).

Następny punkt kontrolny pobierze wymiary raz w głównej metodzie i zapisze je w naszej klasie gier, ponieważ będziemy ich potrzebować wielokrotnie.

Punkt kontrolny: a1f9df3

Renderowanie duszków

W końcu wiemy, jak narysować dowolny kształt w dowolnym miejscu na ekranie. Ale chcemy sprajty! Następny punkt kontrolny dodaje niektóre zasoby, które będziemy używać w odpowiednim folderze zasobów:

Punkt kontrolny: 92ebfd9

A następna robi jedną kluczową rzecz, o której nie możesz zapomnieć: dodaj wszystko do pliku pubsepc.yaml. Kiedy tworzony jest kod, Dart połączy w nim tylko zasoby, które tam określisz.

Checkpoint cf5975f

Wreszcie jesteśmy gotowi narysować naszego duszka. Podstawowym sposobem, w jaki Flame pozwala to zrobić, jest ujawnienie metody Flame.images.load („ścieżka z folderu obrazów”), która zwraca obietnicę dla załadowanego obrazu, którą można następnie narysować za pomocą metody canvas.drawImage.

Jednak w przypadku rysowania skrzynki jest to o wiele prostsze, ponieważ możemy użyć klasy SpriteComponent, w następujący sposób:

Klasa abstrakcyjna Component to interfejs z dwiema metodami, renderowania i aktualizacji, podobnie jak nasza gra. Chodzi o to, że Gra może składać się z Komponentów, których metody renderowania i aktualizacji są wywoływane w ramach metod Gry. SpriteComponent to implementacja, która renderuje duszka, biorąc pod uwagę jego nazwę i rozmiar (kwadratowy lub prostokątny), pozycję (x, y) i kąt obrotu. Odpowiednio zmniejszy lub powiększy obraz, aby dopasować go do pożądanego rozmiaru.

W takim przypadku ładujemy plik „crate.png”, który musi znajdować się w folderze asset / images, i mieć klasę Crate, która rysuje pola o wymiarach 128 x 128 pikseli o kącie obrotu 0.

Następnie dodajemy właściwość Crate do gry, tworzymy ją u góry ekranu, wyśrodkowujemy w poziomie i renderujemy w pętli gry:

To sprawi, że nasza Skrzynia! Niesamowite! Kod jest dość zwięzły i łatwy do odczytania.

Punkt kontrolny 7603ca4

Aktualizacja stanu w pętli gry

Nasza skrzynia jest prawie zatrzymana w powietrzu. Chcemy to przenieść! Każda skrzynia spadnie ze stałą prędkością w dół. Musimy to zrobić w naszej metodzie aktualizacji; wystarczy zmienić pozycję Y pojedynczej skrzynki, którą mamy:

Ta metoda zajmuje czas (w sekundach) wymagany od ostatniej aktualizacji. Zwykle będzie to bardzo małe (rząd 10 ms). Więc SPEED jest stałą w tych jednostkach; w naszym przypadku SPEED = 100 pikseli / sekundę.

Punkt kontrolny: 452dc40

Obsługa danych wejściowych

Hurra! Skrzynie spadają i znikają, ale nie możesz z nimi wchodzić w interakcje. Dodajmy metodę niszczenia skrzynek, które dotykamy. W tym celu użyjemy zdarzenia okna. Obiekt okna jest dostępny w każdym projekcie Flutter na całym świecie i ma kilka przydatnych właściwości. Zamierzamy zarejestrować główną metodą zdarzenie onPointerDataPacket, to znaczy, gdy użytkownik dotknie ekranu:

Po prostu wyodrębniamy współrzędną (x, y) kliknięcia i przekazujemy ją bezpośrednio do naszej Gry; w ten sposób gra może obsługiwać kliknięcie bez martwienia się o szczegóły wydarzeń.

Aby uczynić rzeczy bardziej interesującymi, dokonajmy również refaktoryzacji klasy Gry, aby mieć listę skrzyń zamiast jednej. W końcu tego właśnie chcemy. Zastępujemy metody renderowania i aktualizacji forEach nad skrzynkami, a nowa metoda wprowadzania danych staje się:

Punkt kontrolny: 364a6c2

Renderowanie wielu duszków

Jest tu jedna ważna kwestia, o której należy wspomnieć, i dotyczy ona metody renderowania. Kiedy renderujemy skrzynię, stan płótna jest tłumaczony i obracany dowolnie, aby umożliwić rysowanie. Ponieważ zamierzamy narysować kilka skrzynek, musimy zresetować Płótno między każdą narysowaną. Dokonuje się tego za pomocą metod save, które zapisują bieżący stan, i przywracają, który przywraca poprzednio zapisany stan, usuwając go.

To ważna uwaga, ponieważ jest źródłem wielu dziwnych błędów. Może powinniśmy to zrobić automatycznie przy każdym renderowaniu? Nie wiem co myślisz

Teraz chcemy więcej skrzyń! Jak to zrobić? Metoda aktualizacji może być naszym zegarem. Chcemy więc, aby nowa skrzynia była dodawana do listy (spawnowana) co sekundę. Dlatego stworzyliśmy kolejną zmienną w klasie Game, aby kumulować czasy delta (t) z każdego wywołania aktualizacji. Kiedy przekroczy 1, resetuje się i odradza się nowa skrzynia:

Nie zapomnij zachować poprzedniej aktualizacji, aby skrzynki nie przestały spadać. Ponadto zmieniamy prędkość na 250 pikseli / sekundę, aby uczynić rzeczy nieco bardziej interesującymi.

Punkt kontrolny: 3932372

Animacje renderowania

To powinien być GIF, prawda? Pracujemy nad konfiguracją lepszych zrzutów ekranu i GIF-ów dla tego samouczka!

Teraz znamy podstawy obsługi i renderowania sprite'ów. Przejdźmy do następnego kroku: Wybuchy! Jaka gra jest dobra bez nich? Eksplozja jest bestią innego rodzaju, ponieważ ma animację. Animacje w Płomieniu są wykonywane po prostu przez renderowanie różnych rzeczy podczas renderowania zgodnie z bieżącym tyknięciem. W ten sam sposób, w jaki dodaliśmy ręcznie robiony timer do odradzania skrzynek, dodamy właściwość lifeTime dla każdego wybuchu. Ponadto Explosion nie odziedziczy po SpriteComponent, ponieważ dla tego ostatniego może istnieć tylko jeden Sprite. Zamierzamy rozszerzyć nadklasę, PositionComponent i zaimplementować renderowanie za pomocą Flame.image.load.

Ponieważ każda eksplozja ma wiele ramek i należy je narysować szybko, zamierzamy wstępnie załadować każdą ramkę raz i zapisać w zmiennej statycznej w klasie Explosion; tak:

Pamiętaj, że ładujemy każdą z naszych 7 klatek animacji w kolejności. Następnie w metodzie renderowania dokonujemy prostej logiki, aby zdecydować, którą ramkę narysować:

Pamiętaj, że rysujemy „ręcznie” za pomocą drawImageRect, jak wyjaśniono wcześniej. Ten kod jest podobny do tego, co robi SpriteComponent pod maską. Zauważ też, że jeśli obraz nie znajduje się w tablicy, nic nie jest rysowane - więc po upływie CZASU sekund (ustawiamy go na 0,75 lub 750 ms), nic nie jest renderowane.

To dobrze i dobrze, ale nie chcemy dalej zanieczyszczać naszej tablicy eksplozji eksplozjami eksplozji, dlatego dodajemy również metodę destroy (), która zwraca, na podstawie lifeTime, czy powinniśmy zniszczyć obiekt wybuchu.

Na koniec aktualizujemy naszą grę, dodając Listę wybuchów, renderując je metodą renderowania, a następnie aktualizując metodę aktualizacji. Muszą być aktualizowane, aby wydłużyć ich czas życia. Poświęcamy również ten czas na przeformułowanie tego, co poprzednio było w metodzie Game.update, tj. Powoduje, że pudełka spadają, aby znaleźć się w metodzie Crate.update, ponieważ jest to obowiązkiem skrzynki. Teraz aktualizacja gry przekazuje tylko innym. Wreszcie w aktualizacji musimy usunąć z listy to, co zostało zniszczone. W tym celu List udostępnia bardzo przydatną metodę, removeWhere:

Użyliśmy tego już w metodzie wprowadzania, aby usunąć pola dotknięte z tablicy. Tam również stworzymy eksplozję.

Spójrz na punkt kontrolny, aby uzyskać więcej informacji.

Punkt kontrolny: d8c30ad

Odtwarzanie audio

W następnym zatwierdzeniu w końcu zagramy dźwięk! Aby to zrobić, musisz dodać plik do folderu zasobów, wewnątrz zasobów / audio /. Musi to być plik MP3 lub OGG. Następnie w dowolnym miejscu kodu uruchom:

Gdzie nazwa_pliku.mp3 to nazwa pliku w środku. W naszym przypadku po kliknięciu w pole odtworzymy dźwięk wybuchu mp3.

Ponadto zacznijmy przyznawać interpunkcję. Dodajemy zmienną punktową, aby pomieścić bieżącą liczbę punktów. Zaczyna się od zera; otrzymujemy 10 punktów za kliknięcie pola i tracimy 20, gdy pole uderzy o ziemię.

Mamy teraz obowiązek radzenia sobie z niekontrolowanymi skrzynkami. W oparciu o to, co zrobiliśmy z klasą Explosion, dodajemy metodę zniszczenia dla skrzyni, która zwróci, czy są poza ekranem. To zaczyna stać się wzorem! W przypadku zniszczenia usuwamy z tablicy i dostosowujemy punkty.

Na razie punktacja działa, ale nigdzie nie jest wyświetlana; to już niedługo.

Dźwięk nie będzie działał w następnym punkcie kontrolnym, ponieważ zapomniałem dodać pliki i umieścić je w pubspec.yaml; zrobiono to w poniższym zatwierdzeniu.

Punkt kontrolny: 43a7570

Teraz chcemy więcej dźwięków! Audioplayerzy (pamiętaj o tym), których używa Flame, pozwalają na odtwarzanie wielu dźwięków na raz, co mogłeś już zauważyć, jeśli wpadłeś w szał kliknięcia, ale teraz wykorzystajmy to na naszą korzyść, odtwarzając dźwięk braku, gdy pudełko uderza w ziemię (zniszczyć metodę skrzyni) i podkład muzyczny.

Aby odtwarzać muzykę w tle w pętli, użyj metody pętli, która działa tak jak wcześniej:

W tym zatwierdzeniu naprawiamy również warunek zniszczenia skrzyń, który przeoczyliśmy w poprzednim zatwierdzeniu (ponieważ nie było sposobu, aby się dowiedzieć, teraz jest dźwięk).

Punkt kontrolny: f575150

Renderowanie tekstu

Teraz, gdy mamy wszystko, czego chcemy (tło, muzyka, efekty dźwiękowe, MP3, OGG, pętla, jednocześnie), przejdźmy do renderowania tekstu. W końcu musimy zobaczyć ten wynik. Sposób „ręcznie” polega na utworzeniu obiektu akapitowego i użyciu drawParagraph obszaru roboczego. Wymaga dużej konfiguracji i jest dość mylącym interfejsem API, ale można to osiągnąć w następujący sposób:

Punkt kontrolny: e09221e

Jest rysowany czcionką domyślną i można użyć właściwości fontFamily, aby określić inną wspólną czcionkę systemową; jednak prawdopodobnie w swojej grze będziesz chciał dodać niestandardowy.

Więc udałem się na 1001fonts.com i dostałem tę całkiem komercyjną darmową czcionkę Halo jako TTF. Ponownie, po prostu upuść plik w zasobach / czcionkach, ale teraz musi on zostać zaimportowany inaczej w pliku pubspec.yaml. Zamiast dodawać jeszcze jeden zasób, istnieje dedykowany znacznik czcionki, który jest domyślnie komentowany wraz z kompletnymi instrukcjami dodawania czcionek. Nadaj mu nazwę i zrób coś takiego:

Ta dodatkowa warstwa abstrakcji pochodzi od samego Fluttera i pozwala dodawać kilka plików do tej samej czcionki (aby zdefiniować pogrubienie, większe rozmiary itp.). Wracając do naszego akapitu, po prostu dodajemy właściwość fontFamily: „Halo” do konstruktora TextStyle.

Uruchom, a zobaczysz ładną czcionkę Halo!

Punkt kontrolny: 3155bda

Opisana metoda da ci większą kontrolę, jeśli chcesz na przykład wielu stylów w tym samym akapicie. Ale jeśli chcesz, podobnie jak w tym przypadku, pojedynczy akapit o prostym stylu, po prostu użyj pomocnika Flame.util.text, aby go utworzyć:

Ta pojedyncza linia zastępuje poprzednie 4 i przedstawia najważniejsze funkcje. Tekst (pierwszy argument) jest wymagany, a wszystkie pozostałe są opcjonalne, z rozsądnymi wartościami domyślnymi.

Dla koloru ponownie używamy pomocnika Colors.white, ale możemy również użyć nowego Koloru (0xFFFFFFFF), jeśli chcesz określonego koloru.

Punkt kontrolny: 952a9df

I masz to! Kompletna gra z renderowaniem ikonek, renderowaniem tekstu, dźwiękiem, pętlą gier, zdarzeniami i zarządzaniem stanem.

Wydanie

Czy Twoja gra jest gotowa do wydania?

Wystarczy wykonać kilka prostych kroków z samouczka Flutter.

Wszystkie są dość proste, jak widać w tym ostatnim punkcie kontrolnym, z wyjątkiem części Ikony, która może powodować ból głowy. Radzę zrobić dużą ikonę (512 lub 1024 px) i użyć strony Make App Icon, aby wygenerować plik zip ze wszystkim, czego potrzebujesz (iOS i Android).

Punkt kontrolny: 2974f29

Co jeszcze?

Podobał ci się Flame? Jeśli masz jakieś sugestie, błędy, pytania, prośby o funkcje lub cokolwiek innego, skontaktuj się ze mną!

Chcesz poprawić swoją grę i dowiedzieć się więcej? Co powiesz na dodanie serwera z Firebase i Google Sign In? Co powiesz na umieszczanie reklam? Co powiesz na skonfigurowanie menu głównego i wielu ekranów?

Oczywiście jest wiele do poprawienia - to tylko przykładowa gra. Ale powinien był dać podstawową koncepcję podstawowych koncepcji rozwoju gier za pomocą Fluttera (z Flamerem lub bez).

Mam nadzieję, że wszystkim się podobało!

Pierwotnie opublikowany w GitHub.