(Since I wrote this howto, it has become obsolete through the action letinside() added in FKiSS3. Go to the bottom of the page for an example of how to use it.)
I should first explain what collision detection is. Visually, it is when two cels touch or overlap. Technically, there is object collision detection, where collision means the touching of two objects' bounding boxes, for which FKiSS2 defined the events in(), out(), stillin() and stillout(); and cel collision detection, which means the touching of actual cel pixels, for which FKiSS2.1 added the events collide() and apart(). The events in() and collide() happen when two objects or cels are brought together, while out() and apart() happen when they are moved apart. The events stillin() and stillout(), as their names suggest, detect when objects are in a continued state of (non-)collision; they have no cel-based equivalents. (It should be self-evident that the FKiSS2 events work with object numbers, while the FKiSS2.1 events need cel names.)
Although I just said that visually, collision means two cels coming in contact with each other, this isn't always the case. Collision still happens when the objects or cels are transparent, ie. visually "not there", although I've found that, depending on the viewer, totally transparent (transparency: 255) objects may not have their collisions detected, so to ensure collision detection happens, transparency should be maximally 254.
In the case of object collision detection, the colliding bit is always inside the bounding box, but may not be a visible part of a cel. An object's bounding box is an imaginary square drawn around the visible outlines of all cels in the object. As such, it can contain a lot of empty space. Imagine if one cel in an object was a vertical line, and the other was a horizontal line; they would combine into a cross with four empty squares, one in each corner. Using collide(), collision detection would apply to the visible part of the specified cel; using in(), it would apply to the total outline of both cels - so long as at least one of them is mapped - including the empty space around them.
Two observations: first, does collision happen when cels or bounding boxes overlap, or does it happen even when their edges just touch each other? From my experience with FKiSS viewers, it's safe to assume the latter.
Second, and this matters to the howto: when is collision tested? Unlike, for instance, press() or unfix(), both events that mark one rapid change, the state of being in (non-)collision is constant until FKiSS code or user interaction changes it, so all collision events should be firing non-stop. So when does the viewer go down the list of its objects and cels and check whether any are (just now, still, or no longer) colliding (or not) with each other?
The state of collision depends on object coordinates, and whether an object is (partly) mapped. So a good time to check, even for cel collision detection, would be when the object coordinates change.
As being mapped or not also affects collision detection, another good time to check would be when the "mapped" status of (any part of) the object changes. However, sets were generally not made with this consideration in mind, and collision events triggered by changes in "mappedness" tended to break them. So, while some viewers may detect collision after mapping actions, it's safe to assume most don't.
Restricting collision detection to situations where an object changes position, there still is the question of what caused the change. Did the player drag the object, was the object moved through FKiSS code (and if so, did its coordinates actually change), or is the same object suddenly in a different position due to a page change? For instance, a vase can be in the middle of the playfield on page 0, and at the bottom of the playfield on page 1. If page 1 becomes active, through changeset() or the player choosing that page from the menu, and the vase seems to jump as a result, does that count as a position change, and a reason to check for collision?
That depends on the viewer; if it doesn't detect collision after FKiSS moving events, this howto is moot. That I wrote the howto at all, shows that viewers generally do; and that they generally DON'T detect collision on page change. All viewers detect collision after dragging an object with the mouse: it even says on the FKiSS reference page, which was finalized in 2008, covers all FKiSS syntax up to version 4, and is currently the final authority on FKiSS matters, that collision detection events apply to objects, cels and cel groups that are "moved by the user". So using move() on an object can trigger collision detection, but doesn't have to.
(Again; it usually does. UltraKiSS has many settings that can be turned on or off to make it compatible with sets written for other viewers, and one of those settings is "Enable collision effect on moveby[x/y] commands". In viewers where it doesn't, this howto will not work.)
The test case for this howto is a vase object, which is in a different spot on each page, and a bunch-of-flowers object which has two cels, one simply the bunch of flowers, the other the same bunch of flowers wrapped up in paper. By alternately mapping these two bunch-of-flowers cels, I can "wrap" and "unwrap" the bunch of flowers. When the flowers touch the vase, I want them to pop into the vase and lose their wrap, and when I pull them away from the vase, I want them to become wrapped again. The code for wrapping and unwrapping flowers looks like this:
;@initialize() ;@ unmap("flowers.cel") ; ;@in(#1,#2) ; where #1 is the vase and #2 is the flowers ;@ movebyx(#1,#2,-10) movebyy(#1,#2,-65) ; pop flowers in vase ;@ unmap("flowrap.cel") map ("flowers.cel") ; unwrap flowers ;@out(#1,#2) ;@ map("flowers.cel") unmap ("flowrap.cel") ; re-wrap flowers
If I stay on page 0, this will work; the flowers pop into the vase and lose their wrap as soon as they are dragged over the vase. But now I change to page 2, where the flowers and the vase are not overlapping at all; moving actions only happen on the page being viewed, so the flowers were only put in the vase on page 0, but mapping actions happen on all pages, so the unwrapping happens on both page 0 and 2. The viewer doesn't detect that when I go to page 2, the flowers and vase are separate, and "out(#1,#2)" is not triggered, leaving the unwrapped flowers hanging in mid-air.
But I do want it triggered, else the flower-vase trick doesn't seem to work, so I put in a page change event for each page that features a vase and flowers, which in this case is all pages, so from 0 to 9:
;@set(0) ;@ move(#1,0,100) ; that should move the flowers completely clear of the vase ;@ move(#1,0,-100) ; now they're back where they were ; ;@set(1) ;@ move(#1,0,100) ;@ move(#1,0,-100) ; ;@set(2) ;@ move(#1,0,100) ;@ move(#1,0,-100) [etc]and so on until set(9).
What happens is that, as the end position of the flowers after all these move
actions is the same, they'll stay exactly where they are; but the viewer sees a
moving action, and checks if the flowers are in or out of contact with the vase.
Trying this code again two decades later, in viewers that didn't exist in their present form at that time, I see it doesn't work in one of them; GnomeKiSS, to be exact. Because the move() doesn't trigger out() or in(). Presumably because while the move does trigger collision detection, no change in the state of collision is detected.
There are collision events for when the state of collision does not change: stillin() and stillout(), mentioned at the top of this tutorial. Adding stillout() to out() and putting notify() under both events, I and loading the set in GnomeKiSS, I saw out() triggered only once, but stillout triggered five times when loading the set, twice when dragging the object - when clicking on it, and when releasing it after dragging - and a whopping seven times on page change, with the move() code under set(); after removing that code, stillout() only fired twice, I assume before and after page change. In UltraKiSS, stillout() was triggered twice at page change, with or without the move() code.
Normally, I don't use stillout() and stillin(), as I find them unreliable, but testing them with page change makes me see why stillout() doesn't work as advertised. The same goes for stillin(), which I tested in UltraKiSS; dragging the flowers onto the vase triggered two stillin()s before the in(). The non-change events are not mutually exclusive with the change events; they even seem to be part of the mechanism to detect whether there was a change.
As such, stillin() and stillout() can succeed in detecting collision after page change, where in() and out() cannot.
I changed the code to make the move less extreme, as the flowers no longer need to separate from the vase to get a reaction, and added stillin() and stillout():
;@in(#1, #2) ;@ movebyx(#1, #2, -10) ;@ movebyy(#1, #2, -65) ;@ map("flowers.cel") unmap("flowrap.cel") ;@out(#1, #2) ;@ unmap("flowers.cel") map("flowrap.cel") ; ; in case IN and OUT don't register: ;@stillin(#1, #2) ; the flowers should be aligned with the vase already, so just map: ;@ map("flowers.cel") unmap("flowrap.cel") ;@stillout(#1, #2) ;@ unmap("flowers.cel") map("flowrap.cel") ; retrigger collision detection at each set change ; by moving flowers one pixel back and forth ;@set(0) ;@ move(#1, 0, 1) move(#1, 0, -1) ;@set(1) ;@ move(#1, 0, 1) move(#1, 0, -1) [etc]
This works in both GnomeKiSS and UltraKiSS. But since stillout() happens on page change even without the move() code: is move() under set() still necessary? Maybe for older viewers, but I made a copy of the cnf without this move() code, and that still works in both GnomeKiSS and UltraKiSS.
In a FKiSS3-capable viewer, all of the above can be disregarded anyway, because FKiSS3 has collision detection, not just as an event, but as an action: letinside(). What this means is, you get to choose which event leads to collision detection! Which resolves the whole issue!
Used with variable L and an if-else-end block, letinside() can be hung under set() as follows:
;@set(0) ;@ letinside(L,#1,#2) ; are #1 and #2 colliding right now? ;@ ifequal(L,1) ; YES! ;@ movebyx(#1, #2, -10) ;@ movebyy(#1, #2, -65) ;@ map("flowers.cel") unmap("flowrap.cel") ;@ else() ; NO! ;@ unmap("flowers.cel") map("flowrap.cel") ;@ endif()
Since this collision detection is needed under nine more set() events and a release() event of the dragged object, I move the code from "set(0)" to "label(0)", which the aforementioned events will call:
; Check collision on having dragged flowers ;@release(#1) ;@ goto(0) ; ; Check collision after page change ;@set(0) ;@ goto(0) ;@set(1) ;@ goto(0) ;@set(2) ;@ goto(0) ;@set(3) ;@ goto(0) [etc]
Click on the image to download the demo set, with a cnf for all three cases.