バックキー押下 ― 2013/12/06
今までのサンプルでは、Androidのバックキーを押すと有無を言わせずアプリが終了します。
ゲームなんかだと前の画面に戻ったりメニューを出したりしたいので、native側でバックキーの処理を行えるようにします。
java側の処理です。
BaseActivity.java
onBackPressed()でバックキーの押下を検知して、updateNative()で native側に状態を送る…だけです。
native側です。
SysMain.cpp
java側から受け取ったバックキーの状態を sys::key_statusにセットする…だけです。やっぱり。
common.h
バックキー押下時の sys::key_statusの値はこちらで定義しています。
サンプルです。
AppMain.cpp
バックキーが押されると、
・サウンド se.oggを再生
・60フレームかけてフェードアウトの後、アプリ終了
という処理になっています。
プロジェクト一式はこちらから。
ゲームなんかだと前の画面に戻ったりメニューを出したりしたいので、native側でバックキーの処理を行えるようにします。
java側の処理です。
BaseActivity.java
public final static int KEY_BACK = 1; // バックキー
protected static int key_status; // キー入力状態
/******************** バックキー入力 ********************/ @Override public void onBackPressed() { key_status = KEY_BACK; }
{ int _key = key_status; key_status = 0; if ( !updateNative(touch_status, _key) ) { // native部稼働 finish(); break; } else if ( phase != 1 ) { break; } }
onBackPressed()でバックキーの押下を検知して、updateNative()で native側に状態を送る…だけです。
native側です。
SysMain.cpp
/********** 稼働 **********/ JNIEXPORT jboolean JNICALL Java_sys_BaseActivity_updateNative(JNIEnv* env, jobject, jbyteArray touch, jint key) { Renderer::update(); // 描画前処理 SoundManager::update(); // サウンド処理 key_status = key; // キー入力状態
java側から受け取ったバックキーの状態を sys::key_statusにセットする…だけです。やっぱり。
common.h
// キー種類 enum { KEY_BACK = 1, // バックキー };
バックキー押下時の sys::key_statusの値はこちらで定義しています。
サンプルです。
AppMain.cpp
/****************************** 稼働 戻り値 アプリ続行か ******************************/ Bool update_app(void) { sprite[SPR_PHOTO].draw(0.0f, 0.0f); // 背景 for (int i = 0; i < 4; i++) { // ビー玉 int t = ((cnt + i*15) % 60) - 30; sprite[SPR_BALL_BLUE + i].draw(((i*180 + cnt*4) % 720) - 360, 400 - (30*30 - t*t)/2); } cnt++; if ( end_cnt == 0 ) { if ( sys::key_status == sys::KEY_BACK ) { // バックキー sys::SoundManager::play(0, "se.ogg", sys::SoundPlayer::FILE_ASSET); sys::Renderer::fade_out(60); end_cnt = 60; } } else if ( --end_cnt == 0 ) { return FALSE; // アプリ終了 } return TRUE; }
バックキーが押されると、
・サウンド se.oggを再生
・60フレームかけてフェードアウトの後、アプリ終了
という処理になっています。
プロジェクト一式はこちらから。
端末解像度はさまざま ― 2013/12/04
アプリの画面の大きさは def.hの SCREEN_WIDTH、SCREEN_HEIGHTで定義され、実際のAndoroid端末の画面に合わせて拡大・縮小されて表示されます。しかし、アプリの定義と端末で画面の比率が合っているとは限りません。
例えばアプリの画面が3:4に対して端末の画面が9:16の縦長の場合、描画の上下はカットされ黒い帯となります。
今回は、端末に合わせるための画面サイズと、内部で描画できる画面のサイズを別に持って、画面比率の違う端末で黒い部分にも絵が出せるようにしてみます。
例えば、サンプルの定義
def.h
この場合、アプリの内部での画面範囲は (-640/2, -1136/2) - (640/2, 1136/2)になりますが、640x960の端末では (-640/2, -960/2) - (640/2, 960/2)の範囲しか描画されません。
上下88ドットずつ、端末の画面サイズによって描画されたりされなかったりする部分となります。
640:960より横長の端末では、縦960ドット分が描画され左右に黒い帯ができます。
このように仕様を決めてしまえば、変更点は多くありません。
Renderer.h
クラス sys::Rendererに表示画面解像度を追加しています。
Renderer.cpp
画面初期化部分です。
フレームバッファは表示画面の大きさ LIMIT_WIDTH、LIMIT_HEIGHTに合わせます。
画面の拡大率は今まで同様 SCREEN_WIDTH、SCREEN_HEIGHTで計算して、limit_rectをそこに合わせるようにします。
描画部分です。
ビューポートを limit_rectで設定しています。
描画ではもう screen_rectの方は使用していないのですが、タッチパネルの座標変換で使用しているので screen_rect自体は残しています。
サンプルでは、今まで640x960だった背景を 640x1136のものに差し替えています。
例えばアプリの画面が3:4に対して端末の画面が9:16の縦長の場合、描画の上下はカットされ黒い帯となります。
今回は、端末に合わせるための画面サイズと、内部で描画できる画面のサイズを別に持って、画面比率の違う端末で黒い部分にも絵が出せるようにしてみます。
例えば、サンプルの定義
def.h
static const int SCREEN_WIDTH = 640, // 画面サイズ SCREEN_HEIGHT = 960; static const int LIMIT_WIDTH = 640, // 表示画面サイズ LIMIT_HEIGHT = 1136;
この場合、アプリの内部での画面範囲は (-640/2, -1136/2) - (640/2, 1136/2)になりますが、640x960の端末では (-640/2, -960/2) - (640/2, 960/2)の範囲しか描画されません。
上下88ドットずつ、端末の画面サイズによって描画されたりされなかったりする部分となります。
640:960より横長の端末では、縦960ドット分が描画され左右に黒い帯ができます。
このように仕様を決めてしまえば、変更点は多くありません。
Renderer.h
static SRect screen_rect; // 画面解像度 static SRect limit_rect; // 表示画面解像度
クラス sys::Rendererに表示画面解像度を追加しています。
Renderer.cpp
if ( width*SCREEN_HEIGHT < height*SCREEN_WIDTH ) { // 横長(上下カット) screen_rect.w = width; screen_rect.h = width*SCREEN_HEIGHT/SCREEN_WIDTH; limit_rect.w = width*LIMIT_WIDTH/SCREEN_WIDTH; limit_rect.h = width*LIMIT_HEIGHT/SCREEN_WIDTH; } else { // 縦長(左右カット) screen_rect.w = height*SCREEN_WIDTH/SCREEN_HEIGHT; screen_rect.h = height; limit_rect.w = height*LIMIT_WIDTH/SCREEN_HEIGHT; limit_rect.h = height*LIMIT_HEIGHT/SCREEN_HEIGHT; } screen_rect.x = (width - screen_rect.w)/2; screen_rect.y = (height - screen_rect.h)/2; limit_rect.x = (width - limit_rect.w)/2; limit_rect.y = (height - limit_rect.h)/2;
画面初期化部分です。
フレームバッファは表示画面の大きさ LIMIT_WIDTH、LIMIT_HEIGHTに合わせます。
画面の拡大率は今まで同様 SCREEN_WIDTH、SCREEN_HEIGHTで計算して、limit_rectをそこに合わせるようにします。
// フレームバッファテクスチャ描画 glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(limit_rect.x, limit_rect.y, limit_rect.w, limit_rect.h);
描画部分です。
ビューポートを limit_rectで設定しています。
描画ではもう screen_rectの方は使用していないのですが、タッチパネルの座標変換で使用しているので screen_rect自体は残しています。
サンプルでは、今まで640x960だった背景を 640x1136のものに差し替えています。
フェードイン・フェードアウト ― 2013/12/02
画面描画にはフレームバッファを使用しています(11/6の記事参照)。
最終的に画面に出すフレームバッファはテクスチャとして描画していますので、カラーを設定することができます。
このフレームバッファ描画時のカラーを変更することによって、画面のフェードイン・フェードアウトを実装してみます。
Renderer.h
クラス sys::Rendererの静的メンバ変数です。
固定だったカラーバッファを変更できるようにしています。
フェードイン・フェードアウトの関数です。
フェード時間をフレーム数単位で指定します。
Renderer.cpp
描画前に明るさの変更を行い、カラーバッファに値を設定します(249~268行)。
フレームバッファテクスチャ描画で、設定したカラーバッファを指定しています(307行)。
サンプル
AppMain.cpp
画面をタッチすると、現在の明るさに応じてフェードイン・フェードアウトを行います。
プロジェクト一式は、こちらから。
最終的に画面に出すフレームバッファはテクスチャとして描画していますので、カラーを設定することができます。
このフレームバッファ描画時のカラーを変更することによって、画面のフェードイン・フェードアウトを実装してみます。
Renderer.h
static GLubyte screen_color[4*4]; // 画面描画カラー static int fade_bright; // 画面の明るさ static int fade_speed; // フェードの速さ
クラス sys::Rendererの静的メンバ変数です。
固定だったカラーバッファを変更できるようにしています。
static void fade_in(int _cnt = 500/FRAME_PERIOD) // フェードイン { fade_speed = (_cnt > 0) ? (254 + _cnt)/_cnt : 255; } static void fade_out(int _cnt = 500/FRAME_PERIOD) // フェードアウト { fade_speed = (_cnt > 0) ? -(254 + _cnt)/_cnt : -255; } static int get_bright(void) // 画面明るさ取得 { return fade_bright; }
フェードイン・フェードアウトの関数です。
フェード時間をフレーム数単位で指定します。
Renderer.cpp
/******************** 描画(後処理) ********************/ void Renderer::draw(void) { if ( fade_speed > 0 ) { // フェードイン fade_bright += fade_speed; if ( fade_bright >= 255 ) { fade_bright = 255; fade_speed = 0; } for (int i = 0; i < 4*3; i++) { screen_color[i + i/3] = (GLubyte)fade_bright; } } else if ( fade_speed < 0 ) { // フェードアウト fade_bright += fade_speed; if ( fade_bright <= 0 ) { fade_bright = 0; fade_speed = 0; } for (int i = 0; i < 4*3; i++) { screen_color[i + i/3] = (GLubyte)fade_bright; } } static const GLfloat _projection[4*4] = { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, }; static const GLfloat _texcoords[] = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f }; static const GLfloat _vertices[] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, }; ShaderProgram* _shader = use_shader(SHADER_TEXTURE); // フレームバッファテクスチャ描画 glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(screen_rect.x, screen_rect.y, screen_rect.w, screen_rect.h); glUniformMatrix4fv(_shader->projection, 1, GL_FALSE, _projection); glBindTexture(GL_TEXTURE_2D, frame_buffer->texture); glVertexAttribPointer(_shader->texcoord, 2, GL_FLOAT, GL_FALSE, 0, _texcoords); glVertexAttribPointer(_shader->position, 2, GL_FLOAT, GL_FALSE, 0, _vertices); glVertexAttribPointer(_shader->color, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, screen_color); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); }
描画前に明るさの変更を行い、カラーバッファに値を設定します(249~268行)。
フレームバッファテクスチャ描画で、設定したカラーバッファを指定しています(307行)。
サンプル
AppMain.cpp
/****************************** 稼働 戻り値 アプリ続行か ******************************/ Bool update_app(void) { sprite[SPR_PHOTO].draw(0.0f, 0.0f); // 背景 for (int i = 0; i < 4; i++) { // ビー玉 int t = ((cnt + i*15) % 60) - 30; sprite[SPR_BALL_BLUE + i].draw(((i*180 + cnt*4) % 720) - 360, 400 - (30*30 - t*t)/2); } cnt++; if ( sys::TouchPanel[0].flag & sys::TouchManager::TRIGGER ) { if ( sys::Renderer::get_bright() == 0 ) { sys::Renderer::fade_in(60); // フェードイン } else if ( sys::Renderer::get_bright() == 255 ) { sys::Renderer::fade_out(60); // フェードアウト } } return TRUE; }
画面をタッチすると、現在の明るさに応じてフェードイン・フェードアウトを行います。
プロジェクト一式は、こちらから。
サウンド処理は非同期で その3 ― 2013/11/30
サウンド管理でコマンドを javaに送る処理、そしてキューの中身を受け取ってサウンドプレイヤーの処理を実行する部分を作成します。これでサウンドの非同期処理の実装完了となります。
Sound.cpp
サウンド管理終了時、まだキューにコマンドが残っている可能性がありますので java側の関数を呼んでクリアしています。
再生などサウンドのコマンドを受け取る処理です。
直接サウンドプレイヤーの処理を呼んでいたのを、キューにコマンドを送るように変更しています。
javaからコマンドを受け取って実行する部分です。
コマンドの値に従ってサウンドプレイヤーの関数を呼び出しています。
再生・停止など命令がいろいろあるので処理は多いですが、どれもやっていることは同じです。
サウンドの命令自体は同じなので、サンプルに変更はありません。
プロジェクト一式は、こちらから。
Sound.cpp
/********** 終了 **********/ void SoundManager::quit(void) { { // コマンド停止 JNIEnv* env; Bool attach_flag = FALSE; if ( g_JavaVM->GetEnv((void**)&env, JNI_VERSION_1_6) < 0 ) { if ( g_JavaVM->AttachCurrentThread(&env, NULL) < 0 ) { goto error; } attach_flag = TRUE; } jclass clazz = env->FindClass("sys/SoundManager"); if ( clazz ) { jmethodID mid = env->GetStaticMethodID(clazz, "stop_command", "()V"); if ( mid ) { env->CallStaticVoidMethod(clazz, mid); } } if ( attach_flag ) { g_JavaVM->DetachCurrentThread(); } } error : if ( player ) { delete[] player; player = NULL; } SoundPlayer::quit_engine(); // サウンドエンジン終了 }
サウンド管理終了時、まだキューにコマンドが残っている可能性がありますので java側の関数を呼んでクリアしています。
// サウンドコマンド enum { COMMAND_UPDATE, // 稼働 COMMAND_PREPARE, // 準備 COMMAND_PLAY, // 再生・再開 COMMAND_STOP, // 停止 COMMAND_VOLUME, // 音量設定 COMMAND_NEXT, // 連続再生設定 COMMAND_PAUSE, // 一時停止 COMMAND_PAUSE_SYS, COMMAND_RESUME, // 再開 COMMAND_RESUME_SYS, }; /********** 稼働 **********/ void SoundManager::update(void) { set_command(-1, COMMAND_UPDATE); } /******************************************************** 再生 引数 _channel = チャンネル番号 _data = サウンドデータ _size = サウンドデータサイズ _loop = ループ回数(0:無限ループ) _vol = 音量 *******************************************************/ void SoundManager::prepare(int _channel, const void* _data, u32 _size, int _loop, float _vol) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_PREPARE, _data, _size, _loop, _vol); } void SoundManager::play(int _channel) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_PLAY); } void SoundManager::play(int _channel, const void* _data, u32 _size, int _loop, float _vol) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_PLAY, _data, _size, _loop, _vol); } void SoundManager::play(void) { set_command(-1, COMMAND_PLAY); // 全て再生開始 } /******************************************* 停止 引数 _channel = チャンネル番号 _cnt = フェード時間 *******************************************/ void SoundManager::stop(int _channel, int _cnt) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_STOP, NULL, (u32)_cnt); } void SoundManager::stop(void) { set_command(-1, COMMAND_STOP); // 全て停止 } /********************************************************* 音量設定 引数 _channel = チャンネル番号 _vol = 音量(0.0:最小 ~ 1.0:最大) *********************************************************/ void SoundManager::set_volume(int _channel, float _vol) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_VOLUME, NULL, 0, 0, _vol); } /***************************************************** マスター音量設定 引数 _vol = 音量(0.0:最小 ~ 1.0:最大) *****************************************************/ void SoundManager::set_master_volume(float _vol) { set_command(-1, COMMAND_VOLUME, NULL, 0, 0, _vol); } /******************************************************* 連続再生設定 引数 _channel = チャンネル番号 _data = サウンドデータ _size = サウンドデータサイズ _loop = ループ回数(0:無限ループ) *******************************************************/ void SoundManager::set_next(int _channel, const void* _data, u32 _size, int _loop) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_NEXT, _data, _size, _loop); } /********************************************* 一時停止 引数 _channel = チャンネル番号 *********************************************/ void SoundManager::pause(int _channel) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_PAUSE); } void SoundManager::pause(void) { set_command(-1, COMMAND_PAUSE); // 全て一時停止 } void SoundManager::pause_system(void) // システムによる一時停止 { set_command(-1, COMMAND_PAUSE_SYS); } /********************************************* 再開 引数 _channel = チャンネル番号 *********************************************/ void SoundManager::resume(int _channel) { assert((0 <= _channel) && (_channel < SOUND_CHANNEL_MAX)); set_command(_channel, COMMAND_RESUME); } void SoundManager::resume(void) { set_command(-1, COMMAND_RESUME); // 全て再開 } void SoundManager::resume_system(void) // システムによる再開 { set_command(-1, COMMAND_RESUME_SYS); }
再生などサウンドのコマンドを受け取る処理です。
直接サウンドプレイヤーの処理を呼んでいたのを、キューにコマンドを送るように変更しています。
/******************************** Javaからコマンドを受け取る ********************************/ extern "C" { JNIEXPORT void JNICALL Java_sys_SoundManager_sendSoundCommand(JNIEnv*, jobject, jshort, jshort, jint, jint, jshort, jfloat); } JNIEXPORT void JNICALL Java_sys_SoundManager_sendSoundCommand(JNIEnv*, jobject, jshort _channel, jshort _command, jint _data, jint _size, jshort _loop, jfloat _volume) { SoundManager::get_command((int)_channel, (int)_command, (void*)_data, (u32)_size, (int)_loop, (float)_volume); } void SoundManager::get_command(int _channel, int _command, void* _data, u32 _size, int _loop, float _volume) { switch ( _command ) { case COMMAND_UPDATE : for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { player[i].update(); } break; case COMMAND_PREPARE : player[_channel].prepare(_data, _size, _loop, _volume); break; case COMMAND_PLAY : if ( _channel >= 0 ) { if ( _size ) { player[_channel].prepare(_data, _size, _loop, _volume); } player[_channel].play(); } else { for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { // 全て再開 player[i].play(); } } break; case COMMAND_STOP : if ( _channel >= 0 ) { player[_channel].stop((int)_size); } else { for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { // 全て停止 player[i].stop(); } } break; case COMMAND_VOLUME : if ( _channel >= 0 ) { player[_channel].set_volume(_volume); } else if ( SoundPlayer::set_master_volume(_volume) ) { // マスター音量設定 for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { player[i].set_volume(); // 音量再設定 } } break; case COMMAND_NEXT : player[_channel].set_next(_data, _size, _loop); break; case COMMAND_PAUSE : if ( _channel >= 0 ) { player[_channel].pause(TRUE); } else { for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { // 全て一時停止 player[i].pause(TRUE); } } break; case COMMAND_PAUSE_SYS : for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { // システムによる再開 player[i].pause(FALSE); } break; case COMMAND_RESUME : if ( _channel >= 0 ) { player[_channel].resume(); } else if ( _size ) { for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { // 全て再開 player[i].resume(); } } break; case COMMAND_RESUME_SYS : for (int i = 0; i < SOUND_CHANNEL_MAX; i++) { // システムによる再開 player[i].resume(SoundPlayer::PLAYING); } break; } }
javaからコマンドを受け取って実行する部分です。
コマンドの値に従ってサウンドプレイヤーの関数を呼び出しています。
再生・停止など命令がいろいろあるので処理は多いですが、どれもやっていることは同じです。
サウンドの命令自体は同じなので、サンプルに変更はありません。
プロジェクト一式は、こちらから。
サウンド処理は非同期で その2 ― 2013/11/29
サウンドの非同期処理のための java側の関数はできたので、次は native側の処理です。
ただ、今まで java側から nativeを呼ぶことはあっても native側から javaの関数を呼んでいるところはありませんでした。ということで今回はそのあたりの処理を行うのですが、残念ながら他人に説明できるほどきちんと理解はしていませんので、ソースで手順だけ示します。ご容赦を。
SysMain.cpp
JNI_OnLoadは nativeのライブラリが読み込まれたときに呼ばれる関数です。
引数の JavaVMオブジェクトを保存しておきます。
Sound.cpp
サウンド処理の命令を java側に送る関数です。
呼び出しに必要な JNIEnvを JavaVMオブジェクトから取得しています。
実行は、
865行:sys/SoundManagerクラスの
868行:静的関数 void set_command(short, short, int, int, short, float)を
871行:引数を指定して呼び出す
という流れになっています。
クラスや関数を文字列で指定したり、そこそこ重そうな処理なのでいろいろ使いまわししたいところですが、JNIEnv自体を毎回取得し直さなければいけないそうです。
ただ、今まで java側から nativeを呼ぶことはあっても native側から javaの関数を呼んでいるところはありませんでした。ということで今回はそのあたりの処理を行うのですが、残念ながら他人に説明できるほどきちんと理解はしていませんので、ソースで手順だけ示します。ご容赦を。
SysMain.cpp
JavaVM* g_JavaVM; // JavaVM情報 extern "C" jint JNI_OnLoad(JavaVM* _vm, void*) { g_JavaVM = _vm; // JavaVM保存 JNIEnv* _env; if ( _vm->GetEnv((void**)&_env, JNI_VERSION_1_6) != JNI_OK ) { // JNIのバージョンチェック return -1; } return JNI_VERSION_1_6; }
JNI_OnLoadは nativeのライブラリが読み込まれたときに呼ばれる関数です。
引数の JavaVMオブジェクトを保存しておきます。
Sound.cpp
/*********************************************** コマンドをJavaに送る 引数 _channel = チャンネル番号 _command = コマンド _data = データ _size = サイズ _loop = ループ回数 _volume = 音量 ***********************************************/ void SoundManager::set_command(int _channel, int _command, const void* _data, u32 _size, int _loop, float _volume) { JNIEnv* env; Bool attach_flag = FALSE; if ( g_JavaVM->GetEnv((void**)&env, JNI_VERSION_1_6) < 0 ) { if ( g_JavaVM->AttachCurrentThread(&env, NULL) < 0 ) { return; } attach_flag = TRUE; } jclass clazz = env->FindClass("sys/SoundManager"); if ( clazz ) { jmethodID mid = env->GetStaticMethodID(clazz, "set_command", "(SSIISF)V"); if ( mid ) { env->CallStaticVoidMethod(clazz, mid, (short)_channel, (short)_command, (int)_data, (int)_size, (short)_loop, _volume); } } if ( attach_flag ) { g_JavaVM->DetachCurrentThread(); } }
サウンド処理の命令を java側に送る関数です。
呼び出しに必要な JNIEnvを JavaVMオブジェクトから取得しています。
実行は、
865行:sys/SoundManagerクラスの
868行:静的関数 void set_command(short, short, int, int, short, float)を
871行:引数を指定して呼び出す
という流れになっています。
クラスや関数を文字列で指定したり、そこそこ重そうな処理なのでいろいろ使いまわししたいところですが、JNIEnv自体を毎回取得し直さなければいけないそうです。
最近のコメント