-
Notifications
You must be signed in to change notification settings - Fork 0
Reverse Inheritance
The idea of "reverse inheritance" (as I am calling it because I lack vocabulary) is relatively uncommon, and potentially difficult to wrap ones head around. This is what could hopefully be an article that will clear it up how it works
Normal inheritance is likely familiar. It is GameMaker's object inheritance model, as well as the vast majority of programming language's inheritance model.
An inheritance tree could look like this:
top
| middle
| | bottom
In which middle
is top
's child, and bottom
is middle
's child.
Say that each of these nodes could use a meow()
method. Any child node may want to run their parent's meow()
because of whatever reason.
If bottom
is being dealt with, in traditional inheritance, bottom
's meow()
would be run first. This would be followed by middle
, then top
, because each node records their parent, and thus can easily call their parent's meow()
.
In what I'm calling reverse inheritance, this is flipped. When bottom
is being dealt with, the parent tree is crawled and top
gets run first. This is then followed by middle
, then bottom
.
In other words, in traditional inheritance, the child has the burden of inheritance (by calling super()
or GameMaker's event_inherited()
).
In reverse inheritance, the parent has the burden of inheritance (in calico's case, by calling calico_child()
)
This is potentially unhelpful for programming languages, but for state machines, this is more practical.
That same inheritance tree can be represented in calico like this:
template = calico_template()
.state("top")
.child("middle")
.child("bottom")
Each of these nodes (states) can be given a "meow()
method" (a trigger)
template = calico_template()
.init("bottom")
.state("top")
.on("meow", function() {
show_debug_message("top")
})
.child("middle")
.on("meow", function() {
show_debug_message("middle")
})
.child("bottom")
.on("meow", function() {
show_debug_message("bottom")
})
Note that bottom
is the initial state. When later run with calico_run()
, only "top"
will print to the console, because top
gets run first, and top
doesn't delegate execution to its child.
template = calico_template()
.init("bottom")
.state("top")
.on("meow", function(_machine) {
show_debug_message("top")
calico_child(_machine)
})
.child("middle")
.on("meow", function(_machine) {
calico_child(_machine)
show_debug_message("middle")
})
.child("bottom")
.on("meow", function(_machine) {
show_debug_message("bottom")
calico_child(_machine)
})
"top"
still gets printed first. Then "bottom"
gets printed, because middle
delegates execution to its child before it prints "middle"
. bottom
attempts to delegate, but it doesn't have a child, so nothing happens. Finally, execution is returned to middle
and "middle"
is printed.
The most clear use for this is setting up variables before states are run.
template = calico_template()
.init("idle")
.state("base")
.on("step", function(_machine) {
key_jump = keyboard_check_pressed(vk_space)
calico_child(_machine)
})
.child("idle")
.on("step", function(_machine) {
if key_jump calico_change(_machine, "jump")
})
.state("jump")
.on("step", function(_machine) {
// whatever
})
The input key_jump
is able to be set up inside the state machine, before idle
is run.
If bottom
's meow()
is run here, what should happen?
top
.meow()
| middle
| | bottom
.meow()
Note that middle
doesn't have a meow()
. Theoretically, bottom
's meow()
is unreachable, because the inheritance tree is crawled first, top
is run first, delegates to middle
which can't delegate because it doesn't have a meow()
. In calico, when this happens, middle
delegates automatically.
In other words, a state will automatically delegate execution if a handler trigger wasn't set up.