JavaScriptでゲーム作り「19:Tweenアニメーション」

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

会話イベント立ち絵&名前
⇒ 立ち絵フェード表示の完成形デモ(スペースキーでアクション)

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

(2019.8.6執筆)

アニメーションをどのように実装するか、これまで悩みどころでした。
前回立ち絵のフェードアニメーションを実装する際に、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クラスの定義


class Tween extends EventDispatcher {//Tweenアニメーションを「フレーム」で制御、アクターやウィンドウ、キャンバスのアニメーションに使用
    constructor(target, key, beginProps, finishProps, duration, easing = "linear", callback) {
        super();
        this.target = target; //変化させるプロパティを含んだオブジェクトをターゲットで指定
        this.key = key; //変化させるプロパティ名を文字列で渡す
        //this.target[this.key]で、アニメーション推移させるプロパティにアクセスできるように。

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

        this.timeCount = 0; //アニメーションの経過フレームを記録
        if(callback !== undefined) { this.addEventListener('finish', callback, {once:true}); } //アニメーション終了時に登録されたコールバック関数を1回だけ起動
    }

    release() { this.dispatchEvent('release', new Event(this)); } //自身を削除する関数(シーン側で)

    update() {
        this.timeCount ++; // 経過フレームをカウント
        if (this.timeCount >= this.duration) { // 経過時間がアニメーション終了に達したら
            this.target[this.key] = this.finishProps; // ターゲットを終了値に指定
            this.dispatchEvent('finish'); // 終了時にコールバック関数があれば起動
            this.release(); // アニメーションを終了
        }
        else { this.animation(); } //アニメーション中なら、値の変化を計算する必要がある
    }
    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 ...推移変化をどのようにする? デフォルトでは等間隔


追加で、アニメーション終了時に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.fade = option.fade ? option.fade : 16; //フェード効果の経過フレーム数を指定、デフォルトは16フレーム
        this.enter = option.enter ? true : false; //登場時に画面端からスライドする?
        this.leave = option.leave ? true : false; //退場時に画面端にスライドする?

        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 = []; //立ち絵のアニメーションを管理
    }

    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 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);
        }
    }
    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 = t; //easingによっては、計算前にtを変化させる場合がある
        const cp = this.finishProps - this.beginProps; //開始値と終了値の差...ChangePropsの略
        const result = (changePropsResult) => this.target[this.key] = this.beginProps + changePropsResult; //開始値から変化値を加算して、現在の値を求める

        let s; //easingのBackで使う定数。
        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 '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 '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の種別に応じて、計算式を振り分けております。


【目次】
  1. JavaScriptでゲーム作り「1:基礎編」
  2. JavaScriptでゲーム作り「2:プレイヤーの歩かせ方」
  3. JavaScriptでゲーム作り「3:プレイヤーの向きに応じてアクションを起こす」
  4. JavaScriptでゲーム作り「4:NPCと会話する」
  5. JavaScriptでゲーム作り「5:当たり判定を最適化する」
  6. JavaScriptでゲーム作り「6:線分の当たり判定を実装する」
  7. JavaScriptでゲーム作り「7:フィールドの背景スクロール」
  8. JavaScriptでゲーム作り「8:プログラムの最適化、高速化、コード整理」
  9. JavaScriptでゲーム作り「9:タッチ・マウスイベント入力」
  10. JavaScriptでゲーム作り「10:法線ベクトルと衝突時のバウンス判定」
  11. JavaScriptでゲーム作り「11:多角形を使った魔法陣を実装する」
  12. JavaScriptでゲーム作り「12:キャッシュの扱い方と計算処理の高速化」
  13. JavaScriptでゲーム作り「13:メッセージウィンドウを実装する」
  14. JavaScriptでゲーム作り「14:イベントの仕組みを理解する」
  15. JavaScriptでゲーム作り「15:メッセージテキスト表示の機能追加」
  16. JavaScriptでゲーム作り「16:会話時の立ち絵&名前表示」


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

すぺしゃるさんくす

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


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