ID:35530
 

A BYONDscape Classic! If you're just beginning to learn DM, datums might sound a little intimidating, but they're not so bad. Think of them as friendly ghosts, like Casper. Take it from someone who programs for a living: in the business world, there are no atoms, only datums. In the business world they're not always as friendly as you might hope, but that's another story. --Gughunter

Dream Tutor: Datums Are Our Friends

by Lummox JR

What is a datum? Sure, the DM reference lists it as a common ancestor to all types, from objs to turfs to clients, but is it any use beyond that? As a matter of fact it is.

BYOND is mostly all about atoms, the things with icons that appear on the map. Objs, turfs, mobs... all atoms. Even areas are atoms. And atoms are just great. They're our players, our walls and doors, our weapons, our bonus items. But what about the abstract?

A team, for example, is an abstract concept. It's a group of players that can have its own score, color, goal area, mission... you name it. And on the subject of abstracts, a color or a mission would count as an abstract concept. Kinds of knowledge are abstract, like skills in an RPG. You could use atoms for all of these, but that's kind of silly; these things don't need to have a physical location or move around.

Stat!

Let's start simple: A typical game will have player stats, like health or ammunition. Some of these have no (or few) limits on their value, so an ordinary var will do. But get into something like health and you now have a maximum to deal with; that gets a little complicated, doesn't it? No, of course it doesn't--but just wait until you're keeping track of a full swath of Roguelike stats like strength, dexterity, constitution, intelligence, charisma, and body odor; if each of those has a maximum value, you'll soon find yourself with a frightening number of vars to deal with. Fortunately you can create a datum to keep everything in one place.

stat
var/value = 0
var/max_value = 0

New(n)
if(n)
value = n
max_value = n

proc/Add(n)
value = min(max(value + n, 0), max_value)

proc/ToText()
return "[value]/[max_value]"

proc/ToPercent() // for fancier output
return "[round(100 * value / max_value)]%"

Well that's simple enough, but what does it do? And why isn't it obj/stat or something?

I'll answer the second question first: It's not an obj because it doesn't need to be. Is a stat something you could put in an inventory or pick up? Nope. It's just a set of numbers, so it doesn't have to have a location. By making it a datum instead of an atom, we save a little memory and also some typing. (After all, why type out obj/stat all the time?)

A datum can be a handy thing. Notice the Add() proc above; isn't that a lot easier than repeating the same type of code for a dozen different pairs of variables? In a typical RPG you could have stats out the wazoo, like hit points, magic points, strength, dexterity, constitution, charisma, and a whole lot more. Do you really want to copy and paste code and alter it--possibly risking bugs in the process--to deal with 25 different pairs of vars like health and max_health, MP and max_MP? Anyone who's had to type anything remotely like that knows the value of a time-saver.

To use this stat in a mob, just try something like this:
mob
var/stat/health
var/stat/magic

New()
..()
health = new(50) // health is now 50/50
magic = new(10) // magic is 10/10

Stat()
statpanel("Stats")
stat("Health", health.ToText())
stat("Magic", magic.ToText())

Not a bad way of doing things, is it? But wait... I'm using a datum called /stat, calling a proc called stat(), and using /mob/Stat(). Is that even legal?

Yes, it's perfectly legal. You can use a name for a datum that's also used by a proc. DM knows the difference between /stat and stat(). With the parentheses, DM knows it's a proc, in this case the built-in proc that displays lines on the player's statpanel. With the slash in front, DM knows we're talking about a type path, just like /obj/cookie or /turf/grass. The capitalized /mob/Stat() is yet another proc, but DM knows it's not the same as the built-in stat() because of the capital S. If you want to get masochistic in your code, you can even have a var named stat.

Now when we want to use those stats for something, that's easy enough:

mob
verb/Heal()
set src=usr
if(magic.value < 4)
usr << "You need 4 magic points to heal."
return
magic.value -= 4
health.Add(max(round(health.max_value / 5, 1), 1))
usr << "You restore 20% health."

Okay, that last one's gonna need a little explanation, isn't it? You probably figured out what health.Add() does, but what's that going into it? (This has nothing to do with datums, by the way.)

max(round(health.max_value / 5, 1), 1)

Here's the thing: In this example I wanted to restore 20% health. That would be the same as max_health--or in this case, health.max_value--divided by 5. Then I wanted to round that to the nearest whole number, just in case health.max_value is something like 14 that won't divide evenly by 5; round( ... ,1) will do that. Finally, I want to make sure at least 1 point is restored just in case health.max_value/5 happens to round off to 0. (It's a rare case, but humor me.) So that's why max( ... ,1) is there. I'm just covering all the bases, you see. Robust code is always a good thing.

There's No "I" In "Datum"

Teams and parties are a big concept in a lot of games. So how about a nice team system? It would need a list of players, and a team leader, maybe a color and a score. I'll write a hypothetical team system where each player has an icon matching the team's color.

var/list/teams = list()     // a global list of teams

team
var/name
var/list/players
var/score = 0

var/colorrgb
var/teamicon

New(mob/M, nm, r, g, b) // player M starts a new team
name = nm
players = list()
SetColor(r, g, b)
Add(M) // add M to the team
teams += src // add this team to the global list

Del()
teams -= src // take this off the global teams list
for(var/mob/M in players)
M << "[name] disbands."
M.team = null
M.icon = initial(M.icon)
..() // always call this at the end of Del()

proc/SetColor(r,g,b)
colorrgb = rgb(r, g, b)
var/icon/ic = new('team_player.dmi')
ic.Blend(colorrgb, ICON_MULTIPLY)
teamicon = fcopy_rsc(ic) // convert the /icon to a .dmi
for(var/mob/M in players)
M.icon = teamicon // change color

proc/Add(mob/M)
if(M.team)
if(M.team == src) return
M.team.Remove(M)
players << "[M.name] joins the team."
players += M
M << "You [(players.len>1)?"join":"form"] [name]."
M.team = src
M.icon = teamicon

proc/Remove(mob/M)
if(M.team == src)
M.team = null
M.icon = initial(M.icon)
players -= M
players << "[M.name] leaves the team."
M << "You [(players.len)?"leave":"disband"] [name]."
if(!players.len) del(src) // if the team is empty, delete it

mob
icon='player.dmi'
var/team/team // which team am I on?

Whoa! That's a lot of code! But not to worry, they're all short, simple procs that are pretty easy to understand. Let's start with the simple vars: The team has a name, a score, and a list of players. Then, it has vars for its color, and a var that will hold the team's colored icon.

Moving on to New(), it takes 5 arguments: A mob (the founder of the team), a name for the team, and red, green, and blue values for a color. The players list is initialized (it starts out empty), the color is set, the team's founder joins, and the team is added to the global teams list.

Del() will disband a team for you if you decide to just delete the team. If any players are left on the team, they're told of its disbanding and their icons are reset to normal. The team is removed from the global list, and then it simply no longer exists. The call to ..() is very important; without it, datum.Del() won't be called, and the datum won't actually be deleted. (I learned that one the hard way!)

SetColor() sets up the team's color. This can also be used to change colors, if you don't happen to like the one you've got. All this is pretty standard stuff. What might be new to you are /icon and fcopy_rsc(). The /icon type is like an icon file, but it's in an internally-stored format for quicker operations. In this case it's used to color an icon, multiplying it by rgb(r,g,b). The fcopy_rsc() proc will convert our /icon datum into an icon file that can be used later.

Finally, Add() and Remove() do just what you'd expect them to do. They alter the players list, change the mob's team var (it's nice to know what team you're on!) and icon, and even send out polite little messages to everyone on the team. In Remove(), you'll notice that little extra piece of code that says if the player who just left the team was its last member, the team doesn't exist anymore.

So who's the leader of this team, anyway? That will always be team.players[1]. The first person to join the team is the leader.

Heads Up!

All right, let's move on to something more fun. What if you wanted a player to be able to manage their inventory by clicking on a button on their screen and having a box pop up? What if you could use this to select equipment? That'd be cool, wouldn't it?

So this datum will be for an inventory box. What does the box need? Well, it needs to know which player it belongs to, that's a must. The player also needs to know they have a box, so they can close it. It needs to know which items appear on the player's screen. It also needs to keep track of objects created for the background of the box.

popupbox
var/client/client // who can see this box right now?
var/mob/M // the mob that goes with client
var/list/items // items to be shown on screen
var/list/screenitems
var/list/backdrop // the background
var/x, y
var/width, height

New(mob/_M, list/_items, _x, _y)
if(!_M || !_M.client) del(src)
M = _M
client = _M.client
items = _items
var/viewx, viewy
if(isnum(client.view))
viewx = client.view * 2 + 1
viewy = viewx
else
var/index = findtext(client.view, "x")
viewx = text2num(copytext(client.view, 1, index))
viewy = text2num(copytext(client.view, index+1))
x = _x; y = _y
if(x < 0) x += viewx+1 // x==-1 means right edge
if(y < 0) y += viewy+1 // y==-1 means top edge
// round up from items.len/viewx
height = round((items.len + viewx - 1) / viewx)
// round up from items.len/width
width = round((items.len + height - 1) / height)
if(x + width - 1 > viewx) x = max(viewx - width + 1, 1)
if(y + height - 1 > viewy) y = max(viewy - height + 1, 1)
backdrop = list(new/obj/backdrop(src))
screenitems = new
var/n = 0
for(_y in y+height-1 to y step -1) // fill from the top down
for(_x in x to (x+width-1))
if(++n > items.len) break
screenitems += new/obj/popupitem(src, "[_x],[_y]", items[n])
M.pbox = src

Del()
if(M && M.pbox == src) M.pbox = null
if(backdrop)
if(client) client.screen -= backdrop
for(var/O in backdrop)
del(O)
if(screenitems)
if(client) client.screen -= screenitems
for(var/O in screenitems)
del(O)
..()

proc/Select(atom/item)
del(src)

Okay, it's time for our first check. What is this doing? What you see here is a datum for a generic popup box. It relies on a few other things we'll get to in a minute, but look over the structure: popupbox.New() creates the box and fills it with items, then tells the player that it's their popup box by setting mob.pbox (a var we haven't defined yet). It sets up lists of objects that will be added to client.screen, so only one user will ever see them. popupbox.Del() is responsible for all the cleanup, so the box is easy to get rid of. The Select() proc is called when an item is selected.

You may be wondering why some of the vars above start with an underscore. The reason is that I wanted to use a similar name, but the var name I wanted to use was already taken. This datum defines M, x, and y, so to use local vars with a similar name I just went with _M, _x, and _y. You don't have to do it this way; if you just called them M and so on, DM would treat them as local vars (that is, they're only valid during the proc), and to use the vars belonging to the datum you would have to explicitly say src.M instead of M. I personally think naming local proc vars the same as the datum's vars is a great way to cause confusion, so it's something I rarely do.

The arguments to New() might seem a little weird, so let's go over them: The first is a reference to a mob, the player who owns the popup box. One of the first lines in this proc checks to see if the mob, M, has a client; that is, if a player is actually using this mob. If they don't have a client, there's no point in showing a popup box. Next comes a list of items that will be shown in the box; these items should be atoms. The items themselves aren't drawn on the screen: Instead, duplicates are made using /obj/popupitem. Finally, x and y coordinates are given for the lower left (southwest) corner of the box. Thanks to a couple of lines of code...

    if(x < 0) x += viewx + 1    // x==-1 means right edge
if(y < 0) y += viewy + 1 // y==-1 means top edge

...if you use a negative number for x, the box will start on the right side of the screen; it will stretch out to the west when it runs out of room. You can do the same thing with y, making it start at the top.

To understand all this a little better, we'll need to set up /obj/backdrop and /obj/popupitem.

obj/backdrop
var/popupbox/box
var/client/client
icon = 'popup.dmi'
icon_state = "backdrop"
layer = 20

New(popupbox/_box)
box = _box
client = box.client
screen_loc = "[box.x],[box.y] to \
[box.x+box.width-1],[box.y+box.height-1]"
if(client) client.screen += src

Del()
if(client) client.screen -= src
..()

Click()
box.Select(null)

obj/popupitem
var/popupbox/box
var/client/client
var/atom/item
layer = 21

New(popupbox/_box, newloc, atom/_item)
box = _box
client = box.client
screen_loc = newloc
item = _item
icon = item.icon
icon_state = item.icon_state
dir = item.dir
if(client) client.screen += src

Del()
if(client) client.screen -= src
..()

Click()
box.Select(item)

Now things may be a little clearer. The popup's backdrop is a single item that appears across a range of screen coordinates using screen_loc's special syntax. Both of these objs are similar: They add themselves to the screen, keep track of which box "owns" them, and can be easily deleted later. (They remove themselves from client.screen, but that's actually redundant because popupbox.Del() handles that. It never hurts to be safe, unless you're trying to squeeze out all the speed you can.) And both can be clicked; in either case they send a value to box.Select().

Let's back up a second and look at New() for each of these. Isn't an obj supposed to take its location as the first argument to New()? Yes, it is. But these objs aren't ever supposed to actually have their loc var set. They don't actually "go" anywhere; they're just drawn on the screen. If the first argument to New() isn't an atom, DS will ignore it and loc won't be set. For the backdrop, it only needs to know which box it belongs to, and it can figure out the rest from there. The individual items need to know where they'll be displayed, and which atom they represent.

Now we can put this to use and try out an inventory box.

popupbox/inventory
Select(obj/item/item)
if(istype(item)) item.Use()
del(src) // close the box

obj/item
verb/Get()
set src in oview(1)
if(usr.pbox) del(usr.pbox)
loc = usr
usr << "You pick up \a [src]."
verb/Drop()
set src in usr
if(usr.pbox) del(usr.pbox)
loc = usr.loc
usr << "You drop \a [src]."
verb/Use() // a user can Use any object they're carrying.
set src in usr
if(usr.pbox) del(usr.pbox)

mob
var/popupbox/pbox

Move()
if(pbox) del(pbox)
return ..()

verb/Inventory()
set src = usr
if(pbox) del(pbox)
if(contents.len)
pbox = new/popupbox/inventory(usr, contents, -1, 1)
else usr << "You aren't carrying anything."

That isn't too bad, is it? The mechanics of actually setting up the box are the hard part, and that's all done in the datum. All you have to do after that is decide when to pop it up, what to put in it, and when to delete it. The idea of a datum is to keep all the most complex code in one place, so you can use relatively clearer code everywhere else. To make that point a little better, let's also add a weapon selection popup. We can use /popupbox/inventory for this because it works the same way, but we'll give it a different list of items to choose from.

obj/item/weapon
Drop()
..()
if(usr.weapon == src) usr.weapon = null
Use()
..()
if(usr.weapon == src)
usr.weapon = null
usr << "You put away [src]."
else
usr.weapon = src
usr << "You arm yourself with [src]."

mob
var/obj/item/weapon/weapon

verb/Weapon() // choose a weapon
set src = usr
if(pbox) del(pbox)
var/list/L = list()
if(weapon) L += weapon // put the current weapon first
for(var/obj/item/weapon/w in contents)
if(w != weapon) L += w
if(L.len)
pbox=new/popupbox/inventory(usr, L, -1, 1)
else usr << "You are unarmed."

So you see, a datum can be used for pretty powerful things without making your code too messy.

(Notice I said you could click a button on the screen to pop this up, but I used a verb. It's pretty much the same code; I just didn't want to clutter the article by making those buttons. We'll have to cover special display objects in more detail another time.)

This popup box scheme is far from perfect of course. It has a few major flaws: It stretches out as far as it can in the x direction, which is probably undesirable; it might be more preferable to cover as much of the y direction as possible, or else to try to keep it more or less square. (Square is easy. Use -round(-sqrt(items.len)) to find the width.) And what if you run out of room? There are no scroll buttons set up here, and the box isn't designed to scroll through a list--but you could modify it to do so. One of the reasons popupbox.backdrop is a list, not just a simple var, is so you can add more items of your own--including borders, if you want to get fancy.

Datums FTW, i use them ALOT in WOTS 2.0, i've skimmed over it looks like a good tutorial, i'll look into it more when i have the time.
Attack of the Evil Blog Filter!

I've always liked this article.
You've got some entities leftover from BYONDScape in there. :P
Entities fixed. Incidentally BYOND 4.0 makes datums a lot easier to use.
Lummox JR wrote:
Incidentally BYOND 4.0 makes datums a lot easier to use.

The devil you say! Explain yourself, sir!
Datums are one thing Ive never really bothered looking into actually.
That article though makes it really easy to understand, and as soon as I get home from school datums are going to find heavy usage in making the Sabachthani code much more efficient <.<
Wow this is very useful, I definitely have too many vars and never bothered to learn datums.

Great tutorial!
"So that's why max( ... ,1) is there. I'm just covering all the bases, you see. Robust code is always a good thing."

I'm sure you meant, round( ... ,1) there.
No, I meant max. RTFA.
Lummox JR wrote:
No, I meant max. RTFA.

Could have sworn max() wasn't in there !

o_O Sorry.
Pirion wrote:
shouldn't the Add(M) be players.Add(M) ? sorry just trying to understand =p

No. He isn't calling list.Add(), but rather his own Add() process that he defined for the /team datum (which you'll notice, among other things, does add the player to the players list)