This repository is for the hands-on part in the Godot session for the DEV days 2026.
- "Godot Engine - .NET" version 4.6.x installed
- .NET SDK 10 installed
- Any editor installed to modify the code (e.g. JetBrains Rider, Visual Studio, Visual Studio Code)
- Clone the repository
- Open the Godot editor
- If your firewall does not allow you to open the file, follow these steps:
- Right click on the .exe
- Click on properties
- At the bottom of the appearing window, check the checkbox
Unblock - Click on okay
- Try again
- Click on
Import - Select the folder of where you cloned your project
- Open the project
- In the top of your Godot editor window click on
Editor - Click on
Editor Settings - Search for
editor - Scroll down to the bottom and go to the
Editorsection underDotnet - Select the editor you want to use and have installed.
More details can also be found here, but it should not be needed to consult these docs: https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_basics.html#configuring-an-external-editor
Some things are already set up for you in the project, for example a very basic level, a camera, lighting and imported assets (with collision shapes).
Click on the play button and check if you can run the project and everything is working. What you should see is the following window:

The goal of the tutorial is to create a prototype of a minigolf game. Don't expect that we will have a polished game including a realistic physics simulation after this tutorial.
Let's create a ball:
- Click on
Scene, thenNew Scene - In the scene tree choose
Other Nodeand then search forRigidBody3Dand click onCreate. A RigidBody is an object that can be moved by external forces i.e. a physics simulation.
- Rename the
RigidBody3Dnode toBall(double click on the node, or right click andRename) - Save this scene under
scenesasball.tscn - You will now see a warning that the RigidBody has no collision shape.
Let's fix this:
- Either right-click on the
Ballnode and selectAdd Child Node...or pressCtrl+A - Select a
CollisionShape3Dand create it
- Either right-click on the
- You will now have another warning on the collision shape, that says that we also need an actual shape:

- Let's add a shape by selecting the collision shape.
Then in the the inspector (located on the right side of the godot editor window) we click on the dropdown to the right of the
Shapelabel. We select aSphereShape3D:
- Then we click on the input again to open the shape settings. We set the radius to
0.021
Now we only have a collision shape, but we also want to see a texture. Let's add it:
- Add a new child node to the
Ballnode (not the collision shape). The node type should beMeshInstance3D
Reminder: right-click on the
Ballnode and selectAdd Child Node...or pressCtrl+A. When usingCtrl+Ayou may need to move the node afterwards in the node tree with drag and drop.
- Similar to the collision shape, we need to an actual mesh to the node. Click on the node, in the inspector open the
Meshdropdown and selectSphereMesh
-
Open the
Meshsettings (click on the visual sphere in the input field) -
Set the radius to
0.021and the height to0.042
-
Make sure to save your scene (Ctrl+S)
-
Now, let's go back to the game scene, and add the ball by just dragging it in (in the file system, open the scenes, drag the ball scene). Make sure that it is a direct child of the
Gamenode. -
We must set the position of the ball by following these steps:
- Click on the ball node on the left or on the ball in the 3d view.
- On the right side of the editor in the
Inspectorpanel open theTransformsection. - Set the following position:
x: -0.012,y: 0.08,z: 0.4It should afterwards look like in the screenshot below.
Let's create a club which we will use to hit the ball.
- In the editor right click on the already created
Clubnode and selectSave Branch as Scene...
- Go to the already created
scenesfolder and save your scene asclub.tscn - Click on the clapperboard icon to open the scene in the editor.
-
To later be able to detect collisions we need to create an
Area3Dnode. Do the following to do that: -
You will now see the same warning that the area has no collision shape. We will fix it soon.
-
Let us first add the club texture:
- In the file system panel select the
club-blue.glbfile and drag it below theArea3Dnode, now namedBody(or into the 3d view, but you just need to make sure to reparent the node to below theArea3Dnode). - You should now see the following structure:
- Set this position for the club (in the inspector, transform section):
x: 0.25,y: 0.1,z: -0.25 - Set this rotation:
x: 15,y: 90,z: 0 - We set these specific positions for the club and body, so that it will visually look like the club is offset from the ball but we can set the position of the overall node to the exact same as the ball.
- In the file system panel select the
-
Now we can fix the collision shape warning by adding a new collision shape:
- Add a new child node to the
Body - Choose a
CollisionShape3Dnode - Just like for the Ball, let's add a shape by selecting the node, in the inspector click on the dropdown to the right of the
Shapelabel. But this time we select aBoxShape3D:
- Now we need to resize it to approximately the shape of the bottom of the club. Use the red dots to resize it. It can be helpful to click on the separate axes in the 3d gizmo to get a 2d view out of the view of this axis and do your adjustments in these different views. Your end result should be similar to this:
- Make sure to save your club scene :)
- Add a new child node to the
Before we get to programming, let us first create a so called input map to define which inputs our game should listen to:
- Click on
Project, thenProject Settings - Go to the
Input Maptab - Add the following actions by typing into the
Add New Actioninput and clicking onAddor pressing enter: - Click on
+in the right of all of the actions and define an appropriate key by pressing this key (e.g.Spacefor Shoot,Left Arrow Keyfor TurnLeft andRight Arrow Keyfor TurnRight) and then click onOk. - Define a key for all three actions and then the input map should look similar to the following view depending on what keys you defined:

- You can then close this window.
Now we are finally ready for our first code.
- Right click on the
Clubnode, selectAttach script - In the appearing dialog, make sure
C#is selected and set the path toscripts. It should look like this:
- Click on
Create - Your configured editor should now open this file and syntax highlighting should be working. (If something does not work here, consult the section external editor support again or feel free to ask your instructor for help)
- In your editor in the
scriptsfolder create another C# file and call itInputMapActions. We will use this to have a common place for the names of our input actions if we need them in different scripts. Add the following code:
public static class InputMapActions
{
public const string Shoot = "Shoot";
public const string TurnLeft = "TurnLeft";
public const string TurnRight = "TurnRight";
}Don't use the autogenerated namespace by your editor, or alternatively make sure to additionally import this namespace in the scripts where you use this class.
- Now back to our Club script. Let's make the club move. Add the following code to the
_Processmethod:
[Export]
private float _rotationSpeed = 0;
public override void _Process(double delta)
{
if (Input.IsActionPressed(InputMapActions.TurnLeft))
{
RotateY((float)delta * _rotationSpeed);
}
if (Input.IsActionPressed(InputMapActions.TurnRight))
{
RotateY((float)delta * -_rotationSpeed);
}
}- This code is relatively straightforward: On each idle frame, it checks whether you press the Left/Right inputs and if so it rotates the club. As the club is positioned slightly behind the centre, it will rotate nicely around the centre.
- The
Exportattribute makes a variable settable through the inspector in the godot editor. Build your project, and set the rotation speed to a value you find appropriate (e.g.3). - Test whether the rotation works by starting the game :)
Next, we want to be able to load up the club so that we can shoot the ball:
- Add the following new Export, rebuild the project and set a value in the inspector (e.g.
60):
[Export]
private float _maxRotation = 0;- Add the following code (make sure that you end up with only one _Ready method). This code gives us access to the Area3D node so that we can manipulate it.
private Area3D _body = default!;
private Tween? _loadingUpTween;
public override void _Ready(){
_body = GetNode<Area3D>("Body");
}- In the
_Processmethod add the following code at the bottom of the method (we will explain it after ):
if (Input.IsActionJustPressed(InputMapActions.Shoot))
{
_loadingUpTween = CreateTween();
_loadingUpTween.TweenProperty(_body, "rotation:x", Mathf.DegToRad(_maxRotation), 2)
.FromCurrent();
}
if (Input.IsActionJustReleased(InputMapActions.Shoot))
{
_loadingUpTween?.Kill();
var t = CreateTween();
t.TweenProperty(_body, "rotation:x", Mathf.DegToRad(-_maxRotation), 0.5)
.FromCurrent();
t.TweenProperty(_body, "rotation:x", Mathf.DegToRad(0), 0.5);
}- Try it out: When you now press your shoot action, the club should now rotate upwards on press and forwards when you release the key again.
As this code is not straightforward to understand, let's go through step by step:
- We check if the shoot action was pressed since the last execution of the method
- If so, we create a tween that rotates the club backwards.
- In a tween you define a start and end value of a property and a duration. The program then animates this property from start to end by calculation all values in between.
- The specific tween that we create here, rotates the bodys x-axis until the max rotation (= rotating it backwards), starting from the current rotation. It takes 2s to fully load it up (this is the last parameter of TweenProperty). The max rotation must be converted to radians as Godot always uses radians internally. That's why we need to convert the degrees to radians.
- In case the shoot action is released, we swing the club forwards again with a tween in the other direction. We need to cancel the previous tween (using
kill), as the tween just keeps running until its finished and we could otherwise end up with a different end position than we expect.
You may have noticed that the ball does not move yet when the club hits it. The collision is detected but nothing happens just yet because we didn't tell the game that any forces should be applied on a collision.
Let's change that:
- Go to your ball scene and attach a new C# script to the
Ballnode, save it under scripts and choose the "Object: Empty" template. Or you can afterwards remove the methods in the class. We only need this class to detect whether a colliding object is a ball or not. - Go to your
Clubscript and add the following new method. TheBasis.Zis the vector that points forward from the perspective of the club and we use it to give the direction in which the impulse should be applied.
private void OnBodyEntered(Node3D n)
{
if (n is Ball ball && ball.LinearVelocity.IsZeroApprox())
{
ball.ApplyCentralImpulse(Basis.Z);
}
}- Afterwards, we only need to register the event handler in our
_Readymethod of theClubscript:
public override void _Ready()
{
// ... other code ...
_body.BodyEntered += OnBodyEntered;
}You can now test it and will see that the ball moves after we hit it.
You will also notice that the ball does not get slower or bounces off wall in the way that we would expect it to.
To improve this we would need to add a PhysicsMaterial to the Ball, out of time reasons we will not do that today.
If you are interested you can find some more info about this here: https://docs.godotengine.org/en/stable/classes/class_physicsmaterial.html
You will notice that after you hit the ball for example into the wall and it stops moving, you will not be able to hit it a second time because your club is too far away. Let's add a button that allows you to approach the ball with your club:
- Add the following code to the top of your
Clubscript / your_Readymethod. This code allows you to set a path to the Ball in the godot editor (Export) and it ensures that you don't forget to set it (Debug.Assert). Through the path it can get the node. We use this way of setting the path through the editor instead of specifying it manually becauseBallis not a child node but a sibling node. Meaning theClubis not in control of this node.
[Export] private NodePath _ballPath = default!;
private Ball _ball = default!;
public override void _Ready()
{
// ... other code ...
Debug.Assert(_ballPath != null);
_ball = GetNode<Ball>(_ballPath);
}- Build your project, go to the game scene and drag in the
Ballnode from the scene tree to the ball path property in the inspector of the club node. - Add a new method in the
Clubscript with the following code that allows us to approach the Ball. We again use a tween so that the position is animated smoothly. And because we have offset the clubs position, we can just use the position of the ball as the end position.
public void Approach()
{
CreateTween()
.TweenProperty(this, "position", _ball.Position, 1)
.FromCurrent()
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Cubic);
}- Build your project
Now this method just needs to be called. Let's create a button for this:
- In your main scene add a new child node of the type
Button - In the text input, type "Approach"
- On the right side of editor, change the tab from
InspectortoSignals - In the top section
BaseButton, double-click onpressed()
- In the appearing window, choose the
Clubnode. - Click on
Pick. In the new window click on theApproach()method you created and click onOk.
If you don't see the method, rebuild your project and open this window again.
- Click on
Connect
If you start the game again the button should now be displayed and if you press it, the club should smoothly move to the ball.
Good job, now we have a prototype of our minigolf game!
If you still have time, first think about whether you understood everything you did until now and if not: Try to understand it or otherwise ask.
If you have and you're eager to do more, please go on :)
When the player shoots the ball into the hole, we want there something to happen. For example a finish screen should be displayed. For that to happen, you might have already guessed it, we need to detect the collision by using a collision shape and then through a signal notify the main game script.
Let's do it:
- Under the Level node you will find a node
FinishHole. Add a new child node to it of the typeArea3D - As the child node of the Area, add another child of type
CollisionShape3D - Then we will need to add a new shape, use the type
CyclinderShape3D - Scale it, so that it fits exactly into the hole
- Now we need a new script, that will check that the ball entered. Attach a new script to the
Area3D(not the FinishHole!), save it under scripts and name itHole.cs. Add the following code to the script:
using Godot;
public partial class Hole : Area3D
{
public override void _Ready()
{
BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node3D body)
{
if (body is Ball)
{
GD.Print("Ball is in the hole!");
}
}
}Now when the ball enters the hole, we should see that Ball is in the hole! is printed to the console.
But we would also like to do something useful, like show a finish screen. For that we will create a signal, to which all interested parties (like our game script) can register to listen to so they can react if needed.
This signal must be at a central place, so it will be slightly more complex to set it up.
We will use a Globals script, which is added automatically by Godot on startup and can be used from everywhere.
- Add a new script
LevelSignals.csin your scripts folder. - Add the below code. This creates a custom signal, and when the
FinishedLevelmethod is called, we emit (= trigger) this signal. If you later want to know more about custom signals, you can read more in the docs.
using Godot;
public partial class LevelSignals : Node
{
[Signal] public delegate void LevelFinishedEventHandler();
public void FinishedLevel()
{
EmitSignal(SignalName.LevelFinished);
}
}Now we need to add this script as an autoload script:
- Go to:
Project -> Project Settings -> Globals. - Click on the folder icon, search for your script and select it.
- Then press on Add. If you executed the steps correctly, your UI should now look like this:

With the signal created and the autoload script set up, we must now emit this signal from the hole and then we are ready to listen to this signal in our Game script.
- First go to the
Holescript and replace theGD.Printwith this code:
GetNode<LevelSignals>("/root/LevelSignals")
.FinishedLevel();- Go to the
Gamescript in your text editor (it should already be created as it is included in the template you got) - Listen to the signal by registering an event handler, using the following code:
public override void _Ready()
{
GetNode<LevelSignals>("/root/LevelSignals")
.LevelFinished += OnLevelFinished;
}
private void OnLevelFinished()
{
// TODO show text in UI
}Now the last step, is to show text in the UI. To do this, we keep it very simple:
- Add a new child node to the
Gamenode of the typeLabel. - Rename it to
FinishedLabel - Add some text to the label, e.g.
Congratulations, you finished the game! - Drag the text to the position you want it to appear. Godot has very good UI layout capabilities (the godot editor itself is built with godot), but out of time reasons we will not go into further detail. If you are interested, this documentation article can be a good starting point.
- In the inspector of the label, in the
CanvasItemsection, uncheck theVisibilitycheckbox.
- In the
Gamescript we can now set the label to visible when the level is finished:
private void OnLevelFinished()
{
GetNode<Label>("FinishedLabel").Visible = true;
}Ideas for what you could do now:
- Think about whether you understood everything you did until now and if not: try to understand it or otherwise ask
- Allow the player to move the camera slower by using Shift+Left/Right.
Hint: Add new actions in your input map :)
- Track how many strokes you used and show it in the main UI or the finished screen
Hint: Use a custom signal, use the Game script
- Make the force applied to the ball variable, based on how long the club is loaded
Hint: Delta in the _Process function contains how much time since the last call passed. Also: Use the Mathf.Clamp method.
- This tutorial is based on the tutorial here, but improved on and simplified: https://ivc.pages.ost.ch/Unterricht/VisualComputing/GodotTutorial/csharp/_minigolf.html
- The assets are from kenney: https://kenney.nl/assets/minigolf-kit










