diff --git a/Core/Audio/Audio.cs b/Core/Audio/Audio.cs new file mode 100644 index 0000000..7d9d171 --- /dev/null +++ b/Core/Audio/Audio.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; + +namespace SharpEngine.Core.Audio; + +/// +/// Handles Audio related operations. +/// +public static class Audio +{ + /// + /// Plays an audio file if it is in the WAV format. + /// + /// Specifies the location of the audio file to be played. + /// Thrown when the file format is not supported, such as when it is not a WAV file. + public static void Play(string filePath) + { + switch (Path.GetExtension(filePath)) + { + case ".wav": + new WavPlayer().Play(filePath); + break; + + default: + throw new NotSupportedException("Unsupported file format."); + } + } +} diff --git a/Core/Audio/AudioBuffer.cs b/Core/Audio/AudioBuffer.cs new file mode 100644 index 0000000..506f426 --- /dev/null +++ b/Core/Audio/AudioBuffer.cs @@ -0,0 +1,43 @@ +using Silk.NET.OpenAL; +using System; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents an audio buffer with a unique identifier. +/// +public class AudioBuffer +{ + /// Gets the identifier to the audio buffer. + public uint BufferId => _buffer; + + private readonly uint _buffer; + private readonly AL _al; + + /// + /// Initializes a new instance of . + /// + /// The OpenAL API context. + public AudioBuffer(AL al) + { + _al = al; + _buffer = _al.GenBuffer(); + } + + /// + /// Loads the given into the buffer. + /// + /// The data to be loaded into the buffer. + /// Details about the data. + public void LoadData(ReadOnlySpan data, WavData wavData) + { + unsafe + { + fixed (byte* pData = data) + { + _al.BufferData(_buffer, wavData.Format, pData, data.Length, wavData.SampleRate); + } + } + } + +} \ No newline at end of file diff --git a/Core/Audio/AudioDevice.cs b/Core/Audio/AudioDevice.cs new file mode 100644 index 0000000..aa0560d --- /dev/null +++ b/Core/Audio/AudioDevice.cs @@ -0,0 +1,46 @@ +using Silk.NET.OpenAL; +using System; + +namespace SharpEngine.Core.Audio; + +/// +/// Manages an audio device and its context for audio operations. +/// +public unsafe class AudioDevice : IDisposable +{ + /// Gets the OpenAL context. + public AL Al { get; } + + private readonly ALContext _alc; + private readonly Device* _device; + private readonly Context* _context; + + /// + /// Initializes a new instance of . + /// + /// Thrown when unable to create the audio device. + public AudioDevice() + { + _alc = ALContext.GetApi(); + Al = AL.GetApi(); + _device = _alc.OpenDevice(""); + + if (_device == null) + throw new InvalidOperationException("Could not create device."); + + _context = _alc.CreateContext(_device, null); + _alc.MakeContextCurrent(_context); + Al.GetError(); + } + + /// + public void Dispose() + { + _alc.DestroyContext(_context); + _alc.CloseDevice(_device); + Al.Dispose(); + _alc.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/Core/Audio/AudioFileExtensions.cs b/Core/Audio/AudioFileExtensions.cs new file mode 100644 index 0000000..93160e2 --- /dev/null +++ b/Core/Audio/AudioFileExtensions.cs @@ -0,0 +1,16 @@ +namespace SharpEngine.Core.Audio; + +/// +/// Contains all the supported audio file extensions. +/// +public static class AudioFileExtensions +{ + /// The file extension of the Waveform audio file. + public const string WavExtension = ".wav"; + + /// The file extension of the MPEG-1 Audio Layer III or MPEG-2 Audio Layer III (MP3) file. + public const string Mp3Extension = ".mp3"; + + /// The file extension of the MPEG-4 (MP4) file. + public const string Mp4Extension = ".mp4"; +} diff --git a/Core/Audio/AudioPlayerBase.cs b/Core/Audio/AudioPlayerBase.cs new file mode 100644 index 0000000..5778672 --- /dev/null +++ b/Core/Audio/AudioPlayerBase.cs @@ -0,0 +1,67 @@ +using SharpEngine.Core.Scenes; + +using System; +using System.IO; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents a base class for all audio players. +/// +public abstract class AudioPlayerBase : SceneNode +{ + /// Gets the extension accepted by the audio player. + protected abstract string FileExtension { get; } + + /// Gets or sets the properties for the player. + public AudioProperties AudioProperties { get; set; } + + /// Contains the audio data of the media file. + protected readonly WavData Data; + + /// Represents a container for the . + protected readonly AudioBuffer AudioBuffer; + + /// The source of the audio. + protected readonly AudioSource AudioSource; + + /// + /// Initializes a new instance of . + /// + protected AudioPlayerBase() + { + Data = new WavData(); + + var audioDevice = new AudioDevice(); + AudioBuffer = new AudioBuffer(audioDevice.Al); + AudioSource = new AudioSource(audioDevice.Al); + + AudioProperties = new AudioProperties(audioDevice.Al, AudioSource); + } + + /// + /// Plays a media file specified by the given path. + /// + /// The location of the media file to be played. + public abstract void Play(string filePath); + + /// + /// Validates the specified file by checking its extension and existence. + /// + /// The path to the file that needs to be validated for its type and existence. + /// Thrown when the file does not have the allowed extension. + /// Thrown when the specified file does not exist in the file system. + protected virtual void ValidateFile(string filePath) + { + if (Path.GetExtension(filePath) != FileExtension) + throw new ArgumentException($"Given file is not a {FileExtension} file", nameof(filePath)); + + if (!File.Exists(filePath)) + throw new FileNotFoundException("Given file does not exist", filePath); + } + + /// + /// Stops the audio playback of the associated AudioSource. + /// + public virtual void Stop() => AudioSource.Stop(); +} diff --git a/Core/Audio/AudioProperties.cs b/Core/Audio/AudioProperties.cs new file mode 100644 index 0000000..8a57383 --- /dev/null +++ b/Core/Audio/AudioProperties.cs @@ -0,0 +1,36 @@ +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +/// +/// Contains properties used to control how the audio should be played. +/// +public class AudioProperties +{ + private readonly AL _al; + private readonly uint _audioSourceId; + + /// + /// Initializes a new instance of . + /// + /// The context where the audio should be played. + /// The source at which the audio should be played. + public AudioProperties(AL al, AudioSource audioSource) + { + _al = al; + _audioSourceId = audioSource.Get(); + } + + /// Gets or sets whether the audio should be looping. + public bool IsLooping + { + get => _isLooping; + set + { + _isLooping = value; + _al.SetSourceProperty(_audioSourceId, SourceBoolean.Looping, value); + } + } + + private bool _isLooping; +} diff --git a/Core/Audio/AudioSource.cs b/Core/Audio/AudioSource.cs new file mode 100644 index 0000000..9e59179 --- /dev/null +++ b/Core/Audio/AudioSource.cs @@ -0,0 +1,47 @@ +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents a source for audio +/// +public class AudioSource +{ + private readonly uint _source; + private readonly AL _al; + + /// + /// Initializes a new instance of . + /// + /// An accessor to the OpenAL context. + public AudioSource(AL al) + { + _al = al; + _source = _al.GenSource(); + } + + /// + /// Gets the source identifier. + /// + /// The identifier to the source. + public uint Get() => _source; + + /// + /// Plays the given audio from the . + /// + /// A buffer containing the audio data. + public void Play(AudioBuffer buffer) + { + _al.SetSourceProperty(_source, SourceInteger.Buffer, buffer.BufferId); + _al.SourcePlay(_source); + } + + /// + /// Stops playing the audio. + /// + public void Stop() + { + _al.SourceStop(_source); + _al.DeleteSource(_source); + } +} \ No newline at end of file diff --git a/Core/Audio/WavData.cs b/Core/Audio/WavData.cs new file mode 100644 index 0000000..80ac937 --- /dev/null +++ b/Core/Audio/WavData.cs @@ -0,0 +1,22 @@ +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents audio data in WAV format. +/// +public class WavData +{ + /// Gets or sets the number of channels used in the data. + public short NumChannels { get; set; } + + /// Gets or sets the sample rate of the data. + public int SampleRate { get; set; } + + /// Gets or sets the number of bits per sample. + public short BitsPerSample { get; set; } + + /// Gets or sets the format of the buffer. + /// Defines how data is organized in the buffer. + public BufferFormat Format { get; set; } +} \ No newline at end of file diff --git a/Core/Audio/WavPlayer.cs b/Core/Audio/WavPlayer.cs new file mode 100644 index 0000000..02f9ed7 --- /dev/null +++ b/Core/Audio/WavPlayer.cs @@ -0,0 +1,115 @@ +using Silk.NET.OpenAL; + +using System; +using System.Buffers.Binary; +using System.IO; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents a player for .wav files. +/// +public class WavPlayer : AudioPlayerBase +{ + /// + protected override string FileExtension => ".wav"; + + private static class WavConstants + { + public static ReadOnlySpan RiffHeader => "RIFF"u8; + public static ReadOnlySpan WaveHeader => "WAVE"u8; + public static ReadOnlySpan FmtHeader => "fmt "u8; + public static ReadOnlySpan DataHeader => "data"u8; + + public const int ChunkSizeLength = 4; + public const int HeaderLength = 4; + public const int FormatChunkSkipLength = 6; + public const int AudioFormatLength = 2; + public const int NumChannelsLength = 2; + public const int SampleRateLength = 4; + public const int BitsPerSampleLength = 2; + } + + /// + public override void Play(string filePath) + { + ValidateFile(filePath); + + ReadOnlySpan file = File.ReadAllBytes(filePath); + int index = 0; + + if (!CheckHeader(file, ref index, WavConstants.RiffHeader)) + { + Console.WriteLine("Given file is not in RIFF format"); + return; + } + + index += WavConstants.ChunkSizeLength; // Skip chunk size + + if (!CheckHeader(file, ref index, WavConstants.WaveHeader)) + { + Console.WriteLine("Given file is not in WAVE format"); + return; + } + + while (index + WavConstants.HeaderLength <= file.Length) + { + var identifier = file.Slice(index, WavConstants.HeaderLength); + index += WavConstants.HeaderLength; + + var size = BinaryPrimitives.ReadInt32LittleEndian(file.Slice(index, WavConstants.ChunkSizeLength)); + index += WavConstants.ChunkSizeLength; + + var data = file.Slice(index, size); + switch (identifier) + { + case var id when id.SequenceEqual(WavConstants.FmtHeader): + ParseFormat(data, Data); + break; + + case var id when id.SequenceEqual(WavConstants.DataHeader): + AudioBuffer.LoadData(data, Data); + break; + } + + index += size; + } + + AudioSource.Play(AudioBuffer); + } + + private static bool CheckHeader(ReadOnlySpan file, ref int index, ReadOnlySpan header) + { + if (!file.Slice(index, header.Length).SequenceEqual(header)) + return false; + + index += header.Length; + return true; + } + + private static void ParseFormat(ReadOnlySpan formatChunk, WavData wavData) + { + int index = 0; + var audioFormat = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.AudioFormatLength)); + index += WavConstants.AudioFormatLength; + + if (audioFormat != 1) + throw new InvalidOperationException($"Unknown Audio Format with ID {audioFormat}"); + + wavData.NumChannels = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.NumChannelsLength)); + index += WavConstants.NumChannelsLength; + + wavData.SampleRate = BinaryPrimitives.ReadInt32LittleEndian(formatChunk.Slice(index, WavConstants.SampleRateLength)); + index += WavConstants.SampleRateLength; + + index += WavConstants.FormatChunkSkipLength; // Skip byteRate and blockAlign + wavData.BitsPerSample = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.BitsPerSampleLength)); + + wavData.Format = wavData.NumChannels switch + { + 1 => wavData.BitsPerSample == 8 ? BufferFormat.Mono8 : BufferFormat.Mono16, + 2 => wavData.BitsPerSample == 8 ? BufferFormat.Stereo8 : BufferFormat.Stereo16, + _ => throw new InvalidOperationException($"Can't play audio with {wavData.NumChannels} channels") + }; + } +} diff --git a/Core/Entities/Views/CameraView.cs b/Core/Entities/Views/CameraView.cs index f3eb785..69af67c 100644 --- a/Core/Entities/Views/CameraView.cs +++ b/Core/Entities/Views/CameraView.cs @@ -24,20 +24,6 @@ public CameraView(Vector3 position, IViewSettings settings) : base(settings) AspectRatio = settings.WindowOptions.Size.X / (float)settings.WindowOptions.Size.Y; } - // Those vectors are directions pointing outwards from the camera to define how it rotated. - private Vector3 _front = -Vector3.UnitZ; - private Vector3 _up = Vector3.UnitY; - private Vector3 _right = Vector3.UnitX; - - /// Gets the front vector of the camera. - public Vector3 Front => _front; - - /// Gets the up vector of the camera. - public Vector3 Up => _up; - - /// Gets the right vector of the camera. - public Vector3 Right => _right; - // Rotation around the X axis (radians) private float _pitch; diff --git a/Core/Entities/Views/Settings/DefaultViewSettings.cs b/Core/Entities/Views/Settings/DefaultViewSettings.cs index 5a777e1..cef7852 100644 --- a/Core/Entities/Views/Settings/DefaultViewSettings.cs +++ b/Core/Entities/Views/Settings/DefaultViewSettings.cs @@ -1,5 +1,5 @@ using SharpEngine.Core.Renderers; - +using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.Windowing; @@ -19,7 +19,7 @@ public DefaultViewSettings() } /// - /// Convenience wrapper around creating a new WindowProperties with sensible defaults. + /// Convenience wrapper around creating a new WindowProperties with sensible defaults. /// public static ViewSettings Default { get; private set; } @@ -41,4 +41,10 @@ public DefaultViewSettings() Title = "SharpEngine", Size = new Vector2D(1280, 720), }; + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } diff --git a/Core/Entities/Views/Settings/ViewSettings.cs b/Core/Entities/Views/Settings/ViewSettings.cs index a28ffe3..09c65cb 100644 --- a/Core/Entities/Views/Settings/ViewSettings.cs +++ b/Core/Entities/Views/Settings/ViewSettings.cs @@ -1,6 +1,5 @@ -using SharpEngine.Core.Interfaces; -using SharpEngine.Core.Renderers; -using Silk.NET.Maths; +using SharpEngine.Core.Renderers; +using Silk.NET.Input; using Silk.NET.Windowing; namespace SharpEngine.Core.Entities.Views.Settings; @@ -47,4 +46,10 @@ public ViewSettings() /// public WindowOptions WindowOptions { get; set; } + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } diff --git a/Core/Entities/Views/View.cs b/Core/Entities/Views/View.cs index a6f5bf6..8f78b02 100644 --- a/Core/Entities/Views/View.cs +++ b/Core/Entities/Views/View.cs @@ -1,5 +1,8 @@ using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; +using System; +using System.Collections.Generic; using System.Numerics; namespace SharpEngine.Core.Entities.Views; @@ -18,6 +21,20 @@ public View(IViewSettings settings) Settings = settings; } + // Those vectors are directions pointing outwards from the camera to define how it rotated. + protected Vector3 _front = -Vector3.UnitZ; + protected Vector3 _up = Vector3.UnitY; + protected Vector3 _right = Vector3.UnitX; + + /// Gets the front vector of the camera. + public Vector3 Front => _front; + + /// Gets the up vector of the camera. + public Vector3 Up => _up; + + /// Gets the right vector of the camera. + public Vector3 Right => _right; + /// Gets or sets the settings for the view. public IViewSettings Settings { get; set; } @@ -65,4 +82,18 @@ public virtual void UpdateMousePosition(Vector2 mousePosition) firstMove = false; } } + + /// + /// Checks whether a game object is in view of the camera. + /// + /// + /// Uses a step based ray marching algorithm to check if any object is in view of the camera. + /// + /// The scene where the check should be applied to. + /// The types that are allowed to be hit. + /// The first object that the ray intersected. + /// The position where the object was hit. + /// if any object was hit; otherwise, . + public bool IsInView(Scene scene, out GameObject? intersectingObject, out Vector3 hitPosition, params Type[] allowedTypes) + => new Ray(Position, Front).IsGameObjectInView(scene, out intersectingObject, out hitPosition, allowedTypes); } \ No newline at end of file diff --git a/Core/Interfaces/ISettings.cs b/Core/Interfaces/ISettings.cs index e2ee475..17aa676 100644 --- a/Core/Interfaces/ISettings.cs +++ b/Core/Interfaces/ISettings.cs @@ -1,4 +1,5 @@ using SharpEngine.Core.Renderers; +using Silk.NET.Input; using Silk.NET.Windowing; namespace SharpEngine.Core.Interfaces; @@ -20,6 +21,9 @@ public interface ISettings /// Gets or sets which renderer should be enabled for the current window. public RenderFlags RendererFlags { get; set; } + + public MouseButton PrimaryButton { get; set; } + public MouseButton SecondaryButton { get; set; } } /// @@ -33,4 +37,10 @@ public class DefaultSettings : ISettings /// public RenderFlags RendererFlags { get; set; } = RenderFlags.All; + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } diff --git a/Core/Ray.cs b/Core/Ray.cs index 68f1dee..42bae6e 100644 --- a/Core/Ray.cs +++ b/Core/Ray.cs @@ -13,14 +13,10 @@ namespace SharpEngine.Core; /// public class Ray { - /// - /// Gets or sets the ray origin. - /// + /// Gets or sets the ray origin. public Vector3 Origin { get; set; } - /// - /// Gets or sets the ray direction. - /// + /// Gets or sets the ray direction. public Vector3 Direction { get; set; } /// @@ -35,7 +31,7 @@ public Ray(Vector3 origin, Vector3 direction) } /// - /// Checks if the ray intersects with any objects in the scene. + /// Checks if the ray intersects with any objects in the scene. /// /// The scene to check for intersections. /// The intersecting object; if the ray did not intersect an object. @@ -49,7 +45,7 @@ public bool IsGameObjectInView(Scene scene, out GameObject? intersectingObject, var sceneNodes = scene.GetAllGameObjects(); - if (allowedTypes.Any()) + if (allowedTypes.Length != 0) sceneNodes = [.. sceneNodes.Where(go => allowedTypes.Any(type => type.IsInstanceOfType(go)))]; for (float t = 0; t < maxDistance; t += stepSize) @@ -70,7 +66,7 @@ public bool IsGameObjectInView(Scene scene, out GameObject? intersectingObject, } /// - /// Determines if a point is inside a given object. + /// Determines if a point is inside a given object. /// /// The point to check. /// The object to check against. @@ -86,7 +82,7 @@ public static bool IsPointInsideObject(Vector3 point, GameObject obj) } /// - /// Gets the normal of the closest face of an object to a given point. + /// Gets the normal of the closest face of an object to a given point. /// /// The point to check. /// The object to check against. diff --git a/Core/SharpEngine.Core.csproj b/Core/SharpEngine.Core.csproj index 1489ce2..5ec1e72 100644 --- a/Core/SharpEngine.Core.csproj +++ b/Core/SharpEngine.Core.csproj @@ -1,4 +1,4 @@ - + Everything needed to build games using a purely C# game engine. @@ -11,18 +11,23 @@ $(AssemblyName).NET - Debug;Release;LauncherNonWindows + Debug;Release;LauncherNonWindows + true + + + + 2.22.0 - - - - - - + + + + + + diff --git a/Core/SilkWindow.cs b/Core/SilkWindow.cs index cbb0cb8..bcc1936 100644 --- a/Core/SilkWindow.cs +++ b/Core/SilkWindow.cs @@ -190,14 +190,14 @@ public IWindow CreateWindow(WindowOptions opts) return CurrentWindow; } - public static T Create(WindowOptions opts) where T : SilkWindow, new() - { - var instance = new T(); - var winndow = instance.CreateWindow(opts); - - instance.CurrentWindow = winndow; - return instance; - } + // public static T Create(WindowOptions opts) where T : SilkWindow, new() + // { + // var instance = new T(); + // var winndow = instance.CreateWindow(opts); + // + // instance.CurrentWindow = winndow; + // return instance; + // } /// protected virtual void Dispose(bool disposing) { } diff --git a/Core/Window.cs b/Core/Window.cs index af4cd49..cb1d7e3 100644 --- a/Core/Window.cs +++ b/Core/Window.cs @@ -29,13 +29,14 @@ namespace SharpEngine.Core; /// public class Window : SilkWindow { - private readonly IViewSettings _settings; - private readonly CameraView _camera; - private bool _initialized; private IEnumerable _renderers = []; private ImGuiController? _imGuiController; + public readonly CameraView Camera; + + public IViewSettings Settings; + /// The event executed when mouse events are executed. public event Action? OnHandleMouse; @@ -77,8 +78,8 @@ public Window() { } public Window(CameraView camera, Scene scene, IViewSettings settings) { Scene = scene; - _settings = settings; - _camera = camera; + Settings = settings; + Camera = camera; Initialize(); } @@ -90,8 +91,9 @@ public Window(CameraView camera, Scene scene, IViewSettings settings) public Window(Scene scene, IViewSettings settings) { Scene = scene; - _settings = settings; - _camera = new(Vector3.One, settings); + Settings = settings; + Camera = new(Vector3.One, settings); + Initialize(); } @@ -100,7 +102,7 @@ public Window(Scene scene, IViewSettings settings) // public void Initialize() where T : SilkWindow, new() private void Initialize() { - CurrentWindow = CreateWindow(_settings.WindowOptions); + CurrentWindow = CreateWindow(Settings.WindowOptions); CurrentWindow.Update += OnUpdateFrame; CurrentWindow.Render += RenderFrame; CurrentWindow.Resize += OnResize; @@ -157,7 +159,7 @@ public override void OnLoad() { // Make sure the renderer has the correct constructor parameters! // TODO: The static reference to the context will not work when multiple windows are implemented, since the context will be different. - var requiredArguments = new object[] { _camera, _settings, Scene }; + var requiredArguments = new object[] { Camera, Settings, Scene }; var renderer = (RendererBase)Activator.CreateInstance(type, requiredArguments)!; _renderers = _renderers.Append(renderer); @@ -197,11 +199,11 @@ protected void RenderFrame(double deltaTime) GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - ToggleWireFrame(_settings.UseWireFrame); + ToggleWireFrame(Settings.UseWireFrame); UseShaders(); - var renderTasks = _renderers.Where(renderer => _settings.RendererFlags.HasFlag(renderer.RenderFlag)) + var renderTasks = _renderers.Where(renderer => Settings.RendererFlags.HasFlag(renderer.RenderFlag)) .Select(renderer => renderer.Render()) .ToList(); @@ -249,14 +251,14 @@ protected void OnUpdateFrame(double deltaTime) // if (!_window.IsFocused) // return; - if (_settings.PrintFrameRate) + if (Settings.PrintFrameRate) Console.WriteLine($"FPS: {1f / deltaTime}"); // TODO: Handle multiple mice? var mouse = Input?.Mice[0]; if (mouse is not null) { - _camera.UpdateMousePosition(mouse.Position); + Camera.UpdateMousePosition(mouse.Position); OnHandleMouse?.Invoke(mouse); } @@ -311,14 +313,14 @@ protected void OnMouseWheel(IMouse mouse, ScrollWheel sw) }; HandleMouseWheel?.Invoke(direction, sw); - _camera.Fov -= sw.Y; + Camera.Fov -= sw.Y; } /// protected void OnResize(Vector2D size) { GL.Viewport(size); - _camera.AspectRatio = size.X / size.Y; + Camera.AspectRatio = size.X / size.Y; } /// diff --git a/Editor/EditorSettings.cs b/Editor/EditorSettings.cs index 12e447e..6b92693 100644 --- a/Editor/EditorSettings.cs +++ b/Editor/EditorSettings.cs @@ -1,5 +1,6 @@ using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Renderers; +using Silk.NET.Input; namespace SharpEngine.Editor; @@ -22,4 +23,10 @@ public class EditorSettings : IViewSettings /// public float MouseSensitivity { get; set; } + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } \ No newline at end of file diff --git a/Editor/EditorWindow.cs b/Editor/EditorWindow.cs index f5bc365..d46c5b5 100644 --- a/Editor/EditorWindow.cs +++ b/Editor/EditorWindow.cs @@ -1,13 +1,16 @@ using ImGuiNET; using Launcher.UI; -using Silk.NET.Input; using SharpEngine.Core; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Primitives; using SharpEngine.Core.Scenes; +using SharpEngine.Core.Entities; + using SharpEngine.Editor.Windows; +using Silk.NET.Input; + using System.Numerics; using System.Reflection; @@ -24,8 +27,6 @@ public class EditorWindow : Window private ContextMenuWindow? _contextMenuWindow; private ActionsMenuWindow? _actionsMenuWindow; - public EditorWindow() { } - /// /// Initializes an editor window with a specified scene and view settings. /// @@ -132,7 +133,31 @@ public override void OnMouseClick(IMouse mouse, MouseButton button, Vector2 vect { base.OnMouseClick(mouse, button, vector); - if (button == MouseButton.Right) - _contextMenuWindow?.ShowContextMenu(); + if (button == Settings.PrimaryButton) + { + if (ImGui.IsAnyItemHovered()) + { + // TODO: Right-clicked on an ImGui component + } + else + { + // TODO: Right-clicked outside of ImGui components + Camera.IsInView(Scene, out GameObject? intersectingObject, out Vector3 hitPosition); + _contextMenuWindow?.ShowContextMenu(intersectingObject); + } + } + + if (button == Settings.SecondaryButton) + { + if (ImGui.IsAnyItemHovered()) + { + // TODO: Left-clicked on an ImGui component + } + else + { + Camera.IsInView(Scene, out GameObject? intersectingObject, out Vector3 hitPosition); + Scene.ActiveElement = intersectingObject; + } + } } } diff --git a/Editor/Windows/ActionsMenuWindow.cs b/Editor/Windows/ActionsMenuWindow.cs index 7fb73bd..af5e223 100644 --- a/Editor/Windows/ActionsMenuWindow.cs +++ b/Editor/Windows/ActionsMenuWindow.cs @@ -1,6 +1,7 @@ using ImGuiNET; using Launcher.UI; using SharpEngine.Core; +using SharpEngine.Core.Audio; using SharpEngine.Core.Scenes; using SharpEngine.Shared; @@ -22,8 +23,6 @@ public class ActionsMenuWindow : ImGuiWindowBase /// public override void Render() { - ImGui.Text("some text"); - if (ImGui.Button("Save")) SaveScene(Scene); @@ -32,6 +31,26 @@ public override void Render() if (ImGui.Button("Play")) StartGame(); + + if (ImGui.Button("Test audio")) + PlayTestAudio(); + } + + private void PlayTestAudio() + { + try + { + string filePath = ""; // TODO: Replace with absolute path to .wav file. + + var audioPlayer = new WavPlayer(); + audioPlayer.AudioProperties.IsLooping = false; + + audioPlayer.Play(filePath); + } + catch (Exception ex) + { + Debug.LogInformation($"Error during audio playback: {ex.Message}"); + } } private void StartGame() diff --git a/Editor/Windows/AssetsWindow.cs b/Editor/Windows/AssetsWindow.cs index d8f3dd9..b9b7051 100644 --- a/Editor/Windows/AssetsWindow.cs +++ b/Editor/Windows/AssetsWindow.cs @@ -1,7 +1,5 @@ using ImGuiNET; using Launcher.UI; -using SharpEngine.Core; -using SharpEngine.Core.Scenes; namespace SharpEngine.Editor.Windows { diff --git a/Editor/Windows/ContextMenuWindow.cs b/Editor/Windows/ContextMenuWindow.cs index d7ea5ad..8b10d97 100644 --- a/Editor/Windows/ContextMenuWindow.cs +++ b/Editor/Windows/ContextMenuWindow.cs @@ -1,5 +1,6 @@ using ImGuiNET; using Launcher.UI; +using SharpEngine.Core.Entities; using SharpEngine.Core.Primitives; using SharpEngine.Core.Scenes; using System.Numerics; @@ -13,49 +14,74 @@ public class ContextMenuWindow : ImGuiWindowBase { private bool _showContextMenu; private bool _updateContextMenuLocation; + private GameObject? IntersectedObject; /// public override string Name => "Context Menu"; /// - public override ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; + public override ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.AlwaysAutoResize; /// /// Shows the context menu. /// - public void ShowContextMenu() + /// The object the cursor right-clicked on. + public void ShowContextMenu(GameObject? intersectedObject = null) { + IntersectedObject = intersectedObject; _updateContextMenuLocation = true; _showContextMenu = true; } + /// + public override void PreRender() + { + if (_updateContextMenuLocation) + { + ImGui.SetNextWindowPos(ImGui.GetMousePos()); + _updateContextMenuLocation = false; + } + } + /// public override void Render() { if (!_showContextMenu) + { + IntersectedObject = null; return; + } - if (Scene is null) - throw new InvalidOperationException("Scene is not set."); + if (Scene is not null) + RenderSceneActions(); - if (_updateContextMenuLocation) + if (IntersectedObject is not null) + RenderIntersectedObjectActions(); + } + + private void RenderIntersectedObjectActions() + { + if (ImGui.Button("Delete")) { - ImGui.SetNextWindowPos(ImGui.GetMousePos()); - _updateContextMenuLocation = false; + Scene.Root.RemoveChild(IntersectedObject); + IntersectedObject = null; } + } - ImGui.Begin("Context Menu", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove); - - if (ImGui.Button("Create cube")) + private void RenderSceneActions() + { + var values = Enum.GetValues(); + foreach (var value in values) { - var cube = PrimitiveFactory.Create(PrimitiveType.Cube, new Vector3(0, 0, 0)); - cube.Name = "Cube" + Scene.Root.Children.Count; + if (ImGui.Button($"Create {value}")) + { + var cube = PrimitiveFactory.Create(value, new Vector3(0, 0, 0)); + cube.Name = value.ToString() + Scene.Root.Children.Count; - Scene.ActiveElement = cube; - Scene.Root.AddChild(cube); + Scene.ActiveElement = cube; + Scene.Root.AddChild(cube); + } } - - ImGui.End(); } } } diff --git a/Editor/Windows/ImGuiWindowBase.cs b/Editor/Windows/ImGuiWindowBase.cs index c48c182..a75b2b3 100644 --- a/Editor/Windows/ImGuiWindowBase.cs +++ b/Editor/Windows/ImGuiWindowBase.cs @@ -1,13 +1,8 @@ using ImGuiNET; using Launcher.UI; + using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Scenes; -using Silk.NET.Input; -using Silk.NET.Maths; -using Silk.NET.OpenGL; -using Silk.NET.OpenGL.Extensions.ImGui; -using Silk.NET.Windowing; -using System.Threading; namespace SharpEngine.Editor.Windows { @@ -51,11 +46,18 @@ public ImGuiWindowBase() /// The new project. public void SetProject(Project project) => Project = project; + /// + /// Executes operations required before rendering the ImGui window. + /// + public virtual void PreRender() { } + /// /// Renders a new ImGui window to the main window. /// public void RenderWindow() { + PreRender(); + ImGui.Begin(Name, ImGuiWindowFlags); Render(); @@ -77,7 +79,10 @@ public void RenderWindow() protected virtual void OnWindowUndocked() { Console.WriteLine($"Window {Name} was undocked."); - CreateSilkWindow(); + + // TODO: This method of doing this does not work (at least in .Net 8). + // See issue #44 for more information. + //CreateSilkWindow(); } private void CreateSilkWindow() diff --git a/Editor/Windows/PropertiesWindow.cs b/Editor/Windows/PropertiesWindow.cs index 9faf495..6f998ff 100644 --- a/Editor/Windows/PropertiesWindow.cs +++ b/Editor/Windows/PropertiesWindow.cs @@ -11,13 +11,20 @@ namespace SharpEngine.Editor.Windows; public class PropertiesWindow : ImGuiWindowBase { /// - public override string Name => "Properties"; + public override string Name => Scene.ActiveElement is null ? "Properties" : Scene.ActiveElement!.Name + " properties"; /// public override void Render() { if (Scene.ActiveElement is not null) RenderActiveElementProperties(Scene.ActiveElement); + + if (Scene.ActiveElement is null) + { + ImGui.Text("No properties to display."); + ImGui.End(); + return; + } } /// @@ -28,16 +35,6 @@ public override void Render() /// Marks the starting level or the currently rendered level of properties. public void RenderActiveElementProperties(object obj, int maxRecursionDepth = 5, int currentDepth = 0) { - if (currentDepth == 0) - ImGui.Begin(obj is null ? "Properties" : Scene.ActiveElement!.Name + " properties"); - - if (obj is null) - { - ImGui.Text("No properties to display."); - ImGui.End(); - return; - } - if (currentDepth > maxRecursionDepth) return; @@ -49,6 +46,10 @@ public void RenderActiveElementProperties(object obj, int maxRecursionDepth = 5, foreach (var property in properties) { + // Skip properties with index parameters + if (property.GetIndexParameters().Length > 0) + continue; + var inspectorAttribute = property.GetCustomAttribute(); if (inspectorAttribute != null && !inspectorAttribute.DisplayInInspector) { @@ -83,8 +84,5 @@ public void RenderActiveElementProperties(object obj, int maxRecursionDepth = 5, { Console.WriteLine(e); } - - if (currentDepth == 0) - ImGui.End(); } } diff --git a/Editor/Windows/SceneWindow.cs b/Editor/Windows/SceneWindow.cs index aa4275c..e47065a 100644 --- a/Editor/Windows/SceneWindow.cs +++ b/Editor/Windows/SceneWindow.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using Launcher.UI; using SharpEngine.Core.Scenes; namespace SharpEngine.Editor.Windows diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index 4c6ebf7..e1311bd 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -209,7 +209,7 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) private BlockType DestroyBlock() { - if (!IsBlockInView(out GameObject? intersectingObject, out Vector3 _)) + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 _, allowedTypes: typeof(BlockBase))) return BlockType.None; var block = (BlockBase)intersectingObject!; @@ -220,7 +220,7 @@ private BlockType DestroyBlock() private void PlaceBlock() { - if (!IsBlockInView(out GameObject? intersectingObject, out Vector3 hitPosition)) + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 hitPosition, allowedTypes: typeof(BlockBase))) return; var newBlockPosition = GetNewBlockPosition(hitPosition, intersectingObject!); @@ -240,18 +240,6 @@ private static Vector3 GetNewBlockPosition(Vector3 hitPosition, GameObject inter return intersectingObject.Transform.Position + (normal * intersectingObject.Transform.Scale); } - /// - /// Checks whether a block is in view of the camera. - /// - /// - /// - /// - public bool IsBlockInView(out GameObject? intersectingObject, out Vector3 hitPosition) - { - Ray ray = new Ray(Camera.Position, Camera.Front); - return ray.IsGameObjectInView(_scene, out intersectingObject, out hitPosition, allowedTypes: typeof(BlockBase)); - } - /// public override void HandleMouseWheel(MouseWheelScrollDirection direction, ScrollWheel scrollWheel) { diff --git a/Examples/MultipleWindows/Program.cs b/Examples/MultipleWindows/Program.cs index 73a0908..0f3a748 100644 --- a/Examples/MultipleWindows/Program.cs +++ b/Examples/MultipleWindows/Program.cs @@ -19,27 +19,21 @@ static void Main() static void StartWindow(IWindow window) { - var thread = new Thread(() => + window.Load += () => { - window.Load += () => - { - Console.WriteLine($"Hello from window: " + window.Title); - - var gl = GL.GetApi(window); + Console.WriteLine($"Hello from window: " + window.Title); - unsafe - { - var versionPtr = gl.GetString(StringName.Version); - string? version = SilkMarshal.PtrToString((nint)versionPtr); - Console.WriteLine($"OpenGL version: {version}"); - } + var gl = GL.GetApi(window); - }; + unsafe + { + var versionPtr = gl.GetString(StringName.Version); + string? version = SilkMarshal.PtrToString((nint)versionPtr); + Console.WriteLine($"OpenGL version: {version}"); + } - window.Run(); - }); + }; - thread.SetApartmentState(ApartmentState.STA); - thread.Start(); + window.Run(); } } diff --git a/SharpEngine.sln b/SharpEngine.sln index 8989e1a..3c5d13b 100644 --- a/SharpEngine.sln +++ b/SharpEngine.sln @@ -87,4 +87,7 @@ Global {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AA03EC4B-4E7B-4B73-BEA4-60C539332773} + EndGlobalSection EndGlobal