Outline | Previous: Floor Detection | Next: Hammer
Add ladders to level. Allow the player to climb up and down, and have enemies randomly climb as well.
- 9.1) Add Ladders
- 9.2) Climb Ladders
- 9.3) Disable Physics While Climbing
- 9.4) Random Climb Controller
- 9.5) Stay On Ladders
- 9.6) Move Towards the Center of the Ladder
YouTube | Source before | Source after
Create GameObjects and layout ladders in the world. Set their layer to Ladder.
Add a box collider to each of the ladders and size it to use for climbing and set it as a trigger collider. An entity will be able climb ladders when its bottom is above the bottom of the ladder's collider and its center is inside.
How
Layout ladders:
- Create a parent "Ladder" GameObject, add the ladder sprite. We are using Art/platformer-pack-redux-360-assets/PNG/Tiles/ladderMid.
- Order in Layer: -30
- You may need more than one copy of the sprite as a child in the Ladder GameObject.
- The child sprite GameObjects should have a position of 0, with the exception of the Y position when multiple sprites are used.
- It usually looks fine to overlap sprites a bit, as we do to get the space between ladder steps looking good.
- Position the ladder and the copy and paste to create several ladders.
- Create a parent "BrokenLadder" GameObject, and add the broken ladder sprite. We are using Art/platformer-pack-redux-360-assets/PNG/Tiles/fenceBroken.
- Follow the steps above for the Ladder.
Configure ladders:
- Create a parent "Ladders" GameObject:
- Set the position to 0.
- Add all of the Ladder and BrokenLadder GameObjects.
- Create a layer for "Ladder".
- Select all the Ladder and BrokenLadder GameObjects:
- Lader: Ladder
- Add FadeInThenEnable
Trigger colliders:
- Select all the Ladder and BrokenLadder GameObjects:
- Add BoxCollider2D:
- Check Is Trigger
- Size X: .6
- Size each individually such that:
- The bottom of the collider is:
- Just below the platform for complete ladders.
- Aligned with the last step of broken ladders.
- The top of the collider is just above the upper platform.
- The bottom of the collider is:
- Add BoxCollider2D:
Note that there is nothing to test until we add a script for climbing ladders.
YouTube | Source before | Source after
Add a script to climb ladders, and updated the player controller to match.
How
Create LadderMovement:
- Create script Code/Movement/LadderMovement:
using System;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(Collider2D))]
public class LadderMovement : MonoBehaviour
{
public float desiredClimbDirection;
public event Action onGettingOnLadder;
public event Action onGettingOffLadder;
public bool isOnLadder
{
get
{
return ladderWeAreOn != null;
}
}
[SerializeField]
float climbSpeed = 60;
[SerializeField]
ContactFilter2D ladderFilter;
Rigidbody2D myBody;
FloorDetector floorDetector;
static readonly Collider2D[] tempColliderList
= new Collider2D[3];
GameObject _ladderWeAreOn;
public GameObject ladderWeAreOn
{
get
{
return _ladderWeAreOn;
}
private set
{
if(ladderWeAreOn == value)
{
return;
}
_ladderWeAreOn = value;
if(ladderWeAreOn != null)
{
OnGettingOnLadder();
}
else
{
OnGettingOffLadder();
}
}
}
protected void Awake()
{
myBody = GetComponent<Rigidbody2D>();
floorDetector = GetComponentInChildren<FloorDetector>();
}
protected void FixedUpdate()
{
GameObject ladder = ladderWeAreOn;
if(ladder == null)
{
ladder = FindClosestLadder();
if(ladder == null)
{
return;
}
}
Bounds ladderBounds
= ladder.GetComponent<Collider2D>().bounds;
Bounds entityBounds = floorDetector.feetCollider.bounds;
if(isOnLadder == false)
{
TryGettingOnLadder(ladder, ladderBounds, entityBounds);
}
if(isOnLadder)
{
ConsiderGettingOffLadder(ladderBounds, entityBounds);
if(isOnLadder)
{
ClimbLadder();
}
}
}
public void GetOffLadder()
{
ladderWeAreOn = null;
}
void TryGettingOnLadder(
GameObject ladder,
Bounds ladderBounds,
Bounds entityBounds)
{
if(Mathf.Abs(desiredClimbDirection) > 0.01
&& IsInBounds(ladderBounds, entityBounds)
&& (
desiredClimbDirection > 0
&& entityBounds.min.y < ladderBounds.center.y
|| desiredClimbDirection < 0
&& entityBounds.min.y > ladderBounds.center.y))
{
ladderWeAreOn = ladder;
}
}
void ClimbLadder()
{
myBody.velocity = new Vector2(myBody.velocity.x,
desiredClimbDirection * climbSpeed * Time.fixedDeltaTime);
}
void ConsiderGettingOffLadder(
Bounds ladderBounds,
Bounds entityBounds)
{
float currentVerticalVelocity = myBody.velocity.y;
if(IsInBounds(ladderBounds, entityBounds) == false)
{
GetOffLadder();
}
else if(floorDetector.distanceToFloor < .3f
&& floorDetector.distanceToFloor > .1f)
{
if(currentVerticalVelocity > 0
&& entityBounds.min.y > ladderBounds.center.y)
{
GetOffLadder();
}
else if(currentVerticalVelocity < 0
&& entityBounds.min.y < ladderBounds.center.y)
{
GetOffLadder();
}
}
}
void OnGettingOnLadder()
{
if(onGettingOnLadder != null)
{
onGettingOnLadder();
}
}
void OnGettingOffLadder()
{
desiredClimbDirection = 0;
if(onGettingOffLadder != null)
{
onGettingOffLadder();
}
}
bool IsInBounds(
Bounds ladderBounds,
Bounds entityBounds)
{
float entityCenterX = entityBounds.center.x;
if(ladderBounds.min.x > entityCenterX
|| ladderBounds.max.x < entityCenterX)
{
return false;
}
float entityFeetY = entityBounds.min.y;
if(ladderBounds.min.y > entityFeetY
|| ladderBounds.max.y < entityFeetY)
{
return false;
}
return true;
}
GameObject FindClosestLadder()
{
int resultCount
= floorDetector.feetCollider.OverlapCollider(
ladderFilter, tempColliderList);
GameObject closestLadder = null;
float distanceToClosestLadder = 0;
for(int i = 0; i < resultCount; i++)
{
GameObject ladder = tempColliderList[i].gameObject;
Vector2 delta
= ladder.transform.position
- transform.position;
float distanceToLadder = delta.sqrMagnitude;
if(closestLadder == null
|| distanceToLadder < distanceToClosestLadder)
{
closestLadder = ladder;
distanceToClosestLadder = distanceToLadder;
}
}
return closestLadder;
}
}
Configure entities:
- Add LadderMovement to the Character.
- Update the Ladder filter:
- Check Use Layer Mask
- Check Use Triggers
- Layer: Ladder
- Update the Ladder filter:
Update PlayerController:
- Update Code/Movement/PlayerController:
Existing code
using UnityEngine;
[RequireComponent(typeof(WalkMovement))]
[RequireComponent(typeof(JumpMovement))]
public class PlayerController : MonoBehaviour
{
WalkMovement walkMovement;
JumpMovement jumpMovement; LadderMovement ladderMovement; Existing code
protected void Awake()
{
walkMovement = GetComponent<WalkMovement>();
jumpMovement = GetComponent<JumpMovement>(); ladderMovement = GetComponentInChildren<LadderMovement>();Existing code
}
protected void FixedUpdate()
{
walkMovement.desiredWalkDirection
= Input.GetAxis("Horizontal"); ladderMovement.desiredClimbDirection
= Input.GetAxis("Vertical");Existing code
}
protected void Update()
{
if(Input.GetButtonDown("Jump"))
{
jumpMovement.Jump();
}
}
}
Test:
- The Character should walk up a ladder when you press up.
- If already on a ladder, he can walk down as well. But he cannot yet climb through a Platform to go down.
Explain the code
LadderMovement:
'using' clauses at the top of a file brings APIs into scope. Used for:
- System.Action
- UnityEngine.Bounds
- UnityEngine.Collider2D
- UnityEngine.Debug
- UnityEngine.GameObject
- UnityEngine.LayerMask
- UnityEngine.Mathf
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
- UnityEngine.Rigidbody2D
- UnityEngine.SerializeFieldAttribute
- UnityEngine.Time
- UnityEngine.Vector2
using System;
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 LadderMovement : MonoBehaviour
{This is set by another component to get on a ladder, and once on one climb up or down. It should be set to -1 for down, 0 to stop, and 1 for up; any value in that range is okay.
public float desiredClimbDirection;These are public events for other components to react to getting on or getting off a ladder.
public event Action onGettingOnLadder;
public event Action onGettingOffLadder;This allows other components to know when we are on a ladder.
public bool isOnLadder
{
get
{
return ladderWeAreOn != null;
}
}This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This defines how quickly the entity can climb a ladder. You can change the default in the Inspector.
float climbSpeed = 60;Set to Use Triggers, Use LayerMask, and LayerMask Ladder.
[SerializeField]
ContactFilter2D ladderFilter;The rigidbody on this GameObject. Cached here for performance.
Rigidbody2D myBody;The FloorDetector component on this GameObject. Cached here for performance.
FloorDetector floorDetector;This is a list to use when calling OverlapCollider. Here so that we can reuse the array each time, improving performance.
It is static readonly since we only need one, which can be shared across objects and the array itself will never change.
The list is of size 3 so that we can check multiple ladders in the area.
static readonly Collider2D[] tempColliderList
= new Collider2D[3];A reference to the ladder we are currently climbing, if any.
This data backs the smart property below.
GameObject _ladderWeAreOn;This is a public smart property wrapping a reference to the ladder we are currently on. A smart property was used in order to easily fire events when the ladder changes.
public GameObject ladderWeAreOn
{Any component can get the reference to the ladder we are on.
get
{
return _ladderWeAreOn;
}A private set is used here so that only this component can change the ladder that we are on.
private set
{If setting the ladderWeAreOn to its current value then return so not to fire the events below.
if(ladderWeAreOn == value)
{
return;
}This stores the new value.
_ladderWeAreOn = value;Then we use the helper methods below to fire an event for getting on or off.
if(ladderWeAreOn != null)
{
OnGettingOnLadder();
}
else
{
OnGettingOffLadder();
}
}
}Awake is a Unity method which is called once, when a GameObject is first added to the scene.
protected is optional here. Used for consistency.
protected void Awake()
{Here we get references to components on this GameObject.
- By searching 'InChildren', we will find the FloorDetector on this GameObject or its child GameObject.
myBody = GetComponent<Rigidbody2D>();
floorDetector = GetComponentInChildren<FloorDetector>();
}FixedUpdate is a Unity method which is called every x ms of game time.
protected is optional here. Used for consistency.
protected void FixedUpdate()
{
GameObject ladder = ladderWeAreOn;Check if the entity is currently on a ladder.
if(ladder == null)
{The entity is are not on a ladder at the moment, use the helper method below to find the closest to the entity, if any.
ladder = FindClosestLadder();There is no ladder near the entity, return as there is nothing more to do.
if(ladder == null)
{
return;
}
}Get the bounds for the ladder we are on or near.
Bounds ladderBounds
= ladder.GetComponent<Collider2D>().bounds;Get the bounds for the entity, either the bounds of the entity's collider itself or its feet collider.
Bounds entityBounds = floorDetector.feetCollider.bounds;If not currently on a ladder, use the helper method below to consider getting on the one closest to the entity.
if(isOnLadder == false)
{
TryGettingOnLadder(ladder, ladderBounds, entityBounds);
}If on a ladder, use the helper method below to consider getting off.
if(isOnLadder)
{
ConsiderGettingOffLadder(ladderBounds, entityBounds);After all that, if the entity is on a ladder then use the helper method below to update its rigidbody.
if(isOnLadder)
{
ClimbLadder();
}
}
}This is a public method that other components may use to get off a ladder.
public void GetOffLadder()
{
ladderWeAreOn = null;
}This is a helper method to try getting on a nearby ladder.
void TryGettingOnLadder(
GameObject ladder,
Bounds ladderBounds,
Bounds entityBounds)
{This checks a few requirements before getting on a ladder:
- desiredClimbDirection is not 0.
- Confirm that the entity is still within climbing range of the ladder.
- Want to climb up, and the entity's bottom is below the center of the ladder; or want to climb down, and above the center.
if(Mathf.Abs(desiredClimbDirection) > 0.01
&& IsInBounds(ladderBounds, entityBounds)
&& (
desiredClimbDirection > 0
&& entityBounds.min.y < ladderBounds.center.y
|| desiredClimbDirection < 0
&& entityBounds.min.y > ladderBounds.center.y))
{Get on the ladder so we can start to climb.
ladderWeAreOn = ladder;
}
}A helper method for moving the entity while on a ladder.
void ClimbLadder()
{Here we set the Y velocity to the desiredClimbDirection and speed. This may be 0, allowing the entity to try and hold onto the ladder.
The X velocity is not impacted.
myBody.velocity = new Vector2(myBody.velocity.x,
desiredClimbDirection * climbSpeed * Time.fixedDeltaTime);
}This is a helper method for considering getting off a ladder.
void ConsiderGettingOffLadder(
Bounds ladderBounds,
Bounds entityBounds)
{
float currentVerticalVelocity = myBody.velocity.y;Check if the entity is no longer in the climbable region of the ladder, and then get off.
if(IsInBounds(ladderBounds, entityBounds) == false)
{
GetOffLadder();
}Check if the entity's feet are close to the floor.
else if(floorDetector.distanceToFloor < .3f
&& floorDetector.distanceToFloor > .1f)
{ Feet are near the floor, get off if the entity is going up and is above the top half.
if(currentVerticalVelocity > 0
&& entityBounds.min.y > ladderBounds.center.y)
{
GetOffLadder();
}Get off if the entity is going down and is below the bottom half.
else if(currentVerticalVelocity < 0
&& entityBounds.min.y < ladderBounds.center.y)
{
GetOffLadder();
}
}
}A helper method for firing an event when the entity gets on a ladder.
void OnGettingOnLadder()
{If any other component has subscribed to the onGettingOnLadder event, call those method(s) now.
if(onGettingOnLadder != null)
{
onGettingOnLadder();
}
}A helper method for firing an event when the entity gets off a ladder they were previously climbing.
void OnGettingOffLadder()
{Here we clear the desiredClimbDirection. This generally should not be required, but added to help prevent promptly getting back on the ladder.
desiredClimbDirection = 0;If any other component has subscribed to the onGettingOffLadder event, call those method(s) now.
if(onGettingOffLadder != null)
{
onGettingOffLadder();
}
}A helper method to check if the entity's bounds is within the climbing region of the ladder's bounds.
bool IsInBounds(
Bounds ladderBounds,
Bounds entityBounds)
{
float entityCenterX = entityBounds.center.x;Check if the entity's x position is within the ladder bounds. If not, the entity is not within the climbable region.
if(ladderBounds.min.x > entityCenterX
|| ladderBounds.max.x < entityCenterX)
{
return false;
}Check if the entity's min y position (i.e. the bottom of their feet) is within the ladder bounds.
float entityFeetY = entityBounds.min.y;
if(ladderBounds.min.y > entityFeetY
|| ladderBounds.max.y < entityFeetY)
{
return false;
}If we passed the checks above, the entity is within the ladder bounds and may climb the ladder.
return true;
}A helper method for finding a ladder near the entity to consider getting on.
GameObject FindClosestLadder()
{Here we call OverlapCollider to find all the ladders in the area.
The ladderFilter is used to find just ladders, as opposed to Platforms and other entities.
The tempColliderList is of size 3, which defines the max number of results returned.
int resultCount
= floorDetector.feetCollider.OverlapCollider(
ladderFilter, tempColliderList);Here we loop over each of the results, looking for the closest ladder.
Note that we loop to the resultCount as returned by the OverlapCollider call above and not the tempColliderList.Length, as there may be less results than the list's length.
GameObject closestLadder = null;
float distanceToClosestLadder = 0;
for(int i = 0; i < resultCount; i++)
{Calculate the distance to each of the ladders found.
We use sqrMagnitude for performance as we just want to find the closest, the actual magnitude does not matter here.
GameObject ladder = tempColliderList[i].gameObject;
Vector2 delta
= ladder.transform.position
- transform.position;
float distanceToLadder = delta.sqrMagnitude;Check if this is the first ladder we are considering or if its closer than the previous ladders, and then store this as the closest ladder.
if(closestLadder == null
|| distanceToLadder < distanceToClosestLadder)
{
closestLadder = ladder;
distanceToClosestLadder = distanceToLadder;
}
}Return the closest ladder found. If no ladders were found in the area, this value would be null.
return closestLadder;
}
}
Update PlayerController:
This holds a reference to the LadderMovement component on this GameObject. Cached here for performance.
LadderMovement ladderMovement; Here we get a reference to the LadderMovement component on this GameObject or its child GameObject.
ladderMovement = GetComponentInChildren<LadderMovement>(); Every FixedUpdate, read the vertical input, which is up / down or the keys W / S by default. This value is passed to the ladder movement component to consider getting on a ladder or to climb up or down.
ladderMovement.desiredClimbDirection
= Input.GetAxis("Vertical");Why use sqrMagnitude instead of magnitude?
In this example both magnitude and sqrMagnitude would give us the same result, as is the case anytime we are comparing if one distance is greater or less than another. sqrMagnitude executes much faster, so its preferred anytime you do not require the precision that magnitude gives you.
To calculate magnitude, you first calculate the squared magnitude and then take the square root. Taking the square root is a difficult operation.
YouTube | Source before | Source after
While climbing a ladder disable physics, allowing entities to climb down / through a platform.
How
Create DisablePhysics:
- Create script Code/Movement/DisablePhysics:
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
public class DisablePhysics : MonoBehaviour
{
Rigidbody2D myBody;
readonly List<Collider2D> impactedColliderList
= new List<Collider2D>();
protected void Awake()
{
myBody = GetComponent<Rigidbody2D>();
Collider2D[] colliderList = GetComponentsInChildren<Collider2D>();
for(int i = 0; i < colliderList.Length; i++)
{
Collider2D collider = colliderList[i];
if(collider.isTrigger == false)
{
impactedColliderList.Add(collider);
}
}
}
protected void OnEnable()
{
myBody.gravityScale = 0;
for(int i = 0; i < impactedColliderList.Count; i++)
{
Collider2D collider = impactedColliderList[i];
collider.isTrigger = true;
}
}
protected void OnDisable()
{
myBody.gravityScale = 1;
for(int i = 0; i < impactedColliderList.Count; i++)
{
Collider2D collider = impactedColliderList[i];
collider.isTrigger = false;
}
}
}
Update LadderMovement:
- Update Code/Movement/LadderMovement:
Existing code
using System;
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))][RequireComponent(typeof(DisablePhysics))] Existing code
public class LadderMovement : MonoBehaviour
{
public float desiredClimbDirection;
public event Action onGettingOnLadder;
public event Action onGettingOffLadder;
public bool isOnLadder
{
get
{
return ladderWeAreOn != null;
}
}
[SerializeField]
float climbSpeed = 60;
[SerializeField]
ContactFilter2D ladderFilter;
Rigidbody2D myBody;
FloorDetector floorDetector; DisablePhysics disablePhysics;Existing code
static readonly Collider2D[] tempColliderList
= new Collider2D[3];
GameObject _ladderWeAreOn;
public GameObject ladderWeAreOn
{
get
{
return _ladderWeAreOn;
}
private set
{
if(ladderWeAreOn == value)
{
return;
}
_ladderWeAreOn = value;
if(ladderWeAreOn != null)
{
OnGettingOnLadder();
}
else
{
OnGettingOffLadder();
}
}
}
protected void Awake()
{
myBody = GetComponent<Rigidbody2D>();
floorDetector = GetComponentInChildren<FloorDetector>(); disablePhysics = GetComponent<DisablePhysics>(); Existing code
}
protected void FixedUpdate()
{
GameObject ladder = ladderWeAreOn;
if(ladder == null)
{
ladder = FindClosestLadder();
if(ladder == null)
{
return;
}
}
Bounds ladderBounds
= ladder.GetComponent<Collider2D>().bounds;
Bounds entityBounds = floorDetector.feetBounds;
if(isOnLadder == false)
{
TryGettingOnLadder(ladder, ladderBounds, entityBounds);
}
if(isOnLadder)
{
ConsiderGettingOffLadder(ladderBounds, entityBounds);
if(isOnLadder)
{
ClimbLadder();
}
}
}
public void GetOffLadder()
{
ladderWeAreOn = null;
}
void TryGettingOnLadder(
GameObject ladder,
Bounds ladderBounds,
Bounds entityBounds)
{
if(Mathf.Abs(desiredClimbDirection) > 0.01
&& IsInBounds(ladderBounds, entityBounds)
&& (
desiredClimbDirection > 0
&& entityBounds.min.y < ladderBounds.center.y
|| desiredClimbDirection < 0
&& entityBounds.min.y > ladderBounds.center.y))
{
ladderWeAreOn = ladder;
}
}
void ClimbLadder()
{
myBody.velocity = new Vector2(myBody.velocity.x,
desiredClimbDirection * climbSpeed * Time.fixedDeltaTime);
}
void ConsiderGettingOffLadder(
Bounds ladderBounds,
Bounds entityBounds)
{
float currentVerticalVelocity = myBody.velocity.y;
if(IsInBounds(ladderBounds, entityBounds) == false)
{
GetOffLadder();
}
else if(floorDetector.distanceToFloor < .3f
&& floorDetector.distanceToFloor > .1f)
{
if(currentVerticalVelocity > 0
&& entityBounds.min.y > ladderBounds.center.y)
{
GetOffLadder();
}
else if(currentVerticalVelocity < 0
&& entityBounds.min.y < ladderBounds.center.y)
{
GetOffLadder();
}
}
}
void OnGettingOnLadder()
{ disablePhysics.enabled = true; Existing code
if(onGettingOnLadder != null)
{
onGettingOnLadder();
}
}
void OnGettingOffLadder()
{ disablePhysics.enabled = false; Existing code
desiredClimbDirection = 0;
if(onGettingOffLadder != null)
{
onGettingOffLadder();
}
}
bool IsInBounds(
Bounds ladderBounds,
Bounds entityBounds)
{
float entityCenterX = entityBounds.center.x;
if(ladderBounds.min.x > entityCenterX
|| ladderBounds.max.x < entityCenterX)
{
return false;
}
float entityFeetY = entityBounds.min.y;
if(ladderBounds.min.y > entityFeetY
|| ladderBounds.max.y < entityFeetY)
{
return false;
}
return true;
}
GameObject FindClosestLadder()
{
int resultCount
= floorDetector.feetCollider.OverlapCollider(
ladderFilter, tempColliderList);
GameObject closestLadder = null;
float distanceToClosestLadder = 0;
for(int i = 0; i < resultCount; i++)
{
GameObject ladder = tempColliderList[i].gameObject;
Vector2 delta
= ladder.transform.position
- transform.position;
float distanceToLadder = delta.sqrMagnitude;
if(closestLadder == null
|| distanceToLadder < distanceToClosestLadder)
{
closestLadder = ladder;
distanceToClosestLadder = distanceToLadder;
}
}
return closestLadder;
}
}
Configure Character:
- Add DisablePhysics to the Character.
- Disable the DisablePhysics component.
Test:
- The Character should be able to climb up and down ladders now.
Explain the code
DisablePhysics:
'using' clauses at the top of a file brings APIs into scope. Used for:
- System.Collections.Generic.List
- UnityEngine.Collider2D
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
- UnityEngine.Rigidbody2D
using System.Collections.Generic;
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a rigidbody 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 DisablePhysics : MonoBehaviour
{This is a reference to the rigidbody on this GameObject. Cached here for performance.
Rigidbody2D myBody;This is a list of all the non-trigger colliders on this GameObject or its children.
readonly List<Collider2D> impactedColliderList
= new List<Collider2D>();Awake is a Unity method which is called once, when the GameObject is first added to the scene.
protected is optional here. Used for consistency.
protected void Awake()
{This gets a reference to the rigidbody on this GameObject.
myBody = GetComponent<Rigidbody2D>();Here we get all of the colliders on this GameObject or its children.
Collider2D[] colliderList = GetComponentsInChildren<Collider2D>();Loop over each of the colliders found.
for(int i = 0; i < colliderList.Length; i++)
{
Collider2D collider = colliderList[i];Check if this is a trigger collider. If not, add it to the list of colliders this component will manipulate on enable / disable.
if(collider.isTrigger == false)
{
impactedColliderList.Add(collider);
}
}
}OnEnable is a Unity event which is called each time this component is enabled.
protected void OnEnable()
{Here we set the gravityScale for this GameObject to 0 which effectively disables gravity.
myBody.gravityScale = 0;Loop over each of the non-trigger colliders as found in Awake above.
for(int i = 0; i < impactedColliderList.Count; i++)
{
Collider2D collider = impactedColliderList[i];Change the collider to be a trigger, preventing physical collisions from happening but still allowing scripts to react to overlaps.
collider.isTrigger = true;
}
}OnDisable is a Unity method which is called each time this component is disabled.
protected is optional here. Used for consistency.
protected void OnDisable()
{Here we set the gravityScale to 1, which is the default value. This allows gravity to impact the GameObject again.
myBody.gravityScale = 1;Loop over each of the non-trigger colliders as found in Awake above.
for(int i = 0; i < impactedColliderList.Count; i++)
{
Collider2D collider = impactedColliderList[i];Change the collider to no longer be a trigger, re-enabling physical collisions.
collider.isTrigger = false;
}
}
}
Update LadderMovement:
This is a Unity-specific attribute which informs the editor that this script requires a DisablePhysics component on the GameObject.
[RequireComponent(typeof(DisablePhysics))] A reference to the DisablePhysics component on this GameObject. Cached here for performance.
DisablePhysics disablePhysics;Here we get a reference to the DisablePhysics component on this GameObject.
disablePhysics = GetComponent<DisablePhysics>(); Here we enable the DisablePhysics component, which will disable physics on this GameObject, allowing us to climb through Platforms.
disablePhysics.enabled = true; Here we disable the DisablePhysics component, which will re-enable physics on this GameObject.
disablePhysics.enabled = false; What's a C# List?
In C#, a List is a an array which can easily and automatically be resized as needed. As you add and remove elements, C# will manage the size of the array which holds the information. It does not resize the array every time something is added or removed, it's optimized to try and limit those potentially expensive calls.
When you create a List you give it the type of data it will contain. We make the List for a specific type, as opposed to using objects, to communicate intent and for type safety - e.g., if we had a List it's clear that Cats don't belong there, and if we attempted to add a Cat to the Dog list, C# would throw an error.
What's rigidbody gravityScale do?
You can modify how much gravity impacts a specific object using its rigidbody's gravityScale. Gravity scale is defined in percent, where 1 is the normal amount of gravity and 0 means gravity is disabled.
You can modify the gravity for all objects in the world using Project Settings -> Physics 2D -> Gravity, it defaults to (0, -9.81).
Why store the impacted collider list?
This component is disabling all colliders on the GameObject which were not already triggers. When we undo this change, we don't have a way to detect the colliders original state. We store list so we can change those colliders to not triggers anymore, without unintentionally changing a collider which is always supposed to be a trigger.
YouTube | Source before | Source after
Create a script for the HoverGuy and SpikeBall to control when to climb a ladder.
How
Create RandomClimbController
- Create script Code/Movement/RandomClimbController:
using System.Collections;
using UnityEngine;
[RequireComponent(typeof(LadderMovement))]
public class RandomClimbController : MonoBehaviour
{
[SerializeField]
float oddsOfClimbingLadderUp = .9f;
[SerializeField]
float oddsOfClimbingLadderDown = .1f;
[SerializeField]
float minTimeBetweenReconsideringDirection = 1;
[SerializeField]
float maxTimeBetweenReconsideringDirection = 10;
LadderMovement ladderMovement;
protected void Awake()
{
ladderMovement = GetComponent<LadderMovement>();
}
protected void Start()
{
StartCoroutine(Wander());
}
IEnumerator Wander()
{
while(true)
{
SelectARandomClimbDirection();
float timeToSleep = UnityEngine.Random.Range(
minTimeBetweenReconsideringDirection,
maxTimeBetweenReconsideringDirection);
yield return new WaitForSeconds(timeToSleep);
}
}
void SelectARandomClimbDirection()
{
if(ladderMovement.isOnLadder == false)
{
if(UnityEngine.Random.value <= oddsOfClimbingLadderUp)
{
ladderMovement.desiredClimbDirection = 1;
}
else if(UnityEngine.Random.value
<= oddsOfClimbingLadderDown)
{
ladderMovement.desiredClimbDirection = -1;
}
else
{
ladderMovement.desiredClimbDirection = 0;
}
}
}
}
Configure entities:
- Select the HoverGuy:
- Add RandomClimbController (this should automatically add LadderMovement as well)
- Update LadderMovement:
- Check Use Triggers
- Check Use LayerMask
- Layer Mask: Ladder
- Disable the DisablePhysics component.
- Select the SpikeBall:
- Repeat the steps used for the HoverGuy, and then:
- Odds of climbing up: 0
- Odds of climbing down: .5
- Repeat the steps used for the HoverGuy, and then:
Test:
- HoverGuy enemies will randomly start to climb up or down ladders, and the SpikeBalls will randomly start to climb down. The problem is they are still walking or rolling, so they quickly get off the ladder and then pop back on top of the platform.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- System.Collections.IEnumerator
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
- UnityEngine.SerializeFieldAttribute
- UnityEngine.WaitForSeconds
using System.Collections;
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a LadderMovement component on the GameObject.
[RequireComponent(typeof(LadderMovement))]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 RandomClimbController : MonoBehaviour
{This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This defines how likely the entity will climb up a ladder. You can change the default in the Inspector.
float oddsOfClimbingLadderUp = .9f;This defines how likely the entity will climb down a ladder if not attempting to climb up. You can change the default in the Inspector.
[SerializeField]
float oddsOfClimbingLadderDown = .1f;These define how long between changing directions - e.g. from attempting to go up a ladder to attempting to go down.
[SerializeField]
float minTimeBetweenReconsideringDirection = 1;
[SerializeField]
float maxTimeBetweenReconsideringDirection = 10;This holds a reference to the LadderMovement component on this GameObject. Cached here for performance.
LadderMovement ladderMovement;Awake is a Unity method which is called once, the first time a GameObject is added to the scene.
protected is optional here. Used for consistency.
protected void Awake()
{Here we get a reference to the LadderMovement component on this GameObject.
ladderMovement = GetComponent<LadderMovement>();
}Start is a Unity event which is called once, the first time the component is enabled.
protected is optional here. Used for consistency.
protected void Start()
{This starts the coroutine below.
StartCoroutine(Wander());
}This is the coroutine which will pick a new random climb direction periodically.
IEnumerator Wander()
{We loop forever so that this is running the entire time the entity is alive.
while(true)
{This uses a helper method below to select a new random climb direction.
SelectARandomClimbDirection();Here we select a random time to wait before picking a new random climb direction.
float timeToSleep = UnityEngine.Random.Range(
minTimeBetweenReconsideringDirection,
maxTimeBetweenReconsideringDirection);
yield return new WaitForSeconds(timeToSleep);
}
}This is a helper method for selecting a new random climb direction.
void SelectARandomClimbDirection()
{Check if the entity is already on a ladder, if they are we do nothing to ensure entities climb all the way up or down before getting off.
You could decide to do this differently.
if(ladderMovement.isOnLadder == false)
{Here we get a random number between 0 and 1, and if that number is less than the odds of climbing up then we update LadderMovement to start climbing up.
if(UnityEngine.Random.value <= oddsOfClimbingLadderUp)
{
ladderMovement.desiredClimbDirection = 1;
}Here we do the same, checking if the entity should climb down.
else if(UnityEngine.Random.value
<= oddsOfClimbingLadderDown)
{
ladderMovement.desiredClimbDirection = -1;
}If neither attempting to climbing up or down, then set the desiredClimbDirection which means walk past ladders without getting on.
else
{
ladderMovement.desiredClimbDirection = 0;
}
}
}
}If I set both odds to 50%, why does it go up more often then down?
In order to keep the implementation simple, we are checking if we should go up before checking if we should go down. This order results in effectively lowering the odds for going down.
For example, if both odds were 50%:
- We have a 50% chance of going up.
- If not, then we have a 50% chance to go down.
Since we only consider going down when we are not going up, the actual odds of going down in this example are 25%.
You could update this algorithm to calculate the odds correctly.
YouTube | Source before | Source after
Stop WanderWalkController when climbing up or down.
How
Update WanderWalkController:
- Update Code/Movement/WanderWalkController:
Existing code
using System.Collections;
using UnityEngine;
[RequireComponent(typeof(WalkMovement))]
public class WanderWalkController : MonoBehaviour
{
[SerializeField]
float oddsOfGoingUpHill = .8f;
[SerializeField]
float timeBeforeFirstWander = 10;
[SerializeField]
float minTimeBetweenReconsideringDirection = 1;
[SerializeField]
float maxTimeBetweenReconsideringDirection = 10;
WalkMovement walkMovement;
FloorDetector floorDetector; LadderMovement ladderMovement; Existing code
protected void Awake()
{
walkMovement = GetComponent<WalkMovement>();
floorDetector = GetComponentInChildren<FloorDetector>(); ladderMovement = GetComponent<LadderMovement>();
if(ladderMovement != null)
{
ladderMovement.onGettingOnLadder
+= LadderMovement_onGettingOnLadder;
ladderMovement.onGettingOffLadder
+= LadderMovement_onGettingOffLadder;
}Existing code
}
protected void Start()
{
StartCoroutine(Wander());
} void LadderMovement_onGettingOnLadder()
{
walkMovement.desiredWalkDirection = 0;
}
void LadderMovement_onGettingOffLadder()
{
SelectARandomWalkDirection();
}Existing code
IEnumerator Wander()
{
walkMovement.desiredWalkDirection = 1;
if(timeBeforeFirstWander > 0)
{
yield return new WaitForSeconds(timeBeforeFirstWander);
}
while(true)
{
SelectARandomWalkDirection();
float timeToSleep = UnityEngine.Random.Range(
minTimeBetweenReconsideringDirection,
maxTimeBetweenReconsideringDirection);
yield return new WaitForSeconds(timeToSleep);
}
}
void SelectARandomWalkDirection()
{ if(ladderMovement != null && ladderMovement.isOnLadder)
{
return;
}Existing code
float dot;
if(floorDetector.floorUp != null)
{
dot = Vector2.Dot(floorDetector.floorUp.Value, Vector2.right);
}
else
{
dot = 0;
}
if(dot < 0)
{
walkMovement.desiredWalkDirection
= UnityEngine.Random.value <= oddsOfGoingUpHill ? 1 : -1;
}
else if(dot > 0)
{
walkMovement.desiredWalkDirection
= UnityEngine.Random.value <= oddsOfGoingUpHill ? -1 : 1;
}
else
{
walkMovement.desiredWalkDirection
= UnityEngine.Random.value <= .5f ? 1 : -1;
}
}
}
Create StopMomentumOnLadder:
- Create a script Code/Movement/StopMomentumOnLadder:
using UnityEngine;
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(LadderMovement))]
public class StopMomentumOnLadder : MonoBehaviour
{
Rigidbody2D myBody;
float previousAngularVelocity;
float previousXVelocity;
protected void Awake()
{
myBody = GetComponent<Rigidbody2D>();
LadderMovement ladderMovement
= GetComponent<LadderMovement>();
ladderMovement.onGettingOffLadder
+= ClimbLadder_onGettingOffLadder;
ladderMovement.onGettingOnLadder
+= LadderMovement_onGettingOnLadder;
}
void LadderMovement_onGettingOnLadder()
{
previousAngularVelocity = myBody.angularVelocity;
previousXVelocity = myBody.velocity.x;
myBody.velocity = Vector2.zero;
}
void ClimbLadder_onGettingOffLadder()
{
myBody.angularVelocity = -previousAngularVelocity;
myBody.velocity = new Vector2(
-previousXVelocity, myBody.velocity.y);
}
}
Configure entity:
- Add StopMomentumOnLadder to the SpikeBall.
Test:
- Both HoverGuy and SpikeBall should randomly climb ladders as they pass by.
- SpikeBall always goes down.
- HoverGuy usually goes up, but sometimes will go down.
Explain the code
WanderWalkController:
This holds a reference to the LadderMovement component on this GameObject. Cached here for performance.
LadderMovement ladderMovement; Here we get a reference to the LadderMovement component on this GameObject.
ladderMovement = GetComponent<LadderMovement>(); If this GameObject has a LadderMovement component, then subscribe to the getting on and getting off events.
if(ladderMovement != null)
{
ladderMovement.onGettingOnLadder
+= LadderMovement_onGettingOnLadder;
ladderMovement.onGettingOffLadder
+= LadderMovement_onGettingOffLadder;
}This is the method which is called by the LadderMovement when getting on a ladder.
void LadderMovement_onGettingOnLadder()
{When getting on a ladder, stop walking.
walkMovement.desiredWalkDirection = 0;
}This is the method which is called when getting off a ladder.
void LadderMovement_onGettingOffLadder()
{This calls a helper method below to select a new walk direction as soon as the entity gets off a ladder.
SelectARandomWalkDirection();
}Here we check if the entity is on a ladder, in which case don't select a direction.
We will select a direction next when getting off the ladder.
if(ladderMovement != null && ladderMovement.isOnLadder)
{
return;
}
StopMomentumOnLadder:
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
- UnityEngine.Rigidbody2D
- UnityEngine.Vector2
using UnityEngine;These inform the editor that this script requires a rigidbody and LadderMovement component in the GameObject.
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(LadderMovement))]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 StopMomentumOnLadder : MonoBehaviour
{This is a reference to the rigidbody on this GameObject. Cached here for performance.
Rigidbody2D myBody;This holds the angular velocity the GameObject had when getting on a ladder.
float previousAngularVelocity;This holds the x velocity when getting on a ladder.
float previousXVelocity;Awake is a Unity event which is called once, when the GameObject is first added to the scene.
protected is optional here. Used for consistency.
protected void Awake()
{Get references to the rigidbody and LadderMovement components on this GameObject.
myBody = GetComponent<Rigidbody2D>();
LadderMovement ladderMovement
= GetComponent<LadderMovement>();Here we subscribe to the getting on and getting off events from the LadderMovement component.
ladderMovement.onGettingOffLadder
+= ClimbLadder_onGettingOffLadder;
ladderMovement.onGettingOnLadder
+= LadderMovement_onGettingOnLadder;
}This method is called by the LadderMovement event when getting on a ladder.
void LadderMovement_onGettingOnLadder()
{Here we store the momentum the entity currently has.
previousAngularVelocity = myBody.angularVelocity;
previousXVelocity = myBody.velocity.x;Then we stop its velocity, allowing the entity to climb straight up or down.
myBody.velocity = Vector2.zero;
}This method is called by the LadderMovement event when getting off a ladder.
void ClimbLadder_onGettingOffLadder()
{Here we are restoring the momentum saved when getting on. We also flip the values, so if the object was moving right after getting off a ladder it will move left.
myBody.angularVelocity = -previousAngularVelocity;
myBody.velocity = new Vector2(
-previousXVelocity, myBody.velocity.y);
}
}Why not stop the WalkMovement component instead?
Stopping the HoverGuy via the WalkMovement component instead of the WanderWalkController would work fine for the HoverGuy. However we share the WalkMovement component with the Character as well, and don't want to prevent the player from being able to walk off the side of a ladder.
You could alternatively put this logic in WalkMovement with a flag to indicate if ladders should prevent walking or not.
Why not deregister events here?
We are assuming that this component will never be removed from the GameObject. So both WanderWalkController and WalkMovement are expected to exist from Awake till OnDestroy. When a GameObject is destroyed, the registered events are automatically garbage collected.
If we wanted to optionally remove this component, we would want to deregister the events to prevent a memory leak or unexpected behaviour.
Why not stop and restart the coroutine instead?
You could stop the coroutine when getting on a ladder and then restart it when you get off. The coroutine from WanderWalkController would need to be updated for this to work, ensuring that when we resume we don't sleep for that initial wait time again.
YouTube | Source before | Source after
Add a script to the HoverGuy and SpikeBall to direct them towards the center of a ladder while climbing.
How
Create MoveTowardsCenterWhileClimbing:
- Create script Code/Movement/MoveTowardsCenterWhileClimbing:
using UnityEngine;
[RequireComponent(typeof(LadderMovement))]
public class MoveTowardsCenterWhileClimbing : MonoBehaviour
{
[SerializeField]
float speed = 1f;
LadderMovement ladderMovement;
protected void Awake()
{
ladderMovement = GetComponent<LadderMovement>();
}
protected void FixedUpdate()
{
GameObject ladder = ladderMovement.ladderWeAreOn;
if(ladder != null)
{
float targetX = ladder.transform.position.x;
float myX = transform.position.x;
float deltaX = targetX - myX;
if(Mathf.Abs(deltaX) > 0.01)
{
Vector2 target = transform.position;
target.x += deltaX;
transform.position = Vector2.MoveTowards(
transform.position,
target,
speed * Time.fixedDeltaTime);
}
}
}
}
Configure entities:
- Add MoveTowardsCenterWhileClimbing to the HoverGuy and SpikeBall.
Test:
- When the HoverGuy and SpikeBall are climbing ladders, they should appear to be moving up and down the center -- vs the side as they were before.
Explain the code
'using' clauses at the top of a file brings APIs into scope. Used for:
- UnityEngine.Mathf
- UnityEngine.MonoBehaviour
- UnityEngine.RequireComponentAttribute
- UnityEngine.SerializeFieldAttribute
- UnityEngine.Vector2
using UnityEngine;This is a Unity-specific attribute which informs the editor that this script requires a LadderMovement component on the GameObject.
[RequireComponent(typeof(LadderMovement))]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 MoveTowardsCenterWhileClimbing : MonoBehaviour
{This is a Unity-specific attribute that exposes a field in the Inspector, allowing you to configure it for the object.
[SerializeField]This defines how quickly the entity moves towards the center of the ladder while climbing. You can change the default in the Inspector.
float speed = 1f;This holds a reference to the LadderMovement component on this GameObject. Cached here for performance.
LadderMovement ladderMovement;Awake is a Unity event which is called once, when the GameObject is first added to the scene.
protected is optional here. Used for consistency.
protected void Awake()
{Here we get a reference to the LadderMovement component on this GameObject.
ladderMovement = GetComponent<LadderMovement>();
}FixedUpdated is a Unity event which is called every x ms of game time.
protected is optional here. Used for consistency.
protected void FixedUpdate()
{Check if the entity is currently on a ladder.
GameObject ladder = ladderMovement.ladderWeAreOn;
if(ladder != null)
{Calculate the deltaX, which is how far from the center of the ladder the entity currently is.
float targetX = ladder.transform.position.x;
float myX = transform.position.x;
float deltaX = targetX - myX;Only move if the delta is not close to 0, so that it does not bounce back and forth.
if(Mathf.Abs(deltaX) > 0.01)
{Here we calculate the target position to move the entity to, in order to position that entity perfectly center with the ladder, at the height that they are currently.
Vector2 target = transform.position;
target.x += deltaX;Here we moving towards the target position calculated above. We use MoveTowards instead of simply setting the position so that movement looks smooth.
transform.position = Vector2.MoveTowards(
transform.position,
target,
speed * Time.fixedDeltaTime);
}
}
}
}Why not use velocity to move?
You could.
MoveTowardsCenterWhileClimbing uses MoveTowards to update the transform.position directly instead of moving via the rigidbody as you normally would. We do this as a simplification.
If you use velocity, be careful when you overshoot the target a bit so the entity does not appear to wiggle back and forth trying to settle on the exact center position.
Testing / debugging tips
- If you can jump and start climbing a ladder you barely reach, check the contact filter configuration.
- TODO
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.
