JavaScript
HTML5
ゲーム制作
phina.js
phina.jsDay 16

phina.jsでブロック崩さぬを作ろう

More than 1 year has passed since last update.

本記事はphina.js Advent Calendar 2015の16日目の記事です。

phina.jsについて


こんにちは。

以前phina.jsの前身tmlib.jsでブロック崩さぬというゲームを作った者です。そこそこ小さい話題になり、ニュースサイトに取り上げられたりしました

今回はこのブロック崩さぬをphina.jsに移植しつつ、中身について非常に大ざっぱに解説をいれていこうという記事になります。ついでに2年前くらいに2日ぐらいで書いたきったないコードなので色々ツッコんでいこうと思います。あんまり参考にならなかったらごめんなさい。
詳しい機能の解説などはこのAdvent Calendarの他のメンバーがしていらっしゃるのでそれらをガンガン引用しつつ、実際にその機能を使ってどんな感じでゲームを作っていくかみたいなテーマで書こうと思います。というかそれぐらいしか書けないので勘弁してください。マジで

完成品はこちら

はい、早速ですが完成品がこちらです。まずは一回遊んでみましょう。多分おもしろいはずです。多分。

ブロック崩さぬ phina.js

キャプチャ.PNG

遊びましたか?どうでしたか?よかったら感想ください。
じゃあ解説していきましょう

ちなみにソースコードも全部見れるので、こいつを参照しながら読み進めてやってください
ソースコード

ゲームの概要

簡単にゲームの内容に触れておきます。
その名の通りブロック崩しの逆のゲームで、ブロックを崩さないゲームです。
ボールがポンポン降ってくるのでパドルを操作してボールを下に落としてやると、ブロックを守ってやることができます。ブロックを崩しまくってボールが上にいっちゃうとゲームオーバーです。ゲームが続くと弾幕みたいにボールがふってきたり、ボーナスタイムなどもあります。

要するにブロック崩しのパクリで本質的にゲーム性もブロック崩しとほぼ変わりません(多分)。パクリゲームの良いところは元になったゲームでゲーム性が保証されているところです。そのうえで見栄えと考え方を少し変えてやると低コストで違うプレイ感のゲームが作れたりするので初心者の人におすすめです。訴えられない程度で。

丸パクリはせずに、既存のアイデアに別のアイデアを乗せてパクるのがうまいパクリかたです。なんかの本にそう書いてありました。

tmlib.jsから移植

なにはともあれまずは移植していきます。tmlib.jsで作ったゲームの移植とかしない人は、一気に読み飛ばしちゃっても大丈夫です。GO!!

tmlib.js版のブロック崩さぬはこちら

では、移植するにあたってやったことを大ざっぱに箇条書きにしてみます。

  1. phina.jsのテンプレートをコピペする!!!
  2. tmlib.js版ブロック崩さぬからソースコードをコピペする!!
  3. tmlib.jsからphina.jsで名前が変わったやつの名前とか変える!!
  4. 以上!!!!

以上です。簡単です。
ぶっちゃけ作業量はゲームによってピンキリだと思いますが、俺の場合はこんな感じでした。

まずはphina.jsのサイトからphina.jsのテンプレートをコピってきます。

テンプレート

このテンプレートにtmlib.js版のブロック崩さぬのソースコードから必要なとこをコピって貼り付けます。
tmlib.js版のMainSceneの中身をテンプレ―トのMainSceneにコピったり各種クラスのソースなんかをコピったりするわけです。

次にtmlib.jsからphina.jsになって名前が変わったとこを変えていきます。
例えばクラスを作るときのやつがtm.defineからphina.defineに変わりました。
マウスポインタの位置を取得するヤツとかもpointingからpointerに変わってます。

こういうのを全部変換していくワケです。

とりあえず「tm」って書いてある所を「phina」に変えて、あとは実行してみてエラー吐いたら調べて変換って感じでやりました。
調べる時はphina.jsのAPI Documentationでそれっぽい名前のやつを検索していく感じです。

で、地道にコツコツ全部変えたら終わり!!以上!
つってもそんなに数ないので時間かかりません!たぶん!10分ぐらい!!

具体的にどこの何が変わったか全部書いていったら終わらないので、調べるか聞くか誰かがまとめてくれるのを待ちましょう!!ハイ次!!

タイトル画面

タイトル画面を見ていきます。
キャプチャ.PNG

ハイ、こんな感じです。画面に見えるものを箇条書きしてみます

  • ブロック
  • パドル
  • 「ブロック崩さぬ」ラベル
  • 「phina.js」ラベル
  • 「start」ラベル

ブロックとパドルはあとで説明します!


まずはラベルを見てみます。

    this.titlelabel= Label({
      text: 'ブロック崩さぬ',
      fill: 'white',
      fontSize: 83,
    }).addChildTo(this);
    this.titlelabel.setPosition(this.gridX.center(),this.gridY.center(-1));

    this.phinalabel= Label({
      text: 'phina.js',
      fill: 'white',
      fontSize: 53,
    }).addChildTo(this);
    this.phinalabel.setPosition(this.gridX.center(5),this.gridY.center(1));

    this.startlabel= Label({
      text: 'start',
      fill: 'white',
      fontSize: 53,
    }).addChildTo(this);
    this.startlabel.setPosition(this.gridX.center(),this.gridY.center(3));

ハイ、こんな感じです。上から「ブロック崩さぬ」「phina.js」「start」のラベルですね。
ザッと解説します。まずラベルを作る時のコードはこんな感じです。
var label = Label().addChildTo(親要素);
このLabel()のカッコの中に色んな値を渡してやることで手っ取り早くラベルを作ることができます。作ったラベルはlabelに入ります。
今回は

  • text:文字列
  • fill:文字の色
  • fontsize:文字の大きさ

を設定してるって感じですね
で、addChiltTo(this)でこのthisってヤツにラベルを追加されます。上のコードのthisはMainSceneのことなので、MainSceneにラベルが追加されて、文字が見えるようになります。オッケー?

次にいきます。こんな感じのことが書いてあります。
this.titlelabel.setPosition(this.gridX.center(),this.gridY.center(-1));
setPositionというやつで位置をセットしてます。こいつにX座標とY座標を入れることで位置をセットすることができます。
phina.jsの機能のGridを使ってXとYの座標を調整してますね。Gridについては@alkn203さんが解説しています。
【phina.js】Gridクラスを使いこなそう

要するにgridX.center()で画面の真ん中の位置を取ることができます。カッコの中に数字を入れると数字の分だけ真ん中からちょっとズレます。そうやって調整してます。オッケー?


「start」の文字がチカチカ消えたり出たりしてますね。
これはTweenerアニメーションを使っています。こいつを使うと簡単にアニメーションができるわけです。
Tweenerについての詳しい解説は@simiraaaaさんがしています。
[phina.js] Tweenerを使いこなそう! [Tweener 基本編]

コードはこんな感じ。

    this.startlabel.tweener
        .to({alpha:1}, 700,"easeInSine")
        .to({alpha:0}, 700,"easeInSine")
        .setLoop(true);

ザっと行きます。
this.startlabel.tweenerで今からtweener使うぜって言ってます
.to({alpha:1}, 700,"easeInSine") で700ミリ秒かけてalphaを1にします。alphaは透明度です。
.to({alpha:0}, 700,"easeInSine") で700ミリ秒かけてalphaを0にします。
.setloop(true) で上の2つを無限に繰り返します。よってチカチカしてるように見えます。

easeInSineというのはイージングという設定すると良い感じにアニメーションしてくれるエライ奴です。
詳しくは@simiraaaaさんの作ったコレを見てください。いろんな種類があります。
イージングリスト


タイトル画面をタッチするとゲームが開始します。

    // タッチでゲーム開始
    this.one('pointend', function() {
      this.startlabel.remove();
      this.phinalabel.remove();
      this.titlelabel.remove();
      this.startflg = true;
    });

this.one('pointend', function() {

こいつの中に入れたコードがタッチした時に走る感じです。
remove()というヤツを使ってラベルを消して、this.startflgという安易な名前のフラグにtrueが入れてます。
this.startflgにtrueを入れることでゲームが始まります。
なぜかというとこんな感じになってるからです。

  update: function(app) {

    if(this.startflg){
      this.gamemain(app);
    }
  },

updateというのは毎フレーム呼ばれて中身を毎回処理します。フレームというのは、えーっと。超高速で紙芝居が動いてるとイメージしてください、その一枚の紙がフレームです、そんで紙を変えるごとにupdateが動く感じです。多分そんな感じです。

そんでthis.startflgにtrueが入ってる時にゲームのメイン処理が動くようになってます。安易。


タイトル画面についてはこんな感じです。

ちなみにこのゲームは手抜きをするためにタイトル画面とゲーム画面とリザルト画面をぜんぶMainSceneに入れてるのでシーン遷移しません。シーン遷移については@daishi_hmrさんが解説しています
ManagerSceneでゲームの流れを管理しよう

パドルの生成

パドルを作ります。パドルというのはこいつです。こいつ

キャプチャ.PNG

こいつの間にボールを落としていくわけです。
パドルクラスのコード全文がコチラ

phina.define('Paddle', {
  superClass: 'RectangleShape',
  init: function() {
    this.superInit();

    this.y = 750;
    this.LWD = SCREEN_WIDTH / 2 -  barsize;
    this.RWD = SCREEN_WIDTH / 2 +  barsize;

  },

  update: function(app) {

      if (app.pointer.x + (barsize / 2)> 0 && app.pointer.x < SCREEN_WIDTH - (barsize / 2)) {
      this.LWD = app.pointer.x - barsize;
      this.RWD = app.pointer.x + barsize;

    }
  },

  draw: function(c) {
  c.globalCompositeOperation = "lighter";
  c.fillStyle = "hsla(0, 75%, 100%, 1)"
  c.fillRect(0, 0,this.LWD, 15);
  c.fillRect(SCREEN_WIDTH, 0,-(SCREEN_WIDTH - this.RWD) , 15);

  },

});

このパドルクラスをMainSceneのなかで実体化することで使えるようになります。

bar = Paddle().addChildTo(this);

こんな感じ

結構しょうもない実装をしてると思います。思い出しながら解説していきます。

どんな感じになってるかだいたいのイメージを解説すると

  • タッチしたポイントに向けて、左端と右端からパドルを伸ばす
  • パドルの長さはタッチしたポイントからプラスマイナスbarsizeの位置まで
  • updateでタッチポイントを取得して、毎フレーム左と右のパドルの長さを更新する

って感じです。


initの中がこんな感じになってます。ここで初期値を設定してます。

    this.y = 750;
    this.LWD = SCREEN_WIDTH / 2 -  barsize;
    this.RWD = SCREEN_WIDTH / 2 +  barsize;

SCREEN_WIDTHというのは画面全体の横の長さです
this.yがY方向の高さ。
this.LWDが左のパドルの長さ
this.RWDが右のパドルの長さ(ホントは右のパドルの左端の位置)
って感じです。変な名前のつけ方をしてます。

ちなみに、スマホで操作するときにパドルが指で隠れないようにY位置を高めに設定してます。


updateの中がこんな感じです

      if (app.pointer.x + (barsize / 2)> 0 && app.pointer.x < SCREEN_WIDTH - (barsize / 2)) {
      this.LWD = app.pointer.x - barsize;
      this.RWD = app.pointer.x + barsize;

パドルが画面の外に出ないようになんとなく調整しつつ、左と右のパドルの長さを更新してます。
app.pointer.xというやつでタッチした位置のx座標をとっています。
barsizeというやつが真ん中の穴の長さの半分の値です。わかりにくいですね。タッチした位置から右と左にbarsizeぶんだけいった所までパドルが伸びてくるという感じです。


最後にdrawというヤツがあります。これで描画をやっています。

draw: function(c) {
  c.globalCompositeOperation = "lighter";
  c.fillStyle = "hsla(0, 75%, 100%, 1)"
  c.fillRect(0, 0,this.LWD, 15);
  c.fillRect(SCREEN_WIDTH, 0,-(SCREEN_WIDTH - this.RWD) , 15);
},

c.fillStyle で色の設定をして、c.fillRectで四角形の描画をしてます。
ちなみにこんな感じで設定します

fillRect(X位置, Y位置, 横の長さ, 縦の長さ)

c.fillRectを2回書いてなんか怪しげな処理で左のパドルと右のパドルを作ってます。

とりあえずこれでタッチするとパドルがグイングイン伸びたり縮んだりします。

ブロックの生成

ブロックを作ります。
キャプチャ.PNG

というか昨日@phiさんが書いたブロック崩しのエントリで作り方を解説してます
phina.js でブロック崩しを作ろう

実際のブロックを生成するコードがこちら

    (BLOCK_NUM).times(function(i) {
      // グリッド上でのインデックス
      var xIndex = i%MAX_PER_LINE;
      var yIndex = Math.floor(i/MAX_PER_LINE);
      var angle = (360)/BLOCK_NUM*i;
      var block = Block(angle).addChildTo(blockGroup).setPosition(100, 100);

      block.x = gridX.span(xIndex) + BOARD_OFFSET_X;
      block.y = gridY.span(yIndex)+BOARD_OFFSET_Y;
    }, this);

@phiさんのエントリから丸々コードをパクっているのが分かりますね!この様に参考になるコードはどんどん取り入れるのが近道です。最初のうちはみんな許してくれます。たぶん。

軽く解説を入れていきます。ぶっちゃけ俺もあんまり理解してないですが。


(BLOCK_NUM).times(function(i) {

BLOCK_NUMの数だけカギカッコで囲んだ中身のコードを走らせて一個ずつブロックを作ってる印象を受けます。iは走らせた数だけカウントしてるっぽいですね

で、そのiを使ってグリッドで設定するための値を入れてます。MAX_PER_LINEはブロックの列の数が入ってます。

  // グリッド上でのインデックス
  var xIndex = i%MAX_PER_LINE;
  var yIndex = Math.floor(i/MAX_PER_LINE);

このangleというやつはブロッククラスに送って色を設定するために使います。良い感じにグラデーションしてくれます。

  var angle = (360)/BLOCK_NUM*i;

そしてブロックを生成します。生成するときにangleを渡しております。

  var block = Block(angle).addChildTo(blockGroup).setPosition(100, 100);

addChildTo(blockGroup)ってなってますね。これはグループです。作ったブロックをブロックグループに追加してるわけです。グループについては@alkn203さんが解説しています

【phina.js】グループ管理の基本テクニック

グループを作ってそこに入れておくと、あとでそのグループに入れたヤツらをまとめて操作できたりするので便利です。ちなみにグループはこんな感じで作ります

blockGroup = CanvasElement().addChildTo(this);

最後にブロックの位置を調整して終わりです。blockのxとyに値を入れることで位置を調整してます。

  block.x = gridX.span(xIndex) + BOARD_OFFSET_X;
  block.y = gridY.span(yIndex)+BOARD_OFFSET_Y;

だいたいそんな感じです!次!


ブロッククラスについても少し触れます。つってもほとんど何もやってません。

phina.define('Block', {
  superClass: 'RectangleShape',

    init: function(angle) {
        this.superInit({
        width: BLOCK_SIZE,
        height: BLOCK_SIZE/2,
        fill: 'hsl({0}, 80%, 60%)'.format(angle || 0),
        stroke: null,
        cornerRadius: 8,
      });
        this.vx = 0;
        this.vy = 0;

        //this.v = tm.geom.Vector2(0, 0);
    },

    update: function(app) {

        this.x += this.vx;
        this.y += this.vy;

    },


});

init: function(angle) ってなってますね。生成するときに渡したangleをこれで受け取れます。そんでthis.superInitってやつで色んな値の設定をやってますね。

this.vx,this.vyはブロックを動かすときのために使います。ゲームオーバーのときにブロックがふっとびますが、そのときのための布石です。

update: function(app) {

    this.x += this.vx;
    this.y += this.vy;

},

updateでこうなってますね。this.xとthis.yの値を変えることでブロックを移動することができまっす。この場合だと毎フレームthis.vxの数だけthis.xが動きます。初期値は0になってるので動きません。そんな感じっす。

このthis.x,this.yにthis.vx,this.vyを足して動かすというやりかたは、ボールを動かす時にも使ってます。

ゲームモードについて

ようやっとタイトル画面に出てる要素の解説が終わりました。ゲームのメイン処理の解説に入ろうと思います
MainSceneのupdateの中はこんなんなってます

 update: function(app) {

    if(this.startflg){
      this.gamemain(app);
    }
  },

このgamemainの中がゲームのメイン処理です。結構長いので小分けにして解説していきます。
ハイ、まず最初こんなことが書いてありますね。

  gamemain: function(app){

            switch(gameflg){

              省略

gameflgというやつが出てきました。こいつを変えることによってゲームのモードを変えることができます。1のときにノーマルモード、2のときに弾幕モードという感じです。わかりずらいですね。数字とモードの対応表がこんな感じです

1.ノーマルモード
2.弾幕モード
3.ボーナスモード
9.インターバル
0.ゲームオーバー

それぞれの数字にしたときそれぞれの処理が動きます。1,2,3はまあそのまんまです。9のインターバルはモードを切り替えるときに使ったりします。弾幕モードの玉が全部消えてからノーマルモードに移行する、みたいな時に一旦インターバル入れて玉が消えるまで待ってから移行する、みたいな処理をしてます。0はゲームオーバーした時の処理です

とりあえずノーマルモードから解説していきます

ノーマルモード

ノーマルモードは玉が一個ずつランダムで降ってくるモードです。gameflgに1が入ってるとこのモードが動きます。
キャプチャ.PNG

コードがこんな感じです。ドン

                case 1:
                    // ボールの生成
                    if (this.btimer % this.remit === 0) {
                            this.LR = rand(1);
                            this.vy = 10;
                            this.vx = (rand(200) -100) /10;
                            ball = Ball(this.LR,this.vx * this.speed,this.vy * this.speed,0).addChildTo(ballGroup);
                            this.btimer = 1;

                    }

                    //玉の出る間隔を狭める :狭めるスピードだんだん遅く
                    if(this.rtimer > this.rspeed && this.remit > 5){
                            this.remit -= 1;
                            this.rspeed += 3;
                            if(this.remit < 20){
                                this.rspeed += 10;
                            }
                            this.rtimer = 0;
                    }

                    //弾幕モードに移行
                    if(this.timer % 500 === 0){
                        gameflg = 2;
                        //次から通常モードの玉が早くなる
                        this.speed += 0.1
                        //玉の出る間隔 ジョジョに上がりにくくする



                    }
                    //入れ食いモードに移行
                    if(this.timer % this.ebouns === 0){
                        //インターバル挟んで入れ食いモードへ
                        gameflg = 9;
                        this.nextmode = 3;
                        this.ebouns += 500;
                        this.timer = 1;
                    }

                    ++this.timer;
                    ++this.rtimer;
                    ++this.btimer;

                break;

本格的にコードが汚くなってきた感じがあります。全体として何やってるかザッと箇条書きにしましょう

  • ボールを生成する
  • ボールの出る間隔をだんだん早くする
  • 一定時間たったら弾幕モードに移行する
  • 一定時間たったらボーナスモードに移行する
  • タイマーを加算する

こんな感じっすね。

ちなみにbtimerとかremitとか、パラメータの初期値は全部initのsetparam()で初期化してます。
this.setparam();


とりあえずボールを生成するところから見てみます

                    if (this.btimer % this.remit === 0) {
                            this.LR = rand(1);
                            this.vy = 10;
                            this.vx = (rand(200) -100) /10;
                            ball = Ball(this.LR,this.vx * this.speed,this.vy * this.speed,0).addChildTo(ballGroup);
                            this.btimer = 1;

                    }

this.btimerというやつがボールを生成するためのタイマーですね。毎フレーム加算してthis.remitと同じ値になるとボールを生成する処理が動くようになってます。わかりにくい名前ですね。せめてthis.balltimerとthis.ballremitくらいにしたほうがいいです。

で、ボールを生成する時に必要なパラメータを設定してボールを生成してますね
this.LRはボールを右から出すか左から出すかを決めるやつです。分かりにくい名前です。this.LRが0だと左、1だと右からボールが出ます。randとかいう自作の関数でランダムで値を入れるようにしてます。1を入れると0か1を返します。

this.vx,this.vyはボールの速度を入れてます。速度というか、向きみたいな感じです。this.vxにランダムの値を入れることでボールをバラバラに発射するようにします。実際の速度はボールを生成するときにthis.speedを掛けることで設定してます。

で、このパラメータを入れてボールを生成します。生成したボールはボールグループに入れておくようにします。あとで使います。


ボールの出る間隔をだんだん早くする処理です。ボールがどんどん出て来るようになってゲームの難易度を徐々に上げていくための処理です。

                    //玉の出る間隔を狭める :狭めるスピードだんだん遅く
                    if(this.rtimer > this.rspeed && this.remit > 5){
                            this.remit -= 1;
                            this.rspeed += 3;
                            if(this.remit < 20){
                                this.rspeed += 10;
                            }
                            this.rtimer = 0;
                    }

基本的にボールの生成の時に使ったthis.remitの値を変えるための処理です。this.rtimerとかいうタイマーがthis.rspeedを越えるとthis.remitの値が1減ります。this.remitが減ると、ボールが出て来るのが早くなるわけです。

this.rspeedの値が3ずつ増えてます。こうすることで、this.remitが徐々に減りにくくなります。なにがしたいかというと、最初は難易度が上がるスピードが早め、難易度が上がってからは難易度が上がるスピードが徐々に緩やかになる。みたいなことをやりたかったんだと思います。多分。あんまり記憶が確かではないです。this.remitが20切ってからはthis.rspeedがさらに10増えるようにしてます。

あとthis.remitは5以下になったらそれ以上減らさないようにしてますね。


弾幕モードに移行するときの処理です

                    //弾幕モードに移行
                    if(this.timer % 500 === 0){
                        gameflg = 2;
                        //次から通常モードの玉が早くなる
                        this.speed += 0.1
                    }

this.timerが500の倍数になったときに弾幕モードに移行します。
gameflgに2を入れることで弾幕モードに移行できます。
ついでにthis.speedに0.1加算することで、弾幕モードに移行する度にノーマルモードの玉のスピードが上がるようになってます。


ボーナスモードに移行するときの処理ですね。タイマーがthis.ebounsとかいう変数で割り切れる数になるとボーナスモードになります。もはや日本語でも英語でもないっす

                    //入れ食いモードに移行
                    if(this.timer % this.ebouns === 0){
                        //インターバル挟んで入れ食いモードへ
                        gameflg = 9;
                        this.nextmode = 3;
                        this.ebouns += 500;
                        this.timer = 1;
                    }

gameflgには9を入れます。これで一旦インターバルに入ってボールが消えるのを待ちます。
this.nextmodeはインターバルのあとに移行するモードです。3が入ってるので、インターバルのあとボーナスモードに入るって感じになります。あんまりスマートな処理じゃないです

this.ebounsに500加算することで徐々にボーナスタイムが出にくくなるようになってます。
最後にthis.timerをリセットしてますね

弾幕モード

弾幕モードです。ボールがめっちゃ降ってきます。gameflgに2が入ってると動きます。
キャプチャ.PNG

コードはこんな感じ

                //弾幕モード
                case 2:

                    // 玉の生成(難易度をどんどん上げる)
                    if (this.dantimer % 2 === 0) {

                            this.danvy = 4;
                            ball = Ball(this.LR,this.danvx,this.danvy,1).addChildTo(ballGroup);
                    }

                    if(this.danvx_remit > this.danvx){
                        this.danvx += this.danspeed;
                    }
                    //次の弾幕。玉の出る場所を反転
                    else{
                        if(this.LR == 0){
                            this.LR = 1;
                        }
                        else{
                            this.LR = 0;
                        }
                        this.danvx = 0;

                        this.dancnt++;
                    }

                    //弾幕の終わり
                    if(this.dancnt >= this.danend){
                        gameflg = 9;
                        this.nextmode = 1;
                        this.dancnt = 0;
                        this.danend++; //弾幕回数がだんだん上がる
                        this.danspeed += 0.01;
                    }

                    this.dantimer++;

                break;

ザッと説明すると、2フレームに一回弾を発射して、発射する度に発射の角度を変えていきます。角度のリミットがきたら反対側からまた発射します。リミットを超えるまでが一回の弾幕として、弾幕の総回数を超えるまでこれを繰り返す感じです


弾の生成がこれです。this.danvx,this.danvyというのが弾の速度です。

                    // 玉の生成(難易度をどんどん上げる)
                    if (this.dantimer % 2 === 0) {

                            this.danvy = 4;
                            ball = Ball(this.LR,this.danvx,this.danvy,1).addChildTo(ballGroup);
                    }


this.danvxがthis.danvx_remitを超えるまでthis.danvxを加算していきます。これで弾を発射する角度が変わります。で、リミットを超えたらthis.LRを反転して弾幕カウントを加算します

                    if(this.danvx_remit > this.danvx){
                        this.danvx += this.danspeed;
                    }
                    //次の弾幕。玉の出る場所を反転
                    else{
                        if(this.LR == 0){
                            this.LR = 1;
                        }
                        else{
                            this.LR = 0;
                        }
                        this.danvx = 0;

                        this.dancnt++;
                    }

弾幕カウントがthis.danendを超えると弾幕モードを終了します。
終了するついでにthis.danendとthis.danspeedを加算します。こうすると次の弾幕モードのときに、弾幕回数が増えて弾幕ボールのスピードが上がります。

                    //弾幕の終わり
                    if(this.dancnt >= this.danend){
                        gameflg = 9;
                        this.nextmode = 1;
                        this.dancnt = 0;
                        this.danend++; //弾幕回数がだんだん上がる
                        this.danspeed += 0.01;
                    }

ボールクラス

ノーマルモードと弾幕モードで使うボールクラスの解説をします。結構長いので、ハショりつつ解説します。

phina.define('Ball', {
  superClass: 'CircleShape',

  init: function(LR,vx,vy,ballflg) {
    this.superInit({
      radius: BALL_RADIUS,
      fill: '#eee',
      stroke: null,
      cornerRadius: 8,
    });
    this.color = "hsla(10, 75%, 100%, 1)";
    this.flg=0;

    this.ballflg = ballflg;

    if(LR == 0){
            this.x = 50;
            this.vx = vx;
    }
    else{
            this.x = 620;
            this.vx = vx * -1;
    }
    this.y =320;
    this.vy = vy;

  },

  update: function(app) {

      //バーとの当たり判定
      if(this.y >= bar.y - this.vy && this.y <= bar.y +5){
          //穴に入った
          if(this.x >= app.pointer.x - barsize && this.x <= app.pointer.x + barsize){
              if(this.flg==0){
                  point += 100 + combo * 10;
                  combo++;
                  this.flg = 1;

              }
          }
          else{
              combo = 0;
              this.vy *= -1;
              if(this.ballflg == 0){
              this.color = "hsla(18, 100%, 70%, 1)";
              }
              else if(this.ballflg == 1){
              this.color = "hsla(360, 75%, 50%, 0.8)";
              }

          }
      }

      //ブロックとの当たり判定
      var bc = blockGroup.children;
      var self = this;
      bc.each(function(block) {
        //  if(clash(self,block)){
          if(self.hitTestElement(block)){

              self.vy *= -1;
              self.vx *= -1;
              block.remove();
              self.ballflg++;
             }

      });

      //壁にあたったら反転
      if(this.x < 5 || this.x > SCREEN_WIDTH){
          this.vx *= -1;
      }

      //下に出たら消える
      if(this.y > SCREEN_HEIGHT){
          this.remove();
      }

      //玉が上にでた
      if(this.y < 0){
          //point += 100;
          //ゲームオーバーフラグ
          gameflg = 0;
          this.remove();
      }

      //ブロックに2回あたったら消える
      if(this.ballflg >= 2){
          this.remove();

      }

      this.y += this.vy;
      this.x += this.vx;

  },

  draw: function(c) {
  c.globalCompositeOperation = "lighter";
  c.fillStyle = this.color;
  c.fillCircle(0, 0, BALL_RADIUS);
  },

});

そこそこ長いです。ポイントを箇条書きにします

  • ボールの出現場所設定
  • ボールフラグ
  • パドルとの当たり判定
  • ブロックとの当たり判定
  • 画面外にでたとき

だいたいこんなもんです。


ボールの出現場所を設定する処理がこんな感じです。ボールを作るときに「LR」とかいうヤツを渡しました。あれを使います

    if(LR == 0){
            this.x = 50;
            this.vx = vx;
    }
    else{
            this.x = 620;
            this.vx = vx * -1;
    }

LRが0のときは左の方から、1のときは右の方から弾が出るようにします。1のとき、vxに-1を掛けることで弾の出る方向を反転します。要するに玉が右から左に動くようになります


ボールフラグはボールが何回ブロックに当たったかを記憶します。ブロックにあたった時に加算されます。
this.ballflg = ballflg;

んで、ブロックに2回当たるとボールが消えます。

      if(this.ballflg >= 2){
          this.remove();
      }

ちなみにノーマルモードの時ballflgに0。弾幕モードの時はballflgは1を渡しています。
こうすると、弾幕モードの時だけはブロックに一回あたっただけでボールが消えます。弾幕がブロックにあたって更に跳ね返ってきたら地獄絵図になっちゃうのでこうしてるわけです。


パドルとの当たり判定の解説です。パドル周りは結構ムリヤリな実装してるのでウケます。

      //バーとの当たり判定
      if(this.y >= bar.y - this.vy && this.y <= bar.y +5){
          //穴に入った
          if(this.x >= app.pointer.x - barsize && this.x <= app.pointer.x + barsize){
              if(this.flg==0){
                  point += 100 + combo * 10;
                  combo++;
                  this.flg = 1;

              }
          }
          else{
              combo = 0;
              this.vy *= -1;
              if(this.ballflg == 0){
              this.color = "hsla(18, 100%, 70%, 1)";
              }
              else if(this.ballflg == 1){
              this.color = "hsla(360, 75%, 50%, 0.8)";
              }

          }
      }

汚いコードですね。ざっと解説します。

barというやつにパドルの実体が入ってます。こいつはグローバルで宣言してるのでどこでも使えます。で、barのY位置にボールが到達したらパドルの間の穴の位置にボールが入ってるかどうか調べます。どうやって調べてるかというと、またタッチのポイントからbarsizeを足したり引いたりしてます。そんでその中にballのx位置が入ってるか見てます。barsizeもグローバル宣言されているわけです。

穴に入ってたらスコアとコンボ数を上げる処理

          if(this.flg==0){
              point += 100 + combo * 10;
              combo++;
              this.flg = 1;

          }

パドルに当たってたらボールを反転させてコンボ数もリセットします。

      else{
          combo = 0;
          this.vy *= -1;
          if(this.ballflg == 0){
          this.color = "hsla(18, 100%, 70%, 1)";
          }
          else if(this.ballflg == 1){
          this.color = "hsla(360, 75%, 50%, 0.8)";
          }

      }

this.vyに-1を掛けることでy方向の移動が反転するわけです。
ついでにボールの色も変えてます。1回当たるとオレンジ、2回当たるとレッドになります。色を変えることで、「こいつは1回当たったボールだな」とプレイヤーにわかりやすくなります。ついでにミスをしたことが視覚的にわかりすくなるので焦ります。


ブロックとの当たり判定を解説します。ついにブロックグループの出番です。

      //ブロックとの当たり判定
      var bc = blockGroup.children;
      var self = this;
      bc.each(function(block) {

          if(self.hitTestElement(block)){

              self.vy *= -1;
              self.vx *= -1;
              block.remove();
              self.ballflg++;
             }

      });

var bc = blockGroup.childrenとやるとにbcに今までグループに入れたブロック達が入ります。そんでbc.each(function(block) ってやつで、グループに入れた全部のブロックを一個ずつ取得してなんかしたりできるわけです。blockってやつに取得したブロックの実体が入ってます。

ほんでhitTestElementというヤツでボールとブロックが接触してるかどうかを見てます。接触してたらボールの移動方向を反転して、ブロックを消して、ボールフラグを加算します。


画面外に出た時の処理はこんな感じです

      //壁にあたったら反転
      if(this.x < 5 || this.x > SCREEN_WIDTH){
          this.vx *= -1;
      }

      //下に出たら消える
      if(this.y > SCREEN_HEIGHT){
          this.remove();
      }

      //玉が上にでた
      if(this.y < 0){
          //ゲームオーバーフラグ
          gameflg = 0;
          this.remove();
      }

壁にあたったら反転、下にいったらボールを消す。上にいったらゲームオーバーフラグを立ててボールを消す、って感じです。以上!

ボーナスモード

ゲーム進めたら突然出てくる入れ食いボーナスボールについての解説です。ボールがジャラジャラ落ちてきます。

キャプチャ.PNG

関係ないけどボーナス中にBGM入れるとしたら軍艦行進曲にしようと思ってました。昔のパチンコ屋で流れてたやつです。

コードはこちら

                //入れ食いモード
                case 3:
                    barsize = 120;
                    this.mode.text = "入れ食い\nボーナス!!!"

                    if (this.etimer % 2 === 0) {
                            this.LR = rand(1);
                            this.evy = 0.5;
                            this.evx = (rand(80)) /10;
                            eball = eBall(this.LR,this.evx,this.evy).addChildTo(ballGroup);
                    }
                    this.etimer++;

                    if(this.etimer > 500){

                        this.etimer = 1;
                        this.mode.text = "";
                        barsize = 80;
                        gameflg = 9;
                        this.nextmode = 1;


                    }


                break;

ここまでくるとあんまり解説することもない気がします。一応いっときます。
barsize = 120ってなってますね。入れ食いボーナス中はパドルの穴の大きさをちょっと大きくして「入れ食い感」を出してます
this.mode.textは画面に”入れ食いボーナス!!”って表示させるためのやつです。initの時にこっそり作ってます

弾幕モードと同じ感じで2フレームに一回入れ食いボールを生成してます。this.evy,this.evxに値を入れて速度とか方向を決めます。入れ食いボーナス中は入れ食いボールという特別なボールを生成してます。eBallってやつです。

this.etimerが500を超えるとボーナスモードを終了する処理が動きます。テキストを消してパドルの長さを戻してインターバルに入ったあとノーマルモードに移行します。

入れ食いボールクラス

入れ食いボーナス中は入れ食いボールという特別なボールを使います。主な違いはボールが重力によって自由落下してるように見えるとこです。ブロック崩しと違ってボールを上に飛ばさなきゃいけないワケではないので、じゃあこういうのもできるじゃんと思って作ってみました。

phina.define('eBall', {
  superClass: 'CircleShape',

      init: function(LR,vx,vy) {
          this.superInit();
          this.v = phina.geom.Vector2(0, 0);

          if(LR == 0){
                  this.x = 30;
                  this.v.x = vx;
          }
          else{
                  this.x = 620;
                  this.v.x = vx * -1;
          }
          this.y =280;
          this.vy = vy;

          this.flg = 0;

          this.timer = 1;

          this.color = "hsla(100, 100%, 100%, 1)";

      },

      update: function(app) {
          this.v.y += this.vy;

          //壁にあたったら反転
          if(this.x < 5 || this.x > SCREEN_WIDTH){
              this.x = 5
              this.v.x *= -1;
              this.vx *= -1;
          }

          //バーにあたった
          if(this.y + 10 >= bar.y && this.y + 10<= bar.y + this.v.y ){
              //穴に入った
              if(this.x >= app.pointer.x - barsize && this.x <= app.pointer.x + barsize){
                  if(this.flg==0){
                      point += 50 + combo;
                      combo++;
                      this.flg = 1;

                  }
              }
              else{
                  this.v.y *= -0.6;

              }
          }
          this.position.add(this.v);

          //時間が立ったら変色 > 消える
          if(this.timer > 130){
              this.color = "hsla(360, 75%, 50%, 0.8)";
              if(this.timer > 160){
                  this.remove();
              }

          }

          //下に出たら消える
          if(this.y > SCREEN_HEIGHT){
              this.remove();
          }

          this.timer++;
      },

      draw: function(c) {
      c.globalCompositeOperation = "lighter";
      c.fillStyle = this.color;
      c.fillCircle(0, 0, BALL_RADIUS);
      },
});

中身はボールクラスとだいたい同じです。this.vというやつで移動を処理してるところと、時間がたつとボールが消える所が違います。

this.v = phina.geom.Vector2(0, 0);
って感じでthis.vを定義してます。このthis.vを使ってボールの移動をしてます。それっぽく落下したり跳ねたりするようにするわけです。たしかそうしました。あんまり覚えてないです。

こんな感じで、this.v.yにthis.vyを加算して
this.v.y += this.vy;

で、多分これで位置の更新をしてます。
this.position.add(this.v);

記憶が曖昧なのは多分どっかからコピってきたコードだからです。人のコードをパクってばかりいるとこういうことになるので気を付けましょう。
考え方としては、this.vyがいわば重力です、この重力を常にthis.v.yに加算し続けます。例えばthis.v.yが上方向に向かって進んでいるとしても、重力を加算しまくってるのでいずれ重力の向かう方向に落ちていきます。こうすることで、ボールがパドルに落ちて跳ねたあと、また自然に落ちてくるように見えます。たぶんそんな感じです!たぶん!!!

パドルにボールが落ちた時にこんな処理をしてます。
this.v.y *= -0.6;
this.v.yをちょろっと反転することでボールが跳ねているように見せてるわけです。-0.6を-1に変えるとず~っと跳ね続けます

で、時間がたったらボールが消えるようにしてます

          //時間が立ったら変色 > 消える
          if(this.timer > 130){
              this.color = "hsla(360, 75%, 50%, 0.8)";
              if(this.timer > 160){
                  this.remove();
              }

          }

消えるちょっと前にボールを赤くして「もうすぐ消えるぜ」っていう告知をさせてます。

スコアとコンボ表示

左上にスコアが表示されてますね。
キャプチャ.PNG

あとコンボ数とかも表示されてます。
キャプチャ.PNG

こいつらはMainSceneのinitで作ってます。タイトル画面でスコアが見えないのはtextに何も入ってないからです。

    //ラベル
    this.score = Label({
      text: '',
      fill: 'white',
      fontSize: 32,
    }).addChildTo(this);
    this.score.x = 80;
    this.score.y =20;

    this.Combolabel = Label({
      text: '',
      fill: 'hsla(123, 80%, 50%, 8.0)',
      fontSize: 40,
    }).addChildTo(this);
    this.Combolabel.x = 120;
    this.Combolabel.y =600;

そしてgamemainの中でこんな感じで更新をしてます。

           //ラベル更新
            this.score.text = "SCORE:" + point;
            if(combo > 0){
                this.Combolabel.text = combo + " Combo!";
            }
            else{
                this.Combolabel.text = "";
            }

this.score.textにスコアの文字列を入れると、スコアが見れます。
コンボテキストはコンボが0の時は見えないようにしてます。pointというのが実際のスコアポイントが入ってるヤツですね。comboにコンボ数が入ってます


スコアとコンボの加算はボールクラスで、ボールが穴に入った時にやってます

                  point += 100 + combo * 10;
                  combo++;

こんな感じで穴に入るとスコアが上がります。基本のポイントが100点で、更にコンボ数×10ポイント入ります。コンボ数が上がるほどもらえるポイントがどんどん増えていくワケです。

ゲームオーバー

ボールが上に行くとゲームオーバーです。
image

ゲームオーバー時の処理はこんな感じ

                //ゲームオーバー時の処理
                case 0:
                    this.Combolabel.text = "";
                    combo = 0;

                    this.mba = ballGroup.children;
                    this.mba.each(function(ball) {
                        ball.remove();
                     });

                    if(this.gameovertimer==0){
                        this.mbc = blockGroup.children;
                        this.mbc.each(function(block) {
                                block.vy = rand(40) - 10;
                                block.vx = rand(20) - 10;
                                block.rotation = 30;
                            });
                    }

                    if (this.gameovertimer > 40) {
                        this.mbc.each(function(block) {
                            block.remove();
                        });
                    }

                    if (this.gameovertimer > 70) {
                        //app.replaceScene(EndScene());
                        this.score.tweener.clear()
                                     .to({x:SCREEN_WIDTH/2,y:400,scaleX:1.5,scaleY:1.5}, 150,"easeInSine")



                         this.gameoverlabel.tweener.clear()
                                      .to({y:150,scaleX:2,scaleY:2}, 200,"easeInSine")

                    }

                    ++this.gameovertimer;
                break;

まとめると
1. ボールを消す
2. ブロックを吹っ飛ばす
3. ブロックを消す
4. ゲームオーバーラベルとスコアラベルをズイッと出す

こんな感じです


ボールを消す時の処理です。ついでにコンボラベルも消してます。

                    this.Combolabel.text = "";
                    combo = 0;

                    this.mba = ballGroup.children;
                    this.mba.each(function(ball) {
                        ball.remove();
                     });

ballGroup.childrenからボール達を取得して全部removeしてますね。そんだけです。


ブロックを吹っ飛ばす処理です。なぜブロックを吹っ飛ばすかというと、なんかそのほうがゲームオーバーっぽいからです

                    if(this.gameovertimer==0){
                        this.mbc = blockGroup.children;
                        this.mbc.each(function(block) {
                                block.vy = rand(40) - 10;
                                block.vx = rand(20) - 10;
                                block.rotation = 30;
                            });
                    }

this.gameovertimerというのはゲームオーバーしてからの時間です。時間の経過によって処理を入れていきます。やっつけの実装です

blockGroupからブロックを取得してblock.vy,block.vxにランダムの値を入れてます。こうするとブロックがテキトーな方向に飛んでいきます。block.rotationに値を入れるとブロックの角度が変わります。ちょっと傾けて飛ばすために30入れてます。


this.gameovertimerが40を超えたらブロックを消します。

                    if (this.gameovertimer > 40) {
                        this.mbc.each(function(block) {
                            block.remove();
                        });
                    }

this.gameovertimerが70を超えたらゲームオーバーラベルとスコアをTweenerアニメーションでズイ~っとアップします
ゲームオーバーラベルはinitの時に作って画面外の上の方に隠してます。アニメーションすることで下に降りてきて見えるようになります。原始的です。

                    if (this.gameovertimer > 70) {
                        this.score.tweener.clear()
                                     .to({x:SCREEN_WIDTH/2,y:400,scaleX:1.5,scaleY:1.5}, 150,"easeInSine")

                         this.gameoverlabel.tweener.clear()
                                      .to({y:150,scaleX:2,scaleY:2}, 200,"easeInSine")

                    }

ということで、解説はこれで終わりです

おわりに

ながかったーーーーー!!お付き合いいただきありがとうございます。

まあこんな感じで俺みたいなモンでも簡単にゲームを作れるのがphina.jsの魅力です。
気になった人はとりあえずゲームを作ってみましょう。作ったらtwitterか何かで公開してみんなに見てもらいましょう。

twitterなどで公開するときはツッコミどころを用意しておくと見てもらいやすいかもです。ツッコむためにコメント付きで拡散してくれたりしますから。ブロック崩さぬのツッコミどころは「ブロック崩さぬ」というタイトルです。はい。

ということで終了です。さようならーーーーーーーー!!!終了ーーーーーーーーーーーーーー!!!!さようならーーーーーーーーーーーーー!!!!!

あ、よかったらサイトにゲーム上げてるのでやってみてください。宣伝です。
かちゃコム

ではさようならーーーーーーーーーーーーーーーーーーー!!!!!