feat: fully implemented click-to-move

This commit is contained in:
Sara 2024-06-11 09:08:49 +02:00
parent 1ddfa17659
commit 8cfbbcce27
24 changed files with 684 additions and 90 deletions

6
godot/AI/new_goal.tres Normal file
View file

@ -0,0 +1,6 @@
[gd_resource type="Goal" format=3 uid="uid://b4i4e34046n44"]
[resource]
desired_state_dict = {
"goal1": true
}

View file

@ -0,0 +1,24 @@
[gd_scene load_steps=4 format=3 uid="uid://btnlq0xajbfit"]
[sub_resource type="Goal" id="Goal_nmxy4"]
desired_state_dict = {
"is_at_target": true
}
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_t1mgt"]
albedo_color = Color(0.596078, 0.745098, 0.635294, 1)
[sub_resource type="CylinderMesh" id="CylinderMesh_4i5c2"]
material = SubResource("StandardMaterial3D_t1mgt")
height = 0.1
[node name="GroundGoalMarker" type="GoalMarker"]
goal = SubResource("Goal_nmxy4")
transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, -2.38494, 0, -2.58246)
disable_mode = 1
collision_layer = 0
collision_mask = 0
input_ray_pickable = false
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
mesh = SubResource("CylinderMesh_4i5c2")

View file

@ -1,9 +1,12 @@
[gd_scene load_steps=2 format=3 uid="uid://bmm2quy62exfr"]
[gd_scene load_steps=3 format=3 uid="uid://bmm2quy62exfr"]
[ext_resource type="PackedScene" uid="uid://btnlq0xajbfit" path="res://GameObjects/ground_goal_marker.tscn" id="1_t46fa"]
[sub_resource type="CylinderMesh" id="CylinderMesh_hd6t5"]
height = 0.1
[node name="PlayerCamera" type="RTSPlayer"]
ground_marker_scene = ExtResource("1_t46fa")
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(-1, 7.57103e-08, -4.37114e-08, 0, 0.5, 0.866025, 8.74228e-08, 0.866025, -0.5, 0, 5.86004, -2.59394)

View file

@ -0,0 +1,32 @@
[gd_scene load_steps=4 format=3 uid="uid://pme230qx1377"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n4q15"]
albedo_color = Color(0.407843, 1, 0.447059, 1)
[sub_resource type="CapsuleMesh" id="CapsuleMesh_5r0b3"]
material = SubResource("StandardMaterial3D_n4q15")
[sub_resource type="SphereShape3D" id="SphereShape3D_drlm2"]
radius = 1.0
[node name="Unit" type="Unit"]
collision_layer = 2
collision_mask = 0
[node name="ActorWorldState" type="UnitWorldState" parent="."]
[node name="Planner" type="Planner" parent="."]
actions = [0, 1, 2]
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
target_desired_distance = 0.25
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
mesh = SubResource("CapsuleMesh_5r0b3")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
shape = SubResource("SphereShape3D_drlm2")

View file

@ -1,7 +1,17 @@
[gd_scene load_steps=4 format=3 uid="uid://c62s1jmtgajjk"]
[gd_scene load_steps=7 format=3 uid="uid://c62s1jmtgajjk"]
[ext_resource type="PackedScene" uid="uid://dsalxxq3xs842" path="res://rts_game_mode.tscn" id="1_4nchg"]
[ext_resource type="Environment" uid="uid://cnfk8yrvklysq" path="res://Environments/default_environment.tres" id="2_jq6bw"]
[ext_resource type="PackedScene" uid="uid://pme230qx1377" path="res://GameObjects/unit.tscn" id="3_wl7wm"]
[sub_resource type="NavigationMesh" id="NavigationMesh_8a2j6"]
vertices = PackedVector3Array(-4.5, 0.4, -4.5, -4.5, 0.4, 4.5, 4.5, 0.4, 4.5, 4.5, 0.4, -4.5)
polygons = [PackedInt32Array(3, 2, 0), PackedInt32Array(0, 2, 1)]
geometry_parsed_geometry_type = 1
geometry_collision_mask = 4294967289
[sub_resource type="BoxShape3D" id="BoxShape3D_ubt1u"]
size = Vector3(10, 0.1, 10)
[sub_resource type="PlaneMesh" id="PlaneMesh_hohcb"]
size = Vector2(10, 10)
@ -12,10 +22,23 @@ game_mode_prototype = ExtResource("1_4nchg")
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = ExtResource("2_jq6bw")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="WorldEnvironment"]
navigation_mesh = SubResource("NavigationMesh_8a2j6")
[node name="StaticBody3D" type="StaticBody3D" parent="WorldEnvironment/NavigationRegion3D"]
collision_layer = 5
[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/NavigationRegion3D/StaticBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
shape = SubResource("BoxShape3D_ubt1u")
[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldEnvironment/NavigationRegion3D/StaticBody3D"]
mesh = SubResource("PlaneMesh_hohcb")
skeleton = NodePath("../../../..")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="WorldEnvironment"]
transform = Transform3D(-0.030678, -0.932282, 0.36043, 0, 0.360599, 0.932721, -0.999529, 0.028614, -0.0110625, 0, 1.28927, 0)
light_color = Color(1, 0.926667, 0.78, 1)
[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldEnvironment"]
mesh = SubResource("PlaneMesh_hohcb")
skeleton = NodePath("../..")
[node name="Unit" parent="." instance=ExtResource("3_wl7wm")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7165, 1.8999e-07, 3.29106)

View file

@ -62,3 +62,9 @@ rotate_right={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"echo":false,"script":null)
]
}
[layer_names]
3d_physics/layer_1="Default"
3d_physics/layer_2="Units"
3d_physics/layer_3="Objects"

21
src/goal_marker.cpp Normal file
View file

@ -0,0 +1,21 @@
#include "goal_marker.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/classes/global_constants.hpp>
#include <godot_cpp/classes/node.hpp>
void GoalMarker::_bind_methods() {
#define CLASSNAME GoalMarker
GDPROPERTY_HINTED(goal, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "Goal");
}
void GoalMarker::destroy_on_forgotten() {
callable_mp(gd::Object::cast_to<gd::Node>(this), &gd::Node::queue_free).call_deferred();
}
void GoalMarker::set_goal(gd::Ref<goap::Goal> goal) {
this->goal = goal;
}
gd::Ref<goap::Goal> GoalMarker::get_goal() const {
return this->goal;
}

21
src/goal_marker.hpp Normal file
View file

@ -0,0 +1,21 @@
#ifndef GOAL_MARKER_HPP
#define GOAL_MARKER_HPP
#include "goap/goal.hpp"
#include "godot_cpp/classes/static_body3d.hpp"
#include <godot_cpp/classes/node3d.hpp>
namespace gd = godot;
class GoalMarker : public gd::StaticBody3D {
GDCLASS(GoalMarker, gd::StaticBody3D);
static void _bind_methods();
public:
void destroy_on_forgotten();
void set_goal(gd::Ref<goap::Goal> goal);
gd::Ref<goap::Goal> get_goal() const;
private:
gd::Ref<goap::Goal> goal{};
};
#endif // !GOAL_MARKER_HPP

View file

@ -9,8 +9,8 @@
#define GOAP_ACTION(Name_) \
public: \
_FORCE_INLINE_ static gd::String get_static_class() { return #Name_; }\
_FORCE_INLINE_ virtual gd::String get_class() const override { return #Name_; }\
_FORCE_INLINE_ static gd::StringName get_static_class() { return #Name_; }\
_FORCE_INLINE_ virtual gd::StringName get_class() const override { return #Name_; }\
private:
namespace goap {
@ -23,11 +23,11 @@ typedef int ActionID;
class Action {
friend class ActionDB;
public:
static gd::String get_static_class() { return "Action"; }
virtual gd::String get_class() const { return "Action"; }
static gd::StringName get_static_class() { return "Action"; }
virtual gd::StringName get_class() const { return "Action"; }
virtual ~Action();
virtual State *get_apply_state() const = 0;
virtual State *get_apply_state(ActorWorldState *context) const = 0;
bool is_completed(ActorWorldState *context) const;
bool is_possible(ActorWorldState *context) const;
@ -38,7 +38,8 @@ public:
ActionID get_id() const;
protected:
Action() = default;
template<class TState>
TState *create_state() const;
virtual bool procedural_is_possible(ActorWorldState *context) const;
virtual bool procedural_is_completed(ActorWorldState *context) const;
protected:
@ -49,6 +50,13 @@ protected:
private:
ActionID id{-1};
};
template <class TState>
TState *Action::create_state() const {
TState *state = memnew(TState);
state->action = this;
return state;
}
}
#endif // !GOAP_ACTION_HPP

View file

@ -1,5 +1,6 @@
#include "planner.hpp"
#include "action_db.hpp"
#include "godot_cpp/classes/engine.hpp"
#include "godot_cpp/classes/global_constants.hpp"
#include "godot_cpp/templates/hashfuncs.hpp"
#include "utils/godot_macros.hpp"
@ -106,49 +107,58 @@ Plan Planner::plan_for_goal(gd::Ref<Goal> goal) {
return {};
}
gd::Vector<WorldStateNode> open{goal_node};
NodeMap from{};
NodeScoreMap best_path_cost{};
NodeScoreMap heuristic_cost{};
NodeMap from{}; // mapping nodes to the next node in the plan
NodeScoreMap best_path_cost{}; // map nodes to the cost of the best path found so far
NodeScoreMap heuristic_cost{}; // map nodes to their approximate cost to get to their goal
// insert with the goal node into the cost maps,
// path scored 0 as it's the goal
best_path_cost.insert(goal_node, 0.f);
heuristic_cost.insert(goal_node, goal_node.requirements_unmet());
WorldStateNode current{goal_node};
// repeat until current has no traversible edges
while(!open.is_empty()) {
current = open.get(0);
// return found path when all requirements are met
if(current.requirements_unmet() == 0)
return this->unroll_plan(current, from);
open.remove_at(0);
open.remove_at(0); // pop current off the top of the open set
// get all actions that contribute to the open requirements of `current`
gd::Vector<Action const *> edges{this->get_neighbours(current)};
gd::UtilityFunctions::print("found ", edges.size(), " possible actions");
for(Action const * action : edges) {
// construct the node produced by traversing edge
WorldStateNode node_through{current, action};
// calculate the cost of getting here from current through action.
float const path_cost{best_path_cost.get(current) + 1.f};
gd::UtilityFunctions::print("hashes ", WorldStateNodeHasher::hash(node_through), " ; ", WorldStateNodeHasher::hash(current));
gd::UtilityFunctions::print("equiv: ", current == node_through);
// store only if path through `current` represents a new fastest way to get to the new node
if(!best_path_cost.has(node_through) || best_path_cost.get(node_through) >= path_cost) {
gd::UtilityFunctions::print("node through action ", action->get_class(), " added to open set");
// update cheapest route found
best_path_cost[node_through] = path_cost;
// approximate cost to goal
heuristic_cost[node_through] = path_cost + node_through.requirements_unmet();
// store path to get to this point
from[node_through] = current;
open.erase(node_through);
// (re-) insert node according to priority based on unmet requirements
if(open.has(node_through))
open.erase(node_through);
open.ordered_insert(node_through);
} else {
gd::UtilityFunctions::print("node through action ", action->get_class(), " scores worse than previous at same state. ", path_cost, " vs ", best_path_cost[node_through]);
}
}
}
gd::UtilityFunctions::print("failed to find plan for goal ", goal->get_path());
gd::UtilityFunctions::push_warning("failed to find plan for goal ", goal->get_path());
return {};
}
void Planner::set_actions(gd::Array array) {
this->actions.clear();
for(size_t i = 0; i < array.size(); ++i) {
Action const *action = ActionDB::get_action(array[i]);
Action const *action = int(array[i]) < 0 ? nullptr : ActionDB::get_action(array[i]);
if(action != nullptr && !this->actions.has(action)) {
this->actions.push_back(action);
} else if(gd::Engine::get_singleton()->is_editor_hint() && !this->actions.has(nullptr)) {
this->actions.push_back(nullptr);
}
}
}
@ -156,7 +166,7 @@ void Planner::set_actions(gd::Array array) {
gd::Array Planner::get_actions() const {
gd::Array array{};
for(Action const *action : this->actions)
array.push_back(action->get_id());
array.push_back(action == nullptr ? -1 : action->get_id());
return array;
}

View file

@ -7,7 +7,6 @@
#include <cstdint>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/templates/vector.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
namespace gd = godot;
@ -25,7 +24,7 @@ struct WorldStateNode {
WorldStateNode(WorldStateNode const &last_state, Action const *last_action);
~WorldStateNode() = default;
int requirements_unmet() const;
WorldState state{};
WorldState state{}; // the only part considered in hashing
WorldState open_requirements{};
Action const *last_action{nullptr};
ActorWorldState *context;

View file

@ -1,32 +1,33 @@
#include "state.hpp"
#include "goap/action.hpp"
#include "goap/actor_world_state.hpp"
#include "utils/godot_macros.hpp"
namespace goap {
void State::_bind_methods() {}
void MoveTo::_bind_methods() {}
void MoveTo::_enter_tree() {
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
this->agent->set_target_position(this->target_node->get_global_position());
this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent());
void State::_bind_methods() {
#define CLASSNAME State
GDSIGNAL("end_state");
}
void MoveTo::_exit_tree() {
this->agent->set_target_position(this->parent_node3d->get_global_position());
void State::_enter_tree() {
this->world_state = this->get_node<ActorWorldState>("../ActorWorldState");
}
void MoveTo::_process(double delta_time) {
gd::Vector3 const pos = this->parent_node3d->get_global_position();
gd::Vector3 const target = this->agent->get_next_path_position();
gd::Vector3 const direction = (target - pos).normalized();
this->parent_node3d->set_global_position(direction * delta_time);
this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z});
if(this->agent->is_navigation_finished())
this->queue_free();
void State::_process(double delta_time) {
if(this->action->is_completed(this->world_state)) {
this->end_state();
}
}
void Activate::_bind_methods() {}
void Animate::_bind_methods() {}
Action const *State::get_action() const {
return this->action;
}
void State::_end_state() {}
void State::end_state() {
this->_end_state();
this->queue_free();
this->emit_signal("end_state");
}
}

View file

@ -1,47 +1,29 @@
#ifndef GOAP_STATE_HPP
#define GOAP_STATE_HPP
#include "godot_cpp/classes/navigation_agent3d.hpp"
#include "goap/actor_world_state.hpp"
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/node3d.hpp>
namespace gd = godot;
namespace goap {
class State : public gd::Node {
GDCLASS(State, gd::Node);
static void _bind_methods();
};
class Action;
/*! Uses navigation to chase the desired target node.
*/
class MoveTo : public State {
GDCLASS(MoveTo, State);
class State : public gd::Node {
friend class Action;
GDCLASS(State, gd::Node);
static void _bind_methods();
public:
virtual void _enter_tree() override;
virtual void _exit_tree() override;
virtual void _process(double delta_time) override;
public:
gd::Node3D *target_node{nullptr};
protected:
Action const *get_action() const;
virtual void _end_state();
void end_state();
private:
gd::Node3D *parent_node3d{nullptr};
gd::NavigationAgent3D *agent{nullptr};
};
class Activate : public State {
GDCLASS(Activate, State);
static void _bind_methods();
public:
gd::Node3D *target_node{nullptr};
gd::String animation{};
};
class Animate : public State {
GDCLASS(Animate, State);
static void _bind_methods();
public:
gd::String animation{};
ActorWorldState *world_state{nullptr};
Action const *action{nullptr};
};
}

View file

@ -1,12 +1,16 @@
#include "register_types.h"
#include "goap/state.hpp"
#include "rts_actions.hpp"
#include "rts_game_mode.hpp"
#include "rts_player.hpp"
#include "rts_states.hpp"
#include "unit.hpp"
#include "unit_world_state.hpp"
#include "goap/action.hpp"
#include "goap/action_db.hpp"
#include "goap/actor_world_state.hpp"
#include "goap/goal.hpp"
#include "goap/planner.hpp"
#include "rts_game_mode.hpp"
#include "rts_player.hpp"
#include "unit.hpp"
#include "utils/register_types.hpp"
#include <gdextension_interface.h>
#include <godot_cpp/core/class_db.hpp>
@ -24,14 +28,24 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level)
// always register actions before classes,
// so that ActionDB::get_enum_hint is complete before _bind_methods
ClassDB::register_class<RTSPlayer>();
ClassDB::register_class<Unit>();
ClassDB::register_class<RTSGameMode>();
goap::ActionDB::register_action<MoveToTarget>();
goap::ActionDB::register_action<FireAtTarget>();
goap::ActionDB::register_action<FindTarget>();
ClassDB::register_class<goap::ActorWorldState>();
ClassDB::register_class<goap::Goal>();
ClassDB::register_class<goap::Planner>();
ClassDB::register_class<goap::State>();
ClassDB::register_class<MoveTo>();
ClassDB::register_class<Animate>();
ClassDB::register_class<Activate>();
ClassDB::register_class<UnitWorldState>();
ClassDB::register_class<GoalMarker>();
ClassDB::register_class<Unit>();
ClassDB::register_class<RTSGameMode>();
ClassDB::register_class<RTSPlayer>();
}
extern "C"

47
src/rts_actions.cpp Normal file
View file

@ -0,0 +1,47 @@
#include "rts_actions.hpp"
#include "rts_states.hpp"
#include "goap/actor_world_state.hpp"
#include "goap/state.hpp"
#include <godot_cpp/core/memory.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
MoveToTarget::MoveToTarget()
: Action() {
effects.insert("is_at_target", true);
}
goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const {
gd::Node3D *target = gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"));
if(target == nullptr) {
gd::UtilityFunctions::push_warning("Failed to get target node of action ", get_static_class());
return nullptr;
} else {
MoveTo *state = this->create_state<MoveTo>();
state->target_node = target;
return state;
}
}
FireAtTarget::FireAtTarget()
: Action() {
effects.insert("target_dead", true);
required.insert("can_see_target", true);
}
goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const {
Animate *state = memnew(Animate);
state->animation = "fire_weapon";
return state;
}
FindTarget::FindTarget()
: Action() {
effects.insert("can_see_target", true);
}
goap::State *FindTarget::get_apply_state(goap::ActorWorldState *context) const {
gd::Node3D *target = gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"));
MoveTo *state = memnew(MoveTo);
state->target_node = target;
return state;
}

28
src/rts_actions.hpp Normal file
View file

@ -0,0 +1,28 @@
#ifndef RTS_ACTIONS_HPP
#define RTS_ACTIONS_HPP
#include "goap/action.hpp"
#include "goap/actor_world_state.hpp"
class MoveToTarget : public goap::Action {
GOAP_ACTION(MoveToTarget);
public:
MoveToTarget();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
};
class FireAtTarget : public goap::Action {
GOAP_ACTION(FireAtTarget);
public:
FireAtTarget();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
};
class FindTarget : public goap::Action {
GOAP_ACTION(FindTarget);
public:
FindTarget();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
};
#endif // !RTS_ACTIONS_HPP

View file

@ -1,8 +1,25 @@
#include "rts_player.hpp"
#include "goal_marker.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "unit.hpp"
#include "utils/godot_macros.hpp"
#include "utils/level.hpp"
#include "utils/util_functions.hpp"
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
#include <godot_cpp/classes/physics_ray_query_parameters3d.hpp>
#include <godot_cpp/classes/viewport.hpp>
#include <godot_cpp/classes/world3d.hpp>
#include <godot_cpp/variant/callable_method_pointer.hpp>
void RTSPlayer::_bind_methods() {
#define CLASSNAME RTSPlayer
GDPROPERTY_HINTED(ground_marker_scene, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "PackedScene");
}
void RTSPlayer::_ready() {
this->camera = this->get_node<gd::Camera3D>("Camera3D");
}
void RTSPlayer::_process(double delta_time) {
@ -13,6 +30,7 @@ void RTSPlayer::_process(double delta_time) {
this->camera_mouse_motion = {0.f, 0.f};
this->rotate(gd::Vector3{0.f, 1.f, 0.f}, camera_mouse_rotation * this->camera_mouse_rotation_speed);
this->camera_mouse_rotation = 0.f;
this->process_mouse();
}
void RTSPlayer::setup_player_input(utils::PlayerInput *input) {
@ -34,11 +52,74 @@ void RTSPlayer::spawn_at_position(gd::Transform3D const &at) {
this->set_global_transform(at);
}
void RTSPlayer::process_mouse() {
gd::Viewport *view = this->get_viewport();
gd::Vector2 const pix_coords = view->get_mouse_position();
this->cursor_camera_normal = this->camera->project_ray_normal(pix_coords);
}
void RTSPlayer::move(gd::Vector2 motion) {
gd::Vector3 pos = this->get_global_position();
this->set_global_position(pos + this->get_forward_direction() * motion.y + this->get_left_direction() * motion.x);
}
gd::Dictionary RTSPlayer::raycast_from_cursor(uint32_t layers) const {
gd::Vector3 const origin = this->camera->get_global_position();
gd::Vector3 const destination = origin + this->cursor_camera_normal * 1000.f;
gd::Ref<gd::PhysicsRayQueryParameters3D> const query{gd::PhysicsRayQueryParameters3D::create(origin, destination, layers)};
gd::PhysicsDirectSpaceState3D *state = this->get_world_3d()->get_direct_space_state();
return state->intersect_ray(query);
}
gd::Node3D *RTSPlayer::node_under_cursor(uint32_t layers) const {
gd::Dictionary result = this->raycast_from_cursor(layers);
if(!result.is_empty())
gd::UtilityFunctions::print("Found object ", Object::cast_to<gd::Node>(result["collider"])->get_path());
else
gd::UtilityFunctions::print("No object found");
return result.is_empty() ? nullptr : Object::cast_to<gd::Node3D>(result["collider"]);
}
void RTSPlayer::select_unit_under_cursor() {
Unit *unit = Object::cast_to<Unit>(this->node_under_cursor(0x2));
if(unit != nullptr)
this->select_unit(unit);
}
void RTSPlayer::order_activate_object_under_cursor() {
if(this->selected_unit == nullptr)
return;
gd::Dictionary const dict = this->raycast_from_cursor(0x4);
if(dict.is_empty())
return;
gd::Node3D *hit = gd::Object::cast_to<gd::Node3D>(dict["collider"]);
GoalMarker *marker = gd::Object::cast_to<GoalMarker>(hit);
bool const is_temporary_marker = marker == nullptr;
if(is_temporary_marker) {
marker = gd::Object::cast_to<GoalMarker>(this->ground_marker_scene->instantiate());
this->add_child(marker);
marker->set_as_top_level(true);
marker->set_global_position(dict["position"]);
}
this->selected_unit->plan_for_marker(marker);
if(is_temporary_marker) {
this->selected_unit->get_world_state()->connect("attention_changed", callable_mp(marker, &GoalMarker::destroy_on_forgotten));
}
}
void RTSPlayer::clear_selected_unit() {
if(this->selected_unit != nullptr) {
this->selected_unit->disconnect("tree_exited", this->unit_destroyed_callable());
this->selected_unit = nullptr;
}
}
void RTSPlayer::select_unit(Unit *unit) {
this->clear_selected_unit();
this->selected_unit = unit;
unit->connect("tree_exited", this->unit_destroyed_callable());
}
void RTSPlayer::on_mouse_horizontal(gd::Ref<gd::InputEvent> event, float value) {
if(this->mmb_down)
this->camera_mouse_motion.x = value;
@ -64,10 +145,18 @@ void RTSPlayer::on_rotate_horizontal(gd::Ref<gd::InputEvent> event, float value)
}
void RTSPlayer::on_rclick(gd::Ref<gd::InputEvent> event, float value) {
gd::UtilityFunctions::print("on_rclick on ", this->get_path(), " ---------");
if(value == 0.f && !this->rmb_is_held())
this->order_activate_object_under_cursor();
this->rmb_down = value != 0.f;
this->rmb_last_change = utils::time_seconds();
}
void RTSPlayer::on_lclick(gd::Ref<gd::InputEvent> event, float value) {
if(value == 0.f && !this->lmb_is_held())
this->select_unit_under_cursor();
this->lmb_down = value != 0.f;
this->lmb_last_change = utils::time_seconds();
}
void RTSPlayer::on_mclick(gd::Ref<gd::InputEvent> event, float value) {
@ -83,3 +172,23 @@ gd::Vector3 RTSPlayer::get_left_direction() const {
gd::Vector3 left = this->get_global_basis().get_column(0);
return gd::Vector3{left.x, 0.f, left.z}.normalized();
}
bool RTSPlayer::lmb_is_held() {
return this->lmb_down && utils::time_seconds() >= (this->lmb_last_change + this->time_to_held);
}
bool RTSPlayer::rmb_is_held() {
return this->rmb_down && utils::time_seconds() >= (this->rmb_last_change + this->time_to_held);
}
gd::Callable RTSPlayer::unit_destroyed_callable() {
return callable_mp(this, &RTSPlayer::clear_selected_unit);
}
void RTSPlayer::set_ground_marker_scene(gd::Ref<gd::PackedScene> scene) {
this->ground_marker_scene = scene;
}
gd::Ref<gd::PackedScene> RTSPlayer::get_ground_marker_scene() const {
return this->ground_marker_scene;
}

View file

@ -1,9 +1,13 @@
#ifndef RTS_PLAYER_HPP
#define RTS_PLAYER_HPP
#include "godot_cpp/classes/camera3d.hpp"
#include "godot_cpp/classes/input_event.hpp"
#include "godot_cpp/classes/packed_scene.hpp"
#include "unit.hpp"
#include "utils/player.hpp"
#include "utils/player_input.hpp"
#include <cstdint>
#include <godot_cpp/classes/node3d.hpp>
namespace gd = godot;
@ -13,13 +17,25 @@ class RTSPlayer : public gd::Node3D,
GDCLASS(RTSPlayer, godot::Node3D);
static void _bind_methods();
public:
virtual void _ready() override;
virtual void _process(double delta_time) override;
virtual void setup_player_input(utils::PlayerInput *input) override;
virtual Node *to_node() override;
virtual void spawn_at_position(gd::Transform3D const &at) override;
private:
//! process any changes to mouse position
void process_mouse();
//! move the camera along the ground
void move(gd::Vector2 motion);
gd::Dictionary raycast_from_cursor(uint32_t layers = UINT32_MAX) const;
//! \return the first node from the camera under the cursor
gd::Node3D *node_under_cursor(uint32_t layers = UINT32_MAX) const;
//! add units under the cursor to this->selected_units
void select_unit_under_cursor();
void order_activate_object_under_cursor();
void clear_selected_unit();
void select_unit(Unit *unit);
// input functions
void on_mouse_horizontal(gd::Ref<gd::InputEvent> event, float value);
@ -31,22 +47,39 @@ private:
void on_rclick(gd::Ref<gd::InputEvent> event, float value);
void on_mclick(gd::Ref<gd::InputEvent> event, float value);
// getters
// setters & getters
gd::Vector3 get_forward_direction() const;
gd::Vector3 get_left_direction() const;
bool lmb_is_held();
bool rmb_is_held();
gd::Callable unit_destroyed_callable();
void set_ground_marker_scene(gd::Ref<gd::PackedScene> scene);
gd::Ref<gd::PackedScene> get_ground_marker_scene() const;
private:
Unit* selected_unit{nullptr};
bool mmb_down{false};
bool lmb_down{false};
float lmb_last_change{0.f};
bool rmb_down{false};
float rmb_last_change{0.f};
gd::Vector2 camera_mouse_motion{0.f, 0.f};
float camera_mouse_rotation{0.f};
gd::Vector2 camera_keys_motion{0.f, 0.f};
float camera_keys_rotation{0.f};
gd::Vector3 cursor_camera_normal{0.f, 0.f, 0.f};
gd::Camera3D *camera{nullptr};
gd::Ref<gd::PackedScene> ground_marker_scene{nullptr};
float const camera_keys_speed{10.f};
float const camera_keys_rotation_speed{1.f};
float const camera_mouse_speed{0.01f};
float const camera_mouse_rotation_speed{-0.003f};
double const time_to_held{0.1};
};
#endif // !RTS_PLAYER_HPP

31
src/rts_states.cpp Normal file
View file

@ -0,0 +1,31 @@
#include "rts_states.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
void MoveTo::_bind_methods() {}
void MoveTo::_ready() {
gd::UtilityFunctions::print("_ready", this->get_path());
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
this->agent->set_target_position(this->target_node->get_global_position());
this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent());
}
void MoveTo::_end_state() {
gd::UtilityFunctions::print("_end_state on ", this->get_path());
this->agent->set_target_position(this->parent_node3d->get_global_position());
}
void MoveTo::_process(double delta_time) {
gd::Vector3 const pos = this->parent_node3d->get_global_position();
gd::Vector3 const target = this->agent->get_next_path_position();
gd::Vector3 const direction = (target - pos).normalized();
this->parent_node3d->set_global_position(pos + direction * delta_time);
this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z});
if(this->agent->is_navigation_finished())
this->end_state();
State::_process(delta_time);
}
void Activate::_bind_methods() {}
void Animate::_bind_methods() {}

38
src/rts_states.hpp Normal file
View file

@ -0,0 +1,38 @@
#ifndef RTS_STATES_HPP
#define RTS_STATES_HPP
#include "goap/state.hpp"
#include <godot_cpp/classes/navigation_agent3d.hpp>
/*! Uses navigation to chase the desired target node.
*/
class MoveTo : public goap::State {
GDCLASS(MoveTo, goap::State);
static void _bind_methods();
public:
virtual void _ready() override;
virtual void _end_state() override;
virtual void _process(double delta_time) override;
public:
gd::Node3D *target_node{nullptr};
private:
gd::Node3D *parent_node3d{nullptr};
gd::NavigationAgent3D *agent{nullptr};
};
class Activate : public goap::State {
GDCLASS(Activate, goap::State);
static void _bind_methods();
public:
gd::Node3D *target_node{nullptr};
gd::String animation{};
};
class Animate : public goap::State {
GDCLASS(Animate, goap::State);
static void _bind_methods();
public:
gd::String animation{};
};
#endif // !RTS_STATES_HPP

View file

@ -1,8 +1,70 @@
#include "unit.hpp"
#include "goap/goal.hpp"
#include "godot_cpp/variant/callable_method_pointer.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/classes/navigation_agent3d.hpp>
void Unit::_bind_methods() {
#define CLASSNAME Unit
}
void Unit::_enter_tree() {
void Unit::_enter_tree() { GDGAMEONLY();
this->agent = this->get_node<gd::NavigationAgent3D>("NavigationAgent3D");
this->planner = this->get_node<goap::Planner>("Planner");
this->world_state = this->get_node<UnitWorldState>("ActorWorldState");
this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan));
}
void Unit::_process(double delta_time) {
}
void Unit::stop_plan() {
if(this->is_queued_for_deletion())
return;
this->current_plan.clear();
if(this->state && !this->state->is_queued_for_deletion())
this->destroy_state();
this->state = nullptr;
}
void Unit::plan_for_marker(GoalMarker *marker) {
gd::UtilityFunctions::print("plan_for_marker on ", this->get_path());
this->destroy_state();
this->world_state->set_target_node(marker);
this->plan_for_goal(marker->get_goal());
}
void Unit::plan_for_goal(gd::Ref<goap::Goal> goal) {
gd::UtilityFunctions::print("plan_for_goal on ", this->get_path());
this->current_plan = this->planner->plan_for_goal(goal);
this->next_action();
}
UnitWorldState *Unit::get_world_state() const {
return this->world_state;
}
void Unit::destroy_state() {
if(this->state == nullptr || this->state->is_queued_for_deletion() || !this->state->is_inside_tree())
return;
this->state->queue_free();
this->remove_child(this->state);
this->state = nullptr;
}
void Unit::next_action() {
if(this->state != nullptr && !this->state->is_queued_for_deletion())
this->destroy_state();
this->state = nullptr;
if(this->current_plan.is_empty()) return;
this->state = this->current_plan.get(0)->get_apply_state(this->world_state);
if(state == nullptr) {
this->stop_plan();
gd::UtilityFunctions::push_error("Plan failed to be executed, abandoning");
return;
}
this->current_plan.remove_at(0);
this->add_child(this->state);
this->state->connect("end_state", this->on_end_state);
}

View file

@ -1,19 +1,43 @@
#ifndef RTS_UNIT_HPP
#define RTS_UNIT_HPP
#include "goal_marker.hpp"
#include "unit_world_state.hpp"
#include "goap/goal.hpp"
#include "goap/planner.hpp"
#include <godot_cpp/classes/character_body3d.hpp>
#include <godot_cpp/classes/navigation_agent3d.hpp>
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/variant/callable.hpp>
namespace gd = godot;
class Unit : public gd::Node3D {
GDCLASS(Unit, gd::Node3D);
class Unit : public gd::CharacterBody3D {
GDCLASS(Unit, gd::CharacterBody3D);
static void _bind_methods();
public:
virtual void _enter_tree() override;
virtual void _process(double delta_time) override;
void stop_plan();
void plan_for_marker(GoalMarker *marker);
void plan_for_goal(gd::Ref<goap::Goal> goal);
UnitWorldState *get_world_state() const;
private:
void destroy_state();
void next_action();
private:
goap::Plan current_plan{};
goap::State *state{nullptr};
gd::Node3D *eyes{nullptr};
gd::Callable on_end_state{callable_mp(this, &Unit::next_action)};
gd::NavigationAgent3D *agent{nullptr};
UnitWorldState *world_state{nullptr};
goap::Planner *planner{nullptr};
};
#endif // !RTS_UNIT_HPP

45
src/unit_world_state.cpp Normal file
View file

@ -0,0 +1,45 @@
#include "unit_world_state.hpp"
#include "unit.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/variant/utility_functions.hpp>
void UnitWorldState::_bind_methods() {
#define CLASSNAME UnitWorldState
GDSIGNAL("attention_changed");
GDFUNCTION(get_can_see_target);
GDFUNCTION(get_target_dead);
GDFUNCTION(get_is_at_target);
GDFUNCTION(get_target_node);
}
void UnitWorldState::_enter_tree() { GDGAMEONLY();
this->parent_unit = gd::Object::cast_to<Unit>(this->get_parent());
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
if(this->parent_unit == nullptr)
gd::UtilityFunctions::push_warning("UnitWorldState needs to be a child node of a Unit");
}
bool UnitWorldState::get_can_see_target() const {
return false;
}
bool UnitWorldState::get_is_at_target() const {
if(this->target_node == nullptr) return true;
gd::Vector3 const target = this->target_node->get_global_position();
gd::Vector3 const current = this->parent_unit->get_global_position();
float const min_dist = this->agent->get_target_desired_distance();
return (target - current).length_squared() < min_dist * min_dist;
}
bool UnitWorldState::get_target_dead() const {
return false;
}
void UnitWorldState::set_target_node(gd::Node3D *node) {
this->target_node = node;
this->emit_signal("attention_changed");
}
gd::Node3D *UnitWorldState::get_target_node() const {
return this->target_node;
}

27
src/unit_world_state.hpp Normal file
View file

@ -0,0 +1,27 @@
#ifndef UNIT_WORLD_STATE_HPP
#define UNIT_WORLD_STATE_HPP
#include "goap/actor_world_state.hpp"
#include "godot_cpp/classes/navigation_agent3d.hpp"
#include <godot_cpp/classes/node3d.hpp>
class Unit;
class UnitWorldState : public goap::ActorWorldState {
GDCLASS(UnitWorldState, goap::ActorWorldState);
static void _bind_methods();
public:
virtual void _enter_tree() override;
bool get_can_see_target() const;
bool get_is_at_target() const;
bool get_target_dead() const;
void set_target_node(gd::Node3D *node);
gd::Node3D *get_target_node() const;
private:
Unit *parent_unit{nullptr};
gd::NavigationAgent3D *agent{nullptr};
gd::Node3D *target_node{nullptr};
};
#endif // !UNIT_WORLD_STATE_HPP