ラベル Android の投稿を表示しています。 すべての投稿を表示
ラベル Android の投稿を表示しています。 すべての投稿を表示

2012年2月21日火曜日

計測プログラム

ゲームを作り込めば作り込む程だんだんパフォーマンスが悪くなるものです。 そんなときは計算処理、描画処理のループ部分に計測用プログラムを最初から組み込んでおくと、思わぬボトルネックに気付く事もあります。 せっかくですので、現在個人的に開発中の次回作で使用している計測ユーティリティを載せておきます。 Androidであればコピペして使えるプログラムなので便利ですよ。
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Logger;

public class BenchTable {
    
    private static BenchTable me = new BenchTable();
    
    private HashMap ones = new HashMap();
    private HashMap sums = new HashMap();
    
    public static BenchTable getInstance(){
        return me;
    }
    
    public void start(String key){
        One o = ones.get(key);
        if(o == null){
            o = new One();
            ones.put(key, o);
        }
        o.start = System.currentTimeMillis();
    }
    
    public void stop(String key){
        One o = ones.get(key);
        if(o == null) return;
        
        o.end = System.currentTimeMillis();
        
        Sum s = sums.get(key);
        if(s == null){
            s = new Sum();
            sums.put(key, s);
        }
        s.set(o.end - o.start);
    }
    
    public void dump(){
        Logger logger = Logger.getLogger(this.getClass().getName());
        for (Iterator it = sums.entrySet().iterator(); it.hasNext();) {
            Map.Entry entry = (Map.Entry)it.next();
            String key   = (String)entry.getKey();
            Sum value = (Sum)entry.getValue();
            logger.info("Bench : " + key
                    + " avg :" + (value.total / value.count) 
                    + " max :" + value.max
                    + " total :" + value.total
                    + " count :" + value.count
                    );
            value.reset();
        }
    }
}

class One {
    double start;
    double end;
}

class Sum{
    int count;
    double total;
    double max;
    
    void set(double t){
        count++;
        total += t;
        if(max < t)max = t;
    }
    
    void reset(){
        count = 0;
        total = 0;
        max = 0;
    }
}
BenchTableクラスでは、計測したい箇所の呼び出し回数、合計時間、最大時間、平均時間を HashTableで管理します。 使い方は下記で、気になる関数の前後にラベルを指定してstartとstopを呼び出します。 一定期間が過ぎたらdumpを呼び出せば、集めた結果を出力します。
    private int bm = 0;

    public synchronized void onCalclateObjects(){
     if(state != GameEngineState.GAME_START)return;
     
     if(bm++ > 300){
      bm=0;
         BenchTable.getInstance().dump();
     }

     BenchTable.getInstance().start("phaseCalcAvatars");
     phaseCalcAvatars();
     BenchTable.getInstance().stop("phaseCalcAvatars");
     BenchTable.getInstance().start("phaseCalcFieldItem");
     phaseCalcFieldItem();
     BenchTable.getInstance().stop("phaseCalcFieldItem");
     BenchTable.getInstance().start("phaseCalcAtacks");
     phaseCalcAtacks();
     BenchTable.getInstance().stop("phaseCalcAtacks");
     BenchTable.getInstance().start("phaseCollisionAvatars");
     phaseCollisionAvatars();
     BenchTable.getInstance().stop("phaseCollisionAvatars");
    }
出力結果はこんな感じです。
02-09 11:32:12.272: INFO/BenchTable(27115): Bench : phaseCalcAtacks avg :0.019867549668874173 max :1.0 total :6.0 count :302
02-09 11:32:12.282: INFO/BenchTable(27115): Bench : phaseCollisionAvatars avg :0.8377483443708609 max :3.0 total :253.0 count :302
02-09 11:32:12.292: INFO/BenchTable(27115): Bench : phaseCalcAvatars avg :1.1986754966887416 max :27.0 total :362.0 count :302
02-09 11:32:12.292: INFO/BenchTable(27115): Bench : phaseCalcFieldItem avg :0.10264900662251655 max :1.0 total :31.0 count :302
count :302

最後に、この計測プログラムで自分のアプリを調べたときに残念な事がわかりました。 「AndroidはJNIが遅いので、OpenGLの限界に達する前にJNIの限界でポリゴン数が限られてしまう」 Javaを使ってAndroidアプリを作ったときに3D処理にOpenGLを使うと思うのですが、おおよそ下記のような呼び出しがされます。
Javaアプリ -> OpenGL関数呼び出し -> JNI -> ネイティブOpenGL -> GPU
最近のAndroid端末のGPUはそれなりの性能のものが搭載されていると思うので、それなりのポリゴンを処理できるはずなのですが、JNIがそれほど早くないので、JavaからネイティブOpenGLAPIを呼び出す回数自体が限られてしまうのです。 なのでJavaでOpenGLの処理を書いてしまうとGPUの性能をフルに使う事が出来ない事になります。 本格的な3Dのゲームを作りたかったらC++等で作らないとダメそうですね。残念 orz

2011年2月18日金曜日

魔王なんてたおしちゃうから!2ndリリースしました



この度「魔王なんてたおしちゃうから!2nd」をAndroidMarketにリリースしました。
開発期間が長かったのでリリース前は結構しんどかったです。

リリース後の微調整もだいぶ終りまして、やっとブログも記事を投稿できるぐらいの余裕ができてきました。

かなり作り込んだので面白いゲームに仕上がったと思います。是非お試しください!

魔王なんてたおしちゃうから!2nd

2010年12月5日日曜日

OpenFeintの不具合修正




地下鉄などでゲームを遊んでいるときに、ネットワークがつながらないことがよくあると思います。携帯電話のゲームでは、そういう突然電波が悪くなっても遊べるようにするためのエラー処理がとても大事です。
OpenFeintのバージョン1.0.1ではそのパターンに致命的な不具合が残っているらしく、「初回起動時に電波が通じないと、その後常に画像がすべて表示されなくなる」という現象があります。もしOpenFeintのJava版を使っていて、そういった現象に悩まされた場合com.openfeint.internal.ui.WebViewCache.javaを下記の様に修正すると治ります。


/* この関数をコメントアウトし、処理を置き換える。
private void copyDefaultItems() {
final File baseDir = appContext.getFilesDir();
// deleteAll(new File(baseDir, "webui")); //!!!!!!DELETE THIS!!!!!!
if(!(new File(baseDir, "webui").isDirectory())) {
Thread t = new Thread(new Runnable() {
public void run() {
copyDefaultBackground(baseDir);
}
});
t.start();
}
else {
clientManifest = getDefaultClientManifest();
}
}
*/
/**
* 電波の通じない場所で起動すると画像が出ないバグがあるため、常に
* 画像ファイルをコピーする。
*/
private void copyDefaultItems() {
final File baseDir = appContext.getFilesDir();
Thread t = new Thread(new Runnable() {
public void run() {
copyDefaultBackground(baseDir);
}
});
t.start();
}


たぶんwebuiディレクトリだけできてしまうと次から画像ファイルをコピーしないため、オリジナルの画像を修復できないのが致命的になっている原因だと思います。この修正ではWebViewで使用する画像ファイルのキャッシュを起動時に常に初期化します。
(もしかすると次のバージョンでは治っているかもしれません。)

2010年10月5日火曜日

Androidマーケットのライセンスチェック実装サンプル



いよいよ本格的にAndroid2.2(Floyo)が市場に流通しそうな時期になってきました。2.2から処理が速くなったり、Flashがサポートされたりと、いよいよAndroidが盛り上がりそうな予感です。

ただ、「SDカードへのアプリのコピー機能」はアプリ開発者からすると気になりますよね。Androidはアプリが比較的安かったり、ファミコンエミュレータが出回ったりと比較的無法地帯ですので、ライセンス管理が気になります。
そんななかAndroidMarketはライセンスチェックライブラリを正式に提供した模様です。

Google公式ドキュメント(英語)Licensing Your Applications

ただ、まだ日本語のドキュメントがないのが実状ですので、コピペで使いまわせる簡単な実装を公開することにしました。下記のソースコードをActivityから呼び出してください。


AndroidMarketLicenseCheck.java
---------------------------

package com.yourapp.license;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.provider.Settings.Secure;

import com.android.vending.licensing.LicenseChecker;
import com.android.vending.licensing.LicenseCheckerCallback;
import com.android.vending.licensing.ServerManagedPolicy;
import com.android.vending.licensing.AESObfuscator;

public class AndroidMarketLicenseCheck {
private static AndroidMarketLicenseCheck me = new AndroidMarketLicenseCheck();
public static AndroidMarketLicenseCheck getInstance(){
return me;
}

//ここの数字を何となく変えておく
private static final byte[] SALT = new byte[] {
-2, 8, 30, -12, -10, -57, 74, -64, 51, 88, -95,
-2, 98, -17, -36, -11, -11, 2, -64, 89
};
private static final String BASE64_PUBLIC_KEY
= "MIIBIjA....ここに公開鍵を書いておく";

private Activity activity;

private LicenseCheckerCallback mLicenseCheckerCallback;
private LicenseChecker mChecker;

public void onCreate(Activity activity){
this.activity = activity;
mLicenseCheckerCallback = new MyLicenseCheckerCallback();
String deviceId = Secure.getString(activity.getContentResolver(),
Secure.ANDROID_ID);

mChecker = new LicenseChecker(
activity,
new ServerManagedPolicy(
activity,
new AESObfuscator(SALT,
activity.getPackageName(),
deviceId)),
BASE64_PUBLIC_KEY
);

mChecker.checkAccess(mLicenseCheckerCallback);
}

public void onDestroy(){
mChecker.onDestroy();
}

class MyLicenseCheckerCallback implements LicenseCheckerCallback {
public void allow() {
if(false){
//debug
showLicenseErrorDialog("License OK.");
}
}

public void dontAllow() {
showLicenseErrorDialog(
"Not licensed or The network is not connected. "
+ "Please check Google CheckOut or Android Market.");
}

public void applicationError(ApplicationErrorCode errorCode) {
showLicenseErrorDialog("Android Market Server Error: " + errorCode.name());
}

private void showLicenseErrorDialog(String msg){
AlertDialog.Builder dialog = new AlertDialog.Builder(activity);
dialog.setTitle("Error: Android Market License");
dialog.setMessage(msg);
dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
activity.finish();
}
});
dialog.show();
}
}
}

---------------------------
ここまで


これを呼び出すときは下記のようにActivityのonCreateとonDestroyから呼び出します。


public class HogeActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
AndroidMarketLicenseCheck.getInstance().onCreate(this);
}

@Override
protected void onDestroy(){
super.onDestroy();
AndroidMarketLicenseCheck.getInstance().onDestroy();
}


なおこのプログラムをコンパイルするには下記の操作が必要です

1. EclipseのAMDマネージャツールでAndroid用のライブラリを全て最新のものに置き換え下記が揃っていることを確認する。

- Market Licesing Package, revision 1 以上
- Google APIs by Google Inc, Android API 8 revision 2 以上

2. Market Licesing PackageのソースコードをEclipseプロジェクトにインポートしてしまう。Linuxの場合、例えば下記にあるソースを一緒にコンパイルする。

android-sdk-linux_86/market_licensing/library/src


3. AndroidManifest.xmlにライセンスチェックをしますよと記述しておく

<uses-permission android:name="com.android.vending.CHECK_LICENSE" />


4. デベロッパーコンソールのプロフィール変更画面から公開鍵をコピーして、プログラムに記述する(画面下の方にあります)


プロフィールの編集 URL(http://market.android.com/publish/editProfile)


5. プログラムも準備できたらテストします。デベロッパーコンソールのプロフィール変更画面の下の方に、「テスト応答」という項目があります。ここを変更すると、エラーが返ったり認証OKが返ったりとテストできます。ちなみにテストで使用するアカウントはAndroidMarketに開発者アカウントとして登録したgmailアカウントがデフォルトで使えるので、特に設定する必要はありません。

2010年9月2日木曜日

XMLのシリアライズ




「魔王なんて!」にネットワーク機能を付けようと思い必要な機能を洗い出したのですが、ログインや友達リスト等ゲームの処理に入る前にユーザー管理周りでも通信種類って結構多いのですよね。(メッセージ送信とかいろいろありますよね。)

相変わらず独りで実装しているので通信種類が増えすぎるとプログラム量がどんどん増えて管理しきれなくなるのですよね。個人でプログラムを作る場合はプログラムや処理種類を減らす工夫も大事なので、まずオブジェクトのシリアライズを検討する事にしました。これはサーバーとクライアントで同じクラスを使いたいためです。Android以外の端末にも将来展開したいので、バイナリデータで通信せずXMLが良いかなと思います。

そこで何か使えるライブラリを調べてみたのですが、どうも定義ファイルが必要なものが多いのですよね。で、実装がシンプルなものも少ないですし、ドキュメントは英語ですし。

なんだか英語のドキュメント読んでいる時間で実装できそうな気がしたので実装してみました。

XMLReaderがデシリアライズ。XMLWriterがシリアライズを行います。
key, valueで表しつつ、内部クラスも表現できる最小セットの実装かなとおもいます。


package com.cosmicdragon.darkgreen.xml;

public interface XMLDefine {

public static final String ELEMENT_ROOT = "d";
public static final String ELEMENT_ROW = "r";
public static final String ELEMENT_CLASS = "c";

public static final String ATTR_NAME = "n";
public static final String ATTR_VERSION = "v";
public static final String ATTR_KEY = "k";
public static final String ATTR_TYPE = "t";

public static final String TYPE_INT = "i";
public static final String TYPE_BOOL = "b";
public static final String TYPE_LONG = "l";
public static final String TYPE_STRING = "s";
public static final String TYPE_DATE = "d";

}




package com.cosmicdragon.darkgreen.xml;

import java.util.Date;

public class XMLReadListener {
public void readStart(String version){}
public void readEnd(){}
public void readClassStart(String name){}
public void readClassEnd(){}
public void readRowInt(String key, int value){}
public void readRowLong(String key, long value){}
public void readRowBoolean(String key, boolean value){}
public void readRowString(String key, String value){}
public void readRowDate(String key, Date value){}
}



package com.cosmicdragon.darkgreen.xml;

import java.io.IOException;
import java.util.Date;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

public class XMLReader implements XMLDefine {
private SAXParser parser = null;
private Handler hdl = new Handler();

public void readXML(XMLReadListener listener, InputSource is)
throws ParserConfigurationException, SAXParseException, SAXException, IOException{
hdl.reset(listener);

if(parser == null){
parser = SAXParserFactory.newInstance().newSAXParser();
}

parser.parse(is, hdl);
if(hdl.getException() != null)throw hdl.getException();

hdl.reset(null);
}
}

class Handler extends DefaultHandler {
private SAXParseException exception;
private XMLReadListener listener;

private boolean rowFlag = false;
private String rowType = "";
private String rowKey = "";
private String rowValue= "";

public void reset(XMLReadListener listener){
this.listener = listener;
this.exception = null;
}

public SAXParseException getException(){
return this.exception;
}

public void startElement(String uri, String localName, String qName, Attributes atts)
throws SAXException {
rowFlag = false;
if(qName.equals(XMLDefine.ELEMENT_ROOT)){
listener.readStart(atts.getValue(XMLDefine.ATTR_VERSION));
}else if(qName.equals(XMLDefine.ELEMENT_CLASS)){
listener.readClassStart(atts.getValue(XMLDefine.ATTR_NAME));
}else if(qName.equals(XMLDefine.ELEMENT_ROW)){
rowFlag = true;
rowValue = "";
rowType = atts.getValue(XMLDefine.ATTR_TYPE);
rowKey = atts.getValue(XMLDefine.ATTR_KEY);
System.out.println("startElement() : " + rowType + " : " + rowKey);
}
}

public void endElement(String uri, String localName, String qName)
throws SAXException {
if(qName == XMLDefine.ELEMENT_ROOT){
listener.readEnd();
}else if(qName == XMLDefine.ELEMENT_CLASS){
listener.readClassEnd();
}else if(qName == XMLDefine.ELEMENT_ROW){
if(rowType.equals(XMLDefine.TYPE_INT))
listener.readRowInt(rowKey, Integer.parseInt(rowValue));
else if (rowType.equals(XMLDefine.TYPE_LONG))
listener.readRowLong(rowKey, Long.parseLong(rowValue));
else if (rowType.equals(XMLDefine.TYPE_BOOL))
listener.readRowBoolean(rowKey, Boolean.parseBoolean(rowValue));
else if (rowType.equals(XMLDefine.TYPE_STRING))
listener.readRowString(rowKey, rowValue);
else if (rowType.equals(XMLDefine.TYPE_DATE))
listener.readRowDate(rowKey, new Date(Long.parseLong(rowValue)));
}
}

public void characters(char[] ch, int start, int length)
throws SAXException {
if(rowFlag)
rowValue += String.copyValueOf(ch, start, length);
}

public void error(SAXParseException e) {
exception = e;
}

public void fatalError(SAXParseException e) {
exception = e;
}

}




package com.cosmicdragon.darkgreen.xml;

import java.io.PrintStream;
import java.util.Date;

public class XMLWriter implements XMLDefine {

private int tabNum;

//---------------------------------------- write APIs.
public final void writeStartDocument(PrintStream out){
writeStartDocument(out, "UTF-8", "1.0");
}

public final void writeStartDocument(PrintStream out, String encode, String version){
tabNum = 0;
out.print("<?xml version=\"1.0\" encoding=\""
+ encode +"\"?>");
out.print("\n");
out.print("<" + ELEMENT_ROOT
+ " " + ATTR_VERSION + "=\"" + version + "\""
+ ">");
out.print("\n");
tabNum = 1;
}

public final void writeEndDocument(PrintStream out){
out.print("</" + ELEMENT_ROOT + ">");
}

public final void writeStartClass(PrintStream out, String name){
writeTab(out);
out.print("<" + ELEMENT_CLASS
+ " " + ATTR_NAME + "=\"" + name + "\""
+ ">");
out.print("\n");
tabNum++;
}

public final void writeEndClass(PrintStream out){
tabNum--;
writeTab(out);
out.print("</" + ELEMENT_CLASS + ">");
out.print("\n");
}

public final void writeInt(PrintStream out, String key, int v) {
writeRowStart(out, key, TYPE_INT);
out.print(v);
writeRowEnd(out);
}

public final void writeBoolean(PrintStream out, String key, boolean v){
writeRowStart(out, key, TYPE_BOOL);
out.print(v);
writeRowEnd(out);
}

public final void writeLong(PrintStream out, String key, long v){
writeRowStart(out, key, TYPE_LONG);
out.print(v);
writeRowEnd(out);
}

public final void writeString(PrintStream out, String key, String v){
writeRowStart(out, key, TYPE_STRING);
out.print(v);
writeRowEnd(out);
}

public final void writeDate(PrintStream out, String key, Date v){
writeRowStart(out, key, TYPE_DATE);
out.print(v.getTime());
writeRowEnd(out);
}

private final void writeRowStart(PrintStream out, String key, String type){
writeTab(out);
out.print("<" + ELEMENT_ROW
+ " " + ATTR_KEY + "=\"" + key + "\""
+ " " + ATTR_TYPE + "=\"" + type + "\""
+ ">");
}

private final void writeRowEnd(PrintStream out){
out.print("</" + ELEMENT_ROW + ">");
out.print("\n");
}

private final void writeTab(PrintStream out){
for(int i=0; i<this.tabNum; i++) out.print('\t');
}
}




テストプログラムを簡単ですが書いてみました。PersonクラスをいったんXMLに出力して、XML文字列からクラスを復元しています。


import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.PrintStream;

import java.util.Date;

import javax.xml.parsers.ParserConfigurationException;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import junit.framework.TestCase;

import com.cosmicdragon.darkgreen.xml.XMLReader;
import com.cosmicdragon.darkgreen.xml.XMLWriter;
import com.cosmicdragon.darkgreen.xml.XMLReadListener;

public class Test01 extends TestCase {

public void testA(){
ByteArrayOutputStream buff = new ByteArrayOutputStream();
PrintStream print = new PrintStream(buff);
PersonSerializer serial = new PersonSerializer();

Person p1 = new Person("hero", 18, true);

try{Thread.sleep(2000);}catch(Exception e){}

Person p2 = new Person("lynn", 17, false);


System.out.println("p1: " + p1);
System.out.println("p2: " + p2);

serial.writeXML(print, p1);
System.out.println(buff.toString());

try{
serial.readXML(buff.toByteArray(), p2);
}catch(Exception e){
e.printStackTrace();
}

System.out.println("p1: " + p1);
System.out.println("p2: " + p2);

super.assertEquals(p1.age, p2.age);
super.assertEquals(p1.name, p2.name);
super.assertEquals(p1.flag, p2.flag);
super.assertEquals(p1.date.getTime(), p2.date.getTime());
}
}

class Person extends XMLWriter {
int age;
String name;
boolean flag;
Date date;

public Person(String name, int age, boolean flag){
this.name = name;
this.age = age;
this.flag = flag;
this.date = new Date();
}

public String toString(){
return "Person : "
+ "age:" + age
+ ":name:" + name
+ ":flag:" + flag
+ ":date:" + date
;
}
}

class PersonSerializer extends XMLReadListener {
Person p;

public void writeXML(PrintStream out, Person p){
XMLWriter writer = new XMLWriter();
writer.writeStartDocument(out);
writer.writeStartClass(out, "Person");
writer.writeString(out, "name", p.name);
writer.writeInt(out, "age", p.age);
writer.writeBoolean(out, "flag", p.flag);
writer.writeDate(out, "date", p.date);
writer.writeEndClass(out);
writer.writeEndDocument(out);
}

public void readXML(byte[] in, Person p)
throws ParserConfigurationException, SAXException, IOException{
this.p = p;
InputSource src = new InputSource(new ByteArrayInputStream(in));
XMLReader reader = new XMLReader();
reader.readXML(this, src);
}

public void readRowInt(String key, int value){
p.age = value;
}

public void readRowBoolean(String key, boolean value){
p.flag = value;
}

public void readRowString(String key, String value){
p.name = value;
}

public void readRowDate(String key, Date value){
p.date = value;
}
}


下記は実行結果です。p1をシリアライズしてp2にデシリアライズをしています。


p1: Person : age:18:name:hero:flag:true:date:Thu Sep 02 01:15:23 JST 2010
p2: Person : age:17:name:lynn:flag:false:date:Thu Sep 02 01:15:25 JST 2010
<?xml version="1.0" encoding="UTF-8"?>
<d v="1.0">
<c n="Person">
<r k="name" t="s">hero</r>
<r k="age" t="i">18</r>
<r k="flag" t="b">true</r>
<r k="date" t="d">1283357723441</r>
</c>
</d>
p1: Person : age:18:name:hero:flag:true:date:Thu Sep 02 01:15:23 JST 2010
p2: Person : age:18:name:hero:flag:true:date:Thu Sep 02 01:15:23 JST 2010

2010年7月8日木曜日

小技 無料版と有料版アプリのプロジェクトを効率的に管理

有料版のAndroidアプリを作った場合、有料アプリとあわせてお試し版も用意すると思います。ただ、Androidマーケットは同じjavaのパッケージでアプリをアップできません。また、EclipseでAndroidアプリを作るときはプロジェクト毎に一つのActivityを用意すると思います。しかも、そのActivityのパッケージ位置にRクラスが自動生成されます。この制約があると、アプリでRクラスを参照しているとimport文がプロジェクト毎にべた書きになってしまいます。ちなみにRクラスはfinalクラスとして生成されるので、継承で他のクラスに引き継ぐ事も出来ません。

細かい話なのでまったく伝わらないような気もするのですが。。

こういった事情をあまり考慮せずに進めると、有料版のソースコード等をまるまる無料版のプロジェクトにコピーして、しばらく二重管理が続くような事になります。

この問題に関して2,3日試行錯誤をしたところ、下記の方法で解決しました。

1. Rクラスをコピーして別のパッケージに移動させる(自動生成されたRクラスは直接使わない)

2. 有料版のソースフォルダをsrcとsrc_gameの二つに分け、srcの方にActivityだけ置く。

3. 無料版は有料版のsrc_gameを参照させる([Project]-> [Properties] -> [Java Build Path] -> [Source]で追加できる)

簡単にいうと、Activity以外は極力プロジェクト間で共有させれば良いという事です。
たぶん、この小技は細かすぎて伝わらなさそうですが、もし有料版アプリを作るときがあればこういう話があったなぁと一つの解決法として捉えてくれればと思います。

2010年6月28日月曜日

NexusOne, Droidでテクスチャがでない問題




「魔王なんて!」発売後、かなり悩まされた問題があります。特定の端末で「テクスチャが全く表示されない」という現象です。発生端末は海外の端末全てのようで、主にNexusOne, Droidで発生すると申告が来ていました。
(バグ修正にご協力頂いたGoogle Chris Pruettさん、kerieruさん、ありがとうございました。m(_ _)m)

まったく同じプログラムなのにどうしてこういうことが起きたかというと、OpenGLESの仕様が違うかららしいです。具体的には何が違うかというと、「2の累乗以外のテクスチャをサポートしているかどうか?」です。日本用の端末のDesireとXperiaはてきとうなサイズの画像ファイルを読み込んでも簡単にOpenGLのテクスチャとして使用できますが、海外の携帯は「2の累乗サイズ」でないと全く表示できません。

また、最後に悩まされたのが下記の関数です。これは海外の端末では全く機能しませんでした。


gl.glTexImage2D(GL10.GL_TEXTURE_2D, 0, GL10.GL_RGBA, tex.w, tex.h, 0, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, null);
gl.glTexSubImage2D(GL10.GL_TEXTURE_2D, 0, 0, 0, tex.w, tex.h, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, bb);


どうもこの関数は引数にサイズを指定できるようで、こういう関数を使用してもいけないみたいです。うまくいったのは下記の関数をつかって2の累乗サイズの画像ファイルを読み込んだときだけでした。


GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);


この問題は実機テストがかなり大変なので、横着せずに2の累乗でテクスチャを用意するような実装を最初からした方が良さそうです。

2010年6月25日金曜日

Xperiaのandroid.os.HandlerのsendMessageDelayedが不正確な問題

「魔王なんて!」をリリースしてから2週間と少し経過して、やっとバグ修正も一段落してきました。Xperiaについてかなり重要な事が分かったので情報シェアさせていただきます。

私はXperiaユーザーではないので正確なアップデート日付は分かりませんが、2010年6月中旬ぐらいのアップデートをきっかけにTimer処理がおかしくなりました。具体的にいうとandroid.os.Handlerクラスで処理しているキーリピートの間隔が急にバラバラになった事です。そのため、java.util.Timerやjava.lang.Threadに処理を置き換えましたが全くダメでした。具体的には下記のような実装をしているゲームアプリはいまごろ全てダメになっているような気がします。また、GC(ガベージコレクション)が重たくなりキーリピートがスムーズに動きません。


public class TickHandler extends Handler {
long before = 0;
long delay = 200;
public TickHandler(){
this.sleep(1000);
}

@Override
public void handleMessage(Message msg) {
long current = SystemClock.uptimeMillis();
long x = current - before;
if(x > delay){
before = current;
if(ControllerView.repeatNative)
pushRepeat(true);
}
this.sleep(10);
}

public void sleep(long delayMills) {
removeMessages(0);
sendMessageDelayed(obtainMessage(0),delayMills);
}
}



どうやって回避したかというと、下記二つの修正を行いやっと回避が出来ました。

* 「リアルタイムのインスタンス生成を(new)をとにかく減らす」
* 「GLSurfaceView.RendererのonDrawFrameのイベントでタイマー処理を統一」

2つ目の修正なのですが、どうやらOpenGLのタイマー処理だけ何故か正確だったのでこれに統一しました。どうしてもXperiaでタイマー処理がガタガタになったらこの修正を試す事をお勧めします。

2010年6月10日木曜日

レビューサイト「アンドロイダー」様に「満点+熱+一押し」最高評価頂きました!


Androidアプリレビューサイト「アンドロイダー」様サイト上にて「魔王なんてたおしちゃうから!」のレビューをしていただき、「満点 + 熱」の最高評価をいただきました。

ついに登場 2.5Dのトルネコ風ファンタジーRPG
魔王なんてたおしちゃうから!Lite


きっとゲームのキャラクター達も喜んでいると思います!ありがとうございました!

2010年6月8日火曜日

Desire実機確認



Desireをやっと購入できまして、実際に動かしてみました。Xperiaで起動したときよりテクスチャの読み込みが圧倒的に早く、起動時間も倍速ぐらいの印象です。
たぶんこれはAndroidOS1.6と2.1の差じゃないかと思うのですが、Android2.2にあがったらさらに早くなるらしいですし、これからが楽しみです。

2010年5月29日土曜日

Google Android marketへの登録準備




2ヶ月近くかけて開発していたゲームがほぼ完成したので公式サイトをオープンしました。もともとはGAEの調査をしようと始めたのですが、Googleで公開されていたAndroidのSDKがとてもよく出来ていたので、ついついゲームを作ってしまいました。
androidのアプリ開発コンテストも8月に開催されるようなので、英語訳してエントリーしてみようかとも思います。

「魔王なんてたおしちゃうから!」



ここまで作った訳ですから、android marketで販売してみようかと思います。アップロードはGoogleのアカウントや開発者としてのアカウント登録が必要のようです。

Android Market デベロッパーコンソール

アプリの登録にはGoogle Checkoutでクレジットカードを登録し、$25払う必要があるようです。アプリの売上げを受け取るには、銀行口座番号をその後登録する必要があります。これには3営業日ぐらいかかるみたいです。Googleから自分の口座に小額振り込まれるらしいので、その金額を銀行担当者に聞いて、GoogleCheckの画面に金額を入力すると、口座の確認とされるようです。面白い確認方法ですね。Googleらしいと言えばGoogleらしいです。

デベロッパーコンソールを読んでいると、コピー防止機能もあるみたいです。これは開発用端末経由での流出を防ぐためでしょうか。どちらにせよ大事なアプリであればOnにした方が良いですね。


コピー防止: コピーが防止されているアプリケーションは、Android Dev Phone では表示できません。コピー防止ツールは、アプリケーション デベロッパーに基本的な保護を提供し、ユーザー間の安易なアプリケーションの海賊版の配布を阻止します。Android Dev Phone はデベロッパーに十分な柔軟性を提供するように設計されています。この携帯電話では、携帯端末ソフトウェアのデベロッパーに、この電話のすべての機能のアクセス権が最大限提供されます。Android オープンソース プロジェクトの変更されたバージョンをインストールすることもできます。アプリケーションの無断コピーを最小限に抑えるため、Android Dev Phone には、コピーが防止されているアプリケーションの配布は行っていません。


今回作ったアプリはもう少し微調整してから販売したいので今日は公開しませんが、アプリをアップロードしたら引き続きどうなったかをレポートしたいと思います。

2010年5月28日金曜日

開発中のRPGの動画 2



かなりパフォーマンスチューニングをしました。
結局ボトルネックだったのはjavaのインスタンス生成コストとテクスチャの読み込みでした。
OpenGLに関しては出来るだけ無駄な描画を避けるのがポイントだと思います。

完成したらソースを含めて記事にまとめてみようかと思います。

2010年5月15日土曜日

開発中のRPGの動画



Android向けにゲームを作りたくなりまして、開発を始めてます。ジャンルはRPGです。
せっかくなので開発中のエミュレータでの動画をアップしてみました。

実際に開発していて思う事がいろいろあるのですが、まずエミュレータと実機では実行速度にかなり差があるのが分かりました。
Androidはいろんな性能の端末があるのでフレームレートのバランスをとる仕組みも必要そうですね。

2010年4月11日日曜日

Android-EclipsePluginのセットアップ


下記環境でAndroid開発環境をセットアップしてみようかと思います。
1. Eclipse
2. JDK1.6
3. OS Ubunts

下記を参考にしてみます。

Download the Android SDK


In particular, you may need to install the JDK (version 5 or 6 required) and Eclipse (version 3.4 or 3.5, needed only if you want develop using the ADT Plugin).


JDKとEclipseは入っているのでこのままで大丈夫そうです。


2. Download and install the SDK starter package


Select a starter package from the table at the top of this page
and download it to your development computer. To install the SDK,
simply unpack the starter package to a safe location and then add
the location to your PATH.


Eclipseの設定の前にAndroid SDKを設定しないといけなさそうです。下記のページを参考に設定してみます。

Installing the SDK


Optionally, you may want to add the location of the SDK's primary tools directory to your system PATH. The primary tools/ directory is located at the root of the SDK folder. Adding tools to your path lets you run Android Debug Bridge (adb) and the other command line tools without needing to supply the full path to the tools directory.


* On Linux, edit your ~/.bash_profile or ~/.bashrc file. Look for a line that sets the PATH environment variable and add the full path to the tools/ directory to it. If you don't see a line setting the path, you can add one:
export PATH=${PATH}:/tools


どうやらダウンロードしたSDKを展開後、bashのパス設定に展開したディレクトリを含めればいいようです。android-sdk_r05-linux_86.tgzをダウンロードして、自分のホームディレクトリに展開してみました。~/.bashrcに下記のように設定しておきます


export PATH=${PATH}:~/android-sdk-linux_86/tools


このSDKの説明に大事な事が書いてありまして、下記の用に実行するとソフトウェアアップデートがかかるみたいです。Googleの最新API用開発セットも含まれているようなのでせっかくなので全部入れておきます。もしかしてGoogle API アドオンに関係あるんでしょうか?


android update sdk


では、Eclipseを起動し直して、下記を参考にプラグインをインストールします。

ADT Plugin for Eclipse



1. Start Eclipse, then select Help > Install New Software.
2. In the Available Software dialog, click Add....
3. In the Add Site dialog that appears, enter a name for the remote site (for example, "Android Plugin") in the "Name" field.

In the "Location" field, enter this URL:

https://dl-ssl.google.com/android/eclipse/

Note: If you have trouble acquiring the plugin, you can try using "http" in the URL, instead of "https" (https is preferred for security reasons).

Click OK.
4. Back in the Available Software view, you should now see "Developer Tools" added to the list. Select the checkbox next to Developer Tools, which will automatically select the nested tools Android DDMS and Android Development Tools. Click Next.
5. In the resulting Install Details dialog, the Android DDMS and Android Development Tools features are listed. Click Next to read and accept the license agreement and install any dependencies, then click Finish.
6. Restart Eclipse.


このあとEclipseで使えるようにするには「Window->preferences->android」でAndroid SDKのパスを入力しないといけないようです。(今回の場合~/android-sdk-linux_86)

インストールもうまくいったので早速Hello, Worldを参考にプログラムを作ってみます。

まず事前にエミュレータの設定をする必要があるようです。


:~$ android create avd --target 2 --name my_avd
Android 1.5 is a basic Android platform.
Do you wish to create a custom hardware profile [no]
Created AVD 'my_avd' based on Android 1.5


もう一度Eclipseを起動しなおして、プロジェクトを作ります。詳しくはHello, Worldを参照の事。

ここで気になるのはビルドターゲットと最低SDKのバージョンの関係。下記は大事そうなのでメモをしておく。


ここで、選択した「ビルド ターゲット」で Android 1.1 プラットフォームが使用されることに注目してください。これは、作成するアプリケーションが Android 1.1 プラットフォームライブラリをターゲットとしてコンパイルされることを意味します。先ほど作成した AVD は Android 1.5 プラットフォームで実行されます。バージョンの数字が一致しませんが、Android アプリケーションには上方互換性があるため、1.1 プラットフォーム ライブラリをターゲットとして構築されたアプリケーションでも 1.5 プラットフォームで正常に動作します。ただしその逆の場合は正常に動作しません。


下記はサンプルのソースコードです。


package com.example.helloandroid;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class HelloAndroid extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setText("Hello, Android");
setContentView(tv);
}
}


コンパイルして実行してみます。どうもエミュレータ起動までものすごく時間がかかるようで計測したら1分15秒かかりました。
ここからがかなり大事なのですがAndroidの開発をEclipseでやる場合エミュレータの再起動は必要ありません。修正後、再コンパイルして実行すればエミュレータに最新のプログラムがインストールされます。エミュレータから操作してアプリを再起動させると反映されています。