Almost flocking

This commit is contained in:
Pascal Serrarens 2025-12-03 18:02:25 +01:00
parent ca48381dc2
commit 69c6b1e2b7
11 changed files with 102 additions and 72 deletions

View File

@ -8,6 +8,8 @@ public class NanoBrain_Editor : Editor {
private List<NeuroidLayer> layers = new(); private List<NeuroidLayer> layers = new();
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new(); private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
protected bool breakOnWake = false;
#region Start #region Start
private void OnEnable() { private void OnEnable() {
@ -19,7 +21,7 @@ public class NanoBrain_Editor : Editor {
if (!selectedObject.TryGetComponent(out Boid boid)) if (!selectedObject.TryGetComponent(out Boid boid))
return; return;
Neuroid neuroid = boid.totalForce; Nucleus neuroid = boid.totalForce;
this.currentNucleus = neuroid; this.currentNucleus = neuroid;
BuildLayers(); BuildLayers();
@ -34,12 +36,32 @@ public class NanoBrain_Editor : Editor {
if (this.currentNucleus == null) if (this.currentNucleus == null)
return; return;
breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake);
if (breakOnWake && currentNucleus is Neuroid currentNeuroid) {
if (!currentNeuroid.isSleeping)
Debug.Break();
}
DrawGraph(); DrawGraph();
//DrawDefaultInspector(); //DrawDefaultInspector();
EditorGUILayout.TextField("Name", currentNucleus.name); EditorGUILayout.TextField("Name", currentNucleus.name);
EditorGUILayout.FloatField("Output Value", currentNucleus.outputValue.magnitude); EditorGUILayout.Vector3Field("Output Value", currentNucleus.outputValue);
EditorGUILayout.IntField("# synapses", currentNucleus.synapses.Count); if (currentNucleus.synapses.Count > 0) {
EditorGUI.indentLevel++;
foreach ((Nucleus nucleus, float weight) in currentNucleus.synapses) {
EditorGUI.BeginDisabledGroup(nucleus.isSleeping);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.Vector3Field(nucleus.name, nucleus.outputValue);
EditorGUILayout.FloatField(weight, GUILayout.Width(50));
EditorGUILayout.EndHorizontal();
EditorGUI.EndDisabledGroup();
}
EditorGUI.indentLevel--;
}
} }
private void BuildLayers() { private void BuildLayers() {
@ -140,8 +162,8 @@ public class NanoBrain_Editor : Editor {
float size = 20; float size = 20;
if (layerNeuroid.IsStale()) if (layerNeuroid.isSleeping)
Handles.color = Color.darkCyan; Handles.color = Color.darkRed;
else { else {
float brightness = layerNeuroid.outputValue.magnitude / maxValue; float brightness = layerNeuroid.outputValue.magnitude / maxValue;
Handles.color = new Color(brightness, brightness, brightness); Handles.color = new Color(brightness, brightness, brightness);
@ -224,7 +246,8 @@ public class NanoBrain_Editor : Editor {
// Handles.DrawLine(brain.transform.position, brain.transform.position + Vector3.up); // Handles.DrawLine(brain.transform.position, brain.transform.position + Vector3.up);
Handles.color = Color.yellow; Handles.color = Color.yellow;
Vector3 worldForce = brain.transform.TransformDirection(this.currentNucleus.outputValue); Vector3 worldForce = brain.transform.TransformDirection(this.currentNucleus.outputValue);
Debug.DrawRay(position, worldForce * 10, Color.yellow); //Debug.DrawRay(position, worldForce * 10, Color.yellow);
Handles.DrawLine(position, position + worldForce * 10);
} }
#endregion Update #endregion Update

View File

@ -199,7 +199,7 @@ public class GraphEditorWindow : EditorWindow {
} }
float size = 20; float size = 20;
if (layerNeuroid.IsStale()) if (layerNeuroid.isSleeping)
Handles.color = Color.black; Handles.color = Color.black;
else { else {
float brightness = layerNeuroid.outputValue.magnitude / maxValue; float brightness = layerNeuroid.outputValue.magnitude / maxValue;
@ -272,7 +272,7 @@ public class GraphEditorWindow : EditorWindow {
if (boid == null) if (boid == null)
return; return;
Neuroid neuroid = boid.totalForce; Nucleus neuroid = boid.behaviour;
this.currentNucleus = neuroid; this.currentNucleus = neuroid;
if (neuroid == null) if (neuroid == null)
this.allNeuroids = new(); this.allNeuroids = new();

View File

@ -12,7 +12,7 @@ public class NanoBrain : MonoBehaviour {
public void UpdateNeurons() { public void UpdateNeurons() {
foreach (Neuroid neuroid in neuroids) { foreach (Neuroid neuroid in neuroids) {
neuroid.stale++; neuroid.stale++;
if (neuroid.IsStale()) if (neuroid.isSleeping)
neuroid.outputValue = Vector3.zero; neuroid.outputValue = Vector3.zero;
} }
} }

View File

@ -1,8 +1,6 @@
using UnityEngine; using UnityEngine;
public class Neuroid : Nucleus { public class Neuroid : Nucleus {
public int stale = 0;
public bool average = false; public bool average = false;
public bool inverse = false; public bool inverse = false;
public float exponent = 1.0f; public float exponent = 1.0f;
@ -15,11 +13,6 @@ public class Neuroid : Nucleus {
Debug.LogError("No neuroid network"); Debug.LogError("No neuroid network");
} }
// public void AddSynapse(Neuroid input) {
// input.AddReceiver(this);
// this.synapses[input] = 1.0f;
// }
public void SetWeight(Neuroid input, float weight) { public void SetWeight(Neuroid input, float weight) {
this.synapses[input] = weight; this.synapses[input] = weight;
} }
@ -43,7 +36,7 @@ public class Neuroid : Nucleus {
public virtual void UpdateState() { public virtual void UpdateState() {
Vector3 result = Vector3.zero; Vector3 result = Vector3.zero;
foreach ((Nucleus nucleus, float weight) in this.synapses) { foreach ((Nucleus nucleus, float weight) in this.synapses) {
if (nucleus is Neuroid neuroid && neuroid.IsStale()) if (nucleus is Neuroid neuroid && neuroid.isSleeping)
continue; continue;
Vector3 direction = nucleus.outputValue.normalized; Vector3 direction = nucleus.outputValue.normalized;
@ -65,8 +58,8 @@ public class Neuroid : Nucleus {
receiver.SetInput(this); receiver.SetInput(this);
} }
public bool IsStale() { // public bool IsStale() {
return this.stale > 2; // return this.stale > 2;
} // }
} }

View File

@ -2,9 +2,11 @@ using System.Collections.Generic;
using UnityEngine; using UnityEngine;
public class Nucleus { public class Nucleus {
public int stale = 0;
public NanoBrain brain { get; protected set; } public NanoBrain brain { get; protected set; }
public string name; public virtual string name { get; set; }
public readonly Dictionary<Nucleus, float> synapses = new(); public readonly Dictionary<Nucleus, float> synapses = new();
public HashSet<Neuroid> receivers = new(); public HashSet<Neuroid> receivers = new();
@ -22,4 +24,11 @@ public class Nucleus {
receiver.synapses[this] = 1.0f; // new(this); receiver.synapses[this] = 1.0f; // new(this);
//Debug.Log($"receiver # {this.receivers.Count} synapse count {receiver.synapses.Count}"); //Debug.Log($"receiver # {this.receivers.Count} synapse count {receiver.synapses.Count}");
} }
public bool isSleeping {
get {
return this.stale > 2;
}
}
} }

View File

@ -19,7 +19,7 @@ public class Perception : Nucleus {
this.velocityReceivers = new(); this.velocityReceivers = new();
} }
public void SendPositions(Neuroid receivingNeuroid, float weight = 1.0f, int thingType = 0) { public void SendPositions(Neuroid receivingNeuroid, int thingType = 0, float weight = 1.0f) {
Receiver receiver = new() { Receiver receiver = new() {
thingType = thingType, thingType = thingType,
neuroid = receivingNeuroid neuroid = receivingNeuroid
@ -32,7 +32,7 @@ public class Perception : Nucleus {
} }
} }
} }
public void SendVelocities(Neuroid receivingNeuroid, float weight = 1.0f, int thingType = 0) { public void SendVelocities(Neuroid receivingNeuroid, int thingType = 0, float weight = 1.0f) {
Receiver receiver = new() { Receiver receiver = new() {
thingType = thingType, thingType = thingType,
neuroid = receivingNeuroid neuroid = receivingNeuroid
@ -57,7 +57,7 @@ public class Perception : Nucleus {
return; return;
} }
if (availableIx == -1) { if (availableIx == -1) {
if (sensoryNeuroids[i].IsStale()) if (sensoryNeuroids[i].isSleeping)
leastInterestingIx = i; leastInterestingIx = i;
else if (sensoryNeuroids[i] != null) { else if (sensoryNeuroids[i] != null) {
if (leastInterestingIx == -1 || sensoryNeuroids[leastInterestingIx].receptor.position.magnitude > sensoryNeuroids[i].receptor.position.magnitude) if (leastInterestingIx == -1 || sensoryNeuroids[leastInterestingIx].receptor.position.magnitude > sensoryNeuroids[i].receptor.position.magnitude)
@ -77,7 +77,7 @@ public class Perception : Nucleus {
} }
else { else {
// Debug.Log($"new receptor for {thingId} at {availableIx}"); // Debug.Log($"new receptor for {thingId} at {availableIx}");
neuroid = new(brain, thingId) { name = name }; neuroid = new(brain, thingId, name);
sensoryNeuroids[availableIx] = neuroid; sensoryNeuroids[availableIx] = neuroid;
} }
foreach (Receiver receiver in positionReceivers) { foreach (Receiver receiver in positionReceivers) {

View File

@ -30,33 +30,38 @@ public class SensoryNeuroid : Neuroid {
public VelocityNeuroid velocityNeuroid; public VelocityNeuroid velocityNeuroid;
// public SensoryNeuroid(NeuroidNetwork net, int thingId) : base(net, "sensory neuroid") { // public SensoryNeuroid(NeuroidNetwork net, int thingId) : base(net, "sensory neuroid") {
public SensoryNeuroid(NanoBrain net, int thingId) : base(net, "sensory neuroid") { public SensoryNeuroid(NanoBrain net, int thingId, string name = "sensor") : base(net, name) {
this.name = name + ": position";
this.receptor = new Receptor { this.receptor = new Receptor {
neuroid = this, neuroid = this,
thingId = thingId thingId = thingId
}; };
this.velocityNeuroid = new(net); this.velocityNeuroid = new(net, name + ": velocity");
// The velocity neuroid received position data from this // The velocity neuroid received position data from this
this.AddReceiver(velocityNeuroid); this.AddReceiver(velocityNeuroid);
} }
public void Replace(int thingId, string name = "sensory neuroid") { public void Replace(int thingId, string name = "sensor") {
this.name = name; this.name = name + ": position";
this.receptor.thingId = thingId; this.receptor.thingId = thingId;
this.receptor.localPosition = Vector3.zero; this.receptor.localPosition = Vector3.zero;
this.outputValue = Vector3.zero; this.outputValue = Vector3.zero;
this.receivers = new(); this.receivers = new();
this.AddReceiver(velocityNeuroid); this.AddReceiver(velocityNeuroid);
this.velocityNeuroid.receivers = new();
// this.velocityNeuroid.name = name + ": velocity";
// this.velocityNeuroid.receivers = new();
this.velocityNeuroid.Replace(name + ": velocity");
} }
public override void UpdateState() { public override void UpdateState() {
Vector3 result = receptor.localPosition; Vector3 result = receptor.localPosition;
//foreach ((Nucleus nucleus, Synapse synapse) in this.synapses) {
foreach ((Nucleus nucleus, float weight) in this.synapses) { foreach ((Nucleus nucleus, float weight) in this.synapses) {
Vector3 direction = nucleus.outputValue.normalized; Vector3 direction = nucleus.outputValue.normalized;
float magnitude = nucleus.outputValue.magnitude; float magnitude = nucleus.outputValue.magnitude;
//magnitude = synapse.weight * Mathf.Pow(magnitude, exponent);
magnitude = weight * Mathf.Pow(magnitude, exponent); magnitude = weight * Mathf.Pow(magnitude, exponent);
if (inverse) if (inverse)
magnitude = 1 / magnitude; magnitude = 1 / magnitude;
@ -77,8 +82,14 @@ public class VelocityNeuroid : Neuroid {
private Vector3 lastPosition = Vector3.zero; private Vector3 lastPosition = Vector3.zero;
private float lastValueTime = 0; private float lastValueTime = 0;
// public VelocityNeuroid(NeuroidNetwork net) : base(net, "Velocity") { public VelocityNeuroid(NanoBrain net, string name = "velocity") : base(net, name) {
public VelocityNeuroid(NanoBrain net) : base(net, "Velocity") { }
public void Replace(string name = "velocity") {
this.name = name;
this.receivers = new();
this.lastPosition = Vector3.zero;
this.lastValueTime = 0;
} }
public override void UpdateState() { public override void UpdateState() {
@ -86,17 +97,20 @@ public class VelocityNeuroid : Neuroid {
Vector3 currentPosition = this.synapses.First().Key.outputValue; Vector3 currentPosition = this.synapses.First().Key.outputValue;
float currentValueTime = Time.time; float currentValueTime = Time.time;
float deltaTime = currentValueTime - lastValueTime; if (lastValueTime != 0) {
Vector3 translation = currentPosition - lastPosition; float deltaTime = currentValueTime - lastValueTime;
Vector3 velocity = translation / deltaTime; Vector3 translation = currentPosition - lastPosition;
Vector3 velocity = translation / deltaTime;
// No activation function... // No activation function...
this.outputValue = velocity; this.outputValue = velocity;
foreach (Neuroid receiver in receivers) this.stale = 0;
receiver?.SetInput(this);
this.stale = 0;
this.lastValueTime = Time.time; foreach (Neuroid receiver in receivers)
receiver?.SetInput(this);
}
this.lastValueTime = currentValueTime;
this.lastPosition = currentPosition; this.lastPosition = currentPosition;
} }
} }

View File

@ -372,10 +372,10 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: Assembly-CSharp::SwarmControl m_EditorClassIdentifier: Assembly-CSharp::SwarmControl
speed: 2 speed: 2
inertia: 0.1 inertia: 0.5
alignmentForce: 3 alignmentForce: 2
cohesionForce: 3 cohesionForce: 2
avoidanceForce: 5 avoidanceForce: 1
separationDistance: 0.3 separationDistance: 0.3
perceptionDistance: 1 perceptionDistance: 1
boundaryForce: 5 boundaryForce: 5
@ -393,7 +393,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: ec888ca5333d45a438f9f417fa5ce135, type: 3} m_Script: {fileID: 11500000, guid: ec888ca5333d45a438f9f417fa5ce135, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: Assembly-CSharp::SwarmSpawn m_EditorClassIdentifier: Assembly-CSharp::SwarmSpawn
count: 5 count: 20
boidPrefab: {fileID: 8702527964058765413, guid: f9c706268554ce449a8773675b2864b8, type: 3} boidPrefab: {fileID: 8702527964058765413, guid: f9c706268554ce449a8773675b2864b8, type: 3}
spawnAreaSize: {x: 0.5, y: 0.5, z: 0.5} spawnAreaSize: {x: 0.5, y: 0.5, z: 0.5}
minDelay: 0.05 minDelay: 0.05

View File

@ -2,17 +2,15 @@ using UnityEngine;
[RequireComponent(typeof(NanoBrain))] [RequireComponent(typeof(NanoBrain))]
public class Boid : MonoBehaviour { public class Boid : MonoBehaviour {
public const int BoundaryType = 1; public static int BoundaryType = 1;
public const int BoidType = 2; public static int BoidType = 2;
public SwarmControl sc; public SwarmControl sc;
public Vector3 velocity = Vector3.zero; public Vector3 velocity = Vector3.zero;
public Vector3 acceleration = Vector3.zero; public Vector3 acceleration = Vector3.zero;
private Bounds innerBounds; private Bounds innerBounds;
private Bounds outerBounds;
//public NeuroidNetwork neuroidNet = new();
public NanoBrain neuroidNet; public NanoBrain neuroidNet;
public Perception perception; public Perception perception;
@ -30,7 +28,6 @@ public class Boid : MonoBehaviour {
sc = FindFirstObjectByType<SwarmControl>(); sc = FindFirstObjectByType<SwarmControl>();
innerBounds = new(sc.transform.position, sc.spaceSize - 2 * sc.boundaryWidth); innerBounds = new(sc.transform.position, sc.spaceSize - 2 * sc.boundaryWidth);
outerBounds = new(sc.transform.position, sc.spaceSize);
perception = new Perception(neuroidNet); perception = new Perception(neuroidNet);
@ -49,7 +46,8 @@ public class Boid : MonoBehaviour {
if (neighbour == null || neighbour == this) if (neighbour == null || neighbour == this)
continue; continue;
Vector3 localPosition = neighbour.transform.position - this.transform.position; Vector3 localPosition = this.transform.InverseTransformPoint(neighbour.transform.position);
//Debug.DrawRay(this.transform.position, this.transform.TransformDirection(localPosition), Color.magenta);
int thingId = neighbour.GetInstanceID(); int thingId = neighbour.GetInstanceID();
perception.ProcessStimulus(thingId, BoidType, localPosition, neighbour.gameObject.name); perception.ProcessStimulus(thingId, BoidType, localPosition, neighbour.gameObject.name);
@ -64,13 +62,14 @@ public class Boid : MonoBehaviour {
perception.ProcessStimulus(777, BoundaryType, desiredLocalSpace, "Boundary"); perception.ProcessStimulus(777, BoundaryType, desiredLocalSpace, "Boundary");
} }
Vector3 worldForce = this.transform.TransformDirection(totalForce.outputValue); Vector3 worldForce = this.transform.TransformDirection(behaviour.outputValue);
this.velocity = (1 - sc.inertia) * (worldForce * Time.deltaTime) + sc.inertia * velocity; this.velocity = (1 - sc.inertia) * (worldForce * Time.deltaTime) + sc.inertia * velocity;
if (this.velocity.magnitude > 0) if (this.velocity.magnitude > 0)
this.velocity = this.velocity.normalized * sc.speed; this.velocity = this.velocity.normalized * sc.speed;
else else
this.velocity = this.transform.forward * sc.speed; this.velocity = this.transform.forward * sc.speed;
Debug.DrawRay(this.transform.position, this.velocity, Color.blue);
this.transform.position += this.velocity * Time.deltaTime; this.transform.position += this.velocity * Time.deltaTime;
@ -82,14 +81,4 @@ public class Boid : MonoBehaviour {
neuroidNet.UpdateNeurons(); neuroidNet.UpdateNeurons();
} }
Vector3 ClosestPointOnBoundsSurface(Bounds b, Vector3 p) {
if (!b.Contains(p)) return b.ClosestPoint(p);
Vector3 d = p - b.center;
Vector3 ext = b.extents;
float sx = ext.x / Mathf.Abs(d.x);
float sy = ext.y / Mathf.Abs(d.y);
float sz = ext.z / Mathf.Abs(d.z);
float m = Mathf.Min(sx, Mathf.Min(sy, sz));
return b.center + d * m;
}
} }

View File

@ -5,7 +5,7 @@ public class Roaming : Nucleus {
public Roaming(NanoBrain neuroidNet, Perception perception, SwarmControl sc) : base("Roaming nucleus") { public Roaming(NanoBrain neuroidNet, Perception perception, SwarmControl sc) : base("Roaming nucleus") {
avoidance = new(neuroidNet, "Avoidance") { inverse = true }; avoidance = new(neuroidNet, "Avoidance") { inverse = true };
perception.SendPositions(avoidance, 1.0f, 1); perception.SendPositions(avoidance, Boid.BoundaryType);
this.output = new(neuroidNet, "Roaming"); this.output = new(neuroidNet, "Roaming");
output.GetInputFrom(avoidance, -sc.avoidanceForce); output.GetInputFrom(avoidance, -sc.avoidanceForce);

View File

@ -4,28 +4,30 @@ public class Swarming : Nucleus {
public Neuroid cohesion; public Neuroid cohesion;
public Neuroid alignment; public Neuroid alignment;
public Neuroid avoidance; public Neuroid avoidance;
public Neuroid boundary;
public Neuroid output; public Neuroid output;
public override Vector3 outputValue { get => output.outputValue; set => output.outputValue = value; } public override Vector3 outputValue { get => output.outputValue; set => output.outputValue = value; }
public const int BoundaryType = 1;
public const int BoidType = 2;
public Swarming(NanoBrain neuroidNet, Perception perception, SwarmControl sc) : base("Swarming Nucleus") { public Swarming(NanoBrain neuroidNet, Perception perception, SwarmControl sc) : base("Swarming Nucleus") {
this.cohesion = new(neuroidNet, "Cohesion"); this.cohesion = new(neuroidNet, "Cohesion") { inverse = false };
perception.SendPositions(this.cohesion, 1.0f, BoidType); perception.SendPositions(this.cohesion, Boid.BoidType);
this.alignment = new(neuroidNet, "Alignment") { average = true }; this.alignment = new(neuroidNet, "Alignment") { average = true };
perception.SendVelocities(this.alignment, 1.0f, BoidType); perception.SendVelocities(this.alignment, Boid.BoidType);
this.avoidance = new(neuroidNet, "Avoidance") { inverse = true }; this.avoidance = new(neuroidNet, "Avoidance") { inverse = true };
perception.SendPositions(this.avoidance); perception.SendPositions(this.avoidance);
this.boundary = new(neuroidNet, "Boundary");
perception.SendPositions(this.boundary, Boid.BoundaryType);
this.output = new(neuroidNet, "Swarming"); this.output = new(neuroidNet, "Swarming");
this.output.GetInputFrom(alignment, sc.alignmentForce); this.output.GetInputFrom(alignment, sc.alignmentForce);
this.output.GetInputFrom(cohesion, sc.cohesionForce); this.output.GetInputFrom(cohesion, sc.cohesionForce);
this.output.GetInputFrom(avoidance, -sc.avoidanceForce); this.output.GetInputFrom(avoidance, -sc.avoidanceForce);
this.output.GetInputFrom(boundary, -sc.avoidanceForce);
} }
public override void AddReceiver(Neuroid receiver) { public override void AddReceiver(Neuroid receiver) {