Games on smart terminals are currently in the limelight. Which smartphone does not have several games produced by Penguin Company? I have never dabbled in game development before, but I know that I have to choose a suitable game engine before game development, and the era of typing code from scratch is out. Before searching for a game engine, I need to answer three multiple-choice questions in front of me:
1. 2D engine or 3D engine?
2. Platform-specific engine or cross-platform engine?
3. Pay engine or open source engine?
As an entry-level player, 2D games are obviously more suitable for getting started. In addition, most of the games suitable for early childhood education in Guoguo are suitable for 2D games. 3D games themselves are too difficult. They not only require programming capabilities, but also 3D modeling capabilities. These learning cycles are too long. They have always been Ubuntu Fans, and they don’t have a Mac Book on hand. It becomes a bad thing to develop iOS programs. Building iOS App development environment under Ubuntu is very complicated, and even virtual machines are too lazy to try. But from the perspective of gaming experience, it is better to play on iPad, so it is best to have the engine cross-platform so that it can be subsequently migrated to iOS; open source and open source are used to using open source, and the charging engine is not currently considered. In summary, what I am looking for is an open source, cross-platform Mobile 2D Game Engine.
So I found Cocos2d-x! Cocos2d-x is a cross-platform branch of Cocos2d-iphone. Since it was founded by Chinese people, it has a large user base in China, has a large engine information, and the community is very active. Many Chinese books about Cocos2d-x have been published in China, such as "Cocos2d-x Advanced Development Tutorial: Making Your Own "Fishing Master"" and "Cocos2d-x Authoritative Guide" and so on. More importantly, Cocos2d-x comes with a wealth of examples for beginners to "copy and learn". The example of cocos2d-x-2.2.2/samples/Cpp/TestCpp almost covers most of the functions of the engine. Now let’s start the introductory journey of Cocos2d-x (For Android).
1. Engine installation
Test environment:
gcc 4.6.3
javac 1.7.0_21
java "1.7.0_21" HotSpot 64-bit Server VM
adt-bundle-linux-x86_64
android-ndk-r9d-linux-x86_64.tar.bz2
The official website of Cocos2d-x currently provides downloads of the 2.2.2 stable version and 3.0beta2 version (of course you can also download to older versions). Since 3.0 has large changes, there is not much information, and it has high requirements for compiler and other versions (need to support the C++11 standard), version 2.2.2 is still used as the learning goal here. After downloading Cocos2d-x-2.2.2, unzip it to a certain directory: for example /home1/tonybai/android-dev/cocos2d-x-2.2.2. If you only use Cocos2d-x to develop Android version games, you don't need to do any compilation work. Android Game Project will automatically compile C++ code with the NDK compiler when Project build and link it with the NDK. If you want to see what the example in Cocos2d-x sample looks like earlier, you can compile a Linux version of the game under Ubuntu: execute it under cocos2d-x-2.2.2. Compilation takes a while. After compiling successfully, we can enter the executable file "HelloCpp" under "cocos2d-x-2.2.2/samples/Cpp/HelloCpp//bin/release". The simplest Cocos2d-x game will be displayed in front of you.
The Android sample project is a little more complicated:
First add libcocos2dx Library project from existing code in Eclipse (note: do not copy to workspace, build it in place). The code path of this project is cocos2d-x-2.2.2/cocos2dx/platform/android/java. and appropriately modify the API version you are using to allow the compile to pass. I'm using target=android-19 here.
Then, set the NDK_ROOT environment variable (such as export NDK_ROOT='/home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c') for use by build_native.sh.
Finally add the game project. Add HelloCpp project from existing code in Eclipse, at cocos2d-x-2.2.2/samples/Cpp/HelloCpp/ (Note: do not copy into Workspace, create it in place). Add ".1=../../../../cocos2dx/platform/android/java" in HelloCpp. Also don't forget to modify the API version you are using appropriately to allow the compile to pass.
If all goes well, you will see "**** Build Finished ****" in the Console window. The Problems window displays "0 errors". Start the Android emulator, Run Application, and the same HelloCpp screen will be presented on the emulator.
Cocos2d-x is built on OpenGL technology. For the Android platform, the Android SDK has fully encapsulated the opengl es 1.1/2.0 API (.*;.*;.*), and the engine can be built on this without C++ code. However, Cocos2d-x is a cross-platform 2D game engine, and the core chooses to be implemented with C++ code (C binding provided by iOS does not provide Java binding; Android provides Java and C binding). Therefore, when developing 2D games on the Android platform, the engine part is the SDK and the NDK intersected. For example, the creation and management of GLThread uses the SDK GLSurfaceView and GLThread, but the real Surface drawing part is the drawing implementation written by Cocos2d-x in C++ (link to the library in the NDK).
2. Cocos2d-x Android engineering code organization structure
Taking the Android project of samples/Cpp/HelloApp as an example, the Android version of Cocos2d-x project is not much different from ordinary Android applications. The core part is just an additional jni directory and a build_native.sh script file. The jni directory stores the "glue" code converted by Java and C++ calls; build_native.sh is a construction script used to compile the C++ code and cocos2dx_static library code under jni.
The summary of HelloCpp's construction process is as follows:
**** Build of configuration Default for project HelloCpp ****
bash /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp//build_native.sh
NDK_ROOT = /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c
COCOS2DX_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp//../../../..
APP_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp//..
APP_ANDROID_ROOT = /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/
+ /home1/tonybai/android-dev/adt-bundle-linux-x86_64/android-ndk-r9c/ndk-build -C /home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/proj.androidNDK_MODULE_PATH=/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp//../../../..:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp//../../../../cocos2dx/platform/third_party/android/prebuilt
Using prebuilt externals
Android NDK: WARNING:/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp//../../../../cocos2dx/:cocos2dx_static: LOCAL_LDLIBS is always ignored for static libraries
make: Entering directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/'
[armeabi] Compile++ thumb: hellocpp_shared <=
[armeabi] Compile++ thumb: hellocpp_shared <=
[armeabi] Compile++ thumb: hellocpp_shared <=
[armeabi] Compile++ thumb: cocos2dx_static <=
[armeabi] Compile++ thumb: cocos2dx_static <=
… …
[armeabi] Compile++ thumb: cocos2dx_static <=
[armeabi] StaticLibrary :
[armeabi] Compile thumb : cpufeatures <=
[armeabi] StaticLibrary :
[armeabi] SharedLibrary :
[armeabi] Install : => libs/armeabi/
make: Leaving directory `/home1/tonybai/android-dev/cocos2d-x-2.2.2/samples/Cpp/HelloCpp/'
**** Build Finished ****
The NDK is the file under jni, and its role is similar to Makefile.
3. Cocos2d-x Android engineering code reading
How to read the code separately is to prepare for the subsequent analysis engine driver process. Learning game engines like Cocos2d-x is not able to grasp the essence of the engine just by staying at the game logic layer. Therefore, proper mining engine implementation is actually of great benefit to understanding and using the engine.
Take a Cocos2d-x Android project as an example. Its game logic code and engine code involved are covered in the following path (or take HelloCpp's Android project as an example):
Project layer:
* cocos2d-x-2.2.2/samples/Cpp/HelloCpp//src Implementation of the main Activity;
* cocos2d-x-2.2.2/samples/Cpp/HelloCpp//jni/hellocpp The nativeInit implementation of the Cocos2dxRenderer class is used to introduce the entrance to Application;
* cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes Your game logic is presented in C++ code form;
Engine layer:
* Encapsulation of Android Activity, GLSurfaceView and Render by cocos2d-x-2.2.2/cocos2dx/platform/android/java/src engine layer
* cocos2d-x-2.2.2/cocos2dx/platform/android/jni corresponds to the native method implementation encapsulated above
* cocos2d-x-2.2.2/cocos2dx, cocos2d-x-2.2.2/cocos2dx/platform, cocos2d-x-2.2.2/cocos2dx/platform/android The core implementation of the cocos2dx engine (for Android platforms)
Subsequent code analysis will also start from these two levels and six locations.
4. Start with Activity
I have learned a little about Android App development before, and Android Apps all start with Activity. Games are also a type of app, so on the Android platform, Cocos2d-x games also start with Activity. So Activity, to be precise, Cocos2dxActivity is the starting point for our engine driver mechanism analysis this time.
Looking back at the Lifecycle of Android Activity, the order in which Activity is started is: -> () -> (). Next, we will analyze the engine driving mechanism according to this main line.
The HelloCpp Activity in the Activity is completely ineffective and is just inheriting the implementation of its parent class Cocos2dxActivity.
//
public class HelloCpp extends Cocos2dxActivity{
protected void onCreate(Bundle savedInstanceState){
(savedInstanceState);
}
… …
}
Let's look at the Cocos2dxActivity class.
//
@Override
protected void onCreate(final Bundle savedInstanceState) {
(savedInstanceState);
sContext = this;
= new Cocos2dxHandler(this);
();
(this, this);
}
public void init() {
// FrameLayout
framelayout_params =
new (.FILL_PARENT,
.FILL_PARENT);
FrameLayout framelayout = new FrameLayout(this);
(framelayout_params);
… …
// Cocos2dxGLSurfaceView
= ();
// …add to FrameLayout
();
… …
.setCocos2dxRenderer(new Cocos2dxRenderer());
… …
// Set framelayout as the content view
setContentView(framelayout);
}
From the above code, we can see that the init method called by onCreate is the core of Cocos2dxActivity initialization. In the init method, Cocos2dxActivity creates a Framelayout instance and assigns the instance to an instance of Cocos2dxActivity as a content View. The Framelayout instance is not alone, a GLSurfaceView with the Cocos2dxRenderer instance set is added to it. The initialization of the Cocos2d-x engine has been quietly completed between these lines of code. As for the details of the initialization, we will analyze it later.
Next is the onResume method, which is implemented as follows:
@Override
protected void onResume() {
();
();
();
}
onResume calls the View's onResume().
// Cocos2dxGLSurfaceView:
@Override
public void onResume() {
();
(new Runnable() {
@Override
public void run() {
.();
}
});
}
Cocos2dxGLSurfaceView packages the event into the queue and throws it to another thread for execution (this thread will be explained in detail later). The corresponding method is in the Cocos2dxRenderer class.
public void handleOnResume() {
();
}
Render actually calls native methods.
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeOnResume() {
if (CCDirector::sharedDirector()->getOpenGLView()) {
CCApplication::sharedApplication()->applicationWillEnterForeground();
}
}
The applicationWillEnterForeground method is in yours;
void AppDelegate::applicationWillEnterForeground() {
CCDirector::sharedDirector()->startAnimation();//
// if you use SimpleAudioEngine, it must resume here
// SimpleAudioEngine::sharedEngine()->resumeBackgroundMusic();
}
This is just a regaining time.
5. Render Thread (render thread) - GLThread
The game engine should take into account both UI events and screen frame refresh. Android's OpenGL application adopts the mode of UI thread (Main Thread) + Render Thread. Activity lives in Main Thread, also known as UI thread. The thread is responsible for capturing information and events interacting with the user and interacting with the rendering thread. For example, when a user answers a call and switches to another program, the rendering thread must know that these events have occurred and handle them instantly. These events and processing methods are passed to the rendering thread by the Activity in the main thread and its loaded View. We cannot see the birth process of rendering threads in the framework code of Cocos2dx, because this process is implemented in the Android SDK layer.
Let's review the key code of the method:
// Cocos2dxGLSurfaceView
= ();
// …add to FrameLayout
();
.setCocos2dxRenderer(new Cocos2dxRenderer());
// Set framelayout as the content view
setContentView(framelayout);
Cocos2dxGLSurfaceView is a subclass of . Anyone who is programming natively opengl es 2.0 on Android should know the importance of GLSurfaceView. But the rendering thread is not created when Cocos2dxGLSurfaceView is instantiated, but when setRenderer is set.
Let's look at the implementation of Cocos2dxGLSurfaceView.setCocos2dxRenderer:
public void setCocos2dxRenderer(final Cocos2dxRenderer renderer) {
this.mCocos2dxRenderer = renderer;
(this.mCocos2dxRenderer);
}
setRender is a method implemented by the Cocos2dxGLSurfaceView parent class GLSurfaceView. In the Android SDK file, we see:
public void setRenderer(Renderer renderer) {
checkRenderThreadState();
if (mEGLConfigChooser == null) {
mEGLConfigChooser = new SimpleEGLConfigChooser(true);
}
if (mEGLContextFactory == null) {
mEGLContextFactory = new DefaultContextFactory();
}
if (mEGLWindowSurfaceFactory == null) {
mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
}
mRenderer = renderer;
mGLThread = new GLThread(mThisWeakRef);
();
}
An instance of GLThread is created and executed here. As for what the rendering thread does, we can see through its run method:
@Override
public void run() {
setName("GLThread " + getId());
if (LOG_THREADS) {
("GLThread", "starting tcodetitle"> Copy the codeThe code is as follows:
while (true) {
synchronized (sGLThreadManager) {
while (true) {
…. …
if (! ()) {
event = (0);
break;
}
}
}//end of synchronized (sGLThreadManager)
if (event != null) {
();
event = null;
continue;
}
if needed
(gl, );
if needed
(gl, w, h);
if needed
(gl);
}
Here we see the three callback methods of event and Renderer onSurfaceCreated, onSurfaceChanged and onDrawFrame. We will conduct detailed analysis of these three functions in the future.
6. The entrance to game logic
There are many C++ code files under HelloCpp's Classes (involving specific game logic), and there are also Jni glue codes under HelloCpp's android project jni directory. So how do these codes interact with the engine and take effect?
As mentioned above, some renderings involving the picture are performed in GLThread, which involves three methods: onSurfaceCreated, onSurfaceChanged and onDrawFrame. Let's look at the implementation of the method, which will be called when the Surface is first rendered:
public void onSurfaceCreated(final GL10 pGL10, final EGLConfig pEGLConfig) {
(, );
= ();
}
This method continues to call the nativeInit code in the jni directory of HelloCpp project:
void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv* env, jobject thiz, jint w, jint h)
{
if (!CCDirector::sharedDirector()->getOpenGLView())
{
CCEGLView *view = CCEGLView::sharedOpenGLView();
view->setFrameSize(w, h);
AppDelegate *pAppDelegate = new AppDelegate();
CCApplication::sharedApplication()->run();
}
else
{
ccGLInvalidateStateCache();
CCShaderCache::sharedShaderCache()->reloadDefaultShaders();
ccDrawInit();
CCTextureCache::reloadAllTextures();
CCNotificationCenter::sharedNotificationCenter()->postNotification(EVENT_COME_TO_FOREGROUND, NULL);
CCDirector::sharedDirector()->setGLDefaultValues();
}
}
This seems to give us the entrance to the game logic:
CCEGLView *view = CCEGLView::sharedOpenGLView();
view->setFrameSize(w, h);
AppDelegate *pAppDelegate = new AppDelegate();
CCApplication::sharedApplication()->run();
Continue to track the CCApplication::run method:
int CCApplication::run()
{
// Initialize instance and cocos2d.
if (! applicationDidFinishLaunching())
{
return 0;
}
return -1;
}
applicationDidFinishLaunching, yes, this is the entrance to game logic. We have to go back to the Samples code directory to find the corresponding method implementation.
//cocos2d-x-2.2.2/samples/Cpp/HelloCpp/Classes/
bool AppDelegate::applicationDidFinishLaunching() {
// initialize director
CCDirector* pDirector = CCDirector::sharedDirector();
CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();
pDirector->setOpenGLView(pEGLView);
CCSize frameSize = pEGLView->getFrameSize();
… …
// turn on display FPS
pDirector->setDisplayStats(true);
// set FPS. the default value is 1.0/60 if you don't call this
pDirector->setAnimationInterval(1.0 / 60);
// create a scene. it's an autorelease object
CCScene *pScene = HelloWorld::scene();
// run
pDirector->runWithScene(pScene);
return true;
}
Indeed, we have done a lot of engine parameter settings in applicationDidFinishLaunching. Next, the CCDirector instance appeared and the HelloWorld Scene instance was run. But this is still part of the initialization, although the method name sounds like some kind of continuous coherent behavior:
//cocos2d-x-2.2.2/cocos2dx/
void CCDirector::runWithScene(CCScene *pScene)
{
… …
pushScene(pScene);
startAnimation();
}
void CCDisplayLinkDirector::startAnimation(void)
{
if (CCTime::gettimeofdayCocos2d(m_pLastUpdate, NULL) != 0)
{
CCLOG("cocos2d: DisplayLinkDirector: Error on gettimeofday");
}
m_bInvalid = false;
}
Both methods simply initialize some data member variables and do not really drive the engine.
7. Drive the engine
The reason why the game screen is moving is because the screen refreshes at a higher frame rate, so that the human eye will see continuous movements, which is the same as the movie's projection principle. Where are the codes for these driver screen refreshes in the Cocos2d-x engine?
Let's review the GLThread thread we talked about before. We have said that the work of rendering is done by it. The core of GLThread is the guardedRun function, which calls methods to continuously render the picture in a "dead loop".
Let's take a look at the engine implementation method:
public void onDrawFrame(final GL10 gl) {
/*
* FPS controlling algorithm is not accurate, and it will slow down FPS
* on some devices. So comment FPS controlling code.
*/
/*
final long nowInNanoSeconds = ();
final long interval = nowInNanoSeconds – ;
*/
// should render a frame when onDrawFrame() is called or there is a
// "ghost"
();
/*
// fps controlling
if (interval < ) {
try {
// because we render it before, so we should sleep twice time interval
(( – interval) / );
} catch (final Exception e) {
}
}
= nowInNanoSeconds;
*/
}
This method is implemented strangely, and seems to have been modified many times, but in the end, I decided to only retain one method call: (). Judging from the commented out code, it seems that I want to control the frame rate of Render Thread rendering in this method. But due to the unsatisfactory control, I simply stopped controlling it, making guardedRun really a dead loop. But from the status display when HelloCpp Sample is running, the screen is always maintained at around 60 frames, which is very surprising. It is said that the Cocos2d-x 3.0 version has redesigned the rendering mechanism. (Postscript: Although there is no frame rate control on Android, the real rendered frame rate is actually affected by the "vertical synchronization" signal - vertical sync. In the game, a powerful graphics card may quickly draw the image of one screen, but without the vertical synchronization signal, the graphics card cannot draw the next screen. Only when the vsync signal arrives can it be drawn. In this way, the FPS is actually restricted by the operating system refresh rate value).
From the naming point of view of nativeRender, this is obviously a C++-written function implementation. We can only search in the jni directory.
cocos2d-x-2.2.2/cocos2dx/platform/android/jni/ Java_org_cocos2dx_lib_Cocos2dxRenderer.cpp
JNIEXPORT void JNICALL Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeRender(JNIEnv* env) {
cocos2d::CCDirector::sharedDirector()->mainLoop();
}
nativeRender is also very concise, and it directly calls the mainLoop of CCDirector, which means that the real work in each frame is CCDirector::mainLoop. At this point, we finally found the engine rendered drive: GLThead::guardedRun, refreshing the picture in a "violent loop" way, allowing us to feel the charm of "moving".
8. mainLoop
Let's take a look at the work done by mainLoop. mainLoop is a pure virtual function of the CCDirector class. The subclass of CCDisplayLinkDirector really implements it:
//
void CCDisplayLinkDirector::mainLoop(void)
{
if (m_bPurgeDirecotorInNextLoop)
{
m_bPurgeDirecotorInNextLoop = false;
purgeDirector();
}
else if (! m_bInvalid)
{
drawScene();
// release the objects
CCPoolManager::sharedPoolManager()->pop();
}
}
void CCDirector::drawScene(void)
{
// calculate "global" dt
calculateDeltaTime();
//tick before glClear: issue #533
if (! m_bPaused)
{
m_pScheduler->update(m_fDeltaTime);
}
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/* to avoid flickr, nextScene MUST be here: after tick and before draw.
XXX: Which bug is this one. It seems that it can't be reproduced with v0.9 */
if (m_pNextScene)
{
setNextScene();
}
kmGLPushMatrix();
// draw the scene
if (m_pRunningScene)
{
m_pRunningScene->visit();
}
// draw the notifications node
if (m_pNotificationNode)
{
m_pNotificationNode->visit();
}
if (m_bDisplayStats)
{
showStats();
}
kmGLPopMatrix();
m_uTotalFrames++;
// swap buffers
if (m_pobOpenGLView)
{
m_pobOpenGLView->swapBuffers();
}
if (m_bDisplayStats)
{
calculateMPF();
}
}
Frame rendering is completed by drawScene() called by mainLoop. The drawScene method renders nodes one by one according to the latest attributes of nodes according to the rendering tree under Scene, and adjusts the scheduling timer data of each Node. The details will not be explained in detail here.
9. Interaction between UI thread and GLThread
The user's screen touch action is captured by the UI thread. This type of event needs to be passed to the engine, and GLThread repaints the picture according to the latest status of each screen element. The UI thread is responsible for handling user interaction events and notifying GLThread of specific events. The UI thread passes events and handling methods to GLThread through the queueEvent method of Cocos2dxGLSurfaceView.
The queueEvent method of Cocos2dxGLSurfaceView is inherited from its parent class GLSurfaceView:
public void queueEvent(Runnable r) {
(r);
}
The queueEvent method of GLThread is implemented as follows:
public void queueEvent(Runnable r) {
if (r == null) {
throw new IllegalArgumentException("r must not be null");
}
synchronized(sGLThreadManager) {
(r);
();
}
}
This method puts the event into the EventQueue mutex and notifies the thread blocking the Queue to pick up.
The running GLThread instance will take out the runnable event from the event queue and run it in guardedRun.
while (true) {
synchronized (sGLThreadManager) {
while (true) {
if (mShouldExit) {
return;
}
if (! ()) {
event = (0);
break;
}
…….
}
}
… …
if (event != null) {
();
event = null;
continue;
}
…
}
Various events of the Activity, Pause, Resume, Stop and various screen touch events of the View are passed to GLThread through queueEvent, such as: View's onKeyDown method:
//
@Override
public boolean onKeyDown(final int pKeyCode, final KeyEvent pKeyEvent) {
switch (pKeyCode) {
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_MENU:
(new Runnable() {
@Override
public void run() {
.(pKeyCode);
}
});
return true;
default:
return (pKeyCode, pKeyEvent);
}
}
10. Summary
With the above understanding of the Cocos2d-x engine, it is even more convenient to write game code. At least when problems arise, we know where to look. Just like after knowing the engine of a car, once a power failure occurs, we basically know how to eliminate it. But no matter how thorough the engine is, it cannot mean that you can design and produce a good car. The same is true for games. Understanding the engine is one thing, and designing and implementing a good game is another thing. The learning engine is just the starting point for writing a game.