From 7026be1c90aaccfa4f525cc9f433c1db0c7f6bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Mon, 22 Jan 2024 23:14:10 +0100 Subject: [PATCH] 2D Physics and script improvements - fixed time step - Fixed crash caused by physics script callback being registered in simulation mode - Moved 2D Physics config to file - Finally gave up on the attempt to keep X and Y rotation when applying new Z-rotation from 2D rigidbody to transform and now I can sleep well - Streamlined starting and stopping of scenes - Scripts: execute all inits and OnCreates before OnUpdate --- GlitchyEditor/src/EditorLayer.bf | 16 +- GlitchyEngine/src/Scripting/ScriptGlue.bf | 6 +- GlitchyEngine/src/World/Physics2DSettings.bf | 38 +++ GlitchyEngine/src/World/Scene.bf | 261 +++++++++++-------- 4 files changed, 196 insertions(+), 125 deletions(-) create mode 100644 GlitchyEngine/src/World/Physics2DSettings.bf diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index 1bfcb9f..ab5f103 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -919,8 +919,7 @@ namespace GlitchyEditor /// @param startSimulation If set to true, the physics simulation will be initialized for the given scene. private void SetActiveScene(Scene scene, bool startRuntime, bool startSimulation, PlayMode? newPlayMode = null) { - _activeScene?.StopRuntime(); - _activeScene?.StopSimulation(); + _activeScene?.Stop(); if (newPlayMode != null) { @@ -939,11 +938,7 @@ namespace GlitchyEditor if (scene != null) { - if (startRuntime) - scene.StartRuntime(); - - if (startSimulation) - scene.StartSimulation(); + scene.Start(startRuntime, startSimulation); } SetReference!(_activeScene, scene); @@ -1058,10 +1053,7 @@ namespace GlitchyEditor /// Sets the current editor scene private void SetEditorScene(Scene scene, Dictionary serializedObjects = null) { - if (_editorScene != null) - { - _editorScene.StopRuntime(); - } + _editorScene?.Stop(); SetReference!(_editorScene, scene); _editor.CurrentScene = _editorScene; @@ -1069,7 +1061,7 @@ namespace GlitchyEditor if (_editorScene != null) { - _editorScene.StartRuntime(); + _editorScene.Start(startRuntime: true, startSimulation: false); if (serializedObjects != null) { diff --git a/GlitchyEngine/src/Scripting/ScriptGlue.bf b/GlitchyEngine/src/Scripting/ScriptGlue.bf index 6891ffa..305e90c 100644 --- a/GlitchyEngine/src/Scripting/ScriptGlue.bf +++ b/GlitchyEngine/src/Scripting/ScriptGlue.bf @@ -731,8 +731,7 @@ static class ScriptGlue { Scene scene = ScriptEngine.Context; - var box2DGravity = Box2D.World.GetGravity(scene.[Friend]_physicsWorld2D); - gravity = *(float2*)&box2DGravity; + gravity = scene.Physics2DSettings.Gravity; } [RegisterCall("ScriptGlue::Physics2D_SetGravity")] @@ -740,8 +739,7 @@ static class ScriptGlue { Scene scene = ScriptEngine.Context; -#unwarn - Box2D.World.SetGravity(scene.[Friend]_physicsWorld2D, gravity); + scene.Physics2DSettings.Gravity = gravity; } #endregion Physics2D diff --git a/GlitchyEngine/src/World/Physics2DSettings.bf b/GlitchyEngine/src/World/Physics2DSettings.bf new file mode 100644 index 0000000..3cfd4ac --- /dev/null +++ b/GlitchyEngine/src/World/Physics2DSettings.bf @@ -0,0 +1,38 @@ +using GlitchyEngine.Math; +using System; + +namespace GlitchyEngine.World; + +struct Physics2DSettings +{ + private Scene _scene; + + [Inline] + private Box2D.b2World* _physicsWorld => _scene.[Friend]_physicsWorld2D; + + public float2 _gravity = .(0.0f, -9.8f); + + public float2 Gravity + { + get => _gravity; + set mut + { + if (all(_gravity == value)) + return; + + _gravity = value; + + if (_physicsWorld != null) + Box2D.World.SetGravity(_physicsWorld, _gravity); + } + } + + public int32 VelocityIterations = 6; + public int32 PositionIterations = 2; + public int32 ParticleIterations = 2; + + public this(Scene scene) + { + _scene = scene; + } +} diff --git a/GlitchyEngine/src/World/Scene.bf b/GlitchyEngine/src/World/Scene.bf index 19fd53b..8b59cb2 100644 --- a/GlitchyEngine/src/World/Scene.bf +++ b/GlitchyEngine/src/World/Scene.bf @@ -25,6 +25,13 @@ namespace GlitchyEngine.World b2ContactListener _contactListener; b2Draw draw = .(); + private float _fixedDeltaTime = 1.0f / 60.0f; + + private Physics2DSettings _physics2dSettings = .(this); + + [Inline] + public ref Physics2DSettings Physics2DSettings => ref _physics2dSettings; + private Dictionary _onComponentAddedHandlers = new .() ~ delete _; private uint32 _viewportWidth, _viewportHeight; @@ -196,8 +203,6 @@ namespace GlitchyEngine.World target.AddComponent(*component); } } - - b2Vec2 _gravity2D = .(0.0f, -9.8f); static b2BodyType GetBox2DBodyType(Rigidbody2DComponent.BodyType bodyType) { @@ -215,7 +220,37 @@ namespace GlitchyEngine.World } } - public void StartRuntime() + /** + * Starts the scene. + * @param startRuntime If true, the script runtime will be started. + * @param startSimulation If true, the physics engine will be started. + */ + public void Start(bool startRuntime, bool startSimulation) + { + if (startSimulation) + StartSimulation(); + + if (startRuntime) + { + StartRuntime(); + + if (startSimulation) + { + SetupPhysicsCallbacks(); + } + } + } + + /** + * Stops the scene by shutting down the script runtime and physics simulation. + */ + public void Stop() + { + StopRuntime(); + StopSimulation(); + } + + private void StartRuntime() { ScriptEngine.StartRuntime(this); @@ -228,16 +263,16 @@ namespace GlitchyEngine.World } } - public void StopRuntime() + private void StopRuntime() { ScriptEngine.StopRuntime(); } - public void StartSimulation() + private void StartSimulation() { // TODO: Add Setting for the user to define whether Physics will be initialized or not? - _physicsWorld2D = World.Create(_gravity2D); + _physicsWorld2D = World.Create(_physics2dSettings.Gravity); draw.drawPolygonCallback = (vertices, vertexCount, color, userData) => { @@ -290,8 +325,6 @@ namespace GlitchyEngine.World World.SetDebugDrawFlags(_physicsWorld2D, .e_shapeBit); InitPhysics2D(); - - SetupPhysicsCallbacks(); } private void SetupPhysicsCallbacks() @@ -593,7 +626,7 @@ namespace GlitchyEngine.World polygonCollider.RuntimeFixture = fixture; } - public void StopSimulation() + private void StopSimulation() { if (_physicsWorld2D == null) return; @@ -623,6 +656,114 @@ namespace GlitchyEngine.World private append List _destroyScriptQueue = .(); + float physicsDelta = 0; + + private void UpdatePhysics(GameTime gameTime) + { + physicsDelta += gameTime.DeltaTime; + + while (physicsDelta > _fixedDeltaTime) + { + physicsDelta -= _fixedDeltaTime; + + Debug.Profiler.ProfileScope!("2D Physics Iteration"); + + Box2D.World.Step(_physicsWorld2D, _fixedDeltaTime, Physics2DSettings.VelocityIterations, Physics2DSettings.PositionIterations, Physics2DSettings.ParticleIterations); + + // Retrieve transform from Box2D + for (var entry in _ecsWorld.Enumerate()) + { + Entity entity = .(entry.Entity, this); + + var transform = entity.Transform; + var rigidbody = entry.Component; + + b2Body* body = rigidbody.RuntimeBody; + b2Vec2 position = Box2D.Body.GetPosition(body); + float angle = Box2D.Body.GetAngle(body); + b2Transform bodyTransform = Box2D.Body.GetTransform(body); + + float cos = sqrt((1 + bodyTransform.q.c) / 2); + float sin = sqrt((1 - bodyTransform.q.c) / 2) * sign(bodyTransform.q.s); + + Quaternion rotation = .(0, 0, sin, cos)..Normalize(); + + if (entity.Parent != null) + { + Matrix worldToParent = entity.Parent.Value.Transform.WorldTransform.Invert(); + + float4 newLocalPosition = worldToParent * float4(position, 0, 1); + transform.Position = float3(newLocalPosition.XY, transform.Position.Z); + + // TODO: when the parent of the rigidbody-entity is rotated, the rotation is applied wrong... + Matrix.Decompose(worldToParent, let p, var worldToParentRotation, let s); + //Quaternion newLocalRotation = (worldToParentRotation..Normalize() * rotation..Normalize())..Normalize(); + //Quaternion newLocalRotation = (rotation * worldToParentRotation..Normalize())..Normalize(); + //Quaternion newLocalRotation = (worldToParentRotation * rotation)..Normalize(); + Quaternion newLocalRotation = rotation..Normalize(); + //Quaternion newLocalRotation = Quaternion.FromEulerAngles(0, 0, angle)..Normalize(); + // TODO: when the parent has rotation on X or Y everything breaks even more... + //transform.Rotation = (Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0)..Normalize() * newLocalRotation)..Normalize();//(newLocalRotation * Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0))..Normalize(); + //transform.Rotation = ((transform.Rotation..Normalize() * Quaternion.FromEulerAngles(0, 0, transform.RotationEuler.Z)..Normalize())..Normalize() * newLocalRotation)..Normalize();//(newLocalRotation * Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0))..Normalize(); + transform.Rotation = (newLocalRotation)..Normalize();//(newLocalRotation * Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0))..Normalize(); + } + else + { + transform.Position = .(position.x, position.y, transform.Position.Z); + transform.Rotation = rotation; + } + } + + // TODO: Execute Fixed-Updates + } + } + + private void UpdateScripts(GameTime gameTime, UpdateMode mode) + { + Debug.Profiler.ProfileScope!("Update scripts"); + + // all OnCreates have to be executed before the first OnUpdate! + + // TODO: This could also be handled with a queue! + // Init and OnCreate + for (let (entity, script) in _ecsWorld.Enumerate()) + { + if (script.IsCreated) + continue; + + if (!script.IsInitialized) + ScriptEngine.InitializeInstance(Entity(entity, this), script); + + // Skip OnCreate and OnUpdate invocation if we didn't create an instance + // (happens, if script component has no script class associated) + // Also skip if we are in edit mode and the class doesn't have the RunInEditMode-Attribute + if (script.Instance == null || (mode.HasFlag(.EditMode) && !script.Instance.ScriptClass.RunInEditMode)) + continue; + + script.Instance.InvokeOnCreate(); + } + + // OnUpdate + for (let (entity, script) in _ecsWorld.Enumerate()) + { + if (script.Instance == null) + continue; + + // TODO: When the entity is destroyed in OnCreate it's OnUpdate will still be called. Is this fine? + // It would require that we somehow track whether the entity is to be deleted. We technically have this info but would probably need + // some faster way. If we for some reason ever happen to implement such a fast way we can check for planned deletion here (or after on Create and just continue;). + + // Update the script, if we aren't in editor or it has RunInEditMode-Attribute + if (!mode.HasFlag(.EditMode) || script.Instance.ScriptClass.RunInEditMode) + { + if (_updateBlockList.Contains(entity)) + _updateBlockList.Remove(entity); + else + script.Instance.InvokeOnUpdate(gameTime.DeltaTime); + } + } + } + public void Update(GameTime gameTime, UpdateMode mode) { Debug.Profiler.ProfileRendererFunction!(); @@ -631,52 +772,7 @@ namespace GlitchyEngine.World if (mode.HasFlag(.Scripts)) { - Debug.Profiler.ProfileScope!("Update scripts"); - - // Run scripts - for (let (entity, script) in _ecsWorld.Enumerate()) - { - ScriptInstance scriptInstance = null; - - if (!script.IsCreated) - { - if (!script.IsInitialized) - ScriptEngine.InitializeInstance(Entity(entity, this), script); - - scriptInstance = script.Instance; - - // Skip OnCreate and OnUpdate invocation if we didn't create an instance - // (happens, if script component has no script class associated) - // Also skip if we are in edit mode and the class doesn't have the RunInEditMode-Attribute - if (scriptInstance == null || (mode.HasFlag(.EditMode) && !scriptInstance.ScriptClass.RunInEditMode)) - continue; - - // OnCreate can remove the script, so we have to make sure that we have a reference and it survives. - // Todo: Technically we can rely on scriptInstance surviving a delete because we always defer deletion (unless we might not?) - scriptInstance.AddRef(); - - scriptInstance.InvokeOnCreate(); - } - else - { - scriptInstance = script.Instance..AddRef(); - } - - // TODO: When the entity is destroyed in OnCreate it's OnUpdate will still be called. Is this fine? - // It would require that we somehow track whether the entity is to be deleted. We technically have this info but would probably need - // some faster way. If we for some reason ever happen to implement such a fast way we can check for planned deletion here (or after on Create and just continue;). - - // Update the script, if we aren't in editor or it has RunInEditMode-Attribute - if (!mode.HasFlag(.EditMode) || scriptInstance.ScriptClass.RunInEditMode) - { - if (_updateBlockList.Contains(entity)) - _updateBlockList.Remove(entity); - else - scriptInstance.InvokeOnUpdate(gameTime.DeltaTime); - } - - scriptInstance?.ReleaseRef(); - } + UpdateScripts(gameTime, mode); } if (!_destroyScriptQueue.IsEmpty) @@ -705,60 +801,7 @@ namespace GlitchyEngine.World if (mode.HasFlag(.Physics)) { - // Update 2D physics - { - const int32 velocityIterations = 6; - const int32 positionIterations = 2; - const int32 particleIterations = 2; - - Box2D.World.Step(_physicsWorld2D, gameTime.DeltaTime, velocityIterations, positionIterations, particleIterations); - - // Retrieve transform from Box2D - for (var entry in _ecsWorld.Enumerate()) - { - Entity entity = .(entry.Entity, this); - - var transform = entity.Transform; - var rigidbody = entry.Component; - - b2Body* body = rigidbody.RuntimeBody; - b2Vec2 position = Box2D.Body.GetPosition(body); - float angle = Box2D.Body.GetAngle(body); - b2Transform bodyTransform = Box2D.Body.GetTransform(body); - - float cos = sqrt((1 + bodyTransform.q.c) / 2); - float sin = sqrt((1 - bodyTransform.q.c) / 2) * sign(bodyTransform.q.s); - - Quaternion rotation = .(0, 0, sin, cos)..Normalize(); - - if (entity.Parent != null) - { - Matrix worldToParent = entity.Parent.Value.Transform.WorldTransform.Invert(); - - float4 newLocalPosition = worldToParent * float4(position, 0, 1); - transform.Position = float3(newLocalPosition.XY, transform.Position.Z); - - // TODO: when the parent of the rigidbody-entity is rotated, the rotation is applied wrong... - Matrix.Decompose(worldToParent, let p, var worldToParentRotation, let s); - //Quaternion newLocalRotation = (worldToParentRotation..Normalize() * rotation..Normalize())..Normalize(); - //Quaternion newLocalRotation = (rotation * worldToParentRotation..Normalize())..Normalize(); - //Quaternion newLocalRotation = (worldToParentRotation * rotation)..Normalize(); - Quaternion newLocalRotation = rotation..Normalize(); - //Quaternion newLocalRotation = Quaternion.FromEulerAngles(0, 0, angle)..Normalize(); - // TODO: when the parent has rotation on X or Y everything breaks even more... - //transform.Rotation = (Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0)..Normalize() * newLocalRotation)..Normalize();//(newLocalRotation * Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0))..Normalize(); - //transform.Rotation = ((transform.Rotation..Normalize() * Quaternion.FromEulerAngles(0, 0, transform.RotationEuler.Z)..Normalize())..Normalize() * newLocalRotation)..Normalize();//(newLocalRotation * Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0))..Normalize(); - transform.Rotation = (newLocalRotation)..Normalize();//(newLocalRotation * Quaternion.FromEulerAngles(transform.RotationEuler.Y, transform.RotationEuler.X, 0))..Normalize(); - } - else - { - transform.Position = .(position.x, position.y, transform.Position.Z); - //transform.RotationEuler = .(transform.RotationEuler.XY, angle); - //transform.RotationEuler = .(0, 0, angle); - transform.Rotation = normalize(rotation * normalize(Quaternion.FromEulerAngles(transform.RotationEuler.X, transform.RotationEuler.Y, 0))); - } - } - } + UpdatePhysics(gameTime); } }