ID:1626900
 
This is going to be a big "stop it now" post. Object oriented heirarchy is not a database. Polymorphic design dictates that where behavior differs, a split in object heirarchy will exist.

Following on from what I said last week in the map editor thread, I'm going to go into more depth about when to define objects, and when instead to use a database.


Behavior vs Variation

In order to best understand when to define new object types, we need to understand what behavior and variation are. Behavior is best explained as anything that happens within a proc or verb (function), or when we change the basic properties or structure (members) that a type has. Variation is when we change variables.

For instance:

Creating a type:
turf
sometype


Specifying new behavior:
turf
sometype
Enter(atom/movable/o)
return 0


Also specifying new behavior:
turf
sometype
var
somevar = 0


variation:
turf
sometype
density = 1


Adding new procs, or new variables changes the structure of the type, and therefore, it is considered a change in the behavior of the type. It is not a change of behavior simply to change an existing variable. This is called variation, and results in duplicate nodes in the type tree.

It is sometimes useful to have the duplicate nodes for the sake of reference, but the vast majority of the time, it is not appropriate to have these duplicate nodes lying around in the tree, due to it making the project larger, the type tree more confusing, and it unnecessarily obfuscating the operations of your project.


Heirarchy, not a database, ya dingus!

You will see examples of something like this rampant throughout BYOND projects:

obj/items
newbiesword
attack = 4
defense = 2
bettersword
attack = 8
defense = 4


This approach does have some advantages over my approach, but I will continue to argue that it's wrong for a number of reasons:

1) If an item is removed from the game, loading savefiles with items of that type will begin to fail.

2) It unnecessarily ties the properties of items to specific instances.

3) It unnecessarily bloats the item tree.

Instead, let's introduce a new way of doing things.

First, my requirements are this:

1) Item prototypes should contain all variations for a specified item type.

2) Item prototypes should not need to be subclassed to be functional. They should automagically do their work without regard of what the item itself is.

3) The Item prototypes should be able to be saved and restored from savefiles, as well as defined in code in a single line.

item_prototype
var
id = 0
itemtype
list/properties = list()
proc
Instance(loc=null)
new itemtype(loc,id)

Inherit(obj/item/ref)
for(var/prop in properties)
ref.vars[prop] = properties[prop]

New(id,itemtype,list/properties)
src.id = id
src.itemtype = itemtype
src.properties = properties


That's really all that's required of the item prototype object. You will notice that this prototype object takes a number, a type path, and a list as arguments for initialization. This will come in handy later.

However, we also need to define an item object with the specified New() behavior too:

obj
item
var
id = null
New(loc=null,id=null)
..(loc)
if(id)
src.id = id
if(src.id)
InheritItem(src.id,src)

Read(savefile/F)
..(F)
InheritItem(id,src)


Okay, now we have some global stuff to define. We need to keep a database of items around, and the ID of the item needs to point to a unique item_prototype object in the global database.

var
list/item_database = list()
proc
InheritItem(id,obj/item/ref)
var/item_prototype/proto = item_database[id]
proto.Inherit(ref)



Now, let's take a look at how to define new items for your game!

#define ITEM_SWORD "item_sword"
#define ITEM_SPEAR "item_spear"
#define ITEM_SHIELD "item_shield"
#define ITEM_POTION "item_potion"

var
list/item_database = list(
ITEM_SWORD = new/item_prototype(ITEM_SWORD,/obj/item/equipment/weapon,list(weapon_type="sword",icon='sword.dmi',min_dmg=4,max_dmg=8)),
ITEM_SPEAR = new/item_prototype(ITEM_SPEAR,/obj/item/equipment/weapon,list(weapon_type="spear",icon='spear.dmi',min_dmg=6,max_dmg=12,hands=2),
ITEM_SHIELD = new/item_prototype(ITEM_SHIELD,/obj/item/equipment/shield,list(icon='shield.dmi',defense=10),
ITEM_POTION = new/item_prototype(ITEM_POTION,/obj/item/consumable/health_potion,list(icon='hp_pot.dmi',restore=25))



That's really all there is to it. Notice how we are only defining new types for the underlying behavior, and not one per item? Notice how sword and spear share the same behavior, and therefore share the same type?

There are additional pros to my approach:

1) This approach allows you to add new items in-game on the fly.
2) This approach uses #defines for handling item types. This allows more versatile object creation without costly path traversal operations.
3) This approach doesn't clutter the type tree.


In addition, in case you want the ability to create modified item stacks of a particular item type, I'll go ahead and include a version of the above example that does just that:

var
list/item_database = list()
proc
InheritItem(id,obj/item/ref)
var/item_prototype/proto = item_database[id]
proto.Inherit(ref)

item_prototype
var
id = 0
itemtype
list/properties = list()
proc
Instance(loc=null)
new itemtype(loc,id)

Inherit(obj/item/ref)
for(var/prop in properties)
ref.vars[prop] = properties[prop]
if(ref.modifications)
for(var/prop in ref.modifications)
ref.vars[prop] = ref.modifications[prop]
ref.modifications = null

New(id,itemtype,list/properties)
src.id = id
src.itemtype = itemtype
src.properties = properties

obj
item
var
id = null
tmp
list/modifications
New(loc=null,id=null,list/modifications=null)
..(loc)
if(modifications)
src.modifications = modifications
if(id)
src.id = id
if(src.id)
InheritItem(src.id,src)

Read(savefile/F)
..(F)
InheritItem(id,src)


The above example will allow you to create modified types on the map simply by editing the modifications list and typing in an id. It is also good for initializing modified types via code simply by using the new parameters. In addition, it will allow you to specify the properties that should be modified from the default. This means you can have items that match the prototype's id, but have a different icon, for instance.

Next week's Snippet sunday will actually be postponed until the following tuesday due to the BYOND game Jam. Please provide topic suggestions below!
1+
Ter13 wrote:
automagically

+1 As always!
Excellent post as always! I'll be honest, I've definitely been guilty of this in the past.

Ter13 wrote:
Next week's Snippet sunday will actually be postponed until the following tuesday due to the BYOND game Jam.

Oh no, not a delay! I love my Snippet Sunday! Also does this mean you'd like to join?
Yeah, I'm already tottering around with ideas, but since the themes aren't going to be announced until friday, I can't do too much ahead of time. ;P
Can't wait for the next one.

KEEP IT UPPP!!!
I feel like parent_type should've been mentioned here but it was woefully passed by.
I feel like parent_type should've been mentioned here but it was woefully passed by.

Why would it be required? It's simply a shortcut. Use it or don't. It makes no real difference. I'm not here to enforce standards. I'm here to show some simple tools that allow you to write better, cleaner, stronger code.
If you're gonna go through the effort of describing hierarchy, you should mention parent_type since it's a rather useful tool that isn't used nearly as often as it could/should be.

Unless of course, you're one of those guys that do this
x = x + 10
x = x + 1
// instead of
x += 10
x ++
++x
Did you even read the article? I mean, it's obvious you didn't understand it, but did you even read it?

Talking about parent type in the above would be equivalent to talking about spark plugs while showing someone how to do an oil change. Yeah, it's useful knowledge, but it's slightly off topic.
Update: Thank yous go out to Lige and Higoten for pointing out some problems with my implementation. It should be noted that a large part of the code I write on these forums has never been compiled, and as such, may contain errors. Anyone spotting one, I would be much obliged if you should point it out just like these two fine gentlemen have done.
So if all weapons are of type /obj/item/equipment/weapon and the developer wants to place a spear on the map (in the map editor), how would they do that? Also, I'm guessing to use the code you wrote you can no longer use object constructors for anything else. You're making a lot of things more complicated just for some strange goal of using the object heirarchy only a certain way.

It makes more sense to say that the object hierarchy starts by breaking things down by behavior, but, at some point, transitions to storing variations. It seems obvious that the type hierarchy is more than a "database", as you call it, but it might not be obvious to everyone. A lot of people here probably got their intro to OOP from BYOND (whether they know what OOP means or not!) so they see object types being used to enumerate the possible variations and think "oh, that's what object types are for" without realizing there's more to it. It's not that people are wrong for storing variations as different types but that they're wrong for thinking that's all object types are for.

The native types give you some behavioral distinctions - objs and mobs can move, turfs can't, etc. They only give you behavioral distinctions because that's all they can do, they don't know what kinds of turfs you'll need. Based on the needs of your project you might create some more behavioral distinctions of your own. For example, /obj/item are objects that can be in your inventory, /obj/item/weapon are items that can also be equipped as weapons. But, after that point, the hierarchy does become a database and each child of /obj/item/weapon is likely just a variation with different stats.

The only problem I can see with using object types as a mix of traditional OO stuff and as a database of object variations is that the distinction is kind of arbitrary. At some point along the type path, types stop representing core behavioral differences and start being basic variations. I wonder how it'd work to use parent_type as a way of formalizing this - you collapse the behavioral types down to a "single" type using parent_type and use its children as the variations.

Instead of /obj/item/weapon you'd have this:

weapon
parent_type = /obj/item

var
damage = 1

basic_sword
damage = 5

better_sword
damage = 12


Or maybe it'd be better to use parent_type at the first crucial distinction. So you'd have item's parent_type be /obj, but /weapon might not have a parent_type set.
In response to Forum_account
Forum_account wrote:
So if all weapons are of type /obj/item/equipment/weapon and the developer wants to place a spear on the map (in the map editor), how would they do that?

Place it on the map and edit it's id to "item_spear".
Also, I'm guessing to use the code you wrote you can no longer use object constructors for anything else.

Wrong. Items must be constructed with a base type. There is no other function. This doesn't limit the constructors, it simply defines core behavior. Provided you use ..() properly, you can override and extend the constructor just fine. BYOND is quite good at handling multiple overrides of the same function, most people just don't know it.

So if all weapons are of type /obj/item/equipment/weapon and the developer wants to place a spear on the map (in the map editor), how would they do that?

They would edit the item's id to "item_spear", furthermore, they could also create a modified type by editing the "modifications" variable in the map editor. My approach in no way limits what you can do.

It makes more sense to say that the object hierarchy starts by breaking things down by behavior, but, at some point, transitions to storing variations.

It's insanely wrong to use object hierarchy as a database. Storing variations as a new hierarchal structure is just plain backward. There isn't much more to say about that. If you are going to argue that it's okay to have a type per item in your game's code... Well, frankly I don't know what to do to convince you otherwise. It's just not what OO is supposed to look like, and it's going to clutter your project's hierarchy quite a bit unneccessarily.

For example, /obj/item are objects that can be in your inventory, /obj/item/weapon are items that can also be equipped as weapons. But, after that point, the hierarchy does become a database and each child of /obj/item/weapon is likely just a variation with different stats.

I'm not sure you are following what my code does. It fully eliminates the need for the database within the hierarchy, and allows you to create a database within your code without cluttering the hierarchy with it.

Saying this in any way complicates things is a bit suspect, as it merely changes things, rather than making them more complicated.

I wonder how it'd work to use parent_type as a way of formalizing this - you collapse the behavioral types down to a "single" type using parent_type and use its children as the variations.

This is exactly the habit I'm trying to offer a counter-approach to. Unfortunately, this kind of a habit has become far too common, and it's breaking our developers on ever being able to migrate to Java, C# and C++. They say: "Man, it's so weird I can't change variables between subclasses and their parent class". DM trains them to expect that will work, and unfortunately, it's one of maybe five languages I know of that can do that.

It trains the developer to use OO hierarchies in the wrong way, and simply by using a tiny bit of code, I have corrected that, and honestly made the language a lot more comfortable for myself.
Thank you for this post. I can utilize it immediately, and was having similar thoughts of "Isn't this really unnecessarily cluttered?" myself recently.
Lige wrote:
Place it on the map and edit it's id to "item_spear".

Which takes longer and will be a lot more confusing since it won't inherit values like icon and icon_state until runtime.

Ter13 wrote:
It's insanely wrong to use object hierarchy as a database. ... It's just not what OO is supposed to look like...

OOP doesn't usually involve map editors either. Game development is different than application development so you shouldn't expect OOP to be used the same way.

In application development the difference between objects tends to be in their logic (what their methods do) and your data can vary in nearly limitless ways. You might have a class called Person that has "name" and "age" properties but they can have any value. There's not a finite number of name/age combinations you support - that'd make no sense. In games, that's often exactly what we have. Here we have a base weapon class (which'd be abstract if DM had such a thing) with min/max damage values and child types that define the possible unique weapons. It's not that we're using OOP incorrectly in game development if we define these weapons as child types, it's just a case that rarely comes up in application development.

and it's going to clutter your project's hierarchy quite a bit unneccessarily.

How is that a problem?

It's a tree, if you use it reasonably (e.g. weapons go in /obj/item/weapon and armor goes in /obj/item/armor) there won't be clutter. If you don't want the weapons getting in your way, collapse that node in the tree view. If you're going to have 200 kinds of items you're going to have "clutter" in some sense but that's what the tree is for. Putting all item definitions in a single list is a worse way to organize things.

By using actual types you get all of the benefits of DM's object inheritance (e.g. you can define and override methods) and you get its syntax. Here's some code that uses types to override values but also overrides a method. How would you implement this using your item database?

weapon
var
damage = 1

proc
attack(mob/attacker, mob/defender)
attacker.deal_damage(defender, src.damage)

stick
damage = 3

sword
damage = 8

fire_sword
damage = 10

attack(mob/a, mob/d)
// deal damage like we normally would.
..()

// but also have a 25% chance to set the guy on fire.
if(prob(25))
a.set_on_fire(d)


I have a feeling it either won't be easy, or it'll end up looking just like this code (e.g. you still define the types and you just move the "damage" property to your item_database list).

Soft-coding inheritance might be useful for something. Maybe if you have a special item system where everything is dynamic and generated at runtime you need to have this flexibility, but I wouldn't recommend it as *the* way to implement object types.

This is exactly the habit I'm trying to offer a counter-approach to. Unfortunately, this kind of a habit has become far too common, and it's breaking our developers on ever being able to migrate to Java, C# and C++.

I'd guess that's because people don't really understand object-oriented programming. They primarily use object types to enumerate simple variations in data and they might override methods here and there, but they don't really think about objects and inheritance. They didn't first learn about how inheritance and overriding methods can simplify their code and help them not repeat things, *then* realize they can override a turf's Entered proc. They learned they could override a turf's Entered proc to make it damage all players who step there without really understanding anything about OOP.

Your post seems to say "if you're just going to use object types to store data, put the data in a list instead" but the real problem is that people shouldn't just be storing data in object types. Most developers could likely simplify their code by using inheritance better but they don't really know how, so they don't use it, and that's why they simply use types like a database.
I have a feeling it either won't be easy, or it'll end up looking just like this code (e.g. you still define the types and you just move the "damage" property to your item_database list).

obj
item
equipment
weapon
flaming
attack(mob/a,mob/d)
..()
if(prob(25)
a.onFire(d)


The behavior changes warrant subtypes. I'm not certain you are fully understanding what the purpose of this is, once again.

This merely prevents *variation* filling your type tree. *Behavioral or structural* changes warrant new subtypes. Variation alone does not warrant new subtypes.
In response to Ter13
Ter13 wrote:
I have a feeling it either won't be easy, or it'll end up looking just like this code (e.g. you still define the types and you just move the "damage" property to your item_database list).

> obj
> item
> equipment
> weapon
> flaming
> attack(mob/a,mob/d)
> ..()
> if(prob(25)
> a.onFire(d)
>

The behavior changes warrant subtypes. I'm not certain you are fully understanding what the purpose of this is, once again.

It's good to have some kind of convention you follow but you should have subtypes for whatever makes the most sense. It's not always behavioral differences that make the most sense. DM doesn't have multiple inheritance so if you're using types to mean behavioral differences, this means you can't have one object inherit two types of behavior. For example, if you've got a type for flaming weapons and a type for ranged weapons, you can't make a flamethrower that inherits from both. By using subtypes to represent generalized functionality you've worked yourself into a corner.

Because you're limited to single inheritance you have to be smart about choosing what you use inheritance for. Inheritance is a way to avoid having to repeat yourself. If you create a weapon type for flaming weapons, you're saving yourself from writing 2-4 lines of code for every flaming weapon (based on the example above) - that's not very much, so making this particular behavioral distinction isn't very useful. Considering the limitations with inheritance here, this probably isn't the best way to use subtypes.

This merely prevents *variation* filling your type tree. *Behavioral or structural* changes warrant new subtypes. Variation alone does not warrant new subtypes.

What problems do you run into when the type tree contains many variations? You've said that it's bad but you didn't explain why it's bad. If you're going to jump through hoops to use this item database of yours, there should be a huge problem that you're avoiding - can you describe the problem we'd avoid by using the approach you presented here?
If you're going to jump through hoops

You've yet to clarify what hoops those are. I don't exactly get where you are coming from, at all. On the one hand, you agree that polymorphic types don't constitute a database, on the other hand, you seem to think that it's fine to reinitialize variables as some sort of a database structure.

I'm confused as to what your point is. I suspect you are too.
I'm totally guilty...but I think I will try this in my next project. Just one question, is there any performance increase that could be gained from "doing the right thing?" Or is it really all about robustness and whatnot?
Page: 1 2 3 4