19:Tweenアニメーション

Tweenアニメーションクラスを定義して、アニメーションの雛形を実装。
これで毎フレームに発生するアクターやウィンドウアニメーションを、簡単に管理できるようになります。

アニメーションテスト
⇒ 立ち絵アニメーションのデモ(スペースキーでアクション)

Canvas描画のアニメーションをどう管理する?

(2019.8.6執筆、2021.11.09更新)

アニメーションをどのように実装するか、これまで悩みどころでした。
前回立ち絵のフェードアニメーションを実装する際に、update()に直に書き込むような記述をしてました。

前回参照 ⇒ 17-2:Canvas描画のフェードイン・アウト


しかしこれだと各オブジェクトのupdate()内における動作の場合分けが大変です。 アニメーションを実装する度に、そのオブジェクトのコードが複雑になってしまう。後のメンテナンスも拡張性も壊滅的でした。

そこで古都さんに伺ったところ、アニメーションを定義したクラス(Tween)を用意し、各アニメーションのupdate()は、そのTweenクラスに全て任せてしまう。といった方法があるみたいです。 アニメーションの処理について一つの型を作ってしまえば、後はどれだけアニメーションが発生しようともTweenを管理する配列にて、一括でアニメーションフレームを更新させればよい。 なるほど。様々な動きをするゲームには欠かせない存在になりそうですね。


ひとまず、アニメーションの型となるTweenクラスを作り、立ち絵のフェード効果をTweenに置き換えてみたいと思います。

Tweenアニメーション実装の手順

  • アニメーションを定義するTweenクラスを作る
  • Tweenクラスを、シーン内で追加・保持・更新ができるようにする
  • 実際にtweenを使って動かしてみる
  • easing(アニメーション中の変化曲線)を使い分ける


やってみないとピンとこない。とりまコード描いてみますか(' '*)


Tweenについて参考にした記事はこちら
Flash:ActionScript3.0 Tweenのサンプルと使い方

それから、phinaJSさんのコードも参考にさせて頂きました
phina.js

Tweenクラスの定義


    constructor(target, key, beginProps, finishProps, duration, easing = 'linear', option={}) {
        super();
        this.target = target; //変化させるプロパティを含んだオブジェクトをターゲットで指定
        this.key = key; //変化させるプロパティ名を文字列で渡す
        //this.target[this.key]で、アニメーション推移させるプロパティにアクセスできるように。

        this.beginProps = beginProps; //開始時の値
        this.finishProps = finishProps; //終了時の値
        this.duration = duration; //終了値までの経過ミリ秒(ms)
        this.easing = easing; //値の推移を制御するモード(linearなら等間隔、二次曲線なら加速度的に変化など)

        this.timeCount = 0; //アニメーションの経過ミリ秒を記録

        this.loop = option.loop ? true : false;
        if(option.callback !== undefined) { this.addEventListener('finish', option.callback, {once:true}); } //アニメーション終了時に登録されたコールバック関数を1回だけ起動
    }

    release() { this.dispatchEvent('release', new Event(this)); } //自身を削除する関数(シーン側で)
    reverse() { //アニメーションの動きを逆転させる
        const temp = this.beginProps; 
        this.beginProps = this.finishProps; 
        this.finishProps = temp;
        this.timeCount -= this.duration;
    }

    update(elapsed_ms) {
        this.timeCount += elapsed_ms; // 経過時間(ms)をカウント
        if (this.timeCount >= this.duration) { this.finish(); }// 経過時間がアニメーション終了に達した
        else { this.animation(); } // アニメーション中なら、値の変化を計算する必要がある
    }
    finish() {
        this.target[this.key] = this.finishProps; // ターゲットを終了値に指定
        this.dispatchEvent('finish'); // 終了時にコールバック関数があれば起動
        if(!this.loop) {this.release(); } // アニメーションを終了
        else{ this.reverse(); } //ループ設定されてるなら、アニメーションを反転。
    }
    animation() { //アニメーション中の変化値を求める計算
        const t = this.timeCount / this.duration; //経過時間の割合t(timestamp)を計測
        const cp = this.finishProps - this.beginProps; //開始値と終了値の差...ChangePropsの略
        this.target[this.key] = this.beginProps + cp * t; //これが基本のlinear計算式。ターゲットに指定した値が、開始値から等間隔で変化する
    }
}

アニメーションに必要な情報...

  • this.target ...アニメーションのターゲットとなるオブジェクト
  • this.key ...プロパティ名(this.target[this.key]でアクセスできる)
  • this.beginProp ...ターゲットプロパティの開始値
  • this.finishProps ...ターゲットプロパティの終了値
  • this.duration ...開始から終了までにかかる時間(フレーム数)を指定
  • this.easing ...推移変化をどのようにする? デフォルトでは等間隔
  • this.option={}...{loop:true ? false, callback:()=>{}}オプションで追加情報を記入できるように


追加で、アニメーション終了時にcallbackイベントを起動できるようにもしました。

後はメソッドにupdate()を持たせて、フレーム毎のアニメーション推移を計算できる形に仕上げました。
easingの種類は後から考えるとして、デフォルトの等間隔に推移(linear)として値を計算してます。

アニメーション終了時、自動的に此れ自身がupdate()から外れるよう、release()というメソッドも加えてます。

Scene内にてTweenクラスを管理する

Tweenクラスは、Actorのようにシーンに追加することで毎フレームupdate()が呼び出され、指定された値が推移していきます。 この仕組みを使って様々なアニメーションを実現できるようになります。

ともあれ、Tweenを追加できるようSceneクラスに修正を加えてみます・

Sceneクラスを追記修正


class Scene extends EventDispatcher {//Actorをまとめあげる舞台の役割、シーンの定義
    constructor(name, width, height, bgImageName) {
        super();

        this.name = name;
        this.backgroundImage = assets.get(bgImageName); //背景画像の指定、nullの場合は黒く塗りつぶす

        // 背景スクローラー用のオブジェクト。アクターの描画位置を調整するのに使用。
        // scroller.xやscroller.yの値はプレイヤー側で操作。アクターのアップデート後、各シーンで画面端の調整する
        this.scroller =  { x: 0, y: 0 };

        this.infomation = {width, height, //アクターの各アップデートに渡すシーン情報をまとめたオブジェクト
            scroller:this.scroller,
            playerIsActive:true, //現在プレイヤー操作が可能かどうかを定義
            currentTime:0 //シーンが開幕してからの経過時間
        };

        this.actors = [];//Actorたちを保持する
        this._releasedActors = [];
        this.windowsUI = [];//(メッセージ)ウィンドウを保持する
        this._closedWindowsUI = [];
        this.tweens = [];// アニメーションTweenを保持する

        this._qTree = new LinearQuadTreeSpace(this.width, this.height);
        this._detector = new CollisionDetector();
    }

    changeScene(newScene) {//Sceneを変更します。ですがScene自身がSceneを変更することはできないので、Actorのときと同じようにイベントだけ発生させて、実際の処理は他のクラスに任せます
        this.dispatchEvent('changescene', new Event(newScene));
    }

    add(actor) {//Actorたちを保持する(追加・削除)
        this.actors.push(actor);
        actor.addEventListener('spawnactor', (e) => this.add(e.target));//spawnactorイベントが発生した場合はシーンにActorを追加
        actor.addEventListener('release', (e) => this._releasedActors.push(e.target));//releaseイベントはそのActorを降板リストに追加
        actor.addEventListener('open', (e) => this.open(e.target));//openWindowイベントが起こったら、シーンにメッセージウィンドウを追加
        actor.addEventListener('addTween', (e) => this.addTween(e.target));//addTweenイベントが起こったら、シーンにアニメーションを追加
    }
    open(windowUI) {//(メッセージ)ウィンドウを保持する(追加・削除)
        this.windowsUI.push(windowUI);
        windowUI.addEventListener('open', (e) => this.open(e.target));//openWindowイベントが発生した場合はシーンにWindowを追加
        windowUI.addEventListener('close', (e) => this._closedWindowsUI.push(e.target));//closeイベントはそのWindowを降板リストに追加
        windowUI.addEventListener('addTween', (e) => this.addTween(e.target));//addTweenイベントが起こったら、シーンにアニメーションを追加
        windowUI.addEventListener('playerIsStay', () => this.infomation.playerIsActive = false);//プレイヤーの操作を受け付けないようにする
        windowUI.addEventListener('playerIsActive', () => this.infomation.playerIsActive = null);//プレイヤーの操作を再度可能にする(null指定で、アクターのアップデート後にtrueへ切り替え
    }
    addTween(tween) {// アニメーションTweenを保持する(追加・削除)
        this.tweens.push(tween);
        tween.addEventListener('release', (e) => { 
            const index = this.tweens.indexOf(e.target); //releaseイベントはそのアニメーションを終了させる
            if(index > -1) {this.tweens.splice(index, 1);}
        });
    }
ほぼほぼ、Actorと同じようにTweenを登録できるようにしてみました。
ただしTweenは当たり判定を持たず、描画の必要もない。Sceneから毎フレームupdate()を呼び出せればいいだけなので、Actorとは別枠扱いにしてます。
その分メソッドが増えるのだけど、まぁ良いよね。ムダな動作させたくないし(' '*)。。


後は、Tweenを追加した配列に対して毎フレームupdate()を呼び出せれば良い。 Sceneのupdate()内に、その旨を追記してあげればOKです。

    update(context, input, touch, gameInfo) {//updateメソッドではシーンに登録されているActor全てを更新し、当たり判定を行い、描画します。描画の前に一度画面全体をクリアするのを忘れないで下さい。
        this._updateInfo(gameInfo, input, touch);//アップデートに追加情報を挿入

    //〜〜省略
    }

    _updateInfo(gameInfo, input, touch) {// アップデートに追加情報を挿入
        this.infomation.currentTime += gameInfo.elapsedSec; //経過時間を加算
        this.infomation.elapsedSec = gameInfo.elapsedSec; //現1フレームの経過時間を記録
        this.tweens.forEach((tween) => tween.update(gameInfo.elapsedSec)); //登録されたアニメーションを更新する

        //タッチ座標の設定。タッチした画面座標にスクローラー要素を反映させる
        touch.actorX = touch.x + this.scroller.x;
        touch.actorY = touch.y + this.scroller.y;
    }
Scene.update()内のどこかに
        this.tweens.forEach((tween) => tween.update(gameInfo.elapsedSec)); //登録されたアニメーションを更新する
一行加えてあげたら良いです。

各ActorとWindowUIへの追記

あ、そうそう。addTween()というメソッドを各アクターやウィンドウにも持たせておく必要があります。
spawnActor()とかopenWindow()のように、Sceneに新しいオブジェクト...この場合はTweenですね、を追加するためのものです。

ActorクラスにaddTween()を持たせる


class Actor extends EventDispatcher {//EventDispatcherイベント発生関数を持つActorクラスの定義
    constructor(x, y, hitArea, tags = []) {
        super();
        this.hitArea = hitArea;
        this._hitAreaOffsetX = hitArea.x;
        this._hitAreaOffsetY = hitArea.y;
        this.tags = tags;
        this.x = x;
        this.y = y;
        this.vector = new Vector2D(0, 0); //現在フレームの移動ベクトルを格納、初期値(0, 0)
    }

    hasTag(tagName) { return this.tags.includes(tagName); } //タグは当たり判定などのときに使います
    spawnActor(actor) { this.dispatchEvent('spawnactor', new Event(actor)); } //他のActorを発生させるときに使用
    open(windowUI) { this.dispatchEvent('open', new Event(windowUI)); } //他のWindowを展開するときに使用
    addTween(tween) {this.dispatchEvent('addTween', new Event(tween));} //新しいアニメーションを加える関数(シーン側で)

    //自身をシーンから除外する、しかし関数で自身を消すことはできないので、イベントだけ発生させてより上位のクラスに実際の処理を任せる
    release() { this.dispatchEvent('release', new Event(this)); }
    
    get x() { return this._x; }//x座標を読み込む関数
    set x(value) {//x座標にvalueを代入する関数
        this._x = value; this.hitArea.x = value + this._hitAreaOffsetX;
    }
    get y() { return this._y; }//y座標を読み込む関数
    set y(value) {//y座標にvalueを代入する関数
        this._y = value; this.hitArea.y = value + this._hitAreaOffsetY;
    }
    update(sceneInfo, input, touch) {}//動く仕組みを作る
    render(context, scroller) {}//...線画処理
}

WindowUIクラスにaddTween()を持たせる


class WindowUI extends EventDispatcher {//WindowUIクラスの定義、メッセージ欄やメニュー表示に使用
    constructor() {
        super();
    }
    open(windowUI) { this.dispatchEvent('open', new Event(windowUI)); } //他のWindowUIを展開するときに使用
    addTween(tween) {this.dispatchEvent('addTween', new Event(tween));} //新しいアニメーションを加える関数(シーン側で)

    //このウィンドウ自身を閉じる、しかし関数で自身を消すことはできないので、イベントだけ発生させてより上位のクラスに実際の処理を任せる
    close() { this.dispatchEvent('close', new Event(this)); }
    playerIsStay() { this.dispatchEvent('playerIsStay');} //scene側でプレイヤー操作を受け付けないようにする関数
    playerIsActive() { this.dispatchEvent('playerIsActive');} //scene側でプレイヤー操作受付を再開する関数

    clearRendering(target) {}//__close()に合わせて、Scene側でウィンドウの描画をクリアする
    update(sceneInfo, input, touch) {}//ウィンドウ内のアップデート関数
    render(context) {}//描画関数
}

これで各ActorやWindowUIから、アニメーションを登録できるようになりました。
さっそく立ち絵のフェードイン・アウトを置き換えてみますか。

Tweenクラスを使って立ち絵をフェードイン・アウトをしてみる

ではではTweenの実装テストです!
今回は立ち絵を操作するのでTalkPictクラスからTweenを登録してみます。

TalcPictクラス内の書き換え


class TalkPict extends WindowUI {//会話時のキャラクターイラスト、会話ウィンドウの子要素となるクラス
    constructor(sprite, option={}) {
        super();
        this.sprite = sprite;
        this.duration = option.duration ? option.duration : 16; //フェード効果の経過フレーム数を指定、デフォルトは16フレーム
        this.enter = option.enter ? true : false; //登場時に画面端からスライドする?
        this.leave = option.leave ? true : false; //退場時に画面端にスライドする?
        this.easing = option.easing; //アニメーションの推移パターン(指定されてないなら初期値)

        this.opacity = 0; //描画の透明度。最初は0で透明に。メッセージをフェードイン表示させる
        this.x = screenCanvasWidth - this.sprite.width; //描画開始位置のx座標、登場時のスライドがfalseなら定位置に。
        this.y = Math.max(0, screenCanvasHeight - this.sprite.height); //描画位置のy座標。0より小さくなる場合は0で

        this.tweens = []; //立ち絵のアニメーションを管理
        this.fadeIn(); //フェードインで表示
    }

    addTween(tween) { //立ち絵のアニメーションを登録する関数
        this.tweens.push(tween);
        tween.addEventListener('release', (e) => { 
            const index = this.tweens.indexOf(e.target); //releaseイベントはそのアニメーションを終了させる
            if(index > -1) {this.tweens.splice(index, 1);}
        });
    }

    fadeIn() { //フェードインを設定する関数
        const easing = this.easing ? this.easing : 'easeOutQuad';
        const tween = new Tween(this, 'opacity', 0, 1, this.duration, easing);
        this.addTween(tween);
        if(this.enter) {
            const tween1 = new Tween(this, 'x', screenCanvasWidth, this.x, this.duration, easing);
            this.addTween(tween1);
        }
    }
    fadeOut() { //フェードアウトを設定する関数
        const easing = this.easing ? this.easing : 'easeInQuad';
        const tween = new Tween(this, 'opacity', 1, 0, this.duration, easing, {callback:() => this.close()});
        this.addTween(tween);
        if(this.leave) {
            const tween1 = new Tween(this, 'x', this.x, screenCanvasWidth, this.duration, easing);
            this.addTween(tween1);
        }
    }
    delayClose(value = 200) { //valueミリ秒後にフェードアウトする
        setTimeout(() => this.fadeOut(), value); //表情差分を重ねて表示させる時間を確保するためにdelayClose()を用意した
    }

    update(sceneInfo) {
        this.tweens.forEach((tween) => tween.update(sceneInfo.elapsedSec)); //登録されたアニメーションを更新する
    }

    renderingCharactor(context) {
        context.drawImage(this.sprite.image, //ここからsprite画像の描画
            this.sprite.x, this.sprite.y,
            this.sprite.width, this.sprite.height,
            this.x, this.y
        ); //sprite画像ここまで
    }
    render(context) { //立ち絵表示は、通常のcanvasContextにて行う
        if( this.opacity < 1 ) { //描画時にフェード効果を付ける
            context.setAlpha(this.opacity);
            this.renderingCharactor(context);
            context.setAlpha(1);
        }
        else { this.renderingCharactor(context); }
    }
}

TackPictクラスは、メッセージウィンドウの設計上シーンに追加せずに使ってしまうので、addTween()メソッドは自身のクラス内にtweenを追加するようにしてしまいました。 いやー、長いですね。追加する文なんてほんのちょっとですよ。でも全体見ないと流れが判らん。なのに全体載せると何処に何が在るか、製作者以外には判りづらいっすえ。。。ふぃいいい。機械語。

要点を掻い摘むと、ココです此処。
    fadeIn() { //フェードインを設定する関数
        const tween = new Tween(this, 'opacity', 0, 1, this.fade, "linear");
        this.addTween(tween);
        if(this.enter) {
            const tween1 = new Tween(this, 'x', screenCanvasWidth, this.x, this.fade);
            this.addTween(tween1);
        }
    }
    fadeOut() { //フェードアウトを設定する関数
        const tween = new Tween(this, 'opacity', 1, 0, this.fade, "linear", () => this.close());
        this.addTween(tween);
        if(this.leave) {
            const tween1 = new Tween(this, 'x', this.x, screenCanvasWidth, this.fade);
            this.addTween(tween1);
        }
    }
フェードインとフェードアウトメソッドに、Tweenクラスを利用します。
すると煩わしいupdate()の場合分けが必要なくなり、コードの見通しが良くなる。といったトコロです。

比較として、従来のTalkPictクラスのupdate()を御覧くださいな。
    update(sceneInfo, input, touch) {
        if(this.fade > 0 && this.opacity < 1) { //フェードイン途中の場合
            this.opacity += this.fade;
            if(this.enter){ this.x = screenCanvasWidth - this.sprite.width * (this.opacity*0.5 + 0.5); } // キャラクター登場時のX座標を調整
        }
        else if(this.fade < 0 && this.opacity > 0) { //フェードアウト途中の場合
            this.opacity += this.fade;
            if(this.leave) { this.x = screenCanvasWidth - this.sprite.width * (this.opacity*0.5 + 0.5); } // キャラクター退場時のX座標を調整
        }
        if(this.fade < 0 && this.opacity <= 0) { //フェードアウトしたらこのオブジェクトを閉じる
            this.close();
        }
    }
従来の記述、ほぼアニメーションの動きですね。
update()中にベタ書きしてるため、何とも応用が効きづらい。

それが、↓こう書き換わりました。
 update(sceneInfo) {
        this.tweens.forEach((tween) => tween.update(sceneInfo.elapsedSec)); //登録されたアニメーションを更新する
 }

あいあい、登録されたアニメーションを実行してるだけの関数になっちゃいましたね。本来これはシーンに任せてOKなやつです。 アニメーションの管理はTweenに任せてるんで、Tweenの仕組みを理解すれば様々なアニメーションを表現できるようになると感じます。 今回は立ち絵クラスで試してますが、他のオブジェクトにも気軽にアニメーションのメソッドを実装できるのがtweenの強みですね。

とりあえず何も変わってない会話デモを見てみます。
会話イベント立ち絵&名前
⇒ 立ち絵フェード表示の完成形デモ(スペースキーでアクション)



普通に立ち絵のフェードイン・アウトが動いてます。
まぁいいっ感じですね。

easingの使い分け

ではでは、最後にeasingの使い分けです。easingってなんだ?
文章で説明するより、easingの種類によるグラフ推移を見たほうが早いのではなかろうか。

アニメーションをデザインしよう!

アニメーションの開始から終了までをどのように変化させるか=easing。
ちなみに最初は等間隔(linear)にて値を変更しました。
等間隔以外にも、アニメーションには色々な変化のさせ方があります。

最初はゆっくりだけど加速度的に変化していくようなeaseInだったり
逆に、急に変化して始まるのがだんだんと穏やかな変化になって終わるeaseOutだったり。
ゆっくり変化し、中間で急激に変わり、最後も穏やかに終わるeaseInOutだったり。
特殊な、バウンスするようなアニメーションだったり。色々なeasingの計算式が存在します

計算式はすでに他のライブラリに描かれてあるので、こちらでも実装していきましょう。

Tweenのanimation()メソッドでeasingによる計算式の振り分けを行ってみる

先ほど作ったTweenクラスのanimation()内をこのように書き換えてみます。easingの種類によって、計算方法を振り分ける記述例です。

    animation() { //アニメーション中の変化値を求める計算
        const t = this.timeCount / this.duration; //経過時間の割合t(timestamp)を計測
        let T; //easingによっては、計算前にtを変化させる場合がある
        const cp = this.finishProps - this.beginProps; //開始値と終了値の差...ChangePropsの略
        const result = (changePropsResult) => this.target[this.key] = this.beginProps + changePropsResult; //開始値から変化値を加算して、現在の値を求める

        let s; //easingのBackやElasticで使う定数。
        switch(this.easing) { //easingの種類で値の変化する度合いが変わる、モードに応じて以下の計算式から一つ選択。
            default : this.target[this.key] = this.beginProps + cp * t; break; //これが基本の計算式。開始値からどれだけ変化するか?を = result(cp * t)で置き換えできるよう設定した
            case 'linear' : result(cp * t); break; //等間隔で変化するのがlinear、defaultと同じ挙動

            case 'easeInQuad' : result(cp * t*t); break; //初期値からだんだんと加速的に変化
            case 'easeOutQuad' : T = t-1; result(-cp * (T*T -1) ); break; //初期値から一気に変化し、だんだん減速
            case 'easeInOutQuad' : if(t < 0.5) { result(cp*t*t*2); } //経過時間が半分以下なら、だんだん加速度的に
                            else { T = 2*(t-1); result( -cp*0.5 *(T*T - 2) ); } break; //経過時間が半分以上なら、少しずつ減速

            case 'easeInCubic' : result(cp * t*t*t); break;
            case 'easeOutCubic' : T = t-1; result(cp * (T*T*T + 1)); break;
            case 'easeInOutCubic' : if(t < 0.5) { result(cp*t*t*t*4); } 
                            else { T = 2*(t-1); result( cp*0.5 *(T*T*T + 2) ); } break;

            case 'easeInQuart' : result(cp * t*t*t*t); break;
            case 'easeOutQuart' : T = t-1; result(-cp * (T*T*T*T - 1)); break;
            case 'easeInOutQuart' : if(t < 0.5) { result(cp*t*t*t*t*8); } 
                            else { T = 2*(t-1); result( -cp*0.5 *(T*T*T*T - 2) ); } break;

            case 'easeInQuint' : result(cp * t*t*t*t*t); break;
            case 'easeOutQuint' : T = t-1; result(cp * (T*T*T*T*T + 1)); break;
            case 'easeInOutQuint' : if(t < 0.5) { result(cp*t*t*t*t*t*16); } 
                            else { T = 2*(t-1); result( cp*0.5 *(T*T*T*T*T + 2) ); } break;

            case 'easeInSine' : result( -cp * Math.cos(t *(Math.PI/2)) + cp ); break; //Sin波のカーブで変化、初動が遅い、最後が速い
            case 'easeOutSine' : result( cp * Math.sin(t *(Math.PI/2)) ); break; //Sin波のカーブで変化、初動が速い、最後が遅い
            case 'easeInOutSine' : result( -cp/2 * (Math.cos(Math.PI*t) - 1) ); break; //Sin波のカーブで変化

            case 'easeInCirc' : result( -cp *(Math.sqrt(1 - t*t) - 1) ); break; //円のカーブで変化、初動が遅い、最後が速い
            case 'easeOutCirc' : T = t-1; result( cp * Math.sqrt(1 - T*T) ); break; //円のカーブで変化、初動が速い、最後が遅い
            case 'easeInOutCirc' : if(t < 0.5) { result(-cp*0.5 * (Math.sqrt(1 - t*t*4) - 1)) } 
                            else { T = 2*(t-1); result( cp*0.5 *(Math.sqrt(1 - T*T) + 1) ); } break;

            case 'easeInBack' : s = 1.70158; result( cp*t*t*((s+1)*t - s) ); break; //一旦後方に下がってアニメーション開始
            case 'easeOutBack' : s = 1.70158; T = t-1; result( cp*(T*T*((s+1)*T + s) + 1) ); break; //勢いで少し行き過ぎてから着地点へ
            case 'easeInOutBack' : s = 1.70158 * 1.525;
                            if(t < 0.5) { T = 2*t; result( cp*0.5 * T*T*((s+1)*T -s) ); }
                            else { T = 2*(t-1); result( cp*0.5 * (T*T*((s+1)*T + s) + 2) ); } break;

            case 'easeOutElastic' : const p = this.duration*.3; s = p/4;
                            result( cp * Math.pow(2,-10*t) * Math.sin((this.timeCount-s)*(2*Math.PI)/p ) + cp ); break; //バネの動き

            case 'easeOutBounce':if(t < 4/11 ) { result(cp*(121*t*t)/16 ); } //落下後の着地点からバウンドするアニメーション
                            else if(t < 8/11 ) { T = t - 6/11; result(cp*((121*T*T + 12)/16) ); }
                            else if(t < 10/11) { T = t - 9/11; result(cp*((121*T*T + 15)/16) ); }
                            else { T = t - 10.5/11; result(cp*((121*T*T + 15.75)/16) ); } break;

        }
    }
switch()構文を使ってみました(' '*)
この子、私最初に多用するけど、最後は違う描き方になること多いんですよね〜。今回はどうかな〜。 モードの振り分けが得意な関数なので、ここはswitch()適任かな?

さてさて登録したeasingの種別に応じて、計算式を振り分けております。

立ち絵アニメーションのテスト

このeasingのパターンで、立ち絵の動きがどのように変わるのかテストしてみましょうか。

アニメーションテスト
⇒ 立ち絵アニメーションのデモ(スペースキーでアクション)



どうでしょ?

選択肢のパターンによって、立ち絵の登場や退散の動きが違う。
立ち絵の表示だけでも使い分けると、ちょっとした劇の表現にも使えるかも知れません。

ちなみに、NPCクラスの会話コードはこのような書き方で動くようにしています。


class NPC_girl1 extends NPC {
    constructor(x, y, name) {
        const sprite = new Sprite(assets.get('npc1'), 32, 0, 32, 32);
        const hitArea = new Rectangle(6, 4, 20, 28);
        super(x, y, sprite, hitArea, ['npc']);
        this.name = name;
        this.flag = null; //選択肢のフラグ
        this.illust = [];
        this.illust[0]=new Sprite(assets.get('up_npc_girl0'), 0, 0, 200, 400); //0
        this.illust[1]=new Sprite(assets.get('up_npc_girl1'), 0, 0, 200, 400); //1
    }
    talkStart(talkNumber) { //会話を表示する関数、NPCの拡張クラスではこの関数を上書きして各々の会話内容を設定します。
        let messages=[''];
        let callback=()=>{};
        if(this.flag === null) { //フラグによって会話を切り替えることもできる
          switch (this.talkNumber) { //番号によって、会話を別の内容で用意できる記述です

              default : messages = [ //ここに会話イベントを登録しておく
                  {name:this.name, text:['こんにちは。あのー、アニメーションテストです。', '動#[1,うご]きのパターンをえらんでね'] },
                  {select:[ //ここから選択肢を表示
                      new Select('linear', () => {
                          messages.unshift({
                              name:this.name, text:'linearが選ばれました。', 
                              img:this.illust[0], easing:'linear', enter: true, leave:true, duration:60
                          }); //)new Talk )messages.push
                      }),
                      new Select('easeInQuad', () => {
                          messages.unshift({
                              name:this.name, text:'easeInQuadが選ばれました。',
                              img:this.illust[0], easing:'easeInQuad', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeOutQuad', () => {
                          messages.unshift({
                              name:this.name, text:'easeOutQuadが選ばれました。',
                              img:this.illust[0], easing:'easeOutQuad', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInOutQuad', () => {
                          messages.unshift({
                              name:this.name, text:'easeInOutQuadが選ばれました。',
                              img:this.illust[0], easing:'easeInOutQuad', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInQuart', () => {
                          messages.unshift({
                              name:this.name, text:'easeInQuartが選ばれました。',
                              img:this.illust[0], easing:'easeInQuart', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeOutQuart', () => {
                          messages.unshift({
                              name:this.name, text:'easeOutQuartが選ばれました。',
                              img:this.illust[0], easing:'easeOutQuart', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInOutQuart', () => {
                          messages.unshift({
                              name:this.name, text:'easeInOutQuartが選ばれました。',
                              img:this.illust[0], easing:'easeInOutQuart', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInSine', () => {
                          messages.unshift({
                              name:this.name, text:'easeInSineが選ばれました。',
                              img:this.illust[0], easing:'easeInSine', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeOutSine', () => {
                          messages.unshift({
                              name:this.name, text:'easeOutSineが選ばれました。',
                              img:this.illust[0], easing:'easeOutSine', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInCirc', () => {
                          messages.unshift({
                              name:this.name, text:'easeInCircが選ばれました。',
                              img:this.illust[0], easing:'easeInCirc', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeOutCirc', () => {
                          messages.unshift({
                              name:this.name, text:'easeOutCircが選ばれました。',
                              img:this.illust[0], easing:'easeOutCirc', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInOutCirc', () => {
                          messages.unshift({
                              name:this.name, text:'easeInOutCircが選ばれました。',
                              img:this.illust[0], easing:'easeInOutCirc', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInBack', () => {
                          messages.unshift({
                              name:this.name, text:'easeInBackが選ばれました。',
                              img:this.illust[0], easing:'easeInBack', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeOutBack', () => {
                          messages.unshift({
                              name:this.name, text:'easeOutBackが選ばれました。',
                              img:this.illust[0], easing:'easeOutBack', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeInOutBack', () => {
                          messages.unshift({
                              name:this.name, text:'easeInOutBackが選ばれました。',
                              img:this.illust[0], easing:'easeInOutBack', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeOutElastic', () => {
                          messages.unshift({
                              name:this.name, text:'easeOutElasticが選ばれました。',
                              img:this.illust[0], easing:'easeOutElastic', enter: true, leave:true, duration:60
                          });
                      }),
                      new Select('easeOutBounce', () => {
                          messages.unshift({
                              name:this.name, text:'easeOutBounceが選ばれました。',
                              img:this.illust[0], easing:'easeOutBounce', enter: true, leave:true, duration:60
                          });
                      }),
                  ]},//new SelectWindow
              ];//messages = [
              callback = () => { //ここからセレクトウィンドウが終了した時の動作を記述
                  this.messageOpen(
                      {name:this.name, text:'ふふ、どんな反応だった?#.'}
                  ); 
                  this.talkNumber = 0;
              }//callback = () => {
              break;
          }//switch (this.talkNumber) {
        }//if(this.flag === null) {
        this.messageOpen(messages, callback);
    }
}



次回は、このTweenクラスを使ってシーン推移のアニメーションを試してみたいと思います。
JavaScriptでゲーム作り「19-2:Sceneのアニメーション推移」


古都さんのフレームワークを元にほぼ最初から作ってます。
たぶん順番に見ないとちんぷんかんぷん(' '*)...

すぺしゃるさんくす

https://sbfl.net/
古都さん
JavaScriptで作る弾幕STGの基礎(フレームワーク)を使わせていただいてます。感謝!


プレイヤーキャラ
ぴぽやさんもありがとう、キャラチップをお借りしています。