ID:36663
 
Click here to read "The parent_type var"

The parent_type var



Introducing Paths and Types

One of the first big ideas a programmer new to DM has to learn is the difference between the usr and src vars. Both of these variables hold a mob as their value and equal when dealing with verbs attached to mobs. However, understanding the difference between them is an important step toward mastering the language.

DM also has two very similar features in its object tree: types and paths. The two are related, and are the exact same thing in most projects on BYOND, but there is a difference, and understanding that difference gives a developer a set of powerful tools to work with. Before you understand how paths and types are different, though, you first have to understand what each represents.

Top

The Object Tree

If you've ever used the Dream Maker program, you're probably already familiar with the path tree; it is represented graphically in the left panel of the code editor (with the "Object" tab selected). If you haven't done so already, select the box "Show all nodes", now the entire path tree is visible. Here we can see various root nodes, like /mob, /obj, or /datum, and everything that we've defined under those nodes (like /mob/player, or /obj/sword). All of these are at the top of their path tree, which branches out underneath the root node (so "mob" would be a root node, which would then branch out to "mob/player" underneath, forming a tree structure).

If you've been using DM for a while, you're probably also familiar with the inheritance tree, which is made up of types (it could also be called the type tree). You're probably already familiar with the fact that objs and mobs both inherit from /atom/movable. You probably also know that /atom/movable, /area, and /turf all inherit from /atom. If you've been using DM for a while, you may also know that all objects inherit from /datum (well, there are some quirky exceptions, such as /world, but that's beyond the scope of this article). The important thing to notice is that, while the path tree had multiple root nodes, the inheritance tree has one root node: /datum.

At any one specific node, the type of an object and its path are equal. So, the type of a /mob is /mob, and its path is /mob. Beyond that, each word has a different meaning. Path refers to how the object is defined in the code file. We can think of an object's path as how DM finds the object in the object tree, its address. Type refers to the entire line of inheritance going all the way back to /datum. So for /mob, its full type is /datum/atom/movable/mob. We can think of an object's type as its family history.

Top

The parent_type var

So what is the difference? If our player object has the path /mob/player, and it inherits from /mob, aren't they the same thing? In this example they are, but they don't have to be. Every object in DM has a parent_type variable which determines what the object inherits from. Using the parent_type variable allows us to change the inheritance tree to whatever we want, while the path tree remains the same. Consider the following example.

mob
var
strength = 3
orc
strength = 5
verb/bash_things()
for(var/mob/target in range(1))
target.hit(strength, src)
elf
strength = 2
verb/shoot_arrow(mob/target as mob in view())
target.hit(strength, src)
dark_elf
parent_type = /mob/elf
strength = 3
desc = "pwns all"
shoot_arrow(mob/target as mob in view())
if(rand(1,4) <= 3) // Do the normal thing 3/4s of the time
.=..()
else // One out of four times, go crazy and bash things.
for(var/mob/target in range(1))
target.hit(strength, src)

In this example, the path to dark_elf is /mob/dark_elf, but dark_elf's type is /mob/elf/dark_elf (or, more exactly, /datum/atom/movable/mob/elf/dark_elf). dark_elf now inherits directly from /mob/elf, even though its type path is different. What this means is that we can place any object anywhere, and let it inherit from anywhere else. Though I can't think of a reason why you'd ever want to write something like the following example, it's good to show that such a thing can be done.

obj/item
var/potency = 1
verb/use(mob/target as mob in view())
potion
potency = 10
use(mob/target as mob in view())
target.adjust_hp(potency)
// Heal the target 10 hp.
weapon
use(mob/target as mob in view())
target.hit(potency, src)
sword
potency = 3
knife
potency = 1
darkest_elf
parent_type = /mob/dark_elf
strength = 300
// What's a /mob doing under /obj?

Here, darkest_elf isn't even an /obj, yet it can reside right there with the swords and knives as if it were a valid weapon. In fact, we can even instantiate it like in the following example.

mob/verb/test()
var/obj/item/weapon/darkest_elf/new_elf = new(loc)
world << new_elf.desc

If you compile and run that verb, you should see the text "pwns all".

Top

Type != Path

Though at this point it may seem redundant to state that paths and types are different, it is very important that we understand exactly how DM treats each of them. In many places DM seems like it wants a path, it's actually asking for a type. Consider the locate() proc.

mob/verb/test()
world << locate(/obj/item/weapon) in view()

This test verb will locate /obj/item/weapon, /obj/item/weapon/sword, and /obj/item/weapon/knife, but it will never locate /obj/item/weapon/darkest_elf. Though darkest_elf and the other weapons are all in the same place on the path tree, they are in completely different places on the inheritance tree. So we see that locate(), even though we supply it with a path, deals with types. In fact, most places that use paths in that manner are actually dealing with types:

  • locate(), when in the form of "locate(/type) in list", will search for objects which inherit from /type, whether or not they are located under /type in the path tree.
  • for() loops, when in the form of "for(var/type/T in list)", will loop through all objects which inherit from /type, whether or not they are located under /type in the path tree.
  • ismob(), and the other atomic 'is' procs, all deal with types, not paths.
  • istype() deals with types, not paths.
  • ispath(), when supplied with a second argument, also deals with types, not paths (even though you'd think otherwise).

These various behaviors won't make much sense until you start working with parent_type on a regular basis, and can be the cause of a great many problems when debugging a project. When in doubt, set up a simple test program to determine exactly how a proc functions.

Top

The Benefits of parent_type

Up to this point we have discussed how to use parent_type, the difference between path and type, and some potential problems with procs. What we haven't discussed is why parent_type might be useful in a project.

The largest and most immediate benefit is that we get to define objects the way we want to, putting them where we want in the code, and giving them the line of inheritance our project needs. Beyond this, parent_type can also help to make a project clean, extensible, and modular.

Top

Clean Environments

The parent_type variable gives us the ability to keep important objects within reach. We've already seen how mobs inherit through datum -> atom -> movable -> mob, but isn't it convenient that we can just type /mob? I would certainly hate to have to type /datum/atom/movable/mob every time I wanted to access a mob. I remember the first project I ever made in DM, Peregrine City. I defined NPCs as /mob/player/special/npc, and was annoyed every time I wanted to deal with them. It was important that NPCs inherit from all those nodes, but I used NPCs enough that they should have been closer. If I were writing Peregrine City today, I would define NPCs like in the following example.

NPC
parent_type = /mob/player/special

One of the first things I do in any project now-a-days is to define the base objects I'll be working with, and put those at the root. It's like having my own atomic set close at hand, so I don't have to go digging through the path tree every time I want to use a /unit or a /room. Using parent_type can help to prevent us from becoming confused or lost in our own project.

Top

Extensible Projects

Imagine that you have made a successful RPG, but you woke up today wanting to add a house system. You want players to be able to build houses with furniture and pets in them. So you open up Dream Maker and add the house system. You compile your game and host it. Everything goes fine until a player puts a pet into his house, and suddenly it starts attacking him, causing fatal runtime errors once he dies. What happened? The game worked fine before the house system was added, and there's absolutely nothing in the house system about attacking. Looking back through some of the game's oldest code, you find the following section:

mob
New()
.=..()
if(!client)
find_and_kill_players()

Social Verbs

Ever logged into a game and, before the icons have even loaded, you have access to all the social verbs? This is an example of inextensible code.

The first thing this game's programmer ever did was to add a say verb, right there on /mob. This test verb stayed at /mob/verb while the programmer added combat, shops, guilds, etc.. When he wanted to add a new verb, he added it right to /mob/verb with the rest of them.

The only problem is that clients are connected to a mob from the very instant they log into the game. Had the programmer defined these verbs under something like /mob/player right from the start, and kept /mob clean, new players wouldn't be able to spam the world with wsay messages and marriage proposals from right there at the title screen.

That code works fine in a game where all mobs without clients are monsters, but the moment we want to add pet mobs, the entire system is broken. The project wasn't extensible, because new features couldn't be added without breaking old features. I could devote an entire article (or series of books even) just to writing extensible code, but that's beyond the scope of this article. Suffice to say that code should be written so that new features can be added without making any changes to old code. If old features have to be rewritten every time something is added, the project isn't extensible.

Most projects on BYOND are not extensible, because most everyone defines important code right there at /mob (or /turf, or any of the other root nodes). If I define /mob one way, and you define /mob another, our two programs will never work together. Remember where I said that one of the first things I do in any project is to define important objects at the root? One of the main reasons for this is to avoid just such a situation. Even if we decide (when first writing the game) that all mobs will be able to fight, and we define a /combatant object which we use for players, monsters, and everything else, our project is still extensible. Later, when we want to add houses and pets, we can go back and use /mob without having to worry about our combat system. The following example shows our combatants and pets living peacefully.


combatant
    parent_type = /mob
    New()
        .=..()
        if(!client)
            find_and_kill_players()
pet
    parent_type = /mob
    New()
        .=..()
        purr_and_be_cuddly()


The atomic types (area, turf, obj, and mob in particular) are the basic building blocks of our worlds, so it's important to keep them clean. Once you break them in one place (for instance, by tying all mobs to a rigid combat system) they're broken in all parts of the code, and can't be used for anything else. Extensible code is code that has a future.

Top

Modular Systems

All the other points mentioned in this article are good to know for their own sake. Understanding the way DM internally treats types and paths can really shed some light on the language, and writing clean extensible projects is a good practice in and of itself, but the real beauty of using parent_type is in writing modules.

A module is an independent section of code, capable of functioning in a specific way all on its own. Modules, if programmed correctly, can be taken from one project and put into any other, and they will still function (so long as the new project is extensible, as discussed in the previous section). What is more important is that modules can be changed internally, even changed drastically, so long as the input and output remains consistent. Imagine that every day you go to the local diner, and order Waffles and Maple Syrup. One day the diner is sold, and the new owners install all new equipment, and hire new cooks. You walk in order your regular Waffles and Maple Syrup, and they come out to you just like they always have. You don't know, or even care, that the waffle iron is new, or that they're using a different brand of butter; all that you care about is your order (the input) and the waffles you get in return (the output). In this case, the diner is a module, one which can be changed internally without effecting the rest of its environment.

The diner wasn't always this sophisticated, though. Would you believe that at one point, it was just a guy on the street with Waffles in his pocket? Don't believe me? Here's the code:

obj/waffle
desc = "Tasty Waffly Goodness"
mob/Bob
verb/order(request)
switch(request)
if("Waffles and Maple Syrup")
return new /obj/waffle()

One day that man decided to open his own shop, but he realized that he would need his own building, a waffle iron, cooks, and wait staff. He didn't want to have to run to the store every time he needed a new waffle, he wanted to have everything right there. At that moment his diner became a module.

diner
parent_type = /area
staff
parent_type = /mob
waitress
verb/order(request in list("Waffles and Maple Syrup", "Scrambled Eggs and Coffee"))

var/diner/staff/cook/cook = locate() in view(5,src)
var/meal_type
if(request == "Waffles and Maple Syrup")
meal_type = /diner/breakfast/waffles
else if(request == "Scrambled Eggs and Coffee")
meal_type = /diner/breakfast/scrambled_eggs
var/diner/breakfast/meal = cook.make_food(meal_type)
return meal
cook
var
diner/staff/cook/appliance/waffle_iron/waffle_iron
diner/staff/cook/appliance/frying_pan/frying_pan
proc/make_food(request)
var/diner/breakfast/meal = new request()
switch(meal.type)
if(/diner/breakfast/waffles)
waffle_iron.cook(meal)
if(/diner/breakfast/scrambled_eggs)
frying_pan.cook(meal)
return meal
appliance
parent_type = /obj
proc/cook(diner/breakfast/meal)
sleep(meal.cooking_time)
meal.raw = FALSE
waffle_iron
frying_pan

breakfast
parent_type = /obj
var
cooking_time // number
price // number
raw = TRUE
waffles
desc = "Tasty Waffly Goodness"
cooking_time = 4
price = 2
scrambled_eggs
desc = "Bland Mushy Yellow stuff"
cooking_time = 6
price = 3

The diner example above is a bit lengthy, and can be hard to decipher at first. I would suggest putting it into Dream Maker and playing with it for a while. The important thing to notice is that the diner object, /diner, resides at the root, and defines all the other objects it will need to operate. It is a module, acting on its own, without relying on other blocks of code, or changing other blocks of code. This module could be picked up and put into any other project without causing problems (provided that the other project is extensible), or, it could be changed internally without effecting the project it is in.

Another important feature of this module is that its input and output didn't change. In both cases, you walk up to a person and use the order verb, and you are then given a meal in return. The internal process changed (perhaps to become more stable, to fix a bug, or because of threats from the health department), and the number of foods available was increased, but old code relying on the old behavior will still execute without any problems.

One last thing to notice about the above example. The /diner object has started to define its own sub-module, the /cook object. The cook module has started to define a class of objects called appliances. Right now these are thinly defined, but may be fleshed out in future incarnations of the diner. The rest of the project doesn't care, though; the only object that will ever use them is the /cook object.

After all, a menu system should never intrude into the diner and start redefining how waffle irons work, and you don't need a waitress defining core aspects of your battle system. Keeping all these systems as self contained modules prevents a small change in one place from destroying complex systems all throughout your project.

Top

A Note on Libraries

Perhaps the most important time to program modularly is in the production of libraries: A library's code should intrude as little as possible on any environments which might include it. A library should include as little root nodes as possible, and should only include ones that the user will use directly. Any object your library uses behind the scenes should be subtyped by the main object which uses it.

The same is true for variables and procs. Only variables which the user should have direct access to should be defined at the root, and you should generally keep the number of such global variables close to zero. Libraries also often contain global procs as part of the functionality provided. That being said, any procs used internally by the library shouldn't clutter the global procs list. If users have to dig through a lot of junk they don't understand (and in the worst cases, they don't even know where it came from), then they won't use your library. Remember that your library may be included in other libraries, and any clutter you've failed to clean up will be compounded by the clutter of the other libraries.

As a general rule, you should try and "hide" as much of your library as possible. Think of a microwave oven: Most of us have no clue how a microwave actually produces the waves which cook our food, and that's fine; as long as we know how to use it (put the food in, close the door, press a button) we don't need to know why it works. Your library should function in the same way, and programming it as a module will do just that.

Top

An Example: B17

As an example of what programming modularly can do, I've included a game designed to be clean, extensible, and modular. The entire game resides under one root node, /b17, which defines all the other objects and procs it needs to function.


B17: Solo Fortress source

First things first. Open the environment and uncheck the file 'demo.dm', then compile (you may need to upgrade your version of BYOND to the latest beta for features such as winset() and SEE_PIXELS). Note that the entire game resides snugly under the node /b17, and that each node in the project is an actual object which does something important in the game. In other words, /b17 isn't just a container for other objects that make a game, /b17 is a game object which further defines all the other objects it needs to run. Because the game doesn't define anything at /mob, /datum, or any of the other basic types, multiple games made in this way can reside in the same environment without conflicting.

Now, to see if it really works. Re-include the file 'demo.dm', and compile. If you look in the demo file, you'll see that it defines a basic "wrapper" world for the game to exist in; this world provides a simple skin, entry and exit points for the game, and a nice title screen. Run the .dmb and choose Game->Start from the menu (you may also want to adjust your display size). Standard movement keys to move, Center to drop bombs.

Read through demo.dm until you get a feel for how to interact with the /b17 object. Once you've got a feel for it, and maybe have written your own version of demo.dm, try including B17: Solo Fortress as a mini-game in one of your worlds.

Top


IainPeregrine

Nifty article Iain. I'd never really thought about the difference between paths and types. One of these days I need to learn how to program properly. :P
D=
*jumps back to his game code, eagerly rewriting everything so as to make it extensible and modular*
Good article!

One nitpick: "One of the first big ideas a programmer new to DM has to learn is the difference between the usr and src vars. Both of these variables hold a mob as their value"

This isn't quite true - src is not always a mob. It could be any kind of datum.