diff --git a/game_mode.cpp b/game_mode.cpp new file mode 100644 index 0000000..f13b40b --- /dev/null +++ b/game_mode.cpp @@ -0,0 +1,34 @@ +#include "game_mode.hpp" +#include +#include +#include +#include "utils/godot_macros.h" +#include "game_state.hpp" + +namespace godot { +void GameMode::_bind_methods() { +#define CLASSNAME GameMode + GDPROPERTY_HINTED(game_state, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "GameState"); + GDPROPERTY_HINTED(player_scene, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); +} + +void GameMode::set_player_scene(Ref scene) { + this->player_scene = scene; +} + +Ref GameMode::get_player_scene() const { + return this->player_scene; +} + +void GameMode::set_game_state(Ref state) { + if(state.is_null() || !state.is_valid()) { + this->game_state.unref(); + return; + } + this->game_state = state; +} + +Ref GameMode::get_game_state() { + return this->game_state; +} +} diff --git a/game_mode.hpp b/game_mode.hpp new file mode 100644 index 0000000..d589a37 --- /dev/null +++ b/game_mode.hpp @@ -0,0 +1,23 @@ +#ifndef GAME_MODE_H +#define GAME_MODE_H + +#include +#include +#include "game_state.hpp" + +namespace godot { +class GameMode : public Resource { + GDCLASS(GameMode, Resource); + static void _bind_methods(); +public: + void set_player_scene(Ref scene); + Ref get_player_scene() const; + void set_game_state(Ref state); + Ref get_game_state(); +private: + Ref player_scene{}; + Ref game_state{}; +}; +} + +#endif // !GAME_MODE_H diff --git a/game_root.cpp b/game_root.cpp new file mode 100644 index 0000000..da1952c --- /dev/null +++ b/game_root.cpp @@ -0,0 +1,208 @@ +#include "game_root.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "godot_cpp/templates/pair.hpp" +#include "utils/godot_macros.h" +#include "utils/player_input.hpp" +#include "utils/spawn_point.hpp" +#include "utils/player.hpp" +#include "game_mode.hpp" +#include "level.hpp" + +namespace godot { +void GameRoot::_bind_methods() { +#define CLASSNAME GameRoot + GDFUNCTION(reset_game_mode); + ClassDB::add_signal("GameRoot", MethodInfo("player_connected", PropertyInfo(Variant::OBJECT, "player_input", PROPERTY_HINT_NODE_TYPE, "PlayerInput"))); + ClassDB::add_signal("GameRoot", MethodInfo("player_disconnected", PropertyInfo(Variant::OBJECT, "player_input", PROPERTY_HINT_NODE_TYPE, "PlayerInput"))); +} + +GameRoot *GameRoot::get_singleton() { + return GameRoot::singleton_instance; +} + +bool GameRoot::has_singleton() { + return GameRoot::singleton_instance != nullptr; +} + +void GameRoot::_enter_tree() { GDGAMEONLY(); + // TODO: Replace this with detecting input devices + if(this->players.is_empty()) + this->player_connected(); + this->grab_singleton(); +} + +void GameRoot::_exit_tree() { GDGAMEONLY(); + this->release_singleton(); +} + +void GameRoot::set_game_mode(Ref mode) { + if(mode.is_null() || !mode.is_valid()) { + this->game_mode = Ref(); + return; + } + this->game_mode = mode; +} + +Ref GameRoot::get_game_mode() const { + return this->game_mode; +} + +void GameRoot::player_connected() { + PlayerInput *input = memnew(PlayerInput); + this->add_child(input); + this->players.insert(this->next_player_id++, {input, nullptr}); + this->emit_signal(StringName("player_connected"), input); +} + +bool GameRoot::initialize_player(IPlayer *player) { + KeyValue> *found{nullptr}; + // find an unassigned player input instance + for(KeyValue> &pair : this->players) { + if(pair.value.second == nullptr) { + found = &pair; + break; + } + } + // no player slots available, notify caller + if(!found) + return false; + player->player_id = found->key; + found->value.second = player; + player->setup_player_input(found->value.first); + return true; +} + +void GameRoot::reset_game_mode() { + this->game_mode.unref(); +} + +void GameRoot::grab_singleton() { + if(GameRoot::has_singleton()) { + this->set_process_mode(PROCESS_MODE_DISABLED); + UtilityFunctions::push_error("More than one GameRoot instance active"); + } else { + GameRoot::singleton_instance = this; + } +} + +void GameRoot::release_singleton() { + if(GameRoot::singleton_instance == this) { + GameRoot::singleton_instance = nullptr; + } else { + UtilityFunctions::push_error("GameRoot instance attempted to release singleton while it is not the singleton instance"); + } +} + +GameRoot *GameRoot::singleton_instance{nullptr}; + +#undef CLASSNAME + +void GameRoot3D::_bind_methods() { +#define CLASSNAME GameRoot3D + GDPROPERTY_HINTED(first_boot_level, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); +} + +void GameRoot3D::_ready() { GDGAMEONLY(); + this->load_level(this->first_boot_level); +} + +Level3D *GameRoot3D::load_level(Ref level) { + return this->load_level_at(level, Transform3D()); +} + +Level3D *GameRoot3D::load_level_at(Ref level, Transform3D at) { + if(!GameRoot3D::is_valid_level(level)) { + return nullptr; + } + Level3D *instance = Object::cast_to(level->instantiate()); + if(instance == nullptr) { + UtilityFunctions::push_error("Unexpected failure to instantiate level scene '", level->get_path(), "'."); + return nullptr; + } + // store and add to tree at desired transform + this->add_child(instance); + instance->set_global_transform(at); + this->levels.insert(level->get_path(), instance); + // if this is the first level containing a game mode currently active use it's gamemode as a prototype + if(this->game_mode.is_null()) { + this->change_game_mode(instance->get_game_mode_prototype()); + instance->connect("tree_exited", Callable(this, "reset_game_mode")); + } + return instance; +} + +void GameRoot3D::register_spawn_point(SpawnPoint3D *spawn_point) { + if(this->spawn_points.has(spawn_point)) { + UtilityFunctions::push_error("Duplicate attempt to register spawnpoint '", spawn_point->get_path(), "'"); + return; + } + this->spawn_points.insert(spawn_point); +} + +void GameRoot3D::unregister_spawn_point(SpawnPoint3D *spawn_point) { + if(!this->spawn_points.has(spawn_point)) { + UtilityFunctions::push_error("Attempt to unregister spawnpoint '", spawn_point->get_path(), "', which is not registered."); + return; + } + this->spawn_points.erase(spawn_point); +} + +void GameRoot3D::set_first_boot_level(Ref level) { + if(level.is_null() || !level.is_valid()) { + this->first_boot_level.unref(); + return; + } + StringName const root_type = level->get_state()->get_node_type(0); + if(!ClassDB::is_parent_class("Level3D", root_type)) { + 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; +} + +Ref GameRoot3D::get_first_boot_level() const { + return this->first_boot_level; +} + +bool GameRoot3D::is_valid_level(Ref &level) { + if(level.is_null() || !level.is_valid() || !level->can_instantiate()) { + UtilityFunctions::push_error("Can't load level from invalid packed scene"); + return false; + } + StringName const root_type = level->get_state()->get_node_type(0); + if(!ClassDB::is_parent_class("Level3D", root_type)) { + UtilityFunctions::push_error("Can't load level with root type '", root_type, "'. Root node has to be of type Level3D"); + return false; + } + return true; +} + +void GameRoot3D::change_game_mode(Ref prototype) { + // free all player instances in use + for(KeyValue> &pair : this->players) { + if(pair.value.second == nullptr) continue; + Node *node = dynamic_cast(pair.value.second); + if(node == nullptr) { + UtilityFunctions::push_error("Attempt to cast player '", pair.key, "' to node failed"); + } else { + node->queue_free(); + } + } + // create new gamemode instance + this->game_mode = prototype->duplicate(true); + Node *player_node = this->game_mode->get_player_scene()->instantiate(); + IPlayer *player = dynamic_cast(player_node); + if(player != nullptr) { + this->initialize_player(player); + } else { + } +} +} diff --git a/game_root.hpp b/game_root.hpp new file mode 100644 index 0000000..388575d --- /dev/null +++ b/game_root.hpp @@ -0,0 +1,72 @@ +#ifndef GAME_ROOT_HPP +#define GAME_ROOT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include "game_mode.hpp" +#include "level.hpp" + +namespace godot { +class PlayerInput; +class IPlayer; +class SpawnPoint3D; + +class GameRoot : public Node { + GDCLASS(GameRoot, Node); + static void _bind_methods(); +public: + static GameRoot *get_singleton(); + static bool has_singleton(); + + virtual void _enter_tree() override; + virtual void _exit_tree() override; + + void set_game_mode(Ref mode); + Ref get_game_mode() const; + + void player_connected(); + void player_disconnected(); + bool initialize_player(IPlayer *player); + + void reset_game_mode(); +protected: + void grab_singleton(); + void release_singleton(); +protected: + static GameRoot *singleton_instance; + + uint32_t next_player_id{0}; + HashMap> players{}; + Ref game_mode{}; +}; + +class GameRoot3D : public GameRoot { + GDCLASS(GameRoot3D, GameRoot); + static void _bind_methods(); +public: + virtual void _ready() override; + Level3D *load_level(Ref level); + Level3D *load_level_at(Ref level, Transform3D at); + + void register_spawn_point(SpawnPoint3D *spawn_point); + void unregister_spawn_point(SpawnPoint3D *spawn_point); + + void set_first_boot_level(Ref level); + Ref get_first_boot_level() const; +private: + static bool is_valid_level(Ref &level); + void change_game_mode(Ref prototype); +private: + HashMap levels{}; + HashSet spawn_points{}; + + Ref first_boot_level{}; +}; +} + +#endif // !GAME_ROOT_HPP diff --git a/game_state.cpp b/game_state.cpp new file mode 100644 index 0000000..1976867 --- /dev/null +++ b/game_state.cpp @@ -0,0 +1,7 @@ +#include "game_state.hpp" + +namespace godot { +void GameState::_bind_methods() { +#define CLASSNAME GameState +} +} diff --git a/game_state.hpp b/game_state.hpp new file mode 100644 index 0000000..e3b5615 --- /dev/null +++ b/game_state.hpp @@ -0,0 +1,13 @@ +#ifndef GAME_STATE_HPP +#define GAME_STATE_HPP + +#include "godot_cpp/classes/resource.hpp" +namespace godot { +class GameState : public Resource { + GDCLASS(GameState, Resource); + static void _bind_methods(); +public: +}; +} + +#endif // !GAME_STATE_HPP diff --git a/level.cpp b/level.cpp new file mode 100644 index 0000000..7b0e148 --- /dev/null +++ b/level.cpp @@ -0,0 +1,33 @@ +#include "level.hpp" +#include "utils/godot_macros.h" + +namespace godot { +void Level3D::_bind_methods() { +#define CLASSNAME Level3D + GDPROPERTY_HINTED(game_mode_prototype, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "GameMode"); +} + +void Level3D::set_game_mode_prototype(Ref prototype) { + this->game_mode_prototype = prototype; +} + +Ref Level3D::get_game_mode_prototype() const { + return this->game_mode_prototype; +} + +#undef CLASSNAME // Level3D + +void Level2D::_bind_methods() { +#define CLASSNAME Level3D + GDPROPERTY_HINTED(game_mode_prototype, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "GameMode"); +} + +void Level2D::set_game_mode_prototype(Ref prototype) { + this->game_mode_prototype = prototype; +} + +Ref Level2D::get_game_mode_prototype() const { + return this->game_mode_prototype; +} + +} diff --git a/level.hpp b/level.hpp new file mode 100644 index 0000000..0319a3c --- /dev/null +++ b/level.hpp @@ -0,0 +1,32 @@ +#ifndef LEVEL_HPP +#define LEVEL_HPP + +#include +#include +#include "game_mode.hpp" + +namespace godot { +class Level3D : public Node3D { + GDCLASS(Level3D, Node3D); + static void _bind_methods(); +public: + void set_game_mode_prototype(Ref prototype); + Ref get_game_mode_prototype() const; +private: + Ref game_mode_prototype{}; +}; + +class Level2D : public Node2D { + GDCLASS(Level2D, Node2D); + static void _bind_methods(); +public: + void set_game_mode_prototype(Ref prototype); + Ref get_game_mode_prototype() const; +private: + Ref game_mode_prototype{}; +}; + + +} + +#endif // !LEVEL_HPP diff --git a/player.hpp b/player.hpp new file mode 100644 index 0000000..b66897d --- /dev/null +++ b/player.hpp @@ -0,0 +1,24 @@ +#ifndef UTILS_PLAYER_HPP +#define UTILS_PLAYER_HPP + +#include +#include +namespace godot { +class PlayerInput; +class Node; + +class IPlayer { +friend class GameRoot; +public: + virtual void setup_player_input(PlayerInput *input) = 0; + virtual Node *to_node() = 0; + + uint32_t get_player_id(); + +private: + std::optional player_id{std::nullopt}; +}; +} + +#endif // !UTILS_PLAYER_HPP + diff --git a/spawn_point.cpp b/spawn_point.cpp new file mode 100644 index 0000000..22badca --- /dev/null +++ b/spawn_point.cpp @@ -0,0 +1,23 @@ +#include "spawn_point.hpp" +#include "utils/game_root.hpp" + +namespace godot { +void SpawnPoint3D::_bind_methods() { +} + +void SpawnPoint3D::_enter_tree() { + GameRoot3D *root = 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 new file mode 100644 index 0000000..e0649ce --- /dev/null +++ b/spawn_point.hpp @@ -0,0 +1,15 @@ +#ifndef UTILS_SPAWN_POINT_HPP +#define UTILS_SPAWN_POINT_HPP + +#include "godot_cpp/classes/node3d.hpp" +namespace godot { +class SpawnPoint3D : public Node3D { + GDCLASS(SpawnPoint3D, Node3D); + static void _bind_methods(); +public: + virtual void _enter_tree() override; + virtual void _exit_tree() override; +}; +} + +#endif // !UTILS_SPAWN_POINT_HPP