JavaScriptによる弾幕STG実装

Canvas要素やらFlashとの連携やらを使って無理やり弾幕STG実装。2009年新歓展示で展示したもの。

とりあえずPLAY

対応ブラウザはGoogleChrome、Safari、Firefox、Opera。IEでも一応動くがfpsが悲惨なことになるのでお勧めしない。ロードに結構時間がかかると思われます。要FlashPlayer(ver 8以降)

以上のことを了承の上、以下のリンクからどうぞ。

操作方法は以下の通り。ぶっちゃけ東方。

方向キー自機移動。Shiftキーを押しながらだと低速移動
Zキー札発射。要するに弾を撃つ。
Xキー霊撃発動。要するにボム。

画面上部に行くとアイテムが自動回収される。要は東方風神録以降の仕様。

アイテムについては以下の通り。

点アイテム

点。回収すると10点。なんとなくスコア稼ぎたい人用。

パワーアイテム

パワー。一定量回収すると発射する札が増える。最大32。

ステージは1つだけ。なのでボムやライフのエクステンドはないです。

要素技術

このゲームで使ってるJavaScript絡みの技術に関するメモ。需要なんて知らない。

Canvas要素

HTMLの次期仕様、HTML5で盛り込まれる予定の要素。JavaScript経由で図形や画像の描画が可能。Firefox、Safari、Opera、GoogleChromeで実装されている。IEは未実装だが、explorercanvasを用いることでIEでもある程度Canvas要素で提供される機能を使うことができる。今回のゲームもおおよそIEで機能する範囲内に収まっているはず。Canvas要素に関する詳しい情報はHTML5.JPを参照されたし。

ゲーム開発ではまったところとかメモ。

globalAlphaとdrawImage()

globalAlphaは描画するものの不透明度を指定するプロパティだが、これに1未満の値(つまり半透明)を設定してからdrawImage()で画像を描画した場合、本来描画する画像にglobalAlphaで指定した不透明度が適用されるべきなのだが、GoogleChrome及びexplorercanvasによる描画では適用されない。

要するにGoogleChromeとIEでは画像描画時に不透明度を弄れない。ゲームを作る上ではそこまで致命的ではなかったが、ブラウザによって演出が違ったりする(本来は会話時に話していない方の立ち絵が半透明になる)。

drawImage()でリサイズして描画した場合の振る舞い。

言葉で説明するより実際に見た方が話が早い。

IEとFirefoxでは単なる拡大ではない。

これはdrawImage()で1×1pixelの画像を100×100pixelに拡大して描画したものを各種ブラウザで比較したものである。IEとFirefoxは他のブラウザと違う描画結果になっている。この図ではわかりにくいが、色の薄いところは不透明度も低くなっている。割と使用頻度が高いと思われるメソッドなだけにこの辺の描画が違ってくるのはまずいと思うのだが。

今回のゲームではボスのライフゲージ描画でリサイズ描画を行っているため、ブラウザによってライフゲージの見え方が違ったりする。

画像を回転させて描画

どの変換マトリックスをどの順番で適用するのか迷ったんで書いておく。

以下の関数は画像の中心を軸に反時計回りにangleラジアン回転した画像を、中心が(x,y)の位置に来るように描画する。

//gdcはCanvasコンテキスト。
//<canvas id="cv"></canvas>
//として
//var gdc = document.getElementById("cv").getContext("2d");
//のように初期化されたもの。
var drawImageEx = function(imageObject,x,y,angle){
  var w = imageObject.width;//画像幅取得
  var h = imageObject.height;//画像高さ取得
  gdc.save();//描画状態保存
  gdc.translate(x,y);//原点移動
  gdc.rotate(angle);//回転。原点を中心に回転するところに注意
  gdc.drawImage(imageObject,-w/2,-h/2);//画像描画。画像の中心が原点にくるようにしている。
  gdc.restore();//保存しておいた描画状態に戻す
}

実際に使うときはこんな感じ。

var img = new Image();//Imageオブジェクト生成
img.src = "sample.png" + "?" + new Date();//画像ファイルを指定。キャッシュ対策のため現在時刻をクエリとしてくっつける
img.onload = function(){//画像がロードされてからでないと画像描画はできない。
  drawImageEx(img,10,20,Math.PI/4);//画像を(10,20)の位置に45度回転して描画。
}

Flashとの連携

Canvas要素によってJavaScriptによる描画が割と使えるものになったが、音声など他のメディアを扱うのは依然として困難である。そこでFlash(ActionScript)と連携することで音声を扱うことにした。

今回使ったのはActionScript3。Flashの開発環境は割と金がかかるが、ActionScriptオンリーなものを作るならAdobeで無料配布されているFlex SDKを使えばOK。

今回FlashとJavaScriptの連携にはExternalInterfaceを使った。FlashPlayer8以降で使える機能なので、Wiiのインターネットチャンネルでは動作しない。(WiiのFlashPlayerのバージョンは7。WiiのインターネットチャンネルでFlashコンテンツを表示する:CodeZine参照)

FlashとJavaScriptについて基本的なコードは

を参照したが、IE用コードと他のブラウザ用のコードを混ぜて書く方法やIEではまったところとかをサンプルを交えつつメモ。

Flash(ActionScript3)側コード

基本となるコードはこんな感じ。IE・他ブラウザ共通。

今回のゲームではFlashに音声ファイルを埋め込んだ都合上、Flashがすべて読み込まれたかどうかを知る必要があった。のですべて読み込まれてから初期化処理をFlashから呼び出すという形になっている。

//Main.as
package {
  import flash.display.Sprite;
  import flash.external.ExternalInterface;
 
  public class Main extends Sprite {
    function Main() {//コンストラクタ。Flashが読み込まれたら実行される。
      ExternalInterface.addCallback("hey", hey);//ActionScript側のメソッドhey()をJavaScriptから呼べるように設定。
      ExternalInterface.call("init");//JavaScript側の関数init()を呼び出す。
    }
 
    public function hey():String{//JavaScript側から呼び出すメソッドの定義。
      return "呼んだ?";
    }
  }
}

HTML及びJavaScriptコード

説明は置いといて基本コードはこんな感じ。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ja">
  <head>
    <script>
      var init = function(){setTimeout(init,100);}
      //Flashから呼び出される関数。initが書き換えられるまではこっちが呼び出される。
    </script>
    <script>
      var flash = (function(){//flashオブジェクト生成。
        //privateメンバー変数
        var _flash;
        var isLoad = false;
        return {//publicメソッド
          init:function(){//初期化。Flashから呼び出したい関数。
            _flash = (navigator.appName.indexOf("Microsoft") != -1) ? window["fid"] : document["fid2"];
            isLoad = true;
          },
          hey:function(){//Flashのhey()を呼ぶ関数。
            return isLoad ? _flash.hey() : "まだFlashが読み込まれてない。";
          }
        }
      })();
      init = flash.init;//init書き換え。flashオブジェクトを初期化
    </script>
  </head>
  <body>
<!--  ここからFlash埋め込み  -->
  <!--[if IE]>
    <object id="fid" width="1" height="1" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000">
      <param name="movie" value="sample.swf" />
      <param name="allowScriptAccess" value="always" />
    </object>
  <![endif]-->
  <!--[if IE]><noscript><![endif]-->
    <embed src="sample.swf" name="fid2" width="1" height="1" allowScriptAccess="always" />
  <!--[if IE]></noscript><![endif]-->
<!--  ここまでFlash埋め込み  -->
    <p><input type="button" value="Call hey()" onclick="alert(flash.hey())" /></p>
<!--テストコード-->
  </body>
</html>

JavaScriptコードの要となる部分は

_flash = (navigator.appName.indexOf("Microsoft") != -1) ? window["fid"] : document["fid2"];

というところ。IEとそれ以外のブラウザで処理を分けている。[]内の添え字はそれぞれobject要素のid属性とembed要素のname属性に対応する。

実際書いていてはまった部分はHTMLの方のFlash埋め込み部分。IEとそれ以外のブラウザで異なる。

IEから見た実質的なコードはこんな感じ。

<object id="fid" width="1" height="1" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000">
  <param name="movie" value="sample.swf" />
  <param name="allowScriptAccess" value="always" />
</object>

対してIE以外のブラウザから見た実質的なコードはこんな感じ。

<embed src="sample.swf" name="fid2" width="1" height="1" allowScriptAccess="always" />

<!--[if IE]>〜<![endif]-->の部分はIEでのみ有効となり、他のブラウザではコメントとして扱われる。IEで見た時にembed要素を隠すためにnoscript要素を使っている。正直noscript要素の使い方としてどうかとは思うが・・・。

allowScriptAccessの記述は重要。これを記述しないとJavaScriptからFlashの関数を呼び出すことはできない・・・はずだが試してたらなぜかできたときもあっただけにこのパラメータが必要なことになかなか気づかなかった(特にIE)。ほんとよく分からん。