Pierwsze kroki z GameActivity   Część Android Game Development Kit.

Z tego przewodnika dowiesz się, jak skonfigurować i zintegrować GameActivity oraz obsługiwać zdarzenia w grze na Androida.

GameActivity ułatwia przenoszenie gier napisanych w C lub C++ na Androida, upraszczając proces korzystania z najważniejszych interfejsów API. Wcześniej zalecaną klasą w przypadku gier była NativeActivity. GameActivity zastępuje go jako zalecana klasa w przypadku gier i jest wstecznie zgodny z poziomem API 19.

Przykładowy projekt integrujący GameActivity znajdziesz w repozytorium games-samples.

Zanim rozpoczniesz

Aby uzyskać dystrybucję, zapoznaj się z GameActivitywersjami.

Konfigurowanie kompilacji

Na Androidzie Activity jest punktem wejścia do gry i zapewnia też Window do rysowania. Wiele gier rozszerza tę Activity funkcję za pomocą własnej klasy Java lub Kotlin, aby pokonać ograniczenia w NativeActivity, używając kodu JNI do połączenia z kodem gry w języku C lub C++.

GameActivity oferuje te możliwości:

GameActivity jest rozpowszechniany jako archiwum Androida (AAR). Ten plik AAR zawiera klasę Java, której używasz w AndroidManifest.xml, a także kod źródłowy w językach C i C++, który łączy część GameActivity w języku Java z implementacją aplikacji w językach C/C++. Jeśli używasz GameActivity w wersji 1.2.2 lub nowszej, dostępna jest też statyczna biblioteka C/C++. W odpowiednich przypadkach zalecamy używanie biblioteki statycznej zamiast kodu źródłowego.

Dołącz te pliki źródłowe lub bibliotekę statyczną do procesu kompilacji za pomocą Prefab, które udostępnia biblioteki natywne i kod źródłowy w projekcie CMake lub kompilacji NDK.

  1. Aby dodać zależność biblioteki GameActivity do pliku build.gradle gry, postępuj zgodnie z instrukcjami na stronie Jetpack Android Games.

  2. Aby włączyć prefab, wykonaj te czynności w przypadku wtyczki Androida w wersji 4.1 lub nowszej:

    • Dodaj do bloku android w pliku build.gradle modułu te informacje:
    buildFeatures {     prefab true } 
    android.prefabVersion=2.0.0 

    Jeśli używasz starszych wersji AGP, postępuj zgodnie z instrukcjami konfiguracji podanymi w dokumentacji prefabrykatu.

  3. Zaimportuj do projektu bibliotekę statyczną C/C++ lub kod źródłowy C/++ w ten sposób:

    Biblioteka statyczna

    W pliku CMakeLists.txt projektu zaimportuj bibliotekę statyczną game-activity do modułu prefabrykatu game-activity_static:

    find_package(game-activity REQUIRED CONFIG) target_link_libraries(${PROJECT_NAME} PUBLIC log android game-activity::game-activity_static) 

    Kod źródłowy

    W pliku CMakeLists.txt projektu zaimportuj pakiet game-activity i dodaj go do celu. Pakiet game-activity wymaga pakietu libandroid.so, więc jeśli go brakuje, musisz go też zaimportować.

    find_package(game-activity REQUIRED CONFIG) ... target_link_libraries(... android game-activity::game-activity) 

    Do folderu CmakeLists.txt projektu dodaj też te pliki: GameActivity.cpp, GameTextInput.cppandroid_native_app_glue.c.

Jak Android uruchamia Twoją aktywność

System Android wykonuje kod w instancji działania, wywołując metody zwrotne, które odpowiadają poszczególnym etapom cyklu życia działania. Aby Android mógł uruchomić aktywność i rozpocząć grę, musisz zadeklarować aktywność z odpowiednimi atrybutami w pliku manifestu Androida. Więcej informacji znajdziesz w artykule Wprowadzenie do działań.

Manifest Androida

Każdy projekt aplikacji musi zawierać plik AndroidManifest.xml w katalogu głównym zestawu źródeł projektu. Plik manifestu zawiera podstawowe informacje o aplikacji dla narzędzi do kompilacji Androida, systemu operacyjnego Android i Google Play. Obejmuje to m.in.:

Implementowanie GameActivity w grze

  1. Utwórz lub zidentyfikuj główną klasę Java aktywności (tę, która jest określona w elemencie activity w pliku AndroidManifest.xml). Zmień tę klasę, aby rozszerzyć GameActivity z pakietu com.google.androidgamesdk:

    import com.google.androidgamesdk.GameActivity;  public class YourGameActivity extends GameActivity { ... } 
  2. Upewnij się, że biblioteka natywna jest wczytywana na początku za pomocą bloku statycznego:

    public class EndlessTunnelActivity extends GameActivity {   static {     // Load the native library.     // The name "android-game" depends on your CMake configuration, must be     // consistent here and inside AndroidManifect.xml     System.loadLibrary("android-game");   }   ... } 
  3. Dodaj bibliotekę natywną do AndroidManifest.xml, jeśli nazwa biblioteki nie jest nazwą domyślną (libmain.so):

    <meta-data android:name="android.app.lib_name"  android:value="android-game" /> 

Implementacja funkcji android_main

  1. android_native_app_glue Biblioteka to biblioteka kodu źródłowego, której gra używa do zarządzania GameActivity zdarzeniami cyklu życia w osobnym wątku, aby zapobiec blokowaniu w wątku głównym. Gdy korzystasz z biblioteki, rejestrujesz wywołanie zwrotne do obsługi zdarzeń związanych z cyklem życia, takich jak zdarzenia wejścia dotykowego. Archiwum GameActivity zawiera własną wersję biblioteki android_native_app_glue, więc nie możesz używać wersji zawartej w wersjach NDK. Jeśli Twoje gry korzystają z android_native_app_glue biblioteki dołączonej do NDK, przełącz się na wersję GameActivity.

    Po dodaniu kodu źródłowego biblioteki android_native_app_glue do projektu będzie on współpracować z GameActivity. Zaimplementuj funkcję o nazwie android_main, która jest wywoływana przez bibliotekę i używana jako punkt wejścia do gry. Przekazywana jest do niej struktura o nazwie android_app. Może się to różnić w zależności od gry i silnika. Oto przykład:

    #include <game-activity/native_app_glue/android_native_app_glue.h>  extern "C" {     void android_main(struct android_app* state); };  void android_main(struct android_app* app) {     NativeEngine *engine = new NativeEngine(app);     engine->GameLoop();     delete engine; } 
  2. Przetwarzaj android_app w głównej pętli gry, np. przez odpytywanie i obsługę zdarzeń cyklu życia aplikacji zdefiniowanych w NativeAppGlueAppCmd. Na przykład ten fragment kodu rejestruje funkcję _hand_cmd_proxy jako moduł obsługi NativeAppGlueAppCmd, a następnie odpytuje o zdarzenia cyklu życia aplikacji i wysyła je do zarejestrowanego modułu obsługi(w android_app::onAppCmd) w celu przetworzenia:

    void NativeEngine::GameLoop() {   mApp->userData = this;   mApp->onAppCmd = _handle_cmd_proxy;  // register your command handler.   mApp->textInputState = 0;    while (1) {     int events;     struct android_poll_source* source;      // If not animating, block until we get an event;     // If animating, don't block.     while ((ALooper_pollOnce(IsAnimating() ? 0 : -1, NULL, &events,       (void **) &source)) >= 0) {         if (source != NULL) {             // process events, native_app_glue internally sends the outstanding             // application lifecycle events to mApp->onAppCmd.             source->process(source->app, source);         }         if (mApp->destroyRequested) {             return;         }     }     if (IsAnimating()) {         DoFrame();     }   } } 
  3. Więcej informacji znajdziesz w przykładzie NDK Endless Tunnel. Główna różnica będzie polegać na sposobie obsługi zdarzeń, co pokazujemy w następnej sekcji.

Obsługa zdarzeń

Aby umożliwić zdarzeniom wejściowym docieranie do aplikacji, utwórz i zarejestruj filtry zdarzeń za pomocą funkcji android_app_set_motion_event_filterandroid_app_set_key_event_filter. Domyślnie biblioteka native_app_glue zezwala tylko na zdarzenia ruchu pochodzące z wejścia SOURCE_TOUCHSCREEN. Szczegółowe informacje znajdziesz w dokumencie referencyjnym i w android_native_app_gluekodzie implementacji.

Aby obsługiwać zdarzenia wejściowe, uzyskaj odniesienie do android_input_buffer za pomocą android_app_swap_input_buffers() w pętli gry. Zawierają one zdarzenia związane z ruchemkluczowe zdarzenia, które miały miejsce od ostatniego odpytania. Liczba zdarzeń zawartych w odpowiednich polach jest przechowywana w polach motionEventsCountkeyEventsCount.

  1. Iteruj i obsługuj każde zdarzenie w pętli gry. W tym przykładzie poniższy kod iteruje motionEvents i obsługuje je za pomocą handle_event:

    android_input_buffer* inputBuffer = android_app_swap_input_buffers(app); if (inputBuffer && inputBuffer->motionEventsCount) {     for (uint64_t i = 0; i < inputBuffer->motionEventsCount; ++i) {         GameActivityMotionEvent* motionEvent = &inputBuffer->motionEvents[i];          if (motionEvent->pointerCount > 0) {             const int action = motionEvent->action;             const int actionMasked = action & AMOTION_EVENT_ACTION_MASK;             // Initialize pointerIndex to the max size, we only cook an             // event at the end of the function if pointerIndex is set to a valid index range             uint32_t pointerIndex = GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT;             struct CookedEvent ev;             memset(&ev, 0, sizeof(ev));             ev.motionIsOnScreen = motionEvent->source == AINPUT_SOURCE_TOUCHSCREEN;             if (ev.motionIsOnScreen) {                 // use screen size as the motion range                 ev.motionMinX = 0.0f;                 ev.motionMaxX = SceneManager::GetInstance()->GetScreenWidth();                 ev.motionMinY = 0.0f;                 ev.motionMaxY = SceneManager::GetInstance()->GetScreenHeight();             }              switch (actionMasked) {                 case AMOTION_EVENT_ACTION_DOWN:                     pointerIndex = 0;                     ev.type = COOKED_EVENT_TYPE_POINTER_DOWN;                     break;                 case AMOTION_EVENT_ACTION_POINTER_DOWN:                     pointerIndex = ((action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)                                    >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);                     ev.type = COOKED_EVENT_TYPE_POINTER_DOWN;                     break;                 case AMOTION_EVENT_ACTION_UP:                     pointerIndex = 0;                     ev.type = COOKED_EVENT_TYPE_POINTER_UP;                     break;                 case AMOTION_EVENT_ACTION_POINTER_UP:                     pointerIndex = ((action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)                                    >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);                     ev.type = COOKED_EVENT_TYPE_POINTER_UP;                     break;                 case AMOTION_EVENT_ACTION_MOVE: {                     // Move includes all active pointers, so loop and process them here,                     // we do not set pointerIndex since we are cooking the events in                     // this loop rather than at the bottom of the function                     ev.type = COOKED_EVENT_TYPE_POINTER_MOVE;                     for (uint32_t i = 0; i < motionEvent->pointerCount; ++i) {                         _cookEventForPointerIndex(motionEvent, callback, ev, i);                     }                     break;                 }                 default:                     break;             }              // Only cook an event if we set the pointerIndex to a valid range, note that             // move events cook above in the switch statement.             if (pointerIndex != GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT) {                 _cookEventForPointerIndex(motionEvent, callback,                                           ev, pointerIndex);             }         }     }     android_app_clear_motion_events(inputBuffer); } 

    Przykład implementacji funkcji _cookEventForPointerIndex() i innych powiązanych funkcji znajdziesz w przykładzie w GitHubie.

  2. Po zakończeniu pamiętaj, aby wyczyścić kolejkę zdarzeń, które zostały właśnie obsłużone:

    android_app_clear_motion_events(mApp); 

Dodatkowe materiały

Więcej informacji o GameActivity znajdziesz w tych artykułach:

Aby zgłosić błędy lub poprosić o dodanie nowych funkcji do GameActivity, użyj narzędzia do śledzenia problemów z GameActivity.