Merge pull request 'improved mouse look node and added missing rigid body settings in Properties > physics > leenkx Props' (#98) from wuaieyo/LNXSDK:main into main

Reviewed-on: LeenkxTeam/LNXSDK#98
This commit is contained in:
2025-07-15 02:59:53 +00:00
6 changed files with 422 additions and 194 deletions

View File

@ -6,172 +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 {
// Note: This implementation works in degrees internally and converts to radians only when applying rotations // Configuration properties (set from Blender node interface)
// Sub-pixel interpolation is always enabled for optimal precision public var property0: String; // Front axis: "X", "Y", or "Z"
// Features: Resolution-adaptive scaling and precise low-sensitivity support 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
public var property0: String; // Front axis // Smoothing state variables - maintain previous frame values for interpolation
public var property1: Bool; // Center Mouse var smoothX: Float = 0.0; // Smoothed horizontal mouse delta
public var property2: Bool; // Invert X var smoothY: Float = 0.0; // Smoothed vertical mouse delta
public var property3: Bool; // Invert Y
public var property4: Bool; // Cap Left/Right
public var property5: Bool; // Cap Up/Down
// New strategy toggles // Rotation limits (in radians)
public var property6: Bool; // Resolution-Adaptive Scaling var maxHorizontal: Float = Math.PI; // Maximum horizontal rotation (180 degrees)
var maxVertical: Float = Math.PI / 2; // Maximum vertical rotation (90 degrees)
// Smoothing variables // Current rotation tracking for capping calculations
var smoothX: FastFloat = 0.0; var currentHorizontal: Float = 0.0; // Accumulated horizontal rotation
var smoothY: FastFloat = 0.0; var currentVertical: Float = 0.0; // Accumulated vertical rotation
// Capping limits (in degrees)
var maxHorizontal: FastFloat = 180.0; // 180 degrees
var maxVertical: FastFloat = 90.0; // 90 degrees
// Current accumulated rotations for capping
var currentHorizontal: FastFloat = 0.0;
var currentVertical: FastFloat = 0.0;
// Sub-pixel interpolation accumulators
var accumulatedHorizontalRotation: FastFloat = 0.0;
var accumulatedVerticalRotation: FastFloat = 0.0;
var minimumRotationThreshold: FastFloat = 0.01; // degrees (was 0.0001 radians)
// Frame rate independence removed - not applicable to mouse input
// Resolution adaptive scaling
var baseResolutionWidth: FastFloat = 1920.0;
var baseResolutionHeight: FastFloat = 1080.0;
// 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) { 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 mouse centering/locking // 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 if mouse is active // 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 mouse movement deltas // Get raw mouse movement delta (pixels moved since last frame)
var deltaX: FastFloat = mouse.movementX; var deltaX: Float = mouse.movementX;
var deltaY: FastFloat = mouse.movementY; var deltaY: Float = mouse.movementY;
// Note: Sensitivity will be applied later to preserve precision for small movements // Apply axis inversion if configured
if (property2) deltaX = -deltaX; // Invert horizontal movement
if (property3) deltaY = -deltaY; // Invert vertical movement
// Apply inversion // Calculate resolution-adaptive scaling to maintain consistent sensitivity
if (property2) deltaX = -deltaX; // across different screen resolutions. Higher resolutions will have proportionally
if (property3) deltaY = -deltaY; // higher scaling to compensate for increased pixel density.
var resolutionMultiplier: Float = System.windowWidth() / baseResolutionWidth;
// Strategy 1: Resolution-Adaptive Scaling // Apply movement smoothing if enabled
var resolutionMultiplier: FastFloat = 1.0; // This creates a weighted average between current and previous movement values
if (property6) { // to reduce jittery camera movement, especially useful for low framerates
var currentWidth = System.windowWidth();
var currentHeight = System.windowHeight();
resolutionMultiplier = (currentWidth / baseResolutionWidth) * (currentHeight / baseResolutionHeight);
resolutionMultiplier = Math.sqrt(resolutionMultiplier); // Take square root to avoid over-scaling
}
// Frame Rate Independence disabled for mouse input - mouse deltas are inherently frame-rate independent
// Apply smoothing
if (smoothing > 0.0) { if (smoothing > 0.0) {
var smoothFactor = 1.0 - Math.min(smoothing, 0.99); // Prevent complete smoothing var smoothingFactor: Float = Math.min(smoothing, 0.99); // Cap smoothing to prevent complete freeze
smoothX = smoothX * smoothing + deltaX * smoothFactor; smoothX = smoothX * smoothingFactor + deltaX * (1.0 - smoothingFactor);
smoothY = smoothY * smoothing + deltaY * smoothFactor; smoothY = smoothY * smoothingFactor + deltaY * (1.0 - smoothingFactor);
deltaX = smoothX; deltaX = smoothX;
deltaY = smoothY; deltaY = smoothY;
} }
// Determine rotation axes based on front axis setting // Define rotation axes based on the configured front axis
var horizontalAxis = new Vec4(); // These determine which 3D axes are used for horizontal and vertical rotation
var verticalAxis = new Vec4(); 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": // X is front case "X": // X-axis forward (e.g., for side-scrolling or specific orientations)
horizontalAxis.set(0, 0, 1); // Z axis for horizontal (yaw) horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(0, 1, 0); // Y axis for vertical (pitch) verticalAxis.set(0, 1, 0); // Y-axis for vertical rotation
case "Y": // Y is front (default) case "Y": // Y-axis forward (most common for 3D games)
#if lnx_yaxisup #if lnx_yaxisup
horizontalAxis.set(0, 0, 1); // Z axis for horizontal (yaw) // Y-up coordinate system (Blender default)
verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) 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 axis for horizontal (yaw) // Z-up coordinate system
verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) horizontalAxis.set(0, 0, 1); // Z-axis for horizontal rotation
verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
#end #end
case "Z": // Z is front case "Z": // Z-axis forward (top-down or specific orientations)
horizontalAxis.set(0, 1, 0); // Y axis for horizontal (yaw) horizontalAxis.set(0, 1, 0); // Y-axis for horizontal rotation
verticalAxis.set(1, 0, 0); // X axis for vertical (pitch) verticalAxis.set(1, 0, 0); // X-axis for vertical rotation
} }
// Base scaling // Calculate final sensitivity scaling combining base scale and resolution adaptation
var baseScale: FastFloat = 1500.0; var finalScale: Float = BASE_SCALE * resolutionMultiplier;
var finalScale = baseScale;
// Apply resolution scaling // Apply user-defined sensitivity multiplier
if (property6) {
finalScale *= resolutionMultiplier;
}
// Apply sensitivity scaling after all enhancement strategies to preserve precision
deltaX *= sensitivity; deltaX *= sensitivity;
deltaY *= sensitivity; deltaY *= sensitivity;
// Calculate rotation amounts (in degrees) // Convert pixel movement to rotation angles (radians)
var horizontalRotation: FastFloat = (-deltaX / finalScale) * 180.0 / Math.PI; // Negative values ensure natural movement direction (moving mouse right rotates right)
var verticalRotation: FastFloat = (-deltaY / finalScale) * 180.0 / Math.PI; var horizontalRotation: Float = (-deltaX / finalScale) * RADIAN_SCALING_FACTOR;
var verticalRotation: Float = (-deltaY / finalScale) * RADIAN_SCALING_FACTOR;
// Note: Frame rate independence removed for mouse input as mouse deltas // Apply horizontal rotation capping if enabled
// are already frame-rate independent by nature. Mouse input represents // This prevents the character from rotating beyond specified limits
// instantaneous user intent, not time-based movement. if (property4) {
// Strategy 2: Sub-Pixel Interpolation (always enabled)
accumulatedHorizontalRotation += horizontalRotation;
accumulatedVerticalRotation += verticalRotation;
// Only apply rotation if accumulated amount exceeds threshold
if (Math.abs(accumulatedHorizontalRotation) >= minimumRotationThreshold) {
horizontalRotation = accumulatedHorizontalRotation;
accumulatedHorizontalRotation = 0.0;
} else {
horizontalRotation = 0.0;
}
if (Math.abs(accumulatedVerticalRotation) >= minimumRotationThreshold) {
verticalRotation = accumulatedVerticalRotation;
accumulatedVerticalRotation = 0.0;
} else {
verticalRotation = 0.0;
}
// Apply capping constraints
if (property4) { // Cap Left/Right
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;
@ -181,8 +171,11 @@ class MouseLookNode extends LogicNode {
} }
} }
if (property5) { // Cap Up/Down // Apply vertical rotation capping if enabled
// This prevents looking too far up or down (like human neck limitations)
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;
@ -192,41 +185,49 @@ class MouseLookNode extends LogicNode {
} }
} }
// Apply horizontal rotation to body (yaw) // Apply horizontal rotation to body object (character turning left/right)
if (Math.abs(horizontalRotation) > 0.01) { // 0.01 degrees threshold if (horizontalRotation != 0.0) {
bodyObject.transform.rotate(horizontalAxis, horizontalRotation * Math.PI / 180.0); // Convert degrees to radians bodyObject.transform.rotate(horizontalAxis, horizontalRotation);
// Sync physics if needed // 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 (pitch) if head object is provided // Apply vertical rotation to head object (camera looking up/down)
if (headObject != null && Math.abs(verticalRotation) > 0.01) { // 0.01 degrees threshold if (headObject != null && verticalRotation != 0.0) {
// For head rotation, use the head's local coordinate system 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(); var headVerticalAxis = headObject.transform.world.right();
headObject.transform.rotate(headVerticalAxis, verticalRotation * Math.PI / 180.0); // Convert degrees to radians headObject.transform.rotate(headVerticalAxis, verticalRotation);
}
// Sync physics if needed // 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) { } else if (headObject == null && verticalRotation != 0.0) {
// If no head object, apply vertical rotation to body as well // Fallback: if no separate head object, apply vertical rotation to body
if (Math.abs(verticalRotation) > 0.01) { // 0.01 degrees threshold // This creates a simpler single-object camera control
bodyObject.transform.rotate(verticalAxis, verticalRotation * Math.PI / 180.0); // Convert degrees to radians bodyObject.transform.rotate(verticalAxis, verticalRotation);
// Sync physics if needed // 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

@ -36,6 +36,18 @@ class RigidBody extends iron.Trait {
var useDeactivation: Bool; var useDeactivation: Bool;
var deactivationParams: Array<Float>; var deactivationParams: Array<Float>;
var ccd = false; // Continuous collision detection var ccd = false; // Continuous collision detection
// New velocity limiting properties
var linearVelocityMin: Float;
var linearVelocityMax: Float;
var angularVelocityMin: Float;
var angularVelocityMax: Float;
// New lock properties
var lockTranslationX: Bool;
var lockTranslationY: Bool;
var lockTranslationZ: Bool;
var lockRotationX: Bool;
var lockRotationY: Bool;
var lockRotationZ: Bool;
public var group = 1; public var group = 1;
public var mask = 1; public var mask = 1;
var trigger = false; var trigger = false;
@ -120,7 +132,17 @@ class RigidBody extends iron.Trait {
collisionMargin: 0.0, collisionMargin: 0.0,
linearDeactivationThreshold: 0.0, linearDeactivationThreshold: 0.0,
angularDeactivationThrshold: 0.0, angularDeactivationThrshold: 0.0,
deactivationTime: 0.0 deactivationTime: 0.0,
linearVelocityMin: 0.0,
linearVelocityMax: 0.0,
angularVelocityMin: 0.0,
angularVelocityMax: 0.0,
lockTranslationX: false,
lockTranslationY: false,
lockTranslationZ: false,
lockRotationX: false,
lockRotationY: false,
lockRotationZ: false
}; };
if (flags == null) flags = { if (flags == null) flags = {
@ -139,6 +161,18 @@ class RigidBody extends iron.Trait {
this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ]; this.angularFactors = [params.angularFactorsX, params.angularFactorsY, params.angularFactorsZ];
this.collisionMargin = params.collisionMargin; this.collisionMargin = params.collisionMargin;
this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime]; this.deactivationParams = [params.linearDeactivationThreshold, params.angularDeactivationThrshold, params.deactivationTime];
// New velocity limiting properties
this.linearVelocityMin = params.linearVelocityMin;
this.linearVelocityMax = params.linearVelocityMax;
this.angularVelocityMin = params.angularVelocityMin;
this.angularVelocityMax = params.angularVelocityMax;
// New lock properties
this.lockTranslationX = params.lockTranslationX;
this.lockTranslationY = params.lockTranslationY;
this.lockTranslationZ = params.lockTranslationZ;
this.lockRotationX = params.lockRotationX;
this.lockRotationY = params.lockRotationY;
this.lockRotationZ = params.lockRotationZ;
this.animated = flags.animated; this.animated = flags.animated;
this.trigger = flags.trigger; this.trigger = flags.trigger;
this.ccd = flags.ccd; this.ccd = flags.ccd;
@ -291,11 +325,25 @@ class RigidBody extends iron.Trait {
} }
if (linearFactors != null) { if (linearFactors != null) {
setLinearFactor(linearFactors[0], linearFactors[1], linearFactors[2]); // Apply lock properties by overriding factors
var lx = linearFactors[0];
var ly = linearFactors[1];
var lz = linearFactors[2];
if (lockTranslationX) lx = 0.0;
if (lockTranslationY) ly = 0.0;
if (lockTranslationZ) lz = 0.0;
setLinearFactor(lx, ly, lz);
} }
if (angularFactors != null) { if (angularFactors != null) {
setAngularFactor(angularFactors[0], angularFactors[1], angularFactors[2]); // Apply lock properties by overriding factors
var ax = angularFactors[0];
var ay = angularFactors[1];
var az = angularFactors[2];
if (lockRotationX) ax = 0.0;
if (lockRotationY) ay = 0.0;
if (lockRotationZ) az = 0.0;
setAngularFactor(ax, ay, az);
} }
if (trigger) bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_NO_CONTACT_RESPONSE); if (trigger) bodyColl.setCollisionFlags(bodyColl.getCollisionFlags() | CF_NO_CONTACT_RESPONSE);
@ -411,6 +459,55 @@ class RigidBody extends iron.Trait {
var rbs = physics.getContacts(this); var rbs = physics.getContacts(this);
if (rbs != null) for (rb in rbs) for (f in onContact) f(rb); if (rbs != null) for (rb in rbs) for (f in onContact) f(rb);
} }
// Apply velocity limiting if enabled
if (!animated && !staticObj) {
applyVelocityLimits();
}
}
function applyVelocityLimits() {
if (!ready) return;
// Check linear velocity limits
if (linearVelocityMin > 0.0 || linearVelocityMax > 0.0) {
var velocity = getLinearVelocity();
var speed = velocity.length();
if (linearVelocityMin > 0.0 && speed < linearVelocityMin) {
// Increase velocity to minimum
if (speed > 0.0) {
velocity.normalize();
velocity.mult(linearVelocityMin);
setLinearVelocity(velocity.x, velocity.y, velocity.z);
}
} else if (linearVelocityMax > 0.0 && speed > linearVelocityMax) {
// Clamp velocity to maximum
velocity.normalize();
velocity.mult(linearVelocityMax);
setLinearVelocity(velocity.x, velocity.y, velocity.z);
}
}
// Check angular velocity limits
if (angularVelocityMin > 0.0 || angularVelocityMax > 0.0) {
var angularVel = getAngularVelocity();
var angularSpeed = angularVel.length();
if (angularVelocityMin > 0.0 && angularSpeed < angularVelocityMin) {
// Increase angular velocity to minimum
if (angularSpeed > 0.0) {
angularVel.normalize();
angularVel.mult(angularVelocityMin);
setAngularVelocity(angularVel.x, angularVel.y, angularVel.z);
}
} else if (angularVelocityMax > 0.0 && angularSpeed > angularVelocityMax) {
// Clamp angular velocity to maximum
angularVel.normalize();
angularVel.mult(angularVelocityMax);
setAngularVelocity(angularVel.x, angularVel.y, angularVel.z);
}
}
} }
public function disableCollision() { public function disableCollision() {
@ -745,6 +842,16 @@ typedef RigidBodyParams = {
var linearDeactivationThreshold: Float; var linearDeactivationThreshold: Float;
var angularDeactivationThrshold: Float; var angularDeactivationThrshold: Float;
var deactivationTime: Float; var deactivationTime: Float;
var linearVelocityMin: Float;
var linearVelocityMax: Float;
var angularVelocityMin: Float;
var angularVelocityMax: Float;
var lockTranslationX: Bool;
var lockTranslationY: Bool;
var lockTranslationZ: Bool;
var lockRotationX: Bool;
var lockRotationY: Bool;
var lockRotationZ: Bool;
} }
typedef RigidBodyFlags = { typedef RigidBodyFlags = {

View File

@ -2843,6 +2843,18 @@ class LeenkxExporter:
body_params['linearDeactivationThreshold'] = deact_lv body_params['linearDeactivationThreshold'] = deact_lv
body_params['angularDeactivationThrshold'] = deact_av body_params['angularDeactivationThrshold'] = deact_av
body_params['deactivationTime'] = deact_time body_params['deactivationTime'] = deact_time
# New velocity limit properties
body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min
body_params['linearVelocityMax'] = bobject.lnx_rb_linear_velocity_max
body_params['angularVelocityMin'] = bobject.lnx_rb_angular_velocity_min
body_params['angularVelocityMax'] = bobject.lnx_rb_angular_velocity_max
# New lock properties
body_params['lockTranslationX'] = bobject.lnx_rb_lock_translation_x
body_params['lockTranslationY'] = bobject.lnx_rb_lock_translation_y
body_params['lockTranslationZ'] = bobject.lnx_rb_lock_translation_z
body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x
body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y
body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z
body_flags = {} body_flags = {}
body_flags['animated'] = rb.kinematic body_flags['animated'] = rb.kinematic
body_flags['trigger'] = bobject.lnx_rb_trigger body_flags['trigger'] = bobject.lnx_rb_trigger

View File

@ -2,100 +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:
- Sub-pixel interpolation (always enabled) for optimal precision and smooth low-sensitivity movement - Built-in resolution-adaptive scaling for consistent feel across different screen resolutions
- 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'
lnx_section = 'mouse'
lnx_version = 1
# Front axis property # Blender node identification
bl_idname = 'LNMouseLookNode' # Unique identifier for Blender's node system
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
# 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
# Strategy toggles # Property 6: Head Rotation Space Mode
# Controls whether head rotation uses local or world space coordinates
# Critical for preventing rotation issues when head object is child of body object
property6: HaxeBoolProperty( property6: HaxeBoolProperty(
'property6', 'property6',
name='Resolution Adaptive', name='Head Local Space',
description='Scale sensitivity based on screen resolution', 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):
self.add_input('LnxNodeSocketAction', 'In') """Initialize the node's input and output sockets
self.add_input('LnxNodeSocketObject', 'Body')
self.add_input('LnxNodeSocketObject', 'Head')
self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5)
self.add_input('LnxFloatSocket', 'Smoothing', default_value=0.0)
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')
# Object inputs - require 3D objects from the scene
self.add_input('LnxNodeSocketObject', 'Body') # Main character object (required)
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') """
# Separator # Basic configuration section
layout.separator() layout.prop(self, 'property0', text='Front') # Front axis dropdown
layout.prop(self, 'property1', text='Hide Locked') # Mouse locking checkbox
# Enhancement strategies section # Movement inversion controls section
col = layout.column(align=True) # Group X and Y inversion together for logical organization
col.label(text="Enhancement Strategies:") col = layout.column(align=True) # Create aligned column
col.prop(self, 'property6', text='Resolution Adaptive') 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

View File

@ -368,6 +368,26 @@ def init_properties():
default=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False), default=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False),
size=20, size=20,
subtype='LAYER') subtype='LAYER')
# Linear velocity limits
bpy.types.Object.lnx_rb_linear_velocity_min = FloatProperty(name="Linear Velocity Min", description="Minimum linear velocity", default=0.0, min=0.0)
bpy.types.Object.lnx_rb_linear_velocity_max = FloatProperty(name="Linear Velocity Max", description="Maximum linear velocity", default=0.0, min=0.0)
# Angular velocity limits
bpy.types.Object.lnx_rb_angular_velocity_min = FloatProperty(name="Angular Velocity Min", description="Minimum angular velocity", default=0.0, min=0.0)
bpy.types.Object.lnx_rb_angular_velocity_max = FloatProperty(name="Angular Velocity Max", description="Maximum angular velocity", default=0.0, min=0.0)
# Lock translation axes
bpy.types.Object.lnx_rb_lock_translation_x = BoolProperty(name="Lock Translation X", description="Lock movement along X axis", default=False)
bpy.types.Object.lnx_rb_lock_translation_y = BoolProperty(name="Lock Translation Y", description="Lock movement along Y axis", default=False)
bpy.types.Object.lnx_rb_lock_translation_z = BoolProperty(name="Lock Translation Z", description="Lock movement along Z axis", default=False)
# Lock rotation axes
bpy.types.Object.lnx_rb_lock_rotation_x = BoolProperty(name="Lock Rotation X", description="Lock rotation around X axis", default=False)
bpy.types.Object.lnx_rb_lock_rotation_y = BoolProperty(name="Lock Rotation Y", description="Lock rotation around Y axis", default=False)
bpy.types.Object.lnx_rb_lock_rotation_z = BoolProperty(name="Lock Rotation Z", description="Lock rotation around Z axis", default=False)
bpy.types.Object.lnx_relative_physics_constraint = BoolProperty(name="Relative Physics Constraint", description="Add physics constraint relative to the parent object or collection when spawned", default=False) bpy.types.Object.lnx_relative_physics_constraint = BoolProperty(name="Relative Physics Constraint", description="Add physics constraint relative to the parent object or collection when spawned", default=False)
bpy.types.Object.lnx_animation_enabled = BoolProperty(name="Animation", description="Enable skinning & timeline animation", default=True) bpy.types.Object.lnx_animation_enabled = BoolProperty(name="Animation", description="Enable skinning & timeline animation", default=True)
bpy.types.Object.lnx_tilesheet = StringProperty(name="Tilesheet", description="Set tilesheet animation", default='') bpy.types.Object.lnx_tilesheet = StringProperty(name="Tilesheet", description="Set tilesheet animation", default='')

View File

@ -240,6 +240,36 @@ class LNX_PT_PhysicsPropsPanel(bpy.types.Panel):
layout.prop(obj, 'lnx_rb_linear_factor') layout.prop(obj, 'lnx_rb_linear_factor')
layout.prop(obj, 'lnx_rb_angular_factor') layout.prop(obj, 'lnx_rb_angular_factor')
layout.prop(obj, 'lnx_rb_angular_friction') layout.prop(obj, 'lnx_rb_angular_friction')
# Linear Velocity section
layout.separator()
layout.label(text="Linear Velocity:")
layout.prop(obj, 'lnx_rb_linear_velocity_min')
layout.prop(obj, 'lnx_rb_linear_velocity_max')
# Angular Velocity section
layout.separator()
layout.label(text="Angular Velocity:")
layout.prop(obj, 'lnx_rb_angular_velocity_min')
layout.prop(obj, 'lnx_rb_angular_velocity_max')
# Lock Translation section
layout.separator()
layout.label(text="Lock Translation:")
row = layout.row(align=True)
row.prop(obj, 'lnx_rb_lock_translation_x', text="X")
row.prop(obj, 'lnx_rb_lock_translation_y', text="Y")
row.prop(obj, 'lnx_rb_lock_translation_z', text="Z")
# Lock Rotation section
layout.separator()
layout.label(text="Lock Rotation:")
row = layout.row(align=True)
row.prop(obj, 'lnx_rb_lock_rotation_x', text="X")
row.prop(obj, 'lnx_rb_lock_rotation_y', text="Y")
row.prop(obj, 'lnx_rb_lock_rotation_z', text="Z")
layout.prop(obj, 'lnx_rb_trigger') layout.prop(obj, 'lnx_rb_trigger')
layout.prop(obj, 'lnx_rb_ccd') layout.prop(obj, 'lnx_rb_ccd')
layout.prop(obj, 'lnx_rb_interpolate') layout.prop(obj, 'lnx_rb_interpolate')