diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3887df0 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,154 @@ +# SharpPhysics Project Instructions + +## Project Overview + +SharpPhysics is a 2D physics engine built with C# (.NET 9) and SFML.NET for rendering. It includes: + +- **physics/** (`SharpPhysics.csproj`) - Core physics engine library +- **SharpPhysics.Demo/** - Demo games showcasing the engine +- **PoseIntegrator.Vision/** - YOLO-based pose detection for body tracking +- **PoseIntegrator.Demo/** - Standalone pose detection demo + +## Architecture Principles + +### Engine vs Game Separation + +The `physics/` project is the **core engine** and should contain only: +- Physics simulation (`PhysicsSystem`, collision detection, constraints) +- Core rendering infrastructure (`Renderer`, primitive drawing) +- Input handling (`InputManager`) +- Game loop (`GameEngine`, `IGame` interface) +- Reusable UI components (`UiManager`, `UiButton`, `UiSlider`, etc.) +- Object templates for creating physics objects + +**DO NOT** put game-specific code in the engine: +- Debug UIs specific to one game → put in Demo project +- Visual effects only used by demos → put in Demo project +- Game-specific scene builders → put in Demo project + +### Render Layers + +Games implement `IGame` with these render methods: +1. `RenderBackground(Renderer)` - Optional, renders behind physics objects +2. `Render(Renderer)` - Required, renders in front of physics objects + +The engine calls them in order: Background → Physics Objects → Foreground + +### Naming Conventions + +- Be honest with names - don't call something a "ParticleSystem" if it's just floating circles +- Use descriptive names: `AnimatedBackground`, `SandboxDebugUI`, `DemoSceneBuilder` +- Prefix demo-specific classes appropriately + +## Code Style + +### General +- Use `#nullable enable` at the top of all files +- Use file-scoped namespaces (`namespace X;` not `namespace X { }`) +- Minimize comments - code should be self-documenting +- Remove commented-out code blocks + +### Fields and Properties +- Private fields: `_camelCase` +- Properties: `PascalCase` +- Constants: `PascalCase` or `UPPER_SNAKE_CASE` for true constants + +### Methods +- Keep methods focused and small +- Extract helper methods for clarity +- Group related functionality with `#region` sparingly + +## Project Structure + +``` +physics/Engine/ +├── Core/ # GameEngine, IGame interface +├── Rendering/ # Renderer, UI components +├── Input/ # InputManager +├── Objects/ # PhysicsObject +├── Shapes/ # CirclePhysShape, PolygonPhysShape, etc. +├── Shaders/ # SFML shader implementations +├── Constraints/ # Physics constraints (Weld, Axis, Spring) +├── Helpers/ # Math utilities, extensions +└── Classes/ # Templates, collision data + +SharpPhysics.Demo/ +├── DemoProps/ # Demo-specific components (SandboxDebugUI, DemoSceneBuilder) +├── Helpers/ # Demo utilities (AnimatedBackground, SkeletonRenderer) +├── Integration/ # PersonColliderBridge for pose tracking +├── Designer/ # Prefab designer game +├── Settings/ # GameSettings configuration +└── [Game].cs # Individual game implementations +``` + +## Common Patterns + +### Creating a New Game + +```csharp +public class MyGame : IGame +{ + private GameEngine _engine = null!; + + public void Initialize(GameEngine engine) + { + _engine = engine; + // Setup code here + } + + public void Update(float deltaTime, InputManager input) { } + + public void RenderBackground(Renderer renderer) { } // Optional + + public void Render(Renderer renderer) { } + + public void Shutdown() { } +} +``` + +### Debug UI (Game-Specific) + +Debug/development UIs should be in the Demo project, not the engine: + +```csharp +// In SharpPhysics.Demo/DemoProps/ +public class MyGameDebugUI +{ + private readonly UiManager _uiManager = new(); + + public void Render(Renderer renderer) + { + renderer.Window.SetView(renderer.UiView); + _uiManager.Draw(renderer.Window); + } +} +``` + +### Physics Object Creation + +Use `ObjectTemplates` for creating physics objects: + +```csharp +var templates = new ObjectTemplates(engine.PhysicsSystem); +var ball = templates.CreateSmallBall(x, y); +var box = templates.CreateBox(position, width, height); +``` + +### Constraints + +```csharp +engine.AddWeldConstraint(bodyA, bodyB, anchorA, anchorB); // Rigid +engine.AddAxisConstraint(bodyA, bodyB, anchorA, anchorB); // Rotating joint +engine.AddSpringConstraint(bodyA, bodyB); // Spring +``` + +## Dependencies + +- **SFML.NET** - Windowing and rendering +- **System.Numerics** - Vector math (use `Vector2` from this, not SFML's) +- **OpenCvSharp4** - Camera capture (PoseIntegrator.Vision) +- **Microsoft.ML.OnnxRuntime** - YOLO inference (PoseIntegrator.Vision) + +## Testing + +Run `dotnet build` to verify changes compile. The solution includes `SharpPhysics.Tests` for unit tests. diff --git a/SharpPhysics.Demo/BubblePopGame.cs b/SharpPhysics.Demo/BubblePopGame.cs index 28dcdd5..9248a93 100644 --- a/SharpPhysics.Demo/BubblePopGame.cs +++ b/SharpPhysics.Demo/BubblePopGame.cs @@ -60,9 +60,6 @@ public void Initialize(GameEngine engine) _engine = engine; _physics = engine.PhysicsSystem; - // Hide debug UI for cleaner game experience - _engine.Renderer.ShowDebugUI = false; - // Very light upward "gravity" for floating bubbles _physics.Gravity = new Vector2(0, -2f); _physics.GravityScale = 15f; diff --git a/SharpPhysics.Demo/DemoGame.cs b/SharpPhysics.Demo/DemoGame.cs index 5c8e0a6..123a604 100644 --- a/SharpPhysics.Demo/DemoGame.cs +++ b/SharpPhysics.Demo/DemoGame.cs @@ -1,15 +1,12 @@ #nullable enable using System.Numerics; -using System.Runtime.ConstrainedExecution; using physics.Engine; using physics.Engine.Classes.ObjectTemplates; -using physics.Engine.Constraints; using physics.Engine.Core; using physics.Engine.Helpers; using physics.Engine.Input; using physics.Engine.Objects; using physics.Engine.Rendering; -using physics.Engine.Shaders; using SFML.Graphics; using SFML.Window; using SharpPhysics.Demo.DemoProps; @@ -20,10 +17,6 @@ namespace SharpPhysics.Demo; -/// -/// Demo game showcasing SharpPhysics engine capabilities. -/// Includes physics balls, player controller, and person detection integration. -/// public class DemoGame : IGame { private GameEngine _engine = null!; @@ -32,44 +25,45 @@ public class DemoGame : IGame private PlayerController _playerController = null!; private PersonColliderBridge? _personColliderBridge; private PrefabLoader _prefabLoader = null!; + private AnimatedBackground _background = null!; + private DemoSceneBuilder _sceneBuilder = null!; + private SandboxDebugUI _debugUI = null!; - // Props - private DemoGameCar? DemoCar; + private DemoGameCar? _demoCar; private readonly List _loadedPrefabs = new(); - // Physics sandbox input state - private bool _isGrabbing = false; - private bool _isMousePressedLeft = false; - private bool _isMousePressedRight = false; - private bool _isCreatingBox = false; + // Sandbox input state + private bool _isGrabbing; + private bool _isMousePressedLeft; + private bool _isMousePressedRight; + private bool _isCreatingBox; private Vector2 _startPoint; private Vector2 _mousePosition; private Vector2 _boxStartPoint; private Vector2 _boxEndPoint; - private float _launchTimer = 0f; + private float _launchTimer; private const float LaunchInterval = 0.035f; - // View panning state - private bool _isPanning = false; + // View panning + private bool _isPanning; private Vector2 _panStartScreenPos; - // Weld/Axis constraint creation state + // Constraint creation private enum ConstraintMode { None, Weld, Axis } private ConstraintMode _constraintMode = ConstraintMode.None; private PhysicsObject? _firstSelectedObject; - private Vector2 _firstClickWorldPos; // Store the click position for axis constraints + private Vector2 _firstClickWorldPos; public void Initialize(GameEngine engine) { _engine = engine; _objectTemplates = new ObjectTemplates(engine.PhysicsSystem); - _actionTemplates = new ActionTemplates(engine.PhysicsSystem, _objectTemplates); + _actionTemplates = new ActionTemplates(engine.PhysicsSystem); _prefabLoader = new PrefabLoader(engine, _objectTemplates); + _sceneBuilder = new DemoSceneBuilder(engine, _objectTemplates); + _background = new AnimatedBackground(engine.WindowWidth, engine.WindowHeight); + _debugUI = new SandboxDebugUI(engine); - // Show debug UI for sandbox mode - _engine.Renderer.ShowDebugUI = true; - - // Subscribe to window mouse/keyboard events for sandbox input _engine.Renderer.Window.MouseButtonPressed += OnMouseButtonPressed; _engine.Renderer.Window.MouseButtonReleased += OnMouseButtonReleased; _engine.Renderer.Window.MouseMoved += OnMouseMoved; @@ -77,265 +71,33 @@ public void Initialize(GameEngine engine) InitializeWorld(engine.WindowWidth, engine.WindowHeight); - // Enable or disable body detection if (GameSettings.Instance.PoseTrackingEnabled) - { InitializePersonDetection(engine.WindowWidth, engine.WindowHeight); - } - // Print help for new features PrintControls(); } private void PrintControls() { Console.WriteLine("=== DemoGame Controls ==="); - Console.WriteLine(" W - Weld mode (click two objects to weld)"); - Console.WriteLine(" A - Axis mode (click rotation center on body, then wheel)"); - Console.WriteLine(" Q - Cancel constraint mode"); - Console.WriteLine(" L - Load prefab at mouse position"); - Console.WriteLine(" ESC - Return to menu"); + Console.WriteLine(" W - Weld mode | A - Axis mode | Q - Cancel"); + Console.WriteLine(" L - Load prefab | ESC - Return to menu"); Console.WriteLine("========================"); } private void InitializeWorld(uint worldWidth, uint worldHeight) { - // Create walls - _objectTemplates.CreateWall(new Vector2(0, 0), 15, (int)worldHeight); - _objectTemplates.CreateWall(new Vector2((int)worldWidth - 15, 0), 15, (int)worldHeight); - _objectTemplates.CreateWall(new Vector2(0, 0), (int)worldWidth, 15); - _objectTemplates.CreateWall(new Vector2(0, (int)worldHeight - 15), (int)worldWidth, 15); + _sceneBuilder.CreateWalls(worldWidth, worldHeight); - // Create player var player = _objectTemplates.CreatePolygonCapsule(new Vector2(50, 20)); _playerController = new PlayerController(player); - // Demo objects - CreateCarDemo(worldWidth, worldHeight); - CreateDemoBridge(); - CreateDemoChain(); - CreateDemoSproket(); - CreateConcavePolygonDemo(); - } - - private void CreateConcavePolygonDemo() - { - // L-shaped concave polygon (decomposed into convex pieces and welded) - var lShapeVertices = new Vector2[] - { - new Vector2(0, 0), - new Vector2(60, 0), - new Vector2(60, 25), - new Vector2(25, 25), - new Vector2(25, 60), - new Vector2(0, 60) - }; - var lShape = _objectTemplates.CreateConcavePolygon(new Vector2(600, 100), lShapeVertices, canRotate: true, canBreak: true); - - //_ = _objectTemplates.CreatePolygonTriangle(new Vector2(400, 200)); - - //// Arrow-shaped concave polygon - //var arrowVertices = new Vector2[] - //{ - // new Vector2(0, 20), // Left notch top - // new Vector2(30, 20), // Shaft top-left - // new Vector2(30, 0), // Arrow head left - // new Vector2(60, 25), // Arrow tip - // new Vector2(30, 50), // Arrow head right - // new Vector2(30, 30), // Shaft bottom-left - // new Vector2(0, 30) // Left notch bottom - //}; - //var arrow = _objectTemplates.CreateConcavePolygon(new Vector2(700, 100), arrowVertices, canRotate: true, canBreak: true); - - // Star-like concave polygon (5-pointed, simplified) - var starVertices = new Vector2[] - { - new Vector2(25, 0), // Top point - new Vector2(30, 18), // Inner right-top - new Vector2(50, 18), // Right point - new Vector2(35, 30), // Inner right-bottom - new Vector2(40, 50), // Bottom-right point - new Vector2(25, 38), // Inner bottom - new Vector2(10, 50), // Bottom-left point - new Vector2(15, 30), // Inner left-bottom - new Vector2(0, 18), // Left point - new Vector2(20, 18) // Inner left-top - }; - - _ = _objectTemplates.CreateConcavePolygon(new Vector2(500, 100), starVertices, canRotate: true, canBreak: true); - } - - private void CreateCarDemo(uint worldWidth, uint worldHeight) - { - // Car body dimensions - float bodyWidth = 120f; - float bodyHeight = 30f; - float wheelRadius = 20f; - float wheelInset = 10f; // Distance from body edge to wheel center - - // Position car in center-ish area - float carX = worldWidth / 2f - bodyWidth / 2f; - float carY = worldHeight - 200f; // Near bottom but above floor - - // Create the car body (box) - var carBody = _objectTemplates.CreateBox(new Vector2(carX, carY), (int)bodyWidth, (int)bodyHeight); - - // Wheel X positions (in local body coordinates, where 0 = body center) - float frontWheelLocalX = bodyWidth / 2f - wheelInset; // Right side: +50 - float rearWheelLocalX = -bodyWidth / 2f + wheelInset; // Left side: -50 - - // Wheel world positions for spawning - float frontWheelWorldX = carX + bodyWidth / 2f + frontWheelLocalX; // carX + 60 + 50 = carX + 110 - float rearWheelWorldX = carX + bodyWidth / 2f + rearWheelLocalX; // carX + 60 - 50 = carX + 10 - float wheelWorldY = carY + bodyHeight + wheelRadius; - - // Create wheels (CreateMedBall takes top-left corner, ball radius is ~10) - var frontWheel = _objectTemplates.CreateLargeBall(frontWheelWorldX - 10, wheelWorldY - 10); - var rearWheel = _objectTemplates.CreateLargeBall(rearWheelWorldX - 10, wheelWorldY - 10); - - // Local anchors on body (relative to body center) - Vector2 frontAttachOnBody = new Vector2(frontWheelLocalX, bodyHeight / 2f + wheelRadius); - Vector2 rearAttachOnBody = new Vector2(rearWheelLocalX, bodyHeight / 2f + wheelRadius); - - _engine.AddAxisConstraint(carBody, frontWheel, frontAttachOnBody, Vector2.Zero); - _engine.AddAxisConstraint(carBody, rearWheel, rearAttachOnBody, Vector2.Zero); - - // === WELDED PARTS (rigid attachments) === - - // Spoiler on top-rear of car - float spoilerWidth = 40f; - float spoilerHeight = 8f; - // Position spoiler so its center aligns with where we want to attach - float spoilerLocalX = -bodyWidth / 2f + 75f; // X offset from body center - float spoilerLocalY = -bodyHeight / 2f - spoilerHeight / 2f - 2f; // Just above body - float spoilerWorldX = carX + bodyWidth + spoilerLocalX - spoilerWidth / 2f; - float spoilerWorldY = carY + bodyHeight / 2f + spoilerLocalY - spoilerHeight / 2f; - var spoiler = _objectTemplates.CreateBox(new Vector2(spoilerWorldX, spoilerWorldY), (int)spoilerWidth, (int)spoilerHeight); - spoiler.Angle = 10f; // Tilted angle - // Weld spoiler to body - use CENTER of spoiler (0,0) to minimize rotational coupling - Vector2 spoilerAttachOnBody = new Vector2(spoilerLocalX, spoilerLocalY); - Vector2 spoilerAttachOnSpoiler = Vector2.Zero; // Center of spoiler - - // Add Weld - _engine.AddWeldConstraint(carBody, spoiler, spoilerAttachOnBody, spoilerAttachOnSpoiler); - - // Front bumper - float bumperWidth = 10f; - float bumperHeight = 20f; - float bumperWorldX = carX + bodyWidth; // At front (right side) - float bumperWorldY = carY + 5f; - var frontBumper = _objectTemplates.CreateBox(new Vector2(bumperWorldX, bumperWorldY), (int)bumperWidth, (int)bumperHeight); - - // Weld bumper to body - // Local anchor on body: front-center edge - Vector2 bumperAttachOnBody = new Vector2(bodyWidth / 2f, 0f); - // Local anchor on bumper: left-center edge - Vector2 bumperAttachOnBumper = new Vector2(-bumperWidth / 2f, 0f); - - // Add Weld - _engine.AddWeldConstraint(carBody, frontBumper, bumperAttachOnBody, bumperAttachOnBumper); - - // Rear bumper - float rearBumperWorldX = carX - bumperWidth; - var rearBumper = _objectTemplates.CreateBox(new Vector2(rearBumperWorldX, bumperWorldY), (int)bumperWidth, (int)bumperHeight); - - // Weld rear bumper to body - Vector2 rearBumperAttachOnBody = new Vector2(-bodyWidth / 2f, 0f); - Vector2 rearBumperAttachOnBumper = new Vector2(bumperWidth / 2f, 0f); - - // Add Weld - _engine.AddWeldConstraint(carBody, rearBumper, rearBumperAttachOnBody, rearBumperAttachOnBumper); - - DemoCar = new DemoGameCar(carBody, frontWheel, rearWheel, frontBumper, rearBumper, false); - - } - - private void CreateDemoSproket() - { - - - // circle of circles - Vector2 center = new Vector2(800, 300); - int numBalls = 22; - float radius = 80f; - PhysicsObject? firstBall = null; - PhysicsObject? prevBall = null; - for (int i = 0; i < numBalls; i++) - { - float angle = i * (2 * MathF.PI / numBalls); - Vector2 pos = center + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * radius; - var ball = _objectTemplates.CreateMedBall(pos.X - 10, pos.Y - 10); - if (i == 0) - firstBall = ball; - if (prevBall != null) - { - _engine.AddWeldConstraint(prevBall, ball, true); - } - prevBall = ball; - } - // Close the circle - if (firstBall != null && prevBall != null) - { - _engine.AddWeldConstraint(prevBall, firstBall, true); - } - } - - private void CreateDemoChain() - { - // Add Chain of small balls as rope test - PhysicsObject? prevObject2 = null; - for (int i = 0; i < 12; i++) - { - - var currentObj = _objectTemplates.CreateMedBall(150 + (i * 25), 300); - - // anchor - if (i == 0) - { - var anchor = _objectTemplates.CreateBox(new Vector2(125, 290), 20, 20); - anchor.Locked = true; - // first one weld to anchor - _engine.AddAxisConstraint(anchor, currentObj); - } - - // axis constraint to previous - if (i > 0 && prevObject2 != null) - { - _engine.AddAxisConstraint(prevObject2, currentObj);//, Vector2.Zero, new Vector2(-25, 0)); - } - - // end anchor - if (i == 11) - { - var anchor = _objectTemplates.CreateBox(new Vector2(150 + (i * 25), 290), 20, 20); - anchor.Locked = true; - _engine.AddAxisConstraint(currentObj, anchor); - - } - - prevObject2 = currentObj; - } - } - - private void CreateDemoBridge() - { - - // Add Chain of small balls as rope test - PhysicsObject? prevObject = null; - for (int i = 0; i < 12; i++) - { - - var currentObj = _objectTemplates.CreateMedBall(150 + (i * 25), 150); - - if (prevObject != null) - { - //_engine.AddWeldConstraint(prevObject, currentObj, Vector2.Zero, new Vector2(-25, 0)); - _engine.AddWeldConstraint(prevObject, currentObj); - - } - - prevObject = currentObj; - } + _demoCar = _sceneBuilder.CreateCar(); + _sceneBuilder.CreateBridge(new Vector2(150, 150)); + _sceneBuilder.CreateChain(new Vector2(150, 300)); + _sceneBuilder.CreateSprocket(new Vector2(800, 300)); + _sceneBuilder.CreateConcavePolygonDemo(new Vector2(600, 100)); + _sceneBuilder.CreateBlanket(new Vector2(700, 400)); } private void InitializePersonDetection(uint worldWidth, uint worldHeight) @@ -356,44 +118,28 @@ private void InitializePersonDetection(uint worldWidth, uint worldHeight) maxPeople: settings.MaxPeople ); - _personColliderBridge.OnError += (s, ex) => { Console.WriteLine($"Person Detection Error: {ex.Message}"); }; - - _personColliderBridge.OnPersonBodyUpdated += (s, balls) => - { - // Uncomment for debug output: - // Console.WriteLine($"Tracking balls updated: {balls.Count} active"); - }; + _personColliderBridge.OnError += (s, ex) => Console.WriteLine($"Person Detection Error: {ex.Message}"); - // Start detection using configured camera source if (settings.CameraSourceType == "url") - { _personColliderBridge.Start(url: settings.CameraUrl); - } else - { _personColliderBridge.Start( cameraIndex: settings.CameraDeviceIndex, width: settings.CameraDeviceResolutionX, height: settings.CameraDeviceResolutionY, fps: settings.CameraDeviceFps); - } - Console.WriteLine("Person detection initialized successfully."); - Console.WriteLine($"Model: {Path.GetFullPath(settings.ModelPath)}"); - Console.WriteLine($"Camera: {(settings.CameraSourceType == "url" ? settings.CameraUrl : $"Device {settings.CameraDeviceIndex}")}"); - Console.WriteLine($"Tracking balls: radius={settings.SandboxBallRadius}, smoothing={settings.SandboxSmoothingFactor:F2}"); + Console.WriteLine($"Person detection initialized. Model: {Path.GetFullPath(settings.ModelPath)}"); } catch (Exception ex) { Console.WriteLine($"Failed to initialize person detection: {ex.Message}"); - Console.WriteLine("The application will continue without person detection."); _personColliderBridge = null; } } public void Update(float deltaTime, InputManager inputManager) { - // Check for ESC to return to menu if (inputManager.IsKeyPressedBuffered(Keyboard.Key.Escape)) { _engine.SwitchGame(new MenuGame()); @@ -401,7 +147,18 @@ public void Update(float deltaTime, InputManager inputManager) return; } - // Handle view panning with middle mouse button + HandleViewPanning(inputManager); + HandleScrollZoom(inputManager); + HandleSandboxInput(deltaTime, inputManager); + + _playerController.Update(inputManager); + _personColliderBridge?.ProcessPendingUpdates(); + _demoCar?.Update(deltaTime, inputManager); + _background.Update(deltaTime); + } + + private void HandleViewPanning(InputManager inputManager) + { if (inputManager.IsMousePressed(Mouse.Button.Middle)) { _isPanning = true; @@ -417,14 +174,16 @@ public void Update(float deltaTime, InputManager inputManager) _engine.Renderer.PanViewByScreenDelta(_panStartScreenPos, inputManager.MouseScreenPosition); _panStartScreenPos = inputManager.MouseScreenPosition; } + } - // Handle scroll wheel zoom + private void HandleScrollZoom(InputManager inputManager) + { if (Math.Abs(inputManager.ScrollWheelDelta) > 1e-6) - { _engine.Renderer.ZoomView(inputManager.ScrollWheelDelta, inputManager.MouseScreenPosition); - } + } - // Handle physics sandbox input - ball launching + private void HandleSandboxInput(float deltaTime, InputManager inputManager) + { if (_isGrabbing) { _engine.PhysicsSystem.HoldActiveAtPoint(_mousePosition); @@ -435,68 +194,60 @@ public void Update(float deltaTime, InputManager inputManager) if (_launchTimer >= LaunchInterval) { _actionTemplates.Launch( - _objectTemplates.CreateSmallBall(_startPoint.X, _startPoint.Y), + _objectTemplates.CreateMedBall(_startPoint.X, _startPoint.Y), _startPoint, _mousePosition); _launchTimer = 0f; } } + } - // Update player controller - _playerController.Update(inputManager); - - // Process person detection updates (thread-safe) - _personColliderBridge?.ProcessPendingUpdates(); - - // Process object updates for demo car - DemoCar?.Update(deltaTime, inputManager); + public void RenderBackground(Renderer renderer) + { + _background.Draw(renderer.Window); } public void Render(Renderer renderer) { - // Draw box creation preview (ensure GameView is active for world coordinates) if (_isCreatingBox) { float minX = Math.Min(_boxStartPoint.X, _boxEndPoint.X); float minY = Math.Min(_boxStartPoint.Y, _boxEndPoint.Y); float width = Math.Abs(_boxEndPoint.X - _boxStartPoint.X); float height = Math.Abs(_boxEndPoint.Y - _boxStartPoint.Y); - renderer.DrawRectangle( - new Vector2(minX, minY), - new Vector2(width, height), - new Color(0, 0, 0, 0), - Color.Red, - 2f); + renderer.DrawRectangle(new Vector2(minX, minY), new Vector2(width, height), new Color(0, 0, 0, 0), Color.Red, 2f); } - // Draw skeleton overlay SkeletonRenderer.DrawSkeleton(renderer, _personColliderBridge); - // Draw constraint mode indicator if (_constraintMode != ConstraintMode.None) { string modeText = _constraintMode == ConstraintMode.Weld ? "WELD MODE" : "AXIS MODE"; string stateText = _firstSelectedObject == null ? (_constraintMode == ConstraintMode.Axis ? "Click rotation point on body" : "Click first object") : "Click second object (Q to cancel)"; - renderer.DrawText($"{modeText}: {stateText}", 10, 40, 16, Color.Yellow); + renderer.DrawText($"{modeText}: {stateText}", 10, 170, 16, Color.Yellow); } + + _debugUI.Render(renderer); } public void Shutdown() { - // Unsubscribe from window events _engine.Renderer.Window.MouseButtonPressed -= OnMouseButtonPressed; _engine.Renderer.Window.MouseButtonReleased -= OnMouseButtonReleased; _engine.Renderer.Window.MouseMoved -= OnMouseMoved; _engine.Renderer.Window.KeyPressed -= OnKeyPressed; + _debugUI.Clear(); + _background.Dispose(); _personColliderBridge?.Dispose(); } + #region Input Handlers + private void HandleConstraintClick(Vector2 worldPos) { - // Try to find an object at the click position if (!_engine.PhysicsSystem.ActivateAtPoint(worldPos)) { Console.WriteLine("No object found at click position"); @@ -508,113 +259,62 @@ private void HandleConstraintClick(Vector2 worldPos) if (_firstSelectedObject == null) { - // First click - store the object and click position (the weld/pivot point) _firstSelectedObject = clickedObject; _firstClickWorldPos = worldPos; - string modeHint = _constraintMode == ConstraintMode.Weld - ? "weld point" : "pivot point"; - Console.WriteLine($"Selected object at {modeHint} {worldPos} - now click second object"); + Console.WriteLine($"Selected first object - now click second object"); } else { - // Second click - create the constraint if (clickedObject == _firstSelectedObject) { - Console.WriteLine("Cannot connect object to itself. Select a different object."); + Console.WriteLine("Cannot connect object to itself."); return; } + Vector2 worldOffsetA = _firstClickWorldPos - _firstSelectedObject.Center; + Vector2 worldOffsetB = _firstClickWorldPos - clickedObject.Center; + Vector2 localAnchorA = PhysMath.RotateVector(worldOffsetA, -_firstSelectedObject.Angle); + Vector2 localAnchorB = PhysMath.RotateVector(worldOffsetB, -clickedObject.Angle); + if (_constraintMode == ConstraintMode.Weld) - { - // Weld: Both anchors point to the same world position (the first click point) - // Convert world offset to LOCAL space by un-rotating by object's current angle - Vector2 worldOffsetA = _firstClickWorldPos - _firstSelectedObject.Center; - Vector2 worldOffsetB = _firstClickWorldPos - clickedObject.Center; - Vector2 localAnchorA = PhysMath.RotateVector(worldOffsetA, -_firstSelectedObject.Angle); - Vector2 localAnchorB = PhysMath.RotateVector(worldOffsetB, -clickedObject.Angle); _engine.AddWeldConstraint(_firstSelectedObject, clickedObject, localAnchorA, localAnchorB); - Console.WriteLine($"Created Weld constraint at point {_firstClickWorldPos}"); - } else if (_constraintMode == ConstraintMode.Axis) - { - // Axis: Both anchors point to the same world position (the first click point) - // Convert world offset to LOCAL space by un-rotating by object's current angle - Vector2 worldOffsetA = _firstClickWorldPos - _firstSelectedObject.Center; - Vector2 worldOffsetB = _firstClickWorldPos - clickedObject.Center; - Vector2 localAnchorA = PhysMath.RotateVector(worldOffsetA, -_firstSelectedObject.Angle); - Vector2 localAnchorB = PhysMath.RotateVector(worldOffsetB, -clickedObject.Angle); _engine.AddAxisConstraint(_firstSelectedObject, clickedObject, localAnchorA, localAnchorB); - Console.WriteLine($"Created Axis constraint at pivot point {_firstClickWorldPos}"); - } - // Reset state for next constraint + Console.WriteLine($"Created {_constraintMode} constraint"); _firstSelectedObject = null; _firstClickWorldPos = Vector2.Zero; - // Stay in constraint mode for more connections - Console.WriteLine($"Ready for next {(_constraintMode == ConstraintMode.Weld ? "weld" : "axis")} - click first object or press Q to exit"); } } - #region Physics Sandbox Input Handlers - private void OnMouseButtonPressed(object? sender, MouseButtonEventArgs e) { Vector2 worldPos = _engine.Renderer.Window.MapPixelToCoords( - new SFML.System.Vector2i(e.X, e.Y), - _engine.Renderer.GameView).ToSystemNumerics(); - - // Check if debug UI handles the click first - if (e.Button == Mouse.Button.Left && _engine.Renderer.ShowDebugUI) - { - // Get UI position for debug UI - Vector2 uiPos = _engine.Renderer.Window.MapPixelToCoords( - new SFML.System.Vector2i(e.X, e.Y), - _engine.Renderer.UiView).ToSystemNumerics(); - - if (_engine.Renderer.DebugUiManager.HandleClick(uiPos)) - { - return; // UI handled the click - } - } + new SFML.System.Vector2i(e.X, e.Y), _engine.Renderer.GameView).ToSystemNumerics(); if (e.Button == Mouse.Button.Left) { - // Handle constraint creation mode - if (_constraintMode != ConstraintMode.None) - { - HandleConstraintClick(worldPos); - return; - } + Vector2 uiPos = _engine.Renderer.Window.MapPixelToCoords( + new SFML.System.Vector2i(e.X, e.Y), _engine.Renderer.UiView).ToSystemNumerics(); + if (_debugUI.HandleClick(uiPos)) return; - if (_engine.PhysicsSystem.ActivateAtPoint(worldPos)) - { - _isGrabbing = true; - return; - } + if (_constraintMode != ConstraintMode.None) { HandleConstraintClick(worldPos); return; } + if (_engine.PhysicsSystem.ActivateAtPoint(worldPos)) { _isGrabbing = true; return; } _startPoint = worldPos; _isMousePressedLeft = true; _launchTimer = 0f; } else if (e.Button == Mouse.Button.Right) { - bool objectFound = _engine.PhysicsSystem.ActivateAtPoint(worldPos); - if (objectFound) + if (_engine.PhysicsSystem.ActivateAtPoint(worldPos)) { - if (_isGrabbing) - { - // Lock the grabbed object in place - _engine.PhysicsSystem.ActiveObject.Locked = true; - } - else - { - _engine.PhysicsSystem.RemoveActiveObject(); - } + if (_isGrabbing) _engine.PhysicsSystem.ActiveObject.Locked = true; + else _engine.PhysicsSystem.RemoveActiveObject(); } else { _isCreatingBox = true; - _boxStartPoint = worldPos; - _boxEndPoint = worldPos; + _boxStartPoint = _boxEndPoint = worldPos; } _isMousePressedRight = true; } @@ -622,34 +322,18 @@ private void OnMouseButtonPressed(object? sender, MouseButtonEventArgs e) private void OnMouseButtonReleased(object? sender, MouseButtonEventArgs e) { - // Stop debug UI drag operations - if (e.Button == Mouse.Button.Left) - { - _engine.Renderer.DebugUiManager.StopDrag(); - } - if (e.Button == Mouse.Button.Left) { - if (_isGrabbing) - { - _engine.PhysicsSystem.ReleaseActiveObject(); - _isGrabbing = false; - return; - } - if (_isMousePressedLeft) - { - _isMousePressedLeft = false; - _launchTimer = 0f; - } + _debugUI.StopDrag(); + if (_isGrabbing) { _engine.PhysicsSystem.ReleaseActiveObject(); _isGrabbing = false; return; } + if (_isMousePressedLeft) { _isMousePressedLeft = false; _launchTimer = 0f; } } else if (e.Button == Mouse.Button.Right) { if (_isCreatingBox) { - float minX = Math.Min(_boxStartPoint.X, _boxEndPoint.X); - float minY = Math.Min(_boxStartPoint.Y, _boxEndPoint.Y); - float maxX = Math.Max(_boxStartPoint.X, _boxEndPoint.X); - float maxY = Math.Max(_boxStartPoint.Y, _boxEndPoint.Y); + float minX = Math.Min(_boxStartPoint.X, _boxEndPoint.X), minY = Math.Min(_boxStartPoint.Y, _boxEndPoint.Y); + float maxX = Math.Max(_boxStartPoint.X, _boxEndPoint.X), maxY = Math.Max(_boxStartPoint.Y, _boxEndPoint.Y); _objectTemplates.CreateBox(new Vector2(minX, minY), (int)(maxX - minX), (int)(maxY - minY)); _isCreatingBox = false; } @@ -660,118 +344,52 @@ private void OnMouseButtonReleased(object? sender, MouseButtonEventArgs e) private void OnMouseMoved(object? sender, MouseMoveEventArgs e) { _mousePosition = _engine.Renderer.Window.MapPixelToCoords( - new SFML.System.Vector2i(e.X, e.Y), - _engine.Renderer.GameView).ToSystemNumerics(); + new SFML.System.Vector2i(e.X, e.Y), _engine.Renderer.GameView).ToSystemNumerics(); - // Handle debug UI drag - if (_engine.Renderer.DebugUiManager.DraggedElement != null) + if (_debugUI.IsDragging) { Vector2 uiPos = _engine.Renderer.Window.MapPixelToCoords( - new SFML.System.Vector2i(e.X, e.Y), - _engine.Renderer.UiView).ToSystemNumerics(); - _engine.Renderer.DebugUiManager.HandleDrag(uiPos); + new SFML.System.Vector2i(e.X, e.Y), _engine.Renderer.UiView).ToSystemNumerics(); + _debugUI.HandleDrag(uiPos); } - if (_isCreatingBox) - { - _boxEndPoint = _mousePosition; - } - else if (_isMousePressedRight && !_isCreatingBox) - { - if (_engine.PhysicsSystem.ActivateAtPoint(_mousePosition)) - { - _engine.PhysicsSystem.RemoveActiveObject(); - } - } + if (_isCreatingBox) _boxEndPoint = _mousePosition; + else if (_isMousePressedRight && !_isCreatingBox && _engine.PhysicsSystem.ActivateAtPoint(_mousePosition)) + _engine.PhysicsSystem.RemoveActiveObject(); } private void OnKeyPressed(object? sender, KeyEventArgs e) { switch (e.Code) { - case Keyboard.Key.Space: - _engine.PhysicsSystem.FreezeStaticObjects(); - break; - case Keyboard.Key.P: - _actionTemplates.ChangeShader(new SFMLPolyShader()); - break; - case Keyboard.Key.V: - _actionTemplates.ChangeShader(new SFMLPolyRainbowShader()); - break; - case Keyboard.Key.G: - _objectTemplates.CreateAttractor(_mousePosition.X, _mousePosition.Y); - break; - case Keyboard.Key.Semicolon: - _actionTemplates.PopAndMultiply(); - break; + case Keyboard.Key.Space: _engine.PhysicsSystem.FreezeStaticObjects(); break; + case Keyboard.Key.G: _objectTemplates.CreateAttractor(_mousePosition.X, _mousePosition.Y); break; case Keyboard.Key.Q: - // Exit constraint mode - if (_constraintMode != ConstraintMode.None) - { - _constraintMode = ConstraintMode.None; - _firstSelectedObject = null; - Console.WriteLine("Constraint mode cancelled"); - } + if (_constraintMode != ConstraintMode.None) { _constraintMode = ConstraintMode.None; _firstSelectedObject = null; } break; case Keyboard.Key.W: - // Toggle Weld constraint mode - if (_constraintMode == ConstraintMode.Weld) - { - _constraintMode = ConstraintMode.None; - _firstSelectedObject = null; - Console.WriteLine("Weld mode disabled"); - } - else - { - _constraintMode = ConstraintMode.Weld; - _firstSelectedObject = null; - Console.WriteLine("Weld mode enabled - click first object"); - } + _constraintMode = _constraintMode == ConstraintMode.Weld ? ConstraintMode.None : ConstraintMode.Weld; + _firstSelectedObject = null; break; case Keyboard.Key.A: - // Toggle Axis constraint mode - if (_constraintMode == ConstraintMode.Axis) - { - _constraintMode = ConstraintMode.None; - _firstSelectedObject = null; - Console.WriteLine("Axis mode disabled"); - } - else - { - _constraintMode = ConstraintMode.Axis; - _firstSelectedObject = null; - Console.WriteLine("Axis mode enabled - click first object (click point = rotation center)"); - } - break; - case Keyboard.Key.L: - // Load prefab at mouse position - LoadPrefabAtMouse(); + _constraintMode = _constraintMode == ConstraintMode.Axis ? ConstraintMode.None : ConstraintMode.Axis; + _firstSelectedObject = null; break; + case Keyboard.Key.L: LoadPrefabAtMouse(); break; } } private void LoadPrefabAtMouse() { var prefabFiles = PrefabLoader.GetAvailablePrefabs(); - if (prefabFiles.Length == 0) - { - Console.WriteLine("No prefabs available. Create some in the Prefab Designer first!"); - return; - } - - Console.WriteLine("Available prefabs:"); - for (int i = 0; i < prefabFiles.Length; i++) - { - Console.WriteLine($" {i + 1}: {Path.GetFileNameWithoutExtension(prefabFiles[i])}"); - } + if (prefabFiles.Length == 0) { Console.WriteLine("No prefabs available."); return; } - // Load the most recent prefab by default var prefabPath = prefabFiles[^1]; var instance = _prefabLoader.LoadPrefab(prefabPath, _mousePosition); if (instance != null) { _loadedPrefabs.Add(instance); - Console.WriteLine($"Loaded prefab '{instance.Name}' at position {_mousePosition}"); + Console.WriteLine($"Loaded prefab '{instance.Name}' at {_mousePosition}"); } } diff --git a/SharpPhysics.Demo/DemoProps/DemoSceneBuilder.cs b/SharpPhysics.Demo/DemoProps/DemoSceneBuilder.cs new file mode 100644 index 0000000..2bc31df --- /dev/null +++ b/SharpPhysics.Demo/DemoProps/DemoSceneBuilder.cs @@ -0,0 +1,173 @@ +#nullable enable +using System.Numerics; +using physics.Engine; +using physics.Engine.Classes.ObjectTemplates; +using physics.Engine.Core; +using physics.Engine.Objects; + +namespace SharpPhysics.Demo.DemoProps; + +/// +/// Builds demo scenes with various physics demonstrations. +/// +public class DemoSceneBuilder +{ + private readonly GameEngine _engine; + private readonly ObjectTemplates _templates; + + public DemoSceneBuilder(GameEngine engine, ObjectTemplates templates) + { + _engine = engine; + _templates = templates; + } + + public void CreateWalls(uint worldWidth, uint worldHeight) + { + _templates.CreateWall(new Vector2(0, 0), 15, (int)worldHeight); + _templates.CreateWall(new Vector2((int)worldWidth - 15, 0), 15, (int)worldHeight); + _templates.CreateWall(new Vector2(0, 0), (int)worldWidth, 15); + _templates.CreateWall(new Vector2(0, (int)worldHeight - 15), (int)worldWidth, 15); + } + + public DemoGameCar CreateCar(float carX = 300f, float carY = 500f) + { + float bodyWidth = 120f; + float bodyHeight = 30f; + float wheelRadius = 20f; + float wheelInset = 10f; + + var carBody = _templates.CreateBox(new Vector2(carX, carY), (int)bodyWidth, (int)bodyHeight); + + float frontWheelLocalX = bodyWidth / 2f - wheelInset; + float rearWheelLocalX = -bodyWidth / 2f + wheelInset; + float frontWheelWorldX = carX + bodyWidth / 2f + frontWheelLocalX; + float rearWheelWorldX = carX + bodyWidth / 2f + rearWheelLocalX; + float wheelWorldY = carY + bodyHeight + wheelRadius; + + var frontWheel = _templates.CreateLargeBall(frontWheelWorldX - 10, wheelWorldY - 10); + var rearWheel = _templates.CreateLargeBall(rearWheelWorldX - 10, wheelWorldY - 10); + + Vector2 frontAttachOnBody = new Vector2(frontWheelLocalX, bodyHeight / 2f + wheelRadius); + Vector2 rearAttachOnBody = new Vector2(rearWheelLocalX, bodyHeight / 2f + wheelRadius); + + _engine.AddAxisConstraint(carBody, frontWheel, frontAttachOnBody, Vector2.Zero); + _engine.AddAxisConstraint(carBody, rearWheel, rearAttachOnBody, Vector2.Zero); + + // Spoiler + float spoilerWidth = 40f; + float spoilerHeight = 8f; + float spoilerLocalX = -bodyWidth / 2f + 75f; + float spoilerLocalY = -bodyHeight / 2f - spoilerHeight / 2f - 2f; + float spoilerWorldX = carX + bodyWidth + spoilerLocalX - spoilerWidth / 2f; + float spoilerWorldY = carY + bodyHeight / 2f + spoilerLocalY - spoilerHeight / 2f; + var spoiler = _templates.CreateBox(new Vector2(spoilerWorldX, spoilerWorldY), (int)spoilerWidth, (int)spoilerHeight); + spoiler.Angle = 10f; + _engine.AddWeldConstraint(carBody, spoiler, new Vector2(spoilerLocalX, spoilerLocalY), Vector2.Zero); + + // Bumpers + float bumperWidth = 10f; + float bumperHeight = 20f; + var frontBumper = _templates.CreateBox(new Vector2(carX + bodyWidth, carY + 5f), (int)bumperWidth, (int)bumperHeight); + _engine.AddWeldConstraint(carBody, frontBumper, new Vector2(bodyWidth / 2f, 0f), new Vector2(-bumperWidth / 2f, 0f)); + + var rearBumper = _templates.CreateBox(new Vector2(carX - bumperWidth, carY + 5f), (int)bumperWidth, (int)bumperHeight); + _engine.AddWeldConstraint(carBody, rearBumper, new Vector2(-bodyWidth / 2f, 0f), new Vector2(bumperWidth / 2f, 0f)); + + return new DemoGameCar(carBody, frontWheel, rearWheel, frontBumper, rearBumper, true); + } + + public void CreateSprocket(Vector2 center, int numBalls = 22, float radius = 80f) + { + PhysicsObject? firstBall = null; + PhysicsObject? prevBall = null; + + for (int i = 0; i < numBalls; i++) + { + float angle = i * (2 * MathF.PI / numBalls); + Vector2 pos = center + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * radius; + var ball = _templates.CreateMedBall(pos.X - 10, pos.Y - 10); + + if (i == 0) firstBall = ball; + if (prevBall != null) _engine.AddWeldConstraint(prevBall, ball); + prevBall = ball; + } + + if (firstBall != null && prevBall != null) + _engine.AddWeldConstraint(prevBall, firstBall); + } + + public void CreateBlanket(Vector2 origin, int countX = 12, int countY = 12, int spacing = 20) + { + PhysicsObject[][] grid = new PhysicsObject[countX][]; + + for (int x = 0; x < countX; x++) + { + grid[x] = new PhysicsObject[countY]; + for (int y = 0; y < countY; y++) + { + grid[x][y] = _templates.CreateSmallBall(origin.X + x * spacing, origin.Y + y * spacing); + if (x > 0) _engine.AddSpringConstraint(grid[x - 1][y], grid[x][y]); + if (y > 0) _engine.AddSpringConstraint(grid[x][y - 1], grid[x][y]); + } + } + } + + public void CreateChain(Vector2 start, int links = 12, int linkSpacing = 25) + { + PhysicsObject? prevObject = null; + + for (int i = 0; i < links; i++) + { + var currentObj = _templates.CreateMedBall(start.X + (i * linkSpacing), start.Y); + + if (i == 0) + { + var anchor = _templates.CreateBox(new Vector2(start.X - 25, start.Y - 10), 20, 20); + anchor.Locked = true; + _engine.AddAxisConstraint(anchor, currentObj); + } + + if (prevObject != null) + _engine.AddAxisConstraint(prevObject, currentObj); + + if (i == links - 1) + { + var anchor = _templates.CreateBox(new Vector2(start.X + (i * linkSpacing), start.Y - 10), 20, 20); + anchor.Locked = true; + _engine.AddAxisConstraint(currentObj, anchor); + } + + prevObject = currentObj; + } + } + + public void CreateBridge(Vector2 start, int segments = 12, int segmentSpacing = 25) + { + PhysicsObject? prevObject = null; + + for (int i = 0; i < segments; i++) + { + var currentObj = _templates.CreateMedBall(start.X + (i * segmentSpacing), start.Y); + if (prevObject != null) + _engine.AddWeldConstraint(prevObject, currentObj); + prevObject = currentObj; + } + } + + public void CreateConcavePolygonDemo(Vector2 position) + { + var lShapeVertices = new Vector2[] + { + new(0, 0), new(60, 0), new(60, 25), + new(25, 25), new(25, 60), new(0, 60) + }; + _templates.CreateConcavePolygon(position, lShapeVertices, canRotate: true, canBreak: true); + + var starVertices = new Vector2[] + { + new(25, 0), new(30, 18), new(50, 18), new(35, 30), new(40, 50), + new(25, 38), new(10, 50), new(15, 30), new(0, 18), new(20, 18) + }; + _templates.CreateConcavePolygon(position + new Vector2(-100, 0), starVertices, canRotate: true, canBreak: true); + } +} diff --git a/SharpPhysics.Demo/DemoProps/SandboxDebugUI.cs b/SharpPhysics.Demo/DemoProps/SandboxDebugUI.cs new file mode 100644 index 0000000..2793b06 --- /dev/null +++ b/SharpPhysics.Demo/DemoProps/SandboxDebugUI.cs @@ -0,0 +1,96 @@ +#nullable enable +using System.Numerics; +using physics.Engine; +using physics.Engine.Core; +using physics.Engine.Rendering; +using physics.Engine.Rendering.UI; +using physics.Engine.Shaders; +using SFML.Graphics; + +namespace SharpPhysics.Demo.DemoProps; + +/// +/// Debug UI panel for the physics sandbox with controls for gravity, simulation speed, etc. +/// +public class SandboxDebugUI +{ + private readonly UiManager _uiManager = new(); + private readonly GameEngine _engine; + private readonly PhysicsSystem _physics; + private UiButton? _pauseButton; + + public bool IsVisible { get; set; } = true; + + public SandboxDebugUI(GameEngine engine) + { + _engine = engine; + _physics = engine.PhysicsSystem; + InitializeControls(); + } + + private void InitializeControls() + { + var font = _engine.Renderer.DefaultFont; + + // Contact normals toggle + var normalsLabel = new UiTextLabel("Contact Normals", font) { Position = new Vector2(200, 30), CharacterSize = 14 }; + var normalsCheckbox = new UiCheckbox(new Vector2(350, 30), new Vector2(20, 20)) { IsChecked = SFMLPolyShader.DrawNormals }; + normalsCheckbox.OnClick += isChecked => SFMLPolyShader.DrawNormals = isChecked; + _uiManager.Add(normalsLabel); + _uiManager.Add(normalsCheckbox); + + // Pause button + _pauseButton = new UiButton("Pause", font, new Vector2(450, 30), new Vector2(70, 20)); + _pauseButton.OnClick += _ => + { + _physics.IsPaused = !_physics.IsPaused; + _pauseButton.Text = _physics.IsPaused ? "Resume" : "Pause"; + }; + _uiManager.Add(_pauseButton); + + // Gravity X slider + _uiManager.Add(new UiTextLabel("Gravity X", font) { Position = new Vector2(200, 60), CharacterSize = 14 }); + var gravityXSlider = new UiSlider(new Vector2(200, 80), new Vector2(150, 20), -20f, 20f, _physics.Gravity.X); + gravityXSlider.OnValueChanged += v => _physics.Gravity = new Vector2(v, _physics.Gravity.Y); + _uiManager.Add(gravityXSlider); + + // Gravity Y slider + _uiManager.Add(new UiTextLabel("Gravity Y", font) { Position = new Vector2(400, 60), CharacterSize = 14 }); + var gravityYSlider = new UiSlider(new Vector2(400, 80), new Vector2(150, 20), -20f, 20f, _physics.Gravity.Y); + gravityYSlider.OnValueChanged += v => _physics.Gravity = new Vector2(_physics.Gravity.X, v); + _uiManager.Add(gravityYSlider); + + // Simulation speed slider + _uiManager.Add(new UiTextLabel("Simulation Speed", font) { Position = new Vector2(200, 110), CharacterSize = 14 }); + var simSpeedSlider = new UiSlider(new Vector2(200, 130), new Vector2(150, 20), 0.1f, 2f, _physics.TimeScale); + simSpeedSlider.OnValueChanged += v => _physics.TimeScale = v; + _uiManager.Add(simSpeedSlider); + } + + public bool HandleClick(Vector2 uiPosition) => IsVisible && _uiManager.HandleClick(uiPosition); + + public void HandleDrag(Vector2 uiPosition) => _uiManager.HandleDrag(uiPosition); + + public void StopDrag() => _uiManager.StopDrag(); + + public bool IsDragging => _uiManager.DraggedElement != null; + + public void Render(Renderer renderer) + { + if (!IsVisible) return; + + renderer.Window.SetView(renderer.UiView); + + // Performance stats + renderer.DrawText( + $"ms physics time: {_engine.MsPhysicsTime}\n" + + $"ms draw time: {_engine.MsDrawTime}\n" + + $"frame rate: {1000 / Math.Max(_engine.MsFrameTime, 1)}\n" + + $"num objects: {_physics.ListStaticObjects.Count}", + 40, 40, 12, Color.White); + + _uiManager.Draw(renderer.Window); + } + + public void Clear() => _uiManager.Clear(); +} diff --git a/SharpPhysics.Demo/Designer/PrefabDesignerGame.cs b/SharpPhysics.Demo/Designer/PrefabDesignerGame.cs index dbc66a6..41fff9e 100644 --- a/SharpPhysics.Demo/Designer/PrefabDesignerGame.cs +++ b/SharpPhysics.Demo/Designer/PrefabDesignerGame.cs @@ -58,9 +58,6 @@ public void Initialize(GameEngine engine) _engine = engine; _designerRenderer = new DesignerRenderer(engine.WindowWidth, TOOLBAR_HEIGHT); - // Hide debug UI - _engine.Renderer.ShowDebugUI = false; - // Pause physics simulation _engine.PhysicsSystem.IsPaused = true; _engine.PhysicsSystem.Gravity = Vector2.Zero; diff --git a/SharpPhysics.Demo/Helpers/AnimatedBackground.cs b/SharpPhysics.Demo/Helpers/AnimatedBackground.cs new file mode 100644 index 0000000..d22742c --- /dev/null +++ b/SharpPhysics.Demo/Helpers/AnimatedBackground.cs @@ -0,0 +1,119 @@ +#nullable enable +using System; +using System.Collections.Generic; +using SFML.Graphics; + +namespace SharpPhysics.Demo.Helpers; + +/// +/// Simple animated background with floating colored circles. +/// A decorative screen effect for menus and demos. +/// +public class AnimatedBackground : IDisposable +{ + private readonly List _circles = new(); + private readonly float _screenWidth; + private readonly float _screenHeight; + private readonly Random _random = new(); + + public AnimatedBackground(float screenWidth, float screenHeight, int circleCount = 20) + { + _screenWidth = screenWidth; + _screenHeight = screenHeight; + + for (int i = 0; i < circleCount; i++) + _circles.Add(CreateCircle()); + } + + private FloatingCircle CreateCircle() + { + float size = _random.NextSingle() * 200f + 5f; + float alpha = _random.NextSingle() * 0.3f; + float hue = _random.NextSingle(); + + return new FloatingCircle + { + X = _random.NextSingle() * _screenWidth, + Y = _random.NextSingle() * _screenHeight, + VelX = (_random.NextSingle() - 0.5f) * 30f, + VelY = (_random.NextSingle() - 0.5f) * 30f, + Size = size, + Alpha = alpha, + Hue = hue, + Shape = new CircleShape(size) { FillColor = ColorUtils.HsvToColor(hue, 0.6f, 0.8f, (byte)(alpha * 255)) } + }; + } + + public void Update(float deltaTime) + { + foreach (var c in _circles) + { + c.X += c.VelX * deltaTime; + c.Y += c.VelY * deltaTime; + + if (c.X < -50) c.X = _screenWidth + 50; + if (c.X > _screenWidth + 50) c.X = -50; + if (c.Y < -50) c.Y = _screenHeight + 50; + if (c.Y > _screenHeight + 50) c.Y = -50; + + c.Hue = (c.Hue + deltaTime * 0.05f) % 1f; + c.Shape.FillColor = ColorUtils.HsvToColor(c.Hue, 0.6f, 0.8f, (byte)(c.Alpha * 255)); + } + } + + public void Draw(RenderWindow window) + { + foreach (var c in _circles) + { + c.Shape.Position = new SFML.System.Vector2f(c.X - c.Size, c.Y - c.Size); + window.Draw(c.Shape); + } + } + + public void Dispose() + { + foreach (var c in _circles) + c.Shape.Dispose(); + _circles.Clear(); + } + + private class FloatingCircle + { + public float X { get; set; } + public float Y { get; set; } + public float VelX { get; set; } + public float VelY { get; set; } + public float Size { get; set; } + public float Alpha { get; set; } + public float Hue { get; set; } + public CircleShape Shape { get; set; } = null!; + } +} + +/// +/// Color conversion utilities. +/// +public static class ColorUtils +{ + public static Color HsvToColor(float h, float s, float v, byte alpha) + { + int hi = (int)(h * 6) % 6; + float f = h * 6 - (int)(h * 6); + float p = v * (1 - s); + float q = v * (1 - f * s); + float t = v * (1 - (1 - f) * s); + + float r, g, b; + switch (hi) + { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + default: r = v; g = p; b = q; break; + } + + return new Color((byte)(r * 255), (byte)(g * 255), (byte)(b * 255), alpha); + } +} diff --git a/SharpPhysics.Demo/MenuGame.cs b/SharpPhysics.Demo/MenuGame.cs index 5359a15..b11bde6 100644 --- a/SharpPhysics.Demo/MenuGame.cs +++ b/SharpPhysics.Demo/MenuGame.cs @@ -1,39 +1,26 @@ #nullable enable using System.Numerics; -using physics.Engine; using physics.Engine.Core; using physics.Engine.Input; using physics.Engine.Rendering; using physics.Engine.Rendering.UI; -using physics.Engine.Shaders; -using physics.Engine.Objects; using SharpPhysics.Demo.Designer; +using SharpPhysics.Demo.Helpers; using SFML.Graphics; using SFML.Window; namespace SharpPhysics.Demo; -/// -/// Main menu for selecting between demo games. -/// Features animated background and styled buttons. -/// public class MenuGame : IGame { private GameEngine _engine = null!; - private PhysicsSystem _physics = null!; private UiManager _uiManager = new(); - private Random _random = new(); + private AnimatedBackground _background = null!; - // Menu buttons private readonly List _menuButtons = new(); private float _animationTime; - // Background particle system - private readonly List _particles = new(); - private const int PARTICLE_COUNT = 50; - - // Game factories for switching - private readonly Dictionary> _gameFactories = new() + private static readonly Dictionary> GameFactories = new() { ["RainCatcher"] = () => new RainCatcherGame(), ["BubblePop"] = () => new BubblePopGame(), @@ -46,17 +33,11 @@ public class MenuGame : IGame public void Initialize(GameEngine engine) { _engine = engine; - _physics = engine.PhysicsSystem; - - // Hide debug UI in menu - _engine.Renderer.ShowDebugUI = false; - - // Disable gravity for menu background - _physics.Gravity = new Vector2(0, 0); - _physics.GravityScale = 0; + _engine.PhysicsSystem.Gravity = Vector2.Zero; + _engine.PhysicsSystem.GravityScale = 0; + _background = new AnimatedBackground(engine.WindowWidth, engine.WindowHeight); CreateMenuUI(); - CreateBackgroundParticles(); Console.WriteLine("Menu loaded - click a button to start a game!"); } @@ -70,129 +51,33 @@ private void CreateMenuUI() float buttonHeight = 60f; float spacing = 12f; - // Rain Catcher button - var rainButton = new UiMenuButton( - "Rain Catcher", - "Catch falling balls with your body!", - font, - new Vector2(centerX - buttonWidth / 2, startY), - new Vector2(buttonWidth, buttonHeight), - baseColor: new Color(60, 60, 90), - hoverColor: new Color(80, 80, 130), - borderColor: new Color(100, 180, 255) - ); - rainButton.OnClick += _ => SwitchToGame("RainCatcher"); - _menuButtons.Add(rainButton); - _uiManager.Add(rainButton); - - // Bubble Pop button - var bubbleButton = new UiMenuButton( - "Bubble Pop", - "Pop floating bubbles with your hands!", - font, - new Vector2(centerX - buttonWidth / 2, startY + buttonHeight + spacing), - new Vector2(buttonWidth, buttonHeight), - baseColor: new Color(60, 80, 70), - hoverColor: new Color(80, 120, 100), - borderColor: new Color(100, 255, 180) - ); - bubbleButton.OnClick += _ => SwitchToGame("BubblePop"); - _menuButtons.Add(bubbleButton); - _uiManager.Add(bubbleButton); - - // Platformer button - var platformerButton = new UiMenuButton( - "Platformer", - "Action platformer - keyboard controls!", - font, - new Vector2(centerX - buttonWidth / 2, startY + (buttonHeight + spacing) * 2), - new Vector2(buttonWidth, buttonHeight), - baseColor: new Color(90, 60, 60), - hoverColor: new Color(130, 80, 80), - borderColor: new Color(255, 180, 100) - ); - platformerButton.OnClick += _ => SwitchToGame("Platformer"); - _menuButtons.Add(platformerButton); - _uiManager.Add(platformerButton); - - // Physics Sandbox button - var sandboxButton = new UiMenuButton( - "Physics Sandbox", - "Experiment with physics simulation", - font, - new Vector2(centerX - buttonWidth / 2, startY + (buttonHeight + spacing) * 3), - new Vector2(buttonWidth, buttonHeight), - baseColor: new Color(80, 60, 70), - hoverColor: new Color(120, 80, 100), - borderColor: new Color(255, 150, 180) - ); - sandboxButton.OnClick += _ => SwitchToGame("Sandbox"); - _menuButtons.Add(sandboxButton); - _uiManager.Add(sandboxButton); - - // Prefab Designer button - var prefabButton = new UiMenuButton( - "Prefab Designer", - "Design physics object prefabs and save to JSON", - font, - new Vector2(centerX - buttonWidth / 2, startY + (buttonHeight + spacing) * 4), - new Vector2(buttonWidth, buttonHeight), - baseColor: new Color(70, 70, 80), - hoverColor: new Color(100, 100, 120), - borderColor: new Color(180, 180, 220) - ); - prefabButton.OnClick += _ => SwitchToGame("PrefabDesigner"); - _menuButtons.Add(prefabButton); - _uiManager.Add(prefabButton); - - // Settings button - var settingsButton = new UiMenuButton( - "Settings", - "Configure camera, detection, and display options", - font, - new Vector2(centerX - buttonWidth / 2, startY + (buttonHeight + spacing) * 5), - new Vector2(buttonWidth, buttonHeight), - baseColor: new Color(50, 50, 60), - hoverColor: new Color(70, 70, 90), - borderColor: new Color(150, 150, 180) - ); - settingsButton.OnClick += _ => SwitchToGame("Settings"); - _menuButtons.Add(settingsButton); - _uiManager.Add(settingsButton); - - // Hint at bottom - var exitHint = new UiTextLabel("Press ESC to return to menu from any game", font) + var buttonConfigs = new (string Key, string Title, string Subtitle, Color Base, Color Hover, Color Border)[] { - Position = new Vector2(centerX - 200, _engine.WindowHeight - 50), - CharacterSize = 14 + ("RainCatcher", "Rain Catcher", "Catch falling balls with your body!", new(60, 60, 90), new(80, 80, 130), new(100, 180, 255)), + ("BubblePop", "Bubble Pop", "Pop floating bubbles with your hands!", new(60, 80, 70), new(80, 120, 100), new(100, 255, 180)), + ("Platformer", "Platformer", "Action platformer - keyboard controls!", new(90, 60, 60), new(130, 80, 80), new(255, 180, 100)), + ("Sandbox", "Physics Sandbox", "Experiment with physics simulation", new(80, 60, 70), new(120, 80, 100), new(255, 150, 180)), + ("PrefabDesigner", "Prefab Designer", "Design physics object prefabs and save to JSON", new(70, 70, 80), new(100, 100, 120), new(180, 180, 220)), + ("Settings", "Settings", "Configure camera, detection, and display options", new(50, 50, 60), new(70, 70, 90), new(150, 150, 180)), }; - } - private void CreateBackgroundParticles() - { - for (int i = 0; i < PARTICLE_COUNT; i++) + for (int i = 0; i < buttonConfigs.Length; i++) { - _particles.Add(CreateParticle()); + var cfg = buttonConfigs[i]; + var button = new UiMenuButton( + cfg.Title, cfg.Subtitle, font, + new Vector2(centerX - buttonWidth / 2, startY + (buttonHeight + spacing) * i), + new Vector2(buttonWidth, buttonHeight), + baseColor: cfg.Base, hoverColor: cfg.Hover, borderColor: cfg.Border); + button.OnClick += _ => SwitchToGame(cfg.Key); + _menuButtons.Add(button); + _uiManager.Add(button); } } - private MenuParticle CreateParticle() - { - return new MenuParticle - { - X = _random.NextFloat() * _engine.WindowWidth, - Y = _random.NextFloat() * _engine.WindowHeight, - VelX = (_random.NextFloat() - 0.5f) * 30f, - VelY = (_random.NextFloat() - 0.5f) * 30f, - Size = _random.NextFloat() * 20f + 5f, - Alpha = _random.NextFloat() * 0.3f + 0.1f, - Hue = _random.NextFloat() - }; - } - private void SwitchToGame(string gameKey) { - if (_gameFactories.TryGetValue(gameKey, out var factory)) + if (GameFactories.TryGetValue(gameKey, out var factory)) { Console.WriteLine($"Starting {gameKey}..."); _engine.SwitchGame(factory()); @@ -202,117 +87,36 @@ private void SwitchToGame(string gameKey) public void Update(float deltaTime, InputManager inputManager) { _animationTime += deltaTime; + _background.Update(deltaTime); - // Update particle animation - UpdateParticles(deltaTime); - - // Handle UI clicks if (inputManager.IsMousePressed(Mouse.Button.Left)) - { _uiManager.HandleClick(inputManager.MousePosition); - } - // Check for button hover foreach (var button in _menuButtons) - { button.SetHovered(button.ContainsPoint(inputManager.MousePosition)); - } } - private void UpdateParticles(float deltaTime) + public void RenderBackground(Renderer renderer) { - foreach (var p in _particles) - { - p.X += p.VelX * deltaTime; - p.Y += p.VelY * deltaTime; - - // Wrap around screen - if (p.X < -50) p.X = _engine.WindowWidth + 50; - if (p.X > _engine.WindowWidth + 50) p.X = -50; - if (p.Y < -50) p.Y = _engine.WindowHeight + 50; - if (p.Y > _engine.WindowHeight + 50) p.Y = -50; - - // Slowly change hue - p.Hue = (p.Hue + deltaTime * 0.05f) % 1f; - } + _background.Draw(renderer.Window); } public void Render(Renderer renderer) { - // Draw animated title float titleY = 50 + MathF.Sin(_animationTime * 2f) * 5f; - renderer.DrawText("SharpPhysics Demo Games", - _engine.WindowWidth / 2f - 300, titleY, 42, Color.White); - - // Draw subtitle - renderer.DrawText("Select a game to play", - _engine.WindowWidth / 2f - 100, 100, 20, new Color(180, 180, 200)); - - // Draw animated background particles - DrawParticles(renderer); - - // Draw version/info at bottom + renderer.DrawText("SharpPhysics Demo Games", _engine.WindowWidth / 2f - 300, titleY, 42, Color.White); + renderer.DrawText("Select a game to play", _engine.WindowWidth / 2f - 100, 100, 20, new Color(180, 180, 200)); renderer.DrawText("Use your body to interact! • Body tracking powered by YOLO Pose", _engine.WindowWidth / 2f - 250, _engine.WindowHeight - 30, 14, new Color(120, 120, 140)); - // Draw UI elements (menu buttons) renderer.Window.SetView(renderer.UiView); _uiManager.Draw(renderer.Window); } - private void DrawParticles(Renderer renderer) - { - foreach (var p in _particles) - { - // Convert hue to RGB (simple HSV to RGB) - var color = HsvToColor(p.Hue, 0.6f, 0.8f, (byte)(p.Alpha * 255)); - - var circle = new CircleShape(p.Size) - { - Position = new SFML.System.Vector2f(p.X - p.Size, p.Y - p.Size), - FillColor = color - }; - renderer.Window.Draw(circle); - } - } - - private Color HsvToColor(float h, float s, float v, byte alpha) - { - int hi = (int)(h * 6) % 6; - float f = h * 6 - (int)(h * 6); - float p = v * (1 - s); - float q = v * (1 - f * s); - float t = v * (1 - (1 - f) * s); - - float r, g, b; - switch (hi) - { - case 0: r = v; g = t; b = p; break; - case 1: r = q; g = v; b = p; break; - case 2: r = p; g = v; b = t; break; - case 3: r = p; g = q; b = v; break; - case 4: r = t; g = p; b = v; break; - default: r = v; g = p; b = q; break; - } - - return new Color((byte)(r * 255), (byte)(g * 255), (byte)(b * 255), alpha); - } - public void Shutdown() { + _background.Dispose(); _uiManager.Clear(); - _particles.Clear(); _menuButtons.Clear(); } - - private class MenuParticle - { - public float X { get; set; } - public float Y { get; set; } - public float VelX { get; set; } - public float VelY { get; set; } - public float Size { get; set; } - public float Alpha { get; set; } - public float Hue { get; set; } - } } diff --git a/SharpPhysics.Demo/PlatformerGame.cs b/SharpPhysics.Demo/PlatformerGame.cs index c0cbb46..869c560 100644 --- a/SharpPhysics.Demo/PlatformerGame.cs +++ b/SharpPhysics.Demo/PlatformerGame.cs @@ -55,9 +55,6 @@ public void Initialize(GameEngine engine) _physics = engine.PhysicsSystem; _objectTemplates = new ObjectTemplates(_physics); - // Hide debug UI for clean game experience - _engine.Renderer.ShowDebugUI = false; - // Normal gravity for platformer _physics.Gravity = new Vector2(0, 9.8f); _physics.GravityScale = 35f; diff --git a/SharpPhysics.Demo/RainCatcherGame.cs b/SharpPhysics.Demo/RainCatcherGame.cs index 2f2fb95..219e0f5 100644 --- a/SharpPhysics.Demo/RainCatcherGame.cs +++ b/SharpPhysics.Demo/RainCatcherGame.cs @@ -79,9 +79,6 @@ public void Initialize(GameEngine engine) _physics = engine.PhysicsSystem; _objectTemplates = new ObjectTemplates(_physics); - // Hide debug UI for cleaner game experience - _engine.Renderer.ShowDebugUI = false; - // Reduce gravity for floaty fun feel _physics.Gravity = new Vector2(0, 6f); _physics.GravityScale = 25f; diff --git a/SharpPhysics.Demo/SettingsGame.cs b/SharpPhysics.Demo/SettingsGame.cs index da101e5..16a9dac 100644 --- a/SharpPhysics.Demo/SettingsGame.cs +++ b/SharpPhysics.Demo/SettingsGame.cs @@ -39,7 +39,6 @@ public void Initialize(GameEngine engine) _engine = engine; _settings = GameSettings.Instance; - _engine.Renderer.ShowDebugUI = false; _engine.PhysicsSystem.Gravity = new Vector2(0, 0); _engine.PhysicsSystem.GravityScale = 0; diff --git a/SharpPhysics.Demo/SharpPhysics.Demo.csproj b/SharpPhysics.Demo/SharpPhysics.Demo.csproj index c7223f4..fefc940 100644 --- a/SharpPhysics.Demo/SharpPhysics.Demo.csproj +++ b/SharpPhysics.Demo/SharpPhysics.Demo.csproj @@ -23,8 +23,4 @@ - - - - diff --git a/physics/Engine/Classes/Templates/ActionTemplates.cs b/physics/Engine/Classes/Templates/ActionTemplates.cs index da5e131..c9e1de8 100644 --- a/physics/Engine/Classes/Templates/ActionTemplates.cs +++ b/physics/Engine/Classes/Templates/ActionTemplates.cs @@ -8,12 +8,10 @@ namespace physics.Engine.Classes.ObjectTemplates public class ActionTemplates { private readonly PhysicsSystem _physicsSystem; - private readonly ObjectTemplates _objectTemplates; - public ActionTemplates(PhysicsSystem physicsSystem, ObjectTemplates objectTemplates) + public ActionTemplates(PhysicsSystem physicsSystem) { _physicsSystem = physicsSystem; - _objectTemplates = objectTemplates; } public void Launch(PhysicsObject physObj, Vector2 StartPointF, Vector2 EndPointF) @@ -23,26 +21,5 @@ public void Launch(PhysicsObject physObj, Vector2 StartPointF, Vector2 EndPointF new Vector2 { X = StartPointF.X, Y = StartPointF.Y }); _physicsSystem.AddVelocityToActive(delta * 2); } - - public void ChangeShader(SFMLShader shader) - { - foreach(PhysicsObject obj in _physicsSystem.GetMoveableObjects()) - { - obj.Shader = shader; - } - } - - public void PopAndMultiply() - { - foreach(PhysicsObject obj in _physicsSystem.ListStaticObjects) - { - _physicsSystem.ActivateAtPoint(new Vector2(obj.Center.X, obj.Center.Y)); - var velocity = obj.Velocity; - var origin = obj.Center; - _physicsSystem.RemoveActiveObject(); - _physicsSystem.SetVelocity(_objectTemplates.CreateSmallBall(origin.X, origin.Y), velocity); - _physicsSystem.SetVelocity(_objectTemplates.CreateSmallBall(origin.X, origin.Y), velocity); - } - } } } diff --git a/physics/Engine/Constraints/Constraint.cs b/physics/Engine/Constraints/Constraint.cs index d6b03c3..e65b2eb 100644 --- a/physics/Engine/Constraints/Constraint.cs +++ b/physics/Engine/Constraints/Constraint.cs @@ -13,9 +13,11 @@ public abstract class Constraint public PhysicsObject A { get; protected set; } public PhysicsObject B { get; protected set; } + // Anchor points in local space of each object public Vector2 AnchorA { get; protected set; } public Vector2 AnchorB { get; protected set; } + // Arbitrary break point when the constraint is pushed past its limits public bool CanBreak { get; set; } = false; public bool IsBroken { get; protected set; } = false; @@ -33,16 +35,16 @@ public Constraint(PhysicsObject a, PhysicsObject b) B.CanSleep = false; } - /// - /// Applies the constraint correction (via impulses or position/angle adjustments). - /// - public abstract void ApplyConstraint(float dt); + /// + /// Applies the constraint correction (via impulses or position/angle adjustments). + /// + public abstract void ApplyConstraint(float dt); - /// - /// Called once per physics tick before substeps begin. Used for warm starting. - /// - public virtual void PrepareForTick() { } - } + /// + /// Called once per physics tick before substeps begin. Used for warm starting. + /// + public virtual void PrepareForTick() { } + } /// /// A weld constraint holds two objects together so that the world-space positions @@ -193,44 +195,164 @@ public void PrepareForTick() _warmStartAppliedThisTick = false; } - private float ApplyLinearImpulse(Vector2 axis, Vector2 rA, Vector2 rB, float velError, float posError, - float invMassA, float invMassB, float invInertiaA, float invInertiaB, float dt) + private float ApplyLinearImpulse(Vector2 axis, Vector2 rA, Vector2 rB, float velError, float posError, + float invMassA, float invMassB, float invInertiaA, float invInertiaB, float dt) + { + // Effective mass for this axis: K = mA^-1 + mB^-1 + (rA × axis)² * IA^-1 + (rB × axis)² * IB^-1 + float rAxN = PhysMath.Cross(rA, axis); + float rBxN = PhysMath.Cross(rB, axis); + float effectiveMass = invMassA + invMassB + + (rAxN * rAxN) * invInertiaA + + (rBxN * rBxN) * invInertiaB; + + if (effectiveMass < 1e-10f) + return 0f; + + // Baumgarte bias - clamped to prevent instability with large position errors + float bias = BaumgarteBias * posError / dt; + bias = Math.Clamp(bias, -MaxBiasVelocity, MaxBiasVelocity); + + // Impulse magnitude along this axis + float impulseMag = -(velError + bias) / effectiveMass; + Vector2 impulse = axis * impulseMag; + + // Apply impulse + if (!A.Locked) + { + A.Velocity -= impulse * invMassA; + if (A.CanRotate) + A.AngularVelocity -= PhysMath.Cross(rA, impulse) * invInertiaA; + } + if (!B.Locked) + { + B.Velocity += impulse * invMassB; + if (B.CanRotate) + B.AngularVelocity += PhysMath.Cross(rB, impulse) * invInertiaB; + } + + return impulseMag; + } + } + + /// + /// A weld constraint holds two objects together so that the world-space positions + /// of their respective local anchors remain coincident, and their relative angle remains constant. + /// Uses proper impulse-based constraint solving with Baumgarte stabilization. + /// + public class SpringConstraint : Constraint + { + public float InitialRelativeAngle { get; private set; } + + private const float BaumgarteBias = 0.013f; + private const float AngularBias = 0.010f; + private const float MaxBiasVelocity = 300f; + + public SpringConstraint(PhysicsObject a, PhysicsObject b, Vector2 anchorA, Vector2 anchorB, bool canBreak = false) + : base(a, b) + { + AnchorA = anchorA; + AnchorB = anchorB; + InitialRelativeAngle = b.Angle - a.Angle; + CanBreak = canBreak; + } + + public override void ApplyConstraint(float dt) + { + if (IsBroken) + { + return; + } + // Get lever arms in world space + Vector2 rA = PhysMath.RotateVector(AnchorA, A.Angle); + Vector2 rB = PhysMath.RotateVector(AnchorB, B.Angle); + Vector2 worldAnchorA = A.Center + rA; + Vector2 worldAnchorB = B.Center + rB; + + // Position error + Vector2 posError = worldAnchorB - worldAnchorA; + + // Angular error + float angleError = (B.Angle - A.Angle) - InitialRelativeAngle; + + // break + if (CanBreak && (Math.Abs(posError.Length()) > 80f || Math.Abs(angleError) > 1.5f)) + { + IsBroken = true; + } + + // Normalize angle to [-PI, PI] + while (angleError > MathF.PI) angleError -= 2 * MathF.PI; + while (angleError < -MathF.PI) angleError += 2 * MathF.PI; + + // Get effective inverse masses + float invMassA = A.Locked ? 0f : A.IMass; + float invMassB = B.Locked ? 0f : B.IMass; + float invInertiaA = (A.Locked || !A.CanRotate) ? 0f : A.IInertia; + float invInertiaB = (B.Locked || !B.CanRotate) ? 0f : B.IInertia; + + // Compute velocity at anchor points (after warm start) + Vector2 vA = A.Velocity + PhysMath.Perpendicular(rA) * A.AngularVelocity; + Vector2 vB = B.Velocity + PhysMath.Perpendicular(rB) * B.AngularVelocity; + Vector2 relVel = vB - vA; + + // === LINEAR CONSTRAINT (solve X and Y axes separately for correct effective mass) === + float impulseX = ApplyLinearImpulse(Vector2.UnitX, rA, rB, relVel.X, posError.X, invMassA, invMassB, invInertiaA, invInertiaB, dt); + float impulseY = ApplyLinearImpulse(Vector2.UnitY, rA, rB, relVel.Y, posError.Y, invMassA, invMassB, invInertiaA, invInertiaB, dt); + + // === ANGULAR CONSTRAINT === + float angularEffectiveMass = invInertiaA + invInertiaB; + if (angularEffectiveMass < 1e-10f) + return; + + float relAngVel = B.AngularVelocity - A.AngularVelocity; + float angularBias = (AngularBias / dt) * angleError; + angularBias = Math.Clamp(angularBias, -MaxBiasVelocity, MaxBiasVelocity); + float angularImpulse = -(relAngVel + angularBias) / angularEffectiveMass; + + if (!A.Locked && A.CanRotate) + A.AngularVelocity -= angularImpulse * invInertiaA; + if (!B.Locked && B.CanRotate) + B.AngularVelocity += angularImpulse * invInertiaB; + } + + private float ApplyLinearImpulse(Vector2 axis, Vector2 rA, Vector2 rB, float velError, float posError, + float invMassA, float invMassB, float invInertiaA, float invInertiaB, float dt) + { + // Effective mass for this axis: K = mA^-1 + mB^-1 + (rA × axis)² * IA^-1 + (rB × axis)² * IB^-1 + float rAxN = PhysMath.Cross(rA, axis); + float rBxN = PhysMath.Cross(rB, axis); + float effectiveMass = invMassA + invMassB + + (rAxN * rAxN) * invInertiaA + + (rBxN * rBxN) * invInertiaB; + + if (effectiveMass < 1e-10f) + return 0f; + + // Baumgarte bias - clamped to prevent instability with large position errors + float bias = BaumgarteBias * posError / dt; + bias = Math.Clamp(bias, -MaxBiasVelocity, MaxBiasVelocity); + + // Impulse magnitude along this axis + float impulseMag = -(velError + bias) / effectiveMass; + Vector2 impulse = axis * impulseMag; + + // Apply impulse + if (!A.Locked) + { + A.Velocity -= impulse * invMassA; + if (A.CanRotate) + A.AngularVelocity -= PhysMath.Cross(rA, impulse) * invInertiaA; + } + if (!B.Locked) { - // Effective mass for this axis: K = mA^-1 + mB^-1 + (rA × axis)² * IA^-1 + (rB × axis)² * IB^-1 - float rAxN = PhysMath.Cross(rA, axis); - float rBxN = PhysMath.Cross(rB, axis); - float effectiveMass = invMassA + invMassB + - (rAxN * rAxN) * invInertiaA + - (rBxN * rBxN) * invInertiaB; - - if (effectiveMass < 1e-10f) - return 0f; - - // Baumgarte bias - clamped to prevent instability with large position errors - float bias = BaumgarteBias * posError / dt; - bias = Math.Clamp(bias, -MaxBiasVelocity, MaxBiasVelocity); - - // Impulse magnitude along this axis - float impulseMag = -(velError + bias) / effectiveMass; - Vector2 impulse = axis * impulseMag; - - // Apply impulse - if (!A.Locked) - { - A.Velocity -= impulse * invMassA; - if (A.CanRotate) - A.AngularVelocity -= PhysMath.Cross(rA, impulse) * invInertiaA; - } - if (!B.Locked) - { - B.Velocity += impulse * invMassB; - if (B.CanRotate) - B.AngularVelocity += PhysMath.Cross(rB, impulse) * invInertiaB; - } - - return impulseMag; + B.Velocity += impulse * invMassB; + if (B.CanRotate) + B.AngularVelocity += PhysMath.Cross(rB, impulse) * invInertiaB; } + + return impulseMag; } + } /// /// An AxisConstraint (revolute joint) pins two objects together at specified local anchor points, diff --git a/physics/Engine/Core/GameEngine.cs b/physics/Engine/Core/GameEngine.cs index 28d842c..70922b1 100644 --- a/physics/Engine/Core/GameEngine.cs +++ b/physics/Engine/Core/GameEngine.cs @@ -3,8 +3,7 @@ using System.Diagnostics; using physics.Engine.Input; using physics.Engine.Rendering; -using physics.Engine.Rendering.UI; -using physics.Engine.Objects; +using SFML.Graphics; namespace physics.Engine.Core { @@ -192,19 +191,22 @@ public void Run() _physicsSystem.Tick(deltaTime); MsPhysicsTime = _stopwatch.ElapsedMilliseconds - physicsStart; - // Rendering + // Rendering pipeline (layered) long renderStart = _stopwatch.ElapsedMilliseconds; - // Engine rendering (physics objects, debug UI) - _renderer.Render(MsPhysicsTime, MsDrawTime, MsFrameTime); - - // Game-specific rendering (skeleton overlay, score display, etc.) + _renderer.BeginFrame(Color.Black); + _currentGame.RenderBackground(_renderer); + _renderer.RenderPhysicsObjects(); _currentGame.Render(_renderer); - // Present the frame to screen (after all rendering is complete) + // Counting rendering as just the draw calls (excluding buffer swap) + // Else it will always be high due to VSync wait. + MsDrawTime = _stopwatch.ElapsedMilliseconds - renderStart; + + // Flip buffers _renderer.Display(); - MsDrawTime = _stopwatch.ElapsedMilliseconds - renderStart; + // Total frame time MsFrameTime = _stopwatch.ElapsedMilliseconds - frameStartTime; } diff --git a/physics/Engine/Core/IGame.cs b/physics/Engine/Core/IGame.cs index f5945b4..f03505d 100644 --- a/physics/Engine/Core/IGame.cs +++ b/physics/Engine/Core/IGame.cs @@ -2,37 +2,37 @@ using physics.Engine.Input; using physics.Engine.Rendering; -namespace physics.Engine.Core +namespace physics.Engine.Core; + +/// +/// Interface that games must implement to run on the SharpPhysics engine. +/// Rendering happens in three layers: Background → Physics Objects → Foreground. +/// +public interface IGame { /// - /// Interface that games must implement to run on the SharpPhysics engine. + /// Called once when the game starts. /// - public interface IGame - { - /// - /// Called once when the game starts. Use this to set up the game world, - /// create initial objects, and initialize game-specific systems. - /// - /// The game engine instance providing access to core systems. - void Initialize(GameEngine engine); + void Initialize(GameEngine engine); - /// - /// Called each frame to update game logic. - /// - /// Time elapsed since last frame in seconds. - /// Input manager for querying keyboard and mouse state. - void Update(float deltaTime, InputManager input); + /// + /// Called each frame to update game logic. + /// + void Update(float deltaTime, InputManager input); + + /// + /// Optional: Renders behind physics objects (backgrounds, parallax, skyboxes). + /// Default implementation does nothing. + /// + void RenderBackground(Renderer renderer) { } - /// - /// Called each frame after physics to allow game-specific rendering. - /// The engine handles rendering physics objects; use this for game-specific overlays. - /// - /// The renderer to draw with. - void Render(Renderer renderer); + /// + /// Renders in front of physics objects (UI, score, overlays). + /// + void Render(Renderer renderer); - /// - /// Called when the game is shutting down. Use this to clean up game-specific resources. - /// - void Shutdown(); - } + /// + /// Called when the game is shutting down. + /// + void Shutdown(); } diff --git a/physics/Engine/Helpers/ConstraintHelper.cs b/physics/Engine/Helpers/ConstraintHelper.cs index d613520..e061e28 100644 --- a/physics/Engine/Helpers/ConstraintHelper.cs +++ b/physics/Engine/Helpers/ConstraintHelper.cs @@ -26,6 +26,14 @@ public static Constraint AddWeldConstraint(this GameEngine engine, PhysicsObject return weldConstraint; } + public static Constraint AddSpringConstraint(this GameEngine engine, PhysicsObject objA, PhysicsObject objB) + { + var halfdiff = (objB.Center - objA.Center) / 2f; + var weldConstraint = new SpringConstraint(objA, objB, halfdiff, -halfdiff); + engine.PhysicsSystem.Constraints.Add(weldConstraint); + return weldConstraint; + } + public static Constraint AddAxisConstraint(this GameEngine engine, PhysicsObject objA, PhysicsObject objB, Vector2 localAnchorA, Vector2 localAnchorB) { var axisConstraint = new AxisConstraint(objA, objB, localAnchorA, localAnchorB); diff --git a/physics/Engine/Rendering/Renderer.cs b/physics/Engine/Rendering/Renderer.cs index 91c4a4a..6717b7d 100644 --- a/physics/Engine/Rendering/Renderer.cs +++ b/physics/Engine/Rendering/Renderer.cs @@ -1,14 +1,9 @@ using SFML.Graphics; using SFML.System; using System; -using System.Collections.Generic; -using physics.Engine.Rendering.UI; using SFML.Window; using System.Numerics; -using physics.Engine.Shaders; -using physics.Engine.Objects; using physics.Engine.Helpers; -using System.Security.AccessControl; namespace physics.Engine.Rendering { @@ -17,32 +12,16 @@ public class Renderer public RenderWindow Window { get; private set; } public View GameView { get; private set; } public View UiView { get; private set; } + public View BackgroundView { get; private set; } - private Text debugText; - private Text _reusableText; // Reusable text object for DrawText calls - private Font debugFont; - private UiManager _debugUiManager = new UiManager(); + private Text _reusableText; + private Font _defaultFont; private PhysicsSystem _physicsSystem; + private Color _ConstraintAColor = new Color(255, 0, 0, 100); + private Color _ConstraintBColor = new Color(0, 255, 255, 100); - /// - /// Gets the debug UI manager. Games like DemoGame can use this for physics debug controls. - /// - public UiManager DebugUiManager => _debugUiManager; - - /// - /// Gets the default font used for UI rendering. - /// - public Font DefaultFont => debugFont; - - /// - /// Controls whether the debug UI (sliders, debug text) is shown. - /// - public bool ShowDebugUI { get; set; } = true; + public Font DefaultFont => _defaultFont; - /// - /// Draws text at the specified position (in screen coordinates). - /// Call this during game's Render method. - /// public void DrawText(string text, float x, float y, uint size = 24, Color? color = null) { Window.SetView(UiView); @@ -60,21 +39,19 @@ public Renderer(uint windowWidth, uint windowHeight, string windowTitle, Physics Window = new RenderWindow(new VideoMode(windowWidth, windowHeight), windowTitle, Styles.Close, settings); Window.Closed += (s, e) => Window.Close(); - // Create and set a view covering the whole window. GameView = new View(new FloatRect(0, 0, windowWidth, windowHeight)); UiView = new View(new FloatRect(0, 0, windowWidth, windowHeight)); + BackgroundView = new View(new FloatRect(0, 0, windowWidth, windowHeight)); Window.SetFramerateLimit(144); _physicsSystem = physicsSystem; - InitializeUi(windowWidth, windowHeight); + + _defaultFont = new Font("Resources/good_timing_bd.otf"); + _reusableText = new Text("", _defaultFont, 24); } #region View Pan/Zoom Methods - /// - /// Pans the game view by the specified delta in world coordinates. - /// - /// Amount to pan the view. public void PanView(Vector2 delta) { GameView.Center += new Vector2f(delta.X, delta.Y); @@ -144,149 +121,20 @@ public void ResetView(uint windowWidth, uint windowHeight) #endregion - /// - /// Initialize UI elements for the window - /// - /// - /// - private void InitializeUi(uint windowWidth, uint windowHeight) - { - // Load the font from the embedded Resources folder. - // This path is relative to the working directory (usually the output folder). - debugFont = new Font("Resources/good_timing_bd.otf"); - debugText = new Text("", debugFont, 12) - { - FillColor = Color.White, - Position = new Vector2f(40, 40) - }; - _reusableText = new Text("", debugFont, 24); // Reusable text for DrawText calls - - // Note: Debug UI elements are managed by _debugUiManager - - UiElement roundedRect = new UiRoundedRectangle(new Vector2(140, 80), 5, 32) - { - OutlineColor = Color.Red - }; - roundedRect.Position = new Vector2(30, 30); - _debugUiManager.Add(roundedRect); - - // UI Elements for "Enable viewing normals" - var viewingNormalsLabel = new UiTextLabel("Contact Normals", debugFont) - { - Position = new Vector2(200, 30), - CharacterSize = 14 // optional customization - }; - var viewingNormalsCheckbox = new UiCheckbox(new Vector2(350, 30), new Vector2(20, 20)); - viewingNormalsCheckbox.IsChecked = SFMLPolyShader.DrawNormals; - viewingNormalsCheckbox.OnClick += (isChecked) => - { - SFMLPolyShader.DrawNormals = isChecked; - }; - _debugUiManager.Add(viewingNormalsLabel); - _debugUiManager.Add(viewingNormalsCheckbox); - - // Add Gravity X control - var gravityXLabelPosition = new Vector2(200, 60); - var gravityXSliderPosition = new Vector2(200, 80); - - var gravityXLabel = new UiTextLabel("Gravity X", debugFont) - { - Position = gravityXLabelPosition, - CharacterSize = 14 - }; - _debugUiManager.Add(gravityXLabel); - - var gravityXSlider = new UiSlider(gravityXSliderPosition, new Vector2(150, 20), -20f, 20f, _physicsSystem.Gravity.X); - gravityXSlider.OnValueChanged += (value) => - { - var currentGravity = _physicsSystem.Gravity; - _physicsSystem.Gravity = new Vector2(value, currentGravity.Y); - }; - _debugUiManager.Add(gravityXSlider); - - // Add Gravity Y control - var gravityYLabelPosition = new Vector2(400, 60); - var gravityYSliderPosition = new Vector2(400, 80); - - var gravityYLabel = new UiTextLabel("Gravity Y", debugFont) - { - Position = gravityYLabelPosition, - CharacterSize = 14 - }; - _debugUiManager.Add(gravityYLabel); - - var gravityYSlider = new UiSlider(gravityYSliderPosition, new Vector2(150, 20), -20f, 20f, _physicsSystem.Gravity.Y); - gravityYSlider.OnValueChanged += (value) => - { - var currentGravity = _physicsSystem.Gravity; - _physicsSystem.Gravity = new Vector2(currentGravity.X, value); - }; - _debugUiManager.Add(gravityYSlider); - - // Add Simulation Speed control - var simSpeedLabelPosition = new Vector2(200, 110); - var simSpeedSliderPosition = new Vector2(200, 130); - - var simSpeedLabel = new UiTextLabel("Simulation Speed", debugFont) - { - Position = simSpeedLabelPosition, - CharacterSize = 14 - }; - _debugUiManager.Add(simSpeedLabel); - - var simSpeedSlider = new UiSlider(simSpeedSliderPosition, new Vector2(150, 20), 0.1f, 2f, _physicsSystem.TimeScale); - simSpeedSlider.OnValueChanged += (value) => - { - _physicsSystem.TimeScale = value; - }; - _debugUiManager.Add(simSpeedSlider); - - // Add Pause/Resume button - var pauseButtonPosition = new Vector2(450, 30); - var pauseButton = new UiButton("Pause", debugFont, pauseButtonPosition, new Vector2(70, 20)); - pauseButton.OnClick += (state) => - { - _physicsSystem.IsPaused = !_physicsSystem.IsPaused; - pauseButton.Text = _physicsSystem.IsPaused ? "Resume" : "Pause"; - }; - _debugUiManager.Add(pauseButton); - } - - /// - /// Render the game window view and UI elements view - /// - /// - /// - /// - public void Render(long msPhysicsTime, long msDrawTime, long msFrameTime) + public void BeginFrame(Color backColor) { - - // Draw Game View - DrawGameView(); - - DrawUiView(msPhysicsTime, msDrawTime, msFrameTime); - - // Note: Window.Display() is called by GameEngine after game-specific rendering + Window.SetView(BackgroundView); + Window.Clear(backColor); } /// - /// Presents the rendered frame to the screen. - /// Called by GameEngine after all rendering (engine + game) is complete. + /// Renders physics objects and constraints. + /// Called after game background rendering. /// - public void Display() - { - Window.Display(); - } - - private void DrawGameView() + public void RenderPhysicsObjects() { - // Switch to Game window view Window.SetView(GameView); - // Clear with black color - Window.Clear(Color.Black); - - // Draw all static objects with their shaders. foreach (var obj in _physicsSystem.ListStaticObjects) { var sfmlShader = obj.Shader; @@ -298,18 +146,24 @@ private void DrawGameView() } } - // Draw all static objects with their shaders. foreach (var obj in _physicsSystem.Constraints) { var a = obj.A.Center + PhysMath.RotateVector(obj.AnchorA, obj.A.Angle); var b = obj.B.Center + PhysMath.RotateVector(obj.AnchorB, obj.B.Angle); - DrawLine(obj.A.Center, a, Color.Yellow, 1f); - DrawLine(obj.B.Center, b, Color.Red, 1f); + DrawLine(obj.A.Center, a, _ConstraintAColor, 2f); + DrawLine(obj.B.Center, b, _ConstraintBColor, 2f); } } + public void Display() + { + Window.Display(); + } + #region Public Primitive Drawing Methods + + private readonly VertexArray _lineRenderer = new VertexArray(PrimitiveType.Lines, 2); /// /// Draws a line between two points in game coordinates. /// @@ -321,21 +175,14 @@ public void DrawLine(Vector2 start, Vector2 end, Color color, float thickness = { Window.SetView(GameView); - var direction = end - start; - var length = direction.Length(); - var angle = MathF.Atan2(direction.Y, direction.X) * 180f / MathF.PI; - - var line = new RectangleShape(new Vector2f(length, thickness)) - { - Position = new Vector2f(start.X, start.Y), - FillColor = color, - Rotation = angle, - Origin = new Vector2f(0, thickness / 2) - }; + _lineRenderer[0] = new Vertex(new Vector2f(start.X, start.Y), color); + _lineRenderer[1] = new Vertex(new Vector2f(end.X, end.Y), color); - Window.Draw(line); + Window.Draw(_lineRenderer); } + + private readonly CircleShape _circleShapeRenderer = new CircleShape(); /// /// Draws a circle at the specified position in game coordinates. /// @@ -348,17 +195,16 @@ public void DrawCircle(Vector2 center, float radius, Color fillColor, Color? out { Window.SetView(GameView); - var circle = new CircleShape(radius) - { - Position = new Vector2f(center.X - radius, center.Y - radius), - FillColor = fillColor, - OutlineColor = outlineColor ?? Color.White, - OutlineThickness = outlineThickness - }; + _circleShapeRenderer.Radius = radius; + _circleShapeRenderer.Position = new Vector2f(center.X - radius, center.Y - radius); + _circleShapeRenderer.FillColor = fillColor; + _circleShapeRenderer.OutlineColor = outlineColor ?? Color.White; + _circleShapeRenderer.OutlineThickness = outlineThickness; - Window.Draw(circle); + Window.Draw(_circleShapeRenderer); } + private readonly RectangleShape _rectangleShapeRenderer = new RectangleShape(); /// /// Draws a filled rectangle in game coordinates. /// @@ -371,163 +217,22 @@ public void DrawRectangle(Vector2 position, Vector2 size, Color fillColor, Color { Window.SetView(GameView); - var rect = new RectangleShape(new Vector2f(size.X, size.Y)) - { - Position = new Vector2f(position.X, position.Y), - FillColor = fillColor, - OutlineColor = outlineColor ?? Color.Transparent, - OutlineThickness = outlineThickness - }; + //var rect = new RectangleShape(new Vector2f(size.X, size.Y)) + //{ + // Position = new Vector2f(position.X, position.Y), + // FillColor = fillColor, + // OutlineColor = outlineColor ?? Color.Transparent, + // OutlineThickness = outlineThickness + //}; - Window.Draw(rect); - } + _rectangleShapeRenderer.Size = new Vector2f(size.X, size.Y); + _rectangleShapeRenderer.Position = new Vector2f(position.X, position.Y); + _rectangleShapeRenderer.FillColor = fillColor; + _rectangleShapeRenderer.OutlineColor = outlineColor ?? Color.Transparent; + _rectangleShapeRenderer.OutlineThickness = outlineThickness; - /// - /// Draws a polygon (connected line segments) in game coordinates. - /// More efficient than multiple DrawLine calls for connected paths. - /// - /// Array of points defining the polygon vertices. - /// Color of the lines. - /// Thickness of the lines. - /// If true, connects the last point to the first. - public void DrawPolygon(Vector2[] points, Color color, float thickness = 2f, bool closed = false) - { - if (points == null || points.Length < 2) return; - - Window.SetView(GameView); - - int count = closed ? points.Length : points.Length - 1; - for (int i = 0; i < count; i++) - { - var start = points[i]; - var end = points[(i + 1) % points.Length]; - - var direction = end - start; - var length = direction.Length(); - var angle = MathF.Atan2(direction.Y, direction.X) * 180f / MathF.PI; - - var line = new RectangleShape(new Vector2f(length, thickness)) - { - Position = new Vector2f(start.X, start.Y), - FillColor = color, - Rotation = angle, - Origin = new Vector2f(0, thickness / 2) - }; - - Window.Draw(line); - } - } - - /// - /// Draws a filled convex polygon in game coordinates. - /// - /// Array of points defining the polygon vertices (must be convex). - /// Fill color. - /// Optional outline color. - /// Outline thickness. - public void DrawFilledPolygon(Vector2[] points, Color fillColor, Color? outlineColor = null, float outlineThickness = 0f) - { - if (points == null || points.Length < 3) return; - - Window.SetView(GameView); - - var convex = new ConvexShape((uint)points.Length); - for (int i = 0; i < points.Length; i++) - { - convex.SetPoint((uint)i, new Vector2f(points[i].X, points[i].Y)); - } - - convex.FillColor = fillColor; - convex.OutlineColor = outlineColor ?? Color.Transparent; - convex.OutlineThickness = outlineThickness; - - Window.Draw(convex); + Window.Draw(_rectangleShapeRenderer); } - - /// - /// Draws multiple line segments efficiently (for skeleton rendering, etc.). - /// Each pair of points defines a line segment. - /// - /// Array of line segments as (start, end) tuples. - /// Color for all lines. - /// Thickness of the lines. - public void DrawLineSegments((Vector2 Start, Vector2 End)[] segments, Color color, float thickness = 2f) - { - if (segments == null || segments.Length == 0) return; - - Window.SetView(GameView); - - foreach (var (start, end) in segments) - { - var direction = end - start; - var length = direction.Length(); - if (length < 0.001f) continue; // Skip zero-length lines - - var angle = MathF.Atan2(direction.Y, direction.X) * 180f / MathF.PI; - - var line = new RectangleShape(new Vector2f(length, thickness)) - { - Position = new Vector2f(start.X, start.Y), - FillColor = color, - Rotation = angle, - Origin = new Vector2f(0, thickness / 2) - }; - - Window.Draw(line); - } - } - - /// - /// Draws multiple line segments with individual colors. - /// - /// Array of line segments with colors. - /// Thickness of the lines. - public void DrawColoredLineSegments((Vector2 Start, Vector2 End, Color Color)[] segments, float thickness = 2f) - { - if (segments == null || segments.Length == 0) return; - - Window.SetView(GameView); - - foreach (var (start, end, color) in segments) - { - var direction = end - start; - var length = direction.Length(); - if (length < 0.001f) continue; - - var angle = MathF.Atan2(direction.Y, direction.X) * 180f / MathF.PI; - - var line = new RectangleShape(new Vector2f(length, thickness)) - { - Position = new Vector2f(start.X, start.Y), - FillColor = color, - Rotation = angle, - Origin = new Vector2f(0, thickness / 2) - }; - - Window.Draw(line); - } - } - #endregion - - private void DrawUiView(long msPhysicsTime, long msDrawTime, long msFrameTime) - { - // Switch to UI window view - Window.SetView(UiView); - - // Draw debug info only if enabled - if (ShowDebugUI) - { - debugText.DisplayedString = - $"ms physics time: {msPhysicsTime}\n" + - $"ms draw time: {msDrawTime}\n" + - $"frame rate: {1000 / Math.Max(msFrameTime, 1)}\n" + - $"num objects: {_physicsSystem.ListStaticObjects.Count}"; - Window.Draw(debugText); - - // Draw debug UI elements - _debugUiManager.Draw(Window); - } - } } } diff --git a/physics/Engine/Shapes/CirclePhysShape.cs b/physics/Engine/Shapes/CirclePhysShape.cs index 76e5a7b..50cd9fa 100644 --- a/physics/Engine/Shapes/CirclePhysShape.cs +++ b/physics/Engine/Shapes/CirclePhysShape.cs @@ -21,9 +21,15 @@ public CirclePhysShape(float radius) // Build local vertices approximating a circle with 'resolution' points // around local (0,0). - for (int i = 0; i < SEGMENTS; i++) + + // multiples of 20 + var mulitplier = Math.Max(1,(int)radius / 20); + + var segmentCount = SEGMENTS * mulitplier; + + for (int i = 0; i < segmentCount; i++) { - float theta = (2f * (float)Math.PI * i) / SEGMENTS; + float theta = (2f * (float)Math.PI * i) / segmentCount; float x = Radius * (float)Math.Cos(theta); float y = Radius * (float)Math.Sin(theta); LocalVertices.Add(new Vector2(x, y));