Flex、AIR、Java、Androidなど

つみヨガ
無料
(2014.03.04時点)
posted with ポチレバ

Appbank Felloのプッシュ通知を組み込んだアプリをリリースしたのですが、Production版でプッシュが不通。直そうとアップデートしても不通。

ここまでの流れ

まず、組み込んだ最初のリリース版が不通。

単純に古い証明書が残ってしまってるものと思い、Provisioning Profileを作り直し(既存のものをGenerateするのではなく)、クリーン&Xcodeの再起動。

署名してサブミット。

審査通ったのでインストールしてみると、それでも不通。

現状

Production版の起動ログを見てみると、

1
<Warning>: No valid 'aps-environment' entitlement string found for application 'つみヨガ': (null). Notifications will not be delivered.

と出てます。

ググると、署名でしくってサブミットした.appにaps-environmentがついていない事が多いらしい。

(やったこと)

◆ デバッグ版ではプッシュ通知開通確認済み

インストールすると管理画面上でユーザー数がカウントアップされ、プッシュを送れる

◆ Provisioning Profile

Provisioning ProfileはApp ID上でDevelopment、Provisioning双方のプッシュ通知の設定をしてからGenerateした。

エディターで開くと

1
2
<key>aps-environment</key>
<string>production</string>

はちゃんとある。

◆ サブミット

・クリーン、Xcodeの再起動はやった。
・サブミットしたアプリの.appを

1
codesign -d --entitlements

したところ、

1
2
<key>aps-environment</key>
<string>production</string>

はある。

にも関わらずNo valid ‘aps-environment’ 〜が出て不通。

100個以上アプリ出してるがこんなこと無かったなぁ。なんでだろ。

多分この辺が答えなんだろうけど、これ以上頻繁にアップデートするの嫌なんでしばらく放置します。
xcode – Bundle Identifier and push certificate… aps-environment entitlement error – Stack Overflow

iPhone版つみヨガにAppbank Felloのプッシュ通知と全画面広告(インタースティシャル広告)入れてみたのでメモ。

つみヨガ
無料
(2014.03.04時点)
posted with ポチレバ

ドキュメントを読みながら実装。
間違ってたら教えて下さい。

Appbank Felloのサイトにてデベロッパー&アプリ登録

証明書作る

  • キーチェーンアクセス→証明書アシスタント→認証局に証明書を要求
  • メールアドレスと名前は本人のもの。CAのメールアドレスは空欄のまま
  • パスワードは任意のものを入力
  • CertificateSigningRequest.certSigningRequestができる

App ID

  • iOS Dev Center のCertificates, Identifiers & Profilesに移動
  • App IDのPush Notificationをオンに
  • Development SSL Certificate、Development SSL CertificateそれぞれCertificateSigningRequest.certSigningRequestを使って作成 ※certSigningRequestは使い回し
  • できたCertificateは落としておく。

Provisioning Profiles

  • 作ったApp IDでGenerate、落としてXcodeにドラッグして登録

pem作る

  • 落としといたCertificateをクリックしてキーチェーンアクセスに登録
  • Apple Development IOS Push Services、Apple Production IOS Push Servicesが登録される
  • それぞれを右クリックして書き出す ※ 左の三角クリックすると出てくる鍵ではない
  • パスワードは任意のものを入力
  • 名前はapns_dev.p12、apns_prdc.p12として保存

Terminal開く

1
2
3
4
5
6
7
8
9
openssl pkcs12 -clcerts -nokeys -out apns_dev_cert.pem -in apns_dev.p12
パスワード聞かれる:p12作った時のパスワード
openssl pkcs12 -nocerts -out apns_dev_key.pem -in apns_dev.p12
パスワード聞かれる:p12作った時のパスワード
パスワード聞かれる:pem用のパスワード
新しいパスワードの確認:もう一度pem用のパスワードを入力
openssl rsa -in apns_dev_key.pem -out apns_dev_nopass_key.pem
パスワード聞かれる:pem用のパスワード
cat apns_dev_cert.pem apns_dev_nopass_key.pem > apns_dev.pem

apns_dev.pem完成

同様に

1
2
3
4
openssl pkcs12 -clcerts -nokeys -out apns_prdc_cert.pem -in apns_prdc.p12
openssl pkcs12 -nocerts -out apns_prdc_key.pem -in apns_prdc.p12
openssl rsa -in apns_prdc_key.pem -out apns_prdc_nopass_key.pem
cat apns_prdc_cert.pem apns_prdc_nopass_key.pem > apns_prdc.pem

apns_prdc.pem完成

pemの登録

  • Felloの管理画面→登録したアプリ
  • pemをそれぞれ登録

Push実装

  • 落としたSDKのNotificationsSDK/Debugの中のFelloPush.bundle、FelloPush.frameworkをXcodeのプロジェクトへドラッグ&ドロップ
  • Other Linker Flagsに-framework FelloPushを追加
  • StoreKit.frameworkをAdSupport.framework追加。require→optionalに。
  • ドキュメント通りに実装。但しAppDelegateではなくAppControllerに。

Push開通確認

  • アプリを実機にインストール→Pushの確認は「はい」
  • Felloの管理画面→プッシュ通知→ユーザー管理上でデバッグユーザーが新たに追加されたか確認
  • 確認できたらPushを打ってみる。「デバッグ用アプリ」にチェック。
  • 届けばOK。タイムラグある時もあるので気長に待つ。

広告実装

  • Fello管理画面から広告を有効化
  • 起動時広告等はうざがられそうなのでオフ。インタースティシャル広告のみチェックを外す
  • ネイティブとの連携クラスを書き、C++から[KonectNotificationsAPI beginInterstitial];を呼び出す。
  • 呼んだタイミングで広告が出るのを確認
  • 管理画面から出現確率いじれるのはいいね。

サブミット

  • FelloPush.bundle、FelloPush.frameworkをXcode上から削除
  • 落としたSDKのNotificationsSDK/Releaseの中のFelloPush.bundle、FelloPush.frameworkをXcodeのプロジェクトへドラッグ&ドロップ
  • クリーン
  • 走らせてみて、Fello関連のログが出てなければデバッグからリリースに変更完了してるっぽい。
  • ※追記 Release版に変更しても、実機で走らせると起動時にアラートで「Fello SDKテストサーバー(サンドボックス)に接続中」と表示され、広告はテストAdだが、これはProvisioning Profileを見ているからだそう。
  • ※追記 Distribution Profileで署名したリリース版はアラートも表示されず、広告は本番用がきちんと表示されるそうです。
  • サブミット。

つみヨガ

Androidでそこそこ流行ったつみヨガをcocos2d-xで作りなおしてAppStoreに公開しました。

ユニバーサルで英語対応。Android版も追って。

当たり判定には時間をかけたので面白い動きをすると思います。

つみヨガ

足場を固めてから積もうが、

つみヨガ

ひたすら上に積もうが自由です。

うちのせがれが好きなようで、よくこうやって遊んでますので、お子様にもいいんじゃないでしょうか。笑

以下、cocos2d-x使ってみての感想。

・サンプルのビルドまですんなり行けたので昔に比べると楽。
・Xcode使えるのがうれしい。Eclipse嫌い。
・C++めんどい。文字列まわりとか。
・でもまあObjective-c慣れてたらそんなに大きくは変わらんかな。
・Box2dはAndEngine版と似たような感じ。作法分かってれば簡単。
・ネイティブとの連携は結構面倒。プラグインとか欲しい。
・Everyplayがいい感じ。リプレイ撮って共有したりできる。今回はGameCenter敢えて外した。
・但しAndroidにはUnityしか対応してないので注意。
・結論、時と場合によってAndEngine、cocos2d、cocos2d-xの使い分けだね。どれも一長一短。

赤い水牛

こんにちは。@stachibanaです。

本エントリーはAndroid Advent Calendar 2012の表エントリーです。本日の裏エントリーは@katsummyさんです。

NDKとかADKとかaaptとかガチなネタが続いている中、空気を読まずにソフトなネタを投入します。

AndEngineでつくるAndroid 2Dゲーム (SMART GAME DEVELOPER) AndEngineでつくるAndroid 2Dゲーム (SMART GAME DEVELOPER)
立花 翔

翔泳社
売り上げランキング : 12826

Amazonで詳しく見る

宣伝になりますが、12/13日に翔泳社さんより拙著「AndEngineでつくるAndroid 2Dゲーム」が発売されました。

iPhone業界ではカジュアルゲームで1つで700万稼いだとか、3週間で1000万儲かったとかという話が話題ですね。

さすがにこれは一握りですが、1つのゲームで100万位になった例は結構あります。Androidでもランキングを見ているとその傾向が見られますね。近いうちでしょう。

ということで、今回はゲーム作りを解説してみたいと思います。

これから作るゲームはPlayストアで公開されてますのでまずはそちらをどうぞ。

赤い水牛
赤い水牛
赤い水牛

仮に、「赤い水牛」という飲めばいくらでも働ける炭酸飲料があったとします。それを振って振って振りまくり、噴射させることによって疲れて机で寝てしまっている社員達にぶつけ、翼をさずけられたら素敵だと思いませんか?

Androidの開発環境が揃っている方なら1時間もあれば作れますので、是非体験してみて下さい。

尚、必要な画像素材はここからダウンロードして下さい。ダウンロードできたら、解凍してassetsフォルダにサブフォルダごと放り込んで下さい。

赤い水牛

では始めましょう。Androidの開発環境は入っていることを前提に進めます。

※ 本エントリはJavaのこと、Androidのことが普通に分かる方向けに大分はしょって書いています。本の方にはもっともっともーーーーっと優しく書いてありますのでご安心を。

AndEngineの準備

最初にAndEngineを落とします。

AndEngineのサイトを開き、プロジェクトをgitなりzipなりでローカルに保存します。

ライブラリの形式で配布されていますので、落とせたら、Eclipseのメニュー > インポートからインポートします。ワークスペースにコピーすることをおすすめします。

ゲームの雛形の作成

ゲーム画面を作る前に、ゲームの雛形を作りましょう。

でも解説するのがめんどくさいのでここからダウンロードして下さい。

簡単に言うと、リソースの読み込み関係のユーティリティクラスや、シーンの遷移関連の関数等をまとめた抽象クラスとかが出来上がった状態のプロジェクトです。

詳細の解説は本の方に書いてあるのでそちらをどうぞ。

AndEngineライブラリへのパスは同じディレクトリを指定していますので、ワークスペースにコピーしていない方は修正してください。

MainActivityの編集

まず、MainActivityを編集しましょう。以下の実装を加えます。

Activityでは何も描画をせず、起動するシーンを指示し、描画や処理はシーンが行います。

  • 最初に起動するシーンをゲームのシーンからトップ画面のシーンに変更
  • 毎フレーム加速度センサーから値を取得し、ゲーム進行中ならゲームのシーンに値を渡す実装
  • シーン遷移用の実装
  • ハードウェアキー押下時の実装

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package biz.stachibana.ae.redbaffalo;

import org.andengine.engine.camera.Camera;
import org.andengine.engine.handler.IUpdateHandler;
import org.andengine.engine.options.EngineOptions;
import org.andengine.engine.options.ScreenOrientation;
import org.andengine.engine.options.resolutionpolicy.RatioResolutionPolicy;
import org.andengine.entity.scene.Scene;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.view.KeyEvent;

public class MainActivity extends MultiSceneActivity implements
        SensorEventListener {

    // 画面のサイズ。
    private int CAMERA_WIDTH = 480;
    private int CAMERA_HEIGHT = 800;

    // 傾きの量
    private float velX;
    private float velY;
    private float velZ;

    // センサーマネージャ
    SensorManager mSensorManager;

    // Activity起動時最初に呼び出される
    public EngineOptions onCreateEngineOptions() {
        // サイズを指定し描画範囲をインスタンス化
        final Camera camera = new Camera(0, 0, CAMERA_WIDTH, CAMERA_HEIGHT);
        // ゲームのエンジンを初期化。
        // 第1引数 タイトルバーを表示しないモード
        // 第2引数 画面は縦向き(幅480、高さ800)
        // 第3引数 解像度の縦横比を保ったまま最大まで拡大する
        // 第4引数 描画範囲
        EngineOptions eo = new EngineOptions(true,
                ScreenOrientation.PORTRAIT_FIXED, new RatioResolutionPolicy(
                        CAMERA_WIDTH, CAMERA_HEIGHT), camera);
        return eo;
    }

    // 起動するシーンを返す
    @Override
    protected Scene onCreateScene() {

        // センサー系の処理を管理するクラス
        mSensorManager = (SensorManager) this
                .getSystemService(Context.SENSOR_SERVICE);
        // センサーイベントのリスナーを設定
        mSensorManager.registerListener(this,
                mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                SensorManager.SENSOR_DELAY_GAME);

        // ゲーム画面(MainScene)が起動中の時のみ、加速度センサーから取得した値を渡す
        this.mEngine.registerUpdateHandler(new IUpdateHandler() {
            public void onUpdate(float pSecondsElapsed) {
                if (getEngine().getScene() instanceof MainScene) {
                    ((MainScene) getEngine().getScene()).updateByActivity(velX,
                            velY, velZ);
                }
            }

            public void reset() {

            }
        });

        // InitialSceneをインスタンス化し、返す
        // 同時に、遷移用の配列にも追加
        InitialScene initialScene = new InitialScene(this);
        getSceneArray().add(initialScene);
        return initialScene;
    }

    // Activityが参照するレイアウトID
    @Override
    protected int getLayoutID() {
        // ActivityのレイアウトのIDを返す
        return R.layout.activity_main;
    }

    // レイアウト内で、シーンがセットされるViewのID
    @Override
    protected int getRenderSurfaceViewID() {
        // SceneがセットされるViewのIDを返す
        return R.id.renderview;
    }

    // 遷移用の配列に新たなシーンを追加
    @Override
    public void appendScene(KeyListenScene scene) {
        getSceneArray().add(scene);
    }

    // 遷移用の配列から全てのシーンを削除し、トップ画面を表示する
    @Override
    public void backToInitial() {
        getSceneArray().clear();
        KeyListenScene scene = new InitialScene(this);
        getSceneArray().add(scene);
        getEngine().setScene(scene);
    }

    // 現在のシーンを切り替える。遷移用の配列の状態は変更しない
    @Override
    public void refreshRunningScene(KeyListenScene scene) {
        getSceneArray().remove(getSceneArray().size() - 1);
        getSceneArray().add(scene);
        getEngine().setScene(scene);
    }

    // センサーイベント
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }

    // センサーイベント
    @Override
    public void onSensorChanged(SensorEvent event) {
        synchronized (this) {
            switch (event.sensor.getType()) {
            // 加速度センサーの場合
            case Sensor.TYPE_ACCELEROMETER:
                // 値を格納
                velX = event.values[1];
                velY = event.values[0];
                velZ = event.values[2];
                break;
            }
        }
    }

    // ハードウェア押下時の処理
    @Override
    public boolean dispatchKeyEvent(KeyEvent e) {
        // バックボタンが押された時
        if (e.getAction() == KeyEvent.ACTION_DOWN
                && e.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            // 起動中のSceneのdispatchKeyEvent関数を呼び出し。追加の処理が必要な時はfalseが
            // 返ってくる為、処理
            if (!getSceneArray().get(getSceneArray().size() - 1)
                    .dispatchKeyEvent(e)) {
                // Sceneが1つしか起動していない時はゲームを終了
                if (getSceneArray().size() == 1) {
                    // キャッシュしたテクスチャを廃棄
                    ResourceUtil.getInstance(this).resetAllTexture();
                    finish();
                }
                // 複数のSceneが起動している時は1つ前のシーンへ戻る
                else {
                    getEngine().setScene(
                            getSceneArray().get(getSceneArray().size() - 2));
                    getSceneArray().remove(getSceneArray().size() - 1);
                }
            }
            // メニューボタンが押された時
        } else if (e.getAction() == KeyEvent.ACTION_DOWN
                && e.getKeyCode() == KeyEvent.KEYCODE_MENU) {
            // 起動中のシーンにキーイベントを送信
            getSceneArray().get(getSceneArray().size() - 1).dispatchKeyEvent(e);
            return true;
        }
        return true;
    }
}
MainSceneの編集

次に、MainSceneを編集しましょう。以下の実装を加えます。

MainSceneは実際にゲームを描画するシーンです。

  • イニシャライズの処理
  • 振り開始までのカウントダウン処理
  • 振りによってパワーを蓄積させる処理
  • フレーム毎に缶や社員を移動させたり、画像を切り替えたりする処理
  • ゲームオーバーの処理
  • ゲームオーバー画面からのボタン押下時の処理

MainScene.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
package biz.stachibana.ae.redbaffalo;

import org.andengine.engine.handler.timer.ITimerCallback;
import org.andengine.engine.handler.timer.TimerHandler;
import org.andengine.entity.modifier.FadeInModifier;
import org.andengine.entity.modifier.MoveModifier;
import org.andengine.entity.modifier.ParallelEntityModifier;
import org.andengine.entity.modifier.RotationByModifier;
import org.andengine.entity.modifier.ScaleModifier;
import org.andengine.entity.sprite.AnimatedSprite;
import org.andengine.entity.sprite.ButtonSprite;
import org.andengine.entity.sprite.Sprite;
import org.andengine.entity.text.Text;
import org.andengine.entity.text.TextOptions;
import org.andengine.opengl.font.BitmapFont;
import org.andengine.opengl.font.Font;
import org.andengine.opengl.texture.Texture;
import org.andengine.opengl.texture.TextureOptions;
import org.andengine.opengl.texture.atlas.bitmap.BitmapTextureAtlas;
import org.andengine.opengl.vbo.DrawType;
import org.andengine.util.HorizontalAlign;
import org.andengine.util.color.Color;
import org.andengine.util.modifier.ease.EaseBackOut;
import org.andengine.util.modifier.ease.EaseBounceInOut;

import android.content.Intent;
import android.graphics.Typeface;
import android.view.KeyEvent;

public class MainScene extends KeyListenScene implements
        ButtonSprite.OnClickListener {

    // 社員の状態管理用タグ
    private static final int TAG_WORKER = 10;

    // ボタン用タグ
    private static final int BTN_RETRY = 21;
    private static final int BTN_MENU = 22;
    private static final int BTN_RANKING = 23;
    private static final int BTN_TWEET = 24;

    // 5秒間で蓄積したパワー(加速度センサーから取得)
    private int power;

    // パワーの追加を受け付けるか否か
    private boolean isAcceptShake;
    // カウントダウン用整数
    private int countDownNum = 4;

    // 缶画像
    private AnimatedSprite canSprite;
    // 指示のテキスト
    private Text instructionText;
    // 缶が社員に当たった時に点滅する画像
    private Sprite hitSprite;

    // 点数
    private int score;
    // 結果画面に表示されるテキスト
    private Text resultText;

    // コンストラクタ
    public MainScene(MultiSceneActivity baseActivity) {
        super(baseActivity);
        init();
    }

    // イニシャライザ
    public void init() {
        // 閉経画像をインスタンス化しシーンに貼り付け
        attachChild(getBaseActivity().getResourceUtil()
                .getSprite("main_bg.png"));

        // フォント用のTextureを用意
        Texture textureCountDown = new BitmapTextureAtlas(getBaseActivity()
                .getTextureManager(), 512, 512,
                TextureOptions.BILINEAR_PREMULTIPLYALPHA);
        // フォントをイニシャライズ
        Font fontCountDown = new Font(getBaseActivity().getFontManager(),
                textureCountDown, Typeface.DEFAULT_BOLD, 100, true, Color.BLACK);
        // EngineのTextureManagerにフォントTextureを読み込み
        getBaseActivity().getTextureManager().loadTexture(textureCountDown);
        // FontManagerにフォントを読み込み
        getBaseActivity().getFontManager().loadFont(fontCountDown);
        // 読み込んだフォントを利用して得点を表示

        // 指示用テキスト用のビットマップフォントを生成
        BitmapFont bitmapFont = new BitmapFont(getBaseActivity()
                .getTextureManager(), getBaseActivity().getAssets(),
                "font/red.fnt");
        // 読み込み
        bitmapFont.load();

        // 指示用テキスト
        instructionText = new Text(20, 40, bitmapFont, "", 20, new TextOptions(
                HorizontalAlign.LEFT), getBaseActivity()
                .getVertexBufferObjectManager(), DrawType.DYNAMIC);
        // 貼り付け
        attachChild(instructionText);

        // 缶画像をイニシャライズ。1枚の画像を縦1マス、横3マスに区切ってイニシャライズし、1マス目を使ってインスタンス化
        canSprite = getBaseActivity().getResourceUtil().getAnimatedSprite(
                "main_can.png", 1, 3);
        // x座標は画面中心、y座標は210に設定
        placeToCenterX(canSprite, 210);
        // 重なりを設定。引数が大きい方が上に表示される
        canSprite.setZIndex(2);
        // 貼り付け
        attachChild(canSprite);
        // z-indexを反映
        sortChildren();

        // 缶が社員に当たった時に点滅する画像
        hitSprite = getBaseActivity().getResourceUtil().getSprite(
                "main_hit.png");
        hitSprite.setAlpha(0);
        placeToCenterX(hitSprite, 320);
        attachChild(hitSprite);
        hitSprite.setZIndex(1);
        sortChildren();

        // 振り開始までのカウントダウン開始
        registerUpdateHandler(new TimerHandler(1, new ITimerCallback() {
            @Override
            public void onTimePassed(TimerHandler pTimerHandler) {
                countDown();
            }
        }));
    }

    // 振り開始までのカウントダウン
    public void countDown() {
        // 振り開始前
        if (!isAcceptShake) {
            countDownNum--;
            if (countDownNum == 0) {
                countDownNum = 5;
                instructionText.setText("5秒間振れ!");
                instructionText.setPosition(getBaseActivity().getEngine()
                        .getCamera().getWidth()
                        / 2 - instructionText.getWidth() / 2,
                        instructionText.getY());
                isAcceptShake = true;
            } else {
                instructionText.setText("" + countDownNum);
                instructionText.setPosition(getBaseActivity().getEngine()
                        .getCamera().getWidth()
                        / 2 - instructionText.getWidth() / 2,
                        instructionText.getY());
            }

            // 1秒後に再び関数countDown()を呼び出す
            registerUpdateHandler(new TimerHandler(1.0f, new ITimerCallback() {
                public void onTimePassed(TimerHandler pTimerHandler) {
                    countDown();
                }
            }));
            // 振り受付中
        } else {
            countDownNum--;
            if (countDownNum > 0) {
                // 1秒後に再び関数countDown()を呼び出す
                TimerHandler delayHandler = new TimerHandler(1.0f,
                        new ITimerCallback() {
                            public void onTimePassed(TimerHandler pTimerHandler) {
                                countDown();
                            }
                        });
                registerUpdateHandler(delayHandler);
            } else {
                // 振りの受付を終了
                isAcceptShake = false;
                instructionText.setText("終了!");
                instructionText.setPosition(getBaseActivity().getEngine()
                        .getCamera().getWidth()
                        / 2 - instructionText.getWidth() / 2,
                        instructionText.getY());
                // 缶画像をアニメーションさせながら画面下端に移動
                canSprite.registerEntityModifier(new ParallelEntityModifier(
                        new RotationByModifier(1.0f, 2340), new MoveModifier(
                                1.0f, canSprite.getX(), canSprite.getX(),
                                canSprite.getY(), canSprite.getY() + 230,
                                EaseBackOut.getInstance()), new ScaleModifier(
                                1.0f, 1, 0.1f)));

                // 指示用テキストを消去。社員を準備する処理を行う関数startWakeUpを呼び出す
                registerUpdateHandler(new TimerHandler(2.0f,
                        new ITimerCallback() {
                            public void onTimePassed(TimerHandler pTimerHandler) {
                                instructionText.detachSelf();
                                startWakeUp();
                            }
                        }));
            }
        }
    }

    // Activityから加速度センサーの値が渡ってくる
    public void updateByActivity(float velX, float velY, float velZ) {

        // 振り受け中でなければ(カウントダウン時、ゲームオーバー後等) 何もしない
        if (!isAcceptShake) {
            return;
        }
        // パワーを蓄積
        power += Math.round((Math.abs(velX) + Math.abs(velY) + Math.abs(velZ)));

        // 蓄積したパワーによって缶の画像をふくらんだ物に変更
        if (power > 5000 && canSprite.getCurrentTileIndex() == 0) {
            canSprite.setCurrentTileIndex(1);
        } else if (power > 10000 && canSprite.getCurrentTileIndex() == 1) {
            canSprite.setCurrentTileIndex(2);
        }
    }

    // 的となる社員を準備
    public void startWakeUp() {
        // 社員をシーンに貼り付け。
        for (int i = 0; i < 7; i++) {
            Sprite utsu = getBaseActivity().getResourceUtil().getSprite(
                    "utsu.png");
            placeToCenterX(utsu, 250 - utsu.getHeight() * i);
            utsu.setTag(TAG_WORKER);
            attachChild(utsu);
            sortChildren();
        }
        // 缶を画面中心まで移動
        canSprite.registerEntityModifier(new MoveModifier(0.1f, canSprite
                .getX(), canSprite.getX(), canSprite.getY(),
                canSprite.getY() - 300));
        // 毎フレーム毎にupdateHandlerを呼び出し開始
        registerUpdateHandler(new TimerHandler(0.1f, new ITimerCallback() {
            @Override
            public void onTimePassed(TimerHandler pTimerHandler) {
                registerUpdateHandler(updateHandler);
            }
        }));
    }

    // 缶が飛んでいる缶の描画を行うハンドラ。毎フレーム呼び出される
    public TimerHandler updateHandler = new TimerHandler(1f / 60f, true,
            new ITimerCallback() {
                public void onTimePassed(TimerHandler pTimerHandler) {
                    // 残りパワーによって社員のスクロールスピードを調整。段々遅く。
                    float speed;
                    if (power > 5000) {
                        speed = 40;
                        power -= 20;
                    } else if (power > 4000) {
                        speed = 30;
                        power -= 15;
                    } else if (power > 3000) {
                        speed = 20;
                        power -= 10;
                    } else if (power > 2000) {
                        speed = 10;
                        power -= 5;
                    } else if (power > 1000) {
                        speed = 5;
                        power -= 3;
                    } else {
                        speed = 0;
                        // アップデートハンドラを停止
                        MainScene.this.unregisterUpdateHandler(updateHandler);
                        // 2秒後にゲームオーバー処理
                        registerUpdateHandler(new TimerHandler(2.0f,
                                new ITimerCallback() {
                                    @Override
                                    public void onTimePassed(
                                            TimerHandler pTimerHandler) {
                                        showGameOver();
                                        return;
                                    }
                                }));
                    }
                    // シーン上の画像を1つづつ取得
                    for (int i = 0; i < getChildCount(); i++) {
                        if (getChildByIndex(i) instanceof Sprite) {
                            Sprite worker = (Sprite) getChildByIndex(i);

                            // 社員の画像なら移動処理
                            if (worker.getTag() == TAG_WORKER) {
                                // 移動
                                worker.setPosition(worker.getX(), worker.getY()
                                        + speed);
                                // 画面外に出たら画面上に移動
                                if (worker.getY() > 800 + 154) {
                                    worker.setPosition(worker.getX(),
                                            worker.getY() - 154 * 7);
                                    // 不透明に
                                    worker.setAlpha(1);
                                    // 子要素を削除
                                    worker.detachChildren();
                                    // 画面中央まで来たら
                                } else if (worker.getY() > 200 + 154) {
                                    if (worker.getAlpha() != 0) {
                                        // 透明に
                                        worker.setAlpha(0);
                                        // 翼を授ける!(画像に画像を追加)
                                        Sprite fine = getBaseActivity()
                                                .getResourceUtil().getSprite(
                                                        "fine.png");
                                        worker.attachChild(fine);
                                        // ヒット画像を点滅
                                        hitSprite.setAlpha(1);
                                        registerUpdateHandler(new TimerHandler(
                                                0.05f, new ITimerCallback() {
                                                    @Override
                                                    public void onTimePassed(
                                                            TimerHandler pTimerHandler) {
                                                        hitSprite.setAlpha(0);
                                                    }
                                                }));
                                        //スコアをインクリメント
                                        score++;
                                    }
                                }
                            }
                        }
                    }
                }
            });

    @Override
    public void prepareSoundAndMusic() {

    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent e) {
        return false;
    }

    // ゲームオーバー処理
    public void showGameOver() {

        Sprite resultBg = getBaseActivity().getResourceUtil().getSprite(
                "result_bg.png");
        placeToCenter(resultBg);
        attachChild(resultBg);

        BitmapFont bitmapFont = new BitmapFont(getBaseActivity()
                .getTextureManager(), getBaseActivity().getAssets(),
                "font/result.fnt");
        bitmapFont.load();

        resultText = new Text(46, 73, bitmapFont, "0", 20, new TextOptions(
                HorizontalAlign.LEFT), getBaseActivity()
                .getVertexBufferObjectManager());
        resultBg.attachChild(resultText);

        // スコアアップ
        registerUpdateHandler(resultIncrementHandler);

        // ボタンを追加
        ButtonSprite btnRetry = getBaseActivity().getResourceUtil()
                .getButtonSprite("result_btn_01.png", "result_btn_01_p.png");
        btnRetry.setPosition(33, 220);
        btnRetry.setTag(BTN_RETRY);
        btnRetry.setOnClickListener(this);
        resultBg.attachChild(btnRetry);
        registerTouchArea(btnRetry);

        ButtonSprite btnMenu = getBaseActivity().getResourceUtil()
                .getButtonSprite("result_btn_02.png", "result_btn_02_p.png");
        btnMenu.setPosition(33, 312);
        btnMenu.setTag(BTN_MENU);
        btnMenu.setOnClickListener(this);
        resultBg.attachChild(btnMenu);
        registerTouchArea(btnMenu);

        ButtonSprite btnRanking = getBaseActivity().getResourceUtil()
                .getButtonSprite("result_btn_03.png", "result_btn_03_p.png");
        btnRanking.setPosition(33, 404);
        btnRanking.setTag(BTN_RANKING);
        btnRanking.setOnClickListener(this);
        resultBg.attachChild(btnRanking);
        registerTouchArea(btnRanking);

        ButtonSprite btnShare = getBaseActivity().getResourceUtil()
                .getButtonSprite("result_btn_04.png", "result_btn_04_p.png");
        btnShare.setPosition(33, 496);
        btnShare.setTag(BTN_TWEET);
        btnShare.setOnClickListener(this);
        resultBg.attachChild(btnShare);
        registerTouchArea(btnShare);

        // 上から滑り落ちてくるエフェクト
        resultBg.registerEntityModifier(new ParallelEntityModifier(
                new FadeInModifier(1.0f), new MoveModifier(1.0f, resultBg
                        .getX(), resultBg.getX(), resultBg.getY() - 500,
                        resultBg.getY(), EaseBounceInOut.getInstance())));

    }

    // スコアを1づつ増やしながら表示
    public TimerHandler resultIncrementHandler = new TimerHandler(1f / 60f,
            true, new ITimerCallback() {
                @Override
                public void onTimePassed(TimerHandler pTimerHandler) {
                    if (Integer.parseInt(resultText.getText().toString()) < score) {
                        resultText.setText(""
                                + (Integer.parseInt(resultText.getText()
                                        .toString()) + 1));
                    } else {
                        MainScene.this
                                .unregisterUpdateHandler(resultIncrementHandler);
                    }
                }
            });

    // ボタン押下時の処理
    @Override
    public void onClick(ButtonSprite pButtonSprite, float pTouchAreaLocalX,
            float pTouchAreaLocalY) {
        switch (pButtonSprite.getTag()) {
        case BTN_RETRY:
            MainScene newScene = new MainScene(getBaseActivity());
            getBaseActivity().refreshRunningScene(newScene);
            break;
        case BTN_MENU:
            getBaseActivity().backToInitial();
            break;
        case BTN_RANKING:
            break;
        case BTN_TWEET:
            Intent sendIntent = new Intent(Intent.ACTION_SEND);
            sendIntent.setType("text/plain");
            sendIntent.putExtra(Intent.EXTRA_TEXT, "Androidゲーム「赤い水牛」で" + score
                    + "人日ゲット!ダウンロード&作り方 → http://stachibana.biz #androidadvent2012");
            getBaseActivity().startActivity(sendIntent);
            break;
        }
    }
}
InitialSceneの編集

次に、InitialSceneを編集しましょう。以下の実装を加えます。

InitialSceneはゲームのトップ画面(メニュー画面)を表示するシーンです。

  • タイトル画像の追加
  • タイトルロゴの追加・アニメーション
  • ボタンの追加・アニメーション
  • ランキングの表示処理

InitialScene.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package biz.stachibana.ae.redbaffalo;

import org.andengine.entity.modifier.DelayModifier;
import org.andengine.entity.modifier.FadeInModifier;
import org.andengine.entity.modifier.MoveModifier;
import org.andengine.entity.modifier.ParallelEntityModifier;
import org.andengine.entity.modifier.SequenceEntityModifier;
import org.andengine.entity.sprite.ButtonSprite;
import org.andengine.entity.sprite.Sprite;
import org.andengine.util.modifier.ease.EaseBackInOut;
import org.andengine.util.modifier.ease.EaseBounceInOut;

import android.content.Intent;
import android.net.Uri;
import android.view.KeyEvent;

public class InitialScene extends KeyListenScene implements
        ButtonSprite.OnClickListener {

    // ボタン用タグ
    private static final int INITIAL_START = 1;
    private static final int INITIAL_RANKING = 2;
    private static final int INITIAL_INFO = 3;

    // コンストラクタ
    public InitialScene(MultiSceneActivity context) {
        super(context);
        init();
    }

    // イニシャライザ
    @Override
    public void init() {
        // 背景画像を追加
        attachChild(getBaseActivity().getResourceUtil().getSprite(
                "initial_bg.png"));

        // 鬱社員を追加
        Sprite title01 = getBaseActivity().getResourceUtil().getSprite(
                "initial_title_01.png");
        placeToCenterX(title01, 100);
        attachChild(title01);

        // ロゴを追加
        Sprite title02 = getBaseActivity().getResourceUtil().getSprite(
                "initial_title_02.png");
        placeToCenterX(title02, 290);
        attachChild(title02);
        title02.setAlpha(0);

        // ぼわ〜んっとアニメーション
        title02.registerEntityModifier(new ParallelEntityModifier(
                new FadeInModifier(1.0f), new MoveModifier(1.0f,
                        title02.getX(), title02.getX(), title02.getY() + 100,
                        title02.getY(), EaseBounceInOut.getInstance())));

        // ボタンを追加
        ButtonSprite btnStart = getBaseActivity().getResourceUtil()
                .getButtonSprite("initial_btn_01.png", "initial_btn_01_p.png");
        placeToCenterX(btnStart, 570);
        btnStart.setTag(INITIAL_START);
        btnStart.setOnClickListener(this);
        attachChild(btnStart);
        registerTouchArea(btnStart);

        // ボタンは初期段階では画面右外に配置。1つづつ時間差で滑り込んでくるアニメーション
        float btnX = btnStart.getX();
        btnStart.setPosition(btnStart.getX()
                + getBaseActivity().getEngine().getCamera().getWidth(),
                btnStart.getY());
        btnStart.registerEntityModifier(new SequenceEntityModifier(
                new DelayModifier(1.4f), new MoveModifier(1.0f,
                        btnStart.getX(), btnX, btnStart.getY(),
                        btnStart.getY(), EaseBackInOut.getInstance())));

        ButtonSprite btnRanking = getBaseActivity().getResourceUtil()
                .getButtonSprite("initial_btn_02.png", "initial_btn_02_p.png");
        placeToCenterX(btnRanking, 660);
        btnRanking.setTag(INITIAL_RANKING);
        btnRanking.setOnClickListener(this);
        attachChild(btnRanking);
        registerTouchArea(btnRanking);

        btnX = btnRanking.getX();
        btnRanking.setPosition(btnRanking.getX()
                + getBaseActivity().getEngine().getCamera().getWidth(),
                btnRanking.getY());
        btnRanking.registerEntityModifier(new SequenceEntityModifier(
                new DelayModifier(1.6f), new MoveModifier(1.0f, btnRanking
                        .getX(), btnX, btnRanking.getY(), btnRanking.getY(),
                        EaseBackInOut.getInstance())));

        ButtonSprite btnRecommend = getBaseActivity().getResourceUtil()
                .getButtonSprite("initial_btn_03.png", "initial_btn_03_p.png");
        btnRecommend.setPosition(200, 20);
        btnRecommend.setTag(INITIAL_INFO);
        btnRecommend.setOnClickListener(this);
        attachChild(btnRecommend);
        registerTouchArea(btnRecommend);

        btnX = btnRecommend.getX();
        btnRecommend.setPosition(btnRecommend.getX()
                + getBaseActivity().getEngine().getCamera().getWidth(),
                btnRecommend.getY());
        btnRecommend.registerEntityModifier(new SequenceEntityModifier(
                new DelayModifier(1.8f), new MoveModifier(1.0f, btnRecommend
                        .getX(), btnX, btnRecommend.getY(),
                        btnRecommend.getY(), EaseBackInOut.getInstance())));
    }

    @Override
    public void prepareSoundAndMusic() {

    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent e) {
        return false;
    }

    // ボタン押下時の処理
    public void onClick(ButtonSprite pButtonSprite, float pTouchAreaLocalX,
            float pTouchAreaLocalY) {
        switch (pButtonSprite.getTag()) {
        case INITIAL_START:
            // リソースの解放
            ResourceUtil.getInstance(getBaseActivity()).resetAllTexture();
            KeyListenScene scene = new MainScene(getBaseActivity());
            // MainSceneへ移動
            getBaseActivity().getEngine().setScene(scene);
            // 遷移管理用配列に追加
            getBaseActivity().appendScene(scene);
            break;
        case INITIAL_RANKING:
            break;
        case INITIAL_INFO:
            Intent it = new Intent(Intent.ACTION_VIEW,
                    Uri.parse("http://stachibana.biz"));
            getBaseActivity().startActivity(it);
            break;
        }
    }
}
オンラインランキングの追加

オンラインランキングはOpenFeintが定番だったのですが、サービス終了してしまいました。

Pankiaとかscoreloopとかありますが実装がめんどくさいものばかりですので、シンプルに実装できるRank Parkを使いましょう。

まだ出たばかりで最小限の機能しかないですが、5分で実装できて見た目もなかなかなのでオススメです。

Rank Parkのサイトにアクセス、アプリの登録をし、SDKをダウンロードします。

SDKはjarなのでlibsに放り込んで、プロジェクトに追加してください。

追加できたら、AndroidManifest.xmlを編集します。

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//
// 略
//
    <!-- 追加 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="13" />
//
// 略
//
        </activity>
        <!-- 追加 -->
        <activity
            android:name="com.rankpark.Rankpark_Ranking"
            android:screenOrientation="portrait" >
        </activity>
    </application>

続いて、InitialSceneです。importは省略。

InitialScene.java

1
2
3
4
5
6
7
8
        case INITIAL_RANKING:
        // 追加
        // ランキングを表示
        RankPark.rp_view(getBaseActivity(), getBaseActivity()
                .getPackageName(), Settings.Secure.getString(
                getBaseActivity().getContentResolver(),
                Settings.Secure.ANDROID_ID));  
        break;

最後に、MainSceneです。スコアの登録と表示を行います。同じくimportは省略。

MainScene.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    // ゲームオーバー処理
    public void showGameOver() {
//
// 略
//
        // 上から滑り落ちてくるエフェクト
        resultBg.registerEntityModifier(new ParallelEntityModifier(
                new FadeInModifier(1.0f), new MoveModifier(1.0f, resultBg
                        .getX(), resultBg.getX(), resultBg.getY() - 500,
                        resultBg.getY(), EaseBounceInOut.getInstance())));
        // 追加
        // スコアをポスト
        getBaseActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                RankPark.rp_register(getBaseActivity(), getBaseActivity().getPackageName(), Settings.Secure.getString(
                        getBaseActivity().getContentResolver(), Settings.Secure.ANDROID_ID), "" + score);
            }
        });
    }
//
// 略
//
        case BTN_RANKING:
            // 追加
            // ランキングを表示
            RankPark.rp_view(getBaseActivity(), getBaseActivity()
                    .getPackageName(), Settings.Secure.getString(
                    getBaseActivity().getContentResolver(),
                    Settings.Secure.ANDROID_ID));              
            break;

これでランキングの実装も完了です。ゲームオーバーの度にスコアがサーバーにポストされ、ランキングに参加できるようになります。

赤い水牛

サウンドの追加

AndEngineではサウンドも簡単に再生出来ます。

でも時間が無く出来ませんでした。1000ダウンロード位行ったら音も入れます。スミマセン。

本にはきちんと解説してありますのでご安心を!

まとめ

ということで、赤い水牛ゲームの完成です。

完成したプロジェクトはここからダウンロードして下さいね。権利は完全に放棄しますので、どのファイルでもご自由にお使い下さい。

尚、Rank Parkのjarはライセンス読んでないので同梱してません。Rank Parkのサイトから落としてください。

もう個人の規模でAndroidのアプリを作ってもダウンロードされません!ゲーム作りましょう!

この記事を読んで、少しでも興味がわいた方は是非拙著を手にとってみて下さいね。赤い水牛の規模ですと、デザインにかかった時間を除くと、半日で作れるようになります。

AndEngineでつくるAndroid 2Dゲーム (SMART GAME DEVELOPER) AndEngineでつくるAndroid 2Dゲーム (SMART GAME DEVELOPER)
立花 翔

翔泳社
売り上げランキング : 12826

Amazonで詳しく見る

本を読んでいる中で、分からないことがありましたら質問して下さい!

明日の表は @out_of_kaya さん、裏は @tarotaro4 さんです。

最後に、機会を作って下さったようてんさん、ありがとうございました!

表題の通り、12/13日に著書「AndEngineでつくる Android2Dゲーム」が翔泳社様より発売されます。

Android用の2Dゲームフレームワークを利用して、カジュアルゲームをサクサク作れるようになろうという内容です。

iOSの方はcocos2dという絶対的な2Dゲームのフレームワークがありましたが、Androidにはまだ決定的なものも無く、今年の夏くらいまでは端末のスペック的にも「簡単に」ゲームをつくることはあまり容易ではありませんでした。

私自身が最初にAndEngineに触ったのは2011年の1月と古いのですが、まだ初代XperiaやDesireなどが主流の時代でしたのでシンプルなゲームでもカクカクでゲーム作りは保留していました。

それから2年近く経ち、端末のスペックも上がったことで、スペックを意識しなくてもカジュアルゲームならサクサク動くものが簡単に作れるようになったこともあり、執筆を始めました。

ざっくり言うと、以下のような内容になっています。(Amazonから抜粋)

600万ダウンロードを達成した著者が贈るゲームづくりの基礎とウケる秘訣! スマートフォンの普及により、すきま時間にシンプルなゲームを楽しむ人が増えています。

ゲームはアプリの中で最も人気のあるカテゴリであり、ヒットすれば無料アプリでも広告による収入が見込めます。ゲームづくりはハードルが高いと思われがちですが、シンプルなゲームであれば、経験が浅くても、個人であっても簡単につくることができます。真にアイデアで勝負できる魅力的なジャンルといえるでしょう。

本書では、AndEngineというフリーのゲームエンジンを使った開発を解説します。これにより開発のハードルが大幅に下がり、初心者でもアイデアを形にすることが容易になります。

また、ゲーム開発未経験から始めて、一人で企画・開発・デザインを行いながら数々のヒットアプリを生み続ける著者による、実体験に基づいたヒットのコツを随所に散りばめました。ゲームアプリを作ってみたい人、ゲームづくりで挫折した人、自分のアプリで稼ぎたい人にお勧めの1冊です。

目次は以下のようになっています。

1章 開発環境を構築する
2章 ゲームの雛形を作る
3章 投げ系ゲームを作る
4章 横スクロールゲームを作る
5章 加速度センサーを利用したゲームを作る
6章 外部サービスと連携する
付録 逆引きAndEngine
ヒットの秘訣(1)〜(10)

本書では3つのカジュアルゲームを作りながらゲームの作り方を学んでいきますが、それらの中でカジュアルゲームに必要なテクニックや実装をひと通り詰め込みました。

1ゲーム当たり80ページ程ですので、通して組んでみて下さい。その後はアイディアを思いついてそれをゲームに落としこむところまでできる方なら、2、3日もあればリリースまで持っていけるようになると思います。

本書で作るゲームは実際にPlayストアにリリースされています。

興味のある方は落としてみて下さい。

以前執筆した書籍「コピペではじめるiPhoneゲームプログラミング」では、読者の方がiPhoneのゲーム総合ランキングで1位を取りました。

著者としては、こんなにうれしいことはありませんでした。(→紹介記事

今回も同じように、読者のかたからランキング上位獲得者が現れることを祈っています。

本書を読んでいて、ご不明な点等ありましたらTwitterでご連絡下さい。

もうアプリでは儲かりません!いち早くiPhoneのカジュアルゲームに目をつけた個人開発者は数百万〜数千万設けています。この流れはAndroidにも必ず来ます。是非ともいち早く本書を手にとって、カジュアルゲーム戦国時代の勝者になって下さい!

Categories

 

2014年4月
« 3月    
 123456
78910111213
14151617181920
21222324252627
282930  

  • Emma: Using a motorized abrasive tool, dermabrasion exfoliates away part of the epidermis. Although there are many methods of reducing acne scars today, b
  • Sharyn: I every time spent my half an hour to read this website's articles or reviews all the time along with a mug of coffee.
  • Alina Lutjen: I know this site presents quality based articles or reviews and additional information, is there any other website which offers these things in qual

About

Author: tachibana

  • ちょっとしたことはTwitterに書いています。こっちはアプリの公開等の時に更新されます。
  • 最近はもっぱらJavaとObjective Cです。AS3は飽きました。
  • スクリプト言語ではPerlが好きでしたが最近はGAE/Jで何でもやってます。
  • Linuxは自宅サーバー建てるのがやっとのレベルです。前の会社で何日も徹夜してやったのはいい思い出です。
  • アプリへのご要望などご意見等ありましたらお気軽にご連絡下さい。

Alternative content here