Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

「妄想変身ヒーローズ」コードリーディング

9leapで公開されている妄想変身ヒーローズが楽しく快適だったので、どんなコードなのか読んでみました。JavaScriptでそれなりの規模のプロジェクトを作る場合はどうするのがいいんだろう?という疑問へのヒントをもらえた気がします。enchant.jsに限ればSceneがとにかく重要っぽい。

コード構成

このゲームは大きく分けて6つのコードで構成されています。そのうちの1つであるSceneは、さらに7種類に分かれています。ロード順に並べるとこんな感じ。

  1. Core
  2. Character
  3. Field
  4. Model
  5. Effect
  6. Scene
    1. SceneTutorial
    2. SceneMain
    3. SceneBoss
    4. SceneCutin
    5. SceneFinish
    6. SceneClear
    7. SceneGameover

Coreはユーティリティメソッド・BGM/SEのロードと管理・地形やエンティティのアタリ判定といった基本的なコード群で構成されています。ここで作ったrheroに対してサウンド再生などの依頼をいろんなところから投げているようです。地形とのアタリ判定にはenchant.Mapを使っている様子。判定部分はよくある上下左右への衝突を計算しているコードなので、長いけど読みやすい感じ。
Characterはヒーロー(Pom)と敵キャラ(Gumo, Gimi)をenchant.Spriteのサブクラスとして定義しています。敵キャラの速度はここでは定義されていません。すべてのキャラクターが等速であるという前提があるようです。
Fieldは空や地面といった背景をenchant.Groupのサブクラスとして定義しています。背景にはenchant.Mapを2つ使い、Mapが画面から完全に出たタイミングで画面の右側に移動させることで無限にスクロールするように見せています。私は1カラム出るたびにコピー処理を行なっているのだろうと思っていた*1ので、ここはちょっと意外でした。自分の発想の古さを感じます。
Modelは画面に表示されるもの、例えばライフバーやエンブレムなどが定義されています。役割としてはCharacterとほぼ同じに見えます。まとめて1ファイルに書いちゃう人もいそう。
Effectはその名のとおり画面効果を集めたもので、このゲームの特徴的な背景切り替え処理を実現しているEffectBackクラスを含んでいます。オブジェクトの状態(invisible/appear/visible/disappear)によってフレームごとに実行されるメソッドを変化させるという非常に単純なコードなんですが、遊んでいると効果音と切り替え速度の影響なのか、すごく面白いレスポンスを受けた感じがします。この辺はプログラミングと言うよりはセンスや経験の領域?ぜひ学びたい。

Sceneはこのゲームで最も多くの行数を占めます。これまでに定義してきたクラスを組み合わせてゲームを構成する役割を担う重要なクラスですが、TOUCH_STARTとTOUCH_ENDとENTER_FRAMEしか使っていないですしメソッドもコンストラクタしか存在しないので、読むのは簡単です。Mainは長いのでGameOverあたりから読むとよさそう。
ボス戦への遷移やゲームオーバー画面の表示などは、Sceneの中でpushScene()replaceScene()を呼び出すことで実現しています。普段はSceneを管理するクラスを別に用意する発想になりがちなんですが、こっちの方が読む箇所少ないし便利そうです。次回はこの手法を試します。
スコアや背景といったグローバルな存在は、グローバルオブジェクトのプロパティとして記録されるのではなく、コンストラクタを通じて各Sceneが保持する感じになっています。見通しがきくのでこっちのほうが好み。

テクニック

BGMのループ再生

Coreにループ再生用の処理が書かれているのですが、基本的な処理はこんな感じらしいです。

  • 1秒ごとにBGMの再生時間を調べる
  • 指定した長さを経過していたら再生を止めてはじめから再生しなおす
  • 長さは再生依頼側が明示的に指定する

BGMの長さはdurationプロパティから取得可能なので、自動判定するようにも書けそうです。Sound.loop()メソッドとかフレームワーク側で用意することも可能っぽいけど、durationプロパティが各ブラウザできちんと同じ動作をしてくれるかどうか未確認。

扇形のゲージの描画

rheroModels.jsより引用。save()/restore()clip()をうまく使ってるようです。どちらもまだ使ったことがなかったのでいずれ試したい。

ctx.save();
ctx.beginPath();
ctx.moveTo(30, 30);
startAngle = Math.PI * 1.5;
endAngle = 2 * Math.PI * player.getHeroCounter() / Pom.heroCounter[currentHero] - Math.PI / 2;
ctx.arc(30, 30, 30, startAngle, endAngle, true);
ctx.closePath();
ctx.clip();
ctx.clearRect(0, 0, 60, 60);
ctx.restore();

感想

canvasベースのコーディングではdraw()メソッドを用意してフレームごとに呼び出すのが当たり前だったのですが、その発想から抜けださないとenchant.jsの恩恵、特にSpriteとSceneの良さが活きないんだろうなぁという感じ。Sceneはdivのラッパーっぽいし単なるレイヤーなのかなぁとか思ってましたが、currentSceneにしかenterframeイベントが発生しないとか結構重要なクラスのようです。
以前描画タイミングのためのイベントとか作りましたが、あれは完全にcanvasゲー専用ですね。TowerDefenceとかSpriteベースで書きなおしてみても面白いかも。