Outline | Previous: SpikeBall | Next: HoverGuy
Add a character to the scene that can walk and jump.
- 4.1) Create an Animated Character
- 4.2) Move Left & Right
- 4.3) Add a Player Controller
- 4.4) Turn Around
- 4.5) Jump
- 4.6) Platform Effectors
YouTube | Source before | Source after
Add a GameObject for the character with a walk animation.
How
Set the pivot point:
- Select all the character sprites. We are using kenney_platformercharacters/PNG/Adventurer/Poses/*.
- Pivot: Bottom
Create character:
- Hold Ctrl to select both adventurer_walk1 and 2.
- Drag them into the Scene.
- When prompted, save the animation as Assets/Animations/CharacterWalk.anim
- Select the GameObject just created:
- Order in Layer: 20
- Sprite: adventurer_idle
- Create an empty parent GameObject named "Character":
- Add the sprite GameObject as a child and set the position to 0.
Add a rigidbody and collider:
- Select the Character's parent GameObject:
- Add a Rigidbody2D.
- Expand the 'Constraints' and Check Freeze Rotation: Z.
- Add a Rigidbody2D.
- Add a CapsuleCollider2D to the Character's parent:
- Click 'Edit Collider' and adjust to fit the character.
- Hold Alt while adjusting the sides to pull both sides in evenly.
- Click 'Edit Collider' and adjust to fit the character.
Test:
- The Character should land on a platform and appear to be walking in place.
What does Pivot do?
A pivot point is the main anchor point for the sprite. By default, pivot points are at the center of the sprite.
For the character, we are moving the pivot point to the 'Bottom'. This allows us to position and rotate the character starting at the feet instead of the center of his body.
Here's an example showing a character with a default 'Center' pivot, and one with the recommended 'Bottom' pivot. They both have the same Y position. Notice the vertical position of each character as well as how the rotation centers around the different pivot points:
The pivot point you select is going to impact how we create animations and implement movement mechanics. The significance of this topic should become more clear later in the tutorial.
What's the difference between Animation and Animator?
Dragging multiple sprites into the Hierarchy created:
- The character's GameObject.
- A SpriteRenderer component on the GameObject defaulting to the first selected sprite.
- An Animation representing those 2 sprites changing over time.
- An Animator Controller for the character with a default state for the Walk animation.
- An Animator component on the GameObject configured for the Animator Controller just created.
An animation is a collection of sprites on a timeline, creating an animated effect similar to a flip book. Animations can include transform changes, fire events for scripts to react to, etc. to create any number of effects.
An animator controls which animations should be played at any given time. An animator uses an animator controller, which is a state machine used to select animations.
A state machine is a common pattern in development where logic is split across several states. The state machine selects one primary state, which owns the experience until the state machine transitions to another state. Each animator state has an associated animation to play. When you transition from one state to another, Unity switches from one animation to the next.
We will be diving into more detail about animations and animators later in the tutorial.
How do I know what size to make the collider?
The collider does not fit the character perfectly, and that's okay. In order for the game to feel fair for the player, we should lean in their favor. When designing colliders for the character and enemies, we may prefer to make the colliders a little smaller than the sprite so that there are no collisions in-game which may leave the player feeling cheated.
As the character animates, its limbs may be in different positions. The collider won't always fit the character and for that reason we use a collider focused around the body.
In addition to killing the character when he comes into contact with an enemy, the collider is used to keep the character on top of platforms. For this reason, it's important that the bottom of the collider aligns with the sprite's feet.
Why not use a collider that outlines the character?
Bottom line, it's not worth the trouble. Unity does not provide good tools for more accurate collisions on animated sprites. Implementing this requires a lot of considerations and may be difficult to debug.
Most of the time, the collisions in the game would not be any different if more detailed colliders were used. Typically 2D games use an approach similar to what this tutorial recommends. It creates a good game feel, and the simplifications taken have become industry standard.
Why freeze rotation, and does freezing mean it can never change?
We freeze the character so he does not fall over on the slanted platforms like this:
Adding constraints to the rigidbody only limits the Unity physics engine. Freezing the rigidbody position or rotation means that even if you got hit by a bus, you would not move or rotate. However, you could have a custom component set the position or rotation at any time.
Later in the tutorial, we will be writing a script to rotate entities so that they align with platforms (i.e., their feet sit flat on the floor).
We use constraints to remove capabilities from Unity, allowing us more control where we need it. Specifically here, that means our character is not ever going to fall flat on his face.
YouTube | Source before | Source after
Add a script to the character to be able to move left and right once a controller is added.
How
Create WalkMovement:
- Create script Code/Movement/WalkMovement:
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class WalkMovement : MonoBehaviour
{
public float desiredWalkDirection;
[SerializeField]
float walkSpeed = 100;
Rigidbody2D myBody;
protected void Awake()
{
myBody = GetComponent<Rigidbody2D>();
}
protected void FixedUpdate()
{
float desiredXVelocity
= desiredWalkDirection
* walkSpeed
* Time.fixedDeltaTime;
myBody.velocity = new Vector2(
desiredXVelocity,
myBody.velocity.y);
}
}
Configure Character:
- Add WalkMovement to the Character.
Test:
- Hit play and then select the Character. Change the desiredWalkDirection, and the Character should begin moving left or right.
- Note that the Character will not turn around when walking the other way yet.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.Debug
- UnityEngine.MonoBehaviour
- UnityEngine.RangeAttribute
- UnityEngine.RequireComponentAttribute
- UnityEngine.Rigidbody2D
- UnityEngine.SerializeFieldAttribute
- UnityEngine.Time
- UnityEngine.TooltipAttribute
- UnityEngine.Vector2
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a Rigidbody2D component on the GameObject.
[RequireComponent(typeof(Rigidbody2D))]We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class WalkMovement : MonoBehaviour
{This is a public field for other components to manipulate. It should be set to a value between -1 and 1; where -1 means walk full speed to the left, 0 means stop walking, and 1 means walk full speed to the right.
public float desiredWalkDirection;This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This represents the max walk speed for this GameObject. The default value here may be changed in the Inspector.
float walkSpeed = 100;This is a cached reference to the rigidbody for this GameObject. Here to improve performance.
Rigidbody2D myBody;Awake is a Unity event which is called once for a component when it's first added to a Scene.
protected is optional here. Used for consistency.
protected void Awake()
{Here we get a reference to the rigidbody on this GameObject.
myBody = GetComponent<Rigidbody2D>();
}FixedUpdate is a Unity event which is called every x ms of game time, until disabled or destroyed.
protected is optional here. Used for consistency.
protected void FixedUpdate()
{Here we calculate the speed to walk this FixedUpdate, based on the input desiredWalkDirection.
float desiredXVelocity
= desiredWalkDirection
* walkSpeed
* Time.fixedDeltaTime;Every FixedUpdate, set the X velocity on this GameObject. The Y velocity is not impacted, allowing things like gravity to continue.
myBody.velocity = new Vector2(
desiredXVelocity,
myBody.velocity.y);
}
}What's a controller? Why not read input here?
As discussed in Chapter 3, Unity encourages a component-based solution. This means that we attempt to make each component focused on a single mechanic or feature. Doing so simplifies debugging and enables reuse. For example, we will be creating another enemy type which will use the same WalkMovement component created for the character above.
Why set velocity instead of using AddForce?
AddForce is a way of impacting a rigidbody's velocity indirectly. Anytime you interact with either AddForce or velocity, a similar mechanic could be made using the other.
Generally, the game feel when using AddForce has more gradual changes, and for many experiences that's great. Although there are lots of options for tuning the forces experience, velocity simply gives you more direct control.
That is to say, you could use AddForce here instead. Maybe give it a try and see how it feels. We select velocity because we want the controls for moving left and right to feel crisp. Later in the tutorial, we will use AddForce for the jump effect.
Why FixedUpdate instead of Update?
Update occurs once per rendered frame. FixedUpdate occurs at a regular interval, every x ms of game time. FixedUpdate may run 0 or more times each frame.
FixedUpdate is preferred for mechanics which either require some level of consistency or apply changes incrementally. Physics in Unity are processed in FixedUpdate. Therefore, when manipulating physics for the game such as we are here by changing velocity on the rigidbody, we use FixedUpdate in order to match Unity's expectations.
Why multiply by Time.fixedDeltaTime?
It's optional. Anytime you make a change which includes some speed, such as walking, we multiply by the time elapsed so motion is smooth, even when the frame rate may not be. While using FixedUpdate, the time passed between calls is always the same, so Time.fixedDeltaTime is essentially a constant.
If speed is being processed in an Update, you must multiply by Time.deltaTime for a smooth experience. While in FixedUpdate, you could opt not to use Time.fixedDeltaTime; however, leaving it out may lead to some confusion, as fields which are configured for FixedUpdate may have a different order of magnitude than fields configured for use in Update.
Additionally, you may choose to adjust the time interval between FixedUpdate calls while optimizing your game. By consistently multiplying by the delta time, you can adjust the interval for FixedUpdate without changing the game play.
Why not use HideInInspector on desiredWalkDirection?
desiredWalkDirection is intended to be used by another component, and not configured for the GameObject in the Inspector. This is unusual: normally, if it's not a value we want configured, we would not show it in the Inspector (using the HideInInspector attribute).
There are two reasons that we want to to expose desiredWalkDirection in the Inspector:
- Allows us to test WalkMovement now. By using the Inspector to test setting a desiredWalkDirection, we can have confidence in this component before working on a controller, simplifying debugging these components.
- While debugging the game, you can see changes made by the controller to better understand what is happening.
What do the Tooltip and Range attributes do?
The linked code with comments includes Tooltip and Range attributes not shown in-line above.
Tooltip adds a message to the field when you hover over it with your mouse. It is a way of adding notes/comments/reminders.
Range limits the values you enter in the Inspector.
YouTube | Source before | Source after
Add a script to the character to read user input and drive movement.
How
Create PlayerController:
- Create script Code/Movement/PlayerController:
using UnityEngine;
[RequireComponent(typeof(WalkMovement))]
public class PlayerController : MonoBehaviour
{
WalkMovement walkMovement;
protected void Awake()
{
walkMovement = GetComponent<WalkMovement>();
}
protected void FixedUpdate()
{
walkMovement.desiredWalkDirection
= Input.GetAxis("Horizontal");
}
}
Configure Character:
- Add PlayerController to the Character.
Test:
- Left and Right or A and D should cause the Character to walk.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.Debug
- UnityEngine.Input
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a WalkMovement component on the GameObject.
[RequireComponent(typeof(WalkMovement))]We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class PlayerController : MonoBehaviour
{This is a cached reference to the WalkMovement component for this GameObject. Here to improve performance.
WalkMovement walkMovement;Awake is a Unity event which is called once for a component when it's first added to a Scene.
protected is optional here. Used for consistency.
protected void Awake()
{Here we get a reference to the WalkMovement component on this GameObject.
walkMovement = GetComponent<WalkMovement>();
}FixedUpdate is a Unity event which is called every x ms of game time, until disabled or destroyed.
protected is optional here. Used for consistency.
protected void FixedUpdate()
{This reads player input for the horizontal axis, which by default is the left and right arrow keys and the keys A/D. The value returned from GetAxis should be -1 to 1, and that is sent to the WalkMovement component to cause this entity to walk.
walkMovement.desiredWalkDirection
= Input.GetAxis("Horizontal");
}
}What is an Input 'Axis' and how is it configured?
Unity offers several ways of detecting keyboard/mouse/controller input. 'Axis' is the recommended approach. Each input Axis may be configured in the inspector:
- Edit -> Project Settings -> Input.
- In the 'Inspector', you will find a list of supported input types.
You can add, remove, rename, and configure the inputs for your game. Inputs may also be reconfigured by the player at runtime. For more information about the various options, see Unity's description of the InputManager. We will be using the defaults for this tutorial.
To read / detect Input, Unity offers a few APIs, including:
- GetAxis: Gets the current state as a float. For example, horizontal may return 1 for right and -1 for left.
- GetButtonDown/GetButtonUp: Determines if a button was pressed or released this frame.
- GetMouseButtonDown/GetMouseButtonUp: Same as above, but for mouse buttons.
There are a ton of options - check out the complete list of Input APIs.
Why does desiredWalkDirection slowly change?
If you watch the desiredWalkDirection value in the Inspector, you may notice that the value does not switch from 0 to 1 immediately. This might be unexpected since we are using a keyboard and you would expect that a key is either down or not.
Unity, by default, uses a smoothing filter. This filter slowly ramps up the value to give a more natural feeling of acceleration.
If you do not want this smoothing effect, you can use GetAxisRaw instead.
Generally, the smoothing effect is desirable. You may want to try both and see how it feels.
YouTube | Source before | Source after
Flip the entity's sprite when they switch between walking left and right.
How
Create TurnAround:
- Create script Code/Movement/TurnAround:
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class TurnAround : MonoBehaviour
{
static readonly Quaternion flipRotation =
Quaternion.Euler(0, 180, 0);
Rigidbody2D myBody;
public bool isFacingLeft
{
get; private set;
}
protected void Awake()
{
myBody = GetComponent<Rigidbody2D>();
}
protected void FixedUpdate()
{
float xVelocity = myBody.velocity.x;
if(Mathf.Abs(xVelocity) > 0.1)
{
bool isTravelingLeft = xVelocity < 0;
if(isFacingLeft != isTravelingLeft)
{
isFacingLeft = isTravelingLeft;
transform.rotation *= flipRotation;
}
}
}
}
Configure Character:
- Add TurnAround to the Character.
Test:
- Walk left, then right. The Character should be facing forward.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.RequireComponentAttribute
- UnityEngine.Rigidbody2D
- UnityEngine.MonoBehaviour
- UnityEngine.Mathf
- UnityEngine.Quaternion
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a rigidbody component on the GameObject.
[RequireComponent(typeof(Rigidbody2D))]We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class TurnAround : MonoBehaviour
{This is the rotation to apply which will flip the sprite around. Cached here for performance.
static readonly Quaternion flipRotation =
Quaternion.Euler(0, 180, 0);This is a reference to the rigidbody for this GameObject. Here to improve performance.
Rigidbody2D myBody;This holds the last known facing direction for the entity. It defaults to false, indicating that the entity is facing right, which is how our sprites are currently defined.
This is a public auto-property with a private setter. This means that other components can read this value, but only this script may change it.
public bool isFacingLeft
{
get; private set;
}Awake is a Unity event which is called once for a component when it's first added to a Scene.
protected is optional here. Used for consistency.
protected void Awake()
{Here we get a reference to the rigidbody on this GameObject.
myBody = GetComponent<Rigidbody2D>();
}FixedUpdate is a Unity event which is called every x ms of game time, until disabled or destroyed.
protected is optional here. Used for consistency.
protected void FixedUpdate()
{Here we get the current speed the object is traveling on the X axis. Putting the xVelocity in a temp variable like we do here is optional.
float xVelocity = myBody.velocity.x;Only consider flipping if the velocity is not close to 0. The actual value you compare against here (currently .1) may need to be adjusted depending on the order of magnitude of velocity in your game.
if(Mathf.Abs(xVelocity) > 0.1)
{We determine that that the entity is walking towards the left if the speed is negative. Positive is walking to the right. 0 would not reach this line, c/o the if above.
bool isTravelingLeft = xVelocity < 0;Check if the direction for the entity should change from its current facing direction.
if(isFacingLeft != isTravelingLeft)
{This stores the new facing direction for the GameObject.
isFacingLeft = isTravelingLeft;Here we rotate the GameObject by 180 degrees, flipping the facing direction.
transform.rotation *= flipRotation;
}
}
}
}Why not compare to 0 when checking if there is no movement?
In Unity, numbers are represented with the float data type. Float is a way of representing decimal numbers, but is a not precise representation as you may expect. When you set a float to some value, internally it may be rounded ever so slightly.
The rounding that happens with floats allows operations on floats to be executed very quickly. However it means we should never look for exact values when comparing floats, as a tiny rounding issue may lead to the numbers not being equal.
In the example above, as the velocity approaches zero, the significance of whether the value is positive or negative is lost. It's possible that if we were to compare to 0, at times the float may oscillate between a tiny negative value and a tiny positive value, causing the sprite to flip back and forth.
YouTube | Source before | Source after
Add a script to the character to enable it to jump and update the player controller to match. Play a sound effect when an entity jumps.
How
Create JumpMovement:
- Create script Code/Movement/JumpMovement:
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(AudioSource))]
public class JumpMovement : MonoBehaviour
{
public bool jumpRequested;
[SerializeField]
AudioClip jumpSound;
[SerializeField]
float jumpSpeed = 7f;
Rigidbody2D myBody;
AudioSource audioSource;
protected void Awake()
{
myBody = GetComponent<Rigidbody2D>();
audioSource = GetComponent<AudioSource>();
}
protected void FixedUpdate()
{
if(jumpRequested)
{
myBody.AddForce(
new Vector2(0, jumpSpeed),
ForceMode2D.Impulse);
audioSource.PlayOneShot(jumpSound);
}
jumpRequested = false;
}
}
Configure Character:
- Add JumpMovement to the Character (this will automatically add an AudioSource):
- Select the Jump Sound on the Jump Movement component. We are using phaseJump1.
Update PlayerController:
- Update Code/Movement/PlayerController with the following (or copy paste from the full version):
Existing code
using UnityEngine;
[RequireComponent(typeof(WalkMovement))][RequireComponent(typeof(JumpMovement))]Existing code
public class PlayerController : MonoBehaviour
{
WalkMovement walkMovement; JumpMovement jumpMovement;Existing code
protected void Awake()
{
walkMovement = GetComponent<WalkMovement>(); jumpMovement = GetComponent<JumpMovement>();Existing code
}
protected void FixedUpdate()
{
walkMovement.desiredWalkDirection
= Input.GetAxis("Horizontal");
} protected void Update()
{
if(Input.GetButtonDown("Jump"))
{
jumpMovement.jumpRequested = true;
}
}Existing code
}
Test:
- Hit space to jump. You should hear the sound effect as well.
- Note that you can spam jump to start flying. This will be corrected when we add ground detection later in the tutorial.
Explain the code
JumpMovement:
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.AudioSource
- UnityEngine.Debug
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
- UnityEngine.Rigidbody2D
- UnityEngine.SerializeFieldAttribute
using UnityEngine;These are Unity-specific attributes which inform the editor that this script requires a rigidbody and AudioSource components on the GameObject.
You may want to change this script to make AudioSource optional.
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(AudioSource))]We inherit from MonoBehaviour, which allows this script to be added as a component on a GameObject.
public is optional here. Used for consistency.
public class JumpMovement : MonoBehaviour
{This is a public field for other components to manipulate. True means we should attempt a jump during the next FixedUpdate.
public bool jumpRequested;This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This is the sound file to play when the entity jumps. Set in the Inspector.
AudioClip jumpSound;This defines how much force to add to the entity when it jumps. The default value here can be changed in the Inspector.
[SerializeField]
float jumpSpeed = 7f;These are cached references to the rigidbody and AudioSource for this GameObject. Here to improve performance.
Rigidbody2D myBody;
AudioSource audioSource;Awake is a Unity event which is called once for a component when it's first added to a Scene.
protected is optional here. Used for consistency (explained more in the questions below).
protected void Awake()
{Here we get references to the rigidbody and audio source on this GameObject.
myBody = GetComponent<Rigidbody2D>();
audioSource = GetComponent<AudioSource>();
}FixedUpdate is a Unity event which is called every x ms of game time, until disabled or destroyed.
protected is optional here. Used for consistency.
protected void FixedUpdate()
{Do the jump if another component set jumpRequested to true since the last FixedUpdate.
if(jumpRequested)
{Here we add an impulse force to the GameObject, in the positive Y direction. This causes the entity to jump up.
myBody.AddForce(
new Vector2(0, jumpSpeed),
ForceMode2D.Impulse);This uses the AudioSource component on this GameObject to play the jump sound effect.
audioSource.PlayOneShot(jumpSound);
}Here we clear the jumpRequested flag so that we don't continue adding force every FixedUpdate, until a jump is requested again.
jumpRequested = false;
}
}
PlayerController updates:
This is a Unity-specific attribute which informs the editor that this script requires a JumpMovement component on the GameObject.
[RequireComponent(typeof(JumpMovement))]This is a cached reference to the JumpMovement component for this GameObject. Here to improve performance.
JumpMovement jumpMovement;Here we get a reference to the JumpMovement component on this GameObject.
jumpMovement = GetComponent<JumpMovement>();Update is a Unity event which is called each frame, until disabled or destroyed.
protected is optional here. Used for consistency.
protected void Update()
{Here we check if the configured Jump key was pressed since the last frame.
if(Input.GetButtonDown("Jump"))
{Request that the JumpMovement does the jump on its next FixedUpdate.
jumpMovement.jumpRequested = true;
}
}Why use AddForce here instead of velocity, and what's 'Impulse'?
As discussed above, when creating the WalkMovement component, you could always create mechanics using either AddForce or by modifying the velocity.
In this component, we are using AddForce to jump. Using velocity here instead would actually have created the same basic jump experience we are looking for.
Using AddForce for the jump may provide a better experience for some corner cases or future mechanics. For example, if we wanted to support double jump in this game, initiating the second jump while in the air would feel very differently.
What is ForceMode2D.Impulse and how is it different from ForceMode2D.Force?
These options have very similar effects on objects. The biggest difference is the scale (i.e., how much motion a value of X creates when using ForceMode2D.Impulse vs ForceMode2D.Force). The unit for Impulse is defined as force per FixedUpdate. The unit for Force is defined as force per second.
How do you know when to use Update vs FixedUpdate for Input and rigidbodies?
Unity recommends always using FixedUpdate when interacting with a rigidbody, as physics is processed in FixedUpdate.
There is nothing blocking you from changing the rigidbody in an Update loop. You could, for example, AddForce every Update. This is not recommended and may lead to inconsistent experiences.
For Input:
- When reading the current Input state (e.g., using Input.GetAxis), either FixedUpdate or Update is fine. For example, if you are checking the current position of the joystick, you'll get the same information in FixedUpdate and Update.
- If you need to modify a rigidbody based on current Input state, I recommend reading Input in FixedUpdate to keep it simple.
- When checking for an Input event (e.g., using Input.GetButtonDown), you must use Update. Input is polled in the Update loop. Since it's possible for two Updates to happen before a FixedUpdate, some events may be missed when checking only in FixedUpdate.
- Always read events in Update. Unity will not block or warn you when checking for an event in FixedUpdate, and most of the time it will work, but occasional bugs will arise.
Why is AudioSource on a GameObject vs just a clip?
Audio playback in Unity is built to support 3D audio. 2D audio refers to the left and right channels as the two 'dimensions'. 3D adds distance from your ear as the third dimension that causes an object making noise to be louder the closer it is to your ear. Additionally 3D sound is directional, so sounds to the player's left would be loudest in the left speaker. Note that audio is 2D by default.
Your 'ear' is typically the camera itself. This is managed by the AudioListener component which was placed on the Main Camera by default when the scene was created. You could choose to move this component to the character instead, if appropriate.
To enable 3D audio, sounds need to originate at a position in the world. We use the AudioSource component to play clips. As a component, it must live in a GameObject, which in turn must have a Transform -- the position we are looking for.
For consistency, 2D audio is played the same way. 2D means we don't have the features above - the clip sounds the same regardless of from where in the world it was initiated.
Alternatively, you could use the Unity API to play a clip as shown below. This API will create an empty GameObject at the location provided, add an AudioSource component to it, configure that source to use the clip specified, and have the AudioSource start playing. After the clip completes, the GameObject will be destroyed automatically.
[SerializeField]
AudioClip clip;
protected void Start()
{
AudioSource.PlayClipAtPoint(clip, new Vector3(5, 1, 2));
}Would two separate player controllers be a better component-based solution?
Maybe, but it feels like overkill. The value of separating components is to allow us to mix and match to create new experiences. In this tutorial, we have no use case for using just one or the other player controller mechanic (i.e., just support walking or just support jumping).
TODO why not set the AudioClip instead?
YouTube | Source before | Source after
Add a platform effector to each platform.
How
Add platform effectors:
- Expand the 'Platforms' container and select all of the Platform GameObjects.
- Add PlatformEffector2D:
- Surface Arc: 35
- Under the BoxCollider2D:
- Check Use by Effector.
- Add PlatformEffector2D:
Test:
- You should be able to jump through a platform and land on top.
What are Effectors?
Effectors in Unity are easy ways to add various mechanics to the game.
The PlatformEffector2D creates one-way collisions for our platforms. This allows entities to jump through a platform and land on top, a common mechanic for platformer games.
Unity offers several effectors you can use. They are not doing anything with these components that you technically could not have built yourself in a custom script, but that said, adding the one-way effect the PlatformEffector2D creates would not be easy to do.
Read more about the various 2d effectors in Unity including a conveyor belt, repulsion, and floating effects.
What does Surface Arc do and why not use a value of 1?
The surface arc for an effector changes the supported region, in this case the surfaces which are collidable. By reducing this, we are causing the sides to be treated as non-collidable like the bottoms are by default, preventing the character from sticking to the side in a strange way.
The surface arc is defined in degrees around the Transform's up direction and compared against the normal direction of the surface of the collider at the point of collision to determine if effects apply (in this case, if collisions apply).
A very small surface arc still allows the primary use case to work correctly, i.e., you can still stand on platforms. The sides, where a rounded edge appears, may not be collidable, causing the character to fall off prematurely.
You can adjust the surface arc to find a value that feels good.
Testing / debugging tips
- Try adjusting the WalkMovement's walkSpeed and the JumpMovement's jumpSpeed.
Questions, issues, or suggestions? Please use the YouTube comments for the best fit section.
Support on Patreon, with Paypal, or by subscribing on Twitch (free with Amazon Prime).
License. Created live at twitch.tv/HardlyDifficult August 2017.
