見出し画像

Unityアセット PuppetMasterでラグドールをつかむ機能解説

今作ってるゲームでは根幹システムに、PuppetMasterを使ってます。

PuppetMasterとは

PuppetMasterは、ふつうはキャラのコントロールが完全に失われた状態にしかならないラグドールを、キャラのコントロールを維持したまま部分的にラグドール状態にすることができるというアセットです。
参考記事https://www.asset-sale.net/entry/PuppetMaster

これのサンプルについてくるキャラクターコントローラが、素手殴りに武器殴りに壁走りに2段ジャンプと多機能でなかなか使えるのですが、
サンプルの一つに掴み機能があります。
PuppetMaster-以後パペマスと表記-については、以前別の日記にも使い方を書いたのですが、
https://bta25fun.wixsite.com/modev/post/ai-behaviorとpuppetmasterでラグドールnpc
https://bta25fun.wixsite.com/modev/post/puppetmasterの掴みアクションを使うには
掴み機能について更にしっかり仕様がわかったので、改めて記事を書きます。

掴み機能のスクリプト

公式のサンプルコードGrab.csは実用性がいまいちなので、私がカスタムした掴みシステムPuppetGrabber.csのコードを晒します。要パペマスです。
パペマスのセットアップ方法については、上のリンク先の記事か、https://www.asset-sale.net/entry/PuppetMaster_1とかを参照してください。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using RootMotion.Dynamics;
using UnityEngine.Events;

namespace FUNSET
{
	[RequireComponent(typeof(Rigidbody))]
	/// <summary>
	/// パペマスのラグドールを掴む機能。触ったパペマスキャラのピンを外しラグドール化、触った箇所にジョイントを生成して
	/// </summary>
	public class PuppetGrabber : MonoBehaviour
    {

		[Header("自分がパペマスを使ってる時は割り当てる"), Tooltip("つかみ機能をキャラの手足に実装する場合は、自身のコライダーを除外するためにこれを設定する必要がある")]
		public PuppetMaster puppetMaster;

		[Header("取得したいレイヤー(Puppety)を割り当てる"), Tooltip("取得したいレイヤー(Puppety)を割り当てる")]
		public int grabLayer=9;

		[Header("[参考用]掴んでるかどうか"), Tooltip("[参考用]掴んでるかどうか")]
		public bool grabbed;

		[Tooltip("つかみ判定のためのRigidbody")]
		private Rigidbody r;
		[Tooltip("つかみ判定")]
		private Collider c;

		[Tooltip("掴んだ相手のBehaviourPuppetを取得して、ピン留めを制御する")]
		private BehaviourPuppet otherPuppet;
		private Collider otherCollider;
		private ConfigurableJoint joint;
		private float nextGrabTime;

		private const float massMlp = 10f;//元は5
		private const int solverIterationMlp = 20;//元は10
		int solvermoto;

		[Header("つかみ可能フラグ"), Tooltip("つかみ可能フラグ")]
		public bool CanGrab; //追加したつかみ可能フラグ

		public UnityEvent GrabBegin;//追加したイベント
		public UnityEvent GrabRerease;//追加したイベント

		void Awake()
		{
			r = GetComponent<Rigidbody>();
			c = GetComponent<Collider>();
			r.isKinematic = true;
		}

		/// <summary>
		/// つかみ判定はコリジョンでもトリガーでも対応
		/// </summary>
		/// <param name="collision"></param>
		void OnCollisionEnter(Collision collision)
		{
			GrabCheck(collision.gameObject);
		}

        private void OnTriggerEnter(Collider other)
        {
			GrabCheck(other.gameObject);
        }

        void Update()
		{
			if((otherPuppet == null)&&(!grabbed)) return;
			if (!CanGrab) return;

			if ((otherPuppet != null) && (!grabbed)) { ReleaseOtherPuppet(); }//掴み中にgrabbedをオフになったら手放す(デバッグ用) 

		}

		void GrabCheck(GameObject Other) 
		{
			if (!CanGrab) return;//つかみフラグがオフならすべて無効

			if (grabbed) return; // すでになにか掴んでるなら無効
			if (Time.time < nextGrabTime) { return; } // 最後のリリースから十分に時間経過してるか...
			if (Other.layer != grabLayer) return; // コライダーは正しいレイヤーにあるか..
			var Or = Other.GetComponent<Rigidbody>();
			if (Or == null) return; // 剛体がつけられてるか.

			// MuscleCollisionBroadcasterを見つける 
			var m = Other.GetComponent<MuscleCollisionBroadcaster>();
			if (m == null) return; // 衝突したコライダーがパペットのマッスルであることを確認
			if (puppetMaster != null) { if (m.puppetMaster == puppetMaster) return; }// ...自身のマッスルではない.

			//当たった相手がダウン状態でなかったら無効

			foreach (BehaviourBase b in m.puppetMaster.behaviours)
			{
				if (b is BehaviourPuppet)
				{
					otherPuppet = b as BehaviourPuppet;
					otherPuppet.SetState(BehaviourPuppet.State.Unpinned); // Unpin
					otherPuppet.canGetUp = false; 
				}
			}
			if (otherPuppet == null) return; // If not BehaviourPuppet found, break out

			// 2つのパペットをリンクするためのConfigurableJointの追加
			joint = gameObject.AddComponent<ConfigurableJoint>();
			joint.connectedBody = Or;

			// アンカーを手のある場所に移動します(手の剛体があるためずらすが)
			joint.anchor = new Vector3(-0.15f, 0f, 0f);

			// 関節の直線運動と角運動をロックする
			joint.xMotion = ConfigurableJointMotion.Locked;
			joint.yMotion = ConfigurableJointMotion.Locked;
			joint.zMotion = ConfigurableJointMotion.Locked;
			joint.angularXMotion = ConfigurableJointMotion.Locked;
			joint.angularYMotion = ConfigurableJointMotion.Locked;
			joint.angularZMotion = ConfigurableJointMotion.Locked;

			// リンクされたリジッドボディがチェーンの一部であるときにその質量を増やすことは、リンクの安定性を向上させる最も簡単な方法です。
			r.mass *= massMlp;

			// 2つの長いジョイントチェーン間のリンクの安定性を向上させるには、ソルバーの反復回数を増やす必要があります。
			if (puppetMaster != null)
			{
				solvermoto = puppetMaster.solverIterationCount;
				puppetMaster.solverIterationCount *= solverIterationMlp;
			}

			// つかんだオブジェクトとの衝突を無視させます
			otherCollider = Other.GetComponent<Collider>();
			Physics.IgnoreCollision(c, otherCollider, true);

			// 他のパペットをうまくつかむことができました
			grabbed = true;

			//掴んださいのイベント
			GrabBegin.Invoke();
			//Debug.Log("GrabコンポーネントはOnCollisionEnterでつかんだ");
		}

		//パペットを手放す
		public void ReleaseOtherPuppet()
		{
			//Debug.Log("ReleaseOtherPuppetを呼び出した");
			if (otherPuppet == null) { Debug.Log("パペマスがない 手放せない"); return; }//追加:

			Destroy(joint);
			r.mass /= massMlp;

			if (puppetMaster != null)
			{	puppetMaster.solverIterationCount =solvermoto;}

			Physics.IgnoreCollision(c, otherCollider, false);

			otherPuppet.canGetUp = true;
			otherPuppet = null;
			otherCollider = null;
			grabbed = false;
			nextGrabTime = Time.time + 1f;

			//Debug.Log("つかみを離した");

			//手放すさいのイベント
			GrabRerease.Invoke(); 
		}

		//外部から掴み可能フラグを切り替える
		public void SwhitchCanGrab(bool a) { CanGrab = a; }

		public void SwhitchGrabbed(bool a) { grabbed = a; }

		/// <summary>
		/// 非アクティブになったらグラブ解除
		/// </summary>
        private void OnDisable()
        {
			ReleaseOtherPuppet();
	}
    }
}

(コードは実際に使ってるものから余計なコードとか注釈とかを整理。省略したものです。誤字ってたらごめんなさい…)

使い方

これを、Rigidbodyとコライダーを備えたオブジェクトにアタッチして、
CanGrabをオンにしてぶつけると、PuppetMasterを実装したキャラモデルはunpinned=ラグドール化=ダウン状態になり、触った箇所で固定されます。PuppetGrabberのオブジェクトを移動すればキャラは釣り上げられた状態になり、振り回せます。

メソッド SwhitchCanGrabでCanGrabをオフにするか、SwhitchGrabbedでGrabbedをオフにするか、PuppetGrabberのオブジェクトを非アクティブにすると拘束は解除されます。
コライダーはTriggerにしてても効きます。

これをプレイヤーキャラの手にアタッチすればつかみ機能となります。

ただし、プレイヤーにもパペマスを使用してる場合は、掴み用のコライダーとパペマスラグドールのコライダー(マッスル)がぶつかると体が吹っ飛ぶか判定ができなくなるため、実質手のマッスルにアタッチしてラグドールのコライダーを掴み判定に兼用して使うことになります。

ここにセット


スクリプトの解説

仕様の肝は、MuscleCollisionBroadcasterConfigurableJointにあります。
パペマスはパペマス専用レイヤー(デフォルトでは9番目)を使用して、実行時に全身の部位(PuppetMasterではマッスルと称してます)にMuscleCollisionBroadcasterをアタッチするという仕様になってます。
これによって体のどこかにPuppetGrabberが接触したら、その情報が全身の部位とラグドールの大本を制御してるPuppetMaster.csとBehaviourPuppet.csに伝達され、情報を取得することができるようになり、キャラをダウン状態=ラグドール化するように操作できます。

// MuscleCollisionBroadcasterを見つける 
var m = Other.GetComponent<MuscleCollisionBroadcaster>();

なおプレイヤーにもパペマスを使ってる場合は、プレイヤーのpuppetMaster も登録しておいて、自分の体を掴んでるわけではないという例外処理に使います。

if (puppetMaster != null) { if (m.puppetMaster == puppetMaster) return; }

その後、接触した部位にConfigurableJointを作って、ラグドールを衝突した場所に接続します。

接触した部位にConfigurableJointを作って…てのは通常のラグドールでもできそうなのですが、パペマスの本領はここからで、BehaviourPuppet.csにダウン後時間で復帰するように設定してあると、キャラはダウン状態から立ち上がり、掴まれたまま通常の行動をしようとします。モーションがラグドールとブレンドされて、愉快な絵面になります。

投げを実装するには

投げを実装するには、投げるモーションを用意して、手を振り抜く直前あたりでタイミングよくGrabbedをオフにすれば、投げ飛ばせます。


真正面に飛ばすのはモーション次第ですが、難しいです。Addforceで移動ベクトルを与えて意図した方向に投げる事もできるはずですが、うまくいきませんでした。
カーブ系アセットを使って放物線のパスの上にラグドールキャラを載せて(ConfigurableJointの接続先を変えて)移動させたら、確実に飛ぶんじゃないかな。

注意点

このPuppetGrabber.cs、コライダーが接触した座標にしか掴む位置を設定できません。接触したら掴んだ手もしくは相手を自動的に投げやすいポジションに補正するとかは、できなくもないけど難しいようでした。
そういうのがやりたかったら、FinalIKの方が向いてるかもしれません。

こちらにもパペマスとは異なる仕様の掴み機能があるようです。

また、PuppetGrabber.csでは掴んだキャラのPuppetMaster以外の情報を受け渡すことができません。
掴んだキャラの体力とかステートとか受け渡したかったら、PuppetMaster.csにそのキャラのキャラクターコントローラーなどの情報をひもつけて、PuppetGrabberがそれを取得してプレイヤーや捕まれたキャラのキャラコンまで伝達できるようにする改造が必要です。
改造自体はそこまで苦労はありませんが、PuppetMasterアセットがアップデートしたときにスクリプトが上書きされるから作り直しになるという問題があります。年1-2回の頻度でアップデートするので、ちょっと怖いです。

この記事が気に入ったらサポートをしてみませんか?