🕺

clusterのベータ機能でプレイヤーを踊らせる

2023/09/29に公開

clusterのベータ機能で踊らせるワールドの作り方です。
コードは地味に面倒なんで、ポイントだけ解説します。

clusterでのサンプルワールドはこちらからどうぞ。

よりなめらかに動かすことが可能に(3/25)

CCKのアップデートにより、補完でより滑らかに動かすことができるようになりました。

元ネタ

https://youtu.be/wl-k5mgJLgo?t=3880
こちらのcluster公式さんの解説動画でされていたやり方を、固まったポーズではなくダンスできるようにしただけです。あと、複数のダンスが時間によって切りかわります。

説明は完全にスクリプトを理解している人向けですが、コピペで流用するなら別にスクリプトをほとんどわからない人でも利用可能です(「Unityでの準備」のとこだけマネして、この記事の最後にあるスクリプト全文をコピペしてください。スクリプト自体の基本はこちらの記事を。

Unityでの準備

上の動画にあるように、HumanoidAnimationListをつけたScriptableItemをUnityで作りましょう。mixamoとかで適当な数のモーションをダウンロードしてきて、動画内の操作を参考にHumanoidAnimationListへ登録してください。

アイテムの子にはステージをおおうような形のコライダーをつけて、OverlapDetectorShapeをつけます。この辺も動画と同じです。


MeshRendererをオフにし、「トリガーにする(IsTrigger)」をONにして透明なコライダーにしています。

スクリプトのポイント

$.humanoidAnimationでダンスモーションを取得し、animationArという配列に入れていきます。
Dance0から始まり、最大でDance9までチェックしていきます。
サンプルではDance0~Dance4まで登録してあるので、5つのダンスが配列に入ることになります。

const animationCheckMax = 10;
const animationAr = [];
for (let i = 0; i < animationCheckMax; i++) {
  const anim = $.humanoidAnimation("Dance" + i);
  if (anim != null) {
    animationAr.push(anim);
  } else {
    break;
  }
}

ダンスの長さを取得

後々必要になるので、ダンスの長さをgetLengthで取得しておきます。

$.state.danceLength = animationAr[$.state.currentDanceNo].getLength();

ダンス部分

$.onUpdateでダンスさせます。
onUpdateのdeltaTimeには、前回から何秒経過したのかが入っています。
たいていは0.03とか0.016とか相当小さい数字が入っています。

  $.state.danceTick += deltaTime;
  if ($.state.danceTick > $.state.danceLength) {
    $.state.danceTick -= $.state.danceLength;
  }
  
  let pose = animationAr[$.state.currentDanceNo].getSample($.state.danceTick);
  let currentOverlapPlayers = $.getOverlaps();
  currentOverlapPlayers.forEach((p) => p.setHumanoidPose(pose,{timeoutSeconds: 1.0,timeoutTransitionSeconds: 0.3,transitionSeconds: 0.11}));

基本はこれだけです。自分でダンスの秒数をdeltaTimeを使って計算し、ダンスが終わったら(danceLengthの長さを超えたら)、また秒数をはじめのほうに戻してgetSampleにその秒数を入れればいいわけですね。あとは元ネタの動画と大して変わりません。
なお、3/25にダンスをなめらかに補完にする機能が入ったので、それを追加しています(transitionSecondsとかのあたり)。

ただ、

  • setHumanoidPoseの回数制限を考慮していない
  • getOverlapsでは同じプレイヤーが2回入っていることがある点を考慮していない(2回setHumanoidPoseすると回数制限を回避するのがさらに厳しくなる)

こういった問題があるので、そこに対応し、またランダムにダンスを切りかえていく機能をつけたものが以下の全文です。
コピペして使ってください。

スクリプト全文

const danceChangePerSec = 0.12; //何秒に1回ポーズを切りかえるか

const animationCheckMax = 10; //ダンスを最大いくつまでチェックするか
const animationAr = []; //ダンスの入る配列
//ダンスを登録していく
for (let i = 0; i < animationCheckMax; i++) {
  const anim = $.humanoidAnimation("Dance" + i);
  if (anim != null) {
    animationAr.push(anim);
  } else {
    break;
  }
}

//初期化
const initProc = () => {
  $.state.initialized = true;
  $.state.tick = 0; //ポーズ切りかえを行うまでの秒数を計測
  $.state.danceTick = 0; //現在のダンスの秒数
  $.state.currentDanceNo = 0; //現在のダンスの番号
  $.state.danceLeftNum = 0; //何回今のダンスを行ったら次のダンスに行くか

  $.state.overlapPlayers = []; //重なっているプレイヤーを記録

  checkDanceLength();
  setDanceLeftNum();
};

//ダンスを何回繰り返すか、ランダムに決める
const setDanceLeftNum = () => {
  $.state.danceLeftNum = Math.floor(Math.random() * 3)+2;
};

//ダンスの1回ぶんの長さ(秒数)を取得
const checkDanceLength = () => {
  $.state.danceLength = animationAr[$.state.currentDanceNo].getLength();
};

//ダンスをランダムに変え、長さ(秒数)の取得などをする
const nextDance = () => {
  $.state.currentDanceNo = Math.floor(Math.random() * animationAr.length);
  checkDanceLength();
  setDanceLeftNum();
};

// idでフィルタリングと重複削除を行う関数
function filterAndRemoveDuplicates(array) {
  const seenIds = new Set(); // 重複をチェックするためのセット
  return array.filter((obj) => {
    // 重複チェック
    if (seenIds.has(obj.id)) {
      return false; // すでに同じidのオブジェクトがある場合は除外
    } else {
      seenIds.add(obj.id); // 新しいidをセットに追加
      return true; // 重複していない場合は残す
    }
  });
}

//メインループ deltaTimeには前回からの経過秒数が入る
$.onUpdate((deltaTime) => {
  if (!$.state.initialized) {
    initProc(); //初期化
  }

  //ダンスの秒数を計算
  $.state.danceTick += deltaTime;
  if ($.state.danceTick > $.state.danceLength) {
    //ダンスが終わりまでいったら先頭のほうに戻す
    $.state.danceTick -= $.state.danceLength;
    //何回繰り返すかの回数を減らして
    $.state.danceLeftNum--;
    //0以下になったら次のダンスへ
    if ($.state.danceLeftNum <= 0) {
      nextDance();
    }
  }

  //何秒に一回ポーズを切りかえるか
  $.state.tick += deltaTime;
  //まだその秒数になっていなければ終わり
  if ($.state.tick < danceChangePerSec) {
    return;
  }
  $.state.tick -= danceChangePerSec;

  //ポーズ切りかえのメイン部分(これを0.12秒に1回とかするからダンスして見える)
  
  //この辺りは動画の元ネタと同じ エリアに入っているプレイヤーを配列にする
  let previousOverlapPlayers = $.state.overlapPlayers;
  let currentOverlapPlayers = $.getOverlaps()
    .map((o) => o.object.playerHandle)
    .filter((p) => p != null);
  //重複するプレイヤーを削除
  let duplicatesRemovedArray = filterAndRemoveDuplicates(currentOverlapPlayers);

  //現在のダンス秒数を使い、現在のポーズを取得
  let pose = animationAr[$.state.currentDanceNo].getSample($.state.danceTick);

  //エリアにいる全てのプレイヤーにポーズをとらせる
  duplicatesRemovedArray.forEach((p) => p.setHumanoidPose(pose,{timeoutSeconds: 1.0,timeoutTransitionSeconds: 0.3,transitionSeconds: 0.11}));
  
  //今回エリアの中にいたプレイヤーは記録しておく
  $.state.overlapPlayers = duplicatesRemovedArray;
});

Discussion