So I didn't quite get there in terms of having this release ready. But it's coming along quite well. A few performance things linger, but I can get between 40-60 fps during fights where I was previously getting a choppy 5fps. So it has come a really long way.
Unfortunately my trick with the camera FOV to make all the ships look larger doesn't play well with all the various circles and spheres the game uses for things like forcefields and selection circles, etc. They get super distorted by a larger field of view, that sort of fish-eye effect like you get off a Go Pro camera.
The idea is that with a larger field of view, things look larger (which is true), and that works well for something like a racing game where you're down low, or even for a FPS game where you're looking out of the eyes of the character and the stuff at screen edges is your "peripheral vision." It's one of the reasons why console games often feel so wrong on PC ports, because we sit a different distances from those screens.
At any rate, for a strategy game it just wasn't working out. We're actually using a 40 degree field of view, rather than the default 60, but then with foreshortening on the camera zoom to fake a wider field of view in some ways. This eliminates all edge distortion, which you could see even at 60 (we did this months ago), while letting you still "see as much stuff" as if the fov was wider.
But unfortunately it doesn't contribute to things feeling "large," or at least not "close and large," because the lines of convergence on the invisible horizon are not very sharply inward with that sort of fov. Such is life. It looks the same now as it did last version and the last few months.
I've had a breakthrough, finally, on the background starfields, though. I was unhappy with some of the existing ones we had, and it's annoying that a skybox takes 6 setpass calls. And that I couldn't really customize them much, even with my own hsv-enabled shader for them.
I wound up creating my own new shader that blends a number of textures together using a variety of methods, to come up with something that can be a lot prettier. It takes a lot of fiddling to really get a good result, but then you can save that and use that from then on. The resources are low in terms of what it saves in the game directory, and in ram, and it has a huge amount of flexibility. It also only uses one setpass call.
It can't be randomized because most random values would look like utter nonsense (not just like bad space graphics, but just a riot of nonsensical stuff). It takes a dedicated hand to tune a bunch of things with this and get something worth saving, and it's probably a 20ish minute process at the moment, minimum. But it's worth it! And other people will be able to add to that, not just me, which is exciting for sure. Yay variety.
I'm in the process of switching all the graphics to use a new reflection map that is much more studio-quality for photography, and to using an HDR camera. I needed some idea of how the space backgrounds were going to look in this new system so that ideally they would be dark enough not to get picked up by the bloom levels, which can now be set to only work on light levels above 1, or something along those lines. I'm going to have to redo the entire post-processing visual stack in general, but the groundwork has all been laid for that.
If you wanna nerd out about the details of the SwizzleList, I always like hearing about interesting data structures.
It's stupidly simple, really. I went looking into all sorts of data structures, things like AList and DList and B trees and Hash sets and even things like linked lists, but everything just had too many downsides. We had been using just a generic List<>, which worked well for iterating (which we do a ton of) and adding at the end (which we do a ton of), but not for removing from arbitrary positions (which we also do a ton of). So we'd have spikes of huge amounts of ms every time it had to remove something from low in the list, because then all the other items had to move down.
I got really frustrated with the other data structures, because they're all so complicated and only sort of improve things. And many of them waste GC resources or reduce the quality of the iteration time below O(n). I needed speed on those three things, and just nothing around was really matching up with what I wanted.
Then I started thinking about how I've used swapping lists of things in the past, when doing pathfinding and other logic, to save on GC.
Then I realized that the arrays themselves are incredibly tiny (in terms of data size for the array and their pointers themselves), and I only need like ten at most anyway, and I can predict really safe WAY upper bounds on them.
I also realized that since I'm pooling the data that goes in these, I don't have any need for things to be released for the GC. If a reference gets held for a long time, it affects nothing.
So... I decided to make SwizzleList, which basically has two raw arrays in it, which you initialize to some super large size. For shots, I chose half a million right now.
One list is active, the other is inactive. When you add stuff, it goes in the active list at whatever the next slot is, then it increments a length integer. That's "how much stuff is in the list," as far as the outside world is concerned. You can't remove things from the list, but you can iterate over it in equal time to what you normally would.
At some point you hit a period where things have been flagged for removal outside of this list, and it's time to make a new, condensed list. So basically what it does is it clears the inactive list, loops over all the items in the active list, runs some logic for each one (of my choosing), and then as it does, any that are "not flagged for removal" get put into the inactive list. At the end of that one iteration, I swap which list is active and which list is not. And boom, things are "deleted." I had to do that loop for deletion checks anyway, so the only added cost is copying to the inactive list, which is super cheap.
I feel kinda embarrassed about it because this is not a sophisticated data structure at all, and in many ways it's just a "duh" sort of thing. But, well, I haven't seen anyone else do this for this sort of purpose, and it's wicked fast. If you needed the GC to work, needed to sort, or needed to realtime remove things during list processing, this would not work well. Or if you needed lots of these, of unknown size. But for a pooled set of things with a known upper bound that you can exceed by an order of magnitude without a real ram hit... works very well.
I attached the code for the list itself, and then this is the code that uses it:
#region HandleShotUpdates
public void HandleShotUpdates()
{
float deltaTime = Engine_AIW2.DeltaTime;
GameSettings_AIW2 settings = GameSettings_AIW2.Current;
bool drawAllShotRadii = settings.Debug_DrawAllRadiiAtAllTimes;
bool drawShotDebugging = settings.Debug_DrawShotDebuggingData;
Engine_Universal.BeginProfilerSample( "ShotRemovalChecks" );
int shotLength;
ArcenVisualShot[] shots = ActiveShots.GetActiveList( out shotLength, true );
#region Removal Checks
{
int maxShotsToHandle = Mathf.CeilToInt( settings.Performance_ShotRemovalChecksToAttemptPerSecond * deltaTime );
int min = settings.Performance_ShotRemovalChecksMinPerFrame;
if ( maxShotsToHandle < min )
maxShotsToHandle = min;
int shotsHandled = 0;
try
{
ArcenVisualShot shot;
for ( int i = 0; i < shotLength; i++ )
{
shot = shots[i];
if ( shotsHandled < maxShotsToHandle && shot.RemovalChecks_LastAnimationCycle != this.CurrentShotRemovalChecksPass )
{
shot.RemovalChecks_LastAnimationCycle = this.CurrentShotRemovalChecksPass;
shotsHandled++;
shot.DoRemovalChecks( currentSimFrameVisualOnly );
}
if ( shot.IsConsideredActive )
ActiveShots.AddToInactiveListNoChecks( shot );
}
ActiveShots.SwitchActiveList( true );
if ( shotsHandled < maxShotsToHandle )
{
this.CurrentShotRemovalChecksPass++;
if ( this.CurrentShotRemovalChecksPass > 10000 )
this.CurrentShotRemovalChecksPass = 1;
}
if ( Engine_Universal.IsProfilerEnabled )
Engine_Universal.LodgeMessageInProfilerData( "Total: " + shotLength + ", Handled: " + shotsHandled );
}
finally
{
Engine_Universal.EndProfilerSample( "ShotRemovalChecks" );
}
}
#endregion
shots = ActiveShots.GetActiveList( out shotLength, false );
Engine_Universal.BeginProfilerSample( "ShotMainUpdates" );
if ( Engine_Universal.IsProfilerEnabled )
{
Engine_Universal.LodgeMessageInProfilerData( "Count: " + shotLength );
}
try
{
ArcenVisualShot shot;
for ( int i = 0; i < shotLength; i++ )
{
shot = shots[i];
shot.DoMainUpdate( this, drawShotDebugging, drawAllShotRadii );
}
}
finally
{
Engine_Universal.EndProfilerSample( "ShotMainUpdates" );
}
{
Engine_Universal.BeginProfilerSample( "ShotMovement" );
int maxShotsToHandle = Mathf.CeilToInt( settings.Performance_ShotMovementsToAttemptPerSecond * deltaTime );
int min = settings.Performance_ShotMovementsMinPerFrame;
if ( maxShotsToHandle < min )
maxShotsToHandle = min;
int shotsHandled = 0;
try
{
ArcenVisualShot shot;
for ( int i = 0; i < shotLength; i++ )
{
shot = shots[i];
if ( !shot.IsConsideredActive || !shot.DidMainUpdate )
continue;
shot.ShotMovement_AccumulatedDeltaTime += deltaTime;
if ( shotsHandled < maxShotsToHandle && shot.ShotMovement_LastAnimationCycle != this.CurrentShotMovementPass )
{
shot.ShotMovement_LastAnimationCycle = this.CurrentShotRemovalChecksPass;
shotsHandled++;
shot.DoShotMovement( drawShotDebugging );
shot.ShotMovement_AccumulatedDeltaTime = 0f;
}
}
if ( shotsHandled < maxShotsToHandle )
{
this.CurrentShotMovementPass++;
if ( this.CurrentShotMovementPass > 10000 )
this.CurrentShotMovementPass = 1;
}
if ( Engine_Universal.IsProfilerEnabled )
Engine_Universal.LodgeMessageInProfilerData( "Total: " + shotLength + ", Handled: " + shotsHandled );
}
finally
{
Engine_Universal.EndProfilerSample( "ShotMovement" );
}
}
}
#endregion