I have seen many puzzle games that use a mechanism that simulates putting together pieces of shredded paper... It is a simple mechanism, but when actually implementing it, it can give you headache... 
First, what are the use cases such system needs to achieve?
1.  Drag around pieces in a limited area.
2. When two pieces of matching edges connect, they are "merged", and further dragging should treat those pieces as one single piece.
3. Merged pieces should still be inside the limited area
4. The game should know when the puzzle is completed.
 The difficulty is mostly in the 2nd use case, since "detecting edges" like in computer vision is not very feasible, and you also need to keep track of different groups of merged pieces... what's more, imagine you have a group of 2 pieces and another group of 2 pieces, you will need to merge the two merged pieces as a whole...
So, here is my non-perfect approach... 
I. Drag and Drop
I am a faithful Input System user, so when I started the project, I went for Input System without second thoughts. However when I started implementing it, Input System started to fall short... at least not intuitive enough as the old system. Going though the setup of Actions as button based games can be painful, so I searched elsewhere...
The final answer is to make whatever you want to Drag and Drop, to implement the following interfaces:
IPointerDownHandler (When you press the left mouse button)
 IBeginDragHandler (When mouse button is held and you start to move the mouse)
 IEndDragHandler (When you stop dragging)
 IDragHandler (When you are actually dragging)
Like the old system, these handlers are triggered by raycasting on a collider, so you will need to attach a Collider2D to the object.
There is still one thing you need to add: a Physics 2D Raycaster to your Camera. 
Ok, now about those handlers. They are actually from the UI system, so the PointerEventData that is passed as parameter can be confusing... I have tried to used the mouse position in that data but it give ridiculously huge values even after applying ScreenToWorldPoint... 
Well, you have to use mouse position from something else, and that is:
Mouse.current.position.ReadValue()
Now you can use Camera.main.ScreenToWorldPoint() to convert this value to world position and use it to update the position in your OnDrag.
II. Limiting Movement
The most common practice is to use Mathf.Clamp() to restrict the destinated position. However, it needs information about the delimiting area. It can be done by directly supply values of borders, but for me I usually want something more visual...
My method of choice is to create a dedicated class to store this information, and in most cases, the delimiting area is a rectangle. This class will expose the 4 values of the borders (up,down,left,right), and retrieve these information in its Awake(). To define these informations, it needs to keep reference of two Transforms, one representing the upper left corner, and the other the lower right corner. After reading the corresponding values, these two objects will be set inactive. 
The purpose of doing this is that you can reposition the two corners with drag and drop in Editor. You can add a OnDrawGizmos() like I did to visualize the borders.
III. Determine the Correct Position
As stated before, when determine if two pieces would fit together, it is not feasible to judge based on the shape of the edges. Instead, the viable method that I found is to record the relative position of each of the two adjacent pieces when they are in completed state. This method is certainly not perfect, because the more pieces you have, the more information you need to record. In my case, 6 pieces require 10 adjacency information to well define the puzzle. 
Each adjacency information will contain 3 information: 2 references to the involved pieces, and a Vector2 that represents the relative position (transformB.position - transformA.position when in connected state). Now, with a context menu function or custom editor call, you should be able to generate the relative position fairly easily, the hard work is how to reduce the pain of assigning references... 
The trick that I found is to use Raycasting. It does NOT guarantee the detection of all adjacencies but will significantly reduce the need of manual operation.
The idea is, for each pair of two pieces A and B, draw a line (RaycastAll()) from A to B and see if the second hit collider is on B. Why second? because the first hit will always be A. 
This will work in most cases, EXCEPT if the contacting point is not on the drawn ray (like A and B in the graph above). To reduce these exceptional cases, you can try to sample multiple points on the same pair and see if at least one raycast is successful.  Again, even if you do so, it will still not guarantee that you detect all the adjacencies, as a well designed counter-exemple can easily make your effort useless, though in practice, such case is very rare.
In addition to that, I suggest to add OnDrawGizmos to the class storing the adjacency information, to visually show the adjacency so that it is easier to detect missing adjacencies.
To sum up, when you have properly created your adjacency recording system, first you need to finish the puzzle in the editor manually (so, if you have a large amount of pieces that can be problematic), and use the system to generate potential adjacency information, spot any missing adjacencies and add them manually, and finally use the system again to calculate the relative position in each adjacency.
To determine if two pieces should merge during runtime, first you need to know which piece is being dropped, and then verify for each adjacency information, if the piece is involved. If it is involved (say, it is pieceB), calculate the absolute position based on the position of the other piece (pieceA), and if the dropped piece is in certain range (Tolerance) of the absolute position, they are considered merged. Modify the position of the dropped piece directly to the absolute position, and delete the adjacency information (it is not useful anymore). 
To know which piece is being dropped in the adjacency storage class, my approach is to create an Action<DragAndDrop> onEndDrag in the DragAndDrop class and invoke the action in OnEndDrag(). Since in the adjacency class you have reference to all pieces, simply subscribe your merge verification to that action.
Deleting information after successful merge has two effect: (1) reduce the search time for further merge attempts, and (2) when the list of adjacency information is empty, you know all pieces are connected and the puzzle is complete. 
IV. Move Pieces as One
Now here is another tricky part. I have actually thought about two methods, I implemented the first, and the second is purely theoretical and I will not really discuss it in detail. I will leave the judgement of which one is better to you.
The First Method
This method is straightforward, you store all pieces linked to the piece you are moving in a list, and move them too. So each drag and drop object will also keep a list of references to the linked pieces. There are two critical problems that need to be solved though: (1) how to propagate the information when a new piece or group of pieces is being merged into a group? (2) how to calculate the correct displacement to ensure all of the pieces do not exceed the delimiting area and keep the global shape of the group unchanged?
For the first (1) point:   The process is actually like mathematical induction. We first assume the piece that should be linked contains all pieces currently linked to that piece (excluding itself). Then, follow the following steps illustrated below:

Basically, you construct two lists of linked objects in each group, and for each object in one list, you link it with each object in the other list. 

The presence of group of pieces means you will also need to change something when you detect merges (Step III.). You will also need to check if there is a merge for each linked piece of the dropped piece, otherwise you will have undeleted adjacency in your list even if visually the puzzle is complete. 
For the second (2) point:  You cannot apply movement directly for each piece. Instead, you need to separate the Attempt of moving the piece and actually Moving the piece. The Attempt returns a displacement of the piece, which can be less than the actual mouse delta since it can be truncated by the delimiting area. When you drag the piece under your mouse cursor, you gather all the displacement returned by the move attempts of all the linked pieces, as well as the piece itself. You can then take the shortest displacement to ensure that no piece will exceed the borders and call the actual Moving function with this shortest displacement provided as parameter. 
The Second Method
So this method delegates the moving part to Unity. My thought is that you can reparent the linked pieces to an object created at runtime, and whenever you click a piece in a group, the movement is redirected to the parent object (you can do this by using delegates). 
However when rethinking this potential idea, it can get a little messy, and since my first method is functioning, I quickly abandoned the second method...
Back to Top