はじめに
前回の記事(その2)では、ConfigurableJointやHingeJointを用いて、以下の制御を実現しました。
- 所定の移動速度や角速度でアームのパーツを動かす
- 所定の力で物を2本指で挟む
しかし、Joint系(Rigidbodyベース)の構成では、連結数が増えるにつれて挙動が不安定になったり、指がワークに衝突した際にパーツが外れてしまうといった課題がありました。
今回は、ロボティクス分野で推奨される ArticulationBody を使用して同様の制御を実現できるのか、またJoint系と比較してどのようなメリットがあるのかを検証します。
Joint系 vs ArticulationBody
「Articulation(アーティキュレーション)」は、英語で「関節」を意味します。UnityのArticulationBodyと従来のJoint(Rigidbody)には、設計思想に大きな違いがあります。
| 特徴 | Joint系 (Rigidbody) | ArticulationBody |
|---|---|---|
| 構造 | Rigidbody同士をJointで繋ぐ | 親子階層で関節を定義する |
| 計算方式 | 座標の反復計算(誤差が出やすい) | 順動力学(FEA形式)による高精度計算 |
| 安定性 | 連結が増えると沈み込みや分離が発生 | 多関節でも非常に安定しており、誤差が少ない |
| 自由度 | 非常に柔軟(階層を無視できる) | 親子階層構造に縛られる |
これまでのJoint系はRigidbodyをJointで繋いでいたため、Blender側の階層構造を意識しなくても動作しました。しかし、ArticulationBodyは明確な親子階層を必要とします。
そのため、今回はBlenderに戻り、クレーンゲームのアーム部分に適切な親子階層を持たせる修正を行いました。
ArticulationBodyの設定手順
Joint系をArticulationBodyへ入れ替えていきます。
1. ルート(Pillars)の設定
可動部分の起点となる Pillars(柱)に ArticulationBody をアタッチします。
- Mass: 20kg
- Immovable: チェックを入れる(空中に固定するため)
Pillars
2. 直進移動(Slider / Arm / Hand)の設定
次に、子階層の Slider, Arm, Hand にそれぞれ ArticulationBody をアタッチします。
- Articulation Joint Type: Prismatic(直線移動用)を選択
- Motion: Limited に設定
- Drive Type: Target(スクリプトから位置制御を行うため)
- X/Y/Z Drive (Lower/Upper Limit): 可動範囲を指定
Slider
Arm
Hand
3. 回転動作(FingerL / FingerR)の設定
最末端の Finger(指)パーツの設定です。
- Articulation Joint Type: Revolute(回転移動用)を選択
- Drive Type: Target
- Motion: Limited
- Limits: 指が開閉する範囲(角度)を指定
FingerL
FingerR
スクリプトによる制御と安定化
ArticulationBodyの制御は、ArticulationDrive の Target を更新することで行います。
感圧センサー(力覚センサー)の安定化テクニック
今回の実装で工夫した点は、指のターゲット角度の指定方法です。 Lower Limit と Upper Limit を完全に同じ値に設定すると、感圧センサーが検知する力が不安定になる現象が見られました。
そこで、Lower Limit を Upper Limit よりもわずかに小さい値(0.01程度)に設定する「遊び(Gap)」を設けることで、センサー値が非常に安定するようになりました。
実装コード(ArmControllerArticulated.cs)
// 指のドライブ設定を更新する部分
if (_fingerL != null)
{
var drive = _fingerL.xDrive;
// わずかなGap(minMaxGap=0.01)を設けて安定化させる
drive.lowerLimit = _targetGripAngle - minMaxGap;
drive.upperLimit = _targetGripAngle;
_fingerL.xDrive = drive;
}
全ソースコードを表示
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
public class ArmControllerArticulated : MonoBehaviour
{
[SerializeField] GameObject slider;
[SerializeField] GameObject arm;
[SerializeField] GameObject hand;
[SerializeField] GameObject fingerL;
[SerializeField] GameObject fingerR;
[SerializeField] float speed = 0.1f;
[SerializeField] float gripSpeed = 50.0f;
[SerializeField] float targetForce = 1.0f;
[SerializeField] private float minMaxGap = 0.01f;
[SerializeField] private float slowDownDenominator = 200.0f;
[SerializeField] private float forceReductionDenominator = 500.0f;
private ArticulationBody _fingerL, _fingerR, _armJoint, _handJoint, _sliderJoint;
private float _targetGripAngle = 0.0f;
private float _lastGripAngle = 0.0f;
private float _fingerClosedAngle, _fingerOpenAngle;
private PressureSensor _pressureSensorL, _pressureSensorR;
[SerializeField] TMP_Text textInfo;
[SerializeField] TMP_Text textTargetForce;
[SerializeField] Slider sliderTargetForce;
void Start()
{
_sliderJoint = slider?.GetComponent<ArticulationBody>();
_armJoint = arm?.GetComponent<ArticulationBody>();
_handJoint = hand?.GetComponent<ArticulationBody>();
SetupFinger(fingerL, out _fingerL, out _pressureSensorL);
SetupFinger(fingerR, out _fingerR, out _pressureSensorR);
if (_fingerL != null)
{
_fingerClosedAngle = _fingerL.xDrive.upperLimit;
_fingerOpenAngle = _fingerL.xDrive.lowerLimit;
}
sliderTargetForce.onValueChanged.AddListener((value) => targetForce = value);
}
private void SetupFinger(GameObject finger, out ArticulationBody body, out PressureSensor sensor)
{
body = finger?.GetComponent<ArticulationBody>();
sensor = finger?.GetComponent<PressureSensor>();
}
void Update()
{
// UI更新処理(省略)
}
void FixedUpdate()
{
if (Keyboard.current == null) return;
// 各ジョイントの移動制御
UpdateDriveTarget(_sliderJoint, Key.A, Key.D, 'x');
UpdateDriveTarget(_armJoint, Key.W, Key.S, 'y');
UpdateDriveTarget(_handJoint, Key.DownArrow, Key.UpArrow, 'z');
// グリップ制御
float gripSpeedAdjustment = _pressureSensorL.IsColliding && _pressureSensorR.IsColliding ? gripSpeed / slowDownDenominator : gripSpeed;
if (Keyboard.current.leftArrowKey.isPressed)
_targetGripAngle = Mathf.MoveTowards(_targetGripAngle, _fingerClosedAngle, gripSpeedAdjustment * Time.deltaTime);
else if (Keyboard.current.rightArrowKey.isPressed)
_targetGripAngle = Mathf.MoveTowards(_targetGripAngle, _fingerOpenAngle, gripSpeed * Time.deltaTime);
// フォースフィードバック: 目標の力を超えたら閉じる動きを抑制
if (_targetGripAngle - _lastGripAngle > 0 && _pressureSensorL.LastForce > targetForce && _pressureSensorR.LastForce > targetForce)
{
_targetGripAngle = _lastGripAngle - (_pressureSensorL.LastForce + _pressureSensorR.LastForce - 2 * targetForce) / forceReductionDenominator;
}
ApplyGripAngle(_fingerL);
ApplyGripAngle(_fingerR);
_lastGripAngle = _targetGripAngle;
}
private void ApplyGripAngle(ArticulationBody finger)
{
if (finger == null) return;
var drive = finger.xDrive;
drive.lowerLimit = _targetGripAngle - minMaxGap;
drive.upperLimit = _targetGripAngle;
finger.xDrive = drive;
}
private void UpdateDriveTarget(ArticulationBody joint, Key negKey, Key posKey, char axis)
{
if (joint == null) return;
ArticulationDrive drive = axis switch { 'y' => joint.yDrive, 'z' => joint.zDrive, _ => joint.xDrive };
float value = drive.target;
if (Keyboard.current[negKey].isPressed && value > drive.lowerLimit) value -= speed * Time.deltaTime;
else if (Keyboard.current[posKey].isPressed && value < drive.upperLimit) value += speed * Time.deltaTime;
drive.target = value;
if (axis == 'y') joint.yDrive = drive; else if (axis == 'z') joint.zDrive = drive; else joint.xDrive = drive;
}
}
動作確認と結論
実際に動作させた結果、以下のメリットが確認できました。
- ジッターの解消: ConfigurableJointで見られた指先の震えが消失し、安定した静止・動作が可能になりました。
- パーツの分離を防止: 激しい衝突が起きても、親子階層構造により「指が手から外れる」といった物理的な破綻が起きなくなりました。
- 高精度な力制御: 微細な「遊び(Gap)」の設定により、感圧センサーの数値が安定し、意図した通りの強さでワークを保持できるようになりました。
よって、今回のクレーンゲーム制作においては、ArticulationBodyを採用すべきであるという結論に至りました。
次のステップ
本記事はこれにて完了します。ここで得た知見は、以下の記事の続編で活用していきます。
本記事で紹介した制作物
以下のGitHubプロジェクトで公開します。







