0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unity物理エンジンで挑む!産業用ロボット風クレーンゲームの制作記録 〜 その3:ArticulationBody編

Posted at

はじめに

前回の記事(その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に戻り、クレーンゲームのアーム部分に適切な親子階層を持たせる修正を行いました。

Screenshot 2025-12-29 at 9.24.07.jpg

ArticulationBodyの設定手順

Joint系をArticulationBodyへ入れ替えていきます。

1. ルート(Pillars)の設定

可動部分の起点となる Pillars(柱)に ArticulationBody をアタッチします。

  • Mass: 20kg
  • Immovable: チェックを入れる(空中に固定するため)

Pillars

Screenshot 2025-12-29 at 9.53.09.jpg

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

Screenshot 2025-12-29 at 9.53.17.jpg

Arm

Screenshot 2025-12-29 at 9.53.25.jpg

Hand

Screenshot 2025-12-29 at 9.53.41.jpg

3. 回転動作(FingerL / FingerR)の設定

最末端の Finger(指)パーツの設定です。

  • Articulation Joint Type: Revolute(回転移動用)を選択
  • Drive Type: Target
  • Motion: Limited
  • Limits: 指が開閉する範囲(角度)を指定

FingerL

Screenshot 2025-12-29 at 10.17.33.jpg

FingerR

Screenshot 2025-12-29 at 10.17.41.jpg

スクリプトによる制御と安定化

ArticulationBodyの制御は、ArticulationDrive の Target を更新することで行います。

感圧センサー(力覚センサー)の安定化テクニック

今回の実装で工夫した点は、指のターゲット角度の指定方法です。 Lower Limit と Upper Limit を完全に同じ値に設定すると、感圧センサーが検知する力が不安定になる現象が見られました。

そこで、Lower Limit を Upper Limit よりもわずかに小さい値(0.01程度)に設定する「遊び(Gap)」を設けることで、センサー値が非常に安定するようになりました。

Bones of the robot (17).jpg

実装コード(ArmControllerArticulated.cs)

// 指のドライブ設定を更新する部分
if (_fingerL != null)
{
    var drive = _fingerL.xDrive;
    // わずかなGap(minMaxGap=0.01)を設けて安定化させる
    drive.lowerLimit = _targetGripAngle - minMaxGap;
    drive.upperLimit = _targetGripAngle;
    _fingerL.xDrive = drive;
}
全ソースコードを表示
ArmControllerArticulated.cs
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;
    }
}

動作確認と結論

実際に動作させた結果、以下のメリットが確認できました。

  1. ジッターの解消: ConfigurableJointで見られた指先の震えが消失し、安定した静止・動作が可能になりました。
  2. パーツの分離を防止: 激しい衝突が起きても、親子階層構造により「指が手から外れる」といった物理的な破綻が起きなくなりました。
  3. 高精度な力制御: 微細な「遊び(Gap)」の設定により、感圧センサーの数値が安定し、意図した通りの強さでワークを保持できるようになりました。

よって、今回のクレーンゲーム制作においては、ArticulationBodyを採用すべきであるという結論に至りました。

次のステップ

本記事はこれにて完了します。ここで得た知見は、以下の記事の続編で活用していきます。

本記事で紹介した制作物

以下のGitHubプロジェクトで公開します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?