LoginSignup
5
2

More than 3 years have passed since last update.

黒ひげ危機一〇髪で触るphina.js

Last updated at Posted at 2019-12-22

はじめに

コミックマーケット95にて拓殖大学ディジタルコンテンツ研究愛好会様にて頒布させていただきました、『黒ひげ危機一〇髪で触るphina.js』というコピー本の内容を拓殖大学ディジタルコンテンツ研究愛好会 Advent Calendar 2019 23日目の、この場を借りて公開されて頂きます。
頒布という形であるので、購入者から反感を買うのではないか、と心配の声があるかもしれませんが、悲しいことに一部も売れなかったので問題ありません。

コピー本では開発環境を用意させていただいていたのですが、サービスが終了してしまったので、その部分はカットして公開させていただきます。

表紙

C95表紙_pages-to-jpg-0001.jpg

前書き

はじめまして。ご購入頂きありがとうございます。

この本はJavaScriptの基本的な文法を理解しており、Web上で動くゲームをつくりたい方向けの本になります。

皆さんは、phina.jsというゲームフレームワークをご存知でしょうか? このフレームワークはブラウザ上で動くゲームが比較的簡単につくれて、それをSNSに載せることで承認欲求の満たされる私が愛用しているフレームワークの一つです。

今回は、そんないつも承認欲求を満たすのにお世話になっているphina.jsを知らない方に認知してもらえるように、まだ使ったこと無い方にも使ってもらえるように、今回初めて筆を執らせていただきました。

phina.jsの1ユーザーに過ぎませんが、もしよろしければ最後までご一読頂けるよう、phina.jsを使っていただけるよう、書かせて頂きました。

今回は『黒ひげ危機一〇髪もどき』をつくりながら解説していこうと思います。よろしくお願いします。

参考

参考にさせていただきました。ありがとうございます。

今回作るもの

黒ひげ危機一〇髪のブラウザで遊べるスマートフォン向けゲームをつくります。

完成したゲームは以下のリンクより遊べます。
https://masayasviel.github.io/blackBeard-phina_js/

追記:以下のリンクよりコードを公開しています。
https://github.com/masayasviel/blackBeard-phina_js

六つある樽の中に一つある、黒ひげが隠れているハズレの樽を選ばないようにタッチしていくゲームです。どれだけ空の樽を選べたかをリザルト画面で表示します。

また、スコアをツイートできるようにもします。

phina.jsとは

国産のJavaScriptのゲーム制作向けフレームワークです。HTML5に対応しているので、JavaScriptの動作するブラウザであればphina.jsで書かれたプログラムを動かすことが出来ます。

phina.jsの強みは、公式サイトには

  • アイディアを即座に形にできるゲームライブラリです
  • 初心者でも手軽にゲームを開発できます
  • 様々な Web コンテンツ, アプリで多数の採用実績があります
  • 大学や専門学校といった教育機関で利用されています
  • 国産かつオープンソースでTwitter 駆動開発なので気軽にコントリビューターになれます

と書かれています。加えて個人の初見として、

  • JavaScriptという実行環境を整えやすい言語のフレームワークである
  • HTML、CSSの細かな知識が必要ない
  • PCとスマートフォンどちらでも動く
  • 公式サイトにサンプルが複数用意されている

が挙げられます。

ここでいくら説明しても、良いところを書き連ねても、『百聞は一見に如かず』とはいったもので実際に手を動かしてみるのが一番だと考えます。

それでは早速始めて行こうと思います。

実行環境

今回は実行環境として、jsdo.itを使わせて頂きました。また、今回はバージョン:71.0.3578.98のGoogle chromeで動作確認をしました。
ですが、jsdo.itはサービスが終了してしまったので、登録及び簡単な使い方の説明はカットさせていただきます。

テンプレート

HTML

まずはHTMLの部分に以下のコードを書きましょう。

<!doctype html>

<html>
  <head>
    <meta charset='utf-8' />
    <meta name="viewport" content="width=device-width, user-scalable=no" />
    <meta name="apple-mobile-web-app-capable" content="yes" />

    <title>黒ひげ危機一〇髪</title>
    <!-- phina.js を読み込む -->
    <script src='https://cdn.rawgit.com/phi-jp/phina.js/v0.2.0/build/phina.js'></script>

  </head>
  <body>

  </body>
</html>

これはphina.jsに書かれたindex.htmlのコードをコピペしても構いません。ただし、その場合は

<title>Getting started | phina.js</title>
<script src='http://cdn.rawgit.com/phi-jp/phina.js/v0.2.0/build/phina.js'></script>

の部分を、

<title>黒ひげ危機一〇髪</title>
<script src='https://cdn.rawgit.com/phi-jp/phina.js/v0.2.0/build/phina.js'></script>

と書き直しましょう。httpではなくhttpsに、<title>部分を黒ひげ危機一〇髪にしましょう。(<title>部分は好きにしていいと思いますが、あえてここでは黒ひげ危機一〇髪で統一することにします。)

今後、HTML部分は書き換えません。理由は後述します。

CSSの部分はそのままにしておきましょう。

HTML同様、CSSも今後イジることはありません。

JavaScript

phina.jsでは描写や背景なども全てJavaScriptの部分に記述していきます。これがHTML、CSSの細かな知識が必要ない、書き換えない理由です。

phina.jsには雛型があり、それを元に不要な部分は削除し、必要な部分をつけ足していく、といった流れでコードを書いていきます。

これが雛型となります。

// phina.js をグローバル領域に展開
phina.globalize();

// MainScene クラスを定義
phina.define('MainScene', {
  superClass: 'DisplayScene', // 継承元
  // コンストラクタ
  init: function() {
    this.superInit(); // 親クラスのコンストラクタに値を渡す
  },
});

// メイン処理
phina.main(function() {
  // アプリケーション生成
  let app = GameApp({
    startLabel: 'main', // メインシーンから開始する
  });
  // アプリケーション実行
  app.run();
});

これはphina.jsmain.jsから不要な部分を除いたものになります。また、superClass: CanvasScenesuperClass: 'DisplayScene'直しましょう。

バージョンが変わったときに変更されたみたいです。

【ソース】本日 phina.js version 0.2 をリリースしました

さて、以上で前知識の説明は終わりです。ここからゲームをつくっていきます。

ゲームをつくる

画面をつくる

以下、全てJavaScriptに書いていきます。

まずはタイトル画面を作ってゲームの画面サイズを決めちゃいます。ゲームの画面サイズはスマートフォンに合わせたサイズにします。

// phina.js をグローバル領域に展開
phina.globalize();

// 定数
const SCREEN_WIDTH = 640;            // 画面横サイズ
const SCREEN_HEIGHT = 960;           // 画面縦サイズ

// MainScene クラスを定義
phina.define('MainScene', {
  superClass: 'DisplayScene', // 継承元
  // コンストラクタ
  init: function() {
    this.superInit(); // 親クラスのコンストラクタに値を渡す
  },
});

// メイン処理
phina.main(function() {
  // アプリケーション生成
  let app = GameApp({
    title: '黒ひげ危機一〇髪',
    startLabel: location.search.substr(1).toObject().scene || 'title', // 'title'から開始する
    width: SCREEN_WIDTH,
    height: SCREEN_HEIGHT,
  });
  // アプリケーション実行
  app.run();
});

これだけで画面をタッチするとメインシーンへ移行するタイトル画面は完成です。簡単ですね。

ゲームの画面サイズはウィンドウによって自動伸縮されます。

let app = GameApp({});内のtitle:部分の文字を変えると表示される文字が変わります。

これはphina.jsが用意してくれたテンプレートです。リザルトシーンのもあります。また、タイトルシーンもリザルトシーンも自分でつくることもできますが今回はやりません。

画像を表示

まず、画像を読み込みます。

画像はいらすとや様のを使わせていただきます。ありがとうございます。

画像は以下の画像を使用します。

海賊

JavaScriptで画像を取り扱う場合、通常はImageコンストラクタを使用します。

例:

let myImage = new Image();
myImage.src = "hoge.jpg";

では、phina.jsではどうするのでしょう? こうします。

// phina.js をグローバル領域に展開
phina.globalize();

// 定数
const SCREEN_WIDTH = 640;            // 画面横サイズ
const SCREEN_HEIGHT = 960;           // 画面縦サイズ

const ASSETS = {
  image: {
    pirate:"https://2.bp.blogspot.com/-vic8sGz8fv0/W6XJc9aEH1I/AAAAAAABPDA/zhl2g1Nq2qMRJGflTeOEim_IFdUffz9PwCLcBGAs/s450/kaizoku_man.png",
    barrel:"https://1.bp.blogspot.com/-BtjaBaaAKNw/VkxNbVZhsLI/AAAAAAAA0vs/roxlxfuXIgE/s400/drink_taru.png",
  }
};

// MainScene クラスを定義
phina.define('MainScene', {
  superClass: 'DisplayScene', // 継承元
  // コンストラクタ
  init: function() {
    this.superInit(); // 親クラスのコンストラクタに値を渡す
  },
});

// メイン処理
phina.main(function() {
  // アプリケーション生成
  let app = GameApp({
    title: '黒ひげ危機一〇髪',
    startLabel: location.search.substr(1).toObject().scene || 'title', // 'title'から開始する
    width: SCREEN_WIDTH,
    height: SCREEN_HEIGHT,
    assets: ASSETS,
    backgroundColor: '#191970',
  });
  // アプリケーション実行
  app.run();
});

let app = GameApp({});内にassets: ASSETS,backgroundColor: '#191970',を加えましょう。

assetsはアセットを有効にします。
ここでアセットとは画像や音声データなどのバイナリデータのことです。

また、画像を読み込ませる場合はassetsに以下のような書式のオブジェクトを渡します。

const ASSETS = {
  image: {
    キー名1:画像の場所1,
    キー名2:画像の場所2,
  }
};

【参考】[phina.js-Tips-015] Spriteを表示する

backgroundColorはそのまんま、背景色のことを指します。入れる書式はRGBカラーコードで指定します。今回は黒ひげということで海をイメージして深い青にしてみました。

次に画像を表示させてみたいと思います。

以下、コード中の//...はコードが省略されていることを示します。

//...
// MainScene クラスを定義
phina.define('MainScene', {
  superClass: 'DisplayScene',
  init: function() {
    this.superInit();
      let pirate = Sprite('pirate',100,100).addChildTo(this); 
      pirate.x = SCREEN_WIDTH / 2 - 100;
      pirate.y = SCREEN_HEIGHT / 2;
      let barrel = Sprite('barrel',100,100).addChildTo(this); 
      barrel.x = SCREEN_WIDTH / 2 + 100;
      barrel.y = SCREEN_HEIGHT / 2;
  },
});

// メイン処理
//...

タイトル画面でタッチして、次のシーンへ行くと小さい海賊と樽が真ん中に並んでいますね。

Sprite('画像キー名',横サイズ,縦サイズ)で渡して、.addChildTo()でthisを指定してMainSceneの子要素にしています。

また、それぞれのx、yプロパティに数値を直接入れることによって位置を指定しています。

ですが、これからこの海賊と樽を何度も使っていくとなると、このやり方は面倒ですよね? 私は面倒だと思います。

ですので、pirateとbarrelそれぞれクラス化して再利用できるようにします。

phina.js でのクラス・メソッドのつくり方は

phina.define('クラス名', {
  superClass: '親クラス',
  // コンストラクタ
  init: function() {
    this.superInit();
  },
  メソッド名1: function() {
    // ...
  }, // カンマを忘れずに!
  メソッド名2: function(){
    // ...
  } // 一番後ろのメソッドはカンマは要らないがあっても問題ない
});

このようにします。

上のようにphina.jsで定義したクラスはグローバルのメンバになります。

【参考】[phina.js-Tips-39] クラスを作成してみる

では、これを踏まえた上で、pirateとbarrelそれぞれをクラスとして定義します。

// ...
// MainScene クラスを定義
phina.define('MainScene', {
  superClass: 'DisplayScene',
  init: function() {
    this.superInit();
    // 海賊インスタンス作成
    this.pirate = Pirate().addChildTo(this).setPosition(SCREEN_WIDTH / 2 - 100,SCREEN_HEIGHT / 2);
    // 樽インスタンス作成
    this.barrel = Barrel().addChildTo(this).setPosition(SCREEN_WIDTH / 2 + 100,SCREEN_HEIGHT / 2);
  },
});

// 海賊クラスを定義
phina.define('Pirate',{
  superClass: 'Sprite',
  init:function() {
    this.superInit('pirate',300,300);
  },
});

// 樽クラスを定義
phina.define('Barrel',{
  superClass: 'Sprite',
  init:function() {
    this.superInit('barrel',200,200);
  },
});

// メイン処理
//...

海賊、樽と並んで表示されたはずです。

setPosition(横,縦)で位置を指定して決定することもできます。

またphina.jsで定義したクラスはコンストラクタをnewなしで呼び出します。

【参考】[phina.js-Tips-002] Shapeの位置を指定する

また、海賊クラス、樽クラスで継承している、Spriteクラスの中身を少しだけ覗いてみましょう。

phina.define('phina.display.Sprite', {
  superClass: 'phina.display.DisplayElement',

  init: function(image, width, height) {
    this.superInit();
    this.srcRect = phina.geom.Rect();
    this.setImage(image, width, height);
  },

  draw: function(canvas) {
    //...
  },

  setImage: function(image, width, height) {
    //...
  },
  //...

と、このようになっています。

コンストラクタでsetImage()メソッドにそのまま引数を渡していることからわかる通り、this.superInit('画像',横サイズ,縦サイズ);と渡すことで画像と大きさの要素を持ったクラスを簡単につくることができます。

だいぶ端折りましたが、

phina.js API Documentation

phina.display.Sprite

から全文が見られます。

ゲームにする

まず、先に海賊クラスと樽クラスから書いていきます。先ほどのコードを書き直します。

// 海賊クラスを定義
phina.define('Pirate',{
  superClass: 'Sprite',
  init:function(x, y) {
    this.superInit('pirate',300,300);
    this.setPosition(x, y);
  },
});

// 樽クラスを定義
phina.define('Barrel',{
  superClass: 'Sprite',
  init:function(number, x, y) {
    this.superInit('barrel',200,200);
    this.number = number;
    this.setInteractive(true); // タッチを有効にする
    this.setPosition(x, y);
  },
});

樽クラスのthis.numberについては後述するのでひとまず置いておきます。

樽クラスにあるthis.setInteractive()trueを入れることでタッチを有効にします。逆にfalseを入れるとタッチが無効になります。

次にMainSceneのほうを書いていきます。

// MainScene クラスを定義
phina.define('MainScene', {
  superClass: 'DisplayScene',
  // コンストラクタ
  init: function() {
    this.superInit();
    let score = 0;
    let rand = Random.randint(1, 6);
    let barrel_num = 0; // 樽に番号をつける
    // 位置
    let position_x = [150, 450];
    let position_y = [200, 500, 800];

    // 樽グループ
    this.barrelGroup = DisplayElement().addChildTo(this);

    const self = this;
    for(let x of position_x){
      for(let y of position_y){
        barrel_num++;
        // 樽作成
        let barrel = Barrel(barrel_num, x, y).addChildTo(self.barrelGroup);
        barrel.onpointend = function() {
          this.remove();
          if(rand == barrel.number){
            self.invalid();
            self.popUp(self, x, y);
            setTimeout(function() {
              self.exit({
                score: score,
                message: "黒ひげ危機一〇髪で触るphina.js",
                hashtags: "C95_DCRC"
              });
            }, 500);
          }else{
            score++;
          }
        };
      }
    }
  },
  // 黒ひげインスタンスを後から作成する
  popUp: function(self, position_x, position_y) {
    Pirate(position_x, position_y).addChildTo(self);
  },
  // 樽それぞれのタッチを無効化
  invalid: function(){
    for (const barrel of this.barrelGroup.children) {
      barrel.setInteractive(false);
    }
  }
});

解説

Random.randint(a, b)はa以上b以下のランダムな値を出すメソッドです。
これとbarrel.numberを比較して、同じ値の時に黒ひげが出るようにしています。

【参考】[phina.js-Tips-006] Shapeをランダムな位置に表示する

barrel.onpointendの関数では、タッチされた樽が、ハズレであるかを判定しています。ところが、MainSceneのinitが実行し終わった時点で、barrel_numの値は必ず6になってしまいます。
このため、ランダムの値が6の時以外黒ひげが出てこなくなってしまいます。
また、ランダムの値が6の時、どこを押しても黒ひげが出てくるようになってしまいます。
そのため、barrel.numberにその樽が何個目の樽かを格納しています。

element.onpointendelementのタッチ終了時に代入された関数が実行されます。

他にも、

// タッチ開始時
element.onpointstart = function() {
  // ...
}

// タッチ移動時
element.onpointmove = function() {  
 // ...
}

などがあります。

また、シーン内で

onpointstart: function() {
  // ...
},

このようにメソッドを定義したら、画面をタッチしたとき、そのメソッドが実行されます。

element.remove()は指定されたelementを消去するメソッドです。

上記のコードで使われているremove()メソッドはthis.remove()と書かれていることから分かる通り、タッチされた樽だけを消すようにしています。

MainSceneではpopUpinvalidというメソッドを定義しています。

これらはMainSceneに属しているため、このままだとbarrel.onpointend内からthisを用いて参照できません。

そのため、MainSceneの参照をconst self = thisで保存する必要があります。

【参考】とりあえず試してみたいって方のための phina.js 入門

this.barrelGroup = DisplayElement().addChildTo(this)はグループ化というもので、つくられたインスタンスをひとまとめにしてイジれるようにするものです。

.addChildTo()引数につくったグループを入れてやることで、そのグループの子要素となります。

子要素それぞれをイジりたい場合は

invalid: function(){
  for (const barrel of this.barrelGroup.children) {
    barrel.setInteractive(false);
  };
}

このようにそれぞれ取り出して変えてやる必要があります。

また、MainSceneの初期化後、新しくインスタンスを召喚したい場合は、別に召喚用メソッドを定義してやる必要があります。

self.popUp(self, position_x[x], position_y[y]);
popUp: function(self, position_x, position_y) {
  // 海賊作成
  Pirate(position_x, position_y).addChildTo(self);
},

ここからわかる通り、selfを渡しています。

これは.addChildTo(self)init内のthisの子要素とするためです。こうすることで後からインスタンスをつくって画面に出すことが出来ます。

最後にthis.exit()についてです。

これで次のシーンへと飛べます。phina.jsではデフォルトだと『title➡main➡result➡title』の順番で飛びます。

したがって、MainSceneexit()メソッドが発火するとResultSceneへと飛びます。

ResultSceneはつくっていないので、phina.jsでデフォルトで用意されたResultSceneへ飛びます。
デフォルトでexit()メソッドにどういう値を渡せるのか、ResultSceneクラスを少し覗いて確認してみます。以下はデフォルトの部分です。

_static: {
  defaults: {
    score: 16,

    message: 'this is phina.js project.',
    hashtags: 'phina_js,game,javascript',
    url: phina.global.location && phina.global.location.href,

    width: 640,
    height: 960,

    fontColor: 'white',
    backgroundColor: 'hsl(200, 80%, 64%)',
    backgroundImage: '',
  },
},

phina.game.ResultScene

うおお、色々渡すことができますね……今回この本を書くにあたり、初めてソースを見ましたが、知っている以上にたくさん値を渡すことができてビックリしました。

今回はこの中のscoremessagehashtagsのプロパティに値を渡したいと思います。

this.exit({
  score: score,
  message: "黒ひげ危機一〇髪で触るphina.js",
  hashtags: "C95_DCRC"
});

どれもそのままの意味なのですが、scoreはスコア、messageはメッセージ、hashtagsは共有ボタンを押したとき作成されるテキストにつくハッシュタグのことです。,で区切ることで複数ハッシュタグをつけられます。

あとがき

拙い文章ですが、ここまで読んでいただきありがとうございました。個人のブログで度々技術系の記事は書かせていただいているのですが、これほどの文章を書くのは初めてでとても良い経験となりました。ありがとうございます。

初学者でも触れやすく、すぐに動くものがつくれてプログラミングの楽しさを伝えるには最適なフレームワークである、このphina.jsを触ってみて良いなと思ったら他の人にも是非教えてあげてください。

最後になりますが、thisの参照に気を付けましょう。「なぜか動かない」と思ったら、ひょっとするとそれはthisの参照が間違っているかもしれません。

以上です。二度目になりますが、ご購入いただき、最後まで読んでいただきありがとうございました! 終わり!!

おくづけ

2018年12月30日 初版発行

著者:masayasviel
サークル:拓大ディジコン愛好会
印刷:学内某所

スペシャルサンクス

名前(敬称略)
- alkn203
- なかひこくん
- みっきぃ
- えふてい

名前(敬称略):サイトURL
- 高尾技研:https://takao-giken.github.io/

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2