Probably you are wondering, "That's ok, but I want pieces snap each other". Thats the most complicated part of it.
First you have to add some instance variables to Actualpiece:
"PlaceX": Logical column of the piece inside the puzzle. Set value to 0.
"PieceY": Logical row of the piece inside the puzzle. Set value to 0.
"Group": Group of "snapped" pieces this piece belongs to. Set value to 0.
Add also the "Pin" and "Flash" behaviours to ActualPiece. We will use "Flash" for testing, and "Pin" for piece grouping.
First of all, we have to fill each ActualPiece with the right values. At the end of the "for tilev" event (just after "Paste Object PieceMask into canvas), add these actions:
Set PlaceX to loopindex("tileh")
Set PlaceY to loopindex("tilev")
Set Group to self.PlaceY*(LogicalW)+self.PlaceX
Now we have to compare the pieces when we drop Actualpiece. This is the most complicated part.
Start by making an Actualpiece.OnDrop event.
Add a blank sub-event under it.
Add four local variables over the blank sub-event:
Local number CompareX=0 (To store current Actualpiece.PositionX)
Local number CompareY=0 (To store current Actualpiece.PositionY)
Local number IdealX=0 (To store the ideal X for neighbour piece).
Local number IdealY=0 (To store the ideal Y for neighbour piece).
Inside the blank sub-event, we'll set the values:
(Blank event)=> Set MyX to ActualPiece.X
(Blank event)=> Set MyY to ActualPiece.Y
(Blank event)=> Set CompareX to ActualPiece.PlaceX
(Blank event)=> Set CompareY to ActualPiece.PlaceY
Below it, we'll add another sub-event (so we will be working on a sub-subevent). That subevent will have three conditions:
First condition: System.Pick all ActualPiece.
Second condition: System.For each actualpiece
Checking for pieces on top, bottom, left and right in a single condition
The third condition will be a single condition checking for pieces on top, left, bottom and right. To create that condition, we will follow this logic:
Top and bottom pieces are pieces with the same X than our piece, but 1 less or more in the Y coordinate. This can be represented by:
(ActualPiece.PlaceX=CompareX) & abs(CompareY- ActualPiece.PlaceY)=1)
Left and right pieces are pieces with the same Y than our piece, but 1 less or more in the X coordinate. This can be represented by:
(ActualPiece.PlaceY=CompareY) & (abs(CompareX- ActualPiece.PlaceX)=1)
If we mix both conditions, we get:
((ActualPiece.PlaceX=CompareX) & (abs(CompareY- ActualPiece.PlaceY)=1)) |((ActualPiece.PlaceY=CompareY) & (abs(CompareX- ActualPiece.PlaceX)=1))
Mixing all conditions altogether will save us from repeating similar conditions once and again.
Now, add the following condition inside the sub-event:
System.Compare two values:
First value: ((ActualPiece.PlaceX=CompareX) & (abs(CompareY- ActualPiece.PlaceY)=1)) | ((ActualPiece.PlaceY=CompareY) & (abs(CompareX- ActualPiece.PlaceX)=1))
Comparison: Is different
Second value: 0
Let's test our expression. Add the following action to your three-condition event:
Let's run the layout. If our condition is well-formed, if we drag a piece, the pieces around its "hole" will flash, telling us we are looking for these pieces.
Are out target pieces in the right place?
Now we have to check if these pieces are close enough to be snapped around us. Add the following actions below Actualpiece.flash:
Set IdealX to (ActualPiece.PlaceX-CompareX)*PieceStepX+MyX
Set IdealY to (ActualPiece.PlaceY-CompareY)*PieceStepY+MyY
These two variables will store were we want the neighbour to be.
If our neighbour is on the left (ActualPiece.PlaceX-CompareX=-1), the piece will be PieceStepX pixels on the left of MyX, thus its coordinates will be (PieceStepx * -1) + MyX = -PieceStepX + MyX = MyX-PieceStepX.
If our neighbour is in the right, instead, the piece will be PiecestepX pixels on the right (ActualPiece.PlaceX-CompareX=+1, we will we adding instead of subtracting).
If our neighbour is on top or on bottom, similar calculations will be issued on IdealY.
Add another subevent (a sub-sub-subevent!) with the following condition:
Distance(ActualPiece.X, ActualPiece.Y, Ideal.X, Ideal.Y) = 5
So, we are checking if the pieces are, at least, 5 pixels away. You can change "5" for your preferred "error margin". The biggest, the easiest your game will be.
We use distance() as an easy way to calculate distance between pieces. I think using abs() and subtractions should be faster (distance() calculates a square root), but, after all the previous calculations I think you prefer an easy way.
Finally, move the "Flash" action to the last subevent and run the project. Pieces should flash now when "snapping" at the best position.
The previous point should be the more difficult. Now we just have to join pieces together, using the "Pin" behaviour. But, before we use "Pin", we should move the pieces slightly to their best match. What's their best match? --Guess it: (IdealX, IdealY)
So, below the "Flash" action, add:
ActualPiece=>Set position (IdealX, IdealY)
There is also another obstacle. Imagine you add the following code to your project:
ActualPiece=>Pin to another object =>...
We will find two difficulties.
First of all, we don't know which object to pin ActualPiece to. We need to store an UID to the ActualPiece instance we were dropping, but, if we do that, we will loose our reference of the matching ActualPiece. So pinning between instances of the same object is not easy!
Second, even if we pin two ActualPieces together, the first will move the second, but the second won't move the first (at least on the tests I did a long time ago). This is why we need "grouping".
Add the following to your code:
On the local variables block below "Actualpiece.On dragdrop drop", add a local variable called "MyGroup", and set it to 0.
At the blank event under the local variables, below "Set compareY to ActualPiece.PlaceY", add:
System=>Set Value MyGroup to ActualPiece.Group
Then, at the bottom subevent, after the "ActualPiece.SetPosition" action, add this one:
ActualPiece=>Set Group to ActualPiece.Group.
Now go to the layout view and insert an empty sprite (without image) called Pin. In the sample file, we have made it visible and blue for you to see it, but it should be invisible in a production environment.
Add the "Pin" behaviour to the "Pin" sprite.
Then, return to the "ActualPiece.On Drag" event and add:
System.Create Object Pin at (ActualPiece.X,ActualPiece.Y)
Pin Pin to ActualPiece (Position and angle)
We destroy the Pin before pinning it to the piece. This way, if we have something pinned to the original "Pin" object, it is "un-pinned" from it (this is faster than looking for objects pinned to it).
Below it, add a blank sub event. On top of the blank sub-event, add the following local variables:
MyUID (value:0). This will store current ActualPiece UID.
MyGroup (value:0). This will store current ActualPiece Group.
On the blank subevent, add:
Set MyUID to ActualPiece.UID
Set MyGroup to ActualPiece.Group
Add a sub-subevent to the blank subevent, with the following conditions:
System.Pick all Actualpiece
ActualPiece. Compare instance Variable Group with MyGroup
System. Pick ActualPiece by evaluating ActualPiece.UID<>MyUID.
Put the following action on it:
If you run the project now, you will see you can drag a piece and snap it to another one. But there are undesired behaviours, since pieces in the group will not "snap" if they are not the actual piece we are dragging. To fix it, we should use a loop to check all the pieces in the "dropped" group against pieces not in the group.
We will try to do it in a further tutorial.
Next in series
Making a Jigsaw Puzzle - Part four: Managing grouped pieces explains how to check the whole group for matching pieces and how to join groups together, along with minor details such as adding some audio, shuffling pieces or running a special event when the JigSaw is solved.