This howto is based on a post to the KiSS ML by Dov Sherman about using collide() for "localized" snap-to, and one from Alexandra Werres on how to turn snap-to on and off. I later added a FKiSS3 solution adapted from the FKiSS3 section of the jigsaw puzzle how-I.
"Snap-to" is when a cel, usually a clothing cel, jumps into position over another cel, usually the base doll, often after a collision event is triggered. (For an explanation of collision detection, see the top of the previous howto.) It relies on the FKiSS2 actions movebyx() and movebyy(), which move an object relative to the position of another object.
The general use for snap-to is having clothes position themselves correctly over the doll when the player drags them over it, and it has a few advantages:
However, players sometimes like to position clothing cels in a different way than the artist intended, and may be very irritated at having said cels shoot into place. To please everyone, the artist could make two cnfs, one with and one without snap-to code, or have just one cnf with the possibility of turning off the snap-to effect.
But first, how to add snap-to code:
In PlayFKiSS 0.8x, this is very easy. Right-click on the cel belonging to the "parent" object (usually the base doll) and select "Set Snap-to Parent". If this option is not in the menu, the viewer isn't in editor mode: go to Tools|Options, click on the second tab, and select "Editor mode", then try again. PlayFKiSS now remembers that the coordinates of the newly clicked-on object are the starting point for the snap-to code. Now position another cel over this parent object, right-click on it and select "Add snap-to code". Then, save the cnf. (If you've already added comments to the cnf, then back it up first, as PlayFKiSS rewrites the cnf when saving, flushing out the comments.)
If you open the cnf in a text editor, you will see the added lines in the FKiSS code block:
;@in(#1,#5) ;@ movebyx(#5,#1,9) ;@ movebyy(#5,#1,22)
Note that the order of the object numbers doesn't matter for the in() event, but it does matter for the movebyx/y() actions, as the first object number "snaps to" the second.
The actions under in() use object numbers, not cel names, because only objects can have coordinates. I happen to know that object #1 is the base doll and object #5 is a hat. The "movebyx(), movebyy()" code tells the top left corner of the hat object to move 9 pixels down and 22 pixels to the right from the top left corner of the base doll object. If the numbers had been negative, the hat would have moved up and to the left. The "in()" code says that this movement should happen when the hat object and the base doll object collide. (To be exact, when the bounding boxes of the two objects collide.)
Note that PlayFKiSS assumes you want snap-to to happen as a reaction to in(), ie. collision detection. Snap-to is not an event but a set of actions, and can be made to happen as a result of any event:
To make snap-to code in any other viewer, drag the parent object and its snap-to objects to exactly the right position, then save the cnf and open it in a text editor. The line of coordinates for the page the objects have been arranged on might look like this:
$0 0,0 0,0 59,56 138,156 133,6 9,22 125,120
Objects are counted starting at 0, so I can see that objects 0 to 6 have the
object #0: 0,0
object #1: 0,0
object #2: 59,56
object #3: 138,156
object #4: 133,6
object #5: 9,22
object #6: 125,120
The coordinates tell me how many pixels the top left corner of each object is
away from the top left corner of the screen. Since object #1 is at 0,0 and
object #5 is at 9,22, I can see at a glance that object #5 is 9 pixels down and
22 pixels to the right of object #1. If it had the same position relative to
object #2, and I wanted it to snap to object #2, I would have to do the
59 - 9 = 50, code: movebyx(#2,#5,50)
56 - 22 = 34, code: movebyy(#2,#5,34)
Just because object numbers are used to move the snap-to object, doesn't mean you need to use them for collision detection. If the viewer inderstands FKiSS2.1, there is the more accurate collide() which uses cel names:
;@collide("basedoll.cel","hat.cel") ;@ movebyx(#5,#1,9) ;@ movebyy(#5,#1,22)
If you can use FKiSS3, and think that collision detection is rather flakey, and want to make 100% sure that snap-to only happens as a result of the player dragging cels, you can put the snap-to code under release() rather than in(), and calculate whether the snap-to object is close enough to where it should be, using letobjectx(), letobjecty(), let(), add(), sub() and variables; then, use the FKiSS2.1 command moveto(), rather than movebyx() and movebyy(), for the snap-to movement. First, you have to find out what the snap-to object's coordinates should be, relative to the base doll's coordinates, as explained above.
Then, under the release() event of the snap-to object, using letobjectx/y(), you put the X and Y coordinates of the snap-to object in two variables, say, variable X and variable Y (FKiSS3 variables have a name of one letter, or one letter followed by one number), and the X and Y coordinates of the base object into two other variables, let's say A and B. Now, add the snap-to differences to A and B. The values in A and B are now the snap-to coordinates, ie. the coordinates the snap-to object should be moved to.
;@release(#5) ; snap-to object ; Store snap-to object's coordinates ;@ letobjectx(X,#5) letobjecty(Y,#5) ; Put basedoll's coordinates in variables ;@ letobjectx(A,#1) letobjecty(B,#1) ; Calculate snap-to positions ;@ add(A,A,9) add(B,B,22)
Find the positive difference between the snap-to object's current coordinates (X,Y) and the snap-to coordinates (A,B), by subtracting the smaller numbers from the bigger ones. Add the differences. If the total difference is less than a certain threshold value - let's say 20 - snap-to should happen, by setting X and Y to the snap-to coordinates, and moving the snap-to object to X and Y. Using this threshold value isn't as visually accurate as collide() - there may be no actual touching pixels - and you can't just put the hat on the doll anywhere, you have to move it close to where it needs to go, but if you assign the threshold value to a variable, you can use this variable to set a "snap-to distance" that you can change in a settings screen.
Since this distance check is a calculation you can repeat for any object, as it uses variables that have already been filled, let's put this code under a label that the release() event refers to, using gosub(), so the event goes to the label and then comes back to execute its own actions, in this case, the final moveto().
;@release(#5) ;@ letobjectx(X,#5) letobjecty(Y,#5) ;@ letobjectx(A,#1) letobjecty(B,#1) ;@ add(A,A,9) add(B,B,22) ;@ gosub(0) ; calculate move ;@ moveto(#5,X,Y) ; carry out move ;@label(0) ; Find positive difference on x and y axis, using variables C and D. ;@ ifgreaterthan(A,X) ;@ sub(C,A,X) ;@ else() ;@ sub(C,X,A) ;@ endif() ;@ ifgreaterthan(B,Y) ;@ sub(D,B,Y) ;@ else() ;@ sub(D,Y,B) ;@ endif() ; Put total difference in variable E, quit if too large ;@ add(E,C,D) ;@ ifgreaterthan(E,20) ; difference exceeds threshold value ;@ exitevent() ; quit without changing X and Y (moveto() will do nothing) ;@ else() ; Set snap-to object's new coordinates ;@ let(X,A) let(Y,B) ;@ endif()
Or if you do want collision detection, you can skip the label and forget about the threshold value: FKiSS3 lets you check collision as an action instead of an event, with letinside() or letcollide().
;@release(#5) ; Using object numbers and bounding boxes ;@ letinside(A,#1,#5) ; Or, using cel names (they better not be ambiguous cels!) ;@ letcollide(A,"basedoll.cel","hat.cel") ; A is either 1 (true) or 0 (false). ;@ ifequal(A,1) ; No calculation this time, just relative movement. ;@ movebyx(#5,#1,9) ;@ movebyy(#5,#1,22) ;@ endif()
To sum up before continuing: the FKiSS2 snap-to code in this howto uses collision detection, and the FKiSS3 code uses either collision detection, or variables and a calculation. Now for a refinement...
If I wanted simple snap-to, I could stick to one in() event: if the hat touches any part of the doll, it whizzes to the doll's head. But I may want to be able to put the hat in the doll's hands too, so it should only jump on the doll's head if it touches the doll's actual face. It is then more convenient to take a different object, which may simply be a circle in one colour, and put that in the same position as the doll's face, but one layer below it, so it is hidden by the doll, but can still trigger a collision event when the hat object touches it. If this new object, which is strictly for the localized snap-to, is object #7, then the FKiSS2 code becomes:
;@in(#7,#5) ;@ movebyx(#5,#1,9) ;@ movebyy(#5,#1,22)
Object #5 touches object #7, but snaps to object #1.
In the simpler version of the FKiSS3 code, this becomes:
;@release(#5) ; Using object numbers and bounding boxes ;@ letinside(A,#7,#5) ;@ ifequal(A,1) ;@ movebyx(#5,#1,9) ;@ movebyy(#5,#1,22) ;@ endif()or
;@release(#5) ; Or, using cel names ;@ letcollide(A,"circle.cel","hat.cel") ;@ ifequal(A,1) ;@ movebyx(#5,#1,9) ;@ movebyy(#5,#1,22) ;@ endif()
The complicated FKiSS3 code basically does localized snap-to already, as it doesn't check whether the hat touches the doll, but whether the hat is close to where it should be on the doll.
If snap-to is caused by a cel which is hidden, the snap-to effect can easily be turned off by unmapping that cel, and turned on by remapping it. This can also be done in many ways, but a simple way is with a button cel:
#1.999 basedoll.cel *0 :0 #7.999 circle.cel *0 :0 ; used in collision detection event [...] ;@press("button.cel") ;@ altmap(#7)
If you don't want localized snap-to, but want snap-to to happen when the hat touches any part of the doll, and you still want to be able to turn this on and off, you can assign the base doll cel to two different object numbers:
#1.999 basedoll.cel *0 :0 #7.999 basedoll.cel *0 :0 ; used in collision detection event [...] ;@press("button.cel") ;@ altmap(#7)
This will however not work with collide() or letcollide(), as "basedoll.cel", being used twice, is now ambiguous.
In FKiSS3, the simple snap-to code can also be turned off and on by mapping or unmapping an object, but the complicated code needs a variable, let's say "S". You can still let variable S depend on a cel being mapped, for instance "letmapped(S,"circle.cel")" but it's better to set S directly under the buttonpress event, so you don't have retain unnecessary cels or worry about ambiguous cel names. (Since all variables start out with a value of 0, set S to 1 under initialize() if you want snap-to on by default.)
;@initialize() ;@ let(S,1) ; turn snap-to on ; variable version of altmap code ;@press("button.cel") ;@ ifequal(S,0) ;@ let(S,1) ;@ else() ;@ let(S,0) ;@ endif() [release() event, then label:] ;@label(0) ; Is snap-to turned on? If not, just leave. ;@ ifequal(S,0) ;@ exitevent() ;@ endif() ; Find positive difference on x and y axis, using variables C and D. ;@ ifgreaterthan(A,X) [etc.]
If you use the simple snap-to code, the advantage of letinside() and letcollide() is that they don't care whether the objects or cels were colliding before, but only if they're colliding NOW. So if you turn snap-to off, drag the snap-to object half over the base doll and turn snap-to on again, you don't have to drag the snap-to object away and back again to trigger another in() or collide(); just click on the snap-to object, and it will move into place.
The complicated FKiSS3 code can also be used in FKiSS4, where variables can have longer names, and commands that accept an object number will also accept a variable containing that object number. This means that some object-specific code, like the moveto() action, can be put in the label itself, and the release() event only has to specify the desired snap-to coordinates. If there are many snap-to objects in a set, this cuts down on code.
;@release(#5) ;@ let(ObjNo,5) ; put object number in variable ;@ letobjectx(A,#1) letobjecty(B,#1) ;@ add(A,A,9) add(B,B,22) ;@ goto(0) ; calculate move: note, no gosub() needed ;@label(0) ; Is snap-to turned on? If not, just leave. ;@ ifequal(S,0) ;@ exitevent() ;@ endif() ; Object variable not filled? Leave. ;@ ifequal(ObjNo,-1) ;@ exitevent() ;@ endif() ; Get object's current coordinates ;@ letobjectx(X,ObjNo) letobjecty(Y,ObjNo) ; Find positive difference on x and y axis, using variables C and D. ;@ ifgreaterthan(A,X) ;@ sub(C,A,X) ;@ else() ;@ sub(C,X,A) ;@ endif() ;@ ifgreaterthan(B,Y) ;@ sub(D,B,Y) ;@ else() ;@ sub(D,Y,B) ;@ endif() ; Put total difference in variable E, quit if too large ;@ add(E,C,D) ;@ ifgreaterthan(E,20) ; difference exceeds threshold value ;@ nop() ; do nothing (I could have left this line blank) ;@ else() ;@ moveto(ObjNo,A,B) ; carry out move ;@ endif() ;@ let(ObjNo,-1) ; in case label is called again without setting ObjNo first
You can refine this howto's code examples in any way you like. For instance, if you have a doll where a clothing cel can be various things - big scarf, wide belt, wraparound skirt - you can either (collision detection) put hidden cels under the base doll at various positions, or (calculation) have a label figure out what ideal position the snap-to object is closest to, and make it snap to different positions, possibly mapping different cels to change its appearance according to its function. The demo set deals only with the simplest situation: one hat, one ribbon, one donkey, one button to turn snap-to on and off.
Click on the image to download the demo set.