tdashの日記

開発やサービスに関して書いていこうと思います

Titanium Mobile: 「Appceleorガイドラインを調べてみた & 神経衰弱ゲームも作ってみた」のまとめ

Titanium Mobile を提供している会社のガイドラインは以下の場所にある。

https://wiki.appcelerator.org/display/guides/Mobile+Best+Practices

モバイルベストプラクティス

  • グローバルを汚さない
  • シングルコンテキストで動かす
  • CommonJSを使ってオブジェクト指向っぽく作る
  • 必要になるまで他のJavaScriptファイルを開かない
  • メモリーリーク対策をする

Appceleratorのガイドラインをみると、プログラムの実装方法はいろいろあるので、どのように書くべきかルールを定めないとあった。


そんな感じで、神経衰弱アプリを作ってみた。
ソースはGitHubに入れた。https://github.com/sugie/shinkeisuijaku

命名方法はスネークケースが好きなので、スネークケースで行った。


ファイル構成

Resources/
├── app.js
├── class
│   └── class_card.js
├── ctrl
│   └── ctrl_game.js
├── images
└── view
    └── win_game.js

Titanium mobileは最初にapp.jsを実行する。このapp.jsは以下のように書いた。

app.js
/***************************************
 * 神経衰弱
 */

Ti.include('card/class_card.js');

var app = {};
app.ctrl_game = require('/ctrl/ctrl_game');
app.ctrl_game.init();
app.ctrl_game.go_title();

クラス定義ファイルをインクルードして、CommonJSでゲームコントローラー ctrl_gameを呼んでこれを実行している。
画面デザインはwin_game.jsに書いたが、コントローラーとビューの切り分けは不完全かもしれない。

* コントローラー

ctrl_game.js(抜粋)
/*************************************************
 * 
 */
function go_title() {
	Ti.API.info('#cg11:go_title();')
	if( !win_game )
		win_game = require('/view/win_game')
	var win = win_game.get_win();
	win.open();
};


/****************************************************
 * 
 */
function start_game() {
	Ti.API.info('#cg27:start_game();');
	game_init();
	win_game.hide_title();
};


/***************************************************/
exports.init = init;
exports.go_title = go_title;
exports.start_game = start_game;
exports.check_open = check_open;


go_title で タイトル画面に移動。
go_game でゲーム開始
check_openはカードがクリックされた時に呼ばれる。

カード定義

class_card.js (抜粋)
/*************************************
 * cardクラス:神経衰弱でつかうカード
 */
function class_card() {
	this.view = null;
	this.card_type = null;
	this.index = null;
};

/***************************************************************/
class_card.prototype.make_view = function( _index ) {
	
	this.view = Ti.UI.createView({...	});
	this.img_ura = Ti.UI.createImageView({
		image:'/images/card/ura.png',
		visible: true,
	});
	
	this.img_card = Ti.UI.createImageView({
		visible:false,
	});
	this.view.add( this.img_ura );
	this.view.add( this.img_card );
	
	var ref = {};
	ref._this = this;
	this.view.addEventListener( 'click', function(e) {
		// check 
		app.ctrl_game.check_open( ref._this.index );
	});
};

神経衰弱で使うカードはclass_cardクラスを使って、カードの数だけインスタンスを生成して配置する。
カードがクリックされるとコントローラーのcheck_openを呼ぶ。
インスタンスの生成は以下のように行う。

ctrl_game.init function
	list_card = [];
	for( var i = 0 ; i < MAX_NUM_CARD ; i++ ) {
		list_card[i] = new class_card();
		list_card[i].make_view( i );
		win_game.get_win().add( list_card[i].get_view() );
	}

ざっくりと書きましたが、それなりにスッキリ書けたかと思います。

今まで作ってきたプログラムは、 CommonJSを使っていなかったので、今後は心を入れ替えて、リソースを節約できるように CommonJS を使って行きたいと思います。

__NSCFDictionaryエラーの原因はクォートがひとつ多かった

先週末から機能までサーバー構築をやってて、神経衰弱ゲームサンプルを触れなかった。

と、いうわけで今朝記事を更新。

Titanium mobileで「Terminating app due to uncaught exception of class '__NSCFDictionary'」エラーが発生していた。
原因は、クォートがひとつ多く入っていたことだった。

Bad

	Ti.API.info('#cg51:check_open('+_index+');')';

Ok

	Ti.API.info('#cg51:check_open('+_index+');');

なぜ、この行がエラーとして検知されなかったので__NSCFDictionaryが出なくなるまで、ソースコードを削ってどこを削ったら動くようになるかを調べた。1時間くらいかけてしまった。

なんだかなぁ。

Titanium Mobile:神経衰弱ゲームのコードは機能ひと通り書いたけど、実行時に__NSCFDictionaryエラーが発生するようになった。

一昨日は、帰ってからワイン一本開けてしまい。ダウン。
昨日はHTML5勉強会に参加して、進まなかった。

というわけで、今日更新しました。
ほぼ一通り神経衰弱ゲームの機能は書いたと思いますが、
なんだかTitanium mobileで「Terminating app due to uncaught exception of class '__NSCFDictionary'」エラーが発生し、
実行できなくなってしまいました。
トラブルシューティングが必要ですが、今日はもう出社時刻なので、外出しなければなりません。

Resources/
├── app.js
├── ctrl
│   └── ctrl_game.js
├── card
│   └── class_card.js
└── view
    └── win_game.js

app.js

/***************************************
 * 神経衰弱
 */

Ti.include('data/data_game.js');
Ti.include('card/class_card.js');

var app = {};
app.ctrl_game = require('/ctrl/ctrl_game');
app.ctrl_game.init();
app.ctrl_game.go_title();

app.jsはCommonJSでctrl_gameを作成して、呼び出すだけ。

ctrl_game.js

/**********************************************************************/
var win_game = null;
var PHASE_INIT = 0;
var PHASE_GAME = 1;
var PHASE_GAMEOVER = 2;
var PHASE_CARD_CONFIRM = 3;
var MAX_NUM_CARD = 16;	// 配置するカード数
var MAX_TYPE_CARD = 8;	// カード種類数
var MAIN_LOOP_INTERVAL = 250;	// メインループ実行間隔ミリ秒
var CONFIRM_TIME = 3000;   // カード確認表示時間ミリ秒
var list_card = [];
var score = 0;
var time_count = 0;
var timer_handle = null;
var first_open_flag = false;
var first_card_index = null;
var phase = PHASE_INIT;

/**********************************************************************
 * 
 */
function init() {
	Ti.API.info('#cg6:init();')
	//  init 処理
	if( !win_game )
	win_game = require('/view/win_game')

	list_card = [];
	for( var i = 0 ; i < MAX_NUM_CARD ; i++ ) {
		list_card[i] = new class_card();
		list_card[i].make_view( i );
		win_game.get_win().add( list_card[i].get_view() );
	}
	
	first_open_flag = false;
	first_card_index = null;
};

/**********************************************************************
 * 
 */
function game_init() {
	Ti.API.info('#cg36:game_init();')
	score = 0;
	time_count = 0;
	var wk_list_type = [];
	for( var i = 0 ; i < MAX_NUM_CARD ; i++ ) {
		wk_list_type[i] = 0;
	}
	for( var wk_card_type = 0 ; wk_card_type < MAX_TYPE_CARD ; wk_card_type++ ) {
		for( var j = 0 ; j < 2 ; j++ ) {
			var flag = true;
			while( flag ) {
				var index = Math.floor( Math.random() * MAX_NUM_CARD );
				if( wk_list_type[index]==0 ) {
					wk_list_type[index] = wk_card_type;
					flag = false;
				}
			}
		}
	}
	for( var i = 0 ; i < MAX_NUM_CARD ; i++ ) {
		list_card[i].set_card_type( i, wk_list_type[i] );
	}
	
	phase = PHASE_GAME;
	
};

/**********************************************************************
 * 
 */
function go_title() {
	Ti.API.info('#cg11:go_title();')
	if( !win_game )
		win_game = require('/view/win_game')
	var win = win_game.get_win();
	win.open();
};

/**********************************************************************
 * 
 */
function start_game() {
	Ti.API.info('#cg27:start_game();');
	game_init();
	timer_handle = setInterval( function(e) {
		main_loop();
	}, MAIN_LOOP_INTERVAL );
	win_game.hide_title();
	
};

/**********************************************************************
 * 
 */
function end_game() {
	Ti.API.info('#cg89:end_game();');
	if( timer_handle ) {
		clearInterval( timer_handle );
		timer_handle = null;
	}
	
};

/***********************************************************************/
function append_score( _delta_score ) {
	score += delta_score;
};

/***********************************************************************/
function check_open( _index ) {
	Ti.API.info('#cg51:check_open('+_index+');')';
	if( phase != PHASE_GAME )
		return;
	
	// card open
	list_card[i].open();
	
	if( first_open_flag ) {
		first_open_flag = true;
		first_card_index = _index;
		return;
	}
	
	// match
	phase = PHASE_CARD_CONFIRM;
	first_open_flag = false;
	if( list_card[_index].get_card_type() == list_card[first_card_index].get_card_type() ) {
		// 正解
		list_card[_index].done();
		list_card[first_card_index].done();
		append_score(1);
	}
	else {
		// 不正解
	}
	setTimeout( function(e) {
		// check finish
		list_card[_index].hide();
		list_card[first_card_index].hide();

		if( is_finish() ) {
			go_gameover();
		}
		else {
			phase = PHASE_GAME;
		}
		
	}, CONFIRM_TIME);
};


/***********************************************************************/
function go_gameover() {
	win_game.show_title();
	phase = PHASE_GAMEOVER;
}
/***********************************************************************/
function main_loop() {
	Ti.API.info('#cg93:main_loop();');
	win_game.set_score( score );
	
};

/***********************************************************************/
function is_finish() {
	
};

/***********************************************************************/
function dmy() {};

/**********************************************************************/
exports.init = init;
exports.go_title = go_title;
exports.start_game = start_game;
exports.check_open = check_open;

ctrl_gameはゲーム全体の制御をします。

class_card.js

/*************************************
 * cardクラス:神経衰弱でつかうカード
 */
function class_card() {
	this.view = null;
	this.card_type = null;
	this.index = null;
	
	this.img_ura  = null;
	this.img_card = null;
	this.done_flag = null;	// 取得済みのカードはtrueにする
	
	this.CARD_WIDTH = 75;
	this.CARD_HEIGHT = 75;
	this.pos_list = [{x:10, y:54},{x:85, y:54},{x:160, y:54},{x:235, y:54},{x:10, y:129},{x:85, y:129},{x:160, y:129},{x:235, y:129},{x:10, y:204},{x:85, y:204},{x:160, y:204},{x:235, y:204},{x:10, y:279},{x:85, y:279},{x:160, y:279},{x:235, y:279}];
	
};


/***************************************************************/
class_card.prototype.get_card_type = function( ) {
	return this.card_type;
};

/***************************************************************/
class_card.prototype.is_done = function( ) {
	return this.done_flag;
};

/***************************************************************/
class_card.prototype.done = function( ) {
	this.done_flag = true;;
};

/***************************************************************/
class_card.prototype.set_card_type = function( _index, _card_type ) {
	this.index = _index;
	this.card_type = _card_type;
	this.img_ura.show();
	this.img_card.image = '/images/card/card'+this.card_type+'.png';
	this.img_card.hide();
	this.done_flag = false;
};

/***************************************************************/
class_card.prototype.make_view = function( _index ) {
	
	this.view = Ti.UI.createView({
		top:this.pos_list[_index].y,
		left:this.pos_list[_index].x,
		width:this.CARD_WIDTH,
		height:this.CARD_HEIGHT,
		zIndex:0
	});
	
	this.img_ura = Ti.UI.createImageView({
		top:0,
		left:0,
		width:this.CARD_WIDTH,
		height:this.CARD_HEIGHT,
		image:'/images/cake/ura.png',
	});
	
	this.img_card = Ti.UI.createImageView({
		top:0,
		left:0,
		width:this.CARD_WIDTH,
		height:this.CARD_HEIGHT,
		visible:false,
	});
	
	this.view.add( this.img_ura );
	
	var ref = {};
	ref._this = this;
	this.view.addEventListener( 'click', function(e) {
		// check 
		app.ctrl_game.check_open( ref._this.index );
	});
};

/***************************************************************/
class_card.prototype.get_view = function() {
	return this.view;
};

/***************************************************************/
class_card.prototype.close = function() {
	this.view = null;
	this.img_ura = null;
	this.img_card = null;
};

/***************************************************************/
class_card.prototype.open = function() {
	this.img_ura.hide();
	this.img_card.show();
};

/***************************************************************/
class_card.prototype.close = function() {
	this.img_ura.show();
	this.img_card.hide();
};

/***************************************************************/
class_card.prototype.dmy = function() {  };

class_cardはカードクラスを定義、画面に置く1まい1まいのカードはclass_cardのインスタンスとして
ctrl_game.list_card[]配列に格納される。
カードの描画とイベントのコントトールをclass_cardの中で実行しているので、クラス内部はロジックとデザインの分離が不完全。
もう少し考えが必要かな。

win_game.js

/**********************************************************************/
var win = null;
var img_title_logo = null;
var b_start = null;
var b_restart = null;
var b_help = null;
var l_score = null;

/**********************************************************************
 * 
 */
function make_win() {
	Ti.API.info('#wm5:make_win();')
	win = Ti.UI.createWindow({
		top:0,
		left:0,
		width:320,
		height:460,
		backgroundColor:'#fffffa',
	});
	
	img_title_logo = Ti.UI.createImageView({
		top:125,
		left:50,
		width:225,
		height:50,
		image:'/images/title_logo.png',
		zIndex:100
	});
	win.add( img_title_logo );
	
	b_start = Ti.UI.createButton({
		top:375,
		left:50,
		width:225,
		height:50,
		title:'Start',
		zIndex:100
	});
	win.add( b_start );
	b_start.addEventListener('click', function(e) {
		app.ctrl_game.start_game();
	})
	
	l_score = Ti.UI.createLabel({
		top:10,
		left:10,
		width:150,
		height:31,
		textAlign:'right',
		color:'#000',
	})
	win.add( l_score );
	
	
	return win;
};

/**********************************************************************
 * 
 */
function get_win() {
	Ti.API.info('#wm13:get_win();')
	if( !win )
		make_win();
	return win;
};

/**********************************************************************
 * 
 */
function close_win() {
	Ti.API.info('#wm5:close_win();')
	
	if( win )
		win.close();
	
	win = null;
};

/***********************************************************************/
function hide_title() {
	b_start.hide();
	img_title_logo.hide();
};

/***********************************************************************/
function show_title() {
	b_start.show();
	img_title_logo.show();
};

/***********************************************************************/
function set_score( _score ) {
	if( l_score.text != _score )
		l_score.text = _score;
};

/***********************************************************************/
function dmy() {};

/**********************************************************************/
exports.make_win   = make_win;
exports.get_win    = get_win;
exports.close_win  = close_win;
exports.show_title = show_title;
exports.hide_title = hide_title;
exports.set_score  = set_score;

win_game.jsはCommonJSとして、ctrl_winからrequireされて、ctrl_game.win_game変数にインスタンスを保持する。
ゲームのタイトルや背景などのデザインをこのファイルに実装する。

つぎは、ちゃんと動くようにして、コード一行一行にコメントを付けて、まとめたいと思います。
(今の_NSCFDictionaryエラー発生しますが、最新のコードをGitHubにcommitしてpushしました。)

まだ途中(動かない)けど、githubにリポジトリを作った。

https://github.com/sugie/shinkeisuijaku.git

なんだか、ファイルのアップに失敗したみたい。なんでかな。

出社時間なので、でなければ!

と、思ったら。

cd ~/Documents/shinkeisuijaku/
git remote add origin https://github.com/sugie/shinkeisuijaku.git
git push -u origin master

でアップできた。

しまった、READMEがAppceleratorのままだった、差し替えなきゃ。

続きは、就業時間後ということで。

====Titanium MobileでiPhoneアプリ神経衰弱ゲームを作ってみる

ファイル構成

Resources/
├── app.js
├── api
│   └── api_game_logic.js
├── ctrl
│   └── ctrl_game.js
├── data
│   └── data_game.js
├── images
│   └── title_logo.png
└── view
    └── win_game.js


MVCっぽく書いてみる
M: apiフォルダ以下にロジックを記述したJavaScriptを配置。
C: ctrlフォルダ以下にコントローラーを配置。
V: viewフォルダ以下にViewファイルを配置。

命名ルールなど

Appceleratorはルールを定めない

Appceleratorのガイドラインをみると、プログラムの実装方法はいろいろあるので、どのように書くべきかルールを定めないとあった。

https://wiki.appcelerator.org/display/guides/Mobile+Best+Practices

 There are many different ways to build an application - using an MVC or MVVM architecture, maybe a framework of your own choosing or one you've built yourself. The guidelines in this document are considered the bedrock requirements for a stable and performant Titanium Mobile application. Your own application structures, business logic, and conventions may vary, and we do not endeavor to impose any such conventions in this document.

私はCamelCaseがキライ

キャメルケースってなんとなく馴染めない。
 キャメルケース:ThisIsAPen.
 スネークケース:this_is_a_pen.
ね?スネークケースの方が自然に見えるでしょ。
スネークケースで書くほうがスッキリしているので、スネークケースを使う。

ファイル名

プリフィックス

viewファイルはのプリフィックス
Windowを返すものはwin_
Viewを返すものは view_

コントローラー ctrl_
API関連 api_
データ関連 data_

フォルダ名と重複する場合が多いが、気にしない。

app.jsの記述

/***************************************
 * 神経衰弱
 */

Ti.include('data/data_game.js');

var app = {};
app.ctrl_game = require('/ctrl/ctrl_game');
app.ctrl_game.init();
app.ctrl_game.go_title();

基本的な動きはCommonJSで書いたコントローラーに書く。

コントローラーの記述

/**********************************************************************/
var win_game = null;
var PHASE_INIT = 0;
var PHASE_START = 1;
var PHASE_GAMEOVER = 2;
var MAX_NUM_CARD = 16;	// 配置するカード数
var MAX_TYPE_CARD = 8;	// カード種類数
var list_card = [];

var phase = PHASE_INIT;

/**********************************************************************
 * 
 */
function init() {
	Ti.API.info('#cg6:init();')
	// TODO: init 処理
};


/**********************************************************************
 * 
 */
function game_init() {
	Ti.API.info('#cg6:init();')
	
};


/**********************************************************************
 * 
 */
function go_title() {
	Ti.API.info('#cg11:go_title();')
	if( !win_game )
		win_game = require('/view/win_game')
	var win = win_game.get_win();
	win.open();
};

/**********************************************************************
 * 
 */
function start_game() {
	Ti.API.info('#cg27:start_game();');
	game_init();
};

/**********************************************************************/
exports.init = init;
exports.go_title = go_title;
exports.start_game = start_game;

GitHubでソースを公開

準備中!

乞うご期待!2012/08/27 8時23分

Appceleorガイドラインを調べてみた & 神経衰弱ゲームも作ってみた

https://wiki.appcelerator.org/display/guides/Mobile+Best+Practices

にAppceleratorのガイドラインがある。

言っていること。

  • グローバルを汚さない
  • シングルコンテキストで動かす
  • CommonJSを使ってオブジェクト指向っぽく作る
  • 必要になるまで他のJavaScriptファイルを開かない
  • メモリーリーク対策をする

の5点。

~~グローバルを汚さない

衝突が起こるので、無計画にグローバルを使わない。Ti.include は 基本使わない。Ti.includeは将来無くなる。
わるい例:グローバル変数が衝突する場合。
app.js

var win = Ti.UI.createWindow();

 Ti.include('sub.js');
 win.open();


sub.js

 var win = null;

グローバル変数winが衝突していて、sub.jsで上書きされてしまう。このため動かない。

~~シングルコンテキストで動かす

以下の様にurlでファイルを指定して、コンテキストを増やす書き方をしない。収集がつかなくなる。あとで説明する、CommonJSを使えってことかな。

わるい例:いろんなところでwin.jsを読み込んで、ごちゃごちゃになってる
app.js

 var win = Ti.UI.createWindow({url:'win.js'});
 win.open();


win.js

 var button = Ti.UI.createButton({
 top:10,
 left:10,
width:300,
height:40,
title:'open',
});
Titanium.UI.currentWindow.add( button );
button.addEventListener('click', function(e) {
var win = Ti.UI.createWindow({url:'win.js'});
});

~~CommonJSを使ってオブジェクト指向っぽく作る

使い方の例:
lib/maths.js

var privateData = 'private to the module\'s sandbox, not available globally';
 
//any property attached to the exports object becomes part of the module's public interface
exports.add = function() {
  var result = 0;
  for (var i = 0, l = arguments.length;i<l;i++) {
    result = result+arguments[i];
  }
  return result;
};

app.js

var maths = require('lib/maths');
var sum = maths.add(2,2,2,2,2);
//sum is 10

app.jsでCommonJSをrequireで参照して実行する。

~~必要になるまで他のJavaScriptファイルを開かない

Ti.includeと違って、CommonJSで作れば、必要になるときまでJavaScriptファイルが参照されない。

~~メモリーリーク対策をする

使い終わった変数にはnullを入れて、ガベージコレクション時に開放されるようにする。
実際にはどのようなタイミングでガベージコレクションが行われるか、よくわかりませんが。
とにかくnullを設定しておくべきのようです。

var win = Ti.UI.createWindow();
 
var myBigView = new BigView();
win.add(myBigView.view);
win.open();
 
//...at some point in the future...
win.remove(myBigView);
myBigView = null; //will cause view to be GC'ed

~~で、神経衰弱ゲーム iPhoneアプリを作ってみた

====記事は次のページ