Pour dessiner des objets et des sprites dans votre jeu, vous devez définir les variables d'affichage, de surface et de contexte, configurer le rendu dans la boucle de jeu, puis afficher chaque scène et chaque objet.
Il existe deux façons de dessiner des images à l'écran pour un jeu C ou C++ : avec OpenGL ES ou avec Vulkan.
OpenGL ES fait partie de la spécification Open Graphics Library (OpenGL®) destinée aux appareils mobiles tels qu'Android. Lisez cette rubrique afin d'apprendre à configurer OpenGL ES pour votre jeu.
Si vous utilisez Vulkan pour votre jeu, consultez le guide Premiers pas avec Vulkan.
Avant de commencer
Si ce n'est pas déjà fait, configurez un objet GameActivity dans votre projet Android.
Configurer les variables OpenGL ES
Pour effectuer le rendu de votre jeu, vous aurez besoin d'un affichage, d'une surface, d'un contexte et d'une configuration. Ajoutez les variables OpenGL ES suivantes au fichier d'en-tête de votre moteur de jeu :
class NativeEngine { //... private: EGLDisplay mEglDisplay; EGLSurface mEglSurface; EGLContext mEglContext; EGLConfig mEglConfig; bool mHasFocus, mIsVisible, mHasWindow; bool mHasGLObjects; bool mIsFirstFrame; int mSurfWidth, mSurfHeight; }
Dans le constructeur de votre moteur de jeu, initialisez les valeurs par défaut des variables.
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; }
Initialisez l'affichage dont vous souhaitez effectuer le rendu.
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; }
La surface peut être un tampon hors écran (pbuffer) alloué par EGL, ou une fenêtre allouée par l'OS Android. Initialisez cette surface :
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; }
Initialisez le contexte de rendu. Cet exemple crée un contexte 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; }
Configurez vos paramètres OpenGL ES avant de commencer à dessiner. Cet exemple est exécuté au début de chaque frame. Cela permet d'effectuer des tests de profondeur, de définir une couleur claire sur du noir, ainsi que de vider les tampons de couleur et de profondeur.
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); }
Effectuer le rendu avec la boucle de jeu
La boucle de jeu affiche une frame et se répète indéfiniment jusqu'à ce que l'utilisateur quitte. Entre chaque frame, votre jeu peut :
Traiter les événements tels que les événements d'entrée, de sortie audio et de mise en réseau ;
Mettre à jour la logique et l'interface utilisateur du jeu ;
Afficher un frame à l'écran.
Pour afficher un frame à l'écran, la méthode
DoFrame
est appelée indéfiniment dans la boucle de jeu :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(); } } }
Dans la méthode
DoFrame
, interrogez les dimensions de la surface actuelle, demandez àSceneManager
d'afficher un frame et permutez les tampons d'affichage.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()); } }
Afficher des scènes et des objets
La boucle de jeu traite une hiérarchie de scènes et d'objets visibles pour les afficher. Dans l'exemple Endless Tunnel, un
SceneManager
assure le suivi de plusieurs scènes, avec une seule scène active à la fois. Dans cet exemple, la scène active est affichée :void SceneManager::DoFrame() { if (mSceneToInstall) { InstallScene(mSceneToInstall); mSceneToInstall = NULL; } if (mHasGraphics && mCurScene) { mCurScene->DoFrame(); } }
En fonction de votre jeu, une scène peut contenir un arrière-plan, du texte, des sprites et des objets de jeu. Affichez-les dans l'ordre qui convient à votre jeu. Cet exemple affiche l'arrière-plan, le texte et les widgets :
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); }
Ressources
Lisez ce qui suit pour en savoir plus sur OpenGL ES et Vulkan :
OpenGL ES : Images et graphismes sous Android.
OpenGL ES : Présentation dans Android Source
Vulkan : Premiers pas dans le NDK.
Vulkan : Présentation dans Android Source.
Boucles de jeu Android : apprenez à rythmer les images, à mettre les tampons en file d'attente, à gérer les rappels VSYNC et à gérer les threads.