< Back 

Mastedore Standalone Unicycle

2026-10-01

A Roblox unicycle that lives in a single model and can be pasted into any game. It uses no VehicleSeat and no physical wheels: the player's Humanoid does the moving and a kinematic wheel model decides how.

LuauRobloxGameplayVehicles
Go to quick view

A unicycle that rides on the Humanoid.

Abstract. This is a Roblox unicycle delivered as a single Model. You copy it, paste it into any game as many times as you want, and it works with no setup. It uses no VehicleSeat and no wheels simulated with physics constraints: the player is joined to the unicycle with a Motor6D, their own Humanoid does the moving, and a client script decides every frame where to go and how fast using a kinematic wheel model. In that model there’s no sideways movement, starting takes effort, asking to go backwards brakes, slopes push downhill and a sideways slide turns into rolling. The project has almost no game logic. The interesting parts are the geometry of the mount (where the seat ends up, what HipHeight puts the wheel on the ground for any avatar) and a handful of differential equations picked to feel right rather than to be exact.

1. The problem

A Roblox vehicle is usually built with a VehicleSeat and real wheels, meaning parts joined with HingeConstraints, friction and a motor. For a four-wheeled car that works because the car is stable on its own. A unicycle isn’t. Physically it’s an inverted pendulum on a wheel, and keeping it upright with the physics engine would take a balance controller fighting gravity on every simulation step, with everything that brings over the network: who owns the parts and how another player’s vehicle looks with latency.

I wanted three things. First, that it should ride like a unicycle and not like a walking character, so no sideways movement, turns that cost something and a heavy start. Second, that it should be robust, without flying off because of a physics glitch or jittering on other players’ screens. Third, that it should be a self-contained model: the whole system (parts, scripts and configuration) lives inside Workspace.Monociclo, and taking it to another game is copy and paste, without touching ServerScriptService, ReplicatedStorage or StarterPlayer.

2. The Humanoid as the vehicle

The decision that shapes everything else is not simulating the unicycle at all. The Humanoid already handles nearly everything that’s hard about moving around a map: it stays upright, follows the ground, climbs steps and collides with walls. On top of that, since the character is simulated by its owner’s client, it replicates to everyone else without sending anything over the network. All it’s missing is moving like a wheel instead of like a person, and that can be imposed from outside with two things, Humanoid:Move(direction) and WalkSpeed.

That splits the unicycle in two. One half is geometric: the model’s parts are attached to the character with a Motor6D, massless and without collision, so they show up in the right place and the player’s animations can move them. The other half is kinematic: a three-variable state (heading, forward speed and lateral velocity) that the client integrates every frame and hands to the Humanoid. The unicycle never falls over, because nothing is balancing it. That’s a deliberate trade-off, and I discuss it under limitations (section 10).

3. A model you copy and paste

Having everything boot from inside the model has concrete consequences. The client scripts can’t be LocalScripts, because a LocalScript doesn’t run in Workspace. They’re Scripts with RunContext = Client, which in the Rojo project comes from the emitLegacyScripts: false option. Each script finds its model by walking up from itself (script.Parent.Parent), so there’s no global name two copies could step on.

What is global by nature are the names registered with client services, like the RunService:BindToRenderStep step and the ContextActionService action. With two unicycles in the same game, both would register “UnicycleControl” and the second would replace the first, which is why every copy appends a GUID to those names when it loads.

Every player runs one client script per unicycle, but only the one for the unicycle that player is riding does anything. The server announces who’s riding by writing the OccupantUserId attribute on the model, the attribute replicates on its own, and each client compares it with its UserId. That avoids a “you started riding” RemoteEvent and guarantees the client never thinks it’s riding when the server doesn’t know about it.

The configuration is a set of ValueBases inside Source/Configuration (MaxSpeed, Acceleration, StartFriction and so on). They’re read live on every lookup, so they can be tweaked in Studio while the game runs, and every read checks the type against the default value’s. If someone deletes the folder or puts a StringValue where a number should be, the code’s default is used and the server warns once about which ones are missing, instead of a nil blowing up in the middle of the physics.

4. Mounting

4.1 Why the Motor6Ds move into the character

The seat is a part called Plane. When an R15 character touches it, the server creates a Motor6D with Part0 = LowerTorso and Part1 = Plane, releases the unicycle’s parts (unanchored, with CanCollide = false and Massless = true) and disconnects the touch event so nobody else can get on.

The animations are what move the seat, the wheel and the pedals, and that’s where an engine restriction shows up, because a character’s Animator only animates joints that are descendants of the character. If the wheel’s Motor6D stays inside the unicycle, the pedaling animation doesn’t move it. The obvious fix would be to put the whole unicycle inside the character, but then the scripts would leave the model and be destroyed along with the character when it dies. What the system does instead is move only the joints: the seat’s Motor6D and the model’s own Motor6Ds (wheel and pedals) go into an UnicycleJoints folder inside the character, and the parts and scripts stay where they were.

When the parts are released, Massless keeps the unicycle from changing the mass of the character’s assembly, so the Humanoid’s force control never notices it’s carrying anything. CanCollide = false leaves the Humanoid’s capsule as the only thing touching the ground.

4.2 The transform chain

A Motor6D places its Part1 with

𝐗1=𝐗0C0TC1−1

where 𝐗0 and 𝐗1 are the two parts’ CFrames, C0 and C1 are the joint’s fixed offsets and T is the Transform the Animator writes every frame. The seat’s Motor6D leaves C1 at identity and uses MountOffset as C0, taken from the LowerTorso.Plane joint of the R15 rig the animations were made with. It’s a translation followed by a half turn around the vertical axis:

C0=𝒯(0, 0.934, −0.142)Ry(π)

Copying that C0 instead of working one out by hand is what makes the animations line up, because they were authored against that exact offset. It has a side effect that’s pretty confusing. At rest, with T=I, the seat sits about 1.04 studs higher than during the animations, almost at the center of the UpperTorso (I measured it at 0.135 studs from the center). If the animations don’t run, the unicycle shows up embedded in the player’s chest and it looks like a welding bug, when there are really two possible causes. The most common one is that the game doesn’t own the animation asset, because an animation only plays in experiences with the same owner, so pasting the model into a game from another account means re-uploading them. The other is that some animation in the game with Action4 priority is covering the pedaling one. The client checks both 1.5 s after mounting: if the track has a Length of zero it didn’t load, and if it loaded but the seat’s T is still the identity, something is overriding it. Each case has its own warning in the Output.

4.3 Dismounting and rebuilding

On load, the server takes a snapshot of every Motor6D in the model with its name, parent, Part0, Part1, C0 and C1. On dismount, the joints go back to their original parent. If the character died or the player left, the UnicycleJoints folder was destroyed along with the character, and the joints with it. In that case setting their Parent fails, because a destroyed instance has its parent locked, and they’re rebuilt from the snapshot. The test for a healthy joint is that its parts match the snapshot and that reparenting it inside a pcall succeeds.

A ride can end in several ways: the player asks to get off, the Humanoid dies, it sits in another seat, the character leaves Workspace, something destroys the seat joint or the player leaves the game. All of them call the same function, and since destroying the seat joint in turn fires the “someone destroyed the joint” event, the function has to survive being called from inside itself. The order of operations takes care of it. The first thing it does is set the current ride to nil and disconnect all its connections, and only then does it destroy anything, so a second call finds nil and returns without doing anything.

The player is only moved aside when they ask to get off, because otherwise they’d be left standing inside the unicycle once its collisions come back. Three directions are tried in order (right, left and back, relative to where they’re facing) with a 4-stud ray, and the character is moved 3 studs toward the first free one. After that there’s a 1.5 s wait before that player can mount again, so the same touch that took them off doesn’t put them right back on.

5. HipHeight for any avatar

The Humanoid keeps the bottom of its HumanoidRootPart at HipHeight above the ground. With the unicycle on, what has to touch the ground is the wheel and not the feet, so a new HipHeight has to be computed on mount. The problem is that it depends on the avatar, because the distance between the HumanoidRootPart and the LowerTorso changes with body scale, and so does the size of the HumanoidRootPart.

The first step is finding the lowest point of the unicycle in seat space, once, on load. Every part is an oriented box of size (sx,sy,sz) with local axes 𝐞x,𝐞y,𝐞z and center 𝐩, all expressed relative to the seat. Its half height along the seat’s vertical axis 𝐲ˆ is the box’s projection radius onto that axis [3]:

h=12(|𝐞x⋅𝐲ˆ|sx+|𝐞y⋅𝐲ˆ|sy+|𝐞z⋅𝐲ˆ|sz)

and the model’s lowest point is ymin⁡=mink⁡(pk,y−hk) over every part k. It’s exact for boxes and a conservative bound for meshes, because a mesh’s Size is its bounding box.

The second step, at mount time, brings that point into HumanoidRootPart space. The rest placement of the LowerTorso relative to the root depends on the kind of rig. Classic characters join them with a Motor6D, and then the rest offset is that joint’s O=C0C1−1. Newer avatars use an AnimationConstraint between two Attachments, and the offset is O=A0A1−1 with the attachments’ CFrames. I added that second case after testing with my own avatar, which uses the new rig and made the calculation fail without any error. With that, the lowest point of the wheel in root space is

𝐪=OC0seat(0, ymin⁡, 0)

If the root has height H, its center sits at HipHeight+H/2 above the ground, and the wheel touches the ground when that height plus qy comes out to zero:

HipHeight=−qy−H2

The result is only applied if it lands between 0.1 and 50 studs. Outside that range something is off with the character and it’s safer to keep the HipHeight it had, which is saved and restored on dismount anyway.

6. The kinematic model

6.1 State and constraint

In robotics, the “unicycle model” is exactly the name for the kinematics of a body on a wheel that can’t slip sideways [1][2]:

x˙=vcos⁡ψ,z˙=vsin⁡ψ,ψ˙=ω

The velocity always points along the heading ψ, which is the nonholonomic constraint x˙sin⁡ψ−z˙cos⁡ψ=0. The vehicle can reach any position, but it can’t move sideways to get there.

The client’s state follows that idea with one addition. It keeps a heading 𝐡ˆ (a horizontal unit vector), a forward speed v≥0 and a lateral velocity ℓ that only shows up on slopes and dies out on its own. The velocity handed to the Humanoid is

𝐮=v𝐡ˆ+ℓ

and with ℓ=0 it’s exactly the unicycle model. Everything is integrated on the client once per frame, with Δt clamped to 0.1 s so a hitch of a few hundred milliseconds doesn’t turn into a jump in speed.

6.2 From input to a direction

Movement in Roblox is camera relative, and the system keeps it that way. The input doesn’t say “accelerate” or “turn left”, it says “I want to go over there” in screen coordinates. The move vector 𝐦 comes from the default ControlModule, which means every device Roblox supports works without any code of its own, and if the game replaced the PlayerModule there’s a fallback reader for keyboard and gamepad.

With 𝐟ˆ the camera direction flattened onto the horizontal plane and 𝐫ˆ=(−fz, 0, fx) its right, the requested direction is

𝐝ˆ=mx𝐫ˆ−mz𝐟ˆ‖mx𝐫ˆ−mz𝐟ˆ‖

and the pedaling intensity is a=min⁡(‖𝐦‖,1), with a 0.1 deadzone. If the camera looks straight down, its flattened direction is nearly zero and its up vector is used instead, which in that case is what the player sees as “up” on the screen.

On a keyboard this changes how it feels compared to walking. A doesn’t move the player to the left, it asks to go toward the camera’s left, and the unicycle turns that way while rolling forward, so it traces a curve.

6.3 Rate-limited turning

The heading turns toward 𝐝ˆ with a maximum angular speed ωmax⁡ (TurnSpeed, 200°/s by default). The signed angle between the two vectors is

δ=atan2⁡((𝐡ˆ×𝐝ˆ)y, 𝐡ˆ⋅𝐝ˆ)

and every frame the heading rotates around the vertical axis by clamp⁡(δ, −ωmax⁡Δt, ωmax⁡Δt). Storing the heading as a vector and rotating it, instead of storing an angle, avoids normalizing angles and the jump between −π and π.

Turning hard also takes power away from pedaling. After rotating, the intensity is multiplied by

g=clamp⁡(𝐡ˆ⋅𝐝ˆ+0.4, 0, 1)

which is 1 while the requested direction is within arccos⁡0.6≈53° of the heading, drops linearly with the cosine and reaches zero at arccos⁡(−0.4)≈114°. Without this factor, a sharp turn at full speed would trace a huge arc. With it, the unicycle stops pushing while it turns, the arc gets much tighter and it speeds up again once it’s pointing where the player wanted to go.

6.4 Braking instead of reversing

A unicycle wheel can roll backwards, but riding in reverse isn’t something a player expects or something the animations cover. The rule is that if the requested direction is more than 120° from the heading (𝐝ˆ⋅𝐡ˆ<−0.5) and the unicycle is still rolling faster than 1.5 studs/s, that input brakes: the intensity is zeroed and there’s no turning. Below 1.5 studs/s the same input falls through to the previous section and the unicycle turns around on the spot. From the player’s side, “back” brakes first and then turns around, which is what someone riding a real one would do.

6.5 The heavy start

With constant-acceleration pedaling, the unicycle picks up speed as easily at the start as at the top, and it feels like a character with a wheel stuck on. StartFriction (μ) makes the first push cost more:

dvdt=Aag(1−μ(1−vV)),v≤V

with A the acceleration (Acceleration) and V the top speed (MaxSpeed). At rest the acceleration is A(1−μ), and it grows linearly with speed until it reaches A at V. With the pedal floored and in a straight line (a=g=1) the equation is linear and has a closed-form solution:

v(t)=V(1−μ)μ(ekt−1),k=AμV

It’s a growing exponential, so a convex curve: the start is slow and speed climbs faster and faster near the end. With the default values (A=16, V=24, μ=0.6) it comes out to v(t)=16(e0.4t−1), and top speed arrives at t=ln⁡(2.5)/0.4≈2.29 s. Without start friction it would take 1.5 s, and in the limit μ→0 the expression reduces to the straight line v=At. The code clamps μ to 0.95, because with μ=1 the acceleration at rest is zero and the unicycle would never get going.

The code integrates this with explicit Euler, vn+1=vn+Δtv˙n, which for this equation gives vn=c((1+kΔt)n−1) instead of c(ekt−1). Since (1+kΔt)1/Δt stays below ek, the time to V grows a little with longer frames: 2.30 s at 60 fps and 2.34 s at 10 fps, which is the worst possible case because of the Δt clamp. A 2% difference in the worst case isn’t noticeable in play, and that’s why I didn’t use the closed form, which would mean storing a start time and recomputing it every time the pedal intensity or the turn angle changes.

6.6 Braking and coasting

Both decelerations are constant, not proportional to speed:

v←max⁡(v−BΔt, 0)andv←max⁡(v−CΔt, 0)

with B=36 (BrakeDeceleration) when braking and C=8 (CoastDeceleration) when the pedal is released. I picked constant friction over a viscous drag v˙=−kv because viscous drag never reaches zero. Speed decays forever and the vehicle keeps creeping along, which feels floaty. With a constant deceleration D, on the other hand, the unicycle stops in a finite time and distance, and both are easy to reason about when tuning:

tstop=v0D,dstop=v022D

From 24 studs/s, braking takes 0.67 s and 8 studs, and coasting takes 3 s and 36 studs.

There’s a third case, which is pedaling while going faster than V, for example after riding down a slope with LimitSlopeSpeed off. Pedaling can’t hold that speed, so C is applied but with a floor at V, and the speed drops to the pedaling top speed and stays there.

6.7 Slopes

A ray cast down from the root, of length H/2+HipHeight+1.5, gives the ground normal 𝐧ˆ. On a slope of angle α, the horizontal component of the normal has length sin⁡α and points downhill, so

𝐬=(nx, 0, nz),‖𝐬‖=sin⁡α,𝐠ˆ=𝐬‖𝐬‖

Gravity along an inclined plane is gsin⁡α, and since the Humanoid takes a horizontal velocity, what matters is its horizontal projection, gsin⁡αcos⁡α. With the scale ks (SlopeGravityScale) that gives

gs=gkssin⁡αcos⁡α=gks2sin⁡2α

which in the code is the product of workspace.Gravity, the scale, slopeSine and groundNormal.Y, because ny=cos⁡α. It peaks at 45°. With Roblox’s default gravity (196.2 studs/s²) and ks=0.25, a 20° slope gives gs≈15.8 studs/s², almost the same as pedaling flat out.

That acceleration is split according to where the unicycle is facing. With f=𝐠ˆ⋅𝐡ˆ:

v←v+gsfΔtif f>0ℓ←ℓ+gsw(𝐠ˆ−f𝐡ˆ)Δt,w=clamp⁡(2f+1, 0, 1)

Facing downhill, the full gravity pushes. The part parallel to the heading speeds up the wheel, and the perpendicular part, 𝐠ˆ−f𝐡ˆ, of length |sin⁡β| with β the angle between heading and slope, pushes sideways. Facing uphill, gravity is ignored, and that’s not physics but a gameplay decision: climbing against a constant brake was tedious, and making uphill feel like flat ground is more fun than being accurate. The transition lives in w, which is 1 as long as the unicycle isn’t facing uphill (f≥0) and fades linearly to zero once the heading is within 60° of straight uphill (f≤−0.5). Without w the sideways force would switch on abruptly when crossing the horizontal.

With LimitSlopeSpeed on, speed never goes past V. With the limit off, the only brake when riding straight downhill is C, and gravity wins when gs>C:

sin⁡2α>2Cgks=1649.05⇒α>9.5°

So with the limit off, any slope steeper than about 9.5° accelerates the unicycle without end. That’s what AbsoluteMaxSpeed (400 studs/s) is for, a safety cap that isn’t part of normal tuning.

6.8 Lateral grip

Lateral velocity is the only thing that breaks the wheel constraint, and the system gets rid of it in two ways at once. First, the heading turns toward the actual velocity 𝐮=v𝐡ˆ+ℓ, with an angular speed that grows with the slide:

ωs=ωslope⋅clamp⁡(‖ℓ‖2, 0, 1)

with ωslope=120°/s (SlopeTurnSpeed). It’s what a shopping cart caster does: push it sideways and it swivels until the push is straight ahead of it. Then the velocity is projected back onto the new heading 𝐡ˆ′, and whatever is left over sideways is damped:

v=max⁡(𝐮⋅𝐡ˆ′, 0),ℓ=(𝐮−(𝐮⋅𝐡ˆ′)𝐡ˆ′)e−γΔt

The projection is what turns sliding into rolling, because as the heading lines up with 𝐮, more and more of the velocity counts as forward and less as lateral. The e−γΔt factor, with γ=2.5 (LateralGrip), is the tire’s grip, and it halves whatever slide is left every ln⁡2/γ≈0.28 s. Unlike pedaling, this step uses the exact exponential factor and not Euler, so the decay is the same at any framerate.

On a cross slope, the result is that the unicycle drifts sideways a bit, turns downhill by itself and ends up rolling down, the way a real wheel would.

6.9 Against a wall

If the player pedals into a wall, the kinematic model doesn’t know about it and v would climb all the way to V. Once the player turned away from the wall, the unicycle would shoot off at full speed. To prevent that, while it’s on the ground, the commanded speed is tied to the assembly’s actual speed:

v←min⁡(v, ‖𝐮real‖+4)

The 4 studs/s of slack is what makes starting possible. From rest the actual speed is zero, and with no slack v could never rise above zero. With it, v climbs to 4, the Humanoid starts moving, the actual speed rises and the cap rises with it. Head-on against a wall, the actual speed stays at zero and v stays at 4.

The animations use a different speed, min⁡(v,‖𝐮real‖), so the pedals don’t spin in the air while the player is stuck.

6.10 From the state to the Humanoid

At the end of the frame the state is handed over like this:

𝚆𝚊𝚕𝚔𝚂𝚙𝚎𝚎𝚍=‖𝐮‖,𝙼𝚘𝚟𝚎(𝐮/‖𝐮‖)

and the root’s CFrame is set to face 𝐡ˆ, with AutoRotate turned off. Keeping the two separate lets the player face one way while sliding another, which is exactly what happens during a sideways slide.

The order inside the frame matters. Roblox’s default controls also call Move() every frame, from RenderStep at Input priority. The unicycle registers at Camera + 1 priority, so it runs after both the controls and the camera, and its Move() is the last one in the frame and the one that wins. It also means it reads the camera already updated for that frame, with no one-frame lag.

For the duration of the ride the Climbing, FallingDown and Ragdoll states are disabled (and Jumping too if AllowJump is off), so the unicycle doesn’t start climbing ladders or tumbling around. In the air the heading can still turn, but pedaling, braking and slopes only act with the wheel on the ground, so a jump keeps whatever speed it had.

7. Animations

There are two looped tracks. The idle one plays at Action3 priority for the whole ride and is what places the unicycle under the player (section 4.2). The pedaling one plays on top, at Action4, only while the wheel turns, and its playback speed follows the wheel:

r=clamp⁡(vwheelvref, 0.05, 5)

For the wheel not to look like it’s skidding, the animation’s rotation has to match rolling, v=ωR. If one animation cycle lasts T and the wheel turns n times in it, that happens at normal speed when vref=2πRn/T. AnimationReferenceSpeed is that number, and since it depends on the animation and the wheel’s radius, I left it configurable instead of computing it.

The pedaling track starts when the wheel goes above 0.75 studs/s and stops when it drops below 0.35. With a single threshold, a speed hovering around it (braking against a wall, for example) would switch the animation on and off several times a second. With two thresholds there’s hysteresis and that can’t happen. Both transitions use a 0.25 s fade.

8. Network and authority

The split is simple. The server decides who’s riding: it validates the touch (an R15 character, alive, not in another seat, not already riding another unicycle and past the cooldown), creates the joints and writes the attribute. The rider’s client simulates all the movement. Since the character is network-owned by its player and the unicycle is welded to it and massless, the whole thing replicates as part of the character. There isn’t a single per-frame message between client and server, and the server spends nothing while someone rides.

The only RemoteEvent is Dismount. The client fires it with E, ButtonB or a touch button, with a 0.3 s cooldown, and the server only accepts it if the sender is the current rider. On mobile, the button goes inside the default TouchControlFrame, right above the jump button and at 70% of its size, so it shows and hides together with the rest of the touch controls. If the game replaced those controls, the button falls back to its own ScreenGui in the corner.

The client owning the movement means a modified client could move its unicycle however it wanted. But that’s nothing new, because that same client could already move its character that way before getting on, and protecting against it is the game’s job, not the vehicle’s.

9. What you don’t see

Some details don’t come from any model, but without them the system broke in real games:

  • To find out who touched the seat, the server walks up from the touched part until it finds a Model that’s a player’s character, because accessories and tools touch things too.
  • The RidingUnicycle attribute on the character keeps anyone from riding two unicycles at once when several are close together.
  • Each part’s original collision and mass are saved before releasing them and restored on dismount, instead of being set to a fixed value, because whoever builds the model may have set them up their own way.
  • When the ride ends, the client puts WalkSpeed, AutoRotate and every Humanoid state back to what they were before mounting and not to Roblox’s defaults, because the host game may have its own walk speed.

10. Limitations

The unicycle doesn’t fall over. There’s no simulated balance, so there’s also no way to lose it, and that’s the biggest difference from a real one. Adding an inverted pendulum on top of the Humanoid (a visual lean that grows with acceleration and a fall past a certain angle) is the most obvious extension, and it could be done without touching the rest, since it would just be one more term in the state.

Only R15 characters work, since mounting depends on the LowerTorso. HipHeight is computed from the rest pose and doesn’t see the animations’ Transform, so if a new animation changes the wheel’s height relative to the root, MountOffset has to be adjusted. Euler integration makes acceleration depend on the framerate by 2% at most, which I found acceptable. Uphill gravity is ignored on purpose, which lets the unicycle climb slopes a real one couldn’t. And the animations have to be re-uploaded with the account or group that owns each game the model is pasted into. That’s a Roblox restriction and not the system’s, but it’s the first thing anyone using it will run into.

11. What the project left me with

What helped the most was to stop thinking about the unicycle as a physical object. Once I treated it as a constraint on something that already moved well, the Humanoid, the problem went from “how do I keep a wheel upright over the network” to “what velocity do I ask a character for every frame”. That’s a few first-order equations that can be solved by hand, tuned with numbers that mean something (seconds to top speed, studs to stop) and tested without fighting the physics engine. The hard parts turned out to be the engine ones, like the Animator only seeing the character’s joints or avatars with an AnimationConstraint, and not the math.

References

  1. S. M. LaValle. Planning Algorithms. Cambridge University Press, 2006.
  2. R. Siegwart and I. R. Nourbakhsh. Introduction to Autonomous Mobile Robots. MIT Press, 2004.
  3. C. Ericson. Real-Time Collision Detection. Morgan Kaufmann, 2005.

This text by Mastedore. All rights reserved.