إعداد الرسومات باستخدام OpenGL ES

لرسم كائنات وتركيبات مدمجة في لعبتك، عليك ضبط متغيرات العرض والسطح والسياق، وإعداد العرض في حلقة اللعبة، ورسم كل مشهد وكائن.

هناك طريقتان لرسم الصور على الشاشة لاستخدامها في لعبة C أو C++ ، لا سيما باستخدام OpenGL ES أو Vulkan.

قبل البدء

يجب إعداد عنصر GameActivity في مشروع Android، إذا لم يسبق لك ذلك.

إعداد متغيّرات OpenGL ES

  1. ستحتاج إلى شاشة وسطح عرض وسياق وإعداد لعرض لعبتك. أضِف متغيّرات OpenGL ES التالية إلى ملف العنوان لمحرك اللعبة:

    class NativeEngine {
     //...
     private:
      EGLDisplay mEglDisplay;
      EGLSurface mEglSurface;
      EGLContext mEglContext;
      EGLConfig mEglConfig;
    
      bool mHasFocus, mIsVisible, mHasWindow;
      bool mHasGLObjects;
      bool mIsFirstFrame;
    
      int mSurfWidth, mSurfHeight;
    }
    
  2. في الدالة الإنشائية لمحرك اللعبة، قم بتهيئة القيم الافتراضية للمتغيرات.

    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;
    }
    
  3. قم بتهيئة الشاشة للعرض.

    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;
    }
    
  4. يمكن أن يكون السطح مخزنًا مؤقتًا خارج الشاشة (مخزنًا مؤقتًا) تم تخصيصه بواسطة أداة EGL، أو نافذة يخصّصها نظام التشغيل Android. قم بتهيئة مساحة العرض هذه:

    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;
    }
    
  5. تهيئة سياق العرض. ينشئ هذا المثال سياق 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;
    }
    
  6. عليك ضبط إعدادات 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);
    }
    

العرض باستخدام حلقة اللعبة

  1. تعرض حلقة اللعبة إطارًا وتتكرّر إلى أجل غير مسمى إلى أن يتوقّف المستخدم عن العمل. بين الإطارات، يمكن أن تنفّذ لعبتك ما يلي:

    لعرض إطار على الشاشة، تُسمى طريقة 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();
        }
      }
    }
    
  2. في الإجراء 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());
      }
    }
    

عرض المشاهد والعناصر

  1. وتعالج حلقة اللعبة عرضًا هرميًا للمشاهد والعناصر المرئية المطلوب عرضها. في مثال "نفق لا نهائي"، يتتبّع SceneManager مشاهد متعددة، مع تفعيل مشهد واحد فقط في كل مرة. في هذا المثال، يتم عرض المشهد الحالي:

    void SceneManager::DoFrame() {
      if (mSceneToInstall) {
        InstallScene(mSceneToInstall);
        mSceneToInstall = NULL;
      }
    
      if (mHasGraphics && mCurScene) {
        mCurScene->DoFrame();
      }
    }
    
  2. بناءً على لعبتك، قد يشتمل المشهد على خلفية ونصوص وتركيبات متحركة وكائنات اللعبة. قدِّمها بالترتيب المناسب للعبتك. يعرض هذا المثال الخلفية والنص والتطبيقات المصغّرة:

    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 Source.

  • فهم حلقات ألعاب Android: تعلُّم كيفية تحديد وتيرة عرض الإطارات، وإنشاء قوائم الانتظار الاحتياطي، والتعامل مع استدعاءات VSYNC، وإدارة سلاسل المحادثات