Porting a Legacy Project
One of the major changes is the shift to the module-based BaseGame template as opposed to the preconfigured Full or Empty templates from before. While most changes won't impact a given project unless there was heavy modifications in a wide area of the scripts, some things - such as adopting the module system to help compartmentalize and organize your game's scripts and content - are important.
The big one, as mentioned, is converting your game's scripts and content to a module, or modules, for compartmentalization and organizational purposes.
The easiest way, but doesn't see much of a shift in organization or compartmentalization. The first thing you'll want to do is create a new folder in the data directory of the BaseGame template, calling it something relevent, and convenient. For our example's case, we'll call it MyGame.
Setting up the module
Inside it, you'll want to make 2 files: a <ModuleName>.cs and a <ModuleName>.module. The module file is a TAML file, but generally is XML.
Once those are made, the directory should look like this:
We'll want to make the *.module file to look something like this:
Description="This contains all of my game's scripts and content."
With the MyGame parts and description obviously changed to apply as needed. The script file similarly should be set up like this:
function MyGame::onCreate( %this )
function MyGame::onDestroy( %this )
Save the files, and the basics are in place. Important parts to note:
- ModuleID is the unique module name
- Version ID is the particular version number(which is usually used in dependencies checks)
- Description is the description
- ScriptFile is the name of the initialization script file
- CreateFunction is the function called in our initialization script file when the module is loaded. It's used to set up the common stuff for the module, such as loading datablocks, exec'ing scripts and guis, etc. It uses the ModuleID for the namespace.
- DestroyFunction is the function called when we unload the module, used for cleanup. Similarly used the ModuleID namespace.
- Dependencies is any modules we need to load before this one so we can use their functionality. It's done as <ModuleID>=<VersionNumber>. For multiple dependencies, it's separated by a comma. In our case, because we'll be loading datablocks and have client/server scripts, we'll want the clientServer module and ui module as dependencies so we can properly load everything up in a multiplayer context.
- Group is the group - if any - this module is a part of. This is a convenience feature that allows you to load and unload groups of modules at once automatically. The BaseGame template is designed to automatically load any modules in the Game group during initialization. This can be expanded upon or changed in the main.cs file.
Porting the content to the module
Obviously, we want to move our content over into our new module, so we'll do that now. While you can organize it in any way you want, a way that was done for the FPSGameplay and SpectatorGameplay modules(that replicate the gameplay of the full and empty templates, respectively) is organized like so:
- Art, unsurprisingly, would contain your art files - shapes, animations, textures, particles, skies, decals, etc
- Levels, naturally, contain the *.mis files and any level-specific files such as the posteffect config, preview image, decals file, forest file and so on.
- Scripts contain anything script-based - gameplay scripts, guis, datablocks, etc
- Sounds are your sound files.
The FPSGameplay and EmptyGameplay modules structured the scripts folder as such:
- client - Any client-side specific scripts
- datablocks - Any files that contain your datablocks for all your objects
- gui - Any *.gui files
- server - Any server-side specific scripts
It should be fairly self explanatory with that, but if you want a good comparison example, you can check the FPSGameplay module to compare, but this organization model should be relatively straightforward.
Ensure that your datablocks, gui files, level files, and script files' various paths have been updated to point to the respective folder inside the data/<ModuleName>/ directory. This ensures that it's properly compartmentalized and contained in the module dir. Any content utilized from other modules should also be paired with adding that module's dependency to the module definition as mentioned above.
Initializing the module's scripts and content
Once you've moved your content over to the module folder, we'll want to initialize everything.
Following the FPSGameplay example - assuming you want to take advantage of the clientServer module's functionality, we'll want to make sure to initialize the client and server content appropriately.
Using the FPSGameplay initialization script file as an example, lets look at it:
function FPSGameplay::create( %this )
for( %file = findFirstFile( "data/FPSGameplay/scripts/datablocks/*.cs.dso" );
%file !$= "";
%file = findNextFile( "data/FPSGameplay/scripts/datablocks/*.cs.dso" ))
// Only execute, if we don't have the source file.
%csFileName = getSubStr( %file, 0, strlen( %file ) - 4 );
if( !isFile( %csFileName ) )
// Load all source material files.
for( %file = findFirstFile( "data/FPSGameplay/scripts/datablocks/*.cs" );
%file !$= "";
%file = findNextFile( "data/FPSGameplay/scripts/datablocks/*.cs" ))
for( %file = findFirstFile( "data/FPSGameplay/levels/*.mis" );
%file !$= "";
%file = findNextFile( "data/FPSGameplay/levels/*.mis" ))
$KeybindPath = "data/FPSGameplay/scripts/client/default.keybinds.cs";
%prefPath = getPrefpath();
if(isFile(%prefPath @ "/keybinds.cs"))
exec(%prefPath @ "/keybinds.cs");
function FPSGameplay::destroy( %this )
As we can see, the first thing that happens is we exec our server scripts. This is done because unless you're fully separating the client and server side, a singleplayer game still sets up a server instance, so we'll need to exec the server-side scripts for all the gameplay stuff. It's done first, because in the event this is a dedicated server OR a client, we'll always need these scripts executed, but we may not need to execute the client-side scripts if we're a dedicated server.
Next, we'll iterate through our data/<ModuleName>/scripts/datablocks folder - where we put all our datablock script files - and add them to DatablockFilesList.
This is where our utilization of the clientServer module comes in. This is an array that it set up in the initialization of the clientServer module, and is used to transmit datablocks to the client on connection. All modules can input datablocks into the array to ensure they all get properly transmitted to the client. As this is set up in the clientServer module, we have our dependency to the module so its always ready for us to use because we init after it.
Once we've iterated our datablock files and added them to that list, we want to iterate our data/<ModuleName/levels folder to get any *.mis files to add to the LevelFilesList. This is set up by the UI module, to give us a common list to add any level files from our modules and display that list on our ChooseLevel gui. Obviously, if you've got a different UI configuration, you may not need to do this, and can just reference the directory specifically if you don't have other level directories.
Next, we execute any client-side scripts. We filter this as being a client by checking that this process isn't running as a dedicated server - which means we'll be a client in some capacity. Then just exec the scripts, client preferences and any keybind files you set up for the client.
And you should be good! If your levels and datablocks have had their paths properly loaded, and you hooked the levels list to the UI, then calling StartGame(which is done in the UI module already, but needs to be done by any custom UI work you have) will kick off the creation of the game session!
In the event your project's seen a good deal of custom work, obviously just piggybacking off the FPSGameplay or SpectatorGameplay modules for the baseline isn't sufficient, and you'll need to port your work.
However, given that a fair bit of trimming and re-organization was done to clean up the templates, trying to figure what functions moved where(and even worse, if they were renamed) it can be a bit of a slog to sort through - not insurmountable, but a timesink that we'd rather avoid. So, below is a master list of functions that were moved around, removed or renamed, as well as when other stuff like datablocks got moved around. This way, you can easily look up what's where and port over your custom modifications to the appropriate spot without having to track everything down yourself.
Changes to functions in the template
- createCanvas() moved to core/canvas.cs
- isScriptFile() moved to core/helperFunctions.cs
- onStart() empty function removed
- parseArgs() empty function removed
- onStart() replaced by direct execution, sidestepping the package initialization.
- onExit deprecated
- loadKeybindings() currently deprecated
- parseArgs() moved to core/parseArgs.cs
- clientCmdSyncEditorGui => tools/worldEditor/scripts/cameraCommands.ed.cs