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月11日日曜日

面白いゲームについての考察 1



ゲーム開発は設計云々より、想像力と根性と感情移入が大事なのかなと感じました。これはいろいろ意見が分かれそうではありますが。

数ヶ月前にシステムに感情移入するなみたいなことをTwitterに書いたんですが、ゲーム開発は真逆ですね。「問題なく操作できる」ではなく「面白くて手放せない」というものを作らないといけない訳ですし。普通のシステム開発とは違う貴重な体験でした。

「ゲーム開発とは」みたいな書籍をずっと読みあさっていた時期があるのですが、ゲームの面白さは思いつく限りあげると「パズル」「スリル」「感情移入」「コレクション」「ソーシャル」に分類されるのかなと思えて仕方が無いのです。

『「おもしろい」のゲームデザイン』 http://j.mp/96fiJk 
と言う本があるのですが、一番印象に残っているのは「新しいパターンを学習するときに脳が活性化して面白いと感じる」というような内容です。これはまさにパズル的な面白さだと思います。

初めてグアムに行った時、空港についただけで楽しくなりますが、次の年に再びいっても、単なる移動作業みたいな感覚にとらわれます。こういう感覚にとらわれた事がある人は大勢というかほとんどの人がそう感じると思うのです。

いかに複雑なパターンをプレイヤーに提供し続けるかは、ゲーム開発の鉄則だと個人的に思います。さらにこの「新しいパターンで脳活性」の理屈で究極のAI、NPCとは何かということも妄想するのですが、結局それは人間だと思うのですよね。つまりオンライン対戦です。

対戦ゲームは相手がいろいろ学習して、予想できない動きをしてくる訳でして、人間以上に複雑な動きが出来る NPCはちょっと思いつきませんし。

次に「スリル」なのですが、安全欲求とか基本的な感情をいかにゲームで刺激しようか模索してまして、夜中に大学の心理学の教科書を引っ張りだして、内容を確認しながら調整していました。今思うとよくこんな事一人でやってたなと思うのですが。

極端に強い「中ボス」をランダム出現させる機能を実装した事があってこれはプレイヤーを理不尽に倒すためです。語弊があるかもしれませんが、安全な環境でヌクヌク過ごすよりも突然変異種に急に追いつめられるスリルが大事かなと思いました。

あと「感情移入」なのですがこれはミラーニューロンが関係していると思うのです。僕は茂木健一郎さんの『脳と仮想』という本に感銘をうけて一時期読みあさっていたのですが、ある書籍でミラーニューロンの記述がありました。

専門でないのでざっくりと書くのですが「他人がしている行動を自分の脳の中で同じ事をしたようにシュミレートする」ような機能が脳にはあるようです。逆にこの機能があるから「鏡に映った自分が他人ではなく自分と認識できる」ような記述があったように思えます。

諸説あると思うのですが、シナリオに感情移入する本質的な原理は「ミラーニューロンが働きすぎて錯覚を起こしている状態」と個人的には思えるのです。

いかに錯覚を起こさせるかは、まずゲームの世界に引きずり込むような導線が必要だと思うのですよね。これはゲームニクス http://j.mp/duUlDC という本がとても参考になりました。とにかく引きづり込むことをしないと次が始まらないですし。

次にキャラに感情移入させるにはどうするかなのですが、これは2chの萌え板を見た方が早そうです。ただ「ツンデレ」はアメリカの心理学の研究で有効だとデータがあるそうです。ゲイン・ロス効果というらしいです。

魅力的なキャラクタはつまり現実世界で他人に魅力を感じる心理だったりするので、広範囲に及ぶので割愛しますが、もう一つ別角度から考えると「どう記憶に定着させるか」も大事だと思えます。

キャラに感情移入が出来て長期記憶に定着させる事に成功したらたぶん開発者としては成功なのですよね。一番分かりやすい手法は強烈な事件を起こす事ですよね。いろいろな映画でもよくある方法ですし。これ以上書くとネタバレになるのでゲームが発売中は書けませんが。

次に「コレクション」「ソーシャル」は、これから実装しようと考えているので、まだ調査不足で深い知識も無くかけません。分野的には社会心理学が関係しているかとおもって大学で受講したのですが、多岐にわたるので要点をまとめづらいですね。

あれこれ考えていた事を吐き出してスッキリしました。移植作業に戻ります。ほんとに独り言です。

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

小技 Eclipse pluginの入れ過ぎで落ちる現象はインストールし直すべき

Eclipseでプラグインを入れすぎるとメモリが足りなくなり落ちるようになります。いくつかのサイトでeclipse.iniに下記の行を追加すると直ると書いてあったりしますが、マシンスペックの問題かもしれませんが私の環境では完全に直す事は出来ませんでした。


-XX:MaxPermSize=512m


スペックの低いノートPCや32bit版のEclipseだと限界があるでしょうね。

2、3日試行錯誤した結果、必要最低限のプラグインに留めるしか完全に直す事は出来ませんでした。
また、大抵この現象が起きたときはプラグインを削除しようにも入れ過ぎのためどれを削除して良いか分からないことになっていると思います。
使う機会のない大量のプラグインに悩まされるのは時間がもったいないのでEclipseをインストールし直すことをお勧めします。これが一番早いです。

小技 xcode 自動入力される会社名を変更

xcodeで新規にファイルを追加するときにヘッダーに会社名が入ります。(下記のような感じです)

* Copyright 2010 CosmicDragonGameEngine. All rights reserved.


これを変えるには、xcode上部のメニューから
「プロジェクト」->「プロジェクト設定を編集」->「一般」->「組織名」
で、ここの組織名を変えるだけで会社名を変更する事が出来ます。

小技 Eclipse SVN コンフリクト

EclipseのSVNプラグインを使っているとよくおこるのがコンフリクトです。
一度コンフリクトを起こすとどうにもなりません。とくにファイル削除処理をするとよくわからない事になります。そういうときは

Teram -> 「リポジトリから復元」

でデータをsvnと同期させ直してから修正しなおすのが楽だという事に気づきました。(他に方法があるかもしれません。)

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


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

修正版1.3リリース報告

「魔王なんてたおしちゃうから!」のバージョン1.3を無料版、有料版ともども
リリース致しました。下記2点の不具合が修正されております。

1. 無料版で「ブラウン少佐」が不自然にゲーム世界以外の話(商品の説明)をしていた
2. カメラワークが不完全で視点が小刻みにぶれ、画面がみづらい不具合

ご迷惑おかけしました。
今後とも「魔王なんてたおしちゃうから!」をよろしくお願い致します。

2010年6月8日火曜日

Desire実機確認



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

2010年5月30日日曜日

「魔王なんてたおしちゃうから!」不具合報告ページ



このページは「魔王なんてたおしちゃうから!」の不具合報告フォームです。
不具合やお気づきの点がございましたらコメント欄にご記入ください。
連絡がない場合は直接メールやtweetしていただいてもかまいません。
よろしくお願いいたします。

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月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でやる場合エミュレータの再起動は必要ありません。修正後、再コンパイルして実行すればエミュレータに最新のプログラムがインストールされます。エミュレータから操作してアプリを再起動させると反映されています。

GoogleAppEngine-RPCの使い方


前回のJDOQLクエリフィルタに引き続き、RPCによるGWTとサーバーの通信を実装します。RPCとは簡単に言うと、クライアントとサーバーでJavaのクラスをそのままやりとりするための通信方式です。

通常AJAXでの通信はXMLもしくはJSOPで行いますが、通信毎にプロトコルの仕様を決めなければなりませんでした。しかしGoogleAppEngineJavaはクライアント画面のGWTとサーバーの両方で使えるJavaのクラスを定義すれば簡単にデータのやりとりができます。

RPCでの通信を行うにはいくつか準備する必要があります。

・通信でやりとりされるオブジェクト
・RemoteServiceインターフェースとサーバー側の実装
・通信結果を受け取る非同期インターフェース

今回はつぶやき情報をサーバーに送る機能を実装してみようかと思います。

TweetPutParam.java
---------------------------

package com.devtter.client.rpc;
import com.google.gwt.user.client.rpc.IsSerializable;

public class TweetPutParam implements IsSerializable {
public String tweet;
public double x;
public double y;
public double z;
}

このクラスはつぶやきと、つぶやいた位置情報を保持しています。ここでのポイントはcom.google.gwt.user.client.rpc.IsSerializableクラスをimplemntsしていることです。このインターフェースを使うだけでコンパイル時にシリアライズ処理が実装されるようです。

TweetService.java
---------------------------

package com.devtter.client.rpc;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("tweet")
public interface TweetService extends RemoteService {
public String put(TweetPutParam param) throws IllegalArgumentException;
}


TweetServiceAsync.java
---------------------------

package com.devtter.client.rpc;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface TweetServiceAsync {
void put(TweetPutParam param, AsyncCallback callback)
throws IllegalArgumentException;
}


ここまでがインターフェースの定義です。サーバー側では定義されたTweetServiceをサーブレットとして実装します。具体的にはRPCを処理するcom.google.gwt.user.server.rpc.RemoteServiceServletを実装します。
今回のサンプルではクライアントでつぶやかれた内容をJDOに格納します。

TweetServiceImpl.java
---------------------------

package com.devtter.server.rpc;

import com.devtter.server.model.Tweet;
import com.devtter.server.model.TweetDAO;
import com.devtter.client.rpc.TweetGetParam;
import com.devtter.client.rpc.TweetPutParam;
import com.devtter.client.rpc.TweetResponse;
import com.devtter.client.rpc.TweetService;
import com.devtter.shared.FieldVerifier;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import java.util.logging.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;

@SuppressWarnings("serial")
public class TweetServiceImpl extends RemoteServiceServlet implements TweetService {
private static final Logger log = Logger.getLogger(TweetServiceImpl.class.getName());

public String put(TweetPutParam param) throws IllegalArgumentException{
if (!FieldVerifier.isValidTweet(param.tweet)) {
throw new IllegalArgumentException(
"Tweet must be at less 140 characters long");
}
log.info("put : " + param.tweet);
TweetDAO.tweetAnonymous(param.tweet);
return "";
}
}


次にクライアント側で、つぶやきを送る処理を実装します。実装はちょっと難解な書き方をしますが、まずTweetServiceAsyncをGWTクラスから生成します。これで呼び出す準備ができます。次に実際にコールするときはパラメータとレスポンスを指定することで呼び出すことができます。

TweetPanel.java
---------------------------

private final TweetServiceAsync tweetService = GWT
.create(TweetService.class);

public void sendTweetToServer() {
TweetPutParam putParam = new TweetPutParam();
putParam.tweet = inputPanel.inputBox.getText();
if (!FieldVerifier.isValidTweet(putParam.tweet)) {
return;
}

tweetService.put(putParam,
new AsyncCallback() {
public void onFailure(Throwable caught) {
}

public void onSuccess(String result) {
}
});
}


以上でRPCによるサーバークライアント通信の処理は完了です。慣れるまで敷居が高いですが、一度作ってしまえば、通信内容を変更する時は通信クラスのパラメータをかえるだけで対応出きるのでとても便利です。

なお、今回作ったプログラムは下記に置いてあります
http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/client/rpc
http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/server/rpc
http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/client/ui/tweet

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-GWTの使い方


前回のJDOに引き続き今回はGWTをGoogleAppEngine上で取り扱ってみます。

GWTとは簡単にいえば、JavaのAWTににたライブラリでアプレットのように画面を開発すると、それがJavaScriptに自動的に置き換わる開発キットのことです。
C++やJavaなどで組み込み系やWindows環境でGUIを開発してきたエンジニアにとって、HTMLやJavaScriptは開発スタイルの毛色が異なってやり辛いのですが、こういったクロスコンパイラがあれば今までのやり方でリッチなWEB画面が作れるので、GWTはとても魅力的に感じます。

では早速画面を構成するクラスを提示してみます。今回も引き続きTwitterのようなサイトを作るので、「メインパネルにテキストボックス、送信ボタン、つぶやきリスト」が出てくる部品を作ります。なお、このサンプルはEclipseのGAEプラグインが入っていて、「Web Application Project」というプロジェクトテンプレートを使った開発を想定しています。





TweetPanel.java 画面の部品がすべて乗ってくるパネル
-----------------------------

package com.devtter.client.ui.tweet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Label;

public class TweetPanel extends VerticalPanel {
public TweetInputPanel inputPanel;
public TweetListPanel listPanel;

public TweetPanel(){
this.inputPanel = new TweetInputPanel();
this.listPanel = new TweetListPanel();
super.add(inputPanel);
super.add(listPanel);

MyHandler handler = new MyHandler();
inputPanel.sendButton.addClickHandler(handler);
inputPanel.inputBox.addKeyUpHandler(handler);
}

class MyHandler implements ClickHandler, KeyUpHandler {
public void onClick(ClickEvent event) {
}

public void onKeyUp(KeyUpEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
}
}

private void sendTweetToServer() {
}
}
}


TweetPanelクラスのMyHandlerと言うインナクラスはパネルに対するイベント処理を行います。ここではキーイベントとクリックイベントのリスナが用意されており、送信ボタンが押されたときなどはここにサーバーへの通信処理を記述します。


TweetInputPanel.java 入力用のテキストボックスと送信ボタン
-----------------------------

package com.devtter.client.ui.tweet;

import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.TextBox;

public class TweetInputPanel extends HorizontalPanel {
public TextBox inputBox;
public Button sendButton;

public TweetInputPanel() {
inputBox = new TextBox();
sendButton = new Button("Send");
sendButton.addStyleName("sendButton");
super.add(inputBox);
super.add(sendButton);
inputBox.setFocus(true);
inputBox.selectAll();
}
}


TweetListPanel.java つぶやきリスト。ラベルを乗せる
-----------------------------

package com.devtter.client.ui.tweet;
import com.google.gwt.user.client.ui.VerticalPanel;

public class TweetListPanel extends VerticalPanel {
public TweetListPanel(){
}

public void putItem(String content){
TweetListItemPanel item = new TweetListItemPanel();
item.setText(content);
super.insert(item, 0);
}
}


TweetListItemPanel.java 一つ一つのつぶやき。最初はlabelで実装しておく
-----------------------------

package com.devtter.client.ui.tweet;

import com.google.gwt.user.client.ui.Label;;

public class TweetListItemPanel extends Label{
public TweetListItemPanel(){
this("");
}
public TweetListItemPanel(String label){
super(label);
}
}

-----------------------------

これで最低限の画面構成クラスはできました。これを表示するにはメインで使われるHTMLテンプレートにコンテナを記述するのと、そのコンテナにaddするプログラムを記述します。まず、メインが面に対応したHTMLファイルにmainContainerというidを作ります。これにより、この位置にパネルを配置できるようになります。

war/Devtter.html

<h1>devtter</h1>

<table align="center">
<tr>
<td colspan="2" style="font-weight:bold;">What's Happening?</td>
</tr>
<tr>
<td id="mainContainer"></td>
</tr>
<tr>
<td colspan="2" style="color:red;" id="errorLabelContainer"></td>
</tr>
</table>

---------------------------

次にEntoryPointクラスの初期処理に、UIを乗せる記述をします。なおcom.google.gwt.core.client.EntryPointは画面が最初に呼び出されるエントリーポイントを表すクラスです。通常HTMLのテンプレートとセットで扱います。

Devtter.java 画面表示時に最初に呼び出されるクラス

package com.devtter.client;

import com.devtter.client.ui.tweet.TweetPanel;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.RootPanel;

public class Devtter implements EntryPoint {
public void onModuleLoad() {
final TweetPanel tweetPanel = new TweetPanel();
RootPanel.get("mainContainer").add(tweetPanel);
}
}


これでUIのベースができました。次回はサーバーと連携するプログラムを付け足します
今回作成したプログラムはおおよそ下記にあります

http://code.google.com/p/devtter/source/browse/#svn/trunk/devtter/src/com/devtter/client/ui/tweet

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/

2010年4月7日水曜日

Qt4 で 「手抜き」OpenGL入門


前回、UbuntuでOpenGLとQtの環境を作ることを説明した続きです。

まず、OpenGLの一番有名な床井先生の入門サイトの紹介です

GLUTによる「手抜き」OpenGL入門 和歌山大学 システム工学部 デザイン情報学科 床井浩平

このサイトではGLUTを使ったサンプルや説明がされています。OpenGLの説明自体は前述のサイトを参照していただくとして、今回はこのサイトで取り上げられているサンプルを、GLUTではなく、Qt4のQGLWidgetで実装してみます。ソースコードは下記です。


http://svn.xp-dev.com/svn/algos/algos3d/qt4sample/sample02/


コンパイルと実効は下記です

----------------
make
./sample02

----------------

実行するとQtの画面上でキューブが回転します。今回はQGLWidgetとQThreadを使います。
OpenGLのバッファの制御はQGLWidgetを使っています。ただし、これだけだとアニメーション処理ができないので、QThreadクラスを使って連続描画を行っています。

GLUTはWindowsに持っていった時に終了イベントがとれなかったり、UIも日本語対応が甘かったり意外と融通が効かないので、Qtベースでの実装も試してみると良い事があるかもしれません。


MyGLWidget.h
----------------------------

#include "qapplication.h"
#include "qthread.h"
#include "qgl.h"

#ifndef MYGLWIDGET_H_
#define MYGLWIDGET_H_

class MyGLWidget : public QGLWidget, public QThread
{
Q_OBJECT;
public:
MyGLWidget();
protected:
void initializeGL();
void resizeGL(int w, int h);
void paintGL();
void run();
double r;
};

#endif /* MYGLWIDGET_H_ */

MyGLWidget.cpp
----------------------------
#include "MyGLWidget.h"

MyGLWidget::MyGLWidget()
{
QThread::start();
}

void MyGLWidget::initializeGL()
{
glEnable(GL_DEPTH_TEST);
glClearColor( 1.0, 1.0, 1.0, 1.0 );
}

void MyGLWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0);
glMatrixMode(GL_MODELVIEW);
}

void MyGLWidget::paintGL()
{
GLdouble vertex[][3] = {
{ 0.0, 0.0, 0.0 }, /* A */
{ 1.0, 0.0, 0.0 }, /* B */
{ 1.0, 1.0, 0.0 }, /* C */
{ 0.0, 1.0, 0.0 }, /* D */
{ 0.0, 0.0, 1.0 }, /* E */
{ 1.0, 0.0, 1.0 }, /* F */
{ 1.0, 1.0, 1.0 }, /* G */
{ 0.0, 1.0, 1.0 } /* H */
};

int face[][4] = {
{ 0, 1, 2, 3 },
{ 1, 5, 6, 2 },
{ 5, 4, 7, 6 },
{ 4, 0, 3, 7 },
{ 4, 5, 1, 0 },
{ 3, 2, 6, 7 }
};
GLdouble color[][3] = {
{ 1.0, 0.0, 0.0 },
{ 0.0, 1.0, 0.0 },
{ 0.0, 0.0, 1.0 },
{ 1.0, 1.0, 0.0 },
{ 1.0, 0.0, 1.0 },
{ 0.0, 1.0, 1.0 }
};

int i,j;

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glLoadIdentity();
gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

if(r>360)r=0;
glRotated(r, 0.0, 1.0, 0.0);
glRotated(r+=2, 1.0, 1.0, 0.0);

glBegin(GL_QUADS);
for (j = 0; j < 6; ++j) {
glColor3dv(color[j]);
for (i = 0; i < 4; ++i) {
glVertex3dv(vertex[face[j][i]]);
}
}
glEnd();
}

void MyGLWidget::run()
{
while(1){
QThread::usleep(10000);
update();
}
}

2010年4月6日火曜日

初めてのDeploy




何とかアプリケーションアカウントを設定後、Eclipseプラグインから
シンプルなスケルトンを作成し、deployボタンを押しつつメールアドレスやら
パスワードを設定したらなんと!
動きました!!

http://devtter.appspot.com/

この環境はすごい。Eclipseと完全に連動している。完全に時代は変わった。

GoogleAppEngineアカウント取得!


GoogleAppEngineのアカウントが急に取得できました!
諦めてアマゾン使おうかと落ち込んでいたら急にgmailにメールが
きてちょっと感動!
問い合わせフォームから書き込んだのがよかったのかな?

GoogleAppEngineのダッシュボードが何だか作戦会議室見たいでかっこいいです。


-
Hello,
You're receiving this email because you indicated you'd had some problems verifying your Google App Engine account using SMS. We just wanted to let you know we've enabled your account--you should be able to create applications now!
To start creating applications with Google App Engine, simply follow this link:
https://appengine.google.com/
Thanks!
The Google App Engine Team


俄然やる気が出てきました!

2010年4月5日月曜日

Ubuntu aptコマンドメモ パッケージ情報表示

Ubuntuでよく忘れるコマンドのメモです。

パッケージの情報表示

apt-cache show <パッケージ名>

パッケージを探す

apt-cache search "パターン"

インストールされたパッケージ一覧を表示する

dpkg -l

2010年4月4日日曜日

Qt4とQGLWidget


「Qt使って練習がてらオープンソースやってみない?」と誘われたのがきっかけ
でQtを調べてみたのですが、どうもこのQtライブラリはかなり整備されていて驚
きました。
どうやらQtデベロップメントフレームワークス社で開発されたC++用のオープン
ソースのアプリケーションフレームワークのようです。
クロスプラットフォーム対応を目指しているようで、Lunix, Windows, Macの
グラフィックやネットワークAPIの差を吸収してくれそうな期待が持てます。
採用実績としてはWeb ブラウザ OperaやGoogle Earthで使われているようです。

ドキュメントは下記のようです。
http://doc.trolltech.com/4.6/index.html

OpenGL関係もC++で使いやすくラッピングしているようで、デモを見るかかぎり
3Dグラフィック上に2DのGUIも載せれるようなので、ゲームの開発もなかなか
やりやすいのではないでしょうか。

http://doc.trolltech.com/4.6/demos-boxes.html


OpenGLの実装は下記のWidgetを使いつつOpenGLのAPIを組み合わせて描画するようです。
QGLWidget Class Reference:
http://doc.trolltech.com/4.6/qglwidget.html



ブラウザではちょっと厳しいアプリ開発をやるときはなかなか良さそうです。

UbuntuでQt4 + OpenGL

UbuntuにQt4ベースのOpenGL開発ライブラリを追加する。

------------------------------------------------
sudo apt-get install qt4-qmake
sudo apt-get install libqglviewer-qt4-dev
------------------------------------------------

開発ライブラリが入ったのでプロジェクトフォルダを作りそこにとりあえず
簡単な画面を出すプログラムを配置する。

------------------------------------------------
mkdir sample
cd sample
------------------------------------------------

sampleディレクトリにmain.cppというファイルを配置する。


------------------------------------------------

#include "qapplication.h"
#include "qgl.h"

int main(int argc, char *argv[])
{
QApplication window(argc, argv);
QGLWidget *mainwindow = new QGLWidget;
window.setActiveWindow(mainwindow);
mainwindow->show();
return window.exec();
}

------------------------------------------------

このmain.cppを配置した状態でqmakeを実行すると、このプログラムを認識したうえで
プロジェクトの雛形が作成される。


------------------------------------------------
qmake-qt4 -project
------------------------------------------------

出来上がったsample.proにOpenGLオプションを追記する

------------------------------------------------
QT += opengl
------------------------------------------------

下記のコマンドでMakefileを生成する

------------------------------------------------
qmake-qt4
------------------------------------------------

下記コマンドでコンパイルができる。

------------------------------------------------
make
------------------------------------------------

./sampleを実行すると真っ黒なだけのWindowsが出れば下準備は完了。

まとまったサンプルはこちら
http://svn.xp-dev.com/svn/algos/algos3d/qt4sample/sample01/


GoogleAppEngineのSMSメールが来ない

Google App Engine はローカル環境でも十分開発できるのだが、やはり
Google側にアカウントを用意しておきたい。作ったものが公開できる
保証がないと落ち着かないので。

しかし一晩待っても携帯電話にGoogle App EngineのSMS verification
messageメールがこないので下記のFAQを読んでみた。

http://code.google.com/intl/ja/appengine/kb/sms.html


-----------------------------------
現在問題が発生している携帯サービス会社

現在、次の携帯サービス会社への SMS メッセージ送信には問題があります。

* 日本 - DoCoMo
* 日本 - KDDI

確認コードを記載した SMS メッセージを受信できない場合は、SMS の問題のフォームに記入してください。
-----------------------------------


どうも携帯電話のメール送信が完全ではないようだ。
しょうがないので下記の申告フォームに調べてもらえるように書いてみる。

http://appengine.google.com/waitlist/sms_issues

とりあえずしばらくまた待つことにする。

路線変更

GoogleCodeを使っていたら、GoogleAppEngineが便利そうなことに
気づいたのでこちらを試すことに。

試しに下記から開発用のアカウントをとろうかと思ったのですが

https://appengine.google.com/start

携帯電話のアドレスを入れてもエラーがでるばかりでよくわからない
明日またチャレンジ。

2010年4月2日金曜日

「C++でWebAppを書きたいんだが、フレームワーク作ってみる」
と友達にいったら、Webサービスの方が需要があるんじゃないかと
言われたので、路線変更してそちらも調べてみることに。

GoogleAppEngineを調べてみると、Eclipseプラグインでスラスラ
作れるようなので開発環境だけ整えてみました。

今日はこのくらい。

2010年3月31日水曜日

はじめました。

GoogleCodeで自分のプロジェクト立ち上げてみました/
一緒にBlogも開設しました。
とても新鮮です。