Ghosthande's Hungry Critter Tutorial
This isn't a tutorial so much as a walkthrough of what an eating subroutine looks like. Specifically, it's a generic subroutine I adapted from the Snotrock when I was making my own critters, with explanations as to how everything works.
This is a basic timer script that calls one of two subroutines. Ideally a critter will have a condition near the beginning of the timer script asking what the hunger level is. Conditions should be nested for best results, and listed in order of immediacy.
For this critter, lifespan is defined by the variable ov01, and hunger is ov02. Both variables start at 100 when the critter is born and decrease little by little as it moves about. If hunger reaches a certain level—say, 25 or 30—the timer script calls the "food" subroutine, which will make the critter search for food. The timer script might look something like this:
scrp 2 12 60699 9
* ov01 = critter's lifespan
doif ov01 lt 1
gsub die
else
* ov02 = critter's hunger level
doif ov02 le 30
gsub food
else
gsub wander
endi
endi
endm
You can see what I mean about the conditions being nested: instead of having multiple ELIF's branching off of a single condition, there are multiple conditions nested inside one another. It may not seem necessary, but it's actually much better to do this way. You can read an explanation as to why here.
By the way, it's a good rule of thumb to always make sure a major subroutine like this ends in a blanketing "else" statement, instead of trying to list every possibile state of the agent. That way there is always a default bit of code for the critter to fall back on. If your critter winds up in a condition that doesn't have an action specified for its current situation, and you don't have a backup blanketing statement, the critter will simply not do anything, effectively going into a coma until you either delete it, change its status manually through CAOS, or it gets eaten or something.
Now, let's begin the food subroutine. A subroutine starts with SUBR [insert subroutine name here], and ends with RETN, which basically means "return to wherever we left off before the subroutine was called". A subroutine is not separate from a script; it must go inside the script that uses it.
The RNGE determines how far away the critter can see other agents. A range of 1000 is pretty generous. As far as I know, it is the highest number allowed by the game. A lower number means the critter won't see as far. This will decrease the likelihood that the critter will be able to find food, but it is more desirable for critters that move slowly—there's no point in going after a food item that will have rotted away by the time the critter gets there. Norngirl's caterpillars only have a RNGE of about 200 for this reason.
The variable OV99 wll be the critter's reference to the food agent it finds. We use SETA because its value is going to be a reference to another Agent. We're setting its value to NULL for now because the critter hasn't found anything yet. We just want to make sure that the critter already knows what kind of variable OV99 is—since by default, any unset variables have a numeric value of 0, rather than being agent references. Asking your critter "is zero a valid agent?" is a very good way to cause an error.
Beginning the food subroutine
subr food
rnge 1000
seta ov99 null
retn
As you can see, I have written both the beginning (SUBR) and the end (RETN) of the subroutine. The subroutine isn't finished yet; we're just going to continue writing the code inside of that framework. This may not be apparent in most tutorials, but you should always write the ending of a block of code when you write the beginning, whether it is a script, a subroutine, or a condition. This may seem counter-intuitive and a bit awkward since it isn't how we write English, but CAOS isn't structured like human languages so our normal writing conventions will only trip us up.
It might not sound necessary, but consider that most of the coding errors I see new (and intermediate) developers struggle with end up being caused by missing end tags. The reason more experienced coders don't run into these errors as often isn't because they've mystically trained themselves to remember exactly how many ENDI's they will need to write at the end of a huge block—it's because they've learned to write out the framework for their code at the beginning and then work inside it. This is similar to how you would build a house first, and then put the furniture in afterward—you wouldn't build just one wall of the house and then start furnishing it.
Working this way should not be too confusing, since the CAOS Tool automatically indents your code as you type. Code that is inside of a block such as an event script, a condition, a loop or a subroutine will always be indented one step inward from whatever is holding it. If the indents for your code have gotten messed up and don't look right anymore, just click File -> Refresh Syntax, or hit CTRL+U, and the CAOS Tool will fix them for you.
Since I'm showing only a few lines of a script at a time, I'll be including pieces of code from each prior example as "hints" to help put new lines in context. So after the SETA line, but before our ending RETN, we continue as follows:
seta ov99 null
inst
esee 2 6 0
next
slow
retn
The ESEE line asks, can you see any of this object type (in this case, leaves)? ESEE automatically works within the range set by RNGE. The number 0 is a wildcard, meaning that this critter will search for any kind of leaves, regardless of their species. The NEXT line ends the ESEE block. As it might suggest, once the critter has found a leaf, it will continue looking to see if it can find any others. That's okay--in this case it doesn't really matter.
The INT and SLOW lines at the beginning and end of this section control how quickly the code between them will run. Normally the game processes one line of code in a script every tick (a tick is about 1/20th of a second). We don't want anything to interrupt our ESEE bit, because if something other than the critter was targeted when the interruption occurred, that can mess up our critter in all sorts of ways.
The INT command is short for "instantaneous", and basically means "run all of the following lines in one tick". SLOW tells the game to read the rest of the code at its normal speed. We do this because we don't want the whole script to run instantaneously, just this one part of it—there are other parts of a critter's code that need to occur over time, such as walking, and using INST when you don't need to needlessly eats up memory.
esee 2 6 0
doif targ ne null
endi
next
Now we take a look at what the critter does when it looks for a leaf. ESEE is supposed to enumerate through all agents of a given type, but it has its quirks. One of them is that it will try to run the script inside the ESEE block even if it didn't actually find an agent. We don't want it doing that, so we write a condition to ensure that the TARGeted object (the leaf it supposedly found) is Not Equal to NULL.
It's importnat to always check this about another agent before you try to do anything to it, otherwise you can run into errors. And if the critter itself falls into the classifier you're searching for (for instance, if this was a cannibal critter that ate other critters), you would write doif targ ne null and targ ne ownr to ensure that the TARGeted agent is not the same as the owner (OWNR) that is running the script. Otherwise the critter might literally chase and/or eat itself!
doif targ ne null
setv va02 posx
seta mv99 targ
targ ownr
setv va02 va02
endi
I'm sure this bit looks pretty strange at first glance. We set some variables, target OWNR (the critter), then set some more variables. What we're doing is a bit complex, so let's go through it bit by bit.
At this point, our subroutine should look something like this. I have added a few comments for clarity's sake.
subr food
* Locate a leaf
rnge 1000
seta ov99 null
inst
esee 2 6 0
doif targ ne null
setv va02 posx
seta mv99 targ
targ ownr
setv va02 va02
endi
next
slow
* Eat the leaf
* ???
retn
The block we've just written determines how far the critter can see, prepares our "agent reference" variable OV99, instantly seeks out all visible leaves, and if they really exist, saves their position and reference to VA02 and OV99. Like I mentioned previously, ESEE cycles through all visible leaves. But it saves the position and reference to the same two variables every time, so the critter "forgets" the last leaf it saw each time it sees a new one. There are other methods a critter can use to locate food, but this is generally the preferred one.
Reaching the leaf
Now we just need to make the critter travel over to where the leaf is, and eat it. Let's cover the travel bit first.
* Eat the leaf
loop
untl touc ownr ov99 eq 1 or ov99 eq null
retn
LOOP is a kind of block that just makes whatever is inside it loop continuously. UNTL means "loop until..." and tells the critter when to stop repeating that action. We want our critter to check for two possibilities: either it (OWNR) and the leaf (OV99) are touching, in which the TOUC variable will be equal to 1... or if the leaf has ceased to exist, which might happen if it rots away or is eaten by another critter.
The reason we use a loop is because it means we can make the critter walk a little bit at a time. Basically, the critter takes a couple steps, sees if it's reached the leaf's position yet, and if not keeps going. This is what the code looks like for my critter as it hops along toward that yummy leaf:
loop
doif posx gt va02
gsub left
elif posx lt va02
gsub right
endi
untl touc ownr ov99 eq 1 or ov99 eq null
The condition checks to see if the critter's position (POSX) is greater or less than the leaf's position (VA02). Since the game uses cartesian coordinates, "greater than the leaf's" means the critter is further to the right, and "less than the leaf's" would mean the critter is further to the left. This is how the critter knows if it is supposed to go left or right.
For this critter, I'm assuming there are other subroutines in the timer script that already handle walking left and right, which are used for wandering when the critter is not looking for food. The "left" subroutine is the one that handles walking left, and the "right" subroutine is the one that handles walking right. So instead of making a copy of all the code to make the critter go left or right, I just call the regular subroutine for walking in that direction.
A slightly more complicated version
Have you noticed anything odd about VA02? The critter moves toward the leaf based on the leaf's position, but we only saved that variable when the critter first saw the leaf. So the critter is basically just assuming that the leaf will still be in the same place... but in reality, the leaf might have been carried off by a Creature or the hand, traveled in an elevator, or been carried off by some other agent, like an ant. And we would definitely want to use a different approach for prey that can move on its own, eg. if this critter were hunting another critter, because then the prey critter would definitely be moving around.
To compensate for all of that, we might do something more like this:
setv ov98 va02
loop
inst
doif ov99 ne null
targ ov99
setv mv98 posx
targ ownr
endi
slow
doif posx gt ov98
gsub left
elif posx lt ov98
gsub right
endi
untl touc ownr ov99 eq 1 or ov99 eq null or seee ownr ov99 eq 0
First I save VA02 as OV98, a new permanent variable, so it's easier to access when we're targeting the leaf. (Technically I would just save it as MV98 in the ESEE, instead of VA02... but you get the idea.)
Then, just before I make the critter move, I check if the leaf still exists (just to be on the safe side), target it, and reset the critter's OV98 variable to the leaf's position again. This means that every time the critter goes to move, it also checks to see where the leaf is. So this way if the leaf has moved, the critter will know. I also use INST and SLOW again to be on the safe side.
Finally, I've added a new condition in which the loop might end. SEEE is like TOUC except it checks to see if two agents can see each other, not if they are touching. I want to make sure that the critter can still see the leaf... in case the hand carried it off or it fell down an elevator shaft or something. SEEE will be equal to 0 if the critter can no longer see the leaf.
Eating the leaf
Now that we're finished with our "chase sequence", it's time to make the critter eat the leaf. We start this by double-checking that the leaf still exists, that the critter can still see it, and that nothing is CARRying the leaf (CARR is equal to NULL). Because I don't know about you, but I find it a little awkward when I pick up a piece of food or a leaf and then a critter somewhere on the ground still manages to eat it.
untl touc ownr ov99 eq 1 or ov99 eq null or seee ownr ov99 eq 0
inst
doif ov99 ne null and seee ownr ov99 eq 0 and carr ov99 eq null
sndc "chwp"
kill ov99
addv ov02 30
else
gsub wander
endi
slow
The actual eat bit doesn't need to do a whole lot. My critter uses the CHWP sound effect when it eats leaves. It KILLs the leaf, similar to how a food agent kills itself when it is eaten. And we add 30 to OV02, which if you remember from the beginning, is the critter's hunger level.
That's pretty much the end of the subroutine. Notice that I did tack a "gsub wander" at the end of my condition, as an alternative if the leaf isn't there: this way the critter will wander around randomly if it hasn't found food, instead of just freezing up or something. If the critter wanders around a little, it will hopefully find a place with more food.
Finally, here is the completed sample script:
scrp 2 12 60699 9
* ov01 = critter's lifespan
doif ov01 lt 1
gsub die
else
* ov02 = critter's hunger level
doif ov02 le 30
gsub food
else
gsub wander
endi
endi
subr food
* Locate a leaf
rnge 1000
seta ov99 null
inst
esee 2 6 0
doif targ ne null
setv mv98 posx
seta mv99 targ
endi
next
slow
* Eat the leaf
setv ov98 va02
loop
inst
doif ov99 ne null
targ ov99
setv mv98 posx
targ ownr
endi
slow
doif posx gt ov98
gsub left
elif posx lt ov98
gsub right
endi
untl touc ownr ov99 eq 1 or ov99 eq null or seee ownr ov99 eq 0
inst
doif ov99 ne null and seee ownr ov99 eq 0 and carr ov99 eq null
sndc "chwp"
kill ov99
addv ov02 30
else
gsub normal
endi
slow
retn
subr wander
* Handles movement: checks for nearby walls and calls
* subroutines to move the critter left or right
retn
subr left
* Critter moves left: velocity and animation bits
retn
subr right
* Critter moves right: velocity and animation bits
retn
endm