unity 2d キャラクター 素材 — 2D Character Feel Kit (C# only, no sprite art): coyote time, jump buffer, i-frames, hit flash, health bar. 26 Inspector fields, 6 presets, Unity 2021.3+

Global Game Jam runs 48 hours. Coyote time, jump buffering, variable jump height, an invincibility window and a hit flash are already written and already commented here — 26 values in the Inspector and 6 character presets, so those hours go into the level instead of into re-deriving a jump arc.

Import

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

What is inside (7)

FileWhat it is for
Runtime/Character2DMotor.csThe movement itself. Run, jump, coyote time, jump buffering, variable jump height, air jumps and a fall-speed cap, plus two warnings that catch the two setup mistakes everybody makes: a ground layer that includes the character, and a gravity scale of zero.
Runtime/CharacterHealth.csHit points with a real invincibility window. Contact damage from hazard layers ticks once per window instead of once per physics frame, death fires exactly once however many spikes are touching, and healing cannot go over the maximum.
Runtime/HitFlash.csThe damage feedback. Blinks every SpriteRenderer on the character when a hit lands, restarts cleanly if a second hit arrives mid-blink, and puts the original colours back in OnDisable so the character is never left stuck red.
Runtime/CharacterAnimatorBridge.csFeeds the Animator from the motor and the health without ever asking for a parameter the controller does not have, so a half-built controller cannot flood the console. Also flips the sprite to face the way you are travelling.
Runtime/HealthBarUI.csA health bar on a filled UI Image. Catches up smoothly, turns your low-health colour, can hide itself at full health, survives a max health of zero, and switches itself off with a readable message instead of throwing a null reference every frame.
Presets/character-tuning-presets.jsonSix characters, one formula. Every key matches a public field on Character2DMotor exactly, so you can either type the numbers into the Inspector or pass one preset object to ApplyTuningJson at runtime for an ice level or a pair of heavy boots.
QuickStart.mdImport to a moving, hurting, flashing character in five minutes, plus the two version facts that stop refunds: which Unity versions this runs on, and the fact that there is no material or shader in the pack so no render pipeline can break it.

What you change in the Inspector

FileFieldWhat it does
Runtime/Character2DMotor.csmoveSpeedThe one number that decides whether your game feels like a stroll or a sprint. Set it before you touch anything else, because acceleration is measured against it.
Runtime/Character2DMotor.csgroundAccelerationHow quickly you reach top speed. Above 150 feels arcade and instant; below 40 feels like the character has weight and momentum.
Runtime/Character2DMotor.csgroundDecelerationHow quickly you stop. Set it below groundAcceleration and the character slides past ledges; set it above and stops are crisp.
Runtime/Character2DMotor.csairAccelerationHow much you can steer mid-jump. Low values make a jump a commitment, which is what precision platformers want.
Runtime/Character2DMotor.csjumpHeightSet in world units, not force, so it stays the same when you change gravity scale. Measure your tallest platform and set this a little above it.
Runtime/Character2DMotor.csextraAirJumps0 for a single jump, 1 for a double jump. Change it at runtime the moment the player picks up the wings.
Runtime/Character2DMotor.cscoyoteTimeSeconds after walking off a ledge during which a jump still works. Players never notice it is there; they only notice when it is missing.
Runtime/Character2DMotor.csjumpBufferTimeSeconds before landing during which an early jump press is remembered. This is the difference between a game that feels responsive and one that feels like it ignores you.
Runtime/Character2DMotor.csfallGravityMultiplierRaise this before you touch jump height. A character that falls faster than it rose is the single cheapest fix for a floaty jump.
Runtime/Character2DMotor.cslowJumpGravityMultiplierApplied while rising with the button already released. This is what turns a quick tap into a small hop and a held press into a full jump.
Runtime/Character2DMotor.csmaxFallSpeedKeeps long drops readable and stops a fast fall tunnelling through a thin platform collider.
Runtime/CharacterHealth.csmaxHealthHearts, not percent. Five means the player survives five hits, which is what you are really designing when you place hazards.
Runtime/CharacterHealth.csinvincibleSecondsSeconds of immunity after a hit. Leave it at 0 and standing on a spike drains the whole health bar in a quarter of a second.
Runtime/CharacterHealth.cscontactDamageDamage taken from anything on the hazard layers. Set it to 0 while you are prototyping and want to walk through your own spikes.
Runtime/CharacterHealth.cshazardLayersWhich layers hurt. One field instead of a damage script on every spike, every enemy and every falling rock in the level.
Runtime/CharacterHealth.csdestroyOnDeathLeave it off while you are still building the death animation, so the object stays in the scene for you to look at.
Runtime/HitFlash.csflashColorRed reads as damage; white reads as impact. Change it here rather than in a shader, because there is no shader in this pack to change.
Runtime/HitFlash.csflashSecondsLength of one on-off step. Around 0.06 to 0.10 reads as a hit; longer than 0.2 reads as a bug.
Runtime/HitFlash.csflashCountMatch it to the invincibility window: when the blinking stops, the player is vulnerable again, and that is free information the player reads without being told.
Runtime/HitFlash.csincludeChildSpritesOn, the whole character flashes including the sword and the cape. Off, only the body does, which suits characters made of many pieces.
Runtime/CharacterAnimatorBridge.csspeedParameterType the name your Animator already uses instead of renaming your controller to match somebody else's script. Leave it empty to skip it entirely.
Runtime/CharacterAnimatorBridge.csgroundedParameterThe bool your jump and fall states switch on. If your controller does not have it yet, the bridge stays silent rather than logging an error every frame.
Runtime/CharacterAnimatorBridge.csflipSpriteWithMovementOn for a character that faces the way it walks. Off if your art has separate left and right clips, or if you flip a parent transform yourself.
Runtime/HealthBarUI.cssmoothSeconds0 snaps and reads as arcade. Around 0.25 makes the bar drain visibly, which is what makes a big hit feel big.
Runtime/HealthBarUI.cshideWhenFullOn for an exploration game where a clean screen matters. Off for an action game where the player wants the number in view at all times.
Runtime/HealthBarUI.cslowHealthColorThe colour the bar turns near death. It is the last warning the player gets before the run ends, so make it loud.

Free script — Runtime/Character2DMotor.cs

Copy this file into your project as-is. It is one of the scripts in the pack.

using UnityEngine;

/// <summary>
/// 2D platformer movement with the feel details tutorials leave out:
/// coyote time, jump buffering, variable jump height, air jumps and a
/// separate fall gravity.
///
/// Drive it with the legacy Input Manager (Read Legacy Input, on by default)
/// or turn that off and call SetMoveInput, RequestJump and ReleaseJump from
/// your own input code. Both input systems are supported that way.
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
[DisallowMultipleComponent]
[AddComponentMenu("2D Character Feel Kit/Character 2D Motor")]
public class Character2DMotor : MonoBehaviour
{
    [Header("Run")]
    [Tooltip("Top horizontal speed, in world units per second.")]
    [Min(0f)] public float moveSpeed = 8f;

    [Tooltip("How hard the character is pushed towards top speed on the ground, in units per second squared. Higher feels snappier.")]
    [Min(0f)] public float groundAcceleration = 70f;

    [Tooltip("How hard the character is slowed on the ground when you let go. Set it below groundAcceleration for a slide.")]
    [Min(0f)] public float groundDeceleration = 90f;

    [Tooltip("Air control. Below groundAcceleration makes a jump feel committed instead of steerable.")]
    [Min(0f)] public float airAcceleration = 35f;

    [Header("Jump")]
    [Tooltip("Peak jump height in world units, measured from take-off. The take-off velocity is solved from this and the current gravity, so changing gravity does not change your jump height.")]
    [Min(0f)] public float jumpHeight = 3.2f;

    [Tooltip("Jumps allowed after leaving the ground. 0 is a single jump, 1 is a double jump.")]
    [Min(0)] public int extraAirJumps = 1;

    [Tooltip("Seconds after walking off a ledge during which a jump press still works. 0.08 to 0.15 feels generous without feeling loose.")]
    [Min(0f)] public float coyoteTime = 0.12f;

    [Tooltip("Seconds before landing during which a jump press is remembered and fired the moment the feet touch down.")]
    [Min(0f)] public float jumpBufferTime = 0.12f;

    [Tooltip("Gravity multiplier while falling. Above 1 makes the character drop faster than it rose, which is what most good platformers do.")]
    [Min(0f)] public float fallGravityMultiplier = 2.2f;

    [Tooltip("Gravity multiplier while still rising after the jump button was released. This is what turns a short tap into a short hop.")]
    [Min(0f)] public float lowJumpGravityMultiplier = 2.8f;

    [Tooltip("Fastest the character may fall, in units per second. Also stops fast falls tunnelling through thin colliders.")]
    [Min(0f)] public float maxFallSpeed = 22f;

    [Header("Ground check")]
    [Tooltip("Layers that count as ground. Do not include the layer this character is on.")]
    public LayerMask groundLayers = 1;

    [Tooltip("Thickness of the box used to look for ground under the feet, in world units.")]
    [Min(0f)] public float groundProbeThickness = 0.08f;

    [Header("Input")]
    [Tooltip("Read the legacy Input Manager axes Horizontal and Jump. Turn this off and call SetMoveInput, RequestJump and ReleaseJump from your own input code.")]
    public bool readLegacyInput = true;

    public event System.Action Jumped;
    public event System.Action Landed;

    public bool IsGrounded { get { return _grounded; } }
    public float MoveInput { get { return _moveInput; } }
    public float HorizontalSpeed { get { return _body != null ? _body.velocity.x : 0f; } }
    public float VerticalSpeed { get { return _body != null ? _body.velocity.y : 0f; } }

    Rigidbody2D _body;
    Collider2D _collider;
    float _moveInput;
    float _coyoteTimer;
    float _bufferTimer;
    int _airJumpsUsed;
    bool _grounded;
    bool _wasGrounded;
    bool _jumpHeld;
    bool _jumpFiredThisStep;

    void Awake()
    {
        _body = GetComponent<Rigidbody2D>();
        _collider = GetComponent<Collider2D>();
        _body.freezeRotation = true;

        if (_collider == null)
        {
            Debug.LogWarning("Character2DMotor: there is no Collider2D on this object, so the ground check has nothing to measure from. Add a CapsuleCollider2D or a BoxCollider2D.", this);
        }

        // The mistake that costs an hour: the character is on a layer that is
        // also listed as ground, so the probe finds the character itself and
        // it never falls. Say so out loud instead of letting them guess.
        if (((1 << gameObject.layer) & groundLayers.value) != 0)
        {
            Debug.LogWarning("Character2DMotor: Ground Layers includes this object's own layer, so the ground check will keep finding the character itself and it will never fall. Put the character on its own layer, or clear that layer from Ground Layers.", this);
        }

        if (_body.gravityScale <= 0f)
        {
            Debug.LogWarning("Character2DMotor: Rigidbody2D Gravity Scale is 0, so there is no arc to solve and the character will not come back down. Set it to 1.", this);
        }
    }

    void Update()
    {
        if (readLegacyInput) ReadLegacyInput();

        if (_grounded) _coyoteTimer = coyoteTime;
        else if (_coyoteTimer > 0f) _coyoteTimer -= Time.deltaTime;

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

    void ReadLegacyInput()
    {
        // If the project is set to Input System Package only, the legacy calls
        // throw. Switch ourselves off once, say what to call instead, and never
        // throw again.
        try
        {
            _moveInput = Input.GetAxisRaw("Horizontal");
            if (Input.GetButtonDown("Jump")) RequestJump();
            if (Input.GetButtonUp("Jump")) ReleaseJump();
            _jumpHeld = Input.GetButton("Jump");
        }
        catch (System.Exception error)
        {
            readLegacyInput = false;
            Debug.LogWarning("Character2DMotor: the legacy Input Manager is not active in this project, so Read Legacy Input has been switched off. Call SetMoveInput, RequestJump and ReleaseJump from your own input code instead. Details: " + error.Message, this);
        }
    }

    /// <summary>Horizontal input from -1 to 1. Call this from your own input code.</summary>
    public void SetMoveInput(float horizontal)
    {
        _moveInput = Mathf.Clamp(horizontal, -1f, 1f);
    }

    /// <summary>Jump pressed. Buffered, so it still fires if you were slightly early.</summary>
    public void RequestJump()
    {
        _bufferTimer = jumpBufferTime;
        _jumpHeld = true;
    }

    /// <summary>Jump released. Cuts the rise short for a lower hop.</summary>
    public void ReleaseJump()
    {
        _jumpHeld = false;
    }

    void FixedUpdate()
    {
        _jumpFiredThisStep = false;
        UpdateGrounded();
        ApplyHorizontal();
        TryJump();
        ApplyGravity();
    }

    void UpdateGrounded()
    {
        _wasGrounded = _grounded;

        if (_collider == null) { _grounded = false; return; }

        Bounds bounds = _collider.bounds;
        Vector2 centre = new Vector2(bounds.center.x, bounds.min.y - groundProbeThickness * 0.5f);
        Vector2 size = new Vector2(Mathf.Max(0.01f, bounds.size.x * 0.9f), Mathf.Max(0.01f, groundProbeThickness));
        _grounded = Physics2D.OverlapBox(centre, size, 0f, groundLayers) != null;

        // Only give the air jumps back once we are resting or falling, so the
        // frame we leave the ground does not silently refund one.
        if (_grounded && _body.velocity.y <= 0.01f) _airJumpsUsed = 0;

        if (_grounded && !_wasGrounded && Landed != null) Landed();
    }

    void ApplyHorizontal()
    {
        float target = _moveInput * moveSpeed;

        float rate;
        if (!_grounded) rate = airAcceleration;
        else if (Mathf.Abs(target) > 0.01f) rate = groundAcceleration;
        else rate = groundDeceleration;

        float x = Mathf.MoveTowards(_body.velocity.x, target, rate * Time.fixedDeltaTime);
        _body.velocity = new Vector2(x, _body.velocity.y);
    }

    void TryJump()
    {
        if (_jumpFiredThisStep) return;   // one jump per physics step, never two
        if (_bufferTimer <= 0f) return;

        bool groundJump = _grounded || _coyoteTimer > 0f;
        bool airJump = !groundJump && _airJumpsUsed < extraAirJumps;
        if (!groundJump && !airJump) return;

        float gravity = Mathf.Abs(Physics2D.gravity.y) * _body.gravityScale;
        if (gravity <= 0f) return;        // no gravity, no arc to solve

        if (airJump) _airJumpsUsed++;

        float takeOff = Mathf.Sqrt(2f * gravity * jumpHeight);
        _body.velocity = new Vector2(_body.velocity.x, takeOff);

        _bufferTimer = 0f;
        _coyoteTimer = 0f;
        _grounded = false;
        _jumpFiredThisStep = true;

        if (Jumped != null) Jumped();
    }

    void ApplyGravity()
    {
        float extra = 1f;
        if (_body.velocity.y < 0f) extra = fallGravityMultiplier;
        else if (_body.velocity.y > 0f && !_jumpHeld) extra = lowJumpGravityMultiplier;

        if (extra > 1f)
        {
            float gravity = Physics2D.gravity.y * _body.gravityScale;
            _body.velocity += new Vector2(0f, gravity * (extra - 1f) * Time.fixedDeltaTime);
        }

        if (maxFallSpeed > 0f && _body.velocity.y < -maxFallSpeed)
        {
            _body.velocity = new Vector2(_body.velocity.x, -maxFallSpeed);
        }
    }

    /// <summary>
    /// Overwrites the tuning fields from one preset object out of
    /// Presets/character-tuning-presets.json. Fields the JSON does not name
    /// keep their current value, so a preset can change only the two numbers
    /// an ice level needs.
    /// </summary>
    public void ApplyTuningJson(string json)
    {
        if (string.IsNullOrEmpty(json))
        {
            Debug.LogWarning("Character2DMotor: ApplyTuningJson was given an empty string, so nothing was changed.", this);
            return;
        }

        try
        {
            JsonUtility.FromJsonOverwrite(json, this);
        }
        catch (System.Exception error)
        {
            Debug.LogWarning("Character2DMotor: that preset is not valid JSON, so nothing was changed. Details: " + error.Message, this);
            return;
        }

        // JSON can carry values the Inspector would have refused. Clamp them.
        moveSpeed = Mathf.Max(0f, moveSpeed);
        jumpHeight = Mathf.Max(0f, jumpHeight);
        maxFallSpeed = Mathf.Max(0f, maxFallSpeed);
        extraAirJumps = Mathf.Max(0, extraAirJumps);
    }

    void OnDrawGizmosSelected()
    {
        Collider2D shape = GetComponent<Collider2D>();
        if (shape == null) return;

        Bounds bounds = shape.bounds;
        Vector3 centre = new Vector3(bounds.center.x, bounds.min.y - groundProbeThickness * 0.5f, 0f);
        Vector3 size = new Vector3(Mathf.Max(0.01f, bounds.size.x * 0.9f), Mathf.Max(0.01f, groundProbeThickness), 0f);
        Gizmos.color = Color.green;
        Gizmos.DrawWireCube(centre, size);
    }
}

Get the complete version

This page is the working piece. The full pack has everything below.

Global Game Jam runs 48 hours. Coyote time, jump buffering, variable jump height, an invincibility window and a hit flash are already written and already commented here — 26 values in the In

Where the 48 comes from: Global Game Jam is a 48-hour event, held once a year — check your own jam's rules on pre-made assets before you rely on any pack, including this one. What you can count in this file: 5 C# scripts, 26 field

Send it to me
ENDEJAESPT

Find a tool