Rename Dagger to Voile.

This commit is contained in:
2024-02-06 19:25:36 +01:00
parent 9ec3dcfcca
commit 255545cb71
76 changed files with 158 additions and 158 deletions

View File

@@ -0,0 +1,33 @@
namespace Voile.Audio
{
public abstract class AudioBackend : IDisposable
{
public abstract void Initialize();
public abstract void Update();
public abstract void Shutdown();
// BUS
public abstract void CreateBus(string busName);
public abstract void SetBusVolume(string busName, float volume);
public abstract float GetBusVolume(string busName);
// SOUND
public abstract void PlaySound(Sound sound, string bus = "Master", float pitch = 1.0f, float volume = 1.0f);
public void PlaySound(Sound sound, string bus = "Master") => PlaySound(sound, bus, default, default);
public SoundInstance CreateInstance(Sound sound)
{
var instance = new SoundInstance(this, sound);
return instance;
}
// EFFECTS
public abstract void AddBusEffect<T>(T effect, string bus = "Master") where T : AudioEffect;
public void Dispose()
{
Shutdown();
}
private LehmerRandom _random = new LehmerRandom();
}
}

View File

@@ -0,0 +1,7 @@
namespace Voile
{
public class AudioBus
{
}
}

View File

@@ -0,0 +1,9 @@
namespace Voile
{
public class AudioEffect { }
public class AudioEffectReverb : AudioEffect
{
}
}

View File

@@ -0,0 +1,45 @@
namespace Voile.Audio
{
public class DummyAudioBackend : AudioBackend
{
public override void AddBusEffect<T>(T effect, string bus = "Master")
{
return;
}
public override void CreateBus(string busName)
{
return;
}
public override float GetBusVolume(string busName)
{
return 0.0f;
}
public override void Initialize()
{
return;
}
public override void SetBusVolume(string busName, float volume)
{
return;
}
public override void Shutdown()
{
return;
}
public override void Update()
{
return;
}
public override void PlaySound(Sound sound, string bus = "Master", float pitch = 1, float volume = 1)
{
return;
}
}
}

View File

@@ -0,0 +1,136 @@
using FMOD;
using System.Runtime.InteropServices;
namespace Voile.Audio
{
public class FmodAudioBackend : AudioBackend
{
public override void Initialize()
{
CreateSystem();
_loadedSounds = new Dictionary<string, FMOD.Sound>();
_channelGroups = new Dictionary<string, ChannelGroup>();
CreateBus("Master");
}
public override void Update() => _system.update();
public override void PlaySound(Sound sound, string bus = "Master", float pitch = 1, float volume = 1)
{
int channels = 0;
if (sound.Format == SoundFormat.Mono)
{
channels = 1;
}
else if (sound.Format == SoundFormat.Stereo)
{
channels = 2;
}
var channel = PlaySoundFromBuffer(sound.Path, (int)sound.BufferSize, channels, sound.SampleRate, sound.Buffer, GetChannelGroup(bus));
channel.setVolume(volume);
channel.setPitch(pitch);
}
public override void CreateBus(string busName)
{
ChannelGroup channelGroup;
_system.createChannelGroup(busName, out channelGroup);
_channelGroups.Add(busName, channelGroup);
}
public override void SetBusVolume(string busName, float volume)
{
var channel = GetChannelGroup(busName);
channel.setVolume(volume);
}
public override float GetBusVolume(string busName)
{
float volume;
GetChannelGroup(busName).getVolume(out volume);
return volume;
}
public override void AddBusEffect<T>(T effect, string bus = "Master")
{
var channelGroup = GetChannelGroup(bus);
DSP dsp;
switch (effect)
{
case AudioEffectReverb:
dsp = CreateReverbDsp(effect as AudioEffectReverb);
break;
default:
_system.createDSPByType(DSP_TYPE.UNKNOWN, out dsp);
break;
}
channelGroup.addDSP(0, dsp);
}
private DSP CreateReverbDsp(AudioEffectReverb effectReverb)
{
DSP dsp;
_system.createDSPByType(DSP_TYPE.SFXREVERB, out dsp);
return dsp;
}
private void CreateSystem()
{
var result = FMOD.Factory.System_Create(out _system);
_system.init(128, INITFLAGS.NORMAL, 0);
}
private Channel PlaySoundFromBuffer(string path, int length, int channels, int sampleRate, byte[] buffer, ChannelGroup channelGroup)
{
Channel fmodChannel;
FMOD.Sound fmodSound = IsLoaded(path) ? GetSoundFromLoaded(path) : CreateSound(length, channels, sampleRate, path, buffer);
_system.playSound(fmodSound, channelGroup, false, out fmodChannel);
return fmodChannel;
}
private FMOD.Sound GetSoundFromLoaded(string path) => _loadedSounds[path];
private bool IsLoaded(string path) => _loadedSounds.ContainsKey(path);
private FMOD.Sound CreateSound(int length, int channels, int sampleRate, string path, byte[] buffer)
{
FMOD.Sound sound;
CREATESOUNDEXINFO info = new CREATESOUNDEXINFO()
{
numchannels = channels,
defaultfrequency = sampleRate,
format = SOUND_FORMAT.PCM16,
length = (uint)length,
cbsize = Marshal.SizeOf(typeof(CREATESOUNDEXINFO))
};
var result = _system.createSound(buffer, FMOD.MODE.OPENMEMORY | FMOD.MODE.OPENRAW | FMOD.MODE.CREATESAMPLE, ref info, out sound);
AddToLoaded(path, sound);
return sound;
}
private void AddToLoaded(string path, FMOD.Sound sound) => _loadedSounds.Add(path, sound);
private ChannelGroup GetChannelGroup(string busName)
{
return _channelGroups[busName];
}
public override void Shutdown()
{
throw new NotImplementedException();
}
private FMOD.System _system;
// TODO: use a different key for the dictionary, paths are not good :( (waste of memory lol)
private Dictionary<string, FMOD.Sound> _loadedSounds;
private Dictionary<string, ChannelGroup> _channelGroups;
}
}

View File

@@ -0,0 +1,46 @@
namespace Voile.Audio
{
public class SoundInstance
{
public SoundInstance(AudioBackend backend, Sound sound)
{
_backend = backend;
_sound = sound;
}
public SoundInstance PitchVariation(float min, float max)
{
var random = new LehmerRandom();
_pitch = (float)random.NextDouble() * (max - min) + min;
return this;
}
public SoundInstance VolumeVariation(float min, float max)
{
var random = new LehmerRandom();
_volume = (float)random.NextDouble() * (max - min) + min;
return this;
}
public SoundInstance OnBus(string bus = "Master")
{
_bus = bus;
return this;
}
public void Play()
{
_backend.PlaySound(GetSound(), _bus, _pitch, _volume);
}
protected virtual Sound GetSound()
{
return _sound;
}
private readonly AudioBackend _backend;
private readonly Sound _sound;
private string _bus = "Master";
private float _pitch, _volume = 1.0f;
}
}

110
Voile/Source/Color.cs Normal file
View File

@@ -0,0 +1,110 @@
namespace Voile
{
// Based on https://github.com/ppr-game/PPR/blob/engine/PER.Util/src/Color.cs
/// <summary>
/// A record struct representing a color.
/// </summary>
public record struct Color
{
// TODO: add more HTML colors.
public static Color AliceBlue = new(0xF0F8FF);
public static Color AntiqueWhite = new(0xFAEBD7);
public static Color Aqua = new(0x00FFFF);
public static Color Aquamarine = new(0x7FFFD4);
public static Color Azure = new(0xF0FFFF);
public static Color Beige = new(0xF5F5DC);
public static Color Bisque = new(0xFFE4C4);
public static Color Black = new(0x000000);
public static Color BlanchedAlmond = new(0xFFEBCD);
public static Color Blue = new(0x0000FF);
public static Color BlueViolet = new(0x8A2BE2);
public static Color Brown = new(0xA52A2A);
public static Color BurlyWood = new(0xDEB887);
public static Color CadetBlue = new(0x5F9EA0);
public static Color Chartreuse = new(0x7FFF00);
public static Color Chocolate = new(0xD2691E);
public static Color Coral = new(0xFF7F50);
public static Color CornflowerBlue = new(0x6495ED);
public static Color Cornsilk = new(0xFFF8DC);
public static Color Crimson = new(0xDC143C);
public static Color Cyan = new(0x00FFFF);
public static Color DarkBlue = new(0x00008B);
public static Color DarkCyan = new(0x008B8B);
public static Color White = new(0xFFFFFF);
public static Color Green = new(0x00FF00);
public static Color Red = new(0xFF0000);
public float R { get; set; }
public float G { get; set; }
public float B { get; set; }
public float A { get; set; }
public int Argb
{
get
{
int c = (ushort)Math.Round(A * 255f);
c <<= 8;
c |= (ushort)Math.Round(R * 255f);
c <<= 8;
c |= (ushort)Math.Round(G * 255f);
c <<= 8;
c |= (ushort)Math.Round(B * 255f);
return c;
}
}
public Color(float r, float g, float b, float a)
{
R = r;
G = g;
B = b;
A = a;
}
public Color(byte r, byte g, byte b, byte a)
{
R = r / 255f;
G = g / 255f;
B = b / 255f;
A = a / 255f;
}
public Color(int hex)
{
A = 1.0f;
B = (hex & 0xFF) / 255.0f;
hex >>= 8;
G = (hex & 0xFF) / 255.0f;
hex >>= 8;
R = (hex & 0xFF) / 255.0f;
}
public Color Lightened(float amount)
{
var result = this;
result.R = result.R + (1.0f - result.R) * amount;
result.G = result.G + (1.0f - result.G) * amount;
result.B = result.B + (1.0f - result.B) * amount;
return result;
}
public Color Darkened(float amount)
{
var result = this;
result.R = result.R * (1.0f - amount);
result.G = result.G * (1.0f - amount);
result.B = result.B * (1.0f - amount);
return result;
}
public System.Drawing.Color ToSystemColor()
{
var result = System.Drawing.Color.FromArgb(Argb);
return result;
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Numerics;
namespace Voile.Extensions
{
public static class Mat4Extensions
{
public static Matrix4x4 Scale(this Matrix4x4 mat, Vector2 scale)
{
return mat;
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Numerics;
namespace Voile.Extensions
{
public static class Vector2Extensions
{
public static Vector2 Lerp(this Vector2 a, Vector2 b, double t)
{
return new Vector2(MathUtils.Lerp(a.X, b.X, t), MathUtils.Lerp(a.Y, b.Y, t));
}
}
}

2506
Voile/Source/External/FastNoiseLite.cs vendored Normal file

File diff suppressed because it is too large Load Diff

201
Voile/Source/External/LehmerRandom.cs vendored Normal file
View File

@@ -0,0 +1,201 @@
/// <summary>
/// https://github.com/YourRobotOverlord/LehmerRandom/blob/master/Lehmer/Random.cs
/// Alternative to System.Random based on the Lehmer algorithm.
/// </summary>
public class LehmerRandom
{
// Stores the seed for the Next methods.
private uint _seed;
/// <summary>
/// Initializes a new instance of the Random class, using a time-dependent default seed value.
/// </summary>
public LehmerRandom()
{
_seed = (uint)System.Environment.TickCount;
}
/// <summary>
/// Initializes a new instance of the Random class, using the specified seed value.
/// </summary>
/// <param name="seed"></param>
public LehmerRandom(int seed)
{
_seed = (uint)seed;
}
/// <summary>
/// Initializes a new instance of the Random class, using the specified seed value.
/// </summary>
/// <param name="seed"></param>
public LehmerRandom(uint seed)
{
_seed = seed;
}
/// <summary>
/// Returns a non-negative random integer.
/// </summary>
/// <returns>Int32</returns>
public int Next()
{
return GetNextInt(0, int.MaxValue);
}
/// <summary>
/// Returns a non-negative random integer that is less than the specified maximum.
/// </summary>
/// <param name="maxValue">Int32</param>
/// <returns>Int32</returns>
public int Next(int maxValue)
{
return GetNextInt(0, maxValue);
}
/// <summary>
/// Returns a random integer that is within a specified range.
/// </summary>
/// <param name="minValue">Int32</param>
/// <param name="maxValue">Int32</param>
/// <returns>Int32</returns>
public int Next(int minValue, int maxValue)
{
return GetNextInt(minValue, maxValue);
}
/// <summary>
/// Returns a random floating-point number that is greater than or equal to 0.0, and less than 1.0
/// </summary>
/// <returns></returns>
public double NextDouble()
{
return GetNextDouble(0.0, 1.0);
}
/// <summary>
/// Returns a non-negative random floating-point number that is less than the specified maximum.
/// </summary>
/// <param name="maxValue"></param>
/// <returns></returns>
public double NextDouble(double maxValue)
{
return GetNextDouble(0.0, maxValue);
}
/// <summary>
/// Returns a random floating-point number that is within a specified range.
/// </summary>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
/// <returns></returns>
public double NextDouble(double minValue, double maxValue)
{
return GetNextDouble(minValue, maxValue);
}
/// <summary>
/// Fills the elements of a specified array of bytes with random numbers.
/// </summary>
/// <param name="buffer">The array to be filled with random numbers.</param>
public void NextBytes(byte[] buffer)
{
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = (byte)Next();
}
}
// Returns a random int and sets the seed for the next pass.
internal int GetNextInt(int minValue, int maxValue)
{
_seed = Rnd(_seed);
return ConvertToIntRange(_seed, minValue, maxValue);
}
// Returns a random double and sets the seed for the next pass.
internal double GetNextDouble(double minValue, double maxValue)
{
_seed = Rnd(_seed);
return ConvertToDoubleRange(_seed, minValue, maxValue);
}
/// <summary>
/// Returns a random integer that is within a specified range, using the specified seed value.
/// </summary>
/// <param name="seed"></param>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
/// <returns></returns>
public static int RndInt(uint seed, int minValue, int maxValue)
{
return GetInt(seed, minValue, maxValue);
}
/// <summary>
/// Returns a random integer that is within a specified range, using a time-dependent default seed value.
/// </summary>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
/// <returns></returns>
public static int RndInt(int minValue, int maxValue)
{
return GetInt((uint)System.Environment.TickCount, minValue, maxValue);
}
/// <summary>
/// Returns a random double that is within a specified range, using the specified seed value.
/// </summary>
/// <param name="seed"></param>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
/// <returns></returns>
public static double RndDouble(uint seed, double minValue, double maxValue)
{
return GetDouble(seed, minValue, maxValue);
}
/// <summary>
/// Returns a random double that is within a specified range, using a time-dependent default seed value.
/// </summary>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
/// <returns></returns>
public static double RndDouble(double minValue, double maxValue)
{
return GetDouble((uint)System.Environment.TickCount, minValue, maxValue);
}
internal static int GetInt(uint seed, int minValue, int maxValue)
{
return ConvertToIntRange(Rnd(seed), minValue, maxValue);
}
internal static double GetDouble(uint seed, double minValue, double maxValue)
{
return ConvertToDoubleRange(Rnd(seed), minValue, maxValue);
}
// Converts uint to integer within the given range.
internal static int ConvertToIntRange(uint val, int minValue, int maxValue)
{
return (int)(val % (maxValue - minValue) + minValue);
}
// Converts uint to double within the given range.
internal static double ConvertToDoubleRange(uint val, double minValue, double maxValue)
{
return (double)val / uint.MaxValue * (maxValue - minValue) + minValue;
}
// Pseudo-random number generator based on the Lehmer Algorithm
// and javidx9's implementation:
// https://github.com/OneLoneCoder/olcPixelGameEngine/blob/master/Videos/OneLoneCoder_PGE_ProcGen_Universe.cpp
internal static uint Rnd(uint seed)
{
seed += 0xe120fc15;
ulong tmp = (ulong)seed * 0x4a39b70d;
uint m1 = (uint)((tmp >> 32) ^ tmp);
tmp = (ulong)m1 * 0x12fad5c9;
return (uint)((tmp >> 32) ^ tmp);
}
}

41
Voile/Source/Game.cs Normal file
View File

@@ -0,0 +1,41 @@
namespace Voile
{
public abstract class Game
{
public abstract string ResourceRoot { get; }
/// <summary>
/// Starts the game application.
/// This involves initializing the required subsystems, loading resources from the disk, and then preparing the subsystems for running in the main loop.
/// </summary>
public void Start()
{
Initialize();
LoadResources();
Ready();
Run();
Shutdown();
}
/// <summary>
/// Called when it's time to initialize the subsystems.
/// </summary>
public abstract void Initialize();
/// <summary>
/// Called when it's time to load the application's resources, such as images or sounds.
/// </summary>
protected abstract void LoadResources();
/// <summary>
/// Called when it's safe to manipulate with the resources or/and systems.
/// </summary>
protected abstract void Ready();
/// <summary>
/// Called when everything has been readied to start the main loop.
/// </summary>
protected abstract void Run();
/// <summary>
/// Called when the application quits and it's safe to clean up.
/// </summary>
public abstract void Shutdown();
}
}

View File

@@ -0,0 +1,36 @@
namespace Voile
{
public abstract class InputAction
{
public abstract bool IsDown(InputHandler inputHandler);
public abstract bool IsPressed(InputHandler inputHandler);
public abstract bool IsReleased(InputHandler inputHandler);
}
public class KeyInputAction : InputAction
{
public KeyboardKey Key => _keyboardKey;
public KeyInputAction(KeyboardKey keyboardKey)
{
_keyboardKey = keyboardKey;
}
public override bool IsDown(InputHandler inputHandler)
{
return inputHandler.IsKeyboardKeyDown(_keyboardKey);
}
public override bool IsPressed(InputHandler inputHandler)
{
return inputHandler.KeyboardKeyJustPressed(_keyboardKey);
}
public override bool IsReleased(InputHandler inputHandler)
{
return inputHandler.KeyboardKeyJustReleased(_keyboardKey);
}
private KeyboardKey _keyboardKey;
}
}

View File

@@ -0,0 +1,203 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Voile.Utils;
namespace Voile
{
public abstract class InputHandler
{
public InputHandler()
{
inputMappings = new Dictionary<string, IEnumerable<InputAction>>();
CreateDefaultMappings();
}
public Action OnInput;
public bool Handled { get => _handled; set => _handled = value; }
public abstract bool IsKeyboardKeyDown(KeyboardKey key);
public abstract bool KeyboardKeyJustPressed(KeyboardKey key);
public abstract bool KeyboardKeyJustReleased(KeyboardKey key);
public abstract Vector2 GetInputDirection(KeyboardKey leftKey, KeyboardKey rightKey, KeyboardKey upKey, KeyboardKey downKey);
public abstract Vector2 GetInputDirection(string leftAction, string rightAction, string upAction, string downAction);
public abstract int GetCharPressed();
public abstract bool IsActionDown(string action);
public abstract bool IsActionPressed(string action);
public abstract bool IsActionReleased(string action);
public abstract bool IsMouseButtonDown(MouseButton button);
public abstract float GetMouseWheelMovement();
public abstract void SetMousePosition(Vector2 position);
public abstract Vector2 GetMousePosition();
public abstract void HideCursor();
public abstract void ShowCursor();
public abstract bool IsCursorHidden();
public void SetAsHandled() => _handled = true;
public void AddInputMapping(string actionName, IEnumerable<InputAction> inputActions)
{
inputMappings.Add(actionName, inputActions);
}
private void CreateDefaultMappings()
{
AddInputMapping("up", new InputAction[] {
new KeyInputAction(KeyboardKey.W),
new KeyInputAction(KeyboardKey.Up),
});
AddInputMapping("down", new InputAction[] {
new KeyInputAction(KeyboardKey.S),
new KeyInputAction(KeyboardKey.Down),
});
AddInputMapping("left", new InputAction[] {
new KeyInputAction(KeyboardKey.A),
new KeyInputAction(KeyboardKey.Left),
});
AddInputMapping("right", new InputAction[] {
new KeyInputAction(KeyboardKey.D),
new KeyInputAction(KeyboardKey.Right),
});
}
protected bool TryGetInputMappings(string forAction, [NotNullWhen(true)] out IEnumerable<InputAction>? inputActions)
{
var contains = inputMappings.ContainsKey(forAction);
inputActions = null;
if (!contains)
{
_logger.Error($"The action \"{forAction}\" is not present in the input mappings!");
return false;
}
inputActions = inputMappings[forAction];
return true;
}
private bool _handled;
protected Dictionary<string, IEnumerable<InputAction>> inputMappings;
private Logger _logger = new(nameof(InputHandler));
}
public enum KeyboardKey
{
Null = 0,
Back = 4,
VolumeUp = 24,
VolumeDown = 25,
Spacebar = 32,
Apostrophe = 39,
Comma = 44,
Minus = 45,
Period = 46,
Slash = 47,
Zero = 48,
One = 49,
Two = 50,
Three = 51,
Four = 52,
Five = 53,
Six = 54,
Seven = 55,
Eight = 56,
Nine = 57,
Semicolon = 59,
Equal = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
Menu = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LeftBracket = 91,
Backslash = 92,
RightBracket = 93,
Grave = 96,
Escape = 256,
Enter = 257,
Tab = 258,
Backspace = 259,
Insert = 260,
Delete = 261,
Right = 262,
Left = 263,
Down = 264,
Up = 265,
PageUp = 266,
PageDown = 267,
Home = 268,
End = 269,
CapsLock = 280,
ScrollLock = 281,
NumLock = 282,
PrintScreen = 283,
Pause = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
KP0 = 320,
KP1 = 321,
KP2 = 322,
KP3 = 323,
KP4 = 324,
KP5 = 325,
KP6 = 326,
KP7 = 327,
KP8 = 328,
KP9 = 329,
KPDecimal = 330,
KPDivide = 331,
KPMultiply = 332,
KPSubstract = 333,
KPAdd = 334,
KPEnter = 335,
KPEqual = 336,
LeftShift = 340,
LeftControl = 341,
LeftAlt = 342,
LeftSuper = 343,
RightShift = 344,
RightControl = 345,
RightAlt = 346,
RightSuper = 347,
KBMenu = 348
}
public enum MouseButton
{
Left = 0,
Right = 1,
Middle = 2,
}
}

View File

@@ -0,0 +1,130 @@
using System.Numerics;
using Raylib_cs;
namespace Voile
{
public class RaylibInputHandler : InputHandler
{
public override int GetCharPressed()
{
return Raylib.GetCharPressed();
}
public override Vector2 GetInputDirection(KeyboardKey leftKey, KeyboardKey rightKey, KeyboardKey upKey, KeyboardKey downKey)
{
Vector2 dir = Vector2.Zero;
if (IsKeyboardKeyDown(leftKey))
dir += new Vector2(-1, 0);
if (IsKeyboardKeyDown(rightKey))
dir += new Vector2(1, 0);
if (IsKeyboardKeyDown(upKey))
dir += new Vector2(0, -1);
if (IsKeyboardKeyDown(downKey))
dir += new Vector2(0, 1);
return dir == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(dir);
}
public override Vector2 GetInputDirection(string leftAction, string rightAction, string upAction, string downAction)
{
Vector2 dir = Vector2.Zero;
if (IsActionDown(leftAction))
dir += new Vector2(-1, 0);
if (IsActionDown(rightAction))
dir += new Vector2(1, 0);
if (IsActionDown(upAction))
dir += new Vector2(0, -1);
if (IsActionDown(downAction))
dir += new Vector2(0, 1);
return dir == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(dir);
}
public override Vector2 GetMousePosition()
{
return Raylib.GetMousePosition();
}
public override float GetMouseWheelMovement()
{
return Raylib.GetMouseWheelMove();
}
public override void HideCursor()
{
Raylib.HideCursor();
}
public override bool IsActionDown(string action)
{
IEnumerable<InputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (InputAction inputAction in mappings)
if (inputAction.IsDown(this)) return true;
}
return false;
}
public override bool IsActionPressed(string action)
{
IEnumerable<InputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (InputAction inputAction in mappings)
if (inputAction.IsPressed(this)) return true;
}
return false;
}
public override bool IsActionReleased(string action)
{
IEnumerable<InputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (InputAction inputAction in mappings)
if (inputAction.IsReleased(this)) return true;
}
return false;
}
public override bool IsKeyboardKeyDown(KeyboardKey key)
{
Raylib_cs.KeyboardKey rayKey = (Raylib_cs.KeyboardKey)key;
OnInput?.Invoke();
return Raylib.IsKeyDown(rayKey);
}
public override bool IsMouseButtonDown(MouseButton button)
{
return Raylib.IsMouseButtonDown((Raylib_cs.MouseButton)button);
}
public override bool KeyboardKeyJustPressed(KeyboardKey key)
{
Raylib_cs.KeyboardKey rayKey = (Raylib_cs.KeyboardKey)key;
return Raylib.IsKeyPressed(rayKey);
}
public override bool KeyboardKeyJustReleased(KeyboardKey key)
{
return Raylib.IsKeyReleased((Raylib_cs.KeyboardKey)key);
}
public override void SetMousePosition(Vector2 position)
{
Raylib.SetMousePosition((int)position.X, (int)position.Y);
}
public override void ShowCursor() => Raylib.ShowCursor();
public override bool IsCursorHidden() => Raylib.IsCursorHidden();
}
}

View File

@@ -0,0 +1,49 @@
namespace Voile.Rendering
{
public class ColorRectShader : Shader
{
public override string FragmentSource => @"
#version 450
layout(location = 0) out vec4 fsout_Color;
layout (set = 0, binding = 0) uniform Uniforms
{
vec4 color;
};
void main()
{
fsout_Color = color;
}
";
public override string VertexSource => @"
#version 450
layout(location = 0) in vec2 Position;
// uniform MatrixBlock
// {
// // mat4 model = mat4();
// // mat4 view;
// // mat4 proj;
// mat4 transform = mat4(1.0, 1.0, 1.0, 1.0,
// 1.0, 1.0, 1.0, 1.0,
// 1.0, 1.0, 1.0, 1.0,
// 1.0, 1.0, 1.0, 1.0);
// };
// mat4 transform = mat4(1.0, 1.0, 0.0, 1.0,
// 0.0, 1.0, 1.0, 1.0,
// 0.0, 0.0, 1.0, 1.0,
// 0.0, 0.0, 0.0, 1.0);
mat4 transform = mat4(1.0);
void main()
{
// gl_Position = proj * view * model * vec4(Position, 0, 1);
// gl_Position = vec4(Position, 0, 1);
gl_Position = transform * vec4(Position, 0, 1);
}";
}
}

View File

@@ -0,0 +1,298 @@
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using Raylib_cs;
namespace Voile.Rendering
{
public class RaylibRenderer : Renderer
{
public override bool ShouldRun => !WindowShouldClose();
public override Vector2 WindowSize
{
get
{
return new Vector2(Raylib.GetScreenWidth(), Raylib.GetScreenHeight());
}
set
{
Raylib.SetWindowSize((int)value.X, (int)value.Y);
}
}
public override Vector2 MonitorSize => new Vector2(GetMonitorWidth(GetCurrentMonitor()), GetMonitorHeight(GetCurrentMonitor()));
public override string WindowTitle
{
get => _windowTitle; set
{
SetWindowTitle(value);
}
}
public override int TargetFps { get => _targetFps; set => SetTargetFps(value); }
public override bool VSync { get => _vsync; set => SetWindowVSync(value); }
public override bool Fullscreen { get => _fullscreen; set => SetFullscreen(value); }
public override void CreateWindow(WindowSettings windowSettings)
{
Raylib.SetTraceLogLevel(TraceLogLevel.LOG_NONE);
_defaultWindowSettings = windowSettings;
_windowSize = windowSettings.Size;
ConfigFlags windowFlags = 0;
windowFlags |= windowSettings.Resizable ? ConfigFlags.FLAG_WINDOW_RESIZABLE : 0;
_windowTitle = windowSettings.Title;
if (_fullscreen)
{
var monitorSize = MonitorSize;
Raylib.InitWindow((int)monitorSize.X, (int)monitorSize.Y, windowSettings.Title);
Fullscreen = true;
}
else
{
Raylib.InitWindow((int)_windowSize.X, (int)_windowSize.Y, windowSettings.Title);
}
// Raylib.SetWindowState(windowFlags);
}
protected override void SetWindowTitle(string title)
{
Raylib.SetWindowTitle(title);
}
protected override void SetWindowVSync(bool value)
{
_vsync = value;
// TODO: implement VSync toggle for Raylib.
}
protected override void SetTargetFps(int fps)
{
_targetFps = fps;
Raylib.SetTargetFPS(fps);
}
protected override bool WindowShouldClose()
{
return Raylib.WindowShouldClose();
}
public override void Shutdown()
{
Raylib.CloseWindow();
}
public override void BeginFrame()
{
Raylib.BeginDrawing();
}
public override void EndFrame()
{
Raylib.EndDrawing();
}
public override void BeginCamera2d(Vector2 offset, Vector2 target, float rotation, float zoom)
{
var camera = new Camera2D(offset, target, rotation, zoom);
Raylib.BeginMode2D(camera);
}
public override void EndCamera2d()
{
Raylib.EndMode2D();
}
public override void ClearBackground(Color color)
{
Raylib.ClearBackground(VoileColorToRaylibColor(color));
}
protected override double GetFrameTime()
{
return (double)Raylib.GetFrameTime();
}
public override void DrawCircle(float radius, Color color)
{
Raylib.DrawCircle((int)transformPosition.X, (int)transformPosition.Y, radius, VoileColorToRaylibColor(color));
}
public override void DrawTexture(Texture2d texture, Color tint)
{
if (texture.Handle == -1)
{
LoadTexture(texture);
}
Raylib.DrawTextureV(_texturePool[texture.Handle], transformPosition, VoileColorToRaylibColor(tint));
}
public override void DrawRectangle(Vector2 size, Color color)
{
Raylib.DrawRectanglePro(new Rectangle()
{
x = transformPosition.X,
y = transformPosition.Y,
width = size.X,
height = size.Y
}, transformOffset, transformRotation, VoileColorToRaylibColor(color));
}
public override void DrawDebugText(string text, int fontSize, Color color)
{
Raylib.DrawText(text, (int)transformPosition.X, (int)transformPosition.Y, fontSize, VoileColorToRaylibColor(color));
}
public override void DrawSdfText(string text, int fontSize, Color color)
{
throw new NotImplementedException();
}
public override void Initialize(RendererSettings settings)
{
_targetFps = settings.TargetFps;
ConfigFlags flags = 0;
// MSAA
flags |= settings.Msaa == Msaa.Msaa4x ? ConfigFlags.FLAG_MSAA_4X_HINT : 0;
// VSync
flags |= settings.UseVSync ? ConfigFlags.FLAG_VSYNC_HINT : 0;
_fullscreen = settings.Fullscreen;
_defaultFlags = flags;
Raylib.SetConfigFlags(flags);
}
private void SetFullscreen(bool fullscreen)
{
// var flags = _defaultFlags;
// if (fullscreen && !Raylib.IsWindowFullscreen())
// {
// WindowSize = MonitorSize;
// Raylib.ToggleFullscreen();
// }
// else if (Raylib.IsWindowFullscreen())
// {
// // Raylib.ToggleFullscreen();
// WindowSize = _windowSize;
// }
_fullscreen = fullscreen;
}
// TODO
public override void SetTransform(Matrix4x4 transform) { }
public override void CreateAndInitialize(WindowSettings windowSettings, RendererSettings renderSettings)
{
Initialize(renderSettings);
CreateWindow(windowSettings);
}
private Raylib_cs.Color VoileColorToRaylibColor(Color color)
{
return new Raylib_cs.Color { r = (byte)Math.Round(color.R * 255f), g = (byte)Math.Round(color.G * 255f), b = (byte)Math.Round(color.B * 255f), a = (byte)Math.Round(color.A * 255f) };
}
public override void DrawText(Font font, string text, Color color)
{
if (font.Handle == -1)
{
LoadFont(font);
}
var rayFont = _fontPool[font.Handle];
Raylib.DrawTextPro(rayFont, text, transformPosition, transformOffset, transformRotation, font.Size, 0.0f, VoileColorToRaylibColor(color));
}
protected override int GetMonitorWidth(int monitorId)
{
return Raylib.GetMonitorWidth(monitorId);
}
protected override int GetMonitorHeight(int monitorId)
{
return Raylib.GetMonitorHeight(monitorId);
}
protected override int GetCurrentMonitor()
{
return Raylib.GetCurrentMonitor();
}
private unsafe void LoadFont(Font font)
{
Raylib_cs.Font fontRay;
ReadOnlySpan<char> ext = ".ttf"; // TODO: don't use a hardcoded extension.
Span<byte> extBytes = new byte[Encoding.Default.GetByteCount(ext) + 1];
Encoding.Default.GetBytes(ext, extBytes);
int fontChars = 2048; // TODO: control this dynamically to not load the entire font.
unsafe
{
fixed (byte* extP = extBytes)
{
fixed (byte* bufferP = font.Buffer)
{
fontRay = Raylib.LoadFontFromMemory((sbyte*)extP, bufferP, (int)font.BufferSize, font.Size, null, fontChars);
}
}
}
Raylib.GenTextureMipmaps(ref fontRay.texture);
Raylib.SetTextureFilter(fontRay.texture, TextureFilter.TEXTURE_FILTER_BILINEAR);
_fontPool.Add(fontRay);
font.Handle = _fontPool.Count - 1;
}
private void LoadTexture(Texture2d texture)
{
Image image = new();
unsafe
{
fixed (void* dataPtr = texture.Buffer)
{
image.data = dataPtr;
}
}
image.width = texture.Width;
image.height = texture.Height;
image.mipmaps = texture.Mipmaps;
image.format = (PixelFormat)texture.Format;
Texture2D rayTexture;
rayTexture = Raylib.LoadTextureFromImage(image);
_texturePool.Add(rayTexture);
texture.Handle = _texturePool.Count - 1;
}
private List<Texture2D> _texturePool = new();
private List<Raylib_cs.Font> _fontPool = new();
private Vector2 _windowSize;
private bool _vsync;
private string _windowTitle = string.Empty;
private int _targetFps;
private bool _fullscreen;
private WindowSettings _defaultWindowSettings;
private ConfigFlags _defaultFlags;
}
}

View File

@@ -0,0 +1,183 @@
using System.Numerics;
namespace Voile.Rendering
{
/// <summary>
/// An abstract class representing the graphics renderer.
/// </summary>
public abstract class Renderer
{
// INIT
/// <summary>
/// Creates the renderer window and initializes internal resources.
/// </summary>
/// <param name="windowSettings">Settings for the rendering window.</param>
/// <param name="renderSettings">Rendering settings.</param>
public abstract void CreateAndInitialize(WindowSettings windowSettings, RendererSettings renderSettings);
/// <summary>
/// Initializes internal resources. Should be called before other methods.
/// </summary>
/// <param name="settings">Rendering settings.</param>
public abstract void Initialize(RendererSettings settings);
// UTIL
/// <summary>
/// Indicates if the renderer will render the next frame.
/// </summary>
public abstract bool ShouldRun { get; }
/// <summary>
/// Target frames per second for rendering.
/// </summary>
public abstract int TargetFps { get; set; }
public abstract bool VSync { get; set; }
public double FrameTime => GetFrameTime();
// WINDOW
/// <summary>
/// The size of the render window.
/// </summary>
public abstract Vector2 WindowSize { get; set; }
public abstract string WindowTitle { get; set; }
public abstract bool Fullscreen { get; set; }
/// <summary>
/// Active monitor's size.
/// </summary>
public abstract Vector2 MonitorSize { get; }
/// <summary>
/// Creates a window.
/// </summary>
/// <param name="windowSettings">Window settings to use to create the window.</param>
public abstract void CreateWindow(WindowSettings windowSettings);
protected abstract void SetWindowTitle(string title);
protected abstract void SetWindowVSync(bool value);
protected abstract void SetTargetFps(int fps);
protected abstract double GetFrameTime();
protected abstract int GetMonitorWidth(int monitorId);
protected abstract int GetMonitorHeight(int monitorId);
protected abstract int GetCurrentMonitor();
protected abstract bool WindowShouldClose();
public abstract void Shutdown();
// DRAWING
/// <summary>
/// Prepares the renderer for drawing the next frame.
/// </summary>
public abstract void BeginFrame();
/// <summary>
/// Ends rendering of the frame.
/// </summary>
public abstract void EndFrame();
public abstract void BeginCamera2d(Vector2 offset, Vector2 target, float rotation, float zoom);
public abstract void EndCamera2d();
/// <summary>
/// Clears the render canvas and sets a background color.
/// </summary>
/// <param name="color">Background color.</param>
public abstract void ClearBackground(Color color);
public void ResetTransform()
{
transformPosition = Vector2.Zero;
transformOffset = Vector2.Zero;
transformRotation = 0.0f;
}
/// <summary>
/// Sets transforms for the next draw operation.
/// </summary>
/// <param name="position">Global transform position.</param>
/// <param name="offset">Local offset point around which shapes will rotate.</param>
/// <param name="rotation">Rotation.</param>
public void SetTransform(Vector2 position, Vector2 offset, float rotation = 0.0f)
{
transformPosition = position;
transformOffset = offset;
transformRotation = rotation;
}
/// <summary>
/// Sets the transform for the next draw operation.
/// </summary>
/// <param name="transform">Transform matrix.</param>
public abstract void SetTransform(Matrix4x4 transform);
/// <summary>
/// Draws a filled circle.
/// </summary>
/// <param name="radius">Radius of a circle.</param>
/// <param name="color">Fill color.</param>
public abstract void DrawCircle(float radius, Color color);
/// <summary>
/// Draws a filled rectangle.
/// </summary>
/// <param name="size">Rectangle size.</param>
/// <param name="color">Fill color.</param>
public abstract void DrawRectangle(Vector2 size, Color color);
/// <summary>
/// Draws a debug text with a default font.
/// </summary>
/// <param name="position"></param>
/// <param name="text"></param>
/// <param name="fontSize"></param>
/// <param name="color"></param>
public abstract void DrawDebugText(string text, int fontSize, Color color);
/// <summary>
/// Draws text using a signed distance field font atlas.
/// </summary>
/// <param name="text">Text to draw.</param>
/// <param name="fontSize">Size of the font.</param>
/// <param name="color">Color of the text.</param>
public abstract void DrawSdfText(string text, int fontSize, Color color);
public abstract void DrawText(Font font, string text, Color color);
/// <summary>
/// Draws the texture.
/// </summary>
/// <param name="id">Texture to draw.</param>
/// <param name="tint">Texture tint.</param>
public abstract void DrawTexture(Texture2d texture, Color tint);
protected Vector2 transformPosition, transformOffset;
protected float transformRotation;
}
public enum Msaa
{
None,
Msaa2x,
Msaa4x,
Msaa8x
}
public struct RendererSettings
{
public Msaa Msaa;
public bool UseVSync;
public bool Fullscreen;
public int TargetFps = 60;
public RendererSettings() { }
public static RendererSettings Default => new RendererSettings()
{
UseVSync = true,
TargetFps = 60
};
}
public struct WindowSettings
{
public string Title;
public Vector2 Size = new Vector2(640, 480);
public bool Resizable { get; set; }
public WindowSettings(string title, Vector2 size)
{
Title = title;
Size = size;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Voile.Rendering
{
public abstract class Shader
{
public abstract string FragmentSource { get; }
public abstract string VertexSource { get; }
}
}

View File

@@ -0,0 +1,194 @@
using System.Drawing;
using System.Numerics;
using Silk.NET.GLFW;
using Silk.NET.OpenGL;
namespace Voile.Rendering
{
/// <summary>
/// A standard, OpenGL-based renderer.
/// </summary>
public class StandardRenderer : Renderer
{
/// <inheritdoc />
public override Vector2 WindowSize { get; set; }
/// <inheritdoc />
public override bool ShouldRun => throw new NotImplementedException();
public override Vector2 MonitorSize => throw new NotImplementedException();
public override int TargetFps { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override bool VSync { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override string WindowTitle { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override bool Fullscreen { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
/// <inheritdoc />
public override void Initialize(RendererSettings settings)
{
}
/// <inheritdoc />
public override void CreateAndInitialize(WindowSettings windowSettings, RendererSettings renderSettings)
{
CreateWindow(windowSettings);
Initialize(renderSettings);
}
public override void BeginFrame()
{
_glfw.PollEvents();
_gl.Viewport(new Size((int)_windowSize.X, (int)_windowSize.Y));
}
/// <inheritdoc />
public override void EndFrame()
{
// throw new NotImplementedException();
EndFrameUnsafe();
}
/// <inheritdoc />
public override void ClearBackground(Color color)
{
_gl.ClearColor(color.ToSystemColor());
_gl.Clear((uint)ClearBufferMask.ColorBufferBit);
}
/// <inheritdoc />
public override void Shutdown()
{
CloseWindowUnsafe();
_glfw.Terminate();
}
/// <inheritdoc />
public override void DrawCircle(float radius, Color color)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void DrawDebugText(string text, int fontSize, Color color)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void DrawRectangle(Vector2 size, Color color)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void DrawTexture(Texture2d texture, Color tint)
{
throw new NotImplementedException();
}
/// <inheritdoc />
protected override double GetFrameTime()
{
return 0.0;
}
/// <inheritdoc />
protected override void SetTargetFps(int fps)
{
return;
}
/// <inheritdoc />
public override void SetTransform(Matrix4x4 transform)
{
throw new NotImplementedException();
}
/// <inheritdoc />
protected override void SetWindowTitle(string title)
{
SetWindowTitleUnsafe(title);
}
/// <inheritdoc />
protected override void SetWindowVSync(bool value)
{
throw new NotImplementedException();
}
/// <inheritdoc />
protected override bool WindowShouldClose() => WindowShouldCloseUnsafe();
private unsafe void CreateWindowUnsafe(string title, Vector2 size)
{
_glfw = GlfwProvider.GLFW.Value;
_glfw.Init();
_glfw.WindowHint(WindowHintInt.ContextVersionMajor, 4);
_glfw.WindowHint(WindowHintInt.ContextVersionMinor, 6);
_windowHandle = _glfw.CreateWindow((int)size.X, (int)size.Y, title, null, null);
_glfw.MakeContextCurrent(_windowHandle);
_gl = GL.GetApi(_glfw.GetProcAddress);
_glfw.SwapInterval(1);
_windowSize = size;
}
private unsafe void CloseWindowUnsafe() => _glfw.DestroyWindow(_windowHandle);
private unsafe bool WindowShouldCloseUnsafe() => _glfw.WindowShouldClose(_windowHandle);
private unsafe void SetWindowTitleUnsafe(string title) => _glfw.SetWindowTitle(_windowHandle, title);
private unsafe void EndFrameUnsafe()
{
_glfw.SwapBuffers(_windowHandle);
}
public override void DrawSdfText(string text, int fontSize, Color color)
{
throw new NotImplementedException();
}
public override void BeginCamera2d(Vector2 offset, Vector2 target, float rotation, float zoom)
{
throw new NotImplementedException();
}
public override void EndCamera2d()
{
throw new NotImplementedException();
}
public override void CreateWindow(WindowSettings windowSettings)
{
throw new NotImplementedException();
}
protected override int GetMonitorWidth(int monitorId)
{
throw new NotImplementedException();
}
protected override int GetMonitorHeight(int monitorId)
{
throw new NotImplementedException();
}
protected override int GetCurrentMonitor()
{
throw new NotImplementedException();
}
public override void DrawText(Font font, string text, Color color)
{
throw new NotImplementedException();
}
private GL _gl;
private Glfw _glfw;
private unsafe WindowHandle* _windowHandle;
private Vector2 _windowSize;
}
}

View File

@@ -0,0 +1,13 @@
namespace Voile;
public class Font : Resource
{
/// <summary>
/// Internal handle for the font. If it got successfully loaded into the GPU, the value will be other than -1.
/// </summary>
internal int Handle { get; set; } = -1;
public int Size { get; set; } = 16;
public Font(string path, byte[] buffer) : base(path, buffer)
{
}
}

View File

@@ -0,0 +1,17 @@
namespace Voile.Resources;
public class FontLoader : IResourceLoader<Font>
{
public IEnumerable<string> SupportedExtensions => new string[]
{
"ttf"
};
public Font Load(string path)
{
byte[] fileBuffer = File.ReadAllBytes(path);
var result = new Font(path, fileBuffer);
result.BufferSize = fileBuffer.Length;
return result;
}
}

View File

@@ -0,0 +1,8 @@
namespace Voile.Resources
{
public interface IResourceLoader<T> where T : Resource
{
public IEnumerable<string> SupportedExtensions { get; }
public T Load(string path);
}
}

View File

@@ -0,0 +1,52 @@
using StbVorbisSharp;
namespace Voile.Resources
{
public class SoundLoader : IResourceLoader<Sound>
{
public IEnumerable<string> SupportedExtensions => new string[]
{
"ogg"
};
public Sound Load(string path)
{
Vorbis vorbis;
Sound result;
var fileBuffer = File.ReadAllBytes(path);
vorbis = Vorbis.FromMemory(fileBuffer);
vorbis.SubmitBuffer();
if (vorbis.Decoded == 0)
{
vorbis.Restart();
vorbis.SubmitBuffer();
}
var audioShort = vorbis.SongBuffer;
int length = vorbis.Decoded * vorbis.Channels;
byte[] audioData = new byte[length * 2];
for (int i = 0; i < length; i++)
{
if (i * 2 >= audioData.Length) break;
var b1 = (byte)(audioShort[i] >> 8);
var b2 = (byte)(audioShort[i] & 256);
audioData[i * 2] = b2;
audioData[i * 2 + 1] = b1;
}
result = new Sound(path, audioData);
result.Format = (SoundFormat)vorbis.Channels - 1;
result.SampleRate = vorbis.SampleRate;
result.BufferSize = length;
vorbis.Dispose();
return result;
}
}
}

View File

@@ -0,0 +1,30 @@
using Voile.Resources;
using StbImageSharp;
namespace Voile
{
public class Texture2dLoader : IResourceLoader<Texture2d>
{
public IEnumerable<string> SupportedExtensions => new string[]
{
".png",
".jpg",
".jpeg"
};
public Texture2d Load(string path)
{
ImageResult image;
using (var stream = File.OpenRead(path))
{
image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);
}
Texture2d result = new Texture2d(path, image.Data);
result.Width = image.Width;
result.Height = image.Height;
return result;
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace Voile
{
public abstract class Resource : IDisposable
{
public string? Path { get => _path; set => _path = value; }
[JsonIgnore]
public byte[]? Buffer { get => _buffer; set => _buffer = value; }
[JsonIgnore]
public long BufferSize { get; set; }
public Resource(string path, byte[] buffer)
{
_path = path;
_buffer = buffer;
}
public void Dispose()
{
Buffer = null;
Path = null;
}
private string? _path;
private byte[]? _buffer;
}
}

View File

@@ -0,0 +1,171 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Voile.Utils;
namespace Voile.Resources
{
public class ResourceManager
{
public string ResourceRoot { get; set; } = "Resources/";
public bool TryLoad<T>(string resourceId, string path) where T : Resource
{
T? resource = null;
var fullPath = Path.Combine(ResourceRoot, path);
// TODO: don't check if file doesn't exist in the file system, make it more generic but for now it's fine
if (!File.Exists(fullPath))
{
_logger.Error($"File at \"{path}\" doesn't exist!");
return false;
}
_logger.Info($"Loading {path} as {typeof(T)} with id \"{resourceId}\"...");
if (!TryGetLoader(out IResourceLoader<T>? loader))
{
return false;
}
var extension = Path.GetExtension(fullPath);
var hasExtension = loader.SupportedExtensions.Any(ext => extension[1..] == ext);
if (!hasExtension)
{
_logger.Error($"Extension {extension} is not supported!");
}
if (loader.Load(fullPath) is not T loadedResource)
{
return false;
}
resource = loadedResource;
_loadedResources.Add(resourceId, resource);
_logger.Info($"\"{resourceId}\" was loaded successfully.");
return true;
}
public bool TrySave<T>(string path, in T resource) where T : Resource
{
if (!TryGetSaver(out IResourceSaver<T>? saver))
{
return false;
}
if (!saver.TrySave(path, in resource))
{
return false;
}
return true;
}
public bool TryGetResource<T>(string resourceId, [NotNullWhen(true)] out T? resource) where T : Resource
{
resource = null;
if (!IsResourceLoaded(resourceId))
{
_logger.Error($"Resource \"{resourceId}\" has not yet been loaded!");
return false;
}
var expectedResource = _loadedResources[resourceId];
if (expectedResource is not T loadedResource)
{
_logger.Error($"Given resource is of wrong type: provided {typeof(T)}, expected {expectedResource.GetType()}!");
return false;
}
resource = loadedResource;
return true;
}
public bool IsResourceLoaded(string resourceId) => _loadedResources.ContainsKey(resourceId);
public void AddResourceLoaderAssociation<T>(IResourceLoader<T> loader) where T : Resource
{
_logger.Info($"Added resource loader association for {typeof(T)}.");
_resourceLoaderAssociations.Add(typeof(T), loader);
}
public void AddResourceSaverAssociation<T>(IResourceSaver<T> saver) where T : Resource
{
_logger.Info($"Added resource saver association for {typeof(T)}.");
_resourceSaverAssociations.Add(typeof(T), saver);
}
private bool TryGetLoader<T>([NotNullWhen(true)] out IResourceLoader<T>? loader) where T : Resource
{
loader = null;
if (!_resourceLoaderAssociations.ContainsKey(typeof(T)))
{
_logger.Error($"No loader association found for {typeof(T)}.");
return false;
}
loader = _resourceLoaderAssociations[typeof(T)] as IResourceLoader<T>;
if (loader is not null)
{
_logger.Info($"Using {loader.GetType()} for loading...");
}
else
{
_logger.Error($"No loader association found for {typeof(T)}.");
return false;
}
return true;
}
private bool TryGetSaver<T>([NotNullWhen(true)] out IResourceSaver<T>? saver) where T : Resource
{
saver = null;
if (!_resourceSaverAssociations.ContainsKey(typeof(T)))
{
_logger.Error($"No saver association found for {typeof(T)}.");
return false;
}
saver = _resourceSaverAssociations[typeof(T)] as IResourceSaver<T>;
if (saver is not null)
{
_logger.Info($"Using {saver.GetType()} for saving...");
}
else
{
_logger.Error($"No saver association found for {typeof(T)}.");
return false;
}
return true;
}
private Logger _logger = new(nameof(ResourceManager));
private readonly Dictionary<Type, object> _resourceLoaderAssociations = new()
{
{typeof(Sound), new SoundLoader()},
{typeof(Texture2d), new Texture2dLoader()},
{typeof(Font), new FontLoader()}
};
private readonly Dictionary<Type, object> _resourceSaverAssociations = new()
{
};
private Dictionary<string, Resource> _loadedResources = new();
}
}

View File

@@ -0,0 +1,7 @@
namespace Voile.Resources
{
public interface IResourceSaver<T> where T : Resource
{
public bool TrySave(string path, in T resource);
}
}

View File

@@ -0,0 +1,18 @@
namespace Voile
{
public class Sound : Resource
{
public SoundFormat Format { get; set; }
public int SampleRate { get; set; }
public Sound(string path, byte[] buffer) : base(path, buffer)
{
}
}
public enum SoundFormat
{
Mono,
Stereo
}
}

View File

@@ -0,0 +1,44 @@
namespace Voile
{
public class Texture2d : Resource
{
/// <summary>
/// Internal handle for the texture. If it got successfully loaded into the GPU, the value will be other than -1.
/// </summary>
internal int Handle { get; set; } = -1;
public int Width { get; set; }
public int Height { get; set; }
public int Mipmaps { get; set; } = 1;
public TextureFormat Format { get; set; } = TextureFormat.UncompressedR8G8B8A8;
public Texture2d(string path, byte[] buffer) : base(path, buffer)
{
}
public static Texture2d Empty => new Texture2d(string.Empty, new byte[] { });
}
public enum TextureFormat
{
UncompressedGrayscale = 1,
UncompressedGrayAlpha,
UncompressedR5G6B5,
UncompressedR8G8B8,
UncompressedR5G5B5A1,
UncompressedR4G4B4A4,
UncompressedR8G8B8A8,
UncompressedR32,
UncompressedR32G32B32,
UncompressedR32G32B32A32,
CompressedDXT1Rgb,
CompressedDXT1Rgba,
CompressedDXT3Rgba,
CompressedDXT5Rgba,
CompressedETC1Rgb,
CompressedETC2Rgb,
CompressedETC2EACRgba,
CompressedPVRTRgb,
CompressedPVRTRgba,
CompressedASTC4x4Rgba,
CompressedASTC8x8Rgba
}
}

View File

@@ -0,0 +1,25 @@
using System.Numerics;
namespace Voile.SceneGraph;
public class Camera2d : Entity2d
{
public Vector2 Offset { get; set; }
public float Zoom { get; set; } = 1f;
public bool Current
{
get => _current; set
{
_current = value;
Layer?.UpdateCurrentCamera();
}
}
protected override void OnStart()
{
base.OnStart();
Offset = Renderer.WindowSize / 2;
}
private bool _current;
}

View File

@@ -0,0 +1,18 @@
using Voile.Rendering;
namespace Voile.SceneGraph
{
public class CircleShape2d : Drawable2d
{
public float Radius { get => _radius; set => _radius = value; }
public Color Color { get => _color; set => _color = value; }
public override void OnDraw(Renderer renderer)
{
renderer.DrawCircle(_radius, _color);
}
private float _radius = 16;
private Color _color = Color.White;
}
}

View File

@@ -0,0 +1,17 @@
using System.Numerics;
using Voile.Rendering;
namespace Voile.SceneGraph
{
public abstract class Drawable2d : Entity2d, IDrawable
{
public Vector2 PivotOffset { get; set; }
public void Draw(Renderer renderer)
{
renderer.SetTransform(Position, PivotOffset, Rotation);
OnDraw(renderer);
}
public abstract void OnDraw(Renderer renderer);
}
}

View File

@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Voile.Audio;
using Voile.Rendering;
namespace Voile.SceneGraph
{
public class Entity
{
[JsonIgnore] public EntityLayer? Layer { get; set; }
[JsonIgnore] public InputHandler Input => Layer!.Scene.Input;
[JsonIgnore] public AudioBackend Audio => Layer!.Scene.Audio;
[JsonIgnore] public Renderer Renderer => Layer!.Scene.Renderer;
public int Id { get; set; }
public void Start() => OnStart();
public void Update(double dt) => OnUpdate(dt);
public void ReceiveInput(InputHandler input) => OnInput(input);
protected virtual void OnStart() { }
protected virtual void OnDestroy() { }
protected virtual void OnUpdate(double dt) { }
protected virtual void OnInput(InputHandler input) { }
public void Destroy()
{
OnDestroy();
Layer?.DestroyEntity(Id);
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Entity))]
internal partial class EntitySourceGenerationContext : JsonSerializerContext
{
}
}

View File

@@ -0,0 +1,10 @@
using System.Numerics;
namespace Voile.SceneGraph
{
public class Entity2d : Entity
{
public Vector2 Position { get; set; }
public float Rotation { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using Voile.Rendering;
namespace Voile.SceneGraph
{
public interface IDrawable
{
public void Draw(Renderer renderer);
}
}

View File

@@ -0,0 +1,162 @@
using System.Drawing;
using System.Numerics;
using Voile.Rendering;
namespace Voile.SceneGraph
{
// TODO: add oneshot parameter.
public class Particles2d : Drawable2d
{
public int MaxParticles => _maxParticles;
public ParticleSettings Settings => _settings;
public Particles2d(ParticleSettings settings)
{
_settings = settings;
_maxParticles = _settings.MaxParticles;
_particleIndex = _maxParticles - 1;
_particles = new Particle[_maxParticles];
}
public void Restart()
{
CleanupParticles();
// Allocate a new particle array if max particles property got changed.
if (_maxParticles != _settings.MaxParticles)
{
_particles = new Particle[_maxParticles];
}
}
public override void OnDraw(Renderer renderer)
{
foreach (var particle in _particles)
{
if (!particle.Alive) continue;
var t = particle.LifeTimeRemaining / particle.LifeTime;
var scale = MathUtils.Lerp(_settings.ScaleEnd, _settings.ScaleBegin, t);
var color = MathUtils.LerpColor(_settings.ColorEnd, _settings.ColorBegin, t);
renderer.SetTransform(particle.Position, Vector2.Zero, particle.Rotation);
renderer.DrawRectangle(Vector2.One * scale, color);
}
}
protected override void OnStart()
{
base.OnStart();
// Emit();
}
protected override void OnUpdate(double dt)
{
base.OnUpdate(dt);
var rate = (int)MathUtils.Lerp(1, _maxParticles, _settings.Explosiveness);
for (int i = 0; i < rate; i++)
{
Emit();
}
for (int i = 0; i < _maxParticles; i++)
{
var particle = _particles[i];
if (!particle.Alive) continue;
if (particle.LifeTimeRemaining <= 0.0f)
{
particle.Alive = false;
continue;
}
particle.LifeTimeRemaining -= (float)dt;
particle.Velocity += _settings.Gravity * (float)dt;
particle.Position += particle.Velocity * (float)dt;
particle.Rotation += particle.AngularVelocity * (float)dt;
particle.Velocity -= particle.Velocity * _settings.Damping * (float)dt;
_particles[i] = particle;
}
}
private void Emit()
{
Particle particle = _particles[_particleIndex];
if (!(particle.LifeTimeRemaining <= 0)) return;
particle.Alive = true;
particle.Position = GetEmitPosition();
particle.Velocity = _settings.Direction * _settings.LinearVelocity;
particle.Velocity += Vector2.One * _settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f);
particle.AngularVelocity = _settings.AngularVelocity;
particle.AngularVelocity += 1f * _settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);
particle.LifeTime = _settings.LifeTime;
particle.LifeTimeRemaining = particle.LifeTime;
_particles[_particleIndex] = particle;
_particleIndex = --_particleIndex <= 0 ? _maxParticles - 1 : --_particleIndex;
}
private void CleanupParticles() => Array.Clear(_particles);
private Vector2 GetEmitPosition()
{
// https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image
var angle = _random.NextDouble() * Math.PI * 2;
float radius = (float)Math.Sqrt(_random.NextDouble()) * _settings.EmitRadius;
float x = Position.X + radius * (float)Math.Cos(angle);
float y = Position.Y + radius * (float)Math.Sin(angle);
return new Vector2(x, y);
}
private ParticleSettings _settings;
private int _maxParticles;
private Particle[] _particles;
private int _particleIndex;
// TODO: replace a random function for better distribution and performance.
private LehmerRandom _random = new LehmerRandom();
}
public struct ParticleSettings
{
public ParticleSettings()
{
}
public float EmitRadius;
public float LifeTime;
public float Explosiveness;
public int MaxParticles;
public Vector2 Direction;
public float LinearVelocity;
public float AngularVelocity = 0.0f;
public float AngularVelocityRandom;
public float LinearVelocityRandom;
public Vector2 Gravity;
public float ScaleBegin = 16f;
public float ScaleEnd = 0.0f;
public Color ColorBegin = Color.White;
public Color ColorEnd = Color.Black;
public float Damping = 0.0f;
}
public struct Particle
{
public Particle()
{
}
public Vector2 Position;
public Vector2 Velocity;
public float AngularVelocity;
public float LifeTime;
public float LifeTimeRemaining;
public float Scale;
public float Rotation;
public bool Alive = true;
}
}

View File

@@ -0,0 +1,15 @@
using System.Numerics;
using Voile.Rendering;
namespace Voile.SceneGraph;
public class RectangleShape2d : Drawable2d
{
public Vector2 Size { get; set; } = Vector2.One * 32;
public Color Color { get; set; } = Color.White;
public override void OnDraw(Renderer renderer)
{
PivotOffset = Size / 2;
renderer.DrawRectangle(Size, Color);
}
}

View File

@@ -0,0 +1,23 @@
using System.Drawing;
using Voile.Rendering;
namespace Voile.SceneGraph
{
public class Sprite2d : Drawable2d
{
public Texture2d Texture { get => _texture ?? Texture2d.Empty; set => _texture = value; }
protected override void OnStart()
{
var renderer = Layer.Scene.Renderer;
}
public override void OnDraw(Renderer renderer)
{
renderer.DrawTexture(_texture!, Color.White);
}
private Texture2d? _texture;
}
}

View File

@@ -0,0 +1,42 @@
using Voile.Rendering;
using System.Drawing;
namespace Voile.SceneGraph
{
public class Text2d : Drawable2d
{
public string Text { get => _text; set => _text = value; }
public Color FontColor { get => _fontColor; set => _fontColor = value; }
public Font Font
{
get => _font; set
{
_isDirty = true;
_font = value;
}
}
public Text2d(Font font)
{
_font = font;
}
public override void OnDraw(Renderer renderer)
{
if (_font == null)
{
renderer.DrawDebugText(_text, 20, _fontColor);
}
else
{
renderer.DrawText(_font, _text, _fontColor);
}
}
private string _text = string.Empty;
private Color _fontColor = Color.White;
private Font _font;
private int _fontHandle;
private bool _isDirty;
}
}

View File

@@ -0,0 +1,116 @@
using System.Text.Json.Serialization;
using Voile.Rendering;
using Voile.Utils;
namespace Voile.SceneGraph
{
public class EntityLayer : Layer
{
public List<Entity> Entities { get; set; }
public Camera2d? CurrentCamera { get; set; }
public EntityLayer(List<Entity> entities)
{
Entities = entities;
}
public EntityLayer()
{
Entities = new List<Entity>();
}
public void UpdateCurrentCamera()
{
if (_cameraEntities.Count == 1)
{
CurrentCamera = _cameraEntities[0];
return;
}
else
{
foreach (var camera in _cameraEntities)
{
if (camera.Current) CurrentCamera = camera;
}
}
}
public bool AddEntity(Entity entity)
{
entity.Id = Entities.Count;
entity.Layer = this;
if (entity is Camera2d camera2d)
{
_cameraEntities.Add(camera2d);
UpdateCurrentCamera();
}
Entities.Add(entity);
return true;
}
public void DestroyEntity(int at)
{
Entities.RemoveAt(at);
}
protected override void OnStart()
{
for (int i = 0; i < Entities.Count; i++)
{
var entity = Entities[i];
entity.Layer = this;
entity.Start();
}
}
protected override void OnUpdate(double dt)
{
foreach (var entity in Entities)
{
entity.Update(dt);
}
}
protected override void OnBeginDraw(Renderer renderer)
{
if (CurrentCamera is not null)
{
renderer.BeginCamera2d(CurrentCamera.Offset, CurrentCamera.Position, 0f, CurrentCamera.Zoom);
}
}
protected override void OnEndDraw(Renderer renderer)
{
if (CurrentCamera is not null)
{
renderer.EndCamera2d();
}
}
protected override void OnDraw(Renderer renderer)
{
// TODO: can be done more efficiently, needs rendering redesign.
foreach (var entity in Entities)
{
if (entity is IDrawable drawable)
{
drawable.Draw(renderer);
}
}
}
private List<Camera2d> _cameraEntities = new();
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(EntityLayer))]
[JsonSerializable(typeof(Entity))]
[JsonSerializable(typeof(List<Entity>))]
internal partial class EntityLayerContext : JsonSerializerContext
{
}
}

View File

@@ -0,0 +1,11 @@
namespace Voile.SceneGraph
{
public interface IMainLoop
{
void Init();
void Start();
void Update();
double DeltaTime { get; }
bool ShouldRun { get; }
}
}

View File

@@ -0,0 +1,28 @@
using Voile.Resources;
using Voile.Rendering;
using System.Text.Json.Serialization;
namespace Voile.SceneGraph
{
public abstract class Layer : IDrawable
{
[JsonIgnore] public Scene? Scene { get; set; }
[JsonIgnore] public InputHandler? Input { get; set; }
[JsonIgnore] public ResourceManager ResourceManager => Scene!.ResourceManager;
public void BeginDraw(Renderer renderer) => OnBeginDraw(renderer);
public void Draw(Renderer renderer) => OnDraw(renderer);
public void EndDraw(Renderer renderer) => OnEndDraw(renderer);
public void Start() => OnStart();
public void Update(double dt) => OnUpdate(dt);
public void ReceiveInput(InputHandler input) => OnInput(input);
protected virtual void OnStart() { }
protected virtual void OnUpdate(double dt) { }
protected virtual void OnInput(InputHandler input) { }
protected abstract void OnBeginDraw(Renderer renderer);
protected abstract void OnDraw(Renderer renderer);
protected abstract void OnEndDraw(Renderer renderer);
}
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Voile.SceneGraph
{
public class SerializedScene : Resource
{
public Dictionary<string, Layer> Layers { get; set; }
public SerializedScene(string path, byte[] buffer) : base(path, buffer)
{
}
}
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(SerializedScene))]
[JsonSerializable(typeof(Dictionary<string, Layer>))]
[JsonSerializable(typeof(Entity2d))]
[JsonSerializable(typeof(Layer))]
[JsonSerializable(typeof(EntityLayer))]
internal partial class SerializedSceneContext : JsonSerializerContext
{
}
}

View File

@@ -0,0 +1,23 @@
using Voile.Resources;
using Voile.Utils;
namespace Voile.SceneGraph
{
public class SerializedSceneSaver : IResourceSaver<SerializedScene>
{
public bool TrySave(string path, in SerializedScene resource)
{
if (resource.Buffer is null)
{
_logger.Error($"Tried to save a resource at \"{path}\" with a null buffer!");
return false;
}
File.WriteAllBytes(path, resource.Buffer);
return true;
}
private Logger _logger = new(nameof(SerializedSceneSaver));
}
}

View File

@@ -0,0 +1,130 @@
using System.Text.Json;
using Voile.Audio;
using Voile.Rendering;
using Voile.Resources;
namespace Voile.SceneGraph
{
public class Scene : IMainLoop
{
public Renderer Renderer { get => _renderer; set => _renderer = value; }
public InputHandler? Input { get => _input; set => _input = value; }
public AudioBackend? Audio => _audioBackend;
public ResourceManager ResourceManager => _resourceManager;
public double DeltaTime => _renderer.FrameTime;
public bool ShouldRun => Renderer.ShouldRun;
public Scene(SceneSettings settings)
{
_renderer = settings.Renderer;
_input = settings.InputHandler;
_audioBackend = settings.AudioBackend;
_resourceManager = settings.ResourceManager;
}
public static Scene FromSerialized(SerializedScene serializedScene, SceneSettings settings)
{
var scene = new Scene(settings);
scene.WithLayers(serializedScene.Layers);
return scene;
}
public bool TrySerialize(out SerializedScene serializedScene)
{
serializedScene = new SerializedScene(string.Empty, new byte[] { })
{
Layers = _layers
};
serializedScene.Buffer = JsonSerializer.SerializeToUtf8Bytes(serializedScene, new JsonSerializerOptions
{
TypeInfoResolver = SerializedSceneContext.Default
});
serializedScene.BufferSize = serializedScene.Buffer.LongLength;
return true;
}
public void WithLayers(Dictionary<string, Layer> layers)
{
_layers = layers;
}
public void Init() => SetupRenderer();
public void Start()
{
foreach (var layer in _layers.Values)
{
if (_input is not null)
{
layer.Input = _input;
}
layer.Start();
}
}
public void Update()
{
foreach (var layer in _layers)
{
layer.Value.Update(DeltaTime);
}
Audio?.Update();
}
public void AddLayer(string name, Layer layer)
{
layer.Scene = this;
_layers.Add(name, layer);
}
public void BeginDraw()
{
Renderer.BeginFrame();
Renderer.ClearBackground(Color.Black);
foreach (var layer in _layers.Values)
{
layer.BeginDraw(_renderer);
layer.Draw(_renderer);
}
Renderer.ResetTransform();
}
public void EndDraw()
{
foreach (var layer in _layers.Values)
{
layer.EndDraw(_renderer);
}
Renderer.EndFrame();
}
private void SetupRenderer()
{
Renderer.Initialize(new RendererSettings { Msaa = Msaa.Msaa4x, UseVSync = true });
}
private Dictionary<string, Layer> _layers = new();
private Renderer _renderer;
private AudioBackend? _audioBackend;
private InputHandler? _input;
private ResourceManager _resourceManager;
}
public struct SceneSettings
{
public Renderer Renderer { get; set; }
public AudioBackend AudioBackend { get; set; }
public InputHandler InputHandler { get; set; }
public ResourceManager ResourceManager { get; set; }
}
}

View File

@@ -0,0 +1,63 @@
using System.Numerics;
using Voile.Rendering;
namespace Voile.UI;
/// <summary>
/// A basic container for UI elements. All container's children will update their constraints based on container's sizing and positioning.
/// </summary>
public class Container : UIElement
{
/// <summary>
/// Updates the sizes of the container and rearranges its children.
/// </summary>
/// <param name="position"></param>
/// <param name="size"></param>
public void UpdateRect(Vector2 position, Vector2 size)
{
Rect.Position = position;
UpdateSize(size);
RearrangeChildren();
}
protected void RearrangeChildren()
{
int idx = 0;
foreach (var child in children)
{
if (child is Container container)
{
container.UpdateRect(Rect.Position, Rect.Size);
}
RearrangeChild(idx, child);
idx++;
}
}
protected virtual void RearrangeChild(int idx, UIElement child) { }
protected override void OnRender(Renderer renderer)
{
}
private void UpdateSize(Vector2 baseSize)
{
if (parent == null)
{
Rect.Size = baseSize;
return;
}
if (VerticalSizeFlags == SizeFlags.Fill)
{
Rect.Size = new Vector2(Rect.Size.X, baseSize.Y * ExpandRatio.Y);
}
if (HorizontalSizeFlags == SizeFlags.Fill)
{
Rect.Size = new Vector2(baseSize.X * ExpandRatio.X, Rect.Size.Y);
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Numerics;
namespace Voile.UI;
public class MarginPanel : Panel
{
public Vector2 RelativeMargin { get; set; }
public Vector2 AbsoluteMargin { get; set; }
public MarginPanel(PanelStyle style) : base(style)
{
}
protected override void RearrangeChild(int idx, UIElement child)
{
base.RearrangeChild(idx, child);
var rect = child.Rect;
var absoluteMargin = Rect.Size * RelativeMargin + AbsoluteMargin;
rect.Position = Rect.Position + absoluteMargin;
rect.Size = rect.Size - absoluteMargin * 2;
}
}

20
Voile/Source/UI/Panel.cs Normal file
View File

@@ -0,0 +1,20 @@
using System.Numerics;
using Voile.Rendering;
namespace Voile.UI;
public class Panel : Container
{
public PanelStyle Style { get; set; }
public Panel(PanelStyle style)
{
Style = style;
}
protected override void OnRender(Renderer renderer)
{
base.OnRender(renderer);
renderer.DrawRectangle(Rect.Size, Style.BackgroundColor);
}
}

View File

@@ -0,0 +1,6 @@
namespace Voile.UI;
public struct PanelStyle
{
public Color BackgroundColor { get; set; }
}

10
Voile/Source/UI/Rect.cs Normal file
View File

@@ -0,0 +1,10 @@
using System.Numerics;
namespace Voile.UI;
public class Rect
{
public Vector2 Position { get; set; }
public Vector2 Size { get; set; }
public Vector2 Scale { get; set; }
}

View File

@@ -0,0 +1,31 @@
using Voile.Rendering;
namespace Voile.UI;
public class TextLabel : UIElement
{
public string Text { get; set; } = string.Empty;
public Font Font
{
get => _font; set
{
_font = value;
}
}
public int FontSize { get; set; } = 16;
public Color FontColor { get; set; } = Color.White;
protected override void OnRender(Renderer renderer)
{
if (_font == null)
{
renderer.DrawDebugText(Text, FontSize, FontColor);
}
else
{
renderer.DrawText(_font, Text, FontColor);
}
}
private Font? _font;
}

View File

@@ -0,0 +1,55 @@
using System.Numerics;
using Voile.Rendering;
using Voile.Utils;
namespace Voile.UI;
public abstract class UIElement
{
public Rect Rect { get; set; } = new Rect();
public SizeFlags VerticalSizeFlags { get; set; } = SizeFlags.Fill;
public SizeFlags HorizontalSizeFlags { get; set; } = SizeFlags.Fill;
public Vector2 ExpandRatio { get; set; } = Vector2.One;
public UIElement()
{
children = new();
}
public void AddChild(UIElement child)
{
children.Add(child);
child.parent = this;
}
public void Render(Renderer renderer)
{
Vector2 parentPos = parent != null ? parent.Rect.Position : Vector2.Zero;
renderer.SetTransform(Rect.Position + parentPos, Vector2.Zero, 0);
OnRender(renderer);
foreach (UIElement child in children)
{
renderer.SetTransform(child.Rect.Position, Vector2.Zero, 0);
child.Render(renderer);
}
}
protected abstract void OnRender(Renderer renderer);
protected List<UIElement> children;
protected UIElement? parent;
private Logger _logger = new(nameof(UIElement));
}
[Flags]
public enum SizeFlags
{
ShrinkBegin,
ShrinkCenter,
ShrinkEnd,
Fill
}

View File

@@ -0,0 +1,22 @@
using System.Numerics;
namespace Voile.UI;
public class VerticalPanel : Panel
{
public float Spacing { get; set; } = 16;
public VerticalPanel(PanelStyle style) : base(style)
{
}
protected override void RearrangeChild(int idx, UIElement child)
{
base.RearrangeChild(idx, child);
var yOffset = idx * Spacing;
var rect = child.Rect;
rect.Position = Rect.Position;
rect.Position = new Vector2(rect.Position.X, yOffset);
}
}

View File

@@ -0,0 +1,330 @@
using System.Numerics;
using ImGuiNET;
using Raylib_cs;
using Voile.Rendering;
namespace Voile.SceneGraph
{
public class ImGuiRenderLayer : Layer
{
protected override void OnDraw(Renderer renderer)
{
Layout();
_controller.Draw(renderer);
}
protected virtual void Layout() { }
protected override void OnStart()
{
_controller = new ImGuiController();
_controller.Load(Scene.Renderer.WindowSize);
}
protected override void OnUpdate(double dt)
{
_controller.Update(dt, Input);
}
protected override void OnBeginDraw(Renderer renderer)
{
throw new NotImplementedException();
}
protected override void OnEndDraw(Renderer renderer)
{
throw new NotImplementedException();
}
private ImGuiController _controller;
}
public class ImGuiController : IDisposable, IDrawable
{
public ImGuiController()
{
_context = ImGui.CreateContext();
ImGui.SetCurrentContext(_context);
}
public void Dispose()
{
ImGui.DestroyContext();
}
public void Load(Vector2 size)
{
ImGuiIOPtr io = ImGui.GetIO();
io.Fonts.AddFontDefault();
Resize(size);
LoadFontTexture();
SetupInput();
ImGui.NewFrame();
}
unsafe void LoadFontTexture()
{
ImGuiIOPtr io = ImGui.GetIO();
io.Fonts.GetTexDataAsRGBA32(out byte* pixels, out int width, out int height);
// TODO: use engine API instead of Raylib.
Image image = new Image
{
data = pixels,
width = width,
height = height,
mipmaps = 1,
format = PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
};
_fontTexture = Raylib.LoadTextureFromImage(image);
io.Fonts.SetTexID(new IntPtr(_fontTexture.id));
io.Fonts.ClearTexData();
}
private void SetupInput()
{
ImGuiIOPtr io = ImGui.GetIO();
// Setup config flags
io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard;
// Setup back-end capabilities flags
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors;
io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos;
// Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array.
io.KeyMap[(int)ImGuiKey.Tab] = (int)KeyboardKey.Tab;
io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)KeyboardKey.Left;
io.KeyMap[(int)ImGuiKey.RightArrow] = (int)KeyboardKey.Right;
io.KeyMap[(int)ImGuiKey.UpArrow] = (int)KeyboardKey.Up;
io.KeyMap[(int)ImGuiKey.DownArrow] = (int)KeyboardKey.Down;
io.KeyMap[(int)ImGuiKey.PageUp] = (int)KeyboardKey.PageUp;
io.KeyMap[(int)ImGuiKey.PageDown] = (int)KeyboardKey.PageDown;
io.KeyMap[(int)ImGuiKey.Home] = (int)KeyboardKey.Home;
io.KeyMap[(int)ImGuiKey.End] = (int)KeyboardKey.End;
io.KeyMap[(int)ImGuiKey.Insert] = (int)KeyboardKey.Insert;
io.KeyMap[(int)ImGuiKey.Delete] = (int)KeyboardKey.Delete;
io.KeyMap[(int)ImGuiKey.Backspace] = (int)KeyboardKey.Backspace;
io.KeyMap[(int)ImGuiKey.Space] = (int)KeyboardKey.Spacebar;
io.KeyMap[(int)ImGuiKey.Enter] = (int)KeyboardKey.Enter;
io.KeyMap[(int)ImGuiKey.Escape] = (int)KeyboardKey.Escape;
io.KeyMap[(int)ImGuiKey.A] = (int)KeyboardKey.A;
io.KeyMap[(int)ImGuiKey.C] = (int)KeyboardKey.C;
io.KeyMap[(int)ImGuiKey.V] = (int)KeyboardKey.V;
io.KeyMap[(int)ImGuiKey.X] = (int)KeyboardKey.X;
io.KeyMap[(int)ImGuiKey.Y] = (int)KeyboardKey.Y;
io.KeyMap[(int)ImGuiKey.Z] = (int)KeyboardKey.Z;
}
public void Resize(Vector2 size)
{
ImGuiIOPtr io = ImGui.GetIO();
io.DisplaySize = size / _scaleFactor;
}
public void Update(double dt, InputHandler input)
{
ImGuiIOPtr io = ImGui.GetIO();
io.DisplayFramebufferScale = Vector2.One;
io.DeltaTime = (float)dt;
UpdateKeyboard(input);
UpdateMouse(input);
ImGui.NewFrame();
}
private void UpdateKeyboard(InputHandler input)
{
ImGuiIOPtr io = ImGui.GetIO();
// Modifiers are not reliable across systems
io.KeyCtrl = io.KeysDown[(int)KeyboardKey.LeftControl] || io.KeysDown[(int)KeyboardKey.RightControl];
io.KeyShift = io.KeysDown[(int)KeyboardKey.LeftShift] || io.KeysDown[(int)KeyboardKey.RightShift];
io.KeyAlt = io.KeysDown[(int)KeyboardKey.LeftAlt] || io.KeysDown[(int)KeyboardKey.RightAlt];
io.KeySuper = io.KeysDown[(int)KeyboardKey.LeftSuper] || io.KeysDown[(int)KeyboardKey.RightSuper];
// Key states
for (int i = (int)KeyboardKey.Spacebar; i < (int)KeyboardKey.KBMenu + 1; i++)
{
io.KeysDown[i] = input.IsKeyboardKeyDown((KeyboardKey)i);
}
// Key input
int keyPressed = input.GetCharPressed();
if (keyPressed != 0)
{
io.AddInputCharacter((uint)keyPressed);
}
}
private void UpdateMouse(InputHandler input)
{
ImGuiIOPtr io = ImGui.GetIO();
// Store button states
for (int i = 0; i < io.MouseDown.Count; i++)
{
io.MouseDown[i] = input.IsMouseButtonDown((MouseButton)i);
}
// Mouse scroll
io.MouseWheel += input.GetMouseWheelMovement();
// Mouse position
Vector2 mousePosition = io.MousePos;
// TODO:
// bool focused = Raylib.IsWindowFocused();
if (io.WantSetMousePos)
{
input.SetMousePosition(mousePosition);
}
else
{
io.MousePos = input.GetMousePosition();
}
// Mouse cursor state
if ((io.ConfigFlags & ImGuiConfigFlags.NoMouseCursorChange) == 0 || input.IsCursorHidden())
{
ImGuiMouseCursor cursor = ImGui.GetMouseCursor();
if (cursor == ImGuiMouseCursor.None || io.MouseDrawCursor)
{
input.HideCursor();
}
else
{
input.ShowCursor();
}
}
}
private void Render()
{
ImGui.Render();
}
private void RenderCommandLists(ImDrawDataPtr data)
{
// Scale coordinates for retina displays (screen coordinates != framebuffer coordinates)
int fbWidth = (int)(data.DisplaySize.X * data.FramebufferScale.X);
int fbHeight = (int)(data.DisplaySize.Y * data.FramebufferScale.Y);
// Avoid rendering if display is minimized or if the command list is empty
if (fbWidth <= 0 || fbHeight <= 0 || data.CmdListsCount == 0)
{
return;
}
Rlgl.rlDrawRenderBatchActive();
Rlgl.rlDisableBackfaceCulling();
Rlgl.rlEnableScissorTest();
data.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale);
for (int n = 0; n < data.CmdListsCount; n++)
{
int idxOffset = 0;
ImDrawListPtr cmdList = data.CmdListsRange[n];
// Vertex buffer and index buffer generated by DearImGui
ImPtrVector<ImDrawVertPtr> vtxBuffer = cmdList.VtxBuffer;
ImVector<ushort> idxBuffer = cmdList.IdxBuffer;
for (int cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++)
{
ImDrawCmdPtr pcmd = cmdList.CmdBuffer[cmdi];
// Scissor rect
Vector2 pos = data.DisplayPos;
int rectX = (int)((pcmd.ClipRect.X - pos.X) * data.FramebufferScale.X);
int rectY = (int)((pcmd.ClipRect.Y - pos.Y) * data.FramebufferScale.Y);
int rectW = (int)((pcmd.ClipRect.Z - rectX) * data.FramebufferScale.Y);
int rectH = (int)((pcmd.ClipRect.W - rectY) * data.FramebufferScale.Y);
Rlgl.rlScissor(rectX, Raylib.GetScreenHeight() - (rectY + rectH), rectW, rectH);
if (pcmd.UserCallback != IntPtr.Zero)
{
// pcmd.UserCallback(cmdList, pcmd);
idxOffset += (int)pcmd.ElemCount;
}
else
{
DrawTriangles(pcmd.ElemCount, idxOffset, (int)pcmd.VtxOffset, idxBuffer, vtxBuffer, pcmd.TextureId);
idxOffset += (int)pcmd.ElemCount;
Rlgl.rlDrawRenderBatchActive();
}
}
}
Rlgl.rlSetTexture(0);
Rlgl.rlDisableScissorTest();
Rlgl.rlEnableBackfaceCulling();
}
private Color GetColor(uint hexValue)
{
Color color = new Color();
color.R = (byte)(hexValue & 0xFF) / 255f;
color.G = (byte)((hexValue >> 8) & 0xFF) / 255f;
color.B = (byte)((hexValue >> 16) & 0xFF) / 255f;
color.A = (byte)((hexValue >> 24) & 0xFF) / 255f;
return color;
}
void DrawTriangleVertex(ImDrawVertPtr idxVert)
{
Color c = GetColor(idxVert.col);
Rlgl.rlColor4ub((byte)Math.Round(c.R * 255f), (byte)Math.Round(c.G * 255f), (byte)Math.Round(c.B * 255f), (byte)Math.Round(c.A * 255f));
Rlgl.rlTexCoord2f(idxVert.uv.X, idxVert.uv.Y);
Rlgl.rlVertex2f(idxVert.pos.X, idxVert.pos.Y);
}
// Draw the imgui triangle data
private void DrawTriangles(uint count, int idxOffset, int vtxOffset, ImVector<ushort> idxBuffer, ImPtrVector<ImDrawVertPtr> idxVert, IntPtr textureId)
{
ushort index = 0;
ImDrawVertPtr vertex;
if (Rlgl.rlCheckRenderBatchLimit((int)count * 3))
{
Rlgl.rlDrawRenderBatchActive();
}
Rlgl.rlBegin(DrawMode.TRIANGLES);
Rlgl.rlSetTexture((uint)textureId);
for (int i = 0; i <= (count - 3); i += 3)
{
index = idxBuffer[idxOffset + i];
vertex = idxVert[vtxOffset + index];
DrawTriangleVertex(vertex);
index = idxBuffer[idxOffset + i + 1];
vertex = idxVert[vtxOffset + index];
DrawTriangleVertex(vertex);
index = idxBuffer[idxOffset + i + 2];
vertex = idxVert[vtxOffset + index];
DrawTriangleVertex(vertex);
}
Rlgl.rlEnd();
}
public void Draw(Renderer renderer)
{
ImGui.Render();
RenderCommandLists(ImGui.GetDrawData());
}
private IntPtr _context;
private Texture2D _fontTexture;
private Vector2 _scaleFactor = Vector2.One;
}
}

View File

@@ -0,0 +1,153 @@
#pragma warning disable CA2211
using System.Runtime.CompilerServices;
namespace Voile.Utils
{
public class Logger
{
public static Action<string, LogLevel>? OnLog;
public static string LogPath { get; set; } = "Logs/";
/// <summary>
/// Maximum amount of log files in a log folder. If it reaches the limit, all logs will be written to <c>voile-latest.log</c> instead of creating a new one.
/// </summary>
public static int MaxLogFiles { get; set; } = 5;
/// <summary>
/// Specifies the logging level. In release builds, the log level is <c>Error</c>. In debug, the log level is <c>Echo</c>.
/// </summary>
public static LogLevel LogLevel = LogLevel.Error;
/// <summary>
/// Specifies if the logger should write to file.
/// </summary>
public static bool WriteToFile = true;
public Logger(string className)
{
_className = className;
if (WriteToFile && !_logCreated)
{
var dirInfo = Directory.CreateDirectory(LogPath);
var files = dirInfo.GetFiles();
string logName = $"voile-{DateFormat}-{TimeFormat}.log".Replace(':', '.');
if (files.Length >= MaxLogFiles)
{
logName = "voile-latest.log";
}
var path = Path.Combine(LogPath, logName);
if (File.Exists(path))
{
File.Delete(path);
}
_fileStream = File.Create(path);
_fileWriter = new StreamWriter(_fileStream);
_logCreated = true;
}
#if DEBUG
LogLevel = LogLevel.Echo;
#endif
}
public void Echo(string what)
{
string message = string.Format(EchoFormat, TimeFormat, what);
LogFile(message);
LogConsole(what, LogLevel.Echo);
OnLog?.Invoke(what, LogLevel.Echo);
}
public void Info(string what, [CallerMemberName] string method = "")
{
LogLevel logType = LogLevel.Info;
string message = string.Format(LogFormat, TimeFormat, logType.ToString(), _className, method, what);
LogFile(message);
LogConsole(message, logType);
OnLog?.Invoke(message, logType);
}
public void Warn(string what, [CallerMemberName] string method = "")
{
LogLevel logType = LogLevel.Warn;
string message = string.Format(LogFormat, TimeFormat, logType.ToString(), _className, method, what);
LogFile(message);
LogConsole(message, logType);
OnLog?.Invoke(message, logType);
}
public void Error(string what, [CallerMemberName] string method = "")
{
LogLevel logType = LogLevel.Error;
string message = string.Format(LogFormat, TimeFormat, logType.ToString(), _className, method, what);
LogFile(message);
LogConsole(message, logType);
OnLog?.Invoke(message, logType);
}
private static string TimeFormat => $"{DateTime.Now:HH:mm:ffff}";
private static string DateFormat => $"{DateTime.Now:d:M:yyyy:}";
private static string LogFormat => "({0}) [{1}/{2}/{3}] {4}";
private static string EchoFormat => "({0}) {1}";
private static void LogConsole(string what, LogLevel logType)
{
if (LogLevel < logType) return;
Console.ForegroundColor = ConsoleColorForLog(logType);
Console.WriteLine(what);
Console.ForegroundColor = ConsoleColor.White;
}
private static ConsoleColor ConsoleColorForLog(LogLevel logType)
{
ConsoleColor color = ConsoleColor.White;
switch (logType)
{
case LogLevel.Info:
color = ConsoleColor.Cyan;
break;
case LogLevel.Warn:
color = ConsoleColor.Yellow;
break;
case LogLevel.Error:
color = ConsoleColor.Red;
break;
}
return color;
}
private void LogFile(string message)
{
if (!WriteToFile || _fileWriter is null) return;
_fileWriter.WriteLine(message);
_fileWriter.Flush();
}
private readonly string _className;
private static bool _logCreated;
private static FileStream? _fileStream;
private static StreamWriter? _fileWriter;
}
public enum LogLevel
{
Error,
Warn,
Info,
Echo,
}
}

View File

@@ -0,0 +1,48 @@
using System.Numerics;
namespace Voile
{
public static class MathUtils
{
public static float Lerp(float a, float b, double t) => a + (b - a) * (float)t;
public static Color LerpColor(Color colorA, Color colorB, double t)
{
var r = (byte)Lerp(colorA.R, colorB.R, t);
var g = (byte)Lerp(colorA.G, colorB.G, t);
var b = (byte)Lerp(colorA.B, colorB.B, t);
var a = (byte)Lerp(colorA.A, colorB.A, t);
return new Color(r, g, b, a);
}
public static Vector2 RandomVector2(Vector2 min, Vector2 max)
{
var x = _random.NextDouble(min.X, max.X);
var y = _random.NextDouble(min.Y, max.Y);
return new Vector2((float)x, (float)y);
}
public static float EaseOutBack(float x)
{
var c1 = 1.70158f;
var c3 = c1 + 1f;
return 1f + c3 * (float)Math.Pow(x - 1f, 3f) + c1 * (float)Math.Pow(x - 1, 2);
}
public static float EaseOutElastic(float x)
{
var c4 = 2f * (float)Math.PI / 3f;
return x == 0
? 0
: x == 1
? 1
: (float)Math.Pow(2, -10 * x) * (float)Math.Sin((x * 10 - 0.75f) * c4) + 1;
}
private static LehmerRandom _random = new();
}
}