如需在游戏中绘制对象和精灵,您需要配置显示变量、Surface 变量和上下文变量,在游戏循环中设置渲染,然后绘制每个场景和对象。
对于 C 或 C++ 游戏,您可以通过两种方式将图像绘制到屏幕上,即,使用 OpenGL ES 或 Vulkan。
OpenGL ES 是适用于 Android 等移动设备的 Open Graphics Library (OpenGL®) 规范的一部分。本主题将介绍如何为您的游戏配置 OpenGL ES。
如果您在游戏中使用 Vulkan,请阅读 Vulkan 使用入门指南。
准备工作
如果您尚未在 Android 项目中设置 GameActivity 对象,请执行此操作。
设置 OpenGL ES 变量
您需要使用 Display、Surface、Context 和 Config 来渲染游戏。将以下 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; }
Surface 可以是由 EGL 分配的离屏缓冲区 (pbuffer),也可以是由 Android 操作系统分配的窗口。初始化此 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; }
初始化渲染上下文。以下示例创建了一个 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
方法会查询当前的 Surface 尺寸,请求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 回调以及管理线程。