ジャンル雑多なゲーム・ゲーム制作関連の色々な情報を取り扱っているブログ。最近はBlenderについてが中心。
phina.jsでキャラクターの各部位を動かす方法

phina.jsでキャラクターの各部位を動かす方法

自分への備忘録やまだ習得中なphina.jsの理解を深めるついでに、どなたかの役に立てば御の字。という思惑の元書かせていただきます。

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

目次

はじめに

前提条件として、

  • JavaScriptやHTMLのコードを書いたことがある
  • 作成したファイルをブラウザに表示できる

という方を対象として説明します。

上記2点ができない、または、言っていることが理解できない方は、根性でやってみるか、ほかのサイト・ブログ様で、プログラミング言語やプログラミングの基礎を学ぶことをお勧めいたしますm(_ _)m

phina.jsのさらっと紹介

phina.jsは国産のゲームライブラリです。因みに以前概要だけまとめてみた記事です。

関連記事:ゲームライブラリ『phina.js』試してみた

できれば、phina.jsを使用したことがない方にも、他の方が分かりやすくphina.jsの基本や、コードの書き方を紹介してくださっていますので、例えばこちらのphina.js Tips集などを一度ご覧になってからの方が良いかとは思いますが、とりあえずどんなものかということでご一読くださる場合、出来る限りわかりやすく書かせていただきますので、様々な使い方の一例という感じで見てくだされば幸いです。

今回は、ローカル環境で実行する場合の説明をします。

数日前に公開したこのゲームで登場する、上の何かと動いて視線が行く

これ

今回はこれを例に動かす方法を説明します。

コードを書く前に

とりあえず動かし方の前に、まずは各部位(仮面、腕、手)をご自分で描くなり、どこからかご準備ください。(もし用意するのが面倒でしたら下手ですが私のやつ置いときます)



実際にコーディング

各部位の素材が出来たら、HTMLファイルとJSファイルをご用意ください。

最初に全部載せてしまいますが、HTMLファイルは、こんな感じ。

<!DOCTYPE html>

<html lang="ja">
<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>
</head>
<body>
    <script src='https://cdn.jsdelivr.net/gh/phi-jp/phina.js@0.2.3/build/phina.js'></script>
    <script src="Script1.js"></script>

</body>
</html>

HTMLファイルに関しては、この行が、

<script src='https://cdn.jsdelivr.net/gh/phi-jp/phina.js@0.2.3/build/phina.js'></script>

phina.jsが使えるように呼び出ししているという説明だけで終わらせておきます。

そしてJSファイルはこんな感じ。

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

//アセット
var ASSETS = {
    // 画像
    image: {
        "kamen": "./kamen.png", // 仮面の画像
        "arm": "./arm.png",     // 腕の画像
        "hand": "./hand.png",   // 手の画像
    },
};

/*
 * メインシーン
 */
phina.define("MainScene", {
    // 継承
    superClass: "DisplayScene",
    init: function () {
        // 親クラスの初期化
        this.superInit();
        // 背景色を設定
        this.backgroundColor = "aliceblue";

        // 身体の動きのコア
        this.bodyCore = CircleShape().addChildTo(this)
            .setPosition(
                this.gridX.center(),  // X座標の位置
                this.gridY.center(-2) // Y座標の位置
            );
        this.bodyCore.radius = 1;       // 半径

        // 仮面
        this.kamen = Sprite("kamen")     // スプライトクラスにkamenという名前にした画像を指定
            .addChildTo(this.bodyCore); // コアの子クラスに

        // 左腕
        this.arm_L = Arm()               // 腕クラスを呼び出す
            .addChildTo(this.bodyCore); // コアの子クラスに
        this.arm_L.origin.set(1, 0);    // 右上を原点座標に
        this.arm_L.x = this.gridX.span(7); // x座標の位置

        // 右腕
        this.arm_R = Arm()              //腕クラスを呼び出す
            .addChildTo(this.bodyCore); //コアの子クラスに
        this.arm_R.scaleX *= -1;        // 左右反転
        this.arm_R.origin.set(1, 0);    //右上(反転しているため左上)を原点座標に
        this.arm_R.x = this.gridX.span(-7); // x座標の位置

        // 身体を動かす命令を実行させる
        this.bodyAction();
    },
    bodyAction: function () { // 身体を動かす命令
        // 身体の動きのコアの動作
        this.bodyCore.tweener.clear()
            .to({}, )
            .to({}, )
            .setLoop(true)
            .play();

        // 仮面の動作
        this.kamen.tweener.clear()
            .to({ rotation: 1200 }, 4000)
            .to({ rotation: -60 }, 4000)
            .setLoop(true)
            .play();

        // 左腕の動作
        this.arm_L.tweener.clear()
            .to({ rotation: 125 }, 1000)
            .to({ rotation: -20 }, 1000)
            .setLoop(true)
            .play();

        // 左手の動作
        this.arm_L.hand.tweener.clear()
            .to({ rotation: 1440 }, 4000)
            .to({ rotation: 0 }, 4000)
            .setLoop(true)
            .play();

        // 右腕の動作
        this.arm_R.tweener.clear()
            .to({ rotation: -125 }, 1000)
            .to({ rotation: 20 }, 1000)
            .setLoop(true)
            .play();

        // 右手の動作
        this.arm_R.hand.tweener.clear()
            .rotateTo(1440, 4000)
            .rotateTo(0, 4000)
            .setLoop(true)
            .play();  
    },
});

/*
 * 腕クラス 
 */
phina.define("Arm", {
    superClass: "Sprite",
    init: function () {
        this.superInit("arm");

        // 手
        this.hand = Sprite("hand").addChildTo(this)
            .setPosition(-50, 105);
        this.hand.origin.set(0.9, 0.1);
    },
});

/*
*メイン処理
*/
phina.main(function () {
    // アプリケーション生成
    var app = GameApp({
        startLabel: 'main',// メインシーンから開始
        assets: ASSETS, // アセット読み込み
    });
    // fps表示
    //app.enableStats();
    // アプリケーション実行
    app.run();
});

解説

おまじない

まず一番上と、一番下のところ

// phina.js をグローバル領域に展開
phina.globalize();
  .
  .
  (中略)
  .
  .
/*
*メイン処理
*/
phina.main(function () {
    // アプリケーション生成
    var app = GameApp({
        startLabel: 'main',// メインシーンから開始
        assets: ASSETS, // アセット読み込み
    });
    // fps表示
    //app.enableStats();
    // アプリケーション実行
    app.run();
});

ここは、とりあえずおまじないとして、もし詳しく知りたい方は先ほど上で紹介したTips集をご参考ください。

アセットで今回使用する画像を格納しています。

//アセット
var ASSETS = {
    // 画像
    image: {
        "kamen": "./kamen.png", // 仮面の画像
        "arm": "./arm.png",     // 腕の画像
        "hand": "./hand.png",   // 手の画像
    },
};

このアセットでは、画像のほかサウンドも格納できます。また、一番最後で記述しているメイン処理内のassetsで読み込むのをお忘れなきよう。

各部位をセットする

次にメインシーンのこの部分

phina.define("MainScene", {
    // 継承
    superClass: "DisplayScene",
    init: function () {
        // 親クラスの初期化
        this.superInit();

まずはphina.defineの部分は『クラスを作ります』と言っているので良いかと思います。そしてphina.jsにデフォルトであるDisplaySceneクラスを継承するMainSceneクラスを生成し、そのDisplaySceneクラスの設定を初期化しているところです。initというのが、各クラスの中心で、各クラスに1つはこれがあります。ちなみに、このMainSceneなどはデフォルトで設定されているシーンで、他にもデフォルトで設定されているシーンがあるのですが、詳しくは[phina.js-Tips]デフォルトで用意されているSceneについて知るが参考になるかと思います。そして、

次にここはそのままなんですが背景色をアリスブルーに指定しています。

// 背景色を設定
        this.backgroundColor = "aliceblue";

次が肝心で、ここに身体の各部位をまとめてしまいます。

// 身体の動きのコア
        this.bodyCore = CircleShape().addChildTo(this)
            .setPosition(
                this.gridX.center(),  // X座標の位置
                this.gridY.center(-2) // Y座標の位置
            );
        this.bodyCore.radius = 1;       // 半径

まず、CircleShapeクラスというのは、phina.jsでデフォルトで用意されている円形オブジェクトです。他にも各種図形オブジェクトが用意されています。その円形オブジェクトにbodyCoreという名前をつけています。addChildToで、this(この場合MainScene)に子クラスとして付け足す、ということをしています。

ここで覚えておいてほしいのが、親クラス(この場合MainScene)に変化が起こった時、一部子クラス(この場合bodyCore)に反映されるということです。

そしてsetPositionでX座標を画面中央、Y座標を画面中央より少し上という場所にオブジェクトを設置しています。gridXやらgridYについては、後述。そしてその後に、この円の半径(radius)を1に設定しています(1だと、もはや点)。

今回のキャラクターの部位の設計図(?)を私なりに書いてみました

上の方が親クラスで下の方が子クラスという風に繋いでいます。(わかりにくかったらすみません)

そしていよいよ画像を扱うわけです。

// 仮面
        this.kamen = Sprite("kamen")     // スプライトクラスにkamenという名前にした画像を指定
            .addChildTo(this.bodyCore); // コアの子クラスに

ここで出て来るSpriteクラスというのが、画像を扱う時に使うクラスで、そこに、kamenと名前を付けた仮面の画像を格納して、先ほど作ったbodyCoreオブジェクトの子クラスに設定しています。

同様に腕部分の画像についてなのですが、ここを説明する前に別のクラスとして腕クラスを作っていますのでそちらについて説明します。

/*
 * 腕クラス 
 */
phina.define("Arm", {
    superClass: "Sprite",
    init: function () {
        this.superInit("arm");

        // 手
        this.hand = Sprite("hand").addChildTo(this)
            .setPosition(-50, 105);
        this.hand.origin.set(0.9, 0.1);
    },
});

まず、Spriteクラスを継承して、画像を扱えるクラスにし、superInitの引数に(親クラスの初期化の時に)armという名前を付けた腕の画像を格納しています。そして、腕クラスの子クラスとして、手クラスを生成しています。setPositionは、それぞれ微調整してください。その際、子クラスの位置の(0, 0)は、よくある画面の左上ではなく、親クラスの原点になることに注意してください。そしてorigin.setで、手の原点をXが0.9、Yが0.1に変更しています。0~1まで指定でき、デフォルトは(0.5, 0.5)つまり中心に設定されています。原点変更の詳しい説明は[phina.js-Tips]Shapeの原点を変更するの記事が分かりやすいと思います。

そしてメインシーンに戻って、まずは左腕を作りましょう。

// 左腕
        this.arm_L = Arm()               // 腕クラスを呼び出す
            .addChildTo(this.bodyCore); // コアの子クラスに
        this.arm_L.origin.set(1, 0);    // 右上を原点座標に
        this.arm_L.x = this.gridX.span(7); // x座標の位置

まず、bodyCoreの子クラスにし、origin.setで右上を原点にします。そしてxをspan(7)と指定していますが、Gridクラスについては詳しくは【phina.js】Gridクラスを使いこなそうをご覧ください。

右腕についてなのですが、左腕とほぼ同じなので省略しつつ、一点だけ説明すると、

 this.arm_R.scaleX *= -1;        // 左右反転

この部分のscaleXは、左右のスケールを操作でき、-1を掛けることで画像を左右反転させています。

そしてinitの最後でbodyAction()という各部位動かすをさせるメソッドの実行しているわけです。

各部位を動かす

最初の身体の動きのコアの部分の記述なんですが、何もしていないのですっ飛ばします。

まず最初の仮面の動作のところで、tweenerクラスというのが出てきます。これがphina.js名物(?)のオブジェクトの移動などのアニメーションが簡単に設定できちゃうクラスなんです!色々と出来ることが多すぎてここでは説明しきれないので、例のごとく詳しくは先述ご紹介したTips集をご覧ください。

 // 仮面の動作
        this.kamen.tweener.clear()
            .to({ rotation: 1200 }, 4000)
            .to({ rotation: -60 }, 4000)
            .setLoop(true)
            .play();

clear()は、それまでにしていた動作をクリアしています。(今回のプログラムではぶっちゃけ省略しても変わらない)次のto()では、複数の移動や回転を設定できます。1つ目のto()ではrotationで角度が1200度になるまで回転させる動作を4000ミリ秒で行えと指定しています。その次に続けて、今度は回転の角度が-60度になるまでを4000ミリ秒で行えと指示しています。to()を連ねて記述することで、連ねた動作を繋げて擦るように指定出来ます。

今回は1つの変化しか指定していませんが、これを、to({x:this.gridY.span(3), rotation: 1200},4000)と言う風にx方向の変化を加えて指定すると、回転しながら仮面が右へ移動します。

その後のsetLoop(true)で連ねて記述したこの動作をループさせる指示をしています。この状態だと、何もしなければ所謂無限ループになります。

そのあとのplay()はそのまま再生するという意味です。

あとは個々に適当に回転させているだけなのですが、最後に左手と右手の指定の仕方の違いだけ説明します。

// 左手の動作
        this.arm_L.hand.tweener.clear()
            .to({ rotation: 1440 }, 4000)
            .to({ rotation: 0 }, 4000)
            .setLoop(true)
            .play();
  .
  .
  (中略)
  .
  .
   // 右手の動作
        this.arm_R.hand.tweener.clear()
            .rotateTo(1440, 4000)
            .rotateTo(0, 4000)
            .setLoop(true)
            .play();  

まあお察しの通り、右手も左手も同じ動作を指定しています。他の部位を全部rotateTo()で指定しても良かったのですが、to()だと、複数の指定ができるので、いろんな変化をさせて遊んでもらうためにわざとto()を主に使用してみました。

おまけで、何も記述してない身体の動きのコアの記述をいじると他の部位も一緒に動きます。例えば、回転させると体全体が回転という具合に。色々いじって遊んでみてください。

お疲れさまでした。これでコードの解説終わります。

まとめ

技術については本当に度々ひっぱり出してすみませんalkn203氏って感じなんですが、本当に充実した内容なので、一見どころではなく、度々見る価値ありだと思います。(私自身、何かわからないことがある度辞書みたいに活用させていただいてます)

phina.js Tips集

あと言いたいことは、いろいろいじり倒して遊んでみてください!ということで、ここまでのご拝読ありがとうございました!

Pocket