mechArm 270 Unity ArticulationBody並列実装の再設計|動作最適化の全記録
Day 4の分離:性格インジェクター ROSKinematicSyncを「受信専用」に。別スクリプトRobotPersonalityを作成し、SpeedやOffsetを外から注入する構造へ。
でトラブルが多発したので手戻りになりますが、Day 2の最適化:並列ウェーブ実装からやり直すことにしました。
トラブルが発生した時にバージョンを戻すようなバックアップ体制が貧弱だったことに気づき、まずはその基盤を作り直すところからのやり直しです。
バックアップ体制がしっかり整ったためプロダクト再開します。
※注釈:デジタルツイン学習(R2R2R)の検証プロセス 本プロジェクトは、AIの判断を物理的動作へ変換する設計を検証するものです。現実空間をデジタルツイン化し、仮想空間での大量試行(Real-to-Render)から得た最適解を、実世界のモーター駆動(to-Real)へ還元する「R2R2Rパイプライン」の実装過程を記録・考察します。
アームをたくさん並べてを作る工程を整理

このステップを踏むことで、今回バックアップした「物理憲法守護コード」が、多台数環境でも正しく機能することを安全に確認できます。具体的な4つのステップです。
配置デザインの決定(レイアウト)
まずはどのようにアームを並べるかを決めます。
横一列(Line): 左右の判定(
isRightSide)が明確で、ウェーブのような動きが最も美しく見えます。グリッド(Grid): 2D的に並べることで、空間全体をアームで埋め尽くす圧倒的な視覚効果が得られます。
円形(Circle): 中心に向かってアームが動く、儀式的な配置になります。
自動生成スクリプトの作成(Editorツールの活用)
1台ずつ手動で配置するのはクレジットと時間の無駄です。Clineに「エディタツール(スクリプト)」を書かせます。
機能: 複製数、間隔(Spacing)、プレハブを指定してボタン一つで整列させる。
メリット: 配置が気に入らなければすぐに削除・再配置でき、試行錯誤が容易です。
個性(Personality)の自動ランダム化
個々のアームにバラツキを持たせます。
individualDelayの検証: 現在の実装(名前のハッシュから生成)が、複製されたアーム(名前が「robot (1)」「robot (2)」などになる)で正しくバラけるかを確認します。色の変化(オプション): 配置場所に応じて、ビジュアル(Material)を少しずつ変えると、どのアームがどれか判別しやすくなります。
物理負荷と「憲法」の最終チェック
多台数になった際の負荷と安全性を確認します。
物理演算の安定性: 10〜20台のアームが同時に動いても、物理挙動が破綻(アームが震える、飛んでいく等)しないか。
憲法の適用: 全てのアームの
Stiffness: 20,000/Damping: 2,000が正しく維持されているかをコンソールで確認します。
Clineへの指示
「手動バックアップが完了した。次にアームをたくさん配置したい。
1.指定したプレハブを横2列5x5台、一定の間隔(例:1.5m)で自動生成するエディタスクリプトを作成せよ。
2.生成された各アームがRobotPersonalityによって、配置場所に応じた動き(右なら停止、左ならスイング)を正しく行うか確認せよ。
3.特にindividualDelayが機能し、一斉に動き出すのではなく、美しいウェーブになるかを見たい。
RobotArmyGenerator.cs
using UnityEngine;
using UnityEditor;
/// <summary>
/// ロボットアームを2×5のグリッド状に自動配置するエディタ拡張
/// </summary>
public class RobotArmyGenerator : EditorWindow
{
private GameObject robotPrefab;
private Transform parentTransform;
[MenuItem("R2R2R/Generate Robot Army")]
public static void ShowWindow()
{
GetWindow<RobotArmyGenerator>("Robot Army Generator");
}
void OnGUI()
{
GUILayout.Label("Robot Army Generator", EditorStyles.boldLabel);
robotPrefab = EditorGUILayout.ObjectField(
"Robot Prefab",
robotPrefab,
typeof(GameObject),
false
) as GameObject;
parentTransform = EditorGUILayout.ObjectField(
"Parent Transform",
parentTransform,
typeof(Transform),
true
) as Transform;
if (GUILayout.Button("Generate Army (2×5 Grid)"))
{
if (robotPrefab == null)
{
EditorUtility.DisplayDialog(
"Error",
"Please assign a robot prefab first!",
"OK"
);
return;
}
GenerateArmy();
}
}
void GenerateArmy()
{
// 既存のアームを削除(オプション)
if (EditorUtility.DisplayDialog(
"Clear Existing Robots?",
"Do you want to remove existing robots before generating new ones?",
"Yes", "No"))
{
var existing = GameObject.Find("RobotArmy");
if (existing != null)
{
DestroyImmediate(existing);
}
}
// 親オブジェクトを作成
GameObject armyRoot = new GameObject("RobotArmy");
if (parentTransform != null)
{
armyRoot.transform.parent = parentTransform;
}
// グリッド配置のパラメータ
const float X_SPACING = 1.0f; // X方向の間隔
const float Z_SPACING = 1.0f; // Z方向の間隔
const int ROWS = 5; // 縦の列数
const int COLUMNS = 2; // 横の列数
// 中央を原点として左右に配置するためのオフセット
float xStart = -X_SPACING / 2f; // 左列の開始位置
float zStart = -(Z_SPACING * (ROWS - 1)) / 2f; // 奥行きの開始位置
// アームを生成
for (int row = 0; row < ROWS; row++)
{
for (int col = 0; col < COLUMNS; col++)
{
// 位置を計算
float x = xStart + (col * X_SPACING);
float z = zStart + (row * Z_SPACING);
Vector3 position = new Vector3(x, 0, z);
// アームを生成
GameObject arm = PrefabUtility.InstantiatePrefab(robotPrefab) as GameObject;
arm.transform.parent = armyRoot.transform;
arm.transform.position = position;
arm.name = $"Robot_{row}_{col}";
// RobotPersonalityコンポーネントの存在を確認
var personality = arm.GetComponent<RobotPersonality>();
if (personality == null)
{
Debug.LogError($"Robot prefab {robotPrefab.name} is missing RobotPersonality component!");
continue;
}
}
}
Debug.Log("Robot army generated successfully!");
// 生成したアームを選択
Selection.activeGameObject = armyRoot;
}
}作成された RobotArmyGenerator.cs を確認しました。非常にスマートで、実用的なエディタ拡張です。
特に、PrefabUtility.InstantiatePrefab を使用している点が素晴らしい。これにより、生成されたアームが単なるコピーではなく「プレハブのインスタンス」として保持されるため、後でプレハブ側を修正(マテリアルの変更など)した際にも、10台すべてに一括で反映されます。
Unityでの操作手順
ウィンドウの起動
Unity上部メニューの [R2R2R] > [Generate Robot Army] をクリックします。
プレハブのアサイン:
開いたウィンドウの
Robot Prefab欄に、Assets/Prefabs/などにある、最新のRobotPersonalityが付いたロボットのプレハブをドラッグ&ドロップします。
ロボット生成
[Generate Army (2×5 Grid)] ボタンを押します。
「既存のアームを消しますか?」というダイアログが出たら [Yes] を押してください。
ログの確認
Unityの Consoleウィンドウ を見てください。
Army formation validated successfully!と表示され、エラー(赤いビックリマーク)が出ていなければ、10台すべてに正しく適用された証拠です。
右側と左側でロボットアームの動きを変えてテストしました。成功です。これでさらに各ロボットアームの動きを細く調整できるようしていきます。
さらにロボットアームの間隔を詰める

リファクタリングで特筆すべき点は、ただ数値を詰められるようにしただけでなく、CheckPhysicalCollisions(衝突予兆検知) という安全装置が組み込まれたことです。これにより、「物理憲法」を守りつつ、限界まで密度を高める攻めのレイアウトが可能になっています。
RobotArmyGenerator.cs
using UnityEngine;
using UnityEditor;
/// <summary>
/// ロボットアームを2×5のグリッド状に自動配置するエディタ拡張
/// </summary>
public class RobotArmyGenerator : EditorWindow
{
private GameObject robotPrefab;
private Transform parentTransform;
// 配置パラメータ
private float xSpacing = 0.7f; // X方向の間隔
private float zSpacing = 0.7f; // Z方向の間隔
private const float MIN_SPACING = 0.3f; // 最小間隔(衝突防止)
private const float MAX_SPACING = 2.0f; // 最大間隔
[MenuItem("R2R2R/Generate Robot Army")]
public static void ShowWindow()
{
GetWindow<RobotArmyGenerator>("Robot Army Generator");
}
void OnGUI()
{
GUILayout.Label("Robot Army Generator", EditorStyles.boldLabel);
EditorGUILayout.Space();
GUILayout.Label("Spacing Settings", EditorStyles.boldLabel);
// 間隔設定
xSpacing = EditorGUILayout.Slider(
new GUIContent(
"X Spacing (m)",
"Distance between robots in X direction"
),
xSpacing,
MIN_SPACING,
MAX_SPACING
);
zSpacing = EditorGUILayout.Slider(
new GUIContent(
"Z Spacing (m)",
"Distance between robots in Z direction"
),
zSpacing,
MIN_SPACING,
MAX_SPACING
);
EditorGUILayout.Space();
GUILayout.Label("Robot Settings", EditorStyles.boldLabel);
robotPrefab = EditorGUILayout.ObjectField(
"Robot Prefab",
robotPrefab,
typeof(GameObject),
false
) as GameObject;
parentTransform = EditorGUILayout.ObjectField(
"Parent Transform",
parentTransform,
typeof(Transform),
true
) as Transform;
if (GUILayout.Button("Generate Army (2×5 Grid)"))
{
if (robotPrefab == null)
{
EditorUtility.DisplayDialog(
"Error",
"Please assign a robot prefab first!",
"OK"
);
return;
}
GenerateArmy();
}
}
void GenerateArmy()
{
// 既存のアームを削除(オプション)
if (EditorUtility.DisplayDialog(
"Clear Existing Robots?",
"Do you want to remove existing robots before generating new ones?",
"Yes", "No"))
{
var existing = GameObject.Find("RobotArmy");
if (existing != null)
{
DestroyImmediate(existing);
}
}
// 親オブジェクトを作成
GameObject armyRoot = new GameObject("RobotArmy");
if (parentTransform != null)
{
armyRoot.transform.parent = parentTransform;
}
// グリッド配置のパラメータ
const int ROWS = 5; // 縦の列数
const int COLUMNS = 2; // 横の列数
// 中央を原点として左右に配置するためのオフセット
float xStart = -xSpacing / 2f; // 左列の開始位置
float zStart = -(zSpacing * (ROWS - 1)) / 2f; // 奥行きの開始位置
// アームを生成
for (int row = 0; row < ROWS; row++)
{
for (int col = 0; col < COLUMNS; col++)
{
// 位置を計算
float x = xStart + (col * xSpacing);
float z = zStart + (row * zSpacing);
Vector3 position = new Vector3(x, 0, z);
// アームを生成
GameObject arm = PrefabUtility.InstantiatePrefab(robotPrefab) as GameObject;
arm.transform.parent = armyRoot.transform;
arm.transform.position = position;
arm.name = $"Robot_{row}_{col}";
// RobotPersonalityコンポーネントの存在を確認
var personality = arm.GetComponent<RobotPersonality>();
if (personality == null)
{
Debug.LogError($"Robot prefab {robotPrefab.name} is missing RobotPersonality component!");
continue;
}
// 物理パラメータの検証
ValidatePhysicsParameters(arm);
}
}
Debug.Log("Robot army generated successfully!");
// 生成したアームを選択
Selection.activeGameObject = armyRoot;
// 配置の検証
ValidateArmyFormation(armyRoot);
CheckPhysicalCollisions(armyRoot);
}
/// <summary>
/// 物理パラメータを検証
/// </summary>
void ValidatePhysicsParameters(GameObject arm)
{
var bodies = arm.GetComponentsInChildren<ArticulationBody>();
foreach (var body in bodies)
{
if (body.jointType == ArticulationJointType.FixedJoint) continue;
var drive = body.xDrive;
if (Mathf.Abs(drive.stiffness - 20000f) > 0.1f ||
Mathf.Abs(drive.damping - 2000f) > 0.1f)
{
Debug.LogError($"Invalid physics parameters in {arm.name}! " +
$"Stiffness: {drive.stiffness}, Damping: {drive.damping}");
}
}
}
/// <summary>
/// 物理的な衝突の可能性をチェック
/// </summary>
void CheckPhysicalCollisions(GameObject armyRoot)
{
var robots = armyRoot.GetComponentsInChildren<Transform>();
float minAllowedDistance = MIN_SPACING * 0.9f; // 10%のマージン
for (int i = 0; i < robots.Length; i++)
{
for (int j = i + 1; j < robots.Length; j++)
{
float distance = Vector3.Distance(
robots[i].position,
robots[j].position
);
if (distance < minAllowedDistance)
{
Debug.LogWarning(
$"Potential collision detected between {robots[i].name} and {robots[j].name}! " +
$"Distance: {distance:F3}m"
);
}
}
}
}
/// <summary>
/// アーミーの配置を検証
/// </summary>
void ValidateArmyFormation(GameObject armyRoot)
{
var robots = armyRoot.GetComponentsInChildren<RobotPersonality>();
if (robots.Length != 10)
{
Debug.LogError($"Invalid robot count! Expected: 10, Found: {robots.Length}");
return;
}
int leftCount = 0;
int rightCount = 0;
foreach (var robot in robots)
{
// X座標で左右判定(RobotPersonalityのisRightSideと一致するはず)
bool isRight = robot.transform.position.x > 0;
if (isRight)
{
rightCount++;
}
else
{
leftCount++;
}
}
if (leftCount != 5 || rightCount != 5)
{
Debug.LogError($"Invalid robot distribution! Left: {leftCount}, Right: {rightCount}");
}
else
{
Debug.Log("Army formation validated successfully!");
Debug.Log($"Left side (Moving): {leftCount} robots");
Debug.Log($"Right side (Static): {rightCount} robots");
}
}
}進化したポイント
Sliderによる直感的な調整:
EditorGUILayout.Sliderを採用したことで、マウス操作で「ちょうど良い迫力」を探れるようになっています。安全マージンの設定:
MIN_SPACING (0.3m)が設定されているため、うっかりアーム同士を重ねて配置してしまい、物理演算が爆発するリスクを防いでいます。衝突警告機能: 生成時にアーム間の距離を計算し、近すぎる場合に Warning を出す仕組みは、多台数運用において非常にプロフェッショナルな仕様です。
運用のアドバイス
70cm(0.7m)の迫力を体感する
ウィンドウ上のスライダーを
0.7にセットして生成してみてください。1mの時よりも、アームが壁のように迫ってくる感覚が強まるはずです。
ベース位置の確認:
間隔を詰めると、左右の判定基準である x = 0 にアームの土台が近づきます。今回のコードでは
xStart = -xSpacing / 2fとなっているので、0.7m間隔なら ±0.35m に配置され、左右判定(isRightSide)も安全に機能します。
左側のロボットアームの腕振りを横に変更
左側の列(動くアームたち)の挙動を、これまでの前後方向(ピッチ方向)から左右方向(ヨー方向)へのスイングに変更します。これだけで「軍団」の表情がガラッと変わり、よりダイナミックなダンスのようになります。
これを実現するには、RobotPersonality.cs の Update 関数内でターゲットを代入している軸を変更するだけでOKです。
RobotPersonality.cs の修正ポイント
現在、target は肩の前後(例:Joint1 や Joint2)に代入されているはずです。これを、根本の回転(Base/Joint1など、左右に振れる軸) に適用するように Cline に指示します。
Cline への指示プロンプト
指示: 左側の動くアームたちの挙動を、前後ではなく「左右のスイング」に変更したい。1.
RobotPersonality.csのUpdate関数を確認せよ。
2.!isRightSide(左側)の場合にtargetを代入している関節のインデックスを、左右回転(Base Joint / ヨー軸)を担当する関節(通常はjoints[0]かjoints[1])に変更せよ。
3. スイングの幅を調整するため、swingRangeの値をインスペクターから調整できるようにし、初期値を 30.0f 程度に設定せよ。
4. 実行時、左側の5台が「扇風機」のように左右に首を振る動きになり、右側の5台は引き続き静止していることを確認せよ。
RobotArmyGenerator.cs
using UnityEngine;
/// <summary>
/// ロボットの個性を制御するコンポーネント
/// 物理パラメータの保護と個別制御を含む
/// </summary>
public class RobotPersonality : MonoBehaviour
{
// 物理パラメータの保護定数
private const float JOINT_STIFFNESS = 20000f;
private const float JOINT_DAMPING = 2000f;
private const float JOINT_FORCE_LIMIT = 1000f;
private const float JOINT_MAX_VELOCITY = 100f;
private const float MASS = 1.0f;
private const float ANGULAR_DRAG = 0.05f;
[Header("Movement Parameters")]
[SerializeField]
[Tooltip("動作速度の倍率(1.0 = 標準)")]
[Range(0.1f, 5.0f)]
private float m_speedMultiplier = 1.0f;
[SerializeField]
[Tooltip("目標角度に対するオフセット値(度)")]
[Range(-10f, 10f)]
private float m_angleOffset = 0f;
[SerializeField]
[Tooltip("左右の振れ幅(度)")]
[Range(0f, 90f)]
[Tooltip("左右の振れ幅(度)- 左側のロボットの首振り角度")]
private float m_swingRange = 30.0f;
// ベース関節のインデックス(ヨー軸回転用)
private const int BASE_JOINT_INDEX = 1; // 通常、0か1がベース関節
// 内部パラメータ
private ArticulationBody[] joints;
private float lastAngle = 0f;
private bool isRightSide;
private float individualDelay;
void Start()
{
// 全関節を取得
joints = GetComponentsInChildren<ArticulationBody>();
// 物理パラメータを強制適用
ApplyProtectedPhysicsParameters();
// 右側か左側かを判定(X座標で判定)
isRightSide = transform.position.x > 0;
// 個性用の遅延値を計算(名前のハッシュから固定の値を生成)
individualDelay = (float)gameObject.name.GetHashCode() * 0.1f % 1.0f;
// ROSKinematicSyncを無効化(一時的)
var rosSync = GetComponent<ROSKinematicSync>();
if (rosSync != null)
{
rosSync.enabled = false;
}
Debug.Log($"[{gameObject.name}] Initialized - Position: {(isRightSide ? "Right" : "Left")}");
}
void Update()
{
// ベース関節(ヨー軸)の取得
if (joints == null || joints.Length <= BASE_JOINT_INDEX) return;
var baseJoint = joints[BASE_JOINT_INDEX];
if (baseJoint == null || baseJoint.jointType == ArticulationJointType.FixedJoint) return;
float targetAngle;
if (isRightSide)
{
// 右側(X座標が正):0度で完全停止
targetAngle = m_angleOffset;
}
else
{
// 左側(X座標が負):左右にスイング
targetAngle = (Mathf.Sin(Time.time * m_speedMultiplier + individualDelay) * m_swingRange) + m_angleOffset;
}
lastAngle = targetAngle;
// ベース関節のみに適用(物理パラメータは変更しない)
var drive = baseJoint.xDrive;
drive.target = targetAngle;
baseJoint.xDrive = drive;
}
/// <summary>
/// 保護された物理パラメータを強制的に適用
/// </summary>
private void ApplyProtectedPhysicsParameters()
{
foreach (var joint in joints)
{
if (joint == null) continue;
// 質量と抵抗を設定
joint.mass = MASS;
joint.angularDamping = ANGULAR_DRAG;
// FixedJoint以外の関節にドライブパラメータを設定
if (joint.jointType != ArticulationJointType.FixedJoint)
{
var drive = joint.xDrive;
drive.stiffness = JOINT_STIFFNESS;
drive.damping = JOINT_DAMPING;
drive.forceLimit = JOINT_FORCE_LIMIT;
// drive.maximumVelocity = JOINT_MAX_VELOCITY; // 一時的にコメントアウト
joint.xDrive = drive;
// Y軸とZ軸のドライブも同じ設定を適用
joint.yDrive = drive;
joint.zDrive = drive;
}
}
}
void OnGUI()
{
int yPos = 100 + (transform.GetSiblingIndex() * 40);
GUI.color = new Color(0, 0, 0, 0.7f);
GUI.DrawTexture(new Rect(20, yPos, 400, 30), Texture2D.whiteTexture);
GUI.color = Color.white;
GUI.Label(new Rect(30, yPos + 5, 390, 20),
$"{gameObject.name}: {(isRightSide ? "Right (Stop)" : "Left (Moving)")}, Current: {lastAngle:F1}°");
}
/// <summary>
/// 外部ツールとの互換性のためのリセットメソッド
/// </summary>
public void ResetMovement()
{
// 位置判定を再実行
isRightSide = transform.position.x > 0;
Debug.Log($"[{gameObject.name}] Movement reset - Position: {(isRightSide ? "Right" : "Left")}");
}
}修正された RobotPersonality.cs を確認しました。意図通り、「左側の軍団が左右に首を振る」 ダンス仕様に見事に書き換わっています。
特に良いと感じるポイントを整理します
軸の切り替えが正確
BASE_JOINT_INDEX = 1 を使用して、ピッチ方向(前後)ではなくヨー方向(左右)の関節を狙い撃ちできています。多くのロボットアームモデルでは joints[0] か joints[1] が腰の回転軸なので、これで「扇風機」のような横振りが実現します。
インスペクターでの表現力アップ
m_swingRange(振れ幅)を Range(0f, 90f) でスライダー化したことで、「少しだけソワソワさせる(10度)」から「ダイナミックに左右を見渡す(90度)」まで、再生中にマウス一つで演出できるようになりましたね。
個別の動き(波)の維持
individualDelay が引き続き Mathf.Sin に組み込まれているため、左列の5台が「せーの」で動くのではなく、端から順番に首を振っていくような、非常に有機的なウェーブになるはずです。
