ID:120564
 
In case you missed it, be sure to check out programming tips #1

When you first start programming, the problem you're trying to solve is "how do I write code that'll do ______?". Initially you're happy just to get something working, who cares what the code looks like. But when you try to develop a complete game this indifference can become a problem. As your code gets messier and messier, the project becomes harder to work on - you're more likely to write code that has bugs and they become harder to track down and fix. Eventually it'll get to the point where you're wasting a lot of time, don't feel productive, and are more likely to stop working on the project.

As a more experienced programmer, instead of asking "how do I do ______?", you should start asking "what's the best way to do ______?". In this post we'll look at different ways to write a simple function to see how certain ways are better than others.


Suppose you have a proc that's called when one mob deals damage to another and you're not sure if it should belong to the mob dealing damage or the mob taking damage. There are many ways you could look at this. For instance, you could argue that the proc should belong to the mob that's performing the action. The attacker is doing the action, so it should look like this:

mob
proc
attack(mob/target, damage)
target.health -= damage


That makes sense, but you could also argue that the mob being attacked is the one who's really performing the action. The attacker just generates a damage value, the real work is done by the mob being attacked because there may be a lot of things to take into account - armor, elemental resistances, chance to evade, etc.

So, you could also implement it like this:

mob
proc
attack(mob/target, damage)
target.take_damage(src, damage)

proc
take_damage(mob/attacker, damage)
health -= damage


That also makes sense, but you may have some situations where the attacker is more actively involved. The chance for the attack to hit depends on the attacker and the target, so who does that belong to?

It's very easy to get stuck on an issue like this. You want to do things the right way but you keep going in circles and can't bring yourself to implement the feature at all. While it's good to think about code before you write it, if you spend too much time thinking and not enough time coding, you'll just be wasting time. At some point it becomes better to just pick one way to implement it, write the code, and see how it turns out.

In this case, we'll arbitrarily decide to go with the first method and have everything handled by a proc belonging to the attacker. Here's the code we end up with:

mob
proc
attack(mob/target, damage)

var/hit = rand(speed + target.speed) <= speed

if(hit)
damage = (damage + power) * (speed / target.speed) - armor
else
damage = 0

target.health -= damage


First we determine if the attack hits or not. If it does, we apply some modifications to the damage value based on the attacker and defender's stats (if the attack didn't hit, the damage is zero).

Do you see any problems with that code?

The problem is on the line after the if statement. The end of that line should be target.armor instead of armor because we're reducing damage based on the target's armor value, not the attackers. But, because the proc to attack a mob belongs to the mob, forgetting to type "target." makes it reference the attacker's variable. Since they're both mobs this var exists and the code compiles but is not correct.

You can waste time going in circles like that and completely miss an alternative. Even worse, the alternative can make itself obvious if you just picked a method and went with it. If you had gone ahead and just implemented the proc either way, you'd have more easily come up with a preferred option:

proc
attack(mob/attacker, mob/defender, damage)

var/hit = rand(attacker.speed + defender.speed) <= attacker.speed

if(hit)
damage = (damage + attacker.power) * (attacker.speed / defender.speed) - defender.armor
else
damage = 0

defender.health -= damage


By making it a global proc you're forcing yourself to specify if the var belongs to the attacker or defender. You're less likely to make mistakes and are more likely to catch them because you have to explicitly state which mob each var belongs to.


There are two lessons to take away from this:

1. Know that your code will change so don't think that you're going to get everything right on the first try. Think about your code before writing it, but don't spend too much energy trying to get it right the first time. Write code so you can easily change it and get it right on the second try, but this also requires that you do take a stab at things and make a first attempt.

2. Base your work on concrete results, not theoretical results. Taking a blind stab at implementing a feature is a concrete step. It gives you something to analyze, something to improve, and something that works in the meantime.

How often do you see BYOND developers making concrete progress? How often do you see BYOND developers refine their mob icon while talking theoretically about different ways the game might work? How often do you want to strangle people and say "just make the game already!"

Note: my answers to those questions were: "rarely", "often", and "often".
var/hit = rand(attacker.speed + defender.speed) <= attacker.speed

if(hit)
//strike em dead!


At first I was confused at this, but I eventually got it. I never knew you could put those arguments (correct words?) in a var and then check it in an if statement, like your first lesson, much easier to the eyes.

Thanks for this Forum, glad you made this one too. :)
+yea on this piece.
These posts are helping me as I learn DM entirely!
I'm glad someone is trying to help in-training developers. ^.^ Good job!
Truseeker wrote:
At first I was confused at this, but I eventually got it. I never knew you could put those arguments (correct words?) in a var and then check it in an if statement, like your first lesson, much easier to the eyes.

The boolean operators there just turn var/hit into a boolean.

It's more or less equivalent to saying:
if(rand(attacker.speed+defender.speed)<=attacker.speed)
//strike em dead!


While there isn't much significance in the difference, if you're dealing with more
complex formulas and logic behind these systems, this will help with code readability and annotation.
Code readability is also the reason behind forum_account's generous use of white space. I think there's a little too much white space, but that's more or less a personal preference and it doesn't really amount to anything when it comes to code readability and whatnot.

A comment on the third code example:
I have no doubt that a bug like that will show up during bug testing (especially if you're comparing hand computed data to in-game data), but may prove to be difficult to pinpoint.
It would be helpful to keep a pseudocode version or english version of the damage and hit formulas in comments next to the code implementation, so that a bug like this in the code would stand out more.

I feel like an article, in the style of this one, that focuses on the bigger picture when it comes to project and code organization would be much more helpful than the one here.
This is a step in that direction though, so +yea. :)

[EDIT]: I also think that white space should get at least some mention in one of these articles. It's one of those little things that can make a big difference in code readability.
D4RK3 54B3R wrote:
I feel like an article, in the style of this one, that focuses on the bigger picture when it comes to project and code organization would be much more helpful than the one here.
This is a step in that direction though, so +yea. :)

It'd be harder to come up with examples for a "big picture" article, but it does seem like these little articles are working towards a bigger article like that. It's also harder because larger concepts like that start to be very project-specific. If you have any ideas for topics, please let me know.

[EDIT]: I also think that white space should get at least some mention in one of these articles. It's one of those little things that can make a big difference in code readability.

As you said, whitespace is heavily a personal preference thing. Writing out formulas and storing their values in local variables to simplify the if statements is a style thing, but it's also clearly a better way to organize things. I plan to make more posts like this one so I'm sure I'll get around to mentioning whitespace eventually.
Just out of curiosity, would the following code work?
var/cannotFuse = src.ChooseFuse||M.FuseRequest!="None"
Darker Legends wrote:
Just out of curiosity, would the follow code work?
var/cannotFuse = src.ChooseFuse||M.FuseRequest!="None"


No.
Darker Legends wrote:
Just out of curiosity, would the follow code work?
var/cannotFuse = src.ChooseFuse||M.FuseRequest!="None"


var cannotFuse = !(M.FuseRequest != "None" && ChooseFuse) // parentheses unnecessary
I just realized that my comment had a typo. I meant following.
Darker Legends wrote:
Just out of curiosity, would the following code work?
var/cannotFuse = src.ChooseFuse||M.FuseRequest!="None"


It depends on what you mean by "work". It'll assign some value to the cannotFuse var, but whether it's the correct value or not depends on what the vars mean and what you want the logic to be.

The operators that are often used in if statements (>=, &&, ||, ==, etc.) don't have to be used in if statements. They just return 1 or 0 to indicate whether the condition is true or false.

Additional note: I wouldn't call the var "cannotFuse". I'd instead call it "canFuse" and say "if(!canFuse)" if you want to check for the negative condition. Writing things like "if(!cannotFuse)" can get confusing, especially if the "cannotFuse" var is part of a larger condition you're checking.
I disagree strongly with the notion that making "attack" a global proc is the way to go. I don't even know where to begin here. We disagree on almost everything, but I've always thought that you had a solid understanding of programming. I keep rereading this hoping I've missed the part where you say "don't do this".

The biggest reason to have attack() belong to the attacking mob is so that it can be overridden. My standard example is a vampire that gains health based on the amount of damage done. In order to implement such an object in your example, we'd have to define another pre-attack proc on the mob object.

The other major issue I see here is that the defender isn't involved in the process at all. I understand that you're only getting the basics across here, that the "death check" is assumed. Still, an attacker "checking for death" or causing the death of the defender is not the way to go; you should not be modifying the defender's health points in a proc outside the control of the defender object. Why? The same reason as the attacking proc. What if there's an enemy that gets stronger with each point of damage that it takes? What if there's an enemy that has a 1/3rd chance of attacking when attacked? What if there's an enemy that doesn't die when its health reaches zero, but turns into a zombie that cannot be killed, and the players are expected to run away? You need separate attack() and hurt() procs to define special behavior when a combatant attacks or is hurt.
This isn't an article about how to make an attack() proc. It's an article about how to avoid getting stuck in design. The last section clearly states the lessons that are illustrated here.

You need separate attack() and hurt() procs to define special behavior when a combatant attacks or is hurt.

What if you don't want to do any of those things you mentioned? I was kicking around an idea for the next article and this sets it up perfectly.
IainPeregrine wrote:
The biggest reason to have attack() belong to the attacking mob is so that it can be overridden.

Inheritance is very limiting because the type hierarchy limits what is possible. You can only define new behavior and call the parent proc. Suppose there are two mobs with distinct attacks and you want to make a third mob that does a double attack using both of the other attacks.

Using global procs doesn't have this problem:

proc
attack1()
// do something

attack2()
// do something else

attack3()
// do both
attack1()
attack2()

// if you want to emulate inheritance:
child_of_attack2()
// call its "parent"
attack2()

// do something extra

Relying on inheritance means you can only override a proc to extend the parent proc's behavior or to completely replace it with new behavior. Many things just aren't possible when you're only overriding procs. I'd also be hesitant to make wide use of overriding procs because you have to remember what the parent proc, ..(), does. When you're explicitly calling named procs like in the example above, you can give each proc a descriptive name so you know what it does.

There may be a situation in which inheritance is preferred, but this is highly dependent on the complete context you'd be using this in. To keep this article short I ignored the context the proc is being used in. To get the full benefit of the article you have to ignore the context anyway. This can be applied to more than just attack() procs just like the first article can be applied to much more than the example used there.

Edit: You can still make use of inheritance. These global procs don't call themselves, they'll have to come from some mob proc, so you can override that:
mob
proc
attack()

enemy_1
attack(mob/m)
// call the global attack1 proc
attack1(src, m)

enemy_2
attack(mob/m)
// call the global attack2 proc
attack2(src, m)

enemy_3
attack(mob/m)
// call both of the global attack procs
attack1(src, m)
attack2(src, m)
@ D4RK3 wrote:
The boolean operators there just turn var/hit into a boolean.

It's more or less equivalent to saying:

if(rand(attacker.speed+defender.speed)<=attacker.speed)
//strike em dead!



While there isn't much significance in the difference, if you're dealing with more
complex formulas and logic behind these systems, this will help with code readability and annotation.
Code readability is also the reason behind forum_account's generous use of white space. I think there's a little too much white space, but that's more or less a personal preference and it doesn't really amount to anything when it comes to code readability and whatnot.

Yeah, I just thought statements were a lot easier to read and not so jumbled together.
        var/hit = rand(attacker.speed + defender.speed) <= attacker.speed

if(hit))


Forum wrote:
As you said, whitespace is heavily a personal preference thing. Writing out formulas and storing their values in local variables to simplify the if statements is a style thing, but it's also clearly a better way to organize things. I plan to make more posts like this one so I'm sure I'll get around to mentioning whitespace eventually.

Like Forum said, I prefer "whitespace" since my brain has an easier time reading that.
Truseeker wrote:
Yeah, I just thought statements were a lot easier to read and not so jumbled together.

In addition to making things less jumbled, assigning these values to local variables means you have descriptive variable names to explain what the values are. For example, suppose you have two separate checks, one based on the attacker to determine if they hit and one based on the defender to determine if they evade:

var/hit = rand(attacker.speed * 2) > defender.speed
var/evade = rand(defender.speed * 2) > attacker.power

if(hit && !evade)
damage = ...
else
damage = 0

// compared to:
if(rand(attacker.speed * 2) > defender.speed && rand(defender.speed * 2) <= attacker.power)
damage = ...
else
damage = 0

By using local variables called "hit" and "evade", you know what the calculations are for and you can clearly see that damage is dealt when the attacker hits and the defender doesn't evade. In the second part you could put a comment before the if statement saying "check that the attacker hits and the defender doesn't evade", but if you write the code the first way, you don't need the comment.
Forum_account wrote:
The operators that are often used in if statements (>=, &&, ||, ==, etc.) don't have to be used in if statements. They just return 1 or 0 to indicate whether the condition is true or false.

*The return value of || and && is the last argument to be evaluated, says the DM reference.
var a = ("a" || "b")
var b = ("b" || "a")

var c = ("a" && "b" && "c")
var false = ("a" && FALSE)

I've found this to be useful somewhere, so I thought it might be worth mentioning.