forked from LeenkxTeam/LNXSDK
merge upstream
This commit is contained in:
@ -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
|
|
||||||
var smoothX: FastFloat = 0.0;
|
|
||||||
var smoothY: FastFloat = 0.0;
|
|
||||||
|
|
||||||
// Capping limits (in degrees)
|
// Current rotation tracking for capping calculations
|
||||||
var maxHorizontal: FastFloat = 180.0; // 180 degrees
|
var currentHorizontal: Float = 0.0; // Accumulated horizontal rotation
|
||||||
var maxVertical: FastFloat = 90.0; // 90 degrees
|
var currentVertical: Float = 0.0; // Accumulated vertical rotation
|
||||||
|
|
||||||
// Current accumulated rotations for capping
|
// Resolution scaling reference - base resolution for consistent sensitivity
|
||||||
var currentHorizontal: FastFloat = 0.0;
|
var baseResolutionWidth: Float = 1920.0;
|
||||||
var currentVertical: FastFloat = 0.0;
|
|
||||||
|
|
||||||
// Sub-pixel interpolation accumulators
|
// Sensitivity scaling constants
|
||||||
var accumulatedHorizontalRotation: FastFloat = 0.0;
|
static inline var BASE_SCALE: Float = 1500.0; // Base sensitivity scale factor
|
||||||
var accumulatedVerticalRotation: FastFloat = 0.0;
|
static var RADIAN_SCALING_FACTOR: Float = Math.PI * 50.0 / 180.0; // Degrees to radians conversion with sensitivity scaling
|
||||||
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;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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) {
|
||||||
var headVerticalAxis = headObject.transform.world.right();
|
// Local space rotation - recommended when head is a child of body
|
||||||
headObject.transform.rotate(headVerticalAxis, verticalRotation * Math.PI / 180.0); // Convert degrees to radians
|
// This prevents gimbal lock and rotation inheritance issues
|
||||||
|
headObject.transform.rotate(verticalAxis, verticalRotation);
|
||||||
|
} else {
|
||||||
|
// World space rotation - uses head object's current right vector
|
||||||
|
// More accurate for independent head objects but can cause issues with parenting
|
||||||
|
var headVerticalAxis = headObject.transform.world.right();
|
||||||
|
headObject.transform.rotate(headVerticalAxis, verticalRotation);
|
||||||
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
23
leenkx/Sources/leenkx/logicnode/OnceNode.hx
Normal file
23
leenkx/Sources/leenkx/logicnode/OnceNode.hx
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 = {
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
thread, callback = make.thread_callback_queue.get(block=False)
|
while True:
|
||||||
|
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
|
||||||
try:
|
|
||||||
bpy.app.timers.unregister(poll_threads)
|
# Explicit cleanup for better memory management
|
||||||
except ValueError:
|
del thread, callback
|
||||||
pass
|
|
||||||
bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True)
|
def _handle_callback_error(exception: Exception) -> None:
|
||||||
# Quickly check if another thread has finished
|
"""Centralized error handling with better recovery."""
|
||||||
return 0.01
|
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:
|
||||||
|
bpy.app.timers.unregister(poll_threads)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_polling_stats() -> dict:
|
||||||
|
"""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] = {}
|
||||||
|
@ -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'
|
# 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
|
||||||
|
|
||||||
# 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):
|
||||||
|
"""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('LnxFloatSocket', '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')
|
"""
|
||||||
|
|
||||||
# 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
|
15
leenkx/blender/lnx/logicnode/logic/LN_once.py
Normal file
15
leenkx/blender/lnx/logicnode/logic/LN_once.py
Normal 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')
|
||||||
|
|
||||||
|
|
@ -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"
|
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
Reference in New Issue
Block a user