p5.play のSpriteクラスを継承させたい【改良版】

# はじめに

Qiita記事 [p5.play のSpriteクラスを継承させたい](https://qiita.com/harhid/items/350ac45192d2204dc247) にて p5.play Spriteクラスを継承する方法を書きましたが、もっとスマートに継承クラスを記述したいなと思いました。

そこで『動的にJavascriptファイルをロード』の方向で試行をしました内容をここで共有します。

# 前回Qiita記事のやり方の不満な点

## function(){  } で囲む点

クラスをさくさくと書きたいのに 下記のようなことを新規クラス定義ごとに書かないといけないです。少々めんどくさい感があります。

## init処理でクラスを定義するやり方

```
p5.prototype.registerMethod('init', function() { //<--- 書きたくない！
    const p = this; 
    p.MySprite = class extends p.Sprite {
        //　オーバーライドしたいことを書く。
    }
}; //<--- 書きたくない！
```

#### → こう書きたい！
```
p.MySprite = class extends p.Sprite {
    //　オーバーライドしたいことを書く。
}
```

## 加えて、子・孫と継承できるようにしたい

別のJSファイルに　子・孫の継承クラスを書けるようにしたい。

```: A ファイル ( p.Sprite を継承する )
p.MySprite = class extends p.Sprite {
    //　オーバーライドしたいことを書く。
}
```

```: B ファイル ( Aファイルに書かれているクラスを継承する）
p.MyChildSprite = class extends p.MySprite {
    //　オーバーライドしたいことを書く。
}
```

# 非同期並列型：動的にJavascriptファイルのロード

ロードする処理は非同期です。ロードを完了する前に setup() を開始したくはありません。
よって preload() のなかで p5.js 標準のpreloadメソッドと同様の 独自のpreloadメソッドを作ってみましょう。

## 参考

[Qiita記事：p5.js preloadの仕組みを調べた](https://qiita.com/harhid/items/9059a9caf33194615d20)

## 非同期並列型：サンプル

[p5.js WebEditor(DynamicLoadScriptSample1)](https://editor.p5js.org/amami-harhid/full/vzDKYnzAF)

p5.prototype.dynamicLoadScript() という独自のpreloadメソッドを用意してみました。

```: sketch.js
// 一部抜粋
const sketch = function( p ) {
    p.preload = function() { 
        p.dynamicLoadScript('./js/Pico.js'); // → JSファイルのパスを与える
    };
    p.setup = function() {
        pico = new p.PicoSprite(); // → このタイミングで p.PicoSpriteの参照ＯＫ
    };
}
```
```: js/Pico.js
p.PicoSprite = class extends p.Sprite {
    // オーバーライドしたい内容をここに書く。
}
```
こんな感じで使えます。

## 非同期並列型：特徴

p.preloadの中でロードしないといけません。１つのJSファイルごとに ロードを行います。
複数のJSファイルをロードしようとして p.dynamicLoadScript() を複数回呼び出すと、非同期並行でロードします。

```
p.preload = function() { 
     p.dynamicLoadScript('./js/A.js');
     p.dynamicLoadScript('./js/B.js');
     p.dynamicLoadScript('./js/C.js');
};
```
上記のように A.js, B.js, C.js をロードするとき、ロードの順番は保証されず！（どれが先にロードされるかはわからない）です。


## 非同期並列型：コードの例

### 独自のpreloadメソッドを登録

p5.prototype.registerPreloadMethod を使って 独自のpreloadメソッドを登録します。
preloadメソッドは 非同期の完了を検知して preload処理中の数を デクリメントをせねばなりません（デクリメントしないと setup() が開始されません）。この辺の「詳しく」に興味があれば【[Qiita記事：p5.js preloadの仕組みを調べた](https://qiita.com/harhid/items/9059a9caf33194615d20)】をみてください（読まなくてもいいけど）。


```: js/dynamicLoadScript.js
p5.prototype.registerMethod('init', function() {
  
  // p5インスタンスを退避する
  const p = this;
  
  // p5インスタンスをwindowへ突っ込むことで
  // クラス定義をしているJSファイル内で p を直書きできる。
  window.p = p; 
  
  const _msTime = Date.now(); // Cache回避のために現在ミリ秒を取得する
  // preloadメソッドの登録
  p5.prototype.registerPreloadMethod('dynamicLoadScript', p5.prototype);

  // 非同期ロード処理を定義する
  p5.prototype.dynamicLoadScript = function(_file_url){
    const _promise = _dynamicLoadScript(_file_url, _msTime);
    _promise.then(()=>{
      p._decrementPreload(); // → 非同期の完了のタイミングで preload処理中の数を デクリメントします。
    });
  }
});
```
### p5.prototype.registerPreloadMethod()　引数

- 第一引数は メソッド名です。
- 第二引数は メソッドがある場所です（※）

（※）サンプルでは p5.prototype にメソッドを用意しています。 window　にメソッドを用意する場合は、第二引数は window です。

### 独自preload処理の中身( _dynamicLoadScript )

`script`タグを作り、JSファイルのpathを src へ与えることでロードを実行します。
Promiseを返すことにします。呼び出し元で 非同期完了を検知させるためです。
Cache回避： path の後ろに ?ts=(ミリ秒) をつけています


```: js/dynamicLoadScript.js の下の方
/*
 * _dynamicLoadScript
 * Javascriptを読み込む処理をPromiseとして返す。
*/
const _dynamicLoadScript = function(_file_url, _msTime, _async = true, _type = "text/javascript") {
  
  const _url = `${_file_url}?ts=${_msTime}`;// Cache回避対策
  return new Promise((resolve,reject)=>{
    try {
      const scriptEle = document.createElement("script");
      scriptEle.type = _type;
      scriptEle.async = _async;
      scriptEle.src = _url;

      scriptEle.addEventListener("load", (ev) => {
        resolve({ status: true });
      });

      scriptEle.addEventListener("error", (ev) => {
        reject({
          status: false,
          message: `Failed to load the script ${_url}`
        });
      });
      document.body.appendChild(scriptEle);
      
    } catch (error) {
      reject(error);
    }
  });
}

```


# 非同期直列型：動的にJavascriptファイルのロード

非同期並列型では、複数のJSファイルのロードのときに ロードの順番が保証されません。
並列でロードするので その分早く終わるはずですが、順番を保証したい場合には不向きです。

## 順番にロードしたいとき

### 別々のJSファイルにて 子クラス・孫クラスと継承クラスを書きたいとき

『別のJSファイルに　子・孫の継承クラスを書けるようにしたい！』がそもそもの目標でしたので、並列型ではダメです。直列に A のロードの後、B のロードをする・・・と順番を保証したいわけです。

```: A ファイル ( p.Sprite を継承する )
p.MySprite = class extends p.Sprite {
    //　オーバーライドしたいことを書く。
}
```

```: B ファイル ( Aファイルに書かれているクラスを継承する）
p.MyChildSprite = class extends p.MySprite {
    //　オーバーライドしたいことを書く。
}
```
```: C ファイル ( Aファイルに書かれているクラスを継承する）
p.MyGrandChildSprite = class extends p.MyChildSprite {
    //　オーバーライドしたいことを書く。
}
```

## コード（非同期直列型）

JSファイルのパスを 配列で用意し、配列の要素順にロードをする！としました。
並列型では p5.js の preloadメソッドとして登録していましたが、直列型では preloadメソッド登録はせずに、自前で 『 preload処理中の数』のインクリメント・デクリメントを制御しています。

### 与えられた複数のパスをもとに直列型（順次）にロードのアイデア

preloadメソッドとして登録して、与えられた複数パスのロードがすべて完了したら、デクリメントする。これで preloadメソッドとして成り立つはず。

## Githubでコードを確認（サンプル）

[Github : p5DynamicLoadScript](https://github.com/amami-harhid/p5DynamicLoadScript)

## Github Pageで動きを確認（サンプル）

[Github Page : p5DynamicLoadScript]( https://amami-harhid.github.io/p5DynamicLoadScript)


## WebEditorで動きを確認（サンプル）

[p5.js WebEditor(DynamicSeriallyLoadScriptSample)](https://editor.p5js.org/amami-harhid/sketches/avlkmgR4t)

以下の４つのクラスを JSファイルで用意しています。
説明のためにクラスの中身はを空にしていますが、気になれば上記のサンプルで確認してください。

```: js/Pico.js
p.PicoSprite = class extends p.Sprite {
    // 内容は省略
}
```
```: js/Pico2.js
p.PicoSprite2 = class extends p.SpriteSprite {
    // 内容は省略
}
```
```: js/Pico3.js
p.PicoSprite3 = class extends p.SpriteSprite2 {

}
```
```: js/Pico4.js
p.PicoSprite4 = class extends p.SpriteSprite4 {

}
```
```: sketch.js
const _scriptPathArr = [
  './js/Pico.js',  // Spriteを継承
  './js/Pico2.js', // Picoを継承して speakメソッドをOverride
  './js/Pico3.js', // Pico2を継承
  './js/Pico4.js', // Pico3を継承
];
const sketch = function( p ) {
　p.preload = function(){
    // 引数配の要素順にロード順を保証する
    p.dynamicLoadScript(_scriptPathArr);
    picoImage = p.loadImage('./assets/Pico walk1.svg')
  };
  
  p.setup = function(){
    // 一部記載を割愛
    pico = new p.PicoSprite(W*0.2,H/2);
    pico.addImage(picoImage);
    pico2 = new p.PicoSprite4(W*0.6,H/2); // ４番目のクラスでインスタンス化（成功）
    pico2.addImage(picoImage);
  };
  p.draw = function() {
    p.background(250);
  }
};
```
PicoSpriteは弱くジャンプ, PicoSprite2 は強くジャンプします
PicoSprite2 を継承する PicoSprite4 も強くジャンプします。

![p5play_pico38.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/6397edde-1d88-7d14-619d-a52e032b83d3.gif)



## コード(dynamicLoadScript.js)

```: dynamicLoadScript.js
/*
 * p5のinit処理の中で『dynamicLoadScript』をpreloadメソッドとして登録する。 
 * dynamicLoadScript内でPromise処理で処理完了した数がパス配列の数に達したら、preload数を減算する。
 */
p5.prototype.registerMethod('init', function() {
  const p = this;
  window.p = p;
  
  // preloadメソッドとして登録する
  p5.prototype.registerPreloadMethod('dynamicLoadScript', p5.prototype);

  p5.prototype.dynamicLoadScript = function(path){

    const _msTime = Date.now(); // Cache回避用

    // 引数が配列でないときは配列化する
    let _pathArr;
    if( Array.isArray(path) ) {
      _pathArr = path;
    }else{
      _pathArr = [path];      
    }

    // 引数のパス数
    let _scriptPreLoadCount = _pathArr.length;

    // 順番に直列でロードする。
    (async () => {
      await (async () => {
        for (const _path of _pathArr) {
          const _pathNonCache = `${_path}?ts=${_msTime}`;// Cacheされないように対策
          await _loadScriptDynamic(_pathNonCache)
          .then(()=>{
            // ロード数に達したら デクリメントする
            _scriptPreLoadCount -= 1;
            if( _scriptPreLoadCount == 0){
              p._decrementPreload();
            }
          })
          .catch((err)=>{
            // reject時は意図的にデクリメントしない → setupは動かない。
            console.log("ERROR!=",err);
          });
        }
      })();
    })();
  }
});
```
並列型のときは タイムアウト処理を入れていませんでした。直列型では入れてみました。
```
/*
 * _dynamicLoadScript
 * Javascriptを読み込む処理をPromiseとして返す。
*/
const _loadScriptDynamic =  function(_path, _ms=3000, _async = true, _type = "text/javascript") {
  return new Promise((resolve, reject)=>{
    try {
      setTimeout(() => reject('timeout'), _ms); // タイムアウト処理
      
      const scriptEle = document.createElement("script");
      scriptEle.type = _type;
      scriptEle.async = _async;
      scriptEle.src = _path;

      scriptEle.addEventListener("load", (ev) => {
        resolve({ status: true });
      });

      scriptEle.addEventListener("error", (ev) => {
        reject({
          status: false,
          message: `Failed to load the script ${_path}`
        });
      });
      document.body.appendChild(scriptEle);
      
    } catch (error) {
      reject(error);
    }
  });
}
```

# 非同期直列型：動作結果

継承したクラスの定義に成功し、setup() のなかでのインスタンス化もできました。
まずまずの出来ではないでしょうか（自己満足）。


# まとめ

『p5.play のSpriteクラスを継承したクラスを作りたい』という思いからp5.js や p5.play を調べはじめていました。

やっと、JSファイルを動的にロードする（非同期直列型）のアイデアにたどり着くことができました。

ひとまずは目的を達したのかな？と思います（半年かかったぜ）。

今後も p5.play を使いやすくするアイデアをまとめていこうと思います。