This commit is contained in:
tovjemam 2026-06-13 20:38:32 +02:00
parent 5062ef5cf0
commit 1a80c19355
13 changed files with 391 additions and 79 deletions

View File

@ -38,6 +38,31 @@ std::shared_ptr<const assets::Skeleton> assets::Skeleton::LoadFromFile(const std
Animation::LoadFromFile("data/" + anim_filename + ".anim", skeleton.get());
skeleton->AddAnimation(anim_name, anim);
}
else if (command == "hitbone")
{
auto& hitbone = skeleton->hit_bones_.emplace_back();
std::string shape_name, bone_name;
float sy, sz;
iss >> hitbone.name >> bone_name >> shape_name;
ParseTransform(iss, hitbone.offset);
iss >> sy >> sz;
int bone_idx = skeleton->GetBoneIndex(bone_name);
hitbone.bone_idx = bone_idx >= 0 ? bone_idx : 0;
glm::vec3 shape_size(hitbone.offset.scale, sy, sz);
hitbone.offset.scale = 1.0f;
if (shape_name == "capsule")
{
hitbone.col_shape = std::make_unique<btCapsuleShapeZ>(shape_size.x, shape_size.z); // TODO: check dimenmsions
}
else
{
throw std::runtime_error("Unknown hitbone shape: " + shape_name);
}
}
});
skeleton->AddAimBones();

View File

@ -31,6 +31,14 @@ struct AimBone
glm::vec3 pitch_axis;
};
struct HitBone
{
size_t bone_idx = 0;
std::string name;
Transform offset;
std::unique_ptr<btCollisionShape> col_shape;
};
class Skeleton
{
public:
@ -46,7 +54,8 @@ public:
const Animation* GetAnimation(AnimIdx idx) const;
const Animation* GetAnimation(const std::string& name) const;
const std::vector<AimBone> GetAimBones() const { return aim_bones_; }
const std::vector<AimBone>& GetAimBones() const { return aim_bones_; }
const std::vector<HitBone>& GetHitBones() const { return hit_bones_; }
private:
void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform);
@ -64,6 +73,7 @@ private:
std::map<std::string, AnimIdx> anim_idxs_;
std::vector<AimBone> aim_bones_;
std::vector<HitBone> hit_bones_;
};
} // namespace assets

View File

@ -11,6 +11,22 @@ namespace game
namespace collision
{
enum ObjectGroup : int
{
OG_DEFAULT = btBroadphaseProxy::DefaultFilter,
OG_STATIC = btBroadphaseProxy::StaticFilter,
OG_KINEMATIC = btBroadphaseProxy::KinematicFilter,
OG_DEBRIS = btBroadphaseProxy::DebrisFilter,
OG_SENSOR = btBroadphaseProxy::SensorTrigger,
OG_CHARACTER = btBroadphaseProxy::CharacterFilter,
OG_PROJECTILE = 64,
OG_HITBONES_PROXY = 128,
OG_ALL = -1,
};
enum ObjectType : int
{
OT_UNDEFINED,
@ -40,8 +56,11 @@ class ObjectCallback
public:
ObjectCallback() = default;
virtual void ActivateHitBones() {}
virtual void OnContact(const ContactInfo& info) {}
virtual void OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object) {}
virtual ~ObjectCallback() = default;
};

View File

@ -10,6 +10,7 @@ game::Character::Character(World& world, const CharacterTuning& tuning)
z_offset_ = tuning_.shape.height * 0.5f + tuning_.shape.radius - 0.05f;
sk_ = SkeletonInstance(assets::CacheManager::GetSkeleton("data/" + tuning.model_name + ".sk"), &root_);
SetupHitBones();
}
static bool Turn(float& angle, float target, float step)
@ -36,11 +37,15 @@ void game::Character::Update()
{
Super::Update();
pose_valid_ = false;
hitbones_valid_ = false;
SyncTransformFromController();
UpdateMovement();
UpdateAiming();
UpdateActionAnim();
root_.UpdateMatrix();
UpdateHitBones();
sync_current_ = 1 - sync_current_;
UpdateSyncState();
@ -72,6 +77,20 @@ void game::Character::SendInitData(Player& player, net::OutMessage& msg) const
msg.WriteAt(fields_pos, fields);
}
void game::Character::OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object)
{
std::string_view hit_name = "???";
auto it = hitbone_names_.find(hit_object);
if (it != hitbone_names_.end())
{
hit_name = it->second;
}
std::string text = "au! " + std::string(hit_name);
GetWorld().SendChat(text);
}
void game::Character::Attach(net::EntNum parentnum)
{
Super::Attach(parentnum);
@ -94,6 +113,7 @@ void game::Character::EnablePhysics(bool enable)
else if (!enable && controller_)
{
controller_.reset();
root_.local.rotation = glm::quat(); // reset rotation
}
}
@ -122,6 +142,24 @@ void game::Character::SetPosition(const glm::vec3& position)
SyncControllerTransform();
}
void game::Character::ActivateHitBones()
{
Super::ActivateHitBones();
EnableHitBones(true);
}
void game::Character::FinalizeFrame()
{
Super::FinalizeFrame();
pose_valid_ = false;
hitbones_valid_ = false;
}
game::Character::~Character()
{
DeleteHitBones();
}
void game::Character::SetIdleAnim(const std::string& anim_name)
{
animstate_.idle_anim_idx = GetAnim(anim_name);
@ -310,6 +348,8 @@ void game::Character::UpdateAiming()
if (movement_ == CMT_DISABLED)
{
auto target_yaw = glm::mod(yaw + glm::pi<float>(), glm::two_pi<float>()) - glm::pi<float>();
const float yaw_limit = glm::radians(120.0f);
target_yaw = glm::clamp(target_yaw, -yaw_limit, yaw_limit);
MoveToward(animstate_.yaw, target_yaw, delta);
}
else
@ -456,6 +496,133 @@ assets::AnimIdx game::Character::GetAnim(const std::string& name) const
return sk_.GetSkeleton()->GetAnimationIdx(name);
}
void game::Character::SetupHitBones()
{
const auto& sk_hitbones = sk_.GetSkeleton()->GetHitBones();
hitbones_.resize(sk_hitbones.size());
for (size_t i = 0; i < hitbones_.size(); ++i)
{
auto& hitbone = hitbones_[i];
auto& sk_hitbone = sk_hitbones[i];
// setup node
hitbone.node.parent = &sk_.GetBoneNode(sk_hitbone.bone_idx);
hitbone.node.local = sk_hitbone.offset;
// setup object
auto& col_obj = hitbone.col_obj;
col_obj.setCollisionShape(sk_hitbone.col_shape.get());
collision::SetObjectInfo(&col_obj, collision::OT_ENTITY, 0, this);
hitbone_names_[&col_obj] = sk_hitbone.name;
}
// setup proxy
static btSphereShape proxy_shape(1.5f);
hitbone_proxy_.setCollisionShape(&proxy_shape);
collision::SetObjectInfo(&hitbone_proxy_, collision::OT_ENTITY, 0, this);
GetWorld().GetBtWorld().addCollisionObject(&hitbone_proxy_, collision::OG_HITBONES_PROXY, collision::OG_PROJECTILE);
}
void game::Character::EnableHitBones(bool enable)
{
if (enable)
{
hitbones_timer_ = 2; // reset timer
}
if (enable == hitbones_active_)
return;
hitbones_active_ = enable;
hitbones_valid_ = false;
auto& bt_world = GetWorld().GetBtWorld();
if (enable)
{
UpdateHitBoneTransforms(); // update transforms first
for (auto& hitbone : hitbones_)
{
bt_world.addCollisionObject(&hitbone.col_obj, collision::OG_DEFAULT, collision::OG_PROJECTILE);
}
}
else
{
for (auto& hitbone : hitbones_)
{
bt_world.removeCollisionObject(&hitbone.col_obj);
}
}
}
void game::Character::UpdateHitBones()
{
// update proxy transform
glm::vec3 center = GetRoot().matrix * glm::vec4(0.0f, 0.0f, 1.0f, 1.0f);
btTransform trans;
trans.setIdentity();
trans.setOrigin(btVector3(center.x, center.y, center.z));
hitbone_proxy_.setWorldTransform(trans);
if (hitbones_active_)
{
if (hitbones_timer_ > 0)
--hitbones_timer_;
else
EnableHitBones(false);
}
UpdateHitBoneTransforms();
}
static btTransform BtTransformFromMat4(const glm::mat4& m)
{
btMatrix3x3 basis(
m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2]
);
btVector3 origin(
m[3][0],
m[3][1],
m[3][2]
);
btTransform trans;
trans.setBasis(basis);
trans.setOrigin(origin);
return trans;
}
void game::Character::UpdateHitBoneTransforms()
{
if (hitbones_valid_ || !hitbones_active_)
return;
UpdatePose();
for (auto& hitbone : hitbones_)
{
hitbone.node.UpdateMatrix();
hitbone.col_obj.setWorldTransform(BtTransformFromMat4(hitbone.node.matrix));
// debug boxes
// GetWorld().BeamBox(hitbone.node.GetGlobalPosition() - 0.05f, hitbone.node.GetGlobalPosition() + 0.05f, 0xFFFF00,
// 1.5f / 25.0f);
}
}
void game::Character::DeleteHitBones()
{
EnableHitBones(false);
GetWorld().GetBtWorld().removeCollisionObject(&hitbone_proxy_);
}
void game::Character::UpdateActionAnim()
{
if (action_anim_done_)
@ -481,6 +648,17 @@ void game::Character::UpdateActionAnim()
}
}
void game::Character::UpdatePose()
{
if (pose_valid_)
return;
animstate_.ApplyToSkeleton(sk_);
sk_.UpdateBoneMatrices();
pose_valid_ = true;
}
game::CharacterPhysicsController::CharacterPhysicsController(Character& character, btDynamicsWorld& bt_world, btCapsuleShapeZ& bt_shape)
: character_(character), bt_world_(bt_world), bt_character_(&bt_ghost_, &bt_shape, 0.3f, btVector3(0, 0, 1))
{

View File

@ -52,6 +52,12 @@ enum CharacterMovementType
CMT_DIRECTIONAL,
};
struct CharacterHitBoneInstance
{
btCollisionObject col_obj;
TransformNode node;
};
class Character : public Entity
{
public:
@ -62,6 +68,8 @@ public:
virtual void Update() override;
virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
virtual void OnBulletHit(const game::BulletInfo& bullet, const btCollisionObject* hit_object);
virtual void Attach(net::EntNum parentnum) override;
const CharacterTuning& GetTuning() const { return tuning_; }
@ -86,7 +94,10 @@ public:
void SetPosition(const glm::vec3& position);
~Character() override = default;
virtual void ActivateHitBones() override;
virtual void FinalizeFrame() override;
~Character() override;
protected:
void SetIdleAnim(const std::string& anim_name);
@ -114,8 +125,17 @@ private:
assets::AnimIdx GetAnim(const std::string& name) const;
void SetupHitBones();
void EnableHitBones(bool enable);
void UpdateHitBones();
void UpdateHitBoneTransforms();
void DeleteHitBones();
void UpdateActionAnim();
void UpdatePose();
protected:
float turn_speed_ = 8.0f;
float walk_speed_ = 2.0f;
@ -156,6 +176,15 @@ private:
glm::vec3 aim_dir_ = glm::vec3(0.0f);
std::string item_;
bool pose_valid_ = false;
std::vector<CharacterHitBoneInstance> hitbones_;
btCollisionObject hitbone_proxy_;
bool hitbones_active_ = false;
size_t hitbones_timer_ = 0;
bool hitbones_valid_ = false;
std::map<const btCollisionObject*, std::string_view> hitbone_names_;
};
} // namespace game

View File

@ -34,7 +34,7 @@ public:
std::span<const char> GetUpdateMsg() const { return update_msg_buf_; }
void FinalizeFrame();
virtual void FinalizeFrame();
void SetNametag(const std::string& nametag);

View File

@ -91,6 +91,11 @@ game::OpenWorld::OpenWorld(Game& game) : EnterableWorld("openworld"), game_(game
// cow
auto& cow = Spawn<Cow>(glm::vec3(0.0f, 0.0f, 2.0f), 0.0f);
cow.SetNametag("no ty krávo");
// hit target npc
auto& npc = SpawnRandomNpc();
npc.SetPosition({90.0f, 100.0f, 5.0f});
npc.EnablePhysics(true);
}
void game::OpenWorld::Update(int64_t delta_time)
@ -159,6 +164,15 @@ game::DrivableVehicle& game::OpenWorld::SpawnRandomVehicle()
return vehicle;
}
game::NpcCharacter& game::OpenWorld::SpawnRandomNpc()
{
HumanCharacterTuning npc_tuning;
npc_tuning.clothes.push_back({ "tshirt", GetRandomColor24() });
npc_tuning.clothes.push_back({ "shorts", GetRandomColor24() });
return Spawn<NpcCharacter>(npc_tuning);
}
void game::OpenWorld::SpawnBot()
{
auto roads = GetMap().GetGraph("roads");
@ -173,11 +187,7 @@ void game::OpenWorld::SpawnBot()
auto& vehicle = SpawnRandomVehicle();
vehicle.SetPosition(roads->nodes[start_node].position + glm::vec3{0.0f, 0.0f, 5.0f});
HumanCharacterTuning npc_tuning;
npc_tuning.clothes.push_back({ "tshirt", GetRandomColor24() });
npc_tuning.clothes.push_back({ "shorts", GetRandomColor24() });
auto& driver = Spawn<NpcCharacter>(npc_tuning);
auto& driver = SpawnRandomNpc();
driver.Ride(&vehicle, 0);
}

View File

@ -20,6 +20,7 @@ public:
private:
game::DrivableVehicle& SpawnRandomVehicle();
game::NpcCharacter& SpawnRandomNpc();
void SpawnBot();
void CreateTuningGarage(const glm::vec3& position, float yaw);

View File

@ -100,23 +100,15 @@ void game::PlayerCharacter::UpdateAimTarget()
auto target = eye + forward * 1000.0f;
btVector3 bt_from(eye.x, eye.y, eye.z);
btVector3 bt_to(target.x, target.y, target.z);
btCollisionWorld::ClosestRayResultCallback cb(bt_from, bt_to);
cb.m_collisionFilterGroup = btBroadphaseProxy::DefaultFilter;
cb.m_collisionFilterMask = btBroadphaseProxy::StaticFilter;
GetWorld().GetBtWorld().rayTest(bt_from, bt_to, cb);
if (cb.hasHit())
if (GetAiming()) // save perf if not aiming
{
target = glm::vec3(cb.m_hitPointWorld.x(), cb.m_hitPointWorld.y(), cb.m_hitPointWorld.z());
GetWorld().TraceBullet(eye, target, this, target); // update target if hit
}
SetAimTarget(target);
// GetWorld().Beam(eye, target, 0xFFFF00, 1.0 / 25.0f);
GetWorld().BeamBox(target - 0.05f, target + 0.05f, 0xFFFF00, 1.5f / 25.0f);
}
void game::PlayerCharacter::UpdateUseTarget()

View File

@ -709,7 +709,7 @@ game::VehiclePhysics::VehiclePhysics(collision::DynamicsWorld& world, Transform&
}
auto& bt_world = world_.GetBtWorld();
bt_world.addRigidBody(body_.get(), btBroadphaseProxy::DefaultFilter, btBroadphaseProxy::AllFilter);
bt_world.addRigidBody(body_.get(), collision::OG_DEFAULT, ~collision::OG_PROJECTILE);
bt_world.addAction(vehicle_.get());
}

View File

@ -173,72 +173,23 @@ const game::UseTarget* game::World::GetBestUseTarget(game::PlayerCharacter& char
return cb.best_target;
}
static bool IsMeOrMyRideOrOtherPassengerOfMyRide(const game::HumanCharacter* me, const btCollisionObject* obj)
bool game::World::TraceBullet(const glm::vec3& start, const glm::vec3& end, game::HumanCharacter* shooter,
glm::vec3& out_hit_pos)
{
if (!me) // i am not
return false;
// is me?
auto obj_cb = collision::GetObjectCallback(obj);
if (!obj_cb)
return false; // is nothing
if (obj_cb == me)
return true; // its me
auto my_ride = me->GetRideable();
if (!my_ride)
return false; // im not riding anything
// is my ride?
if (&my_ride->GetEntity() == obj_cb)
return true; // yes
// is other passenger?
auto character = dynamic_cast<game::HumanCharacter*>(obj_cb);
if (!character)
return false; // is not even human
return character->GetRideable() == my_ride;
return TraceBulletInternal(start, end, shooter, out_hit_pos) != nullptr;
}
struct NotMeNotMyRideAndNotOtherPassengersOfMyRideClosestRayResultCallback : public btCollisionWorld::ClosestRayResultCallback
{
using Super = ClosestRayResultCallback;
NotMeNotMyRideAndNotOtherPassengersOfMyRideClosestRayResultCallback(const btVector3& rayFromWorld,
const btVector3& rayToWorld)
: ClosestRayResultCallback(rayFromWorld, rayToWorld)
{
}
game::HumanCharacter* me = nullptr;
virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override
{
if (IsMeOrMyRideOrOtherPassengerOfMyRide(me, rayResult.m_collisionObject))
return rayResult.m_hitFraction;
return Super::addSingleResult(rayResult, normalInWorldSpace);
}
};
void game::World::FireBullet(const BulletInfo& bullet)
{
btVector3 bt_start(bullet.start.x, bullet.start.y, bullet.start.z);
btVector3 bt_end(bullet.end.x, bullet.end.y, bullet.end.z);
NotMeNotMyRideAndNotOtherPassengersOfMyRideClosestRayResultCallback cb(bt_start, bt_end);
cb.me = bullet.shooter;
GetBtWorld().rayTest(bt_start, bt_end, cb);
if (!cb.hasHit() || !cb.m_collisionObject)
glm::vec3 hit_pos;
auto hit_obj = TraceBulletInternal(bullet.start, bullet.end, bullet.shooter, hit_pos);
if (!hit_obj)
return;
auto obj_cb = collision::GetObjectCallback(cb.m_collisionObject);
obj_cb->OnBulletHit(bullet, cb.m_collisionObject);
auto obj_cb = collision::GetObjectCallback(hit_obj);
obj_cb->OnBulletHit(bullet, hit_obj);
glm::vec3 hit_pos(cb.m_hitPointWorld.x(), cb.m_hitPointWorld.y(), cb.m_hitPointWorld.z());
// TODO: remove
const float box_extent = 0.1f;
BeamBox(hit_pos - box_extent, hit_pos + box_extent, 0x0077FF, 1.0f);
}
@ -279,6 +230,12 @@ void game::World::BeamBox(const glm::vec3& min, const glm::vec3& max, uint32_t c
Beam(p3, p7, color, time);
}
void game::World::SendChat(const std::string& text)
{
auto msg = BeginMsg(net::MSG_CHAT);
msg.Write(net::ChatMessage(text));
}
void game::World::HandleContacts()
{
auto& bt_world = GetBtWorld();
@ -377,3 +334,88 @@ void game::World::SendObjRespawnedMsg(net::ObjNum objnum)
auto msg = BeginMsg(net::MSG_OBJRESPAWN);
msg.Write(objnum);
}
static bool IsMeOrMyRideOrOtherPassengerOfMyRide(const game::HumanCharacter* me, const btCollisionObject* obj)
{
if (!me) // i am not
return false;
// is me?
auto obj_cb = collision::GetObjectCallback(obj);
if (!obj_cb)
return false; // is nothing
if (obj_cb == me)
return true; // its me
auto my_ride = me->GetRideable();
if (!my_ride)
return false; // im not riding anything
// is my ride?
if (&my_ride->GetEntity() == obj_cb)
return true; // yes
// is other passenger?
auto character = dynamic_cast<game::HumanCharacter*>(obj_cb);
if (!character)
return false; // is not even human
return character->GetRideable() == my_ride;
}
struct NotMeNotMyRideAndNotOtherPassengersOfMyRideClosestRayResultCallback : public btCollisionWorld::ClosestRayResultCallback
{
using Super = ClosestRayResultCallback;
NotMeNotMyRideAndNotOtherPassengersOfMyRideClosestRayResultCallback(const btVector3& rayFromWorld,
const btVector3& rayToWorld)
: ClosestRayResultCallback(rayFromWorld, rayToWorld)
{
}
game::HumanCharacter* me = nullptr;
virtual btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override
{
if (IsMeOrMyRideOrOtherPassengerOfMyRide(me, rayResult.m_collisionObject))
return rayResult.m_hitFraction;
return Super::addSingleResult(rayResult, normalInWorldSpace);
}
};
const btCollisionObject* game::World::TraceBulletInternal(const glm::vec3& start, const glm::vec3& end,
game::HumanCharacter* shooter, glm::vec3& out_hit_pos)
{
btVector3 bt_start(start.x, start.y, start.z);
btVector3 bt_end(end.x, end.y, end.z);
// find hitbone targets first
btCollisionWorld::AllHitsRayResultCallback hitbone_cb(bt_start, bt_end);
hitbone_cb.m_collisionFilterGroup = collision::OG_PROJECTILE;
hitbone_cb.m_collisionFilterMask = collision::OG_HITBONES_PROXY;
GetBtWorld().rayTest(bt_start, bt_end, hitbone_cb);
for (size_t i = 0; i < hitbone_cb.m_collisionObjects.size(); ++i)
{
auto col_obj = hitbone_cb.m_collisionObjects[i];
auto obj_cb = collision::GetObjectCallback(col_obj);
if (!obj_cb)
continue;
obj_cb->ActivateHitBones();
}
NotMeNotMyRideAndNotOtherPassengersOfMyRideClosestRayResultCallback cb(bt_start, bt_end);
cb.m_collisionFilterGroup = collision::OG_PROJECTILE;
cb.m_collisionFilterMask = ~collision::OG_HITBONES_PROXY;
cb.me = shooter;
GetBtWorld().rayTest(bt_start, bt_end, cb);
if (!cb.hasHit())
return nullptr;
out_hit_pos = glm::vec3(cb.m_hitPointWorld.x(), cb.m_hitPointWorld.y(), cb.m_hitPointWorld.z());
return cb.m_collisionObject;
}

View File

@ -62,11 +62,14 @@ public:
float GetDayTime() const { return daytime_; }
void SetDayTime(float daytime) { daytime_ = glm::mod(daytime, 24.0f); }
bool TraceBullet(const glm::vec3& start, const glm::vec3& end, game::HumanCharacter* shooter, glm::vec3& out_hit_pos);
void FireBullet(const BulletInfo& bullet);
void Beam(const glm::vec3& start, const glm::vec3& end, uint32_t color, float time);
void BeamBox(const glm::vec3& min, const glm::vec3& max, uint32_t color, float time);
void SendChat(const std::string& text);
virtual ~World() = default;
private:
@ -77,6 +80,9 @@ private:
void SendObjDestroyedMsg(net::ObjNum objnum);
void SendObjRespawnedMsg(net::ObjNum objnum);
const btCollisionObject* TraceBulletInternal(const glm::vec3& start, const glm::vec3& end,
game::HumanCharacter* shooter, glm::vec3& out_hit_pos);
private:
MapInstance map_;
std::set<net::ObjNum> destroyed_objs_;

View File

@ -143,7 +143,7 @@ using SoundPitchQ = Quantized<uint8_t, 0, 2>;
using AnimBlendQ = Quantized<uint8_t, 0, 1>;
using AnimTimeQ = Quantized<uint8_t, 0, 1>;
using AnimAimAngleQ = Quantized<uint8_t, -PI_N, PI_N, PI_D * 2>;
using AnimAimAngleQ = Quantized<uint16_t, -PI_N, PI_N, PI_D>;
using NumClothes = uint8_t;
using ClothesName = FixedStr<32>;