MoonWorks/src/Game.cs

271 lines
7.1 KiB
C#
Raw Normal View History

2021-01-20 03:34:26 +00:00
using System.Collections.Generic;
using SDL2;
2021-01-20 02:06:10 +00:00
using MoonWorks.Audio;
2021-01-20 03:34:26 +00:00
using MoonWorks.Graphics;
using MoonWorks.Input;
2021-03-25 22:57:26 +00:00
using System.Text;
2021-07-23 22:47:02 +00:00
using System;
using System.Diagnostics;
2021-01-19 07:29:07 +00:00
namespace MoonWorks
{
2022-04-08 07:03:42 +00:00
public class Game
2022-02-23 05:14:32 +00:00
{
public TimeSpan MAX_DELTA_TIME = TimeSpan.FromMilliseconds(100);
2021-01-23 07:43:48 +00:00
2022-02-23 05:14:32 +00:00
private bool quit = false;
2021-01-19 07:29:07 +00:00
2021-07-23 22:47:02 +00:00
private Stopwatch gameTimer;
2022-02-23 05:14:32 +00:00
private TimeSpan timestep;
2021-07-23 22:47:02 +00:00
private long previousTicks = 0;
2022-02-23 05:14:32 +00:00
TimeSpan accumulatedElapsedTime = TimeSpan.Zero;
2021-07-23 22:47:02 +00:00
// must be a power of 2 so we can do a bitmask optimization when checking worst case
private const int PREVIOUS_SLEEP_TIME_COUNT = 128;
private const int SLEEP_TIME_MASK = PREVIOUS_SLEEP_TIME_COUNT - 1;
private TimeSpan[] previousSleepTimes = new TimeSpan[PREVIOUS_SLEEP_TIME_COUNT];
private int sleepTimeIndex = 0;
private TimeSpan worstCaseSleepPrecision = TimeSpan.FromMilliseconds(1);
2022-02-25 21:23:31 +00:00
public Window Window { get; }
2022-02-23 05:14:32 +00:00
public GraphicsDevice GraphicsDevice { get; }
public AudioDevice AudioDevice { get; }
public Inputs Inputs { get; }
2022-04-08 07:03:42 +00:00
private GameState GameState = null;
2022-02-23 05:14:32 +00:00
private Dictionary<PresentMode, RefreshCS.Refresh.PresentMode> moonWorksToRefreshPresentMode = new Dictionary<PresentMode, RefreshCS.Refresh.PresentMode>
{
{ PresentMode.Immediate, RefreshCS.Refresh.PresentMode.Immediate },
{ PresentMode.Mailbox, RefreshCS.Refresh.PresentMode.Mailbox },
{ PresentMode.FIFO, RefreshCS.Refresh.PresentMode.FIFO },
{ PresentMode.FIFORelaxed, RefreshCS.Refresh.PresentMode.FIFORelaxed }
};
public Game(
WindowCreateInfo windowCreateInfo,
PresentMode presentMode,
int targetTimestep = 60,
bool debugMode = false
)
{
timestep = TimeSpan.FromTicks(TimeSpan.TicksPerSecond / targetTimestep);
2021-07-23 22:47:02 +00:00
gameTimer = Stopwatch.StartNew();
for (int i = 0; i < previousSleepTimes.Length; i += 1)
{
previousSleepTimes[i] = TimeSpan.FromMilliseconds(1);
}
2021-01-19 07:29:07 +00:00
2022-02-23 05:14:32 +00:00
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_TIMER | SDL.SDL_INIT_GAMECONTROLLER) < 0)
{
System.Console.WriteLine("Failed to initialize SDL!");
return;
}
2021-01-19 07:29:07 +00:00
2022-02-23 05:14:32 +00:00
Logger.Initialize();
2021-01-20 02:06:10 +00:00
2022-02-23 05:14:32 +00:00
Inputs = new Inputs();
2021-01-19 07:29:07 +00:00
2022-02-25 21:23:31 +00:00
Window = new Window(windowCreateInfo);
2021-01-19 07:29:07 +00:00
2022-02-23 05:14:32 +00:00
GraphicsDevice = new GraphicsDevice(
Window.Handle,
moonWorksToRefreshPresentMode[presentMode],
debugMode
);
2021-01-19 19:24:23 +00:00
2022-02-23 05:14:32 +00:00
AudioDevice = new AudioDevice();
}
2021-01-19 07:29:07 +00:00
2022-02-23 05:14:32 +00:00
public void Run()
{
2022-04-08 07:03:42 +00:00
#if DEBUG
if (GameState == null)
{
throw new NullReferenceException("Must call SetState before Run!");
}
#endif
2022-02-23 05:14:32 +00:00
while (!quit)
{
2022-04-08 07:03:42 +00:00
Tick();
}
2021-07-23 22:47:02 +00:00
2022-04-08 07:03:42 +00:00
AudioDevice.Dispose();
GraphicsDevice.Dispose();
Window.Dispose();
2021-07-23 22:47:02 +00:00
2022-04-08 07:03:42 +00:00
SDL.SDL_Quit();
}
2021-07-23 22:47:02 +00:00
2022-04-08 07:03:42 +00:00
public void SetState(GameState gameState)
{
GameState = gameState;
GameState.Start();
}
2021-07-23 22:47:02 +00:00
2022-04-08 07:03:42 +00:00
private void Tick()
{
AdvanceElapsedTime();
/* We want to wait until the next frame,
* but we don't want to oversleep. Requesting repeated 1ms sleeps and
* seeing how long we actually slept for lets us estimate the worst case
* sleep precision so we don't oversleep the next frame.
*/
while (accumulatedElapsedTime + worstCaseSleepPrecision < timestep)
{
System.Threading.Thread.Sleep(1);
TimeSpan timeAdvancedSinceSleeping = AdvanceElapsedTime();
UpdateEstimatedSleepPrecision(timeAdvancedSinceSleeping);
}
2021-01-19 07:29:07 +00:00
2022-04-08 07:03:42 +00:00
/* Now that we have slept into the sleep precision threshold, we need to wait
* for just a little bit longer until the target elapsed time has been reached.
* SpinWait(1) works by pausing the thread for very short intervals, so it is
* an efficient and time-accurate way to wait out the rest of the time.
*/
while (accumulatedElapsedTime < timestep)
{
System.Threading.Thread.SpinWait(1);
AdvanceElapsedTime();
}
2021-03-26 19:20:05 +00:00
2022-04-08 07:03:42 +00:00
// Now that we are going to perform an update, let's handle SDL events.
HandleSDLEvents();
2021-01-19 07:29:07 +00:00
2022-04-08 07:03:42 +00:00
// Do not let any step take longer than our maximum.
if (accumulatedElapsedTime > MAX_DELTA_TIME)
{
accumulatedElapsedTime = MAX_DELTA_TIME;
}
2021-01-19 07:29:07 +00:00
2022-04-08 07:03:42 +00:00
if (!quit)
{
while (accumulatedElapsedTime >= timestep)
{
Inputs.Mouse.Wheel = 0;
2021-01-19 07:29:07 +00:00
2022-04-08 07:03:42 +00:00
Inputs.Update();
AudioDevice.Update();
2022-04-08 07:03:42 +00:00
GameState.Update(timestep);
2022-02-19 05:02:16 +00:00
2022-04-08 07:03:42 +00:00
accumulatedElapsedTime -= timestep;
2022-02-23 05:14:32 +00:00
}
2022-02-23 00:44:39 +00:00
2022-04-08 07:03:42 +00:00
var alpha = accumulatedElapsedTime / timestep;
2022-02-23 00:44:39 +00:00
2022-04-08 07:03:42 +00:00
GameState.Draw(timestep, alpha);
}
2022-02-23 05:14:32 +00:00
}
private void HandleSDLEvents()
{
while (SDL.SDL_PollEvent(out var _event) == 1)
{
switch (_event.type)
{
case SDL.SDL_EventType.SDL_QUIT:
quit = true;
break;
case SDL.SDL_EventType.SDL_TEXTINPUT:
HandleTextInput(_event);
break;
case SDL.SDL_EventType.SDL_MOUSEWHEEL:
Inputs.Mouse.Wheel += _event.wheel.y;
break;
}
}
}
private void HandleTextInput(SDL2.SDL.SDL_Event evt)
{
// Based on the SDL2# LPUtf8StrMarshaler
unsafe
{
int bytes = MeasureStringLength(evt.text.text);
if (bytes > 0)
{
/* UTF8 will never encode more characters
2021-03-25 22:57:26 +00:00
* than bytes in a string, so bytes is a
* suitable upper estimate of size needed
*/
2022-02-23 05:14:32 +00:00
char* charsBuffer = stackalloc char[bytes];
int chars = Encoding.UTF8.GetChars(
evt.text.text,
bytes,
charsBuffer,
bytes
);
for (int i = 0; i < chars; i += 1)
{
Inputs.OnTextInput(charsBuffer[i]);
}
}
}
}
2021-03-25 22:57:26 +00:00
2021-07-23 22:47:02 +00:00
private TimeSpan AdvanceElapsedTime()
{
long currentTicks = gameTimer.Elapsed.Ticks;
TimeSpan timeAdvanced = TimeSpan.FromTicks(currentTicks - previousTicks);
accumulatedElapsedTime += timeAdvanced;
previousTicks = currentTicks;
return timeAdvanced;
}
/* To calculate the sleep precision of the OS, we take the worst case
* time spent sleeping over the results of previous requests to sleep 1ms.
*/
private void UpdateEstimatedSleepPrecision(TimeSpan timeSpentSleeping)
{
/* It is unlikely that the scheduler will actually be more imprecise than
* 4ms and we don't want to get wrecked by a single long sleep so we cap this
* value at 4ms for sanity.
*/
var upperTimeBound = TimeSpan.FromMilliseconds(4);
if (timeSpentSleeping > upperTimeBound)
{
timeSpentSleeping = upperTimeBound;
}
/* We know the previous worst case - it's saved in worstCaseSleepPrecision.
* We also know the current index. So the only way the worst case changes
* is if we either 1) just got a new worst case, or 2) the worst case was
* the oldest entry on the list.
*/
if (timeSpentSleeping >= worstCaseSleepPrecision)
{
worstCaseSleepPrecision = timeSpentSleeping;
}
else if (previousSleepTimes[sleepTimeIndex] == worstCaseSleepPrecision)
{
var maxSleepTime = TimeSpan.MinValue;
for (int i = 0; i < previousSleepTimes.Length; i++)
{
if (previousSleepTimes[i] > maxSleepTime)
{
maxSleepTime = previousSleepTimes[i];
}
}
worstCaseSleepPrecision = maxSleepTime;
}
previousSleepTimes[sleepTimeIndex] = timeSpentSleeping;
sleepTimeIndex = (sleepTimeIndex + 1) & SLEEP_TIME_MASK;
}
2021-03-25 22:57:26 +00:00
private unsafe static int MeasureStringLength(byte* ptr)
{
int bytes;
2022-02-23 05:14:32 +00:00
for (bytes = 0; *ptr != 0; ptr += 1, bytes += 1) ;
2021-03-25 22:57:26 +00:00
return bytes;
}
2022-02-23 05:14:32 +00:00
}
2021-01-19 07:29:07 +00:00
}