+3 votes

I'm making a spelling game, and the words to be spelled are encoded as the filenames of the image resources of the game. This works fine on desktop, however, once deploying to Android, it fails miserably.

The code in question is:

var _words : Array = Array()

func _init() -> void:
    var dir = Directory.new()
    if dir.open("res://assets/images/words") == OK:
        dir.list_dir_begin()
        var filename = dir.get_next()
        while (filename != ""):
            if filename.ends_with(".png"):
                _words.append(filename.left(filename.length()-4))
            filename = dir.get_next()

The issue is that the _words array is empty on the Android device. I've tried adding the resources using the Project -> Export -> Android -> Resources -> Filters to export non-resource files, where I set the filter to *.png. I've also tried adding a Node that refers to all the images by including them as sprites. As neither of these tricks work, I've come to the conclusion that the dir() function in combination with res:// paths does not work on Android. Is that by design, or should I report a bug?

in Engine by (19 points)

Are you sure that you did write the directory path correctly? Especially in terms of uppercase/lowercase characters? Remember that Android (just like i.e. linux) is case sensitive in path and file names (extensions as well).

If you develop on Windows then you wouldn't notice it as Windows isn't case sensitive on file names.

I would start opening just res:// and listing the contents to output (and check the output either with adb logcat or inside the godot IDE.

It works on Linux (debian), so I think the case sensitivity should be ok. I'll add some output and see what goes on on the phone compared to desktop.

You need to have permisson check in export project settings when exporting for android under export settings

2 Answers

+6 votes

Interesting - I just stumbled into a similar situation after writing a Singleton AudioManager. In my case, I wanted to load a bunch of OGG files at runtime - similar to what your attempting to do. While the resources were properly loaded while running in-editor, they did not load when running in an exported build (Android or PC in my case).

After a lot of poking around, I discovered that the problem lies in the way resources are packaged into the build. They are not left in their original locations and they do not have their original names - which will trip up some of your code. Specifically, I think this code is likely failing in your case:

if filename.ends_with(".png"):

If you actually debug this on an Android device, you'll see the folder you're iterating does not contain the files you expect. It'll only contain a variation of those file names that ends with .import. So, for example, if the folder originally contained a file named image.png, it will now contain one named image.png.import. The real image will have been moved to the .import folder, and will have been renamed to something like image.png.<hash>.<ext>

Where <hash> is some hash or GUID and <ext> is some other file extension.

Interestingly, Godot's ResourceLoader.load() command knows how to deal with the mapping from original folder / original filename to new folder / new filename.

So, even in the face of the above example, you can still do a ResourceLoader.load("res://assets/images/words/image.png") and Godot will happily load the file you intended.

So, if you can get appropriate file names to the resource loader, things will work as expected.

However, you're not getting that far as (I think) your code is failing at the point where it checks for a "png" extension, because there is no file there with a png extension.

So, how do you fix it?

Assuming that you ultimately load the resources via the resource loader, I'd guess something as simple as this might fix the issue:

    while (filename != ""):
        filename = filename.replace('.import', '') # <--- remove the .import
        if filename.ends_with(".png"):

Essentially, that just removes the ".import" from the end of the filename that was found in the folder, which should return the name your code is expecting. Assuming you eventually tell the ResouceLoader to load the files, giving it that original name should work, even though it'll need to work some magic in the background to follow the folder and name mapping outlined above.

A similar change to my AudioManager's loading mechanism fixed my runtime resource loading issues.

by (13,798 points)
edited by
0 votes

The problem I faced is: I have a lot of resources needs to load and hard-code to load each one of them is not a good solution.

I managed to solve this problem by write an editor plugin to load these resources in editor and save the path to these files into a JSON file. Then, when I run project, I just use that JSON file to locate the resource using ResourceLoader class

Here is the code to load all resources in a folder:

public LinkedList<string> ListAllFilesInDirectory(Directory directory, LinkedList<string> fileNames, Predicate<string> predicate)
    {
        LinkedList<string> subDirectories = new LinkedList<string>();
        directory.ListDirBegin(true);
        var name = directory.GetNext();
        while (name != "")
        {
            if (directory.CurrentIsDir())
            {
                subDirectories.AddLast(directory.GetCurrentDir() + "/" + name);
            }
            else
            {
                Debug.Log("Found file: " + name);
                // Add full path to file to the list, if it fulfills the requirement of predicate.
                if (predicate(directory.GetCurrentDir() + "/" + name))
                    fileNames.AddLast(directory.GetCurrentDir() + "/" + name);
            }
            name = directory.GetNext();
        }
        return (subDirectories.Count != 0) ? subDirectories: null;
    }

Given the Directory class which must be opened, and a list of file names to fill in, the last parameter is a predicate to indicate if that resource is what we intended to load.
It returns a list of subfolders in the current evaluating folder for later use.

Usage is fairly simple in your case:

string rootDir = "res://path-to-some/root-folder"
LinkedList<string> fileNames = new LinkedList<string>();
Directory directory = new Directory();
if (directory.Open(rootDir) == Error.Ok)
{
    var subfolders = ListAllFilesInDirectory(directory, fileNames, x => x.EndsWith(".png"));
}

The remaining work is to save it into a file and when you run the game you just need to load that file in which contains the path to these resources.

But what about resources located in subfolder started from a given "root folder", or even resources located in subfolder of subfolder which may or may not contain another subfolder?

Here is the code which iteratively find resources start from "root folder" to all subfolder of subfolder which may contain other subfolders:

 /*
        This algorithm works by divide all directories in subfolders into layers.
        And walk through all subdirectories we found and evaluate it,
        then returns a list of file names we found.
    */
    public LinkedList<string> ListAllFilesInSubdirectories(string rootDir, Predicate<string> predicate)
    {
        Directory directory = new Directory();
        // Full path to file names in these folders.
        LinkedList<string> fileNames = new LinkedList<string>();
        // Folders organize as layers. 
        LinkedList<LinkedList<string>> layers = new LinkedList<LinkedList<string>>();
        // Current evaluating folders in layer.
        LinkedListNode<string> row = null;
        LinkedListNode<LinkedList<string>> depth = null;

        if (directory.Open(rootDir) == Error.Ok)
        {
            // Initialize with all folders in given directory.
            LinkedList<string> folders = ListAllFilesInDirectory(directory, fileNames, predicate);
            while (null != folders || 0 != layers.Count)
            {
                if (null != folders)
                {
                    foreach (var item in folders)
                        Debug.Log(item);
                    layers.AddLast(folders);
                    folders = null;
                    depth = null == depth ? layers.First : depth.Next;
                    row = depth.Value.First;
                }
                else
                {
                    // Evaluate all folders in this row.
                    while (null == folders && null != row)
                    {
                        Debug.Log("Change directory: " + row.Value);
                        directory.ChangeDir(row.Value);
                        folders = ListAllFilesInDirectory(directory, fileNames, predicate);
                        row = row.Next;
                        if (null == folders)
                        {
                            Debug.Log("Remove: " + depth.Value.First.Value);
                            depth.Value.RemoveFirst();
                        }
                    }

                    if (null != folders)
                        continue;

                    // We has evaluating all element in this row. Step up.
                    if (null == row)
                    {
                        depth = depth.Previous;
                        layers.RemoveLast();
                        if (0 == layers.Count)
                            return fileNames;

                        Debug.Log("Remove: " + depth.Value.First.Value);
                        depth.Value.RemoveFirst();
                        row = depth.Value.First;
                    }
                }
            }
        }
        else
        {
            Debug.Log("An error occurred when trying to access the path.");
        }
        return fileNames;
    }

Usage is fairly simple:
I assume all resources located in "res://scenes/inventory_items" are scenes, so no need to replace "true" with "x.EndsWith(".tscn")

var fileNames = ListAllFilesInSubdirectories("res://scenes/inventory_items/", x => true);

by (67 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read Frequently asked questions and How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to [email protected] with your username.