An Unity 2D Touch Study

An Unity 2D Touch Study

Version 1.0.1 - github repo

I challenged myself to make a quick test of the game mechanics from Cut The Rope using some stuff I never used on Unity before, they are: 2D Joints, Touch and 2D Physics. So, the challenge is quite obvious if you played it before: the candy has physics, and there is a rope connected to it, that can be cut. I will get into every detail I can in this one, including all of the codes I wrote. The first version were made in 4 hours. You can get the final repository if you want. I got some simple results that are very close to the original game, you can check it out on the final gifs:

https://gph.is/2cv7tkS

https://gph.is/2dsdjFZ

Let's begin breaking the whole thing down. I'll start with the rope, which is the most complex thing on the whole game. To make the rope, first of all it needed to be in its own Layer for Raycasting purposes, which I called "Cuttable". Then on its components, we have a Line Renderer, an Edge Collider 2D, a Rigidbody 2D, a Distance Joint 2D and finally our own Rope.cs script. I will explain each of the components and their settings.

The Setup

First of all, LineRenderer. It is the best option to create the visual appearance of the rope, however it has absolutely no physics connection, so I knew I would have to do this by myself. I could work with more than 2 points as I did, to create a more "realistic" rope behaviour instead of a cable, but that would have been a little more complex, and I really wanted to start simple. So, here with my 2 points, the first one is the origin and will always stay the same, and the next one is where the candy is, and it's updated via code (don't worry I will get in details soon). The second point at start, isn't really where the candy is, but it will be updated as soon as the game starts.

Next, we have an Edge Collider 2D, set as a trigger because it should only interact with the Raycasting I used to detect and make the Touch work, and it shouldn't really behave as an obstacle for "collideable" objects. This collider must have its points set exactly as the Line Renderer, because this is what makes the physics work accordingly. Here we also have a Rigidbody 2D, set as kinematic with all the constraints applied and sleeping mode to "Start Asleep". I'm only doing this, because the joint needs two rigidbodies to work properly, so we have this first one here that is almost inactive, and we have the second one that will be the reference to the "active physical part" of the joint.

Now for the Distance Joint 2D. Keep in mind that I could have used other joints to create a similar behavior, as the Hinge Joint 2D (which I actually used in another part of the rope). Here we set the connected Rigidbody to the candy at startup, and disable "Auto Configure Connections", as the connection will change in the moment we cut the rope. I let "Auto Configure Distance" on, because I would have more freedom moving the candy around, so it would set the distance automatically, and then "Max Distance Only" to prevent a "shrinking" effect of the rope, it's size shouldn't change unless cut, but that's a different thing.

Keep calm, there is some things I need to cover before we get into coding. Let's take a look at the rope's hierarchy on the editor. Well the secret is right there, I will break it down for you. You see the "hinge" right there? That's the simplest one, it's just a Sprite Renderer with the hinge image, purely visual, no code, no complications.

Now we have two "Tip"s. Both of them are the exact same thing, they act as a weight on the tip of the rope, so we can have the joints and the physics to work as expected. They have a Circle Collider 2D which radius is just enough to fit the rope's width, so then it can have a yet better physics simulation by colliding with the ground and so on. It also has a Rigidbody 2D, this time with no constrains, not kinematic, with everything ready and set to receive physics simulations. The tip's rigidbody will replace the rope's connected rigidbody parameter on its Distance Joint 2D after the rope is cut, so it will keep swinging in the air.

The Rope Fragment has its own code, separately from the Rope, as when the Rope gets cut, it becomes an independent object. The only difference between the Rope Fragment and the Rope aside from their code, is their Joint 2D and the fact it doesn't have a collider. The Rope Fragment has a Hinge Joint 2D. It's connected rigidbody is already set to the fragment's tip.

We are only one step closer to the coding part! Now I just need to explain the candy and we can get to the fun part. The Candy is a really simple object, it has only a Sprite Renderer, a Circle Collider 2D, a regular non-kinematic Rigidbody 2D with no constrains applied and the Candy script. To be honest, I didn't even bothered modifying the component's properties here, it worked out just fine with the default settings.

The magic behind the curtains

Yes! This must be what you were waiting for (or at least I hope). I will keep our eyes in the core mechanics here, so I will not cover anything from the player character, as it is very simple and doesn't need any special attention. First of all, we have 4 essential scripts here: the Input Manager, the Candy, the Rope and the Rope Fragment. Yes, that's all. I'll let the Input Manager for last, and start with the rope and the candy.

What do we know about the rope? It has a Line Renderer, an Edge Collider 2D, a Distance Joint 2D. Both line renderer and edge collider should have their ponts matched, and matched according to something. We know the rope will get broken and then some things will change. So I guess that saving a reference to the tip's Transform will be a good move too, as it will replace the candy as the "active physical part". Also I will need to save a reference to the candy's Transform. Remember the Rope Fragment? As it will also be affected, I will save a reference to it's Transform too. So far I guess we already have all variables in mind, let's write them down and assign them on the Awake method so it will execute once the object is instantiated.

public class Rope : MonoBehaviour 
{ 
  public bool broken { get; private set; } 

  Transform fragment, 
    candy, 
    tip; 
  LineRenderer line; 
  EdgeCollider2D col; 
  DistanceJoint2D dist; 
  RaycastHit2D[] hit; 

  void Awake() 
  ?{ 
    line = GetComponent<LineRenderer>(); 
    col = GetComponent<EdgeCollider2D>(); 
    dist = GetComponent<DistanceJoint2D>(); 
    fragment = transform.GetChild(0); 
    tip = transform.GetChild(1); 
  } 
}

Now, we need a function to update the line and collider's points, and as our reference to the final point will change, this function must accept a parameter as the reference, then it will update the points' positions. As you may remember, the Line Renderer isn't set to use world positions, so it will use local positions instead. Then, we will need to convert the incoming point position to set accordingly.

void ResizeElements(Vector3 point)
{
  Vector3 localPoint = transform.InverseTransformPoint(point);

  // Resize Edge Collider
  Vector2[] colPoints = col.points;
  colPoints[1] = localPoint;
  col.points = colPoints;

  // Resize Line Renderer
  line.SetPosition(1, localPoint);
}

Alright, we have the function, but we need to execute this somewhere. Update is our best option here, to execute it every frame. But we have two conditions: before and after the rope is broken. Before the rope is broken, the reference is the candy, and after, it is the rope's tip.

void Update() 
?{ 
  if (!broken) 
    ResizeElements(candy.position); 
  else 
    ?ResizeElements(tip.transform.position); 
}

At this point, if you want to test the code, you can set the candy variable as public, and drag your candy to the rope's candy slot in the inspector. As I thought that the candy could react to more than one rope only, I decided to give the candy control over the ropes connected to it, and not the other way around. For that, I created a public method on the rope that simply sets the candy reference, that will be executed on the candy at the game's start. The Candy class, is quite simple, it just needs a public array of ropes, and at start, it gives its reference to all ropes attached to it. We set the ropes in the inspector.

public void AttachCandy(Transform candy) 
?{ 
  this.candy = candy; 
}


public class Candy : MonoBehaviour 
{ 
  public Rope[] ropes; 

  void Awake() 
  ?{ 
    foreach (Rope rope in ropes) 
      rope.AttachCandy(transform); 
  } 
}

Now we can do a quick jump to the RopeFragment class. This one just needs 3 references: to its Line Renderer, its Hinge Joint 2D and its tip's Transform. Here we will need to update only the line position, as this one doesn't have a collider. As the rope doesn't break yet, there's still nothing to test.

public class RopeFragment : MonoBehaviour 
{ 
  Transform tip; 
  LineRenderer line; 
  HingeJoint2D joint; 

  void Awake() 
  ?{ 
    line = GetComponent<LineRenderer>(); 
    joint = GetComponent<HingeJoint2D>(); 
    tip = transform.GetChild(0); 
  } 

  void Update() 
  ?{ 
    line.SetPosition(1, tip.localPosition); 
  } 
}

The base stuff is done, it's time to handle the inputs. My focus was to build something that would work in a touch interface, however I don't have any touch device to test it (as my phone is iOS, and my pc is a Windows), so I had to emulate the touch via mouse. The code I will show, will work in both interfaces, because I managed to test the touch afterwards. First, we need a public LayerMask variable to assign only to the "Cuttable" layer I mentioned on the beginning of the post, in the inspector.

Then, to prevent creating variables every frame, I like do declare them right at start and just update them later, so we will need a reference to the Camera (to convert mouse position or touch position into world position), two Vector2 variables to control the mouse position (for non-touch interfaces only), an array of two Vector2 points that save two world positions from the input (the one in the last fixed frame update, and the one in the current one) and an array of three RaycastHit2D to register at maximum 3 rope cuts on the same frame (trust me, you wouldn't need more than that on a single frame).

public class InputManager : MonoBehaviour 
{ 
  public LayerMask mask; 

  Touch t; 
  Camera cam; 
  Vector2 mousePos; 
  Vector2 lastMousePos; 
  Vector2[] inputs; 
  RaycastHit2D[] hits = new RaycastHit2D[3]; 

  void Awake() 
  ?{ 
    inputs = new Vector2[2]; 
    cam = Camera.main; 
  } 
}

Alright, now we need to check the input every frame, and have a function to handle the collision detection with the rope edges. I will write down the logic for both mouse and touch inputs, but they work almost the same way. On the touch, I calculate two positions: the first one is the actual position minus the deltaPosition (how much it moved since the last frame), and the second one is only the actual position, all converted to world positions with camera.ScreenToWorldPoint.

This will give me two vectors, a start point and an end point so I can use a Linecast and check for collisions. The same happens with the mouse input, but with the variables we created. For the function, it will do a Physics2D.LinecastNonAlloc, so it doesn't fill the memory if no colliders were hit (as we are checking this every frame, it's good to save memory for performance), and then it fills our array variable. In a for loop, we are able to send an event to the ropes that will get broken. I haven't created this event yet, but I know I will want to send the cut point so I can cut the rope at that exact position. Let's get in details soon.

void FixedUpdate() 
?{ 
  if (Input.touchSupported) 
  {
    t = Input.GetTouch(0);
    if (t.phase == TouchPhase.Moved) 
    { 
      inputs[0] = cam.ScreenToWorldPoint(t.position - t.deltaPosition); 
      inputs[1] = cam.ScreenToWorldPoint(t.position); SwipeCollisionDetect(); 
    } 
  } 
  else 
  { 
    mousePos = Input.mousePosition; 
    if (Input.GetMouseButton(0) && mousePos != lastMousePos) 
    { 
      inputs[0] = cam.ScreenToWorldPoint(lastMousePos); 
      inputs[1] = cam.ScreenToWorldPoint(mousePos); 
      SwipeCollisionDetect(); 
    } 
  lastMousePos = mousePos; 
  } 
} 

void SwipeCollisionDetect() 
?{ 
  int detections = Physics2D.LinecastNonAlloc(inputs[0], inputs[1], hits, mask); 
  if (detections > 0) 
  { 
    for (int i = 0; i < detections; i++) 
    // Cut event will go here 
  ?} 
}

The only thing left to do is to prepare the rope to get cut, and so the rope fragment. For the rope, first thing we should avoid is not to break if already broken (we have a variable for it remember?). Then we will resize the line and the collider to the cut point, activate and reposition the tip there, replace the candy with the tip on the joint as the connected rigidbody, and activate and release the fragment. The fragment is quite simpler, it just needs to reposition its own tip to the cut position as well and adjust the joint anchor, so it will rotate accordingly. One quick note here, if somehow the candy is caught from an uncut rope, it would not move anymore, so we need to get ready for that too by not fragmentating the rope in this case.

public void Detach(Vector2 point) 
?{ 
  tip.position = point; 
  joint.connectedAnchor = -transform.InverseTransformPoint(point); 
}


public void Break(Vector2 point) 
?{ 
  Break(point, true); 
} 
public void Break(Vector2 point, bool fragmentate) 
?{ 
  if (broken) 
    return; 
  ResizeElements(point); 
  // Cut and let the tip active to simulate physics 
  ?tip.transform.position = point; 
  tip.gameObject.SetActive(true); 

  dist.connectedBody = tip.GetComponent<Rigidbody2D>(); 
  dist.anchor = Vector2.zero; 
  dist.distance = Vector2.Distance(point, transform.position); 
  if (fragmentate) 
  { 
    // The remaining rope should stick with the candy 
    ?fragment.SetParent(candy, false); 
    fragment.transform.localPosition = Vector2.zero; 
    fragment.gameObject.SetActive(true); 
    fragment.GetComponent<RopeFragment>().Detach(point); 
  } 
  // Prevent from breaking again 
  ?broken = true; 
}

Fantastic, the last thing to do is to make the SwipeCollisionDetect in the InputManager execute these events on the colliders it found during the Linecast.

void SwipeCollisionDetect() 
?{ 
  int detections = Physics2D.LinecastNonAlloc(inputs[0], inputs[1], hits, mask); 
  if (detections > 0) 
  { 
    for (int i = 0; i < detections; i++) 
      hits[i].collider.GetComponent<Rope>().Break(hits[i].point); 
  } 
}

Nicely done, that's enough for this project. Always dive into the Docs, there's always something to learn there, as you can see in this post. I hope it worked at first try. The code isn't perfect, as I did it really fast. I know there is much I could improve or optimize, or make the ropes have more than 2 points to give it a more "realistic" view. Let me know if you have any questions, see you on the next project!

要查看或添加评论,请登录

Jo?o Borks的更多文章

  • Unity Async vs Coroutine [pt-br]

    Unity Async vs Coroutine [pt-br]

    ATUALIZA??O (7/2/2021): Como apontado por @neuecc, um dos autores da UniTask, o teste era inválido e os resultados…

    7 条评论
  • Unity Async vs Coroutine

    Unity Async vs Coroutine

    UPDATE (7/2/2021): As pointed out by @neuecc, one of the authors of UniTask, the stress test was invalid and the…

    7 条评论
  • Esse ano na Unity (2020)

    Esse ano na Unity (2020)

    Tradu??o do original em inglês: This year in Unity (2020) Quer você seja um amador, um profissional ou um jogador, deve…

    2 条评论
  • This year in Unity (2020)

    This year in Unity (2020)

    Whether you are a hobbyist, a professional, or a player, you must be familiar with Unity, and some of the games…

    1 条评论
  • Jack - The Circus of Illusion

    Jack - The Circus of Illusion

    This is a good one, Jack – The Circus of Illusion is the third game made by Emperium, produced in 2 months and…

  • Kitsune - 3D Adventure Demo

    Kitsune - 3D Adventure Demo

    Kitsune is the second digital game I worked, the second game made by Emperium, produced in 2 months and displayed in…

  • Recall - A 2D Platformer

    Recall - A 2D Platformer

    Recall is the first digital game I’ve ever worked, produced in 2 months by Emperium and released in June 2015. It is a…

  • About Coroutines in Unity Engine

    About Coroutines in Unity Engine

    The Unity Engine offers us, developers, a lot of rich resources and content to make our job easier. Amongst them, there…

    2 条评论

社区洞察

其他会员也浏览了