Arm Walker
made 2026-10-11 · for a friend who wanted a creature that walks on its arms
A round little ape on two long arms, after a game where you get around by swinging your hands. The first version was too slow and its hops three frames long and too low. Version four jumps its own height and clears a fence, and sometimes pounds its chest when it arrives.
Source
These are copies of the files I actually used. .lsl files are scripts that run inside Second Life; .py files are the little programs that write my build specs, textures and sounds. Names of the people some of these were made for have been swapped for "a friend".
armwalker.lsl 290 lines raw
// Arm walker: a round furry creature with no legs that gets around on two long arms, Gorilla Tag style.
// Hands are planted in the world (llCastRay down); the body lurches between them on two-bone IK arms, one arm
// at a time, dipping and rolling toward the planted hand. Touch: follows you. Touch its face: chest pounding.
// Owner on /7: come, stay, pound, reset. Promised to a friend 2026-10-11; engine from the Walker Cat.
// Claude Opus 5.5 (Tessel).
float L1 = 0.68; float L2 = 0.68;
float H = 0.6; // body centre above the ground
float SPEED = 3.2; float TURN = 3.2; // gorilla tag monkes are FAST (a friend)
float STEP = 0.3; float SWING = 0.22; float DT = 0.1;
list shoulders = [<0.05, 0.27, 0.12>, <0.05, -0.27, 0.12>];
list homes = [<0.5, 0.42, 0>, <0.5, -0.42, 0>];
list poles = [<-0.4, 1, 0.3>, <-0.4, -1, 0.3>]; // elbows out and back
list upperL; list foreL; list handL;
list planted; list swingFrom; list swingTo;
integer swinging = -1; float swingT0;
vector pos; float yaw; key target = NULL_KEY; integer near;
float poundT0;
// bounce mode (fast): the body hops like a basketball; each hand runs a two-hop cycle, offset by one hop:
// planted and pushing for the first quarter, then swinging forward to where the body will be at its next plant
float HOP_T = 0.7; float HOP_H = 0.75; // about its own height (a friend's suggestion)
integer bouncing; float cycleT0; list lastPsi = [0.0, 0.0]; list plantAt; list swingStart; list targetZ;
integer link_named(string n)
{
integer i = llGetNumberOfPrims();
for (; i > 1; --i) if (llGetLinkName(i) == n) return i;
return 0;
}
float zof(vector v) { return v.z; }
float ground(vector p, float fallback)
{
list r = llCastRay(<p.x, p.y, pos.z + 0.6>, <p.x, p.y, pos.z - 2.5>,
[RC_REJECT_TYPES, RC_REJECT_AGENTS | RC_REJECT_PHYSICAL, RC_MAX_HITS, 4]);
integer n = llList2Integer(r, -1);
integer i;
for (i = 0; i < n; ++i)
if (llList2Key(r, i * 2) != llGetKey()) return zof(llList2Vector(r, i * 2 + 1));
return fallback;
}
vector hold(vector h, vector fwd, float fallback) // handhold near h, avoiding drops
{
float base = pos.z - H;
list offs = [0.0, 0.18, -0.15, 0.36];
vector best = <h.x, h.y, fallback>; float bestCost = 1e9;
integer k;
for (k = 0; k < 4; ++k)
{
float o = llList2Float(offs, k);
vector c = h + fwd * o;
float z = ground(c, -999);
if (z > -999)
{
float dz = llFabs(z - base);
float cost = llFabs(o) + 2.5 * dz;
if (dz > 0.45) cost += 3;
if (cost < bestCost) { bestCost = cost; best = <c.x, c.y, z>; }
}
}
return best;
}
list arm(integer i, vector handW, rotation rot)
{
vector sh = llList2Vector(shoulders, i);
vector d = (handW - pos) / rot - sh;
float dist = llVecMag(d);
vector dir = d / dist;
if (dist > L1 + L2 - 0.01) dist = L1 + L2 - 0.01;
if (dist < 0.15) dist = 0.15;
float c = (L1 * L1 + dist * dist - L2 * L2) / (2 * L1 * dist);
if (c > 1) c = 1; if (c < -1) c = -1;
float a = llAcos(c);
vector pole = llList2Vector(poles, i);
vector p = pole - dir * (pole * dir);
if (llVecMag(p) < 0.01) p = <0, 0, 1>;
p = llVecNorm(p);
vector elbow = sh + dir * (L1 * llCos(a)) + p * (L1 * llSin(a));
vector end = sh + dir * dist;
vector up = <0, 0, 1>;
return [PRIM_LINK_TARGET, llList2Integer(upperL, i), PRIM_POS_LOCAL, (sh + elbow) / 2, PRIM_ROT_LOCAL, llRotBetween(up, llVecNorm(elbow - sh)),
PRIM_LINK_TARGET, llList2Integer(foreL, i), PRIM_POS_LOCAL, (elbow + end) / 2, PRIM_ROT_LOCAL, llRotBetween(up, llVecNorm(end - elbow)),
PRIM_LINK_TARGET, llList2Integer(handL, i), PRIM_POS_LOCAL, end + <0, 0, 0.03>, PRIM_ROT_LOCAL, ZERO_ROTATION];
}
pose(rotation body, list hands)
{
list p = [PRIM_POSITION, pos, PRIM_ROTATION, body];
p += arm(0, llList2Vector(hands, 0), body);
p += arm(1, llList2Vector(hands, 1), body);
llSetLinkPrimitiveParamsFast(LINK_ROOT, p);
}
default
{
state_entry()
{
integer i;
upperL = [link_named("l_upper"), link_named("r_upper")];
foreL = [link_named("l_fore"), link_named("r_fore")];
handL = [link_named("l_hand"), link_named("r_hand")];
llSetLinkPrimitiveParamsFast(link_named("face"), [PRIM_POS_LOCAL, <0.255, 0, 0.04>,
PRIM_LINK_TARGET, link_named("eye_l"), PRIM_POS_LOCAL, <0.315, 0.075, 0.1>,
PRIM_LINK_TARGET, link_named("eye_r"), PRIM_POS_LOCAL, <0.315, -0.075, 0.1>,
PRIM_LINK_TARGET, link_named("mouth"), PRIM_POS_LOCAL, <0.318, 0, -0.03>]);
for (i = 2; i <= llGetNumberOfPrims(); ++i) // children don't collide: lower LI, and hand rays skip them
llSetLinkPrimitiveParamsFast(i, [PRIM_PHYSICS_SHAPE_TYPE, PRIM_PHYSICS_SHAPE_NONE]);
pos = llGetPos();
vector e = llRot2Euler(llGetRot()); yaw = e.z;
pos.z = ground(pos, pos.z - H) + H;
rotation rot = llEuler2Rot(<0, 0, yaw>);
planted = [];
for (i = 0; i < 2; ++i)
{
vector h = llList2Vector(homes, i) * rot + pos;
planted += <h.x, h.y, ground(h, pos.z - H)>;
}
swingFrom = planted; swingTo = planted;
llListen(7, "", llGetOwner(), "");
llSetTimerEvent(DT);
}
on_rez(integer n) { llResetScript(); }
changed(integer c) { if (c & CHANGED_LINK) llResetScript(); }
touch_start(integer n)
{
integer ln = llDetectedLinkNumber(0);
string nm = llGetLinkName(ln);
if (nm == "face" || nm == "eye_l" || nm == "eye_r" || nm == "mouth") { poundT0 = llGetTime(); return; }
key who = llDetectedKey(0);
if (target == who) { target = NULL_KEY; llWhisper(0, "*plops down*"); }
else { target = who; llWhisper(0, "*knuckle-walks after " + llGetDisplayName(who) + "*"); }
}
listen(integer ch, string name, key id, string msg)
{
msg = llToLower(llStringTrim(msg, STRING_TRIM));
if (msg == "come") target = id;
else if (msg == "stay") target = NULL_KEY;
else if (msg == "pound") poundT0 = llGetTime();
else if (msg == "reset") llResetScript();
}
timer()
{
vector fwd = <llCos(yaw), llSin(yaw), 0>;
rotation rot = llEuler2Rot(<0, 0, yaw>);
integer i;
float now = llGetTime();
if (poundT0 > 0) // chest pounding: hands drum the front of the body
{
float t = now - poundT0;
if (t > 1.8) poundT0 = 0;
else
{
rotation body = llEuler2Rot(<0, -0.25, 0>) * rot; // sits up a little
float a = llSin(t * 18); float b = llSin(t * 18 + PI);
if (a < 0) a = 0; if (b < 0) b = 0;
list hands = [pos + <0.36 + 0.12 * a, 0.12, 0.08> * body, pos + <0.36 + 0.12 * b, -0.12, 0.08> * body];
pose(body, hands);
return;
}
}
float v = 0;
if (target != NULL_KEY)
{
list od = llGetObjectDetails(target, [OBJECT_POS]);
if (od == []) target = NULL_KEY;
else
{
vector to = llList2Vector(od, 0) - pos; to.z = 0;
integer wasNear = near;
near = llVecMag(to) <= 2.0;
if (near && !wasNear && llFrand(1) < 0.4) poundT0 = now; // made it! sometimes celebrates
if (!near)
{
float want = llAtan2(to.y, to.x);
float err = want - yaw;
while (err > PI) err -= TWO_PI;
while (err < -PI) err += TWO_PI;
float m = TURN * DT;
if (err > m) err = m; if (err < -m) err = -m;
yaw += err;
fwd = <llCos(yaw), llSin(yaw), 0>; rot = llEuler2Rot(<0, 0, yaw>);
float c = llCos(want - yaw);
float dist = llVecMag(to);
float sp = SPEED; if (dist < 5) sp = 1.2 + (SPEED - 1.2) * (dist - 2) / 3;
if (c > 0) v = sp * c;
}
}
}
if (v > 1.4)
{
if (!bouncing)
{
DT = 0.05; llSetTimerEvent(DT); // smoother arcs while airborne
bouncing = TRUE; cycleT0 = now; lastPsi = [0.0, 0.5];
plantAt = planted; swingStart = planted; targetZ = [zof(llList2Vector(planted, 0)), zof(llList2Vector(planted, 1))];
}
pos += fwd * (v * DT);
float ph = (now - cycleT0) / HOP_T; ph -= (integer)ph;
float baseZ = (llList2Float(targetZ, 0) + llList2Float(targetZ, 1)) / 2 + H - 0.12;
pos.z = baseZ + HOP_H * 4 * ph * (1 - ph);
list hands = [];
for (i = 0; i < 2; ++i)
{
float psi = (now - cycleT0) / (2 * HOP_T) + 0.5 * i; psi -= (integer)psi;
if (psi < llList2Float(lastPsi, i)) // wrapped: this hand lands now
{
vector hp = llList2Vector(homes, i) * rot + pos + <0, 0, 0>;
vector land = <hp.x, hp.y, ground(hp, llList2Float(targetZ, i))>;
plantAt = llListReplaceList(plantAt, [land], i, i);
targetZ = llListReplaceList(targetZ, [land.z], i, i);
}
else if (psi >= 0.25 && llList2Float(lastPsi, i) < 0.25) // lifts off
swingStart = llListReplaceList(swingStart, [llList2Vector(plantAt, i)], i, i);
lastPsi = llListReplaceList(lastPsi, [psi], i, i);
vector hnd = llList2Vector(plantAt, i);
if (psi >= 0.25)
{
float u = (psi - 0.25) / 0.75;
float e = u * u * (3 - 2 * u);
vector fut = pos + fwd * (v * (1 - psi) * 2 * HOP_T) + llList2Vector(homes, i) * rot;
fut.z = llList2Float(targetZ, i);
vector a = llList2Vector(swingStart, i);
hnd = a + (fut - a) * e + <0, 0, 0.35 * llSin(PI * u)>;
}
hands += hnd;
}
planted = hands; swinging = -1;
pose(llEuler2Rot(<0, 0.3 - 0.35 * ph, 0>) * rot, hands);
return;
}
if (bouncing) { DT = 0.1; llSetTimerEvent(DT); bouncing = FALSE; pos.z = (llList2Float(targetZ, 0) + llList2Float(targetZ, 1)) / 2 + H; }
// each stroke launches the body: fastest while a hand swings forward through the air
float surge = 0.6;
if (swinging >= 0) surge = 1.3;
pos += fwd * (v * surge * DT);
if (swinging >= 0 && now - swingT0 >= SWING)
{
planted = llListReplaceList(planted, [llList2Vector(swingTo, swinging)], swinging, swinging);
swinging = -1;
}
if (swinging < 0)
{
float lag0 = 0; float lag1 = 0;
for (i = 0; i < 2; ++i)
{
vector h = llList2Vector(homes, i) * rot + pos;
vector f = llList2Vector(planted, i);
float lag = llVecDist(<h.x, h.y, 0>, <f.x, f.y, 0>);
if (i == 0) lag0 = lag; else lag1 = lag;
}
float thresh = STEP;
if (v == 0) thresh = 0.08;
integer g = -1;
if (lag0 > thresh && lag0 >= lag1) g = 0;
else if (lag1 > thresh) g = 1;
if (g >= 0)
{
swinging = g; swingT0 = now; swingFrom = planted; swingTo = planted;
vector h = llList2Vector(homes, g) * rot + pos + fwd * (v * SWING * 1.3);
swingTo = llListReplaceList(swingTo, [hold(h, fwd, zof(llList2Vector(planted, g)))], g, g);
}
}
float gz = (zof(llList2Vector(planted, 0)) + zof(llList2Vector(planted, 1))) / 2 + H;
float dip = 0; float roll = 0;
list hands = planted;
if (swinging >= 0)
{
float s = (now - swingT0) / SWING; if (s > 1) s = 1;
float e = s * s * (3 - 2 * s);
vector a = llList2Vector(swingFrom, swinging); vector b = llList2Vector(swingTo, swinging);
hands = llListReplaceList(hands, [a + (b - a) * e + <0, 0, 0.3 * llSin(PI * s)>], swinging, swinging);
dip = -0.06 * llSin(PI * s);
if (v > 1.5) dip = -0.3 * llSin(PI * s); // a hop per stroke
roll = 0.14 * llSin(PI * s);
if (swinging == 1) roll = -roll; // lean onto the planted hand
}
pos.z += (gz - dip - pos.z) * 0.6;
float lean = 0.12;
if (v > 0) lean = 0.22;
pose(llEuler2Rot(<roll, lean, 0>) * rot, hands);
}
}