Cat hooks add funny and interesting mechanics to the game. You can use them to move through the levels, battles in arenas and get items. But despite the seeming simplicity, the physics of managing ropes and creating realistic behavior can be challenging!
In the first part of this tutorial, we will implement our own two-dimensional hook-cat system and learn the following:
- Create a sighting system.
- Use render lines and distance joint to create a rope.
- We will teach the rope to turn around around the game objects.
- Calculate the swing angle on the rope and add strength in this direction.
Note : This tutorial is intended for advanced and advanced users, and it does not cover such topics as adding components, creating new GameObject scripts and C # syntax. If you need to improve your Unity skills, then learn our tutorials Getting Started with Unity and Introduction to Unity Scripting . Since this tutorial uses DistanceJoint2D , you should also look at Physics Joints in Unity 2D , and only then go back to this tutorial.
Getting Started
Download the
project template for this tutorial, and then open it in the Unity editor. To work, you need a version of Unity 2017.1 or higher.
Open the
Game scene from the
Scenes folder and see where we start:
While we have a simple player character (slug) and stones hanging in the air.
Important components of GameObject
Player are capsule collider and rigidbody, which allow it to interact with physical objects on the level. Also attached to the character is a simple movement script (
PlayerMovement ), which allows it to slide on the ground and perform simple jumps.
Click on the
Play button to start the game, and try to control the character.
A and
D move it left / right, and when you press the
spacebar, it makes a jump. Try not to slide off or fall off a cliff, otherwise you will die!
We already have the basics of management, but the biggest problem now is the absence of hooks-cats.
Making hooks and ropes
At first, the cat-hook system seems fairly simple, but for its quality implementation, there are many aspects to consider. Here are some of the requirements for the two-dimensional mechanics of hook cats:
- Line Renderer to display the rope. When the rope is wrapped around objects, we can add more segments to the line renderer and position the vertices at the points corresponding to the rope breaks.
- DistanceJoint2D. It can be used to attach the current anchor point of the hook cat so that our slug can swing. It also allows us to adjust the distance that can be used to lengthen and reduce the rope.
- Child GameObject with RigidBody2D, which can be moved depending on the current location of the hook's reference point. In essence, it will be the suspension / anchor point of the rope.
- Raycast to eject the hook and attach to objects.
In the Hierarchy, select the
Player object and add a new child GameObject called
RopeHingeAnchor . This GameObject will be used to position the suspension / pivot point of the hook cat.
Add
SpriteRenderer and
RigidBody2D to RopeHingeAnchor .
For SpriteRenderer, set the
Sprite property to use the
UISprite value and change the
Order in Layer to
2 . Disable the component by
unchecking the box next to its name.
For the
RigidBody2D component
, set the Body Type property to
Kinematic . This point will not be moved by the physics engine, but by the code.
Select the
Rope layer and set the X and Y scale values of the Transform component to
4 .
Select the
Player again and attach the new component
DistanceJoint2D .
Drag
RopeHingeAnchor from the Hierarchy onto the
Connected Rigid Body property of the
DistanceJoint2D component and disable
Auto Configure Distance .
Create a new C # script with the name
RopeSystem in the
Scripts project folder and open it in the code editor.
Remove the
Update
method.
At the top of the script inside the declaration of the
RopeSystem
class
RopeSystem
add new variables, the
Awake()
method and the new
Update
method:
Let's sort each part in order:
- We use these variables to track the various components with which the RopeSystem script will interact.
- The
Awake
method starts at the beginning of the game and disables the ropeJoint
(component DistanceJoint2D). It also sets playerPosition
to the current position of the Player. - This is the most important part of the main
Update()
loop. First we get the mouse position in the world using the ScreenToWorldPoint
camera ScreenToWorldPoint
. Then we calculate the direction of the view, subtracting the position of the player from the position of the mouse in the world. Then use it to create aimAngle
, which is a representation of the aiming angle of the cursor. Value saves a positive value in the if construct. aimDirection
is a turnaround that comes in handy later. We are only interested in the value of Z, because we use a 2D camera, and this is the only corresponding OS. We pass aimAngle * Mathf.Rad2Deg
, which converts a radian angle to an angle in degrees.- The position of the player is tracked using a convenient variable that allows you not to constantly refer to
transform.Position
. - Finally, we have an
if..else
construct, which we will soon use to determine if a rope is attached to a support point.
Save the script and return to the editor.
Attach the
RopeSystem component to the Player object and attach the various components to the public fields that we created in the
RopeSystem script. Drag
Player ,
Crosshair and
RopeHingeAnchor to the appropriate fields:
- Rope Hinge Anchor : RopeHingeAnchor
- Rope Joint : Player
- Crosshair : Crosshair
- Crosshair Sprite : Crosshair
- Player Movement : Player
Now we just perform all these intricate calculations, but so far there is no visualization that could show them in action. But do not worry, we will deal with it soon.
Open the
RopeSystem script and add a new method to it:
private void SetCrosshairPosition(float aimAngle) { if (!crosshairSprite.enabled) { crosshairSprite.enabled = true; } var x = transform.position.x + 1f * Mathf.Cos(aimAngle); var y = transform.position.y + 1f * Mathf.Sin(aimAngle); var crossHairPosition = new Vector3(x, y, 0); crosshair.transform.position = crossHairPosition; }
This method positions the sight based on the transmitted
aimAngle
(the float value that we calculated in
Update()
) so that it rotates around the player with a radius of 1 unit. We also include a sprite sight in case this has not yet been done.
In Update()
we change the !ropeAttached
construct that checks !ropeAttached
so that it looks like this:
if (!ropeAttached) { SetCrosshairPosition(aimAngle); } else { crosshairSprite.enabled = false; }
Save the script and run the game. Now our slug should be able to aim with the help of a sight.
The next part of the logic that needs to be implemented is a shot with a cat hook. We have already determined the direction of aiming, so we need a method that will receive it as a parameter.
Add the following variables under the variables in the
RopeSystem script:
public LineRenderer ropeRenderer; public LayerMask ropeLayerMask; private float ropeMaxCastDistance = 20f; private List<Vector2> ropePositions = new List<Vector2>();
LineRenderer will contain a link to render lines that draws the rope.
LayerMask allows you to customize the layers of physics with which the hook can interact. The value
ropeMaxCastDistance
sets the maximum distance that a raycast can “shoot”.
Finally, the Vector2 position list will be used to track the wrapping points of the rope, which we will look at later.
Add the following new methods:
Here is what the above code does:
- HandleInput is called from the
Update()
loop and simply polls the input of the left and right mouse buttons. - When the left mouse button is registered, the render of the rope line is turned on and a 2D-raycast is fired from the player's position in the direction of aiming. The maximum distance is set so that the hook cat cannot be fired at an infinite distance, and a mask is applied so that you can select the layers of physics that the raycast is capable of colliding with.
- If a raycast hit is detected, then
ropeAttached
is true
, and a list of the positions of the vertices of the rope is checked to make sure that the points are not there yet. - If the check returns true, then a small impulse of force is added to the slug so that it jumps over the ground, the
ropeJoint
(DistanceJoint2D) is ropeJoint
, which is set to a distance equal to the distance between the slug and the raycast hit point. Also included is the base point sprite. - If the raycast does not hit anything, then line renderer and ropeJoint are disabled, and the
ropeAttached
flag is false. - If the right mouse button is pressed, the
ResetRope()
method is ResetRope()
, which disables and resets all parameters related to the string / hook to the values that should be if the hook is not used.
At the very bottom of our
Update
method, add a call to the new
HandleInput()
method and pass it the value of
aimDirection
:
HandleInput(aimDirection);
Save the changes to
RopeSystem.cs and return to the editor.
Add rope
Our slug will not be able to fly through the air without a rope, so it's time to give him what will be a visual display of the rope and has the ability to "turn around" around the corners.
The line renderer is ideal for this, because it allows us to transfer the number of points and their positions in the space of the world.
The idea here is that we always store the first vertex of the rope (0) in the player's position, and all other vertices are positioned dynamically when the rope should turn around something, including the current position of the hinge, which is the next point along the rope from the player.
Select
Player and add the
LineRenderer component to it. Set the
Width to
0.075 . Expand the
Materials list and, as
Element 0, select the
RopeMaterial material located in the
Materials folder of the project. Finally, in the Line Renderer, for the
Texture Mode parameter, select
Distribute Per Segment .
Drag the Line Renderer component into the
Rope Renderer field of the
Rope System component.
Click on the
Rope Layer Mask drop-down list and select as layers with which raycast
Default, Rope and Pivot can interact. Because of this, when “shooting” a raycast, it will only collide with these layers, but not with other objects, such as a player.
If you start the game now, you will notice strange behavior. When we aim at a stone over the head of a slug and shoot a hook, we get a small jump upwards, after which our friend begins to behave rather erratically.
We have not yet set the distance for distance joint, moreover, the vertices of the line render are not configured. Therefore, we do not see the rope, and since the distance joint is directly above the slug's position, the current distance distance distance joint pushes it down to the stones below it.
But do not worry, now we will solve this problem.
In the
RopeSystem.cs script
, add a new operator at the beginning of the class:
using System.Linq;
This allows us to use LINQ queries, which in our case simply allow us to conveniently find the first or last element of the
ropePositions
list.
Note : Language Integrated Query (LINQ) is the name of a set of technologies based on embedding query capabilities directly in C #. Read more about it here .
Add a new private variable bool with the name
distanceSet
under the other variables:
private bool distanceSet;
We will use this variable as a flag so that the script can know that the distance of the rope (for the point between the player and the current reference point where the hook-cat is attached) is set correctly.
Now we will add a new method that we will use to set the position of the rope tops for the line render and adjust the distance joint in the stored position list with the
ropePositions
:
private void UpdateRopePositions() {
Explain the code shown above:
- Perform an exit from the method if the rope is not attached.
- Assign to the render point of the rope line the value of the number of positions stored in the
ropePositions
, plus 1 more (for the player’s position). - We loop around in the reverse direction the list of
ropePositions
and for each position (except the last one), assign the position of the vertex of the line's render to the value of Vector2 position stored by the loop index in the list of ropePositions
. - We assign the second position of the rope to the rope's anchor point, in which the current joint / anchor point should be located, or if we have only one rope position, then we make it a reference point. So we set the distance of the
ropeJoint
equal to the distance between the player and the current position of the rope, which we ropeJoint
in a loop. - The if construct handles the case where the current position of the rope in the loop is second to the end; that is, the point at which the rope connects with the object, i.e. current joint / reference point.
- This
else
block handles the assignment of the position of the last vertex of the rope to the value of the current position of the player.
Don't forget to add an
UpdateRopePositions()
call at the end of
Update()
UpdateRopePositions()
:
UpdateRopePositions();
Save the changes in the script and run the game again. Make a small jump with the "space", aiming and shooting a hook at the stone above the character. Now you can enjoy the fruits of your labors - the slug quietly sways above the stones.
Now you can go to the scene window, select the Player, use the move tool (the
W key by default) to move it and observe how the two vertices of the rope line follow the position of the hook and the position of the player to draw the rope. After we release the player, DistanceJoint2D correctly calculates the distance and the slug will continue to swing on the connected hinge.
Handling Wrap Points
Playing with a swinging slug is no longer useful as a water-repellent towel, so we definitely need to add it.
The good news is that the newly added method of processing rope positions can be used in the future. For now we use only two rope positions. One is connected to the player’s position, and the second to the current position of the hook's reference point when fired.
The only problem is that while we are not tracking all the potential positions of the rope, we need to work a little on this.
To recognize positions on stones around which the rope should be wrapped, adding a new vertex position to the render line, we need a system that determines whether the vertex point of the collider is between a straight line between the current slug position and the current hinge / anchor point of the rope.
Looks like this is work again for the good old raycast!
To begin, we need to create a method that can find the nearest point in the collider based on the hit point of the raycast and the collider faces.
Add a new method to the
RopeSystem.cs script:
If LINQ queries are not familiar to you, then this code may seem like some complicated C # magic.
If so, do not be afraid. LINQ independently does a lot of work for us:
- This method takes two parameters — a RaycastHit2D object and a PolygonCollider2D . All stones on the level have colliders PolygonCollider2D, so if we always use the shapes PolygonCollider2D, it will work fine.
- This is where the magic of LINQ queries begins! Here we convert the collection of points of the polygonal collider into the Vector2 position dictionary (the value of each dictionary element is the position itself), and the key of each element is assigned the distance from this point to the player’s position player (float value). Sometimes something else happens here: the resulting position is converted into the space of the world (by default, the vertex positions of the collider are stored in local space, ie, local relative to the object that owns the collider, and we need positions in the space of the world).
- The dictionary is sorted by keys. In other words, by the distance closest to the current position of the player. The closest distance is returned, that is, any point returned by this method is a collider point between the player and the current hinge point of the rope!
Let's
go back to the
RopeSystem.cs script and add a new private field variable at the top:
private Dictionary<Vector2, int> wrapPointsLookup = new Dictionary<Vector2, int>();
We will use it to track the positions around which the rope can turn.
At the end of the
Update()
method, find the
else
, which contains
crosshairSprite.enabled = false;
and add the following:
We will explain this code snippet:
- If any positions are stored in the
ropePositions
list, then ... - We shoot from the player's position in the direction of the player looking at the last position of the rope from the list — the anchor point at which the hook cat clings to the stone — with a raycast distance equal to the distance between the player and the position of the anchor point of the rope.
- If a raycast encounters something, then the collider of this object is safely converted to the type PolygonCollider2D . While it is real PolygonCollider2D, the closest position of the vertex of this collider is returned using the method we wrote earlier as Vector2 .
- It is checked by
wrapPointsLookup
to ensure that the same position is not checked again. If it is checked, then we drop the rope and cut it, dropping the player. - Then the list of
ropePositions
updated: the position around which the rope should turn is added. The wrapPointsLookup
dictionary is also updated. Finally, the distanceSet
flag is reset so that the UpdateRopePositions()
method can override the rope distances given the new rope length and segments.
In
ResetRope()
we add the following line so that the
wrapPointsLookup
dictionary
wrapPointsLookup
cleared each time the player disconnects the rope:
wrapPointsLookup.Clear();
Save and run the game. Shoot the cat hook into the stone above the slug and use the Move tool in the Scene window to move the slug behind several stone projections.
This is how we taught the rope to turn around objects!
Add rocking ability
Slug hanging on a rope is pretty static. To fix this, we can add the rocking ability.
To do this, we need to get a position perpendicular to the swinging forward position (sideways), regardless of the angle at which he looks.
Open
PlayerMovement.cs and add the following two public variables to the top of the script:
public Vector2 ropeHook; public float swingForce = 4f;
The variable
ropeHook
will be assigned to any position in which the rope hook is currently located, and the
swingForce
is the value that we use to add the swing movement.
Replace the
FixedUpdate()
method
FixedUpdate()
new one:
void FixedUpdate() { if (horizontalInput < 0f || horizontalInput > 0f) { animator.SetFloat("Speed", Mathf.Abs(horizontalInput)); playerSprite.flipX = horizontalInput < 0f; if (isSwinging) { animator.SetBool("IsSwinging", true);
,
isSwinging
, , , , .
- .
- , ,
playerToHookDirection
. , .
RopeSystem.cs else if(!ropeAttached)
Update()
:
playerMovement.isSwinging = true; playerMovement.ropeHook = ropePositions.Last();
if if(!ropeAttached)
:
playerMovement.isSwinging = false;
PlayerMovement, , ( ) — , . , PlayerMovement.
, gizmos A D /:
- . , , , ?
RopeSystem :
public float climbSpeed = 3f; private bool isColliding;
climbSpeed
, ,
isColliding
, distance joint .
:
private void HandleRopeLength() {
if..elseif
(/ W/S ),
ropeAttached iscColliding
ropeJoint
, .
,
Update()
:
HandleRopeLength();
isColliding
.
:
void OnTriggerStay2D(Collider2D colliderStay) { isColliding = true; } private void OnTriggerExit2D(Collider2D colliderOnExit) { isColliding = false; }
MonoBehaviour.
Collider ,
OnTriggerStay2D
,
isColliding
true
. , , isColliding
true
.
OnTriggerExit2D
, , false.
:
OnTriggerStay2D
, .
Where to go next?
W/S, - .
.
— - !
, , - , , , ! Great job.
— «», .
.
, ?
wrapPointsLookup
.