【ベータ機能・サンプル付き】追加のボタン入力と接地状態によってアクションが変わる剣をつくってみよう

本記事はCluster Creator Kitの「ベータ機能」を利用した解説になります。
ベータ機能では正式リリース前の機能を使うことができます。正式リリース前のため、不安定な挙動をしたり将来的に挙動が変わる可能性があります。また、記事公開時点ではベータ機能として紹介していますが、その後正式リリースされている場合もありますので、本記事を参考にする場合はまずScript Referenceを参照することをおすすめします。

Cluster Creator Kit v2.18.0から、ベータ機能として「PlayerScript」が使えるようになりました。

「PlayerScript」ではCluster HUD v2という新しい仕組みを利用して、「キーボードなどから任意の入力を受けてアバターに反映する」「従来よりきめ細かいアニメーションを適用する」「プレイヤーが使用する機器がVRかそうでないか」など多くの新しい機能を使うことができるようになりました。

Creators Guideでは、PlayerScriptで実現できる機能についてサンプルプロジェクト付きでいくつか解説していきます。

この記事では「ボタンを追加する機能」「入力を受けてアバターにアニメーションを反映する機能」「アバターの移動やモーションに関する状態を取得する機能」を組み合わせたアイテムの作成方法を紹介します。

この機能を使って、地上攻撃と空中攻撃ができる剣、つまり「アバターが接地しているかどうかで挙動が変わる」アイテムをつくります。

今回作成するギミックはベータ機能を有効にしたUnity製のワールドで利用可能です。正式版やワールドクラフトでは使えません。

この記事では、すぐに使えるサンプルアイテムを配布しています。
まずは使ってみたいという方は下記からダウンロードしてください。本記事では、サンプルアイテムの構成やコードについて解説していきます。

使用するためにはベータ機能や下記で解説する World Runtime Setting の設定が必要になります。

Cluster Creator Kitをv2.18.0以上にアップデートする

Cluster Creator Kiのアップデート方法はこちらの記事をご覧ください。

ベータ機能を有効にする

利用にはCluster Creator Kitでベータ機能を設定した状態でアップロードを行う必要があります。設定方法はこちらをご覧ください。

HUD v2を有効にする

HUDとは、画面上に表示されるボタンなどの要素を指します。
今回のギミックを使うには、新しく追加された「HUD v2」機能を有効にする必要があります(詳しくはこちら)。
シーン上に設置したWorld Runtime Settingコンポーネントで、「Use Cluster HUD v2」を有効にしてください。

今回のサンプルプロジェクトは「ボタンを追加する機能」「入力を受けてアバターにアニメーションを反映する機能」「アバターの移動やモーションに関する状態を取得する機能」を組み合わせた例として、アバターが接地しているかどうかで挙動が変わる「地上攻撃と空中攻撃ができる剣」を用意しました。

アイテムの構成は下記のようになっています。

このGameObjectの内、「Sword」に下記のコンポーネントが割り当てられています。以下に各コンポーネントの役割・設定を説明します。

Sword
  • Item
  • Grabbable Item
    • アイテムに掴める機能を付与する
  • Scriptable Item
    • 掴んだ時に掴んだプレイヤーに対してPlayerScriptを登録させる役割
  • Player Script
    • ボタンを追加する機能
    • 入力を受けてアバターにアニメーションを反映する機能
    • アバターの移動やモーションに関する状態を取得する機能
  • Icon Asset List
    • HUD上に表示するアイコンを設定するためのリソース
      • Id: SwordAttack
      • Image には攻撃ボタンとして使用するアイコンが割り当てられています。
  • Humanoid Animation List
    • プレイヤーに適用するアニメーションファイルを設定するためのリソース
      • Humanoid Animations: 2
      • 地上攻撃のアニメーション
        • Id: GroundMotion
        • Animationに地上攻撃用のアニメーションファイルを設定しています。
      • 空中攻撃のアニメーション
        • Id: AirMotion
        • Animationに空中攻撃用のアニメーションファイルを設定しています。

Idはスクリプトから参照するため、名前は変更しないようにしてください。

スクリプト

$.onGrab((isGrab, isLeftHand, player) => {

  // アイテムを掴んだ時、掴んだプレイヤーにPlayerScriptを適用する
  if (isGrab) {
    $.setPlayerScript(player);
    player.send("Grabbed", null);
  }

  // アイテムを手放した時、手放したプレイヤーにメッセージを送信する
  else {
    player.send("Released", null);
  }

});

スクリプトの解説

$.onGrab (アイテムを掴んだり、手放したりした時に実行される)にて、以下のように記述しています。

$.setPlayerScript(player);

この記述により、剣のアイテムを掴んだプレイヤーに対して、$.setPlayerScriptを実行しています。
$.setPlayerScriptを実行すると、このアイテムのPlayer Script コンポーネントに設定されたスクリプトを、特定のプレイヤーに登録することができます。

また、剣のアイテムを掴んだり手放したりしたプレイヤーに対して、sendを使用してメッセージを送信しています。これにより、プレイヤーに適用したPlayerScriptの処理を開始するタイミングを制御しています。

スクリプト

// アニメーションクリップを取得
const groundMotion = _.humanoidAnimation("GroundMotion");
const airMotion = _.humanoidAnimation("AirMotion");
const airMotionLength = airMotion.getLength();

// モーションを再生開始してから経過した時間
let motionTime = 0.0;

// モーションの状態を格納する変数
//   0: モーション未実行
//   1: 地上モーション
//   2: 空中モーション
let motionPhase = 0;

_.onReceive((messageType, arg, sender) => {
  
  if (messageType === "Grabbed") {
    // ボタンを表示する
    _.showButton(0, _.iconAsset("SwordAttack"));
  }

  if (messageType === "Released") {
    // ボタンを非表示にする
    _.hideButton(0);
  }
});

// ボタン0の操作時に実行される処理
_.onButton(0, (isDown) => {
  // モーション実行中であれば、何もしない
  if (motionPhase > 0) return;

  if (isDown) { 
    motionTime = 0.0;

    // アバターが地上にいるかどうかをチェックする
    let flags = _.getAvatarMovementFlags();
    let isGrounded = (flags & 0x0001) !== 0;

    if (isGrounded) { 
      motionPhase = 1;
    } else {
      motionPhase = 2;
    }
  }
});

// 毎フレーム実行される処理
_.onFrame((deltaTime) => {

  switch(motionPhase){
    // motionPhaseが1なら地上モーションを更新
    case 1:
      updateGroundMotion(deltaTime);
      break;

    // motionPhaseが2なら空中モーションを更新
    case 2:
      updateAirMotion(deltaTime);
      break;
  }

});

// 地上モーションを更新する関数
function updateGroundMotion(deltaTime) {

  // 再生時間に応じた姿勢を適用する
  motionTime += deltaTime;
  applyPose(groundMotion, motionTime);

  // モーションを最後まで再生したら終了する
  if (motionTime > groundMotion.getLength()) {
    motionPhase = 0;
  }
}

// 空中モーションを更新する関数
function updateAirMotion(deltaTime) { 

  // 再生時間に応じた姿勢を適用する
  // 全フレームを再生したら最終フレームのポーズを維持する
  if(motionTime < airMotionLength){
    motionTime += deltaTime;
  }
  applyPose(airMotion, motionTime);

  // アバターが着地したら終了する
  let flags = _.getAvatarMovementFlags();
  if ((flags & 0x0001) !== 0) {
    motionPhase = 0;
  }
}

// 各種モーションをアバターに適用する関数
function applyPose(anim, time) {
  let pose = anim.getSample(time);
  _.setHumanoidPoseOnFrame(pose);
}

スクリプトの解説

HUD上にアバターにアニメーションを適用するためのボタンを設置する

PlayerScriptのメソッドは、 “$” ではなく “_” で呼び出します。

_.onReceive((messageType, arg, sender) => {
  
  if (messageType === "Grabbed") {
    // ボタンを表示する
    _.showButton(0, _.iconAsset("SwordAttack"));
  }

  if (messageType === "Released") {
    // ボタンを非表示にする
    _.hideButton(0);
  }
});

 _.onReceive では、Scriptable Itemコンポーネントに設定されたコードの $.onGrab の処理からメッセージを受け取って、ボタンの表示・非表示を変更しています。

“Grabbed” のメッセージを受け取った際は _.showButton を実行することで、Cluster HUD v2に攻撃ボタンを表示しています。Icon Asset List コンポーネントで設定した、Idが 「SwordAttack」のアイコンを呼び出しています。

ボタンを押した時の接地判定のための処理

_.onButton では、ボタンを押したときの処理を記述しています。接地判定を行い、地上にいる場合は変数 motionPhase を1に、空中にいる場合は変数 motionPhase を2に設定しています。既にモーションを実行中の場合は、何も行いません。変数 motionPhase の値に応じて、後述の _.onFrame の項で適用するモーションを変更しています。

    // アバターが地上にいるかどうかをチェックする
    let flags = _.getAvatarMovementFlags();
    let isGrounded = (flags & 0x0001) !== 0;

    if (isGrounded) { 
      motionPhase = 1;
    } else {
      motionPhase = 2;
    }

_.getAvatarMovementFlags ではビットフラグを取得することができます。取得したビットフラグを判定するために、&演算子を使用してビット論理積を求めています。もし、接地判定ではなく、よじのぼり中かの判定や、乗り物に乗っているかの判定を行いたい場合は、Cluster Creator Kit Script Reference のサンプルコードを参考にしてみてください。

アニメーションを適用する

以降のコードは、  _.onFrame 内で、フレームごとにプレイヤーにモーションを適用しています。変数 motionPhase が1の時は地上攻撃モーションを、motionPhase が2の時は空中攻撃モーションを適用します。 motionPhase が0の時はモーションの適用を行いません。

// 毎フレーム実行される処理
_.onFrame((deltaTime) => {

  switch(motionPhase){
    // motionPhaseが1なら地上モーションを更新
    case 1:
      updateGroundMotion(deltaTime);
      break;

  // motionPhaseが2なら空中モーションを更新
    case 2:
      updateAirMotion(deltaTime);
      break;
  }

});

地上モーションと空中モーションで、モーションの終了タイミングが異なるようにしています。地上モーションではモーションを最後まで再生したら終了しますが、空中モーションではプレイヤーが着地するまで最終フレームのポーズを維持するようにしています。

// 地上モーションを更新する関数
function updateGroundMotion(deltaTime) {

  // 再生時間に応じた姿勢を適用する
  motionTime += deltaTime;
  applyPose(groundMotion, motionTime);

  // モーションを最後まで再生したら終了する
  if (motionTime > groundMotion.getLength()) {
    motionPhase = 0;
  }
}

// 空中モーションを更新する関数
function updateAirMotion(deltaTime) { 

  // 再生時間に応じた姿勢を適用する
  // 全フレームを再生したら最終フレームのポーズを維持する
  if(motionTime < airMotionLength){
    motionTime += deltaTime;
  }
  applyPose(airMotion, motionTime);

  // アバターが着地したら終了する
  let flags = _.getAvatarMovementFlags();
  if ((flags & 0x0001) !== 0) {
    motionPhase = 0;
  }
}

PlayerScriptでは、モーションの適用に _.setHumanoidPoseOnFrame を使用することができます。

// 各種モーションをアバターに適用する関数
function applyPose(anim, time) {
  let pose = anim.getSample(time);
  _.setHumanoidPoseOnFrame(pose);
}

PlayerHandle.setHumanoidPose ではアニメーションの上書きや解除などを秒数で指定することしかできませんでしたが、setHumanoidPoseOnFrameではフレーム単位でモーションを指定できるため、よりきめ細かいアニメーションを適用できるようになりました。

PlayerScriptを使うことで、プレイヤーに対する状態や姿勢の制御が詳細にできるようになりました。また、Cluster HUD v2では持っているアイテムをアバターの正面に持ち上げなくなったので、剣のような掴むアイテムの操作設計の自由度がより高まったといえるでしょう。

一方で、銃のようなアイテムであれば、従来のようにアバターの正面に持ち上げて使いたいこともあるでしょう。そういった挙動をPlayerScriptで実装したサンプルなども今後公開していく予定です。PlayerScriptの応用例は他にもたくさんあると思いますので、ぜひ活用してみてください。

記事をシェアしてワールド制作を盛り上げよう!

Cluster Creators Guide|バーチャル空間での創作を学ぶならをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む