Optimize Player UI updates with threshold-based rendering and add performance profiler autoload: implement UI_UPDATE_THRESHOLD constant to reduce unnecessary label updates, add conditional DEBUG compilation for debug prints, wrap debug output in preprocessor directives, add PerformanceProfiler autoload, switch to JoltPhysics3D engine, enable VSync, configure rendering quality settings (TAA, SSAA, anisotropic filtering, shadow atlas), and update main scene path
This commit is contained in:
@@ -20,6 +20,11 @@ public partial class Player : FirstPersonController
|
||||
public Label VelocityVal { get; set; }
|
||||
|
||||
private float _lastSpeed = 0f;
|
||||
private float _lastDisplayedSpeed = 0f;
|
||||
private Vector3 _lastDisplayedPosition;
|
||||
private Vector3 _lastDisplayedVelocity;
|
||||
private string _lastDisplayedState = "";
|
||||
private const float UI_UPDATE_THRESHOLD = 0.1f;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
@@ -102,21 +107,25 @@ public partial class Player : FirstPersonController
|
||||
MouseSensitivity = 0.35f;
|
||||
EnableDebugOutput = false;
|
||||
|
||||
#if DEBUG
|
||||
GD.Print($"InputManager assigned: {InputManager != null}");
|
||||
GD.Print($"CameraNode assigned: {CameraNode != null}");
|
||||
GD.Print($"Config assigned: {Config != null}");
|
||||
#endif
|
||||
|
||||
SubscribeToStateChanges(OnMovementStateChanged);
|
||||
|
||||
MovementEvents.PlayerJumped += OnPlayerJumped;
|
||||
MovementEvents.SpeedChanged += OnSpeedChanged;
|
||||
|
||||
#if DEBUG
|
||||
GD.Print("TestPlayer initialized with all libraries!");
|
||||
GD.Print("- Movement: Enabled");
|
||||
GD.Print("- Input: Enabled");
|
||||
GD.Print("- Events: Enabled");
|
||||
GD.Print("- StateManagement: Enabled");
|
||||
GD.Print("- Camera: Enabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
@@ -128,15 +137,15 @@ public partial class Player : FirstPersonController
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
if (SpeedVal != null)
|
||||
if (SpeedVal != null && Mathf.Abs(CurrentSpeed - _lastDisplayedSpeed) > UI_UPDATE_THRESHOLD)
|
||||
{
|
||||
SpeedVal.Text = $"{CurrentSpeed:F1} u/s ";
|
||||
_lastDisplayedSpeed = CurrentSpeed;
|
||||
}
|
||||
|
||||
if (StateVal != null)
|
||||
{
|
||||
string state = "Unknown";
|
||||
// Check crouch first since IsOnFloor() is unreliable when crouched
|
||||
if (IsCrouching)
|
||||
{
|
||||
state = "Crouching";
|
||||
@@ -153,38 +162,49 @@ public partial class Player : FirstPersonController
|
||||
state = "Airborne";
|
||||
}
|
||||
|
||||
StateVal.Text = $"{state} ";
|
||||
if (state != _lastDisplayedState)
|
||||
{
|
||||
StateVal.Text = $"{state} ";
|
||||
_lastDisplayedState = state;
|
||||
}
|
||||
}
|
||||
|
||||
if (PositionVal != null)
|
||||
if (PositionVal != null && GlobalPosition.DistanceSquaredTo(_lastDisplayedPosition) > UI_UPDATE_THRESHOLD)
|
||||
{
|
||||
PositionVal.Text =
|
||||
$"({GlobalPosition.X:F1}, {GlobalPosition.Y:F1}, {GlobalPosition.Z:F1}) ";
|
||||
_lastDisplayedPosition = GlobalPosition;
|
||||
}
|
||||
|
||||
if (VelocityVal != null)
|
||||
if (VelocityVal != null && Velocity.DistanceSquaredTo(_lastDisplayedVelocity) > UI_UPDATE_THRESHOLD)
|
||||
{
|
||||
VelocityVal.Text = $"({Velocity.X:F1}, {Velocity.Y:F1}, {Velocity.Z:F1}) ";
|
||||
_lastDisplayedVelocity = Velocity;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMovementStateChanged(MovementStateChangedEvent evt)
|
||||
{
|
||||
#if DEBUG
|
||||
GD.Print($"[Movement] State changed to: {evt.StateName} (Speed: {evt.Speed:F1})");
|
||||
|
||||
#endif
|
||||
MovementEvents.RaiseStateChanged(evt.StateName);
|
||||
}
|
||||
|
||||
private void OnPlayerJumped()
|
||||
{
|
||||
#if DEBUG
|
||||
GD.Print($"[Event] Player jumped at speed: {CurrentSpeed:F1}");
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnSpeedChanged(float speed)
|
||||
{
|
||||
if (Mathf.Abs(speed - _lastSpeed) > 50f)
|
||||
{
|
||||
#if DEBUG
|
||||
GD.Print($"[Event] Speed changed significantly: {_lastSpeed:F1} -> {speed:F1}");
|
||||
#endif
|
||||
_lastSpeed = speed;
|
||||
}
|
||||
}
|
||||
|
||||
881
Scripts/Tools/CollisionGenerator.cs
Normal file
881
Scripts/Tools/CollisionGenerator.cs
Normal file
@@ -0,0 +1,881 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Voider.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Standalone tool for generating collision shapes from MeshInstance3D nodes in any scene.
|
||||
/// Run the CollisionGenerator.tscn scene, select a target scene, and generate collision.
|
||||
/// </summary>
|
||||
public partial class CollisionGenerator : Control
|
||||
{
|
||||
public enum CollisionMode
|
||||
{
|
||||
Trimesh, // Accurate concave collision - best for static geometry
|
||||
Convex, // Fast convex hull - good for simple objects
|
||||
MultiConvex, // Multiple convex shapes - balanced (falls back to trimesh)
|
||||
Merged // Single merged trimesh - best for large maps with many meshes
|
||||
}
|
||||
|
||||
// UI Elements
|
||||
private LineEdit _scenePathEdit;
|
||||
private Button _browseButton;
|
||||
private Button _generateButton;
|
||||
private Button _clearButton;
|
||||
private Button _mergeButton;
|
||||
private OptionButton _collisionModeOption;
|
||||
private SpinBox _collisionLayerSpin;
|
||||
private SpinBox _collisionMaskSpin;
|
||||
private SpinBox _minSizeThresholdSpin;
|
||||
private CheckBox _skipSmallMeshesCheck;
|
||||
private RichTextLabel _logOutput;
|
||||
private FileDialog _fileDialog;
|
||||
private Panel _previewPanel;
|
||||
private SubViewport _previewViewport;
|
||||
private Node3D _previewRoot;
|
||||
private Camera3D _previewCamera;
|
||||
|
||||
// State
|
||||
private string _targetScenePath = "";
|
||||
private PackedScene _loadedScene;
|
||||
private Node _sceneInstance;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
BuildUI();
|
||||
Log("[b]Collision Generator Tool[/b]\nSelect a scene file to generate collision shapes.");
|
||||
}
|
||||
|
||||
private void BuildUI()
|
||||
{
|
||||
// Main container
|
||||
var mainContainer = new VBoxContainer();
|
||||
mainContainer.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
mainContainer.AddThemeConstantOverride("separation", 10);
|
||||
AddChild(mainContainer);
|
||||
|
||||
// Add some padding
|
||||
var marginContainer = new MarginContainer();
|
||||
marginContainer.AddThemeConstantOverride("margin_left", 20);
|
||||
marginContainer.AddThemeConstantOverride("margin_right", 20);
|
||||
marginContainer.AddThemeConstantOverride("margin_top", 20);
|
||||
marginContainer.AddThemeConstantOverride("margin_bottom", 20);
|
||||
marginContainer.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
marginContainer.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
mainContainer.AddChild(marginContainer);
|
||||
|
||||
var innerContainer = new VBoxContainer();
|
||||
innerContainer.AddThemeConstantOverride("separation", 15);
|
||||
marginContainer.AddChild(innerContainer);
|
||||
|
||||
// Title
|
||||
var title = new Label();
|
||||
title.Text = "Collision Generator";
|
||||
title.AddThemeFontSizeOverride("font_size", 24);
|
||||
innerContainer.AddChild(title);
|
||||
|
||||
// Scene selection section
|
||||
var sceneSection = CreateSection("Target Scene");
|
||||
innerContainer.AddChild(sceneSection);
|
||||
|
||||
var sceneRow = new HBoxContainer();
|
||||
sceneRow.AddThemeConstantOverride("separation", 10);
|
||||
sceneSection.AddChild(sceneRow);
|
||||
|
||||
_scenePathEdit = new LineEdit();
|
||||
_scenePathEdit.PlaceholderText = "Select a .tscn file...";
|
||||
_scenePathEdit.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
_scenePathEdit.Editable = false;
|
||||
sceneRow.AddChild(_scenePathEdit);
|
||||
|
||||
_browseButton = new Button();
|
||||
_browseButton.Text = "Browse...";
|
||||
_browseButton.Pressed += OnBrowsePressed;
|
||||
sceneRow.AddChild(_browseButton);
|
||||
|
||||
// Options section
|
||||
var optionsSection = CreateSection("Options");
|
||||
innerContainer.AddChild(optionsSection);
|
||||
|
||||
var optionsGrid = new GridContainer();
|
||||
optionsGrid.Columns = 2;
|
||||
optionsGrid.AddThemeConstantOverride("h_separation", 20);
|
||||
optionsGrid.AddThemeConstantOverride("v_separation", 10);
|
||||
optionsSection.AddChild(optionsGrid);
|
||||
|
||||
// Collision Mode
|
||||
optionsGrid.AddChild(CreateLabel("Collision Mode:"));
|
||||
_collisionModeOption = new OptionButton();
|
||||
_collisionModeOption.AddItem("Trimesh (Accurate)", 0);
|
||||
_collisionModeOption.AddItem("Convex (Fast)", 1);
|
||||
_collisionModeOption.AddItem("Multi-Convex (Balanced)", 2);
|
||||
_collisionModeOption.Selected = 0;
|
||||
_collisionModeOption.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
optionsGrid.AddChild(_collisionModeOption);
|
||||
|
||||
// Collision Layer
|
||||
optionsGrid.AddChild(CreateLabel("Collision Layer:"));
|
||||
_collisionLayerSpin = new SpinBox();
|
||||
_collisionLayerSpin.MinValue = 1;
|
||||
_collisionLayerSpin.MaxValue = 32;
|
||||
_collisionLayerSpin.Value = 1;
|
||||
optionsGrid.AddChild(_collisionLayerSpin);
|
||||
|
||||
// Collision Mask
|
||||
optionsGrid.AddChild(CreateLabel("Collision Mask:"));
|
||||
_collisionMaskSpin = new SpinBox();
|
||||
_collisionMaskSpin.MinValue = 1;
|
||||
_collisionMaskSpin.MaxValue = 32;
|
||||
_collisionMaskSpin.Value = 1;
|
||||
optionsGrid.AddChild(_collisionMaskSpin);
|
||||
|
||||
// Skip Small Meshes
|
||||
optionsGrid.AddChild(CreateLabel("Skip Small Meshes:"));
|
||||
_skipSmallMeshesCheck = new CheckBox();
|
||||
_skipSmallMeshesCheck.ButtonPressed = true;
|
||||
_skipSmallMeshesCheck.Text = "Filter out tiny objects";
|
||||
optionsGrid.AddChild(_skipSmallMeshesCheck);
|
||||
|
||||
// Min Size Threshold
|
||||
optionsGrid.AddChild(CreateLabel("Min Size (meters):"));
|
||||
_minSizeThresholdSpin = new SpinBox();
|
||||
_minSizeThresholdSpin.MinValue = 0.0;
|
||||
_minSizeThresholdSpin.MaxValue = 10.0;
|
||||
_minSizeThresholdSpin.Step = 0.1;
|
||||
_minSizeThresholdSpin.Value = 0.1;
|
||||
optionsGrid.AddChild(_minSizeThresholdSpin);
|
||||
|
||||
// Actions section
|
||||
var actionsSection = CreateSection("Actions");
|
||||
innerContainer.AddChild(actionsSection);
|
||||
|
||||
var actionsRow = new HBoxContainer();
|
||||
actionsRow.AddThemeConstantOverride("separation", 10);
|
||||
actionsSection.AddChild(actionsRow);
|
||||
|
||||
_generateButton = new Button();
|
||||
_generateButton.Text = "Generate Collision";
|
||||
_generateButton.Disabled = true;
|
||||
_generateButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
_generateButton.Pressed += OnGeneratePressed;
|
||||
actionsRow.AddChild(_generateButton);
|
||||
|
||||
_clearButton = new Button();
|
||||
_clearButton.Text = "Clear Collision";
|
||||
_clearButton.Disabled = true;
|
||||
_clearButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
_clearButton.Pressed += OnClearPressed;
|
||||
actionsRow.AddChild(_clearButton);
|
||||
|
||||
_mergeButton = new Button();
|
||||
_mergeButton.Text = "Merge Existing";
|
||||
_mergeButton.Disabled = true;
|
||||
_mergeButton.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
_mergeButton.Pressed += OnMergePressed;
|
||||
actionsRow.AddChild(_mergeButton);
|
||||
|
||||
// Log output section
|
||||
var logSection = CreateSection("Log");
|
||||
logSection.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
innerContainer.AddChild(logSection);
|
||||
|
||||
var logScroll = new ScrollContainer();
|
||||
logScroll.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
logScroll.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
logSection.AddChild(logScroll);
|
||||
|
||||
_logOutput = new RichTextLabel();
|
||||
_logOutput.BbcodeEnabled = true;
|
||||
_logOutput.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
_logOutput.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
_logOutput.FitContent = true;
|
||||
_logOutput.ScrollFollowing = true;
|
||||
logScroll.AddChild(_logOutput);
|
||||
|
||||
// File dialog
|
||||
_fileDialog = new FileDialog();
|
||||
_fileDialog.FileMode = FileDialog.FileModeEnum.OpenFile;
|
||||
_fileDialog.Access = FileDialog.AccessEnum.Resources;
|
||||
_fileDialog.Filters = new string[] { "*.tscn ; Godot Scene Files" };
|
||||
_fileDialog.Title = "Select Scene to Process";
|
||||
_fileDialog.Size = new Vector2I(800, 600);
|
||||
_fileDialog.FileSelected += OnFileSelected;
|
||||
AddChild(_fileDialog);
|
||||
}
|
||||
|
||||
private VBoxContainer CreateSection(string title)
|
||||
{
|
||||
var container = new VBoxContainer();
|
||||
container.AddThemeConstantOverride("separation", 8);
|
||||
|
||||
var label = new Label();
|
||||
label.Text = title;
|
||||
label.AddThemeFontSizeOverride("font_size", 16);
|
||||
container.AddChild(label);
|
||||
|
||||
var separator = new HSeparator();
|
||||
container.AddChild(separator);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private Label CreateLabel(string text)
|
||||
{
|
||||
var label = new Label();
|
||||
label.Text = text;
|
||||
return label;
|
||||
}
|
||||
|
||||
private void OnBrowsePressed()
|
||||
{
|
||||
_fileDialog.CurrentDir = "res://Scenes";
|
||||
_fileDialog.PopupCentered();
|
||||
}
|
||||
|
||||
private void OnFileSelected(string path)
|
||||
{
|
||||
_targetScenePath = path;
|
||||
_scenePathEdit.Text = path;
|
||||
_generateButton.Disabled = false;
|
||||
_clearButton.Disabled = false;
|
||||
_mergeButton.Disabled = false;
|
||||
|
||||
// Load and analyze the scene
|
||||
LoadAndAnalyzeScene(path);
|
||||
}
|
||||
|
||||
private void LoadAndAnalyzeScene(string path)
|
||||
{
|
||||
Log($"\n[b]Loading:[/b] {path}");
|
||||
|
||||
_loadedScene = GD.Load<PackedScene>(path);
|
||||
if (_loadedScene == null)
|
||||
{
|
||||
LogError($"Failed to load scene: {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
_sceneInstance = _loadedScene.Instantiate();
|
||||
if (_sceneInstance == null)
|
||||
{
|
||||
LogError("Failed to instantiate scene");
|
||||
return;
|
||||
}
|
||||
|
||||
// Count meshes and existing collision
|
||||
var meshInstances = new List<MeshInstance3D>();
|
||||
var existingCollision = new List<Node>();
|
||||
CollectMeshInstances(_sceneInstance, meshInstances);
|
||||
CollectExistingCollision(_sceneInstance, existingCollision);
|
||||
|
||||
Log($"[color=cyan]Found {meshInstances.Count} mesh instances[/color]");
|
||||
Log($"[color=yellow]Found {existingCollision.Count} existing collision nodes[/color]");
|
||||
|
||||
// Don't add to tree, just keep reference for processing
|
||||
}
|
||||
|
||||
private void OnGeneratePressed()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_targetScenePath))
|
||||
{
|
||||
LogError("No scene selected");
|
||||
return;
|
||||
}
|
||||
|
||||
GenerateCollisionForScene();
|
||||
}
|
||||
|
||||
private void OnClearPressed()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_targetScenePath))
|
||||
{
|
||||
LogError("No scene selected");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearCollisionFromScene();
|
||||
}
|
||||
|
||||
private void OnMergePressed()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_targetScenePath))
|
||||
{
|
||||
LogError("No scene selected");
|
||||
return;
|
||||
}
|
||||
|
||||
MergeExistingCollision();
|
||||
}
|
||||
|
||||
private void GenerateCollisionForScene()
|
||||
{
|
||||
Log($"\n[b]Generating collision for:[/b] {_targetScenePath}");
|
||||
|
||||
// Reload fresh instance
|
||||
_loadedScene = GD.Load<PackedScene>(_targetScenePath);
|
||||
_sceneInstance = _loadedScene.Instantiate();
|
||||
|
||||
// We need to add scene to tree temporarily to calculate global transforms
|
||||
AddChild(_sceneInstance);
|
||||
|
||||
var meshInstances = new List<MeshInstance3D>();
|
||||
CollectMeshInstances(_sceneInstance, meshInstances);
|
||||
|
||||
var collisionMode = (CollisionMode)_collisionModeOption.Selected;
|
||||
uint collisionLayer = (uint)_collisionLayerSpin.Value;
|
||||
uint collisionMask = (uint)_collisionMaskSpin.Value;
|
||||
bool skipSmall = _skipSmallMeshesCheck.ButtonPressed;
|
||||
float minSize = (float)_minSizeThresholdSpin.Value;
|
||||
|
||||
// Create a container for all collision at the scene root level
|
||||
var collisionContainer = _sceneInstance.GetNodeOrNull<Node3D>("_GeneratedCollision");
|
||||
if (collisionContainer == null)
|
||||
{
|
||||
collisionContainer = new Node3D();
|
||||
collisionContainer.Name = "_GeneratedCollision";
|
||||
_sceneInstance.AddChild(collisionContainer);
|
||||
collisionContainer.Owner = _sceneInstance;
|
||||
}
|
||||
|
||||
GeneratePerMeshCollision(meshInstances, collisionContainer, collisionMode, collisionLayer, collisionMask, skipSmall, minSize);
|
||||
|
||||
// Remove from tree before saving
|
||||
RemoveChild(_sceneInstance);
|
||||
|
||||
// Restore mouse cursor (Player script may have captured it)
|
||||
Input.MouseMode = Input.MouseModeEnum.Visible;
|
||||
|
||||
// Save the modified scene
|
||||
SaveScene();
|
||||
}
|
||||
|
||||
private void MergeExistingCollision()
|
||||
{
|
||||
Log($"\n[b]Merging existing collision in:[/b] {_targetScenePath}");
|
||||
|
||||
// Reload fresh instance - bypass cache
|
||||
ResourceLoader.Load<PackedScene>(_targetScenePath, cacheMode: ResourceLoader.CacheMode.Ignore);
|
||||
_loadedScene = GD.Load<PackedScene>(_targetScenePath);
|
||||
_sceneInstance = _loadedScene.Instantiate();
|
||||
|
||||
// Add to tree temporarily
|
||||
AddChild(_sceneInstance);
|
||||
|
||||
// Find the collision container
|
||||
var collisionContainer = _sceneInstance.GetNodeOrNull<Node3D>("_GeneratedCollision");
|
||||
if (collisionContainer == null || collisionContainer.GetChildCount() == 0)
|
||||
{
|
||||
RemoveChild(_sceneInstance);
|
||||
_sceneInstance.Free();
|
||||
Log("[color=yellow]No existing collision found to merge[/color]");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already merged
|
||||
if (collisionContainer.GetChildCount() == 1 && collisionContainer.GetChild(0).Name == "MergedCollision")
|
||||
{
|
||||
RemoveChild(_sceneInstance);
|
||||
_sceneInstance.Free();
|
||||
Log("[color=yellow]Collision is already merged[/color]");
|
||||
return;
|
||||
}
|
||||
|
||||
uint collisionLayer = (uint)_collisionLayerSpin.Value;
|
||||
uint collisionMask = (uint)_collisionMaskSpin.Value;
|
||||
|
||||
Log($"[color=cyan]Merging {collisionContainer.GetChildCount()} collision nodes into single shape[/color]");
|
||||
|
||||
// Collect all faces from existing collision shapes
|
||||
var allFaces = new List<Vector3>();
|
||||
var nodesToRemove = new List<Node>();
|
||||
|
||||
// Handle new structure: single StaticBody3D with multiple CollisionShape3D children
|
||||
var generatedBody = collisionContainer.GetNodeOrNull<StaticBody3D>("GeneratedStaticBody");
|
||||
if (generatedBody != null)
|
||||
{
|
||||
foreach (var child in generatedBody.GetChildren())
|
||||
{
|
||||
if (child is CollisionShape3D collisionShape && collisionShape.Shape is ConcavePolygonShape3D concaveShape)
|
||||
{
|
||||
var globalTransform = collisionShape.GlobalTransform;
|
||||
var faces = concaveShape.GetFaces();
|
||||
foreach (var vertex in faces)
|
||||
{
|
||||
allFaces.Add(globalTransform * vertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
nodesToRemove.Add(generatedBody);
|
||||
}
|
||||
|
||||
// Also handle legacy structure: multiple StaticBody3D nodes
|
||||
foreach (var child in collisionContainer.GetChildren())
|
||||
{
|
||||
if (child is StaticBody3D staticBody && child.Name != "GeneratedStaticBody" && child.Name != "MergedCollision")
|
||||
{
|
||||
var globalTransform = staticBody.GlobalTransform;
|
||||
|
||||
foreach (var bodyChild in staticBody.GetChildren())
|
||||
{
|
||||
if (bodyChild is CollisionShape3D collisionShape && collisionShape.Shape is ConcavePolygonShape3D concaveShape)
|
||||
{
|
||||
var faces = concaveShape.GetFaces();
|
||||
foreach (var vertex in faces)
|
||||
{
|
||||
allFaces.Add(globalTransform * vertex);
|
||||
}
|
||||
}
|
||||
}
|
||||
nodesToRemove.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
Log($" Collected {allFaces.Count / 3} triangles from existing shapes");
|
||||
|
||||
if (allFaces.Count < 9)
|
||||
{
|
||||
RemoveChild(_sceneInstance);
|
||||
_sceneInstance.Free();
|
||||
LogError("Not enough faces to create merged collision");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old collision nodes
|
||||
foreach (var node in nodesToRemove)
|
||||
{
|
||||
collisionContainer.RemoveChild(node);
|
||||
node.Free();
|
||||
}
|
||||
|
||||
// Create the merged collision shape
|
||||
var shape = new ConcavePolygonShape3D();
|
||||
shape.SetFaces(allFaces.ToArray());
|
||||
|
||||
var mergedBody = new StaticBody3D();
|
||||
mergedBody.Name = "MergedCollision";
|
||||
mergedBody.CollisionLayer = collisionLayer;
|
||||
mergedBody.CollisionMask = collisionMask;
|
||||
|
||||
var mergedShape = new CollisionShape3D();
|
||||
mergedShape.Name = "CollisionShape3D";
|
||||
mergedShape.Shape = shape;
|
||||
mergedBody.AddChild(mergedShape);
|
||||
|
||||
collisionContainer.AddChild(mergedBody);
|
||||
mergedBody.Owner = _sceneInstance;
|
||||
mergedShape.Owner = _sceneInstance;
|
||||
|
||||
// Remove from tree before saving
|
||||
RemoveChild(_sceneInstance);
|
||||
|
||||
// Restore mouse cursor
|
||||
Input.MouseMode = Input.MouseModeEnum.Visible;
|
||||
|
||||
// Save the modified scene
|
||||
SaveScene();
|
||||
|
||||
Log($"\n[b]Complete![/b] Merged into single collision with {allFaces.Count / 3} triangles");
|
||||
}
|
||||
|
||||
private void GenerateMergedCollision(
|
||||
List<MeshInstance3D> meshInstances,
|
||||
Node3D collisionContainer,
|
||||
uint collisionLayer,
|
||||
uint collisionMask,
|
||||
bool skipSmall,
|
||||
float minSize)
|
||||
{
|
||||
Log("[color=cyan]Using MERGED collision mode - combining all meshes into single shape[/color]");
|
||||
|
||||
// Collect all vertices from all meshes, transformed to world space
|
||||
var allVertices = new List<Vector3>();
|
||||
int meshCount = 0;
|
||||
int skipped = 0;
|
||||
|
||||
foreach (var meshInstance in meshInstances)
|
||||
{
|
||||
if (meshInstance.Mesh == null)
|
||||
continue;
|
||||
|
||||
// Filter by size if enabled
|
||||
if (skipSmall)
|
||||
{
|
||||
var aabb = meshInstance.Mesh.GetAabb();
|
||||
float maxExtent = Mathf.Max(aabb.Size.X, Mathf.Max(aabb.Size.Y, aabb.Size.Z));
|
||||
if (maxExtent < minSize)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Get mesh data and transform vertices to world space
|
||||
var globalTransform = meshInstance.GlobalTransform;
|
||||
var mesh = meshInstance.Mesh;
|
||||
|
||||
for (int surfaceIdx = 0; surfaceIdx < mesh.GetSurfaceCount(); surfaceIdx++)
|
||||
{
|
||||
var arrays = mesh.SurfaceGetArrays(surfaceIdx);
|
||||
if (arrays == null || arrays.Count == 0)
|
||||
continue;
|
||||
|
||||
var vertices = arrays[(int)Mesh.ArrayType.Vertex].AsVector3Array();
|
||||
if (vertices == null)
|
||||
continue;
|
||||
|
||||
foreach (var vertex in vertices)
|
||||
{
|
||||
// Transform vertex to world space
|
||||
allVertices.Add(globalTransform * vertex);
|
||||
}
|
||||
}
|
||||
|
||||
meshCount++;
|
||||
}
|
||||
|
||||
Log($" Processed {meshCount} meshes, skipped {skipped} small meshes");
|
||||
Log($" Total vertices: {allVertices.Count}");
|
||||
|
||||
if (allVertices.Count < 3)
|
||||
{
|
||||
LogError("Not enough vertices to create collision shape");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a single ConcavePolygonShape3D from all vertices
|
||||
// We need to reconstruct faces - for merged collision, we'll collect faces instead
|
||||
var allFaces = new List<Vector3>();
|
||||
|
||||
foreach (var meshInstance in meshInstances)
|
||||
{
|
||||
if (meshInstance.Mesh == null)
|
||||
continue;
|
||||
|
||||
if (skipSmall)
|
||||
{
|
||||
var aabb = meshInstance.Mesh.GetAabb();
|
||||
float maxExtent = Mathf.Max(aabb.Size.X, Mathf.Max(aabb.Size.Y, aabb.Size.Z));
|
||||
if (maxExtent < minSize)
|
||||
continue;
|
||||
}
|
||||
|
||||
var globalTransform = meshInstance.GlobalTransform;
|
||||
var mesh = meshInstance.Mesh;
|
||||
|
||||
for (int surfaceIdx = 0; surfaceIdx < mesh.GetSurfaceCount(); surfaceIdx++)
|
||||
{
|
||||
var arrays = mesh.SurfaceGetArrays(surfaceIdx);
|
||||
if (arrays == null || arrays.Count == 0)
|
||||
continue;
|
||||
|
||||
var vertices = arrays[(int)Mesh.ArrayType.Vertex].AsVector3Array();
|
||||
var indices = arrays[(int)Mesh.ArrayType.Index].AsInt32Array();
|
||||
|
||||
if (vertices == null)
|
||||
continue;
|
||||
|
||||
if (indices != null && indices.Length > 0)
|
||||
{
|
||||
// Indexed mesh - use indices to build triangles
|
||||
for (int i = 0; i + 2 < indices.Length; i += 3)
|
||||
{
|
||||
allFaces.Add(globalTransform * vertices[indices[i]]);
|
||||
allFaces.Add(globalTransform * vertices[indices[i + 1]]);
|
||||
allFaces.Add(globalTransform * vertices[indices[i + 2]]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-indexed mesh - vertices are already in triangle order
|
||||
for (int i = 0; i + 2 < vertices.Length; i += 3)
|
||||
{
|
||||
allFaces.Add(globalTransform * vertices[i]);
|
||||
allFaces.Add(globalTransform * vertices[i + 1]);
|
||||
allFaces.Add(globalTransform * vertices[i + 2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log($" Total triangles: {allFaces.Count / 3}");
|
||||
|
||||
if (allFaces.Count < 9)
|
||||
{
|
||||
LogError("Not enough faces to create collision shape");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the merged collision shape
|
||||
var shape = new ConcavePolygonShape3D();
|
||||
shape.SetFaces(allFaces.ToArray());
|
||||
|
||||
// Create single StaticBody3D at origin (vertices are already in world space)
|
||||
var staticBody = new StaticBody3D();
|
||||
staticBody.Name = "MergedCollision";
|
||||
staticBody.CollisionLayer = collisionLayer;
|
||||
staticBody.CollisionMask = collisionMask;
|
||||
|
||||
var collisionShape = new CollisionShape3D();
|
||||
collisionShape.Name = "CollisionShape3D";
|
||||
collisionShape.Shape = shape;
|
||||
staticBody.AddChild(collisionShape);
|
||||
|
||||
collisionContainer.AddChild(staticBody);
|
||||
staticBody.Owner = _sceneInstance;
|
||||
collisionShape.Owner = _sceneInstance;
|
||||
|
||||
Log($"\n[b]Complete![/b] Created single merged collision with {allFaces.Count / 3} triangles");
|
||||
}
|
||||
|
||||
private void GeneratePerMeshCollision(
|
||||
List<MeshInstance3D> meshInstances,
|
||||
Node3D collisionContainer,
|
||||
CollisionMode collisionMode,
|
||||
uint collisionLayer,
|
||||
uint collisionMask,
|
||||
bool skipSmall,
|
||||
float minSize)
|
||||
{
|
||||
int generated = 0;
|
||||
int skipped = 0;
|
||||
|
||||
// Create or get a single StaticBody3D to hold all collision shapes
|
||||
var staticBody = collisionContainer.GetNodeOrNull<StaticBody3D>("GeneratedStaticBody");
|
||||
if (staticBody == null)
|
||||
{
|
||||
staticBody = new StaticBody3D();
|
||||
staticBody.Name = "GeneratedStaticBody";
|
||||
staticBody.CollisionLayer = collisionLayer;
|
||||
staticBody.CollisionMask = collisionMask;
|
||||
collisionContainer.AddChild(staticBody);
|
||||
staticBody.Owner = _sceneInstance;
|
||||
}
|
||||
|
||||
foreach (var meshInstance in meshInstances)
|
||||
{
|
||||
if (meshInstance.Mesh == null)
|
||||
continue;
|
||||
|
||||
// Filter by size if enabled
|
||||
if (skipSmall)
|
||||
{
|
||||
var aabb = meshInstance.Mesh.GetAabb();
|
||||
float maxExtent = Mathf.Max(aabb.Size.X, Mathf.Max(aabb.Size.Y, aabb.Size.Z));
|
||||
if (maxExtent < minSize)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we already generated collision for this mesh
|
||||
var collisionName = GetUniqueMeshName(meshInstance);
|
||||
if (staticBody.HasNode(collisionName))
|
||||
{
|
||||
Log($" [color=yellow]Skipping[/color] {GetNodePath(meshInstance)} - already has collision");
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var shape = CreateCollisionShape(meshInstance.Mesh, collisionMode);
|
||||
if (shape == null)
|
||||
{
|
||||
LogError($" Failed to create shape for {meshInstance.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var collisionShape = new CollisionShape3D();
|
||||
collisionShape.Name = collisionName;
|
||||
collisionShape.Shape = shape;
|
||||
collisionShape.GlobalTransform = meshInstance.GlobalTransform;
|
||||
|
||||
staticBody.AddChild(collisionShape);
|
||||
collisionShape.Owner = _sceneInstance;
|
||||
|
||||
generated++;
|
||||
}
|
||||
|
||||
Log($"\n[b]Complete![/b] Generated: {generated}, Skipped: {skipped}");
|
||||
}
|
||||
|
||||
private string GetUniqueMeshName(MeshInstance3D mesh)
|
||||
{
|
||||
// Create a unique name based on the mesh's path in the scene
|
||||
var path = GetNodePath(mesh);
|
||||
return path.Replace("/", "_").Replace(" ", "_");
|
||||
}
|
||||
|
||||
private void ClearCollisionFromScene()
|
||||
{
|
||||
Log($"\n[b]Clearing collision from:[/b] {_targetScenePath}");
|
||||
|
||||
// Reload fresh instance - bypass cache
|
||||
ResourceLoader.Load<PackedScene>(_targetScenePath, cacheMode: ResourceLoader.CacheMode.Ignore);
|
||||
_loadedScene = GD.Load<PackedScene>(_targetScenePath);
|
||||
_sceneInstance = _loadedScene.Instantiate();
|
||||
|
||||
// Add to tree temporarily (required for proper scene packing)
|
||||
AddChild(_sceneInstance);
|
||||
|
||||
// Simply remove the _GeneratedCollision container if it exists
|
||||
var collisionContainer = _sceneInstance.GetNodeOrNull<Node3D>("_GeneratedCollision");
|
||||
if (collisionContainer == null)
|
||||
{
|
||||
RemoveChild(_sceneInstance);
|
||||
_sceneInstance.Free();
|
||||
Log("[color=yellow]No generated collision found in this scene[/color]");
|
||||
return;
|
||||
}
|
||||
|
||||
int cleared = collisionContainer.GetChildCount();
|
||||
|
||||
// Must use RemoveChild + Free, not QueueFree (which waits until next frame)
|
||||
_sceneInstance.RemoveChild(collisionContainer);
|
||||
collisionContainer.Free();
|
||||
|
||||
// Remove from tree before saving
|
||||
RemoveChild(_sceneInstance);
|
||||
|
||||
// Restore mouse cursor
|
||||
Input.MouseMode = Input.MouseModeEnum.Visible;
|
||||
|
||||
// Save the modified scene
|
||||
SaveScene();
|
||||
|
||||
Log($"\n[b]Complete![/b] Cleared {cleared} collision nodes");
|
||||
}
|
||||
|
||||
private void SaveScene()
|
||||
{
|
||||
var packedScene = new PackedScene();
|
||||
var error = packedScene.Pack(_sceneInstance);
|
||||
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
LogError($"Failed to pack scene: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
error = ResourceSaver.Save(packedScene, _targetScenePath);
|
||||
|
||||
if (error != Error.Ok)
|
||||
{
|
||||
LogError($"Failed to save scene: {error}");
|
||||
return;
|
||||
}
|
||||
|
||||
Log($"[color=green]Saved:[/color] {_targetScenePath}");
|
||||
}
|
||||
|
||||
private void CollectMeshInstances(Node node, List<MeshInstance3D> meshInstances)
|
||||
{
|
||||
if (node is MeshInstance3D meshInstance)
|
||||
{
|
||||
meshInstances.Add(meshInstance);
|
||||
}
|
||||
|
||||
foreach (var child in node.GetChildren())
|
||||
{
|
||||
CollectMeshInstances(child, meshInstances);
|
||||
}
|
||||
}
|
||||
|
||||
private void CollectExistingCollision(Node node, List<Node> collisionNodes)
|
||||
{
|
||||
if (node is CollisionShape3D || node is StaticBody3D)
|
||||
{
|
||||
collisionNodes.Add(node);
|
||||
}
|
||||
|
||||
foreach (var child in node.GetChildren())
|
||||
{
|
||||
CollectExistingCollision(child, collisionNodes);
|
||||
}
|
||||
}
|
||||
|
||||
private void CollectGeneratedCollision(Node node, List<Node> toRemove, List<StaticBody3D> toUnwrap)
|
||||
{
|
||||
// StaticBody3D nodes we created (named *_collision)
|
||||
if (node is StaticBody3D staticBody && staticBody.Name.ToString().EndsWith("_collision"))
|
||||
{
|
||||
toUnwrap.Add(staticBody);
|
||||
return;
|
||||
}
|
||||
|
||||
// CollisionShape3D nodes that are direct children of MeshInstance3D
|
||||
if (node is CollisionShape3D && node.GetParent() is MeshInstance3D)
|
||||
{
|
||||
toRemove.Add(node);
|
||||
}
|
||||
|
||||
foreach (var child in node.GetChildren())
|
||||
{
|
||||
CollectGeneratedCollision(child, toRemove, toUnwrap);
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasExistingCollision(MeshInstance3D meshInstance)
|
||||
{
|
||||
// Check for collision shape as child
|
||||
foreach (var child in meshInstance.GetChildren())
|
||||
{
|
||||
if (child is CollisionShape3D)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for sibling StaticBody3D with matching name
|
||||
var parent = meshInstance.GetParent();
|
||||
if (parent != null)
|
||||
{
|
||||
var collisionName = $"{meshInstance.Name}_collision";
|
||||
foreach (var sibling in parent.GetChildren())
|
||||
{
|
||||
if (sibling is StaticBody3D && sibling.Name == collisionName)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if parent is a StaticBody3D we created
|
||||
if (parent is StaticBody3D sb && sb.Name.ToString().EndsWith("_collision"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Shape3D CreateCollisionShape(Mesh mesh, CollisionMode mode)
|
||||
{
|
||||
return mode switch
|
||||
{
|
||||
CollisionMode.Trimesh => mesh.CreateTrimeshShape(),
|
||||
CollisionMode.Convex => mesh.CreateConvexShape(),
|
||||
CollisionMode.MultiConvex => mesh.CreateTrimeshShape(), // Fallback
|
||||
_ => mesh.CreateTrimeshShape()
|
||||
};
|
||||
}
|
||||
|
||||
private string GetNodePath(Node node)
|
||||
{
|
||||
var path = node.Name.ToString();
|
||||
var parent = node.GetParent();
|
||||
while (parent != null && parent != _sceneInstance)
|
||||
{
|
||||
path = $"{parent.Name}/{path}";
|
||||
parent = parent.GetParent();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
_logOutput.AppendText(message + "\n");
|
||||
GD.Print(message.Replace("[b]", "").Replace("[/b]", "")
|
||||
.Replace("[color=green]", "").Replace("[color=yellow]", "")
|
||||
.Replace("[color=cyan]", "").Replace("[color=orange]", "")
|
||||
.Replace("[color=red]", "").Replace("[/color]", ""));
|
||||
}
|
||||
|
||||
private void LogError(string message)
|
||||
{
|
||||
Log($"[color=red]ERROR: {message}[/color]");
|
||||
}
|
||||
}
|
||||
1
Scripts/Tools/CollisionGenerator.cs.uid
Normal file
1
Scripts/Tools/CollisionGenerator.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cnmnvswllcl8t
|
||||
306
Scripts/Tools/PerformanceProfiler.cs
Normal file
306
Scripts/Tools/PerformanceProfiler.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Voider.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Unified performance profiling and monitoring tool
|
||||
/// Combines real-time overlay, spike detection, and detailed logging
|
||||
///
|
||||
/// Controls:
|
||||
/// F9 - Toggle overlay visibility
|
||||
/// F10 - Toggle spike logging (to console)
|
||||
/// F11 - Dump spike log to file
|
||||
/// F12 - Print performance summary
|
||||
/// </summary>
|
||||
public partial class PerformanceProfiler : CanvasLayer
|
||||
{
|
||||
#region Export Properties
|
||||
[Export] public bool EnableOverlay { get; set; } = false;
|
||||
[Export] public bool EnableSpikeLogging { get; set; } = false;
|
||||
[Export] public float SpikeThresholdMs { get; set; } = 20.0f;
|
||||
[Export] public int MaxSpikeHistory { get; set; } = 100;
|
||||
[Export] public int SampleWindowSize { get; set; } = 60;
|
||||
#endregion
|
||||
|
||||
#region UI Elements
|
||||
private Label _fpsLabel;
|
||||
private Label _frameTimeLabel;
|
||||
private Label _processTimeLabel;
|
||||
private Label _physicsTimeLabel;
|
||||
private Label _memoryLabel;
|
||||
private Label _drawCallsLabel;
|
||||
private VBoxContainer _container;
|
||||
#endregion
|
||||
|
||||
#region Performance Tracking
|
||||
private List<double> _frameTimeSamples = new List<double>();
|
||||
private List<FrameSpikeData> _spikeHistory = new List<FrameSpikeData>();
|
||||
private double _lastFrameTime;
|
||||
#endregion
|
||||
|
||||
private struct FrameSpikeData
|
||||
{
|
||||
public double Timestamp;
|
||||
public double FrameMs;
|
||||
public double ProcessMs;
|
||||
public double PhysicsMs;
|
||||
public int DrawCalls;
|
||||
public int NodeCount;
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
CreateUI();
|
||||
_container.Visible = EnableOverlay;
|
||||
|
||||
GD.Print("[PerformanceProfiler] Initialized");
|
||||
GD.Print(" F9 - Toggle overlay");
|
||||
GD.Print(" F10 - Toggle spike logging");
|
||||
GD.Print(" F11 - Dump spike log to file");
|
||||
GD.Print(" F12 - Print summary");
|
||||
}
|
||||
|
||||
private void CreateUI()
|
||||
{
|
||||
// Main container
|
||||
_container = new VBoxContainer
|
||||
{
|
||||
Position = new Vector2(10, 10)
|
||||
};
|
||||
AddChild(_container);
|
||||
|
||||
// Title
|
||||
var titleLabel = new Label
|
||||
{
|
||||
Text = "Performance Monitor"
|
||||
};
|
||||
titleLabel.AddThemeFontSizeOverride("font_size", 18);
|
||||
_container.AddChild(titleLabel);
|
||||
|
||||
// Separator
|
||||
var separator = new HSeparator();
|
||||
_container.AddChild(separator);
|
||||
|
||||
// FPS
|
||||
_fpsLabel = CreateLabel("FPS: --");
|
||||
_container.AddChild(_fpsLabel);
|
||||
|
||||
// Frame time
|
||||
_frameTimeLabel = CreateLabel("Frame: -- ms");
|
||||
_container.AddChild(_frameTimeLabel);
|
||||
|
||||
// Process time
|
||||
_processTimeLabel = CreateLabel("Process: -- ms");
|
||||
_container.AddChild(_processTimeLabel);
|
||||
|
||||
// Physics time
|
||||
_physicsTimeLabel = CreateLabel("Physics: -- ms");
|
||||
_container.AddChild(_physicsTimeLabel);
|
||||
|
||||
// Memory
|
||||
_memoryLabel = CreateLabel("Memory: -- MB");
|
||||
_container.AddChild(_memoryLabel);
|
||||
|
||||
// Draw calls
|
||||
_drawCallsLabel = CreateLabel("Draw Calls: --");
|
||||
_container.AddChild(_drawCallsLabel);
|
||||
|
||||
// Add styling
|
||||
var styleBox = new StyleBoxFlat
|
||||
{
|
||||
BgColor = new Color(0, 0, 0, 0.7f)
|
||||
};
|
||||
styleBox.SetContentMarginAll(10);
|
||||
_container.AddThemeStyleboxOverride("panel", styleBox);
|
||||
}
|
||||
|
||||
private Label CreateLabel(string text)
|
||||
{
|
||||
var label = new Label { Text = text };
|
||||
label.AddThemeFontSizeOverride("font_size", 14);
|
||||
return label;
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
var frameMs = delta * 1000.0;
|
||||
_lastFrameTime = frameMs;
|
||||
|
||||
// Track frame time samples
|
||||
_frameTimeSamples.Add(frameMs);
|
||||
if (_frameTimeSamples.Count > SampleWindowSize)
|
||||
{
|
||||
_frameTimeSamples.RemoveAt(0);
|
||||
}
|
||||
|
||||
// Get performance metrics
|
||||
var processMs = Performance.GetMonitor(Performance.Monitor.TimeProcess) * 1000.0;
|
||||
var physicsMs = Performance.GetMonitor(Performance.Monitor.TimePhysicsProcess) * 1000.0;
|
||||
var drawCalls = (int)Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame);
|
||||
var nodeCount = (int)Performance.GetMonitor(Performance.Monitor.ObjectNodeCount);
|
||||
var memoryMb = Performance.GetMonitor(Performance.Monitor.MemoryStatic) / 1_048_576.0;
|
||||
|
||||
// Check for spikes
|
||||
if (EnableSpikeLogging && frameMs > SpikeThresholdMs)
|
||||
{
|
||||
LogSpike(frameMs, processMs, physicsMs, drawCalls, nodeCount);
|
||||
}
|
||||
|
||||
// Update overlay
|
||||
if (_container.Visible)
|
||||
{
|
||||
UpdateOverlay(frameMs, processMs, physicsMs, memoryMb, drawCalls);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOverlay(double frameMs, double processMs, double physicsMs, double memoryMb, int drawCalls)
|
||||
{
|
||||
var fps = 1000.0 / frameMs;
|
||||
|
||||
_fpsLabel.Text = $"FPS: {fps:F1}";
|
||||
_frameTimeLabel.Text = $"Frame: {frameMs:F2} ms";
|
||||
_processTimeLabel.Text = $"Process: {processMs:F2} ms";
|
||||
_physicsTimeLabel.Text = $"Physics: {physicsMs:F2} ms";
|
||||
_memoryLabel.Text = $"Memory: {memoryMb:F1} MB";
|
||||
_drawCallsLabel.Text = $"Draw Calls: {drawCalls}";
|
||||
|
||||
// Color code frame time
|
||||
if (frameMs > 16.67)
|
||||
{
|
||||
_frameTimeLabel.AddThemeColorOverride("font_color", Colors.Red);
|
||||
}
|
||||
else if (frameMs > 8.33)
|
||||
{
|
||||
_frameTimeLabel.AddThemeColorOverride("font_color", Colors.Yellow);
|
||||
}
|
||||
else
|
||||
{
|
||||
_frameTimeLabel.AddThemeColorOverride("font_color", Colors.Green);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogSpike(double frameMs, double processMs, double physicsMs, int drawCalls, int nodeCount)
|
||||
{
|
||||
var spike = new FrameSpikeData
|
||||
{
|
||||
Timestamp = Time.GetTicksMsec() / 1000.0,
|
||||
FrameMs = frameMs,
|
||||
ProcessMs = processMs,
|
||||
PhysicsMs = physicsMs,
|
||||
DrawCalls = drawCalls,
|
||||
NodeCount = nodeCount
|
||||
};
|
||||
|
||||
_spikeHistory.Add(spike);
|
||||
if (_spikeHistory.Count > MaxSpikeHistory)
|
||||
{
|
||||
_spikeHistory.RemoveAt(0);
|
||||
}
|
||||
|
||||
GD.Print($"[SPIKE] Frame: {frameMs:F2}ms | Process: {processMs:F2}ms | Physics: {physicsMs:F2}ms | Draw Calls: {drawCalls} | Nodes: {nodeCount}");
|
||||
}
|
||||
|
||||
private void PrintSummary()
|
||||
{
|
||||
if (_frameTimeSamples.Count == 0)
|
||||
{
|
||||
GD.Print("[PerformanceProfiler] No data to summarize");
|
||||
return;
|
||||
}
|
||||
|
||||
double avgFrame = 0, minFrame = double.MaxValue, maxFrame = 0;
|
||||
|
||||
foreach (var sample in _frameTimeSamples)
|
||||
{
|
||||
avgFrame += sample;
|
||||
if (sample < minFrame) minFrame = sample;
|
||||
if (sample > maxFrame) maxFrame = sample;
|
||||
}
|
||||
avgFrame /= _frameTimeSamples.Count;
|
||||
|
||||
var avgFps = 1000.0 / avgFrame;
|
||||
var processMs = Performance.GetMonitor(Performance.Monitor.TimeProcess) * 1000.0;
|
||||
var physicsMs = Performance.GetMonitor(Performance.Monitor.TimePhysicsProcess) * 1000.0;
|
||||
var drawCalls = (int)Performance.GetMonitor(Performance.Monitor.RenderTotalDrawCallsInFrame);
|
||||
var nodeCount = (int)Performance.GetMonitor(Performance.Monitor.ObjectNodeCount);
|
||||
var memoryMb = Performance.GetMonitor(Performance.Monitor.MemoryStatic) / 1_048_576.0;
|
||||
|
||||
GD.Print("========================================");
|
||||
GD.Print($"[PerformanceProfiler] Summary (last {_frameTimeSamples.Count} frames):");
|
||||
GD.Print($" Frame Time: Avg={avgFrame:F2}ms, Min={minFrame:F2}ms, Max={maxFrame:F2}ms");
|
||||
GD.Print($" Average FPS: {avgFps:F1}");
|
||||
GD.Print($" Current Process: {processMs:F2}ms");
|
||||
GD.Print($" Current Physics: {physicsMs:F2}ms");
|
||||
GD.Print($" Total Nodes: {nodeCount}");
|
||||
GD.Print($" Draw Calls: {drawCalls}");
|
||||
GD.Print($" Memory Usage: {memoryMb:F1} MB");
|
||||
GD.Print($" Spikes Logged: {_spikeHistory.Count} (>{SpikeThresholdMs}ms)");
|
||||
GD.Print("========================================");
|
||||
}
|
||||
|
||||
private void DumpSpikeLog()
|
||||
{
|
||||
if (_spikeHistory.Count == 0)
|
||||
{
|
||||
GD.Print("[PerformanceProfiler] No spikes to dump");
|
||||
return;
|
||||
}
|
||||
|
||||
var logPath = $"user://performance_spikes_{DateTime.Now:yyyyMMdd_HHmmss}.log";
|
||||
using var file = Godot.FileAccess.Open(logPath, Godot.FileAccess.ModeFlags.Write);
|
||||
|
||||
if (file == null)
|
||||
{
|
||||
GD.PrintErr($"[PerformanceProfiler] Failed to create log file: {logPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
file.StoreLine("=== Performance Spike Log ===");
|
||||
file.StoreLine($"Total Spikes: {_spikeHistory.Count}");
|
||||
file.StoreLine($"Threshold: {SpikeThresholdMs}ms");
|
||||
file.StoreLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
|
||||
file.StoreLine("");
|
||||
file.StoreLine("Timestamp(s) | Frame(ms) | Process(ms) | Physics(ms) | Draw Calls | Nodes");
|
||||
file.StoreLine("-----------------------------------------------------------------------------");
|
||||
|
||||
foreach (var spike in _spikeHistory)
|
||||
{
|
||||
file.StoreLine($"{spike.Timestamp:F2} | {spike.FrameMs:F2} | {spike.ProcessMs:F2} | {spike.PhysicsMs:F2} | {spike.DrawCalls} | {spike.NodeCount}");
|
||||
}
|
||||
|
||||
file.Close();
|
||||
|
||||
var absPath = ProjectSettings.GlobalizePath(logPath);
|
||||
GD.Print($"[PerformanceProfiler] Spike log written to: {absPath}");
|
||||
GD.Print($"[PerformanceProfiler] Total spikes logged: {_spikeHistory.Count}");
|
||||
}
|
||||
|
||||
public override void _Input(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventKey keyEvent && keyEvent.Pressed)
|
||||
{
|
||||
switch (keyEvent.Keycode)
|
||||
{
|
||||
case Key.F9:
|
||||
_container.Visible = !_container.Visible;
|
||||
GD.Print($"[PerformanceProfiler] Overlay {(_container.Visible ? "SHOWN" : "HIDDEN")}");
|
||||
break;
|
||||
|
||||
case Key.F10:
|
||||
EnableSpikeLogging = !EnableSpikeLogging;
|
||||
GD.Print($"[PerformanceProfiler] Spike logging {(EnableSpikeLogging ? "ENABLED" : "DISABLED")} (>{SpikeThresholdMs}ms)");
|
||||
break;
|
||||
|
||||
case Key.F11:
|
||||
DumpSpikeLog();
|
||||
break;
|
||||
|
||||
case Key.F12:
|
||||
PrintSummary();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Scripts/Tools/PerformanceProfiler.cs.uid
Normal file
1
Scripts/Tools/PerformanceProfiler.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://uw7vngdxp7s
|
||||
Reference in New Issue
Block a user