NanoBrain-unitypackage/Editor/ClusterEditor.cs
Pascal Serrarens 4363079d43 Squashed 'NanoBrain/' changes from 7ef8e42..ec3b1d4
ec3b1d4 Completed cluster documentation
348fee3 Update .gitea/workflows/copy_documentation.yml
911e52f Update .gitea/workflows/copy_documentation.yml
d472790 Update .gitea/workflows/copy_documentation.yml
b87f40f Trying to get the workflow running...10
2b0db4f Trying to get the workflow running...9
927fd6d Trying to get the workflow running...8
176f399 Trying to get the workflow running...7
3c841c7 Trying to get the workflow running...6
5c798c2 Trying to get the workflow running...5
30b25a1 Trying to get the workflow running...4
5edf019 Trying to get the workflow running...3
587cf82 Trying to get the workflow running...2
a1d3aa7 Trying to get the workflow running...1
97ec277 Removed LinearAlgebra, first setup webserver copy workflow
5827396 Fixed documentation links
ce19335 Added Documentation
da370bb Improvements
32b5885 Multi smell works
33ea14b Single smell works
a651ec6 Add neuron property drawer
baa7def Pheromones WIP
551b4d9 Improve ant walking speed
7187f61 Ant is walking again
c78722a Make it work again
2ff550c Removed clusterPrefab property
2ef67fe Cleanup and fix connect neuron
a9a0072 Merge commit 'dd326823a8256f3ddb808e071d98c4aede72e410'
22ee17c Insect rig improvements
b6a3bc1 Added insect body parts
517e738 Merge commit '4ae9a15fc61f386b96ce0f7b440780f562d7dc68'
033ddf4 Merge commit '05fd588f9bc41d84113d410a2ca992f1a2ee66e0'
ef700c0 Merge commit '3f8716794ad9d685cfb9ed9dd230eb31cd8df10f'
7d5e157 Added NanoBrain namespace
f138201 Merge commit '611055cdcd58b01f2f19991ad35eb8fe8e573ebb'
1c4d361 Merge commit '9fcbaa5bf84f91680d24b56dbf114bcb97de4aee'
0f83945 Added NanoBrain subtree
6f398ad Merge commit '8e87e4ea77308b51c3691bdad96e7f9707952821' as 'NanoBrain'
587f104 Move out non-subtree NanoBrain
fc581a0 cleanup & documentation
837c5ce WIP Physics based walking
63486d1 The ant does it ant things!
ce8e476 Added sample assets...
88d5eb5 Placing home pheromones
481829c Ensure model follows target in editor
018c99d Any walks
e709ea4 Steps to get it working
c1dcc83 Initial Ant setup
af2fa77 Merge commit '04ca8dda0793476a59fc06f1958453730a99c105' as 'NanoBrain'
04ca8dd Squashed 'NanoBrain/' content from commit b3423b9
d9ba98d WIP: Initial scripts
2219e98 Initial commit

git-subtree-dir: NanoBrain
git-subtree-split: ec3b1d46ab2b9f332a8ae63589b09c3fb6fb1b1a
2026-05-07 15:25:20 +02:00

592 lines
28 KiB
C#

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace NanoBrain {
[CustomEditor(typeof(ClusterPrefab))]
public class ClusterEditor : ClusterViewer {
public override VisualElement CreateInspectorGUI() {
ClusterPrefab prefab = target as ClusterPrefab;
if (prefab != null)
prefab.EnsureInitialization();
serializedObject.Update();
VisualElement root = new();
CreateEditor(root, prefab, null);
serializedObject.ApplyModifiedProperties();
return root;
}
public GraphView CreateEditor(VisualElement root, ClusterPrefab cluster, GameObject gameObject) {
root.style.paddingLeft = 0;
root.style.paddingRight = 0;
root.style.paddingTop = 0;
root.style.paddingBottom = 0;
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
VisualElement mainContainer = new() {
style = {
flexDirection = FlexDirection.Row,
}
};
GraphEditor graphContainer = new(cluster);
graphContainer.style.flexShrink = 0;
graphContainer.style.width = 300;
graphContainer.style.overflow = Overflow.Hidden;
VisualElement inspectorContainer = new() {
name = "inspector",
style = {
minHeight = 450,
width = 300,
flexGrow = 0,
flexDirection = FlexDirection.Row,
}
};
mainContainer.Add(graphContainer);
mainContainer.Add(inspectorContainer);
root.Add(mainContainer);
graphContainer.SetGraph(gameObject, inspectorContainer);
return graphContainer;
}
public class GraphEditor : GraphView {
protected ClusterPrefab prefab;
//protected Nucleus currentPrefabNucleus;
protected override Nucleus currentNucleus {
get => base.currentNucleus;
set {
base.currentNucleus = value;
// this.currentPrefabNucleus = value != null ? this.prefab.GetNucleus(value.name) : null;
}
}
public GraphEditor(ClusterPrefab prefab) : base(prefab.cluster.defaultOutput.parent) {
this.prefab = prefab;
// In a Prefab editor, no instance exists but we need it for the ClusterViewer.
// So we create a temporary instance
//this.currentCluster = new(prefab);
this.currentCluster = prefab.cluster;
this.currentCluster.Refresh();
}
public void SetGraph(GameObject gameObject, VisualElement inspectorContainer) {
this.gameObject = gameObject;
if (Application.isPlaying == false)
this.serializedBrain = new SerializedObject(this.prefab);
this.selectedOutput = this.currentCluster.defaultOutput;
this.currentNucleus = this.selectedOutput;
//this.currentCluster = this.currentNucleus.parent;
Rebuild(inspectorContainer);
// if (outputsPopup != null)
// OnOutputChanged(outputsPopup.choices[0]);
}
private void Rebuild(VisualElement inspectorContainer) {
if (this.currentNucleus == null) {
inspectorContainer.Clear();
return;
}
string path = AssetDatabase.GetAssetPath(this.prefab); // or known path
this.prefabAsset = AssetDatabase.LoadAssetAtPath<ClusterPrefab>(path);
if (this.prefabAsset == null) {
// create in memory save if it doesn't exist
this.prefabAsset = CreateInstance<ClusterPrefab>();
//Debug.LogError("Cluster Prefab is not found on disk");
}
// DrawInspector(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(prefabAsset);
// foreach (Nucleus nucleus in this.prefab.cluster.nuclei) {
// nucleus.Initialize();
// }
this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so));
inspectorContainer.Add(inspectorIMGUIContainer);
}
#region Inspector
private VisualElement inspectorIMGUIContainer;
private bool showSynapses = true;
private bool showActivation = true;
protected bool breakOnWake = false;
protected bool trace = false;
void InspectorHandler(SerializedObject serializedObject) {
bool anythingChanged = false;
if (serializedObject == null || serializedObject.targetObject == null)
return;
serializedObject.Update();
GUIStyle boldTextFieldStyle = new(EditorStyles.textField) {
fontStyle = FontStyle.Bold
};
if (this.currentNucleus == null) {
OutputsInspector(ref anythingChanged);
return;
}
else {
GUIStyle headerStyle = new(EditorStyles.boldLabel) {
alignment = TextAnchor.MiddleLeft,
margin = new RectOffset(10, 0, 4, 4)
};
// Nucleus type
string nucleusType = this.currentNucleus.GetType().Name;
GUILayout.Label(nucleusType, headerStyle);
// Nucleus name
string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle);
if (newName != this.currentNucleus.name) {
this.currentNucleus.name = newName;
anythingChanged = true;
}
// Current output value
if (Application.isPlaying) {
if (currentNucleus is Neuron currentNeuron1) {
GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString());
EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude);
}
else
EditorGUILayout.LabelField(" ");
}
else
EditorGUILayout.LabelField(" ");
// Memory cell
if (this.currentNucleus is MemoryCell memory)
MemoryCellInspector(memory, ref anythingChanged);
// Cluster
else if (this.currentNucleus is Cluster cluster)
ClusterInspector(cluster, ref anythingChanged);
// Other
else
NucleusInspector(this.currentNucleus, ref anythingChanged);
if (GUILayout.Button("Delete"))
DeleteNucleus(this.currentNucleus);
}
serializedObject.ApplyModifiedProperties();
if (anythingChanged) {
EditorUtility.SetDirty(prefabAsset);
AssetDatabase.SaveAssets();
}
}
protected void OutputsInspector(ref bool anythingChanged) {
GUIStyle headerStyle = new(EditorStyles.boldLabel) {
alignment = TextAnchor.MiddleLeft,
margin = new RectOffset(10, 0, 4, 4)
};
GUILayout.Label("Outputs", headerStyle);
bool connecting = GUILayout.Button("Add Output Neuron");
if (connecting) {
Nucleus newOutput = new Neuron(this.currentCluster, "New Output");
this.currentCluster.Refresh();
this.currentNucleus = newOutput;
this.selectedOutput = this.currentNucleus;
}
}
protected void MemoryCellInspector(MemoryCell memoryCell, ref bool anythingChanged) {
memoryCell.staticMemory = EditorGUILayout.Toggle("Static Memory", memoryCell.staticMemory);
NucleusInspector(memoryCell, ref anythingChanged);
}
protected void ClusterInspector(Cluster cluster, ref bool anythingChanged) {
EditorGUILayout.BeginHorizontal();
int instanceCount = cluster.instanceCount;
if (instanceCount <= 1) {
if (cluster.instances != null && cluster.instances.Length > 1)
instanceCount = cluster.instances.Count();
else
instanceCount = 1;
}
EditorGUILayout.IntField("Instances", instanceCount, GUILayout.MinWidth(150));
if (GUILayout.Button("Add")) {
Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name);
cluster.AddInstance();
anythingChanged = true;
}
if (GUILayout.Button("Del")) {
Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name);
cluster.RemoveInstance();
anythingChanged = true;
}
EditorGUILayout.EndHorizontal();
// if (GUILayout.Button("Reimport Cluster"))
// ReimportCluster(cluster);
}
protected void NucleusInspector(Nucleus nucleus, ref bool anythingChanged) {
SynapsesInspector(ref anythingChanged);
ActivationInspector(ref anythingChanged);
EditorGUILayout.Space();
breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake);
if (breakOnWake && this.currentNucleus is Neuron currentNeuron) {
if (currentNeuron.isSleeping == false)
Debug.Break();
// trace = EditorGUILayout.Toggle("Trace", trace);
// currentNeuron.trace = trace;
}
}
protected void SynapsesInspector(ref bool anythingChanged) {
showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses");
if (showSynapses) {
if (this.currentNucleus is Neuron neuron2) {
Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator);
anythingChanged |= newCombinator != neuron2.combinator;
neuron2.combinator = newCombinator;
EditorGUIUtility.wideMode = true;
float previousLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = 100;
Vector3 newBias = EditorGUILayout.Vector3Field("Bias", neuron2.bias);
if (newBias != neuron2.bias) {
anythingChanged |= newBias != neuron2.bias;
neuron2.bias = newBias;
}
EditorGUIUtility.labelWidth = previousLabelWidth;
}
Nucleus[] array = null;
int elementIx = -1;
if (this.currentNucleus is Neuron currentNeuron && currentNeuron.synapses.Count > 0) {
Synapse[] synapses = currentNeuron.synapses.ToArray();
foreach (Synapse synapse in synapses) {
if (synapse.neuron == null)
continue;
if (array != null) {
if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) {
int thisElementIx = Cluster.GetNucleusIndex(iCluster.nuclei, synapse.neuron);
if (thisElementIx == elementIx)
continue;
else
elementIx = thisElementIx;
}
if (array.Contains(synapse.neuron))
continue;
else if (array.Contains(synapse.neuron.parent))
continue;
}
else {
if (synapse.neuron.parent is Cluster iReceptor) {
array = iReceptor.instances;
if (iReceptor is Cluster iCluster)
elementIx = Cluster.GetNucleusIndex(iCluster.nuclei, synapse.neuron);
}
}
EditorGUILayout.Space();
if (Application.isPlaying) {
if (synapse.neuron is Neuron synapseNeuron) {
Vector3 value = synapseNeuron.outputValue * synapse.weight;
GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString());
EditorGUILayout.FloatField(synapseValueLabel, synapseNeuron.outputMagnitude);
}
}
else {
EditorGUILayout.BeginHorizontal();
if (synapse.neuron.parent != this.currentNucleus.parent) {
// If it is a different cluster
GUIStyle labelStyle = new(GUI.skin.label);
float labelWidth = 200;
if (synapse.neuron.parent != null) {
labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.name}.")).x;
GUILayout.Label($"{synapse.neuron.parent.name}", GUILayout.Width(labelWidth));
}
string[] options = synapse.neuron.parent.nuclei.Select(n => n.name).ToArray();
int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name);
int newIndex = EditorGUILayout.Popup(selectedIndex, options);
if (newIndex != selectedIndex) {
Neuron newNeuron = synapse.neuron.parent.nuclei[newIndex] as Neuron;
ChangeSynapse(synapse, newNeuron);
}
}
else
GUILayout.Label(synapse.neuron.name);
bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80));
if (disconnecting) {
synapse.neuron.RemoveReceiver(this.currentNucleus);
this.currentCluster.Refresh();
anythingChanged = true;
}
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel++;
float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight);
if (newWeight != synapse.weight) {
synapse.weight = newWeight;
anythingChanged = true;
}
EditorGUI.indentLevel--;
}
}
EditorGUILayout.Space();
anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus);
anythingChanged |= AddSynapse(this.prefab, this.currentNucleus);
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
protected void ActivationInspector(ref bool anythingChanged) {
EditorGUILayout.Space();
showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation");
if (showActivation) {
if (this.currentNucleus is Neuron neuron) {
if (this.currentNucleus is not MemoryCell) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Activation Curve", GUILayout.MinWidth(60));
if (neuron.curveMax > 0)
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax), GUILayout.Width(40));
else
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax), GUILayout.Width(40));
Neuron.ActivationType newPreset = (Neuron.ActivationType)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.MinWidth(50));
anythingChanged |= newPreset != neuron.curvePreset;
neuron.curvePreset = newPreset;
EditorGUILayout.EndHorizontal();
}
// if (neuron is Receptor receptor2) {
// if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0)
// receptor2.array = new NucleusArray(neuron);
// }
}
EditorGUILayout.Space();
}
EditorGUILayout.EndFoldoutHeaderGroup();
}
#region Synapses
protected virtual void AddInput(Nucleus.Type selectedType, Nucleus nucleus) {
switch (selectedType) {
case Nucleus.Type.Neuron:
AddNeuronInput(nucleus);
break;
case Nucleus.Type.MemoryCell:
AddMemoryCellInput(nucleus);
break;
case Nucleus.Type.Cluster:
AddClusterInput(nucleus);
break;
default:
break;
}
}
protected virtual void AddNeuronInput(Nucleus nucleus) {
Neuron newNeuron = new(this.currentCluster, "New Neuron");
//Neuron newNeuroid = new(this.prefab.cluster, "New neuron");
newNeuron.AddReceiver(nucleus);
this.currentNucleus = newNeuron;
}
protected virtual void AddMemoryCellInput(Nucleus nucleus) {
MemoryCell newMemory = new(this.prefab.cluster, "New memory cell");
newMemory.AddReceiver(nucleus);
this.currentNucleus = newMemory;
}
protected virtual void AddClusterInput(Nucleus nucleus) {
ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster");
}
private void OnClusterPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) {
Cluster subclusterInstance = new(selectedPrefab, this.currentCluster);
subclusterInstance.defaultOutput.AddReceiver(nucleus);
}
// private void ReimportCluster(Cluster subCluster) {
// if (subCluster.siblingClusters == null || subCluster.siblingClusters.Length <= 0) {
// Cluster reimportedCluster = new(subCluster.prefab, this.prefab);
// subCluster.MoveReceivers(reimportedCluster);
// // subcluster should be garbage now...
// this.currentNucleus = reimportedCluster;
// }
// else {
// this.currentNucleus = null;
// List<Cluster> newSiblingsList = new();
// foreach (Cluster sibling in subCluster.siblingClusters) {
// Cluster reimportedCluster = new(sibling.prefab, this.prefab) {
// name = sibling.name
// };
// sibling.MoveReceivers(reimportedCluster);
// newSiblingsList.Add(reimportedCluster);
// // make the first reimportedCluster the new current nucleus
// this.currentNucleus ??= reimportedCluster;
// }
// Cluster[] newSiblings = newSiblingsList.ToArray();
// foreach (Cluster sibling in newSiblings)
// sibling.siblingClusters = newSiblings;
// }
// }
int selectedConnectNucleus = -1;
// Connect to another nucleus
protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) {
if (cluster == null)
return false;
Neuron currentNeuron = this.currentNucleus as Neuron;
IEnumerable<Nucleus> synapseNuclei = currentNeuron.synapses
.Where(synapse => synapse.neuron != null)
.Select(synapse => synapse.neuron);
IEnumerable<Nucleus> nuclei = cluster.cluster.nuclei
.Except(synapseNuclei);
IEnumerable<string> nucleiNames = nuclei
.Select(n => {
int idx = n.name.IndexOf(':');
return idx < 0 ? n.name : n.name[..idx];
})
.Distinct();
string[] names = nucleiNames.ToArray();
EditorGUILayout.BeginHorizontal();
selectedConnectNucleus = EditorGUILayout.Popup(selectedConnectNucleus, names);
bool connecting = GUILayout.Button("Connect", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
if (connecting) {
Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus);
// if (nucleus is Cluster subCluster) {
// subCluster.AddArrayReceiver(this.currentNucleus);
// }
// else
if (nucleus is Neuron neuron)
neuron.AddReceiver(this.currentNucleus);
this.currentCluster.Refresh();
}
return connecting;
}
protected virtual void DeleteNucleus(Nucleus nucleus) {
if (nucleus == null)
return;
if (nucleus is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers) {
if (receiver != null) {
this.currentNucleus = receiver;
break;
}
}
}
this.currentCluster.DeleteNucleus(nucleus);//clusterNuclei.Remove(nucleus);
// this.prefab.nuclei.Remove(nucleus);
// Neuron.Delete(nucleus);
this.prefab.cluster.RefreshOutputs();
this.currentNucleus = this.prefab.cluster.defaultOutput;
this.selectedOutput = this.currentNucleus;
}
Nucleus.Type selectedType = Nucleus.Type.None;
protected virtual bool AddSynapse(ClusterPrefab cluster, Nucleus nucleus) {
if (cluster == null)
return false;
EditorGUILayout.BeginHorizontal();
selectedType = (Nucleus.Type)EditorGUILayout.EnumPopup(selectedType);
bool connecting = GUILayout.Button("Add", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
if (connecting) {
AddInput(selectedType, this.currentNucleus);
}
return connecting;
}
protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) {
Neuron synapseNeuron = synapse.neuron;
if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) {
// if (synapse.neuron.parent is ClusterReceptor receptor) {
// // the new nucleus is part of a (cluster) receptor,
// // so we have to change all synapses to this nucleus array elements
// int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron);
// int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus);
// foreach (Nucleus element in receptor.nucleiArray) {
// if (element is not ClusterReceptor clusterReceptor)
// continue;
// // Get the same neuron as the synapse.nucleus in a different element
// // of the ClusterReceptor array
// Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx];
// if (oldElementNucleus is not Neuron oldElementNeuron)
// continue;
// // Get the same neuron as newNucleus in a different element
// // of the ClusterReceptor array
// Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx];
// if (newElementNucleus is not Neuron newElementNeuron)
// continue;
// oldElementNeuron.RemoveReceiver(this.currentNucleus);
// newElementNeuron.AddReceiver(this.currentNucleus);
// // Now find the synapse which pointed to the old Neuron
// // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron);
// // synapseForUpdate.nucleus = newElementNeuron;
// }
// }
// else {
// it is a neuron in a subcluster
synapseNeuron.RemoveReceiver(this.currentNucleus);
newNucleus.AddReceiver(this.currentNucleus);
// }
}
else {
synapseNeuron.RemoveReceiver(this.currentNucleus);
newNucleus.AddReceiver(this.currentNucleus);
}
}
#endregion Synapses
#endregion Inspector
}
}
}