283 lines
14 KiB
C#
283 lines
14 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using SDL2;
|
|
using Serilog;
|
|
using Serilog.Core;
|
|
using SlatedGameToolkit.Framework.Exceptions;
|
|
using SlatedGameToolkit.Framework.Graphics.Textures;
|
|
using SlatedGameToolkit.Framework.Graphics.Window;
|
|
using SlatedGameToolkit.Framework.Input;
|
|
using SlatedGameToolkit.Framework.Input.Devices;
|
|
using SlatedGameToolkit.Framework.StateSystem;
|
|
using SlatedGameToolkit.Framework.StateSystem.States;
|
|
using SlatedGameToolkit.Framework.Utilities;
|
|
|
|
namespace SlatedGameToolkit.Framework {
|
|
/// <summary>
|
|
/// The main engine that will host the game loop.
|
|
/// </summary>
|
|
public static class GameEngine {
|
|
private const int GL_MAJOR_VER = 3, GL_MINOR_VER = 3;
|
|
public static TextureData FillerTextureData {get; private set;}
|
|
public static bool Debugging { get; set; }
|
|
public static Logger Logger { get; private set; }
|
|
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);
|
|
|
|
/// <summary>
|
|
/// The amount of updates per second.
|
|
///
|
|
/// If greater than 0, game loop updates will be fixed (time steps).
|
|
/// More specifically, it will figure out how many updates to perform per frame to allow for physics steps
|
|
/// to be calculated based on a constant time step.
|
|
///
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
private static void Loop(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();
|
|
try {
|
|
IState initialState = (IState) o;
|
|
manager.Initialize(initialState);
|
|
DateTime currentTime = DateTime.Now;
|
|
TimeSpan timePassedFromLastUpdate = TimeSpan.Zero;
|
|
TimeSpan timePassedFromLastRender = TimeSpan.Zero;
|
|
TimeSpan updateDeltaTime = GameEngine.updateDeltaTime;
|
|
TimeSpan frameDeltaTime = GameEngine.frameDeltaTime;
|
|
deltaChanged = true;
|
|
stopped = false;
|
|
Logger.Information("Game engine initiated.");
|
|
while (!exit) {
|
|
//Pull latest deltas.
|
|
if (deltaChanged) {
|
|
lock (deltaUpdateLock) {
|
|
updateDeltaTime = GameEngine.updateDeltaTime;
|
|
frameDeltaTime = GameEngine.frameDeltaTime;
|
|
Logger.Information(String.Format("Deltas were set. Update Delta: {0}, Render target Delta: {1}", updateDeltaTime.TotalSeconds, frameDeltaTime.TotalSeconds));
|
|
}
|
|
deltaChanged = false;
|
|
}
|
|
//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;
|
|
}
|
|
}
|
|
DateTime frameStart = DateTime.Now;
|
|
TimeSpan difference = frameStart - currentTime;
|
|
currentTime = frameStart;
|
|
|
|
timePassedFromLastUpdate += difference;
|
|
while (timePassedFromLastUpdate > updateDeltaTime) {
|
|
//Updates.
|
|
manager.update(updateDeltaTime.TotalMilliseconds <= 0 ? timePassedFromLastUpdate.TotalSeconds : updateDeltaTime.TotalSeconds);
|
|
timePassedFromLastUpdate -= updateDeltaTime;
|
|
if (updateDeltaTime.TotalSeconds <= 0) break;
|
|
}
|
|
|
|
timePassedFromLastRender += difference;
|
|
if (timePassedFromLastRender > frameDeltaTime) {
|
|
//Draw calls.
|
|
manager.render(timePassedFromLastUpdate.TotalSeconds);
|
|
timePassedFromLastRender = TimeSpan.Zero;
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
Logger.Fatal(e.ToString());
|
|
} finally {
|
|
stopped = true;
|
|
manager.Dispose();
|
|
SDL.SDL_Quit();
|
|
Logger.Information("Game engine has stopped.");
|
|
Logger.Dispose();
|
|
WindowContextsManager.DisposeAllWindowContexts();
|
|
Logger = null;
|
|
}
|
|
}
|
|
|
|
/// <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.Warning("Engine is already running.");
|
|
return false;
|
|
}
|
|
GameEngine.Logger = new LoggerConfiguration().MinimumLevel.Debug().
|
|
WriteTo.File("SlatedGameToolKit.log", fileSizeLimitBytes: 1048576, rollOnFileSizeLimit: true).
|
|
CreateLogger();
|
|
SDL.SDL_version SDLVersion;
|
|
SDL.SDL_version SDLBuiltVersion;
|
|
SDL.SDL_GetVersion(out SDLVersion);
|
|
SDL.SDL_VERSION(out SDLBuiltVersion);
|
|
Logger.Information(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.Warning(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));
|
|
if (SDLVersion.major < 2) {
|
|
Logger.Error("This engine was designed to work with SDL2. The version you're currently running is severely outdated.");
|
|
throw new FrameworkUsageException("Outdated SDL binaries.");
|
|
}
|
|
}
|
|
exit = false;
|
|
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO) != 0) {
|
|
throw new FrameworkSDLException();
|
|
}
|
|
if (SDL.SDL_Init(SDL.SDL_INIT_AUDIO) != 0) {
|
|
throw new FrameworkSDLException();
|
|
}
|
|
|
|
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, GL_MAJOR_VER) < 0 ||
|
|
SDL.SDL_GL_SetAttribute(SDL.SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 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 FrameworkSDLException(string.Format("Unable to load correct OpenGL version {0}.{1}.0 Core.", GL_MINOR_VER, GL_MAJOR_VER));
|
|
|
|
LoadEngineAssets();
|
|
|
|
thread = new Thread(Loop);
|
|
thread.Name = "SGTK-Engine";
|
|
thread.Priority = ThreadPriority.AboveNormal;
|
|
thread.Start(initialState);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private unsafe static void LoadEngineAssets() {
|
|
byte[] buffer = EmbeddedResourceUtils.ReadEmbeddedResourceData("filler.png");
|
|
fixed (void* ptr = &buffer[0]) {
|
|
IntPtr rwOps = SDL.SDL_RWFromConstMem(new IntPtr(ptr), buffer.Length * sizeof(byte));
|
|
IntPtr surfacePtr = SDL_image.IMG_Load_RW(rwOps, 1);
|
|
SDL.SDL_Surface surface = Marshal.PtrToStructure<SDL.SDL_Surface>(surfacePtr);
|
|
|
|
SDL.SDL_PixelFormat pixelFormat = Marshal.PtrToStructure<SDL.SDL_PixelFormat>(surface.format);
|
|
if (pixelFormat.format != SDL.SDL_PIXELFORMAT_RGBA8888) {
|
|
IntPtr convertedPtr = SDL.SDL_ConvertSurfaceFormat(surfacePtr, SDL.SDL_PIXELFORMAT_RGBA8888, 0);
|
|
if (convertedPtr == null) throw new OptionalSDLException();
|
|
SDL.SDL_FreeSurface(surfacePtr);
|
|
surfacePtr = convertedPtr;
|
|
surface = Marshal.PtrToStructure<SDL.SDL_Surface>(surfacePtr);
|
|
}
|
|
byte[] data = new byte[surface.pitch * surface.h];
|
|
|
|
Marshal.Copy(surface.pixels, data, 0, data.Length);
|
|
FillerTextureData = new TextureData(surface.w, surface.h, data);
|
|
|
|
SDL.SDL_FreeSurface(surfacePtr);
|
|
}
|
|
}
|
|
|
|
public static bool IsRunning() {
|
|
return !stopped;
|
|
}
|
|
}
|
|
} |