diff --git a/game_mode.cpp b/game_mode.cpp deleted file mode 100644 index 01f2b1d..0000000 --- a/game_mode.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "game_mode.hpp" -#include -#include -#include -#include "utils/godot_macros.hpp" - -namespace utils { -void GameMode::_bind_methods() { -#define CLASSNAME GameMode - GDPROPERTY_HINTED(player_scene, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); -} - -void GameMode::set_player_scene(gd::Ref scene) { - this->player_scene = scene; -} - -gd::Ref GameMode::get_player_scene() const { - return this->player_scene; -} -} diff --git a/game_mode.hpp b/game_mode.hpp deleted file mode 100644 index 33e87cd..0000000 --- a/game_mode.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef UTILS_GAME_MODE_HPP -#define UTILS_GAME_MODE_HPP - -#include -#include - -namespace gd = godot; - -namespace utils { -/*! Stores session-relevant data. - * - * Inheriting classes are intended to keep only data that is relevant for the duration of the current session/match. Use GameState instead if you want data to be saved between sessions. - * Will be destroyed when a level is loaded that does not match the same game mode class. - * The current active game mode can be gotten from the GameRoot3D singleton instance. - */ -class GameMode : public gd::Node { - GDCLASS(GameMode, gd::Node); - static void _bind_methods(); -public: - void set_player_scene(gd::Ref scene); - gd::Ref get_player_scene() const; -private: - gd::Ref player_scene{}; //!< The scene to instantiate when spawning a player. -}; -} - -#endif // !UTILS_GAME_MODE_HPP diff --git a/game_root.cpp b/game_root.cpp deleted file mode 100644 index 2b9a753..0000000 --- a/game_root.cpp +++ /dev/null @@ -1,323 +0,0 @@ -#include "game_root.hpp" -#include "game_mode.hpp" -#include "godot_macros.hpp" -#include "level.hpp" -#include "player.hpp" -#include "player_input.hpp" -#include "player.hpp" -#include "spawn_point.hpp" -#include "utils/game_state.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace utils { -void GameRoot3D::_bind_methods() { -#define CLASSNAME GameRoot3D - GDFUNCTION(reset_game_mode); - GDPROPERTY_HINTED(first_boot_level, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); - GDPROPERTY_HINTED(game_state_prototype, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "GameState"); - GDSIGNAL("player_connected", gd::PropertyInfo(gd::Variant::OBJECT, "player_input", gd::PROPERTY_HINT_NODE_TYPE, "PlayerInput")); - GDSIGNAL("player_disconnected", gd::PropertyInfo(gd::Variant::OBJECT, "player_input", gd::PROPERTY_HINT_NODE_TYPE, "PlayerInput")); - GDSIGNAL("player_spawned", gd::PropertyInfo(gd::Variant::OBJECT, "player_info", gd::PROPERTY_HINT_NODE_TYPE, "Node")); -} - -GameRoot3D *GameRoot3D::get_singleton() { - return GameRoot3D::singleton_instance; -} - -bool GameRoot3D::has_singleton() { - return GameRoot3D::singleton_instance != nullptr; -} - -void GameRoot3D::_enter_tree() { GDGAMEONLY(); - // TODO: Replace this with detecting input devices - this->player_input_connected(); - this->grab_singleton(); -} - -void GameRoot3D::_ready() { GDGAMEONLY(); - this->load_level(this->first_boot_level); - // TODO: try load save data from file. - this->game_state = this->game_state_prototype->duplicate(true); -} - -void GameRoot3D::_exit_tree() { GDGAMEONLY(); - this->release_singleton(); -} - -void GameRoot3D::player_input_connected() { - PlayerInput *input = memnew(PlayerInput); - this->add_child(input); - this->players.insert(this->next_player_id++, {input, nullptr}); - this->emit_signal(gd::StringName("player_connected"), input); -} - -void GameRoot3D::remove_player(uint32_t player_id) { - if(!this->players.has(player_id)) - return; - // convert player object to node - gd::Node *node = this->players.get(player_id).second->to_node(); - if(node == nullptr) { - gd::UtilityFunctions::push_error("IPlayer::to_node failed for player with id '", player_id, "'"); - return; - } - node->queue_free(); - this->players.get(player_id).second = nullptr; -} - -void GameRoot3D::remove_all_players() { - // free all player instances in use - for(gd::KeyValue> &pair : this->players) { - // skip unused player slots - if(pair.value.second == nullptr) - continue; - else - this->remove_player(pair.key); - } -} - -bool GameRoot3D::initialize_player(IPlayer *player, uint32_t id) { - if(!this->players.has(id)) - return false; - // register the player - gd::Pair &found{this->players.get(id)}; - found.second = player; - // set player id - player->player_id = id; - this->emit_signal("player_spawned", player->to_node()); - this->add_child(player->to_node()); - player->setup_player_input(found.first); - return true; -} - -void GameRoot3D::reset_game_mode() { - this->set_game_mode(nullptr); -} - -Level3D *GameRoot3D::load_level(gd::Ref level) { - return this->load_level_at(level, gd::Transform3D()); -} - -Level3D *GameRoot3D::load_level_at(gd::Ref level, gd::Transform3D at) { - if(!GameRoot3D::is_valid_level(level)) { - return nullptr; - } - Level3D *instance = Object::cast_to(level->instantiate()); - if(instance == nullptr) { - gd::UtilityFunctions::push_error("Unexpected failure to instantiate level scene '", level->get_path(), "'."); - return nullptr; - } - this->levels.insert(level->get_path(), instance); - instance->connect("tree_exited", callable_mp(this, &GameRoot3D::level_unloaded).bind(level->get_path())); - // store and add to tree at desired transform - // if this is the first level containing a game mode currently active use it's gamemode as a prototype - gd::Ref game_mode_prototype{instance->get_game_mode_prototype()}; - bool const switch_game_mode{!this->game_mode || this->game_mode->get_scene_file_path() != game_mode_prototype->get_path()}; - if(switch_game_mode) { - this->set_game_mode(instance->get_game_mode_prototype()); - } - this->add_child(instance); - instance->set_global_transform(at); - // set initial player positions if new player were spawned due to game mode switch - if(switch_game_mode && this->game_mode != nullptr) { - for(gd::KeyValue> const &kvp : this->players) { - this->place_player_at_spawnpoint(kvp.value.second); - } - } - return instance; -} - -void GameRoot3D::unload_all_levels() { - gd::HashMap levels = this->get_levels(); - for(gd::KeyValue &kvp : levels) - kvp.value->call_deferred("queue_free"); - this->get_levels().clear(); - this->reset_game_mode(); -} - -void GameRoot3D::replace_levels(gd::Ref scene) { - this->unload_all_levels(); - this->load_level(scene); -} - -void GameRoot3D::register_spawn_point(SpawnPoint3D *spawn_point) { - if(this->spawn_points.has(spawn_point)) { - gd::UtilityFunctions::push_error("Duplicate attempt to register spawnpoint '", spawn_point->get_path(), "'"); - return; - } - if(!this->spawn_points.has(spawn_point)) - this->spawn_points.push_back(spawn_point); -} - -void GameRoot3D::unregister_spawn_point(SpawnPoint3D *spawn_point) { - if(!this->spawn_points.has(spawn_point)) { - gd::UtilityFunctions::push_error("Attempt to unregister spawnpoint '", spawn_point->get_path(), "', which is not registered."); - return; - } - this->spawn_points.erase(spawn_point); -} - -void GameRoot3D::place_player_at_spawnpoint(IPlayer *player) { - if(this->spawn_points.is_empty()) return; - SpawnPoint3D *spawn_point = this->spawn_points[rng->randi() % this->spawn_points.size()]; - player->spawn_at_position(spawn_point->get_global_transform()); -} - -void GameRoot3D::player_despawned(uint32_t id) { - gd::Pair &pair = this->players.get(id); - pair.second = nullptr; - pair.first->clear_listeners(); -} - -void GameRoot3D::set_game_mode(gd::Ref prototype) { - this->remove_all_players(); - if(this->game_mode != nullptr) - this->game_mode->queue_free(); - if(prototype.is_null() || !prototype.is_valid()) - return; // allow "unsetting" the gamemode by passing an invalid gamemode - // Detect passing of valid scene that is an invalid game mode - if(!gd::ClassDB::is_parent_class(prototype->get_state()->get_node_type(0), "GameMode")) { - gd::UtilityFunctions::push_error("Attempted to load non-gamemode scene as gamemode"); - return; - } - // instantiate the game mode as a child - this->game_mode = Object::cast_to(prototype->instantiate()); - this->add_child(game_mode); - // instantiate players - if(this->game_mode->get_player_scene().is_valid()) { - uint32_t new_player_id = this->find_empty_player_slot(); - do { - IPlayer *player = this->spawn_player(new_player_id); - if(player != nullptr) - this->initialize_player(player, new_player_id); - new_player_id = this->find_empty_player_slot(); - } while(new_player_id != 0); - } -} - -GameMode *GameRoot3D::get_game_mode() const { - return this->game_mode; -} - -GameState *GameRoot3D::get_game_state() const { - return this->game_state.ptr(); -} - -gd::HashMap &GameRoot3D::get_levels() { - return this->levels; -} - -IPlayer *GameRoot3D::get_player(uint32_t id) { - return this->players[id].second; -} - -gd::Vector GameRoot3D::get_players() { - gd::Vector players{}; - for(gd::KeyValue> pair : this->players) { - players.push_back(pair.value.second); - } - return players; -} - -void GameRoot3D::set_first_boot_level(gd::Ref level) { - if(level.is_null() || !level.is_valid()) { - this->first_boot_level.unref(); - return; - } - gd::StringName const root_type = level->get_state()->get_node_type(0); - if(!gd::ClassDB::is_parent_class(root_type, "Level3D")) { - gd::UtilityFunctions::push_error("First boot level cannot be of type '", root_type, "'. First boot level has to inherit from Level3D"); - this->first_boot_level.unref(); - return; - } - this->first_boot_level = level; -} - -gd::Ref GameRoot3D::get_first_boot_level() const { - return this->first_boot_level; -} - -void GameRoot3D::set_game_state_prototype(gd::Ref game_state) { - this->game_state_prototype = game_state; -} - -gd::Ref GameRoot3D::get_game_state_prototype() const { - return this->game_state_prototype; -} - -gd::RandomNumberGenerator &GameRoot3D::get_rng() { - return *this->rng.ptr(); -} - -void GameRoot3D::grab_singleton() { - if(GameRoot3D::has_singleton()) { - this->set_process_mode(PROCESS_MODE_DISABLED); - gd::UtilityFunctions::push_error("More than one GameRoot instance active"); - } else { - GameRoot3D::singleton_instance = this; - } -} - -void GameRoot3D::release_singleton() { - if(GameRoot3D::singleton_instance == this) { - GameRoot3D::singleton_instance = nullptr; - } else { - gd::UtilityFunctions::push_error("GameRoot instance attempted to release singleton while it is not the singleton instance"); - } -} - -uint32_t GameRoot3D::find_empty_player_slot() const { - for(gd::KeyValue> const &kvp : this->players) { - if(kvp.value.second == nullptr) { - return kvp.key; - } - } - return 0; -} - -IPlayer *GameRoot3D::spawn_player(uint32_t id) { - if(id == 0) { - gd::UtilityFunctions::push_error("Failed to find any valid player slot when spawning player"); - return nullptr; - } - Node *player_node = this->game_mode->get_player_scene()->instantiate(); - if(player_node == nullptr) { - gd::UtilityFunctions::push_error("Failed to instantiate player scene '", this->game_mode->get_player_scene()->get_path(), "'"); - return nullptr; - } - IPlayer *player = dynamic_cast(player_node); - if(player == nullptr) { - gd::UtilityFunctions::push_error("Player scene does not implement required IPlayer interface"); - player_node->queue_free(); - return nullptr; - } - player_node->connect("tree_exited", callable_mp(this, &GameRoot3D::player_despawned).bind(id)); - return player; -} - -void GameRoot3D::level_unloaded(gd::StringName scene_path) { - this->levels.erase(scene_path); -} - -bool GameRoot3D::is_valid_level(gd::Ref &level) { - if(level.is_null() || !level.is_valid() || !level->can_instantiate()) { - gd::UtilityFunctions::push_error("Can't load level from invalid packed scene"); - return false; - } - gd::StringName const root_type = level->get_state()->get_node_type(0); - if(!gd::ClassDB::is_parent_class(root_type, "Level3D")) { - gd::UtilityFunctions::push_error("Can't load level with root type '", root_type, "'. Root node has to be of type Level3D"); - return false; - } - return true; -} - -GameRoot3D *GameRoot3D::singleton_instance{nullptr}; -} diff --git a/game_root.hpp b/game_root.hpp deleted file mode 100644 index 029342f..0000000 --- a/game_root.hpp +++ /dev/null @@ -1,185 +0,0 @@ -#ifndef UTILS_GAME_ROOT_HPP -#define UTILS_GAME_ROOT_HPP - -#include "game_mode.hpp" -#include "game_state.hpp" -#include "level.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace gd = godot; - -namespace utils { -class PlayerInput; -class IPlayer; -class SpawnPoint3D; - -/*! The root of a game. - * - * A game root node that manages levels and input devices. - * Can be loaded at any point in a game's life, but suggested is setting this as the root of the boot scene. - */ -class GameRoot3D : public gd::Node { - GDCLASS(GameRoot3D, gd::Node); - static void _bind_methods(); -public: - //! get the current active singleton instance of GameRoot - static GameRoot3D *get_singleton(); - //! returns true if there is currently a singleton active for GameRoot - static bool has_singleton(); - - virtual void _enter_tree() override; - virtual void _ready() override; - virtual void _exit_tree() override; - - /*! Instantiate a new PlayerInput. - * - * Does not automatically spawn a new player, but does notify game mode. - */ - void player_input_connected(); - /*! Force-disconnect a player - * - * Calls queue_free on the IPlayer instance - */ - void remove_player(uint32_t player_id); - // calls remove_player for every used player input slot - void remove_all_players(); - /*! Initialize and register a player instance. - * - * The player will be added to the tree and AFTER setup_player_input will be called. - * This way the player can initialize before setting up input - */ - bool initialize_player(IPlayer *player, uint32_t id); - - /*! Un-set game mode. - * Shorthand for `set_game_mode(Ref())` - */ - void reset_game_mode(); - - //! shorthand for load_level(level, Transform3D()) - Level3D *load_level(gd::Ref level); - /*! Load a level, only works if 'level' is a valid scene where the root Node can cast to 'Level3D'. - * - * \param at Sets the root node's global transform. - */ - Level3D *load_level_at(gd::Ref level, gd::Transform3D at); - //! Unload all currently loaded levels. - void unload_all_levels(); - /*! Replace all currently loaded levels with a new level. - * - * Shorthand for - * ``` - * unload_all_levels(); - * load_level(level); - * ``` - */ - void replace_levels(gd::Ref level); - //! Register a spawnpoint for use when spawning players - void register_spawn_point(SpawnPoint3D *spawn_point); - // remove a spawnpoint so it can't be used to spawn players - void unregister_spawn_point(SpawnPoint3D *spawn_point); - void place_player_at_spawnpoint(IPlayer *player); - void player_despawned(uint32_t id); - - /*! Override the current gamemode. - * - * Replaces game mode requires destroying and respawning all players - */ - void set_game_mode(gd::Ref prototype); - //! get the current active game mode. - GameMode *get_game_mode() const; - //! Get the current active game state. - GameState *get_game_state() const; - /*! Returns all currently active levels. - * - * Levels are identified by their packed scene path. - */ - gd::HashMap &get_levels(); - //! Get the player instance associated with id. - IPlayer *get_player(uint32_t id); - //! Get all players in a list. - gd::Vector get_players(); - void set_first_boot_level(gd::Ref level); - gd::Ref get_first_boot_level() const; - void set_game_state_prototype(gd::Ref game_state); - gd::Ref get_game_state_prototype() const; - gd::RandomNumberGenerator &get_rng(); -protected: - //! Attempt to make 'this' the current singleton instance. - void grab_singleton(); - /*! Attempt to stop being the active singleton instance. - * - * Only works if the current singleton is 'this'. - */ - void release_singleton(); - //! Find a Player Input device not yet associated with a player. - uint32_t find_empty_player_slot() const; - //! Spawn a player to be associated with id. - IPlayer *spawn_player(uint32_t id); - //! Callback for a level exiting the tree. - void level_unloaded(gd::StringName scene_path); - //! Check if a scene is a valid level. - static bool is_valid_level(gd::Ref &level); -private: - static GameRoot3D *singleton_instance; - /*! Next available player ID. - * - * Default is 1 because 0 is the "invalid" player id. - */ - uint32_t next_player_id{1}; - /*! All players by id by input device. - * - * `get_players()` - */ - gd::HashMap> players{}; - /*! Global random number generator. - * - * `&get_rng()` - */ - gd::Ref rng{}; - /*! All currently active levels. - * - * Each identified by their resource paths. - * - * `&get_levels()` - */ - gd::HashMap levels{}; - /*! All currently available spawn points. - */ - gd::Vector spawn_points{}; - /*! Current active gamemode. - * - * Replaced when a level is loaded that references a different game mode. - * - * `*get_game_mode()` - */ - GameMode *game_mode{}; - /*! Active game state. - * - * Will be assigned loaded save data, or game_state_prototype if no save data is found. - * - * `*get_game_mode()` - */ - gd::Ref game_state{}; - /*! The level to boot into on startup. - * - * `get_first_boot_level()` `set_first_boot_level(value)` - */ - gd::Ref first_boot_level{}; - /*! The default game state data. - * - * Duplicated and assigned to game_state if no save data is available. - * - * `get_game_state_prototype()` `set_game_state_prototype(value)` - */ - gd::Ref game_state_prototype{}; -}; -} - -#endif // !UTILS_GAME_ROOT_HPP diff --git a/game_state.cpp b/game_state.cpp deleted file mode 100644 index 342381f..0000000 --- a/game_state.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "game_state.hpp" - -using namespace godot; - -namespace utils { -void GameState::_bind_methods() { -#define CLASSNAME GameState -} -} diff --git a/game_state.hpp b/game_state.hpp deleted file mode 100644 index 34b31f4..0000000 --- a/game_state.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef UTILS_GAME_STATE_HPP -#define UTILS_GAME_STATE_HPP - -#include - -namespace utils { -/*! Parent class for saved game state. - * - * Inherit and add godot properties to save persistently. - */ -class GameState : public godot::Resource { - GDCLASS(GameState, godot::Resource); - static void _bind_methods(); -public: -}; -} - -#endif // !UTILS_GAME_STATE_HPP diff --git a/level.cpp b/level.cpp deleted file mode 100644 index d82c69d..0000000 --- a/level.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "level.hpp" -#include -#include "utils/godot_macros.hpp" -#include - -namespace utils { -void Level3D::_bind_methods() { -#define CLASSNAME Level3D - GDPROPERTY_HINTED(game_mode_prototype, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); -} - -void Level3D::set_game_mode_prototype(gd::Ref prototype) { - if(prototype.is_null() || !prototype.is_valid()) - this->game_mode_prototype = gd::Ref(nullptr); - else if(!gd::ClassDB::is_parent_class(prototype->get_state()->get_node_type(0), "GameMode")) - return; - else - this->game_mode_prototype = prototype; -} - -gd::Ref Level3D::get_game_mode_prototype() const { - return this->game_mode_prototype; -} -} diff --git a/level.hpp b/level.hpp deleted file mode 100644 index 1e939dd..0000000 --- a/level.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef UTILS_LEVEL_HPP -#define UTILS_LEVEL_HPP - -#include "game_mode.hpp" -#include - -namespace gd = godot; - -namespace utils { -/*! 3D level root to be used with GameRoot3D. - * - * The configured game mode will become the active GameMode in GameRoot3D if one does not exist yet. - */ -class Level3D : public gd::Node3D { - GDCLASS(Level3D, gd::Node3D); - static void _bind_methods(); -public: - void set_game_mode_prototype(gd::Ref prototype); - gd::Ref get_game_mode_prototype() const; -private: - gd::Ref game_mode_prototype{}; //!< The starting state of the game mode to instantiate if this is the "leading" level. -}; -} - -#endif // !UTILS_LEVEL_HPP diff --git a/player.cpp b/player.cpp deleted file mode 100644 index 5af63ad..0000000 --- a/player.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "player.hpp" - -namespace utils { -uint32_t IPlayer::get_player_id() { - return this->player_id.value_or(0); -} -} - diff --git a/player.hpp b/player.hpp deleted file mode 100644 index 23f4f6f..0000000 --- a/player.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef UTILS_PLAYER_HPP -#define UTILS_PLAYER_HPP - -#include -#include -#include - -namespace gd = godot; - -namespace godot { class Node; } - -namespace utils { -class PlayerInput; - -/*! Interface required for player nodes. - * - * Use multiple inheritance and implement IPlayer to make a regular node usable as a player with GameRoot3D. - */ -class IPlayer { -friend class GameRoot3D; -public: - /*! Called by GameRoot3D when this player is instantiated or assigned a new PlayerInput. - * - * Use PlayerInput::listen_to to register input callbacks. There's no need to keep the input pointer around. As the instance is managed by the GameRoot3D. - */ - virtual void setup_player_input(PlayerInput *input) = 0; - //! Convert IPlayer instance to node. - virtual gd::Node *to_node() = 0; - //! Spawn the player at a given transform, usually the global transform of a SpawnPoint3D. - virtual void spawn_at_position(gd::Transform3D const &at) = 0; - - uint32_t get_player_id(); //!< Returns the player id assigned to this instance. - -private: - std::optional player_id{std::nullopt}; -}; -} - -#endif // !UTILS_PLAYER_HPP diff --git a/register_types.cpp b/register_types.cpp index d867791..26ee14b 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -1,19 +1,9 @@ #include "register_types.hpp" -#include "game_mode.hpp" -#include "game_root.hpp" -#include "game_state.hpp" -#include "level.hpp" #include "player_input.hpp" -#include "spawn_point.hpp" #include namespace utils { void godot_cpp_utils_register_types() { - GDREGISTER_CLASS(utils::GameMode); - GDREGISTER_CLASS(utils::GameRoot3D); - GDREGISTER_CLASS(utils::GameState); - GDREGISTER_CLASS(utils::Level3D); GDREGISTER_CLASS(utils::PlayerInput); - GDREGISTER_CLASS(utils::SpawnPoint3D); } } diff --git a/spawn_point.cpp b/spawn_point.cpp deleted file mode 100644 index e95aad8..0000000 --- a/spawn_point.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "spawn_point.hpp" -#include "utils/game_root.hpp" - -namespace utils { -void SpawnPoint3D::_bind_methods() { -} - -void SpawnPoint3D::_enter_tree() { - GameRoot3D *root = gd::Object::cast_to(GameRoot3D::get_singleton()); - if(root == nullptr) { - return; - } - root->register_spawn_point(this); -} - -void SpawnPoint3D::_exit_tree() { - GameRoot3D *root = Object::cast_to(GameRoot3D::get_singleton()); - if(root == nullptr) { - return; - } - root->unregister_spawn_point(this); -} -} diff --git a/spawn_point.hpp b/spawn_point.hpp deleted file mode 100644 index eeb0b2d..0000000 --- a/spawn_point.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef UTILS_SPAWN_POINT_HPP -#define UTILS_SPAWN_POINT_HPP - -#include - -namespace gd = godot; - -namespace utils { -/*! A location in the game world that the player can spawn at. - * - * Registers and de-registers itself with the GameRoot3D to enable/disable this spawnpoint. - */ -class SpawnPoint3D : public gd::Node3D { - GDCLASS(SpawnPoint3D, gd::Node3D); - static void _bind_methods(); -public: - virtual void _enter_tree() override; - virtual void _exit_tree() override; -}; -} - -#endif // !UTILS_SPAWN_POINT_HPP