A Magic Carpet [without] Ride

In which our heroine shows how she would build a carpet that unrolls and rolls back up again.

In Kuula, doremifasolatido.resident, known as Billy, is rapidly learning building and scripting skills. One of the tasks he set himself was to build a carpet object that rolls out and rolls back up. ┬áBilly likes to ask people to look at his scripts, and to ask people how they would do things. In this article, I’ll build a script like that, and, as I go, show how I go about it.

My overall objective here, as with most programs, is to get the program working (or “working”) as soon as possible, and then keep it working as it grows in capability. Let’s see what happens.

A Little Design

I was taught to start off with just a bit of design thinking. I don’t commit myself to it, but the thinking helps me get a sense of what might be a good way to go.

Seems like the carpet part will be a very thin cube that gets longer and shorter as it rolls. And there will be a cylinder part that moves along, looking like the carpet roll. It should get thinner as the carpet rolls out, thicker as the carpet rolls up.

I suppose I’ll be setting the scale of the carpet, making it longer and shorter. It’s clear that I’ll have to set the position of the roll, and make it thinner and thicker. When we scale a cube, it will grow from its center, which will make the carpet appear to be growing from both ends. To avoid this, we’ll use the old trick of slicing the cube prim to the middle, so that it appears to grow just from the one end.

What else? Well I know that Billy wants the carpet to be able to be of any size, so that means I shouldn’t make any assumptions about how big it is in my code. Or, at least, as few assumptions as possible.

This makes me think that the carpet should grow and shrink in terms of “percentage”, or, because it’s more like one would do in a computer program, to grow in terms of zero (shortest) to one (longest).

OK, enough design. Let’s build something.

Down To Work

I build a cube and slice it, setting Begin to 0.5. It slices off the bottom. This tells me that I need to rotate the cube 90 degrees, and that the z scale will be the scale that grows and shrinks. So I do that, and make it look like a carpet.

Prim for Carpet
Prim for Carpet

My next question is whether to add in the roll right now. In favor of the idea is that since linked objects have different link parameters than unlinked, that aspect won’t change. Against is that it might be simpler not to have the roll there to worry about. I’m not very worried yet, so I decide to add in the roll.

Roll for Carpet
Roll for Carpet

I didn’t worry much about where to put it. I lined it up in X and Y just so it would be about right, but I’m going to concern myself first with the carpet part.

Rolling the Carpet

I’ll add the script and work on rolling the carpet. I suppose I’ll make it roll on touch and whichever way it is, it’ll go the other way. I could imagine doing just one direction and then the other but I’ll start with the whole shell in place.

I was taught to program “by intention”. That means that first I type in what I intend to do, and then define each part bit by bit. I’ll also try to make the program “work” as soon as possible, for some simple definition of “work”. So my initial code looks like this

default { 
    state_entry() { 
    } 

    touch_start(integer total_number) { 
        if ( rolled ) { 
            unroll(); 
        } else { 
            roll(); 
        } 
    } 
}

So what does that code tell us about my intentions? Well, I have in mind a boolean (TRUE/FALSE) variable saying whether we are rolled up or not. When someone touches us, if we are rolled, we unroll, and if we are not, we roll.

As written, this program won’t even compile, much less run. It lacks definitions for rolled, roll() and unroll(). I’ll put those in to make the program begin to work:


integer rolled; 

roll() { 
    llOwnerSay("roll"); 
    rolled = TRUE; 
} 

unroll() { 
    llOwnerSay("unroll"); 
    rolled = FALSE; 
} 

default { 
    state_entry() {
    } 

    touch_start(integer total_number) { 
        if ( rolled ) { 
            unroll(); 
        } else { 
            roll(); 
        } 
    } 
} 

Now, when I click the object, it says “roll” and then “unroll” on the next click, and then “roll” again. It’s working! Doesn’t do much but I know that it is handling the switching between rolling and unrolling. So now let’s express some intention around unrolling. Remember that I’m planning to go from zero to one on unrolling. My thought is this: “I’ll go from fraction = zero to one by some small step. On each step, I’ll set the carpet to the proper scale and position for the value of fraction. The unroll code becomes as shown here, including a new function setScaleAndPosition:



unroll() {
    llOwnerSay("unroll");
    rolled = FALSE;
    float frac;
    for ( frac = 0; frac < 1; frac += 0.1 ) {
        setScaleAndPosition(frac);
    }
}

setScaleAndPosition(float frac) {
    llOwnerSay( "rolled to " + (string) frac);
}

As soon as I type that in, and click the object, I notice two things. First, although the carpet looks rolled on my screen, it is starting out with the “rolled” variable set to FALSE, so it first says “roll” and only then says “unroll”. I need to correctly initialize “rolled”.

Second, I notice that when it displays “rolled to”, it only goes from zero to 0.9, not all the way to one. I can change the for loop to say frac <= 1.0 but that makes me worry about something else.

I happen to know that floats do not always add up just the way one expects. So generally speaking, I don't trust for loops using floats. I'll leave that for now but make a note to be alert for trouble. I might even change it later just to be sure.

For now, though, I might as well add similar code to roll as I have to unroll. Naturally, this will want to count down from one to zero instead of up from zero to one. It will look like this:

integer rolled = TRUE;

roll() {
    llOwnerSay("roll");
    rolled = TRUE;
    float frac;
    for ( frac = 1; frac >= 0; frac -= 0.1 ) {
        setScaleAndPosition(frac);
    }
}

unroll() {
    llOwnerSay("unroll");
    rolled = FALSE;
    float frac;
    for ( frac = 0; frac <= 1; frac += 0.1 ) {
        setScaleAndPosition(frac);
    }
}

setScaleAndPosition(float frac) {
    llOwnerSay( "rolled to " + (string) frac);
}

default {
    state_entry() {
        rolled = TRUE;
    }

    touch_start(integer total_number) {
        if ( rolled ) {
            unroll();
        } else {
            roll();
        }
    }
}

Wow, that happened sooner than I expected. Even though I changed those less-than and greater-than to less-than-or-equal-to and greater-than-or-equal-to, the values don’t go all the way to zero on the way down or one on the way up. I’ll convert to integer right now.

roll() {
    llOwnerSay("roll");
    rolled = TRUE;
    integer percent;
    for ( percent = 100; percent >= 0; percent -= 10 ) {
        setScaleAndPosition(percent/100.0);
    }
}

unroll() {
    llOwnerSay("unroll");
    rolled = FALSE;
    integer percent;
    for ( percent = 0; percent <= 100; percent += 10 ) {
        setScaleAndPosition(percent/100.0);
    }
}

Now notice something: I actually changed the name of the variable from frac to percent, because now I’m going from zero percent to 100 percent, not zero to one. This may seem like a nicety now but when we look at this code a few days from now, I think better naming will help.

I always try to keep the names as communicative as I can. I won’t get it perfect but I do my best.

Now there is something else to worry about: we have two loops, one up and one down. These look very similar: they are almost duplicates. Whenever we see duplicate or near duplicate code, this is the code’s way of telling us that there is a new function that wants to be written. We might write it now or we might defer it. It’s usually best to do it right away, in my experience, because otherwise we’ll forget. In addition, quite often the duplicate code will start turning up again and again.

In this case, I’m sure it won’t occur again, I’m not sure how to make it better, and I am anxious to move on, so I’ll defer this improvement. I sure hope I don’t forget to do it later.

Instead, I decide it’s time to make the carpet actually grow. This is “easy” now: I have a place to do it, namely the setScaleAndPosition function. And notice that function’s name reminds me that there are two things to do. I’ll just do the first one: set the scale, but I’ll express my intention to do both:

setScaleAndPosition(float frac) {
    llOwnerSay( "rolled to " + (string) frac);
    setScale(frac);
    setPosition(frac);
}

Now something interesting is happening here. Notice that the only parameter I’m sending around is “frac”, the fraction of unrolled or rolled that I want to have. At the moment, I see no reason for any part of the program to worry about size or scale or position or anything else. I think all I need so far is the fraction.

Let’s see about setting the scale of the prim now. I’m starting to write the setScale(frac) function. Since frac goes from zero to one, and gets larger as we unroll, we know that we want the scale to, somehow, go from zero to some maximum length. I think right now I’ll make that maximum length be three, just to have a value. But we need to remember that the size needs to be able to vary.

Even though I fully intend to use llSetLinkPrimitiveParamsFast to do this, I’m not going to start there. Why not? Because it’s a pain to create those lists and I want to get the program working as soon as possible. I can refine it to build the lists later. For now, I’ll use llSetScale().



setScaleAndPosition(float frac) {
//    llOwnerSay( "rolled to " + (string) frac);
    setScale(frac);
    setPosition(frac);
}

setPosition(float frac) {
}

setScale(float frac) {
    float maxLength = 3.0;
    float length = maxLength*frac;
    vector scale = llGetScale();
    vector newScale = ;
//    llOwnerSay("new scale " + (string) newScale);
    llSetScale(newScale);
}

No sooner did I run this than I noticed that the carpet goes down to really tiny. Clearly I don’t want that: it should show a little tab. One way to fix that would be to change the loop range. Rather than that, I think I’ll change setScale so that it won’t go below some minimum length … perhaps 0.5.

setScale(float frac) {
    float maxLength = 3.0;
    float length = maxLength*frac;
    if ( length < 0.5 ) length = 0.5;
    vector scale = llGetScale();
    vector newScale = ;
//    llOwnerSay("new scale " + (string) newScale);
    llSetScale(newScale);
}

Now that the carpet has visible behavior, and now that I’m quite certain that the basic code is doing what it should, I decide to remove the chat lines and review the whole program. Here’s what we’ve got now:

integer rolled = TRUE;

roll() {
    rolled = TRUE;
    integer percent;
    for ( percent = 100; percent >= 0; percent -= 10 ) {
        setScaleAndPosition(percent/100.0);
    }
}

unroll() {
    rolled = FALSE;
    integer percent;
    for ( percent = 0; percent <= 100; percent += 10 ) {
        setScaleAndPosition(percent/100.0);
    }
}

setScaleAndPosition(float frac) {
    setScale(frac);
    setPosition(frac);
}

setPosition(float frac) {
}

setScale(float frac) {
    float maxLength = 3.0;
    float length = maxLength*frac;
    if ( length < 0.5 ) length = 0.5;
    vector scale = llGetScale();
    vector newScale = ;
    llSetScale(newScale);
}

default {
    state_entry() {
        rolled = TRUE;
    }

    touch_start(integer total_number) {
        if ( rolled ) {
            unroll();
        } else {
            roll();
        }
    }
}

Frankly, I’m pretty pleased with the shape of this program, and I’m not surprised: generally when I’m able to program “by intention”, things turn out this way. With a little care in naming, the program becomes rather clear, and it tends to work early and stay working right along. And the various functional aspects tend to break out nicely.

Notice that I set the maximum length with a variable but left it in the function for now. I did this to remind myself that I need to make it variable but for now I wanted to keep it near the action. There will be time to move it out later on, when we get to that story.

I think I’m ready to start moving the roll. Let’s roll on. Get it? See what I did there? HAHAhaha … ha … never mind.

Positioning the Roll

Thinking for just a moment suggests to me that the roll wants to move along so that it’s center is right above the end of the growing carpet. Two concerns come to mind: first, its height will need to change as the roll size changes, and second, the position may need to be adjusted depending on carpet rotation. Maybe the second concern won’t arise: possibly we can just set the position and be done with it.

So, hmm. Where does the carpet roll want to go? Though the root prim is oriented with Z in the unrolling direction, the whole carpet linked object is oriented with the carpet unrolling toward positive Y. So, at a random guess, the local position of the roll should be set to the same x and same z as it starts with (for now) and its y should be set to frac*max length. This means that I’ll need to pull max length out into a global (or pass it in as a parameter). For now, I like the global. So to script:



setPosition(float frac) {
    vector pos = getRollPosition();
    float length = maxLength*frac;
    if ( length < 0.5 ) length = 0.5;
    vector newPos = ;
    setRollPosition(newPos);
}

vector getRollPosition() {
    list p = llGetLinkPrimitiveParams(2, [PRIM_POS_LOCAL]);
    return llList2Vector(p, 0);
}

setRollPosition(vector position) {
    llSetLinkPrimitiveParamsFast(2, [PRIM_POS_LOCAL, position]);
}

Now there’s a fair amount of code there, and in fact I didn’t get it all right first time out. What I did do right was to do the getting of the roll position and setting it “by intention”. I just wrote what I wanted, getRollPosition and setRollPosition, then after I wrote the main bit, wrote those little functions. What I got wrong was my guess about which vector value to change in the roll. It turns out it was z, because the local positioning is based on the root prim’s orientation, which is z forward. So I have to change the z coordinate. Also, since the root prim is sliced, the z coordinate has to be cut in half here, or the roll goes too far. But at this point, the rolling position part is working fine.

I’d like to emphasize this. If you tried writing this program on your own, did you spend any time confused, wondering what was broken? Did you spend time worrying about starting and ending values, or how to go forward versus backward?

Notice that this approach has deferred many of those questions, and assumed others away. It might appear that I just lucked into a good design. But that’s not what happened. What happened — and it usually does happen this way — is that by coding to make a small version of the program “work” very soon, and by expressing intention before worrying about implementation, things are almost forced to turn out well. Mistakes are generally localized and discovered quickly, because I only write a few lines of code and then test again. This means my mistake can only be in those few lines I just wrote.

My experience is that working this way, I progress smoothly, with few errors, and small errors when they happen at all. I am less often confused about what I’m doing. That means a lot to me: I’m easily confused. :)

What’s next? The roll diameter, I guess, and the related positioning in the z direction. As soon as I think this, I realize that setPosition isn’t really the right name. It should be something like setRollPositionAndSize. I’ll do some renaming as I do the next step. Here goes.

Size the Roll and Position Z

I feel the need to do a bit of thinking. The way the roll works now is that it preserves its original x and y position, and adjusts z. (Remember, Janet and reader, that to the roll, z is the direction of motion. We’ll need to keep that the same but also adjust … let me edit the object … what turns out to be negative y. Arrgh. Why? Because that happens to be the way I rotated the sliced prim. So we’ll need to take that into account in this next bit. Anyway we want to scale the prim diameter, and adjust its y height. I’ll write those in separate param sets for now.

Why will I do that? Because I like to keep things simple. Editing is easy and thinking is hard. So I’ll break out those operations, and when I get them right, plunk them all back together into param settings. Here goes …



integer rolled = TRUE;
float maxLength = 4.0;

roll() {
    rolled = TRUE;
    integer percent;
    for ( percent = 100; percent >= 0; percent -= 1 ) {
        setScaleAndPosition(percent/100.0);
    }
}

unroll() {
    rolled = FALSE;
    integer percent;
    for ( percent = 0; percent <= 100; percent += 1 ) {
        setScaleAndPosition(percent/100.0);
    }
}

setScaleAndPosition(float frac) {
    setScale(frac);
    setRollPositionAndScale(frac);
}

setRollPositionAndScale(float frac) {
    float maxDiameter = 0.5;
    vector pos = getRollPosition();
    float length = maxLength*frac;
    if ( length < 0.5 ) length = 0.5;
    float diameter = maxDiameter*(1.0-frac);
    vector newPos = ;
    vector scale = getRollScale();
    vector newScale = ;
    setRollScale(newScale);
    setRollPosition(newPos);
}

vector getRollPosition() {
    list p = llGetLinkPrimitiveParams(2, 
        [PRIM_POS_LOCAL]);
    return llList2Vector(p, 0);
}

vector getRollScale() {
    list s = llGetLinkPrimitiveParams(2, 
        [PRIM_SIZE]);
    return llList2Vector(s,0);
}

setRollPosition(vector position) {
    llSetLinkPrimitiveParamsFast(2, 
        [PRIM_POS_LOCAL, position]);
}

setRollScale(vector scale) {
    llSetLinkPrimitiveParamsFast(2, 
        [PRIM_SIZE, scale]);
}

setScale(float frac) {
    float length = maxLength*frac;
    if ( length ;
    llSetScale(newScale);
}

default {
    state_entry() {
        rolled = TRUE;
    }

    touch_start(integer total_number) {
        if ( rolled ) {
            unroll();
        } else {
            roll();
        }
    }
}

OK, there’s the whole program, basically working! The carpet lengthens, the roll moves, and it gets smaller and smaller as it rolls out. In a sense, we’re done, in that the program works. I’m going to take a break now, in fact, and leave room for comments and questions. If anyone reads it. In due time, I’ll come back and see if the code needs a bit more improvement.

Summary … for now

I’ve shown as closely as I thought you could stand, just how I work. I make the program function in some limited way, perhaps even just printing at first. I express my intention for what I’m going to do, by calling a function, and then I write the function.

The result of working this way, for me, is that I’m rarely very confused, and I get all kinds of little jolts of good feelings as the program gets more and more powerful. I keep the program neat and the result is that I can understand it better and change it more readily.

Looking at what we have now, I see at least one iffy area, the setRollPositionAndScale function. It is quite long by my standards: ten lines. And internally it is doing at least two things: it’s setting position, and it’s setting scale. In software design terms, that function is not as “cohesive” as it could be. It’s best if each function does only one thing. What I was taught to do to improve that is to break the function into two, calling both from this one. That expresses the sequential character of the function in one place, and the details in other places. The people who taught me feel that that’s best.

Often, programmers are troubled by all the tiny functions. They feel that they need to look all around to see what is going on. But when we write this way, each function tells its own story. We only look at the details when we care about them.

Anyway, my purpose here is to show you how I program. It works very well for me. As you can probably see, there was little confusion and no debugging to be done. I think the approach would work for you as well, but whether you try it is up to you.

Comments welcome!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: