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 a slight headache…
First, what are the use cases such a system needs to achieve?
-
Drag around pieces in a limited area.
-
When two pieces of matching edges connect, they are “merged”, and further dragging should treat those pieces as one single piece.
-
Merged pieces should still be inside the limited area
-
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 feasible, and you also need to keep track of different groups of merged pieces… also, 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 fell 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 a parameter can be confusing… I have tried to use the mouse position in that data, but it gives ridiculously huge values even after applying ScreenToWorldPoint
…
Well, use mouse position of 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 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 this information in its Awake()
. To define this information, 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 a 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 for 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 most times, 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-example can easily make your effort useless, though such a case is very rare.
Besides that, I suggest adding OnDrawGizmos
to the class by 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 a certain range (Tolerance) of the absolute position, they are 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> Drag
in the DragAndDrop class and invoke the action in Drag()
. Since in the adjacency class you have reference to all pieces, simply subscribe to your merge verification of that action.
Deleting information after successful merge has two effects: (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. I will leave the judgement of which one is better for 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 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 a 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 to each piece. Instead, you need to separate the Attempt to move 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 a parameter.
The Second Method
So this method delegates the moving part to Unity. My thought is that you can re-parent 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…