From fc906125d425a1837f9ce23676823d10cc25d5e6 Mon Sep 17 00:00:00 2001 From: NB2030 Date: Tue, 17 Feb 2026 16:03:58 +0300 Subject: [PATCH 1/4] Sticker3DBehavior --- Extensions/CMakeLists.txt | 3 +- Extensions/Sticker3DBehavior/CMakeLists.txt | 23 ++ Extensions/Sticker3DBehavior/Extension.cpp | 33 +++ Extensions/Sticker3DBehavior/JsExtension.cpp | 190 ++++++++++++ .../Sticker3DBehavior/Sticker3DBehavior.cpp | 52 ++++ .../Sticker3DBehavior/Sticker3DBehavior.h | 37 +++ .../sticker3druntimebehavior.ts | 271 ++++++++++++++++++ GDJS/GDJS/Extensions/JsPlatform.cpp | 6 +- GDevelop.js/CMakeLists.txt | 3 +- newIDE/electron-app/app/package-lock.json | 4 +- 10 files changed, 617 insertions(+), 5 deletions(-) create mode 100644 Extensions/Sticker3DBehavior/CMakeLists.txt create mode 100644 Extensions/Sticker3DBehavior/Extension.cpp create mode 100644 Extensions/Sticker3DBehavior/JsExtension.cpp create mode 100644 Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp create mode 100644 Extensions/Sticker3DBehavior/Sticker3DBehavior.h create mode 100644 Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts diff --git a/Extensions/CMakeLists.txt b/Extensions/CMakeLists.txt index 8d531372e50f..0f3c5a8eaba2 100644 --- a/Extensions/CMakeLists.txt +++ b/Extensions/CMakeLists.txt @@ -15,6 +15,7 @@ set( DraggableBehavior Inventory LinkedObjects + Sticker3DBehavior PanelSpriteObject ParticleSystem PathfindingBehavior @@ -32,4 +33,4 @@ set( # Automatically add all listed extensions foreach(extension ${GD_EXTENSIONS}) add_subdirectory(${extension}) -endforeach() +endforeach() \ No newline at end of file diff --git a/Extensions/Sticker3DBehavior/CMakeLists.txt b/Extensions/Sticker3DBehavior/CMakeLists.txt new file mode 100644 index 000000000000..9feed5657dab --- /dev/null +++ b/Extensions/Sticker3DBehavior/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.5) + +project(Sticker3DBehavior) +gd_add_extension_includes() + +# Defines +# +gd_add_extension_definitions(Sticker3DBehavior) + +# The targets +# +include_directories(.) +file( + GLOB + source_files + *.cpp + *.h) +gd_add_clang_utils(Sticker3DBehavior "${source_files}") +gd_add_extension_target(Sticker3DBehavior "${source_files}") + +# Linker files for the IDE extension +# +gd_extension_link_libraries(Sticker3DBehavior) \ No newline at end of file diff --git a/Extensions/Sticker3DBehavior/Extension.cpp b/Extensions/Sticker3DBehavior/Extension.cpp new file mode 100644 index 000000000000..beb4eb104e3a --- /dev/null +++ b/Extensions/Sticker3DBehavior/Extension.cpp @@ -0,0 +1,33 @@ +/** +GDevelop - Sticker3D Behavior Extension +Copyright (c) 2024 GDevelop Team +This project is released under the MIT License. +*/ + +#include "Sticker3DBehavior.h" +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/Project/BehaviorsSharedData.h" +#include "GDCore/Tools/Localization.h" + +void DeclareSticker3DBehaviorExtension(gd::PlatformExtension& extension) { + extension + .SetExtensionInformation("Sticker3DBehavior", + _("3D sticker"), + _("Stick 3D objects together so they move as one."), + "GDevelop Team", + "Open source (MIT License)") + .SetCategory("General") + .SetTags("3d, stick, attach, parent, child") + .SetExtensionHelpPath("/behaviors/sticker3d"); + + gd::BehaviorMetadata& aut = extension.AddBehavior( + "Sticker3DBehavior", + _("3D sticker"), + "Sticker3D", + _("Stick this 3D object to another 3D object. When the stuck-to 3D object moves, this 3D object will follow it maintaining the offset."), + "", + "res/conditions/3d_box.svg", + "Sticker3DBehavior", + std::make_shared(), + std::make_shared()); +} \ No newline at end of file diff --git a/Extensions/Sticker3DBehavior/JsExtension.cpp b/Extensions/Sticker3DBehavior/JsExtension.cpp new file mode 100644 index 000000000000..a50e0326284c --- /dev/null +++ b/Extensions/Sticker3DBehavior/JsExtension.cpp @@ -0,0 +1,190 @@ +/** +GDevelop - Sticker3D Behavior Extension +Copyright (c) 2024 GDevelop Team +This project is released under the MIT License. +*/ +#if defined(GD_IDE_ONLY) +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/Tools/Localization.h" + +#include + +void DeclareSticker3DBehaviorExtension(gd::PlatformExtension& extension); + +/** + * \brief This class declares information about the JS extension. + */ +class Sticker3DBehaviorJsExtension : public gd::PlatformExtension { + public: + /** + * \brief Constructor of an extension declares everything the extension + * contains: objects, actions, conditions and expressions. + */ + Sticker3DBehaviorJsExtension() { + DeclareSticker3DBehaviorExtension(*this); + + GetBehaviorMetadata("Sticker3DBehavior::Sticker3DBehavior") + .SetIncludeFile("Extensions/Sticker3DBehavior/sticker3druntimebehavior.js"); + + auto& behavior = GetBehaviorMetadata("Sticker3DBehavior::Sticker3DBehavior"); + + behavior + .AddScopedAction( + "LinkToObject", + _("Stick to a 3D object"), + _("Stick this 3D object to another 3D object. It will follow the position of the 3D object it is stuck to."), + _("Stick _PARAM0_ to _PARAM2_"), + _("Sticker"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .AddParameter("objectPtr", _("3D object to stick to"), "", false) + .SetFunctionName("stickToObject"); + + behavior + .AddScopedAction( + "Unstick", + _("Unstick from 3D object"), + _("Unstick this 3D object from the 3D object it is stuck to."), + _("Unstick _PARAM0_"), + _("Sticker"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .SetFunctionName("unstick"); + + behavior + .AddScopedCondition( + "IsStuck", + _("Is stuck to another 3D object"), + _("Check if the 3D object is currently stuck to another 3D object."), + _("_PARAM0_ is stuck to another 3D object"), + _("Sticker"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .SetFunctionName("isStuck"); + + behavior + .AddScopedAction( + "SetOffsetX", + _("Set X offset"), + _("Set the X offset from the stuck-to 3D object."), + _("Set X offset of _PARAM0_ to _PARAM2_"), + _("Sticker ❯ Offset"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .AddParameter("number", _("X offset"), "", false) + .SetFunctionName("setOffsetX"); + + behavior + .AddScopedAction( + "SetOffsetY", + _("Set Y offset"), + _("Set the Y offset from the stuck-to 3D object."), + _("Set Y offset of _PARAM0_ to _PARAM2_"), + _("Sticker ❯ Offset"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .AddParameter("number", _("Y offset"), "", false) + .SetFunctionName("setOffsetY"); + + behavior + .AddScopedAction( + "SetOffsetZ", + _("Set Z offset"), + _("Set the Z offset from the stuck-to 3D object."), + _("Set Z offset of _PARAM0_ to _PARAM2_"), + _("Sticker ❯ Offset"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .AddParameter("number", _("Z offset"), "", false) + .SetFunctionName("setOffsetZ"); + + behavior + .AddExpression( + "OffsetX", + _("X offset"), + _("Get the X offset from the stuck-to 3D object."), + _("Sticker ❯ Offset"), + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .SetFunctionName("getOffsetX"); + + behavior + .AddExpression( + "OffsetY", + _("Y offset"), + _("Get the Y offset from the stuck-to 3D object."), + _("Sticker ❯ Offset"), + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .SetFunctionName("getOffsetY"); + + behavior + .AddExpression( + "OffsetZ", + _("Z offset"), + _("Get the Z offset from the stuck-to 3D object."), + _("Sticker ❯ Offset"), + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .SetFunctionName("getOffsetZ"); + + behavior + .AddScopedAction( + "SetFollowRotation", + _("Follow rotation"), + _("Enable or disable rotation following the 3D object it is stuck to."), + _("Set _PARAM0_ to follow rotation: _PARAM2_"), + _("Sticker"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .AddParameter("yesorno", _("Follow rotation")) + .SetFunctionName("setFollowRotation"); + + behavior + .AddScopedCondition( + "FollowRotation", + _("Follows rotation"), + _("Check if the 3D object follows the rotation of the 3D object it is stuck to."), + _("_PARAM0_ follows rotation"), + _("Sticker"), + "res/conditions/3d_box.svg", + "res/conditions/3d_box.svg") + .AddParameter("object", _("Object"), "", false) + .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") + .SetFunctionName("followsRotation"); + + GD_COMPLETE_EXTENSION_COMPILATION_INFORMATION(); + }; +}; + +#if defined(EMSCRIPTEN) +extern "C" gd::PlatformExtension* CreateGDJSSticker3DBehaviorExtension() { + return new Sticker3DBehaviorJsExtension; +} +#else +/** + * Used by GDevelop to create the extension class + * -- Do not need to be modified. -- + */ +extern "C" gd::PlatformExtension* GD_EXTENSION_API CreateGDJSExtension() { + return new Sticker3DBehaviorJsExtension; +} +#endif +#endif \ No newline at end of file diff --git a/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp b/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp new file mode 100644 index 000000000000..b4b9415c2027 --- /dev/null +++ b/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp @@ -0,0 +1,52 @@ +/** +GDevelop - Sticker3D Behavior Extension +Copyright (c) 2024 GDevelop Team +This project is released under the MIT License. +*/ +#include "Sticker3DBehavior.h" + +#include + +#include "GDCore/CommonTools.h" +#include "GDCore/Project/PropertyDescriptor.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/Tools/Localization.h" + +void Sticker3DBehavior::InitializeContent(gd::SerializerElement& content) { + content.SetAttribute("followRotation", true); + content.SetAttribute("destroyWithStuckToObject", false); +} + +std::map Sticker3DBehavior::GetProperties( + const gd::SerializerElement& behaviorContent) const { + std::map properties; + + properties["followRotation"] + .SetValue(behaviorContent.GetBoolAttribute("followRotation") ? "true" : "false") + .SetType("Boolean") + .SetLabel(_("Follow rotation")) + .SetDescription(_("If enabled, the 3D object will also follow the rotation of the stuck-to 3D object.")); + + properties["destroyWithStuckToObject"] + .SetValue(behaviorContent.GetBoolAttribute("destroyWithStuckToObject") ? "true" : "false") + .SetType("Boolean") + .SetLabel(_("Destroy when the 3D object it's stuck on is destroyed")) + .SetDescription(_("If enabled, this 3D object will be automatically destroyed when the stuck-to 3D object is destroyed.")); + + return properties; +} + +bool Sticker3DBehavior::UpdateProperty(gd::SerializerElement& behaviorContent, + const gd::String& name, + const gd::String& value) { + if (name == "followRotation") { + behaviorContent.SetAttribute("followRotation", value == "1"); + return true; + } + if (name == "destroyWithStuckToObject") { + behaviorContent.SetAttribute("destroyWithStuckToObject", value == "1"); + return true; + } + + return false; +} diff --git a/Extensions/Sticker3DBehavior/Sticker3DBehavior.h b/Extensions/Sticker3DBehavior/Sticker3DBehavior.h new file mode 100644 index 000000000000..a1fddb05d0c1 --- /dev/null +++ b/Extensions/Sticker3DBehavior/Sticker3DBehavior.h @@ -0,0 +1,37 @@ +/** +GDevelop - Sticker3D Behavior Extension +Copyright (c) 2024 GDevelop Team +This project is released under the MIT License. +*/ +#pragma once + +#include +#include "GDCore/Project/Behavior.h" +#include "GDCore/Project/Object.h" + +namespace gd { +class SerializerElement; +class Project; +} // namespace gd + +/** + * \brief Allow to stick 3D objects together so they move as one. + */ +class GD_EXTENSION_API Sticker3DBehavior : public gd::Behavior { + public: + Sticker3DBehavior() {}; + virtual ~Sticker3DBehavior(){}; + + virtual std::unique_ptr Clone() const override { + return gd::make_unique(*this); + } + + virtual std::map GetProperties( + const gd::SerializerElement& behaviorContent) const override; + virtual bool UpdateProperty(gd::SerializerElement& behaviorContent, + const gd::String& name, + const gd::String& value) override; + + virtual void InitializeContent( + gd::SerializerElement& behaviorContent) override; +}; diff --git a/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts new file mode 100644 index 000000000000..8802ab732ffb --- /dev/null +++ b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts @@ -0,0 +1,271 @@ +/* +GDevelop - Sticker3D Behavior Extension +Copyright (c) 2024 GDevelop Team +This project is released under the MIT License. +*/ + +namespace gdjs { + /** + * Sticks a 3D object to another 3D object, making it follow the stuck-to 3D object's + * position and optionally rotation. + * @category Behaviors > 3D Sticker + */ + export class Sticker3DRuntimeBehavior extends gdjs.RuntimeBehavior { + // Configuration + private _offsetX: float = 0; + private _offsetY: float = 0; + private _offsetZ: float = 0; + private _followRotation: boolean = true; + private _destroyWithStuckToObject: boolean = false; + + // State + private _stuckToObject: gdjs.RuntimeObject | null = null; + private _isStuck: boolean = false; + + // Store previous stuck-to 3D object rotation to detect changes + private _lastStuckToRotationX: float = 0; + private _lastStuckToRotationY: float = 0; + private _lastStuckToRotationZ: float = 0; + + constructor( + instanceContainer: gdjs.RuntimeInstanceContainer, + behaviorData: any, + owner: gdjs.RuntimeObject + ) { + super(instanceContainer, behaviorData, owner); + + this._followRotation = behaviorData.followRotation !== undefined + ? behaviorData.followRotation + : true; + this._destroyWithStuckToObject = behaviorData.destroyWithStuckToObject !== undefined + ? behaviorData.destroyWithStuckToObject + : false; + } + + override applyBehaviorOverriding(behaviorData: any): boolean { + if (behaviorData.followRotation !== undefined) { + this._followRotation = behaviorData.followRotation; + } + if (behaviorData.destroyWithStuckToObject !== undefined) { + this._destroyWithStuckToObject = behaviorData.destroyWithStuckToObject; + } + return true; + } + + override onActivate(): void { + // Behavior activated - stick will be set via action + } + + override onDeActivate(): void { + // Keep the stick information but stop updating + } + + override doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer): void { + if (!this._isStuck || !this._stuckToObject || !this._stuckToObject.isIncludedInParentCollisionMask()) { + // Stuck-to 3D object doesn't exist or was deleted + if (this._isStuck) { + if (this._destroyWithStuckToObject) { + // Destroy this 3D object when the stuck-to 3D object is destroyed + this.owner.deleteFromScene(instanceContainer); + return; + } + this.unstick(); + } + return; + } + + const owner = this.owner; + const stuckToObject = this._stuckToObject; + + // Get stuck-to 3D object's current position + const stuckToX = stuckToObject.getX(); + const stuckToY = stuckToObject.getY(); + + // Check if stuck-to 3D object has 3D capabilities + let stuckToZ = 0; + let stuckToRotationX = 0; + let stuckToRotationY = 0; + let stuckToRotationZ = stuckToObject.getAngle(); + + if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(stuckToObject)) { + stuckToZ = stuckToObject.getZ(); + stuckToRotationX = stuckToObject.getRotationX(); + stuckToRotationY = stuckToObject.getRotationY(); + } + + // Calculate absolute target position based on stuck-to 3D object position + offset + // This approach eliminates floating-point drift and ensures offsets are always applied + const targetX = stuckToX + this._offsetX; + const targetY = stuckToY + this._offsetY; + const targetZ = stuckToZ + this._offsetZ; + + // Update this object's position to the target position + owner.setX(targetX); + owner.setY(targetY); + + if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(owner)) { + owner.setZ(targetZ); + } + + // Update rotation if following rotation + if (this._followRotation) { + // Calculate rotation deltas to apply incremental changes + // This preserves any manual rotation adjustments made between frames + let deltaRotationZ = stuckToRotationZ - this._lastStuckToRotationZ; + + // Normalize angle delta to [-180, 180] to handle wrapping correctly + // Example: 350° → 10° should be +20°, not -340° + while (deltaRotationZ > 180) deltaRotationZ -= 360; + while (deltaRotationZ < -180) deltaRotationZ += 360; + + if (deltaRotationZ !== 0) { + owner.setAngle(owner.getAngle() + deltaRotationZ); + } + + if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(stuckToObject) && gdjs.Base3DHandler.is3D(owner)) { + let deltaRotationX = stuckToRotationX - this._lastStuckToRotationX; + let deltaRotationY = stuckToRotationY - this._lastStuckToRotationY; + + // Normalize X and Y rotation deltas as well + while (deltaRotationX > 180) deltaRotationX -= 360; + while (deltaRotationX < -180) deltaRotationX += 360; + while (deltaRotationY > 180) deltaRotationY -= 360; + while (deltaRotationY < -180) deltaRotationY += 360; + + if (deltaRotationX !== 0) { + owner.setRotationX(owner.getRotationX() + deltaRotationX); + } + if (deltaRotationY !== 0) { + owner.setRotationY(owner.getRotationY() + deltaRotationY); + } + } + } + + // Store current rotation for next frame delta calculation + this._lastStuckToRotationX = stuckToRotationX; + this._lastStuckToRotationY = stuckToRotationY; + this._lastStuckToRotationZ = stuckToRotationZ; + } + + /** + * Stick this 3D object to another 3D object. + * @param targetObject The 3D object to stick to + */ + stickToObject(targetObject: gdjs.RuntimeObject): void { + if (!targetObject) { + return; + } + + this._stuckToObject = targetObject; + this._isStuck = true; + + // Calculate and store initial offset based on current positions + const owner = this.owner; + this._offsetX = owner.getX() - targetObject.getX(); + this._offsetY = owner.getY() - targetObject.getY(); + + if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(owner) && gdjs.Base3DHandler.is3D(targetObject)) { + this._offsetZ = owner.getZ() - targetObject.getZ(); + } else { + this._offsetZ = 0; + } + + // Store initial rotation for delta calculation + this._lastStuckToRotationZ = targetObject.getAngle(); + + if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(targetObject)) { + this._lastStuckToRotationX = targetObject.getRotationX(); + this._lastStuckToRotationY = targetObject.getRotationY(); + } else { + this._lastStuckToRotationX = 0; + this._lastStuckToRotationY = 0; + } + } + + /** + * Unstick from the current stuck-to 3D object. + */ + unstick(): void { + this._stuckToObject = null; + this._isStuck = false; + } + + /** + * Check if this 3D object is currently stuck to another 3D object. + * @returns true if stuck, false otherwise + */ + isStuck(): boolean { + return this._isStuck && this._stuckToObject !== null; + } + + /** + * Set the X offset from the stuck-to 3D object. + * @param offsetX The X offset + */ + setOffsetX(offsetX: float): void { + this._offsetX = offsetX; + } + + /** + * Set the Y offset from the stuck-to 3D object. + * @param offsetY The Y offset + */ + setOffsetY(offsetY: float): void { + this._offsetY = offsetY; + } + + /** + * Set the Z offset from the stuck-to 3D object. + * @param offsetZ The Z offset + */ + setOffsetZ(offsetZ: float): void { + this._offsetZ = offsetZ; + } + + /** + * Get the X offset from the stuck-to 3D object. + * @returns The X offset + */ + getOffsetX(): float { + return this._offsetX; + } + + /** + * Get the Y offset from the stuck-to 3D object. + * @returns The Y offset + */ + getOffsetY(): float { + return this._offsetY; + } + + /** + * Get the Z offset from the stuck-to 3D object. + * @returns The Z offset + */ + getOffsetZ(): float { + return this._offsetZ; + } + + /** + * Set whether to follow the rotation of the stuck-to 3D object. + * @param follow true to follow rotation, false otherwise + */ + setFollowRotation(follow: boolean): void { + this._followRotation = follow; + } + + /** + * Check if this 3D object follows the rotation of the stuck-to 3D object. + * @returns true if following rotation, false otherwise + */ + followsRotation(): boolean { + return this._followRotation; + } + } + + gdjs.registerBehavior( + 'Sticker3DBehavior::Sticker3DBehavior', + gdjs.Sticker3DRuntimeBehavior + ); +} + diff --git a/GDJS/GDJS/Extensions/JsPlatform.cpp b/GDJS/GDJS/Extensions/JsPlatform.cpp index 933a4a3587eb..992fc8f5fc3f 100644 --- a/GDJS/GDJS/Extensions/JsPlatform.cpp +++ b/GDJS/GDJS/Extensions/JsPlatform.cpp @@ -59,6 +59,7 @@ gd::PlatformExtension *CreateGDJSPrimitiveDrawingExtension(); gd::PlatformExtension *CreateGDJSTextEntryObjectExtension(); gd::PlatformExtension *CreateGDJSInventoryExtension(); gd::PlatformExtension *CreateGDJSLinkedObjectsExtension(); +gd::PlatformExtension *CreateGDJSSticker3DBehaviorExtension(); gd::PlatformExtension *CreateGDJSSystemInfoExtension(); gd::PlatformExtension *CreateGDJSShopifyExtension(); gd::PlatformExtension *CreateGDJSPathfindingBehaviorExtension(); @@ -180,6 +181,9 @@ void JsPlatform::ReloadBuiltinExtensions() { AddExtension(std::shared_ptr( CreateGDJSLinkedObjectsExtension())); std::cout.flush(); + AddExtension(std::shared_ptr( + CreateGDJSSticker3DBehaviorExtension())); + std::cout.flush(); AddExtension( std::shared_ptr(CreateGDJSSystemInfoExtension())); std::cout.flush(); @@ -231,4 +235,4 @@ extern "C" gd::Platform *GD_API CreateGDPlatform() { extern "C" void GD_API DestroyGDPlatform() { JsPlatform::DestroySingleton(); } #endif -} // namespace gdjs +} // namespace gdjs \ No newline at end of file diff --git a/GDevelop.js/CMakeLists.txt b/GDevelop.js/CMakeLists.txt index fb0841083021..bc51796a1041 100644 --- a/GDevelop.js/CMakeLists.txt +++ b/GDevelop.js/CMakeLists.txt @@ -139,10 +139,11 @@ target_link_libraries(GD PrimitiveDrawing) target_link_libraries(GD TextEntryObject) target_link_libraries(GD Inventory) target_link_libraries(GD LinkedObjects) +target_link_libraries(GD Sticker3DBehavior) target_link_libraries(GD SystemInfo) target_link_libraries(GD Shopify) target_link_libraries(GD PathfindingBehavior) target_link_libraries(GD PhysicsBehavior) target_link_libraries(GD ParticleSystem) target_link_libraries(GD Scene3D) -target_link_libraries(GD SpineObject) +target_link_libraries(GD SpineObject) \ No newline at end of file diff --git a/newIDE/electron-app/app/package-lock.json b/newIDE/electron-app/app/package-lock.json index 63dae7d62944..f049d744a791 100644 --- a/newIDE/electron-app/app/package-lock.json +++ b/newIDE/electron-app/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "gdevelop", - "version": "5.6.255", + "version": "5.6.257", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gdevelop", - "version": "5.6.255", + "version": "5.6.257", "license": "MIT", "dependencies": { "@electron/remote": "2.1.2", From 2fa9624d74f87bf6c2f3e319f3311cbda1e0a073 Mon Sep 17 00:00:00 2001 From: NB2030 Date: Tue, 17 Feb 2026 16:39:22 +0300 Subject: [PATCH 2/4] small fix --- Extensions/CMakeLists.txt | 2 +- Extensions/Sticker3DBehavior/JsExtension.cpp | 4 ++-- Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts | 2 +- GDJS/GDJS/Extensions/JsPlatform.cpp | 2 +- GDevelop.js/CMakeLists.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Extensions/CMakeLists.txt b/Extensions/CMakeLists.txt index 0f3c5a8eaba2..adf3dbc1378a 100644 --- a/Extensions/CMakeLists.txt +++ b/Extensions/CMakeLists.txt @@ -33,4 +33,4 @@ set( # Automatically add all listed extensions foreach(extension ${GD_EXTENSIONS}) add_subdirectory(${extension}) -endforeach() \ No newline at end of file +endforeach() diff --git a/Extensions/Sticker3DBehavior/JsExtension.cpp b/Extensions/Sticker3DBehavior/JsExtension.cpp index a50e0326284c..26c7a4d334e2 100644 --- a/Extensions/Sticker3DBehavior/JsExtension.cpp +++ b/Extensions/Sticker3DBehavior/JsExtension.cpp @@ -30,7 +30,7 @@ class Sticker3DBehaviorJsExtension : public gd::PlatformExtension { behavior .AddScopedAction( - "LinkToObject", + "StickTo3DObject", _("Stick to a 3D object"), _("Stick this 3D object to another 3D object. It will follow the position of the 3D object it is stuck to."), _("Stick _PARAM0_ to _PARAM2_"), @@ -40,7 +40,7 @@ class Sticker3DBehaviorJsExtension : public gd::PlatformExtension { .AddParameter("object", _("Object"), "", false) .AddParameter("behavior", _("Behavior"), "Sticker3DBehavior") .AddParameter("objectPtr", _("3D object to stick to"), "", false) - .SetFunctionName("stickToObject"); + .SetFunctionName("stickTo3DObject"); behavior .AddScopedAction( diff --git a/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts index 8802ab732ffb..6521d17d9421 100644 --- a/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts +++ b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts @@ -151,7 +151,7 @@ namespace gdjs { * Stick this 3D object to another 3D object. * @param targetObject The 3D object to stick to */ - stickToObject(targetObject: gdjs.RuntimeObject): void { + stickTo3DObject(targetObject: gdjs.RuntimeObject): void { if (!targetObject) { return; } diff --git a/GDJS/GDJS/Extensions/JsPlatform.cpp b/GDJS/GDJS/Extensions/JsPlatform.cpp index 992fc8f5fc3f..f8909087314e 100644 --- a/GDJS/GDJS/Extensions/JsPlatform.cpp +++ b/GDJS/GDJS/Extensions/JsPlatform.cpp @@ -235,4 +235,4 @@ extern "C" gd::Platform *GD_API CreateGDPlatform() { extern "C" void GD_API DestroyGDPlatform() { JsPlatform::DestroySingleton(); } #endif -} // namespace gdjs \ No newline at end of file +} // namespace gdjs diff --git a/GDevelop.js/CMakeLists.txt b/GDevelop.js/CMakeLists.txt index bc51796a1041..d155a23f370c 100644 --- a/GDevelop.js/CMakeLists.txt +++ b/GDevelop.js/CMakeLists.txt @@ -146,4 +146,4 @@ target_link_libraries(GD PathfindingBehavior) target_link_libraries(GD PhysicsBehavior) target_link_libraries(GD ParticleSystem) target_link_libraries(GD Scene3D) -target_link_libraries(GD SpineObject) \ No newline at end of file +target_link_libraries(GD SpineObject) From 49c7a8f0b328ebd935d8cf4c4881740deca1d1f8 Mon Sep 17 00:00:00 2001 From: NB2030 Date: Tue, 17 Feb 2026 18:41:44 +0300 Subject: [PATCH 3/4] offset modes --- .../Sticker3DBehavior/Sticker3DBehavior.cpp | 13 +++++ .../sticker3druntimebehavior.ts | 58 ++++++++++++++++--- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp b/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp index b4b9415c2027..57ff29868fc0 100644 --- a/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp +++ b/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp @@ -15,6 +15,7 @@ This project is released under the MIT License. void Sticker3DBehavior::InitializeContent(gd::SerializerElement& content) { content.SetAttribute("followRotation", true); content.SetAttribute("destroyWithStuckToObject", false); + content.SetAttribute("offsetMode", "world"); } std::map Sticker3DBehavior::GetProperties( @@ -27,6 +28,14 @@ std::map Sticker3DBehavior::GetProperties( .SetLabel(_("Follow rotation")) .SetDescription(_("If enabled, the 3D object will also follow the rotation of the stuck-to 3D object.")); + properties["offsetMode"] + .SetValue(behaviorContent.GetStringAttribute("offsetMode", "world")) + .SetType("Choice") + .AddExtraInfo("world") + .AddExtraInfo("local") + .SetLabel(_("Offset mode")) + .SetDescription(_("World space: offset stays fixed in world coordinates (for static objects). Local space: offset rotates with the stuck-to object (for vehicles, trailers, etc).")); + properties["destroyWithStuckToObject"] .SetValue(behaviorContent.GetBoolAttribute("destroyWithStuckToObject") ? "true" : "false") .SetType("Boolean") @@ -43,6 +52,10 @@ bool Sticker3DBehavior::UpdateProperty(gd::SerializerElement& behaviorContent, behaviorContent.SetAttribute("followRotation", value == "1"); return true; } + if (name == "offsetMode") { + behaviorContent.SetAttribute("offsetMode", value); + return true; + } if (name == "destroyWithStuckToObject") { behaviorContent.SetAttribute("destroyWithStuckToObject", value == "1"); return true; diff --git a/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts index 6521d17d9421..d6de5f9ea026 100644 --- a/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts +++ b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts @@ -17,6 +17,7 @@ namespace gdjs { private _offsetZ: float = 0; private _followRotation: boolean = true; private _destroyWithStuckToObject: boolean = false; + private _offsetMode: string = "world"; // "world" or "local" // State private _stuckToObject: gdjs.RuntimeObject | null = null; @@ -40,6 +41,9 @@ namespace gdjs { this._destroyWithStuckToObject = behaviorData.destroyWithStuckToObject !== undefined ? behaviorData.destroyWithStuckToObject : false; + this._offsetMode = behaviorData.offsetMode !== undefined + ? behaviorData.offsetMode + : "world"; } override applyBehaviorOverriding(behaviorData: any): boolean { @@ -49,6 +53,9 @@ namespace gdjs { if (behaviorData.destroyWithStuckToObject !== undefined) { this._destroyWithStuckToObject = behaviorData.destroyWithStuckToObject; } + if (behaviorData.offsetMode !== undefined) { + this._offsetMode = behaviorData.offsetMode; + } return true; } @@ -66,7 +73,7 @@ namespace gdjs { if (this._isStuck) { if (this._destroyWithStuckToObject) { // Destroy this 3D object when the stuck-to 3D object is destroyed - this.owner.deleteFromScene(instanceContainer); + this.owner.deleteFromScene(); return; } this.unstick(); @@ -93,11 +100,28 @@ namespace gdjs { stuckToRotationY = stuckToObject.getRotationY(); } - // Calculate absolute target position based on stuck-to 3D object position + offset - // This approach eliminates floating-point drift and ensures offsets are always applied - const targetX = stuckToX + this._offsetX; - const targetY = stuckToY + this._offsetY; - const targetZ = stuckToZ + this._offsetZ; + // Calculate target position based on offset mode + let targetX: float, targetY: float, targetZ: float; + + if (this._offsetMode === "local") { + // Local space: rotate offset based on stuck-to object's rotation + const angleRad = (stuckToRotationZ * Math.PI) / 180; + const cos = Math.cos(angleRad); + const sin = Math.sin(angleRad); + + // Rotate offset in 2D (X-Y plane) + const rotatedOffsetX = this._offsetX * cos - this._offsetY * sin; + const rotatedOffsetY = this._offsetX * sin + this._offsetY * cos; + + targetX = stuckToX + rotatedOffsetX; + targetY = stuckToY + rotatedOffsetY; + targetZ = stuckToZ + this._offsetZ; // Z offset stays the same + } else { + // World space: offset stays fixed in world coordinates + targetX = stuckToX + this._offsetX; + targetY = stuckToY + this._offsetY; + targetZ = stuckToZ + this._offsetZ; + } // Update this object's position to the target position owner.setX(targetX); @@ -161,8 +185,26 @@ namespace gdjs { // Calculate and store initial offset based on current positions const owner = this.owner; - this._offsetX = owner.getX() - targetObject.getX(); - this._offsetY = owner.getY() - targetObject.getY(); + const targetAngle = targetObject.getAngle(); + + if (this._offsetMode === "local") { + // Local space: calculate offset in target object's local coordinates + // This requires inverse rotation transformation + const angleRad = (targetAngle * Math.PI) / 180; + const cos = Math.cos(-angleRad); // Negative for inverse rotation + const sin = Math.sin(-angleRad); + + const worldOffsetX = owner.getX() - targetObject.getX(); + const worldOffsetY = owner.getY() - targetObject.getY(); + + // Transform world offset to local space + this._offsetX = worldOffsetX * cos - worldOffsetY * sin; + this._offsetY = worldOffsetX * sin + worldOffsetY * cos; + } else { + // World space: offset in world coordinates + this._offsetX = owner.getX() - targetObject.getX(); + this._offsetY = owner.getY() - targetObject.getY(); + } if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(owner) && gdjs.Base3DHandler.is3D(targetObject)) { this._offsetZ = owner.getZ() - targetObject.getZ(); From d625bab54462f72ca41b304abd6b09451f595704 Mon Sep 17 00:00:00 2001 From: NB2030 Date: Wed, 18 Feb 2026 04:48:43 +0300 Subject: [PATCH 4/4] fix destroy checkbox --- .../Sticker3DBehavior/Sticker3DBehavior.cpp | 2 +- .../sticker3druntimebehavior.ts | 132 ++++++++---------- 2 files changed, 61 insertions(+), 73 deletions(-) diff --git a/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp b/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp index 57ff29868fc0..0ec481821a11 100644 --- a/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp +++ b/Extensions/Sticker3DBehavior/Sticker3DBehavior.cpp @@ -34,7 +34,7 @@ std::map Sticker3DBehavior::GetProperties( .AddExtraInfo("world") .AddExtraInfo("local") .SetLabel(_("Offset mode")) - .SetDescription(_("World space: offset stays fixed in world coordinates (for static objects). Local space: offset rotates with the stuck-to object (for vehicles, trailers, etc).")); + .SetDescription(_("World space: offset stays fixed in world coordinates. Local space: offset rotates with the stuck-to object around the Z-axis (yaw rotation only), suitable for vehicles and characters on flat surfaces.")); properties["destroyWithStuckToObject"] .SetValue(behaviorContent.GetBoolAttribute("destroyWithStuckToObject") ? "true" : "false") diff --git a/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts index d6de5f9ea026..65cb386e1e90 100644 --- a/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts +++ b/Extensions/Sticker3DBehavior/sticker3druntimebehavior.ts @@ -22,11 +22,9 @@ namespace gdjs { // State private _stuckToObject: gdjs.RuntimeObject | null = null; private _isStuck: boolean = false; - - // Store previous stuck-to 3D object rotation to detect changes - private _lastStuckToRotationX: float = 0; - private _lastStuckToRotationY: float = 0; - private _lastStuckToRotationZ: float = 0; + + // Callback for when stuck-to object is deleted + private _onStuckToObjectDeleted: (() => void) | null = null; constructor( instanceContainer: gdjs.RuntimeInstanceContainer, @@ -67,17 +65,14 @@ namespace gdjs { // Keep the stick information but stop updating } + override onDestroy(): void { + // Clean up when behavior is destroyed + this.unstick(); + } + override doStepPreEvents(instanceContainer: gdjs.RuntimeInstanceContainer): void { - if (!this._isStuck || !this._stuckToObject || !this._stuckToObject.isIncludedInParentCollisionMask()) { - // Stuck-to 3D object doesn't exist or was deleted - if (this._isStuck) { - if (this._destroyWithStuckToObject) { - // Destroy this 3D object when the stuck-to 3D object is destroyed - this.owner.deleteFromScene(); - return; - } - this.unstick(); - } + // Check if we're stuck and have a valid stuck-to object + if (!this._isStuck || !this._stuckToObject) { return; } @@ -104,15 +99,16 @@ namespace gdjs { let targetX: float, targetY: float, targetZ: float; if (this._offsetMode === "local") { - // Local space: rotate offset based on stuck-to object's rotation + // Local space: rotate offset based on stuck-to object's Z rotation (2D) + // Only Z-axis rotation is applied to keep objects on the same plane (suitable for vehicles, characters) const angleRad = (stuckToRotationZ * Math.PI) / 180; const cos = Math.cos(angleRad); const sin = Math.sin(angleRad); - + // Rotate offset in 2D (X-Y plane) const rotatedOffsetX = this._offsetX * cos - this._offsetY * sin; const rotatedOffsetY = this._offsetX * sin + this._offsetY * cos; - + targetX = stuckToX + rotatedOffsetX; targetY = stuckToY + rotatedOffsetY; targetZ = stuckToZ + this._offsetZ; // Z offset stays the same @@ -133,42 +129,15 @@ namespace gdjs { // Update rotation if following rotation if (this._followRotation) { - // Calculate rotation deltas to apply incremental changes - // This preserves any manual rotation adjustments made between frames - let deltaRotationZ = stuckToRotationZ - this._lastStuckToRotationZ; - - // Normalize angle delta to [-180, 180] to handle wrapping correctly - // Example: 350° → 10° should be +20°, not -340° - while (deltaRotationZ > 180) deltaRotationZ -= 360; - while (deltaRotationZ < -180) deltaRotationZ += 360; - - if (deltaRotationZ !== 0) { - owner.setAngle(owner.getAngle() + deltaRotationZ); - } + // Copy the stuck-to object's rotation directly + // This ensures the sticker always has the exact same orientation as the stuck-to object + owner.setAngle(stuckToRotationZ); if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(stuckToObject) && gdjs.Base3DHandler.is3D(owner)) { - let deltaRotationX = stuckToRotationX - this._lastStuckToRotationX; - let deltaRotationY = stuckToRotationY - this._lastStuckToRotationY; - - // Normalize X and Y rotation deltas as well - while (deltaRotationX > 180) deltaRotationX -= 360; - while (deltaRotationX < -180) deltaRotationX += 360; - while (deltaRotationY > 180) deltaRotationY -= 360; - while (deltaRotationY < -180) deltaRotationY += 360; - - if (deltaRotationX !== 0) { - owner.setRotationX(owner.getRotationX() + deltaRotationX); - } - if (deltaRotationY !== 0) { - owner.setRotationY(owner.getRotationY() + deltaRotationY); - } + owner.setRotationX(stuckToRotationX); + owner.setRotationY(stuckToRotationY); } } - - // Store current rotation for next frame delta calculation - this._lastStuckToRotationX = stuckToRotationX; - this._lastStuckToRotationY = stuckToRotationY; - this._lastStuckToRotationZ = stuckToRotationZ; } /** @@ -180,47 +149,60 @@ namespace gdjs { return; } + // Unregister previous callback if exists + if (this._onStuckToObjectDeleted && this._stuckToObject) { + this._stuckToObject.unregisterDestroyCallback(this._onStuckToObjectDeleted); + } + this._stuckToObject = targetObject; this._isStuck = true; + // Register callback to handle when stuck-to object is deleted + this._onStuckToObjectDeleted = () => { + if (this._destroyWithStuckToObject) { + // Destroy this 3D object when the stuck-to 3D object is destroyed + this.owner.deleteFromScene(); + } else { + // Just unstick without destroying + this.unstick(); + } + }; + targetObject.registerDestroyCallback(this._onStuckToObjectDeleted); + // Calculate and store initial offset based on current positions const owner = this.owner; - const targetAngle = targetObject.getAngle(); if (this._offsetMode === "local") { // Local space: calculate offset in target object's local coordinates - // This requires inverse rotation transformation + // This requires inverse rotation transformation (2D only for Z-axis) + const targetAngle = targetObject.getAngle(); const angleRad = (targetAngle * Math.PI) / 180; const cos = Math.cos(-angleRad); // Negative for inverse rotation const sin = Math.sin(-angleRad); - + const worldOffsetX = owner.getX() - targetObject.getX(); const worldOffsetY = owner.getY() - targetObject.getY(); - - // Transform world offset to local space + + // Transform world offset to local space (2D rotation) this._offsetX = worldOffsetX * cos - worldOffsetY * sin; this._offsetY = worldOffsetX * sin + worldOffsetY * cos; + + // Z offset is always calculated in world space (not affected by rotation) + if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(owner) && gdjs.Base3DHandler.is3D(targetObject)) { + this._offsetZ = owner.getZ() - targetObject.getZ(); + } else { + this._offsetZ = 0; + } } else { // World space: offset in world coordinates this._offsetX = owner.getX() - targetObject.getX(); this._offsetY = owner.getY() - targetObject.getY(); - } - - if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(owner) && gdjs.Base3DHandler.is3D(targetObject)) { - this._offsetZ = owner.getZ() - targetObject.getZ(); - } else { - this._offsetZ = 0; - } - - // Store initial rotation for delta calculation - this._lastStuckToRotationZ = targetObject.getAngle(); - - if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(targetObject)) { - this._lastStuckToRotationX = targetObject.getRotationX(); - this._lastStuckToRotationY = targetObject.getRotationY(); - } else { - this._lastStuckToRotationX = 0; - this._lastStuckToRotationY = 0; + + if (gdjs.Base3DHandler && gdjs.Base3DHandler.is3D(owner) && gdjs.Base3DHandler.is3D(targetObject)) { + this._offsetZ = owner.getZ() - targetObject.getZ(); + } else { + this._offsetZ = 0; + } } } @@ -228,6 +210,12 @@ namespace gdjs { * Unstick from the current stuck-to 3D object. */ unstick(): void { + // Unregister the destroy callback + if (this._onStuckToObjectDeleted && this._stuckToObject) { + this._stuckToObject.unregisterDestroyCallback(this._onStuckToObjectDeleted); + this._onStuckToObjectDeleted = null; + } + this._stuckToObject = null; this._isStuck = false; }