diff --git a/src/microbe_stage/IMembraneDataSource.cs b/src/microbe_stage/IMembraneDataSource.cs
index 1a160cda79..9cca39bb36 100644
--- a/src/microbe_stage/IMembraneDataSource.cs
+++ b/src/microbe_stage/IMembraneDataSource.cs
@@ -1,6 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Generic;
+using System.Linq;
using Godot;
///
@@ -9,6 +10,9 @@
public interface IMembraneDataSource
{
public Vector2[] HexPositions { get; }
+
+ public Vector2[]? MulticellularPositions { get; }
+
public int HexPositionCount { get; }
public MembraneType Type { get; }
}
@@ -18,14 +22,18 @@ public interface IMembraneDataSource
///
public struct MembraneGenerationParameters : IMembraneDataSource
{
- public MembraneGenerationParameters(Vector2[] hexPositions, int hexPositionCount, MembraneType type)
+ public MembraneGenerationParameters(Vector2[] hexPositions, int hexPositionCount, MembraneType type, Vector2[]? multicellularPositions, Vector2? thisCellPosition)
{
HexPositions = hexPositions;
+ MulticellularPositions = multicellularPositions;
+ CellPositionInMulticellular = thisCellPosition;
HexPositionCount = hexPositionCount;
Type = type;
}
public Vector2[] HexPositions { get; }
+ public Vector2[]? MulticellularPositions { get; }
+ public Vector2? CellPositionInMulticellular { get; }
public int HexPositionCount { get; }
public MembraneType Type { get; }
@@ -89,7 +97,7 @@ public static MembranePointData GetOrComputeMembraneShape(IReadOnlyList 0);
}
public static bool MembraneDataFieldsEqual(this IMembraneDataSource dataSource, IMembraneDataSource other)
{
- return dataSource.MembraneDataFieldsEqual(other.HexPositions, other.HexPositionCount, other.Type);
+ return dataSource.MembraneDataFieldsEqual(other.HexPositions, other.HexPositionCount, other.Type, other.MulticellularPositions);
}
public static bool MembraneDataFieldsEqual(this IMembraneDataSource dataSource, Vector2[] otherPoints,
- int otherPointCount, MembraneType otherType)
+ int otherPointCount, MembraneType otherType, Vector2[]? multicellularPositions)
{
if (!dataSource.Type.Equals(otherType))
return false;
@@ -162,6 +172,25 @@ public static bool MembraneDataFieldsEqual(this IMembraneDataSource dataSource,
var sourcePoints = dataSource.HexPositions;
+ if (dataSource.MulticellularPositions != null)
+ {
+ if (multicellularPositions == null)
+ return false;
+
+ if (!dataSource.MulticellularPositions.SequenceEqual(multicellularPositions))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (multicellularPositions != null)
+ {
+ return false;
+
+ }
+ }
+
for (int i = 0; i < count; ++i)
{
if (sourcePoints[i] != otherPoints[i])
diff --git a/src/microbe_stage/MembranePointData.cs b/src/microbe_stage/MembranePointData.cs
index 65da4bad70..238176bb14 100644
--- a/src/microbe_stage/MembranePointData.cs
+++ b/src/microbe_stage/MembranePointData.cs
@@ -23,11 +23,12 @@ public sealed class MembranePointData : IMembraneDataSource, ICacheableData
private bool disposed;
public MembranePointData(Vector2[] hexPositions, int hexPositionCount, MembraneType type,
- IReadOnlyList verticesToCopy)
+ IReadOnlyList verticesToCopy, Vector2[]? multicellularPositions)
{
HexPositions = hexPositions;
Type = type;
HexPositionCount = hexPositionCount;
+ MulticellularPositions = multicellularPositions;
// Setup mesh to be generated (on the main thread) only when required
finalMesh = new Lazy<(ArrayMesh Mesh, int SurfaceIndex)>(() =>
@@ -82,6 +83,11 @@ public MembranePointData(Vector2[] hexPositions, int hexPositionCount, MembraneT
///
public Vector2[] HexPositions { get; }
+ ///
+ /// Positions of other cells in multicellular organism
+ ///
+ public Vector2[]? MulticellularPositions { get; }
+
public int HexPositionCount { get; }
public MembraneType Type { get; }
diff --git a/src/microbe_stage/MembraneShapeGenerator.cs b/src/microbe_stage/MembraneShapeGenerator.cs
index e70a6ccd2e..6087a7c663 100644
--- a/src/microbe_stage/MembraneShapeGenerator.cs
+++ b/src/microbe_stage/MembraneShapeGenerator.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Godot;
using Array = Godot.Collections.Array;
@@ -54,7 +55,8 @@ public static MembraneShapeGenerator GetThreadSpecificGenerator()
/// Computed data in a cache entry format (to be used with , which should be
/// checked for existing data before computing new data)
///
- public MembranePointData GenerateShape(Vector2[] hexPositions, int hexCount, MembraneType membraneType)
+ public MembranePointData GenerateShape(Vector2[] hexPositions, int hexCount, MembraneType membraneType,
+ Vector2[]? cellPositions, Vector2? thisCellPosition)
{
// The length in pixels (probably not accurate?) of a side of the square that bounds the membrane.
// Half the side length of the original square that is compressed to make the membrane.
@@ -109,20 +111,23 @@ public MembranePointData GenerateShape(Vector2[] hexPositions, int hexCount, Mem
// ReSharper restore PossibleLossOfFraction
// Get new membrane points for vertices2D
- GenerateMembranePoints(hexPositions, hexCount, membraneType);
+ GenerateMembranePoints(hexPositions, hexCount, membraneType, cellPositions, thisCellPosition);
+
+ GD.Print(cellPositions == null);
// This makes a copy of the vertices so the data is safe to modify in further calls to this method
- return new MembranePointData(hexPositions, hexCount, membraneType, vertices2D);
+ return new MembranePointData(hexPositions, hexCount, membraneType, vertices2D, cellPositions);
}
public MembranePointData GenerateShape(ref MembraneGenerationParameters parameters)
{
- return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type);
+ return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type,
+ parameters.MulticellularPositions, parameters.CellPositionInMulticellular);
}
public MembranePointData GenerateShape(IMembraneDataSource parameters)
{
- return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type);
+ return GenerateShape(parameters.HexPositions, parameters.HexPositionCount, parameters.Type, null, null);
}
///
@@ -464,7 +469,8 @@ private static ArrayMesh BuildEngulfMesh(Vector2[] vertices2D, int vertexCount,
return generatedMesh;
}
- private void GenerateMembranePoints(Vector2[] hexPositions, int hexCount, MembraneType membraneType)
+ private void GenerateMembranePoints(Vector2[] hexPositions, int hexCount, MembraneType membraneType,
+ Vector2[]? cellPositions, Vector2? thisCellPosition)
{
// Move all the points in the source buffer close to organelles
// This operation used to be iterative but this is now a much faster version that moves things all the way in
@@ -550,5 +556,75 @@ private void GenerateMembranePoints(Vector2[] hexPositions, int hexCount, Membra
vertices2D[i] = point + movement;
}
+
+ var average = Vector2.Zero;
+
+ foreach (var vertex in vertices2D)
+ {
+ average += vertex;
+ }
+
+ average /= vertices2D.Count;
+
+ // Multicellular matrix
+ if (thisCellPosition != null && cellPositions != null)
+ {
+ // Make into constant unless you will forget
+ var verticeDistanceMultiplier = 0.8f;
+
+ for (int i = 0; i < vertices2D.Count; ++i)
+ {
+ var relativeVertex = vertices2D[i] - average;
+
+ bool facesAnEdge = false;
+ float minMultiplier = float.MaxValue;
+
+ foreach (var cellPos in cellPositions)
+ {
+ if (cellPos == thisCellPosition)
+ continue;
+
+ // Coordinates of such a point on the Voronoi edge, that a line from the cell's center to this
+ // point is perpendicular to the edge.
+ var edge = (cellPos + thisCellPosition.Value) * 0.5f;
+
+ var relativeEdge = (edge - thisCellPosition.Value) * 2.0f;
+
+ float dotProduct = relativeVertex.Dot(relativeEdge);
+
+ // If the dotproduct is less that this value, then this edge faces a direction different
+ // than that of the vertex, so it doesn't need to be considered
+ if (dotProduct <= 0.0f)
+ continue;
+
+ // The vertex pos, when multiplied by this, should be placed at the edge
+ float multiplier = relativeEdge.LengthSquared() / dotProduct;
+
+ if (multiplier < minMultiplier)
+ {
+ minMultiplier = multiplier;
+ facesAnEdge = true;
+ }
+ }
+
+ if (!facesAnEdge)
+ {
+ minMultiplier = 1.0f;
+ }
+ else if (minMultiplier > 3.0f)
+ {
+ minMultiplier = 1.0f;
+ }
+
+ vertices2D[i] = average + relativeVertex * minMultiplier * verticeDistanceMultiplier;
+ }
+
+ for (int i = 0, end = startingBuffer.Count; i < end; ++i)
+ {
+ var movement = thisCellPosition.Value;
+
+ startingBuffer[i] = startingBuffer[i] + movement;
+ }
+ }
}
}
diff --git a/src/microbe_stage/systems/MicrobeVisualsSystem.cs b/src/microbe_stage/systems/MicrobeVisualsSystem.cs
index 1e80e9b86d..932a968a60 100644
--- a/src/microbe_stage/systems/MicrobeVisualsSystem.cs
+++ b/src/microbe_stage/systems/MicrobeVisualsSystem.cs
@@ -11,6 +11,7 @@
using DefaultEcs;
using DefaultEcs.System;
using Godot;
+using HarmonyLib;
using World = DefaultEcs.World;
///
@@ -136,8 +137,17 @@ protected override void Update(float delta, in Entity entity)
ref var materialStorage = ref entity.Get();
+ MembranePointData? data = null;
+
// Background thread membrane generation
- var data = GetMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer);
+ if (entity.Has())
+ {
+ data = GetMulticellularMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer, ref entity.Get());
+ }
+ else
+ {
+ data = GetMembraneDataIfReadyOrStartGenerating(ref cellProperties, ref organelleContainer);
+ }
if (data == null)
{
@@ -222,7 +232,70 @@ protected override void PostUpdate(float state)
var hexes = MembraneComputationHelpers.PrepareHexPositionsForMembraneCalculations(
organelleContainer.Organelles!.Organelles, out var hexCount);
- var hash = MembraneComputationHelpers.ComputeMembraneDataHash(hexes, hexCount, cellProperties.MembraneType);
+ var hash = MembraneComputationHelpers.ComputeMembraneDataHash(hexes, hexCount, cellProperties.MembraneType, false);
+
+ var cachedMembrane = ProceduralDataCache.Instance.ReadMembraneData(hash);
+
+ if (cachedMembrane != null)
+ {
+ // TODO: hopefully this can't get into a permanent loop where 2 conflicting membranes want to
+ // re-generate on each game update cycle
+ if (!cachedMembrane.MembraneDataFieldsEqual(hexes, hexCount, cellProperties.MembraneType, null))
+ {
+ CacheableDataExtensions.OnCacheHashCollision(hash);
+ cachedMembrane = null;
+ }
+ }
+
+ if (cachedMembrane != null)
+ {
+ // Membrane was ready now
+ return cachedMembrane;
+ }
+
+ // Need to generate a new membrane
+
+ lock (pendingGenerationsOfMembraneHashes)
+ {
+ if (!pendingGenerationsOfMembraneHashes.Add(hash))
+ {
+ // Already queued, don't need to queue again
+
+ // Return the unnecessary array that there won't be a cache entry to hold to the pool
+ ArrayPool.Shared.Return(hexes);
+
+ return null;
+ }
+ }
+
+ membranesToGenerate.Enqueue(new MembraneGenerationParameters(hexes, hexCount, cellProperties.MembraneType, null, null));
+
+ // Immediately start some jobs to give background threads something to do while the main thread is busy
+ // potentially setting up other visuals
+ StartMembraneGenerationJobs();
+
+ return null;
+ }
+
+ private MembranePointData? GetMulticellularMembraneDataIfReadyOrStartGenerating(ref CellProperties cellProperties,
+ ref OrganelleContainer organelleContainer, ref EarlyMulticellularSpeciesMember multicellular)
+ {
+ // TODO: should we consider the situation where a membrane was requested on the previous update but is not
+ // ready yet? This causes extra memory usage here in those cases.
+ var hexes = MembraneComputationHelpers.PrepareHexPositionsForMembraneCalculations(
+ organelleContainer.Organelles!.Organelles, out var hexCount);
+
+ List positions = new List();
+
+ foreach (var cell in multicellular.Species.Cells)
+ {
+ var cartesian = Hex.AxialToCartesian(cell.Position);
+ positions.Add(new Vector2(cartesian.X, cartesian.Z));
+ }
+
+ var positionsArray = positions.ToArray();
+
+ var hash = MembraneComputationHelpers.ComputeMembraneDataHash(hexes, hexCount, cellProperties.MembraneType, true);
var cachedMembrane = ProceduralDataCache.Instance.ReadMembraneData(hash);
@@ -230,7 +303,7 @@ protected override void PostUpdate(float state)
{
// TODO: hopefully this can't get into a permanent loop where 2 conflicting membranes want to
// re-generate on each game update cycle
- if (!cachedMembrane.MembraneDataFieldsEqual(hexes, hexCount, cellProperties.MembraneType))
+ if (!cachedMembrane.MembraneDataFieldsEqual(hexes, hexCount, cellProperties.MembraneType, positionsArray))
{
CacheableDataExtensions.OnCacheHashCollision(hash);
cachedMembrane = null;
@@ -258,7 +331,10 @@ protected override void PostUpdate(float state)
}
}
- membranesToGenerate.Enqueue(new MembraneGenerationParameters(hexes, hexCount, cellProperties.MembraneType));
+ var thisCartesian = Hex.AxialToCartesian(multicellular.Species.Cells[multicellular.MulticellularBodyPlanPartIndex].Position);
+ var thisVector2 = new Vector2(thisCartesian.X, thisCartesian.Z);
+
+ membranesToGenerate.Enqueue(new MembraneGenerationParameters(hexes, hexCount, cellProperties.MembraneType, positionsArray, thisVector2));
// Immediately start some jobs to give background threads something to do while the main thread is busy
// potentially setting up other visuals