게임에 객체와 스프라이트를 그리려면 디스플레이, 표면 및 컨텍스트 변수를 구성하고 게임 루프에서 렌더링을 설정하고 각 장면과 객체를 그려야 합니다.
C 또는 C++ 게임의 화면에 이미지를 그리는 방법은 OpenGL ES 또는 Vulkan, 두 가지가 있습니다.
OpenGL ES는 Android와 같은 휴대기기용 Open Graphics Library(OpenGL®) 사양의 일부입니다. 이 주제에서 게임의 OpenGL ES를 구성하는 방법을 알아보세요.
게임에 Vulkan을 사용하는 경우 다음을 참고하세요. Vulkan 시작하기 참조하세요.
시작하기 전에
아직 설정하지 않았다면 Android 프로젝트에서 GameActivity 객체를 설정합니다.
OpenGL ES 변수 설정
게임을 렌더링하려면 디스플레이, 표면, 컨텍스트, 구성이 필요합니다. 게임 엔진의 헤더 파일에 다음 OpenGL ES 변수를 추가합니다.
class NativeEngine { //... private: EGLDisplay mEglDisplay; EGLSurface mEglSurface; EGLContext mEglContext; EGLConfig mEglConfig; bool mHasFocus, mIsVisible, mHasWindow; bool mHasGLObjects; bool mIsFirstFrame; int mSurfWidth, mSurfHeight; }
게임 엔진의 생성자에서 변수의 기본값을 초기화합니다.
NativeEngine::NativeEngine(struct android_app *app) { //... mEglDisplay = EGL_NO_DISPLAY; mEglSurface = EGL_NO_SURFACE; mEglContext = EGL_NO_CONTEXT; mEglConfig = 0; mHasFocus = mIsVisible = mHasWindow = false; mHasGLObjects = false; mIsFirstFrame = true; mSurfWidth = mSurfHeight = 0; }
렌더링할 디스플레이를 초기화합니다.
bool NativeEngine::InitDisplay() { if (mEglDisplay != EGL_NO_DISPLAY) { return true; } mEglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (EGL_FALSE == eglInitialize(mEglDisplay, 0, 0)) { LOGE("NativeEngine: failed to init display, error %d", eglGetError()); return false; } return true; }
표면은 EGL에서 할당한 화면 밖 버퍼(pbuffer) 또는 Android OS에서 할당한 창일 수 있습니다. 표면 초기화는 다음과 같습니다.
bool NativeEngine::InitSurface() { ASSERT(mEglDisplay != EGL_NO_DISPLAY); if (mEglSurface != EGL_NO_SURFACE) { return true; } EGLint numConfigs; const EGLint attribs[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, // request OpenGL ES 2.0 EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_DEPTH_SIZE, 16, EGL_NONE }; // Pick the first EGLConfig that matches. eglChooseConfig(mEglDisplay, attribs, &mEglConfig, 1, &numConfigs); mEglSurface = eglCreateWindowSurface(mEglDisplay, mEglConfig, mApp->window, NULL); if (mEglSurface == EGL_NO_SURFACE) { LOGE("Failed to create EGL surface, EGL error %d", eglGetError()); return false; } return true; }
렌더링 컨텍스트를 초기화합니다. 다음 예에서는 OpenGL ES 2.0 컨텍스트를 만듭니다.
bool NativeEngine::InitContext() { ASSERT(mEglDisplay != EGL_NO_DISPLAY); if (mEglContext != EGL_NO_CONTEXT) { return true; } // OpenGL ES 2.0 EGLint attribList[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; mEglContext = eglCreateContext(mEglDisplay, mEglConfig, NULL, attribList); if (mEglContext == EGL_NO_CONTEXT) { LOGE("Failed to create EGL context, EGL error %d", eglGetError()); return false; } return true; }
그리기 전에 OpenGL ES 설정을 구성합니다. 이 예는 모든 프레임의 시작 부분에서 실행됩니다. 심도 테스트를 실행하고, 선명한 색상을 검은색으로 설정하고, 색상 및 심도 버퍼를 삭제합니다.
void NativeEngine::ConfigureOpenGL() { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glEnable(GL_DEPTH_TEST); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); }
게임 루프로 렌더링
게임 루프는 프레임을 렌더링하고 사용자가 종료할 때까지 무기한 반복합니다. 프레임 사이에서 게임은 다음 작업을 수행할 수 있습니다.
프레임을 디스플레이에 렌더링하기 위해
DoFrame
메서드는 게임 루프에서 무기한 호출됩니다.void NativeEngine::GameLoop() { // Loop indefinitely. while (1) { int events; struct android_poll_source* source; // If not animating, block until we get an event. while ((ALooper_pollAll(IsAnimating() ? 0 : -1, NULL, &events, (void **) &source)) >= 0) { // Process events. ... } // Render a frame. if (IsAnimating()) { DoFrame(); } } }
DoFrame
메서드에서 현재 표면 크기를 쿼리하고SceneManager
를 지정하여 프레임을 렌더링한 다음 디스플레이 버퍼를 교환합니다.void NativeEngine::DoFrame() { ... // Query the current surface dimension. int width, height; eglQuerySurface(mEglDisplay, mEglSurface, EGL_WIDTH, &width); eglQuerySurface(mEglDisplay, mEglSurface, EGL_HEIGHT, &height); // Handle dimension changes. SceneManager *mgr = SceneManager::GetInstance(); if (width != mSurfWidth || height != mSurfHeight) { mSurfWidth = width; mSurfHeight = height; mgr->SetScreenSize(mSurfWidth, mSurfHeight); glViewport(0, 0, mSurfWidth, mSurfHeight); } ... // Render scenes and objects. mgr->DoFrame(); // Swap buffers. if (EGL_FALSE == eglSwapBuffers(mEglDisplay, mEglSurface)) { HandleEglError(eglGetError()); } }
장면 및 객체 렌더링
게임 루프는 보이는 장면과 객체의 계층 구조를 처리하여 렌더링합니다. Endless Tunnel 예시에서
SceneManager
는 한 번에 하나의 장면만 활성화하여 여러 장면을 추적합니다. 이 예에서는 현재 장면이 렌더링됩니다.void SceneManager::DoFrame() { if (mSceneToInstall) { InstallScene(mSceneToInstall); mSceneToInstall = NULL; } if (mHasGraphics && mCurScene) { mCurScene->DoFrame(); } }
게임에 따라 장면에 배경, 텍스트, 스프라이트, 게임 객체가 포함될 수 있습니다. 게임에 적합한 순서대로 렌더링합니다. 이 예에서는 배경, 텍스트, 위젯을 렌더링합니다.
void UiScene::DoFrame() { // clear screen glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glDisable(GL_DEPTH_TEST); RenderBackground(); // Render the "Please Wait" sign and do nothing else if (mWaitScreen) { SceneManager *mgr = SceneManager::GetInstance(); mTextRenderer->SetFontScale(WAIT_SIGN_SCALE); mTextRenderer->SetColor(1.0f, 1.0f, 1.0f); mTextRenderer->RenderText(S_PLEASE_WAIT, mgr->GetScreenAspect() * 0.5f, 0.5f); glEnable(GL_DEPTH_TEST); return; } // Render all the widgets. for (int i = 0; i < mWidgetCount; ++i) { mWidgets[i]->Render(mTrivialShader, mTextRenderer, mShapeRenderer, (mFocusWidget < 0) ? UiWidget::FOCUS_NOT_APPLICABLE : (mFocusWidget == i) ? UiWidget::FOCUS_YES : UiWidget::FOCUS_NO,tf); } glEnable(GL_DEPTH_TEST); }
리소스
OpenGL ES 및 Vulkan에 대한 자세한 내용은 다음을 참조하세요.
OpenGL ES - Android의 이미지 및 그래픽.
OpenGL ES - Android 소스 개요.
Vulkan - NDK로 시작하기.
Vulkan - Android 소스 개요.
Android 게임 루프 이해 - 프레임 속도 지정, 대기열 버퍼, VSYNC 콜백 처리, 스레드 관리 방법을 알아봅니다.