入力
エレメントは、アプリケーションのElementInputを通じて、マウス、タッチ、XRコントローラーに反応します。ElementInputはブラウザの入力イベントをリッスンし、ポインターの下にあるエレメントを特定して、そのエレメントのコンポーネントでイベントを発生させます。このページでは、インタラクティブなすべてのエレメントに共通するイベントを扱います。ボタンはこれらのイベントにホバーと押下の状態を加え、スクロールビューはこれらのイベントを使ってドラッグを行います。
UI入力の有効化
エレメントが入力を受け取るには、2つの条件を満たす必要があります。アプリケーションがElementInputを持っていること(セットアップを参照)と、エレメントで入力が有効になっていることです。入力が有効でないエレメントがヒットすることはないので、ユーザーが操作するエレメントで入力を有効にし、装飾やボタンのラベルなど、それ以外のエレメントではオフのままにしておきます。そうすれば、ラベルのクリックはその下にあるボタンに届きます。
- Engine
- Editor
- React
- Web Components
card.element.useInput = true;
addComponent('element', ...)にuseInput: trueを渡すこともできます。ElementInputのないアプリケーションでエレメントの入力を有効にすると、エンジンのデバッグビルドは警告をログに出力します。
ElementコンポーネントのUse Inputにチェックを入れます。
<Element type="image" useInput />
<pc-element type="image" use-input></pc-element>
作成される入力デバイスの組み合わせは環境ごとに異なり、UIとゲームの入力が交わる場面では、この違いが重要になります。
| 環境 | 入力デバイス | エレメント入力 |
|---|---|---|
| エンジン | AppOptionsに渡したもの | マウスとタッチのデバイスより前に、自分で作成します |
| エディター | INPUT設定で有効にしたマウス、タッチ、キーボード、ゲームパッドのデバイス | 常にあり、他のデバイスより前に作成されます |
| React | app.mouseとapp.touch。キーボードデバイスはありません | 常にあり、他のデバイスより前に作成されます |
| Web Components | app.mouseとapp.keyboard。タッチデバイスはありませんが、エレメント入力が自らタッチを処理します | 常にあり、他のデバイスより前に作成されます |
入力イベント
Elementコンポーネントでは、次のイベントが発生します。
| イベント | 発生するタイミング |
|---|---|
mouseenter | ポインターがエレメントの上に入ったとき |
mouseleave | ポインターがエレメントの上から出たとき |
mousemove | ポインターがエレメントの上で動いたとき。エレメント上でボタンが押された後は、ボタンが離されるまで、エレメントがすべての移動を受け取ります |
mousedown | エレメントの上でマウスボタンが押されたとき |
mouseup | エレメントの上でマウスボタンが離されたとき。または、エレメント上で押されたボタンがどこかで離されたとき |
mousewheel | エレメントの上でマウスホイールが回されたとき |
click | マウスボタンまたはタッチが、同じエレメントの上で押されて離されたとき |
touchstart | エレメント上でタッチが始まったとき |
touchmove | エレメント上で始まったタッチが移動したとき。移動先がどこでも発生します |
touchleave | エレメント上で始まったタッチがエレメントの外に出たとき。タッチごとに1回発生します |
touchend | エレメント上で始まったタッチが終わったとき。終わった場所がどこでも発生します |
touchcancel | エレメント上で始まったタッチがブラウザによってキャンセルされたとき |
selectstart, selectend, selectmove, selectenter, selectleave | XRのコントローラーや手がエレメントを指したり、セレクトしたりしたとき。XRのUIを参照してください |
イベントのリッスン
イベントはElementコンポーネントでリッスンします。そのためのコードを実行する場所は、環境ごとに異なります。
- Engine
- Editor
- React
- Web Components
card.element.on('mouseenter', () => {
card.element.opacity = 1;
});
card.element.on('mouseleave', () => {
card.element.opacity = 0.6;
});
onはEventHandleを返します。リッスンをやめるには、そのoff()メソッドを呼び出します。
エレメントのエンティティにScriptコンポーネントを追加し、initializeでリッスンするスクリプトをアタッチします。
import { Script } from 'playcanvas';
export class HoverOpacity extends Script {
static scriptName = 'hoverOpacity';
initialize() {
const element = this.entity.element;
const enter = element.on('mouseenter', () => {
element.opacity = 1;
});
const leave = element.on('mouseleave', () => {
element.opacity = 0.6;
});
// スクリプトがエンティティより先に破棄された場合は、リッスンをやめる
this.once('destroy', () => {
enter.off();
leave.off();
});
}
}
useElementEventは、配置先のエンティティのエレメントでイベントをリッスンし、ElementEventはそれをコンポーネントとしてラップします。リッスンを始める時点でエレメントが存在するように、<ElementEvent>は<Element>の後に配置してください。
import { useEffect, useRef, useState } from 'react';
import { Entity } from '@playcanvas/react';
import { Element } from '@playcanvas/react/components';
import { useParent } from '@playcanvas/react/hooks';
export function useElementEvent(name, handler) {
const entity = useParent();
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
const handle = entity.element?.on(name, event => handlerRef.current(event));
return () => handle?.off();
}, [entity, name]);
}
export function ElementEvent({ name, handler }) {
useElementEvent(name, handler);
return null;
}
function Card() {
const [hovered, setHovered] = useState(false);
return (
<Entity name="card">
<Element type="image" anchor={[0.5, 0.5, 0.5, 0.5]} pivot={[0.5, 0.5]}
width={200} height={120} useInput opacity={hovered ? 1 : 0.6} />
<ElementEvent name="mouseenter" handler={() => setHovered(true)} />
<ElementEvent name="mouseleave" handler={() => setHovered(false)} />
</Entity>
);
}
ハンドラーからエンジン側のコンポーネントを変更するのではなく、この例のように、stateでエレメントのpropsを制御してください。Reactは<Element>に渡したpropsをレンダリングのたびに適用し直すため、ハンドラーでの変更は元に戻ってしまいます。
<Entity>のonClickやonPointer*のpropsは別の仕組みで、レンダリングされたオブジェクトをGPUでピックするポインターイベントです。これらはuseInputがなくてもイメージエレメントやテキストエレメントで発生するので、クリックできるだけのシンプルな画像ならこれで十分です。ただし、UIの入力システムを経由しないため、ボタンの状態、ヒットパディング、スクロールビューのドラッグは使えません。
whenReadyでHTML要素の準備ができるのを待ってから、そのcomponentでリッスンします。
import { whenReady } from '@playcanvas/web-components';
const card = await whenReady('pc-entity[name="card"] > pc-element');
card.component.on('mouseenter', () => card.setAttribute('opacity', '1'));
card.component.on('mouseleave', () => card.setAttribute('opacity', '0.6'));
<pc-entity>のonclick属性やonpointer*属性は別の仕組みで、レンダリングされたオブジェクトをGPUでピックするポインターイベントです。これらはイメージエレメントやテキストエレメントでも発生しますが、UIの入力システムを経由しないため、ボタンの状態、ヒットパディング、スクロールビューのドラッグは使えません。
すべてのハンドラーはイベントオブジェクトを受け取ります。event.elementはイベントが発生したエレメントで、ハンドラーがその祖先のエレメントに登録されている場合も同じです。event.eventは、そのイベントの元になったブラウザのイベントです。
| イベントオブジェクト | 対象のイベント | プロパティ |
|---|---|---|
ElementMouseEvent | mouse*と、マウスによるclick | xとy(キャンバスの左上からのポインターの位置、CSSピクセル単位)、dxとdy(前回のイベントからの移動量)、button、wheelDelta(-1、0、1のいずれか)、ctrlKey、altKey、shiftKey、metaKey |
ElementTouchEvent | touch*と、タッチによるclick | このタッチのxとy、touch(ブラウザのタッチ)、touchesとchangedTouches(ブラウザのタッチイベントと同じもの) |
ElementSelectEvent | select*と、XRのセレクトによるclick | inputSource(コントローラーまたは手) |
3つとも、element、camera(エレメントがヒットしたときのカメラ)、eventも持っています。
イベントのバブリング
イベントはまずヒットしたエレメントで発生し、次にその親エレメント、というようにヒエラルキーを上へたどり、エレメントを持たないエンティティに達するまで続きます。祖先のエレメントは入力が有効かどうかに関係なくイベントを受け取るので、メニューに1つリスナーを登録するだけで、そのすべての項目のクリックを処理できます。
menu.element.on('click', (event) => {
console.log(`${event.element.entity.name} was clicked`);
});
イベントをそれより上に伝えないようにするには、event.stopPropagation()を呼び出します。
UIの入力がゲームに届かないようにする
例えばプレイヤーがクリックしたときに弾を撃つなど、マウスやタッチのデバイスを直接読み取るゲームコードは、UIの上で行われたクリックも検知してしまいます。これもstopPropagation()で対処できます。このメソッドはバブリングを止めるだけでなく、ブラウザのイベントそのものも止めるので、ElementInputのリスナーの後に実行されるはずだったブラウザのリスナーには、そのイベントが届きません。マウスとタッチのデバイスは同じブラウザのイベントをリッスンしているため、ElementInputがそれらより前に作成されていれば、これによってHUD上での押下がapp.mouseに届かなくなり、app.mouse.wasPressed()にも反映されません。
// HUD内の入力が有効なエレメントでの押下は、app.mouseにもapp.touchにも届かない
hud.element.on('mousedown', event => event.stopPropagation());
hud.element.on('touchstart', event => event.stopPropagation());
HUD自体のグループエレメントでは、入力をオフのままにしておきます。子のイベントはいずれにせよバブリングで届きますし、入力をオンにすると、その矩形全体でゲームへの押下が遮られてしまいます。
エディター、React、Web Componentsは、ElementInputを最初に作成します。エンジンのアプリケーションでは、セットアップで示しているように、マウスとタッチのデバイスより前に作成してください。
どのエレメントがイベントを受け取るか
エレメントが重なっているとき、イベントを受け取るのは1つだけです。ElementInputは次の順序でエレメントをテストし、最初にヒットした時点で止まります。
- カメラを上から順に。 カメラは最後に描画されるものから最初のものへと順に試されるので、上に描画されるUIが優先されます。エレメントは、そのエレメントのレイヤーのいずれかをレンダリングするカメラを通してのみテストされます。
- レイヤーを上から順に。 後に描画されるレイヤー上のエレメントが先に試されます。これが関係するのは、インターフェースで複数のレイヤーを使う場合だけです。
- スクリーン空間のエレメントを最初に、次にワールド空間のスクリーン上のエレメント、その次にスクリーンのないエレメントを試します。
- 上に描画されるエレメントを最初に。 それぞれのグループの中では、描画順の値が最も大きいエレメントが最初に試されます。これは通常、ヒエラルキーで最も下にあるエレメントです。描画順とパフォーマンスを参照してください。
スクリーン上のエレメントがヒットすると、ほかにヒットするものよりカメラから遠い場合でも、その時点で探索は終わります。ワールド空間のスクリーンが重なっている場合は、優先度の高いスクリーンが優先されます。距離で比較されるのはスクリーンのないエレメントだけで、最も近いものが優先されます。
テストされる領域はエレメントの矩形であり、画像の見えているピクセルやグリフの形ではありません。ボタンのヒットパディングはこの領域を広げ、マスクはこの領域をクリップします。無効化されたエンティティはスキップされます。
クリックとドラッグ
clickは、マウスボタンが離されるか、タッチが終わったときに、それが押されたのと同じエレメントの上であれば発生します。離す前にエレメントの外に出て戻ってきた場合も、クリックになります。
エレメント上でマウスボタンが押されると、ポインターがどこへ移動しても、そのエレメントがすべてのmousemoveとmouseupを受け取ります。タッチも同様で、touchmoveとtouchendはタッチが始まったエレメントに送られます。これにより、ポインターが外に出ても、スライダーやスクロールビューはドラッグを続けられます。
タッチスクリーン
タップの後、ブラウザはマウスしか扱わないページのために、エミュレートしたマウスイベントをページに送ります。ElementInputは、同じエレメントでのタッチによるクリックに続く、エミュレートされたclickを無視しますが、それ以外のエミュレートされたイベントは届きます。タップされたエレメントは、mouseenter、mousedown、mouseupも受け取ります。また、ダイアログを閉じるボタンのように、タップされたエレメントがそのタップで非表示になると、エミュレートされたクリックはその後ろにあったものに当たります。ブラウザにマウスイベントをエミュレートさせないようにするには、キャンバスのtouchendイベントをキャンセルします。これはどの環境でも使えます。
app.graphicsDevice.canvas.addEventListener('touchend', (event) => {
event.preventDefault();
});
また、ElementInputはキャンバス上のすべてのtouchmoveをキャンセルするので、キャンバス上で始まったタッチでページがスクロールすることはありません。キャンバスが長いページの一部である場合は、この点に注意してください。
ポインターロック
一人称視点のゲームのようにポインターがロックされている間、ElementInputはマウスの押下を無視します。メニューを表示する前にapp.mouse.disablePointerLock()でロックを解除し、メニューが閉じたら再びロックしてください。
UI入力の無効化
- 特定のエレメントが入力を受け取らないようにするには、
useInputをオフにします。無効化されたエンティティが入力を受け取ることはありません。 - ボタンを表示したまま反応しないようにするには、
activeプロパティをfalseに設定します。ボタンを参照してください。 - メニューがアニメーションで消えていく間など、すべてのUI入力を一時停止するには、
app.elementInput.enabledをfalseに設定します。このプロパティはまだAPIリファレンスに記載されていません。
関連情報
- ボタン - ホバーと押下の状態、ボタン独自のイベント
- XRのUI - コントローラーや手でエレメントを指す操作
- タッチスクリーンのジョイパッド操作 - タッチイベントから画面上のジョイスティックを作るチュートリアル
- Elementコンポーネント、
<pc-element>、ElementComponent -useInputを含む、エレメントのすべてのプロパティのリファレンス - ElementInput - エレメントに入力を届けるオブジェクトのAPIリファレンス