Compare commits

...

75 Commits

Author SHA1 Message Date
8b2c26f9bb Work in progress new asset system. 2020-11-17 13:10:44 -06:00
d8eda24809 Playground now has more control; Implemented framework changes.
Live framerate and update rate changing commands added.
2020-08-22 22:41:04 -05:00
c28261dcfa Fixed jumpy interpolation caused by multiple interpolations per frame. 2020-08-22 22:39:05 -05:00
d576600885 Changed a log level for game engine. 2020-07-12 14:12:22 -05:00
e4da7901bd MeshBatchRenderer is now more strict about state. 2020-07-12 12:08:35 -05:00
2a007cfb70 BitmapFont default pixel height bug fixed. 2020-07-11 18:20:31 -05:00
4a836cef75 Added some docs. 2020-07-11 17:18:12 -05:00
e4b7c12dbb Refactored and removed overlap check. Fixed contain check.
Overlap was built in.

check for vector in rectangle was bugged. Fixed now.
2020-07-11 17:07:04 -05:00
72925842b7 Fixed object pool unlimited mode. 2020-07-11 11:45:03 -05:00
326d97bd64 Added size limit to pool. 2020-07-11 11:18:16 -05:00
ab58480434 Added overlap detection against rectangles and points. 2020-07-11 01:38:24 -05:00
d4efce48c3 Added pooling system. 2020-07-11 01:37:45 -05:00
f339a33d3e State Manager now calls activate on the initial state. 2020-07-10 23:48:50 -05:00
1f8bc5fb61 Fixed choppiness with variable framerate.
Note: framerate seemed to decrease, may be due to processes at the time.
2020-07-10 23:48:30 -05:00
110c7dbfad Added easy way of switching to nearest filtering. 2020-07-10 21:56:11 -05:00
75f691133a Rectangle mesh texture bounds can now be changed. 2020-07-10 21:55:12 -05:00
4b1e57ac72 More data safety checking, doesn't load spaces. 2020-07-10 19:00:36 -05:00
f4aa9b7ffb Removed useless code, fixed early SDL quitting. 2020-07-10 18:05:14 -05:00
98869c2d38 Added default path modifier, fixed how it references assets. 2020-07-10 18:04:56 -05:00
68c506670b glClearColor never actually generates an error. 2020-07-10 18:01:09 -05:00
3bb56a50d1 Changed how interpolation between updates happen. 2020-07-07 14:16:31 -05:00
512b5f4b13 Refactored, and added separate constructor for custom shader. 2020-07-07 01:41:12 -05:00
614602e7cd MeshBatch now records states to reduce data sent to shader per call. 2020-07-07 01:07:33 -05:00
fd7edc2629 Fixed typo, playground debug now attaches log listener on toggle. 2020-07-06 18:06:46 -05:00
636efaaaf9 Updated readme. 2020-07-06 14:30:18 -05:00
74b1728dcb Fixed some typos. 2020-07-06 14:04:49 -05:00
80adda7afb Mainstate view ratio corrected, camera2D takes float dimensions now.
added additional debug logging.
2020-07-06 12:23:56 -05:00
319ade258e Refactoring. 2020-07-05 23:45:04 -05:00
b2a47ebefc Refactoring, added more logging, minor command structure change. 2020-07-05 23:32:50 -05:00
63a31c491d So loud added, changed how dependencies are organized. 2020-07-05 22:50:54 -05:00
02c1ceae14 Adding soloud stuff. 2020-07-05 02:06:25 -05:00
7d418a75ff Restructured logging in tools. 2020-07-04 18:57:59 -05:00
b744ae653c Changed to custom logging solution. 2020-07-04 18:41:06 -05:00
30d7221a85 Added unit measurements to bitmap font. 2020-07-04 17:29:20 -05:00
5a8e7a9ccb Added line breaks to bitmap font. 2020-07-04 16:14:32 -05:00
75ef59293f Changed how the bitmap font prepares characters.
Playground was updated to reflect this.
2020-07-04 13:35:38 -05:00
0e3f1dfef3 Main state changed to reflect font rendering capability. 2020-07-04 00:39:40 -05:00
fbda9b912f Added yielding, input refreshes as fast as possible. 2020-07-04 00:39:23 -05:00
3ecd32520c Moved inputs inside update section of game loop. 2020-07-03 18:17:07 -05:00
ad77fec3a2 Bitmap font system works. 2020-07-03 18:07:55 -05:00
378712283a Fixed missing mesh. 2020-07-02 14:51:45 -05:00
a881f1a086 Basic font rendering working. 2020-07-02 13:13:04 -05:00
56eca1b0d6 New LRU cache with unit tests in preparation for TTF font rendering. 2020-06-27 17:11:31 -05:00
bb3d0bced5 Refactoring. 2020-06-27 17:09:21 -05:00
9f6ef94f06 MeshBatch now only pushes camera matrices when nessecary. 2020-06-27 17:09:15 -05:00
364803bf6c Changed dependencies, added dependencies for TTF rendering. 2020-06-26 22:10:29 -05:00
0225f0821c Restructuring and clean up. Preparation for loading library. 2020-06-25 11:19:39 -05:00
bbdafb2489 Added camera axis' for convenience.
Untested.
2020-06-25 11:18:54 -05:00
8827bfbb78 Added linear interpolation between updates to mesh batch.
Cleaned up code.
2020-06-25 00:05:44 -05:00
e781aea776 MeshBatch now renders with or without textures.
General structure clean up as well.
2020-06-23 23:54:42 -05:00
6a19d1f5c7 Basic rendering with camera controls are functional. 2020-06-23 20:07:12 -05:00
1c4ca6c97b Lots of progress on rendering textures. 2020-06-05 23:49:45 -05:00
23597f02b1 Began implementing an asset loading system. 2020-06-03 01:36:24 -05:00
484dbbece3 Fixed issue with incorrect delegate signature. 2020-06-01 16:32:18 -05:00
e59e78e08c General progress on getting OpenGL based rendering. 2020-06-01 01:28:03 -05:00
5e85eb5de1 Cleaned up code and added some parameter checks.
Removed some unused code fragments from older plans.
2020-05-29 00:01:05 -05:00
51cfc84cc7 More progress on shader implementation.
Changed engine OpenGL version to 4.1.

Added OpenGL program pipeline handles.

Added fragment shaders.

Some class name changes.
2020-05-28 23:22:44 -05:00
d269760c80 Progress on adding GL shader system. 2020-05-28 13:51:44 -05:00
93937a8b34 Refactoring. 2020-05-28 10:20:54 -05:00
fb9bbdd123 Added more OpenGL delegates, and restructured game manager.
Largely untested.
2020-05-28 10:19:38 -05:00
1a2ef5a109 Fixed imports. 2020-05-28 00:56:45 -05:00
d1ed96e7ed Progress on OpenGL portion. 2020-05-28 00:55:23 -05:00
af7c1b37f9 Improved window and input handling.
Window now has some events.

Keyboard and mouse implemented.
2020-05-27 20:24:02 -05:00
e6230c013b Made sure division is done to double values instead of ints. 2020-05-27 11:20:18 -05:00
9ecfd079b2 Game engine now uses timespan structures to be more precise. 2020-05-27 11:07:18 -05:00
a3401e1c22 Removed console logging. 2020-05-27 10:24:57 -05:00
4fc84e6a97 Updated gitignore to ignore .log files. 2020-05-27 10:24:47 -05:00
442d357626 Improved dispose structure. 2020-05-27 00:33:26 -05:00
a013c476e7 Began working on a graphical playground for testing.
Improved SDL exception class.

Engine code changes.

General progress.
2020-05-27 00:20:41 -05:00
0f0395fd63 Class rename that accounts for potential changes. 2020-05-26 21:07:58 -05:00
2d9f7617aa Added basic help and quit functions to tools. 2020-05-26 21:06:31 -05:00
d149e12433 Began work on the tools command processing. 2020-05-26 14:47:20 -05:00
065e6d878f Added some documentation and progress on boilerplate. 2020-05-23 23:52:41 -05:00
7a6f709e9a Added window control grabbing and special hit testing.
Also cleaned up and added exceptions to better structure exception handling.

Many small changes to improve useability of framework.

Some refactoring.
2020-05-23 22:48:55 -05:00
bd6b085687 Began implementing windows handle, game state manager, and game loop. 2020-05-23 17:05:23 -05:00
78 changed files with 22831 additions and 37 deletions

3
.gitignore vendored
View File

@ -37,4 +37,5 @@
# Ignore all local history of files
.history
# End of https://www.gitignore.io/api/git,dotnetcore,visualstudiocode
# End of https://www.gitignore.io/api/git,dotnetcore,visualstudiocode
**/*.Log

27
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": "SlatedGameToolkit.Tools",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/SlatedGameToolkit.Tools/bin/Debug/netcoreapp3.1/SlatedGameToolkit.Tools.exe",
"args": [],
"cwd": "${workspaceFolder}/src/SlatedGameToolkit.Tools",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

42
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/SlatedGameToolkit.Tools/SlatedGameToolkit.Tools.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/SlatedGameToolkit.Tools/SlatedGameToolkit.Tools.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"${workspaceFolder}/src/SlatedGameToolkit.Tools/SlatedGameToolkit.Tools.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@ -1,12 +1,29 @@
# A Simple Game Toolkit
# Slated Games Toolkit
A personal game toolkit built on top of SFML with the primary overarching goals of the framework being to implement a game state manager, a comprehensive decent scene graph system, and a solid audio system with reasonable access to the datastream. This toolkit also provides a utility portion that assists in the creation of some file data structures such as texture maps, and 9patches.
## A Simple Game Toolkit
A personal game toolkit built on top of SDL2 with the primary overarching goals of the framework being to implement a game state manager, a comprehensive scene graph system, and SoLoud as the full-fledged audio engine. This toolkit also provides a utility portion that assists in the creation of some file data structures such as texture maps, and 9patches.
## Components
The toolkit is split into 2 portions:
The toolkit is split into 2 main portions:
- Utility
- Framework
The Utility portion is a lightweight program that aids in producing some of the more commonly used file data structures by the framework. The framework itself is the part that provides the libraries and SFML implementation to allow for a smoother game development experience.
The Utility portion is a lightweight program that aids in producing some of the more commonly used file data structures by the framework. The framework itself is the part that provides the libraries and SDL2 implementation to allow for a smoother game development experience.
## Design Objectives
Leave access to low level function calls, but have abstraction layers that makes common game development tasks simple. Ideally, this means that if a client decides to use this Toolkit to make a game that doesn't need to do unconventional things (for a game), they should be able to do so without ever really needing to interact with lower level code, and therefore, should have a fairly smooth, easy development experience. The opposite is also true however, as access to the inner libraries that SGTK was built upon are left open.
## Design Model
The framework was designed with the concept of having an game engine at the center of the framework that handles the actual game loop and many of the properties of said game loop. The rest of the framework is utilized by the engine and the client using the toolkit.
The engine directly interacts with the state manager, which is an object that organizes the various possible states a game may have at any time. Individual states can be created by the client which provides a way for organizing and structuring their game.
Windows are organized and associated with an OpenGL context on creation, and managed by a Window Contexts Manager which keeps track of all the active windows, and the one that is currently being modified by the code.
## Technologies Used
These are the libraries that make SGTK what it is and a special thanks goes to the people that created them:
- OpenGL - To perform graphical interfacing and rendering.
- SDL2 - acts as the interface between the hardware and the framework.
- SoLoud - acts as the game framework's audio engine.
- STBTrueTypeSharp - helps with rasterizing TrueType fonts.
- STBImageSharp - helps with loading images of varying formats.

View File

@ -0,0 +1,26 @@
using SlatedGameToolkit.Framework.AssetSystem;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Textures;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.AssetSystem.AssetData
{
public class TextureData : IAssetData
{
public readonly byte[] data;
public readonly int width;
public readonly int height;
public TextureData(int width, int height, byte[] data) {
this.data = data;
this.width = width;
this.height = height;
}
public IAssetUseable InitializeUseable(GLContext glContext = null)
{
if (glContext == null) glContext = WindowContextsManager.CurrentGL;
return new Texture(this, context: glContext);
}
}
}

View File

@ -0,0 +1,181 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.AssetSystem {
/// <summary>
/// A delegate that should modify a given path.
/// </summary>
/// <param name="path">A path to modify.</param>
/// <returns>The modified path.</returns>
public delegate string ModifyPath(string path);
/// <summary>
/// A delegate that performs the loading of the specific asset.
/// </summary>
/// <param name="name">The path of the asset.</param>
/// <typeparam name="T">The type of the resulting asset.</typeparam>
/// <returns>A loaded, useable asset.</returns>
public delegate IAssetData LoadAsset(string path);
//TODO Implement change to two step asset pipeline.
public class AssetManager {
public IAssetUseable this[string key] {
get {
if (!this.assets.ContainsKey(key)) { // if the data is still uninitialized, then do so.
InitializeData(key);
}
return this.assets[key];
}
}
private readonly object assetThreadLock = new object();
private Thread thread;
private ConcurrentQueue<string> loadables;
private volatile bool load;
public bool Complete {
get {
return loadables.Count == 0;
}
}
private ConcurrentDictionary<string, IAssetUseable> assets;
/// <summary>
/// A concurrent dictionary containing all the path modifier's in association with their extension.
/// This is what the manager uses to determine how to modify a filename or path dependent on it's extension.
/// </summary>
/// <value>A dictionary of file extension associated with respective paths.</value>
public ConcurrentDictionary<string, ModifyPath> PathModifiers { get; private set; }
/// <summary>
/// If this value is not null, the asset manager will modify paths according to this delegate if their is not a more appropriate modifier.
/// </summary>
/// <value>The delegate to use to modify the path.</value>
public ModifyPath DefaultPathModifier {get; set;}
/// <summary>
/// A dictionary containing associations between file extensions and their loaders.
/// All file extensions will be requested in lower cases, and therefore, the extension key in this dictionary should also be all lowercase.
/// </summary>
/// <value>The dictionary that associates the loaders to their file extensions.</value>
public ConcurrentDictionary<string, LoadAsset> Loaders { get ; private set; }
private ConcurrentDictionary<string, IAssetData> uninitializedDataDictionary;
private ConcurrentBag<string> uninitializedDataBag;
private GLContext glContext;
public AssetManager(GLContext glContext = null) {
this.glContext = glContext ?? WindowContextsManager.CurrentGL;
this.loadables = new ConcurrentQueue<string>();
this.assets = new ConcurrentDictionary<string, IAssetUseable>();
this.PathModifiers = new ConcurrentDictionary<string, ModifyPath>();
this.Loaders = new ConcurrentDictionary<string, LoadAsset>();
this.uninitializedDataDictionary = new ConcurrentDictionary<string, IAssetData>();
this.uninitializedDataBag = new ConcurrentBag<string>();
thread = new Thread(Run);
this.load = true;
thread.Name = "Asset-Loader";
thread.IsBackground = true;
thread.Start();
}
/// <summary>
/// Queue's the loadable for batched loading.
/// The file name of the loadable is the one the loadable is saved under.
/// </summary>
/// <param name="name">path of file.</param>
public void QueueLoad(string name) {
loadables.Enqueue(name);
}
private void Run() {
while (load) {
lock (assetThreadLock)
{
string loadable;
while (loadables.TryDequeue(out loadable)) {
Load(loadable);
}
if (load) Monitor.Wait(assetThreadLock);
}
}
}
/// <summary>
/// Loads the loadable and stores it under the filename given to the loadable.
/// </summary>
/// <param name="name">The loadable to load.</param>
public void Load(string name) {
if (glContext == null) glContext = WindowContextsManager.CurrentGL;
string original = name;
string ext = Path.GetExtension(name).ToLower().Substring(1);
if (PathModifiers.ContainsKey(ext)) {
name = PathModifiers[ext](name);
} else if (DefaultPathModifier != null) {
name = DefaultPathModifier(name);
}
if (!Loaders.ContainsKey(ext)) throw new FrameworkUsageException(string.Format("Failed to find associated loader for file \"{0}\" with extension \"{1}\".", name, ext));
uninitializedDataDictionary[original] = Loaders[ext](name);
uninitializedDataBag.Add(name);
GameEngine.ToRunQueue.Enqueue(InitializeDataRandom);
}
private void InitializeDataRandom() {
string name;
if (uninitializedDataBag.TryTake(out name)) {
if (this.uninitializedDataDictionary.ContainsKey(name)) {
this.assets[name] = this.uninitializedDataDictionary[name].InitializeUseable(glContext);
}
} else {
throw new InternalFrameworkException("Difference in queued uninitialized data loading quantity and actual quantity.");
}
}
private void InitializeData(string name) {
this.assets[name] = this.uninitializedDataDictionary[name].InitializeUseable(glContext);
}
/// <summary>
/// Loads the currently queued asynchronously.
/// </summary>
public void LoadQueued() {
if (Monitor.TryEnter(assetThreadLock)) {
Monitor.Pulse(assetThreadLock);
}
}
public void WaitAndLoadAllQueued() {
LoadQueued();
Monitor.Enter(assetThreadLock);
Monitor.Exit(assetThreadLock);
return;
}
/// <summary>
/// Unloads the asset under the given name.
/// This disposes the asset as well as removes it from the manager.
/// </summary>
/// <param name="name">The name of the asset to unload.</param>
public void Unload(string name) {
IAssetUseable asset;
assets.TryRemove(name, out asset);
asset.Dispose();
}
/// <summary>
/// Removes all the assets.
/// </summary>
public void UnloadAll() {
foreach (string name in assets.Keys)
{
Unload(name);
}
}
}
}

View File

@ -0,0 +1,14 @@
using SlatedGameToolkit.Framework.Graphics.OpenGL;
namespace SlatedGameToolkit.Framework.AssetSystem
{
public interface IAssetData
{
/// <summary>
/// Initializes the asset data and returns the useable variant of said data.
/// </summary>
/// <param name="GLContext">The Open GL Context to use.</param>
/// <returns>The associated useable asset.</returns>
IAssetUseable InitializeUseable(GLContext GLContext);
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace SlatedGameToolkit.Framework.AssetSystem
{
public interface IAssetUseable : IDisposable
{
}
}

View File

@ -0,0 +1,27 @@
using System.IO;
using SlatedGameToolkit.Framework.AssetSystem.AssetData;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Textures;
using StbImageSharp;
namespace SlatedGameToolkit.Framework.Loaders
{
public static class TextureLoader
{
/// <summary>
/// Loads a texture using StbImage library.
/// Any format supported by StbImage is therefore supported.
/// </summary>
/// <param name="path">The path of the texture to load.</param>
/// <returns>The data for the texture.</returns>
public static TextureData Load2DTexture(string path)
{
using (Stream stream = File.OpenRead(path))
{
ImageResult image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);
TextureData textureData = new TextureData(image.Width, image.Height, image.Data);
return textureData;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
using System;
namespace SlatedGameToolkit.Framework
{
public class Class1
{
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace SlatedGameToolkit.Framework.Exceptions
{
public abstract class FrameworkException : Exception {
public FrameworkException() {
}
public FrameworkException(string message) : base(message) {
}
public FrameworkException(string message, Exception inner) : base(message, inner) {
}
}
}

View File

@ -0,0 +1,20 @@
using System;
namespace SlatedGameToolkit.Framework.Exceptions
{
/// <summary>
/// An exception that is thrown when there is an inappropriate use of the framework.
/// </summary>
public class FrameworkUsageException : FrameworkException {
public FrameworkUsageException() : base() {
}
public FrameworkUsageException(string message) : base(message) {
}
public FrameworkUsageException(string message, Exception inner) : base(message, inner) {
}
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace SlatedGameToolkit.Framework.Exceptions
{
/// <summary>
/// An exception that is thrown when an error occurs on the game framework level that shouldn't have occurred and is definitely considered a bug.
/// </summary>
internal class InternalFrameworkException : FrameworkException {
const string ADDITIONAL_MESSAGE = "**This exception is a framework bug!**";
public InternalFrameworkException() : base() {
}
public InternalFrameworkException(string message) : base(message + ' ' + ADDITIONAL_MESSAGE) {
}
public InternalFrameworkException(string message, Exception inner) : base(message + ' ' + ADDITIONAL_MESSAGE, inner) {
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using SlatedGameToolkit.Framework.Graphics;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.Exceptions
{
public class OpenGLErrorException : Exception {
public Graphics.OpenGL.ErrorCode ErrorCode {get; private set;}
public OpenGLErrorException(Graphics.OpenGL.ErrorCode error) : base(string.Format("OpenGL error: {0}", error)) {
this.ErrorCode = error;
}
public OpenGLErrorException(Graphics.OpenGL.ErrorCode error, string message) : base(string.Format("OpenGL error: {0}. \"{1}\"", error, message)) {
this.ErrorCode = error;
}
public OpenGLErrorException(Graphics.OpenGL.ErrorCode error, string message, Exception inner) : base(string.Format("OpenGL error: {0}. \"{1}\"", error, message), inner) {
this.ErrorCode = error;
}
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace SlatedGameToolkit.Framework.Exceptions
{
/// <summary>
/// This exception is produced when the error producing SDL feature is not something the framework considers critical.
/// This can happen if not all framework supported platforms support a specific feature that SDL provides.
/// </summary>
public class OptionalSDLException : SDLException {
public OptionalSDLException() : base() {
}
public OptionalSDLException(string message) : base(message) {
}
public OptionalSDLException(string message, Exception inner) : base(message, inner) {
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using SDL2;
namespace SlatedGameToolkit.Framework.Exceptions
{
/// <summary>
/// An SDLException is defined as an exception that is thrown whenever an error occurs in any SDL functions.
/// </summary>
[Serializable]
public class SDLException : FrameworkException {
public string SDLMessage { get; }
/// <summary>
/// Creates an SDL exception.
/// </summary>
/// <param name="Fetch">Whether or not to fetch the last error message that occurred in SDL.</param>
public SDLException(bool autoFlush = true) : base(autoFlush ? SDL.SDL_GetError() : "SDL error has occurred.") {
if (autoFlush) {
SDLMessage = SDL.SDL_GetError();
SDL.SDL_ClearError();
}
}
public SDLException(string message, bool autoFlush = true) : base(message + " (" + (autoFlush ? SDL.SDL_GetError() : "SDL error has occurred.") + ")") {
if (autoFlush) {
SDLMessage = SDL.SDL_GetError();
SDL.SDL_ClearError();
}
}
public SDLException(string message, Exception inner, bool autoFlush = true) : base(message + " (" + (autoFlush ? SDL.SDL_GetError() : "SDL error has occurred.") + ")", inner) {
if (autoFlush) {
SDLMessage = SDL.SDL_GetError();
SDL.SDL_ClearError();
}
}
}
}

View File

@ -0,0 +1,20 @@
using System;
namespace SlatedGameToolkit.Framework.Exceptions
{
public class SoLoudException : FrameworkException
{
public int ErrorCode { get; private set; }
public SoLoudException(int errorCode) : base(GameEngine.SoLoudEngine.getErrorString(errorCode) + " SoLoud error has occurred.")
{
}
public SoLoudException(int errorCode, string message) : base(GameEngine.SoLoudEngine.getErrorString(errorCode) + " SoLoud error has occurred: " + message)
{
}
public SoLoudException(int errorCode, string message, Exception inner) : base(GameEngine.SoLoudEngine.getErrorString(errorCode) + " SoLoud error has occurred: " + message, inner)
{
}
}
}

View File

@ -0,0 +1,289 @@
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using SDL2;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Input.Devices;
using SlatedGameToolkit.Framework.Logging;
using SlatedGameToolkit.Framework.StateSystem;
using SlatedGameToolkit.Framework.StateSystem.States;
using SoLoud;
namespace SlatedGameToolkit.Framework {
/// <summary>
/// The main engine that will host the game loop.
/// </summary>
public static class GameEngine {
private static readonly object ignitionLock = new object();
private static readonly object deltaUpdateLock = new object();
private static Thread thread;
private static volatile bool exit = false, stopped = true;
private static volatile bool deltaChanged;
private static TimeSpan updateDeltaTime = TimeSpan.FromSeconds(1d/40d), frameDeltaTime = TimeSpan.FromSeconds(1d/60d);
public static ConcurrentQueue<ThreadStart> ToRunQueue = new ConcurrentQueue<ThreadStart>();
/// <summary>
/// The amount of updates per second.
///
/// If greater than 0, game loop updates will be fixed (time steps).
/// More specifically, the amount of updates performed per frame to allow for physics calculations
/// to be calculated based on a constant time step will be calculated automatically.
///
/// If set to 0 or less, updates will be as fast as possible.
/// </summary>
/// <value>The updates per second.</value>
public static double UpdatesPerSecond {
get {
if (updateDeltaTime.TotalSeconds <= 0) return 0;
return 1d / updateDeltaTime.TotalSeconds;
}
set {
lock (deltaUpdateLock) {
if (value <= 0) {
updateDeltaTime = TimeSpan.FromSeconds(0);
} else {
updateDeltaTime = TimeSpan.FromSeconds(1d/value);
}
}
deltaChanged = true;
}
}
/// <summary>
/// The target frames per second. Will not go above this number, but may dip below this number.
/// Setting this to 0 renders as many frames as the system can handle.
/// Default is 60.
/// </summary>
/// <value>The target frames per second.</value>
public static double targetFPS {
get {
if (frameDeltaTime.TotalSeconds <= 0) return 0;
return 1 / frameDeltaTime.TotalSeconds;
}
set {
lock (deltaUpdateLock) {
if (value <= 0) {
frameDeltaTime = TimeSpan.FromSeconds(0);
} else {
frameDeltaTime = TimeSpan.FromSeconds(1d/value);
}
}
deltaChanged = true;
}
}
public static Soloud SoLoudEngine { get; private set; }
private static void Run(Object o) {
if (!(o is IState)) throw new InternalFrameworkException(String.Format("Expected initial state object for asynchronous loop. Got {0}", o));
StateManager manager = new StateManager();
IState initialState = (IState) o;
manager.Initialize(initialState);
DateTime previousTime = DateTime.Now;
TimeSpan timePassedFromLastUpdate = TimeSpan.Zero;
TimeSpan timePassedFromLastRender = TimeSpan.Zero;
TimeSpan updateTimePassedFromLastRender =TimeSpan.Zero;
TimeSpan updateDeltaTime = GameEngine.updateDeltaTime;
TimeSpan frameDeltaTime = GameEngine.frameDeltaTime;
deltaChanged = true;
stopped = false;
Logger.Log("Game engine initiated.");
while (!exit) {
//Pull latest deltas.
if (deltaChanged) {
lock (deltaUpdateLock) {
updateDeltaTime = GameEngine.updateDeltaTime;
frameDeltaTime = GameEngine.frameDeltaTime;
Logger.Log(String.Format("Time deltas were set. Update Delta: {0}, Render target Delta: {1}", updateDeltaTime.TotalSeconds, frameDeltaTime.TotalSeconds), LogLevel.DEBUG);
}
deltaChanged = false;
}
#region EventHandling
//Events
SDL.SDL_Event SDL_Event;
while (SDL.SDL_PollEvent(out SDL_Event) != 0) {
switch (SDL_Event.type) {
case SDL.SDL_EventType.SDL_MOUSEMOTION:
Mouse.OnMouseMoved(SDL_Event.motion.x, SDL_Event.motion.y);
break;
case SDL.SDL_EventType.SDL_MOUSEWHEEL:
Mouse.OnScroll(SDL_Event.wheel.x, SDL_Event.wheel.y);
break;
case SDL.SDL_EventType.SDL_MOUSEBUTTONDOWN:
if (SDL.SDL_BUTTON_LEFT == SDL_Event.button.button) {
Mouse.OnLeftChange(true);
} else if (SDL.SDL_BUTTON_RIGHT == SDL_Event.button.button) {
Mouse.OnRightChange(true);
} else if (SDL.SDL_BUTTON_MIDDLE == SDL_Event.button.button) {
Mouse.OnMiddleChange(true);
}
break;
case SDL.SDL_EventType.SDL_MOUSEBUTTONUP:
if (SDL.SDL_BUTTON_LEFT == SDL_Event.button.button) {
Mouse.OnLeftChange(false);
} else if (SDL.SDL_BUTTON_RIGHT == SDL_Event.button.button) {
Mouse.OnRightChange(false);
} else if (SDL.SDL_BUTTON_MIDDLE == SDL_Event.button.button) {
Mouse.OnMiddleChange(false);
}
break;
case SDL.SDL_EventType.SDL_KEYDOWN:
Keyboard.OnKeyPressed(SDL_Event.key.keysym.sym);
break;
case SDL.SDL_EventType.SDL_KEYUP:
Keyboard.OnKeyReleased(SDL_Event.key.keysym.sym);
break;
case SDL.SDL_EventType.SDL_WINDOWEVENT:
WindowContext handle = WindowContextsManager.ContextFromWindowID(SDL_Event.window.windowID);
switch (SDL_Event.window.windowEvent)
{
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
handle.OnResize(SDL_Event.window.data1, SDL_Event.window.data2);
break;
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST:
handle.OnFocusLost();
break;
case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED:
handle.OnFocusGained();
break;
}
break;
case SDL.SDL_EventType.SDL_QUIT:
Stop();
break;
}
}
#endregion
DateTime frameStart = DateTime.Now;
TimeSpan difference = frameStart - previousTime;
previousTime = frameStart;
timePassedFromLastUpdate += difference;
updateTimePassedFromLastRender += difference;
while (timePassedFromLastUpdate > updateDeltaTime) {
//Updates.
manager.Update(updateDeltaTime.TotalSeconds <= 0 ? timePassedFromLastUpdate.TotalSeconds : updateDeltaTime.TotalSeconds);
timePassedFromLastUpdate -= updateDeltaTime;
if (updateDeltaTime.TotalSeconds <= 0) {
timePassedFromLastUpdate = TimeSpan.Zero;
break;
}
}
timePassedFromLastRender += difference;
if (timePassedFromLastRender > frameDeltaTime) {
//Draw calls.
double prog = updateTimePassedFromLastRender / updateDeltaTime;
if (prog > 1) {
prog = 1;
} else if (prog < 0) {
prog = 0;
}
manager.Render(updateDeltaTime.TotalSeconds <= 0 ? 1 : prog);
updateTimePassedFromLastRender = TimeSpan.Zero;
timePassedFromLastRender = TimeSpan.Zero;
} else {
ThreadStart runnable;
if (ToRunQueue.TryDequeue(out runnable)) {
runnable();
} else
{
Thread.Yield();
}
}
}
stopped = true;
manager.Deinitialize();
SoLoudEngine.deinit();
WindowContextsManager.DisposeAllWindowContexts();
SDL.SDL_Quit();
Logger.Log("Game engine has gracefully stopped.");
Logger.FlushListeners();
}
/// <summary>
/// Requests to stop the game engine.
/// Game engine will finish the frame its currently in.
/// This needs to be called to perform proper clean up.
/// </summary>
/// <returns>False if the stop request has already been made.</returns>
public static bool Stop() {
lock (ignitionLock) {
if (exit) return false;
exit = true;
return true;
}
}
/// <summary>
/// Requests to start the game engine.
/// </summary>
/// <returns>True iff the engine is not already running.</returns>
public static bool Ignite(IState initialState) {
lock (ignitionLock) {
if (initialState == null) throw new ArgumentNullException("initialState");
if (!stopped) {
Logger.Log("Engine is already running.", LogLevel.WARNING);
return false;
}
SDL.SDL_version SDLVersion;
SDL.SDL_version SDLBuiltVersion;
SDL.SDL_GetVersion(out SDLVersion);
SDL.SDL_VERSION(out SDLBuiltVersion);
Logger.Log(String.Format("Attempting to initiate game engine with SDL version: {0}.{1}.{2}", SDLVersion.major, SDLVersion.minor, SDLVersion.patch));
if (!SDL.SDL_VERSION_ATLEAST(SDLBuiltVersion.major, SDLBuiltVersion.minor, SDLBuiltVersion.patch)) {
Logger.Log(String.Format("Engine was designed with SDL version {0}.{1}.{2}, currently running on {3}.{4}.{5}", SDLBuiltVersion.major, SDLBuiltVersion.minor, SDLBuiltVersion.patch, SDLVersion.major, SDLVersion.minor, SDLVersion.patch), LogLevel.WARNING);
if (SDLVersion.major < 2) {
Logger.Log("This engine was designed to work with SDL2. The version you're currently running is severely outdated.", LogLevel.FATAL);
throw new FrameworkUsageException("Outdated SDL binaries.");
}
}
exit = false;
Logger.Log("Initializing SDL video subsystem.");
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO) != 0) {
throw new SDLException();
}
Logger.Log("Initializing SDL audio subsystem.");
if (SDL.SDL_Init(SDL.SDL_INIT_AUDIO) != 0) {
throw new SDLException();
}
Logger.Log("Initializing SoLoud Engine.");
SoLoudEngine = new Soloud();
int error = SoLoudEngine.init(aBackend: Soloud.SDL2);
if (error != 0) {
throw new SoLoudException(error);
}
uint soLoudVer = SoLoudEngine.getVersion();
if (soLoudVer != Soloud.SOLOUD_VER) {
if (soLoudVer < Soloud.SOLOUD_VER) {
throw new FrameworkUsageException(string.Format("SoLoud version is out-dated! Detected version {0}. Minimum version requirement is {1}", soLoudVer, Soloud.SOLOUD_VER));
} else {
Logger.Log(string.Format("SoLoud version detected is {0} which is newer than version used to build framework (ver. {1}).", soLoudVer, Soloud.SOLOUD_VER), LogLevel.WARNING);
}
}
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, GLContext.GL_MAJOR_VER) < 0 ||
SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, GLContext.GL_MINOR_VER) < 0 ||
SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_CORE) < 0)
throw new SDLException(string.Format("Unable to retrieve correct OpenGL version {0}.{1}.0 Core.", GLContext.GL_MINOR_VER, GLContext.GL_MAJOR_VER));
Logger.Log(string.Format("OpenGL version set to {0}.{1}.0 Core.", GLContext.GL_MAJOR_VER, GLContext.GL_MINOR_VER));
thread = new Thread(Run);
thread.Name = "SGTK-Engine";
thread.Priority = ThreadPriority.AboveNormal;
thread.Start(initialState);
return true;
}
}
public static bool IsRunning() {
return !stopped;
}
}
}

View File

@ -0,0 +1,188 @@
using System;
using System.Numerics;
using SlatedGameToolkit.Framework.Graphics.Render;
using SlatedGameToolkit.Framework.Logging;
using SlatedGameToolkit.Framework.Utilities;
namespace SlatedGameToolkit.Framework.Graphics
{
public class Camera : IPositionInterpolable {
private bool viewUpdated = true, projectionUpdated = true, orthographic = false;
private Vector3 position, lookAt;
private float nearField = 0.01f, farField = 100f;
private float width, height;
private Matrix4x4 lookAtMatrix = Matrix4x4.Identity;
private Matrix4x4 view = Matrix4x4.Identity, projection;
/// <summary>
/// Position updates the camera without changing camera orientation if true.
/// Otherwise, camera will always attempt to look in the direction of the point given by the LookAt vector.
/// </summary>
/// <value>Whether or not to change orientation while moving camera position.</value>
public bool LockedOrientation { get; set; } = false;
/// <summary>
/// The up direction for orienting the camera.
/// </summary>
/// <value>A vector representing the up direction.</value>
public Vector3 Up { get; set; } = Vector3.UnitY;
/// <summary>
/// What the right direction is for the camera's current orientation.
/// </summary>
/// <value>A vector representing the right direction for the camera's current orientation.</value>
public Vector3 CameraRight
{
get
{
return Vector3.Cross(CameraFront, Up);
}
}
/// <summary>
/// The up direction for the camera's current orientation.
/// </summary>
/// <value>A vector representing the up direction for the camera's current orientation.</value>
public Vector3 CameraUp {
get {
return Vector3.Cross(CameraFront, -CameraRight);
}
}
public bool Orthographic {
get {
return orthographic;
}
set {
orthographic = value;
projectionUpdated = true;
}
}
public Vector3 Position {
get {
return position;
}
set {
Vector3 orientation = CameraFront;
this.position = value;
if (LockedOrientation) CameraFront = orientation;
viewUpdated = true;
}
}
public Vector3 CameraFront {
get {
return (LookAt - Position).NormalizeSafe(default(Vector3));
}
set {
LookAt = (position + value.NormalizeSafe(default(Vector3)));
}
}
public Vector3 LookAt {
get {
return lookAt;
}
set {
this.lookAt = value;
viewUpdated = true;
}
}
public virtual float NearField {
get {
return nearField;
}
set {
nearField = value;
projectionUpdated = true;
}
}
public virtual float FarField {
get {
return farField;
}
set {
farField = value;
projectionUpdated = true;
}
}
public virtual float Width {
get {
return width;
}
set {
width = value;
projectionUpdated = true;
}
}
public virtual float Height {
get {
return height;
}
set {
height = value;
projectionUpdated = true;
}
}
public Matrix4x4 ViewMatrix {
get {
if (viewUpdated) {
view = Matrix4x4.CreateLookAt(Position, LookAt, Up);
viewUpdated = false;
}
return view;
}
}
public Matrix4x4 ProjectionMatrix {
get {
if (projectionUpdated) {
if (Orthographic) {
projection = Matrix4x4.CreateOrthographic(width, height, nearField, farField);
} else {
projection = Matrix4x4.CreatePerspective(width, height, nearField, farField);
}
projectionUpdated = false;
}
return projection;
}
}
public Vector3 MoveTo { get; set; }
/// <summary>
/// Whether or not the projection has changed from the last time it was read.
/// </summary>
/// <value>A bool value indicating if the projection matrix has changed from the last retrieval.</value>
public bool ProjectionChanged { get => projectionUpdated; }
/// <summary>
/// Whether or not the camera view has changed from the last time it was read.
/// </summary>
/// <value>A bool value indicating if the matrix has change from the last retrieval.</value>
public bool ViewChanged { get => viewUpdated; }
public Camera(float width, float height) {
if (width <= 0) throw new ArgumentException("Width can't be less than or equal to 0.");
if (height <= 0) throw new ArgumentException("Height can't be less than or equal to 0.");
this.Width = width;
this.Height = height;
this.CameraFront = -Vector3.UnitZ;
Logger.Log(string.Format("Camera initial dimensions: {0}x{1}, ratio of view: {2}", width, height, width / height), LogLevel.DEBUG);
}
public void InterpolatePosition(double delta)
{
this.Position += (float) delta * (MoveTo - Position);
}
}
}

View File

@ -0,0 +1,33 @@
using System.Numerics;
namespace SlatedGameToolkit.Framework.Graphics
{
public class Camera2D : Camera {
public new Vector2 Position {
get {
return new Vector2(base.Position.X, base.Position.Y);
}
set {
base.Position = new Vector3(value, base.Position.Z);
}
}
public new Vector2 MoveTo {
get {
return new Vector2(base.MoveTo.X, base.MoveTo.Y);
}
set {
base.MoveTo = new Vector3(value, base.MoveTo.Z);
}
}
public Camera2D(float width, float height) : base(width, height) {
this.Orthographic = true;
this.LockedOrientation = true;
this.CameraFront = -Vector3.UnitZ;
base.Position = Vector3.UnitZ;
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
using System;
using System.Drawing;
using System.Numerics;
using SlatedGameToolkit.Framework.Graphics.Textures;
namespace SlatedGameToolkit.Framework.Graphics.Render
{
public interface IMesh
{
/// <summary>
/// Each value in the array represents a 3D coordinate for the vertex, as well as a 2D texture coordinate.
/// </summary>
/// <value></value>
ValueTuple<Vector3, Vector2>[] Vertices { get; }
/// <summary>
/// Returns indicies representing the vertices to use to create the mesh.
/// </summary>
/// <value>unsigned integers that are the indices of the vertices to use.</value>
uint[] Elements { get; }
/// <summary>
/// The texture for this mesh.
/// </summary>
/// <value>Texture for this mesh.</value>
ITexture Texture { get; }
/// <summary>
/// The blended color of this mesh.
/// </summary>
/// <value>A color for this mesh.</value>
Color Color { get; }
}
}

View File

@ -0,0 +1,13 @@
using System.Numerics;
namespace SlatedGameToolkit.Framework.Graphics.Render
{
public interface IPositionInterpolable
{
/// <summary>
/// Allows this object to perform interpolation based on the position (delta) between the update frames.
/// </summary>
/// <param name="delta">A normalized value between [0, 1] representing the current position between the updates.</param>
void InterpolatePosition(double delta);
}
}

View File

@ -0,0 +1,243 @@
using System;
using System.Drawing;
using System.Numerics;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Render.Programs;
using SlatedGameToolkit.Framework.Graphics.Render.Shaders;
using SlatedGameToolkit.Framework.Graphics.Textures;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Utilities;
namespace SlatedGameToolkit.Framework.Graphics.Render
{
/// <summary>
/// Represents a grouping of similar meshes that can be rendered with similar properties.
/// Specifically, properties that need to be the same are:
/// Texture and it's respective properties, model matrix, and vertex buffer array and objects.
/// </summary>
public class MeshBatchRenderer : IDisposable {
public bool Debug { get; set; }
private const int VERTEX_LENGTH = 9;
private bool disposed;
public GLContext GLContext {get; private set; }
private int projULoc, viewULoc, modelULoc, texturedULoc, singleChanneledULoc, flippedULoc;
private Camera camera;
private RenderProgram renderProgram;
private ITexture texture;
private VertexArrayBuffers vertexBuffers;
public bool Batching { get; private set; }
private Matrix4x4 modelsMatrix;
private float[] data;
private uint[] indices;
private int[] lengths;
private int[] indiceOffsets;
private int[] verticeOffsets;
private int dataIndex, indicesIndex, offsetIndex;
/// <summary>
/// Instantiates a mesh batch with a default vertex and fragment shader.
/// </summary>
/// <param name="camera">The camera to use for providing the correct matrices.</param>
/// <param name="BatchVertexSize">The vertex batch size. Defaults to 1024. This is multiplied by 9 for each individual value of the vertex.</param>
/// <param name="glContext">The glContext to associate the shaders with. Defaults to the currently active one.</param>
public MeshBatchRenderer(Camera camera, uint BatchVertexSize = 1024, GLContext glContext = null) {
VertexShader vert = new VertexShader(EmbeddedResUtils.ReadEmbeddedResourceText("default.vert"));
FragmentShader frag = new FragmentShader(EmbeddedResUtils.ReadEmbeddedResourceText("default.frag"));
this.GLContext = glContext ?? WindowContextsManager.CurrentGL;
renderProgram = new RenderProgram(this.GLContext, vert, frag);
this.camera = camera ?? throw new ArgumentNullException("camera");
if (BatchVertexSize < 1) throw new ArgumentException("Batch vertex size cannot be less than 1.");
indices = new uint[BatchVertexSize];
lengths = new int[BatchVertexSize];
indiceOffsets = new int[BatchVertexSize];
data = new float[BatchVertexSize * VERTEX_LENGTH];
verticeOffsets = new int[BatchVertexSize];
vertexBuffers = new VertexArrayBuffers(GLContext);
VertexAttributeDefinition[] definitions = new VertexAttributeDefinition[3];
definitions[0] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, "aPosition"), 3);
definitions[1] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, "aColor"), 4);
definitions[2] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, "aTexCoord"), 2);
vertexBuffers.defineVertexAttributes(definitions: definitions);
renderProgram.Use();
modelULoc = GLContext.GetUniformLocation(renderProgram.Handle, "models");
viewULoc = GLContext.GetUniformLocation(renderProgram.Handle, "view");
projULoc = GLContext.GetUniformLocation(renderProgram.Handle, "projection");
texturedULoc = GLContext.GetUniformLocation(renderProgram.Handle, "textured");
singleChanneledULoc = GLContext.GetUniformLocation(renderProgram.Handle, "singleChanneled");
flippedULoc = GLContext.GetUniformLocation(renderProgram.Handle, "flipped");
GLContext.UniformMatrix4fv(projULoc, 1, false, camera.ProjectionMatrix.ToColumnMajorArray());
}
/// <summary>
/// Instantiates the mesh batch with a custom shader program.
///
/// For this shader to work with this mesh batch, it must have at least defined the required attributes and uniforms.
/// </summary>
/// <param name="aPosition">The position vertex attribute's name. A vec3. To be used to detect the index of the attribute in the shader program.</param>
/// <param name="aColor">The color vertex attribute's name. A vec4. To be used to detect the index of the attribute in the shader program.</param>
/// <param name="aTexCoord">The texture coordinate vertex attribute's name. A vec2. To be used to detect the index of the attribute in the shader program.</param>
/// <param name="uModels">The models 4x4 matrix's uniform name. To be used to detect the index of the uniform in the shader program.</param>
/// <param name="uView">The view 4x4 matrix's uniform name. To be used to detect the index of the uniform in the shader program.</param>
/// <param name="uProjection">The projection 4x4 matrix's uniform name. To be used to detect the index of the uniform in the shader program.</param>
/// <param name="uTextured">The name of a boolean uniform dictating whether or not the batch of meshes have an associated texture.</param>
/// <param name="uSingleChanneled">The name of a boolean uniform dictating whether or not the current texture is single channelled and only uses red.</param>
/// <param name="uFlipped">The name of a boolean uniform dictating whether or not the current texture is to have it's coordinates flipped.</param>
/// <param name="camera">The camera to use to provide the matrices.</param>
/// <param name="renderProgram">The render program to use for the actual rendering.</param>
/// <param name="BatchVertexSize">The vertex buffer size. Defaults to 1024, and is multiplied by 9 for the individual values of each vertex.</param>
public MeshBatchRenderer(string aPosition, string aColor, string aTexCoord, string uModels, string uView, string uProjection, string uTextured, string uSingleChanneled, string uFlipped, Camera camera, RenderProgram renderProgram, uint BatchVertexSize = 1024) {
this.renderProgram = renderProgram ?? throw new ArgumentNullException("renderProgram");
this.camera = camera ?? throw new ArgumentNullException("camera");
if (BatchVertexSize < 1) throw new ArgumentException("Batch vertex size cannot be less than 1.");
this.GLContext = renderProgram.GLContext;
indices = new uint[BatchVertexSize];
lengths = new int[BatchVertexSize];
indiceOffsets = new int[BatchVertexSize];
data = new float[BatchVertexSize * VERTEX_LENGTH];
verticeOffsets = new int[BatchVertexSize];
vertexBuffers = new VertexArrayBuffers(GLContext);
VertexAttributeDefinition[] definitions = new VertexAttributeDefinition[3];
definitions[0] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, aPosition), 3);
definitions[1] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, aColor), 4);
definitions[2] = new VertexAttributeDefinition((uint)GLContext.GetAttribLocation(renderProgram.Handle, aTexCoord), 2);
renderProgram.Use();
modelULoc = GLContext.GetUniformLocation(renderProgram.Handle, uModels);
viewULoc = GLContext.GetUniformLocation(renderProgram.Handle, uView);
projULoc = GLContext.GetUniformLocation(renderProgram.Handle, uProjection);
texturedULoc = GLContext.GetUniformLocation(renderProgram.Handle, uTextured);
singleChanneledULoc = GLContext.GetUniformLocation(renderProgram.Handle, uSingleChanneled);
flippedULoc = GLContext.GetUniformLocation(renderProgram.Handle, uFlipped);
GLContext.UniformMatrix4fv(projULoc, 1, false, camera.ProjectionMatrix.ToColumnMajorArray());
}
/// <summary>
/// Begins a mesh batch for rendering.
/// </summary>
/// <param name="modelsMatrix">The models matrix.</param>
/// <param name="delta">The time elapsed since the last update.</param>
public virtual void Begin(Matrix4x4 modelsMatrix) {
if (Batching) throw new InvalidOperationException("This batch is already started.");
this.Batching = true;
this.modelsMatrix = modelsMatrix;
}
/// <summary>
/// Draws the given mesh.
/// </summary>
/// <param name="mesh">Draws the given mesh.</param>
public virtual void Draw(IMesh mesh) {
if (!Batching) throw new InvalidOperationException("This batch has not been begun.");
if (mesh.Texture?.Handle != this.texture?.Handle) {
Flush();
this.texture = mesh.Texture;
GLContext.Uniform1i(texturedULoc, texture == null ? 0 : 1);
if (texture != null) {
GLContext.Uniform1i(flippedULoc, texture.Flipped ? 1 : 0);
GLContext.Uniform1i(singleChanneledULoc, texture.SingleChanneled ? 1 : 0);
}
}
ValueTuple<Vector3, Vector2>[] vertices = mesh.Vertices;
uint[] indices = mesh.Elements;
if (vertices.Length * VERTEX_LENGTH + dataIndex >= data.Length || indices.Length + indicesIndex >= this.indices.Length)
Flush();
for (int i = 0; i < vertices.Length; i++) {
data[dataIndex] = vertices[i].Item1.X;
dataIndex++;
data[dataIndex] = vertices[i].Item1.Y;
dataIndex++;
data[dataIndex] = vertices[i].Item1.Z;
dataIndex++;
data[dataIndex] = mesh.Color.RedAsFloat();
dataIndex++;
data[dataIndex] = mesh.Color.GreenAsFloat();
dataIndex++;
data[dataIndex] = mesh.Color.BlueAsFloat();
dataIndex++;
data[dataIndex] = mesh.Color.AlphaAsFloat();
dataIndex++;
data[dataIndex] = vertices[i].Item2.X;
dataIndex++;
data[dataIndex] = vertices[i].Item2.Y;
dataIndex++;
}
int elementsCount = indices.Length;
Array.Copy(indices, 0, this.indices, indicesIndex, elementsCount);
indiceOffsets[offsetIndex] = indicesIndex * sizeof(uint);
indicesIndex += elementsCount;
if (offsetIndex + 1 < verticeOffsets.Length) {
verticeOffsets[offsetIndex + 1] = verticeOffsets[offsetIndex] + vertices.Length;
}
lengths[offsetIndex] = elementsCount;
offsetIndex++;
}
public virtual void End() {
if (!Batching) throw new InvalidOperationException("This batch was never started.");
Flush();
this.Batching = false;
}
public virtual void Flush() {
if (!Batching) throw new InvalidOperationException("This batch has not been begun.");
if (offsetIndex == 0) return;
if (texture != null) {
GLContext.BindTexture(TextureTarget.Texture2D, texture.Handle);
}
renderProgram.Use();
vertexBuffers.BufferVertices(data, true);
vertexBuffers.BufferIndices(indices, true);
GLContext.UniformMatrix4fv(modelULoc, 1, false, modelsMatrix.ToColumnMajorArray());
if (camera.ViewChanged) GLContext.UniformMatrix4fv(viewULoc, 1, false, camera.ViewMatrix.ToColumnMajorArray());
if (camera.ProjectionChanged) GLContext.UniformMatrix4fv(projULoc, 1, false, camera.ProjectionMatrix.ToColumnMajorArray());
if (Debug) {
GLContext.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Line);
} else {
GLContext.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);
}
GLContext.MultiDrawElementsBaseVertex(PrimitiveType.Triangles, lengths, DrawElementsType.UnsignedInt, indiceOffsets, offsetIndex, verticeOffsets);
dataIndex = 0;
indicesIndex = 0;
offsetIndex = 0;
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
}
if (disposed) return;
if (Batching) End();
disposed = true;
vertexBuffers.Dispose();
renderProgram.Dispose();
data = null;
indices = null;
lengths = null;
verticeOffsets = null;
}
}
~MeshBatchRenderer()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Text;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Render.Shaders;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.Graphics.Render.Programs
{
public class RenderProgram : IDisposable
{
public GLContext GLContext {get; private set; }
private bool disposed;
public uint Handle { get; private set; }
public RenderProgram(GLContext context = null, params IShadeable[] shaders) {
this.GLContext = context ?? WindowContextsManager.CurrentGL;
Handle = GLContext.CreateProgram();
foreach (IShadeable shader in shaders)
{
if (this.GLContext != shader.GLContext) throw new FrameworkUsageException("OpenGL contexts between attached shaders and program must match.");
GLContext.AttachShader(Handle, shader.Handle);
}
GLContext.LinkProgram(Handle);
int status;
GLContext.GetProgramiv(Handle, ProgramPropertyARB.LinkStatus, out status);
if ((OpenGL.Boolean)status == OpenGL.Boolean.False) {
int logLength;
GLContext.GetProgramiv(Handle, ProgramPropertyARB.InfoLogLength, out logLength);
byte[] logData = new byte[logLength];
int byteCount;
GLContext.GetProgramInfoLog(Handle, logLength, out byteCount, logData);
throw new FrameworkUsageException(Encoding.UTF8.GetString(logData));
}
foreach (IShadeable shader in shaders)
{
shader.Dispose();
}
}
public void Use() {
GLContext.UseProgram(Handle);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
}
GLContext.DeleteProgram(Handle);
disposed = true;
}
}
~RenderProgram()
{
Dispose(false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,180 @@
using System;
using System.Drawing;
using System.Numerics;
using SlatedGameToolkit.Framework.Graphics.Textures;
namespace SlatedGameToolkit.Framework.Graphics.Render
{
public struct RectangleMesh : IMesh
{
private Matrix4x4 matRot;
private bool changed;
private RectangleF rectangle;
private Vector3 rotation;
private RectangleF textureBounds;
private ValueTuple<Vector3, Vector2>[] vertices;
private uint[] indices;
public ValueTuple<Vector3, Vector2>[] Vertices
{
get {
if (changed) CalculateVertices();
return vertices;
}
}
public float X {
get
{
return rectangle.X;
}
set
{
changed = true;
rectangle.X = value;
}
}
public float Y {
get
{
return rectangle.Y;
}
set
{
changed = true;
rectangle.Y = value;
}
}
public float Width
{
get
{
return rectangle.Width;
}
set
{
changed = true;
rectangle.Width = value;
}
}
public float Height
{
get
{
return rectangle.Height;
}
set
{
changed = true;
rectangle.Height = value;
}
}
public RectangleF Bounds {
get {
return new RectangleF(X, Y, Width, Height);
}
set {
X = value.X;
Y = value.Y;
Width = value.Width;
Height = value.Height;
}
}
public RectangleF TextureBounds
{
get
{
return textureBounds;
}
set
{
this.vertices[0].Item2.X = value.X;
this.vertices[0].Item2.Y = value.Y;
this.vertices[1].Item2.X = value.X + value.Width;
this.vertices[1].Item2.Y = value.Y;
this.vertices[2].Item2.X = value.X + value.Width;
this.vertices[2].Item2.Y = value.Y + value.Height;
this.vertices[3].Item2.X = value.X;
this.vertices[3].Item2.Y = value.Y + value.Height;
this.textureBounds = value;
}
}
public uint[] Elements { get {return indices; } }
public Vector3 Rotation
{
get
{
return rotation;
}
set
{
changed = true;
rotation = value;
matRot = Matrix4x4.CreateFromYawPitchRoll(value.X, value.Y, value.Z);
}
}
public ITexture Texture { get; set; }
public Color Color { get; set; }
public RectangleMesh(RectangleF meshBounds, ITexture texture, Color color) {
this.changed = true;
this.rotation = Vector3.Zero;
this.rectangle = RectangleF.Empty;
this.Texture = texture;
this.Color = color;
this.indices = new uint[] {0, 1, 3, 1, 2, 3};
vertices = new ValueTuple<Vector3, Vector2>[4];
this.textureBounds = new RectangleF(0, 0, 1, 1);
this.matRot = Matrix4x4.Identity;
this.TextureBounds = textureBounds;
this.Bounds = meshBounds;
}
public RectangleMesh(ITexture texture, Color color) : this(new RectangleF(0,0,0,0), texture, color)
{
}
public RectangleMesh(RectangleF meshBounds, RectangleF textureRegion, ITexture texture, Color color) : this(meshBounds, texture, color) {
this.TextureBounds = textureRegion;
}
private void CalculateVertices() {
if (!changed) return;
Vector3[] baseVerts = new Vector3[4];
baseVerts[0] = new Vector3(this.rectangle.X, this.rectangle.Y, 0);
baseVerts[1] = new Vector3(this.rectangle.Right, this.rectangle.Y, 0);
baseVerts[2] = new Vector3(baseVerts[1].X, this.rectangle.Bottom, 0);
baseVerts[3] = new Vector3(this.rectangle.X, baseVerts[2].Y, 0);
for (int i = 0; i < vertices.Length; i++)
{
vertices[i].Item1 = Vector3.Transform(baseVerts[i], matRot);
}
changed = false;
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Text;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.Graphics.Render.Shaders
{
public class FragmentShader : IShadeable
{
public GLContext GLContext {get; private set; }
private bool disposed;
public uint Handle { get; private set; }
public FragmentShader(string program, GLContext context = null) {
this.GLContext = context ?? WindowContextsManager.CurrentGL;
Handle = GLContext.CreateShader(ShaderType.FragmentShader);
GLContext.ShaderSource(Handle, 1, new string[] {program}, null);
GLContext.CompileShader(Handle);
int status;
GLContext.GetShaderiv(Handle, ShaderParameterName.CompileStatus, out status);
if ((OpenGL.Boolean)status == OpenGL.Boolean.False) {
int logLength;
GLContext.GetShaderiv(Handle, ShaderParameterName.InfoLogLength, out logLength);
byte[] logData = new byte[logLength];
int byteCount;
GLContext.GetShaderInfoLog(Handle, logLength, out byteCount, logData);
throw new FrameworkUsageException(string.Format("Error compiling shader: {0}", Encoding.UTF8.GetString(logData)));
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
}
GLContext.DeleteShader(Handle);
disposed = true;
}
}
~FragmentShader()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,11 @@
using System;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
namespace SlatedGameToolkit.Framework.Graphics.Render.Shaders
{
public interface IShadeable : IDisposable
{
GLContext GLContext {get;}
uint Handle { get; }
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Text;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.Graphics.Render.Shaders
{
public class VertexShader : IShadeable
{
public GLContext GLContext {get; private set;}
private bool disposed;
public uint Handle { get; private set; }
public VertexShader(string program, GLContext context = null) {
this.GLContext = context ?? WindowContextsManager.CurrentGL;
Handle = GLContext.CreateShader(ShaderType.VertexShader);
GLContext.ShaderSource(Handle, 1, new string[] {program}, null);
GLContext.CompileShader(Handle);
int status;
GLContext.GetShaderiv(Handle, ShaderParameterName.CompileStatus, out status);
if ((OpenGL.Boolean)status == OpenGL.Boolean.False) {
int logLength;
GLContext.GetShaderiv(Handle, ShaderParameterName.InfoLogLength, out logLength);
byte[] logData = new byte[logLength];
int byteCount;
GLContext.GetShaderInfoLog(Handle, logLength, out byteCount, logData);
throw new FrameworkUsageException(string.Format("Error compiling shader: {0}", Encoding.UTF8.GetString(logData)));
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
}
GLContext.DeleteShader(Handle);
disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~VertexShader() {
Dispose(false);
}
}
}

View File

@ -0,0 +1,143 @@
using System;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.Graphics.Render
{
/// <summary>
/// A set of two buffers, one for the vertices, and one for the indices. Also defines an vertex array that defines the attributes of the buffers.
/// </summary>
public class VertexArrayBuffers : IDisposable {
private uint vertexBufferLength, indexBufferLength;
private GLContext glContext;
private bool disposed;
private uint vertexBufferHandle, vertexArrayHandle, indexBufferHandle;
/// <summary>
/// Create a vertex array, a elements buffer, and an vertice array buffer.
///
/// Automatically checks if resizing the buffers are nessecary.
/// </summary>
/// <param name="context"></param>
public VertexArrayBuffers(GLContext context) {
this.glContext = context ?? WindowContextsManager.CurrentGL;
uint[] vertexArrays = new uint[1];
glContext.GenVertexArrays(1, vertexArrays);
vertexArrayHandle = vertexArrays[0];
uint[] vertexBuffers = new uint[2];
glContext.GenBuffers(2, vertexBuffers);
vertexBufferHandle = vertexBuffers[0];
indexBufferHandle = vertexBuffers[1];
}
public void Use() {
if (disposed) throw new ObjectDisposedException("VertexArrayBuffers");
glContext.BindVertexArray(vertexArrayHandle);
glContext.BindBuffer(BufferTargetARB.ElementArrayBuffer, indexBufferHandle);
glContext.BindBuffer(BufferTargetARB.ArrayBuffer, vertexBufferHandle);
}
public unsafe void BufferVertices(float[] data, bool dynamic) {
Use();
uint requiredLength = (uint) (sizeof(float) * data.Length);
fixed (void* pointer = &data[0]) {
if (requiredLength > vertexBufferLength) {
glContext.BufferData(BufferTargetARB.ArrayBuffer, requiredLength, new IntPtr(pointer), dynamic ? OpenGL.BufferUsageARB.DynamicDraw : OpenGL.BufferUsageARB.StaticDraw);
vertexBufferLength = requiredLength;
} else {
glContext.BufferSubData(BufferTargetARB.ArrayBuffer, IntPtr.Zero, new UIntPtr(requiredLength), new IntPtr(pointer));
}
}
}
public unsafe void BufferIndices(uint[] data, bool dynamic) {
Use();
uint requiredLength = (uint) (sizeof(uint) * data.Length);
fixed (void* pointer = &data[0]) {
if (requiredLength > indexBufferLength) {
glContext.BufferData(OpenGL.BufferTargetARB.ElementArrayBuffer, requiredLength, new IntPtr(pointer), dynamic ? OpenGL.BufferUsageARB.DynamicDraw : OpenGL.BufferUsageARB.StaticDraw);
indexBufferLength = requiredLength;
} else {
glContext.BufferSubData(BufferTargetARB.ElementArrayBuffer, IntPtr.Zero, new UIntPtr(requiredLength), new IntPtr(pointer));
}
}
}
/// <summary>
/// Defines the vertex attributes in an OpenGL vertex array object.
/// Sends as floats.
/// </summary>
/// <param name="enableAttributes">Whether or not to automatically enable the attributes that are being defined. Defaults to true.</param>
/// <param name="definitions">The definitions for the vertex array object.</param>
public void defineVertexAttributes(bool enableAttributes = true, params VertexAttributeDefinition[] definitions) {
Use();
int offset = 0;
uint stride = 0;
foreach (VertexAttributeDefinition definition in definitions)
{
stride += (uint)definition.size * sizeof(float);
}
foreach (VertexAttributeDefinition definition in definitions)
{
glContext.VertexAttribPointer(definition.attributeIndex, definition.size, OpenGL.VertexAttribPointerType.Float, false, stride, new IntPtr(offset));
offset += definition.size * sizeof(float);
if (enableAttributes) glContext.EnableVertexAttribArray(definition.attributeIndex);
}
}
/// <summary>
/// Enaables the vertex array object's definitions at the given attribute indices.
/// </summary>
/// <param name="attributeIndices">The attribute indices to be enabled.</param>
public void EnableAttributes(params uint[] attributeIndices) {
Use();
foreach (uint attrib in attributeIndices)
{
glContext.EnableVertexAttribArray(attrib);
}
}
/// <summary>
/// Disables the vertex array object's definitions at the given attribute indices.
/// </summary>
/// <param name="attributeIndices">The attribute indices to be disabled.</param>
public void DisableAttributes(params uint[] attributeIndices) {
Use();
foreach (uint attrib in attributeIndices)
{
glContext.DisableVertexAttribArray(attrib);
}
}
protected virtual void Dispose(bool disposing)
{
if (this.disposed) return;
if (disposing) {
}
this.disposed = true;
glContext.DeleteVertexArrays(1, new uint[] {vertexArrayHandle});
glContext.DeleteBuffers(2, new uint[] {indexBufferHandle, vertexBufferHandle});
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
~VertexArrayBuffers() {
Dispose(false);
}
}
public struct VertexAttributeDefinition {
public uint attributeIndex;
public int size;
public VertexAttributeDefinition(uint attributeIndex, int size) {
this.attributeIndex = attributeIndex;
this.size = size;
}
}
}

View File

@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Numerics;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Render;
using SlatedGameToolkit.Framework.Graphics.Textures;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Utilities;
using SlatedGameToolkit.Framework.Utilities.Collections;
using StbTrueTypeSharp;
namespace SlatedGameToolkit.Framework.Graphics.Text
{
public class BitmapFont : IDisposable
{
private GLContext context;
private LRUCache<char, int> glyphIndices;
private LRUCache<char, CharacterMetrics> metricsCache;
private LRUCache<(int, int), int> kerningCache;
FontTexture[] textures;
StbTrueType.stbtt_fontinfo info;
private int spaceAdvance;
private float scale;
public float PixelHeight
{
get
{
return scale * (ascent - descent);
}
set
{
this.scale = value / (ascent - descent);
}
}
private Dictionary<(char, float), int> glyphTexLocations = new Dictionary<(char, float), int>();
private int ascent, descent, lineGap;
public float PixelsPerUnitHeight { get; set; }
public float PixelsPerUnitWidth { get; set; }
private int drawingTo;
private bool disposed;
public unsafe BitmapFont(byte[] data, GLContext glContext = null, int cacheSize = 1024, int textures = 2, uint textureSizes = 512) {
info = new StbTrueType.stbtt_fontinfo();
fixed(byte* dataPtr = &data[0]) {
int offset = StbTrueType.stbtt_GetFontOffsetForIndex(dataPtr, 0);
if (offset != -1) {
StbTrueType.stbtt_InitFont(info, dataPtr, offset);
} else {
throw new FrameworkUsageException("Could not load ttf file.");
}
}
this.glyphIndices = new LRUCache<char, int>(cacheSize);
this.metricsCache = new LRUCache<char, CharacterMetrics>(cacheSize);
this.kerningCache = new LRUCache<(int, int), int>(cacheSize);
this.textures = new FontTexture[textures];
this.context = glContext ?? WindowContextsManager.CurrentGL;
for (int i = 0; i < this.textures.Length; i++) {
this.textures[i] = new FontTexture(info, context, glyphIndices, textureSizes);
}
int spaceAdvance, leftSideBearing;
StbTrueType.stbtt_GetCodepointHMetrics(info, ' ', &spaceAdvance, &leftSideBearing);
this.spaceAdvance = spaceAdvance;
int ascent, descent, lineGap;
StbTrueType.stbtt_GetFontVMetrics(info, &ascent, &descent, &lineGap);
this.ascent = ascent;
this.descent = descent;
this.lineGap = lineGap;
int vWidth, vHeight, vX, vY;
context.GetViewport(out vX, out vY, out vWidth, out vHeight);
this.PixelsPerUnitWidth = vWidth;
this.PixelsPerUnitHeight = vHeight;
PixelHeight = 64;
}
public BitmapFont(string path, GLContext glContext = null, int cacheSize = 1024, int textures = 2, uint textureSizes = 512) : this(File.ReadAllBytes(path), glContext, cacheSize, textures, textureSizes) {
}
public ITexture GetTextureBacking(int index) {
return textures[index];
}
/// <summary>
/// Prepares the characters for rendering. All characters to be rendered in one frame should be loaded using this method.
/// This will also ensure that the bitmap font is big enough.
/// Calling this multiple times per frame is not good for performance as multiple texture uploads are occurring.
/// </summary>
/// <param name="characters">An array of characters to load. Won't load if already loaded.</param>
public void PrepareCharacterGroup(params char[] characters) {
int textureChanges = 0;
foreach (char c in characters)
{
if (c == ' ') continue;
if (!glyphTexLocations.ContainsKey((c, scale)) || !textures[glyphTexLocations[(c, scale)]].ContainsChar(c, scale)) {
glyphTexLocations.Remove((c, scale));
if (!textures[drawingTo].Upload(scale, c)) {
drawingTo++;
if (drawingTo >= textures.Length) drawingTo = 0;
FontTexture fontTexture = textures[drawingTo];
fontTexture.Clear();
fontTexture.Upload(scale, c);
textureChanges++;
if (textureChanges > textures.Length) throw new FrameworkUsageException(string.Format("Character group \"{0}\" takes up too much texture space! Consider increasing decreasing font size, or increasing texture lengths, or number of backing textures.", new string(characters)));
}
glyphTexLocations.Add((c, scale), drawingTo);
int glyphIndex = glyphIndices.ComputeIfNonExistent(c, (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
metricsCache.ComputeIfNonExistent(c, (p) => new CharacterMetrics(glyphIndex, info));
}
}
}
public unsafe void WriteLine(MeshBatchRenderer batch, float x, float y, string text, Color color) {
float currentPoint = x;
float baseLine = y;
char[] chars = text.ToCharArray();
for (int i = 0; i < chars.Length; i++) {
char c = chars[i];
if (c == ' ') {
currentPoint += (scale * spaceAdvance) / PixelsPerUnitWidth;
} else if (c == '\n') {
currentPoint = x;
baseLine -= ((ascent - descent + lineGap) * scale) / PixelsPerUnitHeight;
} else {
int glyphIndex = glyphIndices.ComputeIfNonExistent(c, (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
CharacterMetrics metrics = metricsCache.ComputeIfNonExistent(c, (p) => new CharacterMetrics(glyphIndex, info));
//Check if glyph is loaded, if not, throw exception.
if (!glyphTexLocations.ContainsKey((c, scale)) || !textures[glyphTexLocations[(c, scale)]].ContainsChar(c, scale)) {
throw new FrameworkUsageException(string.Format("Character \'{0}\' at pixel height {1} was not prepared and is missing!", c, PixelHeight));
}
FontTexture texture = textures[glyphTexLocations[(c, scale)]];
RectangleF texBounds = texture.GetGlyphBoundaries(c, scale);
RectangleF transformedTexBounds = texBounds.MultiplyBy(1f/texture.Length);
transformedTexBounds.Y += transformedTexBounds.Height;
transformedTexBounds.Height *= -1;
RectangleF glyphBounds = new RectangleF(currentPoint, baseLine, texBounds.Width / PixelsPerUnitWidth, texBounds.Height / PixelsPerUnitHeight);
glyphBounds.X += (metrics.leftSideBearing * scale) / PixelsPerUnitWidth;
glyphBounds.Y += (metrics.vertOffset * scale) / PixelsPerUnitHeight;
batch.Draw(new RectangleMesh(glyphBounds, transformedTexBounds, texture, color));
currentPoint += (metrics.advanceWidth * scale) / PixelsPerUnitWidth;
if (i + 1 < chars.Length) {
int nextGlyphIndex = glyphIndices.ComputeIfNonExistent(chars[i + 1], (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
currentPoint += (kerningCache.ComputeIfNonExistent((glyphIndex, nextGlyphIndex), (gIndices) => StbTrueType.stbtt_GetGlyphKernAdvance(info, gIndices.Item1, gIndices.Item2)) * scale) / PixelsPerUnitWidth;
}
}
}
}
private struct CharacterMetrics {
public readonly int vertOffset;
public readonly int leftSideBearing;
public readonly int advanceWidth;
public unsafe CharacterMetrics(int glyphIndex, StbTrueType.stbtt_fontinfo info) {
int x0, y0, leftSideBearing, advanceWidth;
StbTrueType.stbtt_GetGlyphHMetrics(info, glyphIndex, &advanceWidth, &leftSideBearing);
StbTrueType.stbtt_GetGlyphBox(info, glyphIndex, &x0, &y0, null, null);
this.vertOffset = y0;
this.leftSideBearing = leftSideBearing;
this.advanceWidth = advanceWidth;
}
}
private class FontTexture : ITexture {
private LRUCache<char, int> glyphIndices;
public uint Length {get; private set;}
StbTrueType.stbtt_fontinfo info;
GLContext context;
private bool disposed;
public bool SingleChanneled => true;
public uint Handle {get; private set;}
public uint Width => Length;
public uint Height => Length;
public bool Flipped => false;
private Dictionary<(char, float), Rectangle> glyphBounds = new Dictionary<(char, float), Rectangle>();
private int row, column, previousRow;
public FontTexture(StbTrueType.stbtt_fontinfo info, GLContext context, LRUCache<char, int> glyphIndices, uint length) {
this.context = context;
this.Length = length;
this.info = info;
this.glyphIndices = glyphIndices;
context.DetectGLError();
uint[] handles = new uint[1];
context.GenTextures(handles.Length, handles);
Handle = handles[0];
context.BindTexture(TextureTarget.Texture2D, Handle);
context.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Red, length, length, 0, PixelFormat.Red, PixelType.UnsignedByte, IntPtr.Zero);
context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
context.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
}
public bool ContainsChar(char c, float scale) {
return glyphBounds.ContainsKey((c, scale));
}
public Rectangle GetGlyphBoundaries(char c, float scale) {
return glyphBounds[(c, scale)];
}
public unsafe bool Upload(float scale, char c) {
int x0, y0, x1, y1;
int glyphIndex = glyphIndices.ComputeIfNonExistent(c, (p) => StbTrueType.stbtt_FindGlyphIndex(info, p));
StbTrueType.stbtt_GetGlyphBitmapBox(info, glyphIndex, scale, scale, &x0, &y0, &x1, &y1);
int width = x1 - x0, height = y1 - y0;
if (width == 0 || height == 0) throw new InternalFrameworkException("Glyph width or height was 0. Character was: " + c);
if (column + width >= Length) {
column = 0;
if (row + height + 1 >= Length) {
return false;
} else {
previousRow = row;
}
}
if (row - height <= previousRow) {
row = previousRow + height + 1;
}
column += 1 + width;
context.BindTexture(TextureTarget.Texture2D, Handle);
int alignment;
int[] alignments = new int[1];
context.GetIntegerv(GetPName.UnpackAlignment, alignments);
alignment = alignments[0];
if (alignment != 1) {
context.PixelStorei(PixelStoreParameter.UnpackAlignment, 1);
}
byte[] bitmap = new byte[width * height];
fixed(byte* data = &bitmap[0]) {
StbTrueType.stbtt_MakeGlyphBitmap(info, data, width, height, width, scale, scale, glyphIndex);
context.TexSubImage2D(TextureTarget.Texture2D, 0, column - width, row - height, width, height, PixelFormat.Red, PixelType.UnsignedByte, new IntPtr(data));
}
if (alignment != 1) {
context.PixelStorei(PixelStoreParameter.UnpackAlignment, alignment);
}
glyphBounds.Add((c, scale), new Rectangle(column - width, row - height, width, height));
return true;
}
public bool CanFit(int width, int height) {
if (row + height < Length) {
return true;
} else if (column + width < Length) {
return true;
}
return false;
}
public void Clear() {
glyphBounds.Clear();
row = 0;
column = 0;
previousRow = 0;
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
}
context.DeleteTextures(1, new uint[] {Handle});
disposed = true;
}
}
~FontTexture()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
System.GC.SuppressFinalize(this);
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// TODO: dispose managed state (managed objects)
}
foreach (FontTexture texture in this.textures)
{
texture.Dispose();
}
disposed = true;
}
}
~BitmapFont()
{
Dispose(disposing: false);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@ -0,0 +1,18 @@
using System;
using SlatedGameToolkit.Framework.AssetSystem;
namespace SlatedGameToolkit.Framework.Graphics.Textures
{
public interface ITexture : IAssetUseable, IDisposable
{
/// <summary>
/// Whether or not this model uses a texture that is single chanelled.
/// </summary>
/// <value>true for single channeled.</value>
bool SingleChanneled { get; }
bool Flipped { get; }
uint Handle {get;}
uint Width {get;}
uint Height {get;}
}
}

View File

@ -0,0 +1,82 @@
using System;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Graphics.Render;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.AssetSystem;
using SlatedGameToolkit.Framework.AssetSystem.AssetData;
namespace SlatedGameToolkit.Framework.Graphics.Textures
{
public class Texture : IAssetUseable, ITexture {
public readonly uint width, height;
private bool disposed;
private GLContext glContext;
private uint handle;
public uint Handle {get { return handle; }}
public uint Width => width;
public uint Height => height;
public bool SingleChanneled => false;
public bool Flipped => true;
/// <summary>
/// Creates an OpenGL Texture2D in the given GL Context.
/// </summary>
/// <param name="textureData">The texture data to use to create the Texture2D.</param>
/// <param name="context">The openGL context to associate this with. If null, will use the currently active context. Defaults to null.</param>
public unsafe Texture(TextureData textureData, bool nearest = false, GLContext context = null) {
this.glContext = context ?? WindowContextsManager.CurrentGL;
this.width = (uint)textureData.width;
this.height = (uint)textureData.height;
uint[] handles = new uint[1];
glContext.GenTextures(1, handles);
this.handle = handles[0];
SetNearestFilter(nearest);
fixed(void* p = &textureData.data[0]) {
glContext.PixelStorei(PixelStoreParameter.UnpackAlignment, 4);
glContext.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, (uint)textureData.width, (uint)textureData.height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, new IntPtr(p));
}
glContext.GenerateMipmap(OpenGL.TextureTarget.Texture2D);
}
public void SetNearestFilter(bool nearest = false, bool generateMipMap = false) {
Use();
if (nearest) {
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.NearestMipmapNearest);
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
} else {
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
}
if (!generateMipMap) glContext.GenerateMipmap(OpenGL.TextureTarget.Texture2D);
}
public void Use() {
if (disposed) throw new ObjectDisposedException("Texture");
glContext.BindTexture(OpenGL.TextureTarget.Texture2D, handle);
}
public void Dispose()
{
if (disposed) return;
disposed = true;
glContext.DeleteTextures(1, new uint[] {handle});
GC.SuppressFinalize(this);
}
~Texture() {
Dispose();
}
}
}

View File

@ -0,0 +1,367 @@
using System;
using System.Drawing;
using System.Numerics;
using System.Runtime.InteropServices;
using SDL2;
using SlatedGameToolkit.Framework.Exceptions;
using SlatedGameToolkit.Framework.Graphics;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Logging;
namespace SlatedGameToolkit.Framework.Graphics.Window
{
/// <summary>
/// A delegate to determine the type of interaction with a window.
/// The method should be extremely lightweight as it can be called many times.
/// </summary>
/// <param name="hitPoint">The window coordinates that are being interacted with.</param>
/// <returns>The region type.</returns>
public delegate SDL.SDL_HitTestResult WindowRegionHit(Vector2 hitPoint);
/// <summary>
/// A delegate that represents a function to be called on a window resize.
/// </summary>
/// <param name="width">The new width.</param>
/// <param name="height">The new height.</param>
public delegate void WindowResize(int width, int height);
/// <summary>
/// A delegate that represents a function to be called when a window event occurs.
/// </summary>
public delegate void WindowOperation();
/// <summary>
/// A handle for a window.
/// This object itself is not thread safe.
/// </summary>
public sealed class WindowContext : IDisposable
{
public GLContext Context { get; private set; }
/// <summary>
/// The pointer referencing the SDL window.
/// </summary>
private readonly IntPtr windowHandle;
/// <summary>
/// The pointer referencing the OpenGL context.
/// </summary>
public event WindowOperation focusGainedEvent, focusLostEvent;
/// <summary>
/// Invoked when this window resizes.
/// </summary>
public event WindowResize resizeEvent;
/// <summary>
/// Event for when the window is being interacted with.
/// </summary>
public event WindowRegionHit windowRegionHitEvent;
/// <summary>
/// Whether or not to show this window.
/// </summary>
/// <value>True for showing.</value>
public bool Shown {
set {
if (value) {
SDL.SDL_ShowWindow(windowHandle);
} else {
SDL.SDL_HideWindow(windowHandle);
}
}
get {
return ((SDL.SDL_WindowFlags) Enum.Parse(typeof(SDL.SDL_WindowFlags), SDL.SDL_GetWindowFlags(windowHandle).ToString())).HasFlag(SDL.SDL_WindowFlags.SDL_WINDOW_SHOWN);
}
}
/// <summary>
/// Whether or not to attempt to perform vertical synchronization.
/// Will use adaptive sync if supported.
/// If this window is not the current active window, retrieving and setting requires a context switch.
/// If a context switch was necessary, will automatically switch back to previous once operation is complete.
/// If neither normal VSync or adaptive sync are available, enabling will throw an OptionalSDLException.
/// </summary>
/// <value></value>
public bool VSync {
get {
WindowContext actual = WindowContextsManager.CurrentWindowContext;
bool diff = false;
if (actual != this) {
MakeCurrent();
diff = true;
}
bool vSync = SDL.SDL_GL_GetSwapInterval() != 0;
if (diff) actual.MakeCurrent();
return vSync;
}
set {
WindowContext actual = WindowContextsManager.CurrentWindowContext;
bool diff = false;
if (actual != this) {
MakeCurrent();
diff = true;
}
if (SDL.SDL_GL_SetSwapInterval(value ? -1 : 0) < 0) {
if (SDL.SDL_GL_SetSwapInterval(value ? 1 : 0) < 0) {
throw new OptionalSDLException();
}
}
if (diff) actual.MakeCurrent();
}
}
/// <summary>
/// The title displayed on the window.
/// </summary>
/// <value>A string that represents whats to be displayed as the title of the window.</value>
public string WindowTitle {
set {
SDL.SDL_SetWindowTitle(windowHandle, value);
}
get {
return SDL.SDL_GetWindowTitle(windowHandle);
}
}
/// <summary>
/// Whether or not to display the default window border.
/// </summary>
/// <value>True if displaying borders.</value>
public bool WindowBordered {
set {
SDL.SDL_SetWindowBordered(windowHandle, value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE);
}
get {
int top, bottom, left, right;
int errorCode = SDL.SDL_GetWindowBordersSize(windowHandle, out top, out left, out bottom, out right);
if (errorCode < 0) throw new OptionalSDLException();
return top > 0 || bottom > 0 || left > 0 || right > 0;
}
}
/// <summary>
/// Whether or not this window should be resizeable.
/// </summary>
/// <value>True if resizeable.</value>
public bool WindowResizable {
set {
SDL.SDL_SetWindowResizable(windowHandle, value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE);
}
get {
return ((SDL.SDL_WindowFlags) Enum.Parse(typeof(SDL.SDL_WindowFlags), SDL.SDL_GetWindowFlags(windowHandle).ToString())).HasFlag(SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE);
}
}
/// <summary>
/// The boundaries of the window represent both the bottom left corner relative to the screen,
/// as well as the current width and height of this window.
/// </summary>
/// <value>Rectangle representing the boundaries of the window.</value>
public Rectangle WindowBoundaries {
set {
SDL.SDL_SetWindowPosition(windowHandle, value.X, value.Y);
SDL.SDL_SetWindowSize(windowHandle, value.Width, value.Height);
}
get {
int x, y, width, height;
SDL.SDL_GetWindowSize(windowHandle, out x, out y);
SDL.SDL_GetWindowPosition(windowHandle, out width, out height);
Rectangle rectangle = new Rectangle();
rectangle.X = x;
rectangle.Y = y;
rectangle.Width = width;
rectangle.Height = height;
return rectangle;
}
}
/// <summary>
/// The maximum size the window can be at any given time.
/// </summary>
/// <value>A vector that represents the maximum width and height of the window with the x and y components respectively.</value>
public Vector2 MaximumSize {
set {
SDL.SDL_SetWindowMaximumSize(windowHandle, (int)value.X, (int)value.Y);
}
get {
int width, height;
SDL.SDL_GetWindowMaximumSize(windowHandle, out width, out height);
Vector2 maxSize = new Vector2();
maxSize.X = width;
maxSize.Y = height;
return maxSize;
}
}
/// <summary>
/// The minimum size the window can be at any given time.
/// </summary>
/// <value>A vector that represents the minimum width and height of the window with the x and y components respectively.</value>
public Vector2 MinimumSize {
set {
SDL.SDL_SetWindowMinimumSize(windowHandle, (int)value.X, (int)value.Y);
}
get {
int width, height;
SDL.SDL_GetWindowMinimumSize(windowHandle, out width, out height);
Vector2 maxSize = new Vector2();
maxSize.X = width;
maxSize.Y = height;
return maxSize;
}
}
/// <summary>
/// The opacity of the window itself.
/// This is not supported on all targetted platforms, and therefore, can throw an OptionalSDLException for when this is the case.
/// </summary>
/// <value>A float with bounds of [0, 1] representing the opacity of the window.</value>
public float Opacity {
set {
int errorCode = SDL.SDL_SetWindowOpacity(windowHandle, value);
if (errorCode < 0) throw new OptionalSDLException();
}
get {
float value;
int errorCode = SDL.SDL_GetWindowOpacity(windowHandle, out value);
if (errorCode < 0) throw new OptionalSDLException();
return value;
}
}
/// <summary>
/// Whether or not this window is grabbing the users input by making sure the mouse stays within this windows boundaries.
/// </summary>
/// <value>True if grabbing.</value>
public bool GrabbingInput {
set {
SDL.SDL_SetWindowGrab(windowHandle, value ? SDL.SDL_bool.SDL_TRUE : SDL.SDL_bool.SDL_FALSE);
}
get {
return SDL.SDL_GetWindowGrab(windowHandle) == SDL.SDL_bool.SDL_TRUE ? true : false;
}
}
internal uint WindowID {
get {
uint id = SDL.SDL_GetWindowID(windowHandle);
if (id == 0) throw new SDLException();
return id;
}
}
/// <summary>
/// Instantiates a window with the given OpenGL context, or a new context.
/// </summary>
public WindowContext(string title, int x = -1, int y = -1, int width = 640, int height = 480, bool specialRegions = false, SDL.SDL_WindowFlags options = default(SDL.SDL_WindowFlags)) {
Logger.Log(String.Format("Starting openGL window with title \"{0}\"", title), LogLevel.DEBUG);
windowHandle = SDL.SDL_CreateWindow(title, x < 0 ? SDL.SDL_WINDOWPOS_CENTERED : x, y < 0 ? SDL.SDL_WINDOWPOS_CENTERED : y, width, height, SDL.SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL.SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL.SDL_WindowFlags.SDL_WINDOW_MOUSE_FOCUS | options);
if (windowHandle == null) {
throw new SDLException();
}
this.Context = new GLContext(windowHandle);
if (specialRegions) {
if (SDL.SDL_SetWindowHitTest(windowHandle, SpecialRegionHit, IntPtr.Zero) < 0) {
throw new OptionalSDLException();
}
}
WindowContextsManager.RegisterWindow(this);
MakeCurrent();
Vector2 drawable = GetDrawableDimensions();
this.Context.Viewport(0, 0, (int)drawable.X, (int)drawable.Y);
this.Context.Enable(EnableCap.Blend);
Context.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
}
private SDL.SDL_HitTestResult SpecialRegionHit(IntPtr window, IntPtr hitPtr, IntPtr data) {
SDL.SDL_Point SDLPoint = Marshal.PtrToStructure<SDL.SDL_Point>(hitPtr);
Vector2 point = new Vector2(SDLPoint.x, SDLPoint.y);
SDL.SDL_HitTestResult region = windowRegionHitEvent.Invoke(point);
return region;
}
internal void SwapBuffer() {
SDL.SDL_GL_SwapWindow(windowHandle);
}
internal void OnResize(int width, int height) {
resizeEvent?.Invoke(width, height);
}
internal void OnFocusLost() {
focusLostEvent?.Invoke();
}
internal void OnFocusGained() {
focusGainedEvent?.Invoke();
}
/// <summary>
/// Gets the current drawable area of the window.
/// </summary>
/// <returns>A vector with x component representing the width and y component representing the height of the drawable area.</returns>
public Vector2 GetDrawableDimensions() {
int width, height;
SDL.SDL_GL_GetDrawableSize(windowHandle, out width, out height);
return new Vector2(width, height);
}
/// <summary>
/// Makes this window context the active one.
/// There can only be one window context active at any given time.
/// </summary>
public void MakeCurrent() {
if (WindowContextsManager.CurrentWindowContext != this) {
SDL.SDL_GL_MakeCurrent(windowHandle, Context.Handle);
WindowContextsManager.current = this;
}
}
/// <summary>
/// Attempts to raise the window to the top.
/// </summary>
public void RaiseToTop() {
SDL.SDL_RestoreWindow(windowHandle);
SDL.SDL_RaiseWindow(windowHandle);
}
/// <summary>
/// Gets the index of the display that this window resides within.
/// </summary>
/// <returns>An integer representing the display this window resides within.</returns>
public int GetDisplayIndex() {
int index = SDL.SDL_GetWindowDisplayIndex(windowHandle);
if (index < 0) throw new SDLException();
return index;
}
/// <summary>
/// Disposes of this window and it's associated handles.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Dispose(bool disposing) {
if (disposing)
{
}
WindowContextsManager.DeregisterWindow(this);
SDL.SDL_DestroyWindow(windowHandle);
Context.Dispose();
}
~WindowContext() {
Dispose(false);
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Logging;
namespace SlatedGameToolkit.Framework.Graphics.Window
{
public static class WindowContextsManager {
internal static WindowContext current;
/// <summary>
/// The current window context.
/// </summary>
/// <value>The currently active window context.</value>
public static WindowContext CurrentWindowContext {
get {
return current;
}
set {
value.MakeCurrent();
}
}
private static Dictionary<uint, WindowContext> existingWindows = new Dictionary<uint, WindowContext>();
/// <summary>
/// The currently loaded windows.
/// </summary>
/// <typeparam name="uint">The ID of the window.</typeparam>
/// <typeparam name="WindowContext">The window context.</typeparam>
/// <returns>A readonly dictionary of all the window contexts that were created.</returns>
public static ReadOnlyDictionary<uint, WindowContext> LoadedWindows { get; private set; } = new ReadOnlyDictionary<uint, WindowContext>(existingWindows);
/// <summary>
/// The OpenGL context of the active window.
/// Equivalent to retrieving the current window context, and then using the context from that instance.
/// </summary>
/// <value>The currently active window's OpenGL context.</value>
public static GLContext CurrentGL {
get {
return CurrentWindowContext.Context;
}
}
internal static void RegisterWindow(WindowContext windowHandle) {
Logger.Log("Registering window: " + windowHandle.WindowID, LogLevel.DEBUG);
existingWindows.Add(windowHandle.WindowID, windowHandle);
}
internal static void DeregisterWindow(WindowContext windowHandle) {
Logger.Log("Deregistering window: " + windowHandle.WindowID, LogLevel.DEBUG);
existingWindows.Remove(windowHandle.WindowID);
}
public static WindowContext ContextFromWindowID(uint ID) {
return existingWindows[ID];
}
/// <summary>
/// Disposes all the created window contexts.
/// This includes their OpenGL contexts.
/// </summary>
public static void DisposeAllWindowContexts() {
foreach (WindowContext context in LoadedWindows.Values)
{
context.Dispose();
}
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using SDL2;
namespace SlatedGameToolkit.Framework.Input.Devices
{
public delegate void keyboardUpdate(SDL.SDL_Keycode keys, bool pressed);
public static class Keyboard {
private static HashSet<SDL.SDL_Keycode> pressedKeys = new HashSet<SDL.SDL_Keycode>();
/// <summary>
/// Whenever a keyboard update occurs.
/// </summary>
public static event keyboardUpdate keyboardUpdateEvent;
public static bool IsKeyPressed(SDL.SDL_Keycode key) {
return pressedKeys.Contains(key);
}
internal static void OnKeyPressed(SDL.SDL_Keycode key) {
pressedKeys.Add(key);
keyboardUpdateEvent?.Invoke(key, true);
}
internal static void OnKeyReleased(SDL.SDL_Keycode key) {
pressedKeys.Remove(key);
keyboardUpdateEvent?.Invoke(key, false);
}
}
}

View File

@ -0,0 +1,41 @@
namespace SlatedGameToolkit.Framework.Input.Devices
{
public delegate void MouseUpdate(bool leftDown, bool rightDown, bool middle, int x, int y, int scrollX, int scrollY);
public static class Mouse
{
public static event MouseUpdate mouseUpdateEvent;
public static bool LeftButtonPressed { get; private set; }
public static bool RightButtonPressed { get; private set; }
public static bool MiddleButtonPressed { get; private set; }
public static int ScrollChangeX { get; private set; }
public static int ScrollChangeY { get; private set; }
public static int X { get; private set; }
public static int Y{ get; private set; }
internal static void OnMouseMoved(int x, int y) {
X = x;
Y = y;
}
internal static void OnLeftChange(bool down) {
LeftButtonPressed = down;
mouseUpdateEvent?.Invoke(down, RightButtonPressed, MiddleButtonPressed, X, Y, ScrollChangeX, ScrollChangeY);
}
internal static void OnRightChange(bool down) {
RightButtonPressed = down;
mouseUpdateEvent?.Invoke(LeftButtonPressed, down, MiddleButtonPressed, X, Y, ScrollChangeX, ScrollChangeY);
}
internal static void OnMiddleChange(bool down) {
RightButtonPressed = down;
mouseUpdateEvent?.Invoke(LeftButtonPressed, RightButtonPressed, down, X, Y, ScrollChangeX, ScrollChangeY);
}
internal static void OnScroll(int scrollX, int scrollY) {
ScrollChangeX = scrollX;
ScrollChangeY = scrollY;
mouseUpdateEvent?.Invoke(LeftButtonPressed, RightButtonPressed, MiddleButtonPressed, X, Y, scrollX, scrollY);
}
}
}

View File

@ -0,0 +1,21 @@
using System;
namespace SlatedGameToolkit.Framework.Logging
{
public interface ILogListener
{
/// <summary>
/// The severity of the messages this log should receive.
/// </summary>
/// <value>The severity of the logs.</value>
LogLevel Level { get; }
/// <summary>
/// Logs the message.
/// </summary>
/// <param name="message">The message to be logged.</param>
/// <param name="time">The time at which this message was requested to be logged.</param>
/// <param name="level">The severity of this message.</param>
void LogMessage(string message, DateTime time, LogLevel level);
}
}

View File

@ -0,0 +1,10 @@
namespace SlatedGameToolkit.Framework.Logging
{
public enum LogLevel
{
FATAL,
INFO,
WARNING,
DEBUG,
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
namespace SlatedGameToolkit.Framework.Logging
{
public static class Logger {
private static HashSet<ILogListener> listeners = new HashSet<ILogListener>();
/// <summary>
/// Logs the message to listeners that are listening to the set severity of the message or greater.
/// </summary>
/// <param name="message">The message to log.</param>
/// <param name="level">The level of severity, by default, info.</param>
public static void Log(string message, LogLevel level = LogLevel.INFO) {
foreach (ILogListener listener in listeners)
{
if (level <= listener.Level) {
listener.LogMessage(message, DateTime.Now, level);
}
}
}
/// <summary>
/// Adds a log listener.
/// </summary>
/// <param name="listener">The listener to add.</param>
public static void AddLogListener(ILogListener listener) {
listeners.Add(listener);
}
/// <summary>
/// Removes a log listener.
/// </summary>
/// <param name="listener">The listener to remove.</param>
public static void RemoveLogListener(ILogListener listener) {
listeners.Remove(listener);
}
/// <summary>
/// Called when all listeners should perform any flushing they need.
/// </summary>
public static void FlushListeners() {
}
}
}

View File

@ -0,0 +1,27 @@
#version 330
out vec4 outputColor;
in vec2 texCoord;
in vec4 color;
uniform bool singleChanneled;
uniform bool flipped;
uniform bool textured;
uniform sampler2D texture0;
void main()
{
float yVal = texCoord.y;
if (flipped) {
yVal = 1 - texCoord.y;
}
if (textured) {
if (singleChanneled) {
outputColor = vec4(color.xyz, texture(texture0, vec2(texCoord.x, yVal)).r);
} else {
outputColor = texture(texture0, vec2(texCoord.x, yVal)) * color;
}
} else {
outputColor = color;
}
}

View File

@ -0,0 +1,18 @@
#version 330
in vec3 aPosition;
in vec4 aColor;
in vec2 aTexCoord;
uniform mat4 models;
uniform mat4 view;
uniform mat4 projection;
out vec2 texCoord;
out vec4 color;
void main()
{
texCoord = aTexCoord;
color = aColor;
gl_Position = projection * view * models * vec4(aPosition, 1.0);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SFML.Net" Version="2.5.0" />
<PackageReference Include="StbTrueTypeSharp" Version="1.24.6" />
<PackageReference Include="StbImageSharp" Version="2.22.4" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/**"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.StateSystem.States;
using SlatedGameToolkit.Framework.Utilities;
using SlatedGameToolkit.Framework.Graphics.OpenGL;
using SlatedGameToolkit.Framework.Logging;
using System.Collections.Concurrent;
namespace SlatedGameToolkit.Framework.StateSystem
{
public sealed class StateManager {
public Thread thread;
public Color backgroundColour;
private IState currentState;
private IState nextState;
private ConcurrentDictionary<string, IState> states;
/// <summary>
/// Instantiates a game state manager with an initial state, and a set of states to be added at the start.
/// States can later be added or removed.
/// None of the parameters can be null, and the initial state must exist in initial set of states.
/// </summary>
/// <param name="initialState">The name of the initial state.</param>
/// <param name="states">The initial set of game states to be added.</param>
internal StateManager() {
backgroundColour = Color.Orange;
this.states = new ConcurrentDictionary<string, IState>();
}
internal void Initialize(IState initialState) {
if (initialState == null) throw new ArgumentNullException("initialState");
Logger.Log("Initialized state manager with state: " + initialState.getName());
thread = Thread.CurrentThread;
AddState(initialState);
currentState = initialState;
currentState.Activate();
}
internal void Update(double timeStep) {
if (nextState != null) {
if (currentState.Deactivate() & nextState.Activate()) {
currentState = nextState;
nextState = null;
}
}
currentState.Update(timeStep);
}
internal void Render(double delta) {
WindowContext windowContext = WindowContextsManager.CurrentWindowContext;
windowContext.Context.ClearColor(backgroundColour.RedAsFloat(), backgroundColour.GreenAsFloat(), backgroundColour.BlueAsFloat(), 1f);
windowContext.Context.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
currentState.Render(delta);
windowContext.SwapBuffer();
}
/// <summary>
/// Begins a state change. The current stage will be notified, and so will the state that is being changed to.
/// </summary>
/// <param name="name">The name of the state that we are turning to.</param>
public void ChangeState(string name) {
if (thread != Thread.CurrentThread) throw new ThreadStateException("State cannot be changed from a different thread.");
if (!states.TryGetValue(name, out nextState)) throw new ArgumentException("The requested state to change to does not exist in this manager.");
}
/// <summary>
/// Adds the given states to this manager under each of their own respective names.
/// Initializes all the states.
/// If there are two states of the same name, the first state in the array is added and the second is discarded.
/// The discarded state will not be initialized and dispose will not be called on it.
/// </summary>
/// <param name="states">The states to add.</param>
/// <returns>True if all the states were unique, and false if there were repetitions determined by name.</returns>
public bool AddStates(IState[] states) {
if (states == null || states.Length == 0) throw new ArgumentException("The array of states cannot be null, and cannot be of size 0");
bool unique = true;
foreach (IState state in states)
{
unique = unique && AddState(state);
}
return unique;
}
/// <summary>
/// Adds the given state to this manager under it's own name.
/// Initializes the state as well.
/// Will not initialize the state if this method returns false.
/// </summary>
/// <param name="state">The state to be added to this manager.</param>
/// <returns>False if a state of this name has already been registered.</returns>
public bool AddState(IState state) {
if (thread != Thread.CurrentThread) throw new ThreadStateException("Cannot add a state from a different thread.");
Logger.Log("Adding state: " + state.getName(), LogLevel.DEBUG);
if (!this.states.TryAdd(state.getName(), state)) {
Logger.Log(string.Format("State with name \"{0}\" already exists in manager.", state.getName()), LogLevel.WARNING);
}
state.Initialize(this);
return true;
}
/// <summary>
/// Removes the state given the name of the state.
/// If the state doesn't exist, nothing happens.
/// Will call dispose on the state.
/// </summary>
/// <param name="name">The name of the state.</param>
/// <returns>False if the state is being used, or the state doesn't exist.</returns>
public bool RemoveState(string name) {
if (thread != Thread.CurrentThread) throw new ThreadStateException("Cannot remove a state from a different thread.");
if (states[name] == currentState) return false;
IState state = states[name];
Logger.Log("Removing state: " + name, LogLevel.DEBUG);
try {
state.Deinitialize();
Logger.Log("State deinitialized: " + name, LogLevel.DEBUG);
} catch (Exception e) {
Logger.Log(e.ToString(), LogLevel.WARNING);
Logger.Log("Failed to deinitialize state: " + state.getName(), LogLevel.WARNING);
}
IState removedState;
return states.Remove(name, out removedState);
}
/// <summary>
/// Removes all the states in this state manager except for the current one.
/// Deinitializes the removed states.
/// </summary>
public void RemoveAllStates() {
foreach (String state in this.states.Keys)
{
RemoveState(state);
}
}
internal void Deinitialize() {
if (currentState == null) return;
currentState = null;
RemoveAllStates();
Logger.Log("Deinitializing state manager.");
}
}
}

View File

@ -0,0 +1,63 @@
using System;
using SlatedGameToolkit.Framework.Graphics.Window;
namespace SlatedGameToolkit.Framework.StateSystem.States
{
public interface IState
{
/// <summary>
/// Called when this state should be deactivated.
/// Deactivate may be called multiple times:
/// When this method returns false,
/// and
/// When the next state to be activated has not indicated it is ready.
/// </summary>
/// <returns>True if this state was successfully deactivated and false otherwise.</returns>
bool Deactivate();
/// <summary>
/// Called when this state is the active state.
/// This method will be called until:
/// This method returns true,
/// and
/// until the previous state indicates it is ready to be deactivated.
/// </summary>
/// <returns>True if this state was successfully activated and can be shown. False otherwise.</returns>
bool Activate();
/// <summary>
/// Called in intervals dependent on game loop chosen for every update cycle.
/// Only called on if this state is the active state, indicated on the last status method called on this implementation (activate, and deactivate).
/// </summary>
/// <param name="timeStep">The change in time between the updates in seconds. If fixed, this amounts to the portion of time between the set update rate. If unlocked, represents the actual amount of time passed since the last update.</param>
void Update(double timeStep);
/// <summary>
/// Called in intervals dependent on game loop chose for every render cycle.
/// Only called on if this state is the active state, indicated on the last status method called on this implementation (activate, and deactivate).
/// </summary>
/// <param name="delta">Defined as a normalized value between [0, 1] representing the time at which this render was performed between updates. Will always be 1 if update cycle is unbounded.</param>
void Render(double delta);
/// <summary>
/// Called at the game managers convenience, but always before it it shown.
/// Should be used to set up this state.
/// </summary>
/// <param name="manager">The manager that made this call.</param>
void Initialize(StateManager manager);
/// <summary>
/// The name of this state.
/// The name is used to refer to this state.
/// The name needs to be unique in a manager.
/// </summary>
/// <return>A string representing the name of this state.</return>
string getName();
/// <summary>
/// Called when this state is being removed from the manager, and likely not used again in this games life cycle.
/// Any unmanaged resources that needs to be disposed of should be done so here.
/// </summary>
void Deinitialize();
}
}

View File

@ -0,0 +1,217 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace SlatedGameToolkit.Framework.Utilities.Collections
{
public class LRUCache<K, V> : ICollection
{
public V this[K key]
{
get
{
return Get(key);
}
set
{
Set(key, value);
}
}
private Node first, last;
private int maxLength;
public int MaxLength {
get {
return maxLength;
}
set {
maxLength = value;
}
}
Dictionary<K, Node> hashmap;
public int Count => hashmap.Count;
public bool IsSynchronized => false;
public object SyncRoot => this;
public LRUCache(int initialSize = 1024) {
this.maxLength = initialSize;
if (initialSize < 1) throw new ArgumentException("size cannot be less than 1.");
this.hashmap = new Dictionary<K, Node>(maxLength + 1);
}
/// <summary>
/// Sets the value given the key. If the key already registered with a value, the previous value is replaced.
/// If the key did not exist, it is registered with the value.
/// </summary>
/// <param name="key">The key to associate the value with.</param>
/// <param name="value">The value to cache.</param>
public void Set(K key, V value) {
if (!hashmap.ContainsKey(key)) {
Node node = new Node(key, value);
hashmap.Add(key, node);
AddToStack(node);
if (hashmap.Count > maxLength) {
hashmap.Remove(RemoveLastFromStack().key);
}
} else {
Node node = hashmap[key];
node.value = value;
MoveToFrontOfStack(node);
}
hashmap[key].value = value;
}
/// <summary>
/// Retrieves the key, and increases the pair's usage ranking.
/// If the key doesn't exist, a KeyNotFoundException is thrown.
/// </summary>
/// <param name="key">The key to retrieve the value for.</param>
/// <returns>The value.</returns>
public V Get(K key) {
if (ContainsKey(key)) {
Node node = hashmap[key];
MoveToFrontOfStack(node);
return node.value;
}
throw new KeyNotFoundException();
}
/// <summary>
/// Checks if the key exists and returns it if it does, otherwise,
/// uses the function to calculate the value and cache it.
/// </summary>
/// <param name="key">The key to retrieve the value for.</param>
/// <param name="func">The function to calculate the value in the case the key doesn't have a associated with it.</param>
/// <returns>The value.</returns>
public V ComputeIfNonExistent(K key, Func<K, V> func) {
if (ContainsKey(key)) {
return hashmap[key].value;
}
V val = func(key);
Set(key, val);
return val;
}
/// <summary>
/// If the cache currently holds a value for the key.
/// The value for the key can be null.
/// </summary>
/// <param name="key">The key to verify for a value.</param>
/// <returns>True if there does exist a value for the key in the cache.</returns>
public bool ContainsKey(K key) {
return hashmap.ContainsKey(key);
}
private void AddToStack(Node node) {
node.left = null;
if (first != null) {
node.right = first;
first.left = node;
} else {
last = node;
}
first = node;
}
private Node RemoveLastFromStack() {
if (last != null) {
Node last = this.last;
RemoveFromStack(last);
return last;
}
return null;
}
private void MoveToFrontOfStack(Node node) {
RemoveFromStack(node);
AddToStack(node);
}
private void RemoveFromStack(Node node) {
if (node.left == null) {
first = node.right;
} else {
node.left.right = node.right;
}
if (node.right == null) {
last = node.left;
} else {
node.right.left = node.left;
}
node.left = null;
node.right = null;
}
public void CopyTo(Array array, int index)
{
Node currentNode = first;
for (int i = 0; i < array.Length && i < Count; i++) {
array.SetValue(currentNode.value, i + index);
currentNode = currentNode.right;
}
}
public IEnumerator GetEnumerator()
{
return new LinkedHashmapEnumerable(first);
}
private class LinkedHashmapEnumerable : IEnumerator<V>
{
Node start, current;
public LinkedHashmapEnumerable(Node start) {
this.start = start;
this.current = start;
}
public V Current => current.value;
object IEnumerator.Current => current.value;
public void Dispose()
{
}
public bool MoveNext()
{
current = current.right;
return current != null;
}
public void Reset()
{
current = start;
}
}
private class Node {
public Node left, right;
public readonly K key;
public V value;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
public override int GetHashCode() {
return value.GetHashCode();
}
public override bool Equals(object obj)
{
if ((obj == null && value == null) || GetType() != obj.GetType())
{
return false;
}
return value.Equals(obj);
}
}
}
}

View File

@ -0,0 +1,7 @@
namespace SlatedGameToolkit.Framework.Utilities.Collections.Pooling
{
public interface IPoolable
{
void Reset();
}
}

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using SlatedGameToolkit.Framework.Exceptions;
namespace SlatedGameToolkit.Framework.Utilities.Collections.Pooling
{
public class ObjectPool <Poolable> where Poolable : IPoolable
{
public int Size { get; private set; }
public int Count {
get {
return pool.Count;
}
}
private Func<Poolable> creator;
private ConcurrentBag<Poolable> pool;
public ObjectPool(Func<Poolable> creator, int size = 0, int initialCount = 0) {
this.creator = creator ?? throw new ArgumentNullException("creator");
this.pool = new ConcurrentBag<Poolable>();
this.Size = size;
CreateAmount(initialCount);
}
public void CreateAmount(int amount) {
if (Size > 0 && Count + amount >= Size) throw new FrameworkUsageException(string.Format("Object pool surpassed set size of {0}", Size));
for (int i = 0; i < amount; i++) {
pool.Add(creator());
}
}
public Poolable Retrieve() {
if (pool.Count == 0) {
CreateAmount(1);
}
Poolable result;
pool.TryTake(out result);
return result;
}
public void Release(Poolable poolable) {
if (Size > 0 && Count + 1 >= Size) throw new FrameworkUsageException(string.Format("Object pool surpassed set size of {0}", Size));
poolable.Reset();
pool.Add(poolable);
}
}
}

View File

@ -0,0 +1,20 @@
using System.Drawing;
namespace SlatedGameToolkit.Framework.Utilities
{
public static class ColorUtils
{
public static float RedAsFloat(this Color color) {
return (float) color.R / byte.MaxValue;
}
public static float GreenAsFloat(this Color color) {
return (float) color.G / byte.MaxValue;
}
public static float BlueAsFloat(this Color color) {
return (float) color.B / byte.MaxValue;
}
public static float AlphaAsFloat(this Color color) {
return (float) color.A / byte.MaxValue;
}
}
}

View File

@ -0,0 +1,32 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace SlatedGameToolkit.Framework.Utilities
{
public static class EmbeddedResUtils
{
public static string ReadEmbeddedResourceText(string name) {
string res;
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(string.Format("SlatedGameToolkit.Framework.Resources.{0}", name)))
{
using (StreamReader reader = new StreamReader(stream)) {
res = reader.ReadToEnd();
}
}
return res;
}
public static byte[] ReadEmbeddedResourceData(string name) {
byte[] res;
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(string.Format("SlatedGameToolkit.Framework.Resources.{0}", name)))
{
byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
res = buffer;
}
return res;
}
}
}

View File

@ -0,0 +1,13 @@
using System.Numerics;
namespace SlatedGameToolkit.Framework.Utilities
{
public static class MatrixUtils
{
public static float[] ToColumnMajorArray(this Matrix4x4 mat) {
return new float[] {
mat.M11, mat.M12, mat.M13, mat.M14, mat.M21, mat.M22, mat.M23, mat.M24, mat.M31, mat.M32, mat.M33, mat.M34, mat.M41, mat.M42, mat.M43, mat.M44
};
}
}
}

View File

@ -0,0 +1,25 @@
using System.Drawing;
using System.Numerics;
namespace SlatedGameToolkit.Framework.Utilities
{
public static class RectangleUtils
{
public static RectangleF MultiplyBy(this RectangleF rectangle, float multiplier) {
RectangleF rect = rectangle;
rect.X *= multiplier;
rect.Y *= multiplier;
rect.Width *= multiplier;
rect.Height *= multiplier;
return rect;
}
public static bool Contains(this RectangleF rect, Vector2 vector) {
if (vector.X < rect.X) return false;
if (vector.X > rect.X + rect.Width) return false;
if (vector.Y > rect.Y + rect.Height) return false;
if (vector.Y < rect.Y) return false;
return true;
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Numerics;
namespace SlatedGameToolkit.Framework.Utilities
{
public static class VectorUtils
{
public static Vector3 NormalizeSafe(this Vector3 value, Vector3 alternate) {
float l = value.Length();
if (l == 0) {
return alternate;
}
value /= l;
return value;
}
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections;
using System.Collections.Generic;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.CommandSystem
{
public class CommandMap : ICollection<IInvocable>, IDisposable {
Dictionary<string, IInvocable> invocations;
HashSet<IInvocable> values;
public int Count => invocations.Count;
public bool IsReadOnly => false;
public IInvocable this[string invocation] {
get {
IInvocable result = null;
invocations.TryGetValue(invocation, out result);
return result;
}
}
public CommandMap(params IInvocable[] invokables) {
invocations = new Dictionary<string, IInvocable>();
values = new HashSet<IInvocable>();
foreach (IInvocable item in invokables)
{
Add(item);
}
}
public IEnumerator<IInvocable> GetEnumerator()
{
return values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return values.GetEnumerator();
}
public void Add(IInvocable item)
{
values.Add(item);
string[] invokers = item.GetInvokers();
foreach (string invoker in invokers)
{
try {
invocations.Add(invoker, item);
} catch (ArgumentException e) {
throw new ArgumentException("A duplicate invocation string was found!", e);
}
}
}
public void Clear()
{
invocations.Clear();
values.Clear();
}
public bool Contains(IInvocable item)
{
return values.Contains(item);
}
public void CopyTo(IInvocable[] array, int arrayIndex)
{
values.CopyTo(array, arrayIndex);
}
public bool Remove(IInvocable item)
{
string[] invokers = item.GetInvokers();
foreach (string invoker in invokers)
{
if (!invocations.Remove(invoker)) return false;
}
values.Remove(item);
return true;
}
public void Dispose()
{
foreach (IInvocable invocable in this)
{
invocable.Dispose();
}
}
~CommandMap() {
Dispose();
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.CommandSystem
{
public class CommandProcessor
{
CommandMap commandMap;
/// <summary>
/// The general help is the string that is printed when the input is not understood.
/// {input} in the string is replaced with the user input.
/// </summary>
/// <param name="commands"></param>
public CommandProcessor(CommandMap commands) {
this.commandMap = commands;
}
public void Process(IInteractable interactable) {
string message = interactable.Listen().ToLower();
string[] splitMessage = message.Split(' ');
string invocation = splitMessage[0];
IInvocable invocable = commandMap[invocation];
if (invocable != null) {
string[] args = new string[splitMessage.Length - 1];
Array.Copy(splitMessage, 1, args, 0, splitMessage.Length - 1);
if (!invocable.Execute(interactable, args)) {
interactable.Tell(string.Format("The command \"{0}\" arguments were incorrect. Please refer to Please type \"help {0}\" for more information.", invocation));
}
return;
}
interactable.Tell(string.Format("The command \"{0}\" was not understood. Please type \"help\" for more information.", invocation));
}
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Runtime.Serialization;
namespace SlatedGameToolkit.Tools.CommandSystem.Exceptions
{
[Serializable]
public class FatalUsageException : Exception
{
public FatalUsageException() { }
public FatalUsageException(string message) : base(message) { }
public FatalUsageException(string message, Exception inner) : base(message, inner) { }
protected FatalUsageException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
}

View File

@ -0,0 +1,39 @@
using System;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.CommandSystem
{
public interface IInvocable : IDisposable
{
/// <summary>
/// Invokers are the strings that should invoke this command.
/// This should not conflict with any other commands invokers.
/// </summary>
/// <returns>A list of the strings that will invoke this invokable.</returns>
string[] GetInvokers();
/// <summary>
/// The help should be a description of how to properly use this invokable.
/// If an argument is specified, the help should cater to that specific argument.
/// Information in the description does not need to be repeated.
/// Arguments will always be lowercase or null.
/// </summary>
/// <param name="arg">An argument this invokable should describe. May be null, to which should return a string that describes this invokable in general.</param>
/// <returns></returns>
string getUsage(string arg);
/// <summary>
/// Executes this invokable. Arguments will always be lowercase.
/// </summary>
/// <param name="interactable">The interactable object.</param>
/// <param name="args">The arguments that followed the invokable.</param>
/// <returns>True if the invokable was successful, false otherwise to display a generic help message.</returns>
bool Execute(IInteractable interactable, string[] args);
/// <summary>
/// Gets a simple description of the command.
/// </summary>
/// <returns>Return a string with a short description of the command.</returns>
string getDescription();
}
}

View File

@ -0,0 +1,39 @@
using System;
using SlatedGameToolkit.Framework.Logging;
namespace SlatedGameToolkit.Tools.CommandSystem.Interaction
{
public class ConsoleInteraction : IInteractable
{
private volatile bool listening;
public bool Listening { get {return listening;}}
string prefix;
public string Prefix {get {return prefix;}}
public bool Debug { get; set; }
public LogLevel Level => Debug ? LogLevel.DEBUG : LogLevel.INFO;
public ConsoleInteraction(string prefix) {
this.prefix = prefix;
}
public void Separate()
{
listening = false;
Console.WriteLine();
}
public string Listen()
{
listening = true;
Console.Write(prefix + "> ");
return Console.ReadLine();
}
public void Tell(string message)
{
listening = false;
Console.SetCursorPosition(0, Console.CursorTop);
Console.WriteLine(message);
}
}
}

View File

@ -0,0 +1,10 @@
namespace SlatedGameToolkit.Tools.CommandSystem.Interaction
{
public interface IInteractable
{
void Tell(string message);
void Separate();
string Listen();
public bool Listening { get; }
}
}

View File

@ -0,0 +1,33 @@
using System;
using SlatedGameToolkit.Tools.CommandSystem.Exceptions;
namespace SlatedGameToolkit.Tools.CommandSystem.Interaction
{
public class SingleConsoleInteraction : IInteractable
{
private bool interacted;
private string oneTime;
public SingleConsoleInteraction(string oneTime) {
this.oneTime = oneTime;
}
public bool Listening => false;
public string Listen()
{
if (interacted) throw new FatalUsageException("Command attempted to request for more information. This generally occurs if the command being ran requires more user input.");
interacted = true;
return oneTime;
}
public void Separate()
{
Console.WriteLine();
}
public void Tell(string message)
{
Console.WriteLine(message);
}
}
}

View File

@ -0,0 +1,33 @@
using SlatedGameToolkit.Tools.CommandSystem;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.Commands
{
public class ExportCommand : IInvocable
{
public void Dispose()
{
throw new System.NotImplementedException();
}
public bool Execute(IInteractable interactable, string[] args)
{
throw new System.NotImplementedException();
}
public string getDescription()
{
throw new System.NotImplementedException();
}
public string[] GetInvokers()
{
throw new System.NotImplementedException();
}
public string getUsage(string arg)
{
return null;
}
}
}

View File

@ -0,0 +1,119 @@
using SlatedGameToolkit.Framework;
using SlatedGameToolkit.Framework.Logging;
using SlatedGameToolkit.Framework.StateSystem;
using SlatedGameToolkit.Tools.CommandSystem;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
using SlatedGameToolkit.Tools.Utilities.Playground;
namespace SlatedGameToolkit.Tools.Commands
{
public class GraphicalPlaygroundCommand : IInvocable
{
private readonly string[] invokers = new string[] {"playground"};
private ConsolePlaygroundListener logListener;
public bool Execute(IInteractable interactable, string[] args)
{
if (args.Length < 1) return false;
args[0] = args[0].ToLower();
if (args[0].Equals("start")) {
if (GameEngine.IsRunning()) {
interactable.Tell("Engine is already running!");
return true;
}
GameEngine.Ignite(new MainState());
return true;
} else if (args[0].Equals("stop")) {
if (!GameEngine.IsRunning()) {
interactable.Tell("Engine was never running!");
return true;
}
GameEngine.Stop();
return true;
} else if (args[0].Equals("status")) {
interactable.Tell("Running: " + GameEngine.IsRunning());
interactable.Tell("Target FPS: " + (GameEngine.targetFPS <= 0 ? "Not bounded." : GameEngine.targetFPS.ToString()));
interactable.Tell("Update Step: " + (GameEngine.UpdatesPerSecond <= 0 ? "Not Locked." : GameEngine.UpdatesPerSecond.ToString()));
interactable.Tell("Debug logging: " + ((logListener == null) ? "false" : "true"));
return true;
} else if (args[0].Equals("debug")) {
if (logListener == null) {
Logger.AddLogListener((logListener = new ConsolePlaygroundListener(interactable as ConsoleInteraction)));
logListener.Debug = true;
interactable.Tell("Attached log listener.");
} else {
logListener.Debug = !logListener.Debug;
}
interactable.Tell(string.Format("Debug logging has been turned {0}.", logListener.Debug ? "on" : "off"));
return true;
} else if (args[0].Equals("log")) {
if (logListener == null) {
Logger.AddLogListener((logListener = new ConsolePlaygroundListener(interactable as ConsoleInteraction)));
interactable.Tell("Listening to game engine's logging.");
} else {
Logger.RemoveLogListener(logListener);
logListener = null;
interactable.Tell("Stopped listening to game engine's logging.");
}
return true;
} else if (args[0].Equals("updates")) {
int updates;
if (args.Length < 2 || !int.TryParse(args[1], out updates)) {
interactable.Tell("Missing numerical value dictating updates per second.");
} else {
GameEngine.UpdatesPerSecond = updates;
interactable.Tell("Target updates per second set to: " + updates);
}
return true;
} else if (args[0].Equals("framerate")) {
int frames;
if (args.Length < 2 || !int.TryParse(args[1], out frames)) {
interactable.Tell("Missing numerical value dictating framerate.");
} else {
GameEngine.targetFPS = frames;
interactable.Tell("Target framerate set to: " + frames);
}
return true;
}
return false;
}
public string getDescription()
{
return "Controls the playground engine status.";
}
public string[] GetInvokers()
{
return invokers;
}
public string getUsage(string arg)
{
if (arg == null) {
return "\"playground [start | stop | status | log | debug | framerate <framerate> | updates <update rate>]\"";
} else if (arg.Equals("start")) {
return "Starts the playground.";
} else if (arg.Equals("stop")) {
return "Stops the playground.";
} else if (arg.Equals("status")) {
return "Retrieves the FPS, update rate, whether or not the playground is running, and if debugging is turned on.";
} else if (arg.Equals("log")) {
return "Toggles whether or not to attach logging to the console.";
} else if (arg.Equals("debug")) {
return "Toggles debugging. If logging is disabled, logging will be enabled.";
} else if (arg.Equals("framerate")) {
return "sets the FPS. Needs to be followed by a numerical value representing the new target FPS.";
} else if (arg.Equals("updates")) {
return "Sets the new update rate. Needs to be followed by a numerical value representing the target update rate.";
} else {
return null;
}
}
public void Dispose()
{
GameEngine.Stop();
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Text;
using SlatedGameToolkit.Tools.CommandSystem;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.Commands
{
public class HelpCommand : IInvocable
{
private readonly string[] invokers = new string[] {"help"};
private CommandMap commandMap;
public HelpCommand(CommandMap commandMap) {
this.commandMap = commandMap;
}
public bool Execute(IInteractable interactable, string[] args)
{
if (args.Length > 0) {
IInvocable invocable = commandMap[args[0]];
if (invocable == null) interactable.Tell("Unable to find command {0}. Please type \"help\" for more a list of commands.");
StringBuilder builder = new StringBuilder();
builder.AppendJoin(", ", invocable.GetInvokers());
interactable.Tell("Possible aliases: " + builder.ToString());
interactable.Tell("Description: " + invocable.getDescription());
if (args.Length > 1) {
string desc = invocable.getUsage(args[1]);
if (desc == null) {
interactable.Tell(string.Format("Unable to find argument \"{0}\" for command \"{1}\". Please type \"help\" for more info.", args[1], args[0]));
} else {
interactable.Tell(args[1] + ": " + desc);
}
}
} else {
interactable.Tell("--- Help ---");
foreach (IInvocable invocable in commandMap)
{
interactable.Separate();
StringBuilder builder = new StringBuilder();
builder.AppendJoin(", ", invocable.GetInvokers());
interactable.Tell(builder.ToString() + ": ");
interactable.Tell(invocable.getDescription());
}
}
return true;
}
public string getDescription()
{
return "Displays the help message. You're looking at it.";
}
public string getUsage(string arg)
{
return "Usage: \"help [command]\" where [command] is a specific command you want to see usage for.";
}
public string[] GetInvokers()
{
return invokers;
}
public void Dispose()
{
}
}
}

View File

@ -0,0 +1,42 @@
using System.Reflection;
using SlatedGameToolkit.Framework;
using SlatedGameToolkit.Tools.CommandSystem;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.Commands
{
public class ListEmbeddedResourcesCommand : IInvocable
{
private string[] invokers = new string[] {"GetEmbedded"};
public void Dispose()
{
}
public bool Execute(IInteractable interactable, string[] args)
{
Assembly assembly = Assembly.GetAssembly(typeof(GameEngine));
string[] embeddedFiles = assembly.GetManifestResourceNames();
interactable.Tell("Loaded embedded files:");
foreach (string fileName in embeddedFiles)
{
interactable.Tell(fileName);
}
return true;
}
public string getDescription()
{
return "Returns a list of embedded resources in the given assembly.";
}
public string[] GetInvokers()
{
return invokers;
}
public string getUsage(string arg)
{
return "Usage: \"GetEmbedded\" to retrieve a list of embedded files in the framework and tools.";
}
}
}

View File

@ -0,0 +1,35 @@
using SlatedGameToolkit.Tools.CommandSystem;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.Commands
{
public class StopCommand : IInvocable
{
private readonly string[] invokers = new string[] {"stop", "exit", "q", "quit"};
public bool Execute(IInteractable interactable, string[] args)
{
if (args.Length > 1) return false;
Program.Stop();
return true;
}
public string getDescription()
{
return "Exits the tool.";
}
public string getUsage(string arg)
{
return "This command exits the tool and has no additional arguments.";
}
public string[] GetInvokers()
{
return invokers;
}
public void Dispose()
{
}
}
}

View File

@ -1,12 +1,42 @@
using System;
using System.Reflection;
using System.Text;
using SlatedGameToolkit.Tools.Commands;
using SlatedGameToolkit.Tools.CommandSystem;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools
{
class Program
{
static private bool live;
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
CommandMap commands = new CommandMap();
commands.Add(new StopCommand());
commands.Add(new HelpCommand(commands));
commands.Add(new ListEmbeddedResourcesCommand());
commands.Add(new GraphicalPlaygroundCommand());
CommandProcessor processor = new CommandProcessor(commands);
AssemblyName name = Assembly.GetExecutingAssembly().GetName();
IInteractable interactable = (args.Length > 0 ? (IInteractable) new SingleConsoleInteraction(string.Join(' ', args)) : (IInteractable) new ConsoleInteraction("Tools"));
interactable.Tell(String.Format("{0} Version: {1}", name.Name, name.Version));
if (args.Length > 0) {
interactable.Tell("Running in one time use mode.");
processor.Process(interactable);
} else {
interactable.Tell("Welcome to SlatedGameToolkit.Tools! These tools are meant for the developers using the SlatedGameToolkit. Type \"help\" for a list of things this tool can currently do.");
live = true;
while (live) {
processor.Process(interactable);
}
}
interactable.Tell("Exiting tool.");
commands.Dispose();
}
public static void Stop() {
live = false;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -7,6 +7,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Description>A tool to help with developing a game using SlatedGameToolkit.</Description>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Content Include="Resources/**" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using System;
using SlatedGameToolkit.Framework.Logging;
using SlatedGameToolkit.Tools.CommandSystem.Interaction;
namespace SlatedGameToolkit.Tools.Utilities.Playground
{
public class ConsolePlaygroundListener : ILogListener
{
public bool Debug { get; set; }
public LogLevel Level => Debug ? LogLevel.DEBUG : LogLevel.INFO;
private ConsoleInteraction interaction;
public ConsolePlaygroundListener(ConsoleInteraction interaction = null) {
this.interaction = interaction;
}
public void LogMessage(string message, DateTime time, LogLevel level)
{
Console.SetCursorPosition(0, Console.CursorTop);
Console.WriteLine(string.Format("Engine [{0}] [{1}]: {2}", level, time.ToString("H:mm:ss"), message));
if (interaction != null && interaction.Listening) {
Console.SetCursorPosition(0, Console.CursorTop);
Console.Write(interaction.Prefix + "> ");
}
}
}
}

View File

@ -0,0 +1,120 @@
using System.Drawing;
using System.Numerics;
using SDL2;
using SlatedGameToolkit.Framework.Loaders;
using SlatedGameToolkit.Framework.Graphics.Render;
using SlatedGameToolkit.Framework.Graphics.Text;
using SlatedGameToolkit.Framework.Graphics.Textures;
using SlatedGameToolkit.Framework.Graphics.Window;
using SlatedGameToolkit.Framework.Input.Devices;
using SlatedGameToolkit.Framework.StateSystem;
using SlatedGameToolkit.Framework.StateSystem.States;
using SlatedGameToolkit.Framework.Graphics;
using SlatedGameToolkit.Framework.AssetSystem;
namespace SlatedGameToolkit.Tools.Utilities.Playground
{
public class MainState : IState
{
private WindowContext window;
private Camera2D camera;
private MeshBatchRenderer renderer;
private BitmapFont font;
private AssetManager assets;
private RectangleMesh logo, textureTester, untextured;
private string lastPressed;
public WindowContext CurrentWindow { get { return window;}}
public bool Activate()
{
return true;
}
public bool Deactivate()
{
return true;
}
public void Deinitialize()
{
assets.UnloadAll();
font.Dispose();
renderer.Dispose();
window.Dispose();
}
public string getName()
{
return "main state";
}
public unsafe void Initialize(StateManager manager)
{
window = new WindowContext("SlatedGameToolkit Playground");
Vector2 drawableDimensions = window.GetDrawableDimensions();
float ratio = drawableDimensions.Y / drawableDimensions.X;
camera = new Camera2D(2f, ratio * 2f);
renderer = new MeshBatchRenderer(camera, BatchVertexSize: 2048);
assets = new AssetManager();
assets.Loaders["png"] = TextureLoader.Load2DTexture;
assets.QueueLoad("Resources/Playground/yhdnbgnc.png");
assets.QueueLoad("Resources/Playground/filler.png");
assets.WaitAndLoadAllQueued();
logo = new RectangleMesh((ITexture)assets["Resources/Playground/yhdnbgnc.png"], Color.White);
logo.Width = 0.5f;
logo.Height = 0.5f;
logo.X = -0.25f;
logo.Y = -0.25f;
textureTester = new RectangleMesh((ITexture)assets["Resources/Playground/filler.png"], Color.White);
textureTester.Width = 0.15f;
textureTester.Height = 0.15f;
textureTester.X = 0.25f;
untextured = new RectangleMesh(null, Color.Red);
untextured.Width = 0.15f;
untextured.Height = 0.15f;
untextured.X = 0.25f;
untextured.Y = - 0.15f;
font = new BitmapFont("Resources/Playground/earwig_factory_rg.ttf");
font.PixelHeight = 128;
font.PrepareCharacterGroup("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray());
}
public void Render(double delta)
{
camera.InterpolatePosition(delta);
renderer.Begin(Matrix4x4.Identity);
renderer.Draw(logo);
renderer.Draw(textureTester);
renderer.Draw(untextured);
font.WriteLine(renderer, 0.25f, -0.35f, "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n1234567890", Color.White);
renderer.End();
}
public void Update(double timeStep)
{
if (Keyboard.IsKeyPressed(SDL.SDL_Keycode.SDLK_DOWN)) {
camera.MoveTo += new Vector2(0, (-0.25f)) * (float) timeStep;
}
if (Keyboard.IsKeyPressed(SDL.SDL_Keycode.SDLK_UP)) {
camera.MoveTo += new Vector2(0, (0.25f)) * (float) timeStep;
}
if (Keyboard.IsKeyPressed(SDL.SDL_Keycode.SDLK_LEFT)) {
camera.MoveTo += new Vector2((-0.25f) * (float) timeStep, 0);
}
if (Keyboard.IsKeyPressed(SDL.SDL_Keycode.SDLK_RIGHT)) {
camera.MoveTo += new Vector2((0.25f) * (float) timeStep, 0);
}
}
public void KeyPressed(SDL.SDL_Keycode keycode, bool pressed) {
if (pressed) lastPressed = SDL.SDL_GetKeyName(keycode);
}
}
}

View File

@ -9,7 +9,11 @@
<ItemGroup>
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SlatedGameToolkit.Framework\SlatedGameToolkit.Framework.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,74 @@
using NUnit.Framework;
using SlatedGameToolkit.Framework.Utilities.Collections;
namespace SlatedGameToolkit.Framework.Tests.Utilities.Collections.Caching
{
public class LRUCacheTests
{
[SetUp]
public void Setup()
{
}
[Test]
public void SizeLimitTest()
{
LRUCache<int, int> cache = new LRUCache<int, int>(16);
for (int i = 0; i < 32; i++) {
cache.Set(i, i);
}
Assert.AreEqual(16, cache.Count);
}
[Test]
public void ResizeLimitTest() {
LRUCache<int, int> cache = new LRUCache<int, int>(16);
for (int i = 0; i < 32; i++) {
cache.Set(i, i);
}
cache.MaxLength = 64;
for (int i = 32; i < 64; i++) {
cache.Set(i, i);
}
Assert.AreEqual(48, cache.Count);
Assert.AreEqual(63, cache[63]);
}
[Test]
public void LeastUsedTest() {
LRUCache<int, int> cache = new LRUCache<int, int>(4);
for (int i = 0; i < cache.MaxLength; i++) {
cache[i] = i;
}
int used = cache[0];
Assert.AreEqual(0, used);
cache[4] = 4;
Assert.IsFalse(cache.ContainsKey(1));
cache[2] = 2;
cache[5] = 5;
Assert.IsTrue(cache.ContainsKey(2));
}
[Test]
public void ComputeIfNonexistentTest() {
LRUCache<int, int> cache = new LRUCache<int, int>(4);
for (int i = 0; i < cache.MaxLength; i++) {
cache.ComputeIfNonExistent(i, (a) => a);
}
}
[Test]
public void ComputeIfNonexistentTestExists() {
LRUCache<int, int> cache = new LRUCache<int, int>(4);
for (int i = 0; i < cache.MaxLength; i++) {
cache.ComputeIfNonExistent(i, (a) => a);
}
for (int i = 0; i < cache.MaxLength; i++) {
Assert.AreEqual(i, cache.ComputeIfNonExistent(i, (a) => -a));
}
}
}
}

View File

@ -1,18 +0,0 @@
using NUnit.Framework;
namespace tests
{
public class Tests
{
[SetUp]
public void Setup()
{
}
[Test]
public void Test1()
{
Assert.Pass();
}
}
}