Monday, April 6, 2020

Tutorial: How to Make a 2d Platformer in Unity - Custom Physics Shapes on Tiles and Slopes

Creating Custom Physics Shapes:

The source code used for this post is available here.
Open the sprite Editor and click on the ground_46 sprite.  It looks like this:
At the top left of the Window click on the dropdown that says Sprite Editor.  Select Custom Physics Shape.  Then, click in drag inside that sprite to begin creating a shape or you can click the generate button to have Unity generate a shape.  Next, click on the white squares and drag them to create the shape that best fits the sprite.  Also make sure to toggle on snap at the top of the window to make the process easier.  The most important thing is to make sure that neighboring sprites have shapes that line up well.  Once you have the shape you want hit apply to save the shape.  The Tile Palette will automatically be updated.  
 Create Custom Physics Shapes for the following Tiles (ground_1, ground_33, ground_47, ground_48,) and add them all to the Tile Palette to paint with. Below is a screen shot of the Tile Palette I created.

If the Tilemap Collider doesn't update correctly try disabling and reenabling it.

Another error I encountered while making this Tilemap is in the screenshot below.  If you encounter this problem you can solve it by making a sprite material Pixel Snap enabled and apply it to your TilemapRenderer.


Your Tilemap should look like the screenshot below.  Some shortcut keys will help you achieve this result.  
Press [ : Rotate Tile Counter Clockwise
Press ] : Rotate Tile Clockwise
Press Shift+[ : Flip Tile on X axis
Press Shift+] : Flip Tile on Y axis

Our character will have a few issues if we hit play right, now.  The character will launch into the air when reaching the top of a hill and if you move against a wall while in the air character will get stuck.  I'll illustrate this below. 

To fix the stuck to wall problem the character needs a physics material with no friction.  Right click in the Project window and click Create>Physics Material 2D.  Set the material's friction to 0.  This creates another problem which is illustrated below.  This character while lacking friction will simply slide down hills.

To fix this we need to add these we need to freeze the Rigidbody2D's X position when the character isn't moving.  We can do this by adding these lines that are highlighted in red:
     if (x < 0)  
     {  
       playerRig.constraints =  
         RigidbodyConstraints2D.FreezeRotation;  
       playerAnimator.SetFloat(movingFloatName, 1f);  
       playerSpriteRenderer.flipX = true;  
       playerAnimator.speed = 1f;  
     }  
     else if (x > 0)  
     {  
       playerRig.constraints =  
         RigidbodyConstraints2D.FreezeRotation;  
       playerAnimator.SetFloat(movingFloatName, 1f);  
       playerSpriteRenderer.flipX = false;  
       playerAnimator.speed = 1f;  
     }  
     else  
     {  
       playerRig.constraints =  
         RigidbodyConstraints2D.FreezeRotation |  
         RigidbodyConstraints2D.FreezePositionX;  
       playerAnimator.SetFloat(movingFloatName, 0f);  
       playerAnimator.speed = .5f;  
     }  

Next, we need to fix this problem the causes the character to fly up in the air when hitting the top of a wall:
This is caused by the bottom of the capsule collider pressing against another collider and sliding by the collider.  The character gains upward momentum while the sliding against the other collider.  To fix this we will just keep the character from gaining this momentum.  We need to create a new variable to determine when the character is jumping off the ground.
bool leftGround; 

Then we need to add code to use this variable:

In the Idle code block, add this line:
 if (IsGrounded())  
       {  
         playerRig.gravityScale = groundedGravityScale;  
         leftGround = false;  
         if (Input.GetKeyDown(KeyCode.Space))  
         {  
           Jump();  
         }  
       }  


In the Jump state code block add this code:
       if (IsGrounded())  
       {  
         if (playerRig.velocity.y <= 0)  
         {  
           playerAnimator.SetBool(jumpBoolName, false);  
           currentPlayerPhase = PlayerPhase.IDLE;  
           playerRig.gravityScale = groundedGravityScale;  
         }  
         else if (leftGround)  
         {  
           playerRig.gravityScale = groundedGravityScale;  
         }  
       }  
       else if (playerRig.velocity.y <= 0)  
       {  
         playerRig.gravityScale = fallingGravityScale;  
       }  
       else  
       {  
         leftGround = true;  
       }  
What this does is apply higher gravity when the character returns to the ground after jumping.  The result is this:




That finishes up navigating slopes.  The next post will cover parallax backgrounds.


The source code used for this post is available here.

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.