← Back to Documentation

Scripts & Nodes

⚠️ Experimental - Pre-Release

Scripts in Perro are attached to nodes to add behavior and logic. Each script extends a specific node type, giving you access to that node's fields and methods through the self keyword.

Script Definition

Scripts are defined using the @script directive, followed by a name and the node type they extend:

@script Player extends Sprite2D    var speed = 7.5     fn init() {        Console.print("Player script initialized")        self.texture = Texture.load("res://player.png")    }     fn update() {        var delta = Time.get_delta()        self.transform.position.x += speed * delta    }

The script name (Player) is used for identification, and the node type (Sprite2D) determines what fields and methods are available through self.

Extending Node Types

Scripts can extend any node type in the engine. When you extend a node type, you inherit all of its fields and methods:

Extending Node2D

@script MyNode extends Node2D    fn update() {        // Access Node2D fields        self.transform.position.x = 100        self.transform.position.y = 50        self.visible = true        self.z_index = 1    }

Extending Sprite2D

@script MySprite extends Sprite2D    fn init() {        // Access Sprite2D fields (inherits from Node2D)        self.texture = Texture.load("res://sprite.png")        self.transform.position.x = 0        self.transform.position.y = 0    }

Extending Node (Base)

@script MyNode extends Node    fn init() {        // Access Node fields        self.name = "MyNode"        var child = self.get_node("ChildNode")    }

The self Keyword

Use self to access the node that the script is attached to. Through self, you can access all fields and methods of the node type you extended:

@script Player extends Sprite2D    fn init() {        // Access node fields        self.name = "Player"        self.transform.position.x = 0        self.transform.position.y = 0        self.visible = true         // Access node methods        var child = self.get_node("Weapon")        var parent = self.get_parent()         // Add child nodes        var bullet = new Sprite2D()        self.add_child(bullet)    }

Lifecycle Methods

Scripts can define lifecycle methods that are called automatically by the engine:

init()

Called once when the script is first attached to the node. Use this for initialization.

fn init() {    Console.print("Script initialized")    self.texture = Texture.load("res://sprite.png")}

fixed_update()

Called at a fixed rate of updates per second (XPS) based on your target XPS (default 30 XPS). Use this for physics and game logic that needs consistent timing.

fn fixed_update() {    // Physics, collisions, game logic    // Called at fixed rate (e.g., 30 updates per second)}

update()

Called every time the update loop runs, which can be thousands of times per second. Use this for high-frequency updates, input polling, or logic that needs to run as fast as possible.

fn update() {    // High-frequency updates    // Can be called thousands of times per second    if Input.is_key_pressed("Space") {        // Handle input    }}

Speeds have been achieved up to 40,000 updates per second on Windows and 200,000 updates per second on Linux VM

draw()

Called at the current FPS for rendering. Use this for drawing operations and visual updates that should match the display refresh rate.

fn draw() {    // Rendering and visual updates    // Called at current FPS (e.g., 60 FPS)}

Script Attachment

Scripts are attached to nodes through the script_path field. This field contains the path to the script file, or null if no script is attached.

@script MyNode extends Node    fn init() {        // Check if this node has a script attached        if self.script_path != null {            Console.print("Script path: " + self.script_path)        }    }

Scripts are typically attached in scene files or through the editor. The engine automatically loads and executes scripts based on the script_path.

Related