Skip to content

Reverse Inheritance

parchii edited this page Feb 13, 2024 · 2 revisions

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())

Example

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.

Notes

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.

Clone this wiki locally