#include "enemy.hpp" #include "utils/godot_macros.hpp" #include #include #include #include #include void Enemy::_bind_methods() { #define CLASSNAME Enemy GDPROPERTY(update_interval, gd::Variant::FLOAT); } void Enemy::_ready() { this->anim_tree = this->get_node("CharacterModel/AnimationTree"); // character animation tree from model (model shared with player) this->agent = this->get_node("%NavigationAgent3D"); // navigation agent // set up the timer used to reduce process time gd::Timer *timer{memnew(gd::Timer)}; this->add_child(timer); timer->start(this->update_interval + this->update_interval * gd::UtilityFunctions::randf_range(0.0f, 0.25f)); timer->connect("timeout", callable_mp(this, &Enemy::update)); // starting target rotation is just the current rotation this->target_rotation = this->get_rotation().y; // a sound the enemy makes while alive to induce tension and inform the player of an enemy nearby this->drone_sound = this->get_node("%DroneSound"); // debugging label (only enable if available) if(this->has_node("%DebugLabel")) this->debug_label = this->get_node("%DebugLabel"); // setup navigation obstacle avoidance this->agent->connect("velocity_computed", callable_mp(this, &Enemy::_on_velocity_calculated)); // fetch player and setup current action this->player = Player::get_player_instance(); this->current_action_fn = (ActionFn)&Enemy::wait_line_of_sight; } void Enemy::_process(double delta) { // update debuging label this->set_current_state_name(this->current_state_name); // rotate towards target (defined by either aiming or navigation) float const angle_left{gd::Math::wrapf(this->target_rotation - this->get_rotation().y, -Math_PI, Math_PI)}; float const step(gd::Math::sign(angle_left) * delta * (this->anim_tree->get_current_state().begins_with("Run") ? this->TURN_SPEED : this->AIM_SPEED)); if(gd::Math::abs(angle_left) <= gd::Math::abs(step)) { this->rotate_y(angle_left); this->at_target_angle = true; } else { this->rotate_y(step); this->at_target_angle = false; } // keep track of the player's last known transform for chasing if(this->can_see_player) { this->last_known_player_position = this->player->get_global_position(); this->last_known_player_rotation = -this->player->get_global_rotation().y; this->shots_fired = 0; } } void Enemy::_on_velocity_calculated(gd::Vector3 velocity) { if(this->current_action_fn == (ActionFn)&Enemy::chase_player && !this->agent->is_navigation_finished()) this->target_rotation = gd::Vector3{0.f, 0.f, 1.f}.signed_angle_to(velocity, {0.f, 1.f, 0.f}); } void Enemy::update() { if(this->current_action_fn != nullptr) this->current_action_fn = (ActionFn)(this->*current_action_fn)(); } Enemy::ActionFn Enemy::wait_line_of_sight() { this->set_current_state_name("Guard"); this->anim_tree->set_aim_weapon(false); if(this->can_see_player && this->get_global_position().distance_squared_to(this->player->get_global_position()) < this->STAB_RANGE * this->STAB_RANGE) return (ActionFn)&Enemy::stab; else if(this->can_see_player) return (ActionFn)&Enemy::take_aim; else return (ActionFn)&Enemy::wait_line_of_sight; } Enemy::ActionFn Enemy::take_aim() { this->set_current_state_name("Aim"); this->target_rotation = gd::Vector3{0.f, 0.f, 1.f}.signed_angle_to(this->last_known_player_position - this->aim_offset_position(), {0.f, 1.f, 0.f}); this->anim_tree->set_aim_weapon(true); if(this->anim_tree->get_current_state().begins_with("Aim") && this->at_target_angle) return (ActionFn)&Enemy::fire; else return(ActionFn)&Enemy::take_aim; } Enemy::ActionFn Enemy::fire() { this->set_current_state_name("Shoot (fire)"); ++this->shots_fired; this->target_rotation = gd::Vector3{0.f, 0.f, 1.f}.signed_angle_to(this->last_known_player_position - this->aim_offset_position(), {0.f, 1.f, 0.f}); this->anim_tree->set_fire_weapon(); return (ActionFn)&Enemy::wait_end_of_shot; } Enemy::ActionFn Enemy::wait_end_of_shot() { this->set_current_state_name("Shoot (wait)"); this->target_rotation = gd::Vector3{0.f, 0.f, 1.f}.signed_angle_to(this->last_known_player_position - this->aim_offset_position(), {0.f, 1.f, 0.f}); if(this->at_target_angle && !this->anim_tree->get_current_state().begins_with("Fire") && !this->anim_tree->get_fire_weapon()) return this->is_in_stab_range() ? (ActionFn)&Enemy::stab : (this->shots_fired < this->SHOTS_BEFORE_MOVE ? (ActionFn)&Enemy::fire : (ActionFn)&Enemy::wait_line_of_sight); return (ActionFn)&Enemy::wait_end_of_shot; } Enemy::ActionFn Enemy::stab() { this->set_current_state_name("Stab (start)"); this->anim_tree->set_aim_weapon(false); float const target_diff{this->get_global_basis().get_column(2).signed_angle_to(this->last_known_player_position - this->aim_offset_position(), {0.f, 1.f, 0.f})}; this->target_rotation = this->get_global_rotation().y + target_diff; if(this->anim_tree->get_current_state().begins_with("Aim") || !this->at_target_angle) return (ActionFn)&Enemy::stab; this->anim_tree->set_stab(); return (ActionFn)&Enemy::wait_end_of_stab; } Enemy::ActionFn Enemy::wait_end_of_stab() { this->set_current_state_name("Stab (wait)"); float const target_diff{this->get_global_basis().get_column(2).signed_angle_to(this->last_known_player_position - this->aim_offset_position(), {0.f, 1.f, 0.f})}; this->target_rotation = this->get_global_rotation().y + target_diff; if(this->at_target_angle && !this->anim_tree->get_current_state().begins_with("Stab") && !this->anim_tree->get_fire_weapon()) return this->is_in_stab_range() ? (ActionFn)&Enemy::stab : (ActionFn)&Enemy::wait_line_of_sight; return (ActionFn)&Enemy::wait_end_of_stab; } Enemy::ActionFn Enemy::chase_enter() { this->set_current_state_name("Chase (plot)"); this->anim_tree->set_aim_weapon(false); this->agent->set_target_position(this->last_known_player_position); this->agent->set_avoidance_priority(this->MOVING_NAV_PRIORITY); return (ActionFn)&Enemy::chase_player; } Enemy::ActionFn Enemy::chase_player() { this->set_current_state_name("Chase (run)"); if(this->can_see_player && this->is_in_stab_range()) { this->anim_tree->set_lock_running(false); return (ActionFn)&Enemy::stab; } else if(this->agent->is_navigation_finished()) { this->target_rotation = this->last_known_player_rotation; this->anim_tree->set_lock_running(false); return (ActionFn)&Enemy::stop_running; } else { this->anim_tree->set_lock_running(true); return (ActionFn)&Enemy::chase_player; } } Enemy::ActionFn Enemy::stop_running() { this->set_current_state_name("Chase (stop)"); this->agent->set_target_position(this->get_global_position()); this->agent->set_avoidance_priority(this->STATIONARY_NAV_PRIORITY); return this->anim_tree->get_current_state().begins_with("Run") && this->get_global_rotation().y == this->target_rotation ? (ActionFn)&Enemy::stop_running : (ActionFn)&Enemy::wait_line_of_sight; } void Enemy::_physics_process(double delta) { // transform root motion to global velocity gd::Basis const basis{this->get_global_basis()}; gd::Vector3 const motion{this->anim_tree->get_root_motion_position()}; this->set_velocity(gd::Vector3{ basis.get_column(0) * motion.x + basis.get_column(1) * motion.y + basis.get_column(2) * motion.z } / delta); // convert from m/s to m/frame this->move_and_slide(); // update movement this->update_can_see_player(); // check vision } void Enemy::damage() { this->anim_tree->death_animation(); this->set_collision_mask(0x0); this->set_collision_layer(0x0); this->set_process(false); this->set_physics_process(false); this->drone_sound->stop(); this->current_action_fn = nullptr; this->set_current_state_name("None"); } void Enemy::update_can_see_player() { // don't update sightlines if player is not found // also don't update sightlines if the current action is firing, to avoid sudden turn-around shots that feel unfair if(this->player == nullptr || this->current_action_fn == (ActionFn)&Enemy::wait_end_of_shot) return; // calculate line segment to cast gd::Vector3 const origin{this->get_global_position() + gd::Vector3{0.f, 1.8f, 0.f}}; gd::Vector3 const target{this->player->get_global_position() + gd::Vector3{0.f, 1.8f, 0.f}}; // check if the target is in field of view float const dot{(target - origin).normalized().dot(this->get_global_basis().get_column(2))}; if(this->current_action_fn != (ActionFn)&Enemy::chase_player && dot <= 0.7f && target.distance_to(origin) > 1.5f * 1.5f) { this->can_see_player = false; // target not in field of view } else { // check if the sightline is obstructed by raycast gd::PhysicsDirectSpaceState3D *space{this->get_world_3d()->get_direct_space_state()}; gd::Ref query{gd::PhysicsRayQueryParameters3D::create(origin, target)}; gd::Dictionary dict{space->intersect_ray(query)}; this->can_see_player = (dict.is_empty() || gd::Object::cast_to(dict["collider"]) == this->player); } } void Enemy::set_update_interval(float time) { this->update_interval = time; } float Enemy::get_update_interval() const { return this->update_interval; } bool Enemy::is_in_stab_range() const { return this->player->get_global_position().distance_squared_to(this->get_global_position()) < this->STAB_RANGE*this->STAB_RANGE; } void Enemy::set_current_state_name(gd::String name) { this->debug_label->set_text(gd::vformat("Action: %s\nState: %s", name, this->anim_tree->get_current_state())); this->current_state_name = name; } gd::Vector3 Enemy::aim_offset_position() const { gd::Basis const basis{this->get_global_basis()}; return this->get_global_position() + basis.get_column(0) * this->AIM_OFFSET; }