Skip to content

panmona/minigolf-tutorial

Repository files navigation

Minigolf with Godot

This repository is for the hands-on part in the Godot session for the DEV days 2026.

Prerequisites

  • "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)

Getting started

  1. Clone the repository
  2. Open the Godot editor
    • If your firewall does not allow you to open the file, follow these steps:
    1. Right click on the .exe
    2. Click on properties
    3. At the bottom of the appearing window, check the checkbox Unblock
    4. Click on okay
    5. Try again
  3. Click on Import
  4. Select the folder of where you cloned your project
  5. Open the project

Set up external editor support

  1. In the top of your Godot editor window click on Editor
  2. Click on Editor Settings
  3. Search for editor
  4. Scroll down to the bottom and go to the Editor section under Dotnet
  5. 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

Tutorial

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.

Create a ball

Let's create a ball:

  1. Click on Scene, then New Scene
  2. In the scene tree choose Other Node and then search for RigidBody3D and click on Create. A RigidBody is an object that can be moved by external forces i.e. a physics simulation.
  3. Rename the RigidBody3D node to Ball (double click on the node, or right click and Rename)
  4. Save this scene under scenes as ball.tscn
  5. You will now see a warning that the RigidBody has no collision shape. Let's fix this:
    1. Either right-click on the Ball node and select Add Child Node... or press Ctrl+A
    2. Select a CollisionShape3D and create it
  6. You will now have another warning on the collision shape, that says that we also need an actual shape:
  7. 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 Shape label. We select a SphereShape3D:

  1. Then we click on the input again to open the shape settings. We set the radius to 0.021

Add ball texture

Now we only have a collision shape, but we also want to see a texture. Let's add it:

  1. Add a new child node to the Ball node (not the collision shape). The node type should be MeshInstance3D

Reminder: right-click on the Ball node and select Add Child Node... or press Ctrl+A. When using Ctrl+A you may need to move the node afterwards in the node tree with drag and drop.

  1. Similar to the collision shape, we need to an actual mesh to the node. Click on the node, in the inspector open the Mesh dropdown and select SphereMesh

  1. Open the Mesh settings (click on the visual sphere in the input field)

  2. Set the radius to 0.021 and the height to 0.042

  1. Make sure to save your scene (Ctrl+S)

  2. 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 Game node.

  3. We must set the position of the ball by following these steps:

    1. Click on the ball node on the left or on the ball in the 3d view.
    2. On the right side of the editor in the Inspector panel open the Transform section.
    3. Set the following position: x: -0.012, y: 0.08, z: 0.4 It should afterwards look like in the screenshot below.

Create a club

Let's create a club which we will use to hit the ball.

  1. In the editor right click on the already created Club node and select Save Branch as Scene...
  2. Go to the already created scenes folder and save your scene as club.tscn
  3. Click on the clapperboard icon to open the scene in the editor.

  1. To later be able to detect collisions we need to create an Area3D node. Do the following to do that:

    1. Add an Area3D node as a child node of the Club
    2. Rename this node to Body.
    3. Set the following position for the body: x: -0.07, y: 0.65, z: 0.1
  2. You will now see the same warning that the area has no collision shape. We will fix it soon.

  3. Let us first add the club texture:

    1. In the file system panel select the club-blue.glb file and drag it below the Area3D node, now named Body (or into the 3d view, but you just need to make sure to reparent the node to below the Area3D node).
    2. You should now see the following structure:

    1. Set this position for the club (in the inspector, transform section): x: 0.25, y: 0.1, z: -0.25
    2. Set this rotation: x: 15, y: 90, z: 0
    3. 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.
  4. Now we can fix the collision shape warning by adding a new collision shape:

    1. Add a new child node to the Body
    2. Choose a CollisionShape3D node
    3. 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 Shape label. But this time we select a BoxShape3D:

    1. 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:

    1. Make sure to save your club scene :)

Make the club move

Before we get to programming, let us first create a so called input map to define which inputs our game should listen to:

  1. Click on Project, then Project Settings
  2. Go to the Input Map tab
  3. Add the following actions by typing into the Add New Action input and clicking on Add or pressing enter:
    • Shoot
    • TurnLeft
    • TurnRight
  4. Click on + in the right of all of the actions and define an appropriate key by pressing this key (e.g. Space for Shoot, Left Arrow Key for TurnLeft and Right Arrow Key for TurnRight) and then click on Ok.
  5. 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:
  6. You can then close this window.

Create our first script

Now we are finally ready for our first code.

  1. Right click on the Club node, select Attach script
  2. In the appearing dialog, make sure C# is selected and set the path to scripts. It should look like this:
  3. Click on Create
  4. 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)
  5. In your editor in the scripts folder create another C# file and call it InputMapActions. 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.

  1. Now back to our Club script. Let's make the club move. Add the following code to the _Process method:
[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);
    }
}
  1. 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.
  2. The Export attribute 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).
  3. Test whether the rotation works by starting the game :)

Load up the club

Next, we want to be able to load up the club so that we can shoot the ball:

  1. Add the following new Export, rebuild the project and set a value in the inspector (e.g. 60):
[Export] 
private float _maxRotation = 0;
  1. 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");
}
  1. In the _Process method 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);
}
  1. 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.

Make the ball move

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:

  1. Go to your ball scene and attach a new C# script to the Ball node, 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.
  2. Go to your Club script and add the following new method. The Basis.Z is 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);
    }
}
  1. Afterwards, we only need to register the event handler in our _Ready method of the Club script:
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

Approach the ball

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:

  1. Add the following code to the top of your Club script / your _Ready method. 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 because Ball is not a child node but a sibling node. Meaning the Club is 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);
}
  1. Build your project, go to the game scene and drag in the Ball node from the scene tree to the ball path property in the inspector of the club node.
  2. Add a new method in the Club script 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);
}
  1. Build your project

Now this method just needs to be called. Let's create a button for this:

  1. In your main scene add a new child node of the type Button
  2. In the text input, type "Approach"
  3. On the right side of editor, change the tab from Inspector to Signals
  4. In the top section BaseButton, double-click on pressed()
  5. In the appearing window, choose the Club node.
  6. Click on Pick. In the new window click on the Approach() method you created and click on Ok.

If you don't see the method, rebuild your project and open this window again.

  1. 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!

Stretch goal, only if you have time or for at home

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 :)

Show basic finished screen

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:

  1. Under the Level node you will find a node FinishHole. Add a new child node to it of the type Area3D
  2. As the child node of the Area, add another child of type CollisionShape3D
  3. Then we will need to add a new shape, use the type CyclinderShape3D
  4. Scale it, so that it fits exactly into the hole
  5. 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 it Hole.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.

  1. Add a new script LevelSignals.cs in your scripts folder.
  2. Add the below code. This creates a custom signal, and when the FinishedLevel method 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:

  1. Go to: Project -> Project Settings -> Globals.
  2. Click on the folder icon, search for your script and select it.
  3. 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.

  1. First go to the Hole script and replace the GD.Print with this code:
GetNode<LevelSignals>("/root/LevelSignals")
  .FinishedLevel();
  1. Go to the Game script in your text editor (it should already be created as it is included in the template you got)
  2. 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:

  1. Add a new child node to the Game node of the type Label.
  2. Rename it to FinishedLabel
  3. Add some text to the label, e.g. Congratulations, you finished the game!
  4. 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.
  5. In the inspector of the label, in the CanvasItem section, uncheck the Visibility checkbox.
  6. In the Game script we can now set the label to visible when the level is finished:
private void OnLevelFinished()
{
    GetNode<Label>("FinishedLabel").Visible = true;
}

Already finished and still have time?

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.

References

About

Tutorial for creating a POC minigolf game in Godot

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages