Skip to content

Instantly share code, notes, and snippets.

@nklbdev
Created October 5, 2025 11:16
Show Gist options
  • Select an option

  • Save nklbdev/ec5b39420aa68e40ac5106660b49e587 to your computer and use it in GitHub Desktop.

Select an option

Save nklbdev/ec5b39420aa68e40ac5106660b49e587 to your computer and use it in GitHub Desktop.
Blockbench WASD controls plugin for v5 with RMB and mouse lock
(() => {
const deletables = [];
let rightMouseDown = false;
BBPlugin.register("wasd_controls", {
title: "WASD Controls",
icon: "sports_esports",
author: "JannisX11, caioraphael1, nklbdev",
description: "Adds a WASD controlled viewport navigation mode",
about:
"The WASD mode can be enabled from the View menu.\nThe keys can be remapped in the keybindings menu.\nThe sensitivity can be changed in the settings under Preview, along with the movement plane.\nHold Control to move faster.",
version: "1.2.0",
min_version: "4.3.0",
variant: "both",
onload() {
const navigate_forward = new KeybindItem("navigate_forward", {
name: "Move Forward",
icon: "arrow_upward",
category: "navigate",
keybind: new Keybind({ key: "w", ctrl: null }),
});
const navigate_backward = new KeybindItem("navigate_backward", {
name: "Move Backward",
icon: "arrow_downward",
category: "navigate",
keybind: new Keybind({ key: "s", ctrl: null }),
});
const navigate_left = new KeybindItem("navigate_left", {
name: "Move Left",
icon: "arrow_back",
category: "navigate",
keybind: new Keybind({ key: "a", ctrl: null }),
});
const navigate_right = new KeybindItem("navigate_right", {
name: "Move Right",
icon: "arrow_forward",
category: "navigate",
keybind: new Keybind({ key: "d", ctrl: null }),
});
const navigate_down = new KeybindItem("navigate_down", {
name: "Move Down",
icon: "expand_more",
category: "navigate",
keybind: new Keybind({ key: 16, ctrl: null }),
});
const navigate_up = new KeybindItem("navigate_up", {
name: "Move Up",
icon: "expand_less",
category: "navigate",
keybind: new Keybind({ key: 32, ctrl: null }),
});
const navigate_faster = new KeybindItem("navigate_faster", {
name: "Move Faster",
icon: "expand_less",
category: "navigate",
keybind: new Keybind({ key: 17, ctrl: null }),
});
const navigate_slower = new KeybindItem("navigate_slower", {
name: "Move Slower",
icon: "expand_less",
category: "navigate",
keybind: new Keybind({ key: 18, ctrl: null }),
});
const navigation_keybinds = [
navigate_forward,
navigate_backward,
navigate_left,
navigate_right,
navigate_down,
navigate_up,
navigate_faster,
navigate_slower,
];
deletables.push(...navigation_keybinds);
function setupWASDMovement(preview, length = 1) {
const pos = new THREE.Vector3().copy(preview.camera.position);
pos.add(
preview.camera
.getWorldDirection(new THREE.Vector3())
.normalize()
.multiplyScalar(length),
);
preview.controls.target.copy(pos);
}
deletables.push(
new Setting("wasd_enabled", {
name: "WASD Controls: Enabled",
description: "_",
category: "preview",
value: false,
onChange(value) {
BarItems.wasd_movement.value = value;
BarItems.wasd_movement.updateEnabledState();
setupWASDMovement(Preview.selected, value ? 1 : 16);
},
}),
);
const wasd_toggle = new Toggle("wasd_movement", {
name: "WASD Movement",
icon: "sports_esports",
category: "navigate",
value: false,
onChange(value) {
settings.wasd_enabled.value = value;
Settings.saveLocalStorages();
setupWASDMovement(Preview.selected, value ? 1 : 16);
},
});
deletables.push(wasd_toggle);
MenuBar.menus.view.addAction("_");
MenuBar.menus.view.addAction(wasd_toggle);
BarItems.wasd_movement.value = settings.wasd_enabled.value;
BarItems.wasd_movement.updateEnabledState();
function isWASDMovementEnabled() {
return (
rightMouseDown && Preview.selected && BarItems.wasd_movement?.value
);
}
deletables.push(
new Setting("base_speed", {
name: "WASD Controls: Base Speed",
description: "-",
category: "preview",
type: "number",
value: 50,
min: 1,
}),
);
deletables.push(
new Setting("move_faster_mult", {
name: "WASD Controls: Move Faster Multiplier",
description: "-",
category: "preview",
type: "number",
value: 2,
max: 10,
min: 1,
}),
);
deletables.push(
new Setting("move_slower_mult", {
name: "WASD Controls: Move Slower Multiplier",
description: "-",
category: "preview",
type: "number",
value: 0.5,
max: 1,
min: 0.1,
}),
);
deletables.push(
new Setting("wasd_y_level", {
name: "WASD Controls: Navigate at Y Level",
description:
"Navigate using WASD at consistent Y level rather than on camera plane",
category: "preview",
value: true,
}),
);
deletables.push(
new Setting("mouse_sensitivity", {
name: "WASD Controls: Mouse Sensitivity",
description: "-",
category: "preview",
type: "number",
value: 0.005,
max: 0.01,
min: 0.001,
}),
);
// const maxPitch = 85 * (Math.PI / 180);
deletables.push(
new Setting("max_camera_pitch_degrees", {
name: "WASD Controls: Max Camera Pitch Angle (degrees)",
description: "-",
category: "preview",
type: "number",
value: 85,
max: 89,
min: 45,
}),
);
deletables.push(
new Setting("invert_pitch", {
name: "WASD Controls: Invert Pitch",
description: "-",
category: "preview",
value: true,
}),
);
const pressed_keys = [];
Blockbench.on("press_key", (data) => {
// TODO: Заменить which на использование актуального свойства
const key = data.event.which;
const is_keybind_found = navigation_keybinds.find(
(k) => k.keybind.key === key,
);
const is_any_text_input_focused = Boolean(getFocusedTextInput());
if (is_keybind_found && !is_any_text_input_focused) {
pressed_keys.safePush(key);
if (isWASDMovementEnabled()) data.capture();
}
});
document.addEventListener("keyup", (event) => {
// TODO: Заменить which на использование актуального свойства
pressed_keys.remove(event.which);
});
// event.button - индекс изменившей состояние кнопки
// { LEFT = 0, MIDDLE = 1, RIGHT = 2 }
// event.buttons - флаги нажатых в данный момент кнопок (не совпадают с индексами)
// { LEFT = 0001b (1), RIGHT = 0010b (2), MIDDLE = 0100b (4) }
function rightMouseDownUpdater(event) {
const oldRightMouseDown = rightMouseDown;
rightMouseDown = event.buttons & 2;
if (oldRightMouseDown !== rightMouseDown) {
if (rightMouseDown) document.body.requestPointerLock();
else document.exitPointerLock();
}
}
document.addEventListener("mousedown", rightMouseDownUpdater);
document.addEventListener("mouseup", rightMouseDownUpdater);
const vector_zero = new THREE.Vector3();
const targetDistance = 1.0; // Фиксированное расстояние до target
const temp_quaternion = new THREE.Quaternion();
const cameraForward = new THREE.Vector3();
const cameraRight = new THREE.Vector3();
document.addEventListener("mousemove", (event) => {
if (isWASDMovementEnabled()) {
const { camera, controls } = Preview.selected;
// Получаем текущее направление взгляда
cameraForward
.subVectors(controls.target, camera.position)
.normalize();
cameraRight
.crossVectors(camera.up, camera.getWorldDirection(vector_zero))
.normalize();
// Применяем вращения к направлению
const sensitivity = settings.mouse_sensitivity.value;
cameraForward.applyQuaternion(
temp_quaternion.setFromAxisAngle(
camera.up,
-event.movementX * sensitivity,
),
);
// Создаем кватернионы для вращения
cameraForward.applyQuaternion(
temp_quaternion.setFromAxisAngle(
cameraRight,
-event.movementY *
sensitivity *
(settings.invert_pitch.value ? -1 : 1),
),
);
// Вычисляем результирующий угол pitch и ограничиваем его
const resultPitch = Math.asin(cameraForward.y);
const maxPitch =
settings.max_camera_pitch_degrees.value * (Math.PI / 180);
// TODO: исправить ограничение налона
if (Math.abs(resultPitch) > maxPitch) {
// Просто ограничиваем Y-компоненту, сохраняя горизонтальное направление
cameraForward.y = Math.sign(resultPitch) * Math.sin(maxPitch);
cameraForward.normalize();
}
// Обновляем позицию target
controls.target.copy(camera.position);
controls.target.add(cameraForward.multiplyScalar(targetDistance));
// Заставляем камеру смотреть на новый target
camera.lookAt(controls.target);
controls.update();
}
});
function doWASDMovement() {
const movement = new THREE.Vector3(0, 0, 0);
let uses_wasd_movement = false;
function add(x, y, z) {
movement.x += x;
movement.y += y;
movement.z += z;
uses_wasd_movement = true;
}
if (pressed_keys.includes(navigate_forward.keybind.key)) add(0, 0, -1);
if (pressed_keys.includes(navigate_backward.keybind.key)) add(0, 0, 1);
if (pressed_keys.includes(navigate_left.keybind.key)) add(-1, 0, 0);
if (pressed_keys.includes(navigate_right.keybind.key)) add(1, 0, 0);
if (pressed_keys.includes(navigate_down.keybind.key)) add(0, -1, 0);
if (pressed_keys.includes(navigate_up.keybind.key)) add(0, 1, 0);
if (uses_wasd_movement) {
setupWASDMovement(Preview.selected);
let speedMultiplier = 1.0; // Default speed
if (pressed_keys.includes(navigate_faster.keybind.key))
speedMultiplier *= settings.move_faster_mult.value;
else if (pressed_keys.includes(navigate_slower.keybind.key))
speedMultiplier *= settings.move_slower_mult.value;
if (settings.wasd_y_level.value) {
const vec = Preview.selected.controls.object
.getWorldDirection(new THREE.Vector3())
.normalize();
movement.applyAxisAngle(THREE.NormalY, Math.atan2(-vec.x, -vec.z));
} else {
movement.applyEuler(Preview.selected.controls.object.rotation);
}
movement.multiplyScalar(
(Settings.get("base_speed") * speedMultiplier) / 100,
);
Preview.selected.camera.position.add(movement);
Preview.selected.controls.target.add(movement);
}
}
Blockbench.on("render_frame", () => {
if (isWASDMovementEnabled() && pressed_keys.length) doWASDMovement();
// Ensure zoom is disabled when moving
for (const preview of Preview.all)
preview.controls.enableZoom = !isWASDMovementEnabled();
});
},
oninstall() {
MenuBar.menus.view.highlight(BarItems.wasd_movement);
},
onunload() {
for (const action of deletables) action.delete();
setupWASDMovement(Preview.selected, 16);
for (const preview of Preview.all) preview.controls.enableZoom = true;
window.animate = animate_original;
},
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment