Sunday, April 5, 2020

Tutorial: How to Make a 2d Platformer in Unity - Platformer Basics

Setting Up the Player Character:

The source code for this post can be found here or at the bottom of the page.

First, create a script named Player and attach it to your character's GameObject.  Next we need to change the character's layer.  In the Inspector, at the top right is the layer selector.  Click it and select add new layer.  Then, in the layer 8 field type Player.  Go back to your character's Inspector window and change the layer to Player.

To finish, the character needs a CapsuleCollider2D (it is important that we use a CapsuleCollider2D because the rounded edges are necessary to navigate slopes) and a Rigidbody2D.  The Rigidbody2D must have the following settings: In the Inspector set BodyType to Dynamic, click the checkbox next to Simulated, and click the arrow next to Constraints to expand it and click the checkbox next to FreezeRotation>Z.  If you don't freeze the Z rotation you will end up with the problem in the gif below.
Adjust the CapsuleCollider2D to encompass the character.  Below is a screen shot of the settings I used and all the required components.  Ensure that your character has the Player script, an Animator with attached AnimatorController, a CapsuleCollider2D, a RigidBody2D and a SpriteRenderer.  If you need help setting up an AnimatorController check out my Creating Animations Tutorial.

Let's start coding.  

Open the Player script.  First we need a list of Components.  Add the following code inside the Player class declaration.

   //components  
   Rigidbody2D playerRig;  
   Animator playerAnimator;  
   SpriteRenderer playerSpriteRenderer;  
   CapsuleCollider2D playerCapsuleCollider2D;  

Then, add these lines to the Start function.

   void Start()  
   {  
     playerRig=gameObject.GetComponent<Rigidbody2D>();  
     playerAnimator = gameObject.GetComponent<Animator>();  
     playerSpriteRenderer = gameObject.GetComponent<SpriteRenderer>();  
     playerCapsuleCollider2D = gameObject.GetComponent<CapsuleCollider2D>();  
   }  

These lines will grab the components we need or you can also set them to public and fill in the reference in the scene.

Next, we need to create new variables and constants.  Add the following to the class declaration.
   const string movingFloatName = "Moving";  
   const float movementSpeed = 5f, jumpPower=7f,  
     jumpGravityScale = 1f, fallingGravityScale = 5f;  
   public LayerMask groundedLayerMask;  
   Vector2 groundOverlapArea;  
   float groundOffset; 
movingFloatName is used so that we can use code auto completion when interacting with the Animator.  movementSpeed is used to control the speed that the character moves.  jumpPower is used to control jump height.  groundedGravityScale will be used to set the Rigidbody2D's gravity scale value when initially jumping.  fallingGravityScale is used to increase the fall speed of the character.  The groundedLayerMask will specify to Unity which layers are considered to be ground and setting it to public makes it much easier to adjust.  Make sure to set it include only Default.  groundOverlapArea is used to define an area that will detect when the player is on the ground.  By default Unity centers a sprite.  Because we want to use the feet of the sprite to determine when the character is standing on ground, we need to use an offset to determine the position of the feet.  groundOffset is a variable where we can store that offset value.  We need to add two lines of code to the Start function to set the last two values.
     groundOverlapArea = new Vector2((  
       playerCapsuleCollider2D.size.x / 2f) * .9f, .1f);  
     groundOffset = playerCapsuleCollider2D.offset.y -  
       playerCapsuleCollider2D.size.y / 2f; 
The X component of groundOverlapArea is set to half the width of the CapsuleCollider2D but we need to shrink the value down a little so that player won't be able to jump off of vertical walls.  The Y component is just a small value to determine how close the player needs to be to the floor to jump.  For the groundOffset we take the Y offset of the CapsuleCollider2D and subtract half the CapsuleCollider2D's height to get the position where it contacts the ground.

Now we need add some code to the Update function that will move the character around.
     //float x = Input.GetAxis("Horizontal");  
     float x = Input.GetAxisRaw("Horizontal");  
     playerRig.velocity = new Vector2(movementSpeed * x,  
     playerRig.velocity.y);  

We can use GetAxis or GetAxisRaw for horizontal movement.  The difference is that by default Unity adds smoothing to axes.  GetAxisRaw allows more precise movement because it doesn't have smoothing.  You can try both and select your preference.  To move the character we simply set the x velocity to horizontal axis multiplied by the character's movement speed and the Y velocity is just the current velocity.  If you hit play, your character will move but won't be facing the correct direction when moving left and won't play the run animation.

Next we need to set up the player state machine and fix the the character's facing error.  First we add an enum to the bottom (outside of the class declaration) of the script.
 public enum PlayerPhase  
 {  
   IDLE,JUMP  
 } 

Next, we need a state declaration that is under the class declaration.
 public PlayerPhase currentPlayerPhase; 

Finally, we need to add these lines to the update function.  To establish our player state and fix the character's facing error.
     if (currentPlayerPhase == PlayerPhase.IDLE)  
     {  
       if (x < 0)  
       {  
         playerAnimator.SetFloat(movingFloatName, 1f);  
         playerSpriteRenderer.flipX = true;  
         playerAnimator.speed = 1f;  
       }  
       else if (x > 0)  
       {  
         playerAnimator.SetFloat(movingFloatName, 1f);  
         playerSpriteRenderer.flipX = false;  
         playerAnimator.speed = 1f;  
       }  
       else  
       {  
         playerAnimator.SetFloat(movingFloatName, 0f);  
         playerAnimator.speed = .5f;  
       }  
     }  

We simply use the flipX value to determine where the player is facing and leave the value alone when the character is not moving to continue facing in the direction he was last moving.  We set the Animator float to 0 or 1 depending if the player is moving (if the Animator was set up the way I showed you in the second tutorial this should work and below I have included screenshot of the blend tree values I have used).  

So let's hit play and see how it works.
You may notice that this character's idle animation seems a little fast compared to the run animation.  This is because the idle animation has fewer frames but you can fix that easily if you wish to.  Just add this line of code any time you change the animation.
 playerAnimator.speed = .5f; 
Because we use a blend tree we can't just set speed value in the Animator.

Now we need to add jumping.  First we need a function that will determine if the player is on the ground.
   bool IsGrounded()  
   {   
     Vector2 a = new Vector2(  
       transform.position.x - groundOverlapArea.x,  
       transform.position.y + groundOffset);  
     Vector2 b = new Vector2(  
       transform.position.x + groundOverlapArea.x,  
       transform.position.y + groundOffset -  
       groundOverlapArea.y);  
   
     return (Physics2D.OverlapArea(a, b, groundedLayerMask));  
   }  
Point B is set to the bottom and left of the character's collider.  Point B extends below the character's feet and is on the right side of the collider.   We use the physics system to check if that logical box overlaps with anything included in our LayerMask.  Next, we need to add a call this function in the Update function and we need a Jump function.

Here is the Jump function:
   void Jump()  
   {  
     playerRig.gravityScale = jumpGravityScale;  
     playerRig.velocity = new Vector2(playerRig.velocity.x,  
       jumpPower);  
     currentPlayerPhase = PlayerPhase.JUMP;  

   }  
We set the gravityScale equal to jumpGravityScale (this blows the player to have a slower acceleration to his jump peak), Y velocity equal to jumpPower, and we set the Jump state.

Now we add calls to the IsGrounded and Jump functions in the Update function.  Below is the code added to the Update function in the Idle state code block.
       if (IsGrounded())  
       {  
         playerRig.gravityScale = fallingGravityScale;  
         
         if (Input.GetKeyDown(KeyCode.Space))  
         {  
           Jump();  
         }  
       }  

First, we check if the player is grounded.  Then, we check if the the spacebar was pressed. If so we make a call to the Jump function.  We also want to set the gravity scale back to 5f while the player is on the ground.

Then, we need to add the Jump state code block.
     else if (currentPlayerPhase == PlayerPhase.JUMP)  
     {  
       if (x < 0)  
       {  
         playerSpriteRenderer.flipX = true;  
       }  
       else if (x > 0)  
       {  
         playerSpriteRenderer.flipX = false;  
       }  
       if (IsGrounded())  
       {  
         if (playerRig.velocity.y <= 0)  
         {  
           currentPlayerPhase = PlayerPhase.IDLE;  
           playerRig.gravityScale = groundedGravityScale;  
         }  
       }  
       else if (playerRig.velocity.y <= 0)  
       {  
         playerRig.gravityScale = fallingGravityScale;  
       }  
     }  

Here is the result:
The character is affected by stronger gravity as soon as he reaches the arch of the jump.

The Next thing we should do is add a jump animation.  Create an animation using the jump sprites (adventurer-jump-00, adventurer-jump-01, adventurer-jump-03, adventurer-jump-03).  Then, add the animation to your Animator and add a bool parameter called Jump.  Create two transitions with the parameter Jump set to true and false.
Below I have included my transition settings:


Now we just need to add two lines of code.  First, to the Jump function:
 playerAnimator.SetBool(jumpBoolName, true); 

Then, add this line of code to the Update function after the grounded check in the Jump state code block:
       if (IsGrounded())  
       {  
         if (playerRig.velocity.y <= 0)  
         {  
           playerAnimator.SetBool(jumpBoolName, false);  
           currentPlayerPhase = PlayerPhase.IDLE;  
           playerRig.gravityScale = groundedGravityScale;  
         }  
       }  

And here is the result:

In the next post you will learn how to navigate slopes using physics materials.

The source code for this post can be found here.

No comments:

Post a Comment