Game Development Community

dev|Pro Game Development Curriculum

Upfront Datablock Loading

by Dave Young · 09/22/2008 (11:47 am) · 14 comments

Download Code File

Speed loading/persistance ho!

First, a video of speedloading in action, or the effect of it anyway, VERY fast mission loading.
Speedloading in Cubekind

Second, a video of speedloading in action in stock ArcaneFX ComboPack, of which the number of datablocks is a well known quantity
Speedloading in ArcaneFX


I have often noted that I do not change datablocks among server instances, and my favorite kinds of projects to work on are the kinds that need you to jump from server to server, like RPGs and Cubekind. So the datablock loading process has always bugged me. Especially now that I've got lots of other things working like server instancing and multiple varieties of server to server zoning working.

I am also an ArcaneFX addict, and it needs some heavy datablock definitions to get the most bang and variety. So I usually end up with near 4000 datablocks just in a prototype/Alpha, and could probably double that number with some serious content work.

To wit, I have a need to not reload datablocks on mission loads. I want them to stick around and skip that part of the mission loading process. Furthermore, I want the datablocks to load up while credits/titles are loading, as this is an acceptable place for a front load.

The nature of the process is to create and connect to a local server in an optimized way. A mission is not loaded, but the server does its normal exec process and loads all its datablocks. The client then connects locally to the server and disconnects when datablock transmission is complete, setting a flag that says it has the datablocks loaded. From there on in, wether connecting to a local server or remote server, datablocks are not transmitted.

Assumptions:
1) Datablocks are not going to change between server instances ie they all have the same execs and order of execs.
2) Server code/assets goes out with the distribution, making the local server possible

If I did not want to distribute server code I would look further into pulling code out of Jeff Faust's ArcaneFX datablock caching scheme to create a binary datablock cache file and load that instead on startup. That would be really sweet indeed. Nevertheless, I've had troubles with hashes etc and this process has been working very nicely.

Let's begin! Anywhere I use the word game as part of a path, replace with your own... starter.fps, arcane.fx, whatever. It just means the main game folder.

Instructions:
The attached script file can be placed in your game/client/scripts folder and contains NEW functions which are also included here.

in game/client/init.cs

near the end of function initClient()

above:
// Default player key bindings
   exec("./scripts/default.bind.cs");
add:

exec("./scripts/preloader.cs");

This takes care of loading up the preloader functions for us as soon as the client scripts are loaded. I did it this way so that if there were any client datablocks created, hopefully they had a chance to run and sound profiles get setup, etc. Early on I experimented with having the load happen at the top of initClient, but results were not consistent.

What happens now is that the loadLoaderServer function gets called, and that creates a tiny server with no mission file and the client will connect to it when it's done loading.

function doLoaderServer()
{
      createLoaderServer("Loader");
      %conn = new GameConnection(LoaderServerConnection);
      RootGroup.add(LoaderServerConnection);
      %conn.LoaderServer = true;
      %conn.setConnectArgs("LoaderConnect","LoaderConnect","LoaderConnect");
      %conn.setJoinPassword($Client::Password);
      %conn.connectLocal();
}

The client connect/disconnect process is tightly bound, so we are going to do several things to make it easier to tell later on in other parts of code that this connection is a special connection only used to load stuff up. We did this by naming it something besided ServerConnection, attaching a dynamic field named LoaderServer onto the object, and also setting its connect args to "LoaderConnect" in all the available places. The connect args are needed because when the server gets the connect request, it can know that the connection is a special type and must be treated specially.

The createLoaderServer function is really just a specialized version of createServer().
It sets a flag that will be needed later ($LoaderServer) which can be used while execing, etc to skip anything that doesn't need to happen
in this special mini server. It also says $missionRunning = true, so that the normal datablock load sequence can take place.

function createLoaderServer()
{
   echo("*********Datablock Preload*********");
   $missionSequence = 0;
   $LoaderServer = true;
   allowConnections(false);
   
   $ServerGroup = new SimGroup(ServerGroup);

   //This loads up all the datablocks, as normal   
   onServerCreated();
   
   $MissionRunning = true;
}

Now that the server is loaded, the client continues its local connection process and connects locally to the running server.
I frequently use extra arguments in the connection process to do special things in the load order, so I add a few extra arguments (which the engine already supports) in addition to the normal %client and %name arguments.

Now, let's shortcircuit the normal loading process, all we want is a server-provided datablock stream after all.

In common/server/clientConnection.cs, change the function declaration to:
function GameConnection::onConnect( %client, %name, %extra,%extra2)

and add in, near the top:
if(%extra $= "LoaderConnect")
   {
      echo("LoaderConnect");
      %client.LoaderConnect = true;
      %client.transmitDataBlocks($missionSequence);
      return;
   }

This has the effect of flagging the connection as a loaderConnect type connection, because we might need to know this later to do special handling on it or skip other processing. We also start transmitting datablocks instead of telling the connection to load the mission.

This causes function GameConnection::loadMission in game/server/commands.cs to run, the last line of which is:
commandToClient(%this, 'MissionStartPhase1', $missionSequence, $Server::MissionFile, MissionGroup.musicTrack, %cache_crc);

From here, datablock transmission is handled as normal. When the client has all the datablocks, we don't have need of the server anymore, so we destroy it and tell the client to drop.

In common/server/missionDownload.cs, at the top of:
function GameConnection::onDataBlocksDone( %this, %missionSequence )

add:
//Special handling, kill the conn, destroy the server
   if($LoaderServer == true)
   {
      echo("Datablock loading complete, loader server going bye bye");
      commandToClient(%this, 'DatablocksLoaded');
      destroyLoaderServer();
      return;
   }

The client gets told that the datablocks are loaded, and it's their job to then record that and delete the connection.
The $DataBlocksLoaded global is used to determine where in the preload process we are, as this should happen as the first title screens are loading. The $SkipDataBlocks global is almost the same thing, but has its own use on future mission load sequences.

function ClientCmdDatablocksLoaded()
{
   $SkipDataBlocks = true;
   $DataBlocksLoaded = true;
   LoaderServerConnection.delete();
   echo("Datablocks loaded");
}

After the command is sent to the client, the server destroys itself. This causes the server to suicide, and we don't want to lose all the datablocks we have loaded up, so we have a special version of destroyServer called destroyLoaderServer which skips the deleting of datablocks and purging of resources.

function destroyLoaderServer()
{
   $Server::ServerType = "";
   allowConnections(false);
   stopHeartbeat();
   $missionRunning = false;
   
   if (isObject($ServerGroup))
      $ServerGroup.delete();
 
   $Server::GuidList = "";
}

How do we kick this all off?
Well, I wanted to impact as few existing files as possible, so I chose to modify the loading Guis process.

in game/client/ui/StartupGui.Gui, in function loadStartup, add these lines at the top of the function
$DataBlocksLoaded = false;
   doLoaderServer();

And, in function checkStartupDone,
change:

if (StartupGui.done)

to:
Canvas.repaint;
if ($DataBlocksLoaded)

When the gui opens up, the preload process begins, and this will make sure the datablock loading is done during the first credit screen. I also chose to add a progress bar and hook into the normal datablock progression code, but I leave that as an exercise for the reader.

Now we have datablocks loaded up, before we hit the main menu. This takes about 7-10 seconds for me, with all arcaneFX spells loaded and an additional 600 datablocks, putting my number of datablocks near 4000.

The next part of this is to SKIP the datablocks on future mission loads. We used a variable named $SkipDatablocks to set this state, after all our datablocks were loaded.


In game/client/scripts/missionDownload.cs:
function clientCmdMissionStartPhase1
after the echos:
if ($skipdatablocks==true)
  {
     echo("<<<< skip datablocks >>>>");
     // skip datablock transmission and initiate a cache load
      onMissionDownloadPhase1(%missionName, %musicTrack, "PRELOADING DATABLOCKS");
      commandToServer('MissionStartPhase1Ack_UseExistingDatablocks', %seq);
      return;
   }

This is called in the normal mission load process. From here, the rest of the custom loading functions take over. They are all contained in preloader.cs.

function serverCmdMissionStartPhase1Ack_UseExistingDatablocks(%client, %seq)
{
  echo("    <<<< skipping datablock transmission >>>>");

  // Make sure to ignore calls from a previous mission load
  if (%seq != $missionSequence || !$MissionRunning)
    return;
  if (%client.currentPhase != 0)
    return;
  %client.currentPhase = 1;

  // Start with the CRC
  %client.setMissionCRC( $missionCRC );
  %client.onBeginDatablockExistingLoad($missionSequence);
}

function GameConnection::onBeginDatablockExistingLoad( %this, %missionSequence )
{
   // Make sure to ignore calls from a previous mission load
   if (%missionSequence != $missionSequence)
      return;
   if (%this.currentPhase != 1)
      return;
   %this.currentPhase = 1.5;
   commandToClient(%this, 'MissionStartPhase1_LoadExisting', $missionSequence, $Server::MissionFile);
}

function clientCmdMissionStartPhase1_LoadExisting(%seq, %missionName)
{
  if ($skipdatablocks==true)
  {
    echo("<<<< Skipping Datablocks");
    schedule(10, 0, "updateLoadExistingDatablockProgress", %seq, %missionName);
  }
}

function updateLoadExistingDatablockProgress(%seq, %missionName)
{
   echo("<<<< Finished Skipping Datablocks >>>>");
   clientCmdMissionStartPhase2(%seq,%missionName);
}


That really takes care of things from the script side of things. The one last thing to keep in mind is that the client's disconnect() function also calls destroyServer(). destroyServer() has a call in it to deleteDatablocks().

I am using custom disconnect functionality, so if you are using stock disconnect, you will want to add a conditional over it, like this:
if(!$datablocksloaded)
deleteDataBlocks();

For the engine side, use a different transmitDatablocks function. There are some good alternatives in this resource:
http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=12879

I used the versions given by Daniel Eden with the changes noted by Ed Zavada Posted: (Mar 17, 2008 at 23:47). Very nice resource!!

Speed loading ho!! Please post improvements, etc.

#1
09/17/2008 (3:13 pm)
Impressive work!

PS: Why does the rating only go to 5 ;)

Keep it up DAve :P
#2
09/17/2008 (5:09 pm)
Thanks Christian.

As an aside, I did some initial work as noted above in using the arcanefx datablock caching to save the datablocks, and look for it at the top of the doloadserver() process. This allowed me to load the cached datablock file and not have to run the mini server and connect to it, further reducing the initial front load time by about 25%.

I won't post changes here as they were fairly more intricate and need more testing, but suffice it to say that it's a valid goal if you need to further reduce load times. My suggestion would be to create and store the binary datablock cache file as part of an automatic update process, ie include it and maintain it in your distribution and updating mechanic.
#4
09/23/2008 (3:05 am)
Wow, nice work. Can't wait to implement this one, long mission loading times are one of my current annoyances.
#5
09/23/2008 (5:41 am)
The processes involved should work in either TGE or TGEA. Don't forget to apply the linked resource at the bottom of the description, the change to TransmitDatablocks, etc. I also got those crashes when casting a spell without them.
#6
09/23/2008 (8:31 pm)
Thanks Dave! This is exactly what I need for my game.
#7
09/24/2008 (8:45 am)
Just a note, one of these days I've really got to dig in and find out why various implementations of transmitDatablocks cause miscellaneous particle (and effect) textures to not be loaded. It's been a somewhat common issue in most implementations of local datablock speed loading enhancements. The resource I linked does *not* seem to exhibit the behavior, but it would be good to know why...

If anyone has already looked into this, drop a line!
#8
11/12/2008 (4:28 pm)
What can I say but WOW!!! I add this and my load times are almost not even visible. Thanks a ton.
#9
06/19/2009 (1:03 am)
Awesome, definately going to implement this.
#10
11/29/2009 (6:33 pm)
Hey Dave, great resource, sped my load times up to about 4 seconds flat. I've having a bad time with particle effects though, they're just black squares now, which I'm guessing means that they are not loading properly when the datablocks are loaded. You mentioned it above, was curious if you had made any headway on this at all.

Thanks
#11
11/30/2009 (4:36 pm)
Ok I got it working in T3D, pretty flawlessly, or atleast haven't gotten any errors since making my changes.

Here is the updated T3D preloader.cs, gonna post it instead of hosting a file so it never gets deleted.
function doLoaderServer()
{
      createLoaderServer("Loader");
      %conn = new GameConnection(LoaderServerConnection);
      RootGroup.add(LoaderServerConnection);
      %conn.LoaderServer = true;
      %conn.setConnectArgs("LoaderConnect","LoaderConnect","LoaderConnect");
      %conn.setJoinPassword($Client::Password);
      %conn.connectLocal();
}

function createLoaderServer(%serverType)
{
   echo("*********Datablock Preload*********");   
   $Server::MissionType = "";
   
   $missionSequence = 0;
   $LoaderServer = true;
   allowConnections(false);
   
   $ServerGroup = new SimGroup(ServerGroup);
   
   //This loads up all the datablocks, as normal   
   onServerCreated();
   
   $MissionRunning = true;
}



function destroyLoaderServer()
{
   $Server::ServerType = "";
   allowConnections(false);
   stopHeartbeat();
   $missionRunning = false;
   
   if (isObject(MissionGroup))
      MissionGroup.delete();
   if (isObject(MissionCleanup))
      MissionCleanup.delete();
   if (isObject($ServerGroup))
      $ServerGroup.delete();   
 
   $Server::GuidList = "";
}

function ClientCmdDatablocksLoaded()
{
   $skipdatablocks = true;
   $DataBlocksLoaded = true;
   destroyLoaderServer();
   echo("Datablocks loaded");
   loadMainMenu(); // Change this is you want to go somewhere other than the main menu.
   
}

/// Skip datablock functionality

function serverCmdMissionStartPhase1Ack_UseExistingDatablocks(%client, %seq)
{
  echo("    <<<< skipping datablock transmission >>>>");

  // Make sure to ignore calls from a previous mission load
  if (%seq != $missionSequence || !$MissionRunning)
    return;
  if (%client.currentPhase != 0)
    return;
  %client.currentPhase = 1;

  // Start with the CRC
  %client.setMissionCRC( $missionCRC );

  %client.onBeginDatablockExistingLoad($missionSequence);
}

function GameConnection::onBeginDatablockExistingLoad( %this, %missionSequence )
{
   // Make sure to ignore calls from a previous mission load
   if (%missionSequence != $missionSequence)
      return;
   if (%this.currentPhase != 1)
      return;
   %this.currentPhase = 1.5;
   commandToClient(%this, 'MissionStartPhase1_LoadExisting', $missionSequence, $Server::MissionFile);
}

function clientCmdMissionStartPhase1_LoadExisting(%seq, %missionName)
{
  if ($skipdatablocks==true)
  {
    echo("<<<< Skipping Datablocks");
    schedule(10, 0, "updateLoadExistingDatablockProgress", %seq, %missionName);
  }
}

function updateLoadExistingDatablockProgress(%seq, %missionName)
{
   echo("<<<< Finished Skipping Datablocks >>>>");
   clientCmdMissionStartPhase2(%seq,%missionName);
}

Everything else is pretty much the same if I remember correctly. What was happening is I was randomly crashing after the datablocks had finished loading, as well as a few errors. The above seems to have fixed that, if any problems let me know.

Note: Forgot to add this, when using this resource with T3D you do not need to do the engine change discussed above as that functionality is already built in to the engine.
#12
11/30/2009 (5:06 pm)
Nice job! Thanks for posting an update for the original resource. It is quite impressive when you're used to seeing a long loading screen. ziiip!
#13
01/20/2011 (7:08 pm)
Has anyone been able to get this working properly with the lateest version of AFX for T3D 1.1 beta 3? The code seems to work correctly, meaning it loads datablocks ahead of the missions but once a mission is loaded, all of the spells are represented by black or white planes rather than their correct material formats.(Flames that would display fire with proper transparency are now either solid black or white) Also selectrons do not work at all.
#14
05/03/2011 (10:07 am)
Dave, wonderful resource! Thank you!

Just wanted to add my 2 cents here - it works if you also supply the server scripts with the client. For dedicated server - client scenarios where server files are not distributed in the game package it is best to have your versions of the onConnect and onDatablocksDone methods on the client side and keep the server side versions intact.

Thanks again! Saved me lots of time.