サイトアイコン ほろほろりドットコム

[phina.js]オブジェクトの操作 -位置、移動、衝突・クリック判定など- について

ゲーム クリックゲーム 星 連鎖 チェイン chain

[phina.js]オブジェクトの操作 -位置、移動、衝突・クリック判定など- について

今回の記事は、phina.jsでの図形オブジェクトやLabelオブジェクトなどのクラスが継承している、Object2Dクラスについてです。主にこのクラスの中のプロパティやメソッドを読み解きつつ、このクラスの事と、このクラスを使って出来る事を書き連ねてみようと思います。もしphina.jsが分からない方は、[phina.js]基本 — テンプレートについてをご一読いただくことをおすすめいたします。また、図形オブジェクトについては、[phina.js]基本 — 色んな図形オブジェクトについても書いてますので宜しければご覧ください。ラベルオブジェクトについても[phina.js]基本 — ラベルオブジェクトについてを書いていますのでご参考までに。

※2018年9月26日 加筆・修正を行いました。

※2018年10月18日 加筆・修正を行いました。

目次

 

準備

コーディングは、RunstantRunstant liteを使って試されることをおすすめします。

一応雛型として置いておきます。

<script src='https://cdn.jsdelivr.net/gh/phi-jp/phina.js@0.2.3/build/phina.js'></script>
// グローバル領域に展開
phina.globalize();

/*
 * メインシーン
 */
phina.define("MainScene",{
  // 継承
  superClass:"DisplayScene",
  // コンストラクタ
  init: function(){
    // 親クラスの初期化
    this.superInit();
    // ここに処理を書く
  },
  // 更新
  update: function(){
  
  },
});

/*
 * メイン処理
 */
phina.main(function(){
   // アプリケーションを生成
   var app = GameApp({
     // MainSceneから開始
     startLabel: "main",
   });
   // fps表示
   //app.enableStats();
   // 実行
   app.run();
});

Object2Dクラスのプロパティやメソッド一覧

プロパティ

プロパティ名 説明 デフォルト値
x x座標の値が入ります。 0
y y座標の値が入ります。 0
position x, yが入ります。  
scaleX 横幅の大きさの比率の値が入ります。 1
scaleY 高さの大きさの比率の値が入ります。 1
scale scaleX, scaleYが入ります。  
rotation 回転させる角度(ラジアンではなく度)が入ります。 0
originX 原点のx方向の値が入ります。 0.5
originY 原点のy方向の値が入ります。 0.5
origin originX, originYが入ります。  
width 横幅の値が入ります。 64
height 高さの値が入ります。 64
radius 半径の値が入ります。 32
boundingType 形状タイプが入ります。タイプの種類は、’rect’か’circle’です。
※このプロパティについてはここで少し詳しく書きます。
‘rect’
top 最上部のy座標値が入ります。  
right 右端のx座標値が入ります。  
bottom 最下部のy座標値が入ります。  
left 左端のx座標値が入ります。  
centerX 中心のx座標値が入ります。現在、読み取り専用の様です。  
centerY 中心のy座標値が入ります。現在、読み取り専用の様です。  
interactive オブジェクトに対してクリックやタッチイベントがあった時、イベントを受け付けるかというのを真偽値で指定できます。 false
補足

単一の値が格納されているプロパティ(xやyなど)はそのまま、
object.x = 100;
のように設定できますが、複数の値が格納されているプロパティ(positionやscaleなど)は、
object.position.set(100,100);
のように、set()メソッドを使うか、それぞれのプロパティに対応した後述のメソッドを使うかしないと設定できません。

メソッド

衝突判定系
hitTest(x, y)
引数で渡された(x, y)の点とオブジェクトとの衝突判定を行います。オブジェクトの形状のタイプによって、後述のhitTestRectかhitTestCircleに自動的に振り分けて判断してくれます。返り値は真偽値です。形状タイプがnoneの場合、trueが返されるようです。

 

[引数]
x:衝突判定したい点のx座標を指定します。
y:衝突判定したい点のy座標を指定します。

hitTestRect(x, y)
オブジェクトが矩形タイプのオブジェクトの場合、引数で渡された(x, y)の点との衝突判定を行います。返り値は真偽値です。

 

[引数]
x:衝突判定したい点のx座標を指定します。
y:衝突判定したい点のy座標を指定します。

hitTestCircle(x, y)
オブジェクトが円形タイプのオブジェクトの場合、引数で渡された(x, y)の点との衝突判定を行います。返り値は真偽値です。

 

[引数]
x:衝突判定したい点のx座標を指定します。
y:衝突判定したい点のy座標を指定します。

hitTestElement(elm)
引数で渡された他の要素とオブジェクトとの衝突判定を行います。返り値は真偽値です。

 

[引数]
elm:オブジェクトとの衝突判定を行う他のオブジェクトを指定する。

クリック・タッチイベント有効化
setInteractive(flag[, type])
オブジェクトのinteractiveプロパティを設定できます。

 

[引数]
flag:真偽値でinteractiveを有効にするかを指定します。
type:”rect”か”circle”の形状のタイプを指定できます。省略可能です。通常、省略した場合はそのオブジェクトのboundingTypeプロパティがそのまま指定され、オブジェクト全体がイベントの判定範囲になります。ですが、例えば矩形タイプのオブジェクトの時にこの引数に”circle”を指定した場合、オブジェクトの本来の形状を無視して、オブジェクトの中心点からradiusプロパティの半径値で描かれるの円形内がイベントの判定範囲になります。

補足

言葉で説明しづらいのでちょっとしたサンプルを作ってみました。矩形オブジェクトをクリック有効にしたのと一緒に、typeに’circle’を指定しました。クリックされたら一瞬clickの文字が出るようにしたので、実際に触ってみてクリックの判定範囲を確かめてみてください。

ソースコード

// 判定範囲の目印用
this.circle = CircleShape({
}).addChildTo(this).setPosition(this.gridX.center(), this.gridY.center());

// クリック範囲判定をする矩形オブジェクト
this.rect = RectangleShape({
    width: 250,
    height: 200
}).addChildTo(this).setPosition(this.gridX.center(), this.gridY.center());
this.rect.alpha = 0.4;

// クリック有効化
this.rect.setInteractive(true, "circle");
// クリックした後の処理
this.rect.onpointstart = function () {
    const label = Label({ text: "click" }).addChildTo(this).setPosition(0, -200);
    label.update = () => {
        if (label.alpha > 0) {
            label.alpha -= 0.1;
        } else {
            label.remove();
        }
    };
};

ちなみに、上のソースコードのクリック有効化のところは、下のように書いても同じ動作をします。

this.rect.boundingType = "circle";
this.rect.setInteractive(true);
プロパティ値設定系
setX(x)
x座標値を指定します。

 

[引数]
x:x座標値を指定します。

setY(y)
y座標値を設定します。

 

[引数]
y:y座標値を指定します。

setPosition(x, y)
x, y座標値をまとめて設定します。

 

[引数]
x:x座標値を指定します。
y:y座標値を指定します。

setRotation(rotation)
回転させる角度を設定します。

 

[引数]
rotation:回転させる角度を(ラジアンではなく度)指定します。

setScale(x, y)
x, yそれぞれのスケールをまとめて設定します。

 

[引数]
x:横幅の大きさの比率を指定します。
y:高さの大きさの比率を指定します。

setOrigin(x, y)
x, yそれぞれの原点をまとめて設定します。

 

[引数]
x:横方向の原点を指定します。
y:縦方向の原点を指定します。

setWidth(width)
横幅を設定します。

 

[引数]
width:横幅を指定します。

setHeight(height)
高さを設定します。

 

[引数]
height:高さを指定します。

setSize(width, height)
横幅と高さをまとめて設定します。

 

[引数]
width:横幅を指定します。
height:高さを指定します。

setBoundingType(type)
形状のタイプを設定します。

 

[引数]
type:”rect”か”circle”の形状タイプを指定します。

移動操作系
moveTo(x, y)
setPositionと同じくx, y座標値をまとめて設定します。

 

[引数]
x:x座標値を指定します。
y:y座標値を指定します。

moveBy(x, y)
現在の座標からの移動する距離をx, yでまとめて設定します。

 

[引数]
x:現在のx座標からのx方向の移動距離を指定します。
y:現在のy座標からのy方向の移動距離を指定します。

不明
globalToLocal(p)
何するメソッドなのか正直わかりません。すみません。

サンプルゲーム

ソースコード

//グローバルに展開
phina.globalize();

// 定数
const CRUSH_STARS = 10; // 星をクリックした時に出る小さい星
let SCORE = 0;        // スコア
let VELOCITY_Y = 0;   // Yの移動の速さの変化
let HIGHEST_CHAIN = 0;   // 最大連鎖数
const MY_URL = "このページのURL";
/*
 * メインシーン
 */
phina.define("MainScene", {
    // 継承
    superClass: "DisplayScene",
    // コンストラクタ
    init: function () {
        // 親クラス初期化
        this.superInit();
        // 背景
        this.backgroundColor = "white";
        // 星グループを作成
        this.group = DisplayElement().addChildTo(this);
        // 砕けた後の星グループを作成
        this.crushGroup = DisplayElement().addChildTo(this);
        // 次の星を生成するまでのカウント
        this.count = 20;
        // 連鎖カウント
        this.chainCount = 0;
        // 落ちる速度を初期化
        VELOCITY_Y = 0;
        // スコアの初期化
        SCORE = 0;
        // 最大連鎖数の初期化
        HIGHEST_CHAIN = 0;
        // スコアラベル
        this.label = Label({
            text: 'SCORE:' + SCORE.padding(10, '0'),
            fontSize: 39,
            fill: "black",
        }).addChildTo(this).setPosition(this.gridX.span(5), this.gridY.span(1));
    },
    // 更新
    update: function () {
        // スコアラベル
        this.label.text = 'SCORE:' + SCORE.padding(10, '0');

        // 次の星を生成するまでのカウント
        this.count--;

        // 回避用
        const self = this;

        // カウントが0以下になったら星を生成する
        if (this.count < 0) {
            // 次の星を生成するまでのカウントをランダムに指定
            this.count = Math.randint(10, 20);

            // 星の生成
            const color = "hsla({0}, 100%, 50%, 0.8)".format(Math.randint(0, 360));
            const x = Math.randint(0, this.gridX.width);
            const star = Star(color, x, 0).addChildTo(this.group);
            // クリックの有効化
            star.setInteractive(true);
            star.onpointstart = function () {
                this.alpha = 0.3;
                this.removeCount--;
                this.setInteractive(false);
                SCORE += 10 * (1 + VELOCITY_Y);

                // 砕けた星を生成
                self.create(this.x, this.y);
            };
            // 段々星が落ちる速度を加速させる
            VELOCITY_Y += (VELOCITY_Y > 10) ? 0 : 0.2;
            this.group.children.each((elem) => {
                // 落ちる速度が速くなり過ぎないように制御
                elem.physical.velocity.y += (elem.physical.velocity.y > 15)
                    ? 0 : VELOCITY_Y;
            });
        }
        // 連鎖判定
        this.crushGroup.children.each((crush) => {
            self.group.children.each((star) => {
                // 砕けた星とタッチされる前の星の衝突判定
                if (crush.hitTestElement(star) && star.alpha === 1.0) {
                    star.alpha = 0.3;
                    star.removeCount--;
                    star.setInteractive(false);
                    SCORE += 10 * (1 + VELOCITY_Y);

                    // 連鎖ラベル生成
                    ChainLabel("Chain").addChildTo(this).setPosition(star.x, star.y);
                    // 砕けた星を生成
                    self.create(star.x, star.y);
                    self.chainCount++;
                }
            });
        });
        // 砕けた星が無くなったら連鎖カウントをリセット
        if (this.crushGroup.children.length === 0) {
            const chainScore = Math.round(this.chainCount * this.chainCount * 10 * (1 + VELOCITY_Y));
            if (chainScore > 0) {
                ChainLabel("+" + chainScore).addChildTo(this).setPosition(this.gridX.span(6), this.gridY.span(2))
                    .tweener.clear().to({ y: -200, alpha: 0.0 }, 2000).play();

                // スコアと最大連鎖数の更新
                SCORE += chainScore;
                HIGHEST_CHAIN = (this.chainCount > HIGHEST_CHAIN) ? this.chainCount : HIGHEST_CHAIN;
                this.chainCount = 0;
            }
        }
        // 星が画面外に出たらゲームオーバー
        this.group.children.each(function (elm) {
            if (elm.y > 960) {
                const params = {
                    score: SCORE + "\n最大" + HIGHEST_CHAIN + "連鎖",
                    message: "Object2Dクラスサンプル\n【星クリックゲーム】",
                    url: MY_URL
                };
                self.exit(params);
            }
        });
    },
    // 砕けた星生成
    create: function (x, y) {
        (CRUSH_STARS).times(function (i) {
            CrushStars(i).addChildTo(this).setPosition(x, y);
        }, this.crushGroup);
    }
});

/*
 * 星
 */
phina.define("Star", {
    superClass: "StarShape",

    init: function (color, x, y) {
        this.superInit({
            fill: color,
            stroke: null,
            radius: 75,
            rotation: 0,
            x: x,
            y: y
        });

        this.physical.velocity.y = 6; // 1描画毎に下降
        this.vr = 16; // 1描画毎の回転度
        this.removeCount = 10; // クリックされた後に消すまでのカウント

        this.setInteractive(true);
    },

    update: function () {
        this.rotation += this.vr;

        if (this.removeCount < 10) {
            this.removeCount--;
            if (this.removeCount === 0) {
                this.remove();
            }
        }
    },
});

/*
 * 砕けた星
 */
phina.define("CrushStars", {
    superClass: "StarShape",

    init: function (r) {
        this.superInit({
            fill: "hsla({0}, 100%, 50%, 0.8)".format(Math.randint(0, 360)),
            stroke: null,
            radius: 20,
            rotation: 0
        });

        this.vc = Math.cos(r * 36 / 180 * Math.PI);
        this.vs = Math.sin(r * 36 / 180 * Math.PI); // 1描画毎に下降
        this.vr = 16; // 1描画毎の回転度
        this.count = 0;
    },

    update: function () {
        this.count++;
        this.x += this.vc * this.count * 5;
        this.y += this.vs * this.count * 5;
        this.rotation += this.vr;

        // 画面外に出たら消す
        if (this.x < 0 || this.x > 640 || this.y < 0 || this.y > 960) {
            this.remove();
        }
    },
});

/*
 * 連鎖ラベル
 */
phina.define("ChainLabel", {
    superClass: "Label",
    init: function (text) {
        this.superInit({
            text: text,
            fontSize: 40,
            stroke: "black"
        });
        const grad = this.canvas.context.createRadialGradient(-80, 0, 0, 65, 0, 65);
        grad.addColorStop(0, "hsla(50, 100%, 80%, 1.0)");
        grad.addColorStop(0.2, "hsla(100, 100%, 80%, 1.0)");
        grad.addColorStop(0.4, "hsla(150, 100%, 80%, 1.0)");
        grad.addColorStop(0.6, "hsla(200, 100%, 80%, 1.0)");
        grad.addColorStop(0.8, "hsla(250, 100%, 80%, 1.0)");
        grad.addColorStop(1.0, "hsla(300, 100%, 80%, 1.0)");
        this.fill = grad;

        // 消すまでのカウント
        this.removeCount = 0;
    },
    update: function () {
        this.removeCount++;
        if (this.removeCount > 15) {
            this.remove();
        }
    }
});

/*
 *タイトルシーン
 */
phina.define("Scene01", {
    //継承
    superClass: "DisplayScene",
    // コンストラクタ
    init: function () {
        // 親クラス初期化
        this.superInit();

        //ラベル
        Label({
            text: "Click the star",
            fontSize: 64,
        }).addChildTo(this).setPosition(this.gridX.center(), this.gridY.span(3));

        // 星生成
        this.star = StarShape({
            fill: "hsla(180, 100%, 50%, 0.8)",
            stroke: null,
            radius: 100,
        }).addChildTo(this)
            .setPosition(this.gridX.center(), this.gridY.center());
        // 星にアニメーションを設定
        this.star.tweener
            .rotateTo(360, 600)
            .wait(200)
            .rotateTo(0, 1)
            .setLoop(true)
            .play();
        // 星をクリック可能に
        this.star.setInteractive(true);
        // 星がクリックされたら次のシーンへ
        const self = this;
        this.star.onpointend = function () {
            self.exit();
        };
    }
});

/*
 * メイン処理
 */
phina.main(function () {
    // アプリケーションを生成
    const app = GameApp({
        startLabel: 'Scene01',
        scenes: [
            {
                className: 'Scene01',
                label: 'Scene01',
                nextLabel: 'main'
            },
            {
                className: 'MainScene',
                label: 'main',
                nextLabel: 'result'
            },
            {
                className: 'ResultScene',
                label: 'result',
                nextLabel: 'main'
            }
        ]
    });
    // fps表示
    //app.enableStats();
    // 実行
    app.run();
});

ちなみに、今回のサンプルはぶっちゃけ過去に制作したこのゲームを少し改変した使いまわしだったりします。リンク先のページのものはBGMがついていたりと違いがあるので、もしよろしければこちらも遊んでみてください!

ご意見・ご感想、また、ここ間違ってるよとか、もっといい方法あるよといったご指摘などございましたら、お手数ですがコメント欄や当twitterアカウントへお願いしますm(_ _)m

参考にさせていただいたサイト・ページ一覧

phina.js/object2d.js at develop – phinajs/phina.js – GitHub
[phina.js-Tips]Shapeの原点を変更する
[phina.js-Tips-010]タッチイベントを登録する

モバイルバージョンを終了