This tutorial is a modification of yduJ's MOO Wind-Up Duck Tutorial, which is available all over the internet.
This tutorial is a trimmed version of the original for use with the ColdC language running ColdCore. Some of the concepts are general ColdC concepts, and many are those implemented by the ColdCore itself.
Introduction
This is a programming example of reasonable complexity. We are going to demonstrate object variable use, forking, and generic object use.
Our example is a wind-up toy. The basic things that a wind-up toy does is get wound, and then slowly wind its way down, hopping or rolling or twirling along.
First, let's create a couple of objects to work from.
@new $thing named Generic Wind-Up Toy You create "Generic Wind-Up Toy ($thing_3)" @rename $thing_3 to $windup_toy Renamed $thing_3 (Generic Wind-Up Toy) to $windup_toy. @new $windup_toy named "Heisenberg's Wind-Up Duck",hduck You create "Heisenberg's Wind-Up Duck ($windup_toy_1)" @rename $windup_toy_1 to $heisenbergs_duck Renamed $windup_toy_1 (Wind-Up Duck) to $heisenbergs_duck.
We'll refer to these as $windup_toy and $heisenbergs_duck, the object names assigned by @rename.
First, we need some way to tell if the toy has been wound up. We'll create a variable called "wound" on the generic toy. We also need to know if it is currently walking around, so we also define a variable called "going" that will be set to 1 while the duck is walking, and 0 when it is not.
@add-variable $windup_toy,wound = 0 Object variable $windup_toy,wound added with value 0. @add-variable $windup_toy,going = 0 Object variable $windup_toy,going added with value 0.
Before we can add a command to the duck, we need to program the method behind the command. All ColdC commands are actually just syntax descriptions that allow a method to be triggered. We define this method as public, which means that a method on any other object may call this method. There are two other access levels: protected, which means that only methods on the current object and its descendants may call this method, and private, which means that only the methods defined on the object on which this method is defined may call it.
@program $windup_toy.wind_cmd +access=public
arg cmdstr, cmd, @args;
// Increment the wound value x 2
wound+=2;
// Tell the player e has wound the toy
sender().tell("You wind up the " + .name() + ".");
// Tell everyone else in the room the toy was wound
.location().announce(strfmt("%s winds up the %s.", sender().name(), .name()));
.
On the first line we defined the arguments that the wind method will be expecting. These arguments are passed in by the command parsing system which we will define a command for in the next step. The first argument (cmdstr) will contain the entire command as entered by the player. The second argument (cmd) will contain just the command name that was used to activate the method. The third argument (args) will be a list of any other arguments sent to the method. The args argument is preceeded by an '@' symbol to indicate that we want to use scatter-assignment, which means that the args variable will receive any further arguments passed as a list. This helps prevent errors if there are too many arguments sent for some reason.
Here we also see lines that begin with '//' -- these are comments. The code compiler will ignore these lines, but they are extremely useful for documenting your code and making it easier to read. Documentation is important, and performing this task in the code will help both you and anyone else who uses your code!
The wound variable refers to the wound variable we defined on $windup_toy (the Generic Wind-Up Toy). In a ColdC method, local variables (variables used only in the executing method) are defined with the 'var' keyword. If a method references a variable that has not been declared locally with the var keyword, then the method assumes the variable is a member variable of the current object.
The idea behind adding 2 to wound is that you can wind it a bunch of times to make it go longer.
Now let's add the command so we can activate the wind method:
@add-command "wind <this>" to $windup_toy.wind_cmd
Let's try it on our duck:
wind hduck You wind up the Heisenberg's Wind-Up Duck.
Everyone else sees:
Heisenberg winds up the Heisenberg's Wind-Up Duck.
Next, we want to make the duck actually moves around. Remember that we're doing all the programming on the generic toy, and using the specific instance of the duck to test things out. To be properly generic, we need to define different messages that can hold message strings for the motions of the toy. So let's define two messages. One for the toy to start moving, and another for it to print as it continues to wind down.
@def-msg $windup_toy:startup Message $windup_toy:startup defined. @def-msg $windup_toy:continue Message $windup_toy:continue defined.
We used the Messages system to define these messages to give us some powerful messaging options that ColdCore offers. You'll see what I mean in the next section. Let's set values of these messages on the duck. Note that we are setting the message values on the instance (the duck), and not on the parent (the generic).
While we're at it, we should give our duck a description!
@msg hduck:startup=[$this] waddles about and starts rolling forward. startup = [$this] waddles about and starts rolling forward. @msg hduck:continue=[$this] swivels its neck and emits a >> Quack << continue = [$this] swivels its neck and emits a >> Quack << @describe duck as A yellow plastic duck with wheels at the bottom and a knob for winding. Description for Heisenberg's Wind-Up Duck ($heisenbergs_duck) set.
We should be ready to roll now... We're going to have the wind up toy start up when the player drops it. This introduces another concept, that of specializing an existing method. The generic located object ($located) defines the method .did_move that is called after an object has successfully been moved from one location to another. Normally, if we type "drop hduck" the .did_move method on $located gets invoked for the duck (even though it's a few levels removed from $located). If we define a .did_move method on $windup_toy, then that is the first definition of .did_move encountered as the ancestor heirarchy is searched, so it is the one that gets executed (as opposed to the definition on $located). Now we don't want to have to completely recreate everything $located.did_move() does, we just want our special action to happen after the normal drop actions finish. In order to accomplish this, we use the pass() function.
This is the slipperiest concent in ColdC programming, yet the basis for the entire object oriented nature of the system. An easy way to think of pass is like another method call: it just does the "basic thing" that this method normally does. We can add our specialization code both before and after this basic thing. The arguments you put in the pass() call are just like calling the ancester's definition of the method with those arguments. More sophisticated users of pass may wish to change these arguments; for now, just take it as gospel and pass the same arguments up the ancestral heirarchy. Sometimes you won't want to use pass at all, if you don't want the basic thing to happen, but want to completely override the default behavior. This is OK, you just have to think about what you want when deciding whether and where to put the call to pass().
We want the dropping to happen before any of our startup, so we call pass() right away. We better check if the toy has been wound up, and not do anything special if it hasn't. Also, we want to make sure that the location where the toy used to be was a person (or body), and that the new location is a place (or room). After we have established that these conditions are true, we can tell the toy to start moving around... otherwise, we'll make sure that it is not moving after any other move.
@program $windup_toy.did_move +access=protected
arg mover, old_place;
// Let ancestor handle normal tasks
pass(mover, old_place);
// Trigger walk event if dropping from person to room
if (.location().is($place) && old_place.is($body) && wound)
.go();
else
going = 0;
.
Next we'll define the .go() method which will do the actual work of moving the toy around. When we define .go, we set the forked flag. This means that when this method is called, it splits into its own task, allowing the calling method/task to continue on its way as if this method has already finished. We do this so that you and other players in the room will see the 'dropped' message immediately, and not after the toy has finished walking around the room. We separate the two tasks, because they are two different ideas that will execute apart from eachother -- but the dropping just triggers the moving.
@program $windup_toy.go +access=protected +flags=forked
var i, vars;
if (!going) {
going = 1;
vars = #[["$this", this().name()]];
.location().announce(.eval_message("startup", $windup_toy, vars));
for i in [1 .. wound] {
$scheduler.sleep(15);
if (going) {
.location().announce(.eval_message("continue", $windup_toy, vars));
wound--;
} else {
break;
}
}
going = 0;
}
.
You see that in this method we use thw wound and going object variables. This way if the .go() method is called while it is already moving around, the method will ignore the second call because the going object variable will be logically true (1) while the toy is moving around, and logically false (0) when it is idle.
We also use the core object $scheduler here to tell the system to wait for the indicated number of seconds (15) before continuing on to the rest of the code. This is useful if you need to write code that is somehow time-related. The $scheduler object offers many very useful methods for handling timed execution of tasks.
Now, let's drop our duck and see how it works!
drop duck You drop Heisenberg's Wind-Up Duck. Heisenberg's Wind-Up Duck waddles about and starts rolling forward. Heisenberg's Wind-Up Duck swivels its neck and emits a >> Quack << Heisenberg's Wind-Up Duck swivels its neck and emits a >> Quack <<
It actually waited 15 seconds in between each of those messages. I tried it again, but this time I typed @tasks right away to see that the tasks had been scheduled:
@tasks -- Suspended Tasks -- TASK TICKS METHOD RESUMABLE BY 210609 19985 $scheduler.sleep() $scheduler $user_heisenberg
How could we improve on our wind-up toy? Or, rather, what are some of its problems?
What happens when someone picks up the toy while it's going? What if you try to drop it somewhere you're not allowed to? Perhaps we should only allow someone to wind it up if they are holding it. What if someone winds it five thousand times? Perhaps we should allow programmers to make more complicated messages on their child objects.
We'll address each of these issues in chapter 2.
Let's start with requiring someone to be holding the duck in order to wind it. We take the code from before, and stick an if/else around it.
@program $windup_toy.wind_cmd() +access=pub
arg cmdstr, cmd, @args;
if (.location() == sender()) {
wound += 2;
sender().tell("You wind up the " + .name() + ".");
.location().announce(strfmt("%s winds up the %s.", sender().name(), .name()));
} else {
sender().tell("You have to be holding the " + .name() + ".");
}
.
.location() is where the toy is now. If it is in the player's inventory then .location() will be equal to the value of sender() (which is the player who initiated the command that called this method).
Let's complicate the code once more, and then we'll be done with the wind_cmd method. We should have a maximum number of turns the toy will allow, otherwise a malicious player could spend a few hours winding the toy, and then unleash it on an unsuspecting public. If we wanted to be really fancy, we could make the toy break if they wound it too far, and require them to repair it with a screwdriver (which we would create for the purpose), but let's leave that as an exercise for the reader.
We might want different toys to have different maximum wind amounts, so we should have a way to indicate how many winds any given toy can take. One might assume we should use an object variable for this, but here's the problem: if we define a variable on $windup_toy, our windup duck will have that variable, but we won't have any easy way of setting the maximum winds of the duck because of encapsulation and the way that variable inheritance works.
Instead, we will use ColdCore settings. A setting is like an object variable, but both the setting itself and its value is inherited from the parent object to the child object. This way, if you set maximum-winds on $windup_toy to 20, then the windup duck will automatically have a default maximum-winds of 20 -- but you can also change the setting on the windup duck itself using the @set command.
@def-setting $windup_toy:maximum-winds=20 +type='integer
-- Defined setting $windup_toy:maximum-winds as:
+parse=is_type
+parse-args=integer
-- Default Setting:
maximum-winds = 20
--
If you ever wanted to change maximum-winds on $windup_toy or even your windup duck, you would use the @set command. Remember if you have not changed the setting on the windup duck, but you change it on $windup_toy, then the duck will inherit the setting of $windup_toy. However, if you change the setting on just your windup duck, then only the duck will be affected and new $windup_toy children will inherit $windup_toy's maximum-winds.
@set $windup_toy:maximum-winds=30 or @set hduck:maximum-winds=30
Now we insert another set of if/elses inside our current set.
@program $windup_toy.wind_cmd() +access=pub
arg cmdstr, cmd, @args;
var max_winds;
max_winds = toint(.get_setting("maximum-winds", $windup_toy));
if (.location() == sender()) {
if (wound < max_winds) {
wound += 2;
sender().tell("You wind up the " + .name() + ".");
.location().announce(strfmt("%s winds up the %s.", sender().name(), .name()));
if (wound >= max_winds)
sender().tell("The knob comes to a stop while winding.");
} else {
sender().tell("The " + .name() + " is already fully wound.");
}
} else {
sender().tell("You have to be holding the " + .name() + ".");
}
.
Notice that we retrieve the maximum-winds setting using the .get_setting() method. When using .get_setting(), you have to pass the name of the setting you want, as well as the object it was originally defined on. Also note then that we make sure the setting we get is converted to an integer with the built-in function toint().
In order to add to the feel of the toy, we put in another test: if, after adding 2 to the wound variable, it has reached the maximum, we tell the player they've come to the end. The more completely you can describe an object's actions and responses to actions, the richer the feel of the Virtual Reality.