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));