294 lines
11 KiB
C#
294 lines
11 KiB
C#
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 (Neuroid outputNeuroid in selectedNucleus.receivers) {
|
|
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) {
|
|
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)) {
|
|
// 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) {
|
|
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.thingId}" +
|
|
$"\nValue: {neuroid.outputValue}" +
|
|
$"\nStale: {neuroid.stale}");
|
|
}
|
|
else {
|
|
tooltip = new(
|
|
$"{neuroid.name}" +
|
|
$"\nsynapse count {neuroid.synapses.Count}" +
|
|
$"\nValue: {neuroid.outputValue}" +
|
|
$"\nStale: {neuroid.stale}");
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|