Skip to main content

Command Palette

Search for a command to run...

A Basic Networked Godot 3D Game in C++ (GDExtension)

Updated
13 min read

Pretext

When writing a game in Godot you have a few options in terms of coding languages.
(Very simplified):
You can write in GDScript (First class support)
You can write in C# (Somewhat first class support)
You can write in C++ GDExtension (Essentially a dll you inject into godot to load)
And you can write in third party languages like Rust in GDExtension (Also a dll I assume)
You can write in C++ modules (I’m not sure how this works)

If you are coming from an Unreal Engine background or even lower level you may be used to C++ and want to stick with C++, unfortunately the documentation for ‘getting started’ in C++ Godot is a bit confusing, this guide is just a writeup of what I did to get a basic ‘starter’ template that is essentially the boilerplate for a Roblox like movement 3D game with networking.

Getting A Development Environment Setup

If you’re expecting to be able to setup Rider (or Visual Studio) easily, I found it quite hard, I am sure you can find someone who has written up a third-party approach to getting it working but officially it is not supported.

If you need more help - GDExtension C++ example — Godot Engine (4.4) documentation in English is fairly good for the basics

End Goal - Project Structure

Project structure wise we are going to end up looking like this.

-demo => your test godot game to essentially treat as staging grounds
-src => your cpp source code for the extension
-godot => the engine for godot (optional but recommended) => git submodule
-godot-cpp => the official c++ gdextension binding => git submodule
-.vscode => configurations to make vscode build test easier

Step 1 - Create and clone relevant folders

Before cloning on anything decide on a godot version, i chose 4.5 so replace <desired_branch_version> with 4.5

mkdir <godot_project_foldername>
cd <godot_project_foldername>
git init
git submodule add -b <desired_branch_version> https://github.com/godotengine/godot-cpp
cd godot-cpp
git submodule update --init
cd ..
git submodule add -b <desired_branch_version> https://github.com/godotengine/godot
cd godot
git submodule update --init
cd ..
mkdir src

Now you are going to have the godot engine source code as a submodule in godot (you will need this for breakpoints but its optional, feel free to use a release from godot but debugging will be painful), you will have the gd extension bindings in godot-cpp and you have a folder called src we are going to dump our awful C++ code in.

Step 2 - Create some initial files

in src we are going to create a few files, you can structure this however you want however i chose to do src/core/… src/resources/… src/state_machines/… and src/register_types.hpp/cpp

For now, I would suggest copypasta the below, you can tweak/delete after you get this garbage running.
arora_player.hpp

#pragma once

#include <godot_cpp/classes/character_body3d.hpp>
#include <godot_cpp/classes/input_event.hpp>
#include <godot_cpp/classes/packed_scene.hpp>
#include "resources/arora_player_stats.hpp"
#include "resources/arora_settings.hpp"

namespace godot
{
    class AroraPlayer : public CharacterBody3D
    {
        GDCLASS(AroraPlayer, CharacterBody3D)
    private:
        class AroraPlayerMovementStateMachine* movement_state_machine;
        class CollisionShape3D* collider;
        class Node3D* pivot, *third_person_body, *first_person_body;
        class SpringArm3D* camera_boom;
        class Camera3D* camera;

        Ref<AroraPlayerStats> player_stats;
        Ref<AroraSettings> arora_settings;
        Ref<PackedScene> third_person_mesh;
        Ref<PackedScene> first_person_mesh;

        float boom_rot_x = 0.0f, boom_rot_y = 0.0f;

        class Input* InputSubsystem;
        const class Engine* EngineSubsystem;
        class Node3D* _create_pivot();
        class CollisionShape3D* _create_collider();
        class SpringArm3D* _create_spring_arm();
        class Camera3D* _create_camera(class Node3D* parent);
        class Node3D* _create_third_person_body(class Node3D* parent);
        class Node3D* _create_first_person_body(class Node3D* parent);

        void _swap_bodies_visibility(bool p_is_first_person);
        bool _is_editor() const;
    protected:
        static void _bind_methods();
    public:
        AroraPlayer();
        ~AroraPlayer();
        Ref<AroraPlayerStats> get_player_stats() const;
        void set_player_stats(const Ref<AroraPlayerStats> &p_player_stats);
        Ref<PackedScene> get_third_person_mesh() const;
        void set_third_person_mesh(const Ref<PackedScene>& p_third_person_mesh);
        Ref<PackedScene> get_first_person_mesh() const;
        void set_first_person_mesh(const Ref<PackedScene>& p_first_person_mesh);
        Ref<AroraSettings> get_arora_settings() const;
        void set_arora_settings(const Ref<AroraSettings>& p_arora_settings);
        const float get_length() const;
        void set_length(float p_length);
        void rotate_boom_xy(float p_x, float p_y);
        const Basis get_camera_boom_basis() const;
        void face_pivot_to(const Basis& basis);
        bool is_third_person() const;
        void set_first_person();
        void set_third_person();
        virtual void _physics_process(double p_delta) override;
        virtual void _ready() override;
        virtual void _unhandled_input(const Ref<InputEvent> &p_event) override;
        virtual void _input(const Ref<InputEvent> &p_event) override;
    };
}

arora_player.cpp

#include <godot_cpp/classes/input.hpp>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/classes/collision_shape3d.hpp>
#include <godot_cpp/classes/capsule_shape3d.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#include <godot_cpp/classes/input_event.hpp>
#include <godot_cpp/classes/input_event_mouse_motion.hpp>
#include <godot_cpp/classes/input_event_key.hpp>
#include <godot_cpp/classes/camera3d.hpp>
#include <godot_cpp/classes/spring_arm3d.hpp>
#include <godot_cpp/classes/animation_tree.hpp>
#include "arora_player.hpp"
#include "resources/arora_player_stats.hpp"
#include "state_machines/arora_player_movement_statemachine.hpp"

using namespace godot;

static constexpr char *pivot_name = "Pivot";
static constexpr char *collider_name = "Collider";
static constexpr char *camera_name = "Camera";
static constexpr char *spring_arm_name = "CameraBoom";
static constexpr char *third_person_mesh_name = "ThirdPersonBody";
static constexpr char *first_person_mesh_name = "FirstPersonBody";
static constexpr float collider_radius = 2.0f;
static constexpr float collider_height = 8.0f;

Node3D *AroraPlayer::_create_pivot()
{
    if (Node *local_pivot = get_node_or_null(pivot_name))
    {
        print_line("pivot exists, not creating a new one");
        return Object::cast_to<Node3D>(local_pivot);
    }
    print_line("creating a new pivot");
    Node3D *local_pivot = memnew(Node3D);
    local_pivot->set_name(pivot_name);
    add_child(local_pivot);
    local_pivot->set_owner(get_tree()->get_edited_scene_root());
    return local_pivot;
}

CollisionShape3D *AroraPlayer::_create_collider()
{
    if (Node *local_collider = get_node_or_null(collider_name))
    {
        print_line("collider exists, not creating a new one");
        return Object::cast_to<CollisionShape3D>(local_collider);
    }
    print_line("creating a new collider");
    CapsuleShape3D *collider_shape = memnew(CapsuleShape3D);
    collider_shape->set_radius(collider_radius);
    collider_shape->set_height(collider_height);
    CollisionShape3D *local_collider = memnew(CollisionShape3D);
    local_collider->set_shape(collider_shape);
    local_collider->set_name(collider_name);
    add_child(local_collider);
    local_collider->set_owner(get_tree()->get_edited_scene_root());
    return local_collider;
}

SpringArm3D *AroraPlayer::_create_spring_arm()
{
    if (Node *local_spring_arm = get_node_or_null(spring_arm_name))
    {
        print_line("spring_arm exists, not creating a new one");
        return Object::cast_to<SpringArm3D>(local_spring_arm);
    }
    print_line("creating a spring arm");
    SpringArm3D *local_spring_arm = memnew(SpringArm3D);
    local_spring_arm->set_name(spring_arm_name);
    add_child(local_spring_arm);
    local_spring_arm->set_owner(get_tree()->get_edited_scene_root());
    return local_spring_arm;
}

Camera3D *AroraPlayer::_create_camera(Node3D *parent)
{
    if (Node *local_camera = parent->get_node_or_null(camera_name))
    {
        print_line("camera exists, not creating a new one");
        return Object::cast_to<Camera3D>(local_camera);
    }
    print_line("creating a camera");
    Camera3D *local_camera = memnew(Camera3D);
    local_camera->set_name(camera_name);
    parent->add_child(local_camera);
    local_camera->set_owner(parent->get_tree()->get_edited_scene_root());
    return local_camera;
}

Node3D *AroraPlayer::_create_third_person_body(Node3D *parent)
{
    if (!third_person_mesh.is_valid())
    {
        print_line("warning: third_person_mesh not set, this will crash at runtime");
        return nullptr;
    }
    if (Node* local_third_person_body = parent->get_node_or_null(third_person_mesh_name))
    {
        print_line("third_person_mesh exists, not creating a new one");
        return Object::cast_to<Node3D>(local_third_person_body);
    }
    print_line("third_person_mesh doesn't exist, creating a new one");
    Node *instanced_third_person = third_person_mesh->instantiate();
    instanced_third_person->set_name(third_person_mesh_name);
    parent->add_child(instanced_third_person);
    instanced_third_person->set_owner(parent->get_tree()->get_edited_scene_root());
    return Object::cast_to<Node3D>(instanced_third_person);
}

Node3D *godot::AroraPlayer::_create_first_person_body(Node3D *parent)
{
    if (!first_person_mesh.is_valid())
    {
        print_line("warning: first_person_mesh not set, this will crash at runtime");
        return nullptr;
    }
    if (Node* local_first_person_body = parent->get_node_or_null(first_person_mesh_name))
    {
        print_line("first_person_mesh exists, not creating a new one");
        return Object::cast_to<Node3D>(local_first_person_body);
    }
    print_line("first_person_mesh doesn't exist, creating a new one");
    Node *instanced_first_person = first_person_mesh->instantiate();
    instanced_first_person->set_name(first_person_mesh_name);
    parent->add_child(instanced_first_person);
    instanced_first_person->set_owner(parent->get_tree()->get_edited_scene_root());
    return Object::cast_to<Node3D>(instanced_first_person);
}

void AroraPlayer::set_first_person()
{
    _swap_bodies_visibility(true);
    InputSubsystem->set_mouse_mode(Input::MouseMode::MOUSE_MODE_CAPTURED);
}

void AroraPlayer::set_third_person()
{
    _swap_bodies_visibility(false);
    InputSubsystem->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE);
}

void AroraPlayer::_swap_bodies_visibility(bool p_is_first_person)
{
    third_person_body->set_visible(!p_is_first_person);
    first_person_body->set_visible(p_is_first_person);
}

// in godot version currently used is_editor_hint causes crash sometimes (likely some weird engine race condition)
// this is a hack to try and avoid that
bool AroraPlayer::_is_editor() const
{
    try
    {
        return !EngineSubsystem || EngineSubsystem->is_editor_hint();
    }
    catch (...)
    {
        // very noisy log...
        // print_error("editor threw error trying to use the hint, going to just return true and continue rather then hard crashing");
    }
    return true;
}

void AroraPlayer::_bind_methods()
{
    ClassDB::bind_method(D_METHOD("get_third_person_mesh"), &AroraPlayer::get_third_person_mesh);
    ClassDB::bind_method(D_METHOD("set_third_person_mesh", "p_third_person_mesh"), &AroraPlayer::set_third_person_mesh);
    ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "third_person_mesh", PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"), "set_third_person_mesh", "get_third_person_mesh");
    ClassDB::bind_method(D_METHOD("get_player_stats"), &AroraPlayer::get_player_stats);
    ClassDB::bind_method(D_METHOD("set_player_stats", "p_player_stats"), &AroraPlayer::set_player_stats);
    ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "player_stats", PROPERTY_HINT_RESOURCE_TYPE, "AroraPlayerStats"), "set_player_stats", "get_player_stats");
    ClassDB::bind_method(D_METHOD("get_arora_settings"), &AroraPlayer::get_arora_settings);
    ClassDB::bind_method(D_METHOD("set_arora_settings", "p_arora_settings"), &AroraPlayer::set_arora_settings);
    ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "arora_settings", PROPERTY_HINT_RESOURCE_TYPE, "AroraSettings"), "set_arora_settings", "get_arora_settings");
    ClassDB::bind_method(D_METHOD("get_first_person_mesh"), &AroraPlayer::get_first_person_mesh);
    ClassDB::bind_method(D_METHOD("set_first_person_mesh", "p_first_person_mesh"), &AroraPlayer::set_first_person_mesh);
    ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "first_person_mesh", PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"), "set_first_person_mesh", "get_first_person_mesh");
}

AroraPlayer::AroraPlayer()
{
}

AroraPlayer::~AroraPlayer()
{
}

Ref<AroraPlayerStats> AroraPlayer::get_player_stats() const
{
    return player_stats;
}

void AroraPlayer::set_player_stats(const Ref<AroraPlayerStats>& p_player_stats)
{
    player_stats = p_player_stats;
}

Ref<PackedScene> AroraPlayer::get_third_person_mesh() const
{
    return third_person_mesh;
}

void AroraPlayer::set_third_person_mesh(const Ref<PackedScene> &p_third_person_mesh)
{
    third_person_mesh = p_third_person_mesh;
}

Ref<PackedScene> godot::AroraPlayer::get_first_person_mesh() const
{
    return first_person_mesh;
}

void godot::AroraPlayer::set_first_person_mesh(const Ref<PackedScene> &p_first_person_mesh)
{
    first_person_mesh = p_first_person_mesh;
}

Ref<AroraSettings> AroraPlayer::get_arora_settings() const
{
    return arora_settings;
}

void AroraPlayer::set_arora_settings(const Ref<AroraSettings> &p_arora_settings)
{
    arora_settings = p_arora_settings;
}

const float godot::AroraPlayer::get_length() const
{
    return camera_boom->get_length();
}

static constexpr float THIRD_PERSON_MIN_DISTANCE = 0.05f;
void AroraPlayer::set_length(float p_length)
{
    if (third_person_body)
    {
        if (third_person_body->is_visible() && p_length <= THIRD_PERSON_MIN_DISTANCE)
        {
            set_first_person();
        }
        if (!third_person_body->is_visible() && p_length >= THIRD_PERSON_MIN_DISTANCE)
        {
            set_third_person();
        }
    }
    camera_boom->set_length(p_length);
}

static constexpr float MAX_X_ROTATION_DEGREES = 85.0f;
static float MAX_X_ROTATION_RADIANS = Math::deg_to_rad(MAX_X_ROTATION_DEGREES);
// https://docs.godotengine.org/en/stable/tutorials/3d/using_transforms.html
void AroraPlayer::rotate_boom_xy(float p_x, float p_y)
{
    boom_rot_x = CLAMP(boom_rot_x + p_x, MAX_X_ROTATION_RADIANS * -1, MAX_X_ROTATION_RADIANS);
    boom_rot_y += p_y;
    if (Math::abs(Math::rad_to_deg(boom_rot_x)) >= 360.0f)
    {
        boom_rot_x = 0;
        print_line("reset x rotation to 0");
    }
    if (Math::abs(Math::rad_to_deg(boom_rot_y)) >= 360.0f)
    {
        boom_rot_y = 0;
        print_line("reset y rotation to 0");
    }
    camera_boom->set_basis(Basis());
    camera_boom->rotate_object_local(Vector3(0,1,0), boom_rot_y);
    camera_boom->rotate_object_local(Vector3(1,0,0), boom_rot_x);
}

const Basis AroraPlayer::get_camera_boom_basis() const
{
    return camera_boom->get_basis();
}

void AroraPlayer::face_pivot_to(const Basis& basis)
{
    pivot->set_basis(basis);
}

bool AroraPlayer::is_third_person() const
{
    return camera_boom->get_length() >= THIRD_PERSON_MIN_DISTANCE;
}

void AroraPlayer::_physics_process(double p_delta)
{
    if (!InputSubsystem || !EngineSubsystem)
    {
        return;
    }
    if (!_is_editor())
    {
        movement_state_machine->_physics_process(InputSubsystem, p_delta);
    }
}

void AroraPlayer::_ready()
{
    InputSubsystem = Input::get_singleton();
    EngineSubsystem = Engine::get_singleton();
    pivot = _create_pivot();
    collider = _create_collider();
    camera_boom = _create_spring_arm();
    camera = _create_camera(camera_boom);
    third_person_body = _create_third_person_body(pivot);
    first_person_body = _create_first_person_body(camera);
    // AnimationTree* third_person_state_machine = third_person_body->get_node<AnimationTree>("AnimationTree");
    movement_state_machine = new AroraPlayerMovementStateMachine(this, player_stats, arora_settings);
    if (!_is_editor())
    {
        set_length(0.0f);
    }
}

void AroraPlayer::_unhandled_input(const Ref<InputEvent> &p_event)
{
    if (!_is_editor() && movement_state_machine)
    {
        movement_state_machine->_unhandled_input(p_event);
    }
}

void AroraPlayer::_input(const Ref<InputEvent> &p_event)
{
    if (!p_event.is_valid() || !InputSubsystem)
    {
        return;
    }
    if (movement_state_machine)
    {
        movement_state_machine->_input(InputSubsystem, p_event);
    }
}

arora_player_movement_statemachine.hpp

#pragma once

#include <godot_cpp/classes/ref.hpp>
#include <godot_cpp/classes/input_event.hpp>
#include "core/arora_player.hpp"

namespace godot
{
    class AroraPlayerMovementStateMachine
    {
    private:
        class AroraPlayer* arora_player;
        class Ref<class AroraPlayerStats> arora_player_stats;
        class Ref<class AroraSettings> arora_settings;
        Vector3 target_velocity;
        bool is_rotating_in_third_person;
    public:
        AroraPlayerMovementStateMachine(class AroraPlayer* InAroraPlayer, class Ref<class AroraPlayerStats> AroraPlayerStats, class Ref<class AroraSettings> AroraSettings);
        ~AroraPlayerMovementStateMachine();
        void _unhandled_input(const Ref<InputEvent> &p_event);
        void _physics_process(class Input* InputSubsystem, double p_delta);
        void _input(class Input* InputSubsystem, const Ref<InputEvent> &p_event);
    };
}

arora_player_movement_statemachine.cpp

#include <godot_cpp/classes/input_event_mouse_motion.hpp>
#include <godot_cpp/classes/character_body3d.hpp>
#include <godot_cpp/classes/input.hpp>
#include <godot_cpp/classes/spring_arm3d.hpp>
#include "arora_player_movement_statemachine.hpp"
#include "resources/arora_player_stats.hpp"
#include "resources/arora_settings.hpp"

using namespace godot;

AroraPlayerMovementStateMachine::AroraPlayerMovementStateMachine(AroraPlayer* InAroraPlayer, Ref<AroraPlayerStats> AroraPlayerStats, Ref<AroraSettings> AroraSettings)
{
    is_rotating_in_third_person = false;
    arora_player = InAroraPlayer;
    arora_player_stats = AroraPlayerStats;
    arora_settings = AroraSettings;
}

AroraPlayerMovementStateMachine::~AroraPlayerMovementStateMachine()
{
}

void AroraPlayerMovementStateMachine::_unhandled_input(const Ref<InputEvent> &p_event)
{
    if (!p_event.is_valid())
    {
        return;
    }
    Ref<InputEventMouseMotion> input_mouse_motion = p_event;
    if (input_mouse_motion.is_valid() && (is_rotating_in_third_person || !arora_player->is_third_person()))
    {
        arora_player->rotate_boom_xy(
            -input_mouse_motion->get_relative().y * arora_settings->get_mouse_sensitivity(),
            -input_mouse_motion->get_relative().x * arora_settings->get_mouse_sensitivity()
        );
    }
}

void AroraPlayerMovementStateMachine::_physics_process(Input* InputSubsystem, double p_delta)
{
    Vector2 input_direction = InputSubsystem->get_vector("move_left", "move_right", "move_back", "move_forward");
    Basis boom_basis = arora_player->get_camera_boom_basis();
    Vector3 movement_direction = boom_basis.xform(Vector3(input_direction.x, 0.0f, input_direction.y));
    if (!movement_direction.is_zero_approx())
    {
        movement_direction.normalize();
    }
    target_velocity.x = movement_direction.x * -1 * arora_player_stats->get_speed();
    target_velocity.z = movement_direction.z * -1 * arora_player_stats->get_speed();
    if (!arora_player->is_on_floor())
    {
        target_velocity.y = target_velocity.y - (arora_player_stats->get_fall_acceleration() * p_delta);
    }
    else if (InputSubsystem->is_action_pressed("jump"))
    {
        target_velocity.y = arora_player_stats->get_jump_speed();
    }
    // Can now safely modify the vector3 movement_direction to remove y component?
    if (!movement_direction.is_zero_approx())
    {
        movement_direction.y = 0.0f;
        arora_player->face_pivot_to(Basis::looking_at(movement_direction));
    }
    arora_player->set_velocity(target_velocity);
    arora_player->move_and_slide();
}

void AroraPlayerMovementStateMachine::_input(Input *InputSubsystem, const Ref<InputEvent> &p_event)
{
    if (p_event->is_action_pressed("third_person_rotate"))
    {
        InputSubsystem->set_mouse_mode(arora_player->is_third_person() ? Input::MOUSE_MODE_CAPTURED : InputSubsystem->get_mouse_mode());
        is_rotating_in_third_person = true;
    }
    if (p_event->is_action_released("third_person_rotate"))
    {
        InputSubsystem->set_mouse_mode(arora_player->is_third_person() ? Input::MOUSE_MODE_VISIBLE : InputSubsystem->get_mouse_mode());
        is_rotating_in_third_person = false;
    }
    if (p_event->is_action_pressed("open_menu"))
    {
        InputSubsystem->get_mouse_mode() == Input::MouseMode::MOUSE_MODE_CAPTURED ? InputSubsystem->set_mouse_mode(Input::MouseMode::MOUSE_MODE_VISIBLE) : InputSubsystem->set_mouse_mode(Input::MouseMode::MOUSE_MODE_CAPTURED);
    }
    if (p_event->is_action_released("zoom_out"))
    {
        float newLength = MIN(arora_player->get_length() + arora_settings->get_zoom_speed(), arora_settings->get_max_camera_length());
        arora_player->set_length(newLength);
    }
    if (p_event->is_action_released("zoom_in"))
    {
        float newLength = MAX(arora_player->get_length() - arora_settings->get_zoom_speed(), arora_settings->get_min_camera_length());
        arora_player->set_length(newLength);
    }
}

arora_player_stats.hpp

#pragma once

#include <godot_cpp/classes/resource.hpp>
namespace godot
{
    class AroraPlayerStats : public Resource
    {
        GDCLASS(AroraPlayerStats, Resource)
    private:
        float speed, fall_acceleration, jump_speed;
    protected:
        static void _bind_methods();
    public:
        AroraPlayerStats();
        ~AroraPlayerStats();

        void set_speed(const float p_speed);
        float get_speed() const;
        void set_jump_speed(const float p_jump_speed);
        float get_jump_speed() const;
        void set_fall_acceleration(const float p_fall_acceleration);
        float get_fall_acceleration() const;
    };
}

arora_player_stats.cpp

#include "arora_player_stats.hpp"

using namespace godot;

void AroraPlayerStats::_bind_methods()
{
    ClassDB::bind_method(D_METHOD("get_speed"), &AroraPlayerStats::get_speed);
    ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &AroraPlayerStats::set_speed);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed"), "set_speed", "get_speed");
    ClassDB::bind_method(D_METHOD("get_jump_speed"), &AroraPlayerStats::get_jump_speed);
    ClassDB::bind_method(D_METHOD("set_jump_speed", "p_jump_speed"), &AroraPlayerStats::set_jump_speed);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "jump_speed"), "set_jump_speed", "get_jump_speed");
    ClassDB::bind_method(D_METHOD("get_fall_acceleration"), &AroraPlayerStats::get_fall_acceleration);
    ClassDB::bind_method(D_METHOD("set_fall_acceleration", "p_fall_acceleration"), &AroraPlayerStats::set_fall_acceleration);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fall_acceleration"), "set_fall_acceleration", "get_fall_acceleration");
}

AroraPlayerStats::AroraPlayerStats()
{
    jump_speed = 8.0f;
    speed = 14;
    fall_acceleration = 75;
}

AroraPlayerStats::~AroraPlayerStats()
{
}

void AroraPlayerStats::set_speed(const float p_speed)
{
    speed = p_speed;
}

float AroraPlayerStats::get_speed() const
{
    return speed;
}

void AroraPlayerStats::set_jump_speed(const float p_jump_speed)
{
    jump_speed = p_jump_speed;
}

float AroraPlayerStats::get_jump_speed() const
{
    return jump_speed;
}

void AroraPlayerStats::set_fall_acceleration(const float p_fall_acceleration)
{
    fall_acceleration = p_fall_acceleration;
}

float AroraPlayerStats::get_fall_acceleration() const
{
    return fall_acceleration;
}

arora_settings.hpp

#pragma once

#include <godot_cpp/classes/resource.hpp>
namespace godot
{
    // TODO: swap to configfile? https://docs.godotengine.org/en/stable/classes/class_configfile.html
    class AroraSettings : public Resource
    {
        GDCLASS(AroraSettings, Resource)
    private:
        float mouse_sensitivity, max_camera_length, min_camera_length, zoom_speed;
    protected:
        static void _bind_methods();
    public:
        AroraSettings();
        ~AroraSettings();

        void set_mouse_sensitivity(const float p_mouse_sensitivity);
        float get_mouse_sensitivity() const;
        void set_max_camera_length(const float p_max_camera_length);
        float get_max_camera_length() const;
        void set_min_camera_length(const float p_min_camera_length);
        float get_min_camera_length() const;
        void set_zoom_speed(const float p_zoom_speed);
        float get_zoom_speed() const;
    };
}

arora_settings.cpp

#include "arora_settings.hpp"

using namespace godot;

void AroraSettings::_bind_methods()
{
    ClassDB::bind_method(D_METHOD("get_mouse_sensitivity"), &AroraSettings::get_mouse_sensitivity);
    ClassDB::bind_method(D_METHOD("set_mouse_sensitivity", "p_mouse_sensitivity"), &AroraSettings::set_mouse_sensitivity);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mouse_sensitivity"), "set_mouse_sensitivity", "get_mouse_sensitivity");
    ClassDB::bind_method(D_METHOD("get_max_camera_length"), &AroraSettings::get_max_camera_length);
    ClassDB::bind_method(D_METHOD("set_max_camera_length", "p_max_camera_length"), &AroraSettings::set_max_camera_length);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_camera_length"), "set_max_camera_length", "get_max_camera_length");
    ClassDB::bind_method(D_METHOD("get_min_camera_length"), &AroraSettings::get_min_camera_length);
    ClassDB::bind_method(D_METHOD("set_min_camera_length", "p_min_camera_length"), &AroraSettings::set_min_camera_length);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "min_camera_length"), "set_min_camera_length", "get_min_camera_length");
    ClassDB::bind_method(D_METHOD("get_zoom_speed"), &AroraSettings::get_zoom_speed);
    ClassDB::bind_method(D_METHOD("set_zoom_speed", "p_zoom_speed"), &AroraSettings::set_zoom_speed);
    ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom_speed"), "set_zoom_speed", "get_zoom_speed");
}

AroraSettings::AroraSettings()
{
}

AroraSettings::~AroraSettings()
{
}

void AroraSettings::set_mouse_sensitivity(const float p_mouse_sensitivity)
{
    mouse_sensitivity = p_mouse_sensitivity;
}

float AroraSettings::get_mouse_sensitivity() const
{
    return mouse_sensitivity;
}

void godot::AroraSettings::set_max_camera_length(const float p_max_camera_length)
{
    max_camera_length = p_max_camera_length;
}

float godot::AroraSettings::get_max_camera_length() const
{
    return max_camera_length;
}

void godot::AroraSettings::set_min_camera_length(const float p_min_camera_length)
{
    min_camera_length = p_min_camera_length;
}

float godot::AroraSettings::get_min_camera_length() const
{
    return min_camera_length;
}

void AroraSettings::set_zoom_speed(const float p_zoom_speed)
{
    zoom_speed = p_zoom_speed;
}

float AroraSettings::get_zoom_speed() const
{
    return zoom_speed;
}

Step 3 - Register Types

In GD Extension we essentially boil down to a register_types file, this file will handle saying what classes you have created for runtime/internal/etc and just in general what to call as the initializer.
In my case I have called my library arora_library but feel free to just replace with whatever name you want like mycoollib_library

src/register_types.cpp

#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>
#include "register_types.h"
#include "core/arora_player.hpp"
#include "core/arora_multiplayer.hpp"
#include "resources/arora_player_stats.hpp"
#include "resources/arora_settings.hpp"

using namespace godot;

void initialize_arora_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
    GDREGISTER_CLASS(AroraPlayer);
    GDREGISTER_CLASS(AroraPlayerStats);
    GDREGISTER_CLASS(AroraSettings);
    GDREGISTER_RUNTIME_CLASS(AroraMultiplayer);
}

void uninitialize_arora_module(ModuleInitializationLevel p_level) {
    if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
        return;
    }
}

extern "C" {
// Initialization.
GDExtensionBool GDE_EXPORT arora_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) {
    godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);

    init_obj.register_initializer(initialize_arora_module);
    init_obj.register_terminator(uninitialize_arora_module);
    init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE);

    return init_obj.init();
}
}

src/register_types.h

#ifndef GDEXAMPLE_REGISTER_TYPES_H
#define GDEXAMPLE_REGISTER_TYPES_H

#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void initialize_arora_module(ModuleInitializationLevel p_level);
void uninitialize_arora_module(ModuleInitializationLevel p_level);

#endif // GDEXAMPLE_REGISTER_TYPES_H

Step 4 - Setting up Godot

Next build the godot folder so you have a editor to use:

cd godot
scons dev_build=true target=editor debug_symbols=yes

Let this run for a bit and finish, if you fail because of any reason, you’re likely missing some dependency, Building from source — Godot Engine (latest) documentation in English has a good explanation of what’s needed.

Now open the built godot .\godot\bin\godot.windows.editor.dev.x86_64.exe was what i need but for you it might be called something else, create a new godot project in the demo folder and save, close godot.

Create a new file in demo/bin called mylibname.gdextension in my case it was libarora.gdextension

[configuration]

entry_symbol = "arora_library_init"
compatibility_minimum = "4.1"
reloadable = true

[libraries]

macos.debug = "res://bin/libarora.macos.template_debug.dev.framework"
macos.release = "res://bin/libarora.macos.template_release.framework"
ios.debug = "res://bin/libarora.ios.template_debug.dev.xcframework"
ios.release = "res://bin/libarora.ios.template_release.xcframework"
windows.debug.x86_32 = "res://bin/libarora.windows.template_debug.dev.x86_32.dll"
windows.release.x86_32 = "res://bin/libarora.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "res://bin/libarora.windows.template_debug.dev.x86_64.dll"
windows.release.x86_64 = "res://bin/libarora.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libarora.linux.template_debug.dev.x86_64.so"
linux.release.x86_64 = "res://bin/libarora.linux.template_release.x86_64.so"
linux.debug.arm64 = "res://bin/libarora.linux.template_debug.dev.arm64.so"
linux.release.arm64 = "res://bin/libarora.linux.template_release.arm64.so"
linux.debug.rv64 = "res://bin/libarora.linux.template_debug.dev.rv64.so"
linux.release.rv64 = "res://bin/libarora.linux.template_release.rv64.so"
android.debug.x86_64 = "res://bin/libarora.android.template_debug.dev.x86_64.so"
android.release.x86_64 = "res://bin/libarora.android.template_release.x86_64.so"
android.debug.arm64 = "res://bin/libarora.android.template_debug.dev.arm64.so"
android.release.arm64 = "res://bin/libarora.android.template_release.arm64.so"

[dependencies]
ios.debug = {
    "res://bin/libgodot-cpp.ios.template_debug.dev.xcframework": ""
}
ios.release = {
    "res://bin/libgodot-cpp.ios.template_release.xcframework": ""
}

You can modify the values once you see the compiled results (they will be dumped in the bin folder, and you can modify to what you need).

Step 5 - Setup Compilation

In .vscode create the following files:

.vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Godot",
            "request": "launch",
            "type": "cppvsdbg",
            "args": [
                "--editor",
                "--path",
                "demo"
            ],
            "cwd": "${workspaceFolder}",
            "console": "internalConsole",
            "program": "${workspaceFolder}/godot/bin/godot.windows.editor.dev.x86_64.exe",
            "visualizerFile": "${workspaceFolder}/godot/platform/windows/godot.natvis"
        }
    ]
}

.vscode/tasks.json

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",
            "command": "scons",
            "args": [
                // enable for debugging with breakpoints
                "dev_build=yes"
              ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "problemMatcher": "$msCompile"
        }
    ]
}

in the root of the project create a file called SConstruct
This is where the lib output for bin folder is defined (replace libarora with what you want it called)

#!/usr/bin/env python
import os
import sys
import warnings

env = SConscript("godot-cpp/SConstruct")
if "dev_build" in ARGUMENTS and ARGUMENTS["dev_build"] == "yes" or "dev_build" in ARGUMENTS and ARGUMENTS["dev_build"] == True or "include_server_content" in ARGUMENTS:
    #warnings.warn("include_server_content is specified, the output will have server code")
    env.Append(CPPDEFINES=["INCLUDE_SERVER_CONTENT"])

# For reference:
# - CCFLAGS are compilation flags shared between C and C++
# - CFLAGS are for C-specific compilation flags
# - CXXFLAGS are for C++-specific compilation flags
# - CPPFLAGS are for pre-processor flags
# - CPPDEFINES are for pre-processor defines
# - LINKFLAGS are for linking flags

# tweak this if you want to use different folders, or more folders, to store your source code in.
env.Append(CPPPATH=["src/"])
sources = Glob("src/**/*.cpp") + Glob("src/*.cpp")

if env["platform"] == "macos":
    library = env.SharedLibrary(
        "demo/bin/libarora.{}.{}.framework/libarora.{}.{}".format(
            env["platform"], env["target"], env["platform"], env["target"]
        ),
        source=sources,
    )
elif env["platform"] == "ios":
    if env["ios_simulator"]:
        library = env.StaticLibrary(
            "demo/bin/libarora.{}.{}.simulator.a".format(env["platform"], env["target"]),
            source=sources,
        )
    else:
        library = env.StaticLibrary(
            "demo/bin/libarora.{}.{}.a".format(env["platform"], env["target"]),
            source=sources,
        )
else:
    library = env.SharedLibrary(
        "demo/bin/libarora{}{}".format(env["suffix"], env["SHLIBSUFFIX"]),
        source=sources,
    )

Default(library)

Now when you hit Control Shift B it should compile and output relevant bin to bin folder
And hit Control F5 it will start Godot in your project, if you use just F5 (the debugger) you can place breakpoints anywhere and it will work engine code or your code, try it out.

Follow Up

If you got to this point and it builds/runs successfully, you can probably do a final read over GDExtension C++ example — Godot Engine (4.4) documentation in English and understand what each file here is doing.
With this setup you can easily debug and add as you go, goodluck.