feat: created character specializations of unit and world state

This commit is contained in:
Sara 2024-08-18 17:42:20 +02:00
parent 7312df3679
commit c9c41ac2d7
17 changed files with 113 additions and 35 deletions

View file

@ -21,7 +21,7 @@ rings = 3
[sub_resource type="BoxMesh" id="BoxMesh_p8wvo"]
size = Vector3(0.2, 0.2, 0.5)
[node name="Tank" type="Unit"]
[node name="Tank" type="TankUnit"]
configure_team = 3
movement_speed = 1.5
collision_layer = 6

View file

@ -18,12 +18,12 @@ rings = 3
[sub_resource type="BoxMesh" id="BoxMesh_p8wvo"]
size = Vector3(0.2, 0.2, 0.5)
[node name="Player" type="Unit"]
[node name="Player" type="CharacterUnit"]
configure_team = 1
collision_layer = 6
collision_mask = 0
[node name="ActorWorldState" type="UnitWorldState" parent="."]
[node name="ActorWorldState" type="CharacterWorldState" parent="."]
unique_name_in_owner = true
[node name="Planner" type="Planner" parent="."]

21
src/character_unit.cpp Normal file
View file

@ -0,0 +1,21 @@
#include "character_unit.hpp"
void CharacterUnit::_bind_methods() {
#define CLASSNAME CharacterUnit
}
void CharacterUnit::_ready() {
this->parent_type::_ready();
this->inventory = this->get_node<Inventory>("%Inventory");
}
void CharacterUnit::use_weapon() {
gd::Node3D *target{this->world_state->get_target_node()};
Item const *weapon{this->inventory->get_weapon()};
if(weapon == nullptr) {
this->parent_type::use_weapon(); // use the default unarmed attack instead
} else {
this->inventory->get_weapon()->try_use_on(this, target);
}
}

17
src/character_unit.hpp Normal file
View file

@ -0,0 +1,17 @@
#ifndef CHARACTER_UNIT_HPP
#define CHARACTER_UNIT_HPP
#include "unit.hpp"
#include "inventory.hpp"
class CharacterUnit : public Unit {
GDCLASS(CharacterUnit, Unit);
static void _bind_methods();
public:
virtual void _ready() override;
virtual void use_weapon() override;
private:
Inventory *inventory{nullptr};
};
#endif // !CHARACTER_UNIT_HPP

View file

@ -0,0 +1,16 @@
#include "character_world_state.hpp"
#include "utils/godot_macros.hpp"
void CharacterWorldState::_bind_methods() {
#define CLASSNAME CharacterWorldState
}
void CharacterWorldState::_enter_tree() {
this->parent_type::_enter_tree();
this->inventory = this->get_node<Inventory>("%Inventory");
}
bool CharacterWorldState::get_is_weapon_ranged() const {
return this->inventory->get_weapon()->has_capability(ItemCapability::WeaponRanged);
}

View file

@ -0,0 +1,17 @@
#ifndef CHARACTER_WORLD_STATE_HPP
#define CHARACTER_WORLD_STATE_HPP
#include "inventory.hpp"
#include "unit_world_state.hpp"
class CharacterWorldState : public UnitWorldState {
GDCLASS(CharacterWorldState, UnitWorldState);
static void _bind_methods();
public:
virtual void _enter_tree() override;
virtual bool get_is_weapon_ranged() const override;
private:
Inventory *inventory{nullptr};
};
#endif // !CHARACTER_WORLD_STATE_HPP

View file

@ -2,6 +2,8 @@
#define GOAP_STATE_HPP
#include "actor_world_state.hpp"
#include <godot_cpp/core/object.hpp>
#include <godot_cpp/classes/object.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/node3d.hpp>

View file

@ -11,8 +11,8 @@ void Inventory::_bind_methods() {
}
void Inventory::_ready() {
if(gd::Engine::get_singleton()->is_editor_hint())
return;
if(gd::Engine::get_singleton()->is_editor_hint())
return;
this->parent_unit = gd::Object::cast_to<Unit>(this->get_parent());
}

View file

@ -14,7 +14,7 @@ void NavMarker::_bind_methods() {
void NavMarker::_process(double) {
#ifdef DEBUG_ENABLED
// only run in editor while selected
// only run in editor while selected
if(gd::Engine::get_singleton()->is_editor_hint() && gd::EditorInterface::get_singleton()->get_selection()->get_selected_nodes().has(this)) {
gd::RID const map_id{this->get_world_3d()->get_navigation_map()};
gd::Vector3 const new_point{gd::NavigationServer3D::get_singleton()->map_get_closest_point(map_id, this->get_global_position())};

View file

@ -1,4 +1,6 @@
#include "register_types.h"
#include "character_unit.hpp"
#include "character_world_state.hpp"
#include "enemy_world_state.hpp"
#include "entity_health.hpp"
#include "inventory.hpp"
@ -10,6 +12,7 @@
#include "rts_items.hpp"
#include "rts_player.hpp"
#include "rts_states.hpp"
#include "tank_unit.hpp"
#include "unit.hpp"
#include "unit_world_state.hpp"
#include "goap/action.hpp"
@ -60,8 +63,11 @@ void initialize_gdextension_types(gd::ModuleInitializationLevel p_level)
GDREGISTER_RUNTIME_CLASS(Activate);
GDREGISTER_RUNTIME_CLASS(UnitWorldState);
GDREGISTER_RUNTIME_CLASS(EnemyWorldState);
GDREGISTER_RUNTIME_CLASS(CharacterWorldState);
GDREGISTER_RUNTIME_CLASS(GoalMarker);
GDREGISTER_RUNTIME_CLASS(Unit);
GDREGISTER_RUNTIME_CLASS(TankUnit)
GDREGISTER_RUNTIME_CLASS(CharacterUnit);
GDREGISTER_RUNTIME_CLASS(EntityHealth);
GDREGISTER_CLASS(NavMarker);
GDREGISTER_RUNTIME_CLASS(NavRoom);

View file

@ -29,6 +29,7 @@ goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const
FireAtTarget::FireAtTarget()
: Action() {
this->required.insert("can_see_target", true);
this->required.insert("is_weapon_ranged", true);
this->effects.insert("is_target_dead", true);
}
@ -96,7 +97,7 @@ TakeCover::TakeCover()
}
bool TakeCover::procedural_is_possible(goap::ActorWorldState *context) const {
// positions of the context and the target
// positions of the context and the target
gd::Vector3 const context_position{context->get_world_property("parent_global_position")};
gd::Vector3 const target_position{context->get_world_property("target_global_position")};
// if there is no available navigation room, it is not possible to find a cover marker

View file

@ -33,6 +33,9 @@ void Handgun::use_on(Unit* used_by, gd::Object* used_on) const {
used_by->aim_at(target);
EntityHealth *health{target->get_entity_health()};
health->damaged_by(4, used_by);
gd::Vector3 const owner_position{used_by->get_global_position()};
gd::Vector3 const target_position{target->get_global_position()};
used_by->look_at(owner_position-(target_position-owner_position));
}
Lasercutter::Lasercutter()

View file

@ -43,7 +43,6 @@ void RTSPlayer::setup_player_input(utils::PlayerInput *input) {
input->listen_to("_mouse_down", "_mouse_up", callable_mp(this, &RTSPlayer::on_mouse_vertical));
#ifdef DEBUG_ENABLED
input->listen_to("DEBUG_toggle_debug", callable_mp(this, &RTSPlayer::DEBUG_enable_debug));
gd::UtilityFunctions::print("!!! DEBUG_ENABLED");
#endif
}

View file

@ -29,7 +29,6 @@ void Unit::_enter_tree() {
this->anim_player = this->get_node<gd::AnimationPlayer>("%AnimationPlayer");
this->health = this->get_node<EntityHealth>("%EntityHealth");
this->health->connect("death", callable_mp(this, &Unit::on_death));
this->inventory = this->get_node<Inventory>("%Inventory");
}
void Unit::_physics_process(double) {
@ -46,9 +45,7 @@ void Unit::_physics_process(double) {
void Unit::stop_plan() {
this->current_goal.unref();
this->current_plan.clear();
if(this->state && !this->state->is_queued_for_deletion())
this->destroy_state();
this->state = nullptr;
this->destroy_state();
this->call_deferred("emit_signal", "plan_failed");
}
@ -78,16 +75,9 @@ void Unit::begin_goal(gd::Ref<goap::Goal> goal) {
}
void Unit::use_weapon() {
gd::Node3D *target{this->world_state->get_target_node()};
Item const *weapon{this->inventory->get_weapon()};
if(weapon == nullptr) {
// try and do a melee attack instead when no weapon is equipped
Unit *target_unit{gd::Object::cast_to<Unit>(target)};
if(target_unit != nullptr && this->world_state->get_can_see_target() && this->world_state->get_is_in_melee_range())
target_unit->get_entity_health()->damaged_by(1, this);
} else {
this->inventory->get_weapon()->try_use_on(this, target);
}
Unit *target_unit{gd::Object::cast_to<Unit>(this->world_state->get_target_node())};
if(target_unit != nullptr && this->world_state->get_can_see_target() && this->world_state->get_is_in_melee_range())
target_unit->get_entity_health()->damaged_by(1, this);
}
void Unit::aim_at(gd::Node3D *target) {
@ -119,9 +109,10 @@ void Unit::on_velocity_computed(gd::Vector3 vel) {
}
void Unit::destroy_state() {
if(this->state == nullptr || this->state->is_queued_for_deletion() || !this->state->is_inside_tree())
if(this->state == nullptr)
return;
this->state->interrupt_state();
if(!this->state->is_queued_for_deletion() && this->state->is_inside_tree())
this->state->interrupt_state();
this->state = nullptr;
}
@ -132,30 +123,30 @@ void Unit::state_finished() {
}
void Unit::next_action() {
// destroy active state if relevant
this->destroy_state();
// cannot perform actions while dead
if(!this->health->is_conscious())
return;
// destroy active state if relevant
if(this->state != nullptr && !this->state->is_queued_for_deletion())
this->destroy_state();
this->state = nullptr;
if(this->current_plan.is_empty())
return;
goap::Action const *action{this->current_plan.get(0)};
if(!action->is_possible(this->world_state)) {
if(action == nullptr || !action->is_possible(this->world_state)) {
this->emit_signal("plan_interrupted");
return;
}
// pop next action and apply state
// get next action and apply state
this->state = action->get_apply_state(this->world_state);
if(state == nullptr) {
this->emit_signal("plan_interrupted");
return;
}
// pop action from plan
this->current_plan.remove_at(0);
this->add_child(this->state);
// register new state and start
this->state->connect("state_finished", this->on_state_finished);
this->state->connect("state_failed", this->on_plan_failed);
this->add_child(this->state);
}
void Unit::replan_goal() {
@ -168,9 +159,8 @@ void Unit::set_goal_and_plan(gd::Ref<goap::Goal> goal) {
this->current_plan.clear();
} else {
this->current_plan = this->planner->plan_for_goal(goal);
if(this->current_plan.is_empty()) {
if(this->current_plan.is_empty())
this->current_goal.unref();
}
}
}

View file

@ -2,7 +2,6 @@
#define RTS_UNIT_HPP
#include "goal_marker.hpp"
#include "inventory.hpp"
#include "unit_world_state.hpp"
#include "goap/goal.hpp"
#include "goap/planner.hpp"
@ -70,7 +69,6 @@ protected:
goap::Planner *planner{nullptr};
EntityHealth *health{nullptr};
UnitWorldState *world_state{nullptr};
Inventory *inventory{nullptr};
#ifdef DEBUG_ENABLED
public:
gd::String DEBUG_print_debug_info();

View file

@ -12,6 +12,7 @@
void UnitWorldState::_bind_methods() {
#define CLASSNAME UnitWorldState
GDSIGNAL("attention_changed");
GDFUNCTION(get_is_weapon_ranged);
GDFUNCTION(get_can_see_target);
GDFUNCTION(get_is_target_dead);
GDFUNCTION(get_is_at_target);
@ -41,6 +42,10 @@ gd::Variant UnitWorldState::get_world_property(gd::String property) {
return ActorWorldState::get_world_property(property);
}
bool UnitWorldState::get_is_weapon_ranged() const {
return false;
}
bool UnitWorldState::get_can_see_target() {
bool value{this->get_can_see_node(this->target_node)};
this->cache_property("can_see_target", value, utils::time_seconds() + 0.5);

View file

@ -2,6 +2,7 @@
#define UNIT_WORLD_STATE_HPP
#include "entity_health.hpp"
#include "inventory.hpp"
#include "goap/actor_world_state.hpp"
#include <godot_cpp/classes/navigation_agent3d.hpp>
#include <godot_cpp/classes/node3d.hpp>
@ -21,6 +22,8 @@ class UnitWorldState : public goap::ActorWorldState {
public:
virtual void _enter_tree() override;
virtual gd::Variant get_world_property(gd::String property) override;
virtual bool get_is_weapon_ranged() const;
bool get_can_see_target();
bool get_can_see_node(gd::Node3D *node) const;
bool get_is_at_target() const;