Game Development Community

dev|Pro Game Development Curriculum

Get information from datablocks without loading them.

by Richard Ranft · 07/13/2014 (10:28 am) · 17 comments

I wanted to be able to select a player model from a list and use the GuiObjectView to preview the selection. I discovered that the datablocks aren't loaded until level load and so are unavailable when choosing your first level (probably later ones, but I never checked). I also discovered that loading the datablocks when I showed the dialog was slow.

What I needed was a way to go and find my datablocks, then sift through them for their name and shapefile fields. This is the heart of what I came up with:
$PlayerDatablocks = new SimSet()
{
    class = "PlayerDatablockList";
};

function PlayerDatablockList::init(%this)
{
    %this.clearDatablocks();
    %this.loadData();
}

function PlayerDatablockList::addDatablock(%this, %datablock)
{
    if(!%this.isMember(%datablock))
        %this.add(%datablock);
}

function PlayerDatablockList::clearDatablocks(%this)
{
    while(isObject(%obj = %this.getObject(0)))
    {
        %this.remove(%obj);
    }
    %this.clear();
}

function PlayerDatablockList::loadData(%this, %path)
{
    // find main.cs
    %search = "art/datablocks/aiPlayer/*.cs";
    // search for scripts and load them
    for( %file = findFirstFile( %search ); %file !$= ""; %file = findNextFile( %search ) )
    {
        %this.readDatablockInfo(%file);
    }
}

function PlayerDatablockList::readDatablockInfo( %this, %fileName )
{
    %infoObject = new ScriptObject();
    %infoObject.shapeFileInfo = "";
    %infoObject.datablockName = "";
    %file = new FileObject();
    %validFile = false;

    if ( %file.openForRead( %fileName ) )
    {
        %validFile = true;
        %inInfoBlock = false;
        %dbNameFound = false;
		
        while ( !%file.isEOF() )
        {
            %line = %file.readLine();
            %line = trim( %line );
            %tempLine = strreplace(%line, "datablock PlayerData(", "");
			
            if( %line !$= %tempLine ) // deduction by negatives - this is the start of a PlayerData datablock
                %inInfoBlock = true;
            else if( %inInfoBlock && %line $= "};" )
            {
                %inInfoBlock = false;
                %infoObject = %infoObject @ %line; 
                break;
			}
			
			if( %inInfoBlock )
			{
			    if (!%dbNameFound)
			    {
			        // find datablock name
			        %schloc = strchr(%tempLine, ":");
			        %dbName = trim(strreplace(%tempLine, %schloc, ""));
			        %infoObject.datablockName = %dbName;
			        %dbNameFound = true;
			    }
			    else
			    {
			        %tempLine = strreplace(%line, "shapeFile = ", "");
			        if (%line !$= %tempLine) // found the shapefile entry
			        {
			            %sfName = trim(strreplace(%tempLine, """, ""));
			            %sfName = strreplace(%sfName, ";", "");
			            %infoObject.shapeFileInfo = trim(%sfName);
			        }
			    }
			}
		}
		
		%file.close();
    }
    else
    {
	    %validFile = false;
        error("Datablock file " @ %fileName @ " not found.");
    }

	%file.delete();
	%this.addDatablock(%infoObject);
}

$PlayerDatablocks.init();
This can be modified to scrape other information from files, or whatever.

After I aggregated my list I dynamically populated the scroll list for selection, so any time I add a new player-usable model/datablock combo it becomes available automatically.

About the author

I was a soldier, then a computer technician, an electrician, a technical writer, game programmer, and now software test/tools developer. I've been a hobbyist programmer since the age of 13.


#1
07/13/2014 (10:52 am)
Whoa - I forgot to drop this in. You have to modify the functions in the chooseLevelDlg.gui and joinServerDlg.gui files:
function ChooseLevelDlgGoBtn::onMouseUp( %this )
{
    if ($Game::SelectedPlayerClass $= "")
    {
        // get first in container
        %btn = ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.getObject(0);
        $Game::SelectedPlayerClass = %btn.selectedDatablock;
    }
   // So we can't fire the button when loading is in progress.
   if ( isObject( ServerGroup ) )
      return;

   // Launch the chosen level with the editor open?
   if ( ChooseLevelDlg.launchInEditor )
   {
      activatePackage( "BootEditor" );
      ChooseLevelDlg.launchInEditor = false; 
      StartLevel("", "SinglePlayer");
   }
   else
   {
      StartLevel(); 
   }
}
function JoinServerDlg::join(%this)
{
   cancelServerQuery();
   %index = JS_serverList.getSelectedId();

   // The server info index is stored in the row along with the
   // rest of displayed info.

   if( setServerInfo( %index ) )
   {
      Canvas.setContent("LoadingGui");
      LoadingProgress.setValue(1);
      LoadingProgressTxt.setValue("WAITING FOR SERVER");
      Canvas.repaint();

      %conn = new GameConnection(ServerConnection);
      %conn.setConnectArgs($pref::Player::Name, $Game::SelectedPlayerClass);
      %conn.setJoinPassword($Client::Password);
      %conn.connect($ServerInfo::Address);
   }
}
Then, in your game you have to catch the $Game::SelectedPlayerClass variable when you hit your game's <game>::spawnPlayer() function.
#2
07/13/2014 (10:56 am)
Damn....

You also need to grab it in core/scripts/server/server.cs:
function createAndConnectToLocalServer( %serverType, %level )
{
   if( !createServer( %serverType, %level ) )
      return false;
   
   %conn = new GameConnection( ServerConnection );
   RootGroup.add( ServerConnection );

   %conn.setConnectArgs( $pref::Player::Name, $Game::SelectedPlayerClass);
   %conn.setJoinPassword( $Client::Password );
   
   %result = %conn.connectLocal();
   if( %result !$= "" )
   {
      %conn.delete();
      destroyServer();
      
      return false;
   }

   return true;
}
Sorry all - it was a few months ago when I did this bit... forgot about some of it....
#3
07/13/2014 (1:33 pm)
Neat-o, now I imagine this could also be extended to other data formats like checking on the server if a map has a certain game-mode specific simgroup before loading it?
#4
07/13/2014 (2:27 pm)
I think there is already support for that sort of thing in the mission loading code - you can specify a number of properties in the mission file itself, like game type. Perhaps it could expand on that, though.

It is useful for anything where you want to look through a text file for information, really - you could use it implement a simple flat-file database with a little more work. Using SQLite or MySQL or some such would be more powerful, but might be overkill. Sometimes a super simple solution is enough.
#5
07/13/2014 (3:22 pm)
Instant bookmark. I'm doing a bit of level work atm, but when I get back to my GUI I'll definitely give this a go. As a matter of fact, I have login, level selection, and game mode selection GUI's but on the 'character' tab it's empty atm waiting for something just like this! Thanks! I'll also upload a vid of that T3D drag-drop inventory for ya when I get to the GUI again Richard =)
#6
07/13/2014 (5:08 pm)
I should probably push my whole character model selection setup to github....
#7
07/13/2014 (5:37 pm)
@Richard Awesome, I'll have to look for that.
#8
07/13/2014 (6:13 pm)
This is great, thanks Richard!

Just a thought ... before making it part of any "Standard" build;

Would be cool if the character selection "list" was created dynamically -- rather than having it hard coded for specific models.

Then on a new build you just setup a couple actors (datablocks, etc) as possible characters and voila!

#9
07/13/2014 (7:12 pm)
It is dynamically created - by scanning a particular folder for the datablock files and then getting the names of the datablocks in those files.
function PlayerDatablockList::loadData(%this, %path)
{
    // find aiPlayer datablocks
    %search = "art/datablocks/aiPlayer/*.cs";
    // search for scripts and load them
    for( %file = findFirstFile( %search ); %file !$= ""; %file = findNextFile( %search ) )
    {
        %this.readDatablockInfo(%file);
    }
}
The purpose of the PlayerDatablockList script object is just that - to find player datablocks and store a list of them, along with other information from them, without actually loading the datablocks. You then iterate through the list and generate the necessary GUI elements:
function ChooseLevelDlg::onDialogPush(%this)
{
    ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.setVisible(false);
    ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.clear();
    ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.position = "0 0";

    %count = $PlayerDatablocks.getCount();
    for (%i = 0; %i < %count; %i++)
    {
        %this.addPlayerButton($PlayerDatablocks.getObject(%i));
    }
    ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.refresh();
    ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.setVisible(true);
    cldlgPlayerPreview.cameraRotation = "0 0 3.14";
}

function ChooseLevelDlg::addPlayerButton(%this, %data)
{
    %button = new GuiButtonCtrl()
    {
        extent = "118 30";
        text = %data.datablockName;
    };
    %command = "cldlgPlayerPreview.setModel("@%data@".shapeFileInfo);cldlgPlayerPreview.cameraRotation = "0 0 3.14";";
    %command = %command @ "$Game::SelectedPlayerClass = "@%data.datablockName@";";
    %button.command = %command;
    %button.selectedDatablock = %data.datablockName;
    ChooseLevelWindow->clPlayerBox->PreviewScroll->PlayerPreviews.add(%button);
}
For those unfamiliar with it, the "->" operator lets you point to member objects ("child" objects) by internal name. Very handy.
www.roostertailgames.com/images/ChooseLevel01.pngwww.roostertailgames.com/images/GameUI01.png
I don't think there's any danger of this becoming any sort of standard. I wouldn't want that anyway - every game has different needs and this wouldn't fit every game.
#10
07/14/2014 (7:27 am)
That's cool Richard!

thankyou(verymuch);
#11
07/14/2014 (8:57 am)
I'll have to drop my other changes some time soon - the Twillex-driven rollouts are only partially ready. I'm going to add drag-drop to them so you can drag to them from other controls and rearrange the buttons within each rollout. I had to add an onResize() callback to GameTSCtrl to handle positioning the rollouts when the game was resized.
#12
07/14/2014 (4:06 pm)
Or you could move the exec() of the effected torquescript files (that declare your datablocks) out of server starting script code to when main menu is suppose to load instead. ;)
#13
07/14/2014 (5:44 pm)
I did that - very slow. UI is supposed to be responsive - if it takes 2 or 3 seconds for a simple dialog to load it's really annoying. And loading datablocks on the client isn't best practice. And you really don't need the whole datablock loaded for a simple preview of the model.

My way is rarely the "best" way, but I usually try a few things before I settle on a solution.
#14
07/16/2014 (4:01 am)
cool stuff! thanks for sharing.
#15
07/17/2014 (1:28 am)
Bookmarked :D
I use to load and then unload datablocks in my game's "shop" i'm quite happy with that, but is always useful to see a more elegant way!
Thanks!
#16
07/19/2014 (10:18 am)
That's awesome Richard, but what happens if you only release .dso files. You can't read the files anymore ;)
#17
07/25/2014 (7:21 am)
@Jeff -
You can still use a text file with the relevant data extracted from the actual datablocks....

Or you could get TAML working and then use the binary TAML format. Added bonus - you could use a TAML visitor object to search even faster under the right conditions.

There might be a way to decode a .dso without actually executing it....

The main take-away is that you can read through text files and extract what you need. It is probably more useful for beginners than all of you old-timers.... ;p