- Increased minimum version to 4.3 for compatibility with physics bones.

- Added an interface for nodes that can be damaged.
- Character3D implements the above interface to make itself sliceable.
- Added a hurtbox base class.
- MeshEditingLibrary::slice_mesh now uses Godot references when possible.
- Removed semicolon from print macros to enforce placing them in code.
This commit is contained in:
2026-09-07 13:49:13 -04:00
parent c91a539a26
commit 004c845af7
13 changed files with 277 additions and 22 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
[configuration] [configuration]
entry_symbol = "orng_library_init" entry_symbol = "orng_library_init"
compatibility_minimum = "4.2" compatibility_minimum = "4.3"
reloadable = true reloadable = true
[libraries] [libraries]
+1
View File
@@ -1,6 +1,7 @@
env = SConscript("godot-cpp/SConstruct") env = SConscript("godot-cpp/SConstruct")
env.Append(CPPPATH="liborng/") env.Append(CPPPATH="liborng/")
env.Append(CPPPATH="liborng/data_assets") env.Append(CPPPATH="liborng/data_assets")
env.Append(CPPPATH="liborng/nodes/interfaces")
env.Append(CPPPATH="liborng/nodes") env.Append(CPPPATH="liborng/nodes")
env.Append(CPPPATH="liborng/nodes/character_nodes") env.Append(CPPPATH="liborng/nodes/character_nodes")
env.Append(CPPPATH="liborng/resources") env.Append(CPPPATH="liborng/resources")
+8
View File
@@ -0,0 +1,8 @@
/*
* ©2023 Batty Bovine Productions, LLC. All Rights Reserved.
*
* If this source code makes it to the public internet, this software can be
* considered to be protected by the MIT licence. Have fun with it.
*/
#include "i_damageable.h"
+18
View File
@@ -0,0 +1,18 @@
/*
* ©2023 Batty Bovine Productions, LLC. All Rights Reserved.
*
* If this source code makes it to the public internet, this software can be
* considered to be protected by the MIT licence. Have fun with it.
*/
#pragma once
#include <godot_cpp/variant/dictionary.hpp>
using namespace godot;
class IDamageable
{
public:
virtual void take_damage(const Dictionary &hit_data) = 0;
};
@@ -9,7 +9,10 @@
#include "orng_macros.h" #include "orng_macros.h"
#include "nodes/mesh_editing_library.h"
#include <godot_cpp/classes/engine.hpp> #include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/physical_bone3d.hpp>
#include <godot_cpp/variant/utility_functions.hpp> #include <godot_cpp/variant/utility_functions.hpp>
using namespace godot; using namespace godot;
@@ -17,14 +20,51 @@ using namespace godot;
void Character3D::_ready() void Character3D::_ready()
{ {
set_animation_tree_path(this->animation_tree_path); TypedArray<Node> child_mesh_candidates = this->skeleton->get_children();
const uint32_t num_children = child_mesh_candidates.size();
for (uint32_t i = 0; i < num_children; i++)
{
if (MeshInstance3D *mesh = cast_to<MeshInstance3D>(child_mesh_candidates[i]))
{
this->meshes.emplace_back(mesh);
}
}
} }
void Character3D::set_animation_tree_path(NodePath at) void Character3D::take_damage(const Dictionary &hit_data)
{ {
this->animation_tree_path = at; PRINT_LOG(CHARACTER3D_TAG, "Taking damage: ", hit_data["hit_points"]);
this->animation_tree = cast_to<AnimationTree>(Node::get_node_or_null(this->animation_tree_path)); PRINT_LOG(CHARACTER3D_TAG, "Hit bones: ", hit_data["bone_hits"]);
const Transform3D &global_hit_transform = (Transform3D)hit_data["hit_transform"];
const Plane &slice_plane = this->skeleton->get_global_transform().inverse().xform(Plane(global_hit_transform.get_basis().get_column(1), global_hit_transform.origin));
const uint32_t num_meshes = this->meshes.size();
for (uint32_t i = 0; i < num_meshes; i++)
{
MeshInstance3D *original_mesh_instance = this->meshes[i];
Ref<ArrayMesh> first_half; first_half.instantiate();
Ref<ArrayMesh> other_half; other_half.instantiate();
PackedVector3Array &impact_points = MeshEditingLibrary::slice_mesh(original_mesh_instance, this->skeleton, slice_plane, first_half, other_half, MeshEditingLibrary::CAP_UV_FILL_MESH_BOUNDS, original_mesh_instance->get_active_material(0));
if (other_half.is_valid())
original_mesh_instance->set_mesh(other_half);
}
}
void Character3D::_activate_ragdoll()
{
const TypedArray<Node> &physical_bones = this->physical_bone_simulator->get_children();
const uint32_t num_physical_bones = physical_bones.size();
for (uint32_t i = 0; i < num_physical_bones; i++)
{
if (PhysicalBone3D *bone = cast_to<PhysicalBone3D>(physical_bones[i]))
{
bone->set_collision_layer(0);
}
}
this->physical_bone_simulator->physical_bones_start_simulation();
} }
@@ -9,33 +9,49 @@
#include "orng_macros.h" #include "orng_macros.h"
#include "interfaces/i_damageable.h"
#include <map> #include <map>
#include <godot_cpp/classes/animation_tree.hpp> #include <godot_cpp/classes/animation_tree.hpp>
#include <godot_cpp/classes/character_body3d.hpp> #include <godot_cpp/classes/character_body3d.hpp>
#include <godot_cpp/classes/mesh_instance3d.hpp>
#include <godot_cpp/classes/physical_bone_simulator3d.hpp>
#include <godot_cpp/classes/skeleton3d.hpp>
#include <godot_cpp/variant/dictionary.hpp>
using namespace godot; using namespace godot;
#define CHARACTER3D_TAG "Character3D" #define CHARACTER3D_TAG "Character3D"
class Character3D : public CharacterBody3D class Character3D : public CharacterBody3D, public IDamageable
{ {
GDCLASS(Character3D, CharacterBody3D); GDCLASS(Character3D, CharacterBody3D);
public: public:
virtual void _ready() override; virtual void _ready() override;
virtual void take_damage(const Dictionary &hit_data) override;
Skeleton3D *get_skeleton() const { return this->skeleton; }
PhysicalBoneSimulator3D *get_physical_bone_simulator() const { return this->physical_bone_simulator; }
AnimationTree *get_animation_tree() const { return this->animation_tree; }
virtual void _on_fell_out_of_world(); virtual void _on_fell_out_of_world();
protected: protected:
void set_animation_tree_path(NodePath at); void set_skeleton(Skeleton3D *s) { this->skeleton = s; }
NodePath get_animation_tree_path() const { return this->animation_tree_path; } void set_physical_bone_simulator(PhysicalBoneSimulator3D *pbs) { this->physical_bone_simulator = pbs; }
void set_animation_tree(AnimationTree *at) { this->animation_tree = at; }
private: private:
AnimationTree *get_animation_tree() const { return this->animation_tree; } void _activate_ragdoll();
NodePath animation_tree_path; Skeleton3D *skeleton = nullptr;
PhysicalBoneSimulator3D *physical_bone_simulator = nullptr;
AnimationTree *animation_tree = nullptr; AnimationTree *animation_tree = nullptr;
std::vector<MeshInstance3D*> meshes;
// Godot boilerplate below // Godot boilerplate below
protected: protected:
static void _bind_methods() static void _bind_methods()
@@ -44,10 +60,16 @@ protected:
ADD_SIGNAL(MethodInfo("fell_out_of_world")); ADD_SIGNAL(MethodInfo("fell_out_of_world"));
ClassDB::bind_method(D_METHOD("get_animation_tree_path"), &Character3D::get_animation_tree_path); ClassDB::bind_method(D_METHOD("get_skeleton"), &Character3D::get_skeleton);
ClassDB::bind_method(D_METHOD("set_animation_tree_path", "animation_tree"), &Character3D::set_animation_tree_path); ClassDB::bind_method(D_METHOD("set_skeleton", "skeleton"), &Character3D::set_skeleton);
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "animation_tree", PROPERTY_HINT_NODE_TYPE, "AnimationTree"), "set_animation_tree_path", "get_animation_tree_path"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "skeleton", PROPERTY_HINT_NODE_TYPE, "Skeleton3D"), "set_skeleton", "get_skeleton");
ClassDB::bind_method(D_METHOD("get_physical_bone_simulator"), &Character3D::get_physical_bone_simulator);
ClassDB::bind_method(D_METHOD("set_physical_bone_simulator", "physical_bone_simulator"), &Character3D::set_physical_bone_simulator);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "physical_bone_simulator", PROPERTY_HINT_NODE_TYPE, "PhysicalBoneSimulator3D"), "set_physical_bone_simulator", "get_physical_bone_simulator");
ClassDB::bind_method(D_METHOD("get_animation_tree"), &Character3D::get_animation_tree); ClassDB::bind_method(D_METHOD("get_animation_tree"), &Character3D::get_animation_tree);
ClassDB::bind_method(D_METHOD("set_animation_tree", "animation_tree"), &Character3D::set_animation_tree);
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "animation_tree", PROPERTY_HINT_NODE_TYPE, "AnimationTree"), "set_animation_tree", "get_animation_tree");
} }
}; };
@@ -0,0 +1,97 @@
/*
* ©2023 Batty Bovine Productions, LLC. All Rights Reserved.
*
* If this source code makes it to the public internet, this software can be
* considered to be protected by the MIT licence. Have fun with it.
*/
#include "hurtbox_base.h"
#include "orng_macros.h"
#include "character_3d.h"
#include <godot_cpp/classes/physical_bone3d.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#include <godot_cpp/classes/scene_tree_timer.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
void HurtboxBase::_ready()
{
this->set_process(false);
this->remaining_life = this->lifetime;
CALL_NEXT_FRAME(callable_mp(this, &HurtboxBase::_send_next_in_queue));
}
void HurtboxBase::_physics_process(double delta)
{
if (this->remaining_life <= 0.0f)
{
this->collision_shape->set_disabled(true);
}
TypedArray<Character3D> characters_hit_this_frame;
const TypedArray<Node3D> &bodies = this->get_overlapping_bodies();
const uint32_t num_bodies = bodies.size();
for (uint32_t i = 0; i < num_bodies; i++)
{
if (Character3D *body = cast_to<Character3D>(bodies[i]))
{
if (!this->hit_character_bodies.has(body))
{
characters_hit_this_frame.append(body);
TypedArray<int32_t> bone_hits;
for (uint32_t bone_index = 0; bone_index < num_bodies; bone_index++)
{
if (PhysicalBone3D *bone = cast_to<PhysicalBone3D>(bodies[bone_index]))
{
if (String(bone->get_path()).begins_with(String(body->get_path())))
{
bone_hits.append(bone->get_bone_id());
PRINT_LOG(HURTBOX_BASE_TAG, "Bone hit: ", bone->get_name(), " (", bone->get_class(), " : ", bone->get_rid(), ")");
}
}
}
PRINT_LOG(HURTBOX_BASE_TAG, "Character hit: ", body->get_name(), " (", body->get_class(), " : ", body->get_rid(), ")");
Dictionary hit_data;
hit_data["body"] = body;
hit_data["bone_hits"] = bone_hits;
hit_data["hit_points"] = this->hit_points;
hit_data["hit_transform"] = this->get_global_transform();
this->hit_data_queue.emplace_back(hit_data);
}
}
}
this->hit_character_bodies.append_array(characters_hit_this_frame);
this->remaining_life -= delta;
}
void HurtboxBase::_send_next_in_queue()
{
if (!this->hit_data_queue.empty())
{
const Dictionary &hit_data = this->hit_data_queue.front();
Character3D *body = cast_to<Character3D>(hit_data["body"]);
body->take_damage(hit_data);
this->hit_data_queue.erase(this->hit_data_queue.begin());
}
if (this->remaining_life > 0.0f)
{
CALL_NEXT_FRAME(callable_mp(this, &HurtboxBase::_send_next_in_queue));
}
else if (this->hit_data_queue.empty())
{
this->queue_free();
}
}
+65
View File
@@ -0,0 +1,65 @@
/*
* ©2023 Batty Bovine Productions, LLC. All Rights Reserved.
*
* If this source code makes it to the public internet, this software can be
* considered to be protected by the MIT licence. Have fun with it.
*/
#pragma once
#include "orng_macros.h"
#include "character_3d.h"
#include <godot_cpp/classes/collision_shape3d.hpp>
#include <godot_cpp/classes/area3d.hpp>
using namespace godot;
#define HURTBOX_BASE_TAG "HurtboxBase"
class HurtboxBase : public Area3D
{
GDCLASS(HurtboxBase, Area3D);
public:
virtual void _ready() override;
virtual void _physics_process(double delta) override;
void set_collision_shape(CollisionShape3D *cs) { this->collision_shape = cs; }
CollisionShape3D *get_collision_shape() const { return this->collision_shape; }
void set_lifetime(const float lt) { this->lifetime = lt; }
float get_lifetime() const { return this->lifetime; }
void set_hit_points(const int32_t points) { this->hit_points = points; }
int32_t get_hit_points() const { return this->hit_points; }
protected:
std::vector<Dictionary> hit_data_queue;
private:
Dictionary _create_hit_data_struct() const;
void _send_next_in_queue();
CollisionShape3D *collision_shape = nullptr;
float lifetime = 0.05f;
float remaining_life = 0.0f;
int32_t hit_points = 0;
TypedArray<Character3D> hit_character_bodies;
// Godot boilerplate below
protected:
static void _bind_methods()
{
ADD_GETTER_SETTER_HINTED(HurtboxBase, collision_shape, Variant::OBJECT, PROPERTY_HINT_NODE_TYPE, "CollisionShape3D");
ADD_GETTER_SETTER(HurtboxBase, lifetime, Variant::FLOAT);
ADD_GETTER_SETTER(HurtboxBase, hit_points, Variant::INT);
ClassDB::bind_method(D_METHOD("_send_next_in_queue"), &HurtboxBase::_send_next_in_queue);
}
};
+4 -4
View File
@@ -24,7 +24,7 @@ MeshEditingLibrary::~MeshEditingLibrary()
{ {
} }
PackedVector3Array MeshEditingLibrary::slice_mesh(const MeshInstance3D *original_mesh_instance, const Skeleton3D *original_skeleton, const Plane &local_plane, ArrayMesh *out_first_half, ArrayMesh *out_other_half, const MeshEditCapUV cap_option, const Ref<StandardMaterial3D> cap_material) PackedVector3Array MeshEditingLibrary::slice_mesh(const MeshInstance3D *original_mesh_instance, const Skeleton3D *original_skeleton, const Plane &local_plane, Ref<ArrayMesh> out_first_half, Ref<ArrayMesh> out_other_half, const MeshEditCapUV cap_option, const Ref<StandardMaterial3D> cap_material)
{ {
const Time *time = Time::get_singleton(); const Time *time = Time::get_singleton();
@@ -535,6 +535,9 @@ PackedVector3Array MeshEditingLibrary::slice_mesh(const MeshInstance3D *original
polygon_centroid += position; polygon_centroid += position;
} }
polygon_centroid /= polygon_set[polygon_index].vertices.size();
impact_points.append(polygon_centroid);
earcut_polygon.push_back(sub_polygon); earcut_polygon.push_back(sub_polygon);
std::vector<uint32_t> indices = mapbox::earcut<uint32_t>(earcut_polygon); std::vector<uint32_t> indices = mapbox::earcut<uint32_t>(earcut_polygon);
for (uint32_t i = 0; (i+2) < indices.size(); i += 3) for (uint32_t i = 0; (i+2) < indices.size(); i += 3)
@@ -543,9 +546,6 @@ PackedVector3Array MeshEditingLibrary::slice_mesh(const MeshInstance3D *original
cap_section_indices.append(indices[i] + polygon_vertex_base); cap_section_indices.append(indices[i] + polygon_vertex_base);
cap_section_indices.append(indices[i+2] + polygon_vertex_base); cap_section_indices.append(indices[i+2] + polygon_vertex_base);
} }
polygon_centroid /= polygon_set[polygon_index].vertices.size();
impact_points.append(polygon_centroid);
} }
} }
+1 -1
View File
@@ -68,7 +68,7 @@ public:
CAP_UV_TILED CAP_UV_TILED
}; };
static PackedVector3Array slice_mesh(const MeshInstance3D *original_mesh_instance, const Skeleton3D *original_skeleton, const Plane &local_plane, ArrayMesh *out_first_half, ArrayMesh *out_other_half, const MeshEditCapUV cap_option, const Ref<StandardMaterial3D> cap_material); static PackedVector3Array slice_mesh(const MeshInstance3D *original_mesh_instance, const Skeleton3D *original_skeleton, const Plane &local_plane, Ref<ArrayMesh> out_first_half, Ref<ArrayMesh> out_other_half, const MeshEditCapUV cap_option, const Ref<StandardMaterial3D> cap_material);
protected: protected:
enum MeshSlicePlaneOrientation enum MeshSlicePlaneOrientation
+3 -3
View File
@@ -52,9 +52,9 @@
* Logging helpers * Logging helpers
*/ */
#ifdef DEBUG_ENABLED #ifdef DEBUG_ENABLED
#define PRINT_LOG(tag, ...) UtilityFunctions::print("[" tag "] " __VA_ARGS__); #define PRINT_LOG(tag, ...) UtilityFunctions::print("[" tag "] ", __VA_ARGS__)
#define PRINT_WARNING(tag, ...) UtilityFunctions::push_warning("[" tag "] " __VA_ARGS__); #define PRINT_WARNING(tag, ...) UtilityFunctions::push_warning("[" tag "] ", __VA_ARGS__)
#define PRINT_ERROR(tag, ...) UtilityFunctions::printerr("[" tag "] " __VA_ARGS__); #define PRINT_ERROR(tag, ...) UtilityFunctions::printerr("[" tag "] ", __VA_ARGS__)
#else #else
#define PRINT_LOG(tag, ...) #define PRINT_LOG(tag, ...)
#define PRINT_WARNING(tag, ...) #define PRINT_WARNING(tag, ...)
+4
View File
@@ -23,6 +23,8 @@
#include "nodes/sublevel_scene.h" #include "nodes/sublevel_scene.h"
#include "nodes/character_nodes/character_3d.h" #include "nodes/character_nodes/character_3d.h"
#include "nodes/hurtbox/hurtbox_base.h"
#include "resources/level_metadata_map.h" #include "resources/level_metadata_map.h"
#include "resources/level_metadata_resource.h" #include "resources/level_metadata_resource.h"
@@ -69,6 +71,8 @@ void initialize_orng_module(ModuleInitializationLevel p_level) {
ClassDB::register_class<PlayerOrigin>(); ClassDB::register_class<PlayerOrigin>();
ClassDB::register_class<PlayerSpawn>(); ClassDB::register_class<PlayerSpawn>();
ClassDB::register_class<HurtboxBase>();
ClassDB::register_class<SaveFileData>(); ClassDB::register_class<SaveFileData>();
ClassDB::register_class<MeshEditingLibrary>(); ClassDB::register_class<MeshEditingLibrary>();