#include "tunnels_player.hpp" #include "character_actor.hpp" #include "character_data.hpp" #include "goal_marker.hpp" #include "godot_cpp/variant/callable_method_pointer.hpp" #include "godot_cpp/variant/utility_functions.hpp" #include "planner.hpp" #include "tunnels_game_mode.hpp" #include "tunnels_game_state.hpp" #include "utils/game_root.hpp" #include "utils/godot_macros.h" #include "utils/player_input.hpp" #include #include #include #include #include #include #include #include #include #include namespace godot { void TunnelsPlayer::_bind_methods() { #define CLASSNAME TunnelsPlayer GDPROPERTY_HINTED(camera_rotation_ramp, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); } void TunnelsPlayer::_enter_tree() { GDGAMEONLY(); this->initialize_character(); this->camera_rotation_ramp->bake(); this->reticle = this->get_node("Reticle"); } void TunnelsPlayer::_ready() { this->camera = this->get_viewport()->get_camera_3d(); } void TunnelsPlayer::_exit_tree() { GDGAMEONLY(); GameRoot::get_singleton()->remove_player(this->get_player_id()); } void TunnelsPlayer::_process(double delta_time) { GDGAMEONLY(); this->process_mouse_location(delta_time); // get the current screen location of the cursor // convert screen location to world location on the same y plane as the player character Vector3 const mouse_world_location = this->get_mouse_world_position(Vector3{0.f, 1.f, 0.f}, this->character->get_global_position().y + 1.f); // move the reticle this->reticle->set_global_position(mouse_world_location); // rotate the camera this->process_camera_rotation(delta_time); switch(this->state) { default: case State::ManualControl: // send the current wasd input to the character this->character->move(this->get_world_move_input().normalized()); // send the current world cursor position the character this->character->aim(mouse_world_location); // move the camera along with the character this->set_global_position(this->character->get_global_position()); break; case State::Tactics: // move camera along with the input this->set_global_position(this->get_global_position() + this->get_world_move_input().normalized() * delta_time * TunnelsPlayer::TACTICS_MOVEMENT_SPEED); break; case State::Overview: break; } } void TunnelsPlayer::process_mouse_location(double delta_time) { Viewport *view = this->get_viewport(); Vector2 const pixel_location = view->get_mouse_position(); // convert cursor's global pixel position to normalized screen coordinates this->mouse_location = pixel_location / view->get_visible_rect().get_size(); // get the direction the mouse is pointing in the world this->mouse_world_ray_normal = this->camera->project_ray_normal(pixel_location); } void TunnelsPlayer::process_camera_rotation(double delta_time) { Vector3 rotation = this->get_global_rotation(); // the influence of the mouse's y position on the rotation speed float const y_multiplier = std::max(TunnelsPlayer::ROTATION_Y_MIN_INFLUENCE, this->mouse_location.y); float const margin = TunnelsPlayer::ROTATION_MARGIN * (this->state == State::Tactics ? TunnelsPlayer::ROTATION_MARGIN_TACTICS_MUL : 1.f); // rotate the camera when the mouse is close to the edge of the screen if(this->mouse_location.x < margin) { // normalized measurement of how far into the rotation margin the mouse is float const normalized{1.f - (this->mouse_location.x / margin)}; // rotate based on delta time and use a curve to make the rotation zone feel more natural rotation.y += delta_time * double(TunnelsPlayer::ROTATION_SPEED * camera_rotation_ramp->sample(normalized) * y_multiplier); } if(this->mouse_location.x > 1.f - margin) { float const normalized{((this->mouse_location.x - (1.f - margin)) / margin)}; rotation.y -= delta_time * double(TunnelsPlayer::ROTATION_SPEED * camera_rotation_ramp->sample(normalized) * y_multiplier); } // wrap rotation to avoid going into invalid numbers while(rotation.y > 6.283185) rotation.y -= 6.283185; while(rotation.y < 0.f) rotation.y += 6.283185; // apply new rotation this->set_global_rotation(rotation); } void TunnelsPlayer::setup_player_input(PlayerInput *input) { input->listen_to(PlayerInput::Listener("move_left", "move_right", callable_mp(this, &TunnelsPlayer::horizontal_move_input))); input->listen_to(PlayerInput::Listener("move_forward", "move_backward", callable_mp(this, &TunnelsPlayer::vertical_move_input))); input->listen_to(PlayerInput::Listener("fire", callable_mp(this, &TunnelsPlayer::fire_pressed))); input->listen_to(PlayerInput::Listener("tactics_mode", callable_mp(this, &TunnelsPlayer::mode_switch_input))); } Node *TunnelsPlayer::to_node() { return Object::cast_to(this); } void TunnelsPlayer::spawn_at_position(Transform3D const &at) { this->character->set_global_transform(at); this->set_global_basis(at.get_basis()); } void TunnelsPlayer::horizontal_move_input(Ref event, float value) { this->move_input.x = value; } void TunnelsPlayer::vertical_move_input(Ref event, float value) { this->move_input.y = value; } void TunnelsPlayer::mode_switch_input(Ref event, float value) { if(value != 0.f) this->state = this->state == State::Tactics ? State::ManualControl : State::Tactics; } void TunnelsPlayer::fire_pressed(Ref event, float value) { switch(this->state) { case State::ManualControl: this->character->set_firing(value != 0); break; case State::Tactics: if(value == 1.f) this->try_select_marker(); break; case State::Overview: break; } } void TunnelsPlayer::try_select_marker() { UtilityFunctions::print("TunnelsPlayer::try_select_marker()"); Transform3D const &camera_trans{this->camera->get_global_transform()}; // prepare raycast query Ref params{PhysicsRayQueryParameters3D::create(camera_trans.origin, camera_trans.origin + this->mouse_world_ray_normal * 1000.f)}; params->set_collision_mask(1u << 3u); params->set_collide_with_areas(true); // fetch current physics state and cast ray PhysicsDirectSpaceState3D *state = this->get_world_3d()->get_direct_space_state(); Dictionary dict{state->intersect_ray(params)}; // fail if nothing was hit if(dict.is_empty()) return; // attempt to cast hit node to a marker GoalMarker *marker{Object::cast_to(dict["collider"])}; // fail if hit object is not a marker if(marker == nullptr) return; UtilityFunctions::print("Hit: ", marker->get_path()); CharacterActor *target_character{nullptr}; for(CharacterActor *loop_character : Ref(GameRoot::get_singleton()->get_game_mode())->get_player_characters()) { if(loop_character != this->character) { target_character = loop_character; break; } } // no non-player ally was found if(target_character == nullptr) return; // cache planner component goap::Planner *planner{target_character->get_planner()}; // cache previous target in case planning fails Node *previous_target{target_character->get_target()}; // attempt to find a plan to marker's goal target_character->set_target(marker); if(planner->can_do(marker->get_goal())) { planner->add_goal(marker->get_goal()); planner->make_plan(); target_character->force_update_action(); UtilityFunctions::print("Made plan for character ", target_character->get_path()); } else { // reset character to the state it was in before attempts to change goal UtilityFunctions::push_warning("Failed to make plan for ", marker->get_goal()->get_path()); target_character->set_target(previous_target); } } void TunnelsPlayer::initialize_character() { Ref player_scene = ResourceLoader::get_singleton()->load("res://player_character.tscn"); // check if the player scene is a valid player character if(player_scene.is_null() || !player_scene.is_valid()) return; if(player_scene->get_state()->get_node_type(0) != StringName("CharacterActor")) return; UtilityFunctions::print("initialize_character pos: ", this->get_global_position()); // instantiate and store the player character this->character = Object::cast_to(player_scene->instantiate()); this->get_parent()->add_child(this->character); this->character->set_global_transform(this->get_global_transform()); Ref game_state = GameRoot::get_singleton()->get_game_state(); Ref character = game_state->get_characters()[0]; this->character->set_character_data(game_state->get_characters()[0]); // disable navmesh navigation and start using player input this->character->set_manual_mode(true); Ref(GameRoot::get_singleton()->get_game_mode())->set_manual_character(this->character); } Vector3 TunnelsPlayer::get_world_move_input() const { Basis const basis = this->get_global_transform().get_basis(); // get the forward and left vectors, ensuring that they won't produce {0,0,0} when flattened Vector3 x = basis.get_column(0); if(x.x == 0.f && x.z == 0.f) x = basis.get_column(1); Vector3 z = basis.get_column(2); if(z.x == 0.f && z.z == 0.f) z = basis.get_column(1); // convert input vector to world normal by way of flattened forward and left units return this->move_input.x * Vector3{x.x, 0.f, x.z}.normalized() + this->move_input.y * Vector3{z.x, 0.f, z.z}.normalized(); } Vector3 TunnelsPlayer::get_mouse_world_position(Vector3 axis, float depth) const { // cache camera location Vector3 const cam_origin = this->camera->get_global_position(); // get the ray and origin depths along the axis float const cam_depth = axis.dot(cam_origin); float const ray_step = axis.dot(this->mouse_world_ray_normal); // calculate the number of "steps" the ray needs to take to get the target depth from the origin along the axis float const distance = depth - cam_depth; float const steps = distance / ray_step; return cam_origin + this->mouse_world_ray_normal * steps; } void TunnelsPlayer::set_camera_rotation_ramp(Ref curve) { this->camera_rotation_ramp = curve; } Ref TunnelsPlayer::get_camera_rotation_ramp() const { return this->camera_rotation_ramp; } CharacterActor *TunnelsPlayer::get_character() const { return this->character; } float const TunnelsPlayer::ROTATION_SPEED{0.5f}; float const TunnelsPlayer::ROTATION_Y_MIN_INFLUENCE{7.f}; float const TunnelsPlayer::ROTATION_MARGIN{0.4f}; float const TunnelsPlayer::ROTATION_MARGIN_TACTICS_MUL{0.6f}; float const TunnelsPlayer::TACTICS_MOVEMENT_SPEED{10.f}; }