ℐ⁺nvariance
My game, Pure Gauge, where an open developer console is shown. Two lines have already been entered: "spawn skeleton 5" and "additem epic_sword". Two skeletons can be seen standing behind the player character, one of them is being hovered over by the mouse cursor, is highlighted in a bright yellow color, and has an "epic sword" equipped.

Script templates and abstract classes in GDScript

Today I'd like to share with you a neat little Godot feature: script templates. The official documentation does a good job of explaining how it works, so what I'd like to focus on instead is how I use it in my own game, Pure Gauge. But here's a short summary.

Script templates

You may be familiar with the default contents of a newly created GDScript file:

extends Node


# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	pass

It turns out you can also create your own templates: They are files consisting of regular GDScript and some special syntax, and when you put them into a specific folder in your project (res://script_templates/ by default, followed by a subfolder with the name of the base type the template is valid for, e.g. Node3D), you can choose to base a new script on it in the creation dialogue.

Here you can see the three templates I've made so far listed, each representing a custom resource. I base them on Object, meaning they are placed in res://script_templates/Object/. You can only choose built-in types as a base, not custom resources. I could have chosen Resource as a base, but then the entries only appear when you enter "Resource" into the "Inherits:" field, which is way too much work if you ask me. So I chose Object, which both resources and nodes inherit. This way the choices appear without having to painstakingly select which type to inherit from first.

Abstract classes

Now you may wonder why I'd ever need to create so many scripts based on a specific template that setting all this up is worth it. The answer is abstract classes: scripts which are not meant to be instantiated directly, but instead define some common behavior and methods which are meant to be overridden by inheriting classes. Many programming languages offer this functionality out of the box. GDScript does not have abstract classes yet, but there there is an open pull request implementing it. In the meantime, you can still use the pattern behind abstract classes, just without any of the strict guarantees that a native implementation would provide.

For example, you can throw an error when attempting to call a method without overriding it:

extends Node

var health := 100


# --- Shared methods, not intended to be overriden ---
func add_health(amount: int) -> void:
	health += amount


# --- Methods with default implementations ---
func _get_name() -> String:
	return "Default name"


# --- Methods that must be overriden by inheriting scripts ---
func _attack() -> void:
	assert(false, "Not implemented")


func _get_weaknesses() -> Array[String]:
	assert(false, "Not implemented")
	return []

Practical example: Console commands

To illustrate this further, let me introduce a feature of Pure Gauge, the in-game developer's console. Ripped off from heavily inspired by Skyrim's implementation, this is a terminal you can open at any time and enter commands into. These could add items to your inventory, teleport you to a different location, spawn enemies or NPCs, unlock doors, etc.

Usually when I deal with objects that store data and share behavior I write a custom resource and make instances of it. Commands have names, a help text, mandatory and optional arguments each with their own names and descriptions, etc. These could all be stored in @export variables. Then to create a new command, you create a resource of the ConsoleCommand type and set those properties in the inspector. There is a problem, however: the most crucial part of of a command is the actual code it executes. While functions are a type of object, Callable, you can not add them as @export variables and write them in the inspector. So this approach doesn't quite work.

Instead, whenever I have objects with similar behavior but which need to implement custom logic I use the abstract class pattern. This is what my actual ConsoleCommand class roughly looks like:

class_name ConsoleCommand
extends RefCounted
## Defines a game-specific command that can be used by Console.

## Arguments passed to the command, parsed to their respective type.
var args: Array[Variant]

## Reference to the node the command targets, if applicable.
var subject: Node

# --- Regular methods that should be inherited but not overriden ---


## Takes the arguments typed into the console by the player, casts them to correct type, populates the args member variable. Returns a custom result type indicating whether the operation succeeded or if the input was invalid.
func _parse_input(raw_args: PackedStringArray) -> CommandResult:
	# ...

	
## Called by the console class when submitting a command. Populates local data, calls the abstract method _execute, and returns its result.
func run(raw_args: PackedStringArray, p_subject: Node = null) -> CommandResult:
	subject = p_subject
	var parse_error_result := _parse_input(raw_args)
	if parse_error_result != null:
		return parse_error_result
	
	return _execute()

	
# --- Interface methods with default implementations ---

## Some commands require a node to be selected before submitting them. For example, the "unlock" command requires the player to first click on the target door.
func _requires_subject() -> bool:
	return false


## Takes partially typed arguments and returns a list of suggestions. For example, the "loadcell" command compares the input to the list of cells (levels) in the game and returns matching ones-.
func _get_completion_suggestion(partial_args: PackedStringArray) -> String:
	return " ".join(partial_args)


## Returns a list of objects describing the types of arguments the command accepts. These include information on the expected type, used to parse the input later, and whether the argument is optional or mandatory.
func _get_argument_list() -> Array[CommandArgument]:
	return []


# --- Interface methods that must be implemented by inheriting scripts ---

## Returns the name of the command and optional aliases. For example, a command for toggling collision detection might return ["toggle_collisions", "tcl"].
func _get_command_names() -> Array[StringName]:
	assert(false, "Not implemented")
	return []


## Returns the description for the command used by a special help function.
func _get_command_description() -> String:
	assert(false, "Not implemented")
	return ""


## The actual logic to be executed. It can access the subject and arguments parsed earlier.
func _execute() -> CommandResult:
	assert(false, "Not implemented")
	return CommandResult.new("")

With this approach, the workflow of creating a new command involves writing a new script which extends ConsoleCommand and implements at least the last three methods. Since properties such as the description now apply to the whole class and not to instances of it, they are now purely getter methods instead of variables. For example, the "revive" command would be implemented like this:

extends ConsoleCommand


func _get_command_names() -> Array[StringName]:
	return [&"revive"]


func _get_command_description() -> String:
	return "Revive dead actor."


func _requires_subject() -> bool:
	return true


func _execute() -> CommandResult:
	var actor := subject as ActorBody3D
	if not actor:
		return CommandResult.new("Selected node is not an actor.", CommandResult.Status.EXECUTION_ERROR)
	
	actor.data.health.replenish_fully()
	actor.model.ragdoll(false)
	actor.alive = true
	actor.ai.enabled = true
	return CommandResult.new()

As an aside, note that I don't bother to add a class_name. The way I use these scripts is by having the Console class scan the contents of a commands folder, load each script contained within, create an instance, and register them inside an array. When submitting a command, the list is iterated and the first entry whose _get_command_names() contains the first word of the input is used to parse the remaining arguments and finally gets executed.

Now it might be more obvious how script templates might come in handy here. There are several methods that I need to implement for each new command, and copy-pasting them each time is tedious. So, here is the template I use to make the process easier:

# meta-name: Command
# meta-description: Console command
# meta-default: false
# meta-space-indent: 4
@tool
extends ConsoleCommand


func _get_command_names() -> Array[StringName]:
    return [&"command"]


func _requires_subject() -> bool:
    return false


func _get_command_description() -> String:
    return "Command description"


func _get_argument_list() -> Array[CommandArgument]:
    return [
        CommandArgument.new(
            &"argument",
            TYPE_STRING,
            true,
            &"Argument description"
            ),
        ]


func _get_completion_suggestion(partial_args: PackedStringArray) -> String:
    return " ".join(partial_args)


func _execute() -> CommandResult:
    return CommandResult.new()

The #meta comments at the top are special syntax of script templates and are explained in the documentation. The rest is the code that will be created by default whenever you create a new script using the Command template. Optional methods can easily be deleted, and their bodies contain defaults that shouldn't be used as-is but provide a reminder and quick-start for what needs to be implemented.

Conclusion

Abstract methods are not yet implemented in GDScript, but that shouldn't stop you from using the pattern anyway. I use it whenever instances of a certain concept differ not just by data, but also by logic. In Pure Gauge, I use them for console commands, effects (such as physical damage, fire, healing, etc.), and tasks (used by the quest system to determine whether certain objects like collecting a specific item have been completed). Script templates are a very nice quality-of-life feature that are most useful when dealing with abstract classes that you often need to derive from.

This concludes the first actual tutorial-like post for the Invariance blog, thank you very much for reading! If you have feedback, questions, suggestions, or know of further applications of script templates please let me know! You can reach me on Mastodon, Bluesky, or via email at rie at proton dot me.

Home Back to the top