Compare commits
60 Commits
aab6f59934
...
d2b5d2feac
| Author | SHA1 | Date | |
|---|---|---|---|
| d2b5d2feac | |||
| 116a5819e0 | |||
| 15b298f399 | |||
| c110a6e723 | |||
| f72a37b248 | |||
| 76d8714778 | |||
| b644d0fd5b | |||
| e262447174 | |||
| 36e73081f9 | |||
| a394f582cf | |||
| 5fea9880de | |||
| 9cbf0aaa2c | |||
| 885d649be1 | |||
| f8aaa4ca80 | |||
| f295da9c55 | |||
| 9b948fdd07 | |||
| 19398ade98 | |||
| 537064d84b | |||
| 79e300a6b5 | |||
| a62d1cc5d9 | |||
| 42bc32c734 | |||
| dbf24b458a | |||
| 2af597fbdb | |||
| f40d1ea4ae | |||
| c1bf54a1cc | |||
| 8c8d5a5a66 | |||
| ed0a95b4d8 | |||
| 360346eeac | |||
| c011e04650 | |||
| d562c7192b | |||
| 6f67def8e6 | |||
| fd7359acd0 | |||
| 76c5372089 | |||
| a95c685e1e | |||
| c2dc2720b0 | |||
| 1e2726ecd4 | |||
| f5ed87f9e9 | |||
| 9db7de16ea | |||
| e8471683d6 | |||
| 7bcd4a6cf1 | |||
| 2cae7a6c41 | |||
| 278b861a84 | |||
| 790deab7c6 | |||
| 7fffa6dbe5 | |||
| 8eb1cbea1a | |||
| 00dea149f3 | |||
| 1e5a19c1ab | |||
| 351f242f2e | |||
| d48475b483 | |||
| 0d268edd6d | |||
| 0258ef1197 | |||
| b73b28146c | |||
| 16f0c3d3bf | |||
| 3cc5f56f61 | |||
| 60e2b3e33f | |||
| 5982c8acfe | |||
| 08f9f3a3b1 | |||
| c290b62637 | |||
| 25cd5aefd0 | |||
| 811ddd1761 |
524
Cluster.cs
524
Cluster.cs
@ -1,19 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[Serializable]
|
||||
public class Cluster : INucleus {
|
||||
// The ScriptableObject asset from which the runtime object has been created
|
||||
|
||||
[SerializeField]
|
||||
protected string _name;
|
||||
public virtual string name {
|
||||
get => _name;
|
||||
set => _name = value;
|
||||
}
|
||||
public class Cluster : Nucleus {
|
||||
|
||||
#region Init
|
||||
|
||||
@ -22,98 +15,137 @@ public class Cluster : INucleus {
|
||||
this.name = prefab.name;
|
||||
|
||||
this.parent = parent;
|
||||
this.parent?.nuclei.Add(this);
|
||||
this.parent?.clusterNuclei.Add(this);
|
||||
|
||||
ClonePrefab();
|
||||
this.sortedNuclei = TopologicalSort(this.nuclei);
|
||||
_ = this.inputs;
|
||||
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
|
||||
}
|
||||
|
||||
public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) {
|
||||
this.prefab = prefab;
|
||||
this.name = prefab.name;
|
||||
this.cluster = parent;
|
||||
this.clusterPrefab = parent;
|
||||
|
||||
if (this.cluster != null)
|
||||
this.cluster.nuclei.Add(this);
|
||||
if (this.clusterPrefab != null)
|
||||
this.clusterPrefab.nuclei.Add(this);
|
||||
|
||||
ClonePrefab();
|
||||
this.sortedNuclei = TopologicalSort(this.nuclei);
|
||||
_ = this.inputs;
|
||||
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
|
||||
}
|
||||
|
||||
// public Cluster(ClusterPrefab parent, ClusterPrefab realPrefab) {
|
||||
// this.prefab = realPrefab;
|
||||
// this.name = realPrefab.name;
|
||||
// this.cluster = parent;
|
||||
// if (this.cluster != null)
|
||||
// this.cluster.nuclei.Add(this);
|
||||
|
||||
// ClonePrefab();
|
||||
// }
|
||||
|
||||
private void ClonePrefab() {
|
||||
IReceptor[] nucleiArray = this.prefab.nuclei.ToArray();
|
||||
Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray();
|
||||
// first clone the nuclei without their connections
|
||||
foreach (IReceptor nucleus in this.prefab.nuclei)
|
||||
foreach (Nucleus nucleus in this.prefab.nuclei) {
|
||||
// Debug.Log($"prefab clone {nucleus.name}");
|
||||
nucleus.ShallowCloneTo(this);
|
||||
IReceptor[] clonedNuclei = this.nuclei.ToArray();
|
||||
}
|
||||
Nucleus[] clonedNuclei = this.clusterNuclei.ToArray();
|
||||
|
||||
// Now clone the connections
|
||||
for (int nucleusIx = 0; nucleusIx < nucleiArray.Length; nucleusIx++) {
|
||||
IReceptor receptor = nucleiArray[nucleusIx];
|
||||
IReceptor clonedSender = clonedNuclei[nucleusIx];
|
||||
if (clonedSender == null)
|
||||
for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) {
|
||||
Nucleus prefabNucleus = prefabNuclei[nucleusIx];
|
||||
if (prefabNucleus is not Neuron prefabNeuron)
|
||||
continue;
|
||||
|
||||
Nucleus clonedNucleus = clonedNuclei[nucleusIx];
|
||||
if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron)
|
||||
continue;
|
||||
|
||||
// Copy the receivers, which will also create the synapses
|
||||
foreach (INucleus receiver in receptor.receivers) {
|
||||
int ix = GetNucleusIndex(nucleiArray, receiver);
|
||||
// Clusters do not have receivers...
|
||||
foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) {
|
||||
int ix = GetNucleusIndex(prefabNuclei, receiver);
|
||||
if (ix < 0)
|
||||
continue;
|
||||
|
||||
if (clonedNuclei[ix] is not INucleus clonedReceiver)
|
||||
if (clonedNuclei[ix] is not Nucleus clonedReceiver)
|
||||
continue;
|
||||
|
||||
// Find the synapse for the weight
|
||||
float weight = 1;
|
||||
foreach (Synapse synapse in receiver.synapses) {
|
||||
if (synapse.nucleus == receptor) {
|
||||
// Find the weight for this synapse
|
||||
if (synapse.nucleus == prefabNucleus) {
|
||||
weight = synapse.weight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
clonedSender.AddReceiver(clonedReceiver, weight);
|
||||
clonedNeuron.AddReceiver(clonedReceiver, weight);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy nucleus arrays
|
||||
for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) {
|
||||
Nucleus prefabReceptor = prefabNuclei[nucleusIx];
|
||||
if (prefabReceptor is not Receptor prefabNucleus)
|
||||
continue;
|
||||
|
||||
if (prefabNucleus.nucleiArray == null || prefabNucleus.nucleiArray.Length == 0)
|
||||
continue;
|
||||
|
||||
Receptor clonedNucleus = clonedNuclei[nucleusIx] as Receptor;
|
||||
if (prefabNucleus == prefabNucleus.nucleiArray[0]) {
|
||||
// We clone the array only for the first entry
|
||||
NucleusArray clonedArray = new(prefabNucleus.nucleiArray.Length, "array");
|
||||
int arrayIx = 0;
|
||||
foreach (Nucleus prefabArrayNucleus in prefabNucleus.nucleiArray) {
|
||||
int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus);
|
||||
if (arrayNucleusIx >= 0) {
|
||||
Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx];
|
||||
clonedArray.nuclei[arrayIx] = clonedArrayNucleus;
|
||||
}
|
||||
else {
|
||||
Debug.LogError($" Could not find prefab nuclues {prefabNucleus.name} in the clones");
|
||||
}
|
||||
arrayIx++;
|
||||
}
|
||||
clonedNucleus.array = clonedArray;
|
||||
}
|
||||
else {
|
||||
// The others will refer to the array created for the first nucleus in the array
|
||||
int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabNucleus.nucleiArray[0]);
|
||||
Receptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Receptor;
|
||||
clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the nuclei in a correct evaluation order
|
||||
private List<IReceptor> TopologicalSort(List<IReceptor> nodes) {
|
||||
Dictionary<IReceptor, int> inDegree = new();
|
||||
foreach (IReceptor node in nodes)
|
||||
private List<Nucleus> TopologicalSort(List<Nucleus> nodes) {
|
||||
Dictionary<Nucleus, int> inDegree = new();
|
||||
foreach (Nucleus node in nodes)
|
||||
inDegree[node] = 0; // Initialize in-degree to zero
|
||||
|
||||
// Calculate in-degrees
|
||||
foreach (IReceptor node in nodes) {
|
||||
foreach (INucleus receiver in node.receivers)
|
||||
inDegree[receiver]++;
|
||||
foreach (Nucleus node in nodes) {
|
||||
if (node is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers)
|
||||
inDegree[receiver]++;
|
||||
}
|
||||
}
|
||||
|
||||
Queue<IReceptor> queue = new();
|
||||
foreach (IReceptor node in nodes) {
|
||||
Queue<Nucleus> queue = new();
|
||||
foreach (Nucleus node in nodes) {
|
||||
if (inDegree[node] == 0) // Nodes with no dependencies
|
||||
queue.Enqueue(node);
|
||||
}
|
||||
// The queue basically stores all input nuclei?
|
||||
|
||||
List<IReceptor> sortedOrder = new();
|
||||
List<Nucleus> sortedOrder = new();
|
||||
while (queue.Count > 0) {
|
||||
IReceptor current = queue.Dequeue();
|
||||
Nucleus current = queue.Dequeue();
|
||||
sortedOrder.Add(current); // Process the node
|
||||
|
||||
foreach (INucleus receiver in current.receivers) {
|
||||
inDegree[receiver]--;
|
||||
if (inDegree[receiver] == 0) // If all dependencies resolved
|
||||
queue.Enqueue(receiver);
|
||||
if (current is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers) {
|
||||
inDegree[receiver]--;
|
||||
if (inDegree[receiver] == 0) // If all dependencies resolved
|
||||
queue.Enqueue(receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,31 +156,92 @@ public class Cluster : INucleus {
|
||||
return sortedOrder;
|
||||
}
|
||||
|
||||
public virtual IReceptor Clone() {
|
||||
Neuron clone = new(this.cluster, this.name) {
|
||||
array = this.array,
|
||||
};
|
||||
public override Nucleus Clone(ClusterPrefab parent) {
|
||||
Cluster clone = new(this.prefab, parent);
|
||||
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||
clonedSynapse.weight = synapse.weight;
|
||||
}
|
||||
foreach (INucleus receiver in this.receivers) {
|
||||
clone.AddReceiver(receiver);
|
||||
|
||||
foreach (Neuron output in this.outputs) {
|
||||
foreach (Nucleus receiver in output.receivers) {
|
||||
int ix = GetNucleusIndex(this.clusterNuclei.ToArray(), output);
|
||||
if (ix < 0)
|
||||
continue;
|
||||
|
||||
if (clone.clusterNuclei[ix] is not Neuron clonedOutput)
|
||||
continue;
|
||||
|
||||
clonedOutput.AddReceiver(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public IReceptor ShallowCloneTo(Cluster parent) {
|
||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
||||
Cluster clone = new(this.prefab, parent) {
|
||||
name = this.name,
|
||||
clusterPrefab = this.clusterPrefab,
|
||||
};
|
||||
// This cloned the prefab with the clusternuclei,
|
||||
// but did not clone the receivers outside the cluster
|
||||
RestoreExternalReceivers(clone, this.clusterPrefab, parent);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private int GetNucleusIndex(IReceptor[] nucleiArray, IReceptor nucleus) {
|
||||
for (int i = 0; i < nucleiArray.Length; i++) {
|
||||
if (nucleus == nucleiArray[i])
|
||||
protected void RestoreExternalReceivers(Cluster clone, ClusterPrefab prefabParent, Cluster clonedParent) {
|
||||
for (int nucleusIx = 0; nucleusIx < this.clusterNuclei.Count; nucleusIx++) {
|
||||
Nucleus prefabNucleus = this.clusterNuclei[nucleusIx];
|
||||
if (prefabNucleus is not Neuron prefabNeuron)
|
||||
continue;
|
||||
|
||||
Nucleus clonedNucleus = clone.clusterNuclei[nucleusIx];
|
||||
if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron)
|
||||
continue;
|
||||
|
||||
// Copy the receivers, which will also create the synapses
|
||||
foreach (Nucleus receiver in prefabNeuron.receivers) {
|
||||
int ix = GetNucleusIndex(prefabParent.nuclei, receiver);
|
||||
if (ix < 0)
|
||||
continue;
|
||||
|
||||
//if (clone.clusterNuclei[ix] is not Nucleus clonedReceiver)
|
||||
if (clonedParent.clusterNuclei[ix] is not Nucleus clonedReceiver)
|
||||
continue;
|
||||
|
||||
// Find the synapse for the weight
|
||||
float weight = 1;
|
||||
foreach (Synapse synapse in receiver.synapses) {
|
||||
// Find the weight for this synapse
|
||||
if (synapse.nucleus == prefabNucleus) {
|
||||
weight = synapse.weight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
clonedNeuron.AddReceiver(clonedReceiver, weight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) {
|
||||
for (int i = 0; i < nuclei.Length; i++) {
|
||||
if (nucleus == nuclei[i])
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected int GetNucleusIndex(List<Nucleus> nuclei, Nucleus nucleus) {
|
||||
int i = 0;
|
||||
foreach (Nucleus nucleiElement in nuclei) {
|
||||
//for (int i = 0; i < nuclei.Length; i++) {
|
||||
if (nucleus == nucleiElement)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
@ -158,172 +251,243 @@ public class Cluster : INucleus {
|
||||
|
||||
public ClusterPrefab prefab;
|
||||
|
||||
public ClusterPrefab cluster { get; set; }
|
||||
public Cluster parent { get; set; }
|
||||
|
||||
[SerializeReference]
|
||||
public List<IReceptor> nuclei = new();
|
||||
public List<Nucleus> clusterNuclei = new();
|
||||
// the nuclei sorted using topological sorting
|
||||
// to ensure that the cluster is computer in the right order
|
||||
public List<IReceptor> sortedNuclei;
|
||||
public List<Nucleus> sortedNuclei;
|
||||
//public Dictionary<string, Nucleus> nucleiDict = new();
|
||||
|
||||
public List<INucleus> _inputs = null;
|
||||
public virtual List<INucleus> inputs {
|
||||
public List<Nucleus> _inputs = null;
|
||||
public virtual List<Nucleus> inputs {
|
||||
get {
|
||||
if (this._inputs == null) {
|
||||
this._inputs = new();
|
||||
foreach (IReceptor receptor in this.nuclei) {
|
||||
if (receptor is INucleus nucleus) {
|
||||
// inputs have no incoming synapses yet.
|
||||
if (nucleus.synapses.Count == 0)
|
||||
this._inputs.Add(nucleus);
|
||||
}
|
||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
||||
// inputs have no synapses
|
||||
if (nucleus.synapses.Count == 0)
|
||||
this._inputs.Add(nucleus);
|
||||
}
|
||||
ComputeOrders();
|
||||
}
|
||||
return this._inputs;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual INucleus output {//=> this.nuclei[0] as INucleus;
|
||||
get {
|
||||
if (this.nuclei.Count > 0)
|
||||
return this.nuclei[0] as INucleus;
|
||||
return null;
|
||||
public Dictionary<Nucleus, List<Nucleus>> computeOrders = new();
|
||||
private void ComputeOrders() {
|
||||
foreach (Nucleus input in this._inputs) {
|
||||
computeOrders[input] = TopologicalSort2(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Not sure if this belongs here...
|
||||
[SerializeReference]
|
||||
private NucleusArray _array;
|
||||
public NucleusArray array {
|
||||
get { return _array; }
|
||||
set { _array = value; }
|
||||
private List<Nucleus> TopologicalSort2(Nucleus startNode) {
|
||||
Dictionary<Nucleus, int> inDegree = new Dictionary<Nucleus, int>();
|
||||
HashSet<Nucleus> visited = new HashSet<Nucleus>();
|
||||
|
||||
// Initialize in-degrees and mark all nodes as unvisited
|
||||
foreach (Nucleus node in this.clusterNuclei) {
|
||||
inDegree[node] = 0;
|
||||
}
|
||||
|
||||
// Calculate in-degrees for all nodes reachable from the start node
|
||||
Queue<Nucleus> queue = new Queue<Nucleus>();
|
||||
queue.Enqueue(startNode);
|
||||
visited.Add(startNode);
|
||||
|
||||
while (queue.Count > 0) {
|
||||
Nucleus current = queue.Dequeue();
|
||||
if (current is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers) {
|
||||
if (!visited.Contains(receiver)) {
|
||||
visited.Add(receiver);
|
||||
queue.Enqueue(receiver);
|
||||
}
|
||||
inDegree[receiver]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform topological sort on all reachable nodes
|
||||
queue.Clear();
|
||||
foreach (var node in visited) {
|
||||
if (inDegree[node] == 0) {
|
||||
queue.Enqueue(node);
|
||||
}
|
||||
}
|
||||
|
||||
List<Nucleus> sortedOrder = new List<Nucleus>();
|
||||
while (queue.Count > 0) {
|
||||
Nucleus current = queue.Dequeue();
|
||||
sortedOrder.Add(current); // Process the node
|
||||
|
||||
if (current is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers) {
|
||||
if (visited.Contains(receiver)) {
|
||||
inDegree[receiver]--;
|
||||
if (inDegree[receiver] == 0) // If all dependencies resolved
|
||||
queue.Enqueue(receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cycles in the graph
|
||||
if (sortedOrder.Count != visited.Count)
|
||||
throw new InvalidOperationException("Graph is not a DAG; a cycle exists.");
|
||||
|
||||
return sortedOrder;
|
||||
}
|
||||
|
||||
#region Synapses
|
||||
private List<Nucleus> TopologicalSort3(Nucleus startNode) {
|
||||
Dictionary<Nucleus, int> inDegree = new();
|
||||
foreach (Nucleus node in this.clusterNuclei)
|
||||
inDegree[node] = 0; // Initialize in-degree to zero
|
||||
|
||||
[SerializeField]
|
||||
private List<Synapse> _synapses = new();
|
||||
public List<Synapse> synapses => _synapses;
|
||||
// Calculate in-degrees
|
||||
foreach (Nucleus node in this.clusterNuclei) {
|
||||
if (node is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers)
|
||||
inDegree[receiver]++;
|
||||
}
|
||||
}
|
||||
|
||||
public Synapse AddSynapse(IReceptor sendingNucleus, float weight = 1.0f) {
|
||||
Synapse synapse = new(sendingNucleus, weight);
|
||||
this._synapses.Add(synapse);
|
||||
return synapse;
|
||||
Queue<Nucleus> queue = new();
|
||||
queue.Enqueue(startNode);
|
||||
|
||||
List<Nucleus> sortedOrder = new();
|
||||
while (queue.Count > 0) {
|
||||
Nucleus current = queue.Dequeue();
|
||||
sortedOrder.Add(current); // Process the node
|
||||
|
||||
if (current is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers) {
|
||||
inDegree[receiver]--;
|
||||
if (inDegree[receiver] == 0) // If all dependencies resolved
|
||||
queue.Enqueue(receiver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"Compute order for {startNode.name} length = {sortedOrder.Count}");
|
||||
// Check for cycles in the graph
|
||||
// if (sortedOrder.Count != this.nuclei.Count)
|
||||
// throw new InvalidOperationException("Graph is not a DAG; a cycle exists.");
|
||||
|
||||
return sortedOrder;
|
||||
}
|
||||
|
||||
// Does this even exist already?
|
||||
public void RemoveSynapse() {
|
||||
|
||||
public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus;
|
||||
get {
|
||||
if (this.clusterNuclei.Count > 0)
|
||||
return this.clusterNuclei[0] as Neuron;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
protected List<Neuron> _outputs = null;
|
||||
public List<Neuron> outputs {
|
||||
get {
|
||||
if (this._outputs == null) {
|
||||
this._outputs = new();
|
||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
||||
if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0)
|
||||
this._outputs.Add(neuron);
|
||||
}
|
||||
}
|
||||
return this._outputs;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Synapses
|
||||
public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) {
|
||||
foreach (Nucleus receptor in this.clusterNuclei) {
|
||||
if (receptor is Nucleus nucleus)
|
||||
if (nucleus.name == nucleusName) {
|
||||
foundNucleus = nucleus;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
foundNucleus = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Nucleus GetNucleus(string nucleusName) {
|
||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
||||
if (nucleus.name == nucleusName)
|
||||
return nucleus;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public IReceptor GetReceptor(string receptorName) {
|
||||
string receptorName0 = receptorName + ": 0";
|
||||
foreach (Nucleus nucleus in this.clusterNuclei) {
|
||||
if (nucleus is IReceptor receptor) {
|
||||
if (nucleus.name == receptorName | nucleus.name == receptorName0)
|
||||
//if (receptor.GetName() == receptorName)
|
||||
return receptor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#region Receivers
|
||||
|
||||
[SerializeReference]
|
||||
private List<INucleus> _receivers = new();
|
||||
public List<INucleus> receivers {
|
||||
get { return _receivers; }
|
||||
set { _receivers = value; }
|
||||
}
|
||||
|
||||
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
|
||||
this._receivers.Add(receivingNucleus);
|
||||
receivingNucleus.AddSynapse(this, weight);
|
||||
}
|
||||
|
||||
public void RemoveReceiver(INucleus receiverNucleus) {
|
||||
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
|
||||
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
||||
public virtual List<Nucleus> CollectReceivers() {
|
||||
List<Nucleus> receivers = new();
|
||||
foreach (Neuron output in this.outputs) {
|
||||
receivers.AddRange(output.receivers);
|
||||
}
|
||||
return receivers;
|
||||
}
|
||||
|
||||
#endregion Receivers
|
||||
|
||||
#region Runtime
|
||||
|
||||
[NonSerialized]
|
||||
private int stale = 1000;
|
||||
public bool isSleeping => lengthsq(this.outputValue) == 0;
|
||||
|
||||
[NonSerialized]
|
||||
protected float3 _outputValue;
|
||||
public virtual float3 outputValue {
|
||||
get { return _outputValue; }
|
||||
set {
|
||||
this.stale = 0;
|
||||
_outputValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
#region Update
|
||||
|
||||
public virtual void UpdateState() {
|
||||
UpdateState(new float3(0, 0, 0));
|
||||
public void UpdateFromNucleus(Nucleus startNucleus) {
|
||||
// no bias+synapse input state calculation for now...
|
||||
|
||||
List<Nucleus> computeOrder = this.computeOrders[startNucleus];
|
||||
if (startNucleus.trace)
|
||||
Debug.Log($"Update from {startNucleus.name}");
|
||||
foreach (Nucleus nucleus in computeOrder) {
|
||||
nucleus.UpdateStateIsolated();
|
||||
if (startNucleus.trace)
|
||||
Debug.Log($" {nucleus.name} = {nucleus.outputValue}");
|
||||
}
|
||||
|
||||
this.outputValue = this.defaultOutput.outputValue;
|
||||
this.stale = 0;
|
||||
|
||||
UpdateNuclei();
|
||||
}
|
||||
|
||||
public void UpdateState(float3 bias) {
|
||||
float3 sum = bias; // new(0, 0, 0);
|
||||
public override void UpdateStateIsolated() {
|
||||
float3 sum = this.bias;
|
||||
|
||||
//Applying the weight factors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||
if (lengthsq(synapse.nucleus.outputValue) > 0) {
|
||||
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||
this.stale = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//this.inputs[0].UpdateState(sum);
|
||||
this.inputs[0].UpdateStateIsolated(sum);
|
||||
foreach (IReceptor receptor in this.sortedNuclei) {
|
||||
if (receptor is INucleus nucleus && nucleus != this.inputs[0])
|
||||
nucleus.UpdateStateIsolated();
|
||||
}
|
||||
foreach (Nucleus nucleus in this.sortedNuclei)
|
||||
nucleus.UpdateStateIsolated();
|
||||
|
||||
UpdateResult(this.output.outputValue);
|
||||
this.outputValue = this.defaultOutput.outputValue;
|
||||
this.stale = 0;
|
||||
|
||||
UpdateNuclei();
|
||||
}
|
||||
|
||||
public void UpdateStateIsolated() {
|
||||
float3 bias = new(0,0,0);
|
||||
UpdateStateIsolated(bias);
|
||||
}
|
||||
public void UpdateStateIsolated(float3 bias) {
|
||||
float3 sum = bias; // new(0, 0, 0);
|
||||
|
||||
//Applying the weight factors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||
}
|
||||
|
||||
//this.inputs[0].UpdateState(sum);
|
||||
this.inputs[0].UpdateStateIsolated(sum);
|
||||
foreach (IReceptor receptor in this.sortedNuclei) {
|
||||
if (receptor is INucleus nucleus && nucleus != this.inputs[0])
|
||||
nucleus.UpdateStateIsolated();
|
||||
}
|
||||
this.outputValue = this.output.outputValue;
|
||||
}
|
||||
|
||||
public virtual void UpdateResult(Vector3 result) {
|
||||
// float d = Vector3.Distance(result, this.outputValue);
|
||||
// if (d < 0.5f) {
|
||||
// //Debug.Log($"insignificant update: {d}");
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.outputValue = result;
|
||||
foreach (INucleus receiver in this.receivers)
|
||||
receiver.UpdateState();
|
||||
}
|
||||
|
||||
public void UpdateNuclei() {
|
||||
this.stale++;
|
||||
if (this.stale > 2)
|
||||
_outputValue = Vector3.zero;
|
||||
|
||||
//foreach (IReceptor nucleus in this.prefab.nuclei)
|
||||
foreach (IReceptor nucleus in this.nuclei)
|
||||
public override void UpdateNuclei() {
|
||||
foreach (Nucleus nucleus in this.clusterNuclei)
|
||||
nucleus.UpdateNuclei();
|
||||
}
|
||||
|
||||
#endregion Update
|
||||
|
||||
#endregion Runtime
|
||||
}
|
||||
|
||||
@ -3,22 +3,21 @@ using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Passer/Cluster")]
|
||||
public class ClusterPrefab : ScriptableObject {
|
||||
|
||||
// The ScriptableObject asset from which the runtime object has been created
|
||||
|
||||
[SerializeReference]
|
||||
public List<IReceptor> nuclei = new();
|
||||
public List<Nucleus> nuclei = new();
|
||||
|
||||
|
||||
public virtual INucleus output => this.nuclei[0] as INucleus;
|
||||
public virtual Nucleus output => this.nuclei[0] as Nucleus;
|
||||
|
||||
public List<INucleus> _inputs = null;
|
||||
public virtual List<INucleus> inputs {
|
||||
public List<Nucleus> _inputs = null;
|
||||
public virtual List<Nucleus> inputs {
|
||||
get {
|
||||
if (this._inputs == null) {
|
||||
this._inputs = new();
|
||||
foreach (IReceptor receptor in this.nuclei) {
|
||||
if (receptor is INucleus nucleus) {
|
||||
foreach (Nucleus receptor in this.nuclei) {
|
||||
if (receptor is Nucleus nucleus) {
|
||||
// inputs have no incoming synapses yet.
|
||||
if (nucleus.synapses.Count == 0)
|
||||
this._inputs.Add(nucleus);
|
||||
@ -28,53 +27,91 @@ public class ClusterPrefab : ScriptableObject {
|
||||
return this._inputs;
|
||||
}
|
||||
}
|
||||
private List<Nucleus> _outputs = null;
|
||||
public List<Nucleus> outputs {
|
||||
get {
|
||||
if (this._outputs == null)
|
||||
RefreshOutputs();
|
||||
return this._outputs;
|
||||
}
|
||||
}
|
||||
public void RefreshOutputs() {
|
||||
this._outputs = new();
|
||||
foreach (Nucleus nucleus in this.nuclei) {
|
||||
if (nucleus is Neuron neuron && neuron.receivers.Count == 0)
|
||||
this._outputs.Add(nucleus);
|
||||
}
|
||||
}
|
||||
|
||||
public Nucleus GetNucleus(string nucleusName) {
|
||||
foreach (Nucleus nucleus in this.nuclei) {
|
||||
if (nucleus.name == nucleusName)
|
||||
return nucleus;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Call this function to ensure that there is at least one nucleus
|
||||
// This is an invariant and should be ensured before the nucleus is used
|
||||
// because output requires it.
|
||||
public void EnsureInitialization() {
|
||||
nuclei ??= new List<IReceptor>();
|
||||
nuclei ??= new List<Nucleus>();
|
||||
if (nuclei.Count == 0)
|
||||
new Neuron(this, "Output"); // Every cluster should have at least 1 neuron
|
||||
}
|
||||
|
||||
public void GarbageCollection() {
|
||||
HashSet<INucleus> visitedNuclei = new();
|
||||
MarkNuclei(visitedNuclei, this.output);
|
||||
HashSet<Nucleus> visitedNuclei = new();
|
||||
foreach (Nucleus output in this.outputs)
|
||||
MarkNuclei(visitedNuclei, output);
|
||||
//MarkNuclei(visitedNuclei, this.output);
|
||||
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
|
||||
this.nuclei.RemoveAll(nucleus => nucleus is INucleus n && visitedNuclei.Contains(n) == false);
|
||||
this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false);
|
||||
}
|
||||
|
||||
public void MarkNuclei(HashSet<INucleus> visitedNuclei, INucleus nucleus) {
|
||||
public void MarkNuclei(HashSet<Nucleus> visitedNuclei, Nucleus nucleus) {
|
||||
if (nucleus is null)
|
||||
return;
|
||||
|
||||
visitedNuclei.Add(nucleus);
|
||||
if (nucleus.parent != null && nucleus.parent.prefab != this)
|
||||
visitedNuclei.Add(nucleus.parent);
|
||||
else
|
||||
visitedNuclei.Add(nucleus);
|
||||
if (nucleus.synapses != null) {
|
||||
HashSet<Synapse> visitedSynapses = new();
|
||||
foreach (Synapse synapse in nucleus.synapses) {
|
||||
if (synapse != null && synapse.nucleus != null) {
|
||||
visitedSynapses.Add(synapse);
|
||||
if (synapse.nucleus is INucleus synapse_nucleus)
|
||||
if (synapse.nucleus is Nucleus synapse_nucleus)
|
||||
MarkNuclei(visitedNuclei, synapse_nucleus);
|
||||
}
|
||||
}
|
||||
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
|
||||
}
|
||||
if (nucleus.receivers != null) {
|
||||
HashSet<INucleus> visitedReceivers = new();
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
if (nucleus is Neuron neuron && neuron.receivers != null) {
|
||||
HashSet<Nucleus> visitedReceivers = new();
|
||||
foreach (Nucleus receiver in neuron.receivers) {
|
||||
if (receiver != null && receiver != null) {
|
||||
visitedReceivers.Add(receiver);
|
||||
visitedNuclei.Add(receiver);
|
||||
}
|
||||
}
|
||||
nucleus.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
|
||||
neuron.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void UpdateNuclei() {
|
||||
foreach (IReceptor nucleus in this.nuclei)
|
||||
foreach (Nucleus nucleus in this.nuclei)
|
||||
nucleus.UpdateNuclei();
|
||||
}
|
||||
|
||||
public int GetNucleusIndex(Nucleus receiver) {
|
||||
int ix = 0;
|
||||
foreach (Nucleus nucleus in this.nuclei) {
|
||||
if (receiver == nucleus)
|
||||
return ix;
|
||||
ix++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
110
ClusterReceptor.cs
Normal file
110
ClusterReceptor.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[Serializable]
|
||||
public class ClusterReceptor : Cluster, IReceptor {
|
||||
public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) {
|
||||
this.name = name;
|
||||
this.array ??= new NucleusArray(this);
|
||||
}
|
||||
public ClusterReceptor(ClusterPrefab prefabToInstantiate, ClusterPrefab parent, string name) : base(prefabToInstantiate, parent) {
|
||||
this.name = name;
|
||||
this.array = new NucleusArray(this);
|
||||
}
|
||||
|
||||
public string GetName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
||||
ClusterReceptor clone = new(this.prefab, parent, this.name) {
|
||||
clusterPrefab = this.clusterPrefab,
|
||||
array = this.array
|
||||
};
|
||||
|
||||
// This cloned the prefab with the clusternuclei,
|
||||
// but did not clone the receivers outside the cluster
|
||||
RestoreExternalReceivers(clone, this.clusterPrefab, parent);
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override Nucleus Clone(ClusterPrefab parent) {
|
||||
ClusterReceptor clone = new(prefab, parent, this.name) {
|
||||
array = this.array
|
||||
};
|
||||
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||
clonedSynapse.weight = synapse.weight;
|
||||
}
|
||||
|
||||
// foreach (Nucleus receiver in this.receivers) {
|
||||
// clone.AddReceiver(receiver);
|
||||
// }
|
||||
|
||||
this._outputs = null; // Make sure the output are regenerated
|
||||
foreach (Neuron output in this.outputs) {
|
||||
int ix = GetNucleusIndex(this.clusterNuclei, output);
|
||||
if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput)
|
||||
continue;
|
||||
|
||||
foreach (Nucleus receiver in output.receivers)
|
||||
clonedOutput.AddReceiver(receiver);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
[SerializeReference]
|
||||
private NucleusArray _array;
|
||||
public NucleusArray array {
|
||||
get { return _array; }
|
||||
set { _array = value; }
|
||||
}
|
||||
|
||||
//[SerializeReference]
|
||||
//private Nucleus[] _nucleusArray;
|
||||
public Nucleus[] nucleiArray {
|
||||
get { return _array.nuclei; }
|
||||
set { _array.nuclei = value; }
|
||||
}
|
||||
|
||||
public void AddReceptorElement(ClusterPrefab prefab) {
|
||||
this.nucleiArray = IReceptorHelpers.AddReceptorElement(this.nucleiArray, prefab);
|
||||
}
|
||||
|
||||
public void RemoveReceptorElement() {
|
||||
this.nucleiArray = IReceptorHelpers.RemoveReceptorElement(this.nucleiArray);
|
||||
}
|
||||
|
||||
public override void UpdateStateIsolated() {
|
||||
float3 sum = this.bias;
|
||||
|
||||
foreach (Nucleus nucleus in this.sortedNuclei)
|
||||
nucleus.UpdateStateIsolated();
|
||||
|
||||
this.outputValue = this.defaultOutput.outputValue;
|
||||
this.stale = 0;
|
||||
|
||||
UpdateNuclei();
|
||||
}
|
||||
|
||||
public override void UpdateNuclei() {
|
||||
this.stale++;
|
||||
if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) {
|
||||
this.bias = new float3(0, 0, 0);
|
||||
this.parent.UpdateFromNucleus(this);
|
||||
}
|
||||
|
||||
foreach (Nucleus nucleus in this.clusterNuclei)
|
||||
nucleus.UpdateNuclei();
|
||||
}
|
||||
|
||||
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
|
||||
this.array ??= new NucleusArray(this.parent);
|
||||
this.array.ProcessStimulus(thingId, inputValue, thingName);
|
||||
}
|
||||
}
|
||||
2
ClusterReceptor.cs.meta
Normal file
2
ClusterReceptor.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f64f5d72a422a7c8bb9ace598432aad
|
||||
365
Editor/BrainEditorWindow.cs
Normal file
365
Editor/BrainEditorWindow.cs
Normal file
@ -0,0 +1,365 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
// Simple DAG data model
|
||||
[System.Serializable]
|
||||
public class DagNode {
|
||||
public int id;
|
||||
public string title;
|
||||
public Vector2 position;
|
||||
public float radius = 20f; // circle radius
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class DagEdge {
|
||||
public int fromId;
|
||||
public int toId;
|
||||
}
|
||||
|
||||
public class BrainEditorWindow : EditorWindow {
|
||||
readonly List<DagNode> nodes = new();
|
||||
readonly List<DagEdge> edges = new();
|
||||
|
||||
Vector2 pan = Vector2.zero;
|
||||
float zoom = 1.0f;
|
||||
const float minZoom = 0.5f;
|
||||
const float maxZoom = 2.0f;
|
||||
|
||||
// Vector2 dragStart;
|
||||
// bool draggingNode = false;
|
||||
// int draggingNodeId = -1;
|
||||
|
||||
private readonly System.Type acceptedType = typeof(ClusterPrefab);
|
||||
|
||||
[MenuItem("Window/Brain Viewer")]
|
||||
public static void ShowWindow() {
|
||||
var w = GetWindow<BrainEditorWindow>("Brain Viewer");
|
||||
w.minSize = new Vector2(500, 300);
|
||||
}
|
||||
|
||||
void OnEnable() {
|
||||
// if (nodes.Count == 0)
|
||||
// CreateSampleGraph();
|
||||
|
||||
|
||||
// Register callback so window updates when selection changes
|
||||
Selection.selectionChanged += OnSelectionChanged;
|
||||
RefreshSelection();
|
||||
ComputeLeftToRightLayout();
|
||||
}
|
||||
|
||||
private void OnDisable() {
|
||||
Selection.selectionChanged -= OnSelectionChanged;
|
||||
}
|
||||
|
||||
private void OnSelectionChanged() {
|
||||
RefreshSelection();
|
||||
ComputeLeftToRightLayout();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void RefreshSelection() {
|
||||
ClusterPrefab prefab = Selection.activeObject as ClusterPrefab;
|
||||
if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) {
|
||||
GenerateGraph(prefab);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateGraph(ClusterPrefab prefab) {
|
||||
nodes.Clear();
|
||||
edges.Clear();
|
||||
|
||||
int ix = 0;
|
||||
foreach (Nucleus nucleus in prefab.nuclei) {
|
||||
nodes.Add(new DagNode() { id = ix, title = nucleus.name });
|
||||
if (nucleus is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers) {
|
||||
int receiverIx = prefab.GetNucleusIndex(receiver);
|
||||
edges.Add(new DagEdge() { fromId = ix, toId = receiverIx });
|
||||
}
|
||||
}
|
||||
ix++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// void CreateSampleGraph() {
|
||||
// nodes.Clear();
|
||||
// edges.Clear();
|
||||
|
||||
// nodes.Add(new DagNode() { id = 0, title = "In1" });
|
||||
// nodes.Add(new DagNode() { id = 1, title = "In2" });
|
||||
// nodes.Add(new DagNode() { id = 2, title = "A" });
|
||||
// nodes.Add(new DagNode() { id = 3, title = "B" });
|
||||
// nodes.Add(new DagNode() { id = 4, title = "C" });
|
||||
// nodes.Add(new DagNode() { id = 5, title = "Out1" });
|
||||
// nodes.Add(new DagNode() { id = 6, title = "Out2" });
|
||||
|
||||
// edges.Add(new DagEdge() { fromId = 0, toId = 2 });
|
||||
// edges.Add(new DagEdge() { fromId = 1, toId = 2 });
|
||||
// edges.Add(new DagEdge() { fromId = 2, toId = 3 });
|
||||
// edges.Add(new DagEdge() { fromId = 2, toId = 4 });
|
||||
// edges.Add(new DagEdge() { fromId = 3, toId = 5 });
|
||||
// edges.Add(new DagEdge() { fromId = 4, toId = 6 });
|
||||
// }
|
||||
|
||||
void OnGUI() {
|
||||
HandleInput();
|
||||
|
||||
Rect rect = new(0, 0, position.width, position.height);
|
||||
EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f));
|
||||
|
||||
// compute window center
|
||||
Vector2 windowCenter = new(position.width / 2f, position.height / 2f);
|
||||
|
||||
// compute graph bounds center (in graph space)
|
||||
Rect bounds = GetGraphBounds();
|
||||
Vector2 graphCenter = bounds.center;
|
||||
|
||||
// compute autoPan that recenters the graph (does not modify node positions)
|
||||
Vector2 autoPan = -graphCenter; // moves graph center to origin
|
||||
// total translation = windowCenter + autoPan + user pan
|
||||
Matrix4x4 oldMatrix = GUI.matrix;
|
||||
GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one * zoom) *
|
||||
Matrix4x4.TRS(-windowCenter, Quaternion.identity, Vector3.one);
|
||||
|
||||
|
||||
// Draw edges first
|
||||
foreach (DagEdge e in edges) {
|
||||
DagNode from = GetNodeById(e.fromId);
|
||||
DagNode to = GetNodeById(e.toId);
|
||||
if (from == null || to == null) continue;
|
||||
DrawEdgeCircleNodes(from, to);
|
||||
}
|
||||
|
||||
// Draw nodes (circles)
|
||||
foreach (DagNode n in nodes)
|
||||
DrawNucleus(n);
|
||||
|
||||
GUI.matrix = oldMatrix;
|
||||
|
||||
// Footer toolbar
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView();
|
||||
if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
void HandleInput() {
|
||||
Event e = Event.current;
|
||||
|
||||
// Zoom with scroll
|
||||
if (e.type == EventType.ScrollWheel) {
|
||||
float oldZoom = zoom;
|
||||
float delta = -e.delta.y * 0.01f;
|
||||
zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom);
|
||||
Vector2 mouse = e.mousePosition;
|
||||
pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom);
|
||||
e.Use();
|
||||
}
|
||||
|
||||
// Pan with middle or right+ctrl drag
|
||||
if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) {
|
||||
pan += e.delta;
|
||||
e.Use();
|
||||
}
|
||||
}
|
||||
|
||||
DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id);
|
||||
List<DagEdge> GetIncomingEdges(DagNode node) {
|
||||
List<DagEdge> incoming = new();
|
||||
foreach (DagEdge e in edges) {
|
||||
if (e.toId == node.id)
|
||||
incoming.Add(e);
|
||||
}
|
||||
return incoming;
|
||||
}
|
||||
List<DagEdge> GetOutgoingEdges(DagNode node) {
|
||||
List<DagEdge> outgoing = new();
|
||||
foreach (DagEdge e in edges) {
|
||||
if (e.fromId == node.id)
|
||||
outgoing.Add(e);
|
||||
}
|
||||
return outgoing;
|
||||
}
|
||||
|
||||
void DrawNucleus(DagNode n) {
|
||||
Vector3 position = n.position;
|
||||
|
||||
Handles.color = Color.white * 0.9f;
|
||||
Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius);
|
||||
|
||||
if (GetIncomingEdges(n).Count == 0)
|
||||
DrawArrowHead(n.position - new Vector2(n.radius + 10, 0), n.position - new Vector2(n.radius + 5, 0), 10f / zoom, 12f / zoom, Color.white);
|
||||
if (GetOutgoingEdges(n).Count == 0)
|
||||
DrawArrowHead(n.position + new Vector2(n.radius + 10, 0), n.position + new Vector2(n.radius + 15, 0), 10f / zoom, 12f / zoom, Color.white);
|
||||
|
||||
Handles.color = Color.white;
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
Vector3 labelPos = position - Vector3.down * (n.radius + 10f); // below disc along up axis
|
||||
Handles.Label(labelPos, n.title, style);
|
||||
}
|
||||
|
||||
void DrawEdgeCircleNodes(DagNode from, DagNode to) {
|
||||
Vector2 a = from.position;
|
||||
Vector2 b = to.position;
|
||||
if (a == b) return;
|
||||
|
||||
Handles.color = Color.white * 0.9f;
|
||||
Handles.DrawLine(from.position, to.position);
|
||||
|
||||
// Vector2 dir = (b - a).normalized;
|
||||
// Vector2 start = a + dir * from.radius;
|
||||
// Vector2 end = b - dir * to.radius;
|
||||
|
||||
//DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white);
|
||||
|
||||
}
|
||||
|
||||
void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) {
|
||||
Vector2 dir = (to - from).normalized;
|
||||
if (dir == Vector2.zero) return;
|
||||
Vector2 right = new Vector2(-dir.y, dir.x);
|
||||
|
||||
Vector3 p1 = to;
|
||||
Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f;
|
||||
Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f;
|
||||
|
||||
Handles.color = color;
|
||||
Handles.DrawAAConvexPolygon(p1, p2, p3);
|
||||
}
|
||||
|
||||
// Left-to-right layered layout (sources on the left, sinks on the right)
|
||||
void ComputeLeftToRightLayout() {
|
||||
// build adjacency and indegree
|
||||
var adj = nodes.ToDictionary(n => n.id, n => new List<int>());
|
||||
var indeg = nodes.ToDictionary(n => n.id, n => 0);
|
||||
foreach (var e in edges) {
|
||||
if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue;
|
||||
adj[e.fromId].Add(e.toId);
|
||||
indeg[e.toId]++;
|
||||
}
|
||||
|
||||
// Kahn's algorithm to compute topological layers (horizontal layers)
|
||||
Dictionary<int, int> layer = new();
|
||||
Queue<int> q = new(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key));
|
||||
foreach (var id in q) layer[id] = 0;
|
||||
|
||||
while (q.Count > 0) {
|
||||
int u = q.Dequeue();
|
||||
int l = layer[u];
|
||||
foreach (var v in adj[u]) {
|
||||
// prefer placing v at least one layer after u
|
||||
if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1;
|
||||
indeg[v]--;
|
||||
if (indeg[v] == 0) q.Enqueue(v);
|
||||
}
|
||||
}
|
||||
|
||||
// Any unreachable nodes -> assign next layers
|
||||
int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0;
|
||||
foreach (var n in nodes) {
|
||||
if (!layer.ContainsKey(n.id)) {
|
||||
maxLayer++;
|
||||
layer[n.id] = maxLayer;
|
||||
}
|
||||
}
|
||||
|
||||
// Group nodes by layer (left to right)
|
||||
var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList();
|
||||
|
||||
// Layout parameters (horizontal spacing drives left->right)
|
||||
float hSpacing = 150f;
|
||||
float vSpacing = 100f;
|
||||
|
||||
// Place nodes: x increases with layer index, y spaced within layer
|
||||
for (int li = 0; li < layers.Count; li++) {
|
||||
var lst = layers[li];
|
||||
float totalHeight = (lst.Count - 1) * vSpacing;
|
||||
for (int i = 0; i < lst.Count; i++) {
|
||||
int id = lst[i];
|
||||
var n = GetNodeById(id);
|
||||
if (n == null) continue;
|
||||
float x = hSpacing + li * hSpacing;
|
||||
float y = 400 - totalHeight / 2f + i * vSpacing;
|
||||
// Debug.Log($"({li}, {i}) -> {x}, {y}");
|
||||
n.position = new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
void FitToView() {
|
||||
if (nodes.Count == 0) return;
|
||||
// compute bounds including radii
|
||||
Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f);
|
||||
foreach (var n in nodes)
|
||||
bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f));
|
||||
|
||||
// center graph at origin (0,0) then set pan so it appears centered in window
|
||||
Vector2 graphCenter = bounds.center;
|
||||
// move nodes so center is at origin
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
nodes[i].position -= graphCenter;
|
||||
|
||||
// reset pan/zoom so centered
|
||||
pan = Vector2.zero;
|
||||
zoom = 1.0f;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
|
||||
static Rect RectUnion(Rect a, Rect b) {
|
||||
float xMin = Mathf.Min(a.xMin, b.xMin);
|
||||
float xMax = Mathf.Max(a.xMax, b.xMax);
|
||||
float yMin = Mathf.Min(a.yMin, b.yMin);
|
||||
float yMax = Mathf.Max(a.yMax, b.yMax);
|
||||
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
|
||||
}
|
||||
|
||||
Vector2 ScreenToGraph_old(Vector2 screenPos) {
|
||||
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
|
||||
// invert the GUI.matrix transform (approx for current simple transforms)
|
||||
return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom);
|
||||
}
|
||||
Vector2 ScreenToGraph(Vector2 screenPos) {
|
||||
Vector2 windowCenter = new Vector2(position.width / 2f, position.height / 2f);
|
||||
Rect bounds = GetGraphBounds();
|
||||
Vector2 graphCenter = bounds.center;
|
||||
Vector2 autoPan = -graphCenter;
|
||||
// inverse of: screen -> translate by -(windowCenter+autoPan+pan), scale by 1/zoom, translate by windowCenter
|
||||
return (screenPos - (windowCenter + autoPan + pan)) / zoom + windowCenter;
|
||||
}
|
||||
|
||||
|
||||
Rect GetGraphBounds() {
|
||||
if (nodes == null || nodes.Count == 0) return new Rect(Vector2.zero, Vector2.one);
|
||||
Rect bounds = new(
|
||||
nodes[0].position - Vector2.one * nodes[0].radius,
|
||||
2f * nodes[0].radius * Vector2.one);
|
||||
foreach (var n in nodes)
|
||||
bounds = RectUnion(bounds,
|
||||
new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one));
|
||||
return bounds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
int HitTestNode(Vector2 graphPos) {
|
||||
// returns node id under point or -1
|
||||
for (int i = nodes.Count - 1; i >= 0; i--) {
|
||||
var n = nodes[i];
|
||||
if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
2
Editor/BrainEditorWindow.cs.meta
Normal file
2
Editor/BrainEditorWindow.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f041740900808273ab006e7d276a78e9
|
||||
1100
Editor/ClusterInspector.cs
Normal file
1100
Editor/ClusterInspector.cs
Normal file
File diff suppressed because it is too large
Load Diff
393
Editor/DAGWindow.cs
Normal file
393
Editor/DAGWindow.cs
Normal file
@ -0,0 +1,393 @@
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
// Simple DAG data model
|
||||
// [System.Serializable]
|
||||
// public class DagNode
|
||||
// {
|
||||
// public int id;
|
||||
// public string title;
|
||||
// public Vector2 position;
|
||||
// public float radius = 36f; // circle radius
|
||||
// }
|
||||
|
||||
// [System.Serializable]
|
||||
// public class DagEdge
|
||||
// {
|
||||
// public int fromId;
|
||||
// public int toId;
|
||||
// }
|
||||
|
||||
public class DAGEditorWindow : EditorWindow
|
||||
{
|
||||
List<DagNode> nodes = new List<DagNode>();
|
||||
List<DagEdge> edges = new List<DagEdge>();
|
||||
|
||||
Vector2 pan = Vector2.zero;
|
||||
float zoom = 1.0f;
|
||||
const float minZoom = 0.5f;
|
||||
const float maxZoom = 2.0f;
|
||||
|
||||
GUIStyle labelStyle;
|
||||
int selectedNodeId = -1;
|
||||
|
||||
Vector2 dragStart;
|
||||
bool draggingNode = false;
|
||||
int draggingNodeId = -1;
|
||||
|
||||
[MenuItem("Window/DAG Viewer (LR, Circles)")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var w = GetWindow<DAGEditorWindow>("DAG Viewer (LR)");
|
||||
w.minSize = new Vector2(500, 300);
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
labelStyle = new GUIStyle(EditorStyles.label);
|
||||
labelStyle.alignment = TextAnchor.MiddleCenter;
|
||||
labelStyle.normal.textColor = Color.white;
|
||||
labelStyle.fontStyle = FontStyle.Bold;
|
||||
|
||||
if (nodes.Count == 0)
|
||||
CreateSampleGraph();
|
||||
|
||||
ComputeLeftToRightLayout();
|
||||
}
|
||||
|
||||
void CreateSampleGraph()
|
||||
{
|
||||
nodes.Clear();
|
||||
edges.Clear();
|
||||
|
||||
nodes.Add(new DagNode() { id = 0, title = "In1" });
|
||||
nodes.Add(new DagNode() { id = 1, title = "In2" });
|
||||
nodes.Add(new DagNode() { id = 2, title = "A" });
|
||||
nodes.Add(new DagNode() { id = 3, title = "B" });
|
||||
nodes.Add(new DagNode() { id = 4, title = "C" });
|
||||
nodes.Add(new DagNode() { id = 5, title = "Out1" });
|
||||
nodes.Add(new DagNode() { id = 6, title = "Out2" });
|
||||
|
||||
edges.Add(new DagEdge() { fromId = 0, toId = 2 });
|
||||
edges.Add(new DagEdge() { fromId = 1, toId = 2 });
|
||||
edges.Add(new DagEdge() { fromId = 2, toId = 3 });
|
||||
edges.Add(new DagEdge() { fromId = 2, toId = 4 });
|
||||
edges.Add(new DagEdge() { fromId = 3, toId = 5 });
|
||||
edges.Add(new DagEdge() { fromId = 4, toId = 6 });
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
HandleInput();
|
||||
|
||||
Rect rect = new Rect(0, 0, position.width, position.height);
|
||||
EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f));
|
||||
|
||||
Matrix4x4 oldMatrix = GUI.matrix;
|
||||
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
|
||||
GUI.matrix = Matrix4x4.TRS(origin + pan, Quaternion.identity, Vector3.one * zoom) *
|
||||
Matrix4x4.TRS(-origin, Quaternion.identity, Vector3.one);
|
||||
|
||||
// Draw edges first
|
||||
foreach (var e in edges)
|
||||
{
|
||||
var from = GetNodeById(e.fromId);
|
||||
var to = GetNodeById(e.toId);
|
||||
if (from == null || to == null) continue;
|
||||
DrawEdgeCircleNodes(from, to);
|
||||
}
|
||||
|
||||
// Draw nodes (circles)
|
||||
foreach (var n in nodes)
|
||||
{
|
||||
DrawNodeCircle(n);
|
||||
}
|
||||
|
||||
GUI.matrix = oldMatrix;
|
||||
|
||||
// Footer toolbar
|
||||
GUILayout.FlexibleSpace();
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView();
|
||||
if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout();
|
||||
if (GUILayout.Button("Add Node", EditorStyles.toolbarButton))
|
||||
{
|
||||
AddNode("N" + nodes.Count);
|
||||
ComputeLeftToRightLayout();
|
||||
}
|
||||
if (GUILayout.Button("Add Edge (selected->new)", EditorStyles.toolbarButton))
|
||||
{
|
||||
if (selectedNodeId != -1)
|
||||
{
|
||||
var newNode = AddNode("N" + nodes.Count);
|
||||
edges.Add(new DagEdge() { fromId = selectedNodeId, toId = newNode.id });
|
||||
ComputeLeftToRightLayout();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
void HandleInput()
|
||||
{
|
||||
Event e = Event.current;
|
||||
|
||||
// Zoom with scroll
|
||||
if (e.type == EventType.ScrollWheel)
|
||||
{
|
||||
float oldZoom = zoom;
|
||||
float delta = -e.delta.y * 0.01f;
|
||||
zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom);
|
||||
Vector2 mouse = e.mousePosition;
|
||||
pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom);
|
||||
e.Use();
|
||||
}
|
||||
|
||||
// Pan with middle or right+ctrl drag
|
||||
if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control)))
|
||||
{
|
||||
pan += e.delta;
|
||||
e.Use();
|
||||
}
|
||||
|
||||
// Node dragging & selection (convert mouse to graph space)
|
||||
Vector2 graphMouse = ScreenToGraph(e.mousePosition);
|
||||
if (e.type == EventType.MouseDown && e.button == 0)
|
||||
{
|
||||
int hit = HitTestNode(graphMouse);
|
||||
if (hit != -1)
|
||||
{
|
||||
selectedNodeId = hit;
|
||||
draggingNode = true;
|
||||
draggingNodeId = hit;
|
||||
dragStart = graphMouse;
|
||||
e.Use();
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedNodeId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (draggingNode && draggingNodeId != -1)
|
||||
{
|
||||
if (e.type == EventType.MouseDrag && e.button == 0)
|
||||
{
|
||||
Vector2 graphDelta = e.delta / zoom;
|
||||
var n = GetNodeById(draggingNodeId);
|
||||
if (n != null)
|
||||
{
|
||||
n.position += graphDelta;
|
||||
Repaint();
|
||||
e.Use();
|
||||
}
|
||||
}
|
||||
if (e.type == EventType.MouseUp && e.button == 0)
|
||||
{
|
||||
draggingNode = false;
|
||||
draggingNodeId = -1;
|
||||
e.Use();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DagNode AddNode(string title)
|
||||
{
|
||||
int nextId = nodes.Count > 0 ? nodes.Max(n => n.id) + 1 : 0;
|
||||
var n = new DagNode() { id = nextId, title = title, position = Vector2.zero };
|
||||
nodes.Add(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id);
|
||||
|
||||
void DrawNodeCircle(DagNode n)
|
||||
{
|
||||
Vector2 center = n.position;
|
||||
float r = n.radius;
|
||||
Rect nodeRect = new Rect(center.x - r, center.y - r, r * 2, r * 2);
|
||||
|
||||
// circle background
|
||||
Color bg = (n.id == selectedNodeId) ? new Color(0.15f, 0.5f, 0.9f) : new Color(0.2f, 0.2f, 0.2f);
|
||||
EditorGUI.DrawRect(nodeRect, bg);
|
||||
|
||||
// anti-aliased circle outline
|
||||
Handles.color = Color.white * 0.9f;
|
||||
Handles.DrawAAPolyLine(3f / zoom, GetCircleOutlinePoints(center, r, 48).ToArray());
|
||||
|
||||
// label
|
||||
Vector2 labelPos = center - new Vector2(0, 8);
|
||||
GUI.Label(new Rect(labelPos.x - r, labelPos.y - 8, r * 2, 18), n.title, labelStyle);
|
||||
}
|
||||
|
||||
List<Vector3> GetCircleOutlinePoints(Vector2 center, float radius, int segments)
|
||||
{
|
||||
var pts = new List<Vector3>(segments + 1);
|
||||
for (int i = 0; i <= segments; i++)
|
||||
{
|
||||
float a = (float)i / segments * Mathf.PI * 2f;
|
||||
pts.Add(new Vector3(center.x + Mathf.Cos(a) * radius, center.y + Mathf.Sin(a) * radius, 0));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
void DrawEdgeCircleNodes(DagNode from, DagNode to)
|
||||
{
|
||||
Vector2 a = from.position;
|
||||
Vector2 b = to.position;
|
||||
if (a == b) return;
|
||||
|
||||
// Compute edge line that starts/ends at circle circumferences
|
||||
Vector2 dir = (b - a).normalized;
|
||||
Vector2 start = a + dir * from.radius;
|
||||
Vector2 end = b - dir * to.radius;
|
||||
|
||||
// Use a simple curved line: start -> control -> end (bezier)
|
||||
Vector2 control = new Vector2((start.x + end.x) / 2f, (start.y + end.y) / 2f);
|
||||
// Slight vertical offset to separate overlapping lines based on node ids
|
||||
float offset = ((from.id * 7 + to.id * 11) % 7 - 3) * 6f / zoom;
|
||||
control += new Vector2(0, offset);
|
||||
|
||||
Handles.color = Color.white * 0.9f;
|
||||
Handles.DrawAAPolyLine(3f / zoom, 20, GetBezierPoints(start, control, end, 24).ToArray());
|
||||
|
||||
// Arrow at end pointing towards 'b'
|
||||
DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white);
|
||||
}
|
||||
|
||||
List<Vector3> GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg)
|
||||
{
|
||||
var pts = new List<Vector3>(seg + 1);
|
||||
for (int i = 0; i <= seg; i++)
|
||||
{
|
||||
float t = (float)i / seg;
|
||||
Vector2 p = (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
|
||||
pts.Add(new Vector3(p.x, p.y, 0));
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color)
|
||||
{
|
||||
Vector2 dir = (to - from).normalized;
|
||||
if (dir == Vector2.zero) return;
|
||||
Vector2 right = new Vector2(-dir.y, dir.x);
|
||||
|
||||
Vector3 p1 = to;
|
||||
Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f;
|
||||
Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f;
|
||||
|
||||
Handles.color = color;
|
||||
Handles.DrawAAConvexPolygon(p1, p2, p3);
|
||||
}
|
||||
|
||||
// Left-to-right layered layout (sources on the left, sinks on the right)
|
||||
void ComputeLeftToRightLayout()
|
||||
{
|
||||
// build adjacency and indegree
|
||||
var adj = nodes.ToDictionary(n => n.id, n => new List<int>());
|
||||
var indeg = nodes.ToDictionary(n => n.id, n => 0);
|
||||
foreach (var e in edges)
|
||||
{
|
||||
if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue;
|
||||
adj[e.fromId].Add(e.toId);
|
||||
indeg[e.toId]++;
|
||||
}
|
||||
|
||||
// Kahn's algorithm to compute topological layers (horizontal layers)
|
||||
Dictionary<int, int> layer = new Dictionary<int, int>();
|
||||
Queue<int> q = new Queue<int>(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key));
|
||||
foreach (var id in q) layer[id] = 0;
|
||||
|
||||
while (q.Count > 0)
|
||||
{
|
||||
int u = q.Dequeue();
|
||||
int l = layer[u];
|
||||
foreach (var v in adj[u])
|
||||
{
|
||||
// prefer placing v at least one layer after u
|
||||
if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1;
|
||||
indeg[v]--;
|
||||
if (indeg[v] == 0) q.Enqueue(v);
|
||||
}
|
||||
}
|
||||
|
||||
// Any unreachable nodes -> assign next layers
|
||||
int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0;
|
||||
foreach (var n in nodes)
|
||||
{
|
||||
if (!layer.ContainsKey(n.id))
|
||||
{
|
||||
maxLayer++;
|
||||
layer[n.id] = maxLayer;
|
||||
}
|
||||
}
|
||||
|
||||
// Group nodes by layer (left to right)
|
||||
var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList();
|
||||
|
||||
// Layout parameters (horizontal spacing drives left->right)
|
||||
float hSpacing = 220f;
|
||||
float vSpacing = 120f;
|
||||
|
||||
// Place nodes: x increases with layer index, y spaced within layer
|
||||
for (int li = 0; li < layers.Count; li++)
|
||||
{
|
||||
var lst = layers[li];
|
||||
float totalHeight = (lst.Count - 1) * vSpacing;
|
||||
for (int i = 0; i < lst.Count; i++)
|
||||
{
|
||||
int id = lst[i];
|
||||
var n = GetNodeById(id);
|
||||
if (n == null) continue;
|
||||
float x = li * hSpacing;
|
||||
float y = -totalHeight / 2f + i * vSpacing;
|
||||
n.position = new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
Repaint();
|
||||
}
|
||||
|
||||
void FitToView()
|
||||
{
|
||||
if (nodes.Count == 0) return;
|
||||
Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f);
|
||||
foreach (var n in nodes)
|
||||
bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f));
|
||||
|
||||
Vector2 center = bounds.center;
|
||||
pan = -center;
|
||||
zoom = 1.0f;
|
||||
Repaint();
|
||||
}
|
||||
|
||||
static Rect RectUnion(Rect a, Rect b)
|
||||
{
|
||||
float xMin = Mathf.Min(a.xMin, b.xMin);
|
||||
float xMax = Mathf.Max(a.xMax, b.xMax);
|
||||
float yMin = Mathf.Min(a.yMin, b.yMin);
|
||||
float yMax = Mathf.Max(a.yMax, b.yMax);
|
||||
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
|
||||
}
|
||||
|
||||
Vector2 ScreenToGraph(Vector2 screenPos)
|
||||
{
|
||||
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
|
||||
// invert the GUI.matrix transform (approx for current simple transforms)
|
||||
return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom);
|
||||
}
|
||||
|
||||
int HitTestNode(Vector2 graphPos)
|
||||
{
|
||||
// returns node id under point or -1
|
||||
for (int i = nodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var n = nodes[i];
|
||||
if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
2
Editor/DAGWindow.cs.meta
Normal file
2
Editor/DAGWindow.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95393aed582b8b30d965400672aec4d8
|
||||
47
Editor/NanoBrain_Editor.cs
Normal file
47
Editor/NanoBrain_Editor.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[CustomEditor(typeof(NanoBrain))]
|
||||
public class NanoBrainComponent_Editor : Editor {
|
||||
protected static VisualElement mainContainer;
|
||||
protected static VisualElement inspectorContainer;
|
||||
|
||||
protected NanoBrain component;
|
||||
private SerializedProperty brainProp;
|
||||
|
||||
ClusterInspector.GraphView board;
|
||||
|
||||
public void OnEnable() {
|
||||
component = target as NanoBrain;
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
brainProp = serializedObject.FindProperty(nameof(NanoBrain.defaultBrain));
|
||||
}
|
||||
|
||||
public override VisualElement CreateInspectorGUI() {
|
||||
Cluster brain = component.brain;
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedObject.Update();
|
||||
|
||||
|
||||
VisualElement root = new();
|
||||
if (Application.isPlaying == false) {
|
||||
PropertyField brainField = new(brainProp) {
|
||||
label = "Nano Brain"
|
||||
};
|
||||
root.Add(brainField);
|
||||
}
|
||||
|
||||
if (brain != null)
|
||||
ClusterInspector.CreateInspector(root, brain.prefab, brain.defaultOutput, component.gameObject);
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
return root;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,302 +0,0 @@
|
||||
/*
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class NeuroidLayer {
|
||||
public int ix = 0;
|
||||
public List<Nucleus> neuroids = new();
|
||||
}
|
||||
|
||||
public class GraphEditorWindow : EditorWindow {
|
||||
private Nucleus currentNucleus;
|
||||
private List<Neuroid> allNeuroids;
|
||||
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
|
||||
|
||||
private List<NeuroidLayer> layers = new();
|
||||
|
||||
private void OnEnable() {
|
||||
EditorApplication.update += EditorUpdate;
|
||||
Selection.selectionChanged += OnSelectionChange;
|
||||
SelectNeuron();
|
||||
}
|
||||
|
||||
private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) {
|
||||
layer.neuroids.Add(nucleus);
|
||||
nucleus.layerIx = layer.ix;
|
||||
// Store its position
|
||||
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
|
||||
neuroidPositions[nucleus] = neuroidPosition;
|
||||
|
||||
}
|
||||
|
||||
private void BuildLayers() {
|
||||
// A temporary list to track what's been added to layers
|
||||
this.layers = new();
|
||||
int layerIx = 0;
|
||||
|
||||
Nucleus selectedNucleus = this.currentNucleus;
|
||||
if (selectedNucleus == null)
|
||||
return;
|
||||
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||
|
||||
//foreach (Nucleus outputNeuroid in selectedNucleus.receivers) {
|
||||
foreach (Receiver receiver in selectedNucleus.receivers) {
|
||||
Nucleus outputNeuroid = receiver.nucleus;
|
||||
if (outputNeuroid != null) {
|
||||
AddToLayer(currentLayer, outputNeuroid);
|
||||
Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
|
||||
}
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
}
|
||||
|
||||
AddToLayer(currentLayer, selectedNucleus);
|
||||
this.layers.Add(currentLayer);
|
||||
Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
|
||||
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
|
||||
int six = 0;
|
||||
// foreach (Synapse synapse in selectedNucleus.synapses.Values) {
|
||||
// Debug.Log($"Synapse {six}");
|
||||
// Nucleus input = synapse.neuroid;
|
||||
//foreach ((Nucleus input, Synapse synapse) in selectedNucleus.synapses) {
|
||||
//foreach ((Nucleus input, float weight) in selectedNucleus.synapses) {
|
||||
foreach (Synapse synapse in selectedNucleus.synapses) {
|
||||
Nucleus input = synapse.nucleus;
|
||||
if (input != null) {
|
||||
AddToLayer(currentLayer, input);
|
||||
Debug.Log($"layer {layerIx} nucleus {input.name}");
|
||||
}
|
||||
six++;
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLayers_old(List<Neuroid> neuroids) {
|
||||
if (neuroids == null)
|
||||
return;
|
||||
|
||||
// A temporary list to track what's been added to layers
|
||||
this.layers = new();
|
||||
HashSet<Neuroid> neuronVisited = new();
|
||||
int layerIx = 0;
|
||||
|
||||
// While there are unvisited neuroid
|
||||
while (neuroids.Any(neuroid => !neuronVisited.Contains(neuroid))) {
|
||||
// Create the next layer
|
||||
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||
int neuroidIx = 0;
|
||||
|
||||
foreach (Neuroid neuroid in neuroids) {
|
||||
// Skip neurons we already processed
|
||||
if (neuronVisited.Contains(neuroid))
|
||||
continue;
|
||||
|
||||
// if (neuroid.IsStale()) {
|
||||
// Debug.Log($"neuron {neuroid.name} is stale {neuroid.stale}");
|
||||
// neuronVisited.Add(neuroid);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// If the output neuroid is visited
|
||||
// Note: this does not yet work for multiple outputs yet (see the use of First())
|
||||
// if (neuroid.receivers.Count == 0 // make sure the root neuroids are processed directly
|
||||
// || (neuronVisited.Contains(neuroid.receivers.First()) && neuroid.receivers.First().layerIx == layerIx - 1)) {
|
||||
if (neuroid.receivers.Count == 0 // make sure the root neuroids are processed directly
|
||||
|| (neuronVisited.Contains(neuroid.receivers.First().nucleus) && neuroid.receivers.First().nucleus.layerIx == layerIx - 1)) {
|
||||
// Add it to the next layer
|
||||
currentLayer.neuroids.Add(neuroid);
|
||||
neuroid.layerIx = layerIx;
|
||||
// Register it as visited
|
||||
neuronVisited.Add(neuroid);
|
||||
// Store its position
|
||||
Vector2Int neuroidPosition = new(layerIx, neuroidIx);
|
||||
neuroidPositions[neuroid] = neuroidPosition;
|
||||
neuroidIx++;
|
||||
Debug.Log($"Layer {layerIx} neuron {neuroidIx} name {neuroid.name}");
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
layerIx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable() {
|
||||
EditorApplication.update -= EditorUpdate;
|
||||
Selection.selectionChanged -= OnSelectionChange;
|
||||
}
|
||||
|
||||
private void OnSelectionChange() {
|
||||
SelectNeuron();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void EditorUpdate() {
|
||||
if (EditorApplication.isPlaying)
|
||||
Repaint();
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
GUILayout.Label("Graph Visualizer", EditorStyles.boldLabel);
|
||||
|
||||
DrawGraph();
|
||||
}
|
||||
|
||||
private void DrawGraph() {
|
||||
if (currentNucleus == null)
|
||||
return;
|
||||
|
||||
foreach (NeuroidLayer layer in layers)
|
||||
DrawLayer(layer);
|
||||
}
|
||||
|
||||
private void DrawLayer(NeuroidLayer layer) {
|
||||
int column = layer.ix * 100;
|
||||
int nodeCount = layer.neuroids.Count;
|
||||
float maxValue = 0;
|
||||
foreach (Nucleus nucleus in layer.neuroids) {
|
||||
if (nucleus is Neuroid neuroid) {
|
||||
float value = neuroid.outputValue.magnitude;
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
}
|
||||
}
|
||||
float spacing = 400f / nodeCount;
|
||||
float margin = 100 + spacing / 2;
|
||||
foreach (Nucleus layerNucleus in layer.neuroids) {
|
||||
if (layerNucleus is Neuroid layerNeuroid) {
|
||||
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNeuroid];
|
||||
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
|
||||
|
||||
int i = 0;
|
||||
float inputSpacing = 400f / layerNeuroid.synapses.Count;
|
||||
float inputMargin = 100 + inputSpacing / 2;
|
||||
// foreach (Synapse synapse in layerNeuroid.synapses.Values) {
|
||||
// if (synapse.neuroid != null) {
|
||||
// if (this.neuroidPositions.ContainsKey(synapse.neuroid)) {
|
||||
|
||||
// Vector2Int inputNeuroidPos = this.neuroidPositions[synapse.neuroid];
|
||||
//foreach ((Nucleus neuroid, Synapse synapse) in layerNeuroid.synapses) {
|
||||
//foreach ((Nucleus neuroid, float weight) in layerNeuroid.synapses) {
|
||||
foreach (Synapse synapse in layerNeuroid.synapses) {
|
||||
Nucleus neuroid = synapse.nucleus;
|
||||
float weight = synapse.weight;
|
||||
if (neuroid != null) {
|
||||
if (this.neuroidPositions.ContainsKey(neuroid)) {
|
||||
Vector2Int inputNeuroidPos = this.neuroidPositions[neuroid];
|
||||
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
|
||||
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
|
||||
|
||||
//float brightness = synapse.weight / 10.0f;
|
||||
float brightness = weight / 10.0f;
|
||||
Handles.color = new Color(brightness, brightness, brightness);
|
||||
Handles.DrawLine(parentPos, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float size = 20;
|
||||
if (layerNeuroid.isSleeping)
|
||||
Handles.color = Color.black;
|
||||
else {
|
||||
float brightness = layerNeuroid.outputValue.magnitude / maxValue;
|
||||
Handles.color = new Color(brightness, brightness, brightness);
|
||||
}
|
||||
Handles.DrawSolidDisc(parentPos, Vector3.forward, size);
|
||||
Vector3 labelPos = parentPos - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||
GUIStyle style = new GUIStyle(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold
|
||||
};
|
||||
Handles.Label(labelPos, layerNeuroid.name, style);
|
||||
|
||||
Rect neuronRect = new(parentPos.x - size, parentPos.y - size, size * 2, size * 2);
|
||||
Event e = Event.current;
|
||||
if (e != null && neuronRect.Contains(e.mousePosition)) {
|
||||
HandleMouseHover(layerNeuroid, neuronRect);
|
||||
// Process click
|
||||
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||
// Consume the event so the scene doesn't also handle it
|
||||
e.Use();
|
||||
HandleDiscClicked(layerNeuroid);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMouseHover(Neuroid neuroid, Rect rect) {
|
||||
GUIContent tooltip;
|
||||
// if (neuroid is SensoryNeuroid sensoryNeuroid) {
|
||||
// tooltip = new(
|
||||
// $"{sensoryNeuroid.name}" +
|
||||
// $"\nThing {sensoryNeuroid.receptor.thingType}" +
|
||||
// $"\nValue: {neuroid.outputValue}");
|
||||
// }
|
||||
// else {
|
||||
tooltip = new(
|
||||
$"{neuroid.name}" +
|
||||
$"\nsynapse count {neuroid.synapses.Count}" +
|
||||
$"\nValue: {neuroid.outputValue}");
|
||||
// }
|
||||
|
||||
Vector2 mousePosition = Event.current.mousePosition;
|
||||
|
||||
// Display tooltip with some offset
|
||||
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
|
||||
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
|
||||
|
||||
GUI.Box(tooltipRect, tooltip);
|
||||
}
|
||||
|
||||
private void HandleDiscClicked(Nucleus nucleus) {
|
||||
this.currentNucleus = nucleus;
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
// Update node colors based on selected GameObjects
|
||||
private void SelectNeuron() {
|
||||
GameObject[] selectedObjects = Selection.gameObjects;
|
||||
if (selectedObjects.Length == 0)
|
||||
return;
|
||||
|
||||
GameObject selectedObject = selectedObjects[0];
|
||||
Boid boid = selectedObject.GetComponent<Boid>();
|
||||
if (boid == null)
|
||||
return;
|
||||
|
||||
// Nucleus neuroid = boid.behaviour;
|
||||
// this.currentNucleus = neuroid;
|
||||
// if (neuroid == null)
|
||||
// this.allNeuroids = new();
|
||||
// else
|
||||
// this.allNeuroids = neuroid.brain.neuroids;
|
||||
|
||||
|
||||
// Debug.Log($"Neuroncount = {this.allNeuroids.Count}");
|
||||
// BuildLayers();
|
||||
// Debug.Log($"Layercount = {this.layers.Count}");
|
||||
|
||||
}
|
||||
|
||||
[MenuItem("Window/Neuroid Visualizer")]
|
||||
public static void ShowWindow() {
|
||||
GetWindow<GraphEditorWindow>("Neuroid Visualizer");
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 26e68838038ea5243ae57bc81f4db8a8
|
||||
55
INucleus.cs
55
INucleus.cs
@ -1,55 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Mathematics;
|
||||
|
||||
public interface INucleus : IReceptor {
|
||||
|
||||
#region static struct
|
||||
|
||||
// Cluster
|
||||
public ClusterPrefab cluster { get; }
|
||||
|
||||
// Senders
|
||||
public List<Synapse> synapses { get; }
|
||||
public Synapse AddSynapse(IReceptor sender, float weight = 1.0f);
|
||||
|
||||
public NucleusArray array { get; set; }
|
||||
|
||||
#endregion static struct
|
||||
|
||||
#region dynamic state
|
||||
|
||||
public void UpdateState();
|
||||
public void UpdateState(float3 inputValue);
|
||||
public void UpdateStateIsolated();
|
||||
public void UpdateStateIsolated(float3 inputValue);
|
||||
|
||||
#endregion dynamic state
|
||||
}
|
||||
|
||||
public interface IReceptor {
|
||||
#region static
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
// Receivers
|
||||
public List<INucleus> receivers { get; set; }
|
||||
|
||||
public void AddReceiver(INucleus receiver, float weight = 1);
|
||||
public void RemoveReceiver(INucleus receiverNucleus);
|
||||
|
||||
#endregion static
|
||||
|
||||
#region dynamic
|
||||
|
||||
// float3 to prepare for SIMD
|
||||
public float3 outputValue { get; }
|
||||
|
||||
public void UpdateNuclei();
|
||||
public bool isSleeping { get; }
|
||||
|
||||
#endregion dynamic
|
||||
|
||||
public IReceptor ShallowCloneTo(Cluster parent);
|
||||
public IReceptor Clone();
|
||||
}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a8a0e8965cea660abff254cab8a4723
|
||||
51
IReceptor.cs
Normal file
51
IReceptor.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
|
||||
public interface IReceptor {
|
||||
public string GetName();
|
||||
|
||||
// public NucleusArray array {
|
||||
// get; set;
|
||||
// }
|
||||
public Nucleus[] nucleiArray { get; set; }
|
||||
|
||||
public void AddReceptorElement(ClusterPrefab prefab);
|
||||
public void RemoveReceptorElement();
|
||||
|
||||
public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null);
|
||||
}
|
||||
|
||||
public static class IReceptorHelpers {
|
||||
public static Nucleus[] AddReceptorElement(Nucleus[] nucleiArray, ClusterPrefab prefab) {
|
||||
if (nucleiArray.Length == 0) {
|
||||
Debug.LogError("Empty perceptoid array, cannot add");
|
||||
return null;
|
||||
}
|
||||
int newLength = nucleiArray.Length + 1;
|
||||
Nucleus[] newArray = new Nucleus[newLength];
|
||||
|
||||
for (int i = 0; i < nucleiArray.Length; i++)
|
||||
newArray[i] = nucleiArray[i];
|
||||
if (nucleiArray[0] is Nucleus nucleus) {
|
||||
newArray[newLength - 1] = nucleus.Clone(prefab);
|
||||
newArray[newLength - 1].name += $": {newLength - 1}";
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
public static Nucleus[] RemoveReceptorElement(Nucleus[] nucleiArray) {
|
||||
int newLength = nucleiArray.Length - 1;
|
||||
if (newLength == 0) {
|
||||
Debug.LogWarning("Perceptoid array cannot be empty");
|
||||
return null;
|
||||
}
|
||||
Nucleus[] newPerceptei = new Nucleus[newLength];
|
||||
for (int i = 0; i < newLength; i++)
|
||||
newPerceptei[i] = nucleiArray[i];
|
||||
// Delete the last perception
|
||||
if (nucleiArray[newLength] is Nucleus nucleus)
|
||||
Neuron.Delete(nucleus); //this._nuclei[newLength]);
|
||||
|
||||
return newPerceptei;
|
||||
}
|
||||
}
|
||||
2
IReceptor.cs.meta
Normal file
2
IReceptor.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73f052292ad16bb53a3c07aa1694c705
|
||||
@ -4,90 +4,57 @@ using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[Serializable]
|
||||
public class MemoryCell : Neuron, INucleus {
|
||||
public class MemoryCell : Neuron {
|
||||
|
||||
public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { }
|
||||
public MemoryCell(Cluster parent, string name) : base(parent, name) { }
|
||||
// this.parent = parent;
|
||||
// this.name = name;
|
||||
// this.parent?.nuclei.Add(this);
|
||||
// }
|
||||
|
||||
public override IReceptor ShallowCloneTo(Cluster newParent) {
|
||||
MemoryCell clone = new(newParent, this.name) {
|
||||
array = this.array,
|
||||
curve = this.curve,
|
||||
curvePreset = this.curvePreset,
|
||||
curveMax = this.curveMax,
|
||||
average = this.average
|
||||
};
|
||||
public bool staticMemory = false;
|
||||
public override bool isSleeping {
|
||||
get {
|
||||
if (staticMemory)
|
||||
return false;
|
||||
|
||||
return base.isSleeping;
|
||||
}
|
||||
}
|
||||
|
||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
||||
MemoryCell clone = new(newParent, this.name);
|
||||
CloneFields(clone);
|
||||
clone.staticMemory = this.staticMemory;
|
||||
return clone;
|
||||
}
|
||||
|
||||
#region State
|
||||
|
||||
private bool initialized = false;
|
||||
|
||||
private float3 _memorizedValue;
|
||||
private float _memorizedTime;
|
||||
|
||||
public override void UpdateState(float3 bias) {
|
||||
// A memorycell does not have an activation function
|
||||
float3 result = bias;
|
||||
int n = 0;
|
||||
|
||||
//Applying the weight factgors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
result += synapse.weight * synapse.nucleus.outputValue;
|
||||
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||
n++;
|
||||
}
|
||||
|
||||
if (this.average)
|
||||
result /= n;
|
||||
|
||||
UpdateResult(result);
|
||||
}
|
||||
|
||||
public override void UpdateStateIsolated() {
|
||||
float3 bias = new(0, 0, 0);
|
||||
UpdateStateIsolated(bias);
|
||||
}
|
||||
public override void UpdateStateIsolated(float3 bias) {
|
||||
// A memorycell does not have an activation function
|
||||
float3 result = bias;
|
||||
int n = 0;
|
||||
float3 result = Combinator();
|
||||
|
||||
//Applying the weight factgors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
result += synapse.weight * synapse.nucleus.outputValue;
|
||||
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||
n++;
|
||||
if (initialized)
|
||||
// Output the previous, memorized value
|
||||
this.outputValue = this._memorizedValue;
|
||||
else {
|
||||
// The first time, the result is directly set in output
|
||||
this.outputValue = result;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
if (this.average)
|
||||
result /= n;
|
||||
|
||||
this.outputValue = this._memorizedValue;
|
||||
|
||||
// Store the result for the next time
|
||||
this._memorizedValue = result;
|
||||
this._memorizedTime = Time.time;
|
||||
}
|
||||
|
||||
public override void UpdateResult(Vector3 result) {
|
||||
// output value is the previous value
|
||||
// if (this.deltaValue) {
|
||||
// float deltaTime = Time.time - this._memorizedTime;
|
||||
// this._outputValue = this._memorizedValue * deltaTime;
|
||||
// }
|
||||
//else
|
||||
this.outputValue = this._memorizedValue;
|
||||
public override void UpdateNuclei() {
|
||||
if (staticMemory)
|
||||
// Static memory does not get stale or go to sleep
|
||||
return;
|
||||
|
||||
// Store the result for the next time
|
||||
this._memorizedValue = result;
|
||||
this._memorizedTime = Time.time;
|
||||
|
||||
foreach (INucleus receiver in this.receivers)
|
||||
receiver.UpdateState();
|
||||
base.UpdateNuclei();
|
||||
}
|
||||
|
||||
#endregion State
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class NanoBrainComponent : MonoBehaviour {
|
||||
public class NanoBrain : MonoBehaviour {
|
||||
public ClusterPrefab defaultBrain;
|
||||
|
||||
[NonSerialized]
|
||||
@ -13,19 +13,12 @@ public class NanoBrainComponent : MonoBehaviour {
|
||||
name = defaultBrain.name + " (Instance)"
|
||||
};
|
||||
}
|
||||
SwarmControl sc = FindFirstObjectByType<SwarmControl>();
|
||||
if (sc != null) {
|
||||
UpdateWeight(brainInstance, "Containment", sc.containmentForce);
|
||||
UpdateWeight(brainInstance, "Cohesion", sc.cohesionForce);
|
||||
UpdateWeight(brainInstance, "Separation", sc.separationForce);
|
||||
UpdateWeight(brainInstance, "Alignment", sc.alignmentForce);
|
||||
}
|
||||
return brainInstance;
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateWeight(Cluster brain, string name, float weight) {
|
||||
INucleus root = brain.output;
|
||||
Nucleus root = brain.defaultOutput;
|
||||
foreach (Synapse synapse in root.synapses) {
|
||||
if (synapse.nucleus.name == name) {
|
||||
if (synapse.weight != weight) {
|
||||
@ -35,5 +28,4 @@ public class NanoBrainComponent : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
447
Neuron.cs
447
Neuron.cs
@ -7,35 +7,31 @@ using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[Serializable]
|
||||
public class Neuron : INucleus {
|
||||
public class Neuron : Nucleus {
|
||||
|
||||
[SerializeField]
|
||||
protected string _name;
|
||||
public virtual string name {
|
||||
get => _name;
|
||||
set => _name = value;
|
||||
public Neuron(Cluster parent, string name) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
this.parent?.clusterNuclei.Add(this);
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
private List<Synapse> _synapses = new();
|
||||
public List<Synapse> synapses => _synapses;
|
||||
|
||||
[SerializeReference]
|
||||
private List<INucleus> _receivers = new();
|
||||
public List<INucleus> receivers {
|
||||
get { return _receivers; }
|
||||
set { _receivers = value; }
|
||||
}
|
||||
|
||||
[SerializeReference]
|
||||
private NucleusArray _array;
|
||||
public NucleusArray array {
|
||||
get { return _array; }
|
||||
set { _array = value; }
|
||||
public Neuron(ClusterPrefab prefab, string name) {
|
||||
this.clusterPrefab = prefab;
|
||||
this.name = name;
|
||||
if (this.clusterPrefab != null)
|
||||
this.clusterPrefab.nuclei.Add(this);
|
||||
else
|
||||
Debug.LogError("No prefab when adding neuron to prefab");
|
||||
}
|
||||
|
||||
#region Serialization
|
||||
|
||||
public enum CombinatorType {
|
||||
Sum,
|
||||
Product,
|
||||
Max
|
||||
}
|
||||
public CombinatorType combinator = CombinatorType.Sum;
|
||||
|
||||
public enum CurvePresets {
|
||||
Linear,
|
||||
Power,
|
||||
@ -44,7 +40,7 @@ public class Neuron : INucleus {
|
||||
Custom
|
||||
}
|
||||
[SerializeField]
|
||||
private CurvePresets _curvePreset;
|
||||
public CurvePresets _curvePreset;
|
||||
public CurvePresets curvePreset {
|
||||
get { return _curvePreset; }
|
||||
set {
|
||||
@ -55,12 +51,6 @@ public class Neuron : INucleus {
|
||||
public AnimationCurve curve;
|
||||
public float curveMax = 1.0f;
|
||||
|
||||
#region Parameters
|
||||
|
||||
public bool average = false;
|
||||
|
||||
#endregion Parameters
|
||||
|
||||
public AnimationCurve GenerateCurve() {
|
||||
switch (this.curvePreset) {
|
||||
case CurvePresets.Linear:
|
||||
@ -81,17 +71,6 @@ public class Neuron : INucleus {
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Deserialize(Neuron nucleus) { }
|
||||
|
||||
#endregion Serialization
|
||||
|
||||
#region Runtime state (not serialized)
|
||||
|
||||
public ClusterPrefab cluster { get; set; }
|
||||
public Cluster parent { get; set; }
|
||||
|
||||
#region Activation
|
||||
|
||||
public static class Presets {
|
||||
private const int samples = 32;
|
||||
public static AnimationCurve Linear(float weight) {
|
||||
@ -136,127 +115,43 @@ public class Neuron : INucleus {
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Activation
|
||||
|
||||
protected float3 _outputValue;
|
||||
public virtual float3 outputValue {
|
||||
get { return _outputValue; }
|
||||
set {
|
||||
this.stale = 0;
|
||||
// this._isSleeping = false;
|
||||
_outputValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
[NonSerialized]
|
||||
private int stale = 1000;
|
||||
|
||||
// private bool _isSleeping = false;
|
||||
// public bool isSleeping => _isSleeping;
|
||||
public bool isSleeping => lengthsq(this.outputValue) == 0;
|
||||
|
||||
public void UpdateNuclei() {
|
||||
this.stale++;
|
||||
// this._isSleeping = this.stale > 2;
|
||||
// if (isSleeping)
|
||||
if (this.stale > 2)
|
||||
_outputValue = Vector3.zero;
|
||||
}
|
||||
|
||||
#endregion Runtime state
|
||||
|
||||
public Neuron(Cluster parent, string name) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
this.parent?.nuclei.Add(this);
|
||||
}
|
||||
public Neuron(ClusterPrefab parent, string name) {
|
||||
this.cluster = parent;
|
||||
this.name = name;
|
||||
if (this.cluster != null) {
|
||||
this.cluster.nuclei.Add(this);
|
||||
}
|
||||
// else
|
||||
// Debug.LogError("No neuroid network");
|
||||
}
|
||||
#endregion Serialization
|
||||
|
||||
// this clone the nucleus without the synapses and receivers
|
||||
public virtual IReceptor ShallowCloneTo(Cluster newParent) {
|
||||
Neuron clone = new(newParent, this.name) {
|
||||
array = this.array,
|
||||
curve = this.curve,
|
||||
curvePreset = this.curvePreset,
|
||||
curveMax = this.curveMax,
|
||||
average = this.average
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
public virtual IReceptor ShallowCloneTo(ClusterPrefab newParent) {
|
||||
Neuron clone = new(newParent, this.name) {
|
||||
array = this.array,
|
||||
curve = this.curve,
|
||||
curvePreset = this.curvePreset,
|
||||
curveMax = this.curveMax,
|
||||
average = this.average
|
||||
};
|
||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
||||
Neuron clone = new(newParent, this.name);
|
||||
CloneFields(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public virtual IReceptor CloneTo(ClusterPrefab parent) {
|
||||
Neuron clone = new(parent, this.name) {
|
||||
array = this.array,
|
||||
curve = this.curve,
|
||||
curvePreset = this.curvePreset,
|
||||
curveMax = this.curveMax,
|
||||
average = this.average
|
||||
};
|
||||
|
||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
||||
Neuron clone = new(prefab, this.name);
|
||||
CloneFields(clone);
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||
clonedSynapse.weight = synapse.weight;
|
||||
}
|
||||
foreach (INucleus receiver in this.receivers) {
|
||||
clone.AddReceiver(receiver);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
public virtual IReceptor Clone() {
|
||||
Neuron clone = new(this.cluster, this.name) {
|
||||
array = this.array,
|
||||
curve = this.curve,
|
||||
curvePreset = this.curvePreset,
|
||||
curveMax = this.curveMax,
|
||||
average = this.average
|
||||
};
|
||||
// if (clone.cluster != null)
|
||||
// clone.cluster.nuclei.Add(clone);
|
||||
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||
clonedSynapse.weight = synapse.weight;
|
||||
}
|
||||
foreach (INucleus receiver in this.receivers) {
|
||||
foreach (Nucleus receiver in this.receivers) {
|
||||
clone.AddReceiver(receiver);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
|
||||
this._receivers.Add(receivingNucleus);
|
||||
receivingNucleus.AddSynapse(this, weight);
|
||||
protected virtual void CloneFields(Neuron clone) {
|
||||
clone.clusterPrefab = this.clusterPrefab;
|
||||
clone.bias = this.bias;
|
||||
clone.combinator = this.combinator;
|
||||
clone.curve = this.curve;
|
||||
clone.curvePreset = this.curvePreset;
|
||||
clone.curveMax = this.curveMax;
|
||||
}
|
||||
|
||||
public void RemoveReceiver(INucleus receiverNucleus) {
|
||||
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
|
||||
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
||||
}
|
||||
|
||||
public static void Delete(INucleus nucleus) {
|
||||
public static void Delete(Nucleus nucleus) {
|
||||
foreach (Synapse synapse in nucleus.synapses) {
|
||||
if (synapse.nucleus is Neuron synapse_nucleus) {
|
||||
if (synapse_nucleus._receivers.Count > 1) {
|
||||
if (synapse_nucleus.receivers.Count > 1) {
|
||||
// there is another nucleus feeding into this input nucleus
|
||||
synapse_nucleus._receivers.RemoveAll(r => r == nucleus);
|
||||
synapse_nucleus.receivers.RemoveAll(r => r == nucleus);
|
||||
}
|
||||
else {
|
||||
// No other links, delete it.
|
||||
@ -264,124 +159,162 @@ public class Neuron : INucleus {
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
if (receiver != null && receiver.synapses != null)
|
||||
receiver.synapses.RemoveAll(s => s.nucleus == nucleus);
|
||||
}
|
||||
|
||||
if (nucleus.cluster != null) {
|
||||
nucleus.cluster.nuclei.RemoveAll(n => n == nucleus);
|
||||
nucleus.cluster.GarbageCollection();
|
||||
}
|
||||
}
|
||||
|
||||
public Synapse AddSynapse(IReceptor sendingNucleus, float weight = 1.0f) {
|
||||
Synapse synapse = new(sendingNucleus, weight);
|
||||
this.synapses.Add(synapse);
|
||||
return synapse;
|
||||
}
|
||||
|
||||
public virtual void UpdateState() {
|
||||
//UpdateState(new float3(0, 0, 0));
|
||||
this.parent?.UpdateState();
|
||||
}
|
||||
|
||||
public virtual void UpdateState(float3 inputValue) {
|
||||
float3 sum = inputValue;
|
||||
int n = 0;
|
||||
|
||||
//Applying the weight factgors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||
|
||||
// Perhaps synapses should be removed when the output value goes to 0....
|
||||
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||
n++;
|
||||
}
|
||||
if (this.average && n > 0)
|
||||
sum /= n;
|
||||
|
||||
// Activation function
|
||||
Vector3 result;
|
||||
switch (this.curvePreset) {
|
||||
case CurvePresets.Linear:
|
||||
result = sum;
|
||||
break;
|
||||
case CurvePresets.Sqrt:
|
||||
result = normalize(sum) * System.MathF.Sqrt(length(sum));
|
||||
break;
|
||||
case CurvePresets.Power:
|
||||
result = normalize(sum) * System.MathF.Pow(length(sum), 2);
|
||||
break;
|
||||
case CurvePresets.Reciprocal:
|
||||
result = normalize(sum) * (1 / length(sum));
|
||||
break;
|
||||
default:
|
||||
float activatedValue = this.curve.Evaluate(length(sum));
|
||||
result = normalize(sum) * activatedValue;
|
||||
break;
|
||||
}
|
||||
UpdateResult(result);
|
||||
}
|
||||
|
||||
public virtual void UpdateStateIsolated() {
|
||||
UpdateStateIsolated(new float3(0, 0, 0));
|
||||
}
|
||||
public virtual void UpdateStateIsolated(float3 bias) {
|
||||
float3 sum = bias;
|
||||
int n = 0;
|
||||
|
||||
//Applying the weight factgors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||
|
||||
// Perhaps synapses should be removed when the output value goes to 0....
|
||||
if (lengthsq(synapse.nucleus.outputValue) != 0)
|
||||
n++;
|
||||
}
|
||||
if (this.average && n > 0)
|
||||
sum /= n;
|
||||
|
||||
// Activation function
|
||||
float3 result = Vector3.zero;
|
||||
switch (this.curvePreset) {
|
||||
case CurvePresets.Linear:
|
||||
result = sum;
|
||||
break;
|
||||
case CurvePresets.Sqrt:
|
||||
result = normalize(sum) * System.MathF.Sqrt(length(sum));
|
||||
break;
|
||||
case CurvePresets.Power:
|
||||
result = normalize(sum) * System.MathF.Pow(length(sum), 2);
|
||||
break;
|
||||
case CurvePresets.Reciprocal: {
|
||||
float magnitude = length(sum);
|
||||
if (magnitude > 0)
|
||||
result = normalize(sum) * (1 / magnitude);
|
||||
break;
|
||||
if (nucleus is Neuron neuron) {
|
||||
foreach (Nucleus receiver in neuron.receivers) {
|
||||
if (receiver != null && receiver.synapses != null)
|
||||
receiver.synapses.RemoveAll(s => s.nucleus == nucleus);
|
||||
}
|
||||
} else if (nucleus is Cluster cluster) {
|
||||
// remove all receivers for this cluster
|
||||
foreach (Neuron output in cluster.outputs) {
|
||||
foreach (Nucleus receiver in output.receivers) {
|
||||
receiver.synapses.RemoveAll(s => s.nucleus == output);
|
||||
}
|
||||
default:
|
||||
float activatedValue = this.curve.Evaluate(length(sum));
|
||||
result = normalize(sum) * activatedValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (nucleus.clusterPrefab != null) {
|
||||
nucleus.clusterPrefab.nuclei.RemoveAll(n => n == nucleus);
|
||||
nucleus.clusterPrefab.RefreshOutputs();
|
||||
nucleus.clusterPrefab.GarbageCollection();
|
||||
}
|
||||
this.outputValue = result;
|
||||
}
|
||||
|
||||
public virtual void UpdateResult(Vector3 result) {
|
||||
// float d = Vector3.Distance(result, this.outputValue);
|
||||
// if (d < 0.5f) {
|
||||
// //Debug.Log($"insignificant update: {d}");
|
||||
// return;
|
||||
public override void UpdateStateIsolated() {
|
||||
float3 result = Combinator();
|
||||
this.outputValue = Activator(result);
|
||||
}
|
||||
|
||||
#region Combinator
|
||||
|
||||
protected Func<float3> Combinator => combinator switch {
|
||||
CombinatorType.Sum => CombinatorSum,
|
||||
CombinatorType.Product => CombinatorProduct,
|
||||
CombinatorType.Max => CombinatorMax,
|
||||
_ => CombinatorSum
|
||||
};
|
||||
|
||||
public float3 CombinatorSum() {
|
||||
float3 sum = this.bias;
|
||||
foreach (Synapse synapse in this.synapses)
|
||||
sum += synapse.weight * synapse.nucleus.outputValue;
|
||||
return sum;
|
||||
}
|
||||
|
||||
public float3 CombinatorProduct() {
|
||||
float3 product = this.bias;
|
||||
foreach (Synapse synapse in this.synapses)
|
||||
product *= synapse.weight * synapse.nucleus.outputValue;
|
||||
return product;
|
||||
}
|
||||
|
||||
public float3 CombinatorMax() {
|
||||
float3 max = this.bias;
|
||||
float maxSqrLength = lengthsq(max);
|
||||
|
||||
//Applying the weight factors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
float3 input = synapse.weight * synapse.nucleus.outputValue;
|
||||
|
||||
float inputSqrlength = lengthsq(input);
|
||||
if (inputSqrlength > maxSqrLength) {
|
||||
max = input;
|
||||
maxSqrLength = inputSqrlength;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
#endregion Combinator
|
||||
|
||||
#region Activator
|
||||
|
||||
protected Func<float3, float3> Activator => this.curvePreset switch {
|
||||
CurvePresets.Linear => ActivatorLinear,
|
||||
CurvePresets.Sqrt => ActivatorSqrt,
|
||||
CurvePresets.Power => ActivatorPower,
|
||||
CurvePresets.Reciprocal => ActivatorReciprocal,
|
||||
_ => ActivatorCustom
|
||||
};
|
||||
|
||||
protected float3 ActivatorLinear(float3 input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
protected float3 ActivatorSqrt(float3 input) {
|
||||
float3 result = normalize(input) * System.MathF.Sqrt(length(input));
|
||||
return result;
|
||||
}
|
||||
|
||||
protected float3 ActivatorPower(float3 input) {
|
||||
float3 result = normalize(input) * System.MathF.Pow(length(input), 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected float3 ActivatorReciprocal(float3 input) {
|
||||
float magnitude = length(input);
|
||||
if (magnitude == 0)
|
||||
return new float3(0, 0, 0);
|
||||
|
||||
float3 result = normalize(input) * (1 / magnitude);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected float3 ActivatorCustom(float3 input) {
|
||||
float activatedValue = this.curve.Evaluate(length(input));
|
||||
float3 result = normalize(input) * activatedValue;
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion Activator
|
||||
|
||||
#region Receivers
|
||||
|
||||
[SerializeReference]
|
||||
private List<Nucleus> _receivers = new();
|
||||
public virtual List<Nucleus> receivers {
|
||||
get { return _receivers; }
|
||||
set { _receivers = value; }
|
||||
}
|
||||
|
||||
public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) {
|
||||
// if (this is IReceptor receptor) {
|
||||
// foreach (Nucleus element in receptor.array.nuclei) {
|
||||
// if (element is Neuron neuron) {
|
||||
// neuron._receivers.Add(receiverToAdd);
|
||||
// receiverToAdd.AddSynapse(element, weight);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
this._receivers.Add(receiverToAdd);
|
||||
receiverToAdd.AddSynapse(this, weight);
|
||||
// }
|
||||
|
||||
this.outputValue = result;
|
||||
if (lengthsq(outputValue) != 0) {
|
||||
Debug.Log($"{this.parent.name}.{this.name}: {this.outputValue}");
|
||||
}
|
||||
|
||||
foreach (INucleus receiver in this.receivers)
|
||||
receiver.UpdateState();
|
||||
|
||||
}
|
||||
|
||||
public virtual void RemoveReceiver(Nucleus receiverToRemove) {
|
||||
if (this is IReceptor receptor) {
|
||||
foreach (Nucleus element in receptor.nucleiArray) {
|
||||
if (element is Neuron neuron) {
|
||||
neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove);
|
||||
receiverToRemove.synapses.RemoveAll(synapse => synapse.nucleus == neuron);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._receivers.RemoveAll(receiver => receiver == receiverToRemove);
|
||||
receiverToRemove.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion Receivers
|
||||
|
||||
public virtual void ProcessStimulus(Vector3 inputValue, string thingName = null) {
|
||||
this.stale = 0;
|
||||
this.bias = inputValue;
|
||||
this.parent.UpdateFromNucleus(this);
|
||||
}
|
||||
|
||||
}
|
||||
93
Nucleus.cs
Normal file
93
Nucleus.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[Serializable]
|
||||
public abstract class Nucleus {
|
||||
public string name;
|
||||
|
||||
[SerializeReference]
|
||||
public ClusterPrefab clusterPrefab;
|
||||
[SerializeReference]
|
||||
public Cluster parent;
|
||||
|
||||
protected float3 _outputValue;
|
||||
public virtual float3 outputValue {
|
||||
get { return _outputValue; }
|
||||
set {
|
||||
_outputValue = value;
|
||||
if (this.isFiring)
|
||||
WhenFiring?.Invoke();
|
||||
}
|
||||
}
|
||||
public bool isFiring => length(_outputValue) > 0.5f;
|
||||
public Action WhenFiring;
|
||||
|
||||
public virtual bool isSleeping => lengthsq(this.outputValue) == 0;
|
||||
[NonSerialized]
|
||||
public int stale = 1000;
|
||||
public readonly int staleValueForSleep = 20;
|
||||
public bool trace = false;
|
||||
|
||||
public abstract Nucleus ShallowCloneTo(Cluster parent);
|
||||
public abstract Nucleus Clone(ClusterPrefab prefab);
|
||||
|
||||
public enum Type {
|
||||
None,
|
||||
Neuron,
|
||||
MemoryCell,
|
||||
Selector,
|
||||
Cluster,
|
||||
Pulsar,
|
||||
Receptor,
|
||||
ReceptorArray,
|
||||
ClusterReceptor,
|
||||
}
|
||||
|
||||
#region Synapses
|
||||
|
||||
public Vector3 bias = Vector3.zero;
|
||||
|
||||
[SerializeField]
|
||||
private List<Synapse> _synapses = new();
|
||||
public List<Synapse> synapses => _synapses;
|
||||
|
||||
public Synapse AddSynapse(Nucleus sendingNucleus, float weight = 1.0f) {
|
||||
Synapse synapse = new(sendingNucleus, weight);
|
||||
this.synapses.Add(synapse);
|
||||
return synapse;
|
||||
}
|
||||
|
||||
public Synapse GetSynapse(Nucleus sender) {
|
||||
foreach (Synapse synapse in this.synapses)
|
||||
if (synapse.nucleus == sender)
|
||||
return synapse;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void RemoveSynapse(Nucleus sendingNucleus) {
|
||||
this.synapses.RemoveAll(synapse => synapse.nucleus == sendingNucleus);
|
||||
}
|
||||
|
||||
#endregion Synapses
|
||||
|
||||
|
||||
|
||||
#region Update
|
||||
|
||||
public abstract void UpdateStateIsolated();
|
||||
|
||||
public virtual void UpdateNuclei() {
|
||||
}
|
||||
|
||||
public virtual void SetBias(Vector3 inputValue) {
|
||||
this.stale = 0;
|
||||
this.bias = inputValue;
|
||||
this.parent.UpdateFromNucleus(this);
|
||||
}
|
||||
|
||||
#endregion Update
|
||||
|
||||
}
|
||||
2
Nucleus.cs.meta
Normal file
2
Nucleus.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4310eea6ab77628b085387a226c1c386
|
||||
133
NucleusArray.cs
133
NucleusArray.cs
@ -1,49 +1,48 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[System.Serializable]
|
||||
public class NucleusArray {
|
||||
[SerializeReference]
|
||||
private INucleus[] _nuclei;
|
||||
private ClusterPrefab[] _clusters;
|
||||
public IEnumerable<INucleus> nuclei {
|
||||
private Nucleus[] _nuclei;
|
||||
public Nucleus[] nuclei {
|
||||
get {
|
||||
// if (_nuclei == null)
|
||||
// return _clusters;
|
||||
// else if (_clusters == null)
|
||||
return _nuclei;
|
||||
// else
|
||||
// return _nuclei.Concat(_clusters);
|
||||
return _nuclei;
|
||||
}
|
||||
set {
|
||||
_nuclei = value;
|
||||
}
|
||||
}
|
||||
public string name;
|
||||
|
||||
public NucleusArray(INucleus nucleus) {
|
||||
this.name = nucleus.name;
|
||||
this._nuclei = new INucleus[1];
|
||||
public NucleusArray(Nucleus nucleus) {
|
||||
this._nuclei = new Nucleus[1];
|
||||
this._nuclei[0] = nucleus;
|
||||
this._clusters = new ClusterPrefab[0];
|
||||
}
|
||||
public NucleusArray(ClusterPrefab cluster) {
|
||||
this.name = cluster.name;
|
||||
this._nuclei = new INucleus[0];
|
||||
this._clusters = new ClusterPrefab[1];
|
||||
this._clusters[0] = cluster;
|
||||
this._nuclei = new Nucleus[0];
|
||||
}
|
||||
public NucleusArray(int size, string name) {
|
||||
this._nuclei = new Nucleus[size];
|
||||
}
|
||||
|
||||
public void AddNucleus() {
|
||||
|
||||
public void AddNucleus(ClusterPrefab prefab) {
|
||||
if (this._nuclei.Length == 0) {
|
||||
Debug.LogError("Empty perceptoid array, cannot add");
|
||||
return;
|
||||
}
|
||||
int newLength = this._nuclei.Length + 1;
|
||||
INucleus[] newArray = new INucleus[newLength];
|
||||
Nucleus[] newArray = new Nucleus[newLength];
|
||||
|
||||
for (int i = 0; i < this._nuclei.Length; i++)
|
||||
newArray[i] = this._nuclei[i];
|
||||
if (this._nuclei[0] is INucleus nucleus)
|
||||
newArray[newLength - 1] = (INucleus) nucleus.Clone();
|
||||
if (this._nuclei[0] is Nucleus nucleus) {
|
||||
newArray[newLength - 1] = nucleus.Clone(prefab);
|
||||
newArray[newLength - 1].name += $": {newLength - 1}";
|
||||
}
|
||||
|
||||
this._nuclei = newArray;
|
||||
}
|
||||
@ -54,14 +53,100 @@ public class NucleusArray {
|
||||
Debug.LogWarning("Perceptoid array cannot be empty");
|
||||
return;
|
||||
}
|
||||
INucleus[] newPerceptei = new INucleus[newLength];
|
||||
Nucleus[] newPerceptei = new Nucleus[newLength];
|
||||
for (int i = 0; i < newLength; i++)
|
||||
newPerceptei[i] = this._nuclei[i];
|
||||
// Delete the last perception
|
||||
Neuron.Delete(this._nuclei[newLength]);
|
||||
if (this._nuclei[newLength] is Nucleus nucleus)
|
||||
Neuron.Delete(nucleus); //this._nuclei[newLength]);
|
||||
|
||||
this._nuclei = newPerceptei;
|
||||
}
|
||||
|
||||
public Dictionary<int, Nucleus> thingReceivers = new();
|
||||
|
||||
|
||||
private Nucleus FindReceiver(int thingId, float3 inputValue) {
|
||||
// No existing nucleus for this thing
|
||||
float inputMagnitude = length(inputValue);
|
||||
Nucleus selectedReceiver = null;
|
||||
float selectedMagnitude = 0;
|
||||
foreach (Nucleus receiver in this._nuclei) {
|
||||
if (thingReceivers.ContainsValue(receiver) == false) {
|
||||
// We found an unusued receiver
|
||||
thingReceivers.Add(thingId, receiver);
|
||||
return receiver;
|
||||
}
|
||||
else if (receiver.isSleeping) {
|
||||
// A sleeping receiver is not active and can therefore always be used
|
||||
thingReceivers.Add(thingId, receiver);
|
||||
return receiver;
|
||||
}
|
||||
else if (selectedReceiver == null) {
|
||||
// If we haven't found a receiver yet, just start by taking the first
|
||||
selectedReceiver = receiver;
|
||||
selectedMagnitude = length(selectedReceiver.outputValue);
|
||||
}
|
||||
// Look for the receiver with the lowest magnitude
|
||||
else {
|
||||
float magnitude = length(receiver.outputValue);
|
||||
|
||||
if (magnitude < inputMagnitude && length(receiver.outputValue) < selectedMagnitude) {
|
||||
selectedReceiver = receiver;
|
||||
selectedMagnitude = length(selectedReceiver.outputValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedReceiver != null) {
|
||||
// Replace the receiver
|
||||
// Find the thingId current associated with the receiver
|
||||
int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key;
|
||||
if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove))
|
||||
thingReceivers.Remove(keyToRemove);
|
||||
// And add the new association
|
||||
thingReceivers.Add(thingId, selectedReceiver);
|
||||
}
|
||||
return selectedReceiver;
|
||||
}
|
||||
|
||||
public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) {
|
||||
CleanupReceivers();
|
||||
if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) {
|
||||
// No existing nucleus for this thing
|
||||
selectedReceiver = FindReceiver(thingId, inputValue);
|
||||
}
|
||||
if (selectedReceiver == null)
|
||||
return;
|
||||
|
||||
if (thingName != null) {
|
||||
string baseName = selectedReceiver.name;
|
||||
int colonPos = selectedReceiver.name.IndexOf(":");
|
||||
if (colonPos > 0)
|
||||
baseName = selectedReceiver.name[..colonPos];
|
||||
selectedReceiver.name = baseName + ": " + thingName;
|
||||
}
|
||||
|
||||
if (selectedReceiver is Neuron selectedNucleus)
|
||||
selectedNucleus.ProcessStimulus(inputValue);
|
||||
//selectedReceiver.parent.UpdateFromNucleus(selectedReceiver);
|
||||
}
|
||||
|
||||
private void CleanupReceivers() {
|
||||
// Remove a thing-receiver connection when the nucleus is inactive
|
||||
List<int> receiversToRemove = new();
|
||||
foreach (KeyValuePair<int, Nucleus> item in thingReceivers) {
|
||||
if (item.Value != null && item.Value.isSleeping)
|
||||
receiversToRemove.Add(item.Key);
|
||||
}
|
||||
foreach (int thingId in receiversToRemove) {
|
||||
Nucleus selectedReceiver = thingReceivers[thingId];
|
||||
|
||||
thingReceivers.Remove(thingId);
|
||||
|
||||
int colonPos = selectedReceiver.name.IndexOf(":");
|
||||
if (colonPos > 0)
|
||||
selectedReceiver.name = selectedReceiver.name[..colonPos];
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Pulsar.cs
Normal file
54
Pulsar.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Unity.Mathematics;
|
||||
|
||||
/// <summary>
|
||||
/// The Pulsar represents a type of neuron that operates based on
|
||||
/// the product of its weighted inputs rather than the traditional summation.
|
||||
/// Drawing inspiration from the concept of pulsars in astrophysics
|
||||
/// —highly magnetized rotating neutron stars that emit beams of radiation—
|
||||
/// the Pulsar could symbolize dynamic, focused output based on the interaction of multiple factors.
|
||||
/// </summary>
|
||||
/// Multiplicative Functionality:
|
||||
/// Instead of summing inputs, the Pulsar takes the weighted product of its inputs.
|
||||
/// This means that all inputs must be active (non-zero) for the neuron to "pulse" or activate.
|
||||
/// Output Behavior:
|
||||
/// The output could amplify or diminish depending on the magnitude of the inputs.
|
||||
/// The product would be sensitive to small values,
|
||||
/// which means that even a small input could significantly lower the overall output if multiplied.
|
||||
/// Activation Mechanism:
|
||||
/// The activation function can further refine the output from the product result.
|
||||
/// For instance, a certain threshold could be used to determine if a pulse occurs.
|
||||
/// Modeling Complex Interactions:
|
||||
/// The Pulsar could be particularly beneficial for modeling situations where interactions multiply rather than add.
|
||||
/// This is useful in fields such as economics (e.g., compound growth models),
|
||||
/// biology (e.g., interaction of hormones), and machine learning where multiplicative relationships exist.
|
||||
[Serializable]
|
||||
public class Pulsar : Neuron {
|
||||
public Pulsar(Cluster parent, string name) : base(parent, name) {
|
||||
// To prevent mistakes, bias one (instead of zero for standard neurons)
|
||||
this.bias = new float3(1, 1, 1);
|
||||
}
|
||||
public Pulsar(ClusterPrefab parent, string name) : base(parent, name) {
|
||||
// To prevent mistakes, bias one (instead of zero for standard neurons)
|
||||
this.bias = new float3(1, 1, 1);
|
||||
}
|
||||
|
||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
||||
Pulsar clone = new(newParent, this.name);
|
||||
CloneFields(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override void UpdateStateIsolated() {
|
||||
float3 product = this.bias;
|
||||
|
||||
//Applying the weight factors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
float3 input = synapse.weight * synapse.nucleus.outputValue;
|
||||
product *= input;
|
||||
}
|
||||
|
||||
// Activation function
|
||||
this.outputValue = Activator(product);
|
||||
}
|
||||
}
|
||||
2
Pulsar.cs.meta
Normal file
2
Pulsar.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46bd155173053a01585411c3e07f85d4
|
||||
208
Receptor.cs
208
Receptor.cs
@ -1,191 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
public class Receptor : IReceptor {
|
||||
|
||||
private ClusterPrefab cluster;
|
||||
private Cluster parent;
|
||||
|
||||
[SerializeField]
|
||||
protected string _name;
|
||||
public virtual string name {
|
||||
get => _name;
|
||||
set => _name = value;
|
||||
[System.Serializable]
|
||||
public class Receptor : Neuron, IReceptor {
|
||||
public Receptor(Cluster parent, string name) : base(parent, name) {
|
||||
this.array = new NucleusArray(this);
|
||||
if (this.name.IndexOf(":") < 0)
|
||||
this.name += ": 0";
|
||||
}
|
||||
public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) {
|
||||
this.array = new NucleusArray(this);
|
||||
}
|
||||
|
||||
public Receptor(Cluster parent) {
|
||||
this.parent = parent;
|
||||
if (parent != null)
|
||||
parent.nuclei.Add(this);
|
||||
}
|
||||
public Receptor(ClusterPrefab cluster) {
|
||||
this.cluster = cluster;
|
||||
if (cluster != null)
|
||||
cluster.nuclei.Add(this);
|
||||
public string GetName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public Receptor(ClusterPrefab cluster, INucleus nucleus) {
|
||||
this.cluster = cluster;
|
||||
if (cluster != null)
|
||||
cluster.nuclei.Add(this);
|
||||
this.AddReceiver(nucleus);
|
||||
}
|
||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
||||
Receptor clone = new(parent, name) {
|
||||
|
||||
public static Receptor CreateReceptor(Cluster cluster, string nucleusName) {
|
||||
if (cluster == null)
|
||||
return null;
|
||||
|
||||
Receptor receptor = new(cluster);
|
||||
foreach (INucleus nucleus in cluster.inputs) {
|
||||
if (nucleus != null && nucleus.name == nucleusName) {
|
||||
receptor.AddReceiver(nucleus);
|
||||
}
|
||||
}
|
||||
if (receptor._receivers.Count == 0)
|
||||
return null;
|
||||
else
|
||||
return receptor;
|
||||
}
|
||||
|
||||
public virtual IReceptor ShallowCloneTo(Cluster parent) {
|
||||
Receptor clone = new(parent);
|
||||
};
|
||||
CloneFields(clone);
|
||||
return clone;
|
||||
}
|
||||
public virtual IReceptor ShallowCloneTo(ClusterPrefab parent) {
|
||||
Receptor clone = new(parent);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public virtual IReceptor CloneTo(ClusterPrefab parent) {
|
||||
Receptor clone = new(parent);
|
||||
|
||||
foreach (INucleus receiver in this.receivers) {
|
||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
||||
Receptor clone = new(prefab, name) {
|
||||
array = this._array
|
||||
};
|
||||
CloneFields(clone);
|
||||
// Adding receivers will also add synapses to the receivers
|
||||
foreach (Nucleus receiver in this.receivers.ToArray())
|
||||
clone.AddReceiver(receiver);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
public virtual IReceptor Clone() {
|
||||
Receptor clone = new(this.cluster);
|
||||
|
||||
foreach (INucleus receiver in this.receivers) {
|
||||
clone.AddReceiver(receiver);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
class Receiver {
|
||||
public INucleus nucleus;
|
||||
public int thingId;
|
||||
public string thingName;
|
||||
public Receiver(INucleus nucleus, int thingId, string thingName) {
|
||||
this.nucleus = nucleus;
|
||||
this.thingId = thingId;
|
||||
this.thingName = thingName;
|
||||
}
|
||||
}
|
||||
|
||||
[SerializeReference]
|
||||
private List<INucleus> _receivers = new();
|
||||
public List<INucleus> receivers {
|
||||
get { return _receivers; }
|
||||
set { _receivers = value; }
|
||||
private NucleusArray _array;
|
||||
public NucleusArray array {
|
||||
set { _array = value; }
|
||||
}
|
||||
|
||||
protected int[] thingIds; // every receiver can handle a thing with this id
|
||||
|
||||
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
|
||||
this._receivers.Add(receivingNucleus);
|
||||
receivingNucleus.AddSynapse(this, weight);
|
||||
public Nucleus[] nucleiArray {
|
||||
get { return _array.nuclei; }
|
||||
set { _array.nuclei = value; }
|
||||
}
|
||||
|
||||
public void RemoveReceiver(INucleus receiverNucleus) {
|
||||
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
|
||||
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
|
||||
public void AddReceptorElement(ClusterPrefab prefab) {
|
||||
this.nucleiArray = IReceptorHelpers.AddReceptorElement(this.nucleiArray, prefab);
|
||||
}
|
||||
|
||||
private int stale = 1000;
|
||||
|
||||
private bool _isSleeping = false;
|
||||
public bool isSleeping => _isSleeping;
|
||||
|
||||
public Vector3 localPosition {
|
||||
set {
|
||||
this.stale = 0;
|
||||
this._isSleeping = false;
|
||||
this._outputValue = value;
|
||||
|
||||
}
|
||||
public void RemoveReceptorElement() {
|
||||
this.nucleiArray = IReceptorHelpers.RemoveReceptorElement(this.nucleiArray);
|
||||
}
|
||||
public float distanceResolution = 0.1f;
|
||||
public float directionResolution = 5;
|
||||
|
||||
private float3 _outputValue;
|
||||
public float3 outputValue {
|
||||
get { return this._outputValue; }
|
||||
set {
|
||||
this.stale = 0;
|
||||
this._isSleeping = false;
|
||||
this._outputValue = value;
|
||||
public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) {
|
||||
foreach (Nucleus element in this._array.nuclei) {
|
||||
if (element is Neuron neuron) {
|
||||
neuron.AddReceiver(receiverToAdd, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ProcessStimulus(int thingId, Vector3 newLocalPositionVector, string thingName = null) {
|
||||
this.localPosition = newLocalPositionVector;
|
||||
if (this._receivers == null)
|
||||
return;
|
||||
|
||||
thingIds ??= new int[this._receivers.Count];
|
||||
|
||||
int receiverIx = 0;
|
||||
INucleus selectedReceiver = null;
|
||||
int selectedReceiverIx = 0;
|
||||
foreach (INucleus receiver in this.receivers) {
|
||||
if (thingIds[receiverIx] == thingId) {
|
||||
// We found an existing receiver for this thing
|
||||
selectedReceiver = receiver;
|
||||
selectedReceiverIx = receiverIx;
|
||||
// Do not look any further
|
||||
break;
|
||||
}
|
||||
else if (receiver.isSleeping) {
|
||||
// A sleeping receiver is not active and can therefore always be used
|
||||
selectedReceiver = receiver;
|
||||
selectedReceiverIx = receiverIx;
|
||||
// Look further because we may find an existing receiver for this thing
|
||||
}
|
||||
else if (selectedReceiver == null) {
|
||||
// If we haven't found a receiver yet, just start by taking the first
|
||||
selectedReceiver = receiver;
|
||||
selectedReceiverIx = receiverIx;
|
||||
}
|
||||
else if (selectedReceiver.isSleeping == false) {
|
||||
// If no existing or sleeping receiver is found, we look for
|
||||
// the receiver with the furthest/least interesting stimulus
|
||||
if (length(receiver.outputValue) < length(selectedReceiver.outputValue)) {
|
||||
// Debug.Log($"{selectedReceiver.name}[{selectedReceiverIx}] {length(selectedReceiver.outputValue)}" +
|
||||
// $" {receiver.name}[{receiverIx}] {length(receiver.outputValue)} ");
|
||||
selectedReceiver = receiver;
|
||||
selectedReceiverIx = receiverIx;
|
||||
}
|
||||
}
|
||||
receiverIx++;
|
||||
}
|
||||
// Debug.Log($"Receiver {selectedReceiver.name}[{selectedReceiverIx}] for thing {thingId}");
|
||||
thingIds[selectedReceiverIx] = thingId;
|
||||
// if (thingName != null)
|
||||
// selectedReceiver.nucleus.name = selectedReceiver.nucleus.baseName + " " + thingName;
|
||||
selectedReceiver.UpdateState();
|
||||
public override void UpdateStateIsolated() {
|
||||
this.outputValue = this.bias;
|
||||
//Debug.Log($"Receptor {this.name} outputvalue = {this.outputValue}");
|
||||
}
|
||||
|
||||
public void UpdateNuclei() {
|
||||
public override void UpdateNuclei() {
|
||||
this.stale++;
|
||||
this._isSleeping = this.stale > 2;
|
||||
if (isSleeping)
|
||||
this._outputValue = Vector3.zero;
|
||||
if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) {
|
||||
this.bias = new float3(0, 0, 0);
|
||||
this.parent.UpdateFromNucleus(this);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
|
||||
this._array ??= new NucleusArray(this.parent);
|
||||
this._array.ProcessStimulus(thingId, inputValue, thingName);
|
||||
}
|
||||
}
|
||||
261
ReceptorArray.cs
Normal file
261
ReceptorArray.cs
Normal file
@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[Serializable]
|
||||
public class ReceptorInstance : Nucleus {
|
||||
public ReceptorInstance(Cluster parent, string name) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
// We explicitly do not add this to the parent, as it is serialized in the ReceptorArray
|
||||
}
|
||||
public ReceptorInstance(ClusterPrefab prefab, string name) {
|
||||
this.clusterPrefab = prefab;
|
||||
this.name = name;
|
||||
// We explicitly do not add this to the prefab, as it is serialized in the ReceptorArray
|
||||
}
|
||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
||||
ReceptorInstance clone = new(parent, name + " +1") {
|
||||
receptor = this.receptor
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
||||
ReceptorInstance clone = new(prefab, name) {
|
||||
receptor = this.receptor
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
|
||||
[SerializeReference]
|
||||
public ReceptorArray receptor;
|
||||
|
||||
public override void UpdateStateIsolated() {
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ReceptorArray : Nucleus {
|
||||
public ReceptorArray(Cluster parent, string name) {
|
||||
this.parent = parent;
|
||||
this.name = name;
|
||||
this._instances = new ReceptorInstance[1];
|
||||
this._instances[0] = new ReceptorInstance(parent, this.name + "[0]") {
|
||||
receptor = this
|
||||
};
|
||||
this.parent?.clusterNuclei.Add(this);
|
||||
}
|
||||
public ReceptorArray(ClusterPrefab prefab, string name) {
|
||||
this.clusterPrefab = prefab;
|
||||
this.name = name;
|
||||
this._instances = new ReceptorInstance[1];
|
||||
this._instances[0] = new ReceptorInstance(prefab, this.name + "[0]") {
|
||||
receptor = this
|
||||
};
|
||||
if (this.clusterPrefab != null)
|
||||
this.clusterPrefab.nuclei.Add(this);
|
||||
else
|
||||
Debug.LogError("No prefab when adding receptor to prefab");
|
||||
|
||||
}
|
||||
|
||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
||||
ReceptorArray clone = new(parent, name) {
|
||||
_instances = new ReceptorInstance[this.instances.Length]
|
||||
};
|
||||
for (int ix = 0; ix < this.instances.Length; ix++) {
|
||||
clone._instances[ix] = new ReceptorInstance(parent, $"{this.name} [{ix}]") {
|
||||
receptor = clone
|
||||
};
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override Nucleus Clone(ClusterPrefab prefab) {
|
||||
ReceptorArray clone = new(prefab, this.name) {
|
||||
_instances = new ReceptorInstance[this.instances.Length]
|
||||
};
|
||||
for (int ix = 0; ix < this.instances.Length; ix++) {
|
||||
clone._instances[ix] = new ReceptorInstance(prefab, this.name) {
|
||||
receptor = this
|
||||
};
|
||||
}
|
||||
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
|
||||
clonedSynapse.weight = synapse.weight;
|
||||
}
|
||||
// foreach (Nucleus receiver in this.receivers) {
|
||||
// clone.AddReceiver(receiver);
|
||||
// }
|
||||
return clone;
|
||||
}
|
||||
|
||||
[SerializeReference]
|
||||
private ReceptorInstance[] _instances = new ReceptorInstance[0];
|
||||
public ReceptorInstance[] instances {
|
||||
get {
|
||||
return _instances;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddReceptor(ClusterPrefab prefab) {
|
||||
if (this._instances.Length == 0) {
|
||||
Debug.LogError("Empty receptor array, cannot add");
|
||||
return;
|
||||
}
|
||||
int newLength = this._instances.Length + 1;
|
||||
ReceptorInstance[] newArray = new ReceptorInstance[newLength];
|
||||
|
||||
for (int i = 0; i < this._instances.Length; i++)
|
||||
newArray[i] = this._instances[i];
|
||||
ReceptorInstance newReceptor = (ReceptorInstance)this._instances[0].Clone(prefab);
|
||||
newReceptor.name = $"{this.name} [{this._instances.Length}]";
|
||||
newArray[newLength - 1] = newReceptor;
|
||||
|
||||
this._instances = newArray;
|
||||
}
|
||||
|
||||
public void RemoveReceptor() {
|
||||
int newLength = this._instances.Length - 1;
|
||||
if (newLength == 0) {
|
||||
Debug.LogWarning("Receptor array cannot be empty");
|
||||
return;
|
||||
}
|
||||
ReceptorInstance[] newPerceptei = new ReceptorInstance[newLength];
|
||||
for (int i = 0; i < newLength; i++)
|
||||
newPerceptei[i] = this._instances[i];
|
||||
// Delete the last perception
|
||||
if (this._instances[newLength] is Nucleus nucleus)
|
||||
Neuron.Delete(nucleus); //this._nuclei[newLength]);
|
||||
|
||||
this._instances = newPerceptei;
|
||||
}
|
||||
|
||||
private Dictionary<int, Nucleus> thingReceivers = new();
|
||||
|
||||
// public override void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) {
|
||||
// ProcessStimulus(inputValue, thingId, thingName);
|
||||
// }
|
||||
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
|
||||
CleanupReceivers();
|
||||
if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) {
|
||||
//Debug.Log($" no receiver found for {thingId}");
|
||||
// No existing nucleus for this thing
|
||||
selectedReceiver = SelectReceptor(thingId, inputValue);
|
||||
}
|
||||
if (selectedReceiver == null)
|
||||
return;
|
||||
|
||||
if (thingName != null) {
|
||||
string baseName = selectedReceiver.name;
|
||||
int colonPos = selectedReceiver.name.IndexOf(":");
|
||||
if (colonPos > 0)
|
||||
baseName = selectedReceiver.name[..colonPos];
|
||||
selectedReceiver.name = baseName + ": " + thingName;
|
||||
}
|
||||
|
||||
//if (selectedReceiver is Neuron selectedNucleus) {
|
||||
selectedReceiver.stale = 0;
|
||||
selectedReceiver.outputValue = inputValue;
|
||||
this.parent.UpdateFromNucleus(this);
|
||||
//selectedNucleus.ProcessStimulus(inputValue);
|
||||
//}
|
||||
}
|
||||
|
||||
private void CleanupReceivers() {
|
||||
// Remove a thing-receiver connection when the nucleus is inactive
|
||||
List<int> receiversToRemove = new();
|
||||
thingReceivers ??= new();
|
||||
|
||||
foreach (KeyValuePair<int, Nucleus> item in thingReceivers) {
|
||||
if (item.Value.isSleeping)
|
||||
receiversToRemove.Add(item.Key);
|
||||
}
|
||||
foreach (int thingId in receiversToRemove) {
|
||||
Nucleus selectedReceiver = thingReceivers[thingId];
|
||||
|
||||
// Debug.Log($" removed receiver for {thingId}");
|
||||
thingReceivers.Remove(thingId);
|
||||
|
||||
int colonPos = selectedReceiver.name.IndexOf(":");
|
||||
if (colonPos > 0)
|
||||
selectedReceiver.name = selectedReceiver.name[..colonPos];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Nucleus SelectReceptor(int thingId, float3 inputValue) {
|
||||
// No existing nucleus for this thing
|
||||
float inputMagnitude = length(inputValue);
|
||||
Nucleus selectedReceiver = null;
|
||||
float selectedMagnitude = 0;
|
||||
this._instances ??= new ReceptorInstance[0];
|
||||
foreach (Nucleus receiver in this._instances) {
|
||||
if (thingReceivers.ContainsValue(receiver) == false) {
|
||||
// We found an unusued receiver
|
||||
// Debug.Log($"{thingId} -> [{receiver.name}]");
|
||||
thingReceivers.Add(thingId, receiver);
|
||||
return receiver;
|
||||
}
|
||||
else if (receiver.isSleeping) {
|
||||
// A sleeping receiver is not active and can therefore always be used
|
||||
thingReceivers.Add(thingId, receiver);
|
||||
Debug.Log($"{thingId} -> [{selectedReceiver.name}]");
|
||||
return receiver;
|
||||
}
|
||||
else if (selectedReceiver == null) {
|
||||
// If we haven't found a receiver yet, just start by taking the first
|
||||
selectedReceiver = receiver;
|
||||
selectedMagnitude = length(selectedReceiver.outputValue);
|
||||
}
|
||||
// Look for the receiver with the lowest magnitude
|
||||
else {
|
||||
float magnitude = length(receiver.outputValue);
|
||||
|
||||
if (magnitude < inputMagnitude && length(receiver.outputValue) < selectedMagnitude) {
|
||||
selectedReceiver = receiver;
|
||||
selectedMagnitude = length(selectedReceiver.outputValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedReceiver != null) {
|
||||
// Replace the receiver
|
||||
// Find the thingId current associated with the receiver
|
||||
int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key;
|
||||
if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove))
|
||||
thingReceivers.Remove(keyToRemove);
|
||||
// And add the new association
|
||||
thingReceivers.Add(thingId, selectedReceiver);
|
||||
}
|
||||
return selectedReceiver;
|
||||
}
|
||||
|
||||
public override void UpdateStateIsolated() {
|
||||
float3 sum = this.bias;
|
||||
|
||||
// Receptors do not have inputs, so we ignore the synapses
|
||||
foreach (Nucleus nucleus in this._instances)
|
||||
sum += nucleus.outputValue;
|
||||
|
||||
this.outputValue = sum / _instances.Length;
|
||||
this.stale = 0;
|
||||
|
||||
UpdateNuclei();
|
||||
}
|
||||
|
||||
public override void UpdateNuclei() {
|
||||
foreach (Nucleus nucleus in this.instances) {
|
||||
nucleus.stale++;
|
||||
if (nucleus.stale > staleValueForSleep && lengthsq(nucleus.outputValue) > 0) {
|
||||
nucleus.outputValue = Vector3.zero;
|
||||
//this.UpdateStateIsolated();
|
||||
this.parent.UpdateFromNucleus(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
ReceptorArray.cs.meta
Normal file
2
ReceptorArray.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e915c8563642f23891b20522b3589cf
|
||||
7
Scene/TestScene Boid.unity.meta
Normal file
7
Scene/TestScene Boid.unity.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f343147e37db9eeda3e98058c553c92
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
365
Scene/TestScene Experiment.unity
Normal file
365
Scene/TestScene Experiment.unity
Normal file
@ -0,0 +1,365 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!29 &1
|
||||
OcclusionCullingSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 2
|
||||
m_OcclusionBakeSettings:
|
||||
smallestOccluder: 5
|
||||
smallestHole: 0.25
|
||||
backfaceThreshold: 100
|
||||
m_SceneGUID: 00000000000000000000000000000000
|
||||
m_OcclusionCullingData: {fileID: 0}
|
||||
--- !u!104 &2
|
||||
RenderSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 10
|
||||
m_Fog: 0
|
||||
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||
m_FogMode: 3
|
||||
m_FogDensity: 0.01
|
||||
m_LinearFogStart: 0
|
||||
m_LinearFogEnd: 300
|
||||
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
|
||||
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
|
||||
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
|
||||
m_AmbientIntensity: 1
|
||||
m_AmbientMode: 0
|
||||
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
|
||||
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_HaloStrength: 0.5
|
||||
m_FlareStrength: 1
|
||||
m_FlareFadeSpeed: 3
|
||||
m_HaloTexture: {fileID: 0}
|
||||
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
|
||||
m_DefaultReflectionMode: 0
|
||||
m_DefaultReflectionResolution: 128
|
||||
m_ReflectionBounces: 1
|
||||
m_ReflectionIntensity: 1
|
||||
m_CustomReflection: {fileID: 0}
|
||||
m_Sun: {fileID: 0}
|
||||
m_UseRadianceAmbientProbe: 0
|
||||
--- !u!157 &3
|
||||
LightmapSettings:
|
||||
m_ObjectHideFlags: 0
|
||||
serializedVersion: 13
|
||||
m_BakeOnSceneLoad: 0
|
||||
m_GISettings:
|
||||
serializedVersion: 2
|
||||
m_BounceScale: 1
|
||||
m_IndirectOutputScale: 1
|
||||
m_AlbedoBoost: 1
|
||||
m_EnvironmentLightingMode: 0
|
||||
m_EnableBakedLightmaps: 1
|
||||
m_EnableRealtimeLightmaps: 0
|
||||
m_LightmapEditorSettings:
|
||||
serializedVersion: 12
|
||||
m_Resolution: 2
|
||||
m_BakeResolution: 40
|
||||
m_AtlasSize: 1024
|
||||
m_AO: 0
|
||||
m_AOMaxDistance: 1
|
||||
m_CompAOExponent: 1
|
||||
m_CompAOExponentDirect: 0
|
||||
m_ExtractAmbientOcclusion: 0
|
||||
m_Padding: 2
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_LightmapsBakeMode: 1
|
||||
m_TextureCompression: 1
|
||||
m_ReflectionCompression: 2
|
||||
m_MixedBakeMode: 2
|
||||
m_BakeBackend: 2
|
||||
m_PVRSampling: 1
|
||||
m_PVRDirectSampleCount: 32
|
||||
m_PVRSampleCount: 512
|
||||
m_PVRBounces: 2
|
||||
m_PVREnvironmentSampleCount: 256
|
||||
m_PVREnvironmentReferencePointCount: 2048
|
||||
m_PVRFilteringMode: 1
|
||||
m_PVRDenoiserTypeDirect: 1
|
||||
m_PVRDenoiserTypeIndirect: 1
|
||||
m_PVRDenoiserTypeAO: 1
|
||||
m_PVRFilterTypeDirect: 0
|
||||
m_PVRFilterTypeIndirect: 0
|
||||
m_PVRFilterTypeAO: 0
|
||||
m_PVREnvironmentMIS: 1
|
||||
m_PVRCulling: 1
|
||||
m_PVRFilteringGaussRadiusDirect: 1
|
||||
m_PVRFilteringGaussRadiusIndirect: 1
|
||||
m_PVRFilteringGaussRadiusAO: 1
|
||||
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
|
||||
m_PVRFilteringAtrousPositionSigmaIndirect: 2
|
||||
m_PVRFilteringAtrousPositionSigmaAO: 1
|
||||
m_ExportTrainingData: 0
|
||||
m_TrainingDataDestination: TrainingData
|
||||
m_LightProbeSampleCountMultiplier: 4
|
||||
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_LightingSettings: {fileID: 0}
|
||||
--- !u!196 &4
|
||||
NavMeshSettings:
|
||||
serializedVersion: 2
|
||||
m_ObjectHideFlags: 0
|
||||
m_BuildSettings:
|
||||
serializedVersion: 3
|
||||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
agentSlope: 45
|
||||
agentClimb: 0.4
|
||||
ledgeDropHeight: 0
|
||||
maxJumpAcrossDistance: 0
|
||||
minRegionArea: 2
|
||||
manualCellSize: 0
|
||||
cellSize: 0.16666667
|
||||
manualTileSize: 0
|
||||
tileSize: 256
|
||||
buildHeightMesh: 0
|
||||
maxJobWorkers: 0
|
||||
preserveTilesOutsideBounds: 0
|
||||
debug:
|
||||
m_Flags: 0
|
||||
m_NavMeshData: {fileID: 0}
|
||||
--- !u!1 &388118692
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 388118694}
|
||||
- component: {fileID: 388118693}
|
||||
m_Layer: 0
|
||||
m_Name: GameObject
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &388118693
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 388118692}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9051408e82b511584998506096af4bf0, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::SelectorBrain
|
||||
defaultBrain: {fileID: 11400000, guid: d5b3a22d9bb7d13aeb3174077125967b, type: 2}
|
||||
input1: {x: 0, y: 0, z: 1}
|
||||
input2: {x: 0, y: -2, z: 0}
|
||||
output: {x: 0, y: 0, z: 0}
|
||||
--- !u!4 &388118694
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 388118692}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: -2.01476, y: -0, z: 0.65362}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &968074744
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 968074747}
|
||||
- component: {fileID: 968074746}
|
||||
- component: {fileID: 968074745}
|
||||
m_Layer: 0
|
||||
m_Name: Main Camera
|
||||
m_TagString: MainCamera
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!81 &968074745
|
||||
AudioListener:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 968074744}
|
||||
m_Enabled: 1
|
||||
--- !u!20 &968074746
|
||||
Camera:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 968074744}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 2
|
||||
m_ClearFlags: 1
|
||||
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_Iso: 200
|
||||
m_ShutterSpeed: 0.005
|
||||
m_Aperture: 16
|
||||
m_FocusDistance: 10
|
||||
m_FocalLength: 50
|
||||
m_BladeCount: 5
|
||||
m_Curvature: {x: 2, y: 11}
|
||||
m_BarrelClipping: 0.25
|
||||
m_Anamorphism: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
y: 0
|
||||
width: 1
|
||||
height: 1
|
||||
near clip plane: 0.3
|
||||
far clip plane: 1000
|
||||
field of view: 60
|
||||
orthographic: 0
|
||||
orthographic size: 5
|
||||
m_Depth: -1
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_RenderingPath: -1
|
||||
m_TargetTexture: {fileID: 0}
|
||||
m_TargetDisplay: 0
|
||||
m_TargetEye: 3
|
||||
m_HDR: 1
|
||||
m_AllowMSAA: 1
|
||||
m_AllowDynamicResolution: 0
|
||||
m_ForceIntoRT: 0
|
||||
m_OcclusionCulling: 1
|
||||
m_StereoConvergence: 10
|
||||
m_StereoSeparation: 0.022
|
||||
--- !u!4 &968074747
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 968074744}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 1, z: -10}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &2011285159
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2011285161}
|
||||
- component: {fileID: 2011285160}
|
||||
m_Layer: 0
|
||||
m_Name: Directional Light
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!108 &2011285160
|
||||
Light:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2011285159}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 12
|
||||
m_Type: 1
|
||||
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
|
||||
m_Intensity: 1
|
||||
m_Range: 10
|
||||
m_SpotAngle: 30
|
||||
m_InnerSpotAngle: 21.80208
|
||||
m_CookieSize2D: {x: 0.5, y: 0.5}
|
||||
m_Shadows:
|
||||
m_Type: 2
|
||||
m_Resolution: -1
|
||||
m_CustomResolution: -1
|
||||
m_Strength: 1
|
||||
m_Bias: 0.05
|
||||
m_NormalBias: 0.4
|
||||
m_NearPlane: 0.2
|
||||
m_CullingMatrixOverride:
|
||||
e00: 1
|
||||
e01: 0
|
||||
e02: 0
|
||||
e03: 0
|
||||
e10: 0
|
||||
e11: 1
|
||||
e12: 0
|
||||
e13: 0
|
||||
e20: 0
|
||||
e21: 0
|
||||
e22: 1
|
||||
e23: 0
|
||||
e30: 0
|
||||
e31: 0
|
||||
e32: 0
|
||||
e33: 1
|
||||
m_UseCullingMatrixOverride: 0
|
||||
m_Cookie: {fileID: 0}
|
||||
m_DrawHalo: 0
|
||||
m_Flare: {fileID: 0}
|
||||
m_RenderMode: 0
|
||||
m_CullingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 4294967295
|
||||
m_RenderingLayerMask: 1
|
||||
m_Lightmapping: 4
|
||||
m_LightShadowCasterMode: 0
|
||||
m_AreaSize: {x: 1, y: 1}
|
||||
m_BounceIntensity: 1
|
||||
m_ColorTemperature: 6570
|
||||
m_UseColorTemperature: 0
|
||||
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_UseBoundingSphereOverride: 0
|
||||
m_UseViewFrustumForShadowCasterCull: 1
|
||||
m_ForceVisible: 0
|
||||
m_ShadowRadius: 0
|
||||
m_ShadowAngle: 0
|
||||
m_LightUnit: 1
|
||||
m_LuxAtDistance: 1
|
||||
m_EnableSpotReflector: 1
|
||||
--- !u!4 &2011285161
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2011285159}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
|
||||
m_LocalPosition: {x: 0, y: 3, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 968074747}
|
||||
- {fileID: 2011285161}
|
||||
- {fileID: 388118694}
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62a58c801eda0c9eab7a49fb1d0840cb
|
||||
guid: 363b69b84de0e4b729794c10e7c40ab5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e47ea55fc051fcdcb8ae6197d1105cc0
|
||||
guid: 2c1e3956a0b70ae6b8d09fb467b73621
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
62
Selector.cs
Normal file
62
Selector.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[Serializable]
|
||||
public class Selector : Neuron {
|
||||
public Selector(Cluster parent, string name) : base(parent, name) { }
|
||||
public Selector(ClusterPrefab parent, string name) : base(parent, name) {}
|
||||
|
||||
public override Nucleus ShallowCloneTo(Cluster newParent) {
|
||||
Selector clone = new(newParent, this.name) {
|
||||
// array = this.array,
|
||||
curve = this.curve,
|
||||
curvePreset = this.curvePreset,
|
||||
curveMax = this.curveMax,
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
|
||||
public override void UpdateStateIsolated() { //float3 bias) {
|
||||
float3 max = this.bias;
|
||||
float maxSqrLength = lengthsq(max);
|
||||
|
||||
//Applying the weight factors
|
||||
foreach (Synapse synapse in this.synapses) {
|
||||
float3 input = synapse.weight * synapse.nucleus.outputValue;
|
||||
|
||||
float inputSqrlength = lengthsq(input);
|
||||
if (inputSqrlength > maxSqrLength) {
|
||||
max = input;
|
||||
maxSqrLength = inputSqrlength;
|
||||
}
|
||||
}
|
||||
|
||||
// Activation function
|
||||
float3 result;
|
||||
switch (this.curvePreset) {
|
||||
case CurvePresets.Linear:
|
||||
result = max;
|
||||
break;
|
||||
case CurvePresets.Sqrt:
|
||||
result = normalize(max) * System.MathF.Sqrt(length(max));
|
||||
break;
|
||||
case CurvePresets.Power:
|
||||
result = normalize(max) * System.MathF.Pow(length(max), 2);
|
||||
break;
|
||||
case CurvePresets.Reciprocal: {
|
||||
float magnitude = length(max);
|
||||
if (magnitude > 0)
|
||||
result = normalize(max) * (1 / magnitude);
|
||||
else
|
||||
result = float3(0, 0, 0);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
float activatedValue = this.curve.Evaluate(length(max));
|
||||
result = normalize(max) * activatedValue;
|
||||
break;
|
||||
}
|
||||
this.outputValue = result;
|
||||
}
|
||||
}
|
||||
2
Selector.cs.meta
Normal file
2
Selector.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4218c2f3f15af944db0eadd6e1500d17
|
||||
11
Synapse.cs
11
Synapse.cs
@ -3,19 +3,12 @@ using UnityEngine;
|
||||
|
||||
[Serializable]
|
||||
public class Synapse {
|
||||
// Support access to cluster of basic nucleus
|
||||
//public IReceptor nucleus => basicNucleus;
|
||||
|
||||
|
||||
//[SerializeReference]
|
||||
//public Cluster cluster;
|
||||
|
||||
[SerializeReference]
|
||||
public IReceptor nucleus;
|
||||
public Nucleus nucleus;
|
||||
|
||||
public float weight;
|
||||
|
||||
public Synapse(IReceptor nucleus, float weight = 1.0f) {
|
||||
public Synapse(Nucleus nucleus, float weight = 1.0f) {
|
||||
this.nucleus = nucleus;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
@ -1,728 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using Unity.Mathematics;
|
||||
using static Unity.Mathematics.math;
|
||||
|
||||
[CustomEditor(typeof(ClusterPrefab))]
|
||||
public class ClusterInspector : Editor {
|
||||
protected static VisualElement mainContainer;
|
||||
protected static VisualElement inspectorContainer;
|
||||
|
||||
protected bool breakOnWake = false;
|
||||
|
||||
#region Start
|
||||
|
||||
public override VisualElement CreateInspectorGUI() {
|
||||
ClusterPrefab cluster = target as ClusterPrefab;
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
VisualElement root = new();
|
||||
//root.style.flexDirection = FlexDirection.Row; // side-by-side layout
|
||||
//root.style.flexGrow = 1;
|
||||
//root.style.minHeight = 600;
|
||||
root.style.paddingLeft = 0;
|
||||
root.style.paddingRight = 0;
|
||||
root.style.paddingTop = 0;
|
||||
root.style.paddingBottom = 0;
|
||||
|
||||
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||
|
||||
mainContainer = new() {
|
||||
// name = "main",
|
||||
style = {
|
||||
// flexDirection = FlexDirection.Row,
|
||||
// flexGrow = 1,
|
||||
height = 450,
|
||||
}
|
||||
};
|
||||
GraphView graph = new();
|
||||
graph.style.flexGrow = 1;
|
||||
|
||||
inspectorContainer = new VisualElement {
|
||||
// name = "inspector"
|
||||
};
|
||||
|
||||
mainContainer.Add(graph);
|
||||
mainContainer.Add(inspectorContainer);
|
||||
root.Add(mainContainer);
|
||||
|
||||
// Run once for initial state (use resolved style width if available)
|
||||
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
|
||||
UpdateLayout(initialWidth);
|
||||
|
||||
// React to size changes of root (or parent if appropriate)
|
||||
root.RegisterCallback<GeometryChangedEvent>(evt => {
|
||||
UpdateLayout(evt.newRect.width);
|
||||
});
|
||||
|
||||
if (cluster != null) {
|
||||
cluster.EnsureInitialization();
|
||||
graph.SetGraph(null, cluster, cluster.output, inspectorContainer);
|
||||
}
|
||||
else
|
||||
Debug.LogWarning(" No brain!");
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
return root;
|
||||
}
|
||||
|
||||
public class GraphView : VisualElement {
|
||||
ClusterPrefab cluster;
|
||||
SerializedObject serializedBrain;
|
||||
INucleus currentNucleus;
|
||||
GameObject gameObject;
|
||||
private List<NeuroidLayer> layers = new();
|
||||
private readonly Dictionary<IReceptor, Vector2Int> neuroidPositions = new();
|
||||
private bool expandArray = false;
|
||||
|
||||
//Vector2 pan = Vector2.zero;
|
||||
//float zoom = 1f;
|
||||
//bool draggingCanvas = false;
|
||||
//Vector2 lastMouse;
|
||||
ClusterWrapper currentWrapper;
|
||||
|
||||
public GraphView() {
|
||||
name = "content";
|
||||
style.flexGrow = 1;
|
||||
|
||||
IMGUIContainer imguiContainer = new(OnIMGUI);
|
||||
imguiContainer.style.position = Position.Absolute;
|
||||
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
|
||||
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
|
||||
imguiContainer.pickingMode = PickingMode.Position;
|
||||
imguiContainer.focusable = true;
|
||||
Add(imguiContainer);
|
||||
|
||||
//RegisterCallback<WheelEvent>(OnWheel);
|
||||
// RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
// RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
// RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
|
||||
// Subscribe when added to panel (editor UI ready)
|
||||
RegisterCallback<AttachToPanelEvent>(evt => Subscribe());
|
||||
RegisterCallback<DetachFromPanelEvent>(evt => Unsubscribe());
|
||||
}
|
||||
|
||||
|
||||
bool subscribed = false;
|
||||
void Subscribe() {
|
||||
if (subscribed) return;
|
||||
SceneView.duringSceneGui += OnSceneGUI;
|
||||
subscribed = true;
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
void Unsubscribe() {
|
||||
if (!subscribed) return;
|
||||
SceneView.duringSceneGui -= OnSceneGUI;
|
||||
subscribed = false;
|
||||
}
|
||||
|
||||
public void SetGraph(GameObject gameObject, ClusterPrefab brain, INucleus nucleus, VisualElement inspectorContainer) {
|
||||
this.gameObject = gameObject;
|
||||
this.cluster = brain;
|
||||
if (Application.isPlaying == false)
|
||||
this.serializedBrain = new SerializedObject(brain);
|
||||
this.currentNucleus = nucleus;
|
||||
Rebuild(inspectorContainer);
|
||||
}
|
||||
|
||||
void Rebuild(VisualElement inspectorContainer) {
|
||||
BuildLayers();
|
||||
|
||||
if (this.currentNucleus == null) {
|
||||
inspectorContainer.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentWrapper != null)
|
||||
DestroyImmediate(currentWrapper);
|
||||
currentWrapper = CreateInstance<ClusterWrapper>().Init(this.currentNucleus, cluster);
|
||||
DrawInspector(inspectorContainer);
|
||||
}
|
||||
|
||||
private void BuildLayers() {
|
||||
// A temporary list to track what's been added to layers
|
||||
this.layers = new();
|
||||
int layerIx = 0;
|
||||
|
||||
INucleus selectedNucleus = this.currentNucleus;
|
||||
if (selectedNucleus == null)
|
||||
return;
|
||||
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||
|
||||
if (selectedNucleus.receivers != null) {
|
||||
foreach (INucleus receiver in selectedNucleus.receivers) {
|
||||
INucleus outputNeuroid = receiver;
|
||||
if (outputNeuroid != null) {
|
||||
AddToLayer(currentLayer, outputNeuroid);
|
||||
// Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
}
|
||||
|
||||
AddToLayer(currentLayer, selectedNucleus);
|
||||
this.layers.Add(currentLayer);
|
||||
// Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
|
||||
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
|
||||
if (selectedNucleus.synapses != null) {
|
||||
foreach (Synapse synapse in selectedNucleus.synapses) {
|
||||
IReceptor input = synapse.nucleus;
|
||||
AddToLayer(currentLayer, input);
|
||||
// Debug.Log($"layer {layerIx} nucleus {input.name}");
|
||||
}
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToLayer(NeuroidLayer layer, IReceptor nucleus) {
|
||||
if (nucleus == null)
|
||||
return;
|
||||
layer.neuroids.Add(nucleus);
|
||||
//nucleus.layerIx = layer.ix;
|
||||
// Store its position
|
||||
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
|
||||
neuroidPositions[nucleus] = neuroidPosition;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void OnIMGUI() {
|
||||
if (currentNucleus == null)
|
||||
return;
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedBrain.Update();
|
||||
|
||||
Handles.BeginGUI();
|
||||
DrawGraph();
|
||||
Handles.EndGUI();
|
||||
|
||||
}
|
||||
|
||||
private void DrawGraph() {
|
||||
float size = 20;
|
||||
Vector3 position = new(150, 210, 0);
|
||||
|
||||
DrawReceivers(this.currentNucleus, position, size);
|
||||
DrawSynapses(this.currentNucleus, position, size);
|
||||
|
||||
// Draw selected Nucleus
|
||||
if (expandArray) {
|
||||
float maxValue = 0;
|
||||
foreach (INucleus nucleus in this.currentNucleus.array.nuclei) {
|
||||
float value = length(nucleus.outputValue);
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
}
|
||||
|
||||
float spacing = 400f / this.currentNucleus.array.nuclei.Count();
|
||||
float margin = 10 + spacing / 2;
|
||||
float xMin = 150 - size;
|
||||
float xMax = 150 + size;
|
||||
float yMin = 10 + margin - size / 2;
|
||||
float yMax = 400 - margin + size;
|
||||
Vector3[] verts = new Vector3[4] {
|
||||
new(xMin, yMin, 0),
|
||||
new(xMax, yMin, 0),
|
||||
new(xMax, yMax, 0),
|
||||
new(xMin, yMax, 0)
|
||||
};
|
||||
Handles.color = Color.black;
|
||||
Handles.DrawAAConvexPolygon(verts);
|
||||
int row = 0;
|
||||
foreach (INucleus nucleus in this.currentNucleus.array.nuclei) {
|
||||
Vector3 pos = new(150, margin + row * spacing, 0.0f);
|
||||
Handles.color = Color.white;
|
||||
//Handles.DrawLine(parentPos, pos);
|
||||
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawSolidDisc(pos, Vector3.forward, size + 2);
|
||||
DrawNucleus(nucleus, pos, maxValue, size);
|
||||
row++;
|
||||
}
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
Vector3 labelPos = new Vector3(150, yMax, 0) - Vector3.down * (size + 10); // below disc along up axis
|
||||
Handles.Label(labelPos, this.currentNucleus.name, style);
|
||||
}
|
||||
else {
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
|
||||
DrawNucleus(this.currentNucleus, position, length(this.currentNucleus.outputValue), 20);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawReceivers(INucleus nucleus, Vector3 parentPos, float size) {
|
||||
int nodeCount = nucleus.receivers.Count();
|
||||
|
||||
// Determine the maximum value in this layer
|
||||
// This is used to 'scale' the output value colors of the nuclei
|
||||
float maxValue = 0;
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
if (receiver is Neuron neuroid) {
|
||||
float value = length(neuroid.outputValue);
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the spacing of the nuclei in the layer
|
||||
float spacing = 400f / nodeCount;
|
||||
float margin = 10 + spacing / 2;
|
||||
|
||||
int row = 0;
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
INucleus receiverNucleus = receiver;
|
||||
if (receiverNucleus == null)
|
||||
continue;
|
||||
|
||||
Vector3 pos = new(50, margin + row * spacing, 0.0f);
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawLine(parentPos, pos);
|
||||
|
||||
DrawNucleus(receiverNucleus, pos, maxValue, size);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSynapses(INucleus nucleus, Vector3 parentPos, float size) {
|
||||
int nodeCount = nucleus.synapses.Count;
|
||||
|
||||
// Determine the maximum value in this layer
|
||||
// This is used to 'scale' the output value colors of the nuclei
|
||||
float maxValue = 0;
|
||||
int neuronCount = 0;
|
||||
List<NucleusArray> drawnArrays = new();
|
||||
foreach (Synapse synapse in nucleus.synapses) {
|
||||
if (synapse.nucleus is Neuron neuroid) {
|
||||
if (drawnArrays.Contains(neuroid.array))
|
||||
continue;
|
||||
drawnArrays.Add(neuroid.array);
|
||||
|
||||
}
|
||||
float value = length(synapse.nucleus.outputValue) * synapse.weight;
|
||||
// Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}");
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
neuronCount++;
|
||||
}
|
||||
|
||||
// Determine the spacing of the nuclei in the layer
|
||||
float spacing = 400f / neuronCount;
|
||||
float margin = 10 + spacing / 2;
|
||||
|
||||
int row = 0;
|
||||
drawnArrays = new();
|
||||
foreach (Synapse synapse in nucleus.synapses) {
|
||||
if (synapse.nucleus is Neuron neuron) {
|
||||
if (drawnArrays.Contains(neuron.array))
|
||||
continue;
|
||||
drawnArrays.Add(neuron.array);
|
||||
}
|
||||
Vector3 pos = new(250, margin + row * spacing, 0.0f);
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawLine(parentPos, pos);
|
||||
if (synapse.nucleus != null) {
|
||||
Color color = Color.black;
|
||||
if (synapse.nucleus.isSleeping)
|
||||
color = Color.darkRed;
|
||||
else if (Application.isPlaying) {
|
||||
float brightness = length(synapse.nucleus.outputValue) * synapse.weight / maxValue;
|
||||
color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
DrawNucleus(synapse.nucleus, pos, maxValue, size, color);
|
||||
}
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size) {
|
||||
Color color;
|
||||
if (nucleus.isSleeping)
|
||||
color = Color.darkRed;
|
||||
else {
|
||||
if (Application.isPlaying) {
|
||||
float brightness = length(nucleus.outputValue) / maxValue;
|
||||
color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
else
|
||||
color = Color.black;
|
||||
}
|
||||
DrawNucleus(nucleus, position, maxValue, size, color);
|
||||
}
|
||||
|
||||
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size, Color color) {
|
||||
if (nucleus is MemoryCell memory) {
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawWireDisc(position + Vector3.right * 10, Vector3.forward, size);
|
||||
}
|
||||
|
||||
Handles.color = color;
|
||||
Handles.DrawSolidDisc(position, Vector3.forward, size);
|
||||
|
||||
Handles.color = Color.white;
|
||||
// Position the label in front of the disc
|
||||
Vector3 labelPosition = position + (Vector3.forward * 0.1f);
|
||||
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
if (nucleus is INucleus neuron) {
|
||||
if (neuron.array == null || neuron.array.nuclei == null || neuron.array.nuclei.Count() == 0)
|
||||
neuron.array = new NucleusArray(neuron);
|
||||
|
||||
if ((!expandArray || neuron.array.nuclei.First() != this.currentNucleus) && neuron.array.nuclei.Count() > 1) {
|
||||
Handles.Label(labelPosition, neuron.array.nuclei.Count().ToString(), style);
|
||||
}
|
||||
if (expandArray && neuron.array.nuclei.First() == this.currentNucleus) {
|
||||
int arrayIx = 0;
|
||||
foreach (INucleus n in neuron.array.nuclei) {
|
||||
if (n == neuron)
|
||||
break;
|
||||
arrayIx++;
|
||||
}
|
||||
Handles.Label(labelPosition, $"[{arrayIx}]", style);
|
||||
}
|
||||
else {
|
||||
style.alignment = TextAnchor.UpperCenter;
|
||||
Vector3 labelPos = position - Vector3.down * (size + 10f); // below disc along up axis
|
||||
Handles.Label(labelPos, nucleus.name, style);
|
||||
}
|
||||
|
||||
if (nucleus is Cluster cluster) {
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawWireDisc(position, Vector3.forward, size + 10);
|
||||
}
|
||||
}
|
||||
else {
|
||||
style.alignment = TextAnchor.UpperCenter;
|
||||
Vector3 labelPos = position - Vector3.down * (size + 10); // below disc along up axis
|
||||
Handles.Label(labelPos, nucleus.name, style);
|
||||
}
|
||||
|
||||
Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2);
|
||||
int id = GUIUtility.GetControlID(FocusType.Passive);
|
||||
Event e = Event.current;
|
||||
EventType et = e.GetTypeForControl(id);
|
||||
if (e != null && neuronRect.Contains(e.mousePosition)) {
|
||||
// Process Hover
|
||||
HandleMouseHover(nucleus, neuronRect);
|
||||
// Process click
|
||||
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||
// Consume the event so the scene doesn't also handle it
|
||||
e.Use();
|
||||
HandleClicked(nucleus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMouseHover(IReceptor nucleus, Rect rect) {
|
||||
GUIContent tooltip;
|
||||
if (nucleus is INucleus n) {
|
||||
tooltip = new(
|
||||
$"{nucleus.name}" +
|
||||
$"\nsynapse count {n.synapses.Count}" +
|
||||
$"\nValue: {nucleus.outputValue}");
|
||||
}
|
||||
else {
|
||||
tooltip = new(
|
||||
$"{nucleus.name}" +
|
||||
$"\nValue: {nucleus.outputValue}");
|
||||
}
|
||||
|
||||
Vector2 mousePosition = Event.current.mousePosition;
|
||||
|
||||
// Display tooltip with some offset
|
||||
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
|
||||
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
|
||||
|
||||
GUI.Box(tooltipRect, tooltip);
|
||||
}
|
||||
|
||||
private void HandleClicked(IReceptor nucleus) {
|
||||
if (nucleus == this.currentNucleus) {
|
||||
if (nucleus is INucleus n) {
|
||||
expandArray = !expandArray;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (nucleus is INucleus n) {
|
||||
this.currentNucleus = n;
|
||||
BuildLayers();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawInspector(VisualElement inspectorContainer) {
|
||||
if (inspectorContainer == null)
|
||||
return;
|
||||
|
||||
inspectorContainer.Clear();
|
||||
if (this.currentNucleus == null)
|
||||
return;
|
||||
|
||||
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
|
||||
SerializedObject so = new(currentWrapper);
|
||||
IMGUIContainer container = new(() => {
|
||||
if (so.targetObject == null)
|
||||
return;
|
||||
so.Update();
|
||||
|
||||
if (this.currentNucleus == null)
|
||||
return;
|
||||
|
||||
this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name);
|
||||
if (this.currentNucleus is Neuron neuroid) {
|
||||
if (this.currentNucleus is MemoryCell memory) {
|
||||
}
|
||||
else {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150));
|
||||
if (neuroid.curveMax > 0)
|
||||
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax));
|
||||
else
|
||||
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax));
|
||||
neuroid.curvePreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
if (neuroid.array == null || neuroid.array.nuclei == null || neuroid.array.nuclei.Count() == 0)
|
||||
neuroid.array = new NucleusArray(neuroid);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.IntField("Array size", neuroid.array.nuclei.Count());
|
||||
if (GUILayout.Button("Add"))
|
||||
neuroid.array.AddNucleus();
|
||||
if (GUILayout.Button("Del"))
|
||||
neuroid.array.RemoveNucleus();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
EditorGUILayout.FloatField("Output", length(this.currentNucleus.outputValue));
|
||||
else
|
||||
EditorGUILayout.LabelField(" ");
|
||||
|
||||
if (this.currentNucleus.synapses.Count > 0) {
|
||||
EditorGUILayout.LabelField("Synapses");
|
||||
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
|
||||
foreach (Synapse synapse in synapses) {
|
||||
if (synapse.nucleus != null) {
|
||||
EditorGUILayout.Space();
|
||||
|
||||
//EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
|
||||
if (Application.isPlaying)
|
||||
EditorGUILayout.FloatField(synapse.nucleus.name, length(synapse.nucleus.outputValue) * synapse.weight);
|
||||
else {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(synapse.nucleus.name);
|
||||
if (GUILayout.Button("Disconnect"))
|
||||
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight);
|
||||
EditorGUI.indentLevel--;
|
||||
//EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
ConnectNucleus(this.cluster, this.currentNucleus);
|
||||
if (GUILayout.Button("Add Input Neuron"))
|
||||
AddInputNeuron(this.currentNucleus);
|
||||
if (GUILayout.Button("Add Input MemoryCell"))
|
||||
AddInputMemoryCell(this.currentNucleus);
|
||||
if (GUILayout.Button("Add Input Cluster"))
|
||||
AddCluster(this.currentNucleus);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Delete this neuron"))
|
||||
DeleteNeuron(this.currentNucleus);
|
||||
|
||||
if (this.currentNucleus is Cluster subCluster) {
|
||||
if (GUILayout.Button("Edit Cluster"))
|
||||
EditCluster(subCluster);
|
||||
}
|
||||
|
||||
// if (this.gameObject != null) {
|
||||
// Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
|
||||
// //Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow);
|
||||
// Handles.color = Color.yellow;
|
||||
// Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
|
||||
// }
|
||||
});
|
||||
|
||||
inspectorContainer.Add(container);
|
||||
}
|
||||
|
||||
void OnSceneGUI(SceneView sceneView) {
|
||||
if (this.gameObject != null) {
|
||||
Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
|
||||
Handles.color = Color.yellow;
|
||||
Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void AddInputNeuron(INucleus nucleus) {
|
||||
Neuron newNeuroid = new(this.cluster, "New neuron");
|
||||
newNeuroid.AddReceiver(nucleus);
|
||||
this.currentNucleus = newNeuroid;
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
protected virtual void DeleteNeuron(INucleus nucleus) {
|
||||
if (nucleus == null)
|
||||
return;
|
||||
if (nucleus.cluster != null)
|
||||
this.currentNucleus = nucleus.cluster.output;
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
if (receiver != null) {
|
||||
this.currentNucleus = receiver;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Neuron.Delete(nucleus);
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
protected virtual void AddInputMemoryCell(INucleus nucleus) {
|
||||
MemoryCell newMemory = new(this.cluster, "New memory cell");
|
||||
newMemory.AddReceiver(nucleus);
|
||||
this.currentNucleus = newMemory;
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
protected virtual void AddCluster(INucleus nucleus) {
|
||||
ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster");
|
||||
}
|
||||
|
||||
private void OnClusterPicked(INucleus nucleus, ClusterPrefab prefab) {
|
||||
Cluster subclusterInstance = new(prefab, this.cluster);
|
||||
subclusterInstance.AddReceiver(nucleus);
|
||||
// This does not work somehow
|
||||
// this.currentNucleus = subclusterInstance;
|
||||
// BuildLayers();
|
||||
}
|
||||
|
||||
private void EditCluster(Cluster subCluster) {
|
||||
// May be used with storedPrefab...
|
||||
Selection.activeObject = subCluster.prefab;
|
||||
EditorGUIUtility.PingObject(subCluster.prefab);
|
||||
var editor = Editor.CreateEditor(subCluster.prefab);
|
||||
}
|
||||
|
||||
// Connect to another nucleus in the same cluster
|
||||
protected virtual void ConnectNucleus(ClusterPrefab cluster, INucleus nucleus) {
|
||||
if (cluster == null)
|
||||
return;
|
||||
|
||||
IEnumerable<string> synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus != null ? synapse.nucleus.name : "");
|
||||
//IEnumerable<string> perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei);
|
||||
IEnumerable<string> nuclei = cluster.nuclei.Select(i => i.name).Except(synapseNuclei);
|
||||
//string[] names = perceptei.Concat(nuclei).ToArray();
|
||||
string[] names = nuclei.ToArray();
|
||||
int selectedIndex = -1;
|
||||
selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names);
|
||||
if (selectedIndex >= 0) {
|
||||
// if (selectedIndex < perceptei.Count()) {
|
||||
// Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex];
|
||||
// n.AddReceiver(this.currentNucleus);
|
||||
// }
|
||||
// else {
|
||||
// Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()];
|
||||
// n.AddReceiver(this.currentNucleus);
|
||||
// }
|
||||
IReceptor receptor = cluster.nuclei[selectedIndex];
|
||||
receptor.AddReceiver(this.currentNucleus);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void DisconnectNucleus(Neuron nucleus) {
|
||||
if (this.currentNucleus.cluster == null)
|
||||
return;
|
||||
string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray();
|
||||
int selectedIndex = -1;
|
||||
selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names);
|
||||
//if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) {
|
||||
if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.cluster.nuclei.Count) {
|
||||
Synapse synapse = this.currentNucleus.synapses[selectedIndex];
|
||||
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Start
|
||||
|
||||
#region Update
|
||||
|
||||
private void UpdateLayout(float containerWidth) {
|
||||
if (containerWidth > 600f) {
|
||||
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||
inspectorContainer.style.width = 300; // fixed sidebar width
|
||||
inspectorContainer.style.flexGrow = 0;
|
||||
}
|
||||
else {
|
||||
mainContainer.style.flexDirection = FlexDirection.Column;
|
||||
inspectorContainer.style.width = Length.Percent(100); // full width below
|
||||
inspectorContainer.style.flexDirection = FlexDirection.Column;
|
||||
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Update
|
||||
}
|
||||
|
||||
public class NeuroidLayer {
|
||||
public int ix = 0;
|
||||
public List<IReceptor> neuroids = new();
|
||||
}
|
||||
|
||||
public class ClusterWrapper : ScriptableObject {
|
||||
// expose fields that map to GraphNode
|
||||
//public string title;
|
||||
public Vector2 position;
|
||||
INucleus node;
|
||||
ClusterPrefab graph; // needed to write back and mark dirty
|
||||
|
||||
public ClusterWrapper Init(INucleus node, ClusterPrefab graphAsset) {
|
||||
this.node = node;
|
||||
this.graph = graphAsset;
|
||||
//this.title = " A " + node.name;
|
||||
//position = node.position;
|
||||
return this;
|
||||
}
|
||||
void OnValidate() {
|
||||
if (node != null) {
|
||||
//node.name = title;
|
||||
//node.position = position;
|
||||
#if UNITY_EDITOR
|
||||
if (graph != null)
|
||||
UnityEditor.EditorUtility.SetDirty(graph);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[CustomEditor(typeof(NanoBrainComponent))]
|
||||
public class NanoBrainComponent_Editor : Editor {
|
||||
protected static VisualElement mainContainer;
|
||||
protected static VisualElement inspectorContainer;
|
||||
|
||||
protected NanoBrainComponent component;
|
||||
private SerializedProperty brainProp;
|
||||
|
||||
ClusterInspector.GraphView board;
|
||||
|
||||
public void OnEnable() {
|
||||
component = target as NanoBrainComponent;
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
brainProp = serializedObject.FindProperty(nameof(NanoBrainComponent.defaultBrain));
|
||||
|
||||
}
|
||||
|
||||
public override VisualElement CreateInspectorGUI() {
|
||||
//ClusterPrefab brain = Application.isPlaying ? component.brain.prefab : component.defaultBrain;
|
||||
Cluster brain = component.brain;
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedObject.Update();
|
||||
|
||||
|
||||
VisualElement root = new();
|
||||
root.style.flexDirection = FlexDirection.Column; // side-by-side layout
|
||||
root.style.flexGrow = 1;
|
||||
root.style.minHeight = 600;
|
||||
root.style.paddingLeft = 0;
|
||||
root.style.paddingRight = 0;
|
||||
root.style.paddingTop = 0;
|
||||
root.style.paddingBottom = 0;
|
||||
|
||||
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||
|
||||
if (Application.isPlaying == false) {
|
||||
PropertyField brainField = new(brainProp) {
|
||||
label = "Nano Brain"
|
||||
};
|
||||
root.Add(brainField);
|
||||
}
|
||||
|
||||
mainContainer = new() {
|
||||
name = "main",
|
||||
style = {
|
||||
flexDirection = FlexDirection.Row,
|
||||
flexGrow = 1,
|
||||
minHeight = 500,
|
||||
}
|
||||
};
|
||||
board = new ClusterInspector.GraphView();
|
||||
board.style.flexGrow = 1;
|
||||
mainContainer.Add(board);
|
||||
|
||||
inspectorContainer = new VisualElement {
|
||||
name = "inspector",
|
||||
style = {
|
||||
width = 400,
|
||||
}
|
||||
};
|
||||
|
||||
mainContainer.Add(inspectorContainer);
|
||||
root.Add(mainContainer);
|
||||
|
||||
// Run once for initial state (use resolved style width if available)
|
||||
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
|
||||
UpdateLayout(initialWidth);
|
||||
|
||||
// React to size changes of root (or parent if appropriate)
|
||||
root.RegisterCallback<GeometryChangedEvent>(evt => {
|
||||
UpdateLayout(evt.newRect.width);
|
||||
});
|
||||
|
||||
if (brain != null && board != null)
|
||||
board.SetGraph(component.gameObject, brain.prefab, brain.output, inspectorContainer);
|
||||
// else
|
||||
// Debug.LogWarning(" No brain!");
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
return root;
|
||||
}
|
||||
|
||||
// void OnSceneGUI() {
|
||||
// if (Application.isPlaying && board != null)
|
||||
// board.OnIMGUI();
|
||||
// }
|
||||
|
||||
void OnSceneGui(SceneView sv) {
|
||||
if (Application.isPlaying == false)
|
||||
return;
|
||||
// May need some throttling here...
|
||||
if (board != null) {
|
||||
Debug.Log(".");
|
||||
board.OnIMGUI();
|
||||
}
|
||||
|
||||
// EditorApplication.delayCall = UpdateInspectorUI;
|
||||
}
|
||||
|
||||
void UpdateInspectorUI() {
|
||||
if (board != null) {
|
||||
Debug.Log(".");
|
||||
board.OnIMGUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLayout(float containerWidth) {
|
||||
if (containerWidth > 800f) {
|
||||
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||
inspectorContainer.style.width = 400; // fixed sidebar width
|
||||
inspectorContainer.style.flexGrow = 0;
|
||||
}
|
||||
else {
|
||||
mainContainer.style.flexDirection = FlexDirection.Column;
|
||||
inspectorContainer.style.width = Length.Percent(100); // full width below
|
||||
inspectorContainer.style.flexDirection = FlexDirection.Column;
|
||||
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,511 +0,0 @@
|
||||
/*
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.Callbacks;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class NucleusLayer {
|
||||
public int ix = 0;
|
||||
public List<Nucleus> neuroids = new();
|
||||
}
|
||||
|
||||
public class NanoBrainEditor : EditorWindow {
|
||||
public NanoBrain brain;
|
||||
|
||||
public static VisualElement inspectorContainer;
|
||||
|
||||
[MenuItem("Window/NanoBrain Editor")]
|
||||
public static void ShowWindow() {
|
||||
GetWindow<NanoBrainEditor>("NanoBrain Editor");
|
||||
}
|
||||
|
||||
public static void Open(NanoBrain asset) {
|
||||
NanoBrainEditor editor = GetWindow<NanoBrainEditor>("NanoBrain Editor");
|
||||
editor.brain = asset;
|
||||
editor.Show();
|
||||
}
|
||||
|
||||
GraphBoardView board;
|
||||
|
||||
private void OnEnable() {
|
||||
OnFocus();
|
||||
}
|
||||
private void OnFocus() {
|
||||
if (brain == null) {
|
||||
// brain = CreateInstance<NanoBrainObj>();
|
||||
// EditorUtility.SetDirty(brain);
|
||||
return;
|
||||
}
|
||||
|
||||
VisualElement root = rootVisualElement;
|
||||
root.Clear();
|
||||
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||
|
||||
VisualElement main = new() {
|
||||
name = "main",
|
||||
style = {
|
||||
flexDirection = FlexDirection.Row,
|
||||
flexGrow = 1
|
||||
}
|
||||
};
|
||||
board = new GraphBoardView();
|
||||
board.style.flexGrow = 1;
|
||||
inspectorContainer = new VisualElement {
|
||||
name = "inspector",
|
||||
style = {
|
||||
width = 400
|
||||
}
|
||||
};
|
||||
|
||||
main.Add(board);
|
||||
main.Add(inspectorContainer);
|
||||
root.Add(main);
|
||||
|
||||
board.SetGraph(brain, brain.root);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class GraphBoardView : VisualElement {
|
||||
NanoBrain brain;
|
||||
SerializedObject serializedBrain;
|
||||
Nucleus currentNucleus;
|
||||
private List<NeuroidLayer> layers = new();
|
||||
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
|
||||
|
||||
Vector2 pan = Vector2.zero;
|
||||
//float zoom = 1f;
|
||||
bool draggingCanvas = false;
|
||||
Vector2 lastMouse;
|
||||
GraphNodeWrapper currentWrapper;
|
||||
|
||||
public GraphBoardView() {
|
||||
name = "content";
|
||||
style.flexGrow = 1;
|
||||
|
||||
IMGUIContainer imguiContainer = new(OnIMGUI);
|
||||
imguiContainer.style.position = Position.Absolute;
|
||||
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
|
||||
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
|
||||
imguiContainer.pickingMode = PickingMode.Position;
|
||||
imguiContainer.focusable = true;
|
||||
Add(imguiContainer);
|
||||
|
||||
//RegisterCallback<WheelEvent>(OnWheel);
|
||||
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
}
|
||||
|
||||
public void SetGraph(NanoBrain brain, Nucleus nucleus) {
|
||||
this.brain = brain;
|
||||
this.serializedBrain = new SerializedObject(brain);
|
||||
this.currentNucleus = nucleus;
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
void Rebuild() {
|
||||
BuildLayers();
|
||||
|
||||
if (currentNucleus == null) {
|
||||
NanoBrainEditor.inspectorContainer.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentWrapper != null)
|
||||
Object.DestroyImmediate(currentWrapper);
|
||||
currentWrapper = ScriptableObject.CreateInstance<GraphNodeWrapper>().Init(currentNucleus, brain);
|
||||
DrawInspector();
|
||||
}
|
||||
|
||||
private void BuildLayers() {
|
||||
// A temporary list to track what's been added to layers
|
||||
this.layers = new();
|
||||
int layerIx = 0;
|
||||
|
||||
Nucleus selectedNucleus = this.currentNucleus;
|
||||
if (selectedNucleus == null)
|
||||
return;
|
||||
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||
|
||||
//foreach (Nucleus outputNeuroid in selectedNucleus.receivers) {
|
||||
foreach (Receiver receiver in selectedNucleus.receivers) {
|
||||
Nucleus outputNeuroid = receiver.nucleus;
|
||||
if (outputNeuroid != null) {
|
||||
AddToLayer(currentLayer, outputNeuroid);
|
||||
// Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
|
||||
}
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
}
|
||||
|
||||
AddToLayer(currentLayer, selectedNucleus);
|
||||
this.layers.Add(currentLayer);
|
||||
// Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
|
||||
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
|
||||
//foreach (Nucleus input in selectedNucleus.synapses.Keys) {
|
||||
foreach (Synapse synapse in selectedNucleus.synapses) {
|
||||
Nucleus input = synapse.nucleus;
|
||||
AddToLayer(currentLayer, input);
|
||||
// Debug.Log($"layer {layerIx} nucleus {input.name}");
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) {
|
||||
if (nucleus == null)
|
||||
return;
|
||||
layer.neuroids.Add(nucleus);
|
||||
nucleus.layerIx = layer.ix;
|
||||
// Store its position
|
||||
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
|
||||
neuroidPositions[nucleus] = neuroidPosition;
|
||||
|
||||
}
|
||||
|
||||
// basic pan/zoom handling
|
||||
// void OnWheel(WheelEvent e) {
|
||||
// if (e.ctrlKey) {
|
||||
// float delta = -e.delta.y * 0.001f;
|
||||
// zoom = Mathf.Clamp(zoom + delta, 0.25f, 2f);
|
||||
// content.transform.rotation = Quaternion.identity; // keep transform accessible
|
||||
// content.transform.scale = new Vector3(zoom, zoom, 1);
|
||||
// e.StopPropagation();
|
||||
// }
|
||||
// else {
|
||||
// pan += e.delta;
|
||||
// content.style.left = pan.x;
|
||||
// content.style.top = pan.y;
|
||||
// }
|
||||
// }
|
||||
|
||||
void OnMouseDown(MouseDownEvent e) {
|
||||
if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); }
|
||||
}
|
||||
void OnMouseMove(MouseMoveEvent e) {
|
||||
if (draggingCanvas) {
|
||||
var delta = e.mousePosition - lastMouse;
|
||||
pan += delta;
|
||||
//content.style.left = pan.x;
|
||||
//content.style.top = pan.y;
|
||||
lastMouse = e.mousePosition;
|
||||
}
|
||||
}
|
||||
void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; }
|
||||
|
||||
void OnIMGUI() {
|
||||
if (currentNucleus == null)
|
||||
return;
|
||||
|
||||
serializedBrain.Update();
|
||||
|
||||
Handles.BeginGUI();
|
||||
foreach (NeuroidLayer layer in layers)
|
||||
DrawLayer(layer);
|
||||
Handles.EndGUI();
|
||||
|
||||
}
|
||||
|
||||
private void DrawLayer(NeuroidLayer layer) {
|
||||
int nodeCount = layer.neuroids.Count;
|
||||
float maxValue = 0;
|
||||
foreach (Nucleus nucleus in layer.neuroids) {
|
||||
if (nucleus is Neuroid neuroid) {
|
||||
float value = neuroid.outputValue.magnitude;
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
}
|
||||
}
|
||||
float spacing = 400f / nodeCount;
|
||||
float margin = 10 + spacing / 2;
|
||||
foreach (Nucleus layerNucleus in layer.neuroids) {
|
||||
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNucleus];
|
||||
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
|
||||
|
||||
//int i = 0;
|
||||
float inputSpacing = 400f / layerNucleus.synapses.Count;
|
||||
float inputMargin = 10 + inputSpacing / 2;
|
||||
// int minStale = 10000;
|
||||
//foreach ((Nucleus nucleus, float weight) in layerNucleus.synapses) {
|
||||
foreach (Synapse synapse in layerNucleus.synapses) {
|
||||
Nucleus nucleus = synapse.nucleus;
|
||||
if (nucleus != null) {
|
||||
float weight = synapse.weight;
|
||||
if (this.neuroidPositions.ContainsKey(nucleus)) {
|
||||
Vector2Int inputNeuroidPos = this.neuroidPositions[nucleus];
|
||||
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
|
||||
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
|
||||
|
||||
float brightness = weight / 10.0f;
|
||||
Handles.color = new Color(brightness, brightness, brightness);
|
||||
Handles.DrawLine(parentPos, pos);
|
||||
}
|
||||
}
|
||||
// if (nucleus is Neuroid neuroid && neuroid.stale < minStale)
|
||||
// minStale = neuroid.stale;
|
||||
}
|
||||
}
|
||||
|
||||
// if (layerNucleus.synapses.Count > 0 && minStale > 2 && layerNucleus.stale < 3)
|
||||
// Debug.LogWarning($"Strange {minStale} is big duing update");
|
||||
|
||||
|
||||
float size = 20;
|
||||
if (layerNucleus.isSleeping)
|
||||
Handles.color = Color.darkRed;
|
||||
else {
|
||||
float brightness = layerNucleus.outputValue.magnitude / maxValue;
|
||||
Handles.color = new Color(brightness, brightness, brightness);
|
||||
}
|
||||
Handles.DrawSolidDisc(parentPos, Vector3.forward, size);
|
||||
Vector3 labelPos = parentPos - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||
GUIStyle style = new GUIStyle(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold
|
||||
};
|
||||
Handles.Label(labelPos, layerNucleus.name, style);
|
||||
|
||||
Rect neuronRect = new(parentPos.x - size, parentPos.y - size, size * 2, size * 2);
|
||||
int id = GUIUtility.GetControlID(FocusType.Passive);
|
||||
Event e = Event.current;
|
||||
EventType et = e.GetTypeForControl(id);
|
||||
if (e != null && neuronRect.Contains(e.mousePosition)) {
|
||||
// Process Hover
|
||||
HandleMouseHover(layerNucleus, neuronRect);
|
||||
// Process click
|
||||
// Debug.Log($"{et} {e.type}");
|
||||
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||
// Consume the event so the scene doesn't also handle it
|
||||
e.Use();
|
||||
HandleDiscClicked(layerNucleus);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMouseHover(Nucleus neuroid, Rect rect) {
|
||||
GUIContent tooltip;
|
||||
// if (neuroid is SensoryNeuroid sensoryNeuroid) {
|
||||
// tooltip = new(
|
||||
// $"{sensoryNeuroid.name}" +
|
||||
// $"\nThing {sensoryNeuroid.receptor.thingType}" +
|
||||
// $"\nValue: {neuroid.outputValue}");
|
||||
// }
|
||||
// else {
|
||||
tooltip = new(
|
||||
$"{neuroid.name}" +
|
||||
$"\nsynapse count {neuroid.synapses.Count}" +
|
||||
$"\nValue: {neuroid.outputValue}");
|
||||
// }
|
||||
|
||||
Vector2 mousePosition = Event.current.mousePosition;
|
||||
|
||||
// Display tooltip with some offset
|
||||
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
|
||||
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
|
||||
|
||||
GUI.Box(tooltipRect, tooltip);
|
||||
}
|
||||
|
||||
private void HandleDiscClicked(Nucleus nucleus) {
|
||||
this.currentNucleus = nucleus;
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
|
||||
void DrawInspector() {
|
||||
if (NanoBrainEditor.inspectorContainer == null)
|
||||
return;
|
||||
|
||||
NanoBrainEditor.inspectorContainer.Clear();
|
||||
if (this.currentNucleus == null)
|
||||
return;
|
||||
|
||||
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
|
||||
SerializedObject so = new SerializedObject(currentWrapper);
|
||||
IMGUIContainer container = new IMGUIContainer(() => {
|
||||
so.Update();
|
||||
currentNucleus.name = EditorGUILayout.TextField(currentNucleus.name);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Output Value", GUILayout.Width(100));
|
||||
EditorGUILayout.Vector3Field(GUIContent.none, currentNucleus.outputValue);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
if (currentNucleus.synapses.Count > 0) {
|
||||
EditorGUILayout.LabelField("Synapses");
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
//List<Nucleus> nuclei = currentNucleus.synapses.Keys.ToList();
|
||||
// foreach (Nucleus nucleus in nuclei) {
|
||||
foreach (Synapse synapse in currentNucleus.synapses) {
|
||||
EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(synapse.nucleus.name, GUILayout.Width(120));
|
||||
EditorGUI.indentLevel--;
|
||||
EditorGUILayout.LabelField("Weight", GUILayout.Width(45));
|
||||
// float weight = currentNucleus.synapses[nucleus];
|
||||
// currentNucleus.synapses[nucleus] = EditorGUILayout.FloatField(weight, GUILayout.Width(40));
|
||||
synapse.weight = EditorGUILayout.FloatField(synapse.weight, GUILayout.Width(40));
|
||||
EditorGUI.indentLevel++;
|
||||
EditorGUILayout.Vector3Field(GUIContent.none, synapse.nucleus.outputValue, GUILayout.Width(180));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
if (GUILayout.Button("Add Neuron"))
|
||||
AddInputNeuron(currentNucleus);
|
||||
|
||||
});
|
||||
|
||||
NanoBrainEditor.inspectorContainer.Add(container);
|
||||
}
|
||||
|
||||
protected virtual void AddInputNeuron(Nucleus receiver) {
|
||||
Neuroid newNeuroid = new(brain, "New neuron");
|
||||
newNeuroid.AddReceiver(receiver);
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
private Vector3 NodePosition(Nucleus nucleus, int layerNodeCount = 1) {
|
||||
if (this.neuroidPositions.ContainsKey(nucleus)) {
|
||||
Vector2Int nucleusPos = this.neuroidPositions[nucleus];
|
||||
return NodePosition(nucleusPos, layerNodeCount);
|
||||
}
|
||||
else {
|
||||
return Vector3.zero;
|
||||
}
|
||||
}
|
||||
private Vector3 NodePosition(Vector2Int location, int layerNodeCount = 1) {
|
||||
float spacing = 400f / layerNodeCount;
|
||||
float margin = 10 + spacing / 2;
|
||||
float size = 20;
|
||||
Vector3 parentPos = new(100 + location.x * 100 - size, margin + location.y * spacing - size, 0.1f);
|
||||
return parentPos;
|
||||
}
|
||||
|
||||
|
||||
// public void CreateEdge(string fromId, string toId) {
|
||||
// if (fromId == toId) return;
|
||||
// Undo.RecordObject(graph, "Create Edge");
|
||||
// graph.edges.Add(new GraphEdge { fromNodeId = fromId, toNodeId = toId });
|
||||
// EditorUtility.SetDirty(graph);
|
||||
// Rebuild();
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
public class NodeView : VisualElement {
|
||||
Nucleus data;
|
||||
GraphBoardView board;
|
||||
Label titleLabel;
|
||||
//bool dragging = false;
|
||||
Vector2 localDragStart;
|
||||
|
||||
public NodeView(Nucleus node, GraphBoardView boardView) {
|
||||
data = node;
|
||||
board = boardView;
|
||||
name = "node";
|
||||
style.width = 20; //node.size.x;
|
||||
style.height = 20; //node.size.y;
|
||||
|
||||
titleLabel = new Label(node.name) { name = "title" };
|
||||
Add(titleLabel);
|
||||
|
||||
// ports
|
||||
// var outPort = new Button(() => StartEdgeDrag(true)) { text = "◀", name = "out" };
|
||||
// var inPort = new Button(() => StartEdgeDrag(false)) { text = "▶", name = "in" };
|
||||
// Add(outPort);
|
||||
// Add(inPort);
|
||||
|
||||
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
// RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
//RegisterCallback<MouseLeaveEvent>(e => dragging = false);
|
||||
}
|
||||
|
||||
// void StartEdgeDrag(bool isOutput) {
|
||||
// // simplified: on first click store source; on second click on target port call board.CreateEdge
|
||||
// if (EdgeDragState.active == null) EdgeDragState.active = new EdgeDragState { fromNode = data, fromIsOutput = isOutput };
|
||||
// else {
|
||||
// var src = EdgeDragState.active.fromNode;
|
||||
// if (src != null && src.id != data.id) board.CreateEdge(src.id, data.id);
|
||||
// EdgeDragState.active = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
void OnMouseDown(MouseDownEvent e) {
|
||||
if (e.button == 0 && e.target == this) {
|
||||
//dragging = true;
|
||||
localDragStart = e.mousePosition;
|
||||
e.StopPropagation();
|
||||
}
|
||||
}
|
||||
// void OnMouseMove(MouseMoveEvent e) {
|
||||
// if (!dragging) return;
|
||||
// var delta = e.mousePosition - localDragStart;
|
||||
// var worldPos = new Vector2(layout.x + delta.x, layout.y + delta.y);
|
||||
// style.left = worldPos.x;
|
||||
// style.top = worldPos.y;
|
||||
// // commit on every move
|
||||
// board.UpdateNodePosition(data, worldPos);
|
||||
// }
|
||||
void OnMouseUp(MouseUpEvent e) {
|
||||
//dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class GraphNodeWrapper : ScriptableObject {
|
||||
// expose fields that map to GraphNode
|
||||
public string title;
|
||||
public Vector2 position;
|
||||
Nucleus node;
|
||||
NanoBrain graph; // needed to write back and mark dirty
|
||||
|
||||
public GraphNodeWrapper Init(Nucleus node, NanoBrain graphAsset) {
|
||||
this.node = node;
|
||||
this.graph = graphAsset;
|
||||
this.title = " A " + node.name;
|
||||
//position = node.position;
|
||||
return this;
|
||||
}
|
||||
void OnValidate() {
|
||||
if (node != null) {
|
||||
node.name = title;
|
||||
//node.position = position;
|
||||
#if UNITY_EDITOR
|
||||
if (graph != null)
|
||||
UnityEditor.EditorUtility.SetDirty(graph);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
//static class EdgeDragState { public static EdgeDragState active; public GraphNode fromNode; public bool fromIsOutput; }
|
||||
|
||||
public static class OpenAssetHandler {
|
||||
// Called when an asset is double-clicked or opened.
|
||||
[OnOpenAsset]
|
||||
public static bool OpenMyScriptableObject(int instanceID, int line) {
|
||||
NanoBrain obj = EditorUtility.EntityIdToObject(instanceID) as NanoBrain;
|
||||
if (obj != null) {
|
||||
NanoBrainEditor.Open(obj);
|
||||
return true; // handled
|
||||
}
|
||||
return false; // let Unity open normally
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c57f78e25f0e55b96a50fd5592b26317
|
||||
@ -1,645 +0,0 @@
|
||||
/*
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
[CustomEditor(typeof(NanoBrain))]
|
||||
public class NanoBrainInspector : Editor {
|
||||
protected static VisualElement mainContainer;
|
||||
protected static VisualElement inspectorContainer;
|
||||
|
||||
protected bool breakOnWake = false;
|
||||
|
||||
#region Start
|
||||
|
||||
public override VisualElement CreateInspectorGUI() {
|
||||
NanoBrain brain = target as NanoBrain;
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
VisualElement root = new();
|
||||
//root.style.flexDirection = FlexDirection.Row; // side-by-side layout
|
||||
//root.style.flexGrow = 1;
|
||||
//root.style.minHeight = 600;
|
||||
root.style.paddingLeft = 0;
|
||||
root.style.paddingRight = 0;
|
||||
root.style.paddingTop = 0;
|
||||
root.style.paddingBottom = 0;
|
||||
|
||||
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||
|
||||
mainContainer = new() {
|
||||
// name = "main",
|
||||
style = {
|
||||
// flexDirection = FlexDirection.Row,
|
||||
// flexGrow = 1,
|
||||
height = 450,
|
||||
}
|
||||
};
|
||||
GraphView graph = new();
|
||||
graph.style.flexGrow = 1;
|
||||
|
||||
inspectorContainer = new VisualElement {
|
||||
// name = "inspector"
|
||||
};
|
||||
|
||||
mainContainer.Add(graph);
|
||||
mainContainer.Add(inspectorContainer);
|
||||
root.Add(mainContainer);
|
||||
|
||||
// Run once for initial state (use resolved style width if available)
|
||||
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
|
||||
UpdateLayout(initialWidth);
|
||||
|
||||
// React to size changes of root (or parent if appropriate)
|
||||
root.RegisterCallback<GeometryChangedEvent>(evt => {
|
||||
UpdateLayout(evt.newRect.width);
|
||||
});
|
||||
|
||||
if (brain != null)
|
||||
graph.SetGraph(null, brain, brain.output, inspectorContainer);
|
||||
else
|
||||
Debug.LogWarning(" No brain!");
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
return root;
|
||||
}
|
||||
|
||||
public class GraphView : VisualElement {
|
||||
NanoBrain brain;
|
||||
SerializedObject serializedBrain;
|
||||
INucleus currentNucleus;
|
||||
GameObject gameObject;
|
||||
private List<NeuroidLayer> layers = new();
|
||||
private readonly Dictionary<IReceptor, Vector2Int> neuroidPositions = new();
|
||||
|
||||
Vector2 pan = Vector2.zero;
|
||||
//float zoom = 1f;
|
||||
bool draggingCanvas = false;
|
||||
Vector2 lastMouse;
|
||||
GraphNodeWrapper currentWrapper;
|
||||
|
||||
public GraphView() {
|
||||
name = "content";
|
||||
style.flexGrow = 1;
|
||||
|
||||
IMGUIContainer imguiContainer = new(OnIMGUI);
|
||||
imguiContainer.style.position = Position.Absolute;
|
||||
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
|
||||
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
|
||||
imguiContainer.pickingMode = PickingMode.Position;
|
||||
imguiContainer.focusable = true;
|
||||
Add(imguiContainer);
|
||||
|
||||
//RegisterCallback<WheelEvent>(OnWheel);
|
||||
RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
}
|
||||
|
||||
public void SetGraph(GameObject gameObject, NanoBrain brain, Nucleus nucleus, VisualElement inspectorContainer) {
|
||||
this.gameObject = gameObject;
|
||||
this.brain = brain;
|
||||
if (Application.isPlaying == false)
|
||||
this.serializedBrain = new SerializedObject(brain);
|
||||
this.currentNucleus = nucleus;
|
||||
Rebuild(inspectorContainer);
|
||||
}
|
||||
|
||||
void Rebuild(VisualElement inspectorContainer) {
|
||||
BuildLayers();
|
||||
|
||||
if (this.currentNucleus == null) {
|
||||
inspectorContainer.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentWrapper != null)
|
||||
DestroyImmediate(currentWrapper);
|
||||
currentWrapper = CreateInstance<GraphNodeWrapper>().Init(this.currentNucleus, brain);
|
||||
DrawInspector(inspectorContainer);
|
||||
}
|
||||
|
||||
private void BuildLayers() {
|
||||
// A temporary list to track what's been added to layers
|
||||
this.layers = new();
|
||||
int layerIx = 0;
|
||||
|
||||
INucleus selectedNucleus = this.currentNucleus;
|
||||
if (selectedNucleus == null)
|
||||
return;
|
||||
NeuroidLayer currentLayer = new() { ix = layerIx };
|
||||
|
||||
if (selectedNucleus.receivers != null) {
|
||||
foreach (INucleus receiver in selectedNucleus.receivers) {
|
||||
INucleus outputNeuroid = receiver;
|
||||
if (outputNeuroid != null) {
|
||||
AddToLayer(currentLayer, outputNeuroid);
|
||||
// Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
}
|
||||
|
||||
AddToLayer(currentLayer, selectedNucleus);
|
||||
this.layers.Add(currentLayer);
|
||||
// Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
|
||||
|
||||
layerIx++;
|
||||
currentLayer = new() { ix = layerIx };
|
||||
|
||||
if (selectedNucleus.synapses != null) {
|
||||
foreach (Synapse synapse in selectedNucleus.synapses) {
|
||||
IReceptor input = synapse.nucleus;
|
||||
AddToLayer(currentLayer, input);
|
||||
// Debug.Log($"layer {layerIx} nucleus {input.name}");
|
||||
}
|
||||
}
|
||||
if (currentLayer.neuroids.Count > 0) {
|
||||
this.layers.Add(currentLayer);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddToLayer(NeuroidLayer layer, IReceptor nucleus) {
|
||||
if (nucleus == null)
|
||||
return;
|
||||
layer.neuroids.Add(nucleus);
|
||||
//nucleus.layerIx = layer.ix;
|
||||
// Store its position
|
||||
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
|
||||
neuroidPositions[nucleus] = neuroidPosition;
|
||||
|
||||
}
|
||||
|
||||
void OnMouseDown(MouseDownEvent e) {
|
||||
if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); }
|
||||
}
|
||||
void OnMouseMove(MouseMoveEvent e) {
|
||||
if (draggingCanvas) {
|
||||
var delta = e.mousePosition - lastMouse;
|
||||
pan += delta;
|
||||
//content.style.left = pan.x;
|
||||
//content.style.top = pan.y;
|
||||
lastMouse = e.mousePosition;
|
||||
}
|
||||
}
|
||||
void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; }
|
||||
|
||||
void OnIMGUI() {
|
||||
if (currentNucleus == null)
|
||||
return;
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedBrain.Update();
|
||||
|
||||
Handles.BeginGUI();
|
||||
DrawGraph();
|
||||
Handles.EndGUI();
|
||||
|
||||
}
|
||||
|
||||
private void DrawGraph() {
|
||||
float size = 20;
|
||||
Vector3 position = new(150, 210, 0);
|
||||
|
||||
DrawReceivers(this.currentNucleus, position, size);
|
||||
DrawSynapses(this.currentNucleus, position, size);
|
||||
|
||||
// Draw selected Nucleus
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
|
||||
DrawNucleus(this.currentNucleus, position, this.currentNucleus.outputValue.magnitude, 20);
|
||||
}
|
||||
|
||||
private void DrawReceivers(INucleus nucleus, Vector3 parentPos, float size) {
|
||||
int nodeCount = nucleus.receivers.Count;
|
||||
|
||||
// Determine the maximum value in this layer
|
||||
// This is used to 'scale' the output value colors of the nuclei
|
||||
float maxValue = 0;
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
if (receiver is Neuroid neuroid) {
|
||||
float value = neuroid.outputValue.magnitude;
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the spacing of the nuclei in the layer
|
||||
float spacing = 400f / nodeCount;
|
||||
float margin = 10 + spacing / 2;
|
||||
|
||||
int row = 0;
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
INucleus receiverNucleus = receiver;
|
||||
if (receiverNucleus == null)
|
||||
continue;
|
||||
|
||||
Vector3 pos = new(50, margin + row * spacing, 0.0f);
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawLine(parentPos, pos);
|
||||
|
||||
DrawNucleus(receiverNucleus, pos, maxValue, size);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSynapses(INucleus nucleus, Vector3 parentPos, float size) {
|
||||
int nodeCount = nucleus.synapses.Count;
|
||||
|
||||
// Determine the maximum value in this layer
|
||||
// This is used to 'scale' the output value colors of the nuclei
|
||||
float maxValue = 0;
|
||||
foreach (Synapse receiver in nucleus.synapses) {
|
||||
if (receiver.nucleus is Neuroid neuroid) {
|
||||
float value = neuroid.outputValue.magnitude;
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the spacing of the nuclei in the layer
|
||||
float spacing = 400f / nodeCount;
|
||||
float margin = 10 + spacing / 2;
|
||||
|
||||
int row = 0;
|
||||
List<PercepteiArray> drawnArrays = new();
|
||||
foreach (Synapse synapse in nucleus.synapses) {
|
||||
Vector3 pos = new(250, margin + row * spacing, 0.0f);
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawLine(parentPos, pos);
|
||||
// if (synapse.nucleus is Perceptoid perceptoid && perceptoid.array != null) {
|
||||
// // if (drawnArrays.Contains(perceptoid.array))
|
||||
// // // We already drawn this array
|
||||
// // continue;
|
||||
|
||||
// drawnArrays.Add(perceptoid.array);
|
||||
// DrawArray(perceptoid.array, pos, size);
|
||||
// }
|
||||
// else {
|
||||
|
||||
DrawNucleus(synapse.nucleus, pos, maxValue, size);
|
||||
row++;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size) {
|
||||
if (nucleus.isSleeping)
|
||||
Handles.color = Color.darkRed;
|
||||
else {
|
||||
if (Application.isPlaying) {
|
||||
float brightness = nucleus.outputValue.magnitude / maxValue;
|
||||
Handles.color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
else
|
||||
Handles.color = Color.black;
|
||||
}
|
||||
Handles.DrawSolidDisc(position, Vector3.forward, size);
|
||||
|
||||
Handles.color = Color.white;
|
||||
// Position the label in front of the disc
|
||||
Vector3 labelPosition = position + (Vector3.forward * 0.1f);
|
||||
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
if (nucleus is Perceptoid perceptoid) {
|
||||
if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0)
|
||||
perceptoid.array = new PercepteiArray(perceptoid);
|
||||
|
||||
if (perceptoid.array.perceptei.Length > 1) {
|
||||
Handles.Label(labelPosition, perceptoid.array.perceptei.Length.ToString(), style);
|
||||
}
|
||||
}
|
||||
|
||||
style.alignment = TextAnchor.UpperCenter;
|
||||
Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||
Handles.Label(labelPos, nucleus.name, style);
|
||||
|
||||
Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2);
|
||||
int id = GUIUtility.GetControlID(FocusType.Passive);
|
||||
Event e = Event.current;
|
||||
EventType et = e.GetTypeForControl(id);
|
||||
if (e != null && neuronRect.Contains(e.mousePosition)) {
|
||||
// Process Hover
|
||||
HandleMouseHover(nucleus, neuronRect);
|
||||
// Process click
|
||||
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||
// Consume the event so the scene doesn't also handle it
|
||||
e.Use();
|
||||
HandleClicked(nucleus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawArray(PercepteiArray array, Vector3 position, float size) {
|
||||
Vector3 offset = new(size / 4, size / 4, 0);
|
||||
Handles.color = Color.black;
|
||||
Handles.DrawSolidDisc(position, Vector3.forward, size);
|
||||
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold
|
||||
};
|
||||
Handles.Label(position, array.perceptei.Length.ToString(), style);
|
||||
Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis
|
||||
Handles.Label(labelPos, array.name, style);
|
||||
|
||||
// To do: add HandleClick (see above) to expand the array
|
||||
}
|
||||
|
||||
private void HandleMouseHover(IReceptor nucleus, Rect rect) {
|
||||
GUIContent tooltip;
|
||||
if (nucleus is Perceptoid perceptoid) {
|
||||
if (perceptoid.receptor != null) {
|
||||
tooltip = new(
|
||||
$"{perceptoid.name}" +
|
||||
// $"\nType {perceptoid.receptor.thingType}" +
|
||||
$" Thing {perceptoid.thingId}" +
|
||||
$"\nValue: {nucleus.outputValue}");
|
||||
}
|
||||
else {
|
||||
tooltip = new(
|
||||
$"{perceptoid.name}" +
|
||||
$"\nThing {perceptoid.thingId}" +
|
||||
$"\nValue: {nucleus.outputValue}");
|
||||
}
|
||||
}
|
||||
else if (nucleus is INucleus n) {
|
||||
tooltip = new(
|
||||
$"{nucleus.name}" +
|
||||
$"\nsynapse count {n.synapses.Count}" +
|
||||
$"\nValue: {nucleus.outputValue}");
|
||||
}
|
||||
else {
|
||||
tooltip = new(
|
||||
$"{nucleus.name}" +
|
||||
$"\nValue: {nucleus.outputValue}");
|
||||
}
|
||||
|
||||
Vector2 mousePosition = Event.current.mousePosition;
|
||||
|
||||
// Display tooltip with some offset
|
||||
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
|
||||
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
|
||||
|
||||
GUI.Box(tooltipRect, tooltip);
|
||||
}
|
||||
|
||||
private void HandleClicked(IReceptor nucleus) {
|
||||
if (nucleus is INucleus n) {
|
||||
this.currentNucleus = n;
|
||||
BuildLayers();
|
||||
}
|
||||
}
|
||||
|
||||
void DrawInspector(VisualElement inspectorContainer) {
|
||||
if (inspectorContainer == null)
|
||||
return;
|
||||
|
||||
inspectorContainer.Clear();
|
||||
if (this.currentNucleus == null)
|
||||
return;
|
||||
|
||||
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
|
||||
SerializedObject so = new(currentWrapper);
|
||||
IMGUIContainer container = new(() => {
|
||||
if (so.targetObject == null)
|
||||
return;
|
||||
so.Update();
|
||||
|
||||
if (this.currentNucleus == null)
|
||||
return;
|
||||
|
||||
this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name);
|
||||
if (this.currentNucleus is Perceptoid perceptoid) {
|
||||
// perceptoid.receptor.thingType = EditorGUILayout.IntField("Thing Type", perceptoid.receptor.thingType);
|
||||
|
||||
if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0)
|
||||
perceptoid.array = new PercepteiArray(perceptoid);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.IntField("Array size", perceptoid.array.perceptei.Length);
|
||||
if (GUILayout.Button("Add"))
|
||||
perceptoid.array.AddPerceptoid();
|
||||
if (GUILayout.Button("Del"))
|
||||
perceptoid.array.RemovePerceptoid();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
else if (this.currentNucleus is Neuroid neuroid) {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150));
|
||||
if (neuroid.curveMax > 0)
|
||||
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax));
|
||||
else
|
||||
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax));
|
||||
neuroid.curvePreset = (Neuroid.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
EditorGUILayout.FloatField("Output", this.currentNucleus.outputValue.magnitude);
|
||||
else
|
||||
EditorGUILayout.LabelField(" ");
|
||||
|
||||
if (this.currentNucleus.synapses.Count > 0) {
|
||||
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
|
||||
foreach (Synapse synapse in synapses) {
|
||||
if (synapse.nucleus != null) {
|
||||
EditorGUILayout.Space();
|
||||
|
||||
EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
|
||||
if (Application.isPlaying)
|
||||
EditorGUILayout.FloatField(synapse.nucleus.name, synapse.nucleus.outputValue.magnitude * synapse.weight);
|
||||
else {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(synapse.nucleus.name);
|
||||
// if (synapse.nucleus is Perceptoid perceptoid) {
|
||||
// if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) {
|
||||
// perceptoid.array = new PercepteiArray(perceptoid);
|
||||
// }
|
||||
// EditorGUILayout.IntField(perceptoid.array.perceptei.Length);
|
||||
// if (GUILayout.Button("Add"))
|
||||
// perceptoid.array.AddPerceptoid();
|
||||
// }
|
||||
if (GUILayout.Button("Disconnect"))
|
||||
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight);
|
||||
EditorGUI.indentLevel--;
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
ConnectNucleus(this.currentNucleus);
|
||||
if (GUILayout.Button("Add Input Neuron"))
|
||||
AddInputNeuron(this.currentNucleus);
|
||||
if (GUILayout.Button("Add Input Perceptoid"))
|
||||
AddPerceptoid(this.currentNucleus);
|
||||
if (GUILayout.Button("Add Input Cluster"))
|
||||
AddCluster(this.currentNucleus);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Delete this neuron"))
|
||||
DeleteNeuron(this.currentNucleus);
|
||||
|
||||
//DisconnectNucleus(this.currentNucleus);
|
||||
|
||||
if (this.gameObject != null) {
|
||||
Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
|
||||
Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow);
|
||||
}
|
||||
});
|
||||
|
||||
inspectorContainer.Add(container);
|
||||
}
|
||||
|
||||
protected virtual void AddInputNeuron(INucleus nucleus) {
|
||||
Neuroid newNeuroid = new(this.brain.cluster, "New neuron");
|
||||
newNeuroid.AddReceiver(nucleus);
|
||||
this.currentNucleus = newNeuroid;
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
protected virtual void DeleteNeuron(INucleus nucleus) {
|
||||
if (nucleus == null)
|
||||
return;
|
||||
if (nucleus.cluster != null)
|
||||
this.currentNucleus = nucleus.cluster.output;
|
||||
foreach (INucleus receiver in nucleus.receivers) {
|
||||
if (receiver != null) {
|
||||
this.currentNucleus = receiver;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Nucleus.Delete(nucleus);
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
protected virtual void AddPerceptoid(INucleus nucleus) {
|
||||
Perceptoid newPerceptoid = new(this.brain, 0, "New Perceptoid");
|
||||
newPerceptoid.AddReceiver(nucleus);
|
||||
this.currentNucleus = newPerceptoid;
|
||||
BuildLayers();
|
||||
}
|
||||
|
||||
protected virtual void AddCluster(INucleus nucleus) {
|
||||
BrainPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster");
|
||||
}
|
||||
|
||||
private void OnClusterPicked(INucleus nucleus, NanoBrain brain) {
|
||||
NanoBrain brainInstance = Instantiate(brain);
|
||||
brainInstance.AddReceiver(nucleus);
|
||||
}
|
||||
|
||||
protected virtual void ConnectNucleus(INucleus nucleus) {
|
||||
if (this.currentNucleus.cluster == null)
|
||||
return;
|
||||
|
||||
IEnumerable<string> synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name);
|
||||
//IEnumerable<string> perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei);
|
||||
IEnumerable<string> nuclei = this.currentNucleus.cluster.nuclei.Select(i => i.name).Except(synapseNuclei);
|
||||
//string[] names = perceptei.Concat(nuclei).ToArray();
|
||||
string[] names = nuclei.ToArray();
|
||||
int selectedIndex = -1;
|
||||
selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names);
|
||||
if (selectedIndex >= 0) {
|
||||
// if (selectedIndex < perceptei.Count()) {
|
||||
// Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex];
|
||||
// n.AddReceiver(this.currentNucleus);
|
||||
// }
|
||||
// else {
|
||||
// Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()];
|
||||
// n.AddReceiver(this.currentNucleus);
|
||||
// }
|
||||
INucleus n = this.currentNucleus.cluster.nuclei[selectedIndex];
|
||||
n.AddReceiver(this.currentNucleus);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void DisconnectNucleus(Nucleus nucleus) {
|
||||
if (this.currentNucleus.cluster == null)
|
||||
return;
|
||||
string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray();
|
||||
int selectedIndex = -1;
|
||||
selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names);
|
||||
//if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) {
|
||||
if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.cluster.nuclei.Count) {
|
||||
Synapse synapse = this.currentNucleus.synapses[selectedIndex];
|
||||
synapse.nucleus.RemoveReceiver(this.currentNucleus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Start
|
||||
|
||||
#region Update
|
||||
|
||||
private void UpdateLayout(float containerWidth) {
|
||||
if (containerWidth > 600f) {
|
||||
mainContainer.style.flexDirection = FlexDirection.Row;
|
||||
inspectorContainer.style.width = 300; // fixed sidebar width
|
||||
inspectorContainer.style.flexGrow = 0;
|
||||
}
|
||||
else {
|
||||
mainContainer.style.flexDirection = FlexDirection.Column;
|
||||
inspectorContainer.style.width = Length.Percent(100); // full width below
|
||||
inspectorContainer.style.flexDirection = FlexDirection.Column;
|
||||
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Update
|
||||
}
|
||||
|
||||
/*
|
||||
public class NeuroidLayer {
|
||||
public int ix = 0;
|
||||
public List<INucleus> neuroids = new();
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
public class GraphNodeWrapper : ScriptableObject {
|
||||
// expose fields that map to GraphNode
|
||||
//public string title;
|
||||
public Vector2 position;
|
||||
INucleus node;
|
||||
NanoBrain graph; // needed to write back and mark dirty
|
||||
|
||||
public GraphNodeWrapper Init(INucleus node, NanoBrain graphAsset) {
|
||||
this.node = node;
|
||||
this.graph = graphAsset;
|
||||
//this.title = " A " + node.name;
|
||||
//position = node.position;
|
||||
return this;
|
||||
}
|
||||
void OnValidate() {
|
||||
if (node != null) {
|
||||
//node.name = title;
|
||||
//node.position = position;
|
||||
#if UNITY_EDITOR
|
||||
if (graph != null)
|
||||
UnityEditor.EditorUtility.SetDirty(graph);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c96ad47c3d4498640b52630789e38573
|
||||
@ -1,102 +0,0 @@
|
||||
/*
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Passer/NanoBrain")]
|
||||
public class NanoBrain : ScriptableObject, ISerializationCallbackReceiver {
|
||||
public List<Neuroid> nuclei = new();
|
||||
public List<Perceptoid> perceptei = new();
|
||||
public List<Receptor> receptors = new();
|
||||
|
||||
|
||||
// This is probably always the first element in the nuclei list...
|
||||
[System.NonSerialized]
|
||||
public Nucleus output;
|
||||
public int rootId;
|
||||
|
||||
public NanoBrain() {
|
||||
// this.cluster = new();
|
||||
// this.output = new Neuroid(this.cluster, "Root");
|
||||
}
|
||||
|
||||
public Cluster cluster;
|
||||
|
||||
public void AddReceiver(INucleus receiver) {
|
||||
output.AddReceiver(receiver);
|
||||
}
|
||||
|
||||
public Neuroid AddNeuron(string name) {
|
||||
Neuroid neuroid = new(this.cluster, name);
|
||||
return neuroid;
|
||||
}
|
||||
|
||||
public void UpdateNuclei() {
|
||||
foreach (Nucleus nucleus in nuclei)
|
||||
nucleus.IncreaseAge();
|
||||
foreach (Perceptoid perception in perceptei)
|
||||
perception.IncreaseAge();
|
||||
}
|
||||
|
||||
public void OnBeforeSerialize() {
|
||||
if (output != null) {
|
||||
this.rootId = output.id;
|
||||
}
|
||||
}
|
||||
public void OnAfterDeserialize() {
|
||||
try {
|
||||
foreach (Nucleus nucleus in this.nuclei.ToArray()) {
|
||||
if (this.rootId == nucleus.id)
|
||||
this.output = nucleus;
|
||||
nucleus.Rebuild(this);
|
||||
}
|
||||
|
||||
foreach (Perceptoid perceptoid in this.perceptei.ToArray())
|
||||
perceptoid.Rebuild(this);
|
||||
}
|
||||
catch (System.Exception) { }
|
||||
if (this.cluster != null)
|
||||
this.cluster.GarbageCollection();
|
||||
}
|
||||
|
||||
|
||||
public void GarbageCollection() {
|
||||
HashSet<INucleus> visitedNuclei = new();
|
||||
MarkNuclei(visitedNuclei, this.output);
|
||||
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
|
||||
this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false);
|
||||
this.perceptei.RemoveAll(perceptoid => visitedNuclei.Contains(perceptoid) == false);
|
||||
}
|
||||
|
||||
public void MarkNuclei(HashSet<INucleus> visitedNuclei, INucleus nucleus) {
|
||||
if (nucleus is null)
|
||||
return;
|
||||
|
||||
if (nucleus.brain == null)
|
||||
nucleus.brain = this;
|
||||
|
||||
visitedNuclei.Add(nucleus);
|
||||
if (nucleus.synapses != null) {
|
||||
HashSet<Synapse> visitedSynapses = new();
|
||||
foreach (Synapse synapse in nucleus.synapses) {
|
||||
if (synapse != null && synapse.nucleus != null) {
|
||||
visitedSynapses.Add(synapse);
|
||||
MarkNuclei(visitedNuclei, synapse.nucleus);
|
||||
}
|
||||
}
|
||||
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
|
||||
}
|
||||
if (nucleus.receivers != null) {
|
||||
HashSet<Receiver> visitedReceivers = new();
|
||||
foreach (Receiver receiver in nucleus.receivers) {
|
||||
if (receiver != null && receiver.nucleus != null) {
|
||||
visitedReceivers.Add(receiver);
|
||||
visitedNuclei.Add(receiver.nucleus);
|
||||
}
|
||||
}
|
||||
nucleus.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36081359186edfec998d891a1feeb17b
|
||||
Loading…
x
Reference in New Issue
Block a user