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.Devices; using SlatedGameToolkit.Framework.StateSystem; using SlatedGameToolkit.Framework.StateSystem.States; using SlatedGameToolkit.Framework.Utilities; namespace SlatedGameToolkit.Framework { /// /// The main engine that will host the game loop. /// public static class GameEngine { private const int GL_MAJOR_VER = 3, GL_MINOR_VER = 3; 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); /// /// 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. /// /// The updates per second. 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; } } /// /// 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. /// /// The target frames per second. 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.TotalSeconds <= 0 ? timePassedFromLastUpdate.TotalSeconds : updateDeltaTime.TotalSeconds); timePassedFromLastUpdate -= updateDeltaTime; if (updateDeltaTime.TotalSeconds <= 0) break; } timePassedFromLastRender += difference; if (timePassedFromLastRender > frameDeltaTime) { //Draw calls. manager.render(updateDeltaTime.TotalSeconds <= 0 ? updateDeltaTime.TotalSeconds : (timePassedFromLastUpdate / updateDeltaTime)); 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; } } /// /// 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. /// /// False if the stop request has already been made. public static bool Stop() { lock (ignitionLock) { if (exit) return false; exit = true; return true; } } /// /// Requests to start the game engine. /// /// True iff the engine is not already running. 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)); thread = new Thread(Loop); thread.Name = "SGTK-Engine"; thread.Priority = ThreadPriority.AboveNormal; thread.Start(initialState); return true; } } public static bool IsRunning() { return !stopped; } } }