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

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

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年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年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月18日日曜日

UbuntuでJavaDocコマンドがインストールできない

UbuntuのJavaDocコマンドがパッケージの依存関係の問題でapt-getでインストールできません。Ubuntuのバージョンは9.10です。下記のようなエラーが出たら同じ現象だと思われます。


sun-java6-doc (6-15-1) を設定しています ...
This package is an installer package, it does not actually contain the
JDK documentation. You will need to go download one of the
archives:

jdk-6u10-docs.zip jdk-6u10-docs-ja.zip

(choose the non-update version if this is the first installation).
Please visit

http://java.sun.com/javase/downloads/

now and download. The file should be owned by root.root and be copied
to /tmp.

[Press RETURN to try again, 'no' + RETURN to abort] no
Abort installation of JDK documentation
dpkg: sun-java6-doc の処理中にエラーが発生しました (--configure):
サブプロセス installed post-installation script はエラー終了ステータス 1 を返しました
以下のパッケージの処理中にエラーが発生しました:
sun-java6-doc
E: Sub-process /usr/bin/dpkg returned an error code (1)


どうやらUbuntuの sun-java6-doc パッケージがおかしいようです。回避方法を色々試したところ、gjdocを無理やりインストールすればjavadocコマンドが使えるようになりました。


sudo apt-get install gjdoc


gjdocとはGNUライセンスで開発されているオープンソースのJVM関連のツールの実装のようですね。sun純正のパッケージの依存関係がおかしいようなので、これを直接インストールすればよかったみたいです。

http://www.gnu.org/software/classpath/cp-tools/

こういった役に立つツールのオープンソースプロジェクトは素晴らしいですね。ちなみにバージョンを表示したら下記のようです。


javadoc --version
gjdoc 0.7.9

2010年4月10日土曜日

GoogleAppEngine-JDOQLクエリフィルタの使い方

前回のGoogleAppEngine-GWTの使い方の続きです

今回はGWT UIとサーバーを連携させる前に、JDOQLの高度なフィルタ機能を少し説明します。

JDOQLはSQLのような構文でGoogleのストレージを操作することができます。基本的には文字列で条件文を記述すれば操作はできますが、JDOQLにJavaのクラスを混ぜたいときがあります。例えば、オブジェクト内にDateクラスが含まれていて、ある日時以降のデータを検索したい場合です。
そういったときに、JDOQLのクエリフィルタAPIを使用し、高度な条件を指定します。今回は、「指定した位置から指定個数検索する」と「指定時間以降のデータを指定個数検索する」の二つの機能を作ってみます。


Tweet.java
----------------------------------


package com.devtter.server.model;

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Tweet {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private User googleId;

@Persistent
private String content;

@Persistent
private Date date;
}


TweetDAO.java
----------------------------------

package com.devtter.server.model;

import java.util.List;
import java.util.Date;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;


public class TweetDAO {

/**
* 指定した位置から指定個数のつぶやきを検索します
*
*
* @param start 開始位置
* @param num 個数
* @return Tweetクラスのリスト
*/
public static List find(int start, int num) {
PersistenceManager pm = PMF.get();
String query = "select from "
+ Tweet.class.getName()
+ " order by date desc range "
+ start + "," + num;
List tweets = (List) pm.newQuery(query).execute();
pm.detachCopyAll(tweets);
pm.close();
return tweets;
}

/**
* 指定時間以降につぶやかれたTweetを検索します。
*
* @param starttime 開始時間
* @param num 個数
* @param desc 降順にするかどうか。
* @return Tweetクラスのリスト
*/
public static List findBeforeStartTime(long starttime, int num, boolean desc) {
PersistenceManager pm = PMF.get();
Date dateStartTime = new Date(starttime);
Query query = pm.newQuery(Tweet.class);
query.declareImports("import java.util.Date");
query.declareParameters("Date ParamStartDate");
query.setFilter("date > ParamStartDate");
if(desc)
query.setOrdering("date desc");
else
query.setOrdering("date asc");

query.setRange(0,num);

List tweets
= (List) pm.newQuery(query).execute(dateStartTime);
pm.detachCopyAll(tweets);
pm.close();
return tweets;
}
}


一つ目のfind関数では、クエリフィルタAPI使わず文字列で検索文を作成し、実効しています。この処理はおおよそ下記のような文字列になります。

select from com.devtter.server.model.Tweet order by date desc range 0, 5

次にfindBeforeStartTime関数ではTweetクラスのDateオブジェクトが指定した日時以降かを条件に検索しています。この処理をするにはまず、declareImports関数でDate型をimportします。これによりQueryクラス内でjava.util.Dateクラスが使えるようになります。次にdeclareParameters関数で変数を宣言します。そして、setFilter関数で条件判定を指示しています。ここで指定しているParamStartDate変数に値を代入するには、検索実行時のexecute関数の引数に変数を代入します。

Twitterのようなアプリケーションを実際に作り込むと、必ず高度な条件での検索が必要になるのでクエリフィルタは重要な機能ですね。
なお今回のプログラムはおおよそ下記にあります。
http://code.google.com/p/devtter/source/browse/trunk/devtter/src/com/devtter/server/model/TweetDAO.java

GoogleAppEngine-JDOの使い方

GoogleAppEngineの練習としてTwitterのようなサイトを作ってみようと思います。

JDOとは簡単にいえば、Googleの高性能ストレージにデータを保存するためのインターフェースです。今回はJDOを使ってつぶやきデータの保存が出きるような機能を用意してみます。

最初に定義するのはTweetクラスとPositionクラスです。Tweetクラスはつぶやきを表しPositionクラスは発言時の位置情報を表します。ポイントとしてはJDO用のオブジェクトが親子関係になる場合のアノテーションの書き方です。子クラスの方はimport com.google.appengine.api.datastore.Keyをプライマリーキーに使います。

Tweet.java

package com.devtter.server.model;

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Tweet {

@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;

@Persistent
private User author;

@Persistent
private String content;

@Persistent
private Date date;

@Persistent
private Position pos;

public Tweet(User author, String content, Date date) {
this.author = author;
this.content = content;
this.date = date;
}

public Long getId() {
return id;
}

public User getAuthor() {
return author;
}

public String getContent() {
return content;
}

public Date getDate() {
return date;
}

public void setAuthor(User author) {
this.author = author;
}

public void setContent(String content) {
this.content = content;
}

public void setDate(Date date) {
this.date = date;
}

public Position getPotision() {
return pos;
}

public void setPosition(Position pos) {
this.pos = pos;
}
}


Position.java


package com.devtter.server.model;

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;


@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Position {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Key key;

@Persistent
public double x;
@Persistent
public double y;
@Persistent
public double z;
@Persistent
private Date date;

public Key getKey(){
return key;
}

public Date getDate() {
return date;
}

public void updateDate() {
this.date = new Date();;
}
}


次に、オブジェクトを操作するクラスです。ポイントはdetachCopyAll関数を使って、PersistenceManagerをクローズ後もJDOオブジェクトに参照出きるよう、オブジェクトをデタッチしているところです。簡単に言えばオブジェクトのインスタンスのコピー、Java風に言えばクローンを作成します。これをしないでクローズ後に参照しようとすると下記のエラーが発生します。

org.datanucleus.exceptions.NucleusUserException: Object Manager has been closed

JDOがORマッピングのような作りになっているので、実際にデータをストレージに反映
するタイミングとメモリ上のインスタンスの差分の関係を解決するためにこういった設計に
なっていると思われます。

TweetDAO.java


package com.devtter.server.model;

import java.util.List;
import java.util.Date;
import javax.jdo.PersistenceManager;

public class TweetDAO {

public static void tweetAnonymous(String comment){
Tweet t = new Tweet(null, comment, new Date());
save(t);
}

public static void save(Tweet t) {
PersistenceManager pm = PMF.get();
pm.makePersistent(t);
pm.close();
}

public static List find(int start, int num) {
PersistenceManager pm = PMF.get();
String query = "select from "
+ Tweet.class.getName()
+ " order by date desc range "
+ start + "," + num;
List tweets = (List) pm.newQuery(query).execute();
pm.detachCopyAll(tweets);
pm.close();
return tweets;
}
}



この作りでデータを格納していくと、もしこれがMySQLなどであれば一つのテーブルにすべてのつぶやきが入ってしまうことになります。しかしGoogleAppEngineの説明を読む限り、GAE上でJDOを使う限りは負荷分散は自動化されるようです。この作りで大量にデータを投入し、パフォーマンスがどうなるか実験してみようかと思います。


引用:
Google App Engine なら、そのようなことを気にする必要はありません。
App Engine のインフラは、シンプルな API を使って、データの配布、
複製、負荷分散を一手に引き受けます。ユーザーは強力なクエリ エンジン
とトランザクションのメリットを享受できます。



今回使ったプログラムはおおよそ下記にあります。

http://code.google.com/p/devtter/source/browse/trunk/devtter/src/com/devtter/server/model/