7. Tips and Tricks – Moving from Unity to Godot: An In-Depth Handbook to Godot for Unity Users

© Alan Thorn 2020
A. ThornMoving from Unity to Godothttps://doi.org/10.1007/978-1-4842-5908-5_7

7. Tips and Tricks

Alan Thorn1 
(1)
High Wycombe, UK
 

Previous chapters typically focused on specific groups of features within Godot, such as 2D and 3D worlds. This chapter, by contrast, focuses on how to achieve common tasks within Godot. The Unity documentation on such tasks is extensive. But the Godot documentation is limited. So let’s now take a look at some common tasks.

How to Make Objects Look at the Cursor

You’ll often want objects – like the player or enemy characters – to face the mouse cursor as it moves around the screen. Twin-stick shooter games are a common example. In these games, the player normally controls character movement – up, down, left, and right – using keyboard arrow keys or gamepad presses; and they control player orientation through mouse movement, specifically the player look direction. See Figure 7-1.
Figure 7-1

Controlling an Object’s Look Direction

In short, we want to adjust an object’s rotation to face the mouse cursor. Listing 7-1 demonstrates how a 2D Node can be rotated to face the cursor at any time.
using Godot;
using System;
public class LookMouse : Sprite
{
    private float StartRot = 0f;
    public override void _Ready()
    {
        StartRot = Rotation;
    }
  public override void _Process(float delta)
  {
        Vector2 CursorPos = GetLocalMousePosition();
        Rotation += CursorPos.Angle() + StartRot;
        Rotation = Mathf.Wrap(Rotation, Mathf.Deg2Rad(-360),Mathf.Deg2Rad(360));
  }
}
Listing 7-1

Making an Object Look at the Cursor

Listing 7-1 should be attached to any sprite node, and it’ll make the node point toward the cursor. This code assumes that an object, by default, is rightward facing. That is, its resting pose will see the character looking toward the right, along the X axis. See Figure 7-2. If that’s not the case for your object, then simply use the RotationDegrees field from the inspector to rotate the node so that it’s rightward facing. The _Process event happens on each frame, and it will call GetLocalMousePosition to retrieve the mouse cursor location on screen, relative to the calling node.
Figure 7-2

Controlling an Object’s Look Direction

Singletons and Auto-Loading

Sometimes you’ll need – and depend upon – behaviors that should exist in every scene. This may include player scores, player inventory, health statistics, and other kinds of global data or functionality that needs to be available almost everywhere. Now, when you’re creating objects in different scenes – such as a player scene, or an NPC scene, or an environment scene – it can be tricky to properly test these scenes in isolation from each other without the needed global functionality being present. In our use case here, we’ll write some code that simply prints the names of nodes as they’re added to the scene for any scene anywhere. If the added node belongs to a specific group, such as “Lava Pit,” then we’ll connect up its OnEntered signal to a function so we can receive notifications about when the player enters the lava pit, presumably to take damage or be destroyed. Now, this behavior needs to exist in every scene and to be present before any gameplay nodes are added to the scene, so we can be sure of catching every subsequently added node that could be a lava pit. We can achieve this behavior using Singletons configured to Auto-Load. Singletons are simply globally accessible classes, of which there can be only one instance simultaneously. The Auto-Load feature of Godot ensures your Singleton classes are added automatically to each and every scene at runtime, and further that’ll be added before other scene nodes. Let’s start by creating a new empty scene with a single Node2D object. See Figure 7-3.
Figure 7-3

Creating an Empty Scene…

Next, let’s add the following script, as shown in Listing 7-2, and then attach it to the root node of the scene. This code prints the name of each newly added node and also connects its signal to an event handler if the node belongs to the lava pit group.
using Godot;
using System;
public class ObjectChecker : Node2D
{
    public override void _EnterTree()
    {
        //Subscribe to the node added signal
        GetTree().Connect("node_added", this, "NodeAdded");
    }
    public void NodeAdded(Node N)
    {
        //Node has been added, print name
        GD.Print("Added node: " + N.Name);
        //Check group membership, and add to lava pit handler if needed
        if(N.IsInGroup("LavaPit"))
            N.Connect("OnLavaEntered", this, "OnLavaEnter");
    }
    private void OnLavaEnter()
    {
        //Do damage stuff here
    }
}
Listing 7-2

Print the Name of Newly Added Nodes

Now save the scene and access the Project Settings. Choose ProjectSettings from the Application Menu, and access the AutoLoad tab. From there, search for and find your SingletonScene, and click the Add button to add it to the AutoLoad list. See Figure 7-4.
Figure 7-4

Adding an Autoload Scene…

Great! Now your Singleton scene will automatically be added to any and every scene at runtime prior to any other nodes. This means our code will successfully detect the addition of new nodes, including nodes created at scene startup, because they are added after the creation of our Singleton.

Batch Renaming

Often, you’ll be working with scenes that contain many nodes. Some of these nodes will be manually created and named. But some will be imported from mesh files or auto-generated by add-ons, and these nodes feature default naming. In these instances, you could have a ton of nodes to rename if you want a nice, neat naming convention applied to all nodes in the scene tree. Additionally, you may need to refer to nodes by name using the GetNode or FindNode functions, and you’ll probably want memorable, meaningful, and structured names for your nodes. You can rename nodes individually by right-clicking a node and choosing Rename from the context menu. However, Godot also features a Batch Rename tool for renaming multiple nodes in one operation. To access this, select multiple nodes in the scene tree, and right-click. The context menu should display a Batch Rename option. If it doesn’t, you can also access the Batch Rename tool by pressing Ctrl+F2 on a PC or Cmd+F2 on a Mac. See Figure 7-5.
Figure 7-5

Accessing the Batch Rename Tool

After accessing the Batch Rename tool, you’ll be presented with an options menu. This allows you to specify object names, and naming patterns, to search for in the selection and then rename based on specific criteria. See Figure 7-6. In this example, the aim is to rename all objects consistently and to append sequential numbering.
Figure 7-6

Accessing the Batch Rename Tool

When you’re ready to apply Batch Rename to the selected objects, just click the Rename button. When completed in this example, the renamed objects are as shown in Figure 7-7.
Figure 7-7

Renamed Objects

Textures As Masks

Consider Figure 7-8. This shows two separate image files. One is a brick texture, and the other is a simple black and white outline. Sometimes, you’ll have two separate textures like this in Godot, and you’ll want to use the shape image as a frame or space inside which the other image should display. The outline or shape image is known as the Mask – or sometimes the Stencil.
Figure 7-8

Importing Image Masks. The Black Mask Should Import As White Instead. Black Is Used Here for Clarity

Let’s see how we can create a dynamic mask inside Godot – a mask that can be moved, changed, or updated over time, if we need it to. Start by importing two images into Godot; for our example here, I’ll use the brick image and the black and white outline. See Figure 7-9. Then create a new scene and add the brick image as a Sprite texture.
Figure 7-9

Importing Images into Godot and Setting Up a 2D Scene. Bricks Are Shown Here As a Sprite

Next, add a 2D light node to the scene. Right-click the root and choose Add Node, and then select Light2D. See Figure 7-10. The act will act like a projector for the mask.
Figure 7-10

Adding a Light2D to the Scene

After adding the Light2D node, load your mask texture into the Texture slot from the Inspector, and then position the light over the brick texture. You’ll notice that, by default, the light brightens the texture behind. See Figure 7-11.
Figure 7-11

Assign a Texture to a Light2D Node

Now select the brick texture in the background, and then add a new CanvasItemMaterial from the Material slot in the Inspector. This material will control how the texture should interact with intersecting light. See Figure 7-12.
Figure 7-12

Assigning a New CanvasItemMaterial to the Texture

After creating the CanvasItemMaterial, change the Light Mode to Light Only. When you do this, the texture changes showing only the areas within the light texture and effectively creating a mask. However, the pixels still seem slightly washed out. We’ll fix this in the next step. See Figure 7-13.
Figure 7-13

Assigning a Light Only Mode to a Canvas Material Produces a Mask

We can eliminate the washed out look by selecting the Light2D node and by changing its Mode to Mix. Now, the pixel saturation will be restored, and the texture behind will continue to be masked even after you move the Light around! Great. You’ve just created an effective 2D Mask. See Figure 7-14.
Figure 7-14

Completing the 2D Mask

Type-Independent Function Calling

As you develop real-world games with Godot, you’ll make lots of classes and functions and properties. As your projects develop in complexity, you’ll need to call different functions on different classes, but it can sometimes be difficult or messy to ascertain a Node’s type in advance. That’s where Godot’s HasMethod and Call functions are useful. These functions are part of the ultimate ancestor Object class and so are supported by every object. This means you can try to call a function of a specified name on any object using the following code in Listing 7-3.
if(MyNode.HasMethod("Explode"))
            MyNode.Call("Explode");
Listing 7-3

Calling a Named Function

Progress Bars and Loading

Large scenes with many assets – such as an RPG town or a village – can take quite a while to load fully. Having your game suspend or hang for long periods is a frustrating experience. Consequently, it makes sense to load larger scenes in a separate thread, or process, to avoid stalling the game entirely. And you may show a progress bar or loading screen to express the loading progress. You can achieve that in Godot easily using the TextureProgress node and a simple script. Simply create a new 2D scene and create a TextureProgress node. See Figure 7-15.
Figure 7-15

Setting Up a Texture Progress Node…

Next, attach the following script, as shown in Listing 7-4, to the TextureProgress node. This script will load the specified scene and update its progress in the bar.
using Godot;
using System;
public class TextureProgressMain : TextureProgress
{
    [Export]
    public string ScenePath;
    [Export]
    public uint MinimumTime = 2000;
    private uint TimeStart = 0;
    private Thread ThisThread = null;
    private void ThreadLoad(string ResPath)
    {
        ResourceInteractiveLoader RIL = ResourceLoader.LoadInteractive(ResPath);
        if(RIL==null)return;
        int StageCount = 0;
        while(true)
        {
            uint TimeElapsed = OS.GetTicksMsec() - TimeStart;
            float TimeProgress = (float)TimeElapsed/(float)(MinimumTime*1000f)*(float)MaxValue;
            if(StageCount < RIL.GetStageCount())
                StageCount = RIL.GetStage();
            float LoadProgress = (float)StageCount/(float)RIL.GetStageCount()*(float)MaxValue;
            CallDeferred("set_value", (TimeProgress<LoadProgress)?TimeProgress:LoadProgress);
            //Take a break
            OS.DelayMsec(100);
            if(Value>=MaxValue)
            {
                CallDeferred("ThreadDone", RIL.GetResource() as PackedScene);
                return;
            }
            if(StageCount >= RIL.GetStageCount())
                continue;
            //Poll current stats
            Error PollData = RIL.Poll();
            if(PollData == Error.FileEof)
            {
                StageCount = RIL.GetStageCount();
                continue;
            }
            if(PollData != Error.Ok)
            {
                GD.Print("Error Loading");
                return;
            }
        }
    }
    private void ThreadDone(PackedScene R)
    {
        ThisThread.WaitToFinish();
        SceneTree ST = GetTree();
        Node Root = R.Instance();
        ST.CurrentScene.Free();
        ST.CurrentScene = null;
        ST.Root.AddChild(Root);
        ST.CurrentScene = Root;
    }
    public override void _Ready()
    {
        TimeStart = OS.GetTicksMsec();
        ThisThread = new Thread();
        ThisThread.Start(this, "ThreadLoad", ScenePath);
    }
}
Listing 7-4

Level Loading

When this script is attached to the TextureProgress node, several parameters are exposed from the Inspector, as shown in Figure 7-16.
Figure 7-16

Controlling Scene Loading

This script accepts two parameters. The first is Scene Path, which is the resource path to the scene, which should be loaded. The second is Minimum Time. This specifies a minimum amount of time, in seconds, for which the loading bar will show. In cases where a scene loads incredibly fast, this will prevent the loading screen from simply flickering into view briefly and then disappearing. The progress bar will represent either the loading progress of the scene or the progress of the minimum time, whichever is the slowest.

How to Save Game States

Save states let gamers retain their progress, resuming from earlier sessions. With save states, your game remembers how much progress you’ve made. There are many different ways to code save states, and this section looks at JSON – a text-based language for saving game data easily and quickly. Let’s start by considering the following sample scene, shown in Figure 7-17 and included in the book companion files.
Figure 7-17

A Sample Scene with Three Zombies

The sample scene features three zombie characters. We’ll save their position and color, allowing it to be restored in later play sessions. You can, of course, save any data you want. To get started, we’ll need to create two script files: one to be attached to each object that can be saved and the other which is a global script that manages the save and load process. First, select all objects to be saved – the zombies in our example – and add them to the same group. Here, I’ve created a group called Persistent . See Figure 7-18.
Figure 7-18

Adding Zombies to a Persistent Group

Next, the following script should be attached to each sprite to be saved. This script file is intended to be called, or invoked, by the Save Game Manager. It serves only two purposes. First, it converts its properties – like position and color – into a JSON string for Save operations. And second, it converts a JSON string back into properties for Load operations. These two critical processes effectively convert an object to and from JSON. See Listing 7-5.
using Godot;
using System;
public class SerializeNode : Sprite
{
    public Godot.Collections.Dictionary<string, object> Save()
    {
        return new Godot.Collections.Dictionary<string, object>()
        {
            {"Filename", Filename},
            {"Parent", GetParent().GetPath()},
            {"PositionX", Position.x},
            {"PositionY", Position.y},
            {"ModColor", Modulate.ToHtml(true)}
        };
    }
    public void Load(Godot.Collections.Dictionary<string, object> SavedData)
    {
        Position = new Vector2((float)SavedData["PositionX"],(float)SavedData["PositionY"]);
        Modulate = new Color((string)SavedData["ModColor"]);
    }
}
Listing 7-5

Serializing a Sprite Object

Next, create a Node2D object, and attach a new script (SaveState), which can be used to invoke saving and loading behavior for each object. See Listing 7-6.
using Godot;
using System;
public class SaveState : Node
{
    //Name of file for saving and loading
    public string SGDName = "GameData.sav";
    public void Save()
    {
        //Open file for writing
        File SaveFile = new File();
        SaveFile.Open("user://"+SGDName, File.ModeFlags.Write);
        //Find all objects in scene to be saved
        Godot.Collections.Array Nodes = GetTree().GetNodesInGroup("persistent");
        //For each object, get its JSON representation
        foreach(Node N in Nodes)
        {
            if(N.Filename.Empty())
                continue;
            if(!N.HasMethod("Save"))
                continue;
            //Save JSON to file
            var SaveData = N.Call("Save");
            SaveFile.StoreLine(JSON.Print(SaveData));
        }
        //Close file
        SaveFile.Close();
    }
    public void Load()
    {
        File SaveFile = new File();
        if(!SaveFile.FileExists("user://"+SGDName))
            return;
        //Open file for reading data
        SaveFile.Open("user://"+SGDName, File.ModeFlags.Read);
        //Remove any duplicate objects already in scene
        Godot.Collections.Array Nodes = GetTree().GetNodesInGroup("persistent");
        foreach(Node N in Nodes)
            N.QueueFree();
        //Loop through file
        while(SaveFile.GetPosition() < SaveFile.GetLen())
        {
            string Line = SaveFile.GetLine();
            if(Line.Empty())break;
            Godot.Collections.Dictionary NodeData = (Godot.Collections.Dictionary)JSON.Parse(Line).Result;
            //Load objects back into scene
            PackedScene PS = (PackedScene)ResourceLoader.Load(NodeData["Filename"].ToString());
            Node NewScene = PS.Instance();
            Node ParentNode = GetNode(NodeData["Parent"].ToString())as Node;
            ParentNode.AddChild(NewScene);
            if(NewScene.HasMethod("Load"))
                NewScene.Call("Load", NodeData);
        }
        //Close file
        SaveFile.Close();
    }
    //Test functionality. Can be removed
    public override void _UnhandledInput(InputEvent @event)
    {
        if (@event is InputEventKey eventKey)
        {
            //Pressing S will save
            if (eventKey.Pressed && eventKey.Scancode == (int)KeyList.S)
            {
                Save();
                return;
            }
            //Pressing L will load
            if (eventKey.Pressed && eventKey.Scancode == (int)KeyList.L)
            {
                Load();
                return;
            }
        }
    }
}
Listing 7-6

Save States

Excellent. Together, these two scripts support load and saving behavior. Be sure to check out the project included in the book companion files.

Summary

Congratulations! You’ve now completed the final chapter and the book. This chapter presented a selection of super-handy tips and tricks for achieving common gameplay behaviors in Godot using C#.