メインコンテンツまでスキップ

アニメーションイベント

アニメーションイベントは、クリップ内のある瞬間に名前で印を付けるものです。Animコンポーネントの再生がその瞬間を過ぎると、コンポーネントがイベントを発火し、リスナーが実行されます。イベントは、物事のタイミングをアニメーションに合わせるために使います。たとえば、足が着地したときの足音、その足元に立つ土煙、ダメージを与える攻撃のフレーム、別の何かを始めるきっかけになるクリップの終わりなどです。

イベントの追加​

イベントはクリップに属するので、そのクリップを再生するすべてのAnimコンポーネントがイベントを発火します。各イベントは、クリップの先頭からの秒数であるtime、name、そしてリスナーに渡したいほかの任意のプロパティを持ちます。

クリップのeventsに、そのクリップのすべてのイベントを保持するAnimEventsを設定します。イベントは、どの順番で並べてもかまいません。

walkTrack.events = new pc.AnimEvents([
{ time: 0.2, name: 'footstep', foot: 'left' },
{ time: 0.7, name: 'footstep', foot: 'right' }
]);

これにより、クリップがすでに持っているイベントはすべて置き換えられます。

イベントのリッスン​

Animコンポーネントは、各イベントをその名前で発火し、イベントのプロパティを保持するオブジェクトを渡します。このオブジェクトには、イベントのname、time、イベントが属するtrack、そしてエディターで設定するnumberやstringのような、イベントに与えたほかのプロパティが含まれます。

character.anim.on('footstep', (event) => {
console.log(`${event.foot} foot down at ${event.time} s of ${event.track.name}`);
});

イベントが発火するタイミング​

イベントは、再生がその時刻を通過したフレームで発火します。

  • すべてのループで。 ループするクリップは、1周するたびにイベントを再び発火します。
  • クリップの終わりで。 クリップの長さと同じ時刻にあるイベントは、クリップがループしない場合でも、再生が終わりに達したときに発火します。そのため、クリップが終了したことを知るのに使えます。
  • どの速度でも。 クリップを速く、遅く、または逆方向に再生しても、イベントは発火します。
  • すべてのレイヤーで。 各レイヤーは、ウェイトが0であっても、自身が再生するクリップのイベントを発火します。同じイベントを持つクリップを2つのレイヤーで再生すると、そのイベントは2回発火します。
  • クロスフェード中も。 遷移元のステートはクロスフェード中も再生を続け、クロスフェードが終わるまでイベントを発火し続けます。
AI

1つのエフェクトが複数のアニメーションにまたがる場合は、AIアシスタントにEditor MCP Serverを通じてイベント名とペイロードをスクリプトのリスナーと照合させ、その後Launcherでアニメーションを再現して、欠けているエフェクトやタイミングのずれたエフェクトを見つけます。

例​

このエンジンアプリは、ロボットアームのpickクリップをループ再生し、クリップに2つのイベントを追加します。1つは爪が閉じたとき、もう1つは爪が再び開いたときのイベントです。リスナーは、爪が閉じている間それを光らせ、最後のイベントを表示します。

ライブサンプル
import * as pc from 'playcanvas';

// 使用するコンポーネントシステムとリソースハンドラーを指定してアプリケーションを作成します
const canvas = document.getElementById('application');
const device = await pc.createGraphicsDevice(canvas, {
deviceTypes: [pc.DEVICETYPE_WEBGPU]
});
device.maxPixelRatio = Math.min(window.devicePixelRatio, 2);

const options = new pc.AppOptions();
options.graphicsDevice = device;
options.componentSystems = [
pc.RenderComponentSystem,
pc.CameraComponentSystem,
pc.LightComponentSystem,
pc.AnimComponentSystem
];
options.resourceHandlers = [pc.ContainerHandler, pc.TextureHandler];

const app = new pc.AppBase(canvas);
app.init(options);
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
app.setCanvasResolution(pc.RESOLUTION_AUTO);
window.addEventListener('resize', () => app.resizeCanvas());
app.start();

app.scene.ambientLight = new pc.Color(0.35, 0.35, 0.4);

// 5つのアニメーションを持つGLBである、ロボットアームを読み込みます
const asset = new pc.Asset('robot-arm', 'container', {
url: 'https://developer.playcanvas.com/assets/robot-arm.glb'
});
await new Promise((resolve) => {
asset.ready(resolve);
app.assets.add(asset);
app.assets.load(asset);
});

const camera = new pc.Entity('camera');
camera.addComponent('camera', {
clearColor: new pc.Color(0.11, 0.12, 0.17)
});
camera.setPosition(1.75, 1.4, 2.15);
camera.lookAt(0, 0.75, 0);
app.root.addChild(camera);

const light = new pc.Entity('light');
light.addComponent('light', {
castShadows: true,
shadowBias: 0.2,
normalOffsetBias: 0.05
});
light.setEulerAngles(48, 38, 0);
app.root.addChild(light);

const floor = new pc.Entity('floor');
const floorMaterial = new pc.StandardMaterial();
floorMaterial.diffuse = new pc.Color(0.24, 0.26, 0.31);
floorMaterial.update();
floor.addComponent('render', { type: 'plane', material: floorMaterial });
floor.setLocalScale(20, 1, 20);
app.root.addChild(floor);

// アームのエンティティを作成し、そのルートにAnimコンポーネントを追加します
const arm = asset.resource.instantiateRenderEntity({ castShadows: true });
arm.addComponent('anim', { activate: true });
app.root.addChild(arm);

// グリッパーの爪に専用のマテリアルを与えます。イベントによって、爪が閉じている間はこのマテリアルが光ります
const jaws = ['arm_jaw_l', 'arm_jaw_r'].flatMap(name => arm.findByName(name).render.meshInstances);
const jawMaterial = jaws[0].material.clone();
jaws.forEach((meshInstance) => {
meshInstance.material = jawMaterial;
});

// pickクリップに2つのイベントを追加します。爪は2.3秒の時点で閉じており、4.6秒で再び開きます
const pick = asset.resource.animations.find(animation => animation.resource.name === 'pick').resource;
pick.events = new pc.AnimEvents([
{ time: 2.3, name: 'jaws', string: 'closed' },
{ time: 4.6, name: 'jaws', string: 'open' }
]);
arm.anim.assignAnimation('Pick', pick);

// Animコンポーネントは、ループのたびに、再生が各イベントを通過したときにそのイベントを発火します
const label = document.createElement('div');
label.style.cssText = 'position: absolute; left: 12px; bottom: 10px; color: #ccc; font: 14px sans-serif;';
label.textContent = 'Waiting for an event...';
document.body.appendChild(label);
arm.anim.on('jaws', (event) => {
jawMaterial.emissive = event.string === 'closed' ? new pc.Color(0.2, 1, 0.3) : pc.Color.BLACK;
jawMaterial.update();
label.textContent = `Event: ${event.name}, ${event.string}, at ${event.time} seconds into ${event.track.name}`;
});

次のことを試してみてください。

  • closedイベントを、爪が閉じ始める1.9秒に移動します。
  • { time: 0, name: 'jaws', string: 'open' }を追加すると、ループが始まるたびにイベントが発火します。
  • arm.anim.assignAnimation('Pick', pick, undefined, -1);で、クリップを逆方向に再生します。イベントが逆の順番で発火するので、爪は開いている間に光るようになります。
Events

関連情報​