fixed properties numbering, comments and LNXfactor to LNXFloat

This commit is contained in:
2025-07-08 22:48:16 +02:00
parent e234c8615c
commit 57f0e937d0
2 changed files with 227 additions and 90 deletions

View File

@ -6,102 +6,162 @@ import iron.object.Object;
import kha.System; import kha.System;
import kha.FastFloat; 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 { class MouseLookNode extends LogicNode {
public var property0: String; // Configuration properties (set from Blender node interface)
public var property1: Bool; public var property0: String; // Front axis: "X", "Y", or "Z"
public var property2: Bool; public var property1: Bool; // Hide Locked: auto-lock mouse cursor
public var property3: Bool; public var property2: Bool; // Invert X: invert horizontal mouse movement
public var property4: Bool; public var property3: Bool; // Invert Y: invert vertical mouse movement
public var property5: Bool; public var property4: Bool; // Cap Left/Right: limit horizontal rotation
public var property7: Bool; public var property5: Bool; // Cap Up/Down: limit vertical rotation
public var property6: Bool; // Head Local Space: use local space for head rotation
var smoothX: Float = 0.0; // Smoothing state variables - maintain previous frame values for interpolation
var smoothY: Float = 0.0; var smoothX: Float = 0.0; // Smoothed horizontal mouse delta
var maxHorizontal: Float = Math.PI; var smoothY: Float = 0.0; // Smoothed vertical mouse delta
var maxVertical: Float = Math.PI / 2;
var currentHorizontal: Float = 0.0; // Rotation limits (in radians)
var currentVertical: Float = 0.0; 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; var baseResolutionWidth: Float = 1920.0;
static inline var BASE_SCALE: Float = 1500.0; // Sensitivity scaling constants
static var RADIAN_SCALING_FACTOR: Float = Math.PI * 50.0 / 180.0; 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) { public function new(tree: LogicTree) {
super(tree); 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) { override function run(from: Int) {
// Get input values from connected nodes
var bodyObject: Object = inputs[1].get(); var bodyObject: Object = inputs[1].get();
var headObject: Object = inputs[2].get(); var headObject: Object = inputs[2].get();
var sensitivity: FastFloat = inputs[3].get(); var sensitivity: FastFloat = inputs[3].get();
var smoothing: FastFloat = inputs[4].get(); var smoothing: FastFloat = inputs[4].get();
// Early exit if no body object is provided
if (bodyObject == null) { if (bodyObject == null) {
runOutput(0); runOutput(0);
return; return;
} }
// Get mouse input state
var mouse = Input.getMouse(); var mouse = Input.getMouse();
// Handle automatic mouse cursor locking for FPS controls
if (property1) { if (property1) {
if (mouse.started() && !mouse.locked) { if (mouse.started() && !mouse.locked) {
mouse.lock(); 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()) { if (!mouse.locked && !mouse.down()) {
runOutput(0); runOutput(0);
return; return;
} }
// Get raw mouse movement delta (pixels moved since last frame)
var deltaX: Float = mouse.movementX; var deltaX: Float = mouse.movementX;
var deltaY: Float = mouse.movementY; var deltaY: Float = mouse.movementY;
if (property2) deltaX = -deltaX; // Apply axis inversion if configured
if (property3) deltaY = -deltaY; if (property2) deltaX = -deltaX; // Invert horizontal movement
if (property3) deltaY = -deltaY; // Invert vertical movement
// Always apply resolution-adaptive scaling // 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; 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) { if (smoothing > 0.0) {
var smoothingFactor: Float = Math.min(smoothing, 0.99); var smoothingFactor: Float = Math.min(smoothing, 0.99); // Cap smoothing to prevent complete freeze
smoothX = smoothX * smoothingFactor + deltaX * (1.0 - smoothingFactor); smoothX = smoothX * smoothingFactor + deltaX * (1.0 - smoothingFactor);
smoothY = smoothY * smoothingFactor + deltaY * (1.0 - smoothingFactor); smoothY = smoothY * smoothingFactor + deltaY * (1.0 - smoothingFactor);
deltaX = smoothX; deltaX = smoothX;
deltaY = smoothY; deltaY = smoothY;
} }
var horizontalAxis = new Vec4(); // Define rotation axes based on the configured front axis
var verticalAxis = new Vec4(); // 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) { switch (property0) {
case "X": case "X": // X-axis forward (e.g., for side-scrolling or specific orientations)
horizontalAxis.set(0, 0, 1); horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(0, 1, 0); verticalAxis.set(0, 1, 0); // Y-axis for vertical rotation
case "Y": case "Y": // Y-axis forward (most common for 3D games)
#if lnx_yaxisup #if lnx_yaxisup
horizontalAxis.set(0, 0, 1); // Y-up coordinate system (Blender default)
verticalAxis.set(1, 0, 0); horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
#else #else
horizontalAxis.set(0, 0, 1); // Z-up coordinate system
verticalAxis.set(1, 0, 0); horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
#end #end
case "Z": case "Z": // Z-axis forward (top-down or specific orientations)
horizontalAxis.set(0, 1, 0); horizontalAxis.set(0, 1, 0); // Y-axis for horizontal rotation
verticalAxis.set(1, 0, 0); verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
} }
// Always apply resolution-adaptive scaling // Calculate final sensitivity scaling combining base scale and resolution adaptation
var finalScale: Float = BASE_SCALE * resolutionMultiplier; var finalScale: Float = BASE_SCALE * resolutionMultiplier;
// Apply user-defined sensitivity multiplier
deltaX *= sensitivity; deltaX *= sensitivity;
deltaY *= 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 horizontalRotation: Float = (-deltaX / finalScale) * RADIAN_SCALING_FACTOR;
var verticalRotation: Float = (-deltaY / 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) { if (property4) {
currentHorizontal += horizontalRotation; currentHorizontal += horizontalRotation;
// Clamp rotation to maximum horizontal range and adjust current frame rotation
if (currentHorizontal > maxHorizontal) { if (currentHorizontal > maxHorizontal) {
horizontalRotation -= (currentHorizontal - maxHorizontal); horizontalRotation -= (currentHorizontal - maxHorizontal);
currentHorizontal = maxHorizontal; currentHorizontal = maxHorizontal;
@ -111,8 +171,11 @@ class MouseLookNode extends LogicNode {
} }
} }
// Apply vertical rotation capping if enabled
// This prevents looking too far up or down (like human neck limitations)
if (property5) { if (property5) {
currentVertical += verticalRotation; currentVertical += verticalRotation;
// Clamp rotation to maximum vertical range and adjust current frame rotation
if (currentVertical > maxVertical) { if (currentVertical > maxVertical) {
verticalRotation -= (currentVertical - maxVertical); verticalRotation -= (currentVertical - maxVertical);
currentVertical = maxVertical; currentVertical = maxVertical;
@ -122,38 +185,49 @@ class MouseLookNode extends LogicNode {
} }
} }
// Apply horizontal rotation to body object (character turning left/right)
if (horizontalRotation != 0.0) { if (horizontalRotation != 0.0) {
bodyObject.transform.rotate(horizontalAxis, horizontalRotation); bodyObject.transform.rotate(horizontalAxis, horizontalRotation);
// Synchronize physics rigid body if present
// This ensures physics simulation stays in sync with visual transform
#if lnx_physics #if lnx_physics
var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody);
if (rigidBody != null) rigidBody.syncTransform(); if (rigidBody != null) rigidBody.syncTransform();
#end #end
} }
// Apply vertical rotation to head object (camera looking up/down)
if (headObject != null && verticalRotation != 0.0) { if (headObject != null && verticalRotation != 0.0) {
if (property7) { if (property6) {
// Local space rotation (recommended for FPS) // Local space rotation - recommended when head is a child of body
// This prevents gimbal lock and rotation inheritance issues
headObject.transform.rotate(verticalAxis, verticalRotation); headObject.transform.rotate(verticalAxis, verticalRotation);
} else { } else {
// World space rotation // 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(); var headVerticalAxis = headObject.transform.world.right();
headObject.transform.rotate(headVerticalAxis, verticalRotation); headObject.transform.rotate(headVerticalAxis, verticalRotation);
} }
// Synchronize head physics rigid body if present
#if lnx_physics #if lnx_physics
var headRigidBody = headObject.getTrait(leenkx.trait.physics.RigidBody); var headRigidBody = headObject.getTrait(leenkx.trait.physics.RigidBody);
if (headRigidBody != null) headRigidBody.syncTransform(); if (headRigidBody != null) headRigidBody.syncTransform();
#end #end
} else if (headObject == null && verticalRotation != 0.0) { } 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); bodyObject.transform.rotate(verticalAxis, verticalRotation);
// Synchronize body physics rigid body
#if lnx_physics #if lnx_physics
var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody); var rigidBody = bodyObject.getTrait(leenkx.trait.physics.RigidBody);
if (rigidBody != null) rigidBody.syncTransform(); if (rigidBody != null) rigidBody.syncTransform();
#end #end
} }
// Continue to next connected node in the logic tree
runOutput(0); runOutput(0);
} }
} }

View File

@ -2,95 +2,158 @@ from lnx.logicnode.lnx_nodes import *
class MouseLookNode(LnxLogicTreeNode): class MouseLookNode(LnxLogicTreeNode):
"""Controls object rotation based on mouse movement for FPS-style camera control. """MouseLookNode - Blender UI interface for FPS-style mouse look camera controller
This class defines the Blender node interface for the MouseLookNode logic node.
It creates the visual node that appears in Blender's logic tree editor and
defines all the properties that configure the mouse look behavior.
The node provides controls for:
- Axis orientation configuration
- Mouse cursor behavior
- Movement inversion options
- Rotation limiting/capping
- Head rotation space behavior
Features: Features:
- Built-in resolution-adaptive scaling for consistent feel across different screen resolutions - Built-in resolution-adaptive scaling for consistent feel across different screen resolutions
- Automatic physics synchronization for rigid bodies
- Support for both single-object and dual-object (body/head) setups
""" """
bl_idname = 'LNMouseLookNode'
bl_label = 'Mouse Look' # Blender node identification
lnx_section = 'mouse' bl_idname = 'LNMouseLookNode' # Unique identifier for Blender's node system
lnx_version = 1 bl_label = 'Mouse Look' # Display name in node menu and header
lnx_section = 'mouse' # Category section in node add menu
lnx_version = 1 # Node version for compatibility tracking
# Front axis property # Property 0: Front Axis Configuration
# Determines which 3D axis represents the "forward" direction of the character/camera
# This affects how horizontal and vertical rotations are applied to the objects
property0: HaxeEnumProperty( property0: HaxeEnumProperty(
'property0', 'property0',
items=[('X', 'X Axis', 'X Axis as front'), items=[('X', 'X Axis', 'X Axis as front'), # X-forward (side-scrolling, specific orientations)
('Y', 'Y Axis', 'Y Axis as front'), ('Y', 'Y Axis', 'Y Axis as front'), # Y-forward (most common for 3D games)
('Z', 'Z Axis', 'Z Axis as front')], ('Z', 'Z Axis', 'Z Axis as front')], # Z-forward (top-down, specific orientations)
name='Front', default='Y') name='Front',
default='Y') # Y-axis is default as it's most common in 3D game development
# Hide Locked property # Property 1: Automatic Mouse Cursor Management
# When enabled, automatically centers and locks the mouse cursor when mouse input starts
# This is essential for FPS games to prevent cursor from leaving game window
property1: HaxeBoolProperty( property1: HaxeBoolProperty(
'property1', 'property1',
name='Hide Locked', name='Hide Locked',
description='Automatically center and lock the mouse cursor', description='Automatically center and lock the mouse cursor when mouse input begins',
default=True) default=True) # Enabled by default for typical FPS behavior
# Invert X property # Property 2: Horizontal Movement Inversion
# Allows users to invert horizontal mouse movement (left becomes right, right becomes left)
# Some players prefer inverted controls for consistency with flight simulators
property2: HaxeBoolProperty( property2: HaxeBoolProperty(
'property2', 'property2',
name='Invert X', name='Invert X',
description='Invert horizontal mouse movement', description='Invert horizontal mouse movement - moving mouse right turns character left',
default=False) default=False) # Most players expect non-inverted horizontal movement
# Invert Y property # Property 3: Vertical Movement Inversion
# Allows users to invert vertical mouse movement (up becomes down, down becomes up)
# More commonly used than horizontal inversion, especially by flight sim players
property3: HaxeBoolProperty( property3: HaxeBoolProperty(
'property3', 'property3',
name='Invert Y', name='Invert Y',
description='Invert vertical mouse movement', description='Invert vertical mouse movement - moving mouse up looks down',
default=False) default=False) # Most players expect non-inverted vertical movement
# Cap Left/Right property # Property 4: Horizontal Rotation Limiting
# Prevents the character from rotating beyond specified horizontal limits
# Useful for fixed-perspective games or when character shouldn't turn completely around
property4: HaxeBoolProperty( property4: HaxeBoolProperty(
'property4', 'property4',
name='Cap Left / Right', name='Cap Left / Right',
description='Limit horizontal rotation', description='Limit horizontal rotation to prevent full 360-degree turns',
default=False) default=False) # Disabled by default - most FPS games allow full horizontal rotation
# Cap Up/Down property # Property 5: Vertical Rotation Limiting
# Prevents looking too far up or down, simulating human neck movement limitations
# Essential for realistic FPS games to prevent disorienting over-rotation
property5: HaxeBoolProperty( property5: HaxeBoolProperty(
'property5', 'property5',
name='Cap Up / Down', name='Cap Up / Down',
description='Limit vertical rotation', description='Limit vertical rotation to simulate natural neck movement (±90 degrees)',
default=True) default=True) # Enabled by default for realistic FPS behavior
# Head rotation space toggle # Property 6: Head Rotation Space Mode
property7: HaxeBoolProperty( # Controls whether head rotation uses local or world space coordinates
'property7', # Critical for preventing rotation issues when head object is child of body object
property6: HaxeBoolProperty(
'property6',
name='Head Local Space', name='Head Local Space',
description='Enable it if the Head is child of the Body to avoid weird rotation', description='Use local space for head rotation - enable when Head is child of Body to avoid gimbal lock',
default=False) default=False) # Disabled by default, enable when using parent-child object relationships
def lnx_init(self, context): def lnx_init(self, context):
"""Initialize the node's input and output sockets
This method is called when the node is first created in Blender.
It defines all the connection points (sockets) that other nodes can connect to.
Input Sockets:
- Action In: Execution flow input (when this node should run)
- Body: The main character/player object that rotates horizontally
- Head: Optional camera/head object that rotates vertically (can be child of Body)
- Sensitivity: Mouse sensitivity multiplier (0.5 = half sensitivity, 2.0 = double sensitivity)
- Smoothing: Movement smoothing factor (0.0 = no smoothing, higher = more smoothing)
Output Sockets:
- Action Out: Execution flow output (continues to next node after processing)
"""
# Execution flow input - connects from previous logic node
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Body')
self.add_input('LnxNodeSocketObject', 'Head') # Object inputs - require 3D objects from the scene
self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5) self.add_input('LnxNodeSocketObject', 'Body') # Main character object (required)
self.add_input('LnxFactorSocket', 'Smoothing', default_value=0.0) self.add_input('LnxNodeSocketObject', 'Head') # Camera/head object (optional)
# Numeric inputs with sensible defaults
self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5) # Medium sensitivity
self.add_input('LnxFloatSocket', 'Smoothing', default_value=0.0) # No smoothing by default
# Execution flow output - connects to next logic node
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, 'property0', text='Front') """Draw the node's user interface in Blender's logic tree editor
layout.prop(self, 'property1', text='Hide Locked')
# Invert XY section This method creates the visual controls that appear on the node in Blender.
col = layout.column(align=True) It organizes properties into logical groups for better usability.
col.label(text="Invert XY:")
row = col.row(align=True)
row.prop(self, 'property2', text='X', toggle=True)
row.prop(self, 'property3', text='Y', toggle=True)
# Cap rotations section Args:
col = layout.column(align=True) context: Blender context (current scene, selected objects, etc.)
col.prop(self, 'property4', text='Cap Left / Right') layout: UI layout object for arranging interface elements
col.prop(self, 'property5', text='Cap Up / Down') """
# Head behavior section # Basic configuration section
col = layout.column(align=True) layout.prop(self, 'property0', text='Front') # Front axis dropdown
col.prop(self, 'property7', text='Head Local Space') layout.prop(self, 'property1', text='Hide Locked') # Mouse locking checkbox
# Movement inversion controls section
# Group X and Y inversion together for logical organization
col = layout.column(align=True) # Create aligned column
col.label(text="Invert XY:") # Section header
row = col.row(align=True) # Create horizontal row within column
row.prop(self, 'property2', text='X', toggle=True) # X inversion toggle button
row.prop(self, 'property3', text='Y', toggle=True) # Y inversion toggle button
# Rotation limiting controls section
# Group rotation caps together since they're related functionality
col = layout.column(align=True) # Create new aligned column
col.prop(self, 'property4', text='Cap Left / Right') # Horizontal capping checkbox
col.prop(self, 'property5', text='Cap Up / Down') # Vertical capping checkbox
# Advanced head behavior section
# Separate advanced option that affects technical behavior
col = layout.column(align=True) # Create new aligned column
col.prop(self, 'property6', text='Head Local Space') # Head rotation space checkbox