package leenkx.logicnode; import iron.math.Vec4; import iron.system.Input; import iron.object.Object; import kha.System; import kha.FastFloat; /** * MouseLookNode - FPS-style mouse look camera controller * * This node provides smooth, resolution-independent mouse look functionality for * first-person perspective controls. It supports separate body and head objects, * allowing for realistic FPS camera movement where the body rotates horizontally * and the head/camera rotates vertically. * * Key Features: * - Resolution-adaptive scaling for consistent feel across different screen sizes * - Configurable axis orientations (X, Y, Z as front) * - Optional mouse cursor locking and hiding * - Invertible X/Y axes * - Rotation capping/limiting for both horizontal and vertical movement * - Smoothing support for smoother camera movement * - Physics integration with automatic rigid body synchronization * - Support for both local and world space head rotation */ class MouseLookNode extends LogicNode { // Configuration properties (set from Blender node interface) public var property0: String; // Front axis: "X", "Y", or "Z" public var property1: Bool; // Hide Locked: auto-lock mouse cursor public var property2: Bool; // Invert X: invert horizontal mouse movement public var property3: Bool; // Invert Y: invert vertical mouse movement public var property4: Bool; // Cap Left/Right: limit horizontal rotation public var property5: Bool; // Cap Up/Down: limit vertical rotation public var property6: Bool; // Head Local Space: use local space for head rotation // Smoothing state variables - maintain previous frame values for interpolation var smoothX: Float = 0.0; // Smoothed horizontal mouse delta var smoothY: Float = 0.0; // Smoothed vertical mouse delta // Rotation limits (in radians) var maxHorizontal: Float = Math.PI; // Maximum horizontal rotation (180 degrees) var maxVertical: Float = Math.PI / 2; // Maximum vertical rotation (90 degrees) // Current rotation tracking for capping calculations var currentHorizontal: Float = 0.0; // Accumulated horizontal rotation var currentVertical: Float = 0.0; // Accumulated vertical rotation // Resolution scaling reference - base resolution for consistent sensitivity var baseResolutionWidth: Float = 1920.0; // Sensitivity scaling constants static inline var BASE_SCALE: Float = 1500.0; // Base sensitivity scale factor static var RADIAN_SCALING_FACTOR: Float = Math.PI * 50.0 / 180.0; // Degrees to radians conversion with sensitivity scaling public function new(tree: LogicTree) { super(tree); } /** * Main execution function called every frame when the node is active * * Input connections: * [0] - Action trigger (not used in current implementation) * [1] - Body Object: the main object that rotates horizontally * [2] - Head Object: optional object that rotates vertically (typically camera) * [3] - Sensitivity: mouse sensitivity multiplier * [4] - Smoothing: movement smoothing factor (0.0 = no smoothing, 0.99 = maximum smoothing) */ override function run(from: Int) { // Get input values from connected nodes var bodyObject: Object = inputs[1].get(); var headObject: Object = inputs[2].get(); var sensitivity: FastFloat = inputs[3].get(); var smoothing: FastFloat = inputs[4].get(); // Early exit if no body object is provided if (bodyObject == null) { runOutput(0); return; } // Get mouse input state var mouse = Input.getMouse(); // Handle automatic mouse cursor locking for FPS controls if (property1) { if (mouse.started() && !mouse.locked) { mouse.lock(); // Center and hide cursor, enable unlimited movement } } // Only process mouse look when cursor is locked or mouse button is held // This prevents unwanted camera movement when UI elements are being used if (!mouse.locked && !mouse.down()) { runOutput(0); return; } // Get raw mouse movement delta (pixels moved since last frame) var deltaX: Float = mouse.movementX; var deltaY: Float = mouse.movementY; // Apply axis inversion if configured if (property2) deltaX = -deltaX; // Invert horizontal movement if (property3) deltaY = -deltaY; // Invert vertical movement // Calculate resolution-adaptive scaling to maintain consistent sensitivity // across different screen resolutions. Higher resolutions will have proportionally // higher scaling to compensate for increased pixel density. var resolutionMultiplier: Float = System.windowWidth() / baseResolutionWidth; // Apply movement smoothing if enabled // This creates a weighted average between current and previous movement values // to reduce jittery camera movement, especially useful for low framerates if (smoothing > 0.0) { var smoothingFactor: Float = Math.min(smoothing, 0.99); // Cap smoothing to prevent complete freeze smoothX = smoothX * smoothingFactor + deltaX * (1.0 - smoothingFactor); smoothY = smoothY * smoothingFactor + deltaY * (1.0 - smoothingFactor); deltaX = smoothX; deltaY = smoothY; } // Define rotation axes based on the configured front axis // These determine which 3D axes are used for horizontal and vertical rotation var horizontalAxis = new Vec4(); // Axis for left/right body rotation var verticalAxis = new Vec4(); // Axis for up/down head rotation switch (property0) { case "X": // X-axis forward (e.g., for side-scrolling or specific orientations) horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation verticalAxis.set(0, 1, 0); // Y-axis for vertical rotation case "Y": // Y-axis forward (most common for 3D games) #if lnx_yaxisup // Y-up coordinate system (Blender default) horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation verticalAxis.set(1, 0, 0); // X-axis for vertical rotation #else // Z-up coordinate system horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation verticalAxis.set(1, 0, 0); // X-axis for vertical rotation #end case "Z": // Z-axis forward (top-down or specific orientations) horizontalAxis.set(0, 1, 0); // Y-axis for horizontal rotation verticalAxis.set(1, 0, 0); // X-axis for vertical rotation } // Calculate final sensitivity scaling combining base scale and resolution adaptation var finalScale: Float = BASE_SCALE * resolutionMultiplier; // Apply user-defined sensitivity multiplier deltaX *= sensitivity; deltaY *= sensitivity; // Convert pixel movement to rotation angles (radians) // Negative values ensure natural movement direction (moving mouse right rotates right) var horizontalRotation: Float = (-deltaX / finalScale) * RADIAN_SCALING_FACTOR; var verticalRotation: Float = (-deltaY / finalScale) * RADIAN_SCALING_FACTOR; // Apply horizontal rotation capping if enabled // This prevents the character from rotating beyond specified limits if (property4) { currentHorizontal += horizontalRotation; // Clamp rotation to maximum horizontal range and adjust current frame rotation if (currentHorizontal > maxHorizontal) { horizontalRotation -= (currentHorizontal - maxHorizontal); currentHorizontal = maxHorizontal; } else if (currentHorizontal < -maxHorizontal) { horizontalRotation -= (currentHorizontal + maxHorizontal); currentHorizontal = -maxHorizontal; } } // Apply vertical rotation capping if enabled // This prevents looking too far up or down (like human neck limitations) if (property5) { currentVertical += verticalRotation; // Clamp rotation to maximum vertical range and adjust current frame rotation if (currentVertical > maxVertical) { verticalRotation -= (currentVertical - maxVertical); currentVertical = maxVertical; } else if (currentVertical < -maxVertical) { verticalRotation -= (currentVertical + maxVertical); currentVertical = -maxVertical; } } // Apply horizontal rotation to body object (character turning left/right) if (horizontalRotation != 0.0) { bodyObject.transform.rotate(horizontalAxis, horizontalRotation); // Synchronize physics rigid body if present // This ensures physics simulation stays in sync with visual transform #if lnx_physics var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); if (rigidBody != null) rigidBody.syncTransform(); #end } // Apply vertical rotation to head object (camera looking up/down) if (headObject != null && verticalRotation != 0.0) { if (property6) { // Local space rotation - recommended when head is a child of body // This prevents gimbal lock and rotation inheritance issues headObject.transform.rotate(verticalAxis, verticalRotation); } else { // World space rotation - uses head object's current right vector // More accurate for independent head objects but can cause issues with parenting var headVerticalAxis = headObject.transform.world.right(); headObject.transform.rotate(headVerticalAxis, verticalRotation); } // Synchronize head physics rigid body if present #if lnx_physics var headRigidBody = headObject.getTrait(leenkx.trait.physics.RigidBody); if (headRigidBody != null) headRigidBody.syncTransform(); #end } else if (headObject == null && verticalRotation != 0.0) { // Fallback: if no separate head object, apply vertical rotation to body // This creates a simpler single-object camera control bodyObject.transform.rotate(verticalAxis, verticalRotation); // Synchronize body physics rigid body #if lnx_physics var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); if (rigidBody != null) rigidBody.syncTransform(); #end } // Continue to next connected node in the logic tree runOutput(0); } }