Unity 2D キャラクター 素材|2Dキャラ制御スクリプト一式(移動・二段ジャンプ・被弾無敵・体力バー・トゲ判定・カメラ追従)Unity 2021.3以降

C#スクリプト6本・Inspectorで触れる公開調整値54項目。コードを1行も開かずに、移動の重さ・ジャンプ高さ・無敵時間・ノックバックを数値だけで詰められる。マテリアルとシェーダーを含まないので Built-in / URP / HDRP のどれでもピンク化しない。

インポート

  1. Unity 2021.3+ : Assets > Import Package > Custom Package
  2. Assets/Unity2DCharacterKit

収録内容 (7)

ファイル用途
Runtime/Character2DController.cs横スクロール2Dキャラの移動とジャンプ本体。加減速・空中制御・コヨーテタイム(崖を踏み外した直後もジャンプを受け付ける猶予)・入力バッファ(着地直前のジャンプ押しを覚える)・ボタンを離すと低く跳ぶ可変ジャンプ・落下速度上限まで入っている。これが無料で配る一本で、単体で完結して動く。Ground Check が空でも例外で落ちず、警告を1回だけ出して原点で代用する。
Runtime/Character2DHealth.cs体力・被弾無敵(Iフレーム)・点滅・死亡処理。死亡は何度呼ばれても1回しか走らない二重発火ガード付きで、無敵が切れた瞬間にスプライトを必ず表示状態へ戻すため『点滅したまま消えたキャラ』が発生しない。体力の変化はイベントで飛ぶので、UI側はポーリングしない。
Runtime/Character2DAnimatorBridge.cs移動・接地・ジャンプ・被弾・死亡を Animator のパラメータへ橋渡しする。パラメータ名は Inspector で自分の Animator の命名に合わせられ、存在しない名前は起動時に判定して黙って飛ばすので、アニメーションを作りかけの状態でもコンソールが赤くならない。OnDisable で必ずイベント購読を解除する。
Runtime/HealthBarUI.csuGUI の体力バー。Character2DHealth のイベントを購読し、減った分をなめらかに追う。最大体力が0でもゼロ除算しない、二重購読しない、そして『満タン時に自分自身を非表示にして二度と戻らない』事故を検出して代わりにバー本体だけを隠す。
Runtime/Character2DDamageZone.csトゲ・溶岩・敵の当たり判定にそのまま付けるダメージ源。同じ相手を連打しない再ヒット間隔、対象レイヤー指定、ノックバック付き。無敵時間で弾かれた時はノックバックも与えない(弾かれたのに吹き飛ぶ違和感を消す)。範囲から出た相手は記録から削除するので、ステージが長くても辞書が膨れない。
Runtime/Character2DFollowCamera.cs2D用の追従カメラ。先読み(進行方向に少し先を見せる)と、ステージ枠の外を映さないクランプ付き。部屋が画面より小さい場合は左右のクランプが喧嘩して震えるので、その時は部屋の中心に固定する。カメラが正射影でない場合と枠が未設定の場合は警告を1回だけ出して落ちない。
QUICKSTART_JA.md日本語のクイックスタート。インポート手順(バージョンとUI言語でメニュー名が変わることも明記)、動作条件、3ステップのセットアップ、Inspectorで触る値の一覧、詰まりやすい箇所の確認順、購入後の窓口までを1枚にまとめる。これを読めばコードを開かずに自分のキャラが動く。

インスペクターで変更する値

ファイルフィールド効果
Runtime/Character2DController.csmoveSpeed自分のタイルサイズに合わせる最初の一箇所。1タイル=1unit なら 6 前後で走っている感じになる。ここだけ変えても加速の質は Acceleration が保つ。
Runtime/Character2DController.csacceleration小さくすると助走の要る重量級、大きくすると即最高速のアクション寄り。ジャンルの手触りを決める値なので Inspector に出してある。
Runtime/Character2DController.csdeceleration小さいと氷の上のように滑る。滑る床のギミックはこの値を下げるだけで作れる。
Runtime/Character2DController.csairControl0に近いほど跳んだ後に方向転換できない硬派な操作、1に近いほど現代的で親切な操作。難易度そのものに直結する。
Runtime/Character2DController.csjumpHeight初速ではなく到達高さで指定する。重力から初速を逆算しているので、Rigidbody2D の Gravity Scale を後から変えても届く高さが崩れない。レベル設計の段差寸法とそのまま同じ数字で話せる。
Runtime/Character2DController.csmaxJumps1で通常ジャンプ、2で二段ジャンプ。ステージ途中で能力解放する場合はスクリプトから代入するだけで済む。
Runtime/Character2DController.cscoyoteTime崖を踏み外した直後でもジャンプを受け付ける猶予。0だと『押したのに跳ばない』という理不尽さが出る。0.1前後が扱いやすい。
Runtime/Character2DController.csjumpBufferTime着地の少し前に押したジャンプを覚えておく。連続ジャンプの気持ちよさはほぼこの値で決まる。
Runtime/Character2DController.csfallGravityMultiplier1のままだと放物線が間延びして『ふわふわ』する。2前後にすると上りは軽く落ちは速い、というアクションらしい弧になる。
Runtime/Character2DController.csmaxFallSpeed落ちすぎると薄い床をすり抜ける。上限を切ることで長い落下のあるステージでも床を貫通しない。
Runtime/Character2DController.csgroundLayersここを自分の地形レイヤーだけにするのがセットアップの最重要点。敵やアイテムを含めてしまうと空中で跳べてしまう。
Runtime/Character2DHealth.csmaxHealth買い手のゲームの難易度設計そのもの。体力バーの表示にも同じ値が使われる。
Runtime/Character2DHealth.csinvulnerableSecondsトゲの上で連続ダメージを受けて即死する事故を防ぐ値。0にすると無敵なしの高難度になる。
Runtime/Character2DHealth.csblinkInterval無敵中であることをプレイヤーに伝える唯一の手がかり。小さいほど激しく点滅する。
Runtime/Character2DAnimatorBridge.csspeedParameter自分の Animator の命名に合わせる場所。既存プロジェクトの命名規則を崩さずに使えるようにしてある。存在しない名前を入れてもエラーにはならず、無視される。
Runtime/Character2DAnimatorBridge.cswalkThresholdこれ未満の速度は0として渡す。ごく僅かな滑りで待機と歩行のアニメがちらつくのを止める。
Runtime/HealthBarUI.cssmoothSpeed0で即座に反映、小さくするとじわっと減る演出になる。Time.unscaledDeltaTime を使うのでポーズ中でも止まらない。
Runtime/HealthBarUI.cslabelFormat{0}が現在値、{1}が最大値。『3 / 5』でも『HP 3』でも自分のUIに合わせられる。
Runtime/Character2DDamageZone.csdamage同じスクリプトのままトゲ1・溶岩3のように危険度を分けられる。
Runtime/Character2DDamageZone.csretriggerInterval範囲に留まり続けた時の連打間隔。無敵時間と合わせて、トゲ床の上を歩き抜けられるかどうかが決まる。
Runtime/Character2DDamageZone.csknockbackForceダメージ源から離す押し戻し量。0にすればその場で止まる。無敵で弾かれた時は押し戻さないので違和感が出ない。
Runtime/Character2DFollowCamera.cssmoothTime0でキャラに固定、大きくすると遅れて付いてくる。2Dアクションの見やすさはここで決まる。
Runtime/Character2DFollowCamera.cslookAheadDistance進む方向を少し多く見せる。高速スクロールのゲームほど大きくすると先の地形が読める。
Runtime/Character2DFollowCamera.csuseBoundsONにして Bounds Box にステージ大のBoxCollider2Dを入れると、枠の外(何もない空間)を映さなくなる。部屋が画面より小さい場合は自動で中心固定に切り替わる。

無料スクリプト — Runtime/Character2DController.cs

このファイルをそのままプロジェクトにコピーしてください。パック収録の1本です。

using UnityEngine;

// Side-scrolling 2D character movement. Put this on the character root (needs a Rigidbody2D).
// Everything a designer touches is a public field, so the code never has to be opened.
[RequireComponent(typeof(Rigidbody2D))]
[DisallowMultipleComponent]
public class Character2DController : MonoBehaviour
{
    [Header("Input (legacy Input Manager)")]
    [Tooltip("Axis name from Project Settings > Input Manager.")]
    public string horizontalAxis = "Horizontal";
    [Tooltip("Button name from Project Settings > Input Manager.")]
    public string jumpButton = "Jump";

    [Header("Move")]
    [Tooltip("Top horizontal speed in world units per second.")]
    [Min(0)] public float moveSpeed = 6f;
    [Tooltip("How fast the character reaches Move Speed. Higher feels lighter.")]
    [Min(0)] public float acceleration = 60f;
    [Tooltip("How fast the character stops when there is no input. Higher feels stickier.")]
    [Min(0)] public float deceleration = 70f;
    [Tooltip("0 = no steering while airborne, 1 = full ground control while airborne.")]
    [Range(0f, 1f)] public float airControl = 0.65f;

    [Header("Jump")]
    [Tooltip("Peak jump height in world units. Gravity is solved for this height, so the height stays true even after Rigidbody2D Gravity Scale changes.")]
    [Min(0)] public float jumpHeight = 3.2f;
    [Tooltip("Jumps available before touching ground again. 1 = single jump, 2 = double jump.")]
    [Min(0)] public int maxJumps = 1;
    [Tooltip("Seconds after leaving a ledge during which a jump press is still accepted.")]
    [Min(0)] public float coyoteTime = 0.12f;
    [Tooltip("Seconds before landing during which a jump press is remembered.")]
    [Min(0)] public float jumpBufferTime = 0.12f;
    [Tooltip("Extra gravity while falling. Above 1 makes the fall snappier than the rise.")]
    [Min(0)] public float fallGravityMultiplier = 2.2f;
    [Tooltip("Extra gravity while rising with the jump button released. Gives a short hop.")]
    [Min(0)] public float lowJumpMultiplier = 2.6f;
    [Tooltip("Maximum downward speed. Stops the character tunnelling through thin colliders. 0 = no limit.")]
    [Min(0)] public float maxFallSpeed = 22f;

    [Header("Ground check")]
    [Tooltip("Empty child object placed at the feet. Leave empty and the character origin is used instead.")]
    public Transform groundCheck;
    [Tooltip("Radius of the ground probe, in world units.")]
    [Min(0)] public float groundCheckRadius = 0.16f;
    [Tooltip("Layers that count as ground.")]
    public LayerMask groundLayers = 1;
    [Tooltip("Flip localScale.x so the sprite faces the direction of movement.")]
    public bool flipSpriteWithMovement = true;

    public bool IsGrounded { get; private set; }
    public float HorizontalInput { get; private set; }
    public int Facing { get; private set; }
    public bool ControlEnabled { get; set; }

    Rigidbody2D _rb;
    float _coyoteTimer;
    float _bufferTimer;
    int _jumpsLeft;
    bool _jumpHeld;
    bool _missingGroundCheckWarned;

    void Awake()
    {
        _rb = GetComponent<Rigidbody2D>();
        _rb.freezeRotation = true;
        Facing = 1;
        ControlEnabled = true;
        _jumpsLeft = Mathf.Max(1, maxJumps);
    }

    void Update()
    {
        if (ControlEnabled)
        {
            HorizontalInput = Input.GetAxisRaw(horizontalAxis);
            if (Input.GetButtonDown(jumpButton)) _bufferTimer = jumpBufferTime;
            _jumpHeld = Input.GetButton(jumpButton);
        }
        else
        {
            // Death or a cutscene: drop input instead of freezing the physics body.
            HorizontalInput = 0f;
            _jumpHeld = false;
            _bufferTimer = 0f;
        }

        if (_bufferTimer > 0f) _bufferTimer -= Time.deltaTime;

        if (flipSpriteWithMovement && Mathf.Abs(HorizontalInput) > 0.01f)
        {
            int want = HorizontalInput > 0f ? 1 : -1;
            if (want != Facing)
            {
                Facing = want;
                Vector3 s = transform.localScale;
                s.x = Mathf.Abs(s.x) * want;
                transform.localScale = s;
            }
        }
    }

    void FixedUpdate()
    {
        IsGrounded = CheckGround();

        if (IsGrounded && _rb.velocity.y <= 0.01f)
        {
            _coyoteTimer = coyoteTime;
            _jumpsLeft = Mathf.Max(1, maxJumps);
        }
        else if (_coyoteTimer > 0f)
        {
            _coyoteTimer -= Time.fixedDeltaTime;
        }

        Vector2 v = _rb.velocity;

        float target = HorizontalInput * moveSpeed;
        float rate = (Mathf.Abs(target) > 0.01f ? acceleration : deceleration) * (IsGrounded ? 1f : airControl);
        v.x = Mathf.MoveTowards(v.x, target, rate * Time.fixedDeltaTime);

        if (_bufferTimer > 0f && (_coyoteTimer > 0f || _jumpsLeft > 0))
        {
            // Consume both the buffer and the coyote window, so one press can never fire twice.
            _bufferTimer = 0f;
            _coyoteTimer = 0f;
            _jumpsLeft = Mathf.Max(0, _jumpsLeft - 1);
            v.y = JumpVelocity();
        }

        float g = Physics2D.gravity.y * _rb.gravityScale;
        if (v.y < 0f) v.y += g * (fallGravityMultiplier - 1f) * Time.fixedDeltaTime;
        else if (v.y > 0f && !_jumpHeld) v.y += g * (lowJumpMultiplier - 1f) * Time.fixedDeltaTime;

        if (maxFallSpeed > 0f && v.y < -maxFallSpeed) v.y = -maxFallSpeed;

        _rb.velocity = v;
    }

    float JumpVelocity()
    {
        float g = Mathf.Abs(Physics2D.gravity.y) * Mathf.Max(0.0001f, _rb.gravityScale);
        return Mathf.Sqrt(2f * g * Mathf.Max(0f, jumpHeight));
    }

    bool CheckGround()
    {
        Vector2 point;
        if (groundCheck != null)
        {
            point = groundCheck.position;
        }
        else
        {
            // Warn once, not every physics step, and keep the game running.
            if (!_missingGroundCheckWarned)
            {
                _missingGroundCheckWarned = true;
                Debug.LogWarning("[Character2DController] Ground Check is empty. Using the character origin. Create an empty child at the feet and assign it.", this);
            }
            point = transform.position;
        }
        return Physics2D.OverlapCircle(point, Mathf.Max(0.01f, groundCheckRadius), groundLayers) != null;
    }

    void OnDrawGizmosSelected()
    {
        Vector3 p = groundCheck != null ? groundCheck.position : transform.position;
        Gizmos.color = Color.green;
        Gizmos.DrawWireSphere(p, Mathf.Max(0.01f, groundCheckRadius));
    }
}

完全版を受け取る $20

このページは実際に動く一部です。完全版には以下がすべて含まれます。

C#スクリプト6本・Inspectorで触れる公開調整値54項目。コードを1行も開かずに、移動の重さ・ジャンプ高さ・無敵時間・ノックバックを数値だけで詰められる。マテリアルとシェーダーを含まないので Built-in / URP / HDRP のどれでもピンク化しない。

同種の2Dキャラクター制御アセットが現在 20 USD 前後で販売されている(当社が本棚の実売価格を実測した帯)。本パックは同じ価格で、C#6本・Inspector公開調整値54項目・日本語クイックスタートを付ける。外注時給や人月単価は当社が実測していないため金額としては書かない。ゼロから書いた場合の作業量として提示できる事実は、この一式を実際に書き上げるのにかかった作業=2日ぶんという当社の実績のみ。

受け取る