Having the basis of our world is great, but if we don't have a player, it doesn't matter how nice the level looks. In this section, we will create the actual player that will walk around and move in the world:
The 2D and 3D Physics systems are not interchangeable as in either can be used but they cannot interact with each other. You'll need to choose one or the other when working on a project. We're using 3D right now, so you can have a good idea of the differences between 2D and 3D and what to look out for.
PlayerBehaviour
and open it up in your IDE of choice.With the PlayerBehaviour
script opened, let's first write down each of the issues we need to solve and make them functions. As programmers, it's our job to solve problems, and separating problems into smaller pieces will make it easier to solve each part rather than try to solve the entire thing all at once.
void FixedUpdate() { // Move the player left and right Movement(); // Sets the camera to center on the player's position. // Keeping the camera's original depth Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, Camera.main.transform.position.z); }
Update
function:void Update() { // Have the player jump if they press the jump button Jumping(); }
Update()
is great and is called every frame, but it's called at random times, leading to more instant but less constant things, such as input. Instead of that, FixedUpdate()
is a great function to use for things that need to happen consistently and for things like physics due to its fixed delta time (the Time.deltaTime
value we've been using previously changes depending on the frame rate). However, in a platformer, the player needs to feel a jump instantly, so that's why I put the Jumping
function inside of Update
.
So at this point, we have broken the player's behavior into two sections—their movement and their jumping.
// A reference to our player's rigidbody component private Rigidbody rigidBody; // Force to apply when player jumps public Vector2 jumpForce = new Vector2(0, 450); // How fast we'll let the player move in the x axis public float maxSpeed = 3.0f; // A modifier to the force applied public float speed = 50.0f; // The force to apply that we will get for the player's // movement private float xMove; // Set to true when the player can jump private bool shouldJump;
Start
function:void Start () { rigidBody = GetComponent<Rigidbody>(); shouldJump = false; xMove = 0.0f; }
Movement
function now:void Movement() { // Get the player's movement (-1 for left, 1 for right, // 0 for none) xMove = Input.GetAxis("Horizontal"); if (xMove != 0) { // Setting player horizontal movement float xSpeed = Mathf.Abs(xMove * rigidBody.velocity.x); if (xSpeed < maxSpeed) { Vector3 movementForce = new Vector3(1, 0, 0); movementForce *= xMove * speed; rigidBody.AddForce(movementForce); } // Check speed limit if (Mathf.Abs(rigidBody.velocity.x) > maxSpeed) { Vector2 newVelocity; newVelocity.x = Mathf.Sign(rigidBody.velocity.x) * maxSpeed; newVelocity.y = rigidBody.velocity.y; rigidBody.velocity = newVelocity; } } else { // If we're not moving, get slightly slower Vector2 newVelocity = rigidBody.velocity; // Reduce the current speed by 10% newVelocity.x *= 0.9f; rigidBody.velocity = newVelocity; } }
In this section of code, we use a different way to get input from the player, the GetAxis
function. When called, GetAxis
will return a value for the direction that you are moving in a particular axis. In this instance, -1
for going all the way to the left, 0
for stationary, and 1
for all the way to the right. GetAxis can return any number between -1
and 1
as using a game controller you may only slightly move the analog stick. This would allow you to sneak in an area rather than always be running. In addition to the Horizontal Axis, there are a number of others included in Unity by default, but we can also customize or create our own.
At this point, we can move left and right in the game, but we're unable to jump. Let's fix this now:
up
:Jumping
function:void Jumping() { if(Input.GetButtonDown("Jump")) { shouldJump = true; } // If the player should jump if(shouldJump) { rigidBody.AddForce(jumpForce); shouldJump = false; } }
Now, if we press Spacebar or Up, we will change the shouldJump
Boolean value to true. If it's true, then we'll apply the jumpForce
to our character.
Player
and then attach the newly created behavior to our player if you haven't done so already:Great start! We now have a player in our world, and we're able to move around and jump. However, if you keep playing with it, you'll note some of the issues that this has, namely the fact that you can always jump up as many times as you want and if you hold a direction key hitting a wall, you'll stay stuck in the air. This could make for interesting game mechanics, but I'm going to assume that this is not what you're looking for.
Next, before we solve our movement issues, I wanted to show you a tool that you can use as a developer to help you when working on your own projects. Add the following function to your script:
void OnDrawGizmos() { Debug.DrawLine(transform.position, transform.position + rigidBody.velocity, Color.red); }
OnDrawGizmos
is a function inherited by the MonoBehaviour
class that will allow us to draw things that will appear in the Scene view. Sure enough, if you play the game, you will not see anything in the Game view, but if you look at the Scene tab, you'll be able to see the velocity that our object is traveling by. To make it easier to see, feel free to hit the 2D button in the top toolbar on the Scene tab to get a side view:
In this example here, the red line is showing that I'm jumping up and moving to the left. If you'll look at the Scene view when the player is walking, you'll see little bumps occuring. This isn't something we'd like to see as we expect the collision to flow together. These bumps occur because the moment that we hit the edges of two separate boxes, the collision engine will try to push the player in different directions to prevent the collision from happening. After the collisions occur, the physics engine will try to combine both of those forces into one that causes these hickups. We can fix this by telling Unity to spend some extra time doing the calcuations.
Go into Unity's Physics properties by going to Edit | Project Settings | Physics. Change the Default Contact Offset property to 0.0001:
The Contact Offset property allows the collision detection system to predicatively enforce the contact constraint even when the objects are slightly separated, we decrease that number as we don't want the collisions to happen.
Now there's the matter of being able to jump anytime we want. What we want to have happen is to have the player not be able to jump unless they're on the ground. This is to prevent the case of being able to jump while falling:
private bool onGround; private float yPrevious;
Start
function:onGround = false; yPrevious = Mathf.Floor(transform.position.y);
Jumping
function, we just need to add the following code in bold: void Jumping()
{
if(Input.GetButtonDown("Jump"))
{
shouldJump = true;
}
// If the player should jump
if(shouldJump && onGround)
{
rigidBody.AddForce(jumpForce);
shouldJump = false;
}
}
Update
function, we will add in a new function for us to check whether we are grounded:// Update is called once per frame void Update () { // Check if we are on the ground CheckGrounded(); // Have the player jump if they press the jump button Jumping(); }
CheckGrounded
. This isn't exactly a simple issue to solve without math, so we will actually need to use some linear algebra to solve the issue for us as follows:void CheckGrounded() { // Check if the player is hitting something from // the center of the object (origin) to slightly below // the bottom of it (distance) float distance = (GetComponent<CapsuleCollider>().height / 2 * this.transform.localScale.y) + .01f; Vector3 floorDirection = transform.TransformDirection(-Vector3.up); Vector3 origin = transform.position; if (!onGround) { // Check if there is something directly below us if (Physics.Raycast(origin, floorDirection, distance)) { onGround = true; } } // If we are currently grounded, are we falling down or // jumping? else if ((Mathf.Floor(transform.position.y) != yPrevious)) { onGround = false; } // Our current position will be our previous next frame yPrevious = Mathf.Floor(transform.position.y); }
This function uses a Raycast to cast an invisible line (ray) from origin in the direction of the floor for a certain distance, which is just slightly further than our player. If it finds an object colliding with this, it will return true
, which will tell us that we are indeed on the ground.
In the game, we can leave the ground in two ways—by jumping or by falling down a platform. Either way, we will be changing our y position; if that's the case, we are no longer on the ground, so onGround
will be set to false
. The Floor
function will remove the decimal from a number to allow for some leeway for a floating point error.
Movement
function:// Movement() // if xMove != 0... if (xSpeed < maxSpeed) { Vector3 movementForce = new Vector3(1,0,0); movementForce *= xMove * speed; RaycastHit hit; if(!rigidBody.SweepTest(movementForce, out hit, 0.05f)) { rigidBody.AddForce(movementForce); } } // Etc.
The SweepTest
function will check in the direction the rigid body is traveling, and if it sees something within a certain direction, it will fill hit with the object that it touched and return true
. We want to stop the player from being able to move into the wall, so we will not add force if that's the case.
private bool collidingWall;
Start
:collidingWall = false;
// If we hit something and we're not grounded, it must be a wall // or a ceiling. void OnCollisionStay(Collision collision) { if (!onGround) { collidingWall = true; } } void OnCollisionExit(Collision collision) { collidingWall = false; }
You'll note that the functions look quite similar to the 2D functions aside from not having the word 2D.
Movement
function, add the following bolded lines:void Movement() { //Get the player's movement (-1 for left, 1 for //right, 0 for none) xMove = Input.GetAxis("Horizontal"); if(collidingWall && !onGround) { xMove = 0; } // Etc.
Now if we are colliding against a wall, we will stop the player from applying force.
Now our player can jump along walls and fall like normal, and the player can jump only when he is on the ground! We now have the basis to make a platformer game completed!