ID:44633
 
Part II of a BYONDscape Classic!


Kunark's DM Programming Tutorial (Part II) by Kunark VI. Conquering loops (for(), while(), do while(), and goto)


Need monsters to run around the screen? Need a game play handler? Need a whole lot of something? Well, you can make it all happen with loops!

Loops are a very important part of DM, and very few good games can be created without them.

In general, what they do is repeat a set of commands more than once -- or just once, or not at all, depending on the conditions. They can also be used to iterate through the elements in lists.

The format of most loops goes like this:

loop
what the loop does.

Example:

while(1)
world << "Hello"
sleep(1)

That would infinitely output "Hello" to the world every 1/10th of a second.


for() loops:

I will be explaining for() loops in more detail in an upcoming chapter, but they deserve some attention here as well.

They have two main purposes:

When you know for sure a set of commands should be repeated a precise number of times, you use a for() loop.

Format: for(varname,varname (==,>,<,><=,>=) amount,varname(++,--,-= var,+= var,*= var,etc.))

As you can see above, for() loops have 3 arguments. Each of these arguments has a different purpose:

1st: The variable the loop will use. This can be an already-initialized var or a temporary var initialized within the first loop argument.
2nd: This is the condition. It determines if the loop should be executed or not. Generally, you will want to apply the condition to the variable defined in the first argument. If it fits the condition, the loop will execute.
3rd: If the second condition is true, execute this command. Generally, the execution will be incrementing or decrementing (adding or subtracting 1 to or from) the first argument's var.

Example:

for(var/i = 0,i < 10,i++)  //This basically says, "There is a var named i
//that starts with value 0. Execute this loop while i is less than 10.
//After each execution of the loop, add 1 to i."

Thus, the loop would execute 10 times.

The other main purpose of for() loops is to access each of the elements of a list.

There are two versions of this type of for() loop. I will explain only one in this chapter. Look at chapter IX to see the other type for accessing lists.

This is the more simple/common version; it accesses all of the elements of the list (until the list ends or it is manually stopped), and in the process, declares "var" as that element so you can access it:

for(var in list)

Examples:

for(var/obj/O in oview(1,src))  //oview() will return a list of everything
//within one space of src, and for each one of those things, O will
//be declared as that thing in oview(), as long as it is a /obj.
src.contents.Add(O)

The previous example would add all /objs in one step from src to src's contents list.

var/list/L = list("Hello.","My name is John Doe.","I like to work with DM!")
//Just know that this is a list with the 3 elements you see there.
for(var/T in L)
world << T

That would output this to the world:

Hello.
My name is John Doe.
I like to work with DM!



while() loops:

while() loops are very flexible. A while() loop is basically an if() statement that repeats the block of code beneath it until the conditions are met or the loop is broken.

Format:

while(conditions)

Example:

var/GeneralAIDelay = 5
while(src.HP > 0)
src.AI()
sleep(GeneralAIDelay)

That would call the src.AI() (which would tell the mob what to do) forever as long as src.HP is more than 0.

Something you should note is that in procs like the one above, you aren't sure when it will end, because you aren't sure when the mob's HP will be 0 or under. That uncertainty is something you have in while() loops most of the time, so you probably will always have a sleep statement within it to make sure it doesn't freeze your game. If a while() loop existed with a condition like "src.HP > 0", and the while() loop didn't modify the mob's HP, you'd have an infinite loop: it would never end, and it wouldn't let anything else happen because there'd be no sleep() statement, so your game would freeze immediately. More about infinite loops later in this chapter.

Just like if() statements, you can have multiple conditions using || and &&:

while((src.HP > 0) && (src.AI == 0))


do while() loops:

Think of these as while() loops that execute their commands under them, but they check the conditions at the end instead of the beginning like normal while loops. (Note that the loop's block of code will be executed at least once -- that won't necessarily be the case with an ordinary while().)

Format:

do
//commands here...
while(conditions)

Example:

var/i = 3
do
world << i
i--
while(i) //This would check to see if i had any sort of nonzero
//value.

That would output to the world:

3
2
1


The break statement:

break is similar to a proc's return statement, in the respect that it will immediately escape from the current block of code. However, break only quits a loop -- it doesn't end a proc or return a value.

Format:

loop
//Conditions, commands, etc. here...
break

Examples:

turf/proc/isdense()
var/IsDense = 0

for(var/atom/a in src.contents)
if(a.density == 1)
IsDense = 1
break //This would stop the for() loop in it's tracks, but
//not the entire proc like return would.
if((src.density == 1) || (IsDense == 1))
return 1
else
return 0 //note that this is optional -- a proc that reaches its
//end without a return statement automatically returns null.

The break statement in the above proc saves CPU because it stops looking at the contents of the turf after the first dense object is found, and therefore makes your game run faster. The performance gain may not be too apparent when there's only one proc like this, but your game may use many loops of this kind, and the time savings can add up fast.

proc/Proc1(mob/M)
do
sleep(5)
if(M.HP <= 0)
break
world << "still alive"

while(M.HP > 0)

In the above example, lets say that before the check on their HP here, you had to sleep(5) for some reason. This would allow things to happen since the do while() loop's would be 5/10ths of a second ago, possibly allowing change to take place. You don't want it to say "still alive!" if he is dead for some reason suddenly during this 5/10th second interval! (Note that you could also create this same behavior by creating a "while" loop with the sleep() at the end of its code block.)


Infinite Loops:

Much of the time you can accomplish all you need with the conditional in a while() statement, but some people use infinite loops with breaks governed by a separate if() instead.

You can make two types of infinite loops:

For a while() (or do while()), all you need to do is put a 1 as the conditional. The 1 isn't going to ever change and will always be nonzero.

Example:

while(1)
//Blah blah blah
if(conditional)
break

You can also do a for() infinite loop as shown below:

for(;;)
//Blah blah blah
if(conditional)
break

You will probably not want to use this technique unless you really know what you are doing. It is worth teaching, though, because then you can know what other people are doing when you see them do this. Some programmers will use these infinite loops with breaks as a way of imitating the "do until()" loops that are found in some other programming languages. (Another way to imitate "do until(X)" is to use "do while(!X)".)


goto:

Just don't use goto. There MAY be a time you need it, but I personally can't think of any time it is better to use than a for() loop or a while() loop.

I'll show you what it does, though, and why it is worse.

Format:

NodeName

//blah blah blah

goto NodeName

Example:

proc/Proc1(mob/M)
Start
world << "Still alive!"
if(M.HP > 0)
return
sleep(1)
goto Start

That would repeat "Still alive!" for as long as M's HP was more than 0. You can name the node whatever you want.

You can also make goto go ahead in time like in the below proc:

proc/Proc1(mob/M)
Start

world << "Still alive!"
if(M.HP > 0)
goto End
sleep(1)
goto Start
//blah blah blah
End

world << "[M] IS DEAD!"

That would output "Still alive!" until the mob's HP was 0 or less, and say "[M] IS DEAD!" afterwards, because the goto under the conditional went past the goto Start loop.

Not only is the general syntax worse for goto -- you also have to deal with its loop not being indented. This makes the loop harder to make out from other code.







VII. The benefits of sleep()


This will be a short chapter, but sleep() deserves a chapter of its own because it's important.

As we've already seen, the sleep() statement causes the proc to pause for a certain amount of ticks, and then continue.

Some people don't realize just how important sleep() is to making your game run well. It can mean the difference between a clunky, slow game, and a fast and efficient game.

Format:
sleep(ticks)

Something to understand with sleep() is that each of those ticks it is "sleeping", there is no processing going on with that proc, and no processing means no CPU being used.

Because of this, sometimes it is a good idea to use a conditional to see if a proc has any use for running, and if it doesn't, just sleep. Most of the time it is better to just shut off the proc, but in some instances you'll find it better (because of speed needed to start the proc, etc.) to use sleep:

proc/Proc1()
while(1)
//(Assume there's lots and lots of processing here instead of this comment.)

while(Player1TurnOver == 0)
sleep(5) //This would make it so the only processing
//that would go on would be a conditional and a sleep()
//for as long as Player1TurnOver was 0.
sleep(1)

As seen above, in some infinite loops, there should always be a sleep(1+) statement to make sure your game doesn't crash.

In loops or procs that generate enormous amounts of CPU, such as dungeon generators and other things that create extremely large amounts of objects at a time, sleep statements can be called every so often to keep the game from have a CPU lag spike. It can also prevent the world from outputting an infinite loop warning if you're using a really long loop on purpose.

var/Counter = 0

for(var/i = 10000,i > 0,i--)
//Processing here such as object creation.
Counter++
if(Counter == 100)
Counter = 0
sleep(1)

**Note, though, that it isn't always better to have slight CPU lag spikes over a period of time than one large CPU spike. It takes a shorter time for things to generate when it is a large CPU spike, because even though it looks like the game isn't doing anything, all that time it's frozen it is processing. If the proc is being done in play-time with players running around the world, most of the time it is better to have the world freeze for a few second and not for a while than have a continued slight CPU lag spike for over a minute. Continual small delays are very annoying to players, when the large spikes can just be played off as "Loading...", which is true, but it's less annoying.

Like I mentioned in the proc chapter, remember that if you have a sleep() proc in a proc called by another proc, both procs will be paused until that is over, unless you use a spawn() statement. Look back in chapter V for an example.







VIII. Text manipulation


Text manipulation is exciting to learn because it teaches you how to really manage the chat in your game, and can be used in tons of other places in your game.

Text manipulation is the editing of the internals of text strings.


Combining text strings:

There are a few ways to add text strings together; all can be used pretty interchangeably.

addtext() proc:

The addtext() proc can have any number of arguments, like this:
addtext("arg1","arg2",...)

It will then return all of those procs added together.

Example:

var/T = addtext("Hello.  ","My name is John Doe"," and I like to program.")


That would make T be "Hello. My name is John Doe and I like to program.".

Another way to add text together is to use the + operator between text strings:

var/TheLastText = " and I like to program."
var/T = "Hello. "+"My name is John Doe"+TheLastText

As you can see above, you can use vars just like raw text, of course.

The last way uses the [] operator to insert text directly into a text string:

var/TheTextToAdd = "5"
var/T = "The monster did [TheTextToAdd] damage!"


Taking text away from a string:

This is a little bit different from adding text. You can't just use operators.

To "subtract" text, you actually just copy the rest of the text and leave out the part you want to "subtract".

You will be using copytext() to do it.


copytext():

The format for copytext() is:
copytext(String,startposition,endposition).


Example of copytext():

var/T = copytext(Txt,1,10)

The above would copy the text from the Txt var from position 1 to position 9.

If no 3rd argument (end position) is specified, it just copies everything after the start position, like this:

var/T = copytext(Txt,5)

**Note that the third argument is a little different from the second. The second is "the position to start the copy", and the third is "the position after the final character of the text to copy". This means 1,10 would copy characters 1-9. If you have the position of the ending character and want to copy that character as well, just put +1 onto the end position:

var/T = copytext(Txt,1,10+1)

That would copy text from position 1 through 10.


Now, we are going to use copytext() to subtract text from a text string. Put the second argument in copytext() as the starting of the text you are keeping, and put the third argument as the position after the last character you want to keep.

Example:

var/Txt = "We don't need no education!"
var/Txt2 = copytext(Txt,1,13+1) + copytext(Txt,17)
world << Txt2

Above would output "We don't need education!" to the world.

Notice how in order to omit the part we didn't need, " no", we added the two parts we did need together.


findtext()/findText():

What about when we don't know the position of the text we are looking for? This is where findtext() comes in.

Format:
findtext(StringToSearchThrough,TextToSearchFor,StartingSearchPosition,EndingSearchPosition)


findText() is just a case sensitive version of findtext(), so you probably won't use it that often.

Most of the time you won't need a starting or ending position to search through, because you'll normally be looking through the entire string.

Often, you will use this in if() conditionals to determine whether a certain word appears anywhere in a string. This works because findtext() returns a 0 if it didn't find anything, or returns the starting position of the text it found.

Example:

var/Txt = "Hello, my name is John Doe."
if(findtext(Txt,"name"))
//blah blah blah

Above, the if() statement will evaluate to true because Txt contains the word "name". findtext() would have returned the number 11.

Once you know whether the word you're looking for is within the string you searched, then you can do things to that text using that nifty position-returning aspect of findtext().

Example:

proc/Proc1()
var/Txt = "Hello, my name is John Doe."
var/Txt2 = Txt
if(findtext(Txt,"name"))
Txt2 = "[copytext(Txt,1,findtext(Txt,"name")+1)]dearly beloved alias[copytext(Txt,findtext(Txt,"name")+4)]"
world << Txt2

The above proc would output "Hello, my dearly beloved alias is John Doe." because it replaced "name" with "dearly beloved alias". Note that in the second copytext(), it says "+4". This is because copytext() returns the starting position, so we'd need to copy the text AFTER the length of the text we no longer want. "name"'s length is 4.


length():

So how about when we don't know the length of the text? This is where length() comes in.

Format:
length(Text)

It returns the length of the text, which is also the position of the last character in the text.

If we didn't know the length of "name" (or any other text) like we did before, we could use this:

proc/Proc1()
var/Txt = "Hello, my name is John Doe."
var/Txt2 = Txt
var/OffendingText = "name"
if(findtext(Txt,OffendingText))
Txt2 = "[copytext(Txt,1,findtext(Txt,OffendingText)+1)]dearly
beloved alias[copytext(Txt,findtext(Txt,OffendingText)+length(OffendingText)]"
world << Txt2

It is especially useful when you don't know what exactly OffendingText will be.


Other useful text manipulation procs to note:

num2text(num): This will return text string of a number that was converted from a raw number. Often in games you will see output of numbers display scientific notation instead of the full number. You can use num2text()'s second argument "SigFig" to increase the number of digits until scientific notation occurs. Normally, it occurs at the 1 million mark.

text2num(text): This returns a number converted from a text string consisting of numbers. This is useful for when you need to do math to the values you copy from a text string. **Note that if it finds something other than a number, a decimal, or a negative sign, it won't convert the rest of the string from that point. In other words, text2num("2435t67") would return 2345. If the string doesn't start with a number, it will return null.

list2params(list): This returns a text string that contains the elements and associated values of a list. It is very useful for quickly outputting lists onto a single line for debugging.

uppertext(text): This returns the entire text string rendered completely in upper case.

lowertext(text): Similar to uppertext, but it converts the string to lower case.


Now, let's see just some of the things you can do with text manipulation:


proc/ReplaceText(Txt="",OffendingText="",Char2ReplaceWith="*")
var/Txt2 = Txt
var/OffendingPosition = findtext(Txt2,OffendingText)
while(OffendingPosition)
Txt2 = "[copytext(Txt,1,findtext(Txt,OffendingText)+1)][ReturnCuss(length(OffendingText),Char2ReplaceWith)][copytext(Txt,findtext(Txt,OffendingText)+length(OffendingText)]"
OffendingPosition = findtext(Txt2,OffendingText)
return Txt2

proc/ReturnCuss(Length,Char)
var/ReturnedText = ""
for(Length,Length > 0,Length--)
ReturnedText = "[ReturnedText][Char]"

return ReturnedText


The above proc, ReplaceText(), will search through a text string for words it needs to replace (like cussing), and then it will replace it with a text string of symbols of your choice.

ReturnCuss() is what returns "****" and so on to ReplaceText().

var/T = ReplaceText("Hello, my name is my name is John Doe.","name","*")
world << T

That would output "Hello, my **** is my **** is John Doe." to the world.


proc/FlipText(Txt)
var/Txt2 = ""
for(var/i = length(Txt),i > 0,i--)
var/CopiedText = copytext(Txt,i,i+1) //Notice how I am using the
//variable defined in the for() statement. That var starts out
//as the position of the last character in the string, and each
//time it loops it goes farther back into the screen. I will
//talk more about the usefulness of that in the next chapter.

Txt2 = Txt2+CopiedText
return Txt2


The above proc will take a text string you put into it, and return the same string, but inverted (backwards, flipped).






IX. Using lists and more about for() loops


Lists are everywhere in DM, and even when newbies think they aren't using them, they probably are. Using lists well can be difficult, but hopefully this chapter will give you a better understanding of how they work and how to use them. Keep in mind that we will be using several of the terms defined in Chapter II.


The first step of using lists is to initialize a list variable.

We do that like this:

var/list/L = list()
var/L[]

Both of the above lines do the exact same thing: create a variable whose value is an empty list.

Make sure that when you define a list with the first method, you define it as a /var/list instead of a /var. That way you can access the special properties and procs that are used to manipulate lists.

We can also initialize a list by calling a proc that returns a list with specific contents:

var/list/L = oview(1,src) //oview() is built in to the DM language
var/list/L = ReturnMobList() //you could define your own proc that returns a list of mobs

**Note: Be careful when defining lists that equal other lists. The new list might end up just pointing to the other list:

var/list/L = list("Hi","hello")
var/list/L2 = L

The above examples are the wrong way to do it. This would make L2 simply "reference" L, and make it so if you do anything to L2 or L, either action will affect the same single list. So if I removed "Hi" from L2, and then looked for "Hi" in L, I wouldn't find it there.

Instead of having the new list reference the other, this is how you'd make a list have the same contents as the other list: Add() them.

var/list/L = list("Hi","hello")
var/list/L2 = list()
L2.Add(L)

Add() will add all the contents from L to L2, so it is a duplicate instead of a reference.


The Add() proc:


Add() allows you to add multiple elements to your list. You can also include entire lists (as shown in an above example) as arguments to add all of the contents of those lists.

Example:

var/list/PreList = list("I like to play sports, ","and program in DM.")
var/list/L = list()
L.Add("Hello! ","My name is John Doe. ",PreList)

That would make L be a list that contains: "Hello! ", "My name is John Doe. ","I like to play sports, ","and program in DM."

There is also a Remove() proc that works similarly, but removes the contents listed.

Now that we've given the list some contents, how do we access them when we need them? Well, there are multiple methods.


Using the [] operator to add elements:

You can also add to a list with [], but first, we'll talk about initializing a list with null elements using [].

To do this, when you initialize the list, use [] to initialize it and put a number between the [] operator.

var/L[5]

That would make a list with 5 null elements.


Using the [] operator to access elements:

You can initialize lists with [], but you can also access the already-created lists by using [] as well. When using [] to access elements, you have to know the element number or index of the element you want.

The element number is the position of the element in the list:

var/list/L = list("Hello.","How are you","John Doe.")

var/T = L[2]
world << L[1]
world << "[T]? My name is [L[3]]" //Notice how [] in text strings has
//a different meaning -- it allows you to include the values of vars.

The above example would output this to the world:
Hello.
How are you? My name is John Doe.

It is possible to mix different types of values in a single list (like numbers, objects, and strings), but remember that if you do, you will usually need to check the value with istype() or isnum() or so forth to know how to use it.

First method of using for() loops to access loops:

As shown in the chapter about loops, you can use a for() loop to go through the entire list by it declaring each element as a variable:

for(var in list)

This goes through the list, from element 1 to the last element, and declares var as that element each time it loops.

Examples:

var/list/L = list("hello","I am john doe","I like to program in DM")
for(var/T in L)
world << T

This would loop 3 times, once for every element, and output each element to the world. It would output this:

hello
I am john doe
I like to program in DM

This works the same for objects:

proc/Proc1(mob/M1,mob/Player/M2,mob/Monster/M3)
var/list/L = list(M1,M2,M3)
for(var/mob/M in L)
world << M.name

That would output those three mobs' names.


Second method of using for() loops:

In this method, we will use the other type of for() loop, the kind that doesn't use a list in its arguments.

If you know your for() loops well, you know that programmers most commonly set them up to increase or decrease a variable with each pass through the loop. We can use this to access lists by element numbers (as we did earlier in the chapter):

var/list/L = list("I like to program in DM.","My name is John Doe.","Hello.")
for(var/i = 1,i < L.len,i++) //len (or "length") will be explained later in this chapter.
world << L[i]

This would output this:

I like to program in DM.
My name is John Doe.
Hello.

This is because i would be increasing after each loop, so [] can be used to access it with the element number.

But wait... Its output is backwards. We need to flip it around. That is the beauty of for() loops: you can start at any position in the list.

var/list/L = list("I like to program in DM.","My name is John Doe.","Hello.")
<br>
for(var/i = L.len,i > 0,i--) //len (or "length") will be explained later in this chapter.
world << L[i]

The above proc would output the list starting from the last element (because the initial value of i is L.len) and would take 1 away from i each loop, so it would go from the back of the list to the front.


len:

len stands for length, and it is a property of lists. It is equal to the total number of elements in the list. This can be used, as it was above, to get the last element number of the list.

Example:

var/list/L = list(56,78,990,45,609)

var/N = L[L.len]
world << N

That would output 609, because L[L.len] is the same as L[5], which is the last element in the list.

So in other words, lists start at 1 and end at len.

Finally, I'd like to show you a shortcut used for debugging purposes that will allow you to quickly see the contents of a list:

world << list2params(list)

That would output all of the contents of a list with a single line of code.





X. List associations


Lists in DM have a special feature that allow you to associate values with other values within the list. (If you haven't read chapter IX (lists and for loops), please do that before continuing.)


List associations allow you not only to associate raw text or numeric values with each other, or text values with objects, but also to make more advanced associations. We will get into that in the next chapter, however; for now, we will focus on the basics.

Associative lists can be very useful in many situations, especially things like saving many values at once, and editing large amounts of values needed for a parent proc with a single child proc.

Example of an associative list:

var/list/L = list("Name" = "John Doe","Score" = 1000)

As seen above, associative lists are created by making something "equal" another thing in each argument in the list.

What the above stands for is that the person's name is "John Doe" and the person's score is 1000.

So that is how we define them when initializing the list; how do we put more into the list once the list is already created?

We will use the [] operator.

var/mob/M = ReturnCompanion(src)
var/list/L = list("Name" = "John Doe","Score" = 1000)
L["Companion"] = M

That would make a new index in L that is associated with M (src's companion).


But wait!

We can use that method to edit existing contents as well!

var/mob/M = ReturnCompanion(src)
var/list/L = list("Name" = "John Doe","Score" = 1000)
L["Score"] = 2000

Being able to edit and create elements using the same method is nice because it wouldn't come back with a run-time error if you tried to define the association and the index wasn't there. In other words, it creates the index if it isn't there, but if it is there, it changes the associated value.



Using [] to access associations:

Now that we know how to create associative lists, we will need to know how to access the associations.

We already know that we can use for(var in list) to access all of the contents of a non-associative list, and we know we can use [] using element number to get contents in a list, so how do we get associations?

Well, you know how when you use element numbers with [] to access them? Well, think of those element numbers as the indices of the normal contents.

var/list/L = list("Name" = "John Doe","Score" = 1000)

In that list, "Name" and "Score" are indices. "John Doe" and 1000 are the associations.

Now, all we have to do to access the values is the same thing we do to retrieve normal elements with their element number index. We use the index (which we know) to get the value of the association:

var/list/L = list("Name" = "John Doe","Score" = 1000)
var/Score = L["Score"]

world << L["Name"]

That would output "John Doe" to the world, and make the Score variable defined after the list equal 1000.

To see the difference between element number and regular contents, look at this:

var/list/L = list("Name" = "John Doe","Score" = 1000)
var/Score = L["Score"]
world << L["Name"]

world << L[1] //This and the output below both use element numbers instead of
//the content indices.
world << L[2]

The above example would output this to the world:

John Doe
Name
Score


Find():

What about when you know (or suspect) something is in the list, and want to know where it appears? This is where /list/Find() comes in handy. You can use Find() to return an element number.

This is generally used to get the element after or before the element you know.

Example:

var/list/L = list("Name","Height","Language","Country")
var/ElementBeforesNumber = L.Find("Language")-1
world << L[ElementBeforesNumber]

That would output the element before "Language", which would be "Height". This is because ElementBeforesNumber is equal to one less than the element number of Language, and then we use that element number to access the list in the world output.
Very helpful library, taught me about a few loops I've had, and I fixed em, thanks.
Loads of Unicode conversion errors wherever an apostrophe symbol was used -- I presume this was pasted from MS Word or something?
This was really helpful! I learned quite a few new things about for() and associated lists. :)
Jtgibson wrote:
Loads of Unicode conversion errors wherever an apostrophe symbol was used -- I presume this was pasted from MS Word or something?

Actually I took it from the HTML source code of the original BYONDscape article, manually edited the lt's, gt's, amps, etc., added DM tags where needed... but something always seems to get by me!