feat(app): add CameraController with F toggle and cursor capture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-10 20:27:11 +02:00
parent 7cf6ea267a
commit 22f684e8c6
2 changed files with 100 additions and 15 deletions

View file

@ -0,0 +1,31 @@
// src/AcDream.App/Rendering/CameraController.cs
namespace AcDream.App.Rendering;
public sealed class CameraController
{
public OrbitCamera Orbit { get; }
public FlyCamera Fly { get; }
public ICamera Active { get; private set; }
public bool IsFlyMode => Active == Fly;
public event Action<bool>? ModeChanged;
public CameraController(OrbitCamera orbit, FlyCamera fly)
{
Orbit = orbit;
Fly = fly;
Active = orbit;
}
public void ToggleFly()
{
Active = IsFlyMode ? (ICamera)Orbit : Fly;
ModeChanged?.Invoke(IsFlyMode);
}
public void SetAspect(float aspect)
{
Orbit.Aspect = aspect;
Fly.Aspect = aspect;
}
}

View file

@ -15,7 +15,8 @@ public sealed class GameWindow : IDisposable
private IInputContext? _input;
private TerrainRenderer? _terrain;
private Shader? _shader;
private OrbitCamera? _camera;
private CameraController? _cameraController;
private IMouse? _capturedMouse;
private DatCollection? _dats;
private float _lastMouseX;
private float _lastMouseY;
@ -42,6 +43,7 @@ public sealed class GameWindow : IDisposable
_window = Window.Create(options);
_window.Load += OnLoad;
_window.Update += OnUpdate;
_window.Render += OnRender;
_window.Closing += OnClosing;
@ -55,26 +57,49 @@ public sealed class GameWindow : IDisposable
foreach (var kb in _input.Keyboards)
kb.KeyDown += (_, key, _) =>
{
if (key == Key.Escape)
_window!.Close();
if (key == Key.F)
_cameraController?.ToggleFly();
else if (key == Key.Escape)
{
if (_cameraController?.IsFlyMode == true)
_cameraController.ToggleFly(); // exit fly, release cursor
else
_window!.Close();
}
};
foreach (var mouse in _input.Mice)
{
mouse.MouseMove += (m, pos) =>
{
if (m.IsButtonPressed(MouseButton.Left))
if (_cameraController is null) return;
if (_cameraController.IsFlyMode)
{
_camera!.Yaw -= (pos.X - _lastMouseX) * 0.005f;
_camera!.Pitch = Math.Clamp(
_camera.Pitch + (pos.Y - _lastMouseY) * 0.005f,
0.1f, 1.5f);
// Raw cursor mode: Silk.NET gives deltas via position. Compute delta from last.
float dx = pos.X - _lastMouseX;
float dy = pos.Y - _lastMouseY;
_cameraController.Fly.Look(dx, dy);
}
else
{
if (m.IsButtonPressed(MouseButton.Left))
{
_cameraController.Orbit.Yaw -= (pos.X - _lastMouseX) * 0.005f;
_cameraController.Orbit.Pitch = Math.Clamp(
_cameraController.Orbit.Pitch + (pos.Y - _lastMouseY) * 0.005f,
0.1f, 1.5f);
}
}
_lastMouseX = pos.X;
_lastMouseY = pos.Y;
};
mouse.Scroll += (_, scroll) =>
_camera!.Distance = Math.Clamp(_camera.Distance - scroll.Y * 20f, 50f, 2000f);
{
if (_cameraController is null || _cameraController.IsFlyMode) return;
_cameraController.Orbit.Distance = Math.Clamp(
_cameraController.Orbit.Distance - scroll.Y * 20f, 50f, 2000f);
};
}
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
@ -89,10 +114,10 @@ public sealed class GameWindow : IDisposable
Path.Combine(shadersDir, "mesh.vert"),
Path.Combine(shadersDir, "mesh.frag"));
_camera = new OrbitCamera
{
Aspect = _window!.Size.X / (float)_window.Size.Y,
};
var orbit = new OrbitCamera { Aspect = _window!.Size.X / (float)_window.Size.Y };
var fly = new FlyCamera { Aspect = _window.Size.X / (float)_window.Size.Y };
_cameraController = new CameraController(orbit, fly);
_cameraController.ModeChanged += OnCameraModeChanged;
_dats = new DatCollection(_datDir, DatAccessType.Read);
@ -199,11 +224,40 @@ public sealed class GameWindow : IDisposable
Console.WriteLine($"hydrated {_entities.Count} entities");
}
private void OnUpdate(double dt)
{
if (_cameraController is null || _input is null) return;
if (!_cameraController.IsFlyMode) return;
var kb = _input.Keyboards[0];
_cameraController.Fly.Update(
dt,
w: kb.IsKeyPressed(Key.W),
a: kb.IsKeyPressed(Key.A),
s: kb.IsKeyPressed(Key.S),
d: kb.IsKeyPressed(Key.D),
up: kb.IsKeyPressed(Key.Space),
down: kb.IsKeyPressed(Key.ControlLeft));
}
private void OnCameraModeChanged(bool isFlyMode)
{
if (_input is null) return;
var mouse = _input.Mice.FirstOrDefault();
if (mouse is null) return;
mouse.Cursor.CursorMode = isFlyMode ? CursorMode.Raw : CursorMode.Normal;
_capturedMouse = isFlyMode ? mouse : null;
}
private void OnRender(double deltaSeconds)
{
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_terrain?.Draw(_camera!);
_staticMesh?.Draw(_camera!, _entities);
if (_cameraController is not null)
{
_terrain?.Draw(_cameraController.Active);
_staticMesh?.Draw(_cameraController.Active, _entities);
}
}
private void OnClosing()