📜 ⬆️ ⬇️

As I wrote Pacman'a and what came of it. Part 2


Hello, habr! In the second part of the article, I will continue the story of how I wrote a clone of the game Pacman. The first part can be read here .
From the moment when I last worked on papman about three weeks passed. Most of the session was over, it took a little more time and I decided to continue. At this point, there was a desire to finish the game to a state where it can be put on the Google Play Market, although at the very beginning of development I didn’t even think about it. In addition, finishing to a playable state is a good exercise. Somewhere I heard that games (and indeed applications) should be finished.
Let me remind you that the development of the game was carried out using the Android NDK (C ++) and OpenGL ES 2.0.



To begin with, I made a list of what I thought was necessary to complete the work on the game:

Now in more detail, on points:

Bonuses

Bonuses in the game are needed for a change. In order not to spend a lot of time on them, I introduced a new abstract class Bonus , from which LifeBonus immediately inherited. As you might guess, LifeBonus gives the player one life. I must say, bonuses are very organically fit into the existing hierarchy:

I stopped at this point. It is extremely easy to create other bonuses, you only need to inherit them from Bonus.
In connection with the bonuses, the Statistics class is worth mentioning. This class is needed to collect various statistics, such as input / output / pause level, counting points and time inside the level. All these statistics are collected and can be used to create a table of achievements or even network tables of records. Inside, the Statistics class is implemented as a deterministic finite state machine.
')
Text output

At first, I wanted to do without textual information at all, because (IMHO) embedding the text entails crutches. It turned out that it was difficult to do without text, it was easier to implement its output.

To display the text, I used a simple technique: the graphic representation of symbols of a monospaced font is taken by rectangles from a texture of approximately the same type as in the figure.
The first character is a space, the rest are consecutive. The raster in the figure is needed only for convenience (the baseline is visible and the fact that all the characters are aligned). In the application, the texture is the same, but with a transparent background. It would be more correct to render the font into the texture at the execution stage, and not to store the static texture, but this would only add complexity, since It is not clear how to align characters in rectangles.
A special GUI element, Label , is derived from Control 'a. It is used in the title of the game window for displaying game statistics, in the Win / GameOver menu to notify the player about winning or losing, respectively.

Sound

A rare game goes without sound (I guess I won’t be able to name such games). So I decided to add background music and game sounds to my game too.

Technical part

Before that, I had no experience with sound. There are at least 3 options:

I dropped the first option immediately, because I considered that it was not a very elegant solution. The choice of the two remaining ones was made in favor of OpenSL ES (I wrote about this article , thereby earning an invite here).
The Audio class has been developed for working with music. It has a set of static methods for switching on one or another background music, quickly playing sounds and controlling the audibility of music and sounds (separately from each other).
The user controls from the main menu of the game, which for this is similar to the state buttons - CheckBox , which is inherited from Control 'a.

Composer part

At first, I wanted to choose music and sounds from publicly available on a huge number of music sites. But this idea failed as it turned out to be problematic for me to pick up music.
Fortunately, my friend musician Timur Ramazanov came to help me, who agreed to write tracks for me. To me personally, music seems very appropriate to the design and mood of the game. Those who are interested in his other works can get acquainted with them on VKontakte or on soundcloud.
Background music is divided into two parts: the game and the menu. It is looped and saved in ogg format. Game sounds are saved in wav format.

Saving information

During the game, various information must be stored permanently. This, for example, records the player or his sound settings.
To do this, a wrapper is written over android.content.SharedPreferences. Appeal to the wrapper occurs through jni.
Wrapper code
 public class StoreManager { public static final String PACMAN_PREFERENCES = "com_zagayevskiy_pacman_store"; private Context context; /*   */ public StoreManager(Context _context){ context = _context; } /*         .       */ public void saveBoolean(String key, boolean value){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putBoolean(key, value); editor.commit(); } public boolean loadBoolean(String key, boolean defValue){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); return sp.getBoolean(key, defValue); } public void saveInt(String key, int value){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putInt(key, value); editor.commit(); } public int loadInt(String key, int defValue){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); return sp.getInt(key, defValue); } } 


C ++ code to access StoreManager via jni
Store.h:
 #include <stdlib.h> #include <stdio.h> #include <jni.h> class Store { public: static void init(JNIEnv* env, jobject _storeManager); static void saveBool(const char* name, bool value); static bool loadBool(const char* name, bool defValue); static void saveInt(const char* name, int value); static int loadInt(const char* name, int defValue); private: static JavaVM* javaVM; static jobject storeManager; static jclass storeManagerClass; static jmethodID saveBoolId; static jmethodID loadBoolId; static jmethodID saveIntId; static jmethodID loadIntId; static JNIEnv* getJNIEnv(JavaVM* jvm); }; 

Store.cpp:
 /*env  _storeManager     */ void Store::init(JNIEnv* env, jobject _storeManager){ /*   Java-,  */ if(env->GetJavaVM(&javaVM) != JNI_OK){ LOGE("Can not Get JVM"); return; } storeManager = env->NewGlobalRef(_storeManager); if(!storeManager){ LOGE("Can not create NewGlobalRef on storeManager"); return; } storeManagerClass = env->GetObjectClass(storeManager); if(!storeManagerClass){ LOGE("Can not get StoreManager class"); return; } saveBoolId = env->GetMethodID(storeManagerClass, "saveBoolean", "(Ljava/lang/String;Z)V"); if(!saveBoolId){ LOGE("Can not find method saveBoolean"); return; } /*   */ } } void Store::saveBool(const char* name, bool value){ LOGI("Store::saveBool(%s, %d)", name, value); JNIEnv* env = getJNIEnv(javaVM); if(!env){ LOGE("Can not getJNIEnv"); return; } jstring key = env->NewStringUTF(name); if(!key){ LOGE("Can not create NewStringUTF"); } env->CallVoidMethod(storeManager, saveBoolId, key, value); } bool Store::loadBool(const char* name, bool defValue){ LOGI("Store::loadBool(%s, %d)", name, defValue); JNIEnv* env = getJNIEnv(javaVM); if(!env){ LOGE("Can not getJNIEnv"); return defValue; } jstring key = env->NewStringUTF(name); if(!key){ LOGE("Can not create NewStringUTF"); } return env->CallBooleanMethod(storeManager, loadBoolId, key, defValue); } /*     load/saveInt()*/ /*   JNIEnv   ,    Java-*/ JNIEnv* Store::getJNIEnv(JavaVM* jvm){ JavaVMAttachArgs args; args.version = JNI_VERSION_1_6; args.name = "PacmanNativeThread"; args.group = NULL; JNIEnv* result; if(jvm->AttachCurrentThread(&result, &args) != JNI_OK){ result = NULL; } return result; } 



More beautiful animation and design

Initially, I only animated Pacman. I wanted to make the animation more beautiful (and not in 4 frames), and make animation for bonuses and enemies. All this in one style.
At some point, the idea arose to make Pacman in the form of a fireball, and his enemies in the form of water droplets.
The most ideal option for me was to make a beautiful frame-by-frame animation. There are no problems in the program plan, but there is a problem of drawing frames. I ran into the problem of finding a designer and explaining exactly what I want. I have not solved this problem. Then I thought for a while and decided to make a fully program animation. And the designer ordered only tiles of different sizes, which cost me $ 50.

Software animation


In order to make the animation easy to use, I implemented two successor classes of the aforementioned IRenderable : Plume for the animation of the “loop” and Pulsation for the “pulsations”.
In the screenshot, the trails of various lengths are characters - Pacman and monsters, and the ripple is a larger point in the center of the heart. This is how extra life is shown on the map.
The idea of ​​both classes is based on the “brush” effect. At each step, an object of the Plume class receives the coordinates of the object being animated and remembers (or does not remember, depending on the desired length of the loop — the more often the memorization, the shorter the loop) are in the container-queue. Then, using the already stored coordinates, circles are drawn using a texture similar to the one shown below.


The green-black gradient corresponds to the gradient of the alpha channel. Green - full opacity, black - full transparency. This texture is generated when the game is initialized using a fragment shader and a render to texture.
The older the coordinates, the smaller the radius of the drawn circle. Circles are drawn with the texture overlay specified when creating the loop object. At the same time, the texture coordinates are shifted depending on the coordinates being drawn and, additionally, according to the formula of Archimedes' spiral (so that when the characters stop, the animation does not freeze).
For animation Pacman'a and monsters are used plumes of different lengths, with different textures. An additional requirement for the textures of water and flame - they must be "looped", i.e. no joints should be visible. Pacman himself also uses frame-by-frame animation of jaw movement.
A ripple is realized in a similar way, in which gradient circles of various sizes simply replace each other with a certain frequency.

App name and icon

When choosing a name, I wanted to beat the fact that the game is a clone of Pacman, and Pacman is fiery. In this case, it was necessary not to offend Namco. There were various options: Fireman, Fire Man, Pyro Man, Pacman: Jaws of Fire. In the end, I settled on Pyroman: Jaws of Fire . A reference to the game Pac-Man left in the description.
I painted the application icon in Photoshop, beating Pacman's fieryness. It turned out like a goldfish and, in my opinion, funny =)

I also wanted to talk about participation in the past competition The Tactrick Android Developer Cup , in the category "Games". But, in fact, there is nothing to tell, since the competition ended abruptly - by hanging the WINNERS dies to the winners and writing a letter “Thank you for participating” to the others. I did not claim any prizes, but it was interesting where in the competition I will be. Let 66 out of 66, but it will be clear that somehow the program was evaluated.

The game is available on github .

Thanks

I want to say thanks to my girlfriend Yulia for understanding and support. In her honor drawn the first level
I also want to thank my guru and mentor Bulat Tanirbergen for the friendly support and conviction that everything is in my power
Ramazanov Timur for the tracks to the game - thanks.
Special thanks to the company ZeptoLab, thanks to which I now know Android NDK quite well, read the book by Sylvain Retabouil about the NDK and Stephan Dewhurst's book Slippery C ++, thus raising my programming level.

Source: https://habr.com/ru/post/178523/


All Articles