< Back 

Blocks Simulator: Move

2021-12-22

The move tool slides a block along its own axes in snapped steps of up to 255 studs. This article covers the one line that does it, why rounding up makes forward and backward asymmetric, a bug in the position fields, and what the May 2022 rewrite changed.

LuauRobloxGameplayMath
Go to quick view

One line that moves a block along its own axes.

Abstract. The move tool (X) puts six arrows on the selected block, one per face, along the block’s own axes. Dragging one slides the block in that direction in steps of a configurable increment, from 0.01 to 255 studs. All of the math is one line: it takes the block’s CFrame from the moment the button went down, turns the dragged face’s normal into world space and adds it, scaled by the snapped distance. This article explains that line, the asymmetry that comes from snapping with math.ceil, the three position fields and a bug that sent rotated blocks somewhere else when you typed a coordinate, what goes to the server, and what changed in the rewrite dated 22 May 2022, which kept the line and replaced almost everything around it.

1. The problem

The tool uses Handles in movement style. As in the scale tool, they report the face being dragged and how far the mouse has moved along its normal since the button went down. The block has to move that far in that direction, snapped to the increment, and the direction has to be the block’s and not the world’s, because a wall rotated 30 degrees should slide along itself.

2. The line

bloque.CFrame = cframe + cframe:VectorToWorldSpace(Vector3.FromNormalId(lado) * (math.ceil(posicion/incrementar) * incrementar))

cframe is the block’s CFrame saved when the button went down, lado the face and posicion the distance. With 𝐩0 and R0 the block’s position and rotation at that moment, nˆ the face’s normal in the block’s own axes, d the distance and i the increment, the line is

𝐩′=𝐩0+R0nˆi⌈di⌉

Vector3.FromNormalId gives nˆ, a unit vector along one of the block’s axes, for example (0,1,0) for the top. VectorToWorldSpace multiplies it by the rotation only, without the translation, which turns a direction in the block’s axes into the same direction in the world. And adding a Vector3 to a CFrame moves it without touching its rotation, so the block slides along its own axis and keeps facing the same way.

Computing from the state at mouse-down, instead of adding a little on every event, gives the line two properties. The result depends only on how far the mouse is at that moment, not on how it got there, so a fast drag and a slow one end in the same place, which is exactly what the scale tool lacks. And no error builds up over a long drag.

The line is the same in the first version, the one I wrote as juank, and in the rewrite.

3. Rounding up

The snapping uses math.ceil, so the snapped distance is

q(d)=i⌈di⌉,q(d)−d∈[0,i)

It’s never behind the mouse and it can be up to a whole increment ahead. That has a visible effect. As soon as you drag an arrow outwards by any amount, even a hundredth of a stud, the block jumps a whole increment, because ⌈0.01/i⌉=1. Dragging back towards the block does the opposite: for d∈(−i,0] the ceiling is 0, so you have to pull back a whole increment before the block moves at all. With the default 0.5 you hardly notice, but at 50 studs the block jumps the moment you touch the arrow.

Rounding to the nearest keeps the error within half an increment on both sides:

q(d)=i⌊di+12⌋,q(d)−d∈[−i2,i2)

and it’s what the rotate tool does with its angle. The build tool snaps its grid with this same increment and the same ceil, and right next to it there’s a commented-out i * math.round(integer), so I did consider it at some point.

The increment field takes values up to 255 studs. Leaving it empty means 0.5, and typing “0” means 0.01.

4. The position fields

The window shows the block’s position in three fields, and typing in one of them should move the block to that coordinate. For the X field the code does this:

obj.Position = Vector3.new(tonumber(x.Text), obj.CFrame.Y, obj.CFrame.Z)
local cfX = Vector3.new(tonumber(x.Text), obj.CFrame.Y, obj.CFrame.Z)
obj.Position = obj.CFrame:VectorToWorldSpace(cfX)
evento:FireServer(flechas.Adornee,flechas.Adornee.CFrame)

The first line is right. The third takes that position, a point in world space, and passes it through VectorToWorldSpace, which rotates it by the block’s rotation around the world’s origin. On a block with no rotation, the rotation is the identity and nothing happens, which is probably why I didn’t notice. On a block rotated 90° around Y at (10,5,0), typing X = 10 sends it to

Ry(90°)(10,5,0)=(0,5,−10)

more than 14 studs from where it was. The Y and Z fields have the same lines. The fix is to delete the second and third.

The clear button (the x) next to each field has a problem of its own. It sends the server CFrame.new(0, y, z), which has that position and no rotation, so clearing a coordinate also straightens the block on the server. And since it only fires the remote without changing anything locally, the player sees nothing until replication brings the server’s version back.

5. What reaches the server

On release, the tool fires MoveRep once with the block and its new CFrame, and the server assigns it after the checks described in the overview. The whole CFrame travels, twelve numbers, when only the position changed. That costs a few bytes, but the rotation goes along with it, so the server can’t end up with a different one. The server doesn’t check how far the block moved.

6. What the rewrite changed

The rewrite’s note says it replaced the old version with one “with fewer errors and partially made from scratch”. The line from section 2 survived. Around it, the first version was a Roblox Tool. Equipping it connected the mouse’s Move and Idle events again every time, and unequipping never disconnected them, so they piled up. It fired the remote on every drag event. And it had what looks like a piece of the build tool pasted in: every click anywhere connected a listener to the mouse’s X coordinate that set the last selected block’s Position to the grid cell under the cursor. After clicking anywhere, moving the mouse dragged that block along, and every click added another listener. The rewrite removed all that, sends one message on release, and lives in a window that Transform.exe starts and stops.

7. Limitations

  • ceil instead of rounding, from section 3.
  • The position fields rotate the coordinate you type, and the clear buttons drop the rotation.
  • The server accepts any CFrame, so a modified client can send a block anywhere, rotation included.

8. What I took away

The line in section 2 is the best piece of code in this tool, and what makes it good is that it’s absolute: everything is computed from where the block was when you pressed. The bug in the fields came from using a function I had seen work without knowing what it did. VectorToWorldSpace rotates without translating, which is what a direction needs. A position the player typed in world space needed neither; it only had to be assigned.

This text by Mastedore. All rights reserved.