On Metacomputers - PART II


LAST TIME: PART I

Chicken AI

Recently I wrote a very simple metacomputer for processing NPC cutscenes. My gargantuan, ugly and bug-prone cutscene code was reduced to a few lines of Lua.

Chickens in The Real Texas

The past few days I've begun this same process for enemy AI. Last night I ported the chicken to the new AI system. The previous code was 380 lines; the new code is closer to 60. The chicken has complex behaviout that can branch a number of different ways.

Real Chicken Source Code Here

-- we prefer to spawn around chickenfeed

{ "find_prefer_objects_by_property", "chickenfeed", 0.75, "chickenfeed" },

{ "find_movement_pos", 0, 0, "prefer" },

{ "set_object", "chicken" },

-- periodically refresh our scare object (anything within 5 blocks that scares chickens)

{ "call_refresh", "refresh_scare_object", 0.1 },

{ "on_object_goto", "scare_object", "flapping" },

{ "on_offside_goto", "offside" },

{ "on_zapped_goto", "zapped" },

-- general behaviour decider; normally we will step, since stepping is a shorter time frame at least

{ "label", "decide" },

{ "goto_if_spread_mag_gt", 2, "stepping" },

{ "goto_if_spread_mag_gt", 1, "decide_stepping_90" },

{ "label", "decide_stepping_70" },

{ "goto_RN_lt", 0.3, "standing" },

{ "goto", "stepping" },

{ "label", "decide_stepping_90" },

{ "goto_RN_lt", 0.1, "standing" },

{ "goto", "stepping" },

-- set state: standing

{ "label", "standing" },

{ "move_still" },

{ "animate", "standing", "chickencluck" },

{ "wait_seconds", 0.5, 1.25 },

{ "goto", "decide" },

-- set state: stepping

{ "label", "stepping" },

{ "move_spread", 1.0, 2.0, "walking" },

{ "wait_seconds", 0.5, 1.25 },

{ "goto", "standing" },

-- target when we get zapped

{ "label", "zapped" },

{ "on_object_goto", "scare_object" },

{ "move_still" },

{ "animate", "vibrating", "zap" },

{ "effect", "spark" },

{ "wait_seconds", 1.5 },

{ "effect" },

{ "animate" },

{ "on_object_goto", "scare_object", "flapping" },

{ "on_zapped_goto", "zapped" },

{ "goto", "standing" },

-- target if we go offside (just disappear)

{ "label", "offside" },

{ "end" },

-- target if we get scared (have a scare object)

{ "label", "flapping" },

{ "choose_scatter_reverse_factor", 0.2, 0.2 },

{ "move_scatter_from_object", "scare_object", 1.0, 5.25, "flapping", "chickenflap" },

{ "wait_seconds", 0.1, 0.2 },

{ "goto_if_object", "scare_object", "flapping" }, -- loop while we have a scare object

{ "on_object_goto", "scare_object", "flapping" }, -- reset our scare target so this will get called again

{ "goto", "standing" },

-- periodically called to refresh our scare object

{ "label", "refresh_scare_object" },

{ "find_near_object_by_property", "scare_object", 5, "scares_chickens" },

{ "return" },

That's it. Each line contains a command, and parameters. For example, { "move_still" } means "stand still". { "smackplayer", 2.5 } means push the player back with magnitude 2.5.

Adding Context

Context is what really gives power to a metacomputer. The above code runs on a metacomputer that understands a certain amount of context. For instance, if we tell it to move somewhere, it implicitly knows it's destination.

Suppose we tell it to walk to a certain position. We can then tell it { "on_neardest_goto", 0.25, "some_label" } which means, "when you are near to your destination (less than .25 bocks away), stop whatever you're doing and go to some_label". This meaning is intuitive to the game designer and is easy to read. The previous code for this would have been split between a few places in the source file and not easy to understand.

Syntax

The big weakness with this style of programming is syntax. A normal programming language will have a lot of syntactical "sugar", little helpers in the language that let us do things. This metacomputer does not even allow for expressions, so for instance to go somewhere we have to set a context variable:

{ "find_player_pos" }

{ "add_pos", 0, 0, -1.5 }

Means, more or less, "cursor = player_position (); cursor.z -= 1.5;" in a more conventional programming language. The cursor is part of context; it's like a variable, and later when we call e.g., { "move_line" }, we just implicitly know the cursor. We only have one such cursor, so we can't even define variables!

General Specific

However there is a balancing act here. Normally programming langauges are highly general, and so they have a lot of tools built in. These tools are meant to make it easier to attack certain problems, which I would argue is the same as helping the programmer to create appropriate metacomputers inside the language.

But these types of metacomputers such as those above are highly specific. They don't need to attack a broad range of problems, just a very specialized one. For that reason, a dumb list of commands such as the one above tends to be more than enough.

Complex Commands

Some commands imply quite a lot! For instance, the "move_scatter_from_object" employs an entire swarming subsystem that helps objects move away from each other, so we don't get a chicken buildup in one corner.

Creating this metacomputer in C++ or another OO language, we would probably create a "ObjectScatter" class that encapsulated this information. But using this class still requires us to (in code) understand all of the context that integrates this code with other code. When we create a proper metacomputer such as the one above, we can still encapsulate this code but we only need to integrate it with the metacomputer itself, not every enemy AI we write.

You will need...

For this type of metaprogramming to really work well, I would recommend a language that supports:

- Typeless variables

- Easily described arrays (e.g., JSON would be fine, here, too.)

- Closures

Conclusion

These past two blog posts have been a bit more technical than I usually write. Hopefully it has been helpful to somebody, at least in thinking about problems a little bit differently.

Most programmers I meet are busy making highly generalized systems, with lovely interchangable components. I wonder sometimes if we don't lose a lot of context when we do this. We end up with the feeling, at the end, that we've created a system that we can make anything with, but that doesn't actually save us that much work.

If you like, you can see these blog posts as an suggestion to think about more specific, context-driven systems. Just my $0.02!

Thanks for reading and next time we'll try and keep it a bit lighter, hmn!

2010-04-09


◀ Back