window node

src:https://medium.com/@Jun0h/windows-node-on-godot-a-new-gameplay-perspective-d1565ca68504
Press enter or click to view image in full size

Screen as example of the final product

Here, you will find all my process documented on my gameplay reproduction of WindowKill by Torcado on Itch.io .

For this, I’ll be using Godot 4.2 and Visual Studio as the IDE with the C# language.

It’s important to note that I’ll be using Godot in its 4th version, as it implements the new Window Node.

This node allows us to generate and manipulate different windows on our computer, meaning this project will only work on desktop versions.

Introduction

First of all, let’s consider the various uses such nodes can have. Godot 4 has opened up many opportunities, and we can see new games emerging, with one of the most famous recently being KinitoPet which emulates an old Windows desktop, even using your own wallpaper.

KinitoPet screenshot from Steam

KinitoPet screenshot from Steam

We can think of many gameplay ideas. The first that comes to mind are UI-based games like management games, but what if the window is part of the gameplay?

The Window Node

Screenshot from Godot node list

The Window Node is a new node introduced in the latest Godot version, every scene has a native window node, this window is the main window of your scene where everything is rendered, it’s just not visible on the inspector but it’s here in the background.

You can simply access to this invsible node with a simple line :

private Window _MainWindow => GetWindow();

From this you can entirely control your current Window entirely with code.

It has many properties, obviously the transform’s one such as Position and Size. Natively a Window size will be set from the project settings.

Press enter or click to view image in full size

Screenshot from project settings

As you can see you can only set the viewport, you can imagine it as a screen onto which the game is projected.

Now let’s imagine we need 2 or more windows :

Screenshot from Window node inspector

This is the inspector of the node, as you can see it has a lot of different parameters. Let’s explain them in order.

Mode

Title

By default your main window name is your game title but you can easily change it using code :

private Window _MainWindow => GetWindow();

public override void _Ready()
{
_MainWindow.Title = "My Window Title";
}

This can be useful if you want to dynamically change the title like a progress percent or the current state of your player for example.

Initial Position

Natively your main window will appear in the center of your primary screen, this can be changed direcetly on inspector or using the enumerator directly on code :

public override void _Ready()
{
_MainWindow.InitialPosition = Window.WindowInitialPosition.Absolute;
}

Flags

The flags are going to be our best friends experimenting with it, they allow you to have control on your windows behavior, avoiding them to be resized or being borderless for example. Thos are just bools that you can enable or disable from the inspector.

Press enter or click to view image in full size

Project settings advanced settings

Note : When you create a Window node and test your project, you might notice it appears by default as a Control node, embeded in the main window. To fix that, go in the Project Settings > Window > switch to Advanced Settings > disable Embed Subwindows

Signals

When you create a window it will not close automatically when you click on the close button, that’s why you will need to connect the close request event.

public override void _Ready()
{
Connect("close_requested",new Callable(this,MethodName.OnClosed));
}

private void OnClosed()
{
QueueFree();
}

Uses in pratice

Of course before writting all of this I tried myself, here’s my demo project if you want to try it by yourself.

When you generate a new Window, it will create their own viewport, which means you’re going to have 2 different worlds on separated windows, this really depends on your needs but for this tutorial we’re going to make them share the same world.

public partial class GameManager : Node2D
{
private Window _MainWindow => GetWindow();
[Export] private Window _AnotherWindow;

public override void _Ready()
{
_AnotherWindow.World2D = _MainWindow.World2D;
}

Press enter or click to view image in full size

Easy, right? Both windows are now sharing a world but there is a little problem, they both are rendering the same thing, that’s because they are sharing a camera but to solve this, we add a Camera2D node as child of the extra window to indicate to that camera to only render that part of the world on the extra window position for that we need to constantly update the camera for that window.

public partial class GameManager : Node2D
{
private Window _MainWindow => GetWindow();
[Export] private Window _AnotherWindow;
[Export] private Camera2D _Camera;

public override void _Ready()
{
_AnotherWindow.World2D = _MainWindow.World2D;
_Camera.AnchorMode = Camera2D.AnchorModeEnum.FixedTopLeft;
}

public override void _Process(double pDelta)
{
_Camera.Position = Position;
}

Note : Make sure to set the camera anchor mode to FixedTopLeft so it’s going to be easier to move with the window since their origin point is on the top left.

Press enter or click to view image in full size

Great ! Now we have both our Player able to travel through different windows !

Tools and Tips

Alright, now we have 2 separated windows interacting in the same world but we need to make them interesting…

I first thought about annoying of annoying Pop Ups that will disturb the playing during his run, as simple of that sounds we need 2 things.
First we have to make sure the window is only moving inside the current screen bounds. For this you need to get the screen size, your player can have muliple screens which means you need to get the primary screen or impose the screen to the player.

DisplayServer.ScreenGetSize(0);

So in order to make them move I just generated a random position through and then I used a Tween

public partial class PopUpWindow : Window
{

private RandomNumberGenerator _Rand = new RandomNumberGenerator();

public override void _Ready()
{
_Rand.Randomize();
MoveAround();
}

private void MoveAround()
{

Tween lTween = CreateTween();

lTween.Connect("finished", new Callable(this, nameof(MoveAround)));

lTween.TweenProperty(this, "position", new Vector2I(_Rand.RandiRange(0, DisplayServer.ScreenGetSize(0).X),
_Rand.RandiRange(0, DisplayServer.ScreenGetSize(0).Y)), 1).SetTrans(Tween.TransitionType.Expo);
}
}

For the boss, we need for the player to understand that their going to attack. That’s why the window containing the enemy is going to bounce, we use Tweens to interpolate the window size to give it an effect of bouncing.

_Tween.TweenProperty(GetParent(), "WindowSize",
_Window.WindowSize * 1.2f, 0.2).SetTrans(Tween.TransitionType.Bounce)
.SetEase(Tween.EaseType.Out);

Press enter or click to view image in full size

And last but not least the main feature of our windows is to resize them by shooting on the borders.

The easiest way to manipulate the window size is using a Rect2I, with this you can easily change the size of each border.

public partial class Main : Node2D
{
private Rect2I _WinRect;
public Window MainWindow => GetWindow();

public override void _Ready()
{
_WinRect = new Rect2I(MainWindow.Position, MainWindow.Size);
}

public override void _Process(double pDelta)
{
MainWindow.Position = (Vector2I)_WinRect.Position;
MainWindow.Size = (Vector2I)_WinRect.Size;
}

For this, you’ll need 4 Area2Ds that will be placed automatically on each border of your main window. First of all your Area2Ds need A Collision Shape as children, this is really important since you will need to resize them depending on the actual window size.

public partial class Border : Area2D
{

public CollisionShape2D Collision => GetChild(0);
public RectangleShape2D Shape = new RectangleShape2D();

public override void _Process(double delta)
{
Collision.Shape = Shape;
}
}

On our main script we setup the Area2Ds with our current window size :

public partial class Main : Node2D
{
private Rect2I _WinRect;
public Window MainWindow => GetWindow();

private Border _DetectorUp => GetNode(Constants.DETECTOR_UP_PATH) as Border;
private Border _DetectorDown => GetNode
(Constants.DETECTOR_DOWN_PATH) as Border;
private Border _DetectorLeft => GetNode(Constants.DETECTOR_LEFT_PATH) as Border;
private Border _DetectorRight => GetNode
(Constants.DETECTOR_RIGHT_PATH) as Border;

public override void _Ready()
{
_WinRect = new Rect2I(MainWindow.Position, MainWindow.Size);
DetectorSetup();
}

public override void _Process(double pDelta)
{
MainWindow.Position = (Vector2I)_WinRect.Position;
MainWindow.Size = (Vector2I)_WinRect.Size;
}

private void DetectorSettings()
{

_DetectorLeft.Position = (Vector2I)_WinRect.Position + new Vector2(0, _WinRect.Size.Y / 2);
_DetectorLeft.Shape.Size = new Vector2(1, _WinRect.Size.Y);

_DetectorRight.Position = _WinRect.Position + new Vector2(_WinRect.Size.X, _WinRect.Size.Y / 2);
_DetectorRight.Shape.Size = new Vector2(1, _WinRect.Size.Y);

_DetectorUp.GlobalPosition = _WinRect.Position + new Vector2(_WinRect.Size.X / 2, 0);
_DetectorUp.Shape.Size = new Vector2(_WinRect.Size.X, 1);

_DetectorDown.Position = _WinRect.Position + new Vector2(_WinRect.Size.X / 2, _WinRect.Size.Y);
_DetectorDown.Shape.Size = new Vector2(_WinRect.Size.X, 1);

}

private void DetectorSetup()
{
DetectorSettings();

_DetectorUp.Connect("area_entered", new Callable(this, nameof(UpHit)));
_DetectorDown.Connect("area_entered", new Callable(this, nameof(DownHit)));
_DetectorLeft.Connect("area_entered", new Callable(this, nameof(LeftHit)));
_DetectorRight.Connect("area_entered", new Callable(this, nameof(RightHit)));

}

Now our Borders colliders are setup, we just need to add the action that will change the _WinRect size using a State Machine, those methods are going to change the resize value for each of the sides.

public partial class Main : Node2D
{
private float _ExpandUp;
private float _ExpandDown;
private float _ExpandLeft;
private float _ExpandRight;

private float _MoveDown;
private float _MoveRight;

protected Action doAction;
protected float processDelta;

public override void _Process(double pDelta)
{

base._Process(pDelta);
if (doAction != null) doAction();
processDelta = (float)pDelta;
}

private void UpHit(Area2D pEntered)
{
if (pEntered is PlayerBullet)
{
_ExpandUp = 10f;
SetModeResize();
}
}

private void DownHit(Area2D pEntered)
{
if (pEntered is PlayerBullet)
{

_ExpandDown = 10f;
_MoveDown += 10f;
SetModeResize();

}
}

private void LeftHit(Area2D pEntered)
{
if (pEntered is PlayerBullet)
{
_ExpandLeft = 10f;
SetModeResize();
}
}

private void RightHit(Area2D pEntered)
{
if (pEntered is PlayerBullet)
{
_ExpandRight = 10f;
_MoveRight += 10f;
SetModeResize();
}
}

Here comes the most important part, the State Machine, it’s going to resize the _WinRect with a certain speed.

First, the mandatory SetModeVoid(), it usually doesn’t usually do something but we’re exploiting it to be sure our window come back to it’s original size.

private void SetModeVoid()
{
doAction = DoActionVoid;
}

private void DoActionVoid()
{
if (_WinRect.Size != _DefaultWindowSize)
{
_WinRect.Size = _WinRect.Size.Lerp(_DefaultWindowSize, 10 * processDelta);
}
}

Now, the SetModeResize(), here’s the useful part of the Rect2I class, you can individualy control each side of the rect with the GrowIndividual() method with a lerp to interpolate the values with a speed.

private void SetModeResize()
{
doAction = DoActionResize;

}

private void DoActionResize()
{
if (Mathf.Abs(_ExpandLeft) < 0.01f)
{
_ExpandLeft = 0.0f;
}
else
_ExpandLeft = Mathf.Lerp(_ExpandLeft, 0, 10 * processDelta);

if (Mathf.Abs(_ExpandRight) < 0.01f)
{
_ExpandRight = 0;
_MoveRight = 0;

}

else
{
_ExpandRight = Mathf.Lerp(_ExpandRight, 0, 10 * processDelta);

_WinRect.Position = _WinRect.Position.Lerp(_WinRect.Position + new Vector2(_MoveRight, 0), 10 * processDelta);

}

if (Mathf.Abs(_ExpandUp) < 0.01f)
{
_ExpandUp = 0.0f;

}
else
_ExpandUp = Mathf.Lerp(_ExpandUp, 0, 10 * processDelta);

if (Mathf.Abs(_ExpandDown) < 0.01f)
{
_ExpandDown = 0.0f;
_MoveDown = 0;
}

else
{
_WinRect.Position = _WinRect.Position.Lerp(_WinRect.Position + new Vector2(0, _MoveDown), 10 * processDelta);

_ExpandDown = Mathf.Lerp(_ExpandDown, 0, 10 * processDelta);

}

_WinRect = _WinRect.GrowIndividual(_ExpandLeft, _ExpandUp, _ExpandRight, _ExpandDown);

if (_ExpandDown == 0 && _ExpandLeft == 0 && _ExpandUp == 0 && _ExpandRight == 0)
{
SetModeVoid();

}
}

And ready! Now your window will start to move and resize depending on where your bullet hit.

Conclusion

In conclusion, the advent of window nodes in Godot 4 opens up a myriad of possibilities for game design innovation. From emulating classic desktop experiences to integrating windows as dynamic gameplay elements, this feature heralds a new era of creativity within the gaming landscape. While not every game concept may seamlessly align with this approach, it nonetheless represents a promising avenue for exploration and experimentation among game designers.

Sources : Godot Documentation & Torcado

Powered by Forestry.md