컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

GameActivity 시작하기   Android Game Development Kit에 포함되어 있음.

GameActivity는 중요한 API 사용 프로세스를 간소화하여 C 또는 C++ 게임을 Android에 출시할 수 있도록 지원합니다.

GameActivity를 통합하는 샘플은 games-samples 저장소를 참고하세요.

빌드 설정

Android에서 Activity는 게임의 진입점 역할을 하며 게임 내에 불러올 Window도 제공합니다. 많은 게임에서 JNI 코드를 사용하여 C 또는 C++ 게임 코드와 연결하면서 NativeActivity의 제한사항을 우회하기 위해 자체 자바 또는 Kotlin 클래스로 Activity를 확장합니다.

GameActivity에서 제공하는 기능은 다음과 같습니다.

GameActivityAndroid 보관 파일(AAR)로 배포됩니다. 이 AAR에는 AndroidManifest.xml에 사용할 자바 클래스와 GameActivity의 네이티브 기능을 구현하는 C, C++ 소스 코드가 포함되어 있습니다. CMake 프로젝트 또는 NDK 빌드에 네이티브 라이브러리 및 소스 코드를 노출하는 Prefab을 사용하는 빌드 프로세스에 이러한 소스 파일이 포함됩니다.

  1. Jetpack Android 게임 페이지의 안내에 따라 GameActivity 라이브러리 종속 항목을 게임의 build.gradle 파일에 추가합니다.

  2. Android 플러그인 버전(AGP) 4.1 이상으로 다음을 실행하여 prefab을 사용 설정합니다.

    • 모듈 build.gradle 파일의 android 블록에 다음을 추가합니다.
    buildFeatures {
        prefab true
    }
    
    • Prefab 버전을 선택하고 gradle.properties 파일로 설정합니다.
    android.prefabVersion=2.0.0
    

    이전 AGP 버전을 사용하는 경우 관련 구성 안내는 prefab 문서를 따르세요.

  3. 프로젝트의 CMakeLists.txt 파일에서 game-activity 패키지를 가져와 대상에 추가합니다(game-activity에는 libandroid.so가 필요하므로 없는 경우 추가).

    find_package(game-activity REQUIRED CONFIG)
    ...
    target_link_libraries(... android game-activity::game-activity)
    
  4. 게임의 기존 .cpp 파일 중 하나에서 또는 새 .cpp 파일에서 다음을 추가하여 GameActivity, GameTextInput, 컴패니언 네이티브 글루 구현을 포함합니다.

    #include <game-activity/GameActivity.cpp>
    #include <game-text-input/gametextinput.cpp>
    extern "C" {
      #include <game-activity/native_app_glue/android_native_app_glue.c>
    }
    

Android에서 Activity를 실행하는 방법

Android 시스템은 활동 수명 주기의 특정 단계에 해당하는 콜백 메서드를 호출하여 활동 인스턴스의 코드를 실행합니다. Android에서 활동을 실행하고 게임을 시작하려면 Android 매니페스트에서 적절한 속성으로 활동을 선언해야 합니다. 자세한 내용은 Activity 소개를 참조하세요.

Android 매니페스트

모든 앱 프로젝트의 프로젝트 소스 세트 루트에는 AndroidManifest.xml 파일이 있어야 합니다. 매니페스트 파일은 Android 빌드 도구, Android 운영체제, Google Play에 앱에 관한 필수 정보를 설명합니다. 여기에는 다음과 같은 항목이 포함됩니다.

게임에 GameActivity 구현

  1. 기본 활동 자바 클래스(AndroidManifest.xml 파일 내 activity 요소에 지정된 클래스)를 만들거나 찾습니다. 이 클래스를 변경하여 com.google.androidgamesdk 패키지에서 GameActivity를 확장합니다.

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. 정적 블록을 사용하여 맨 처음에 네이티브 라이브러리를 로드합니다.

    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. 라이브러리 이름이 기본 이름 libmain.so가 아닌 경우 AndroidManifest.xml에 네이티브 라이브러리를 추가합니다.

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

android_main 구현

  1. android_native_app_glue 라이브러리는 게임이 기본 스레드에서의 차단을 방지하기 위해 별도의 스레드에서 GameActivity 수명 주기 이벤트를 관리하는 데 사용하는 소스 코드 라이브러리입니다. 이 라이브러리를 사용하면 콜백을 등록하여 터치 입력 이벤트와 같은 수명 주기 이벤트를 처리합니다. GameActivity 보관 파일에는 자체 버전의 android_native_app_glue 라이브러리가 포함되어 있으므로 NDK 출시에 포함된 버전은 사용할 수 없습니다. 게임에서 NDK에 포함된 android_native_app_glue 라이브러리를 사용하는 경우 GameActivity 버전으로 전환합니다.

    android_native_app_glue 라이브러리 소스 코드를 프로젝트에 추가하면 GameActivity와 연결됩니다. 이 라이브러리에 의해 호출되어 게임의 진입점 역할을 하는 android_main 함수를 구현합니다. 함수는 android_app 구조에 전달됩니다. 이는 게임 및 엔진에 따라 다를 수 있습니다. 다음 예를 참고하세요.

    #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. NativeAppGlueAppCmd에서 정의된 앱 주기 이벤트의 폴링 및 처리와 같이 기본 게임 루프에서 android_app을 처리합니다. 예를 들어 다음 스니펫은 _hand_cmd_proxy 함수를 NativeAppGlueAppCmd 핸들러로 등록하고 앱 주기 이벤트를 폴링한 후 처리를 위해 android_app::onAppCmd의 등록된 핸들러로 전송합니다.

    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_pollAll(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. 자세한 내용은 NDK를 통한 Endless Tunnel 구현 예시를 참고합니다. 주요 차이점은 다음 섹션에서 설명하는 것처럼 이벤트를 처리하는 방법입니다.

이벤트 처리

입력 이벤트가 애플리케이션에 도달하도록 하려면 이벤트 필터를 만들고 그 필터를 android_app_set_motion_event_filterandroid_app_set_key_event_filter와 함께 등록합니다. 기본적으로 native_app_glue 라이브러리는 SOURCE_TOUCHSCREEN 입력의 모션 이벤트만 허용합니다. 세부정보는 참조 문서android_native_app_glue 구현 코드를 확인하세요.

입력 이벤트를 처리하려면 게임 루프에서 android_app_swap_input_buffers()를 사용해 android_input_buffer에 관한 참조를 가져옵니다. 여기에는 마지막 폴링 이후 발생한 모션 이벤트키 이벤트가 포함되어 있습니다. 포함된 이벤트 수는 각각 motionEventsCountkeyEventsCount에 저장됩니다.

  1. 게임 루프의 각 이벤트를 반복하고 처리합니다. 이 예에서 다음 코드는 motionEvents를 반복하고 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);
    }
    

    _cookEventForPointerIndex() 및 기타 관련 함수의 구현은 GitHub 샘플을 참고하세요.

  2. 완료되면 방금 처리한 이벤트의 큐를 삭제해야 합니다.

    android_app_clear_motion_events(mApp);
    

참조

GameActivity에 관해 자세히 알아보려면 다음을 참고하세요.

GameActivity 관련 버그를 신고하거나 새로운 기능을 요청하려면 GameActivity Issue Tracker를 사용하세요.