CocosCreatorのLife cycle callbackを実動作から把握する

# 1. はじめに

Webページ読み込み完了時にユーザのonLoad関数が呼び出されるように、CocosCreatorのComponentスクリプトにもNodeの状態遷移に対応したコールバック機能が用意されています。
公式のマニュアルにも説明はありますが、細かな動きが分かりづらかったため、実際に検証して整理してみました。

使用したバージョン: CocosCreator 2.1
参考: [公式マニュアル(Life cycle callback)](https://docs.cocos2d-x.org/creator/2.1/manual/en/scripting/life-cycle-callbacks.html)

# 2. 検証方法

実際のゲーム開発でよく見られそうなNode構成を対象に、サンプルを用いてコールバック関数がどのような順番で呼び出されるのかを中心に確認しました。

今回用いたサンプルの概要（検証構成）は以下の通りです。

- 二重のNodeで構成されたPrefabを作成（2つのNodeを子Nodeと孫Nodeと呼ぶ）
- 上記Prefabをインスタンス化し、別のNodeにaddChildする(このNodeを親Nodeと呼ぶ)
- 5秒後にPrefabをremoveChildし、10秒後に再度addChildする
- 15秒後に子Node自らがdestroyする

![callback4.png](https://qiita-image-store.s3.amazonaws.com/0/94561/34135597-0f5c-b6fb-6f64-2ddf790e235a.png)

# 3. サンプルの作成手順

※※※ 長いので結果だけ知りたい方は読み飛ばしてかまいません ※※※
空プロジェクトを作成し、親・子・孫Nodeに関連づける3つのJavaScriptを作成します。
![callback1.png](https://qiita-image-store.s3.amazonaws.com/0/94561/cd1764ac-9b16-3945-d0f6-beb88ff69344.png)


＜親のスクリプト＞

```javascript:ParentNode.js
cc.Class({
    extends: cc.Component,

    properties: {
        TargetPrefab: {
            default: null,
            type: cc.Prefab
        }
    },

    onLoad () {
        cc.log(new Date() + " : 親 onLoad() called.");
        let target = cc.instantiate(this.TargetPrefab);
        this.node.addChild(target);

        this.scheduleOnce(function (dt) {
            this.node.removeChild(target);
        }, 5);

        this.scheduleOnce(function (dt) {
            this.node.addChild(target);
        }, 10);
    }
});
```

親のスクリプトは、onLoad時にPrefabをaddChildします。
5秒後にPrefabをremoveChildしてdisable化、10秒後に再度addChildしてenable化します。
<br>

＜子のスクリプト＞

```javascript:ChildNode.js
cc.Class({
    extends: cc.Component,

    onLoad () {
        cc.log(new Date() + " : 子 onLoad() called.");
        cc.log(new Date() + " : 孫(子から見た子)のchildrenCount = " + this.node.getChildByName("GrandchildNode").childrenCount);

        // 10秒後に自分自身を消滅させる
        // (5秒間のDisable状態による中断を含むため、実際は15秒後に消滅する)
        this.scheduleOnce(function (dt) {
            this.node.destroy();
        }, 10);
    },

    start () {
        cc.log(new Date() + " : 子 start() called.");
    },

    onEnable () {
        cc.log(new Date() + " : 子 onEnable() called.");
    },

    onDisable () {
        cc.log(new Date() + " : 子 onDisable() called.");
    },

    onDestroy () {
        cc.log(new Date() + " : 子 onDestroy() called.");
        // cc.log(new Date() + " : 孫(子から見た子)のchildrenCount = " + this.node.getChildByName("GrandchildNode").childrenCount);  ⇒nullアクセスエラーになる
    }
});
```

子のスクリプトは、各callback関数が呼び出されるとログを出力します。
onLoad時とonDestroy時に孫Nodeにアクセスしてみます。
（onDestroy時はnullアクセスエラーになるのでコメントアウト）
また、onLoadから15秒後に自分自身をdestroyさせます。
<br>

＜孫のスクリプト＞

```javascript:GrandchildNode.js
cc.Class({
    extends: cc.Component,

    onLoad () {
        cc.log(new Date() + " : 孫 onLoad() called.");
        cc.log(new Date() + " : 子(孫から見た親)のchildrenCount = " + this.node.getParent().childrenCount);
    },

    start () {
        cc.log(new Date() + " : 孫 start() called.");
    },

    onEnable () {
        cc.log(new Date() + " : 孫 onEnable() called.");
    },

    onDisable () {
        cc.log(new Date() + " : 孫 onDisable() called.");
    },

    onDestroy () {
        cc.log(new Date() + " : 孫　onDestroy() called.");
        cc.log(new Date() + " : 子(孫から見た親)のchildrenCount = " + this.node.getParent().childrenCount);
    }
});
```

孫のスクリプトは、各callback関数が呼び出されるとログを出力します。
onLoad時とonDestroy時に"孫から見た親"である子Nodeにアクセスしてみます。
<br>

下の画面のように、2つの空Nodeを親子関係で構成したPrefabを作成します。
（Node名はそれぞれChildNodeとGrandchildNode、Prefab名はTargetPrefab）
またGUI上で、ChildNodeにはChildNode.jsを、GrandchildNodeにはGrandchildNode.jsをAdd Componentします。

![callback3.png](https://qiita-image-store.s3.amazonaws.com/0/94561/93c17e79-104e-50ca-c637-b15a11f5d53b.png)

Node Tree上に空Nodeを作成します（Node名はParentNode）。
GUI上でParentNodeにParentNode.jsをAdd Componentし、「Target Prefab」の入力欄に先ほど作成したTargetPrefabをドラッグ&ドロップします。

![callback2.png](https://qiita-image-store.s3.amazonaws.com/0/94561/7c00086f-0080-74de-1332-8ddc9a588773.png)


# 4. 検証結果

以下のログ出力結果となりました。

```
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 子 onLoad() called. (ChildNode.js, line 11)
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 孫(子から見た子)のchildrenCount = 0 (ChildNode.js, line 12)
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 孫 onLoad() called. (GrandchildNode.js, line 11)
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 子(孫から見た親)のchildrenCount = 1 (GrandchildNode.js, line 12)
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 子 onEnable() called. (ChildNode.js, line 24)
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 孫 onEnable() called. (GrandchildNode.js, line 18)
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 子 start() called. (ChildNode.js, line 21)
[Log] Mon Jan 28 2019 17:15:32 GMT+0900 (JST) : 孫 start() called. (GrandchildNode.js, line 15)
[Log] Mon Jan 28 2019 17:15:37 GMT+0900 (JST) : 子 onDisable() called. (ChildNode.js, line 27)
[Log] Mon Jan 28 2019 17:15:37 GMT+0900 (JST) : 孫 onDisable() called. (GrandchildNode.js, line 21)
[Log] Mon Jan 28 2019 17:15:42 GMT+0900 (JST) : 子 onEnable() called. (ChildNode.js, line 24)
[Log] Mon Jan 28 2019 17:15:42 GMT+0900 (JST) : 孫 onEnable() called. (GrandchildNode.js, line 18)
[Log] Mon Jan 28 2019 17:15:47 GMT+0900 (JST) : 子 onDisable() called. (ChildNode.js, line 27)
[Log] Mon Jan 28 2019 17:15:47 GMT+0900 (JST) : 孫 onDisable() called. (GrandchildNode.js, line 21)
[Log] Mon Jan 28 2019 17:15:47 GMT+0900 (JST) : 孫　onDestroy() called. (GrandchildNode.js, line 24)
[Log] Mon Jan 28 2019 17:15:47 GMT+0900 (JST) : 子(孫から見た親)のchildrenCount = 1 (GrandchildNode.js, line 25)
[Log] Mon Jan 28 2019 17:15:47 GMT+0900 (JST) : 子 onDestroy() called. (ChildNode.js, line 30)
```

以下に結果の要点を整理します。

- 子Nodeの状態遷移と呼び出されるcallback関数は下図の通り
- いずれのcallback関数も子、孫の順
番に呼ばれる。ただしonDestroyは例外で孫、子の順番で呼ばれる
- 子NodeのonLoad（まだ孫のonLoadは呼ばれていない）時点でも、孫Nodeにアクセスできる
- 子NodeのonDestroy時点では<font color="red">孫Nodeにアクセスできない</font>

![callback5.png](https://qiita-image-store.s3.amazonaws.com/0/94561/9cd911ab-abe3-854f-81cc-e545c1333b13.png)

# 5. コールバック関数の整理

上記の検証結果を踏まえて、各コールバック関数の仕様や用途を以下に整理しました。
※※※ 今回のサンプル実行結果の範囲、かつ主観的な記述のためご注意ください ※※※

## onLoad
初回のaddChild等、NodeがSceneに読み込まれたタイミングで呼び出される。
自Nodeの機能が使えることが保証され、子Nodeの機能も使用可能。
最初に一度だけ呼ばれるので、パラメータ初期化やNodeの初期設定等の記述に向く。

## onEnable
NodeがEnable状態に遷移したタイミングで呼び出される。
初回のaddChild時は、onLoadの後に呼び出される。
Disable化で無効化したものを元に戻す処理の記述に向く。

## onStart
最初にupdateを実行する直前に一度だけ呼び出される。
初回のaddChild時は、onEnableの後に呼び出される。
（onStartに向く処理は思い浮かびませんでした）

## onDisable
NodeがDisable状態に遷移したタイミングで呼び出される。
一時的に無効化する処理の記述に向く。

## onDestroy
Node削除時に呼び出される。
自Nodeに関連するリソース（特に親Node管理されているリソース）の削除処理の記述に向く。
なお、子Nodeはすでに削除済みのためアクセスできない。