Search code examples
godotgdscript

How to create and set a new script to a node during runtime?


I'm trying to create and set a new script to an object during runtime
and so far this is what I've come up with:

extends Node2D

func _ready():
    var object=$Obj # type KinematicBody2D
    var code_path="res://code.gd"
    
    var f=File.new()
    f.open(code_path, File.READ)
    
    var new_script=GDScript.new()
    new_script.source_code=f.get_as_text()
    new_script.resource_path="user://new_script.gd"
    ResourceSaver.save(new_script.resource_path, new_script)
    new_script.reload()
    f.close()
    
    object.set_script(new_script)
    object._ready()

This works perfectly when the scene is played from the game engine but as soon as I export it and run the executable it doesn't seem to work and gives the following error in the terminal:

Godot Engine v3.5.stable.mono.official.991bb6ac7 - https://godotengine.org
libGL error: glx: failed to create dri3 screen
libGL error: failed to load driver: nouveau
OpenGL ES 3.0 Renderer: RENOIR (renoir, LLVM 15.0.7, DRM 3.47, 5.19.0-45-generic)
Async. shader compilation: OFF
 
Mono: Log file is: '/home/mm/.local/share/godot/app_userdata/Testing/mono/mono_logs/2023-06-25_12.39.43_14788.log'
ERROR: File must be opened before use.
   at: get_as_text (core/bind/core_bind.cpp:2092)
ERROR: Script inherits from native type 'Reference', so it can't be instanced in object of type 'KinematicBody2D'.
   at: instance_create (modules/gdscript/gdscript.cpp:312)

Is there any solution for this? or is it simply not possible in export?

(currently using -v3.5, would something like this be possible in 4.x?)


Solution


  • Using a String literal

    One solution is to inline the text. You can use multi-line string literals:

    var like_this := """
    LOOK
    A
    STRING
    WITH
    MULTIPLE
    LINES
    """
    

    About .gd

    Godot 3 didn't scrap unused scripts by default (the setting is in the Resources tab of the Export Preset).


    On export the scripts would be compiled to bytecode by default (the setting is under the Script tab of the Export Preset), you can select Text. And yes, that would be for all scripts, there is no way to set it one by one.


    Aside from that, the script might be failing to import for some other reason. For example, it might have parsing errors... Which might make sense if it is is intended as a template. In my case, for templates I ended up using String literals.


    Also, assuming everything is OK with the script, you should be able to load it as a GDScript directly (e.g. with load) and use it.


    About .txt

    I believe the issue is that the .txt file is not being kept in the exported game.

    This was indeed an issue in Godot 3. You were supposed to tell Godot in the export setting to keep the file. Which never worked for me.

    It was supposedly fixed in Godot 3.3... You are supposed to select the file in the file system, and then import it as "Keep File (No Import)". And it was indeed fixed for .csv, but not for .txt because .txt files do not show up in the file system to begin with, because they are not a recognized Resource type. No, Godot won't recognize a .txt as a TextFile, and I have no idea if that is possible.


    No? You want .txt? OK, we are going to make Godot recognize it.

    So, define a custom resource type:

    class_name TXTResource
    extends Resource
    
    
    export(String, MULTILINE) var text:String
    

    And we need a custom loader:

    tool
    class_name TXTLoader
    extends ResourceFormatLoader
    
    
    func get_recognized_extensions() -> PoolStringArray:
        return PoolStringArray(["txt"])
    
    
    func get_resource_type(_path:String) -> String:
        return "Resource"
    
    
    func handles_type(typename: String) -> bool:
        return typename == "Resource"
    
    
    func load(path: String, _original_path: String):
        var file := File.new()
        var err := file.open(path, File.READ)
        if err != OK:
            return err
    
        var result = TXTResource.new()
        result.text = file.get_as_text()
        file.close()
        return result
    

    Place them anywhere in your project (they don't need to be in the addons folder).

    With that, the .txt file should be imported as a Resource correctly.

    Addendumm: Depending on you use them, you might also have to implement an EditorImportPlugin. The purpose of the EditorImportPlugin is to read the resource file and save in it as a format Godot understands natively (so instead of creating a resource object, it save a resource file in the .import folder in Godot 3 or the .godot/imported folder in Godot 4).


    Ah, but it does not open in the code editor! I hear you. TextFile is supposed to do that, but there is no way to use it. I'm not sure if there is a way to hack it, but I don't think it is worth the hassle if there is.