Roblox Vector3 & CFrame Explained (Movement Math)
Last updated:
Quick answer: A Vector3 is a position or offset made of three numbers (X, Y, Z), while a CFrame is a position plus a rotation: use Vector3 for distances and math, CFrame for placing and orienting parts, and combine them with multiplication and :Lerp to move things exactly where you want.
A Vector3 describes a position or offset in 3D space as three numbers (X, Y, Z), while a CFrame (coordinate frame) describes both a position and a rotation at once. Roblox movement math combines the two: Vector3 for distances and offsets, CFrame for placing and orienting parts, multiplied in the right order.
What is a Vector3 in Roblox?
A Vector3 is three numbers — X (right), Y (up), Z (forward) — in studs, Roblox's unit of distance. Every part's Position is a Vector3, and every movement you compute starts here:
local position = Vector3.new(0, 5, 0)
local offset = Vector3.new(10, 0, 10)
print(position.Magnitude) -- 5: distance from the origin (0, 0, 0)
print(offset.Magnitude) -- ~14.14: distance of this offset
print(offset.Unit) -- same direction, length exactly 1
local combined = position + offset -- new Vector3: (10, 5, 10)
local doubled = offset * 2 -- same direction, twice as far
Two properties do most of the work. Magnitude is the length of the vector — distance between two points is (a - b).Magnitude. Unit is the same direction scaled to length 1, which is how you turn "where is the enemy" into "which way do I walk". Vector3 values are immutable: position + offset builds a new Vector3 and leaves the originals untouched. This is also why the touched event tutorial measures (hit.Position - part.Position).Magnitude to check how close a collision was.
What is a CFrame in Roblox?
A CFrame (coordinate frame) is a position plus a rotation. A Vector3 alone cannot say which way a part faces: two parts at the same Position can point in opposite directions. The CFrame stores both, and Roblox uses it as the single source of truth for a part's placement:
local part = workspace:FindFirstChild("Part")
print(part.CFrame.Position) -- Vector3: where the part is
print(part.CFrame.LookVector) -- Vector3 of length 1: where its front face points (-Z axis)
print(part.CFrame.RightVector) -- its +X axis
print(part.CFrame.UpVector) -- its +Y axis
Think of a CFrame as "the origin and orientation of a tiny coordinate system attached to the part". The three unit vectors above are that coordinate system's axes. Setting part.Position moves the part and keeps its rotation; setting part.CFrame sets both at once. (For meshes and models the internal pivot plays the same role — see PivotTo below.)
What is the difference between CFrame.new, CFrame.Angles, and CFrame.fromEulerAnglesXYZ?
All three build CFrame values; they differ in how much rotation they encode. CFrame.new(position) is position only (no rotation). CFrame.Angles(rx, ry, rz) is rotation only, built from three angles in radians. CFrame.fromEulerAnglesXYZ(rx, ry, rz) is the same rotation-only CFrame, produced through a different constructor that spells out the rotation order:
local position = Vector3.new(0, 5, 0)
local placed = CFrame.new(position) -- at (0, 5, 0), unrotated
local turned = CFrame.new(position) * CFrame.Angles(0, math.rad(90), 0)
-- same position, rotated 90 degrees around the Y axis
local turned2 = CFrame.new(position) * CFrame.fromEulerAnglesXYZ(0, math.rad(90), 0)
-- identical result: fromEulerAnglesXYZ applies X, then Y, then Z
The practical differences to remember:
CFrame.AnglesandCFrame.fromEulerAnglesXYZwant radians, not degrees. Convert withmath.rad(90); feeding in 90 means ~5157 degrees and your part spins wildly.- Euler angles apply in a fixed order (X then Y then Z for
fromEulerAnglesXYZ), so swapping the order of the same two angles gives a different orientation — the usual cause of "my rotation is slightly off". - To combine a position and a rotation, multiply:
CFrame.new(pos) * CFrame.Angles(...)places a rotated frame at pos. - To read rotation back out, use
CFrame:ToEulerAnglesXYZ(), which returns three radian angles.
How do I move a part with PivotTo and CFrame multiplication?
Assigning part.CFrame moves one part. Moving a whole model — a car, a house, an NPC rig — needs Model:PivotTo(cframe), which repositions the model's pivot while keeping every part's offset inside the model:
local part = workspace:FindFirstChild("Part")
-- world-space move: straight up 5 studs, no matter how the part faces
part.CFrame = part.CFrame + Vector3.new(0, 5, 0)
-- object-space move: 5 studs along the part's own forward direction
part.CFrame = part.CFrame * CFrame.new(0, 0, -5)
-- move and turn a whole model in one call
local house = workspace:FindFirstChild("House")
house:PivotTo(CFrame.new(0, 10, 0) * CFrame.Angles(0, math.rad(45), 0))
Multiplication order is the whole game. In a * b, the offset b is interpreted in a's local (object) space, so part.CFrame * CFrame.new(0, 0, -5) moves forward along the part's own LookVector whatever direction it faces. Adding a Vector3 (part.CFrame + Vector3.new(...)) offsets in world space. Both are correct — they answer different questions.
One caveat: for a script to move a part with CFrame, set part.Anchored = true first. An unanchored part is owned by the physics engine, which will fight (and overwrite) your per-frame CFrame assignments.
How do lookAt and object-space vs world-space offsets work?
CFrame.lookAt(eye, target) builds the CFrame for "stand at eye, face target" — the one call that replaces a pile of trigonometry:
local npcPosition = Vector3.new(0, 3, 0)
local target = Vector3.new(20, 3, 40)
local npcCFrame = CFrame.lookAt(npcPosition, target)
-- OBJECT-space offset: 5 studs in front of the NPC (its -Z / LookVector),
-- even after it turns
local inFront = npcCFrame * CFrame.new(0, 0, -5)
-- WORLD-space offset: 5 studs above, no matter which way the NPC faces
local above = CFrame.new(npcCFrame.Position + Vector3.new(0, 5, 0))
print(npcCFrame.LookVector) -- unit Vector3 pointing at the target
The rule that resolves 90% of offset bugs: base * CFrame.new(x, y, z) places the offset in the base's rotated space; base.Position + Vector3.new(x, y, z) places it in world space. If a "5 studs to the side" keeps landing in the wrong place after the part turns, you used world space where you wanted object space. The same distinction drives steering code: to move toward a point, add the direction's Unit scaled by speed; to orbit around a point, build positions from math.cos/math.sin on a circle and use CFrame.lookAt to keep facing the center — the pattern used in the raycasting guide when aiming a line of sight from a rotated part.
What are practical CFrame patterns: orbit, follow, and lerp?
Most movement code is one of three recipes. Orbit: walk a circle around a center over time.
local RunService = game:GetService("RunService")
local part = workspace:FindFirstChild("Part")
local center = Vector3.new(0, 10, 0)
local radius = 20
local angle = 0
RunService.Heartbeat:Connect(function(deltaTime)
angle += deltaTime -- advance smoothly with real frame time
local orbitPosition = center + Vector3.new(math.cos(angle) * radius, 0, math.sin(angle) * radius)
part.CFrame = CFrame.lookAt(orbitPosition, center) -- circle around, always facing inward
end)
Follow: glide toward a target instead of teleporting, using CFrame:Lerp(goal, alpha), which returns a CFrame alpha of the way (0 to 1) from the current one to the goal:
RunService.Heartbeat:Connect(function()
local goal = CFrame.lookAt(part.Position, targetPosition)
part.CFrame = part.CFrame:Lerp(goal, 0.1) -- 10% of the gap each frame
end)
Snap with easing: for one-off moves that should look smooth, loop a Lerp toward a fixed goal until the remaining distance is tiny. And if the target is a property like Position or Transparency rather than a whole CFrame, TweenService tweens parts declaratively, no loop required.
Use RunService.Heartbeat (server) or RunService.RenderStepped (client camera) for per-frame movement — never a while true do task.wait() loop, which is choppy and scheduler-dependent.
Common mistakes with Vector3 and CFrame
- Multiplying in the wrong order.
CFrame.new(offset) * baseandbase * CFrame.new(offset)are different transforms; if an offset ignores rotation (or respects it when you didn't want that), swap the order. - Passing degrees to CFrame.Angles. All angle-based CFrame constructors take radians — wrap degrees in
math.rad(...). - Teleporting instead of lerping. Assigning a far-away CFrame once per frame looks like jitter; interpolate with
:Lerp(goal, alpha)or a tween. - Moving unanchored parts. Physics owns them; anchor the part (or use constraints/body movers) before writing CFrame in a loop.
- Setting .Position when you meant .CFrame. Position changes keep the current rotation, which silently preserves the wrong facing; set the CFrame when orientation matters.
- Using Vector3 where a CFrame belongs.
part.CFrame = someVector3does not type-check conceptually: wrap it asCFrame.new(someVector3)— and remember a CFrame's position is.Position, not the CFrame itself. - Forgetting that Vector3 math creates new values.
local v = Vector3.new(0, 0, 0); v + Vector3.new(1, 0, 0)leavesvat zero; capture the result withv = v + Vector3.new(1, 0, 0).
Learn Luau by doing
Movement math only clicks once parts start moving because you told them to. On Luablox you can practice the language side on /theory, drill the tricky bits (rotation order, * vs +) on /problems, and follow structured Luablox lessons that build from variables up to full movement systems. Then combine this guide with tweening and raycasting to make parts that chase, orbit, and aim.
Frequently asked questions
What's the difference between Vector3 and CFrame?
A Vector3 is just three numbers (X, Y, Z): a position, an offset, or a direction. A CFrame is a position plus a rotation, so it can describe which way a part faces as well as where it is. Use Vector3 for distances, speeds, and offsets; use CFrame whenever orientation matters — part placement, facing a target, moving a model.
Why does my part rotate wrongly when I move it?
The two usual causes: passing degrees to CFrame.Angles or CFrame.fromEulerAnglesXYZ (they take radians — use math.rad), and Euler rotation order, which applies X then Y then Z, so combining tilts in a different sequence gives a different final orientation. Setting part.Position keeps the old rotation while setting part.CFrame replaces it, and multiplying in the wrong order applies an offset in world space instead of the part's own space.
How do I offset a part relative to another part?
Multiply the base CFrame by a local offset: base.CFrame * CFrame.new(x, y, z) places the offset in the base part's own rotated space (negative Z is in front, via LookVector), so it stays correct as the base turns. For an offset in world space, use base.CFrame.Position + Vector3.new(x, y, z) instead. Models use Model:PivotTo with the same multiplication to move as a unit.
Do CFrame.Angles and CFrame.fromEulerAnglesXYZ use degrees or radians?
Radians. Convert degrees with math.rad: CFrame.Angles(0, math.rad(90), 0) is a quarter turn around the Y axis. Feeding raw degrees works but produces wildly wrong angles (90 radians is about 5157 degrees), which is why parts seem to spin out of control.
How do I smoothly move a part to a new CFrame?
Interpolate with CFrame:Lerp: on each RunService.Heartbeat, set part.CFrame = part.CFrame:Lerp(goal, 0.1) so the part covers 10% of the remaining gap per frame. For one-shot moves, tween the CFrame with TweenService; for constant-speed motion, move along the direction's Unit vector scaled by speed each frame.
Master 3D spatial math in Roblox, including CFrames, Vectors, and coordinate system manipulation.