よく使うウィジェット
UIコンポーネントは、UIを組み立てるための部品です。このページでは、それらを組み合わせて、ほとんどのインターフェースに必要なウィジェットを作ります。各レシピでは、エディターまたはコードで構築するヒエラルキーと、それを動作させるコードを示します。
プログレスバーとヘルスバー
health bar イメージエレメント:背景、300 × 30
└── track グループエレメント:Stretchアンカー、マージン4、フィルが占められる範囲
└── fill イメージエレメント:Stretchアンカー、マージン0、色はバーの色
フィルの右アンカーは0から1までの値に追従するので、フィルはトラックのその割合を占め、トラックによってバーの内側に縁が残ります。
function setHealth(value) {
fill.element.anchor = new pc.Vec4(0, 0, value, 1);
}
setHealth(0.7);
この方法では、テクスチャを使ったフィルは押しつぶされます。代わりにテクスチャを切り取るには、フィルをトラックの左端にアンカーし、ピボットも左端に置いて、widthとrectを一緒に設定します(トラックの幅を使ったfill.element.width = value * 292と、fill.element.rect = new pc.Vec4(0, 0, value, 1))。詳しくは、プログレスバーとローディングサークルのチュートリアルを参照してください。
スライダー
ハンドルがトラックより小さいスクロールバーは、スライダーになります。ハンドルをドラッグするとvalueが0から1まで変化し、垂直のスクロールバーでは上端が0です。
volume イメージエレメント:トラック、300 × 20、水平方向のScrollbarコンポーネント付き
└── handle 入力を有効にしたイメージエレメント:トラックの高さに合わせてアンカー
volume.scrollbar.handleSize = 0.1;
volume.scrollbar.value = 0.8;
volume.scrollbar.on('set:value', (value) => {
app.systems.sound.volume = value;
});
トグルとラジオグループ
トグルは、見た目で状態を示すボタンです。
music toggle 入力を有効にしたイメージエレメント、Buttonコンポーネント付き
└── check イメージエレメント:チェックマーク、トグルがオンのときに表示
let musicOn = true;
musicToggle.button.on('click', () => {
musicOn = !musicOn;
musicToggle.findByName('check').enabled = musicOn;
});
ラジオグループは、こうしたボタンをまとめたグループエレメントで、一度にオンになるのはそのうち1つだけです。
const options = difficulty.children;
for (const option of options) {
option.button.on('click', () => {
for (const other of options) {
other.findByName('check').enabled = other === option;
}
});
}
モーダルダイアログ
モーダルダイアログは、インターフェースの他の部分を覆い、ダイアログが閉じられるまでその入力をブロックします。
screen
├── hud …
└── dialog グループエレメント:Stretchアンカー
├── backdrop イメージエレメント:Stretchアンカー、黒、不透明度0.6、入力を有効化
└── panel 入力を有効にしたイメージエレメント:テキストとボタンを含むダイアログ本体
ダイアログはヒエラルキーの最後にあるため、スクリーンの他の部分より上に描画され、最初に入力を受け取ります。背景幕はスクリーン全体を覆い、入力が有効になっているため、パネルから外れたクリックをすべて受け止め、その下にあるHUDには何も届きません。パネルの空いている部分へのクリックが背景幕に届かないように、パネルでも入力を有効にしています。ダイアログへのクリックがゲームにも届かないようにするには、クリックの伝播を止めます。UIの入力がゲームに届かないようにするを参照してください。
function openDialog() {
dialog.enabled = true;
}
function closeDialog() {
dialog.enabled = false;
}
// パネルの外側をクリックするとダイアログを閉じる
dialog.findByName('backdrop').element.on('click', closeDialog);
複数のスクリーンの上にダイアログを表示するには、より高い優先度を持つ専用のスクリーンにダイアログを配置します。
ツールチップ
ツールチップは、ポインターがエレメントに重なっている間に表示されるパネルです。
// ツールチップはアイコンの子で、アイコンの上にアンカーされている
icon.element.useInput = true;
icon.element.on('mouseenter', () => {
tooltip.enabled = true;
});
icon.element.on('mouseleave', () => {
tooltip.enabled = false;
});
子は、親の後に続く兄弟よりも先に描画されるため、それらに覆われることがあります。ツールチップをすべての上に描画するには、代わりにスクリーンの最後の子にして、表示するときにエレメントの位置へ移動します。タッチスクリーンにはホバーがないため、長押しでツールチップを表示するか、その情報を画面に表示しておきます。
動的リスト
リーダーボードやインベントリのように、項目がデータから作られるリストは、データから子を作成するレイアウトグループです。リストが領域を超えて大きくなる可能性がある場合は、スクロールビューの中に入れます。
- Engine
- Editor
- React
- Web Components
function showScores(scores) {
// 古い行を削除してから、スコアごとに行を1つ追加する
for (const row of [...list.children]) {
row.destroy();
}
for (const { name, score } of scores) {
const row = new pc.Entity(name);
row.addComponent('element', {
type: pc.ELEMENTTYPE_TEXT,
fontAsset: font.id,
text: `${name} ${score}`,
fontSize: 28
});
list.addChild(row);
}
}
showScores([{ name: 'Ada', score: 1250 }, { name: 'Grace', score: 990 }]);
ヒエラルキーで1つの行を作成し、その下にnameとscoreという名前の2つのテキストエレメントを置いて、テンプレートにします。次に、スクリプトから行を作成します。
import { Asset, Entity, Script } from 'playcanvas';
export class Leaderboard extends Script {
static scriptName = 'leaderboard';
/**
* 行のテンプレート。
*
* @attribute
* @type {Asset}
* @resource template
*/
rowTemplate;
/**
* 行を追加するレイアウトグループ。
*
* @attribute
* @type {Entity}
*/
list;
showScores(scores) {
for (const row of [...this.list.children]) {
row.destroy();
}
for (const { name, score } of scores) {
const row = this.rowTemplate.resource.instantiate();
row.findByName('name').element.text = name;
row.findByName('score').element.text = String(score);
this.list.addChild(row);
}
}
}
レイアウトグループの中に、項目ごとに安定したkeyを付けた<Entity>をレンダリングします。
export function Scores({ font, scores }) {
return (
<Entity name="list">
<Element type="group" anchor={[0.5, 0.5, 0.5, 0.5]} pivot={[0.5, 0.5]} width={300} height={400} />
<LayoutGroup orientation={ORIENTATION_VERTICAL} spacing={[0, 10]} />
{scores.map(({ name, score }) => (
<Entity key={name} name={name}>
<Element type="text" fontAsset={font} text={`${name} ${score}`} fontSize={28} />
</Entity>
))}
</Entity>
);
}
LayoutGroupはレイアウトグループの作成にあるコンポーネントです。そこでは、項目が挿入されたときに行の順序を保つ方法も説明しています。
DOM APIで行を作成します。
function showScores(scores) {
const list = document.querySelector('pc-entity[name="list"]');
// 古い行を削除する。リスト自身のコンポーネントは残す
list.querySelectorAll(':scope > pc-entity').forEach(row => row.remove());
for (const { name, score } of scores) {
const row = document.createElement('pc-entity');
row.setAttribute('name', name);
const text = document.createElement('pc-element');
text.setAttribute('type', 'text');
text.setAttribute('font-asset', 'arial');
text.setAttribute('text', `${name} ${score}`);
text.setAttribute('font-size', '28');
row.appendChild(text);
list.appendChild(row);
}
}
ドラッグ&ドロップ
ElementDragHelperは、ドラッグされている間、エレメントをポインターに追従させます。エレメントの入力が有効になっている必要があり、アプリケーションにはマウスまたはタッチのデバイスが必要です。
const drag = new pc.ElementDragHelper(card.element);
const home = card.getLocalPosition().clone();
// エレメントの境界。キャンバスの左上からのCSSピクセル
const bounds = (entity) => {
const [bottomLeft, , topRight] = entity.element.canvasCorners;
return { left: bottomLeft.x, right: topRight.x, top: topRight.y, bottom: bottomLeft.y };
};
drag.on('drag:end', () => {
const a = bounds(card);
const b = bounds(slot);
const overSlot = a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
// カードをスロットにはめ込むか、元の位置に戻す
if (overSlot) {
card.setPosition(slot.getPosition());
} else {
card.setLocalPosition(home);
}
});
1つの軸に沿ってだけドラッグさせるには、2番目の引数に'x'または'y'を渡します。drag:startとdrag:moveは、ドラッグの開始時と継続中に発火します。
テキストフィールド
UIシステムにはテキスト入力のエレメントがありません。テキストを入力させるには、キャンバスの上にHTMLの<input>を配置します。これにより、プレイヤーはブラウザのキーボード、テキスト選択、オートコンプリート、アクセシビリティも利用できます。HTMLとCSSを参照してください。キャンバス内インターフェースの他の部分と見た目をそろえたフィールドについては、UIテキスト入力チュートリアルを参照してください。
UIのアニメーション
エレメントのアニメーションは、プロパティを変えることで行います。対象は、エンティティの位置、回転、スケールと、エレメントの色、不透明度、アンカー、マージンです。これらを毎フレーム少しずつ変更するか、トゥイーンライブラリを使って変更します。
// パネルを0.5秒かけて左からスライドインさせる
let t = 0;
const handle = app.on('update', (dt) => {
t = Math.min(t + dt / 0.5, 1);
const eased = 1 - Math.pow(1 - t, 3);
panel.setLocalPosition(pc.math.lerp(-600, 0, eased), 0, 0);
if (t === 1) handle.off();
});
不透明度は子に継承されないため、パネルをフェードさせるには、その下にあるすべてのエレメントをフェードさせます。
function setOpacity(entity, opacity) {
if (entity.element && entity.element.type !== pc.ELEMENTTYPE_GROUP) {
entity.element.opacity = opacity;
}
for (const child of entity.children) {
setOpacity(child, opacity);
}
}
この関数はすべてのエレメントに同じ不透明度を設定するため、エレメントごとに不透明度が異なる場合は、先に元の不透明度を保存しておきます。ボタンはfadeDurationでティント間をフェードします。トゥイーンライブラリについては、トゥイーンチュートリアルを参照してください。
ローディング画面とシーンの切り替え
- ローディング画面。 エディターのプロジェクトは、シーンが読み込まれる前、つまりUIが読み込まれる前にローディング画面を表示するため、ローディング画面はHTMLとCSSで作ります。ローディング画面を参照してください。
- シーンの切り替え。 シーンに属するインターフェースは、シーンと一緒に破棄されます。シーンを切り替えてもHUDを残すには、すべてを置き換えるのではなく、新しいシーンのヒエラルキーをHUDと並べて読み込みます。シーンの読み込みとシーンの切り替えチュートリアルを参照してください。
タッチジョイスティック
タッチデバイス向けの画面上のジョイスティックやボタンは、タッチイベントを読み取るエレメントです。タッチスクリーンのジョイパッド操作チュートリアルでは、再利用できるツインスティックのレイアウトを作成します。