Relations
Overview
The purpose of the relations.t extension is to allow game code to set up and then test relations between objects (or other entities). This could be used, for example, to model social relationships between the NPCs in your game. The extension also enables pathfinding through any relation you define.
New Classes, Objects and Properties
In addition to a number of items intended purely for internal use, this extension defines the following new classes and functions:
- Classes: Relation, DerivedRelation
- functions: relate(), related(), unrelate() and relationPath().
Usage
Include the relations.t file after the library files but before your game source files.
Defining Relations
You define a relation by defining a Relation object. The properties you need to define on Relation are name, reverseName, relationType and reciprocal. For example:
loving: Relation name = 'loves' reverseName = 'loved by' relationType = manyToOne ;
This can be abbreviated via the Relation template (defined in advlite.h) to:
loving: Relation 'loves' 'loved by' @manyToOne;
The name property can be uses to describe the relationship as a verb (e.g. John loves Mary). The reverseName is the name to use when we want to describe the relationship the other way round (if John loves Mary then Mary is loved by John); we could have defined the reverseName as 'is loved by', but that's a bit more typing. The relationType defines how many parties can appear on each side of the relationship. Here we've defined the loving relationship (in the sense of 'is in love with') to be manyToOne, meaning that each person can love at most one other person, but can be loved by several other people. (Jack and Jim may both Jill, but they can't simultaneously love anyone else - at least, not as we have defined the loving relationship). The other possible values of the relationType property are oneToOne (e.g. the marriage relationship) oneToMany (e.g. the father relationship) or manyToMany (e.g. the sibling relationship). Relations that are oneToOne or manyToMany can also be reciprocal (just define reciprocal = true on the Relationship object in question), as indeed the marriage and sibling relationships would be.
Making, Breaking and Testing Relations
Relationships are normally manipulated through the relate(), related() and unrelate() functions. To set up a relationship between a and b you call the function relate(a relation b). To cancel the relation between a and b you call unrelate(a relation b). To test whether a and b are related you use related(a relation b) and to get a list of items related to a via relation you use related(a relation). For example:
relate(jack, loving, jill); // makes Jack love Jill relate(jack, 'loves', jill); // another way of doing the same thing. relate(jill, 'loved by', jim) // makes Jill loved by Jim (another of making Jim love Jill) related(jack, loves', jim) // tests whether Jack loves Jim; in this case the answer would be nil related(jill, 'loved by') // returns a list of the people whom Jill is loved by; is this case [Jack, Jim] related(jack, 'loves') //returns a list of the people Jack loves; in this case [Jill] related(jim, loving) // returns a list of the people Jim loves; in this case [Jill]
It will be observed that when you specify the relationship in the forward direction (x loves y) you can use either the Relation object name (e.g. loving) or its name property (e.g. 'loves'), but when specifying it in reverse (x loved by y) you have to use the string defined on the reverseName property.
Breaking relations can be done in much the same way using the unrelate() function. If Jim no longer loves Jill you just call unrelate(jim, 'loves', jill).
To break all relations someone has, you can call relate() with nil as the third parameter; e.g.
relate(jim, 'loves', nil); // now Jim loves no one. relate(jill, 'loved by', nil); // now Jill isn't loved by anyone.
If you have defined a relation as reciprocal (by setting its reciprocal property to true), there's no need to define each related pair both ways round. For example if sibling is a reciprocal relationship then you don't need to use both relate(jack sibling jill) and relate(jill sibling jack), since the libary knows that the one implies the other. Likewise there's no need to define the reverseName property for a reciprocal relationship, since related(jack sibling jill) and related(jill sibling jack) mean exactly the same thing. A reciprocal relationship is already its own reverse.
Finally the Relation method listKeys() lists all the key values in the Relation's key table, in other words all a values for which this relationship has been defined (a related to b).
.Derived Relations
A good example of a many-to-many reciprocal relation might be a sibling relationship, which could be defined as:
sibling: Relation 'sibling of' @manyToMany +true; InitObject execute() { relate(john, sibling, mary); relate(john, sibling, luke); relate(mary, sibling, luke); }
Here the + property at the end of template defines the reciprocal property, so we're saying that sibling is a reciprocal relationship. Because of this the Relation class knows that if John is the sibling of Mary, then Mary must be the sibling of John (so we don't need to define it both ways round). Also, there's no need to define the inverseName property on a reciprocal Relation, since the inverse of 'sibling of' is simply 'sibling of'. But suppose we were also to define a fatherOf relationship, and that James was the father of John, Mary and Luke. We'd then end up doing something like this:
sibling: Relation 'sibling of' @manyToMany +true; fatherOf: Relation 'father of' 'child of' @oneToMany; InitObject execute() { relate(john, sibling, mary); relate(john, sibling, luke); relate(mary, sibling, luke); relate(james, fatherOf, mary); relate(james, fatherOf, luke); relate(james, fatherOf, john); }
At this point there seems to be some redundancy, since if James is the father of Mary, Luke and John, then Mary, Luke and John must be siblings of one another, so it seems a bit of a chore to have to state this explicitly. The solution is to use a DerivedRelation, which is a special kind of Relation that instead of storing any data of its own, works out what is related to what according to the custom relatedTo() and inverselyRelatedTo() methods you define, both of which should return a list of objects to which their argument is related; for example:
sibling: DerivedRelation 'sibling of' @manyToMany +true relatedTo(a) { local parent = related(a, 'child of'); return parent.length > 0 ? related(parent[1], fatherOf) - a : []; } inverselyRelatedTo(a) { return relatedTo(a); } ;
Here, in the relatedTo(a) method, we first find the father (or parent) of a by using the inverse of the fatherOf relationship ('child of') to get the person of whom a is the child. We then return a list of the children of this parent, less a, since a is not his or her own sibling. The method thus returns a list of a's siblings without the sibling Relation having to store any data of its own and without game code having to define explicitly who is the sibling of whom.
By default a DerivedRelation will complain if you try to use it to set a relation directly, e.g. (john, sibling, mary), since in general this may make no sense when the relationship in question depends on another one. In principle you could override the sibling's addRelation() method so that sibling.addRelation([a, b]) tried to give a and b a common father, but in general this is probably not a good idea. What, if a and b start out with different fathers, for example? It would be an even worse idea to override a DeriveRelation's removeRelation() method to allow the use of, say, unrelate(mary, sibling, john), since if two people have a common father, for example, what would it mean for them to cease to be siblings? Thus, once a DerivedRelation has been defined, you should stick to using it through the related() function or the relationPath() function discussed immediately below.
When setting up a DerivedRelation it can be useful to override its listKeys() method to return a list of all the values that might enter into the left-hand side of the relationship a related to b. At the very least this enables the RELATION debugging command to list the current relationships our DerivedRelation creates. For example, in the case of the sibling relation defined above, this could be either every Actor in the game, or every Actor that has at least one sibling (perhaps the better option if this isn't going to change in the course of the game):
sibling: DerivedRelation 'sibling of' @manyToMany +true relatedTo(a) { local parent = related(a, 'child of'); return parent.length > 0 ? related(parent[1], fatherOf) - a : []; } listKeys() { local vec = new Vector; /* Loop over every Actor in the game. */ for(local actor = firstObj(Actor); actor != nil; actor = nextObj(actor, Actor)) { if(relatedTo(actor).length > 0 ) vec.append(actor); } local lst = vec.toList(); /* * We can do this if we know this is never going to change to avoid * having to build this list more than once. */ listKeys = lst; return lst; } ;
We also have a choice how we want to define the derived relation. We can either override the relatedTo(a) method (as above) to return a list of items related to a or override the isRelated(a,b) to return true or nil according to whether a and b are related through this relationship. We must do either one or the other, but we shouldn't have to do both. In the same way, unless our DerivedRelation is reciprocal we must also define either its isInverselyRelated(a,b) method or its inverselyRelatedTo(a) method.
If we chose the second method, our definition of our sibling relation would become:
sibling: DerivedRelation 'sibling of' @manyToMany +true related(a,b) { /* a and b are siblings if a is not b and a and b have the same father */ return a != b && related(a, 'child of') == related(b, 'child of'); } listKeys() { local vec = new Vector; /* Loop over every Actor in the game. */ for(local actor = firstObj(Actor); actor != nil; actor = nextObj(actor, Actor)) { /* Calling relatedTo() here would create an infinite loop. */ if(related(actor, 'child of').length > 0) vec.append(actor); } local lst = vec.toList(); /* * We can do this if we know this is never going to change to avoid * having to build this list more than once. */ listKeys = lst; return lst; } ;
Note that the default relatedTo() on DerivedRelation calls listKeys() to help build its list, so if we haven't overridden relatedTo() to our own version of this method not referencing listKeys() we must avoid calling relatedTo() from listKeys(), otherwise our code will end up in a vicious circle causing our game to hang. Here we instead restrict our list of Actors to those whose fathers have been defined.
Finally, although here our example of a DerivedRelation has made use of other Relations, we're not restricted to this; we can define a DerivedRelation using whatever data structure(s) we find useful.
Relation Pathfinding
Relation pathfinding allows us to find the shortest path from a to b via any given relation (assuming that any such path exists). In general, the function relationPath(a, rel, b) returns a list giving the shortest path from a to b via rel (if there is one) or nil if no such path exists. For example, suppose that in addition to defining James as the father of John, we defined Andrew as the father of James, and John as the father of Mark. Then relationPath(andrew, fatherOf, mark) would return the list [andrew, james, john, mark], while relationPath(mark, 'child of', andrew) would return the list [mark, john, james, andrew]. On the other hand relationPath(mary, fatherOf, mark) would return nil, since there's no path from Mary to Mark through the fatherOf relation.
A potentially more interesting variant of this allows you to pass a list of relations as the second parameter. The function will then try to find a path from a to b via any of the relations listed, and if it finds a path it then returns a list of two-element lists showing the steps it took to get from a to b. For example, if we also made Simon the father of Aaron, then relationPath(mark, [fatherOf, sibling, 'childOf'], aaron) would return the list [[nil, mark], ['child of', john], ['child of', james], ['sibling of', simon], ['father of', aaron]], which means that Mark is the child of John who is the child of James who is the sibling of Simon who is the father of Aaron. By such means you could in principle trace the shortest relationship path through a family tree of any complexity, or of any other relationship network you wished to devise.
A Note on Syntax
The Relations extension makes use of a number of functions of the form relxxx(a, rel, b) or relxxx(a, b) since in the main this seems the most intuitive way of setting up, describing and testing relations, rather than through method calls on the underlying objects. In some cases, though, you may find it more convenient to use other forms of syntax.
In particular, if a game starts out with a number of items or people already related via a relation, you may find it more convenient to define these starting relations on the relTab property of the relevant Relation object rather than running code to call a whole lot of relate() functions. For this you purpose you would probably use the shortcut syntax for setting up the initial values of a LookupTable (which is what the relTab property should contain) like this:
someRelation: Relation relTab = [ obj1 -> [a, b] obj2 -> [c] ] ;
So, for example, instead of initializing the values of the fatherOf relation like so:
InitObject execute() { relate(james, fatherOf, mary); relate(james, fatherOf, luke); relate(james, fatherOf, john); relate(andrew, fatherOf, james); relate(andrew, fatherOf, simon); relate(john, fatherOf, mark); } ;
You could just do this:
fatherOf: Relation 'father of' 'child of' @oneToMany relTab = [ james -> [mary, luke, john], andrew -> [james, simon], john -> [mark] ] ;
There are two other abbreviated forms of syntax you can use if you really want to (although they're probably less clear than their slightly more verbose equivalents, and the way they read is potentially a little counter-intuitive):
- relation[a] = b is equivalent to relate(a, relation, b); e.g. fatherOf[john] = luke does the same as relate(john, fatherOf, luke)
- relation[obj] is equivalent to related(obj, relation); e.g. fatherOf[john] does the same as related(john, fatherOf).
Debugging Commands
When a game is compiled for debugging, the following commands are available to query and test relations:
The relations command lists all the relations defined in the game, with information about their type, e.g.:
>relations fatherOf oneToMany: name = 'father of' reverseName = 'child of' knows oneToOne: name = 'knows' reverseName = 'known by' loving manyToOne: name = 'loves' reverseName = 'loved by' overlooking manyToMany: name = 'overlooks' reverseName = 'overlooked by' sibling (DerivedRelation) manyToMany: (reciprocal): name = 'sibling of'
REL, RELATION or RELATIONS followed by the name of a relation lists the items related via that relation, e.g.:
>rel father of fatherOf oneToMany: name = 'father of' reverseName = 'child of' andrew -> [james, simon] james -> [mary, luke, john] john -> [mark] simon -> [aaron]
The EVAL command (which can be used with any valid expression) can also be used to set and test relations, e.g.:
>eval related(mark, loving) >eval relate(mark, loving, jill) nil >eval relate(jack, loving, jill) nil >eval related(mark, loving) Jill (Actor) >eval related(jill, 'loved by') Mark (Actor),Jack (Actor) >rel loving loving manyToOne: name = 'loves' reverseName = 'loved by' jack -> [jill] mark -> [jill]
This covers most of what you need to know to use this extension. For additional information see the source code and comments in the relations.t file.