Skip to main content

Creating custom modules

Custom modules in CORE are special scripts that run inside and interact with the framework. They can be accessed globally and come with many enhancements over using autoload singletons.

What should be packaged as a custom module?

Before you write any code, you should decide wheneter to write your code as a module or as a simple script. Generally you not write everything used between two classes as a module. Use custom modules for stuff you may want to use across multiple objects or need to use frequently in your codebase. One example of this is a transition script. You'll probably want to transition from one scene to another in a game quite often, writing it as a custom module makes much more sense as it can be accessed globally much easier and can be shared across codebases.

Bad practise

Before we finally come to some code, never avoid these things while writing a custom module:

  • Depend on your project code. Modules are intended to be project independent and work in every environment where CORE runs. If you need to access some asset or scene, use a variable to store the path.
  • Never crash the entire framework on invalid user input, only crash when an internal error occurred.
    Let's say that you have two functions: An internal _display_scene method and a transition_between_scenes method.
    When the developer calls transition_between_scenes and supplies an invalid object to the method, you should call logger.error (and return the error) as the error comes from the developer and should be handled by the developer's code.
    If however your transition_between_scenes calls _display_scene and supplies an invalid transition type, you should call logger.crash instead as your module code has an inconsistency and may not account for that. This ensures your code does not do stupid things plus you get a bug report or pull request from an annoyed developer :)
  • Never create, instantiate or duplicate objects without removing them from the SceneTree and freeing them first. This ensures no memory leaks occur and makes your codebase a bit better.
    If you wonder how to do this, look at CORE's logger. It frees unused CoreLoggerInstances every time CORE's scheduler (_schedule) is executed and frees all CoreLoggerInstances on framework cleanup (_cleanup).
  • Not defining variable and method return types. Forcing a variable to match a String or a method to always return an int ensures your code always runs without flaws and invalid user input, saving you a few bugs while improving code quality. If a variable may contain or a method may return multiple types, use Variant for everything but objects and Object for everything that is an object.
  • Not defining the containing type in for Arrays. Unless your array should hold multiple types, replace Array with Array[Type]. It also ensures no bugs occur and improves code quality.
  • Not writing unit tests. Even if you don't like them (we neither), please unit test your custom module and your project(s) generally! Since we added unit tests to CORE we already identified multiple bugs inside our framework.
    If you are searching for a lightweight unit testing solution that is 100% compatible with CORE look try Bessere Tests (made by StarOpenSource too). For reference, look at CORE's unit testing code here and config here.
  • Initialize your module using _init or _ready. This will only cause issues and break your module or even the entire framework. Please run your initialization code in _initialize.
  • Annotate your variables using @onready. Please update your variables in _initialize, see the above note.

Writing an example module

Create a new folder in your project root and call it COREmodules (note that the script's location does not matter, it could be stored in user:// even). In there, create a new file called hello.gd and remove all the pregenerated code (if there) except extends Node. Now you should have an empty script file with only extends Node sitting in front of you. Now replace it with extends CoreBaseModule. This marks your script as a valid CORE module and inherits it's variables and methods.

Initializing the module

Now, create a new void-returning method called _initialize without any arguments. In there, write the following:

loggeri.info("Hello World!")
initialized = true # Required or the framework will wait for your module to initialize forever (see `complete_init`).

What this code will do is print Hello World! as an informational message into the log and mark the module as initialized.

Executing code in regular intervals

In case you don't know, the CORE Framework as something called the scheduler. It runs (by default) every minute and calls the _cleanup method on all loaded modules. This can be used to clean unused references or do other maintenance tasks. In our case however, we want Hello Scheduler! to be printed every time the scheduler does it's thing. To do that, create a new void-returning method called _schedule without any arguments and paste loggeri.info("Hello Scheduler!") into it.

Uninitializing the module

Now, when the application shuts down or quit_safely is called, all modules will recieve a call to _cleanup, requesting the module to stop what it's doing and cleanup everything it did. In our case however, we want to say Goodbye World! on cleanup. For that to happen, create a new void-returning method called _cleanup without any arguments. Then paste loggeri.info("Goodbye World!") into the function.

The full code

Now your hello.gd should have the following contents (except comments):

extends CoreBaseModule

# Initialization
func _initialize() -> void:
# Print 'Hello World!'
loggeri.info("Hello World!")
# Mark the module as initialized
initialized = true

# Maintenance tasks
func _schedule() -> void:
# Print 'Hello Scheduler!'
loggeri.info("Hello Scheduler")

# Cleanup module
func _cleanup() -> void:
# Print 'Goodbye World!'
loggeri.info("Goodbye World!")

Registering your custom module

Before you can use your custom module you need to register it. To do that, simply execute this code in your initialization script:

# Creates new CoreBaseModule object and assigns it to a new variable
var custom_module: CoreBaseModule = CoreBaseModule.new()
# Loads a script located at `res://COREmodules/hello.gd` and sets it as the script for 'custom_module'
custom_module.set_script(load("res://COREmodules/hello.gd"))
# Registers the custom module
core.register_custom_module("hello", "res://COREmodules/hello.gd", custom_module)
# Wait for all built-in and custom modules to fully initialize
await core.complete_init()

# Wait one minute and five seconds for the scheduler message to appear and shut down, remove this when you are actually adding a custom module to your project.
await get_tree().create_timer(65).timeout
await core.quit_safely()

Testing your custom module

Now run your project and you should see something like this somewhere in your console:

[14:52:11] [INFO res://COREmodules/hello.gd] Hello World!

Now wait for about one minute and you should see this message in your console:

[14:53:11] [INFO res://COREmodules/hello.gd] Hello Scheduler!

... which should be followed by this a few seconds later:

[14:53:16] [INFO src/core.gd] Shutting down (code 0)
[14:53:17] [INFO src/core.gd] Cleaning up
[14:53:17] [INFO res://COREmodules/hello.gd] Goodbye World!

If this matches your log output, then you've successfully created your own custom module. You can now modify the code of the custom module however you like. But before that, please read the CoreBaseModule reference.