merge upstream

This commit is contained in:
2025-07-16 05:57:15 +00:00
11 changed files with 669 additions and 216 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

@ -0,0 +1,23 @@
package leenkx.logicnode;
class OnceNode extends LogicNode {
var triggered:Bool = false;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
if(from == 1){
triggered = false;
return;
}
if (!triggered) {
triggered = true;
runOutput(0);
}
}
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
import iron.object.Object;
import iron.math.Vec4;
import iron.math.Mat4;
import iron.system.Time;
class SetObjectDelayedLocationNode extends LogicNode {
public var use_local_space: Bool = false;
private var initialOffset: Vec4 = null;
private var targetPos: Vec4 = new Vec4();
private var currentPos: Vec4 = new Vec4();
private var deltaVec: Vec4 = new Vec4();
private var tempVec: Vec4 = new Vec4();
private var lastParent: Object = null;
private var invParentMatrix: Mat4 = null;
public function new(tree: LogicTree) {
super(tree);
}
override function run(from: Int) {
var follower: Object = inputs[1].get();
var target: Object = inputs[2].get();
var delay: Float = inputs[3].get();
if (follower == null || target == null || delay == null) return runOutput(0);
if (initialOffset == null) {
initialOffset = new Vec4();
var followerPos = follower.transform.world.getLoc();
var targetPos = target.transform.world.getLoc();
initialOffset.setFrom(followerPos);
initialOffset.sub(targetPos);
}
targetPos.setFrom(target.transform.world.getLoc());
currentPos.setFrom(follower.transform.world.getLoc());
tempVec.setFrom(targetPos).add(initialOffset);
deltaVec.setFrom(tempVec).sub(currentPos);
if (deltaVec.length() < 0.001 && delay < 0.01) {
runOutput(0);
return;
}
if (delay == 0.0) {
currentPos.setFrom(tempVec);
} else {
var smoothFactor = Math.exp(-Time.delta / Math.max(0.0001, delay));
currentPos.x = tempVec.x + (currentPos.x - tempVec.x) * smoothFactor;
currentPos.y = tempVec.y + (currentPos.y - tempVec.y) * smoothFactor;
currentPos.z = tempVec.z + (currentPos.z - tempVec.z) * smoothFactor;
}
if (use_local_space && follower.parent != null) {
if (follower.parent != lastParent || invParentMatrix == null) {
lastParent = follower.parent;
invParentMatrix = Mat4.identity();
invParentMatrix.getInverse(follower.parent.transform.world);
}
tempVec.setFrom(currentPos);
tempVec.applymat(invParentMatrix);
follower.transform.loc.set(tempVec.x, tempVec.y, tempVec.z);
} else {
follower.transform.loc.set(currentPos.x, currentPos.y, currentPos.z);
}
follower.transform.buildMatrix();
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,7 +2,10 @@ import importlib
import os import os
import queue import queue
import sys import sys
import threading
import time
import types import types
from typing import Dict, Tuple, Callable, Set
import bpy import bpy
from bpy.app.handlers import persistent from bpy.app.handlers import persistent
@ -30,6 +33,10 @@ if lnx.is_reload(__name__):
else: else:
lnx.enable_reload(__name__) lnx.enable_reload(__name__)
# Module-level storage for active threads (eliminates re-queuing overhead)
_active_threads: Dict[threading.Thread, Callable] = {}
_last_poll_time = 0.0
_consecutive_empty_polls = 0
@persistent @persistent
def on_depsgraph_update_post(self): def on_depsgraph_update_post(self):
@ -135,35 +142,113 @@ def always() -> float:
def poll_threads() -> float: def poll_threads() -> float:
"""Polls the thread callback queue and if a thread has finished, it
is joined with the main thread and the corresponding callback is
executed in the main thread.
""" """
Improved thread polling with:
- No re-queuing overhead
- Batch processing of completed threads
- Adaptive timing based on activity
- Better memory management
- Simplified logic flow
"""
global _last_poll_time, _consecutive_empty_polls
current_time = time.time()
# Process all new threads from queue at once (batch processing)
new_threads_added = 0
try: try:
while True:
thread, callback = make.thread_callback_queue.get(block=False) thread, callback = make.thread_callback_queue.get(block=False)
_active_threads[thread] = callback
new_threads_added += 1
except queue.Empty: except queue.Empty:
pass
# Early return if no active threads
if not _active_threads:
_consecutive_empty_polls += 1
# Adaptive timing: longer intervals when consistently empty
if _consecutive_empty_polls > 10:
return 0.5 # Back off when no activity
return 0.25 return 0.25
if thread.is_alive():
try: # Reset empty poll counter when we have active threads
make.thread_callback_queue.put((thread, callback), block=False) _consecutive_empty_polls = 0
except queue.Full:
return 0.5 # Find completed threads (single pass, no re-queuing)
return 0.1 completed_threads = []
for thread in list(_active_threads.keys()):
if not thread.is_alive():
completed_threads.append(thread)
# Batch process all completed threads
if completed_threads:
_process_completed_threads(completed_threads)
# Adaptive timing based on activity level
active_count = len(_active_threads)
if active_count == 0:
return 0.25
elif active_count <= 3:
return 0.05 # Medium frequency for low activity
else: else:
return 0.01 # High frequency for high activity
def _process_completed_threads(completed_threads: list) -> None:
"""Process a batch of completed threads with robust error handling."""
for thread in completed_threads:
callback = _active_threads.pop(thread) # Remove from tracking
try: try:
thread.join() thread.join() # Should be instant since thread is dead
callback() callback()
except Exception as e: except Exception as e:
# If there is an exception, we can no longer return the time to # Robust error recovery
# the next call to this polling function, so to keep it running _handle_callback_error(e)
# we re-register it and then raise the original exception. continue # Continue processing other threads
# Explicit cleanup for better memory management
del thread, callback
def _handle_callback_error(exception: Exception) -> None:
"""Centralized error handling with better recovery."""
try:
# Try to unregister existing timer
bpy.app.timers.unregister(poll_threads)
except ValueError:
pass # Timer wasn't registered, that's fine
# Re-register timer with slightly longer interval for stability
bpy.app.timers.register(poll_threads, first_interval=0.1, persistent=True)
# Re-raise the original exception after ensuring timer continuity
raise exception
def cleanup_polling_system() -> None:
"""Optional cleanup function for proper shutdown."""
global _active_threads, _consecutive_empty_polls
# Wait for remaining threads to complete (with timeout)
for thread in list(_active_threads.keys()):
if thread.is_alive():
thread.join(timeout=1.0) # 1 second timeout
# Clear tracking structures
_active_threads.clear()
_consecutive_empty_polls = 0
# Unregister timer
try: try:
bpy.app.timers.unregister(poll_threads) bpy.app.timers.unregister(poll_threads)
except ValueError: except ValueError:
pass pass
bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True)
# Quickly check if another thread has finished def get_polling_stats() -> dict:
return 0.01 """Get statistics about the polling system for monitoring."""
return {
'active_threads': len(_active_threads),
'consecutive_empty_polls': _consecutive_empty_polls,
'thread_ids': [t.ident for t in _active_threads.keys()]
}
loaded_py_libraries: dict[str, types.ModuleType] = {} loaded_py_libraries: dict[str, types.ModuleType] = {}

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

@ -0,0 +1,15 @@
from lnx.logicnode.lnx_nodes import *
import bpy
class OnceNode(LnxLogicTreeNode):
bl_idname = 'LNOnceNode'
bl_label = 'Once'
lnx_version = 1
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'Run Once')
self.add_input('LnxNodeSocketAction', 'Reset')
self.add_output('LnxNodeSocketAction', 'Out')

View File

@ -0,0 +1,28 @@
from lnx.logicnode.lnx_nodes import *
import bpy
from bpy.props import BoolProperty
class LNSetObjectDelayedLocationNode(LnxLogicTreeNode):
bl_idname = 'LNSetObjectDelayedLocationNode'
bl_label = 'Set Object Delayed Location'
lnx_section = 'props'
lnx_version = 1
use_local_space: BoolProperty(
name="Use Local Space",
description="Move follower in local space instead of world space",
default=False
)
def lnx_init(self, context):
self.inputs.new('LnxNodeSocketAction', 'In')
self.inputs.new('LnxNodeSocketObject', 'Source Object')
self.inputs.new('LnxNodeSocketObject', 'Target Object')
self.inputs.new('LnxFloatSocket', 'Delay')
self.outputs.new('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'use_local_space')
def draw_label(self):
return "Set Object Delayed Location"

View File

@ -369,6 +369,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='')
@ -594,7 +614,7 @@ def update_leenkx_world():
if bpy.data.filepath != '' and (file_version < sdk_version or wrd.lnx_commit != lnx_commit): if bpy.data.filepath != '' and (file_version < sdk_version or wrd.lnx_commit != lnx_commit):
# This allows for seamless migration from earlier versions of Leenkx # This allows for seamless migration from earlier versions of Leenkx
for rp in wrd.lnx_rplist: # TODO: deprecated for rp in wrd.lnx_rplist: # TODO: deprecated
if rp.rp_gi != 'Off': if hasattr(rp, 'rp_gi') and rp.rp_gi != 'Off':
rp.rp_gi = 'Off' rp.rp_gi = 'Off'
rp.rp_voxels = rp.rp_gi rp.rp_voxels = rp.rp_gi

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')