p5.play Spriteのクローン

# p5.play Sprite のクローン

スプライトといったらクローンさせたいよね。
でも、p5PlayではSpriteをクローンする標準の方法がないみたい（あったらごめんなさい）。

ということで クローンさせるための試行錯誤をしてみました。

## 前提条件

|  | Right align |
|:-----------|:------------:|
| p5.js       | 1.7.0        |
| plank.js     | v1.0.0-beta.16      |
| p5play.js     | 3.14     |

p5 はインスタンスモードで動かす。

p5 は WebEditor で動かす (　 [ここ](https://editor.p5js.org/)　）

# まず普通にSpriteを作る

## index.html

```: index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/addons/p5.sound.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/planck@latest/dist/planck.min.js"></script>
    <script src="https://p5play.org/v3/p5play.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />
  </head>
  <body>
    <main>
    </main>
    <script src="js/pico.js"></script>
    <script src="sketch.js"></script>
  </body>
</html>
```
## sketch.js

p.world で gravity.y を設定しています。

```
const sketch = function( p ) {

　p.preload = function() {
  };
  
  p.setup = function() {
    p.world.gravity.y = 5;
    new p.Canvas(W,H);
    pico = new p.PicoSprite(); // ← p.Sprite を継承したクラスよりインスタンスを作る
    p.noLoop();
  };
  p.draw = function() {
    p.background(100);
  };

  p.mousePressed = function() {
    p.loop();
  }
};
```

## js/pico.js (継承したクラスを init で定義するところ )

```:js/pico.js

p5.prototype.registerMethod('init', function() {

    let _p5 = this; // p5を参照できるように退避

    _p5.PicoSprite = class extends _p5.Sprite {
      
      constructor(...args) {       
        super(...args);
      }      
      
    };
});

```

![p5play_pico30.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/25219437-14a9-0237-ba8e-1544c7518aaa.gif)


# 自己クローンするcloneメソッドをPicoSpriteクラスへ追加

## sketch.js

p.world で gravity.y を設定しています。

```
const sketch = function( p ) {

　p.preload = function() {
  };
  
  p.setup = function() {
    p.world.gravity.y = 5;
    new p.Canvas(W,H);
    pico = new p.PicoSprite();
    p.noLoop();
  };
  p._timer = 0;
  p.draw = function() {
    p._timer += 1;
    if(p._timer > 2) {
      let _clone = pico.clone();
      p._timer = 0;
    }
    
    p.background(100);
  };

  p.mousePressed = function() {
    p.loop();
  }
};
```
## pico.js (継承したクラスを init で定義するところ )
```
p5.prototype.registerMethod('init', function() {

    let _p5 = this; // p5を参照できるように退避
  
    _p5.PicoSprite = class extends _p5.Sprite {
      
    constructor(...args) {        
      super(...args);
    }      
      
    clone() {
        
      let _clone = new this.constructor();
      for( let _prop in _p5.Sprite.propTypes ) {
        if( this[_prop] ) {
          _clone[_prop] = this[_prop];
        }
      }
        
      return _clone;
    }
      
  };
});
```

![p5play_pico31.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/98dbda7f-63b4-2c92-70a3-4ba2513ec0d6.gif)

## 途中評価(OK)

cloneメソッド内では、
- 自分(this)のコンストラクターでインスタンスを作ります。
- Sprite.propTypes は、スプライトがもつプロパティの名前のリストです。
- 自分(this)のプロパティを クローンインスタンスへコピーしています。

元のスプライトは重力で落ちていきながら、クローンを作っていきます。
落ちている現在値(座標) は クローンへ引き継がれていることが見てとれると思います。
またクローンができるのでスプライト同士で衝突している様子も見ることができます。

うん！『そこそこ』うまくできている気がします。

# アニメーションを追加してもうまくいくかな？

## 画像の用意

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/55ea6145-9009-8203-1e62-9902ddba9d9f.png)

## loadImage

```:sketch.js
window.onload = () => {
  new p5( sketch );
};

const W = innerWidth;
const H = innerHeight;

const sketch = function( p ) {

　p.preload = function() {
    image01 = p.loadImage('assets/Pico walk1.svg'); // ← 追加
    image02 = p.loadImage('assets/Pico walk2.svg'); // ← 追加
    image03 = p.loadImage('assets/Pico walk3.svg'); // ← 追加
    image04 = p.loadImage('assets/Pico walk4.svg'); // ← 追加
  };
  
  p.setup = function() {
    p.world.gravity.y = 5;
    new p.Canvas(W,H);
    pico = new p.PicoSprite();
    pico.addAni(image01,image02,image03,image04);　 // ← 追加
    p.noLoop();
  };
  p._timer = 0;
  p.draw = function() {
    p._timer += 1;
    if(p._timer > 2) {
      let _clone = pico.clone();
      p._timer = 0;
    }
    
    p.background(100);
  };

  p.mousePressed = function() {
    p.loop();
  }
};
```
![p5play_pico32.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/24015729-c75e-9ed1-1902-4748f53dad21.gif)


## 途中評価(OK)

元のスプライト(=pico) へ `addAni()` で画像を追加しています。
クローンには `addAni()` をしていませんが、プロパティコピーにより画像を引き継いでいます。

うん！うまくできたような気がします。

# Collider は引き継ぐでしょうか？

元のスプライト(=pico)へ　Collider。
なお、初期状態で作られているColliderは最初に消しておきます。
Colliderの様子をわかりやすくするために debug = true とします。

```:sketch.js
    ;
    (省略)
    ;

const sketch = function( p ) {

　p.preload = function() {
    ;
    (省略)
    ;
  };
  
  p.setup = function() {
    p.world.gravity.y = 0; // ← クローン側の様子をわかりやすくするため重力効果ゼロ
    new p.Canvas(W,H);
    pico = new p.PicoSprite();
    pico.debug = true
    pico.scale = 0.5; // ← 小さくしてクローン効果を 見やすくする。
    pico.addAni(image01,image02,image03,image04);
    pico.removeColliders();   // ← Collider を全部消す
    pico.addCollider(0,-10,50); // ← Collider 50 の円( やや上に )
    pico.addCollider(0, 10,50); // ← Collider 50 の円( やや下に )
    p.noLoop();
  };
  p._timer = 0;
  p.draw = function() {
    p._timer += 1;
    if(p._timer > 10) {  // クローンする頻度を落とす( 2 → 10 )
      let _clone = pico.clone();
    　_clone.x += 80;  // クローンは右側へ寄せておく（見やすくするため）
      p._timer = 0;
    }
    
    p.background(100);
  };

  p.mousePressed = function() {
    p.loop();
  }
};
```
![p5play_pico34.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/3c5c737e-2605-cafa-e71c-3884483a0832.gif)

## 途中評価(NG)

scale の設定は、クローンへ引き継がれています。
Colliderの設定ですが、引き継がれていません。ダメでした。

# Colliderを引き継げるのか？

p5Playは、Plank.js の衝突判定の機能に依存しています。p5PlayのColliderを引き継ぐためにはPlank.js の仕組みを解読する必要があります。

結論から述べると『私』はPlank.jsの解読をすることができませんでした。元のスプライトの情報を取り込み、Colliderの情報をクローンへ引き継いだように見えることもありましたが、衝突判定をしませんでした。

それで元スプライト情報からクローンのColliderを作り出す作戦はあきらめまして、別の方法を検討することにしました。


## 検討案

p5Play Sprite のCollider にかかわるメソッドは次のとおりです。

- addCollider()
- removeColliders()

Spriteクラスを継承する新しいクラスで 上の２つのメソッドをオーバーライドし、Colliderの設定を自分のインスタンスへため込みます。ため込んだColliderの設定をクローンした後に再現してあげることにします。

## 具体的には？

```:js/pico.js
p5.prototype.registerMethod('init', function() {

    let _p5 = this; // p5を参照できるように退避
  
    _p5.PicoSprite = class extends _p5.Sprite {
      
    constructor(...args) {        
      super(...args);
    }      
      
    clone() {
        
      let _clone = new this.constructor();
      for( let _prop in _p5.Sprite.propTypes ) {
        if( this[_prop] ) {
          _clone[_prop] = this[_prop];
        }
      }
      this._colliderArgArr.forEach((_m)=>{
        if( _m.has("remove")) {     // ← removeしていたら 同じく removeColliders()を実行
          _clone.removeColliders();
        }else if( _m.has("add")) {  // ← addしていたら 同じく addCollider(引数)を実行
          let _args = _m.get("add");
          _clone.addCollider(..._args)
        }
        
      });
      return _clone;
    }

    // オーバーライドする
    removeColliders() {      
      super.removeColliders();

      if(this._colliderArgArr == undefined) {
        this._colliderArgArr = [];
      }
      const map = new Map();
      map.set('remove',[]);
      this._colliderArgArr.push(map); // ← removeしたことを記録
    }
    
    // オーバーライドする
    addCollider(...args) {
      super.addCollider(...args);

      if(this._colliderArgArr == undefined) {
        this._colliderArgArr = [];
      }
      const map = new Map();
      map.set('add',args);
      this._colliderArgArr.push(map); // ← addColliderの引数をため込む
    }
  };
});

```

## 評価(OK)

クローンへ 複数の Collider を引き継ぎ、衝突もする

![p5play_pico35.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/9ee0f499-433b-67bb-435e-2780f34d55da.gif)


# 最終結論

## 完全な（すべてに対応できる）クローンの方法

とても難しい。あきらめる。

## とりあえず便利そうなクローンの方法

基本的なプロパティを引き継ぎ、アニメーションを引き継ぎ、Colliderを引き継ぐ程度で 『私』は満足なので、上で書いた方法でよいのではないかと思う次第です。

(2023/10/25 追記)
WebEditor 上にあるソースを公開しておきます。
https://editor.p5js.org/amami-harhid/full/GXpYaQUh1