Google+ もご覧ください
ユーザーアイコン

連載

ソースリーディング!Herlockの仕組みをゲームのソースコードから解説します!

ソースリーディング!Herlockの仕組みをゲームのソースコードから解説します!

前回はアプリストアにリリースされているねこ穴の開発版を自分のプロジェクトとして実機テストできるところまで試しました。今回はその中で使われているソースコードを読み、どういった処理が行われているのかを解説したいと思います。

構造

前回も書きましたが、まずは全体構造を見てみましょう。

$ tree .
├── libs                          // サードパーティのライブラリやよく使う関数があります
│   ├── common.js
│   └── require.min.js
├── main.js                       // 一番最初に読み込まれるスクリプトです
└── nekoana
    ├── config.js                 // 設定、定数などが指定されています
    ├── images                    // ゲーム中で使われる画像リソースです
    │   ├── game_black.png
    │   │      :
    │   └── game_top_bg.png
    ├── lib.js                     // ゲームやステージの管理、猫画像の表示処理などを行います
    ├── main.js                    // ゲームの初期化処理やプリロードを行います
    ├── scene
    │   ├── game.js               // ゲーム画面での処理を取り回します
    │   ├── result.js             // 結果画面での処理を取り回します
    │   └── top.js                // トップ画面での処理を取り回します
    └── sounds                     // サウンドファイル
        ├── README.txt
        │      :
        └── cat3.wav

5 directories, 36 files

画像や音声ファイルと言ったリソースファイルを除くと、JavaScriptファイルはたった9ファイルしかありません。これであれば全体像の把握もさほど難しくないでしょう。さらにライブラリや外部ファイルの読み込みはrequire.jsを使って行っています(libs以下)ので、8ファイルの内容が分かれば全体像が掴めるはずです。

起動時

Herlockは起動した際にルートディレクトリ以下のmain.jsを読み込みます。内容は以下の通り、ごく短いです。

var BASE_URL = ".";
( function( ) {
    "use strict";
    var loader = new Script( BASE_URL + "/libs/require.min.js" );
    loader.onload = function() {
        require.config( {
            baseUrl: BASE_URL,
            paths: {
                nekoana: "nekoana",
                libs: "libs"
            }
        } );
        require( ["nekoana/main"], function(main){
            main();
        } );
    };
} )();

最初にrequire.jsを読み込み、それが終わったタイミング(onload)で nekoana/main.js を読み込み、そのまま実行しています。

nekoana/main.js

まず最初にJavaScriptファイルを全て読み込んでいます。

define( "nekoana/main",
  [
    "nekoana/lib",
    "nekoana/config",
    "nekoana/scene/top",
    "nekoana/scene/game",
    "nekoana/scene/result"
  ],
  :

読み込んだ結果は次の function に送られます。

function( lib, config, sceneTop, sceneGame, sceneResult ) {
  //ゲームの初期化と開始画面の呼び出し
  function initialize() {
    var game = new lib.Game( 640, 1136 );
    game.addScene( config.SCENE.TOP, sceneTop );
    game.addScene( config.SCENE.GAME, sceneGame );
    game.addScene( config.SCENE.RESULT, sceneResult );
    game.addEventListener( lib.Game.PRELOADED, preloadedHandler );
    game.preload( config.materials );
  }
  function preloadedHandler( event ) {
    event.target.removeEventListener(lib.Game.PRELOADED, preloadedHandler );
    event.target.loadScene( config.SCENE.TOP );
  }
  return initialize;
}

行っているのは初期化処理(initialize)で、まず画面を作り、続いてシーンを作成しています。素材 config.materials の読み込みが終わったタイミングで preloadedHandler を実行し、 event.target.loadScene( config.SCENE.TOP ) にてシーンをトップページに切り替えています。

ここまでが nekoana/main.js の処理になります。

nekoana/config.js

config.jsはその名の通り設定値が集まったファイルです。画像/音声リソース、シーン名の定義、ネコの状態、穴の位置、配置用の定数、それぞれのシーンのオブジェクト配置を定義してオブジェクトで返却しています。

nekoana/scene/top.js、nekoana/scene/result.js

それぞれゲーム画面の処理になります。言わばコントローラです。つまりこのねこ穴では3つの画面が用意されています。

  • 最初の画面:top.js
  • ゲーム中画面:game.js
  • 結果画面:result.js

です。例えばtop.jsを見た場合、

define( "nekoana/scene/top", ["nekoana/lib", "nekoana/config"], function( lib, config ) {
  //ゲーム開始画面-----------
  //初期化
  function initializeScene( game ) {
    //スタートボタンの取得
    var btnStart = game.getCurrentStage().getChildByName( config.IMG.START_BTN );
    //ボタンイベントの設定
    btnStart.addEventListener( "touchTap", function gotoGame( event ) {
      btnStart.removeEventListener( "touchTap", gotoGame, false );
      game.loadScene( config.SCENE.GAME );
    }, false );
  }
  return initializeScene;
});

となっています。initializeSceneが初期に呼ばれる仕組みで、ゲーム開始ボタンを取得し、そこにタップイベントを追加しています。イベントとしては画面をゲーム中画面に変更しているだけです。とても簡単ですね。

結果画面であるresult.jsも基本は同じで、後はゲームのスコアを表示したり、これまでのベストスコアと比べる処理が入っています。

//過去のベストスコアの取得と保持
function getBestScore(game) {
  var storage = window.localStorage;
  var bestScore = storage.getItem( STORAGE_ITEM );
  if ( game.score > bestScore || bestScore === null ) {
    storage.setItem( STORAGE_ITEM, game.score.toString() );
    bestScore = game.score;
  }
  return bestScore;
}
//スコアの表示
function showBestScore(game) {
  var scoreBmd = game.loader.getBitmapData( config.IMG.SCORE );
  var score = lib.ScoreSpriteFactory.create( scoreBmd, 32, 32, 6 );
  game.getCurrentStage().addChild(score);
  score.setNumber(getBestScore(game));
  score.x = 240;
  score.y = 860;
}

ここで注目すべきはスコアを lib.ScoreSpriteFactory.create( scoreBmd, 32, 32, 6 ) で作成している点でしょう。libについては後ほど詳細を見ていきます。

nekoana/scene/game.js

game.jsはゲーム中画面の処理になりますので最も処理が多い画面になります。とは言え基本は同じで、initializeScene(game) から処理がはじまります。まず最初に変数を初期化しています。後、以下の3つのイベントを定義しています。

stage.addEventListener(Event.ENTER_FRAME, enterFrameHandler, false);
stage.addEventListener("touchBegin", touchBeginHandler, false);
stage.addEventListener("touchEnd", touchEndHandler, false);

まず Event.ENTER_FRAME は毎フレームごとのネコの動きをコントロールするイベントになります。

最初にネコを表示させるかどうかを乱数を使って判定しています。

function enterFrameHandler() {
  if (Math.random() < crateRate) {
    var hole = config.HOLE_POINTS[Math.floor(Math.random() * 8)];
    var cat = catFactory.create(hole);
    //飛ぶ高さ
    cat.pointY = Math.floor(Math.random() * 100) + 200;
    //該当する穴の背面に挿入
    stage.insertBeforeByName(cat, hole.ground);
    cats.push(cat);
    :

作成するとなったら、穴(hole)の背後にネコ(cat)を挿入します。それとは別途、ネコの配列(cats)の中にcatを追加します。catsはその後、ネコの動きとして制御されます。

cats.forEach(function(cat, i){
  if (cat.status === config.CAT_STATS.JUMP) {//飛び上がるとき
    cat.y += ( cat.pointY - cat.y ) * 0.15;
    var distance = Math.abs(cat.pointY - cat.y);
    if (distance < 10) {
      cat.status = config.CAT_STATS.JUMP_DOWN;
    }
    if (distance < cat.pointY * 0.3) {
      stage.removeChild(cat.pon);
    }
  } else if (cat.status === config.CAT_STATS.JUMP_DOWN) {//落ちるとき
    cat.y += ( cat.y - cat.pointY ) * 0.15;
    if (cat.y > game.height) {
      stage.removeChild(cat);
      cats.splice(i, 1);
    }
  } else if (cat.status === config.CAT_STATS.CAUGHT) {//捕まえられた後
    cat.x += cat.speed.x;
    cat.y += cat.speed.y;
    if (cat.x < 0 || cat.x > game.width) {
      stage.removeChild(cat);
      removes.push(i);
    }
  }
});
//削除したやつの後処理
removes.reverse().forEach(function(index, i){
  cats[index].clear();
  cats.splice(index, 1);
});

これで分かる通り、基本はcatごとの状態(cat.status)を見て飛び上がっているか、落ちているか、または捕まえられたのかを判別しています。そして画面の端まで飛んでいったらremovesに追加してcatsから消すと言った具合です。

下の二つ、touchBeginはネコを捕まえたかどうかの判定を行います。touchEndはtouchMoveのイベントリスナーを消しているだけです。touchMoveは後述しますがタッチ方向や速度の管理をしています。

// touch events. touchBegin用
function touchBeginHandler (e) {
  touchPrev = {
    x: e.stageX,
    y: e.stageY
  };
  //ねこを捕まえたか
  catTouchTest(e.stageX, e.stageY);
  stage.addEventListener("touchMove", touchMoveHandler, false);
}

タッチした座標を catTouchTest に送り、ネコを捕まえたかどうかの判定処理を行っています。

//ねこを捕まえたか
function catTouchTest(sx, sy) {
  for (var i = 0; i < cats.length; i++) {
    var cat = cats[i];
    //タッチされた領域がネコに当たっているか
    if (cat.hitTestGlobal(sx, sy)) {
      onCatch(cat);
    }
  }
}

cat.hitTestGlobal(sx, sy) にてタッチされた領域のチェックをネコごとに行っています。これがtrueであればonCatchメソッドに飛ばしています。

//捕まったときの処理
function onCatch(cat) {
  //鳴き声
  cat.voice.play();
  stage.removeChild(cat.pon);
  if (cat.status !== config.CAT_STATS.CAUGHT) {//捕まったあとのものは無視する
    if (cat.name === config.IMG.NEKOTA) {//ネコ田さんは捕まえてはいけない
      catchMiss();
    }
    else {//ネコの場合
      catchSuccess();
    }
    //飛んでいる画像にする
    cat.gotoFrameIndex(1);
    //ねこのスピードを計算
    var p = new Point(touchVector.x, touchVector.y);
    if (p.x === 0 && p.y === 0 || isNaN(p.x) || isNaN(p.y)) {
      p.x = Math.random() - 0.5;
      p.y = -Math.random();
    }
    if (p.y > 0) {
      p.y = -Math.random();
    }
    p.normalize(20);
    cat.speed = {
      x: p.x,
      y: p.y
    };
    //飛ぶ方向に応じて向きの調整
    if (p.x < 0) {
      if (cat.name !== config.IMG.NEKOTA) {
        cat.scaleX = -1;
      }
      cat.x += cat.width;
    }
    //最前面に出す
    stage.setChildIndex(cat, stage.numChildren - 1);
    //状態変更
    cat.status = config.CAT_STATS.CAUGHT;
  }
}

処理としては ネコの鳴き声を再生して、成功(ネコ)か失敗(ネコ田)かを判定しています。その後は飛んでいる画像に変更します。

面白いのはtouchVector.x/touchVector.yによって速度を計算している点です。指先を動かした時にその方向を計算することでネコが飛んでいく方向を決めています。

nekoana/lib.js

lib.jsはオブジェクト周りや画像リソースの取り回しを行っています。先ほどのネコを生成する、捕まったかどうかの判定をするのもこのライブラリになります。

var CatSpriteFactory = function() {
  this.initialize.apply( this, arguments );
};
CatSpriteFactory.prototype = {
  //ネコ素材
  imageNames: [
    :
  ]
}

となっていて、prototypeで拡張する方法をとっています。そしてネコを生成する際には、

var cat = ImageSpriteFactory.create( this.bmds[name], this.frames.concat() );
Util.mixin( cat, this.methods );

のように処理をしてCatSpriteFactoryのメソッドをmixinしています。そのため、ネコを捕まえたかどうかの判定は CatSpriteFactory.methods.hitTestGlobal になります。

hitTestGlobal: function( pointX, pointY ) {
  var bx = this.x;
  var by = this.y;
  var bw = this.width;
  var bh = this.height;
  var b_x_max = bx + bw;
  var b_y_max = by + bh;
  return !( pointX < bx || b_x_max < pointX || pointY < by || b_y_max < pointY );
}

このように画像の高さ、幅などからタッチした座標が入っているかどうかを判定しています。また、その他lib.jsではゲームの状態を管理するGame、画像の表示(スプライト含む)を管理するImageSpriteFactory、スコアの管理を行うScoreSpriteFactoryなどが定義されています。

lib/common.js

common.jsには基本的な機能である通知機能であったり、素材読み込みを行うライブラリが入っています。どちらも nekoana/lib.js のGame内で使われています。

値の保存

ねこ穴であればベストスコアなどをどこかに保存しておきたいというケースはあると思います。Herlockでは window.localStorage が使えます。ここに保存しておくことで、過去のスコアなどがアプリを終了させた後でも取得できます。

//過去のベストスコアの取得と保持
function getBestScore(game) {
  var storage = window.localStorage;
  var bestScore = storage.getItem( STORAGE_ITEM );
  if ( game.score > bestScore || bestScore === null ) {
    storage.setItem( STORAGE_ITEM, game.score.toString() );
    bestScore = game.score;
  }
  return bestScore;
}

全体の流れ

処理全体を見ると、main.js、nekoana/main.jsはライブラリの読み込み程度しか行っていません。実際にはlib.jsとscene以下のJavaScriptファイルが全体の処理を行っています。設定や定数を変更する際にはconfig.jsを変更すれば良いでしょう。

例えば設定画面を追加したいとなったら、sceneディレクトリにsetting.jsを追加します。そしてconfig.jsに画面情報を追加し、他の画面から設定画面を呼び出せるようにすると言った具合です。

まとめ

ゲームの処理を行っている部分を完全に把握するのは多少時間がかかるかも知れませんが、見るべきファイルさえ分かっていれば難しくはないでしょう。ゲーム内で取り扱うオブジェクトが定義できれば、後はlib.jsと各画面のJavaScriptファイルでゲームを開発できます。

全体を通して分かったかと思うのですが、画面を作るような処理は殆どありません。背景画像を読み込んだり、そこにボタンを配置する程度です。シンプルなゲームを手軽に作れるのが大きな利点と言えるでしょう。

Herlock自体はもっとシンプルにmain.jsで全ての処理を行ってしまうこともできますが、ねこ穴の作法に則って行うことで見通しの良いコードが書けるのではないでしょうか。

今回紹介していないですが、HerlockにはXMLHttpRequestやデバイスからのセンサー取得、TweenJSの利用など魅力的な機能がたくさんあります。それらを組み合わせることでより高度なゲームアプリが開発できるようになるでしょう。

何よりWeb IDEを使ってコンパイルレスでiOS/Androidの両デバイスに対応したゲームが作成できてしまうのが大きな利点です。ぜひHerlockを習得してゲーム開発をはじめてみましょう。

Herlock - JavaScriptクロスプラットフォームゲームエンジン

[PR] Herlock MBaaS近日リリース

Herlockでは、開発者の皆様に簡単にアプリを制作して頂ける様 バックエンドの機能を、クラウドサービスとして提供致します。 機能は順次拡充していきますが、まずは下記の機能のリリースを予定しております。

Push通知

iOS/Android両プラットフォーム向けに、管理画面上よりPush通知を送信出来るようになります。

ユーザー管理

開発したアプリのユーザー管理をクラウド上で行えるようになります。

リリース日はHerlockサイト上にて改めて告知させて頂きますが、今月中旬頃のリリースを予定しております。楽しみにお待ちください!

Herlock - JavaScriptクロスプラットフォームゲームエンジン

Cover

記事をリクエストする

関連記事

コメント