質問: ### 前提・実現したいこと Unityでクレーンゲームのシュミレーターを作成しています。 アームユニットを下降させる際にrigidbody(Is Kinematic)を用いているのですが、 アームユニットが物体に衝突しても、トリガーを設定していないのでそのまま下降していき非現実的な挙動となってしまいます。(y座標の最低高度は指定してあります。) 現実のクレーンゲームだと紐を巻き取ったりして制御しているそうです。(負荷がかかると紐がたるんで下降がストップする) どのようなアルゴリズムを用いれば良いでしょうか? まだプログラムに関するリテラシーが浅いので質問させていただきました。 ### 試したこと _★_ ◯◯◯ ◯◆◯ ◯_◯ ◯◯◯ ★がトリガー(Is trigger) ◯がrigidbody(Use Gravity)を適用した物体(繋がっています) ◆がユニット 上記で◯が★に接触した場合に下降を停止するようにしていますが、ほかの方法の方が良いのではという勘です。
回答: 申し訳ないです、せっかく図で説明していただきましたが、いまいち状況が把握しきれず、勝手な解釈での回答です(クレーンが景品に下降していく際、何も対策しないとクレーンが景品の山にめり込んでいってしまうので、個々の景品の上面にクレーン停止用トリガー領域を設置する方法をとったが、これ以外に案はないだろうか...ということでしょうか?)。 トリガー方式も悪くないと思うのですが、違ったアプローチもご所望でしたら、一案としてクレーンをキネマティックなRigidbodyと非キネマティックなRigidbodyを組み合わせた構造にして、両者をFixedJointなど適当なジョイントで繋ぎ、そのジョイントのcurrentForceを監視して負荷のかかり方を検出する...というのを思いつきましたが、いかがでしょう。 クレーンが空中にあるときは、非キネマティック部分にかかる重力によってジョイントに下向きの負荷がかかるが、景品の上に非キネマティック部分が乗っかると、景品からの抗力で下方向の負荷が消失するはず...という目論見で実験してみました。 下図のような構造のクレーンを作って、 !クレーン クレーンに下記スクリプトをアタッチしました。 ``` using System.Collections; using System.Linq; using UnityEngine; public class Crane : MonoBehaviour { public Transform armTransform; // アームのTransform...アームはキネマティックとし、Transformを操作して移動させる public Joint LoadDetectorJoint; // アームと円盤(非キネマティック)を繋ぐジョイント...ここにかかる負荷を見てアームの衝突を検知する(今回はFixedJointを使用しました) public HingeJoint] ClawHinges; // クローと円盤を繋ぐヒンジ...Use Springをオンにし、適当なSpring(大きいほど握力が強まる)を設定しておく private bool playing; private float homePosition; private void Start() { this.homePosition = armTransform.position.y; // アームを上げるとき、この高さまで戻す } private void Update() { if (!this.playing && Input.GetKeyDown(KeyCode.Space)) { this.StartCoroutine(this.StartGame()); // スペースキー押下で一連のモーションを開始 } } private IEnumerator StartGame() { this.playing = true; Debug.Log("Start!"); yield return this.StartCoroutine(this.MoveClawsMotion(60.0f)); // クローを開く yield return new WaitForSeconds(1.0f); // 少し待機 yield return this.StartCoroutine(this.MoveDownMotion()); // アームを下げる yield return new WaitForSeconds(1.0f); // 少し待機 yield return this.StartCoroutine(this.MoveClawsMotion(0.0f)); // クローを閉じる yield return new WaitForSeconds(1.0f); // 少し待機 yield return this.StartCoroutine(this.MoveUpMotion()); // アームを上げる yield return new WaitForSeconds(1.0f); // 少し待機 yield return this.StartCoroutine(this.MoveClawsMotion(60.0f)); // クローを開く yield return new WaitForSeconds(1.0f); // 少し待機 yield return this.StartCoroutine(this.MoveClawsMotion(0.0f)); // クローを閉じる this.playing = false; } private IEnumerator MoveDownMotion() { const int loadMovingAverageLength = 30; // 負荷を積算するフレーム数(試しに「1」などとしてみると、アーム降下開始時の加速によって負荷が消失し、すぐにアームが止まってしまうと思います) const float loadThreshold = 0.0f; // アームを停止する負荷値 const float acceleration = 0.5f; // アームの下降加速度 const float velocityMax = 1.0f; // アームの最大下降速度 var loads = Enumerable.Repeat(this.LoadDetectorJoint.currentForce.y, loadMovingAverageLength).ToArray(); var loadSum = loads.Sum(); var loadIndex = 0; var velocity = 0.0f; while (true) { var previousLoad = loads[loadIndex]; var newLoad = this.LoadDetectorJoint.currentForce.y; // currentForceのY成分を負荷の指標とする loadSum += (newLoad - previousLoad); var averageLoad = loadSum / loadMovingAverageLength; // 過去loadMovingAverageLengthフレーム分の負荷を平均して、それで停止するべきか判定する Debug.LogFormat("Load:{0}", averageLoad); if (averageLoad >= loadThreshold) { Debug.Log("Stop!"); break; } velocity = Mathf.Max(velocity - acceleration * Time.deltaTime, -velocityMax); this.armTransform.Translate(0.0f, velocity * Time.deltaTime, 0.0f, Space.World); loadIndex = (loadIndex + 1) % loadMovingAverageLength; yield return null; } } private IEnumerator MoveUpMotion() { const float acceleration = 0.5f; // アームの上昇加速度 const float velocityMax = 1.0f; // アームの最大上昇速度 var velocity = 0.0f; while (this.armTransform.position.y < this.homePosition) { velocity = Mathf.Min(velocity + acceleration * Time.deltaTime, velocityMax); this.armTransform.Translate(0.0f, velocity * Time.deltaTime, 0.0f, Space.World); yield return null; } this.armTransform.position = new Vector3(0.0f, this.homePosition, 0.0f); } private IEnumerator MoveClawsMotion(float angle, float timeout = 3.0f) { const float angularSpeed = 30.0f; // 毎秒30°で開閉 var time = 0.0f; while (time < timeout) // 景品を掴もうとしているときなど、クローに負荷がかかっているといつまで経っても目標角に到達できない場合があるので、一定時間が経過したら強制的にモーションを終了 { var reached = true; var angleDeltaMax = angularSpeed * Time.deltaTime; foreach (var clawHinge in this.ClawHinges) { var spring = clawHinge.spring; spring.targetPosition = Mathf.MoveTowardsAngle(spring.targetPosition, angle, angleDeltaMax); clawHinge.spring = spring; reached &= Mathf.Approximately(spring.targetPosition, angle); } if (reached) { break; // すべてのクローが目標角に到達したらモーションを終了 } time += Time.deltaTime; yield return null; } } } ``` 円盤が景品に乗っかっている状態以外でも、アームの加速時など一時的に負荷がなくなる場合があるので、一定フレームの間の平均の負荷でアーム停止の判定をしています。 アームの下降に伴う負荷の変化は下図のようになりました(あくまでも一例で、クレーンの各部位や景品の質量設定などの要因でクレーンの挙動は変わってくると思います)。 ![負荷の変化 [追記] 補足として、今回はcurrentForceのyをそのまま使っていますが、ジョイントでRigidbody同士を接続した場合、currentForceは接続相手の座標系に従うみたいですので、今回のように重力の負荷を調べる場合は、適宜ワールド座標系に直す必要があるかもしれません。