Unityを触り始めた頃って、つい何でもかんでも Update() に書いてしまいがちですよね。
プレイヤーの移動、カメラの追従、武器の回転、アニメ再生、入力処理…全部ひとつのスクリプトに押し込んでしまうと、だんだん何をどこで書いたのか分からなくなってきます。
特に「武器の見た目の制御(どこを中心に回転させるか)」までプレイヤーのスクリプトに混ぜてしまうと、
- キャラごとに持ち手の位置が違うたびにコードを書き換える
- 武器を変えるたびにオフセット計算がぐちゃぐちゃになる
- アニメーターで肩の位置を変えたら、全部の計算がずれる
といった地獄が待っています。
そこでこの記事では、「WeaponPivot(武器持ち手)」コンポーネントを作って、
「親オブジェクトの中心」ではなく「肩の位置」など任意のポイントを中心に武器スプライトを回転させる方法を、コンポーネント指向でスッキリ分離して実装していきます。
【Unity】肩を軸に武器を振り回す!「WeaponPivot」コンポーネント
この WeaponPivot コンポーネントの役割はシンプルです。
- 「持ち手(肩・手首など)」の Transform を基準に
- マウスや入力方向に向けて武器を回転させる
- 2D/トップダウンなどでよくある「向いている方向に武器を構える」処理をひとまとめにする
プレイヤー本体のスクリプトからは、
「武器の見た目をどう回転させるか」という責務を切り離して、
WeaponPivot が肩を中心にクルクル回す役を担当するイメージですね。
フルコード:WeaponPivot.cs
using UnityEngine;
/// <summary>
/// 武器スプライトを「親の中心」ではなく、
/// 任意の「持ち手(肩・手首など)」を中心に回転させるコンポーネント。
///
/// - 2Dゲーム想定(Z軸回転)
/// - マウスカーソル方向、または任意のターゲット方向を向く
/// - 左右反転にも対応(キャラの向きに合わせて武器を反転)
///
/// 責務を「武器の見た目の回転」に限定しているので、
/// 入力処理や攻撃ロジックとは分離して使うことを想定しています。
/// </summary>
[ExecuteAlways] // エディタ上でも動作させて、調整しやすくする
public class WeaponPivot : MonoBehaviour
{
// --- 設定項目 ---
[Header("回転の基準となる持ち手(肩・手首など)")]
[Tooltip("このTransformを回転の中心として扱います。通常は肩や手首のボーン、または子オブジェクト。")]
[SerializeField] private Transform pivotPoint;
[Header("回転対象の武器オブジェクト")]
[Tooltip("回転させたい武器のTransform。未指定なら、このコンポーネントが付いているオブジェクト自身を使います。")]
[SerializeField] private Transform weaponTransform;
[Header("方向の決め方")]
[Tooltip("true: マウスカーソル方向を見る / false: 任意のターゲットTransform方向を見る")]
[SerializeField] private bool useMouseDirection = true;
[Tooltip("useMouseDirection = false のとき、ここに指定したTransform方向を向きます。")]
[SerializeField] private Transform lookTarget;
[Header("回転設定")]
[Tooltip("回転スピード。0なら即座に向きます。")]
[SerializeField] private float rotateSpeed = 720f;
[Tooltip("回転に加えるオフセット角度(度数法)。武器スプライトの向き調整用。")]
[SerializeField] private float angleOffset = 0f;
[Header("左右反転設定")]
[Tooltip("キャラクターの向きに合わせて武器を左右反転するかどうか。")]
[SerializeField] private bool autoFlipByCharacterFacing = true;
[Tooltip("キャラクターの左右反転を判定するTransform。通常はキャラ本体。")]
[SerializeField] private Transform characterRoot;
[Tooltip("右向きのときのローカルスケール。左右反転時にXだけ反転します。")]
[SerializeField] private Vector3 weaponLocalScaleRight = Vector3.one;
// 内部用:左向きのローカルスケール(Xを反転したもの)
private Vector3 weaponLocalScaleLeft;
// エディタ上で値が変わったときにも呼ばれる
private void OnValidate()
{
// weaponTransform が未設定なら自分自身を使う
if (weaponTransform == null)
{
weaponTransform = transform;
}
// characterRoot が未設定なら親階層から探す
if (characterRoot == null)
{
characterRoot = transform.root;
}
// 左向きスケールを計算
weaponLocalScaleLeft = new Vector3(
-weaponLocalScaleRight.x,
weaponLocalScaleRight.y,
weaponLocalScaleRight.z
);
}
private void Awake()
{
// 実行時にも念のため同じ初期化を行う
if (weaponTransform == null)
{
weaponTransform = transform;
}
if (characterRoot == null)
{
characterRoot = transform.root;
}
weaponLocalScaleLeft = new Vector3(
-weaponLocalScaleRight.x,
weaponLocalScaleRight.y,
weaponLocalScaleRight.z
);
}
private void LateUpdate()
{
// エディタ上で動かしたいので、play中/非play問わず実行
if (pivotPoint == null || weaponTransform == null)
{
return;
}
// 1. 目標方向ベクトルを取得
Vector3 targetDirection = GetTargetDirection();
if (targetDirection.sqrMagnitude < 0.0001f)
{
return; // ほぼゼロ方向なら何もしない
}
// 2D前提なのでZ軸回転を使う(XY平面上)
float targetAngle = Mathf.Atan2(targetDirection.y, targetDirection.x) * Mathf.Rad2Deg;
// 角度オフセットを加算(スプライトの向き調整用)
targetAngle += angleOffset;
// 2. 現在角度から目標角度へ補間回転
float currentAngle = weaponTransform.eulerAngles.z;
float newAngle;
if (rotateSpeed <= 0f)
{
// 即座に目標角度へ
newAngle = targetAngle;
}
else
{
// 一定スピードで補間
newAngle = Mathf.MoveTowardsAngle(
currentAngle,
targetAngle,
rotateSpeed * Time.deltaTime
);
}
// 3. pivotPoint を中心に武器を回転させる
ApplyRotationAroundPivot(newAngle);
// 4. キャラクターの向きに合わせて左右反転
if (autoFlipByCharacterFacing)
{
ApplyFlipByCharacterFacing();
}
}
/// <summary>
/// マウスまたはターゲットTransformから、pivotPoint から見た方向ベクトルを取得。
/// </summary>
private Vector3 GetTargetDirection()
{
if (useMouseDirection)
{
// 画面上のマウス位置をワールド座標に変換
Camera mainCam = Camera.main;
if (mainCam == null)
{
return Vector3.right; // カメラがなければとりあえず右向き
}
Vector3 mouseScreenPos = Input.mousePosition;
Vector3 mouseWorldPos = mainCam.ScreenToWorldPoint(mouseScreenPos);
// 2DなのでZをpivotPointと合わせる
mouseWorldPos.z = pivotPoint.position.z;
return (mouseWorldPos - pivotPoint.position).normalized;
}
else
{
if (lookTarget == null)
{
return Vector3.right;
}
Vector3 targetPos = lookTarget.position;
targetPos.z = pivotPoint.position.z;
return (targetPos - pivotPoint.position).normalized;
}
}
/// <summary>
/// 指定した角度で、pivotPoint を中心に weaponTransform を回転させる。
/// </summary>
private void ApplyRotationAroundPivot(float angleDeg)
{
// 現在のローカル位置を、pivotPoint基準のローカル座標として扱う
// 1. pivotPointの位置に武器を合わせる
Vector3 pivotPos = pivotPoint.position;
Vector3 weaponPos = weaponTransform.position;
Vector3 offset = weaponPos - pivotPos;
// 2. オフセットを回転させる
float rad = angleDeg * Mathf.Deg2Rad;
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
Vector3 rotatedOffset = new Vector3(
offset.x * cos - offset.y * sin,
offset.x * sin + offset.y * cos,
offset.z // 2DなのでZはそのまま
);
// 3. 回転後の位置を反映
weaponTransform.position = pivotPos + rotatedOffset;
// 4. 武器の見た目の回転も合わせる(Z軸回転)
Vector3 euler = weaponTransform.eulerAngles;
euler.z = angleDeg;
weaponTransform.eulerAngles = euler;
}
/// <summary>
/// キャラクターの向き(右向き or 左向き)に応じて武器のスケールを反転させる。
/// </summary>
private void ApplyFlipByCharacterFacing()
{
if (characterRoot == null || weaponTransform == null)
{
return;
}
// ここでは「右向き = localScale.x >= 0」とする簡易判定
bool isFacingRight = characterRoot.localScale.x >= 0f;
weaponTransform.localScale = isFacingRight
? weaponLocalScaleRight
: weaponLocalScaleLeft;
}
/// <summary>
/// 外部から「この方向を向いてほしい」と指示したいとき用のメソッド。
/// 例えばゲームパッドのスティック入力ベクトルを渡すなど。
/// </summary>
/// <param name="worldDirection">ワールド座標系での方向ベクトル</param>
public void SetDirection(Vector2 worldDirection)
{
if (pivotPoint == null || weaponTransform == null)
{
return;
}
if (worldDirection.sqrMagnitude < 0.0001f)
{
return;
}
// 直接角度を計算して即座に反映
float angle = Mathf.Atan2(worldDirection.y, worldDirection.x) * Mathf.Rad2Deg;
angle += angleOffset;
ApplyRotationAroundPivot(angle);
}
}
使い方の手順
ここでは 2D アクションゲームで「プレイヤーがマウス方向に銃を向ける」例をベースに説明します。
① シーン階層を用意する
- プレイヤー(Player)オブジェクトを作成
- その子に「肩」用の空オブジェクトを追加(例:
ShoulderPivot) - さらにその子か同階層に、武器スプライト用オブジェクトを配置(例:
Rifle)
Player
├─ Sprite (キャラ見た目)
├─ ShoulderPivot <-- 持ち手(pivotPoint)に指定
└─ Rifle <-- WeaponPivot を付ける武器
※ ShoulderPivot は、キャラの肩・手首あたりに位置を合わせておきましょう。
② WeaponPivot コンポーネントを追加
Rifleオブジェクトを選択Add ComponentからWeaponPivotを追加- インスペクタで以下を設定
- Pivot Point :
ShoulderPivotをドラッグ&ドロップ - Weapon Transform : 空欄なら自動で
Rifleが使われます - Use Mouse Direction : ON(マウス方向を向かせる)
- Rotate Speed : 720 くらいから調整
- Angle Offset : スプライトの向きに合わせて 90 / -90 など調整
- Auto Flip By Character Facing : ON(プレイヤーの向きで反転)
- Character Root :
Playerを指定 - Weapon Local Scale Right : 右向き時のスケール(通常は (1,1,1))
- Pivot Point :
③ プレイヤーの左右反転と連動させる
プレイヤーの移動スクリプト側で、向きに応じて Player の localScale.x を反転させておきます。
public class SimplePlayerMove : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
private void Update()
{
float inputX = Input.GetAxisRaw("Horizontal");
// 移動
Vector3 velocity = new Vector3(inputX, 0f, 0f) * moveSpeed;
transform.position += velocity * Time.deltaTime;
// 向きの反転(localScale.x で判定する想定)
if (inputX > 0.01f)
{
Vector3 scale = transform.localScale;
scale.x = Mathf.Abs(scale.x);
transform.localScale = scale;
}
else if (inputX < -0.01f)
{
Vector3 scale = transform.localScale;
scale.x = -Mathf.Abs(scale.x);
transform.localScale = scale;
}
}
}
これで WeaponPivot の Auto Flip By Character Facing が、
Player の localScale.x を見て自動的に武器の左右を反転してくれます。
④ 敵や動く床など別の例で使う
- 敵のタレット:
敵オブジェクトの子にTurretPivot(回転の中心)とGun(銃身)を作り、
WeaponPivotをGunに付けて、Use Mouse Direction = false、Look Target = プレイヤーにすると、
「常にプレイヤーを向く砲台」が簡単に作れます。 - 動く床の上の砲台:
動く床の子に敵タレットを置いても、WeaponPivotは「pivotPoint を中心に回転するだけ」なので、
床の移動ロジックとは完全に独立して動かせます。
メリットと応用
WeaponPivot を導入することで、
- プレイヤーのスクリプトが「移動・入力・攻撃ロジック」に集中できる
見た目の回転処理はWeaponPivotに丸投げできます。 - キャラごとに持ち手の位置が違っても、プレハブ差し替えだけで対応
「このキャラは肩が高い」「この敵は手首持ち」なども、
pivotPointに違うオブジェクトを指定するだけでOKです。 - レベルデザインが楽になる
砲台やギミックにWeaponPivotを付けておけば、
「どの方向を向くか」「どこを中心に回転させるか」をシーン上で視覚的に調整できます。 - アニメーションとの相性が良い
肩や手首のボーンをpivotPointにすれば、アニメで腕を振っても、
常に「その時点の手首位置」を中心に武器を回転できます。
コンポーネント単位で責務を分けておくと、
「武器の見た目の制御」だけを差し替えたり、別プロジェクトに持ち出したりしやすくなりますね。
改造案:攻撃ボタンを押したときだけ、スイング演出を入れる
例えば、WeaponPivot に「軽い振りかぶりモーション」を足したい場合、
こんな感じのメソッドを追加して、アニメーションの代わりに簡易スイングを表現するのもありです。
/// <summary>
/// 一瞬だけ武器を振りかぶるように角度をずらす簡易スイング。
/// 攻撃ボタンを押した瞬間などに外部から呼び出します。
/// </summary>
/// <param name="swingAngle">現在の向きに対して追加する角度(度数法)</param>
public void AddSwing(float swingAngle)
{
if (weaponTransform == null || pivotPoint == null)
{
return;
}
// 現在の角度を取得して、そこに swingAngle を足す
float currentAngle = weaponTransform.eulerAngles.z;
float targetAngle = currentAngle + swingAngle;
// 即座に反映(必要ならコルーチンで徐々に戻す処理を足す)
ApplyRotationAroundPivot(targetAngle);
}
攻撃処理側からは、
- ダメージ計算やヒット判定は「攻撃コンポーネント」
- 見た目の振りかぶりは「WeaponPivot.AddSwing」
といった具合に、さらに責務を分けていくと、
あとから「エフェクトを変えたい」「別の武器アニメを試したい」となっても柔軟に対応できます。
小さくて役割のはっきりしたコンポーネントを積み重ねて、
巨大な God クラスから卒業していきましょう。
