File System
Unigine engine has its own file system module used to access files and folders. It has a few peculiarities you should be aware of when loading resources or organizing the structure of your Unigine-based project.
Data Directory
When Unigine engine is started, it checks for a folder where all resources are stored. This is a data folder specified via data_path start-up option. By default, it is <UnigineSDK>\data (of course, another folder can be specified).
- Here, all base Unigine resources (shaders, default textures, material libraries, etc.) are found. Without them, the application cannot be launched.
- By default, all project resources also need to be stored under this data folder (unless accessed from an external location, as described below).
External Directories
By default, the file system is limited to the data folder (unless absolute paths are used). To extend the file system outside it, three ways are possible:
- Load external ZIP and UNG packages
- Add directories via ULINK
- Access external resources by using the -extern_directory command-line option
To access resources from outside the data directory, use the -extern_directory command-line option:
bin\main_x86d.exe -extern_directory "D:/resources/my_project/tests/" -console_command "world_load test"
Paths can be both relative or absolute.
- Relative to the data directory:
../../my_project/resources/
- An absolute path:
D:/resources/my_project/tests/
Also *.ulink files can be used to add a folder (together with all its sub-folders and UNG or ZIP archives, if any) from outside the data directory to the cached file system. In such files, a path to the directory is stored. A link file can be stored anywhere inside data. On the start-up or when filesystem_reload is called to update the list of resources, the engine scans through files, finds a link file and adds a linked directory as if its root is a data folder.
Paths inside *.ulink files can be relative or absolute.
- Relative to the data directory (a slash in the end is required)
../../my_project/resources/
- An absolute path can be specified (a slash in the end is required)
For example, on Windows:On Linux or Mac OS X:D:\resources\
/username/resources/
Paths
A root data folder allows addressing files with relative or partial paths.
Partial
Partial paths simplify handling of files. If you want to load a file and you are sure that there is only one resource with a given name in the data folder, only a file name can be provided without a path. (If a name is not unique, the first found file with such name is loaded.) It is also possible to provide a sub-path that uniquely specifies a file. For example, to load data/project/image_1.tga, you can use image_1.tga (if a name is unique) or project/image_1.tga.
Relative vs Absolute
When relative paths are used, you can relocate your Unigine-based application or copy it onto another machine, and all resources will be properly loaded. There is no loading speed penalty as well: it is as fast as loading files by an absolute path due to name caching.
As file names are cached, usually the same name and path should be used to load and remove file when accessing from your Unigine script by using engine.filesystem functions:
- For default resources, functions return full paths relative to the data folder.
- If you load a file and specify a relative path, use a relative path to delete the resource.
- If you load a file using an absolute path, use an absolute path to delete the resource.
For example, to delete a loaded resource:
// Deleting a default resource (a library automatically loaded from *.world file)
engine.materials.removeWorldLibrary("project/my_materials_sfx.mat");
// Using a relative path
engine.materials.addWorldLibrary("project/my_materials.mat");
engine.materials.removeWorldLibrary("project/my_materials.mat");
// Using a name only
engine.materials.addWorldLibrary("my_materials.mat");
engine.materials.removeWorldLibrary("my_materials.mat");
// Using an absolute path
engine.materials.addWorldLibrary("D:/Unigine SDK/data/project/my_materials.mat");
engine.materials.removeWorldLibrary("D:/Unigine SDK/data/project/my_materials.mat");
engine.materials.addWorldLibrary("my_materials.mat");
engine.materials.removeWorldLibrary("project/my_materials.mat");
Case Sensitivity
By default, all of the file names inside archives are case sensitive. It is true even for Windows, where unpacked files are case independent. So, if you have errors on file opening, check the file name case first.
You can make the file system case insensitive by using the filesystem_icase console command.
File System Update
Dynamic Scanning vs Pre-Cached
Dynamic scanning allows the engine to cache names of all files within the data folder on the startup; this enables tracking file changes in real-time.
If the dynamic scanning is not necessary or takes too many resources, you can use the pre-cashed file hierarchy that is specified in the .ulist file. When the engine finds this file in a folder during the startup scanning, it stops and uses the list of files specified there.
The .ulist file is generated by using the special script externs/bin/datalist.py that should be run from the data folder.
Automatic Resource Reloading
The dynamic scanning of the file system allows automatic resource reloading. For example, if an artist changes a texture or a mesh file on the disk, it will automatically change in the world.
File Packages
Types
Unigine supports two types of file archives to save space or pack the production version of resources:
- UNG (a Unigine-native format for archives created with Archiver tool)
- ZIP
UNG and ZIP archives are loaded automatically if they are found within the data folder. File names are cached just like in case with non-archived files.
Content Access
Archives are completely transparent to the engine. There is no need to explicitly unpack the archives, as their content is automatically handled as not packed. Archived files are addressed as if they are non-archived. For example, if you have data/project/archive.ung and want to address directory/file.txt within it, simply specify the following path: project/archive/directory/file.txt or even file.txt (only if the file name is unique).
Inside the archive, files can be organized in any way. However, in the root of the archive only files with unique names should be placed. Otherwise, the file search will return incorrect results.
Here is an example of an incorrect file tree for an archive:
-
my_archive.ung
- my_folder
- file_2.txt
- file_1.txt
- file_2.txt
- my_folder
The correct archive structure can be specified as follows:
-
my_archive.ung
- my_folder
- file_2.txt
- another_folder
- file_2.txt
- file_1.txt
- my_folder
If there is a name collision between an archived file and a non-archived one, the first matching file is returned. The search is performed in the following order:
- Unarchived files
- Files in UNG archives
- Files in ZIP archives
From Unigine script, archives are handled using engine.filesystem functions as well.
External Packages
It is possible to add UNG and ZIP archives to the cached file system even if they are stored outside the data folder. External packages are added only on the start-up. Use one of the following methods for that:
- Add archives on the start-up via extern_package CLI option (together with the rest of required ones). Relative and absolute paths can be used.
Several packages:
bin\main_x86d.exe -extern_package "../../my_project/archive.ung"
bin\main_x86d.exe -extern_package "../2/core.ung,D:/Unigine/core.ung"
- Add archives via the extern_package console command (their names will be written into the configuration file and will be loaded next time Unigine is launched, unless overridden on the start-up)
- Using UnigineScript, via engine.filesystem.loadPackage() function.
Modifiers
File modifiers serve to automatically choose what resources to load when a Unigine project is run on different platforms or with different localizations. Instead of keeping multiple versions of the same project and copying shared data between them, you can add a custom postfix to file or folder names, and load only required resources on demand.
Modifiers are added to file or folder names as a postfix (only one can be specified). Any custom postfix can be used. For example, it could be:
- File name modifier: file.mobile.node (here, a node for a mobile version) or texture.eng.dds (a texture with English text)
- Folder name modifier: textures.android
If a folder has a modifier, files inside of it should not have modifiers. Otherwise, their modifiers will be ignored.
Register necessary modifiers in code via engine.filesystem.addModifier(). When the project is run, resources with the registered modifiers will be automatically loaded. Files without modifiers have the lowest priority (can be used for default resources).
For example, three localization languages are supported in the project: English (by default), German and French. Depending on the language, different splash textures need to be loaded on the start-up. Plus there is a PC version and an Android version of the project. For Android, downsized textures need to be loaded.
To organize your resources, name them using the following file modifiers:
-
data
-
splashes
- splash.png (this would be a default version of the texture. In our case, a texture with an English title)
- splash.de.png (a German title)
- splash.fr.png (a French title)
- textures
- interface.png (default texture for PC)
- textures.android
- interface.png (for Android)
-
splashes
After that, in the code you need to specify what modifier to use via engine.filesystem.addModifier(). This function is called in the system script (unigine.cpp) since a modifier need to be registered before the world and its resources start to be loaded. For example, to load a German splash screen and the Android interface:
// unigine.cpp
int init() {
...
// Register modifiers
engine.filesystem.addModifier("de");
#ifdef _ANDROID
engine.filesystem.addModifier("android");
#endif
// Set a splash texture
engine.splash.setWorld("textures/splash.png"); // splash.de.png will be automatically used
...
return 1;
}
// my_world.cpp
int init() {
...
Gui gui = engine.getGui();
// Load textures.android/interface if an Android version is run
WidgetSprite interface = new WidgetSprite(gui,"textures/interface.png");
// If a PC version is run, textures/interface is loaded
gui.addChild(interface);
return 1;
}
You can use -extern_define CLI option to pass the language (for example, if a user chooses a language in the launcher).
bin\main_x86d.exe -extern_define "LANG_DE"
And here is how passed defines can be handled in the code.
// unigine.cpp
string lang = "";
int init() {
...
// Parse EXTERN_DEFINE
#ifdef LANG_DE
lang = "de";
#elif LANG_FR
lang = "fr";
#endif
if(lang != "") {
engine.filesystem.addModifier(lang);
}
// Set a splash texture: splash.de.png or splash.fr.png will be used if the language is passed
engine.splash.setWorld("textures/splash.png"); // otherwise, splash.png
...
return 1;
}