0 votes

Hello,

I'm attempting to set up some "static" / hard-coded objects that can be referenced throughout the code in an attempt to keep things DRY.

My goal is to have an enumerated class which encapsulates some common properties that can be used across multiple classes, but can also be specified by an export variable. In this case, "Alignment" contains information for good/evil/neutral entities' UI color, collision layer bits for various physics bodies, and more down the road I'm sure.

Previously I used a simple enum enum Alignment { GOOD, EVIL, NEUTRAL } which allowed both referencing constants AND exporting them for easy drop-down selection in the editor, but I found myself writing very similar conditional statements against these values such as:

Old Character.gd:

extends Spatial
class_name Character

enum Alignment { GOOD, EVIL, NEUTRAL }

onready var hitbox = $Hitbox
onready var box: CSGBox = $Box

export (Alignment) var alignment = Alignment.EVIL setget set_alignment;    

func set_alignment(val):
    alignment = val
    if (alignment == Alignment.GOOD):
        box.material.albedo_color = Color( 0.25, 1, 1, 0.25)
        hitbox.set_collision_layer_bit(2, true)
        hitbox.set_collision_mask_bit(7, true)
    elif (alignment == Alignment.EVIL):
        box.material.albedo_color = Color( 1, 0.25, 0.25, 0.25)
        hitbox.set_collision_layer_bit(5, true)
        hitbox.set_collision_mask_bit(8, true)
    else:
        # etc

After doing this in a few different places, I wanted to move certain properties to a class and define some hard-coded instances of that class. I've come up with a solution that doesn't feel very clean, but am wondering if there is a better approach to a scenario like this.

Globals.gd (autoloaded singleton):

extends Node

enum AlignmentKeys {
    GOOD,
    EVIL,
    NEUTRAL
}

var Alignments = {
    AlignmentKeys.GOOD: {
        "description": "Good",
        "color": Color(0.25, 1, 1, 0.25),
        "collision_layer": 0,
        "movebox_layer": 1,
        "hitbox_layer": 2,
        "hitbox_mask": 7,
        "hurtbox_layer": 3
    },
    AlignmentKeys.EVIL: {
        "description": "Evil",
        "color": Color(1, 0.25, 0.25, 0.25),
        "collision_layer": 4,
        "movebox_layer": 5,
        "hitbox_layer": 6,
        "hitbox_mask": 3,
        "hurtbox_layer": 7
    },
    AlignmentKeys.NEUTRAL: {
        # ...
    }
}

Basically the enum AlignmentKeys are the possible keys for the dictionary Alignments, which contains all the static data defined for each alignment. To handle exports as well as referencing those "constants", it looks something like this:

Revised Character.gd:

extends Spatial
class_name Character

onready var hitbox = $Hitbox
onready var box: CSGBox = $Box

export (Globals.AlignmentKeys) var alignment_key = Globals.AlignmentKeys.EVIL;
var alignment = Globals.Alignments[alignment_key] setget set_alignment;

func set_alignment(val):
    alignment = val
    box.material.albedo_color = alignment.color
    hitbox.set_collision_layer_bit(alignment.hitbox_layer, true) 
    hitbox.set_collision_mask_bit(alignment.hitbox_mask, true)

Again, this works and has cleaned up the amount of conditionals but I can't help but feel there's a cleaner way. I was also hoping to use some static typing with a class such as:

class Alignment extends Node:
    var description: String;
    var color: Color;
    var collision_layer: int;
    var movebox_layer: int;
    var hurtbox_layer: int;
    var hitbox_layer: int;

...but I understand that arrays and dictionaries cannot be staticly typed, so I'm not sure if that would be possible.

In short, I am wondering if there's any way to reference staticly-typed, predefined variables by name across classes in general, but also a strategy to export a possible set of those objects.

Put another way, I can express this pretty easily in Java enum terms and am curious to know if there's some sort of analog in gdscript.

Alignment.java:

public enum Alignment {
    GOOD("Good", Color.BLUE, 0, 1, 2, 7, 3),
    EVIL("Evil", Color.RED, 4, 5, 6, 3, 7);

    public final String description;
    public final Color color;
    public final int collisionLayer;
    public final int moveboxLayer;
    public final int hitboxLayer;
    public final int hitboxMask;
    public final int hurtboxLayer;

    private Alignment(String description, Color color, int collisionLayer, int moveboxLayer, int hitboxLayer, int hitboxMask, int hurtboxLayer) {
        this.description = description;
        this.color = color;
        this.collisionLayer = collisionLayer;
        this.moveboxLayer = moveboxLayer;
        this.hitboxLayer = hitboxLayer;
        this.hitboxMask = hitboxMask;
        this.hurtboxLayer = hurtboxLayer;
}

// getters for each property

}

Thanks for any advice! I've had trouble finding other Q&A's about this topic, so I hope it all makes sense.

Godot version v3.2.3
in Engine by (12 points)

1 Answer

–2 votes

A brief look at static typing
With typed GDScript, Godot can detect even more errors as you write code! It gives you and your teammates more information as you're working, as the arguments' types show up when you call a method.

Imagine you're programming an inventory system. You code an Item node, then an Inventory. To add items to the inventory, the people who work with your code should always pass an Item to the Inventory.add method. With types, you can enforce this:

In 'Item.gd'.

class_name Item

In 'Inventory.gd'.

class_name Inventory

func add(reference: Item, amount: int = 1):
var item = finditem(reference)
if not item:
item = _instance
itemfromdb(reference)

item.amount += amount

Another significant advantage of typed GDScript is the new warning system. From version 3.1, Godot gives you warnings about your code as you write it: the engine identifies sections of your code that may lead to issues at runtime, but lets you decide whether or not you want to leave the code as it is. More on that in a moment.

Static types also give you better code completion options. Below, you can see the difference between a dynamic and a static typed completion options for a class called PlayerController.

You've probably stored a node in a variable before, and typed a dot to be left with no autocomplete suggestions:

code completion options for dynamic
This is due to dynamic code. Godot cannot know what node or value type you're passing to the function. If you write the type explicitly however, you will get all public methods and variables from the node:

code completion options for typed
In the future, typed GDScript will also increase code performance: Just-In-Time compilation and other compiler improvements are already on the roadmap!

Overall, typed programming gives you a more structured experience. It helps prevent errors and improves the self-documenting aspect of your scripts. This is especially helpful when you're working in a team or on a long-term project: studies have shown that developers spend most of their time reading other people's code, or scripts they wrote in the past and forgot about. The clearer and the more structured the code, the faster it is to understand, the faster you can move forward.

How to use static typing
To define the type of a variable or a constant, write a colon after the variable's name, followed by its type. E.g. var health: int. This forces the variable's type to always stay the same:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0
Godot will try to infer types if you write a colon, but you omit the type:

var life_points := 4
var damage := 10.5
var motion := Vector2()
Currently you can use three types of… types:

Built-in
Core classes and nodes (Object, Node, Area2D, Camera2D, etc.)
Your own, custom classes. Look at the new class_name feature to register types in the editor.
Note

You don't need to write type hints for constants, as Godot sets it automatically from the assigned value. But you can still do so to make the intent of your code clearer.

Custom variable types
You can use any class, including your custom classes, as types. There are two ways to use them in scripts. The first method is to preload the script you want to use as a type in a constant:

const Rifle = preload("res://player/weapons/Rifle.gd")
var myrifle: Rifle
The second method is to use the class
name keyword when you create. For the example above, your Rifle.gd would look like this:

extends Node2D
classname Rifle
If you use class
name, Godot registers the Rifle type globally in the editor, and you can use it anywhere, without having to preload it into a constant:

var my_rifle: Rifle
Variable casting
Type casting is a key concept in typed languages. Casting is the conversion of a value from one type to another.

Imagine an Enemy in your game, that extends Area2D. You want it to collide with the Player, a KinematicBody2D with a script called PlayerController attached to it. You use the onbodyentered signal to detect the collision. With typed code, the body you detect is going to be a generic PhysicsBody2D, and not your PlayerController on the onbody_entered callback.

You can check if this PhysicsBody2D is your Player with the as casting keyword, and using the colon : again to force the variable to use this type. This forces the variable to stick to the PlayerController type:

func onbody_entered(body: PhysicsBody2D) -> void:
var player := body as PlayerController
if not player:
return

player.damage()

As we're dealing with a custom type, if the body doesn't extend PlayerController, the playervariable will be set to null. We can use this to check if the body is the player or not. We will also get full autocompletion on the player variable thanks to that cast.

Note

If you try to cast with a built-in type and it fails, Godot will throw an error.

Safe lines
You can also use casting to ensure safe lines. Safe lines are a new tool in Godot 3.1 to tell you when ambiguous lines of code are type-safe. As you can mix and match typed and dynamic code, at times, Godot doesn't have enough information to know if an instruction will trigger an error or not at runtime.

This happens when you get a child node. Let's take a timer for example: with dynamic code, you can get the node with $Timer. GDScript supports duck-typing, so even if your timer is of type Timer, it is also a Node and an Object, two classes it extends. With dynamic GDScript, you also don't care about the node's type as long as it has the methods you need to call.

You can use casting to tell Godot the type you expect when you get a node: ($Timer as Timer), ($Player as KinematicBody2D), etc. Godot will ensure the type works and if so, the line number will turn green at the left of the script editor.

Unsafe vs Safe Line
Unsafe line (line 7) vs Safe Lines (line 6 and 8)

Note

You can turn off safe lines or change their color in the editor settings.

Define the return type of a function with the arrow ->
To define the return type of a function, write a dash and a right angle bracket -> after its declaration, followed by the return type:

func _process(delta: float) -> void:
pass
The type void means the function does not return anything. You can use any type, as with variables:

func hit(damage: float) -> bool:
healthpoints -= damage
return health
points <= 0
You can also use your own nodes as return types:

Inventory.gd

Adds an item to the inventory and returns it.

func add(reference: Item, amount: int) -> Item:
var item: Item = finditem(reference)
if not item:
item = ItemDatabase.get
instance(reference)

item.amount += amount
return item

Typed or dynamic: stick to one style
Typed GDScript and dynamic GDScript can coexist in the same project. But I recommend to stick to either style for consistency in your codebase, and for your peers. It's easier for everyone to work together if you follow the same guidelines, and faster to read and understand other people's code.

Typed code takes a little more writing, but you get the benefits we discussed above. Here's an example of the same, empty script, in a dynamic style:

extends Node

func _ready():
pass

func _process(delta):
pass
And with static typing:

extends Node

func _ready() -> void:
pass

func process(delta: float) -> void:
pass
As you can see, you can also use types with the engine's virtual methods. Signal callbacks, like any methods, can also use types. Here's a body
entered signal in a dynamic style:

func onArea2Dbodyentered(body):
pass
And the same callback, with type hints:

func onarea_entered(area: CollisionObject2D) -> void:
pass
You're free to replace, e.g. the CollisionObject2D, with your own type, to cast parameters automatically:

func onarea_entered(bullet: Bullet) -> void:
if not bullet:
return

take_damage(bullet.damage)

The bullet variable could hold any CollisionObject2D here, but we make sure it is our Bullet, a node we created for our project. If it's anything else, like an Area2D, or any node that doesn't extend Bullet, the bullet variable will be null.

by (44 points)

Hi, this appears to be a copy paste of the static typing page from the official docs. Not exactly relevant. https://docs.godotengine.org/en/stable/getting_started/scripting/gdscript/static_typing.html

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 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 webmaster@godotengine.org with your username.