Before continuing to read this tutorial, the reader should be fairly comfortable with IID, UID, the differences between an object type and an instance of an object, SOL and picking. To provide a brief terminology refresher:
A unique ID that is assigned at runtime for the lifetime of an object (effectively equivalent to a GUID), regardless of whether that object is destroyed, the game will not re-use that UID.
Index of an object within its own object type (this applies to families as well)
ObjectType vs Objects
This is a one-to-many relationship. If you have an ObjectType called RocketShip, you could put N number of that object on your layout.
The Selected Object List is the list of objects that have been filtered via the use of conditional Events. For example, if you have an Event which states EnemyShip Life < 10, then the SOL for EnemyShip will be restricted to EnemyShip objects with Life less than 10.
Why is it important to be able to distinguish between two picked objects that belong to the same Family in a single event? Why can't I just make On Collision events for each type of object in my game?
It's important because it helps with scalability. I think an example might be helpful here, let's say that you have 50 monsters in a game, ogres, trolls, imps, etc. All these objects belong to a family called Monsters. This family has several instance variables, one of which is a numerical variable called Power. Additionally, you have two other variables, Function_WinsBattle and Function_LosesBattle. These are string variables which store the names of functions (please see the Function plugin help for usage) that are specific to the type of monster. Each function takes a UID as a parameter so that they know what monster they should be modifying. Finally, all Monsters have an Experience level which may go up after defeating a monster.
So our Monster Family Variables List looks like this:
In this example, we've defined two general Win and Loss functions which are by default the functions for every monster. However, you might choose to override these on a per monster basis. You could have a function called "ImpWinsBattle" and "ImpLosesBattle" which do different things then "TrollLosesBattle" and "TrollWinsBattle". This allows you to setup customization per monster. By grouping everything in a family, it allows generalization and makes your script more flexible.
When a monster collides with another monster, you want to compare their power and run WinningMonster.Function_WinsBattle and LosingMonster.Function_LosesBattle for each monster respectively.
If you have 50 different monsters, you don't want to have fifty different On Collision routines, it's inefficient and it's not maintainable. (For more details from an optimization perspective, see the addendum to this article.) By putting them in a single family, we can shorten our Collision detection to a single event:
Now, here's the crux of the issue, the SOL for Monsters is two monsters of random type, yet you need to be able to distinguish between each Monster at this point in order to compare their Power levels and call the correct function for each one. So how do we do this? Strictly dealing with Family on Family collisions, there are a number of different approaches. I will discuss the pros and cons of each.
Solution #1: Can I just create two identical families?
For the above scenario we described, you would create two Families, Monsters_1 and Monsters_2 and put all the monsters in both of them. So your Projects folder would look like this:
Then you would just call:
While this solves the problem of being able to access both objects distinctly in the same descending subevent tree, you lose access to behaviors and instance variables that belong to Monsters_1 when referring to the Monsters_2 collision instance, in this case, Monsters_2 doesn't have the family variable Power.
Solution #2: Can I save off the IID's of each object after doing a Pick nth instance on each, and then use them through the parathentical operators ( ) ?
The answer is sort of but ONLY IF you are picking via Object and not Family. Since this entire tutorial is based on leveraging Families, we don't really get into this.
Why won't it work for Families?
Because an IID has no context within a Family, an IID always refers to the specific object's index within its own Object Type.
Even if you could assume that those IIDs would stay consistent through the lifetime of the function, it still wouldn't work as the IID would actually be the child object's IID, not the IID in context of the Family. To put it in simpler terms, let's say you had two Object Types in the game Box and Circle. On the layout, you have a single instance of each one, so one Box and one Circle. Internally, the Box has an IID of 0 since IID is a index to the set of objects of the same type. So can you guess what Circle's IID is? It's also 0.
Now let's assume you have an On Family Collides with Family Event between a Box and a Circle. When you use two subevents, Pick 0th Family, Pick 1th Family and save off the IID values into temporary variables for the Circle and Square, they're both equal to 0. Referring to Family.IID is going to get the IID for that specific object within its type.
This is rather abstract, so I've attached a Capx example to demonstrate why this doesn't work.
Solution #3: Saving the relevant information from each object into local variables
This involves making use of the "System Pick nth instance" event. In this approach, you pick each one in a separate subevent and save off the information you need to local variables. So what information do we need? Remember that we need to be able to compare their Power variables, and run the winner's Function_WinsBattle and conversely run the loser's Function_LosesBattle and also pass in the UID for the winner and loser to each one respectively.
Let's see what this would look like:
As you can see, we've created a set of local variables for all information we need. We save off that information in individual pick subevents. After this we have power comparisons and we call the appropriate function.
Now, let's define our GeneralWin and GeneralLoss functions.
Note that in each Function, it also does a simultaneous pick operation based on the UID value that we pass in from the Monster collision event routine. Since we passed the UID value of the specific Monster, we can pick using standard function parameter notation Function.Param(0).
Okay, let's say at this point you want to have a slightly different behavior for the troll if he loses. In our project asset list, we just change the Function_LosesBattle for Troll object to "TrollLosesBattle", and then we define the function in our event sheet:
All of this is vastly simplified for purposes of explanation but I'm sure you can see the power of combining Families and Functions.
Thanks for reading and feel free to leave some comments and/or questions if you have any.
There is another very important reason to take every effort to reduce the number of collision-related events, and that is performance. Construct's engine does not cache collision detection. What this means is that any time you do an Overlap or Collision detection event, it will recheck all objects related to that event even if you might have compared some of the objects before.
Let's take an example:
You have a family called BoxFamily with three object types, GreenBox, PinkBox, and YellowBox. On the layout, you have one of each object.
In order to be able to distinguish between the two objects involved in the collision, you might assume you can just write three collision checks from object to family, like so:
Every game cycle, it would execute the following:
GreenBox On Collision with Family - Two separate collision checks: X-Y and X-Z
PinkBox On Collision with Family - Two separate collision checks: Y-X and Y-Z
YellowBox On Collision with Family - Two separate collision checks: Z-X and Z-Y
Making for a total of 6 collision checks.
Not let's see what would happen if we used a single Family to Family collision event, as in the following:
Every game cycle, it would run the following check:
Collision Check: X-Y (the same as Y-X)
Collision Check: X-Z (the same as Z-X)
Collision Check: Y-Z (the same as X-Y)
Allowing us to half the number of collision checks! For a concrete example of this kind of performance gain in action, see the attached Benchmarking Capx. Click the buttons to switch between the types of collision detection, and hold the right-mouse button down in the layout to increase the number of boxes.