From 41e66edac66a2307e0b50b977ca1d4826fc23316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henri=20Hyyryl=C3=A4inen?= Date: Wed, 1 Nov 2023 14:41:32 +0200 Subject: [PATCH] Physics shutdown and other fixing (#4542) * Did some fixes for simulation world releases and physics destruction shutdown * Fixed microbe emission system not running at all added a world release bug suppression I saw crashing once * Fixed shutting down world simulation with disabled bodies * Fixed engulfing system not clearing endosome references correctly, and fixed scale getting for engulfed microbes * Turns out cilia animation did work already adjusted just some TODO comments instead --- .../common_systems/FadeOutActionSystem.cs | 5 +- .../PhysicsBodyCreationSystem.cs | 35 ++++++++---- .../PhysicsBodyDisablingSystem.cs | 26 +++++++-- .../PhysicsCollisionManagementSystem.cs | 19 +++++++ .../common_systems/SoundEffectSystem.cs | 47 +++++++++++++++ .../common_systems/SpatialAttachSystem.cs | 10 ++++ src/engine/physics/NativePhysicsBody.cs | 7 ++- src/general/base_stage/WorldSimulation.cs | 44 ++++++++++---- .../base_stage/WorldSimulationWithPhysics.cs | 35 +++++++++--- src/microbe_stage/MicrobeStage.cs | 2 + src/microbe_stage/MicrobeWorldSimulation.cs | 28 ++++++++- src/microbe_stage/Spawners.cs | 8 +-- .../components/OrganelleContainer.cs | 3 +- .../editor/CellEditorComponent.cs | 4 +- .../organelle_components/CiliaComponent.cs | 4 +- src/microbe_stage/systems/EngulfingSystem.cs | 57 +++++++++++++++++-- .../systems/MicrobeEmissionSystem.cs | 1 + .../systems/MicrobeMovementSystem.cs | 5 +- 18 files changed, 284 insertions(+), 56 deletions(-) diff --git a/src/engine/common_systems/FadeOutActionSystem.cs b/src/engine/common_systems/FadeOutActionSystem.cs index a4da11671c..e3bd90d80e 100644 --- a/src/engine/common_systems/FadeOutActionSystem.cs +++ b/src/engine/common_systems/FadeOutActionSystem.cs @@ -133,7 +133,10 @@ private void DisableParticleEmission(Entity entity) var particles = spatial.GraphicalInstance as Particles; if (particles == null) - throw new NullReferenceException("Graphical instance casted as particles is null"); + { + throw new NullReferenceException( + $"Graphical instance ({spatial.GraphicalInstance}) casted as particles is null"); + } particles.Emitting = false; diff --git a/src/engine/common_systems/PhysicsBodyCreationSystem.cs b/src/engine/common_systems/PhysicsBodyCreationSystem.cs index bdbea057ff..c00ade0485 100644 --- a/src/engine/common_systems/PhysicsBodyCreationSystem.cs +++ b/src/engine/common_systems/PhysicsBodyCreationSystem.cs @@ -18,23 +18,41 @@ public sealed class PhysicsBodyCreationSystem : AEntitySetSystem { private readonly IWorldSimulationWithPhysics worldSimulationWithPhysics; - private readonly OnBodyDeleted? deleteCallback; private readonly List createdBodies = new(); - public PhysicsBodyCreationSystem(IWorldSimulationWithPhysics worldSimulationWithPhysics, - OnBodyDeleted? deleteCallback, World world, IParallelRunner runner) : base(world, runner) + public PhysicsBodyCreationSystem(IWorldSimulationWithPhysics worldSimulationWithPhysics, World world, + IParallelRunner runner) : base(world, runner) { this.worldSimulationWithPhysics = worldSimulationWithPhysics; - this.deleteCallback = deleteCallback; } - public delegate void OnBodyDeleted(NativePhysicsBody body); + /// + /// Destroys a body collision body immediately. This is needed to be called by the world to ensure that + /// physics bodies of destroyed entities are immediately destroyed + /// + public void OnEntityDestroyed(in Entity entity) + { + if (!entity.Has()) + return; + + ref var physics = ref entity.Get(); + + if (physics.Body != null) + { + if (!createdBodies.Remove(physics.Body)) + GD.PrintErr("Body creation system told about a destroyed physics body it didn't create"); + + worldSimulationWithPhysics.DestroyBody(physics.Body); + + physics.Body = null; + } + } protected override void PreUpdate(float delta) { - // TODO: would it be better to have the world take care of destroying physics bodies when the entity - // destruction is triggered? + // Immediate body destruction is handled by the world, but we still do this to detect if a physics + // component gets removed while things are running foreach (var createdBody in createdBodies) { createdBody.Marked = false; @@ -136,9 +154,6 @@ private bool DestroyBodyIfNotMarked(NativePhysicsBody body) if (body.Marked) return false; - // Notify external things about the deleted body - deleteCallback?.Invoke(body); - // TODO: ensure this works fine if the body is currently in disabled state worldSimulationWithPhysics.DestroyBody(body); diff --git a/src/engine/common_systems/PhysicsBodyDisablingSystem.cs b/src/engine/common_systems/PhysicsBodyDisablingSystem.cs index e7017909fc..8ec438841a 100644 --- a/src/engine/common_systems/PhysicsBodyDisablingSystem.cs +++ b/src/engine/common_systems/PhysicsBodyDisablingSystem.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Components; + using DefaultEcs; using DefaultEcs.System; using Godot; using World = DefaultEcs.World; @@ -22,6 +23,23 @@ public PhysicsBodyDisablingSystem(PhysicalWorld physicalWorld, World world) : ba this.physicalWorld = physicalWorld; } + /// + /// Forgets about a disabled body on entity. Needs to be called before + /// so that this can see the body to be destroyed + /// + public void OnEntityDestroyed(in Entity entity) + { + if (!entity.Has()) + return; + + ref var physics = ref entity.Get(); + + if (physics.Body != null) + { + disabledBodies.Remove(physics.Body); + } + } + // TODO: figure out where this would need to be called /// /// Needs to be called when a body is deleted so that state tracking for body disabling can remove it @@ -85,11 +103,9 @@ private void Dispose(bool disposing) { if (disposing) { - // TODO: would this be needed? - foreach (var disabledBody in disabledBodies) - { - physicalWorld.DestroyBody(disabledBody); - } + disabledBodies.Clear(); + + // The bodies are destroyed by the creation system / the world. Also see OnEntityDestroyed } } } diff --git a/src/engine/common_systems/PhysicsCollisionManagementSystem.cs b/src/engine/common_systems/PhysicsCollisionManagementSystem.cs index d0bc9c1374..5ead4f1793 100644 --- a/src/engine/common_systems/PhysicsCollisionManagementSystem.cs +++ b/src/engine/common_systems/PhysicsCollisionManagementSystem.cs @@ -27,6 +27,25 @@ public PhysicsCollisionManagementSystem(PhysicalWorld physicalWorld, World world this.physicalWorld = physicalWorld; } + /// + /// Stops collision recording for a removed entity + /// + public void OnEntityDestroyed(in Entity entity) + { + if (!entity.Has()) + return; + + ref var collisionManagement = ref entity.Get(); + + if (collisionManagement.ActiveCollisions != null) + { + ref var physics = ref entity.Get(); + + if (physics.Body != null) + physicalWorld.BodyStopCollisionRecording(physics.Body); + } + } + protected override void Update(float delta, in Entity entity) { ref var physics = ref entity.Get(); diff --git a/src/engine/common_systems/SoundEffectSystem.cs b/src/engine/common_systems/SoundEffectSystem.cs index 2b65eb4b72..30c6fe5eb5 100644 --- a/src/engine/common_systems/SoundEffectSystem.cs +++ b/src/engine/common_systems/SoundEffectSystem.cs @@ -52,6 +52,44 @@ public void ReportPlayerPosition(Vector3 position) playerPosition = position; } + public void FreeNodeResources() + { + // Free all used player nodes + foreach (var player in freePositionalPlayers) + { + player.QueueFree(); + } + + freePositionalPlayers.Clear(); + + foreach (var player in usedPositionalPlayers) + { + player.Player.QueueFree(); + } + + usedPositionalPlayers.Clear(); + + foreach (var player in free2DPlayers) + { + player.QueueFree(); + } + + free2DPlayers.Clear(); + + foreach (var player in used2DPlayers) + { + player.Player.QueueFree(); + } + + used2DPlayers.Clear(); + } + + public override void Dispose() + { + Dispose(true); + base.Dispose(); + } + protected override void PreUpdate(float delta) { base.PreUpdate(delta); @@ -481,6 +519,15 @@ private void ExpireOldAudioCacheEntries(float delta) soundCacheEntriesToClear.Clear(); } + private void Dispose(bool disposing) + { + if (disposing) + { + // Free cached sounds immediately + soundCache.Clear(); + } + } + /// /// This and derived classes include extra info that is needed on a currently playing audio player for this /// system diff --git a/src/engine/common_systems/SpatialAttachSystem.cs b/src/engine/common_systems/SpatialAttachSystem.cs index 0f90ad1193..7ba5f9e989 100644 --- a/src/engine/common_systems/SpatialAttachSystem.cs +++ b/src/engine/common_systems/SpatialAttachSystem.cs @@ -23,6 +23,16 @@ public SpatialAttachSystem(Node godotWorldRoot, World world) : base(world, null) this.godotWorldRoot = godotWorldRoot; } + public void FreeNodeResources() + { + foreach (var entry in attachedSpatialInstances) + { + entry.Key.QueueFree(); + } + + attachedSpatialInstances.Clear(); + } + protected override void PreUpdate(float state) { // Unmark all diff --git a/src/engine/physics/NativePhysicsBody.cs b/src/engine/physics/NativePhysicsBody.cs index 2bb0e2a47a..e677e5c72a 100644 --- a/src/engine/physics/NativePhysicsBody.cs +++ b/src/engine/physics/NativePhysicsBody.cs @@ -33,7 +33,7 @@ public class NativePhysicsBody : IDisposable, IEquatable /// /// Storage variable for collision recording, when this is active the pin handle is used to pin down this - /// piece of memory to ensure the native code size can directly write here with pointers + /// piece of memory to ensure the native code side can directly write here with pointers /// private PhysicsCollision[]? activeCollisions; @@ -60,7 +60,8 @@ internal NativePhysicsBody(IntPtr nativeInstance) /// /// Active collisions for this body. Only updated if started through - /// + /// . For entities prefer + /// /// public PhysicsCollision[]? ActiveCollisions => activeCollisions; @@ -69,6 +70,8 @@ internal NativePhysicsBody(IntPtr nativeInstance) /// public bool MicrobeControlEnabled { get; set; } + public bool IsDisposed => disposed; + public static bool operator ==(NativePhysicsBody? left, NativePhysicsBody? right) { return Equals(left, right); diff --git a/src/general/base_stage/WorldSimulation.cs b/src/general/base_stage/WorldSimulation.cs index 2c0c21ba6a..10c7d9103b 100644 --- a/src/general/base_stage/WorldSimulation.cs +++ b/src/general/base_stage/WorldSimulation.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using DefaultEcs; using DefaultEcs.Command; using Godot; @@ -30,11 +31,6 @@ public abstract class WorldSimulation : IWorldSimulation protected float accumulatedLogicTime; - /// - /// True when multithreaded system updates are running - /// - protected bool runningMultithreaded; - // TODO: implement saving // ReSharper disable once CollectionNeverQueried.Local private readonly List entitiesToNotSave = new(); @@ -172,10 +168,10 @@ public virtual bool ProcessLogic(float delta) public Entity CreateEmptyEntity() { // Ensure thread unsafe operation doesn't happen - if (runningMultithreaded) + if (Processing) { throw new InvalidOperationException( - "Can't use thread unsafe create entity at this time, use deferred create"); + "Can't use entity create at this time, use deferred create"); } return entities.CreateEntity(); @@ -202,13 +198,12 @@ public void DestroyAllEntities(Entity? skip = null) { ProcessDestroyQueue(); - foreach (var entity in entities) + // If destroy all is used a lot then this temporary memory use (ToList) here should be solved + foreach (var entity in entities.ToList()) { if (entity == skip) continue; - // TODO: check that this destroy while looping entities doesn't cause an issue - PerformEntityDestroy(entity); } @@ -330,6 +325,14 @@ public void SetLogicMaxUpdateRate(float logicFPS) minimumTimeBetweenLogicUpdates = 1 / logicFPS; } + public virtual void FreeNodeResources() + { + } + + /// + /// Note that often when this is disposed, the Nodes are already disposed so this has to skip releasing them. + /// If that is not the case it is required to call before calling Dispose. + /// public void Dispose() { Dispose(true); @@ -383,10 +386,26 @@ protected void PerformEntityDestroy(Entity entity) { entitiesToNotSave.Remove(entity); + OnEntityDestroyed(entity); + // Destroy the entity from the ECS system entity.Dispose(); } + /// + /// Called when an entity is being destroyed (but before it is). Used by derived worlds to for example delete + /// physics data. + /// + /// + /// + /// Note that when the entire world is disposed this is not called for each entity. + /// + /// + /// The entity that is being destroyed + protected virtual void OnEntityDestroyed(in Entity entity) + { + } + protected void ThrowIfNotInitialized() { if (!Initialized) @@ -395,7 +414,10 @@ protected void ThrowIfNotInitialized() protected virtual void Dispose(bool disposing) { - DestroyAllEntities(); + // TODO: decide if destroying all entities on world destroy is needed. It seems that disposing systems can + // release their allocated resources so this all can just be let to go for memory to be garbage collected later + // for faster world destroy + // DestroyAllEntities(); if (disposing) { diff --git a/src/general/base_stage/WorldSimulationWithPhysics.cs b/src/general/base_stage/WorldSimulationWithPhysics.cs index 9febe48945..d2ec47863d 100644 --- a/src/general/base_stage/WorldSimulationWithPhysics.cs +++ b/src/general/base_stage/WorldSimulationWithPhysics.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Godot; /// @@ -50,7 +51,15 @@ public void DestroyBody(NativePhysicsBody body) return; } + // Stop collision recording if it is active to make sure the memory for that is returned to the pool + if (body.ActiveCollisions != null) + physics.BodyStopCollisionRecording(body); + physics.DestroyBody(body); + + // Other code is not allowed to hold on to physics bodies on entities that are destroyed so we dispose this + // here to get the native side wrapper released as well + body.Dispose(); } protected override void WaitForStartedPhysicsRun() @@ -71,22 +80,32 @@ protected override void OnStartPhysicsRunIfTime(float delta) protected override void Dispose(bool disposing) { + // Derived classes should also wait for this before destroying things + WaitForStartedPhysicsRun(); + ReleaseUnmanagedResources(); - if (disposing) - { - physics.Dispose(); - } + + // if (disposing) + // { + // + // } base.Dispose(disposing); } private void ReleaseUnmanagedResources() { - foreach (var createdBody in createdBodies) + while (createdBodies.Count > 0) { - physics.DestroyBody(createdBody); + var body = createdBodies[createdBodies.Count - 1]; + + // This should never happen but this is here in case this does happen to give a better error message + if (body.IsDisposed) + throw new Exception("World physics body was disposed by someone else"); + + DestroyBody(body); } - createdBodies.Clear(); + physics.Dispose(); } } diff --git a/src/microbe_stage/MicrobeStage.cs b/src/microbe_stage/MicrobeStage.cs index 8fdf4dbaec..afd4a92202 100644 --- a/src/microbe_stage/MicrobeStage.cs +++ b/src/microbe_stage/MicrobeStage.cs @@ -836,6 +836,8 @@ protected override void Dispose(bool disposing) { if (disposing) { + WorldSimulation.Dispose(); + GuidanceLinePath?.Dispose(); } diff --git a/src/microbe_stage/MicrobeWorldSimulation.cs b/src/microbe_stage/MicrobeWorldSimulation.cs index c6ce60847e..4ec0a6ebe1 100644 --- a/src/microbe_stage/MicrobeWorldSimulation.cs +++ b/src/microbe_stage/MicrobeWorldSimulation.cs @@ -1,4 +1,5 @@ -using DefaultEcs.Threading; +using DefaultEcs; +using DefaultEcs.Threading; using Godot; using Newtonsoft.Json; using Systems; @@ -52,6 +53,7 @@ public class MicrobeWorldSimulation : WorldSimulationWithPhysics private MicrobeAISystem microbeAI = null!; private MicrobeCollisionSoundSystem microbeCollisionSoundSystem = null!; private MicrobeDeathSystem microbeDeathSystem = null!; + private MicrobeEmissionSystem microbeEmissionSystem = null!; private MicrobeEventCallbackSystem microbeEventCallbackSystem = null!; private MicrobeFlashingSystem microbeFlashingSystem = null!; private MicrobeMovementSoundSystem microbeMovementSoundSystem = null!; @@ -122,7 +124,7 @@ public void Init(Node visualDisplayRoot, CompoundCloudSystem cloudSystem) fadeOutActionSystem = new FadeOutActionSystem(this, EntitySystem, parallelRunner); pathBasedSceneLoader = new PathBasedSceneLoader(EntitySystem, nonParallelRunner); physicsBodyControlSystem = new PhysicsBodyControlSystem(physics, EntitySystem, parallelRunner); - physicsBodyCreationSystem = new PhysicsBodyCreationSystem(this, null, EntitySystem, nonParallelRunner); + physicsBodyCreationSystem = new PhysicsBodyCreationSystem(this, EntitySystem, nonParallelRunner); physicsBodyDisablingSystem = new PhysicsBodyDisablingSystem(physics, EntitySystem); physicsCollisionManagementSystem = new PhysicsCollisionManagementSystem(physics, EntitySystem, parallelRunner); physicsUpdateAndPositionSystem = new PhysicsUpdateAndPositionSystem(physics, EntitySystem, parallelRunner); @@ -156,6 +158,7 @@ public void Init(Node visualDisplayRoot, CompoundCloudSystem cloudSystem) // TODO: this definitely needs to be (along with the process system) the first systems to be multithreaded microbeAI = new MicrobeAISystem(cloudSystem, EntitySystem, parallelRunner); microbeCollisionSoundSystem = new MicrobeCollisionSoundSystem(EntitySystem, parallelRunner); + microbeEmissionSystem = new MicrobeEmissionSystem(this, cloudSystem, EntitySystem, parallelRunner); microbeEventCallbackSystem = new MicrobeEventCallbackSystem(cloudSystem, microbeAI, EntitySystem); microbeFlashingSystem = new MicrobeFlashingSystem(EntitySystem, parallelRunner); @@ -242,6 +245,14 @@ public void ClearPlayerLocationDependentCaches() SpawnSystem.ClearSpawnCoordinates(); } + public override void FreeNodeResources() + { + base.FreeNodeResources(); + + soundEffectSystem.FreeNodeResources(); + spatialAttachSystem.FreeNodeResources(); + } + internal void OverrideMicrobeAIRandomSeed(int seed) { microbeAI.OverrideAIRandomSeed(seed); @@ -303,6 +314,8 @@ protected override void OnProcessFixedLogic(float delta) microbeAI.Update(delta); } + microbeEmissionSystem.Update(delta); + countLimitedDespawnSystem.Update(delta); SpawnSystem.Update(delta); @@ -342,8 +355,18 @@ protected override void OnProcessFixedLogic(float delta) reportedPlayerPosition = null; } + protected override void OnEntityDestroyed(in Entity entity) + { + base.OnEntityDestroyed(in entity); + + physicsBodyDisablingSystem.OnEntityDestroyed(entity); + physicsBodyCreationSystem.OnEntityDestroyed(entity); + } + protected override void Dispose(bool disposing) { + WaitForStartedPhysicsRun(); + if (disposing) { nonParallelRunner.Dispose(); @@ -384,6 +407,7 @@ protected override void Dispose(bool disposing) microbeAI.Dispose(); microbeCollisionSoundSystem.Dispose(); microbeDeathSystem.Dispose(); + microbeEmissionSystem.Dispose(); microbeEventCallbackSystem.Dispose(); microbeFlashingSystem.Dispose(); microbeMovementSoundSystem.Dispose(); diff --git a/src/microbe_stage/Spawners.cs b/src/microbe_stage/Spawners.cs index 7db1b5a67c..88f190767f 100644 --- a/src/microbe_stage/Spawners.cs +++ b/src/microbe_stage/Spawners.cs @@ -103,8 +103,6 @@ public static EntityCommandRecorder SpawnAgentProjectileWithoutFinalizing(IWorld entity = SpawnAgentProjectileWithoutFinalizing(worldSimulation, recorder, properties, amount, lifetime, location, direction, scale, emitter); - worldSimulation.FinishRecordingEntityCommands(recorder); - return recorder; } @@ -166,6 +164,9 @@ public static EntityRecord SpawnAgentProjectileWithoutFinalizing(IWorldSimulatio // Callbacks are initialized by ToxinCollisionSystem }); + // Needed for fade actions + entity.Set(); + entity.Set(new ReadableName(properties.Name)); return entity; @@ -199,9 +200,6 @@ public static EntityRecord SpawnChunkWithoutFinalizing(IWorldSimulation worldSim // Resolve the final chunk settings as the chunk configuration is a group of potential things var selectedMesh = chunkType.Meshes.Random(random); - // TODO: do something with these properties: - // selectedMesh.SceneModelPath, - // Chunk is spawned with random rotation (in the 2D plane if it's an Easter egg) var rotationAxis = chunkType.EasterEgg ? new Vector3(0, 1, 0) : new Vector3(0, 1, 1); diff --git a/src/microbe_stage/components/OrganelleContainer.cs b/src/microbe_stage/components/OrganelleContainer.cs index 9dad4dab3e..c7ddbcec06 100644 --- a/src/microbe_stage/components/OrganelleContainer.cs +++ b/src/microbe_stage/components/OrganelleContainer.cs @@ -38,7 +38,8 @@ public struct OrganelleContainer [JsonIgnore] public List? ThrustComponents; - // TODO: implement cilia animation speed + // Note that this exists here for the potential future need that MicrobeMovementSystem will need to use cilia + // and reduce rotation rate if not enough ATP to rotate at full speed // ReSharper disable once CollectionNeverQueried.Global /// /// Cilia components that need to be animated when the cell is rotating fast diff --git a/src/microbe_stage/editor/CellEditorComponent.cs b/src/microbe_stage/editor/CellEditorComponent.cs index 31cbc8473a..beb131a1f6 100644 --- a/src/microbe_stage/editor/CellEditorComponent.cs +++ b/src/microbe_stage/editor/CellEditorComponent.cs @@ -1259,6 +1259,8 @@ protected override void Dispose(bool disposing) { if (disposing) { + previewSimulation.Dispose(); + if (TopPanelPath != null) { TopPanelPath.Dispose(); @@ -1303,8 +1305,6 @@ protected override void Dispose(bool disposing) AutoEvoPredictionExplanationLabelPath.Dispose(); OrganelleUpgradeGUIPath.Dispose(); } - - previewSimulation.Dispose(); } base.Dispose(disposing); diff --git a/src/microbe_stage/organelle_components/CiliaComponent.cs b/src/microbe_stage/organelle_components/CiliaComponent.cs index c4079ad35a..440d291515 100644 --- a/src/microbe_stage/organelle_components/CiliaComponent.cs +++ b/src/microbe_stage/organelle_components/CiliaComponent.cs @@ -88,13 +88,12 @@ public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity mic if (timeSinceRotationSample < Constants.CILIA_ROTATION_SAMPLE_INTERVAL) return; - // ref var control = ref microbeEntity.Get(); - // Calculate how fast the cell is turning for controlling the animation speed var rawRotation = previousCellRotation.Value.AngleTo(currentCellRotation); var rotationSpeed = rawRotation * Constants.CILIA_ROTATION_ANIMATION_SPEED_MULTIPLIER; // TODO: pulling cilia reimplementation + // ref var control = ref microbeEntity.Get(); // if (control.State == MicrobeState.Engulf && attractorArea != null) // { // // We are using cilia pulling, play animation at fixed rate @@ -127,6 +126,7 @@ public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity mic if (availableEnergy < requiredEnergy) { // TODO: slow down rotation when we don't have enough ATP to use our cilia + // Might need to move this code to MicrobeMovementSystem for this to be possible } } diff --git a/src/microbe_stage/systems/EngulfingSystem.cs b/src/microbe_stage/systems/EngulfingSystem.cs index 9cc99fe092..a36909eab2 100644 --- a/src/microbe_stage/systems/EngulfingSystem.cs +++ b/src/microbe_stage/systems/EngulfingSystem.cs @@ -277,6 +277,7 @@ protected override void Update(float delta, in Entity entity) { endosome.Hide(); DeleteEndosome(endosome); + RemoveEndosomeFromEntity(entity, endosome); } transportData.TargetValuesToLerp = (null, transportData.OriginalScale, null); @@ -347,6 +348,9 @@ protected override void PostUpdate(float state) if (container.TryGetValue(toDelete.Value, out var endosome)) { + if (!container.Remove(toDelete.Value)) + GD.PrintErr("Failed to remove endosome from entity list it was just deleted from"); + DeleteEndosome(endosome); } else @@ -539,20 +543,40 @@ private static Vector3 CalculateEngulfableTargetPosition(ref CellProperties engu boundingBoxSize = Vector3.One; - if (targetSpatial.GraphicalInstance is GeometryInstance geometryInstance) + if (targetSpatial.GraphicalInstance != null) { - boundingBoxSize = geometryInstance.GetAabb().Size; + var geometryInstance = targetSpatial.GraphicalInstance as GeometryInstance; + + // TODO: should this use EntityMaterial.AutoRetrieveModelPath to find the path of the graphics instance + // in the node? This probably doesn't work for all kinds of chunks correctly + + // Most engulfables have their graphical node as the first child of their primary node + if (geometryInstance == null && targetSpatial.GraphicalInstance.GetChildCount() > 0) + { + geometryInstance = targetSpatial.GraphicalInstance.GetChild(0) as GeometryInstance; + } + + if (geometryInstance != null) + { + boundingBoxSize = geometryInstance.GetAabb().Size; + } + else + { + GD.PrintErr("Engulfed something that couldn't have AABB calculated (graphical instance: ", + targetSpatial.GraphicalInstance, ")"); + } } else { - GD.PrintErr("Engulfed something that couldn't have AABB calculated (graphical instance: ", - targetSpatial.GraphicalInstance, ")"); + GD.PrintErr( + "Engulfed something with no graphical instance set, can't calculate bounding box for scaling"); } // In the case of flat mesh (like membrane) we don't want the endosome to end up completely flat // as it can cause unwanted visual glitch if (boundingBoxSize.y < Mathf.Epsilon) boundingBoxSize = new Vector3(boundingBoxSize.x, 0.1f, boundingBoxSize.z); + return relativePosition; } @@ -684,6 +708,23 @@ private Endosome CreateEndosome(in Entity entity, ref SpatialInstance endosomePa return null; } + private void RemoveEndosomeFromEntity(in Entity entity, Endosome endosome) + { + if (entityEngulfingEndosomeGraphics.TryGetValue(entity, out var dataContainer)) + { + foreach (var entry in dataContainer) + { + if (entry.Value == endosome) + { + if (dataContainer.Remove(entry.Key)) + return; + } + } + } + + GD.PrintErr("Failed to unlink endosome from engulfer that should have been using it"); + } + private void EjectEverythingFromDeadEngulfer(ref Engulfer engulfer, in Entity entity) { if (engulfer.EngulfedObjects == null) @@ -1334,6 +1375,9 @@ private bool AnimateBulkTransport(in Entity entity, ref Engulfable engulfable, i animation.TargetValuesToLerp.EndosomeScale.Value, fraction); } + // Update endosome position in the animation + phagosome.Translation = relativePosition.RelativePosition; + return false; } @@ -1392,6 +1436,11 @@ private void SetPhagosomeColours(in Entity entity, Color colour) } } + /// + /// Performs the deletion of endosome operation. Note that contexts where the endosome may still be used + /// by an entity needs to be called + /// + /// The endosome object that is no longer required private void DeleteEndosome(Endosome endosome) { try diff --git a/src/microbe_stage/systems/MicrobeEmissionSystem.cs b/src/microbe_stage/systems/MicrobeEmissionSystem.cs index d67c1e2648..f9c058fa2e 100644 --- a/src/microbe_stage/systems/MicrobeEmissionSystem.cs +++ b/src/microbe_stage/systems/MicrobeEmissionSystem.cs @@ -44,6 +44,7 @@ protected override void Update(float delta, in Entity entity) ref var control = ref entity.Get(); // Reduce agent emission cooldown + // TODO: is it faster to check than to just blindly always subtract delta here? control.AgentEmissionCooldown -= delta; if (control.AgentEmissionCooldown < 0) control.AgentEmissionCooldown = 0; diff --git a/src/microbe_stage/systems/MicrobeMovementSystem.cs b/src/microbe_stage/systems/MicrobeMovementSystem.cs index d896a0f88a..ccdb9f0ae2 100644 --- a/src/microbe_stage/systems/MicrobeMovementSystem.cs +++ b/src/microbe_stage/systems/MicrobeMovementSystem.cs @@ -106,9 +106,8 @@ private static float CalculateRotationSpeed(in Entity entity, ref OrganelleConta { float rotationSpeed = organelles.RotationSpeed; - // Note that cilia rotation taking ATP is in CiliaComponent class - // TODO: especially as there's a comment in there about slowing down rotation when there isn't enough ATP - // for the cilia + // Note that cilia taking ATP is actually calculated later, this is the max speed rotation calculation + // only // Lower value is faster rotation if (CheatManager.Speed > 1 && entity.Has())