How to Add Unity 2D Player Movement to a Bullet Hell Game

Unity 2D player movement is only part of making a controllable character: its animations also need to respond when you start and stop. Zenva brings its experience helping a community of over one million learners and developers gain digital skills to that practical next step in this walkthrough: replacing a moving placeholder with a pixel-art character for a bullet hell game.

This tutorial assumes you have Unity and the supplied project assets ready, with a placeholder Player object, a Rigidbody2D, and the Player Input component and movement actions already configured.

Download Project Files

The files and full project are available through the course included in the Unity Game Development Mini-Degree if you want the complete guided path. You can keep reading this free tutorial to build the movement script, prepare sprite animations, and connect idle and run transitions.

Make a Complete Card Battler in Godot e1788415191322 - How to Add Unity 2D Player Movement to a Bullet Hell Game
FREE GODOT COURSE
LEARN GODOT, UNITY, UNREAL & MORE
ACCESS FOR FREE
AVAILABLE FOR A LIMITED TIME ONLY

Set Up Unity 2D Player Movement

Begin with the circle placeholder so you can test input and movement before changing the character’s visuals.

Creating the PlayerMovement Script

Select Player in the Hierarchy. In the Inspector, choose Add Component, search for PlayerMovement, and select New script. Name it PlayerMovement and place it in a new Scripts folder under Assets. Double-click the script to open it in your code editor.

Unity editor with the Player object selected in the Hierarchy and the Add Component button visible in the Inspector.

Declaring Movement Variables

Declare the movement speed, Rigidbody2D reference, and input vector inside the class:

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerMovement : MonoBehaviour
{
    [SerializeField] private float _movementSpeed;
    private Rigidbody2D _rb;
    private Vector2 _movementInput;
}
  • _movementSpeed controls how fast the player moves. [SerializeField] keeps the field private while exposing it in the Inspector.
  • _rb stores the player’s Rigidbody2D component.
  • _movementInput stores the two-dimensional direction received from the Input System.

Keep fields private unless another script needs to access them. The underscore prefix distinguishes these private fields.

Save the script, return to Unity, and set Movement Speed on the Player Movement component to an initial value such as 5. The remaining methods will make that value affect movement.

Getting the Rigidbody2D Reference

Cache the Rigidbody2D in Start so the script can change its physics properties during gameplay:

void Start()
{
    _rb = GetComponent<Rigidbody2D>();
}

Applying Velocity in Update

Add the movement logic inside Update:

void Update()
{
    _rb.linearVelocity = _movementInput * _movementSpeed;
}

Each frame, this sets linearVelocity to the input direction multiplied by the movement speed.

Reading Input with the Move Method

Add a public callback for the Input System:

public void Move(InputAction.CallbackContext context)
{
    _movementInput = context.ReadValue<Vector2>();
}

InputAction.CallbackContext carries information about the input action. Calling context.ReadValue<Vector2>() reads a two-dimensional value, such as WASD or a gamepad stick, and stores it in _movementInput.

To connect the callback in Unity:

  1. Select Player and expand Events on its Player Input component.
  2. Under the Default map, add an event entry for movement with the plus button.
  3. Drag the Player object into the entry’s object slot.
  4. Choose PlayerMovement > Move from the dynamic callback context list.

Unity editor in Play Mode showing the Player Input component events and the Player Movement script with a Movement Speed value of 5 in the Inspector.

The movement action can now call Move and update the input vector used by Update.

Testing Movement

Enter Play Mode and move the white circle with the controls you configured. Adjust Movement Speed in the Inspector while the game is running to find a suitable value.

Set the Game view to Full HD (1920×1080) and adjust its scale to suit your screen while testing.

Runtime Inspector Values

Inspector changes made during Play Mode apply immediately but revert when you stop. For example, changing Movement Speed to 10 makes the character faster during that test, then restores the previous value when you exit Play Mode. The same applies to properties such as the Rigidbody2D’s Gravity Scale.

Prepare the Pixel-Art Idle Animation

With the placeholder moving, you can bring in the wizard sprite and correct its import settings before transferring the player components.

Importing the Idle Sprite

Locate IdleR under Assets/CourseAssets/Female Wizard in the Project panel. Drag it into the Scene view to create a character object. Unity generates an animation clip and an Animator Controller.

Create an Animations folder under Assets and save the new clip there as player_anim_idle_right. On the first preview, the wizard may appear small, blurry, or distorted because its texture settings still need adjustment.

The Game view showing the blurry pixelated wizard sprite next to the white and yellow placeholder circles before the import settings are corrected.

Configuring Sprite Import Settings

Select the IdleR asset. Set Sprite Mode to Multiple because the sheet contains several frames, and set Pixels Per Unit to 60. Pixels Per Unit controls how many sprite pixels correspond to one world unit.

The Unity Inspector panel for the IdleR texture asset showing Sprite Mode set to Multiple and Pixels Per Unit set to 60.

Change Filter Mode from Bilinear to Point (no filter) so Unity does not smooth the pixel edges. Set Compression to None, and click Apply after making the changes.

The Unity Inspector panel showing the Filter Mode and Compression settings for the IdleR texture asset.

Slicing the Sprite Sheet

If the animation shifts or jumps, the automatically detected frame boundaries may be uneven. Open Sprite Editor, then change Slice > Type from Automatic to Grid By Cell Size.

The Unity Sprite Editor showing the automatically sliced sprite frames with uneven sizes and positions.

The supplied idle sheet is 1400 pixels wide with 10 frames, giving a cell width of 140 pixels. Its single row is also 140 pixels high. Enter 140 by 140 in the Pixel Size fields, then click Slice and Apply.

The Unity Sprite Editor Slice panel with Grid By Cell Size selected and a red grid overlay on the sprite sheet.

The evenly sized frames keep the animation from shifting as it plays.

The Unity Sprite Editor after grid slicing showing 10 evenly sized frames of the wizard idle animation.

Viewing the Result

Press Play to preview the idle animation. Check the Game view’s Full HD (1920×1080) setting and scale slider if the display still looks wrong. The wizard should now have a smooth idle animation with crisp pixel art.

The Game view in Full HD showing the wizard idle animation rendered sharply with crisp pixel art.

Challenge

Try replacing the player circle with the animated character by transferring the components it needs to move. For now, concentrate on getting the idle sprite moving; its facing direction will be handled separately. The next section walks through the component transfer.

Add the Run Animation to the Player

Setting Up the Player Components

The new character already has a Transform, Sprite Renderer, and Animator. Copy the following components from the circle placeholder:

  • Rigidbody 2D
  • Player Input
  • Player Movement

For each component, open its gear menu on the circle and select Copy Component. On the character, use Paste Component As New. This preserves the component settings, including Gravity Scale.

Unity Inspector showing the Player object with Transform, Sprite Renderer, Rigidbody 2D, Player Input, and Player Movement components attached.

Fixing the Movement Bug

If the character does not move after the transfer, check the copied Player Input events. The Move event still points to the circle’s Player Movement component. Replace that reference with the new character’s Player Movement > Move callback.

Unity editor in Play mode with the player character selected in the Hierarchy and visible in the Scene view.

Once both objects respond correctly, delete the circle placeholder, rename the character Player, and reset its Transform to center it in the scene. You can turn off scene gizmos if they are distracting.

Preparing the Animator Controller

Select the character and find its controller in the Animator component. Rename the generated IdleR_0 controller to a clearer name such as PlayerAnimController. Double-click it to open the Animator window and see the existing idle state.

Unity Inspector showing the Player object selected with the Animator component and Controller field set to IdleR_0.

Creating the Run Animation Clip

Open Window > Animation > Animation and dock the window. With Player selected, choose Create New Clip from the clip dropdown and save the clip as PlayerAnimRun in your animation folder.

Unity Animation window showing the PlayerAnimIdleR clip with sprite keyframes across the timeline.

The screenshot labels the existing right-idle clip PlayerAnimIdleR; this is the role of the player_anim_idle_right clip you created above.

Adding Run Sprites to the Clip

In the Animation window, choose Add Property > Sprite Renderer > Sprite. Select all the run sprites under CourseAssets > Female Wizard > RunR and drag them into the dope sheet to create the sprite keyframes.

Unity Animation window showing the populated PlayerAnimRun clip with sprite keyframes and the Samples field set to 12.

Adjusting the Sample Rate

The default sample rate of 60 can make the clip play too quickly. Change Samples to 12 to match the idle animation’s speed. If the field is hidden, open the Animation window’s three-dots menu and enable Show Sample Rate.

Fixing Blurry Sprites

Select all the female wizard sprite textures in the Project panel. Set Pixels Per Unit to 60, Filter Mode to Point (no filter), and Compression to None, then click Apply.

Unity Scene view showing the 2D character in a running pose alongside the Texture 2D Import Settings in the Inspector.

Aligning the Sprite Frames

If the run animation shifts, select RunR, make sure Sprite Mode is Multiple, and open the Sprite Editor. Choose Slice > Type: Grid By Cell Size with the same cell dimensions used for the idle sprites, then click Slice and Apply.

Next Challenge

Try making Run Left and Idle Left clips with the same workflow. You can also experiment with the death animation for extra practice. Next, you will complete the directional clips and connect idle and run in the Animator Controller.

Connect Idle and Run with Animator Blend Trees

Creating the Left and Right Animation Clips

Select Player and create a clip in the Animation window named PlayerAnimIdleL. Save it in Animations, drag in the left-idle sprites, and set Samples to 12. Then create PlayerAnimRunL, add the left-run sprites, and set its sample rate to 12 as well.

Unity Animation window with the PlayerAnimRunL clip created and the Samples field set to 12, next to the Inspector showing the sprite import settings for the character.

Creating clips in the Animation window produces animation assets without generating another Animator Controller. Dragging sprites directly into the scene is also valid, but it creates both the clips and a controller. Here, you will use the existing controller.

Rebuilding the Animator Controller with Blend Trees

Open the Animator Controller assigned to Player. The following examples call it PlayerAC; if you used PlayerAnimController above, continue with that controller. In the Animator window, scroll to zoom and use the middle mouse button to pan. A two-column Project panel can also make the animation assets easier to scan.

Delete the existing animation states to clear the controller. The player’s Animation window temporarily showing no clip is expected at this stage.

Empty Unity Animator window for the PlayerAC controller showing only the Any State and Entry nodes after all previous states have been deleted.

Right-click in the graph, create a state of type Blend Tree, and name it idle. Double-click it and add two Motion fields in the Inspector:

  • First motion: PlayerAnimIdleL, the left-idle clip.
  • Second motion: the right-idle clip, labeled PlayerAnimIdleR in the controller example.

Keep the default Blend parameter and Automate Thresholds enabled. Preview the tree in the Animator and move its blend slider to see the character switch between left and right.

Unity Animator Blend Tree named Idle configured with two motion fields, PlayerAnimIdleL and PlayerAnimIdleR, driven by the Blend parameter with automate thresholds on.

Create a second blend tree named run. Add PlayerAnimRunL as its first motion and the right-run clip as its second. The controller examples call that right-run clip PlayerAnimRunR; use the right-facing PlayerAnimRun clip you created earlier.

Save and preview the run tree, adjusting Blend to check its two directions. You can test facing with the slider now, but driving this parameter from movement code is a later step.

Setting Up Transitions Between Idle and Run

Return to the controller’s main graph. Right-click idle, choose Make Transition, and connect it to run. Select the transition arrow and set:

  • Has Exit Time: unchecked
  • Fixed Duration: unchecked
  • Transition Duration: 0

Unity Inspector showing transition settings from Idle to Run with Has Exit Time unchecked, Fixed Duration unchecked, Transition Duration at zero, and the Conditions list open.

These settings let the animation switch without waiting for the current clip to finish or blending over a transition duration.

In Parameters, add a bool named isMoving. Add it to the idle-to-run transition’s Conditions and set it to true. Keep this separate from Blend, which controls left and right facing.

Create a transition from run back to idle. Apply the same timing settings, but set its isMoving condition to false.

Unity Animator window with Idle and Run states connected by transitions in both directions, and the Parameters panel on the left showing the new isMoving bool parameter.

Driving the Transitions from PlayerMovement

Open PlayerMovement. Add a private Animator field and cache it in Start alongside the Rigidbody2D reference. The fields and movement portion of the script now look like this:

using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerMovement : MonoBehaviour
{
    [SerializeField] private float _movementSpeed;
    private Rigidbody2D _rb;
    private Vector2 _movementInput;

    private Animator _animator;

    void Start()
    {
        _rb = GetComponent<Rigidbody2D>();
        _animator = GetComponent<Animator>();
    }

    void Update()
    {
        _rb.linearVelocity = _movementInput * _movementSpeed;
    }
}

Next, update the Move callback in the same class. Start by setting isMoving to true. When the action is released, context.canceled identifies that cancellation so you can set the boolean back to false:

public void Move(InputAction.CallbackContext context)
{
    _animator.SetBool("isMoving", true);

    if (context.canceled)
    {
        _animator.SetBool("isMoving", false);
    }

    _movementInput = context.ReadValue<Vector2>();
}

The order matters: the callback first assumes movement, then overrides that value if the action was canceled. Finally, it stores the movement vector that Update uses. Without the cancellation check, the Animator would switch to running but never return to idle.

Testing in Play Mode

Save the script and enter Play Mode. With no movement input, the character should stay in the idle blend tree. Hold a movement key to switch to run, then release it to return to idle. The zero-duration transitions and disabled exit time make that switch immediate.

Unity Editor in Play mode showing the wizard player character in the Game view on a blue background.

The character still faces left regardless of its movement direction. That is expected here because the Blend parameter has not yet been connected to input in code.

Challenge: Face the Right Direction

As your next challenge, use the horizontal component of _movementInput to decide which direction the sprite should face. The hint is to set the Animator’s Blend parameter from code, just as you have set isMoving. The facing-direction implementation comes after the movement and transition setup covered here.

Recap and Next Steps

You have replaced a moving circle with a pixel-art wizard whose idle and run animations respond to input. Your setup now includes:

  • A movement callback that reads a Vector2 and uses it to set Rigidbody2D velocity.
  • Sprite import settings and grid slicing for crisp, consistently aligned frames.
  • Player components transferred to the animated character with the input event reference corrected.
  • Directional idle and run blend trees, with an isMoving boolean controlling immediate transitions.

You can now work on the facing-direction challenge to connect the blend parameter to movement. Continue building the project with the Unity Game Development Mini-Degree for the full guided course and its next steps.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image - How to Add Unity 2D Player Movement to a Bullet Hell Game

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.