Lets Make a Breedable - Part 3

Posted on

Movement

Movement can vary, for example a pet that flies or hops would have different movement code than one that crawls along the ground. In this tutorial we are going to go with crawling along the ground as it is the simplest.

We start the script the same as the ager and hunger script with a default state that waits for a signal to begin. The SPEED vairiable is how fast to walk in metres per second.

vector home_position = ZERO_VECTOR;
float range = 3.0;
float SPEED = 0.1;

default {
    link_message(integer sender, integer number, string message, key id) {
        if (number == 9999) {
            home_position = (vector)llLinksetDataRead("home-position");
            range = (float)llLinksetDataRead("range");

            state frozen;
        }
    }
}

You may be wondering why we start as frozen, we don’t want movement to be enabled by default - that will be something that a breeder needs to enable.

The frozen state needs to look for a signal to go to the resting state. It will also need to watch for any updates to the home position or range, and reset the home position if it is rezzed.

state frozen {
    link_message(integer sender, integer number, string message, key id) {
        if (number == 10) {
            state resting;
        }
    }

    linkset_data(integer action, string name, string value) {
        if (action == LINKSETDATA_UPDATE) {
            if (name == "home-position") {
                home_position = (vector)value;
            } else if (name == "range") {
                range = (float)value;
            }
        }
    }

    on_rez(integer number) {
        home_position = llGetPos();
        llLinksetDataWrite("home-position", (string)home_position);
    }
}

Next we have the resting state. Here the resting state will wait between 15 and 45 seconds, then move to the walking state. We also need to watch for a link_message to move to the frozen state, linkset data to check if the home position and range has been changed, and also check if we are being rezzed - then change to the frozen state if we are.

state resting {
    state_entry() {
        llSetTimerEvent(llFrand(30.0) + 15.0);
    }

    timer() {
        llSetTimerEvent(0);
        state walking;
    }

    link_message(integer sender, integer number, string message, key id) {
        if (number == 11) {
            llSetTimerEvent(0);
            state frozen;
        }
    }

    linkset_data(integer action, string name, string value) {
        if (action == LINKSETDATA_UPDATE) {
            if (name == "home-position") {
                home_position = (vector)value;
            } else if (name == "range") {
                range = (float)value;
            }
        }
    }

    on_rez(integer number) {
        home_position = llGetPos();
        llLinksetDataWrite("home-position", (string)home_position);
        llSetTimerEvent(0);

        llMessageLinked(LINK_THIS, 11, "", "");
        state frozen;
    }
}

Next, we need to write the walking state. The walking state needs to select a destination, face the destination, then walk to it. Once it reaches the destination it goes back to the resting state. Like the other states we need to keep an eye on the command to freeze, the linkset data for home and range updates, and a check if we are being rezzed.

First well create a function to select the destination. Functions should go near the top of your script, just after the global declarations.

vector select_destination() {
    vector destination;

    destination.z = home_position.z;

    destination.x = home_position.x + (llFrand(range * 2) - range);
    destination.y = home_position.y + (llFrand(range * 2) - range);

    return destination;
}

As you can see the destination is simply a random point in a square with the home position in the middle, and the square is range times 2 by range times 2.

We also need a face target function - this involves some tricky math with rotations..

face_target(vector lookat) {
    vector currentpos = llGetPos();
    vector  forward = llVecNorm(lookat - currentpos);
    vector left = llVecNorm(<0,0,1> % forward);
    rotation rot = llAxes2Rot(forward, left, forward % left);
    llSetRot(rot);
}

With these functions in place we can work on our walking state.

state walking {
    state_entry() {
        vector destination = select_destination();

        face_target(destination);

        float distance = llVecDist(llGetPos(), destination);
        float time = distance / SPEED;

        if (time < 0.134) time = 0.134;

        llSetKeyframedMotion([destination - llGetPos(), ZERO_ROTATION, time], []);
        llSetTimerEvent(time);
    }

    timer() {
        llSetTimerEvent(0);
        llSetKeyframedMotion([], []);

        state resting;
    }

    link_message(integer sender, integer number, string message, key id) {
        if (number == 11) {
            llSetTimerEvent(0);
            llSetKeyframedMotion([], []);
            state frozen;
        }
    }

    linkset_data(integer action, string name, string value) {
        if (action == LINKSETDATA_UPDATE) {
            if (name == "home-position") {
                home_position = (vector)value;
            } else if (name == "range") {
                range = (float)value;
            }
        }
    }

    on_rez(integer number) {
        llSetTimerEvent(0);
        llSetKeyframedMotion([], []);
        home_position = llGetPos();
        llLinksetDataWrite("home-position", (string)home_position);

        llMessageLinked(LINK_THIS, 11, "", "");
        state frozen;
    }
}

We use llSetKeyframedMotion for smooth movement, this function requires a list of waypoints and rotations and times, we just have one waypoint - the destination, however the destination must be relative to the current position. The time is distance divided by speed. There are a couple of caveats with this function, the first is that the time must not be lower than 0.134, so if your pet picks a very close destination, we need to adjust the time to meet the requirements. The other is the object must use the new Prim Equivelency system, if you’re using mesh, it will already be using this, if you’re using a prim based object, you can change the physics shape to convex hull to make it use the correct mode.

llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_CONVEX]);

This line would set the physics shape to convex hull, so if you’re not using a mesh, add this line to the default state’s state_entry.

Growth

First we need add a new list global variable, this list will store the prims original size and relative position of each prim in the linkset we also need to know the number of prims in the linkset. The growth factor will be the scale increase each day will increment the linkset. The last age will store the age the pet was when it last increased in size. old_scale stores the last scale of the pet.

list size_data = [];
integer prim_count;
float growth_factor = 0.15;
integer last_age = 0;
float old_scale = 1.0;

To calculate the number of prims in the linkset we will use this function..

integer get_number_of_prims(){
    if (llGetObjectPrimCount(llGetKey()) == 0 ) {
        return llGetNumberOfPrims(); // attachment
    }
    return llGetObjectPrimCount(llGetKey());   // non-attachment
}

Next we need to query the linkset for each primitive in the linkset’s size and position. place this in the default state’s state_entry section.

prim_count = get_number_of_prims();
if (prim_count == 1) {
    size_data = size_data + llGetScale();
    size_data = size_data + llGetPos();
} else {
    integer i;
    for (i = 0; i < prim_count; i++) {
        list pp = [];
        if (i == 0) {
            pp = llGetLinkPrimitiveParams(i + 1, [PRIM_SIZE, PRIM_POSITION]);
        } else {
            pp = llGetLinkPrimitiveParams(i + 1, [PRIM_SIZE, PRIM_POS_LOCAL]);
        }

        size_data = size_data + llList2Vector(pp, 0);
        size_data = size_data + llList2Vector(pp, 1);
    }
}

As you can see, things are different for single prim objects, you could avoid a lot this code if your object does not contain multiple prims.

We’ll also need to add to the default states link_message event, to check the age and grow the pet if needed from an update..

integer age = (integer)llList2String("age");

if (age >= 1 && age <= 5) {
    set_growth_size((integer)age);
    last_age = (integer)age;
} else if (age > 5) {
    set_growth_size(5);
    last_age = (integer)age;
}

Next we need a function that given an age, will resize the object to the appropriate size, this function also moves the pet up to compensate for the size increase (so it’s feed dont sink into the ground) and adjusts the home position accordingly.

set_growth_size(integer my_age)
{
    vector new_size;
    vector new_pos;
    float new_scale = (growth_factor * my_age) + 1.0;

    integer i;
    vector original_size = llList2Vector(size_data, 0);

    if (prim_count == 1) {
        new_size = llList2Vector(size_data, (i * 2)) * new_scale;
        llSetScale(new_size);
    } else {
        for (i = 0; i <= prim_count; i++) {
            new_size = llList2Vector(size_data, (i * 2)) * new_scale;
            new_pos = llList2Vector(size_data, (i * 2) + 1) * new_scale;
            if (i == 0) {
                llSetLinkPrimitiveParams(i + 1, [PRIM_SIZE, new_size]);
            } else {
                llSetLinkPrimitiveParams(i + 1, [PRIM_SIZE, new_size, PRIM_POS_LOCAL, new_pos]);
            }
        }
    }
    vector pos = llGetPos();

    pos.z = pos.z +  ((original_size.z * new_scale / 2) - original_size.z * old_scale / 2);
    home_position.z = pos.z;
    llLinksetDataWrite("home-position", (string)home_position);

    llSetPos(pos);

    old_scale = new_scale;
}

Finally we need to call this function when the age increases, up until the maximum age. We can do this via the linkset data update event in each of the resting, walking and frozen states.

In the frozen state:

linkset_data(integer action, string name, string value) {
    if (action == LINKSETDATA_UPDATE) {
        if (name == "home-position") {
            home_position = (vector)value;
        } else if (name == "range") {
            range = (float)value;
        } else if (name == "age") {
            if (last_age < (integer)value && (integer)value <= 5) {
                last_age = (integer)value;
                set_growth_size(last_age);
            }
        }
    }
}

The resting state will look the same as the frozen state, however, the walking state will be slightly different, because the set growth function moves the pet, we must stop moving with llSetKeyframedMotion before resizing, so the linkset_Data event in the walking section would look like this:

linkset_data(integer action, string name, string value) {
    if (action == LINKSETDATA_UPDATE) {
        if (name == "home-position") {
            home_position = (vector)value;
        } else if (name == "range") {
            range = (float)value;
        } else if (name == "age") {
            if (last_age < (integer)value && (integer)value <= 5) {
                llSetTimerEvent(0);
                llSetKeyframedMotion([], []);

                last_age = (integer)value;
                set_growth_size(last_age);

                state resting;
            }
        }
    }
}

The completed script should look like this:

vector home_position = ZERO_VECTOR;
float range = 3.0;
float SPEED = 0.1;
list size_data = [];
integer prim_count;
float growth_factor = 0.15;
integer last_age = 0;
float old_scale = 1.0;

set_growth_size(integer my_age)
{
    vector new_size;
    vector new_pos;
    float new_scale = (growth_factor * my_age) + 1.0;

    integer i;
    vector original_size = llList2Vector(size_data, 0);

    if (prim_count == 1) {
        new_size = llList2Vector(size_data, (i * 2)) * new_scale;
        llSetScale(new_size);
    } else {
        for (i = 0; i <= prim_count; i++) {
            new_size = llList2Vector(size_data, (i * 2)) * new_scale;
            new_pos = llList2Vector(size_data, (i * 2) + 1) * new_scale;
            if (i == 0) {
                llSetLinkPrimitiveParams(i + 1, [PRIM_SIZE, new_size]);
            } else {
                llSetLinkPrimitiveParams(i + 1, [PRIM_SIZE, new_size, PRIM_POS_LOCAL, new_pos]);
            }
        }
    }
    vector pos = llGetPos();

    pos.z = pos.z +  ((original_size.z * new_scale / 2) - original_size.z * old_scale / 2);
    home_position.z = pos.z;
    llLinksetDataWrite("home-position", (string)home_position);

    llSetPos(pos);

    old_scale = new_scale;
}

integer get_number_of_prims(){
    if (llGetObjectPrimCount(llGetKey()) == 0 ) {
        return llGetNumberOfPrims(); // attachment
    }
    return llGetObjectPrimCount(llGetKey());   // non-attachment
}


vector select_destination() {
    vector destination;

    destination.z = home_position.z;

    destination.x = home_position.x + (llFrand(range * 2) - range);
    destination.y = home_position.y + (llFrand(range * 2) - range);

    return destination;
}

face_target(vector lookat) {
    vector currentpos = llGetPos();
    vector  forward = llVecNorm(lookat - currentpos);
    vector left = llVecNorm(<0,0,1> % forward);
    rotation rot = llAxes2Rot(forward, left, forward % left);
    llSetRot(rot);
}

default {
    state_entry() {
        llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_CONVEX]);
        prim_count = get_number_of_prims();
        if (prim_count == 1) {
            size_data = size_data + llGetScale();
            size_data = size_data + llGetPos();
        } else {
            integer i;
            for (i = 0; i < prim_count; i++) {
                list pp = [];
                if (i == 0) {
                    pp = llGetLinkPrimitiveParams(i + 1, [PRIM_SIZE, PRIM_POSITION]);
                } else {
                    pp = llGetLinkPrimitiveParams(i + 1, [PRIM_SIZE, PRIM_POS_LOCAL]);
                }

                size_data = size_data + llList2Vector(pp, 0);
                size_data = size_data + llList2Vector(pp, 1);
            }
        }
    }

    link_message(integer sender, integer number, string message, key id) {
        if (number == 9999) {
            home_position = (vector)llLinksetDataRead("home-position");
            range = (float)llLinksetDataRead("range");
            integer age = (integer)llLinksetDataRead("age");

            if (age >= 1 && age <= 5) {
                set_growth_size((integer)age);
                last_age = (integer)age;
            }
            state frozen;
        }
    }
}

state frozen {
    link_message(integer sender, integer number, string message, key id) {
        if (number == 10) {
            state resting;
        }
    }


    linkset_data(integer action, string name, string value) {
        if (action == LINKSETDATA_UPDATE) {
            if (name == "home-position") {
                home_position = (vector)value;
            } else if (name == "range") {
                range = (float)value;
            } else if (name == "age") {
                if (last_age < (integer)value && (integer)value <= 5) {
                    last_age = (integer)value;
                    set_growth_size(last_age);
                }
            }
        }
    }

    on_rez(integer number) {
        home_position = llGetPos();
        llLinksetDataWrite("home-position", (string)home_position);
    }
}

state resting {
    state_entry() {
        llSetTimerEvent(llFrand(30.0) + 15.0);
    }

    timer() {
        llSetTimerEvent(0);
        state walking;
    }

    link_message(integer sender, integer number, string message, key id) {
        if (number == 11) {
            llSetTimerEvent(0);
            state frozen;
        }
    }


    linkset_data(integer action, string name, string value) {
        if (action == LINKSETDATA_UPDATE) {
            if (name == "home-position") {
                home_position = (vector)value;
            } else if (name == "range") {
                range = (float)value;
            } else if (name == "age") {
                if (last_age < (integer)value && (integer)value <= 5) {
                    last_age = (integer)value;
                    set_growth_size(last_age);
                }
            }
        }
    }

    on_rez(integer number) {
        home_position = llGetPos();
        llLinksetDataWrite("home-position", (string)home_position);
        llSetTimerEvent(0);
        llMessageLinked(LINK_THIS, 11, "", "");
        state frozen;
    }
}

state walking {
    state_entry() {
        vector destination = select_destination();

        face_target(destination);

        float distance = llVecDist(llGetPos(), destination);
        float time = distance / SPEED;

        if (time < 0.134) time = 0.134;

        llSetKeyframedMotion([destination - llGetPos(), ZERO_ROTATION, time], []);
        llSetTimerEvent(time);
    }

    timer() {
        llSetTimerEvent(0);
        llSetKeyframedMotion([], []);

        state resting;
    }

    link_message(integer sender, integer number, string message, key id) {
        if (number == 11) {
            llSetTimerEvent(0);
            llSetKeyframedMotion([], []);
            state frozen;
        }
    }

    linkset_data(integer action, string name, string value) {
        if (action == LINKSETDATA_UPDATE) {
            if (name == "home-position") {
                home_position = (vector)value;
            } else if (name == "range") {
                range = (float)value;
            } else if (name == "age") {
                if (last_age < (integer)value && (integer)value <= 5) {
                    llSetTimerEvent(0);
                    llSetKeyframedMotion([], []);

                    last_age = (integer)value;
                    set_growth_size(last_age);

                    state resting;
                }
            }
        }
    }

    on_rez(integer number) {
        llSetTimerEvent(0);
        llSetKeyframedMotion([], []);
        home_position = llGetPos();
        llLinksetDataWrite("home-position", (string)home_position);
        llMessageLinked(LINK_THIS, 11, "", "");
        state frozen;
    }
}