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

이 가이드에서는 Android 게임에서 GameActivity를 설정 및 통합하고 이벤트를 처리하는 방법을 설명합니다.

GameActivity는 중요한 API 사용 프로세스를 간소화하여 C 또는 C++ 게임을 Android에 출시할 수 있도록 지원합니다. 이전에는 NativeActivity가 게임에 권장되는 클래스였습니다. 현재는 GameActivity가 그 자리를 대체하고 있으며 API 수준 19와 하위 호환도 됩니다.

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

시작하기 전에

배포판을 가져오려면 GameActivity 출시를 참고하세요.

빌드 설정

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

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

GameActivityAndroid 보관 파일(AAR)로 배포됩니다. 이 AAR에는 AndroidManifest.xml에서 사용하는 Java 클래스와 GameActivity의 Java 측을 앱의 C/C++ 구현에 연결하는 C 및 C++ 소스 코드가 포함되어 있습니다. GameActivity 1.2.2 이상을 사용하는 경우 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. C/C++ 정적 라이브러리나 C/++ 소스 코드를 다음과 같이 프로젝트로 가져옵니다.

    정적 라이브러리

    프로젝트의 CMakeLists.txt 파일에서 game-activity 정적 라이브러리를 game-activity_static prefab 모듈로 가져옵니다.

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

    소스 코드

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

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

    또한 프로젝트의 CmakeLists.txtGameActivity.cpp, GameTextInput.cpp, 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를 사용하세요.