Yet Another Way to Tie Strings to a Puppet
Micha Davis · Follow
6 min read · Jun 3, 2021
--
If you’re keeping track (and, given the blog, it’s clear that I am) this will be the fifth character controller I’ve built from scratch in Unity. And each one has involved slightly different methods of input or moving the player. That’s one great thing about Unity: there are countless paths leading to your goal. All you need to do is pick one.
So, this time we’ll use Unity’s rigidbody physics to add values to the rigidbody’s velocity property. This being a 2D game, we’re only concerned with 2 vector coordinates, but everything works the same with three. And you’ll see at least one point in a future article where it’s necessary to refer to three vector coordinates even though we’re nominally operating in two.
Preparing the Character
In order for the control scripting to work we will need the following components on our player:
- Rigidbody 2D — This is the component we’ll be modifying to add velocity vectors to the player object. The default settings will be fine.
- Box Collider 2D — So we can collide with the floor. Make sure it fits the player sprite snugly.
- Player Input — This is how Unity’s Input System routes input information to our script. See below for configuring this component.
To configure the Player Input component, first click in the Actions area and create a new Input Actions Asset. It will suggest naming the asset after your project, but that seems like a bad idea. I called mine “Input_Actions” and created a folder for it in my Project tree. Opening that asset reveals the Input Actions editor.
Clear away the default actions (except for Move) and delete the UI category. Click the + button in the actions to define a new action for Jump (the Attack action shown here will be covered in a future article). Drop down the action and define a binding using the + button for that action. This will be a Button Action Type.
Close the Input Actions editor and select the Input Actions asset. In the Inspector, there is a box to check labeled Generate C# Class. Enable that and Apply. This will create a script with (among other things) an interface for passing input information to our Player class.
To implement the interface, we’ll need to create a Player class and add the interface declaration to our class definition and then define the methods required by the interface.
Next we need to go back to the Player and set the Player Input Behavior to Invoke Unity Events.
Then open the Events detail dropdown, open the Player detail dropdown, and we’ll see our actions from the IPlayerActions interface. Drag the Player object from the Hierarchy into the space provided and select the appropriate method from the Player script component.
We’re now ready to start scripting our movement.
NOTE: In the following sections I’ll be removing references to the animation system in my code. I won’t be covering the process of animating the character in these articles. If you’d like to read more about that, you can see my previous article series for my 2.5D platformer game.
Moving
To move the character, I need to fill out the OnMove() method provided by the IPlayerActions interface. The Input System can send input information as a value between 0 and 1, which is exactly what we need for vector-based movement. So we will use the callback context method ReadValue<T>(), where T will be a Vector2 (because we’re potentially getting input from the left thumbstick, which has two axes). We only actually need one axis from the vector, so we’ll isolate the x-axis value and pass that to the rigidbody component’s velocity property while maintaining whatever y-axis velocity may already exist.
Codewise, that looks like this:
Now whenever the user applies x-axis input, the rigidbody will move appropriately. You’ll notice this controller is different from past movement implementations which always relied on translating the transform of the object — those implementations needed a maximum distance per frame in the form of a speed value multiplied by the time elapsed since the last frame. Rigidbody velocity calculations handle the application of time to the formula for us. All we need to do is supply a speed multiple to the input vector. Without a speed multiple, the object will move one unit per second while input is applied.
Jumping
Jumping is a lot like moving. All we need to do is take input information (this time as a Button, not a value) and apply a jump force variable to our rigidbody’s y-axis velocity while maintaining whatever x-axis velocity may already exist. The main difference is we need to make sure we are touching the ground when we jump. We’re not using Unity’s Character Controller component this time, so there’s not a convenient “isGrounded” property to ask about.
Instead, we’ll be using Unity’s Physics2D.Raycast() to detect the ground. I don’t need to be constantly checking, so I won’t be casting rays in Update() every frame. Instead I’ll use a return function, a coroutine, and a couple of bool variable to toss information around and gatekeep code blocks where it is needed, when it is needed.
Here’s the required C# methods:
Picking this apart a bit: you can see that we first check to see if the player is grounded when a jump is attempted. If the Raycast finds a collider below the player on layerMask 8 (my ground layer), then the function returns true. We then apply velocity to the rigidbody based on the jumpForce variable. We set a grounded bool variable to false — we don’t want to fire another ray to confirm that, as we’re not very far off the ground in the first frame. We know we’ve successfully jumped, so we can safely declare ourselves off the ground without checking.
Last in the OnJump() method, we call the JumpFrameSkipRoutine. This coroutine keeps the Update method from checking for the ground until we’ve had time to actually clear the ground. Originally I thought a single frame would do it (thus the name of the coroutine), but it turned out that a tenth of a second was necessary to get out of ray range. A shorter ray fails to detect the ground properly, so this is a compromise measure.
Once we’re clear of the ground, the coroutine signals that a sufficient wait has passed and the Update method starts looking for the ground again, sets the grounded bool variable to whatever the Grounded() method finds and turns itself off again.
This way, I’m only firing rays when I absolutely need to know where the ground is. If the Update method is constantly looking for the ground, it causes input problems and makes ground detection buggy.
That’s all I have for today. Tomorrow I’ll be using class inheritance and virtual methods to create a modular enemy AI system.
Until then.