CocosCreator システムイベントがどのように生成され、トリガーされるかについての詳細な説明

CocosCreator システムイベントがどのように生成され、トリガーされるかについての詳細な説明

環境

ココスクリエイター2.4
クローム88

まとめ

モジュール機能

イベント監視メカニズムは、すべてのゲームに不可欠な部分である必要があります。ボタンをクリックする場合でも、オブジェクトをドラッグする場合でも、イベントの監視と配信は不可欠です。
主な機能は、ノードのオン/ワンス機能を通じてシステム イベント (タッチ、クリックなど) を監視し、対応するゲーム ロジックをトリガーすることです。同時に、カスタム イベントの起動/リッスンもサポートされています。この点については、公式ドキュメントのイベントのリッスンと起動を参照してください。

関連文書

このうち、CCGame と CCInputManager はどちらもイベントの登録に関与していますが、担当する部分は異なります。

ソースコード分析

イベントはどのようにしてエンジンに届きますか (ブラウザから)?

この質問に答えるには、エンジンとブラウザ間のやりとりがどこから来るのかを理解する必要があります。
ここにコードがあります。

CCGame.js

// イベントを初期化する system_initEvents: function () {
  var win = window、hiddenPropName;

  //_ システムイベントを登録する
  // システムイベントを登録します。ここでは CCInputManager メソッドを呼び出します if (this.config.registerSystemEvent)
    _cc.inputManager.registerSystemEvent(this.canvas);

  // document.hidden はページが非表示であることを意味します。次の if はブラウザの互換性を処理するために使用されます if (typeof document.hidden !== 'undefined') {
    hiddenPropName = "隠し";
  } そうでない場合 (typeof document.mozHidden !== 'undefined') {
    隠しプロパティ名 = "mozHidden";
  } そうでない場合 (typeof document.msHidden !== 'undefined') {
    隠しプロパティ名 = "msHidden";
  } そうでない場合 (typeof document.webkitHidden !== 'undefined') {
    隠しプロパティ名 = "webkitHidden";
  }

  // 現在のページは非表示ですか? var hidden = false;

  //ページが非表示になったときにコールバックし、game.EVENT_HIDEイベントを発行します。function onHidden() {
    (!隠し)の場合{
      非表示 = true;
      game.emit(game.EVENT_HIDE);
    }
  }
  //_ ほとんどのプラットフォームに適応するために、onshow API を使用します。
  // ほとんどのプラットフォームの onshow API に適応します。パラメータを渡す部分を参照する必要があります...
  // ページが表示されているときにコールバックし、game.EVENT_SHOW イベントを発行します。 function onShown (arg0, arg1, arg2, arg3, arg4) {
    if (隠し) {
      非表示 = false;
      game.emit(game.EVENT_SHOW, arg0, arg1, arg2, arg3, arg4);
    }
  }

  // ブラウザが隠しプロパティをサポートしている場合は、ページの視覚状態変更イベントを登録します。if (hiddenPropName) {
    var チェンジリスト = [
      「可視性の変更」、
      「mozvisibilitychange」、
      "msvisibilitychange",
      「webkitvisibilitychange」、
      「qbrowserVisibilityChange」
    ];
    // 互換性のために上記のリストのイベントをループします // 非表示状態が変更された後、表示状態に基づいて onHidden/onShown コールバック関数を呼び出します for (var i = 0; i < changeList.length; i++) {
      document.addEventListener(changeList[i], 関数(イベント) {
        var visible = document[hiddenPropName];
        //_QQアプリ
        visible = 表示 || イベント["hidden"];
        (表示)の場合
          非表示();
        それ以外
          表示中();
      });
    }
  }
  // ページの視覚状態の変更に関する一部の互換性コードはここでは省略されています // 非表示および表示イベントを登録し、ゲームのメインロジックを一時停止または再開します。
  this.on(game.EVENT_HIDE, 関数() {
    ゲームを一時停止します。
  });
  this.on(game.EVENT_SHOW, 関数() {
    ゲームを再開します。
  });
}

実はコアコードはほんの少ししかありません…さまざまなプラットフォームとの互換性を保つために、
重要なポイントは2つあります。

  1. CCInputManager メソッドの呼び出し
  2. ページのビジュアル状態変更イベントを登録し、game.EVENT_HIDE イベントと game.EVENT_SHOW イベントをディスパッチします。

CCInputManager を見てみましょう。

CCInputManager.js

// システムイベントを登録する要素はキャンバスです
registerSystemEvent (要素) {
  if(this._isRegisterEvent) 戻り値:

  // すでに登録済みなので直接戻ります
  this._glView = cc.view;
  selfPointer を this とします。
  キャンバスバウンディングレクトを this._canvasBoundingRect とします。

  // サイズ変更イベントをリッスンし、this._canvasBoundingRect を変更します
  window.addEventListener('resize', this._updateCanvasBoundingRect.bind(this));

  禁止 = sys.isMobile とします。
  supportMouse = ('mouse' in sys.capabilities); とします。
  // タッチレットをサポートするかどうか supportTouches = ('touches' in sys.capabilities);
	
  // マウスイベントの登録コードを省略 //_タッチイベントを登録
  // タッチイベントを登録する if (supportTouches) {
    // イベントマップ
    _touchEventsMap = { を設定します
      "touchstart": 関数 (touchesToHandle) {
        selfPointer.handleTouchesBegin(touchesToHandle);
        要素にフォーカスを当てる();
      },
      "touchmove": 関数 (touchesToHandle) {
        selfPointer.handleTouchesMove(touchesToHandle);
      },
      "touchend": 関数 (touchesToHandle) {
        selfPointer.handleTouchesEnd(touchesToHandle);
      },
      "touchcancel": 関数 (touchesToHandle) {
        selfPointer.handleTouchesCancel(touchesToHandle);
      }
    };

    // マップをトラバースしてイベントを登録する let registerTouchEvent = function (eventName) {
      ハンドラーを_touchEventsMap[イベント名]に設定します。
      // キャンバスにイベントを登録する element.addEventListener(eventName, (function(event) {
        if (!event.changedTouches) が return;
        本文を document.body とします。

        // オフセットを計算します canvasBoundingRect.adjustedLeft = canvasBoundingRect.left - (body.scrollLeft || window.scrollX || 0);
        canvasBoundingRect.adjustedTop = canvasBoundingRect.top - (body.scrollTop || window.scrollY || 0);
        // イベントからタッチ ポイントを取得し、コールバック関数を呼び出します。handler(selfPointer.getTouchesByEvent(event, canvasBoundingRect));
        // イベントのバブリングを停止します。event.stopPropagation();
        イベントをデフォルトにしない();
      })、 間違い);
    };
    for (let eventName in _touchEventsMap) {
      タッチイベントを登録します(イベント名);
    }
  }

  // イベント登録が完了したことを示すプロパティを変更します。this._isRegisterEvent = true;
}

コードでは、主に touchstart などの一連のネイティブ イベントを登録します。イベント コールバックでは、selfPointer(=this) 内の関数が呼び出されて処理されます。ここでは、touchstart イベント、つまり handleTouchesBegin 関数を例として使用します。

// タッチスタートイベントを処理する handleTouchesBegin (touches) {
  selTouch、index、curTouch、touchID、
      handleTouches = []、locTouchIntDict = this._touchesIntegerDict、
      今 = sys.now();
  // タッチポイントをトラバースする for (let i = 0, len = touches.length; i < len; i ++) {
    // 現在のタッチポイント selTouch = touches[i];
    // タッチポイントID
    タッチID = selTouch.getID();
    // タッチポイントリスト内のタッチポイントの位置 (this._touches) index = locTouchIntDict[touchID];

    // インデックスが取得されない場合は、新しいタッチポイント(押されたばかり)であることを意味します
    インデックスが null の場合
      // 未使用のインデックスを取得する
      unusedIndex を this._getUnUsedIndex() とします。
      // 取得できません。エラーが発生します。サポートされているタッチポイントの最大数を超えている可能性があります。
      未使用インデックス === -1 の場合 {
        cc.logID(2300、未使用インデックス);
        続く;
      }
      //_curTouch = this._touches[未使用インデックス] = selTouch;
      //タッチポイントを保存します。 curTouch = this._touches[unusedIndex] = new cc.Touch(selTouch._point.x, selTouch._point.y, selTouch.getID());
      curTouch._lastModified = 現在;
      curTouch._setPrevPoint(selTouch._prevPoint);
      locTouchIntDict[タッチID] = 未使用インデックス;
      // 処理する必要があるタッチポイントのリストに追加します。handleTouches.push(curTouch);
    }
  }
  // 新しい接触があった場合は、タッチイベントを生成し、それをeventManagerに配布します
  ハンドルタッチの長さが0より大きい場合
    // このメソッドは、スケールに応じてタッチ ポイントの位置を処理します。this._glView._convertTouchesWithScale(handleTouches);
    touchEvent を new cc.Event.EventTouch(handleTouches); に設定します。
    touchEvent._eventCode = cc.Event.EventTouch.BEGAN;
    イベントマネージャ。イベントをディスパッチします。
  }
},

この関数では、コードの一部は新しいタッチポイントがあるかどうかをフィルタリングするために使用され、他の部分はイベントを処理および配布するために使用されます (必要な場合)。
この時点で、イベントはブラウザからエンジンへの変換を完了し、eventManager に到達しています。では、エンジンとノードの間で何が起こるのでしょうか?

イベントはどのようにしてエンジンからノードに届くのでしょうか?

イベントをノードに渡す作業は、主に CCEventManager クラスで行われます。ストレージ イベント リスナー、配布イベントなどが含まれます。エントリ ポイントとして _dispatchTouchEvent から始めましょう。

CCイベントマネージャ.js

// イベントのディスパッチ_dispatchTouchEvent: function (event) {
  // タッチ リスナーを並べ替えます // TOUCH_ONE_BY_ONE: タッチ イベント リスナー タイプ。タッチ ポイントは 1 つずつディスパッチされます // TOUCH_ALL_AT_ONCE: タッチ ポイントは一度にすべてディスパッチされます this._sortEventListeners(ListenerID.TOUCH_ONE_BY_ONE);
  this._sortEventListeners(リスナーID.TOUCH_ALL_AT_ONCE);

  // リスナーリストを取得します。 var oneByOneListeners = this._getListeners(ListenerID.TOUCH_ONE_BY_ONE);
  var allAtOnceListeners = this._getListeners(ListenerID.TOUCH_ALL_AT_ONCE);

  //_ タッチリスナーが存在しない場合は、直接戻ります。
  // リスナーがいない場合は、そのまま戻ります。
  if (null === oneByOneListeners && null === allAtOnceListeners)
    戻る;

  // 変数を保存します var originalTouches = event.getTouches(), mutableTouches = cc.js.array.copy(originalTouches);
  var oneByOneArgsObj = {event: event、needsMutableSet: (oneByOneListeners && allAtOnceListeners)、touches: mutableTouches、selTouch: null};

  //
  //_ 最初にターゲットハンドラを処理する
  // 反転しません。まず、シングルタッチイベントを処理する感じです。
  if (oneByOneListeners) {
    // コンタクトを走査し、順番に分配します for (var i = 0; i < originalTouches.length; i++) {
      イベントの現在のタッチ = 元のタッチ[i];
      イベント._propagationStopped = イベント._propagationImmediateStopped = false;
      this._dispatchEventToListeners(oneByOneListeners、this._onTouchEventCallback、oneByOneArgsObj);
    }
  }

  //
  //_ 標準ハンドラを2番目に処理する
  // 反転しません。 2番目はマルチタッチイベントを処理する(一度にディスパッチする)ことのようです
  if (allAtOnceListeners && mutableTouches.length > 0) {
    this._dispatchEventToListeners(allAtOnceListeners、this._onTouchesEventCallback、{イベント: event、タッチ: mutableTouches});
    イベントが停止した場合
      戻る;
  }
  // タッチ リスナー リストを更新します (主にリスナーの削除と追加) this._updateTouchListeners(event);
},

この関数で行われる主な処理は、ソート、登録されたリスナー リストへの配信、リスナー リストの更新です。特に何もないです。なぜ突然このような命令が下されるのかと不思議に思うかもしれません。ああ、これが一番大事なことだよ!ソートの役割については、タッチイベントの送信に関する公式ドキュメントを参照してください。このソートにより、異なるレベル/異なる zIndex のノード間の接触ポイントの帰属問題が実現されます。ソートについては後述しますが、すごいです。
イベントのディスパッチは、_dispatchEventToListeners 関数を呼び出すことによって実現されます。その内部実装を見てみましょう。

/**
* リスナーリストにイベントを配布します * @param {*} listeners リスナーリスト * @param {*} onEvent イベントコールバック * @param {*} eventOrArgs イベント/パラメータ */
_dispatchEventToListeners: 関数 (リスナー、onEvent、eventOrArgs) {
  // 配布を停止する必要がありますか? var shouldStopPropagation = false;
  // 固定優先度のリスナー(システムイベント)を取得します
  var fixedPriorityListeners = listeners.getFixedPriorityListeners();
  // シーングラフの優先度を持つリスナーを取得します (通常、追加するリスナーはここにあります)
  var sceneGraphPriorityListeners = listeners.getSceneGraphPriorityListeners();

  /**
  * リスナーのトリガー順序:
  * 固定優先度レベル < 0
  * シーングラフの優先度 * 固定優先度 > 0
  */
  var i = 0, j, selListener;
  if (fixedPriorityListeners) { //_ 優先度 < 0
    固定優先度リスナーの長さが 0 の場合
      // リスナーをトラバースしてイベントを配布します for (; i < listeners.gt0Index; ++i) {
        selListener = 固定優先度リスナー[i];
        // リスナーがアクティブ化され、一時停止されておらず、イベント マネージャーに登録されている場合 // 最後の onEvent は、_onTouchEventCallback 関数を使用してリスナーにイベントを配布します // onEvent は、後続のリスナーにイベントを配布し続ける必要があるかどうかを示すブール値を返します。true の場合、配布を停止します if (selListener.isEnabled() && !selListener._isPaused() && selListener._isRegistered() && onEvent(selListener, eventOrArgs)) {
          伝播を停止する必要があります = true;
          壊す;
        }
      }
    }
  }
  //他の2つの優先度のトリガーコードを省略します},

関数内では、リスナーリストをトラバースしてイベントを 1 つずつ配信し、onEvent の戻り値に基づいて配信を継続するかどうかを決定します。通常、タッチ イベントはノードによって受信されると、ディスパッチされなくなります。次に、このノードからバブリング配布のロジックが実行されます。これも重要なポイントで、タッチ イベントに応答するのは 1 つのノードだけです。ノードの優先順位については、前述のソート アルゴリズムに従います。
ここでの onEvent は実際には _onTouchEventCallback 関数です。見てみましょう。

// タッチイベントのコールバック。イベントを listeners_onTouchEventCallback に配布します: function (listener, argsObj) {
  //_ リスナーが削除された場合はスキップします。
  // リスナーが削除されている場合はスキップします。
  if (!listener._isRegistered())
    false を返します。

  var event = argsObj.event、selTouch = event.currentTouch;
  イベント.currentTarget = リスナー._node;

  // isClaimed: イベントを請求するかどうかのリスナー var isClaimed = false, removedIdx;
  var getCode = event.getEventCode()、EventTouch = cc.Event.EventTouch;
  // イベントがタッチ開始イベントの場合 if (getCode === EventTouch.BEGAN) {
    // マルチタッチがサポートされておらず、タッチポイントがすでに存在する場合 if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch) {
      // タッチ ポイントがノードによって要求され、そのノードがノード ツリー内でアクティブな場合、イベントは処理されません。let node = eventManager._currentTouchListener._node;
      if (ノード && node.activeInHierarchy) {
        false を返します。
      }
    }

    // リスナーに対応するイベントがある場合 if (listener.onTouchBegan) {
      // リスナーへの配信を試み、リスナーがイベントを要求したかどうかを示すブール値を返します isClaimed = listener.onTouchBegan(selTouch, event);
      // イベントが要求され、リスナーが登録されている場合、いくつかのデータを保存します if (isClaimed && listener._registered) {
        listener._claimedTouches.push(selTouch);
        eventManager._currentTouchListener = リスナー;
        イベントマネージャの_currentTouch = selTouch;
      }
    }
  } 
  // リスナーがすでにタッチを要求しており、現在のタッチが現在のリスナーによって要求されている場合、else if (listener._claimedTouches.length > 0
           && ((removedIdx = listener._claimedTouches.indexOf(selTouch)) !== -1)) {
    // 直接持ち帰ります isClaimed = true;

    // マルチタッチがサポートされておらず、タッチポイントがあり、それが現在のタッチポイントでない場合は、イベントを処理しません。if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch && eventManager._currentTouch !== selTouch) {
      false を返します。
    }

    // イベントをリスナーに配布します // ENDED または CANCELED の場合、リスナーとイベント マネージャーの連絡先をクリーンアップする必要があります if (getCode === EventTouch.MOVED && listener.onTouchMoved) {
      リスナー.onTouchMoved(selTouch、イベント);
    } そうでない場合 (getCode === EventTouch.ENDED) {
      (リスナーがタッチ終了した場合)
        リスナーの onTouchEnded(selTouch、イベント);
      if (リスナー._登録済み)
        リスナー._claimedTouches.splice(削除されたIdx、1);
      イベントマネージャ。
    } そうでない場合 (getCode === EventTouch.CANCELED) {
      (リスナー.onTouchCancelled) の場合
        リスナーの onTouchCancelled(selTouch、イベント);
      if (リスナー._登録済み)
        リスナー._claimedTouches.splice(削除されたIdx、1);
      イベントマネージャ。
    }
  }

  //_ イベントが停止された場合は直接戻ります。
  // イベントが停止された場合は直接戻ります (イベントで stopPropagationImmediate() を呼び出すなど)
  イベントが停止した場合
    イベントマネージャでイベントを更新します。
    true を返します。
  }

  // イベントが要求され、リスナーがイベント (x) を消費する場合 (渡す必要はありません。デフォルトは false ですが、Node のタッチ シリーズ イベントでは true になります)
  if (isClaimed && listener.swallowTouches) {
    (argsObj.needsMutableSet) の場合
      argsObj.touches.splice(selTouch, 1);
    true を返します。
  }
  false を返します。
},

主な機能は、複数のタッチポイントに対してイベントを配信し、互換性処理を行うことです。重要なのは戻り値です。リスナーによってイベントが要求されると、イベントが渡されないように true が返されます。
イベントを配信する場合、タッチ開始イベントを例にとると、リスナーの onTouchBegan メソッドが呼び出されます。それはおかしいですね、ノードに配布されるのではないですか?なぜリスナーに電話するのですか?モニターとは何ですか?これには、ノードにイベントを登録するために on 関数を呼び出すときに、イベントがどこに登録されるかを調べる必要があります。

イベントはどこに登録されていますか?

ノード上で呼び出される on 関数の場合、関連するコードは当然 CCNode にあります。 on 関数が何をするのか見てみましょう。

/**
* 指定されたタイプのコールバック関数をノードに登録します * @param {*} type イベントタイプ * @param {*} callback コールバック関数 * @param {*} target ターゲット(これをバインドするために使用)
* @param {*} キャプチャフェーズで登録された useCapture */
on (タイプ、コールバック、ターゲット、useCapture) {
  // システムイベント(マウス、タッチ)ですか?
  forDispatch = this._checknSetupSysEvent(type); とします。
  if (forDispatch) {
    //イベントを登録します。 return this._onDispatch(type, callback, target, useCapture);
  }
  // 位置の変更、サイズの変更など、システム以外のイベントを省略します。
},

公式コメントはかなり長いので、簡略版を書きます。つまり、イベントのコールバック関数を登録するために使用されます。
コンテンツが少ないのでは?と思うかもしれません。 ? ?ただし、ここでは 2 つのブランチがあり、1 つは _checknSetupSysEvent 関数を呼び出し、もう 1 つは _onDispatch 関数を呼び出しており、コードはすべて 555 です。
登録に関連するものは _onDispatch 関数です。もう 1 つについては後で説明します。

// 配布を登録する event_onDispatch (type, callback, target, useCapture) {
  //_ (type、callback、useCapture) のようなパラメータも受け入れます
  // 次のようなパラメータも受け取ることができます: (type, callback, useCapture)
  // パラメータ互換性処理 if (typeof target === 'boolean') {
    useCapture = ターゲット;
    ターゲット = 未定義;
  }
  それ以外の場合は、useCapture = !!useCapture;
  // コールバック関数がない場合は、エラーを報告して戻ります。
  if (!コールバック) {
    cc.エラーID(6800);
    戻る;
  }

  // useCapture に基づいて異なるリスナーを取得します。
  var リスナー = null;
  キャプチャを使用する場合
    リスナー = this._capturingListeners = this._capturingListeners || 新しい EventTarget();
  }
  それ以外 {
    リスナー = this._bubblingListeners = this._bubblingListeners || 新しい EventTarget();
  }

  // 同じコールバックイベントが登録されている場合、処理は実行されません if ( !listeners.hasEventListener(type, callback, target) ) {
    // イベントをリスナーに登録します listeners.on(type, callback, target);

    // これをターゲットの __eventTargets 配列に保存します。これは、ターゲットから targetOff 関数を呼び出してリスナーをクリアするために使用されます。
    if (ターゲット && target.__eventTargets) {
      ターゲット.__eventTargets.push(これを);
    }
  }

  コールバックを返します。
},

ノードには 2 つのリスナーが保持されます。1 つは _capturingListeners、もう 1 つは _bubblingListeners です。違いは何でしょうか?前者はキャプチャフェーズで登録され、後者はバブリングフェーズで登録されます。より具体的な違いについては後で説明します。
listeners.on(type, callback, target);から、イベントが実際にはノードではなく、これら 2 つのリスナーに登録されていることがわかります。
それでは中身を見てみましょう。

イベントターゲット

//_イベント ターゲットの特定のイベント タイプのコールバックを登録します。このタイプのイベントは `emit` を使用して発行する必要があります。
proto.on = 関数 (型、コールバック、ターゲット、1回) {
    // コールバック関数が渡されない場合はエラーを報告して戻ります
    if (!コールバック) {
        cc.エラーID(6800);
        戻る;
    }

    // コールバックがすでに存在する場合は処理しない if ( !this.hasEventListener(type, callback, target) ) {
        //イベントを登録します this.__on(type, callback, target, once);

        if (ターゲット && target.__eventTargets) {
            ターゲット.__eventTargets.push(これを);
        }
    }
    コールバックを返します。
};

最後に、もう 1 つあります... js.extend(EventTarget, CallbacksInvoker);から、EventTarget が CallbacksInvoker を継承していることがわかります。もう 1 つのレイヤーを掘り下げてみましょう。

callbacks-invoker.js(コールバックインボーカー)

//_ イベント追加管理 proto.on = function (key, callback, target, once) {
    // イベントに対応するコールバックリストを取得します。let list = this._callbackTable[key];
    // 存在しない場合はプールから取得します if (!list) {
        リスト = this._callbackTable[キー] = callbackListPool.get();
    }
    // コールバック関連の情報を保存します。let info = callbackInfoPool.get();
    info.set(コールバック、ターゲット、1回);
    リスト.callbackInfos.push(情報);
};

ついに終わりました!このうち、callbackListPool と callbackInfoPool はどちらも js.Pool オブジェクトであり、オブジェクト プールです。コールバック関数は最終的に _callbackTable に保存されます。
ストレージの場所がわかったので、イベントはどのようにトリガーされるのでしょうか?

イベントはどのようにトリガーされますか?

トリガーを理解する前に、トリガーの順序を見てみましょう。まずは公式コメントを見てみましょう。

マウスまたはタッチ イベントは、システムが dispatchEvent メソッドを呼び出すことによってトリガーされます。トリガー プロセスは次の 3 つの段階で構成されます。
* 1. キャプチャフェーズ: キャプチャターゲット ( _getCapturingTargetsを通じて取得) にイベントをディスパッチします。たとえば、キャプチャフェーズの親ノードがノードツリーに登録され、ルートノードからターゲットノードにディスパッチされます。
* 2. ターゲットフェーズ: ターゲットノードのリスナーにディスパッチします。
* 3. バブリングステージ: バブリングターゲット( _getBubblingTargetsを通じて取得)にイベントをディスパッチします。たとえば、バブリングステージの親ノードがノードツリーに登録され、ターゲットノードからルートノードにディスパッチされます。

それはどういう意味ですか? on 関数の 4 番目のパラメータ useCapture が true の場合、イベントはキャプチャ フェーズに登録され、最も早く呼び出すことができます。
キャプチャ フェーズは、親ノードから子ノードの順に (ルート ノードから開始して) トリガーされることに注意することが重要です。ノード自体によって登録されたイベントがトリガーされます。最後に、バブリング フェーズに入り、親ノードからルート ノードにイベントを渡します。
簡単に理解すると、キャプチャ フェーズは上から下へ、次にキャプチャ フェーズ自体、そして最後にバブリング フェーズは下から上へです。
理論は少し難しいかもしれませんが、コードを見れば理解できると思います。
_checknSetupSysEvent 関数を覚えていますか? 前のコメントでは、システム イベントであるかどうかのみをチェックしていますが、実際にはそれ以上のことを行います。

// システムイベントかどうかをチェックする_checknSetupSysEvent (type) {
  // 新しいリスナーを追加する必要がありますか? let newAdded = false;
  // 配布が必要かどうか(システムイベントに必要)
  forDispatch = false とします。
  // イベントがタッチイベントの場合 if (_touchEvents.indexOf(type) !== -1) {
    // タッチイベントリスナーがない場合は、新しいリスナーを作成します。if (!this._touchListener) {
      this._touchListener = cc.EventListener.create({
        イベント: cc.EventListener.TOUCH_ONE_BY_ONE、
        ツバメタッチ: true、
        オーナー: これ、
        マスク: _searchComponentsInParent(this, cc.Mask),
        onTouchBegan: _touchStartHandler、
        タッチ移動時: _touchMoveHandler、
        onTouchEnded: _touchEndHandler、
        タッチキャンセル時: _touchCancelHandler
      });
      // イベントマネージャにリスナーを追加する
      イベントマネージャにリスナーを追加します(this._touchListener、this);
      新しく追加されました = true;
    }
    ディスパッチ = true;
  }
  // 省略されたイベントはマウス イベントのコードで、タッチ イベントに似ています // リスナーが追加され、現在のノードがアクティブでない場合 if (newAdded && !this._activeInHierarchy) {
    // しばらく経ってもノードがまだアクティブでない場合は、ノードのイベント配信を一時停止します。
    cc.director.getScheduler().schedule(関数() {
      if (!this._activeInHierarchy) {
        イベントマネージャを一時停止します。
      }
    }, this, 0, 0, 0, false);
  }
  ディスパッチを返します。
},

何がポイントですか? eventManager.addListener(this._touchListener, this);の行で、ご覧のとおり、各ノードは _touchListener を保持し、それを eventManager に追加します。見覚えがありますか?ねえ、これは先ほどイベントを配信するときに eventManager が行ったことではないですか?これは接続されていませんか? eventManager はノードを保持していませんが、これらのリスナーを保持しています。
新しいリスナーを作成するときに、多くのパラメータが渡され、使い慣れたタッチ開始イベントonTouchBegan: _touchStartHandlerが引き続き使用されますが、これは何ですか?

// タッチ開始イベントハンドラー var _touchStartHandler = function (touch, event) {
    var pos = touch.getLocation();
    var ノード = this.owner;

    // タッチ ポイントがノード範囲内にある場合、イベントがトリガーされ、イベントを取得したことを示す true が返されます。
    (node._hitTest(pos、this))の場合{
        イベントタイプ = EventType.TOUCH_START;
        イベントをタッチします。
        イベントのバブルは true です。
        //このノードに配布しますnode.dispatchEvent(event);
        true を返します。
    }
    false を返します。
};

とても簡単です。接触点を取得し、接触点がノード内に収まるかどうかを判断します。収まる場合は、配布します。

//_ イベントをイベント ストリームに配信します。
ディスパッチイベント(イベント){
  _doDispatchEvent(これ、イベント);
  _cachedArray.length = 0;
},
//イベントを配布する function _doDispatchEvent (owner, event) {
    var ターゲット、i;
    イベントターゲット = 所有者;

    //_ イベント.CAPTURING_PHASE
    // キャプチャ phase_cachedArray.length = 0;
    // キャプチャフェーズでノードを取得し、_cachedArray に保存します
    所有者._getCapturingTargets(イベントタイプ、_cachedArray);
    //_ キャプチャ
    イベント.イベントフェーズ = 1;
    // 最後から最初まで(つまり、ルートノードからターゲットノードの親ノードまで)トラバースします
    (i = _cachedArray.length - 1; i >= 0; --i) {
        ターゲット = _cachedArray[i];
        // ターゲットノードがキャプチャフェーズのリスナーを登録する場合 if (target._capturingListeners) {
            イベント.currentTarget = ターゲット;
            //_ 火災イベント
            // ターゲット ノードでイベントを処理します target._capturingListeners.emit(event.type, event, _cachedArray);
            //_ 伝播が停止したかどうかをチェックする
            // イベントの配信が停止した場合は、
            if (event._propagationStopped) {
                _cachedArray.length = 0;
                戻る;
            }
        }
    }
    // Clear_cachedArray
    _cachedArray.length = 0;

    //_ イベント.AT_TARGET
    //_ キャプチャコールバックで破棄されたかどうかをチェックします
    // ターゲットノード自身のフェーズ event.eventPhase = 2;
    イベント.currentTarget = 所有者;
    // オーナーがキャプチャフェーズのリスナーを登録している場合は、イベントを処理します。if (owner._capturingListeners) {
        所有者._capturingListeners.emit(イベント.type、イベント);
    }
    // イベントが停止されておらず、伝播リスナーが登録されている場合は、イベントを処理します if (!event._propagationImmediateStopped && owner._bubblingListeners) {
        owner._bubblingListeners.emit(イベントタイプ、イベント);
    }

    // イベントが停止されておらず、イベントをバブルする必要がある場合(デフォルトは true)
    if (!event._propagationStopped && event.bubbles) {
        //_ イベント.BUBBLING_PHASE
        // バブリング ステージ // バブリング ステージ内のノードを取得します。owner._getBubblingTargets(event.type, _cachedArray);
        //_ 伝播する
        イベントフェーズ = 3;
        // 最初から最後まで(親ノードからルートノードまで)トラバースし、トリガーロジックはキャプチャフェーズと一致します for (i = 0; i < _cachedArray.length; ++i) {
            ターゲット = _cachedArray[i];
            ターゲット_bubblingListenersの場合{
                イベント.currentTarget = ターゲット;
                //_ 火災イベント
                target._bubblingListeners.emit(イベントタイプ、イベント);
                //_ 伝播が停止したかどうかをチェックする
                if (event._propagationStopped) {
                    _cachedArray.length = 0;
                    戻る;
                }
            }
        }
    }
    // Clear_cachedArray
    _cachedArray.length = 0;
}

これを読んで、イベントのトリガーシーケンスについて理解が深まったでしょうか?
キャプチャ フェーズのノードとバブリング フェーズのノードは、他の関数を通じて取得されます。キャプチャ フェーズのコードは例として使用されます。この 2 つは似ています。

_getCapturingTargets (型、配列) {
  // 親ノードから開始します var parent = this.parent;
  // 親ノードが空でない場合(ルートノードの親ノードが空の場合)
  (親){
    // ノードにキャプチャフェーズのリスナーと対応するタイプのリスニングイベントがある場合は、ノードを配列に追加します。if (parent._capturingListeners && parent._capturingListeners.hasEventListener(type)) {
      配列をプッシュします(親);
    }
    // ノードを親ノードに設定します。parent = parent.parent;
  }
},

ボトムアップのトラバーサルでは、途中で条件を満たすノードが配列に追加され、処理する必要があるすべてのノードが取得されます。
少し話がそれますが、先ほどのイベント配信に戻ると、同様に、キャプチャ フェーズのリスナーとバブリング フェーズのリスナーはどちらも EventTarget なので、ここではトリガー自体を例として取り上げます。
owner._bubblingListeners.emit(event.type, event);
上記のコード行は、イベントを独自のノードのバブリング リスナーに配布します。では、emit の内容を見てみましょう。
emitting は実際には CallbacksInvoker のメソッドです。

コールバック呼び出し元.js

proto.emit = 関数 (キー、引数1、引数2、引数3、引数4、引数5) {
    // イベントリストを取得します。const list = this._callbackTable[key];
    // イベントリストが存在する場合 if (list) {
        // list.isInvoking イベントがトリガーされているかどうか const rootInvoker = !list.isInvoking;
        リストを呼び出し中 = true;

        // コールバック リストを取得してトラバースします。const infos = list.callbackInfos;
        (i = 0、len = infos.length、i < len、++i とします) {
            定数info = infos[i];
            if (情報) {
                ターゲットを info.target とします。
                コールバックを info.callback とします。
                // コールバック関数がonceに登録されている場合は、まずこの関数をキャンセルします。if (info.once) {
                    this.off(キー、コールバック、ターゲット);
                }

                // ターゲットが渡された場合は、call を使用して、これが正しいターゲットを指していることを確認します。if (target) {
                    callback.call(ターゲット、arg1、arg2、arg3、arg4、arg5);
                }
                それ以外 {
                    コールバック(arg1、arg2、arg3、arg4、arg5);
                }
            }
        }
        // 現在のイベントがトリガーされていない場合 if (rootInvoker) {
            リスト.isInvoking = false;
            // キャンセルされたコールバックがある場合は、purgeCanceled関数を呼び出して削除されたコールバックをフィルタリングし、配列を圧縮します。if (list.containCanceled) {
                リストを消去キャンセルしました();
            }
        }
    }
};

中心となるのは、イベントに基づいてコールバック関数のリストを取得し、呼び出しを走査し、最後に必要に応じてリサイクルを実行することです。それだけです!

結論

興味深いリスナーソートアルゴリズムを追加する

前回のコンテンツでは、トリガーの優先順位に従ってリスナーをソートするために使用される _sortEventListeners 関数について説明しました。このアルゴリズムは非常に興味深いと思うので、皆さんと共有したいと思います。
まず理論から。名前が示すように、ノード ツリーは間違いなくツリー構造です。ツリーから 2 つのノード A と B をランダムに選択する場合、次のような特殊なケースがあります。

  1. AとBは同じ親ノードに属している
  2. AとBは同じ親ノードに属していない
  3. AはBの親ノードである(逆も同様)

優先順位をつけたい場合、どのようにすればよいでしょうか? p1 と p2 をそれぞれ AB と等しくします。上へ進む: A = A.parent

  1. 最も単純なのは_localZOrderを直接比較することです
  2. A と B は、その起源を上向きにたどると、遅かれ早かれ共通の親ノードを持つことになります。この時点で、_localZOrder を比較すると、少し不公平になる可能性があります。長い距離を移動した (レベルが高い) ノードがあり、最初にトリガーされる必要がある可能性があるためです。この時点で、別の状況が存在します。A と B は同じレベルにあります。次に、p1 と p2 は上へ移動して同じ親ノードに到達し、_localZOrder を比較します。A のレベルは B のレベルよりも高くなります。 p がルート ノードに到達したら、p を別の開始点に交換します。たとえば、p2 が最初にルート ノードに到達します。このとき、p2 を位置 A に置いて続行します。遅かれ早かれ、それらは同じ距離を移動することになり、その時点で親ノードは同じになります。 p1 p2 の _localZOrder に従ってソートし、その逆を取るだけです。大きい方が反対側に交換されているからです。この段落は見直す必要があります。素晴らしいです。
  3. 同じことがソースに戻りますが、違いは、親子関係のため、交換して同じ距離を移動した後、p1 と p2 が最終的にノード A または B で出会うことです。ですから、この時は、A か B かを判断するだけでいいのです。A であれば、A のレベルは低く、逆もまた同様です。したがって、出会うノードは優先度が低くなります。

書かれた言葉がたくさんあり、ここに簡潔で強力なコードがあります!

//シーングラフのアルゴリズムのソート優先リスナー// -1(負の数)を返すことはL1がL2より優先されることを意味し、正の数を返すことは逆を意味します。
  //リスナーが配置されているノードを取得しますnode1 = l1._getscenegrappriority()、
      node2 = l2._getscenegraphpriority();

  //リスナー2が空の場合、またはノード2が空またはノード2がアクティブでない場合、またはノード2がルートノードです。L1は優先されます
    -1 を返します。
  //上記と同じelse(!l1 ||!node1 ||!node1._activeinhierarchy || node1._parent === null)
    1 を返します。

  // P1 P2を使用してノード1とノード2を一時的に保存します
  //例:私はそれが交換が発生するかどうかを意味すると思います(交換)
  p1 = node1、p2 = node2、ex = falseとします。
  // p1とp2の親ノードが等しくない場合、while(p1._parent._id!== p2._parent._id){p1._parent._Id!==
    // P1の祖父母ノードが空(P1の親ノードがルートノード)の場合、EXはTRUEに設定され、P1ポイントはノード2に設定されます。それ以外の場合、P1は親ノードP1 = P1._Parent._Parent === nullを指します。
    p2 = p2._parent._parent === null?&& node1:p2._parent;
  }

  // P1とP2が同じノードを指している場合、つまり、ノード1と2には親子関係があります。つまり、ケース3
  if(p1._id === p2._id){
    // P1がノード2を指している場合、L1が優先されます。それ以外の場合は、L2が優先されます(p1._id === node2._id) 
      -1 を返します。
    if(p1._id === node1._id)
      1 を返します。
  }

  //注:この時点で、P1とP2の親ノードは同じです// EXが真である場合、ノード1と2には親子関係、つまりケース2にはありません。
  // exがfalseの場合、ノード1と2には同じ親ノードがあります。これはケース1です
  exを返しますか?
},

要約する

ゲームはCCGAMEから始まり、CCINPUTMANAGERとCCEVENTMANAGERを呼び出してイベントを登録します。その後のインタラクションでは、エンジンのコールバックがCCEVentManagerのリスナーに呼び出し、CCNodeがイベントを処理します。ヒットすると、EventTargetに保存されているイベントリストに渡され、旅が完了します。
モジュールは実際にはそれほど複雑ではありませんが、いくつかのファイルに加えて、さまざまな互換性とセキュリティ処理が含まれているため、多くのようです。

上記は、CocosCreatorシステムイベントの詳細については、CocosCreatorシステムイベントの詳細については、123WordPress.comの他の関連記事に注意してください。

以下もご興味があるかもしれません:
  • CocosCreator 入門チュートリアル: ネットワーク通信
  • Cocos2d-x 3.x 入門チュートリアル (パート 2): ノード クラス
  • Cocos2d-x 3.x 入門チュートリアル (I): 基本概念
  • Cocos2d-x 入門チュートリアル (詳細な例と説明)
  • CocosCreatorでシューティングゲームを作る詳しい解説
  • CocosCreatorでクールなレーダーチャートを描く方法
  • CocosCreator MVCアーキテクチャの詳細な説明
  • CocosCreatorメッセージ配信メカニズムの詳細な説明
  • CocosCreatorでWeChatゲームを作成する方法
  • CocosCreatorでゲームコントローラーを使用する方法
  • CocosCreator Huarongdaoデジタルパズルの詳しい説明
  • CocosCreator 入門チュートリアル: TS で初めてのゲームを作る

<<:  ffmpeg コマンドラインを使用してビデオを変換するためのサンプルコード

>>:  CentOS7 で MySQL データベースにリモート接続できない理由と解決策

推薦する

JavaScriptの動作メカニズムの詳細な説明とイベントループについての簡単な説明

目次1. JavaScript がシングルスレッドなのはなぜですか? 2. タスクキュー3. イベン...

ubuntu15.10 での hadoop2.7.2 の詳細なインストールと設定

Linux での Hadoop インストール チュートリアルはインターネットや書籍に多数ありますが、...

Dockerでk8sをデプロイする方法

K8s k8s はクラスターです。クラスターには複数の名前空間があります。名前空間の下には複数のポッ...

Vue モバイル プロジェクトでページ キャッシュを実装する方法のサンプル コード

背景モバイル デバイスでは、ページ ジャンプ間のキャッシュが必須要件です。例: ホームページ =&g...

XHTML 入門チュートリアル: テキストの書式設定と特殊文字

<br />このセクションでは、XHTML でテキストの書式設定と特殊文字を実装する方法...

Linux Centos8 CA証明書作成チュートリアル

必要なファイルをインストールする Yum インストール openssl-* -yデータベースインデッ...

1 つの記事で Vue ミドルウェア パイプラインを学ぶ

SPA を構築する場合、多くの場合、特定のルートを保護する必要があります。たとえば、認証されたユーザ...

ラムダ式の原則と例

ラムダ式ラムダ式 (クロージャとも呼ばれる) は、Java 8 のリリースを推進した最も重要な新機能...

Linux のよく使うコマンドの使い方を詳しく解説(第 2 回)———— テキストエディタのコマンド vi/vim

vi/vim の紹介どちらもマルチモード エディターです。違いは、vim が vi のアップグレー...

MySQLのROUND関数の丸め演算における落とし穴の分析

この記事では、MySQL の ROUND 関数を使用した丸め操作の落とし穴を例を使って説明します。ご...

MySQLの半同期の詳細な説明

目次序文MySQL マスタースレーブレプリケーションMySQL でサポートされているレプリケーション...

Linuxカーネルマクロcontainer_ofの詳細な分析

1. 前述の通り数年前、Linux ドライバーのコードを読んでいたときにこのマクロを見ました。長い間...

W3C チュートリアル (8): W3C XML スキーマのアクティビティ

XML スキーマは、DTD に代わる XML ベースのものです。 XML スキーマは、DTD に代わ...

Linux ssh サービス情報と実行ステータスを表示する方法

Linux での ssh サービス構成など、ssh サーバー構成に関する記事は多数あります。ここでは...