★はじめに★
第8話 パケ代定額のWIN端末で動くアドベンチャーゲームを作る
[JAVA PRESS Vol.35]

ゲーム:アドベンチャーゲーム
対応端末:W11H・W11K


 テキスト 布留川 英一 (ん・ぱか工房)
 イラスト つきみの せひら (せひらねっと)



へにへに〜、この記事見てっ!
なんとauから、パケ代定額の『WIN端末』が発売されたんだって〜!
これでようやくあたしもパケ死から解放かな??
そらみちゃんのパケ代を払ってるのは母星のお父さんだから、むしろそらみちゃんはパケ殺しの犯人でしょうに…。
いーのっ! パケ代は父さんに送ってる写メとかで使ってんだから。
でね、WIN端末って『EZアプリ(Java) Phase3』に対応してて、505iのアプリより機能がすごいんだって!
「150KBの実行ファイル」「パケ代定額」「QVGA対応」という利点だけでも今までにないスゴいゲームを作れると思うよ。
…それにしても、ドコモやボーダフォンに比べるとauのアプリって少ないよね。
auは今後、JavaよりもBREWを重視していくみたいだけど、BREWは開発効率が悪いし、Javaは切られようとしてるからメーカーも力を入れにくいしねぇ…。
せっかくパケ代定額のサービスも始まって、大容量アプリもじゃんじゃん落とせる環境ができたっていうのに、ホント、もったいないよね〜。
まあ、auのJava勝手アプリを増やそうって意味も込めて、今回はアドベンチャーゲームでも作ってみない?
へにへに、たまには前向きで良いこと言うね〜!
WIN端末なら、いくら追加画像やテキストを読み込んでも定額だしね。さっそく作ってみよう!


★EZアプリ(Java) Phase3★


EZアプリ(Java)
「EZアプリ(Java)」はauの携帯電話におけるJavaアプリダウンロードサービスのことです。EZアプリ(Java)ではJava API仕様として「MIDP(Mobile Information Device Profile)1.0」が採用されています。MIDP1.0は携帯電話をはじめとする多くの携帯端末で採用されている仕様です。しかし、携帯端末の進化は速く、MIDP1.0の仕様だけではアプリ作成には不十分なため、EZアプリ(Java)には「KDDIプロファイル」と呼ぶ拡張APIが用意されました。執筆時現在(2004年1月)、「Phase1」「Phase2」「Phase2.5」「Phase3」の4つのバージョンがあります。


Phase3の実行ファイルとデータ保存領域
Phase3では実行ファイルとデータ保存領域のサイズが大幅に拡張されました。EZアプリ(Java)の実行ファイルは「KJXファイル」と呼ばれ、前Phaseの3倍の150Kバイト、データ保存領域は従来からの10Kバイトの「レコードストア」に加えて、200Kバイトの「拡張データストレージ」が追加されました。
各Phaseの実行ファイルとデータ保存領域のサイズ
ファイル種別 ファイルサイズ 合計サイズ
Phase3 KJXファイル 150Kバイト 360Kバイト
レコードストア 10Kバイト
拡張データストレージ 200Kバイト
Phase1/2/2.5 KJXファイル 50Kバイト 60Kバイト
レコードストア 10Kバイト
執筆時現在(2004年1月)、「Phase3」の仕様は一般公開されていないため、「拡張データストレージ」を勝手アプリで使用することはできませんが、150KのKJXファイルのアプリを作成することは可能です。


Phase3の処理速度
Phase3の対応端末は処理速度も格段にアップしています。次の表はベンチマークアプリ「かたぷ〜べんち」により計測した結果を元に、C3001Hを1として何倍速いかを比較したものです。C3001Hに比べて、Forループは249倍、数値演算は81倍、文字列操作は48倍、描画は5倍速くなっていることがわかります。
C3001Hを1として何倍速いかを比較したもの
Forループ 数値演算 文字列操作 描画
W11H高速 249.92 81.99 48.07 5.89
W11H通常 170.46 54.66 35.42 5.69
(参考)A5303HII最速 47.42 18.01 31.29 3.52
(参考)C3001H 1.00 1.00 1.00 1.00


Phase3の新機能
Phase3では3Dポリゴンや2Dスプライト描画など、たくさんの新機能が追加されました。
各Phaseにおいて拡張された機能
対応機種 前Phaseからの拡張点
Phase1 C451H/C452CA
Phase2 C3001H/C3002K/C3003P
C5001T
A3011SA/A3012CA/A3013T/
A3014S/A3015SA
バックライト制御
ブラウザやメーラーとの連携
音声通話連携
自動起動
Phase2.5 A5301T/A5302CA/A5303H/
A5303H U/A5305K/A5401CA/
A5402S
複数の音の同時再生
待ち受けアプリ
一時停止機能
ブラウザからのアプリ起動時のパラメータ引渡し
Phase3 W11H/W11K/CA5403 3Dポリゴン描画
2Dスプライト描画
カメラ制御
図形描画の拡張
JPEGエンコード
データフォルダ書き込み
外部メモリ連携
アドレス帳編集
バーコード解読
HV-script再生機能


★CDMA 1X WIN★


CDMA 1X WIN
CDMA 1X WIN」は、auの携帯電話によるブロードバンドサービスのことで、最大で上り144kbps、下り2.4Mbpsという高速なデータ通信が行なえます。そして、最も大きな特徴として挙げられるのが「EZフラット」と呼ぶパケット料金の定額プランが用意されていることです。ただし、パソコンなど外部と接続して行うモバイル通信のパケット料金は、定額に含まれないので注意してください。

また、auは「CDMA 1X WIN」向けに大容量の番組配信サービス「EZチャンネル」も開始しました。見たい番組を登録するだけで、深夜の寝ている間に自動でダウンロードします。各番組の容量は約3Mバイトで、動画や音声を多用したものが多く、再生時間は数分から十数分ほどです。

執筆時現在(2004年1月)、「CDMA 1X WIN」に対応している端末は「W11H」「W11K」の2機種です。

端末スペック
識別コード 画面サイズ フォント
LARGE
フォント
MEDIUM
フォント
SMALL
Javaヒープ
W11H HI31 240x268 24x24 20x20 12x12 2097152
W11K KC31 240x268 24x24 20x20 12x12 2097152
(参考)C3001H HI21 120x130 14x16 14x16 12x13 524288


★開発環境を整える★


今回のゲームを作るのに必要な開発ツールは次の4つです。全て無償で入手できます。


Java 2 SDK, Standard Edition (JDK) Version 1.3(1.3.1推奨)
パソコン上で動くJavaアプリを作るための開発キットです。サン・マイクロシステムズのサイトで入手できます。次に説明する「J2ME Wireless Toolkit」を実行するのに必要なので、それより先にインストールします。インストーラの指示に従ってインストールしてください。JDK1.4でない点に注意してください。


J2ME Wireless Toolkit 1.0(1.0.4推奨)
MIDP1.0仕様のJavaアプリを作るための開発キットです。サン・マイクロシステムズのサイトで入手できます。インストーラの指示に従ってインストールします。J2ME Wireless Toolkit 2.0でない点に注意してください。MIDPのAPIリファレンスはdocsディレクトリの中(C:\J2mewtk\docs\api)にあります。

KJX作成ツール〜EZアプリ(Java)Phase3対応版〜
EZアプリ(Java)を作るための開発キットです。KDDIのサイトで入手できます。インストーラの指示に従ってインストールしてください。同サイトには、 などの開発に役立つドキュメントもあるので、いっしょに入手してください。


★HTTP通信によるテキストの読み込み★


EZアプリ(Java)の開発の基礎に関しては、前々回(JavaPress Vol.33)解説したので省略します。それでは、HTTP通信によるテキストの読み込みを行うプログラム「HttpEx」を作ります。ソフトキーの「読込」を押すと、ネット上にあるテキストファイルを読み込み、キャンバスに表示します。


今回のプログラムは、以下の2つのクラスで構成されています。


テキストの準備
次のテキストファイルを作り、ネット上に公開して下します。文字エンコードはShift-JISにして下さい。Webブラウザからテキストファイルにアクセスすることにより、無事に公開できているかどうかを確認することができます。

・テキスト
test.txt
これはテストだよ。

au端末とWebサーバ間で送受信可能なデータは次の通りです。Webサーバで扱えるように、.htaccess等でMIMEタイプを設定する必要があります。詳しくは、Webサーバ管理者の人に聞いてください。
au端末とWebサーバ間で送受信可能なデータ
拡張子 MIMEタイプ
.bmp image/bmp
.png image/png
.jpg image/jpeg
.gif image/gif
.qcp audio/vnd.qcelp
.pmd application/x-pmd
.mmf application/x-smaf
.amc application/x-mpeg
テキスト text/plain
バイナリ application/octet-stream


HttpExクラス
HttpExクラスは、プログラムの本体となるクラスです。
HttpEx.java
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

//HTTP通信によるテキスト読み込み(本体)
public class HttpEx extends MIDlet {

    //コンストラクタ
    public HttpEx() {
        Display.getDisplay(this).setCurrent(new HttpCanvas());
    }

    //アプリの開始
    public void startApp() {
    }

    //アプリの一時停止
    public void pauseApp() {
    }

    //アプリの終了
    public void destroyApp(boolean unconditional) {
    }
}


HttpCanvasクラス
HttpCanvasクラスは、キャンバスとなるクラスです。

HttpCanvas.java
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import java.io.*;

//HTTP通信によるテキスト読み込み(キャンバス)
class HttpCanvas extends Canvas
    implements CommandListener {
    private Command soft1;  //ソフトキー1
    private String  info="";//情報

    //コンストラクタ
    HttpCanvas() {
        //ソフトキー
        soft1=new Command("読込",Command.SCREEN,1);
        addCommand(soft1);
        setCommandListener(this);
    }

    //HTTP通信によるテキスト読み込み
    private String readText(String url) {
        byte[] data;
        HttpConnection c =null;
        InputStream    in=null;
        try {
            //ネットと接続する
            c =(HttpConnection)Connector.open(url);
            in=c.openInputStream();

            //ネットからデータを読み込む
            data=new byte[(int)c.getLength()];
            for (int i=0;i<data.length;i++) data[i]=(byte)in.read();

            //ネットと切断する
            in.close();
            c.close();

            //文字列の生成
            return new String(data);
        } catch (Exception e) {
            //例外処理
            try {
                if (in!=null) in.close();
                if (c !=null) c.close();
            } catch (Exception e2) {
            }
            return null;
        }
    }

    //描画
    public void paint(Graphics g) {
        g.setColor(255,255,255);
        g.fillRect(0,0,getWidth(),getHeight());
        g.setColor(0,0,0);
        g.drawString(info,0,0,g.LEFT|g.TOP);
    }

    //コマンドイベント
    public void commandAction(Command c,Displayable s) {
        if (c==soft1) {
            //データ置き場のURLの指定
            info=readText("http://サーバ/test.txt");
            if (info==null) info="エラー";
            repaint();
        }
    }
}


HTTPとHTTPS
au端末で使える通信プロトコルは「HTTP」と「HTTPS」(HTTPにデータ暗号化機能を付けたもの)です。「HTTP」は、「http://」で始まるURLのリソースにアクセスするためのプロトコルです。HTTPで通信するには、GETとPOSTの2種類の方法があります。単純にテキストファイルや画像ファイルをダウンロードするにはGETを使い、端末からサーバに情報をアップロードするにはPOSTを使います。データの長さが制限されていますが、GETでもサーバに情報をアップロードすることができます。

また、GETのレスポンスとしてダウンロードできるデータサイズには、次のような制限があります。値は実機で測定したおおよその値です。仕様では9000バイト以上は動作保証外となっています。
HTTP通信でダウンロード可能なデータサイズ
GETのレスポンス
W11H・W11K 97280バイト程度
A5403CA 46080バイト程度
Phase3以前 9000バイト程度
さらに、WIN端末には過度の通信によるネットワークの負荷を避けるためか、次のような制限が設けられています。

ネットと接続する
ネットに接続するには、javax.microedition.ioパッケージに含まれるConnectorクラスのopen()メソッドを使います。
static Connection open(String url)
url :接続先URL
戻り値:Connectorクラスのオブジェクト
static Connection open(String url,int mode)
url :接続先URL
mode :アクセスモード
戻り値:Connectorクラスのオブジェクト
static Connection open(String url,int mode,boolean timeouts)
url :接続先URL
mode :アクセスモード
timeout:タイムウアウト例外が必要か
戻り値 :Connectorクラスのオブジェクト
urlには接続先URLを指定します。modeにはGETを使う時はConnector.READ、POSTを使う時にはConnector.READ_WRITEを指定します。timeoutsにはタイムアウト例外が必要かどうかを指定します。

戻り値のConnectorクラスのオブジェクトをHttpConnectionクラスにキャストします。入力ストリームを取得するには、HttpConnectionクラスのopenInputStream()メソッドを使います。
HttpCanvas.javaの一部
c =(HttpConnection)Connector.open(url);
in=c.openInputStream();


ネットからデータを読み込む
読み込むデータのバイト数を取得するには、HttpConnectionクラスのgetLength()メソッドを使います。
long getLength()
戻り値:データのバイト数
読み込むデータのバイト数を取得したら、HttpConnectionクラスのread()メソッドでバイトデータを読み込みます。
HttpCanvas.javaの一部
data=new byte[(int)c.getLength()];
for (int i=0;i<data.length;i++) data[i]=(byte)in.read();


ネットと切断する
ネットと切断するには、InputStreamクラスとHttpConnectionクラスのclose()メソッドを使います。
HttpCanvas.javaの一部
in.close();
c.close();


例外処理
例外が発生した時も、切断するのを忘れないでください。アプリが終了するまで、ネットと接続することができなくなります。

HttpCanvas.javaの一部
try {
  if (in!=null) in.close();
  if (c !=null) c.close();
} catch (Exception e2) {
}


文字列の生成
バイトデータをString型の文字列に変換するには、Stringクラスのコンストラクタを使います。今回は、ネットから受信したバイトデータをString型の文字列に変換しています。
String(byte[] bytes)
bytes:文字列に変換されるバイト

HttpCanvas.javaの一部
return new String(data);


データ置き場のURLの指定
サンプルプログラムで指定しているURL「http://サーバ/test.txt」は実際には存在しません。自分でテキストをアップロードして、そのURLを指定して下さい。
HttpCanvas.javaの一部
info=readText("http://サーバ/test.txt");


属性の設定
EZアプリ(Java)でHTTP通信を行うには、「MIDlet-X-AllowURL-<n>」属性を設定する必要があります。「MIDlet-X-AllowURL-<n>」属性は、HTTP通信時に接続を許可するホストのURLを最大3つまで指定できます。

メニュー「project→edit env...」でEnvironmentEditダイアログを開き、「Other keyword」ボタンを押してEnvironmentEditSubダイアログを開きます。ここで、自分でテキストをアップロードしたホストのURLを入力してください。各URLは、"http://"または"https://"ではじめて"/"で終わり、63バイト以下で記述します。「add」ボタンでリストに追加され、「OK」ボタンで反映されます。
  


★HTTP通信による画像の読み込み★


次に、HTTP通信による画像の読み込みを行うプログラム「HttpImageEx」を作ります。ソフトキー「読込」を押すと、ネット上にあるイメージファイルを読み込み、キャンバスに表示します。このプログラムは、以下の2つのクラスで構成されています。

今回のプログラムは、以下の2つのクラスで構成されています。


画像ファイルの準備
次の画像ファイルを作り、ネット上に公開して下さい。Webブラウザから画像ファイルにアクセスすることにより、無事に公開できているかどうかを確認することができます。

・画像ファイル
test.png
240x160



HttpImageExクラス
HttpImageExクラスは、プログラムの本体となるクラスです。

HttpImageEx.java
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

//HTTPによるイメージ取得(本体)
public class HttpImageEx extends MIDlet {

    //コンストラクタ
    public HttpImageEx() {
        Display.getDisplay(this).setCurrent(new HttpImageCanvas());
    }

    //アプリの開始
    public void startApp() {
    }

    //アプリの一時停止
    public void pauseApp() {
    }

    //アプリの終了
    public void destroyApp(boolean unconditional) {
    }
}


HttpImageCanvasクラス
HttpImageCanvasクラスは、キャンバスとなるクラスです。

HttpImageCanvas.java
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import java.io.*;

//HTTP通信による画像読み込み(キャンバス)
class HttpImageCanvas extends Canvas
    implements CommandListener {
    private Command soft1; //ソフトキー1
    private String info="";//情報
    private Image  image;  //イメージ

    //コンストラクタ
    HttpImageCanvas() {
        //ソフトキー
        soft1=new Command("読込",Command.SCREEN,1);
        addCommand(soft1);
        setCommandListener(this);
    }

    //HTTP通信による画像読み込み
    private Image readImage(String url) {
        byte[] data;
        HttpConnection c =null;
        InputStream    in=null;
        try {
            //ネットと接続する
            c =(HttpConnection)Connector.open(url);
            in=c.openInputStream();

            //ネットからデータを読み込む
            data=new byte[(int)c.getLength()];
            for (int i=0;i<data.length;i++) data[i]=(byte)in.read();

            //ネットと切断する
            in.close();
            c.close();

            //イメージの生成
            return Image.createImage(data,0,data.length);
        } catch (Exception e) {
            //例外処理
            try {
                if (in!=null) in.close();
                if (c !=null) c.close();
            } catch (Exception e2) {
            }
            return null;
        }
    }

    //描画
    public void paint(Graphics g) {
        g.setColor(255,255,255);
        g.fillRect(0,0,getWidth(),getHeight());
        if (image!=null) {
            g.drawImage(image,0,0,g.LEFT|g.TOP);
        } else {
            g.setColor(0,0,0);
            g.drawString(info,0,0,g.LEFT|g.TOP);
        }
    }

    //コマンドイベント
    public void commandAction(Command c,Displayable s) {
        if (c==soft1) {
            //データ置き場のURLの指定
            image=readImage("http://サーバ/test.png");
            if (image==null) info="エラー";
            repaint();
        }
    }
}


イメージの生成
ネット上から画像ファイルのバイトデータを読み込む方法は、テキストを読み込む方法と基本的に同じです。今回は、ネットから受信したバイトデータをImageクラスのcreateImage()メソッドによってイメージに変換しています。
Image createImage(byte[] data,int offset,int size)
data :バイトデータ
offset:バイトデータの先頭インデックス
size :バイトデータのサイズ
HttpImageCanvas.javaの一部
Image.createImage(data,0,data.length);


データ置き場のURLの指定
サンプルプログラムで指定しているURL「http://サーバ/test.png」は実際には存在しません。自分で画像ファイルをアップロードして、そのURLを指定して下さい。
HttpImageCanvas.javaの一部
info=readText("http://サーバ/test.png");

また、HttpExと同様に、「MIDlet-X-AllowURL-<n>」属性にHTTP通信を許可するホストのURLを指定するのも忘れないで下さい。
MIDlet-X-AllowURL-1: http://サーバ/


★アドベンチャーゲームを作る★


それでは、本題のアドベンチャーゲーム「そらみミラクル」を作ります。会話の選択によりシナリオを進めていくゲームです。選択キーでシナリオが進みます。たまに選択肢が現れるので、上下キーと選択キーで選んでください。それに応じて、シナリオが変化していきます。
  

今回のプログラムは、次の2つのクラスで構成されています。

画像ファイルの用意
今回、JARファイルに含める画像ファイルは1つです。resディレクトリに置いてください。

・ウィンドウ
window.png
230x80ドット



それとは別に、シナリオで使用する画像ファイルをネット上に公開して下さい。量と内容はシナリオによって変わります。

・タイトル
title.png
240x240



・そらみ-公園
0.png
240x160ドット


・そらみ-港
1.png
240x160ドット


・へにへに-繁華街
2.png
240x160ドット


・そらみ-繁華街
3.png
240x160ドット


・へにへに-ぐべっ!!
4.png
240x160ドット


・完
5.png
240x160ドット



テキストファイルの準備
シナリオで使用するテキストファイルをネット上に公開します文字エンコードは「Shift-JIS」にしてください。テキストファイルの書式は次のようになります。
テキストファイルの書式
画像ファイル名
1行目<タブ>2行目<タブ>3行目
:
1行目<タブ>2行目<タブ>3行目
選択肢1<タブ>選択肢2<タブ>選択肢3
遷移先テキスト名1<タブ>遷移先テキスト名2<タブ>遷移先テキスト名3

改行とタブで区切っています。タブはそのままだと分かり辛いので、紙面では<タブ>と記述しています。実際に作る時はタブ文字を入力してください。

1行目には画面に表示する画像のファイル名を指定します。
2行目から最終行の3つ前の行まではトークを記述します。<タブ>で改行位置を指定しています。1トークで全角9文字(半角18文字)を最大3行まで表示できます。
最終行の1つ前の行は選択肢を記述します。タブ区切りで最大3つまでです。
最終行には選択肢を選択した時に次に実行するテキストファイル名を記述します。選択肢1が選択された時は、遷移先テキスト1が実行されます。

・title.txt
title


0

・0.txt
0
そらみ<タブ>「うぅ、へにへにと<タブ>はぐれちゃった…」
そらみ<タブ>「このあたりにいる<タブ>とは思うんだけど」
そらみ<タブ>「いったいどこから<タブ>探そうかな〜?」
≫港のほう<タブ>≫繁華街のほう
1<タブ>2

・1.txt
1
そらみ<タブ>「うーん、ここには<タブ>いないみたい…」
そらみ<タブ>「他のところも探し<タブ>てみようかな?」
≫繁華街のほう
2

・2.txt
2
そらみ<タブ>「あっ、へにへに見<タブ>つけた〜!」

3

・3.txt
3
そらみ<タブ>「なんでいなくなっ<タブ>ちゃうのよぉ〜!」

4

・4.txt
2
へにへに<タブ>「だってそらみちゃ<タブ>ん、トイレ長すぎ…

5

・5.txt
4
へにへに<タブ>「ぐべっ!!」

6

・6.txt
3
そらみ<タブ>「えーと、地獄へ落<タブ>ちやがれ」

7

・7.txt
5


title


AdventureGameクラス
AdventureGameクラスは、プログラムの本体となるクラスです。

AdventureGame.java
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

//アドベンチャーゲーム(本体)
public class AdventureGame extends MIDlet {
    //コンストラクタ
    public AdventureGame() {
        AdventureCanvas canvas=new AdventureCanvas();
        Display.getDisplay(this).setCurrent(canvas);
        (new Thread(canvas)).start();
    }

    //アプリの開始
    public void startApp() {
    }

    //アプリの一時停止
    public void pauseApp() {
    }

    //アプリの終了
    public void destroyApp(boolean unconditional) {
    }
}


AdventureCanvasクラス
AdventureCanvasクラスは、キャンバスとなるクラスです。

AdventureGame.java
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import java.io.*;

//アドベンチャーゲーム(キャンバス)
class AdventureCanvas extends Canvas
    implements Runnable {
    private final static String URL=//データ置き場のURL
        "http://サーバ/data/";
    private final static int A_LT=  //配置左上
        Graphics.LEFT|Graphics.TOP;
    private static int      event;  //イベント
    private static Image    offImg; //オフイメージ
    private static Graphics offGra; //オフグラフィックス

    //実行
    public void run() {
        int i,j;

        //システム
        String scene    =null;   //シーン
        String init     ="title";//初期化
        Image  windowImg=null;   //ウィンドウイメージ
        Image  cashImg  =null;   //キャッシュイメージ

        //トーク
        String[][] talk   =null;//トーク
        int        talkPos=0;   //トーク位置

        //選択肢
        String[] select    =null;//選択肢
        String[] selectJump=null;//選択肢遷移先
        int      selectPos =0;   //選択肢位置

        //ダブルバッファリング
        offImg=Image.createImage(240,240);
        offGra=offImg.getGraphics();
        offGra.setFont(Font.getFont(Font.FACE_MONOSPACE,
            Font.STYLE_PLAIN,Font.SIZE_LARGE));
        offGra.setColor(255,255,255);
        offGra.fillRect(0,0,240,240);
        offGra.setColor(0,0,0);
        offGra.drawString("Now Loading...",10,180,A_LT);
        try {
            //イメージ読み込み
            windowImg=Image.createImage("/window.png");
            cashImg  =null;
            while (true) {
                //シーンの初期化
                if (init!=null) {
                    scene=init;
                    init =null;

                    //メモリの解放
                    cashImg   =null;
                    talk      =null;
                    select    =null;
                    selectJump=null;
                    System.gc();

                    //シナリオ
                    String[] str=parseString(
                        new String(readBinarry(URL+scene+".txt")),'\n');
                    byte[] data=readBinarry(URL+str[0]+".png");
                    cashImg=Image.createImage(data,0,data.length);
                    talk=new String[str.length-3][];
                    for (i=0;i<talk.length;i++) {
                        talk[i]=parseString(str[i+1],'\t');
                    }
                    talkPos=0;
                    select=parseString(str[str.length-2],'\t');
                    selectJump=parseString(str[str.length-1],'\t');
                    selectPos=0;
                }

                //イメージの描画
                offGra.setColor(0,0,0);
                offGra.fillRect(0,0,240,240);
                i=7;
                if (scene.equals("title")) i=0;
                offGra.drawImage(cashImg,0,i,A_LT);
                if (talkPos==999 || !(talk[talkPos].length==1 && talk[talkPos][0].length()==0)) {
                    offGra.drawImage(windowImg,5,155,A_LT);
                }

                //トークの描画
                if (talkPos!=999) {
                    offGra.setColor(255,255,255);
                    offGra.drawString(talk[talkPos][0],12,157,A_LT);
                    if (talk[talkPos].length>1) offGra.drawString(talk[talkPos][1],12,182,A_LT);
                    if (talk[talkPos].length>2) offGra.drawString(talk[talkPos][2],12,207,A_LT);
                }

                //選択肢の描画
                else {
                   offGra.setColor(255,255,255);
                   offGra.drawString("カニカニ、どこカニ?",12,157,A_LT);
                   for (i=0;i<select.length;i++) {
                        if (i==selectPos) {
                            offGra.setColor(255,50,50);
                            offGra.fillRect(12,182+25*i,select[i].getBytes().length*12,24);
                        }
                        offGra.setColor(255,255,255);
                        offGra.drawString(select[i],12,182+25*i,A_LT);
                    }
                }

                //画面に反映
                repaint();serviceRepaints();

                //トークのイベント
                if (talkPos!=999) {
                    if (event==FIRE) {
                        if (talkPos+1<talk.length) {
                            talkPos+=1;
                        } else if (select.length==1 && select[0].length()==0) {
                            init=selectJump[selectPos];
                        } else {
                            talkPos=999;
                        }
                    }
                    event=999;
                }

                //選択肢のイベント
                else {
                    if (event==UP   && selectPos!=0)              selectPos--;
                    if (event==DOWN && selectPos<select.length-1) selectPos++;
                    if (event==FIRE) init=selectJump[selectPos];
                    event=999;
                }

                //スリープ
                Thread.sleep(100);
            }
        } catch (Exception e) {
            //通信失敗
            offGra.setColor(255,255,255);
            offGra.fillRect(0,0,240,240);
            offGra.setColor(0,0,0);
            offGra.drawString("通信失敗しました。",10,10,A_LT);
            repaint();serviceRepaints();
        }
    }

    //キーイベント
    public void keyPressed(int keyCode) {
        if (keyCode!=0) {
            int gameAction=getGameAction(keyCode);
            switch(gameAction) {
            case UP:
            case DOWN:
            case LEFT:
            case RIGHT:
            case FIRE:
                event=gameAction;break;
            }
        }
    }

    //描画
    public void paint(Graphics g) {
        if (offImg!=null) {
            g.drawImage(offImg,(getWidth()-240)/2,(getHeight()-240)/2,A_LT);
        }
    }

    //HTTP通信によるバイナリ読み込み
    private byte[] readBinarry(String url) throws Exception {
        byte[] data;
        HttpConnection c =null;
        InputStream    in=null;
        while (true) {
            try {
                //ネットと接続する
                c =(HttpConnection)Connector.open(url);
                in=c.openInputStream();

                //ネットからデータを読み込む
                data=new byte[(int)c.getLength()];
                for (int i=0;i<data.length;i++) data[i]=(byte)in.read();

                //ネットと切断する
                in.close();
                c.close();

                //バイナリのまま戻す
                return data;
            } catch (Exception e) {
                //例外処理
                try {
                    if (in!=null) in.close();
                    if (c !=null) c.close();
                    Thread.sleep(3000);
                } catch (Exception e2) {
                }
            }
        }
    }

    //文字列を任意の文字で分割
    private String[] parseString(String str,char sep) {
        int i,j,size;
        String[] result;

        //最後尾に分割文字
        if (str.equals("")||str.charAt(str.length()-1)!=sep) str+=sep;

        //\rを削除
        if (str.indexOf('\r')>=0) {
            StringBuffer sb=new StringBuffer();
            for (i=0;i<str.length();i++) {
                if (str.charAt(i)!='\r') sb.append(str.charAt(i));
            }
            str=sb.toString();
        }

        //サイズを得る
        size=0;
        i=str.indexOf(sep);
        while (i>=0) {
            size++;
            i=str.indexOf(sep,i+1);
        }

        //分割する
        result=new String[size];
        size=0;
        j=0;
        i=str.indexOf(sep);
        while (i>=0) {
            result[size++]=str.substring(j,i);
            j=i+1;
            i=str.indexOf(sep,j);
        }
        return result;
    }
}


データ置き場のURLの指定
サンプルプログラムで指定しているURL「http://サーバ/data/」は実際には存在しません。自分でデータをアップロードして、そのURLを指定して下さい。
AdventureCanvas.javaの一部
private final static String URL=//データ置き場のURL
    "http://サーバ/data/";

また、HttpExと同様に、「MIDlet-X-AllowURL-<n>」属性にHTTP通信を許可するホストのURLを指定するのも忘れないで下さい。
MIDlet-X-AllowURL-1: http://サーバ/


ダブルバッファリング
ダブルバッファリングとは、画面の描画部分と同じサイズのイメージ(オフイメージと呼びます)を用意し、必要なものをこのイメージに描画してから、実際の画面にまとめて描画させるという処理で、画面がちらつくことを防ぎます。

イメージを生成するには、ImageクラスのcreateImage()メソッドを使います。また、イメージに描画するためのGraphicsオブジェクトを取得するには、getGraphics()メソッドを使います。
AdventureCanvas.javaの一部
private static Image    offImg; //オフイメージ
private static Graphics offGra; //オフグラフィックス
AdventureCanvas.javaの一部
offImg=Image.createImage(240,240);
offGra=offImg.getGraphics();


オフイメージや実際の画面に描画する処理は、paint()メソッドで行っています。アプリ起動時など再描画が必要な時や、Canvasクラスのrepaint()メソッドを呼んだ時、実機からこのメソッドが呼ばれます。
AdventureCanvas.javaの一部
public void paint(Graphics g) {
    if (offImg!=null) {
        g.drawImage(offImg,(getWidth()-240)/2,(getHeight()-240)/2,A_LT);
    }
}


ただし、repaint()メソッドだけではpaint()メソッドの描画が完了する前に、処理が戻ってきてしまうため、うまく同期がとれなくなってしまうことがあります。paint()メソッドの描画が完了するまで待ちたい時はserviceRepaints()メソッドも呼んでください。
AdventureCanvas.javaの一部
repaint();serviceRepaints();


シーンの初期化
このゲームでは、1つのテキストファイルに記述されているシナリオを、1つのシーンとしています。ゲーム開始時はタイトル画面を表示するシーン「title」からはじまります。現在のシーンはscene変数で保持しています。次に遷移するシーンはinit変数で保持しています。遷移しない時はinit変数はnullを保持しています。

init変数がnullでない時はシーンの初期化を行います。

まずはじめに、メモリを解放するため、各種テキストとイメージ変数にnullを指定し、System.gc()メソッドでガーベージコレクションを行います。
AdventureCanvas.javaの一部
cashImg   =null;
talk      =null;
select    =null;
selectJump=null;
System.gc();

次にシナリオを記述しているテキストファイルを読み込みます。ファイル名は「シーン名+.txt」です。
AdventureCanvas.javaの一部
String[] str=parseString(
    new String(readBinarry(URL+scene+".txt")),'\n');

テキストデータの1行目に表示する画像ファイル名が記述されているので、その画像ファイルを読み込みます。
AdventureCanvas.javaの一部
byte[] data=readBinarry(URL+str[0]+".png");
cashImg=Image.createImage(data,0,data.length);

2行目〜「最終-2」行目に会話データが記述されているので、talk配列に代入します。talkPos変数は現在どのトークを表示しているかを保持します。talkPos変数が999の時はトークを表示しないで選択肢を表示します。
AdventureCanvas.javaの一部
talk=new String[str.length-3][];
for (i=0;i<talk.length;i++) {
    talk[i]=parseString(str[i+1],'\t');
}
talkPos=0;

「最終-1」行目に選択肢、最終行に選択肢を選んだ時の遷移先が記述されているので、select配列とselectJump配列に代入します。selectPos変数は現在どの選択肢を選んでいるかを保持します。
AdventureCanvas.javaの一部
select=parseString(str[str.length-2],'\t');
selectJump=parseString(str[str.length-1],'\t');
selectPos=0;


キーイベント
event変数には現在押されているキーのゲームアクションを保持します。キーが押されていない時は999を代入しています。999というのはキー定数に割り当てられていないだろうと予測して、勝手に使用している値です。今回はkeyPressed()メソッドで受け取ったゲームアクションをevent変数に代入するので、次のように記述します。
AdventureCanvas.javaの一部
public void keyPressed(int keyCode) {
    if (keyCode!=0) {
        int gameAction=getGameAction(keyCode);
        switch(gameAction) {
        case UP:
        case DOWN:
        case LEFT:
        case RIGHT:
        case FIRE:
            event=gameAction;break;
        }
    }
}


HTTP通信の回数制限の回避
WIN端末には「HTTP通信の回数は1分間につき8回まで 」という制限があります。この制限を回避するために、このゲームでは「通信失敗したら3秒後に再度通信」するという処理を行っています。
AdventureCanvas.javaの一部
private byte[] readBinarry(String url) throws Exception {
    byte[] data;
    HttpConnection c =null;
    InputStream    in=null;
    while (true) {
        try {
            //ネットと接続する
            c =(HttpConnection)Connector.open(url);
            in=c.openInputStream();

            //ネットからデータを読み込む
            data=new byte[(int)c.getLength()];
            for (int i=0;i<data.length;i++) data[i]=(byte)in.read();

            //ネットと切断する
            in.close();
            c.close();

            //バイナリのまま戻す
            return data;
        } catch (Exception e) {
            //例外処理
            try {
                if (in!=null) in.close();
                if (c !=null) c.close();
                Thread.sleep(3000);
            } catch (Exception e2) {
            }
        }
    }
}

ただし、これだけでは通信回数の多い時、ゲームのテンポが悪くなってしまうので、「複数のファイルを1つのバイナリファイルにまとめて一括ダウンロード」などの工夫もした方が良いでしょう。


文字列を任意の文字で分割
今回のプログラムでは、文字列を任意の文字で分割する「parseString()メソッド」を作りました。
static String[] parseString(String str,char sep)
str:分割前の文字列
sep:分割文字
戻り値:分割後の文字列


はじめに、分割前の文字列の最後尾に分割文字あるかどうか調べ、ない時は分割文字を追加します。その後、「\r」(キャリッジリターン)が存在する時は消去し、分割文字の数を調べ、分割後の文字列の配列を作ります。最後にその配列に、分割した文字列をセットしてから戻します。
AdventureCanvas.javaの一部
private String[] parseString(String str,char sep) {
    int i,j,size;
    String[] result;

    //最後尾に分割文字
    if (str.equals("")||str.charAt(str.length()-1)!=sep) str+=sep;

    //\rを削除
    if (str.indexOf('\r')>=0) {
        StringBuffer sb=new StringBuffer();
        for (i=0;i<str.length();i++) {
            if (str.charAt(i)!='\r') sb.append(str.charAt(i));
        }
        str=sb.toString();
    }

    //サイズを得る
    size=0;
    i=str.indexOf(sep);
    while (i>=0) {
        size++;
        i=str.indexOf(sep,i+1);
    }

    //分割する
    result=new String[size];
    size=0;
    j=0;
    i=str.indexOf(sep);
    while (i>=0) {
        result[size++]=str.substring(j,i);
        j=i+1;
        i=str.indexOf(sep,j);
    }
    return result;
}


★おわりに★


次回は、ドラクエやFFも遊べる本気のFOMA「900iシリーズ」で動くゲームを作る予定です。お楽しみに。



−戻る−


(C)Npaka/Sehira, 2003-2004