Um Objekte und Sprites in Ihrem Spiel zu zeichnen, müssen Sie die Anzeige-, Oberflächen- und Kontextvariablen konfigurieren, das Rendering in Ihrer Spielschleife einrichten und jede Szene und jedes Objekt zeichnen.
Für ein C- oder C++-Spiel gibt es zwei Möglichkeiten, Bilder auf dem Bildschirm zu zeichnen: mit OpenGL ES oder Vulkan.
OpenGL ES ist Teil der Spezifikation Open Graphics Library (OpenGL®) für Mobilgeräte wie Android. In diesem Thema erfahren Sie, wie Sie OpenGL ES für Ihr Spiel konfigurieren.
Wenn Sie Vulkan für Ihr Spiel verwenden, lesen Sie die Anleitung Erste Schritte mit Vulkan.
Vorbereitung
Falls noch nicht geschehen, richten Sie in Ihrem Android-Projekt ein GameActivity-Objekt ein.
OpenGL ES-Variablen einrichten
Sie benötigen ein Display, eine Oberfläche, einen Kontext und eine Konfiguration, um Ihr Spiel zu rendern. Fügen Sie der Header-Datei Ihrer Spiel-Engine die folgenden OpenGL ES-Variablen hinzu:
class NativeEngine { //... private: EGLDisplay mEglDisplay; EGLSurface mEglSurface; EGLContext mEglContext; EGLConfig mEglConfig; bool mHasFocus, mIsVisible, mHasWindow; bool mHasGLObjects; bool mIsFirstFrame; int mSurfWidth, mSurfHeight; }
Initialisieren Sie im Konstruktor Ihrer Spiel-Engine die Standardwerte für die Variablen.
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; }
Initialisieren Sie die zu rendernde Anzeige.
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; }
Die Oberfläche kann ein von EGL zugewiesener Zwischenspeicher (außerhalb des Bildschirms) oder ein vom Android-Betriebssystem zugewiesenes Fenster sein. Initialisieren Sie diese Oberfläche:
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; }
Initialisieren Sie den Renderingkontext. In diesem Beispiel wird ein OpenGL ES 2.0-Kontext erstellt:
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; }
Konfigurieren Sie vor dem Zeichnen Ihre OpenGL ES-Einstellungen. Dieses Beispiel wird am Anfang jedes Frames ausgeführt. Sie ermöglicht Tiefentests, legt die Farbe in Schwarz fest und löscht die Farb- und Tiefenpuffer.
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); }
Mit der Spielschleife rendern
Die Spielschleife rendert einen Frame und wird unbegrenzt wiederholt, bis der Nutzer die Wiedergabe beendet. Zwischen den Frames kann Ihr Spiel Folgendes tun:
Verarbeitungsereignisse wie Eingabe, Audioausgabe und Netzwerkereignisse
Logik und Benutzeroberfläche des Spiels aktualisieren
Hiermit wird ein Frame auf dem Display gerendert.
Zum Rendern eines Frames in der Anzeige wird die Methode
DoFrame
in der Spielschleife unbegrenzt aufgerufen: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(); } } }
Fragen Sie in der Methode
DoFrame
die aktuellen Oberflächendimensionen ab, fordern SieSceneManager
an, einen Frame zu rendern, und tauschen Sie die Anzeigezwischenspeicher.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()); } }
Szenen und Objekte rendern
Die Spielschleife verarbeitet eine Hierarchie sichtbarer Szenen und Objekte, die gerendert werden sollen. Im Endless Tunnel-Beispiel erfasst ein
SceneManager
mehrere Szenen, wobei jeweils nur eine Szene aktiv ist. In diesem Beispiel wird die aktuelle Szene gerendert:void SceneManager::DoFrame() { if (mSceneToInstall) { InstallScene(mSceneToInstall); mSceneToInstall = NULL; } if (mHasGraphics && mCurScene) { mCurScene->DoFrame(); } }
Je nach Spiel kann eine Szene Hintergrund, Text, Sprites und Spielobjekte enthalten. Rendere sie in der für dein Spiel geeigneten Reihenfolge. In diesem Beispiel werden Hintergrund, Text und Widgets gerendert:
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); }
Ressourcen
Im Folgenden finden Sie weitere Informationen zu OpenGL ES und Vulkan:
OpenGL ES: Bilder und Grafiken in Android.
OpenGL ES – Übersicht in Android Source.
Vulkan – Erste Schritte in NDK.
Vulkan – Übersicht in Android Source.
Android-Spielschleifen verstehen: Hier erfahren Sie, wie Sie Frames takten, Warteschlangenzwischenspeichern, VSYNC-Callbacks verarbeiten und Threads verwalten.