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のRigidbodyで無重力空間で噴射移動するアクションゲームを実装する

Last updated at Posted at 2025-12-21

この記事は
Unity Advent Calendar 2025 20日目の記事です。

Advent Calendar は久々に参加している最中のミズサワキヌコです。
普段はUnityを使って個人でゲーム制作もしています。よろしくお願いします。

※本記事は Chat GPT から得られた情報、構成案、本文を精査、リライトして制作しています

はじめに

この記事でやること

無重力っぽい空間で、Rigidbodyのプレイヤーを「噴射」で動かすミニ実装のメモです。
今回はゲームパッドでR2/RTを押した瞬間だけ速度が乗る
(=一回だけ力を加える)形にしています。

「ずっと押してる間加速」もできるのですが、
今回はミニゲーム用途で「押した瞬間だけ」という仕様にしたかったので、
そちらに寄せました。

完成イメージ

Gif.gif

今回作るゲームの操作はゲームパッドで行う想定です。
左スティックと右スティックそれぞれの向きを取得し、
エアーを噴射する方向を決めます。

対応するボタン(R2/RT)を押したら押した瞬間にエアーの噴射が行われる仕様です。
ボタンを押し続けたらずっとエアーの噴射がされる仕様ではありません。

開発&実行環境

  • Unity 2021.3.12f1
  • PC, Windows 11

最小コード(コピペで動きます)

先に動く形を置きます。自分の環境ではこのコードで狙い通り動きました。

Input System を使っていて、Action Map は Player、
Action 名は Orientation1 / Orientation2 / Shot の想定です。

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(PlayerInput))]
public class ZeroGImpulseMove : MonoBehaviour
{
    [Header("Tuning")]
    [SerializeField] float impulsePower = 3.0f;
    [SerializeField] ForceMode forceMode = ForceMode.Impulse;

    [Header("Fallback (world space)")]
    [SerializeField] Vector3 noInputThrustDirection = Vector3.forward; // 入力ゼロ時の推進方向(ワールド+Z)

    Rigidbody rb;
    PlayerInput playerInput;

    InputAction orientation1;
    InputAction orientation2;
    InputAction shot;

    Quaternion initialRotation;
    Quaternion targetRotation;

    // 入力がゼロのときに「固定向きで進む」仕様に合わせる
    Vector3 thrustDirectionWS;

    // 押した瞬間をUpdateで拾って、FixedUpdateでAddForceする
    bool shotPressed;

    // Axis(0..1)用の立ち上がり検出
    float prevShotValue = 0f;
    [SerializeField] float shotThreshold = 0.5f;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
        playerInput = GetComponent<PlayerInput>();

        rb.freezeRotation = true;

        initialRotation = rb.rotation;
        targetRotation = initialRotation;

        thrustDirectionWS = noInputThrustDirection.normalized;
    }

    void OnEnable()
    {
        // 「Player」マップ前提です。デフォルトのマップはインスペクタで設定します
        orientation1 = playerInput.actions["Orientation1"];
        orientation2 = playerInput.actions["Orientation2"];
        shot = playerInput.actions["Shot"];

        orientation1?.Enable();
        orientation2?.Enable();
        shot?.Enable();
    }

    void Update()
    {
        if (playerInput.currentActionMap == null || playerInput.currentActionMap.name != "Player")
        {
            return;
        }

        //      方向入力(例:左スティックX/Y + 右スティックY を合成)
        Vector2 o1 = orientation1 != null ? orientation1.ReadValue<Vector2>() : Vector2.zero;
        Vector2 o2 = orientation2 != null ? orientation2.ReadValue<Vector2>() : Vector2.zero;

        Vector3 direction = new Vector3(o1.x, o2.y, o1.y);

        if (direction.sqrMagnitude > 1e-6f)
        {
            Vector3 directionN = direction.normalized;

            // モデル都合で -方向に向けたい
            targetRotation = Quaternion.LookRotation(-directionN, Vector3.up);

            // 入力があるときの推進方向(ワールド空間)
            thrustDirectionWS = directionN;
        }
        else
        {
            // 入力がないときは「向き固定・所定方向に進む」仕様
            targetRotation = initialRotation;
            thrustDirectionWS = noInputThrustDirection.normalized;
        }

        //              Shotの「押した瞬間」検出
        bool pressedThisFrame;
        if (shot != null && shot.type == InputActionType.Button)
        {
            pressedThisFrame = shot.WasPressedThisFrame();
        }
        else
        {
            float v = shot != null ? shot.ReadValue<float>() : 0f;
            pressedThisFrame = (prevShotValue <= shotThreshold) && (v > shotThreshold);
            prevShotValue = v;
        }

        if (pressedThisFrame)
        {
            shotPressed = true;
        }
    }

    void FixedUpdate()
    {
        if (playerInput.currentActionMap == null || playerInput.currentActionMap.name != "Player")
        {
            return;
        }

        // Rigidbodyの回転はFixedUpdate側で反映
        rb.MoveRotation(targetRotation);

        if (!shotPressed)
        {
            return;
        }

        shotPressed = false;

        rb.AddForce(thrustDirectionWS * impulsePower, forceMode);
    }
}

設定と補足

設定:Rigidbody

image.png

自分は「宇宙っぽい=勝手に減速しない」を優先したかったので、
こんな感じに寄せました。

  • Is Kinematic:OFF(物理演算で動かしたいため)
  • Use Gravity:OFF(無重力空間の前提のため)
  • Drag:0(減速なし)
  • Angular Drag:0〜0.05(あまり意味は出にくいけど気持ちです)
  • Collision Detection:Discrete(速度が上がりすぎたら見直したいです)
  • Interpolate:None(見た目がガタついたら後で試します)

※Dragを0にしたので、当然ですが 噴射すると止まりません。
 止めたい場合は後述する「詰まったところ」に書いたような調整が必要でした。

設定:入力マッピング

Input System 側は以下の割り当てで実装しました。

image.png

image.png

  • Orientation1:左スティック(Vector2)
  • Orientation2:右スティック(Vector2)
  • Shot:R2 / RT(Button/Value, Axis)

Shotは、環境によって「Button」でも「Axis(0..1)」でも取れることもあり、
自分の手元では両方に対応できるようにしました。問題なく動いています。

補足:Rigidbodyの採用

キャラクターの移動になるので、CharacterControllerの利用も検討するところですが、
今回は Rigidbody を採用しました。

理由は、衝突・反動・浮遊感といった挙動を簡単に実装したかったからです。
Rigidbody 特有の現象もあるとは思いましたが、
ひとまず無重力らしい動きを検証してみたかったため採用を決めています。

補足:向き(LookRotationのゼロ対策、モデル都合の見た目の向き変更)

Quaternion.LookRotation(Vector3.zero)は、実行するとログに警告が出ました。
自分のケースでは「入力がない=方向がゼロ」になるので、そこが踏みやすかったです。

この対策としてやったことは単純で、
入力がなかった時には初期向きに更新する
でした。

具体的には、Awakeで初期回転を取得しておいてelseで戻す実装で作っています。

補足:回転運動を止める(freezeRotationをtrueに設定)

rb.freezeRotation = true;としているのは、
こうすることで力を加えてもプレイヤーキャラを回転運動させないように出来るからです。

なぜプレイヤーキャラが回転してしまうと困るのか?

それは左右のスティックで向きを完全に制御できるようにしたかったからです。
このため、力をかける際には回転しないよう、freezeRotationの設定をしています。

補足:推進(AddForce / ForceModeの選び方)

今回はミニゲームの仕様で噴射の回数を制限する必要があったため
「押した瞬間だけ噴射」にしたかったので
AddForceを押下の瞬間だけ1回呼ぶ形にしました。

ForceModeは、最初Impluseにしていて、ミニゲームとしてはこれでも期待通り動きました。
(質量が効く感じになるので、係数を増やす必要はありますが、「物理っぽさ」は出ます)

しかしながら、リファレンスをよく見てみると、
「質量に関係なく瞬間的な推進力を与えたい」なら
VelocityChangeの方が合うようでした。

質量を無視したいなら、VelocityChangeもアリそうです。

詰まったところ

UpdateでAddForceすると、挙動が揺れやすかった

最初はUpdate側でAddForceしていたのですが、
ちょっと安定していないような感触がありました。
最終的にはUpdateで「押した瞬間」を拾って、
FixedUpdateでAddForceにしたら納得感が出ました。

止まらない(Drag 0のため当たり前)

宇宙なのでそれっぽいのですが、遊びとしては「移動を抑えたい瞬間」があります。
必要なら Drag を少し入れる / 逆噴射の仕様を入れる / 速度上限を付ける、
などが出来るとよさそうです。

おわりに

今回のポイントは

  • 入力がゼロになる瞬間があるので、向き更新はゼロ対策を入れる
  • 物理はFixedUpdateで触る
  • RigidbodyのDrag=0で止まらないのは仕様(止めたいなら別途調整)

...という感じになります。自分はこの形でミニゲームの操作感が成立しました。

今後もミニゲームの完成を目指しますが、
動くものが出せたら続編として実際の調整(推進力・制動・カメラ)もまとめる予定です。

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?