2025-09-16 01:16:02 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2025-09-16 10:45:09 +02:00
|
|
|
using System.Dynamic;
|
2025-09-16 01:16:02 +02:00
|
|
|
using Unity.VisualScripting;
|
|
|
|
|
using UnityEditor;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.Rendering;
|
2025-09-16 14:20:19 +02:00
|
|
|
using UnityEngine.UIElements;
|
2025-09-16 01:16:02 +02:00
|
|
|
using static UnityEngine.GraphicsBuffer;
|
|
|
|
|
|
|
|
|
|
[ExecuteAlways]
|
|
|
|
|
public class GameManager : MonoBehaviour
|
|
|
|
|
{
|
2025-09-16 16:50:21 +02:00
|
|
|
public Transform ConnectionParent;
|
2025-09-16 10:45:09 +02:00
|
|
|
public Transform NodeParent;
|
|
|
|
|
public GameObject NodePrefab;
|
2025-09-16 01:16:02 +02:00
|
|
|
|
2025-09-16 18:19:40 +02:00
|
|
|
public static GameManager Instance { get; private set; }
|
2025-09-16 01:16:02 +02:00
|
|
|
|
2025-09-16 16:50:21 +02:00
|
|
|
[HideInInspector] public float maxConnectionLength = 6;
|
|
|
|
|
[HideInInspector] public int nodeCount = 100;
|
2025-09-16 17:40:34 +02:00
|
|
|
[HideInInspector] public int hoverRadiusCon = 50;
|
|
|
|
|
[HideInInspector] public int hoverRadiusNode = 50;
|
2025-09-16 00:08:50 +02:00
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
[SerializeField]
|
2025-09-16 16:50:21 +02:00
|
|
|
[HideInInspector] public List<Connection> connections = new List<Connection>();
|
|
|
|
|
[HideInInspector] public List<Node> nodes = new List<Node>();
|
2025-09-16 10:45:09 +02:00
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
public class Connection
|
2025-09-16 00:08:50 +02:00
|
|
|
{
|
2025-09-16 01:16:02 +02:00
|
|
|
public Node nodeA, nodeB;
|
2025-09-16 16:50:21 +02:00
|
|
|
public bool allowed = true;
|
|
|
|
|
public bool hovered = false;
|
2025-09-16 14:20:19 +02:00
|
|
|
public LineRenderer lineRenderer;
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-16 18:19:40 +02:00
|
|
|
void Awake()
|
2025-09-16 17:40:34 +02:00
|
|
|
{
|
|
|
|
|
if (Instance != null && Instance != this)
|
|
|
|
|
{
|
|
|
|
|
if (Application.isPlaying)
|
|
|
|
|
Destroy(gameObject);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Instance = this;
|
|
|
|
|
if (Application.isPlaying)
|
|
|
|
|
DontDestroyOnLoad(gameObject);
|
|
|
|
|
|
|
|
|
|
}
|
2025-09-16 16:50:21 +02:00
|
|
|
|
2025-09-16 01:16:02 +02:00
|
|
|
public void GenerateAlongSphere()
|
2025-09-16 00:08:50 +02:00
|
|
|
{
|
|
|
|
|
connections.Clear();
|
|
|
|
|
nodes.Clear();
|
|
|
|
|
|
2025-09-16 01:16:02 +02:00
|
|
|
for (int i = NodeParent.childCount - 1; i >= 0; i--)
|
|
|
|
|
DestroyImmediate(NodeParent.GetChild(i).gameObject);
|
2025-09-16 00:08:50 +02:00
|
|
|
|
|
|
|
|
float radius = 20f;
|
|
|
|
|
float goldenRatio = (1f + Mathf.Sqrt(5f)) / 2f;
|
|
|
|
|
float angleIncrement = 2f * Mathf.PI * goldenRatio;
|
|
|
|
|
|
2025-09-16 01:16:02 +02:00
|
|
|
for (int i = 0; i < nodeCount; i++)
|
2025-09-16 00:08:50 +02:00
|
|
|
{
|
2025-09-16 01:16:02 +02:00
|
|
|
float t = (float)i / nodeCount; // von 0 bis 1
|
2025-09-16 00:08:50 +02:00
|
|
|
float inclination = Mathf.Acos(1f - 2f * t);
|
|
|
|
|
float azimuth = angleIncrement * i;
|
|
|
|
|
|
|
|
|
|
float x = Mathf.Sin(inclination) * Mathf.Cos(azimuth);
|
|
|
|
|
float y = Mathf.Sin(inclination) * Mathf.Sin(azimuth);
|
|
|
|
|
float z = Mathf.Cos(inclination);
|
|
|
|
|
|
|
|
|
|
Vector3 pos = new Vector3(x, y, z) * radius;
|
|
|
|
|
|
2025-09-16 14:20:19 +02:00
|
|
|
var auto = PrefabUtility.InstantiatePrefab(NodePrefab, NodeParent) as GameObject;
|
2025-09-16 00:08:50 +02:00
|
|
|
auto.transform.localPosition = pos;
|
|
|
|
|
nodes.Add(auto.GetComponent<Node>());
|
|
|
|
|
}
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void GenerateConnections()
|
2025-09-16 00:08:50 +02:00
|
|
|
{
|
|
|
|
|
connections.Clear();
|
2025-09-16 16:50:21 +02:00
|
|
|
|
|
|
|
|
foreach (LineRenderer line in ConnectionParent.GetComponentsInChildren<LineRenderer>())
|
|
|
|
|
DestroyImmediate(line.gameObject);
|
|
|
|
|
|
2025-09-16 01:16:02 +02:00
|
|
|
foreach (Node nodeA in nodes)
|
|
|
|
|
{
|
|
|
|
|
if (nodeA == null) continue;
|
|
|
|
|
foreach (Node nodeB in nodes)
|
|
|
|
|
{
|
|
|
|
|
if (nodeB == null) continue;
|
|
|
|
|
bool conExists = false;
|
|
|
|
|
if (nodeA == nodeB || Math.Abs(Vector3.Distance(nodeA.transform.position, nodeB.transform.position)) > maxConnectionLength)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
foreach (Connection con in connections)
|
|
|
|
|
{
|
|
|
|
|
if ((con.nodeA == nodeA && con.nodeB == nodeB) || (con.nodeA == nodeB && con.nodeB == nodeA))
|
|
|
|
|
{
|
|
|
|
|
conExists = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!conExists)
|
2025-09-16 14:20:19 +02:00
|
|
|
{
|
2025-09-16 16:50:21 +02:00
|
|
|
var dummy = new GameObject("dummy");
|
|
|
|
|
dummy.transform.SetParent(ConnectionParent);
|
2025-09-16 14:20:19 +02:00
|
|
|
var newCon = new Connection
|
|
|
|
|
{
|
|
|
|
|
nodeA = nodeA,
|
|
|
|
|
nodeB = nodeB,
|
|
|
|
|
lineRenderer = dummy.AddComponent<LineRenderer>()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
newCon.lineRenderer.enabled = false;
|
|
|
|
|
newCon.lineRenderer.material = new Material(Shader.Find("Sprites/Default"));
|
|
|
|
|
newCon.lineRenderer.positionCount = 3;
|
|
|
|
|
|
|
|
|
|
connections.Add(newCon);
|
|
|
|
|
}
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
2025-09-16 14:20:19 +02:00
|
|
|
|
2025-09-16 00:08:50 +02:00
|
|
|
}
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
2025-09-16 13:04:05 +02:00
|
|
|
|
2025-09-16 14:20:19 +02:00
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
foreach (var con in connections)
|
|
|
|
|
{
|
2025-09-16 17:40:34 +02:00
|
|
|
if(Application.isPlaying && !con.allowed)
|
|
|
|
|
{
|
|
|
|
|
con.lineRenderer.enabled = false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 16:50:21 +02:00
|
|
|
float width = (con.hovered ? 0.6f : 0.3f) * (con.allowed ? 1f : 0.3f);
|
|
|
|
|
|
|
|
|
|
con.lineRenderer.startColor = con.allowed ? con.nodeA.transform.GetChild(0).GetComponent<Renderer>().sharedMaterial.color : new Color(0.2f, 0.2f, 0.2f);
|
|
|
|
|
con.lineRenderer.endColor = con.allowed ? con.nodeB.transform.GetChild(0).GetComponent<Renderer>().sharedMaterial.color : new Color(0.2f, 0.2f, 0.2f);
|
|
|
|
|
con.lineRenderer.startWidth = width;
|
|
|
|
|
con.lineRenderer.endWidth = width;
|
2025-09-16 14:20:19 +02:00
|
|
|
|
|
|
|
|
con.lineRenderer.SetGreatCircleArc(con.nodeA.transform.position, con.nodeB.transform.position, 5, 20.5f);
|
|
|
|
|
con.lineRenderer.enabled = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-16 10:45:09 +02:00
|
|
|
}
|
2025-09-16 01:16:02 +02:00
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
[CustomEditor(typeof(GameManager))]
|
|
|
|
|
public class GameManagerEditor : Editor
|
|
|
|
|
{
|
|
|
|
|
int clickedConIdx = -1;
|
2025-09-16 01:16:02 +02:00
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
public override void OnInspectorGUI()
|
|
|
|
|
{
|
|
|
|
|
DrawDefaultInspector();
|
|
|
|
|
|
2025-09-16 16:50:21 +02:00
|
|
|
GameManager gm = (GameManager)target;
|
2025-09-16 10:45:09 +02:00
|
|
|
|
2025-09-16 16:50:21 +02:00
|
|
|
GUILayout.Label("Generation Parameters", EditorStyles.boldLabel);
|
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
|
gm.nodeCount = EditorGUILayout.IntSlider("Node Count", gm.nodeCount, 0, 200);
|
|
|
|
|
gm.maxConnectionLength = EditorGUILayout.Slider("Max Connection Length", gm.maxConnectionLength, 0, 100);
|
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
|
|
|
{
|
|
|
|
|
gm.GenerateAlongSphere();
|
|
|
|
|
gm.GenerateConnections();
|
|
|
|
|
SceneView.RepaintAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GUILayout.Space(10);
|
|
|
|
|
GUILayout.Label("Connection Gizmos", EditorStyles.boldLabel);
|
2025-09-16 10:45:09 +02:00
|
|
|
|
2025-09-16 17:40:34 +02:00
|
|
|
gm.hoverRadiusCon = EditorGUILayout.IntSlider("Hover Radius (Connection)", gm.hoverRadiusCon, 0, 100);
|
|
|
|
|
gm.hoverRadiusNode = EditorGUILayout.IntSlider("Hover Radius (Node)", gm.hoverRadiusNode, 0, 100);
|
2025-09-16 16:50:21 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GUILayout.BeginHorizontal();
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("Generate Nodes"))
|
|
|
|
|
{
|
|
|
|
|
gm.GenerateAlongSphere();
|
|
|
|
|
SceneView.RepaintAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (GUILayout.Button("Generate Connections"))
|
|
|
|
|
{
|
|
|
|
|
gm.GenerateConnections();
|
|
|
|
|
SceneView.RepaintAll();
|
|
|
|
|
}
|
2025-09-16 10:45:09 +02:00
|
|
|
|
2025-09-16 16:50:21 +02:00
|
|
|
if (GUILayout.Button("Generate Both"))
|
2025-09-16 10:45:09 +02:00
|
|
|
{
|
|
|
|
|
gm.GenerateAlongSphere();
|
2025-09-16 16:50:21 +02:00
|
|
|
gm.GenerateConnections();
|
|
|
|
|
SceneView.RepaintAll();
|
2025-09-16 10:45:09 +02:00
|
|
|
}
|
2025-09-16 16:50:21 +02:00
|
|
|
|
|
|
|
|
GUILayout.EndHorizontal();
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
2025-09-16 17:40:34 +02:00
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
private void OnSceneGUI()
|
2025-09-16 00:08:50 +02:00
|
|
|
{
|
2025-09-16 10:45:09 +02:00
|
|
|
Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual;
|
2025-09-16 01:16:02 +02:00
|
|
|
|
2025-09-16 18:19:40 +02:00
|
|
|
GameManager gm = FindFirstObjectByType<GameManager>();
|
2025-09-16 17:40:34 +02:00
|
|
|
if (gm == null) return;
|
|
|
|
|
|
|
|
|
|
GameObject selected = Selection.activeGameObject;
|
|
|
|
|
bool allowHover = false;
|
|
|
|
|
|
|
|
|
|
if (selected == gm.gameObject)
|
|
|
|
|
allowHover = true;
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Node node = selected?.GetComponent<Node>();
|
|
|
|
|
if (node != null && gm.nodes.Contains(node))
|
|
|
|
|
allowHover = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!allowHover) return;
|
|
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
if (gm.connections == null || gm.connections.Count == 0) return;
|
2025-09-16 01:16:02 +02:00
|
|
|
Camera cam = SceneView.lastActiveSceneView.camera;
|
2025-09-16 10:45:09 +02:00
|
|
|
if (cam == null) return;
|
2025-09-16 00:08:50 +02:00
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
Vector2 mouse = Event.current.mousePosition;
|
|
|
|
|
mouse.y = cam.pixelHeight - mouse.y;
|
|
|
|
|
Vector3 mousePos = new Vector3(mouse.x, mouse.y, 0);
|
2025-09-16 00:08:50 +02:00
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
int closestIdx = -1;
|
2025-09-16 01:16:02 +02:00
|
|
|
float closestDist = float.MaxValue;
|
2025-09-16 17:40:34 +02:00
|
|
|
bool cancelHover = false;
|
2025-09-16 01:16:02 +02:00
|
|
|
|
2025-09-16 17:40:34 +02:00
|
|
|
// Hover-Ermittlung Connections
|
2025-09-16 10:45:09 +02:00
|
|
|
for (int i = 0; i < gm.connections.Count; i++)
|
2025-09-16 00:08:50 +02:00
|
|
|
{
|
2025-09-16 16:50:21 +02:00
|
|
|
gm.connections[i].hovered = false;
|
|
|
|
|
|
2025-09-16 17:40:34 +02:00
|
|
|
if (cancelHover)
|
|
|
|
|
continue;
|
|
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
var con = gm.connections[i];
|
2025-09-16 01:16:02 +02:00
|
|
|
Vector3 a = cam.WorldToScreenPoint(con.nodeA.transform.position);
|
|
|
|
|
Vector3 b = cam.WorldToScreenPoint(con.nodeB.transform.position);
|
|
|
|
|
|
|
|
|
|
Vector3 ab = b - a;
|
|
|
|
|
Vector3 am = mousePos - a;
|
|
|
|
|
float t = Mathf.Clamp01(Vector3.Dot(am, ab) / ab.sqrMagnitude);
|
|
|
|
|
Vector3 proj = a + t * ab;
|
|
|
|
|
float dist = Vector3.Distance(mousePos, proj);
|
|
|
|
|
|
2025-09-16 17:40:34 +02:00
|
|
|
if (Vector3.Distance(a, mousePos) <= gm.hoverRadiusNode || Vector3.Distance(b, mousePos) <= gm.hoverRadiusNode)
|
|
|
|
|
cancelHover = true;
|
|
|
|
|
|
|
|
|
|
if (dist < closestDist && dist < gm.hoverRadiusCon)
|
2025-09-16 01:16:02 +02:00
|
|
|
{
|
|
|
|
|
closestDist = dist;
|
2025-09-16 10:45:09 +02:00
|
|
|
closestIdx = i;
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 17:40:34 +02:00
|
|
|
if (closestIdx < 0)
|
|
|
|
|
cancelHover = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!cancelHover)
|
2025-09-16 16:50:21 +02:00
|
|
|
gm.connections[closestIdx].hovered = true;
|
|
|
|
|
|
|
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
// Klick-Behandlung
|
|
|
|
|
Event e = Event.current;
|
2025-09-16 16:50:21 +02:00
|
|
|
|
|
|
|
|
|
2025-09-16 17:40:34 +02:00
|
|
|
if (e.type == EventType.MouseDown && e.button == 0 && !cancelHover)
|
2025-09-16 01:16:02 +02:00
|
|
|
{
|
2025-09-16 10:45:09 +02:00
|
|
|
clickedConIdx = closestIdx;
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
if (e.type == EventType.MouseUp && e.button == 0)
|
2025-09-16 01:16:02 +02:00
|
|
|
{
|
2025-09-16 17:40:34 +02:00
|
|
|
if (closestIdx == clickedConIdx && !cancelHover)
|
2025-09-16 10:45:09 +02:00
|
|
|
{
|
|
|
|
|
gm.connections[closestIdx].allowed = !gm.connections[closestIdx].allowed;
|
|
|
|
|
e.Use(); // Event als verarbeitet markieren
|
|
|
|
|
}
|
2025-09-16 01:16:02 +02:00
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
clickedConIdx = -1;
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
|
|
|
|
|
2025-09-16 10:45:09 +02:00
|
|
|
// SceneView kontinuierlich aktualisieren
|
|
|
|
|
SceneView.RepaintAll();
|
2025-09-16 01:16:02 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2025-09-16 14:20:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
public static class LineRendererExtensions
|
|
|
|
|
{
|
|
|
|
|
// Zeichnet eine Quadratic Bezier Kurve (Start, Control, End) mit N Punkten
|
|
|
|
|
public static void SetQuadraticBezier(this LineRenderer line, Vector3 start, Vector3 control, Vector3 end, int segments)
|
|
|
|
|
{
|
|
|
|
|
line.positionCount = segments + 1;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i <= segments; i++)
|
|
|
|
|
{
|
|
|
|
|
float t = i / (float)segments;
|
|
|
|
|
// Quadratic Bezier Formel
|
|
|
|
|
Vector3 point = (1 - t) * (1 - t) * start +
|
|
|
|
|
2 * (1 - t) * t * control +
|
|
|
|
|
t * t * end;
|
|
|
|
|
|
|
|
|
|
line.SetPosition(i, point);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void SetGreatCircleArc(this LineRenderer line, Vector3 start, Vector3 end, int segments, float radius)
|
|
|
|
|
{
|
|
|
|
|
// Normalize to sphere radius
|
|
|
|
|
Vector3 startNorm = start.normalized * radius;
|
|
|
|
|
Vector3 endNorm = end.normalized * radius;
|
|
|
|
|
|
|
|
|
|
line.positionCount = segments + 1;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i <= segments; i++)
|
|
|
|
|
{
|
|
|
|
|
float t = i / (float)segments;
|
|
|
|
|
// spherical linear interpolation (slerp) between start and end
|
|
|
|
|
Vector3 point = Vector3.Slerp(startNorm, endNorm, t);
|
|
|
|
|
line.SetPosition(i, point);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|