Javaアプレットを蘇らせる

Javaアプレットは絶滅したのか? …いいえ、今でも生きています。 Java仕様から完全に削除されても、Webブラウザがappletタグに対応しなくなっても、大丈夫。 とはいえ実際にやってみるといろいろ試行錯誤が必要な部分があったので、それも含めて自分用メモ。

CheerpJ

たしかに昔ながらのJavaアプレット実行環境はもうないけど、Javaのバイトコードを実行する仮想マシンすなわちJVM相当のものは作ろうと思えば作れるわけで、まさにそういうもののひとつがCheerpJというやつ。後述の問題点はあるけど基本的に昔のままのJavaアプレットのjarファイルとかがそのままで(概ね)動いてしまう。詳しいことはドキュメントを見てもらうとして、めちゃくちゃ雑な理解としては、モダンなWebブラウザ上で動くJVMの一種と考えてもいいだろう。

使い方は大きく分けて2種類ある。一つは、ブラウザ上で動くプラグインとしての利用。この場合はサイト側(ホスティング側)は昔のままで良くて、その代わり各ユーザがアドインをインストールする必要がある。もう一つはサイト側で細工して裏でCheerpJを動作させるもの。この場合は各ユーザは特に何の準備も必要なく普通にページを開くだけでいい。この記事ではこの後者の方法について説明する。

具体的には、アプレットのあるページにこういう記述を追加すればいい。

<script src="https://cjrtnc.leaningtech.com/4.3/loader.js"></script>
<applet codebase="/a" archive="a.jar" code="Main" width="100" height="100"></applet>
<script>cheerpjInit();</script>

appletタグの部分はもともとの記述のままでいい。(これはただの例なのでこの通りに書く必要はない。) 1行目はできればheadに入れたほうが良いらしいけどbodyの中に書いても動く。

これで、まるで昔のようにブラウザがappletタグを実行しているような動きになる。 もっとも最初は裏で準備するためにけっこう時間がかかるので、アプレットの画面が表示されるまでしばらく待つ必要がある。

いつまで経ってもアプレットの画面が表れない場合は、おそらく何らかの例外で止まっている。ブラウザのコンソールを開くとエラーメッセージが出ているはず。(これを利用してデバッグ用にメッセージを表示したりもできる。ここは昔のJavaアプレット実行環境とだいたい同じと考えて良い。)

JDK8

既存のアプレットのjarをそのまま動かすだけじゃなくてちょっとでも変更するとなったらソースからリコンパイルする必要がある。でもアプレットは現在のJava仕様では完全に廃止されているので、最新のJDKではコンパイルできない。アプレットの開発に使える最後のJDKはJDK8で、これは今でも入手することができる。本家のOracleから入手しようとするとやけに細かい個人情報を求められたりして厄介なので、(そういうのを気にしないとか既に登録済みとかいう人はそれでいいけど私の場合はそうじゃないので)こっちのほうがおすすめ。たぶん内容に差はないのでどちらでもいいはず。(バージョンは日々更新されているようです。JDKのインストールや環境設定は普通にJDKの話なので、ここでは細かく説明しません。)

音が出ない

この方法でJavaアプレットを動かすうえでの問題点というのがこれ。詳しいことはわからないけどいくつかの要因によりそのままでは音が出ないらしい。Javaアプレットのゲームとして音が出ないのは結構な大問題だ。

原因の一つとして考えられるのが、音声ファイルの形式。Javaアプレットでは.au形式のファイルを用意しておき再生するのが最も手軽な方法だった。でも今のブラウザではそもそも.au形式がサポートされてなかったりするらしい。昔はとりあえず.auにしておけばよかったけど今では.wav形式のほうが無難。効果音なら.wavでもたいしたサイズにはならないし。(というか今どきそんなことは気にする必要はない。)

CheerpJの制限というのもあるらしく、これはバージョンによっても違ったりするし細かいことはよくわからない。またそれとは別にjarやアプレット自体の仕様についてもいろいろあるらしく、さらにややこしい。既に廃止されている仕様なので何が最新とかもよくわからないし。

とりあえず、すべてのファイルを適当にjarの中に入れておけばいい、というわけではないらしい。いろいろ試してみたけど、どうやってもjarの中のファイルを読み取ってくれなかった。結局それはあきらめて、jarの中ではなく外にwavファイルを置くという方法をとることにした。管理が若干面倒にはなるけどそれほど困るわけでもないのでこれでヨシ!としておく。

ただ、全部のソースが同じフォルダではなく音を出す処理のソースがサブフォルダにある場合は注意が必要だ。たとえば、

a.jar:
Main.class
lib/Sound.class

のような構成で、音をロードして再生するSoundクラスがlibフォルダにある場合、 wavファイルはjarと同じフォルダではなくlibフォルダの中に置く必要がある。

.../a.jar
.../lib/sound.wav

このあたりの事情は次のこととも関係していそう。

AudioClip

音が出ない問題への対処としていろいろやったことの一つとして、java.applet.AudioClip を使うのをやめて javax.sound.sampled.Clip に変更した。 本当にそうする必要があったのかはわからないけど、音量の調節ができるようになったりもしたので結果的にこれでよかったと思っている。

最適なのかはわからないけどとりあえず動いたコードのサンプル。例外処理とかは省略している。

旧処理:
import java.applet.AudioClip;
AudioClip c = getAudioClip(getCodeBase(),"sound.au");
c.play();

新処理:
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.FloatControl;
Clip c = AudioSystem.getClip();
c.open(AudioSystem.getAudioInputStream(getClass().getResource("sound.wav")));
FloatControl g = (FloatControl)c.getControl(FloatControl.Type.MASTER_GAIN);
g.setValue(volume);
c.setFramePosition(0);
c.start();

volumeはdBで表したボリューム指定値。0が最大で、負の値を指定すると小さくなる。なので0%~100%みたいな指定はしにくいけど実用上はむしろこの方が良い気がする。 本当に音を一切出したくなければ再生しなければ良い。

hugo

ここから先は主に、静的サイトジェネレータhugoを使う場合の余談。

具体的にはjarとwavをこう配置しておくと良い。(これは上記の例の場合。wavをサブフォルダ入れてるのはソースがそういう構成になっているからで、そうじゃなければjarと同じフォルダで良い。)

static/a/a.jar
static/a/lib/sound.wav

本体ページの記述はこう。

game.md:
<script src="https://cjrtnc.leaningtech.com/4.3/loader.js"></script>
<applet codebase="/a" archive="a.jar?v={{ $build.build }}" code="Main" width="100" height="100"></applet>
<script>cheerpjInit();</script>

ちなみに私の場合はこれを直接mdに書くとうまくいかなかったので、とりあえずこういうショートコードで囲むようにした。このあたりはhugoのノウハウだと思うのでここでは深入りしない。(というか私もよくわかってない。)

layouts/shortcodes/raw.html:
{{ .Inner }}

jarの後ろにくっついてるのはキャッシュを無効化するためのおまじない。これでビルドするたびにURLが変わる。開発中はキャッシュのせいでjarが更新されなくて困ったりするので、こうしておくと予防になる。URLが変わるのはあくまでもhugoでのビルドごとなので、本番用のビルドでもこのままで支障ない。 (というかむしろそうしておいたほうが公開後に更新したときに起きうるキャッシュ問題の予防にもなると思う。)

assets/build.toml:
build = {{ now.Unix }}

ただ、それでも親切なブラウザが勝手にキャッシュしてくれたりすることもあるので油断はできない。ゲーム本体側でバージョンを表示するようにしておいて、それが更新されてなかったらブラウザのキャッシュを削除するようにすると良い。この点についてはhugoに限ったことでもないはず。