Hacer crecer un bloque desde una cara.
Resumen. La herramienta de escalar (Z) pone seis handles en el bloque seleccionado, uno por cara. Arrastrar un handle hace crecer o encoger el bloque solo hacia esa cara, y la cara opuesta se queda donde estaba. Roblox cambia el tamaño de las partes alrededor de su centro, así que cada cambio de tamaño tiene que ir acompañado de un corrimiento del bloque igual a la mitad de ese cambio, hacia la cara, en los ejes del propio bloque. Este artículo recorre ese corrimiento y por qué deja fija la cara opuesta, el truco que usé para sacar una dirección por cara, la fórmula de ajuste (que mezcla unidades y solo se porta bien si arrastras despacio), los campos donde se puede escribir un tamaño, lo que llega al servidor y la primera versión, que trataba tres de las seis caras como si apuntaran al revés. El script actual tiene una nota fechada el 15 de mayo de 2022 que dice que reemplazó a esa versión y que fue hecho “totalmente desde 0”.
1. El problema
Una parte de Roblox es una caja con un centro, una rotación y un tamaño. Cambiar Size deja el centro donde está, así que la caja crece lo mismo a los dos lados de cada eje. En una herramienta de construcción eso casi nunca es lo que uno quiere. Cuando estiras hacia arriba la cara superior de una pared, esperas que la de abajo se quede en el piso.
La herramienta usa los Handles de Roblox en modo de redimensionar. Reportan qué cara (NormalId) se está arrastrando y cuánto se ha movido el mouse a lo largo de la normal de esa cara, en studs, desde que se presionó el botón. Con esos dos valores la herramienta tiene que sacar un tamaño y una posición nuevos, ajustados a un incremento que elige el jugador, y tiene que funcionar en bloques rotados.
2. El corrimiento a la mitad
Toma un eje del bloque, con dirección unitaria en el espacio del mundo, centro y tamaño a lo largo de ese eje. Sus dos caras están en
Si la cara que se arrastra es y el tamaño cambia en , el tamaño nuevo es , y el centro se corre la mitad de eso hacia la cara arrastrada:
Entonces la cara opuesta queda en
o sea que no se movió, mientras la cara arrastrada se movió el completo. Lo mismo vale al encoger, con , y para la otra cara, con en lugar de . El código hace exactamente esto en cada evento de arrastre. Esta es la rama de la cara derecha:
local cf = part.CFrame * CFrame.fromAxisAngle(Vector3.new(0, 1, 0), -math.pi/2)
local lv = cf.LookVector
local cf2 = part.CFrame
local size = part.Size
part.Size = Vector3.new(part.Size.X + NewDelta, part.Size.Y, part.Size.Z)
if part.Size == size then
part.CFrame = cf2 + lv * (0.5*NewDelta)
cf2 = part.CFrame
end
part.CFrame = cf2 + lv * (0.5*NewDelta)
lv es , NewDelta es y la última línea es el corrimiento. Como el CFrame se asigna a partir de una copia guardada antes de tocar Size, la posición siempre sale de la fórmula y no de lo que Roblox haya hecho con la parte al cambiarle el tamaño.
3. Una dirección por cara
La fórmula necesita , la dirección de la cara arrastrada en el espacio del mundo, que depende de cómo esté rotado el bloque. El código saca las seis de LookVector, que apunta hacia afuera de la cara frontal. Para cada una de las otras caras rota el CFrame del bloque hasta que su frente apunte a donde necesita, y lee el LookVector de eso:
| Cara | Rotación aplicada al CFrame del bloque |
Equivale a |
|---|---|---|
| Frontal | ninguna | LookVector |
| Trasera | alrededor de Y | -LookVector |
| Derecha | alrededor de Y | RightVector |
| Izquierda | alrededor de Y | -RightVector |
| Superior | alrededor de X | UpVector |
| Inferior | alrededor de X | -UpVector |
Es correcto. Una rotación manda la dirección frontal a , que para es , la dirección derecha. Como la rotación se multiplica a la derecha del CFrame del bloque, ocurre en los ejes del propio bloque, así que lo que sale es la dirección derecha del bloque en el espacio del mundo. Revisé las seis contra un bloque con una rotación arbitraria y coinciden hasta la precisión de los floats.
Las seis también son cf:VectorToWorldSpace(Vector3.FromNormalId(face)), una sola línea para todas las caras, y la herramienta de mover, de la misma época, ya usaba exactamente esa expresión. No conecté una cosa con la otra, y eso explica en parte por qué el script de escalar tiene seis copias del mismo bloque de unas 50 líneas, una por cara, que solo cambian en la rotación y en qué componente de Size tocan.
4. El ajuste al incremento
4.1 La fórmula
Cada cara guarda un acumulador con lo que ya aplicó desde que se presionó el botón. Si es la distancia que reporta el handle, es cuánto va el mouse por delante del bloque. Cuando el bloque crece, con incremento , el paso es
y al encoger es lo mismo con cuando . El primer caso multiplica una cantidad de studs por otra cantidad de studs. Con incrementos pequeños eso hace que el bloque se quede atrás del mouse, y con incrementos grandes saltaría studs de golpe, 16 por paso con un incremento de 4. El segundo caso, un if i >= 2 then NewDelta /= i, tiene toda la pinta de ser un parche para los grandes: se vuelve a dividir por el incremento y los saltos desaparecen.
4.2 Por qué parecía funcionar
Con el incremento por defecto de 0.5 y un arrastre suave, la fórmula se porta bien. El bloque solo se mueve cuando el mouse va por lo menos un stud adelante, donde , así que cada paso suma exactamente y el bloque sigue al mouse entre medio stud y un stud por detrás. Ese es el caso con el que lo probaba. Los problemas aparecen con otros incrementos o con un mouse rápido. Reescribí la rama en un script pequeño y simulé dos arrastres: uno lento, en el que el mouse avanza 0.05 studs por evento a lo largo de 10 studs, y uno rápido, de 3 studs por evento a lo largo de 12.
| Incremento | Arrastre lento: creció | Arrastre lento: retraso máximo | Arrastre rápido: creció |
|---|---|---|---|
| 0.01 | 5.31 | 4.69 | 0.27 |
| 0.25 | 9.25 | 1 | 5.75 |
| 0.5 | 9.5 | 1 | 9 |
| 1 | 10 | 1 | 12 |
| 2 | 10 | 2 | 12 |
| 4 | 8 | 4 | 12 |
Por debajo de un incremento de 1, un evento puede sumar como mucho , así que la velocidad del bloque queda limitada por qué tan seguido se dispara el evento. Con 0.01 el bloque crece una centésima de stud por evento y termina 4.69 studs atrás en el arrastre lento, y en el rápido casi no se mueve. Escribir “0” en el campo de incremento elige justo 0.01, así que la herramienta tenía un modo lento de precisión por accidente. De 2 para arriba, el incremento es solo un umbral. El bloque espera a que el mouse vaya más de studs adelante y entonces suma studs enteros, el número que sea, y en el arrastre rápido con incremento 4 los pasos fueron de 6 studs.
La fórmula que buscaba es la que ya usaba la herramienta de mover:
que siempre avanza en múltiplos de , nunca se queda más de atrás y no depende de qué tan rápido vaya el mouse.
4.3 El tamaño mínimo
Cuando encoger dejaría el bloque por debajo del incremento, la herramienta pone , así que el bloque queda exactamente de studs de grosor y el incremento funciona también como tamaño mínimo. El campo acepta de 0.01 a 4 studs. La condición que dispara ese límite compara con , mezclando otra vez la distancia cruda con la escalada, así que puede activarse un poco antes o un poco después de la cuenta.
4.4 Cuando el tamaño no cambia
El if part.Size == size del fragmento atrapa el caso en que asignar Size no cambió nada, por ejemplo en el límite de tamaño de Roblox. Ahí el corrimiento corre dos veces y el bloque se mueve el completo. El handle sigue al mouse, pero el bloque se desliza en vez de crecer.
5. Escribir un tamaño
La ventana muestra el tamaño en tres campos, redondeado a tres decimales con Tena.DecimalRedux, que es un string.format("%.3f"). Al escribir un número se filtra todo lo que no sea dígito o punto, se asigna ese componente de Size y se manda el bloque al servidor. Ese camino no corre el bloque, así que un tamaño escrito crece desde el centro, simétrico, mientras que uno arrastrado crece desde una cara. Son dos operaciones distintas, y la ventana no te dice cuál te va a tocar.
6. Lo que llega al servidor
Mientras arrastras, todos los cambios son locales. Cuando sueltas el botón, la herramienta dispara ScaleRep una vez con el bloque, su tamaño y su CFrame, y el servidor, después de las revisiones que describe el resumen general, asigna Size y después CFrame. El orden importa por la misma razón que en el cliente: el CFrame es el que calculó la fórmula, así que tiene la última palabra sobre la posición. El servidor no mira los valores, así que acepta cualquier tamaño que mande el cliente.
7. La primera versión
La versión que escribí como juank sigue en el place, desactivada. Calculaba todo a partir del estado del momento en que se presionó el botón, que es buena idea, con esto:
donde es Vector3.FromNormalId(face). Para las caras derecha, superior y trasera la normal es positiva y el bloque crecía. Para la izquierda, la inferior y la frontal es , y , así que jalar esas caras hacia afuera achicaba el bloque. Después la posición se reconstruía con CFrame.new(x, y, z), que no lleva rotación, a partir de desplazamientos como cframe.X - size.X/2 tomados sobre los ejes del mundo. En un bloque que no estuviera alineado con el mundo, escalar le borraba la rotación y lo ubicaba con los ejes equivocados. Disparaba el remote en cada evento de arrastre, y su tamaño máximo era math.huge / 8, que sigue siendo infinito. La reescritura arregló los tres problemas de verdad: una dirección por cara en los ejes del bloque, el corrimiento a la mitad y un solo mensaje al soltar.
8. Limitaciones
- Seis copias del mismo bloque, que es justo como un arreglo en una cara se le escapa a las otras cinco.
- La fórmula de ajuste de la sección 4. Como cada evento suma un paso encima del tamaño actual, el resultado depende de cómo se movió el mouse y no solo de dónde terminó. Calcular desde el estado del momento de presionar, como la primera versión y la herramienta de mover, evita eso.
- El servidor confía en el tamaño y el
CFrameque recibe. - El incremento lo lee una corrutina que revisa el campo de texto en cada frame en vez de reaccionar cuando cambia.
9. Lo que me dejó
El corrimiento a la mitad es lo que hizo funcionar la herramienta, y sigo creyendo que es la idea correcta: cambiar el tamaño y luego correr el centro la mitad de ese cambio hacia la cara. Lo que me faltaba era vocabulario. Saqué las seis direcciones rotando un CFrame hasta que su frente apuntara a donde necesitaba, cuando el bloque ya las tenía como RightVector y UpVector, y ajusté con una fórmula que multiplicaba studs por studs y la parché cuando se rompió. Las dos cosas funcionan en el caso que probaba, el incremento por defecto y una mano tranquila, y la simulación muestra lo angosto que es ese caso.
Este texto de Mastedore. Todos los derechos reservados.