バックキー押下2013/12/06

今までのサンプルでは、Androidのバックキーを押すと有無を言わせずアプリが終了します。
ゲームなんかだと前の画面に戻ったりメニューを出したりしたいので、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
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
	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;
}

画面をタッチすると、現在の明るさに応じてフェードイン・フェードアウトを行います。


プロジェクト一式は、こちらから。

サウンド処理は非同期で その32013/11/30

サウンド管理でコマンドを 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からコマンドを受け取って実行する部分です。
コマンドの値に従ってサウンドプレイヤーの関数を呼び出しています。

再生・停止など命令がいろいろあるので処理は多いですが、どれもやっていることは同じです。


サウンドの命令自体は同じなので、サンプルに変更はありません。

プロジェクト一式は、こちらから。

サウンド処理は非同期で その22013/11/29

サウンドの非同期処理のための java側の関数はできたので、次は native側の処理です。

ただ、今まで 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自体を毎回取得し直さなければいけないそうです。