Improve persisting changes

This commit is contained in:
Pascal Serrarens 2026-02-11 11:49:03 +01:00
parent 8c8d5a5a66
commit c1bf54a1cc
2 changed files with 253 additions and 183 deletions

View File

@ -18,9 +18,17 @@ public class ClusterInspector : Editor {
public override VisualElement CreateInspectorGUI() {
ClusterPrefab prefab = target as ClusterPrefab;
string path = AssetDatabase.GetAssetPath(prefab); // or known path
Debug.Log($"{path}");
ClusterPrefab currentWrapper = AssetDatabase.LoadAssetAtPath<ClusterPrefab>(path);
if (currentWrapper == null)
Debug.LogError("CreateInspectorGUI: Cluster Prefab is not found on disk");
if (prefab != null)
prefab.EnsureInitialization();
serializedObject.Update();
VisualElement root = new();
@ -76,7 +84,7 @@ public class ClusterInspector : Editor {
private readonly Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
private bool expandArray = false;
ClusterWrapper currentWrapper;
ClusterPrefab prefabAsset;
readonly PopupField<string> outputsField;
public GraphView(ClusterPrefab prefab) {
@ -168,9 +176,20 @@ public class ClusterInspector : Editor {
return;
}
if (currentWrapper != null)
DestroyImmediate(currentWrapper);
currentWrapper = CreateInstance<ClusterWrapper>().Init(this.currentNucleus, prefab);
// if (currentWrapper != null)
// DestroyImmediate(currentWrapper);
// currentWrapper = CreateInstance<ClusterWrapper>().Init(this.currentNucleus, prefab);
string path = AssetDatabase.GetAssetPath(this.prefab); // or known path
this.prefabAsset = AssetDatabase.LoadAssetAtPath<ClusterPrefab>(path);
if (this.prefabAsset == null) {
// create and save if it doesn't exist
this.prefabAsset = CreateInstance<ClusterPrefab>();
// AssetDatabase.CreateAsset(currentWrapper, "Assets/ClusterPrefab.asset");
// AssetDatabase.SaveAssets();
Debug.LogError("Cluster Prefab is not found on disk");
}
//currentWrapper.Init(this.currentNucleus, prefab);
DrawInspector(inspectorContainer);
}
@ -510,146 +529,164 @@ public class ClusterInspector : Editor {
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;
GUIStyle headerStyle = new(EditorStyles.boldLabel) {
alignment = TextAnchor.MiddleLeft,
margin = new RectOffset(10, 0, 4, 4)
};
GUIStyle boldTextFieldStyle = new(EditorStyles.textField) {
fontStyle = FontStyle.Bold
};
GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle);
if (this.currentNucleus is Neuron neuron1) {
neuron1.type = (Nucleus.Type)EditorGUILayout.EnumPopup(neuron1.type);
}
string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle);
if (newName != this.currentNucleus.name) {
this.currentNucleus.name = newName;
this.prefab.RefreshOutputs();
outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList();
//outputsField.value = newName;
}
if (Application.isPlaying) {
GUIContent nameLabel = new("Output", this.currentNucleus.outputValue.ToString());
EditorGUILayout.FloatField(nameLabel, length(this.currentNucleus.outputValue));
}
else
EditorGUILayout.LabelField(" ");
if (this.currentNucleus is MemoryCell memory) {
memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory);
}
// Synapses
showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses");
if (showSynapses) {
ConnectNucleus(this.prefab, this.currentNucleus);
AddSynapse(this.prefab, this.currentNucleus);
this.currentNucleus.bias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias);
NucleusArray array = null;
if (this.currentNucleus.synapses.Count > 0) {
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
foreach (Synapse synapse in synapses) {
if (synapse.nucleus != null) {
if (array != null) {
if (array.nuclei.Contains(synapse.nucleus))
continue;
}
else {
if (synapse.nucleus.array != null && synapse.nucleus.array.nuclei.Length > 1)
array = synapse.nucleus.array;
}
EditorGUILayout.Space();
if (Application.isPlaying) {
Vector3 value = synapse.nucleus.outputValue * synapse.weight;
GUIContent synapseValueLabel = new(synapse.nucleus.name, synapse.nucleus.outputValue.ToString());
EditorGUILayout.FloatField(synapseValueLabel, length(synapse.nucleus.outputValue));
}
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--;
}
}
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
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.Width(150));
if (neuron.curveMax > 0)
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax));
else
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax));
neuron.curvePreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
if (neuron.array == null || neuron.array.nuclei == null || neuron.array.nuclei.Count() == 0)
neuron.array = new NucleusArray(neuron);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.IntField("Array size", neuron.array.nuclei.Count());
if (GUILayout.Button("Add"))
neuron.array.AddNucleus(this.prefab);
if (GUILayout.Button("Del"))
neuron.array.RemoveNucleus();
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space();
}
EditorGUILayout.EndFoldoutHeaderGroup();
if (GUILayout.Button("Delete this neuron"))
DeleteNeuron(this.currentNucleus);
if (this.currentNucleus is Cluster subCluster) {
if (GUILayout.Button("Edit Cluster"))
EditCluster(subCluster);
}
EditorGUILayout.Space();
breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake);
if (breakOnWake) {
if (this.currentNucleus.isSleeping == false)
Debug.Break();
}
trace = EditorGUILayout.Toggle("Trace", trace);
this.currentNucleus.trace = trace;
});
SerializedObject so = new(prefabAsset);
IMGUIContainer container = new(() => InspectorHandler(so));
inspectorContainer.Add(container);
}
void InspectorHandler(SerializedObject serializedObject) {
bool anythingChanged = false;
if (serializedObject == null || serializedObject.targetObject == null)
return;
if (this.currentNucleus == null)
return;
serializedObject.Update();
GUIStyle headerStyle = new(EditorStyles.boldLabel) {
alignment = TextAnchor.MiddleLeft,
margin = new RectOffset(10, 0, 4, 4)
};
GUIStyle boldTextFieldStyle = new(EditorStyles.textField) {
fontStyle = FontStyle.Bold
};
GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle);
string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle);
if (newName != this.currentNucleus.name) {
this.currentNucleus.name = newName;
this.prefab.RefreshOutputs();
outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList();
//outputsField.value = newName;
}
if (Application.isPlaying) {
GUIContent nameLabel = new("Output", this.currentNucleus.outputValue.ToString());
EditorGUILayout.FloatField(nameLabel, length(this.currentNucleus.outputValue));
}
else
EditorGUILayout.LabelField(" ");
if (this.currentNucleus is MemoryCell memory) {
memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory);
}
// Synapses
showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses");
if (showSynapses) {
ConnectNucleus(this.prefab, this.currentNucleus);
AddSynapse(this.prefab, this.currentNucleus);
EditorGUILayout.Space();
if (this.currentNucleus is Neuron neuron2)
neuron2.combinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator);
this.currentNucleus.bias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias);
NucleusArray array = null;
if (this.currentNucleus.synapses.Count > 0) {
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
foreach (Synapse synapse in synapses) {
if (synapse.nucleus != null) {
if (array != null) {
if (array.nuclei.Contains(synapse.nucleus))
continue;
}
else {
if (synapse.nucleus.array != null && synapse.nucleus.array.nuclei.Length > 1)
array = synapse.nucleus.array;
}
EditorGUILayout.Space();
if (Application.isPlaying) {
Vector3 value = synapse.nucleus.outputValue * synapse.weight;
GUIContent synapseValueLabel = new(synapse.nucleus.name, synapse.nucleus.outputValue.ToString());
EditorGUILayout.FloatField(synapseValueLabel, length(synapse.nucleus.outputValue));
}
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--;
}
}
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
// Activation
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.Width(150));
if (neuron.curveMax > 0)
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax));
else
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax));
neuron.curvePreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
if (neuron.array == null || neuron.array.nuclei == null || neuron.array.nuclei.Count() == 0)
neuron.array = new NucleusArray(neuron);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.IntField("Array size", neuron.array.nuclei.Count());
if (GUILayout.Button("Add")) {
Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name);
neuron.array.AddNucleus(this.prefab);
anythingChanged = true;
}
if (GUILayout.Button("Del")) {
Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name);
neuron.array.RemoveNucleus();
anythingChanged = true;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space();
}
EditorGUILayout.EndFoldoutHeaderGroup();
if (GUILayout.Button("Delete this neuron"))
DeleteNeuron(this.currentNucleus);
if (this.currentNucleus is Cluster subCluster) {
if (GUILayout.Button("Edit Cluster"))
EditCluster(subCluster);
}
EditorGUILayout.Space();
breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake);
if (breakOnWake) {
if (this.currentNucleus.isSleeping == false)
Debug.Break();
}
trace = EditorGUILayout.Toggle("Trace", trace);
this.currentNucleus.trace = trace;
serializedObject.ApplyModifiedProperties();
if (anythingChanged) {
EditorUtility.SetDirty(prefabAsset);
AssetDatabase.SaveAssets();
}
}
void OnSceneGUI(SceneView sceneView) {
if (this.gameObject != null) {
@ -829,28 +866,28 @@ public class NeuroidLayer {
public List<Nucleus> neuroids = new();
}
public class ClusterWrapper : ScriptableObject {
// expose fields that map to GraphNode
//public string title;
public Vector2 position;
Nucleus node;
ClusterPrefab graph; // needed to write back and mark dirty
// public class ClusterWrapper : ScriptableObject {
// // expose fields that map to GraphNode
// //public string title;
// public Vector2 position;
// Nucleus node;
// ClusterPrefab graph; // needed to write back and mark dirty
public ClusterWrapper Init(Nucleus 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
}
}
}
// public ClusterWrapper Init(Nucleus 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
// }
// }
// }

View File

@ -26,7 +26,13 @@ public class Neuron : Nucleus {
#region Serialization
public Type type = Type.Neuron;
//public Type type = Type.Neuron;
public enum CombinatorType {
Sum,
Product,
Max
}
public CombinatorType combinator = CombinatorType.Sum;
public enum CurvePresets {
Linear,
@ -150,9 +156,9 @@ public class Neuron : Nucleus {
}
protected virtual void CloneFields(Neuron clone) {
clone.array = null;
clone.array = this.array;
clone.bias = this.bias;
clone.type = this.type;
clone.combinator = this.combinator;
clone.curve = this.curve;
clone.curvePreset = this.curvePreset;
clone.curveMax = this.curveMax;
@ -184,17 +190,19 @@ public class Neuron : Nucleus {
}
public override void UpdateStateIsolated() {
switch (this.type) {
case Type.Neuron:
UpdateSum();
break;
case Type.Pulsar:
UpdateProduct();
break;
default:
UpdateSum();
break;
}
float3 result = CombinatorAction();
this.outputValue = Activation(result);
// switch (this.type) {
// case Type.Neuron:
// UpdateSum();
// break;
// case Type.Pulsar:
// UpdateProduct();
// break;
// default:
// UpdateSum();
// break;
// }
// Vector3 sum = this.bias;
// int n = 0;
@ -219,20 +227,45 @@ public class Neuron : Nucleus {
// this.outputValue = result;
}
public void UpdateSum() {
private Func<float3> CombinatorAction => combinator switch {
CombinatorType.Sum => UpdateSum,
CombinatorType.Product => UpdateProduct,
CombinatorType.Max => UpdateMax,
_ => UpdateSum
};
public float3 UpdateSum() {
Vector3 sum = this.bias;
foreach (Synapse synapse in this.synapses)
sum += synapse.weight * synapse.nucleus.outputValue;
this.outputValue = Activation(sum);
return sum;
//this.outputValue = Activation(sum);
}
public void UpdateProduct() {
public float3 UpdateProduct() {
float3 product = this.bias;
foreach (Synapse synapse in this.synapses)
product *= synapse.weight * synapse.nucleus.outputValue;
return product;
//this.outputValue = Activation(product);
}
this.outputValue = Activation(product);
public float3 UpdateMax() {
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;
}
protected float3 Activation(float3 input) {