#include "vehicle.hpp" #include "assets/cache.hpp" #include "net/utils.hpp" #include "player.hpp" #include "player_input.hpp" #include "utils/random.hpp" #include "utils/math.hpp" #include static std::shared_ptr LoadVehicleModelByName(const std::string& model_name) { return assets::CacheManager::GetVehicleModel("data/" + model_name + ".veh"); } game::Vehicle::Vehicle(World& world, const VehicleSpawnInfo& info) : Entity(world, net::ET_VEHICLE), tuning_(info.tuning), model_(LoadVehicleModelByName(info.tuning.model)), tuninglist_(VehicleTuningList::LoadFromFile("data/" + info.tuning.model + ".tun")) { root_.local.position = info.position; root_.local.rotation = glm::angleAxis(info.yaw, glm::vec3(0.0f, 0.0f, 1.0f)); wheels_.resize(model_->GetWheels().size()); ApplyTuning(info.tuning); // init deform gfx::DeformGridInfo deform_info{}; deform_info.min = glm::vec3(-1.0f, -2.5f, 0.10f); deform_info.max = glm::vec3(1.0f, 2.0f, 1.8f); deform_info.res = glm::ivec3(8, 16, 8); deform_info.max_offset = 0.1f; deformgrid_ = std::make_unique(deform_info); Update(); } void game::Vehicle::Update() { Super::Update(); if (physics_) { physics_->Update(); } root_.UpdateMatrix(); flags_ = 0; UpdateCrash(); ProcessInput(); UpdateWheels(); UpdateLights(); sync_current_ = 1 - sync_current_; UpdateSyncState(); SendUpdateMsg(); } void game::Vehicle::SendInitData(Player& player, net::OutMessage& msg) const { Super::SendInitData(player, msg); msg.Write(net::ModelName(tuning_.model)); WriteTuning(msg); // write state against default static const VehicleSyncState default_state; size_t fields_pos = msg.Reserve(); auto fields = WriteState(msg, default_state); msg.WriteAt(fields_pos, fields); WriteDeformSync(msg); } void game::Vehicle::OnContact(const collision::ContactInfo& info) { Super::OnContact(info); crash_intensity_ += info.impulse; if (info.impulse < 1000.0f) return; ApplyDamage(info.impulse * 0.01f); Deform(info.pos, -glm::normalize(info.normal) * 0.1f, 1.0f); } void game::Vehicle::ReceiveDamage(const DamageInfo& damage) { Super::ReceiveDamage(damage); if (!physics_) return; if (damage.type == DAMAGE_BULLET) { // TODO: adjust impulse auto impulse = damage.normal * -60.0f; auto& bt_body = physics_->GetBtBody(); bt_body.activate(); bt_body.applyImpulse(btVector3(impulse.x, impulse.y, impulse.z), btVector3(damage.impact_pos.x, damage.impact_pos.y, damage.impact_pos.z) - bt_body.getCenterOfMassPosition()); ApplyDamage(damage.damage * 0.2f); // Deform(damage.impact_pos, damage.normal * -0.1f, 1.0f); } } void game::Vehicle::SetInput(VehicleInputType type, bool enable) { if (enable) in_ |= (1 << type); else in_ &= ~(1 << type); } void game::Vehicle::SetPosition(const glm::vec3& pos) { auto& body = physics_->GetBtBody(); auto t = body.getWorldTransform(); t.setOrigin(btVector3(pos.x, pos.y, pos.z)); body.setWorldTransform(t); } float game::Vehicle::GetSpeed() const { return physics_->GetBtVehicle().getCurrentSpeedKmHour(); } void game::Vehicle::SetSteering(bool analog, float value) { steering_analog_ = analog; target_steering_ = value; } void game::Vehicle::SetTuning(const VehicleTuning& tuning) { ApplyTuning(tuning); // send to clients auto msg = BeginEntMsg(net::EMSG_TUNING); WriteTuning(msg); } void game::Vehicle::ProcessInput() { // TODO: totally fix // std::string nt = ""; if (in_) { // nt += "in "; // body_->setActivationState(ACTIVE_TAG); physics_->GetBtBody().activate(); } else { // nt += "no in"; } // nt += std::to_string(body_->getActivationState()); // SetNametag(nt); float t_delta = 1.0f / 25.0f; // float steeringIncrement = .04 * 60; // float steeringClamp = .5; // float maxEngineForce = 7000; float maxEngineForce = tuning_ctx_.engine_force; float maxBreakingForce = tuning_ctx_.braking_force; float speed = GetSpeed(); if (glm::abs(speed) > 200.0f) maxEngineForce = 100.0f; float engineForce = 0; float breakingForce = 0; float maxsc = .5f; float minsc = .08f; float sl = 130.f; float steeringClamp = std::max(minsc, (1.f - (std::abs(speed) / sl)) * maxsc); // steeringClamp = .5f; float steeringSpeed = steeringClamp * 5.0f; if (steering_analog_) steeringSpeed *= 3.0f; float steeringInc = steeringSpeed * t_delta; float steeringDec = steeringInc * 2.0f; const bool in_forward = in_ & (1 << VIN_FORWARD); const bool in_backward = in_ & (1 << VIN_BACKWARD); const bool in_left = in_ & (1 << VIN_LEFT); const bool in_right = in_ & (1 << VIN_RIGHT); bool active_braking = false; bool active_gas = false; if (in_forward) { if (speed < -1) { breakingForce = maxBreakingForce; active_braking = true; } else { engineForce = maxEngineForce; active_gas = true; } } if (in_backward) { if (speed > 1) { breakingForce = maxBreakingForce; active_braking = true; } else { engineForce = -maxEngineForce / 2; active_gas = true; } } // idle breaking if (!active_braking && !active_gas) { breakingForce = 20.0f; } if (!steering_analog_) { if (in_left) { if (steering_ < steeringClamp) steering_ += steeringInc; } else { if (in_right) { if (steering_ > -steeringClamp) steering_ -= steeringInc; } else { if (steering_ < -steeringInc) steering_ += steeringInc; else { if (steering_ > steeringInc) steering_ -= steeringInc; else { steering_ = 0.0f; } } } } } else { auto target_steering_clamped = glm::clamp(target_steering_, -steeringClamp, steeringClamp); MoveToward(steering_, target_steering_clamped, glm::abs(target_steering_clamped) < glm::abs(steering_) ? steeringInc : steeringDec); } auto& vehicle = physics_->GetBtVehicle(); for (size_t i = 0; i < wheels_.size(); ++i) { float engine_factor = tuning_ctx_.wheels[i].engine_factor; if (engine_factor > 0.0001f) { vehicle.applyEngineForce(engine_factor * engineForce, i); } float braking_factor = tuning_ctx_.wheels[i].braking_factor; if (braking_factor > 0.0001f) { vehicle.setBrake(braking_factor * breakingForce, i); } float steering_factor = tuning_ctx_.wheels[i].steering_factor; if (std::abs(steering_factor) > 0.0001f) { vehicle.setSteeringValue(steering_factor * steering_, i); } } if (active_gas) flags_ |= VF_ACCELERATING; if (active_braking) flags_ |= VF_BRAKING; if (active_gas && engineForce < 0.0f) flags_ |= VF_REVERSING; const bool can_roll = wheels_on_ground_ <= (wheels_.size() / 2); if (can_roll) ++can_roll_frames_; else can_roll_frames_ = 0; const bool roll_left = in_left || (steering_analog_ && target_steering_ < -0.1f); const bool roll_right = in_right || (steering_analog_ && target_steering_ > 0.1f); // check if airborne and apply roll if right/left pressed if (can_roll_frames_ >= 50 && (roll_left || roll_right)) { btVector3 ang_vel = physics_->GetBtBody().getAngularVelocity(); const float max_vel = 1.0f; btTransform trans = physics_->GetBtBody().getWorldTransform(); btQuaternion quat = trans.getRotation(); glm::quat rot_quat(quat.getW(), quat.getX(), quat.getY(), quat.getZ()); glm::vec3 local_up_world = rot_quat * glm::vec3(0.0f, 0.0f, 1.0f); float roll_factor = glm::clamp(1.0f - local_up_world.z, 0.0f, 1.0f); glm::vec3 local_roll(0.0f, 0.5f * roll_factor, 0.0f); glm::vec3 local_ang_vel = glm::inverse(rot_quat) * glm::vec3(ang_vel.x(), ang_vel.y(), ang_vel.z()); if (glm::abs(local_ang_vel.y) < max_vel) { glm::vec3 world_roll = rot_quat * local_roll; glm::vec3 new_ang_vel = glm::vec3(ang_vel.x(), ang_vel.y(), ang_vel.z()); if (roll_left) new_ang_vel -= world_roll; if (roll_right) new_ang_vel += world_roll; ang_vel = btVector3(new_ang_vel.x, new_ang_vel.y, new_ang_vel.z); physics_->GetBtBody().setAngularVelocity(ang_vel); } } } void game::Vehicle::UpdateCrash() { if (health_ <= 0.0f) flags_ |= VF_BROKENWINDOWS; if (no_crash_frames_) { --no_crash_frames_; } else { if (physics_) { auto bt_vel = physics_->GetBtBody().getLinearVelocity(); glm::vec3 velocity(bt_vel.x(), bt_vel.y(), bt_vel.z()); float diff = glm::distance(velocity, prev_velocity_); prev_velocity_ = velocity; if (glm::length2(velocity) > 9.0f) diff = 0.0f; float snd_crash_intensity = glm::max(diff * 500.0f, crash_intensity_); if (snd_crash_intensity > 1000.0f) { float volume = RandomFloat(0.9f, 1.2f); float pitch = RandomFloat(1.0f, 1.3f); if (snd_crash_intensity > 12000.0f) { volume *= 1.7f; pitch *= 0.8f; } if (snd_crash_intensity > 4000.0f) { volume *= 1.3f; pitch *= 0.8f; } else { volume *= 0.8f; pitch *= 1.2f; } PlaySound("crash", volume, pitch); no_crash_frames_ = 8 + rand() % 5; } } } crash_intensity_ = 0.0f; } void game::Vehicle::UpdateWheels() { auto& vehicle = physics_->GetBtVehicle(); wheels_on_ground_ = 0; for (size_t i = 0; i < wheels_.size(); ++i) { auto& bt_wheel = vehicle.getWheelInfo(i); wheels_[i].speed = -(bt_wheel.m_rotation - wheels_[i].rotation) * 25.0f; wheels_[i].rotation = bt_wheel.m_rotation; wheels_[i].z_offset = tuning_ctx_.wheels[i].z_offset - bt_wheel.m_raycastInfo.m_suspensionLength; if (bt_wheel.m_raycastInfo.m_isInContact) ++wheels_on_ground_; } } void game::Vehicle::UpdateLights() { if (lights_on_) flags_ |= VF_LIGHTS_ON; // TODO: orange lights } void game::Vehicle::UpdateSyncState() { VehicleSyncState& state = sync_[sync_current_]; state.flags = flags_; net::EncodePosition(root_.local.position, state.pos); net::EncodeRotation(root_.local.rotation, state.rot); state.steering.Encode(steering_); for (size_t i = 0; i < wheels_.size(); ++i) { auto& wheel = wheels_[i]; state.wheels[i].z_offset.Encode(wheel.z_offset); state.wheels[i].speed.Encode(wheel.speed); } } game::VehicleSyncFieldFlags game::Vehicle::WriteState(net::OutMessage& msg, const VehicleSyncState& base) const { VehicleSyncFieldFlags fields = 0; const VehicleSyncState& curr = sync_[sync_current_]; if (curr.flags != base.flags) { fields |= VSF_FLAGS; msg.Write(curr.flags); } if (curr.pos.x.value != base.pos.x.value || curr.pos.y.value != base.pos.y.value || curr.pos.z.value != base.pos.z.value) { fields |= VSF_POSITION; net::WriteDelta(msg, curr.pos.x, base.pos.x); net::WriteDelta(msg, curr.pos.y, base.pos.y); net::WriteDelta(msg, curr.pos.z, base.pos.z); } if (curr.rot.x.value != base.rot.x.value || curr.rot.y.value != base.rot.y.value || curr.rot.z.value != base.rot.z.value) { fields |= VSF_ROTATION; net::WriteDelta(msg, curr.rot.x, base.rot.x); net::WriteDelta(msg, curr.rot.y, base.rot.y); net::WriteDelta(msg, curr.rot.z, base.rot.z); } if (curr.steering.value != base.steering.value) { fields |= VSF_STEERING; net::WriteDelta(msg, curr.steering, base.steering); } bool wheels_changed = false; for (size_t i = 0; i < wheels_.size(); ++i) { if (curr.wheels[i].z_offset.value != base.wheels[i].z_offset.value || curr.wheels[i].speed.value != base.wheels[i].speed.value) { wheels_changed = true; break; } } if (wheels_changed) { fields |= VSF_WHEELS; for (size_t i = 0; i < wheels_.size(); ++i) { net::WriteDelta(msg, curr.wheels[i].z_offset, base.wheels[i].z_offset); net::WriteDelta(msg, curr.wheels[i].speed, base.wheels[i].speed); } } return fields; } void game::Vehicle::SendUpdateMsg() { auto msg = BeginUpdateMsg(); auto fields_pos = msg.Reserve(); auto fields = WriteState(msg, sync_[1 - sync_current_]); if (fields == 0) { DiscardUpdateMsg(); return; } msg.WriteAt(fields_pos, fields); } void game::Vehicle::ApplyDamage(float damage) { if (health_ <= 0.0f) return; health_ -= damage; if (health_ <= 0.0f) // just broken { PlaySound("breakwindow", 1.0f, 1.0f); health_ = 0.0f; } } void game::Vehicle::WriteDeformSync(net::OutMessage& msg) const { const auto texels = deformgrid_->GetData(); auto numtexels_pos = msg.Reserve(); net::NumTexels numtexels = 0; size_t last = 0; for (size_t i = 0; i < texels.size(); ++i) { if (texels[i] == glm::i8vec3(0)) continue; // unchanged, no write auto diff = i - last; msg.WriteVarInt(diff); for (size_t j = 0; j < 3; ++j) { msg.Write(texels[i][j]); } last = i; ++numtexels; } msg.WriteAt(numtexels_pos, numtexels); } void game::Vehicle::Deform(const glm::vec3& pos, const glm::vec3& deform, float radius) { if (health_ > 0.0f) return; net::PositionQ pos_q; net::PositionQ deform_q; net::EncodePosition(pos, pos_q); net::EncodePosition(deform, deform_q); SendDeformMsg(pos_q, deform_q); // defeorm locally glm::vec3 new_pos, new_deform; net::DecodePosition(pos_q, new_pos); net::DecodePosition(deform_q, new_deform); deformgrid_->ApplyImpulse(new_pos, new_deform, 0.3f); } void game::Vehicle::SendDeformMsg(const net::PositionQ& pos, const net::PositionQ& deform) { auto msg = BeginEntMsg(net::EMSG_DEFORM); net::WritePositionQ(msg, pos); net::WritePositionQ(msg, deform); } void game::Vehicle::ApplyTuning(const VehicleTuning& tuning) { tuning_ = tuning; tuning_ctx_ = VehicleTuningContext{}; // setup wheels const auto& model_wheels = model_->GetWheels(); tuning_ctx_.wheels.resize(model_wheels.size()); for (size_t i = 0; i < model_wheels.size(); ++i) { tuning_ctx_.wheels[i].front = !(model_wheels[i].type & assets::WHEEL_REAR); } // apply tunning to ctx for (const auto& func : tuninglist_->default_funcs) { func(tuning_ctx_); } for (const auto& group : tuninglist_->groups) { auto group_it = tuning_.parts.find(group.id); if (group_it == tuning_.parts.end()) continue; const auto& part_name = group_it->second; const auto& part = group.parts.at(part_name); for (const auto& func : part.funcs) { func(tuning_ctx_); } } if (tuning_ctx_.colors[2] == 0) // secondary <- primary { tuning_ctx_.colors[2] = tuning_ctx_.colors[0]; } health_ = tuning_ctx_.health; // (re)create physics physics_.reset(); physics_ = std::make_unique(world_, root_.local, *this, *model_, tuning_ctx_); OnPhysicsChanged(); } void game::Vehicle::WriteTuning(net::OutMessage& msg) const { // write colors for (const auto& color : tuning_ctx_.colors) { net::WriteRGB(msg, color); } // write wheel models for (const auto& wheel : tuning_ctx_.wheels) { msg.Write(net::ModelName(wheel.modelname)); } } // PHYSICS game::VehiclePhysics::VehiclePhysics(collision::DynamicsWorld& world, Transform& transform, collision::ObjectCallback& obj_cb, const assets::VehicleModel& model, const VehicleTuningContext& tuning) : world_(world), motion_(transform) { // setup chassis rigidbody btCollisionShape* shape = model.GetModel()->GetColShape(); if (!shape) throw std::runtime_error("Making vehicle with no shape"); btVector3 local_inertia(0, 0, 0); shape->calculateLocalInertia(tuning.mass, local_inertia); btRigidBody::btRigidBodyConstructionInfo rb_info(tuning.mass, &motion_, shape, local_inertia); body_ = std::make_unique(rb_info); // body_->setActivationState(DISABLE_DEACTIVATION); collision::SetObjectInfo(body_.get(), collision::OT_ENTITY, collision::OF_NOTIFY_CONTACT | collision::OF_DESTRUCTING, &obj_cb); // setup vehicle btRaycastVehicle::btVehicleTuning bt_tuning; vehicle_ = std::make_unique(bt_tuning, body_.get(), &world_.GetVehicleRaycaster()); vehicle_->setCoordinateSystem(0, 2, 1); // setup wheels // btVector3 wheelDirectionCS0(0, -1, 0); // btVector3 wheelAxleCS(-1, 0, 0); btVector3 wheelDirectionCS0(0, 0, -1); btVector3 wheelAxleCS(1, 0, 0); const auto& model_wheels = model.GetWheels(); size_t num_wheels = model_wheels.size(); for (size_t i = 0; i < num_wheels; ++i) { const auto& wheel_mdl = model_wheels[i]; const auto& wheel_tun = tuning.wheels[i]; // float wheelRadius = wheel_tun.radius; // float friction = wheel_tun.friction // float suspensionStiffness = 50.0f; // // float suspensionDamping = 2.3f; // // float suspensionCompression = 4.4f; // float suspensionRestLength = 0.3f; // float rollInfluence = 0.3f; // float maxSuspensionForce = 90000.0f; // float maxSuspensionTravelCm = 15.0f; float k = 0.2f; // if (!is_front) // friction *= 1.5f; btVector3 wheel_pos(wheel_mdl.position.x, wheel_mdl.position.y, wheel_mdl.position.z + wheel_tun.z_offset); auto& wi = vehicle_->addWheel(wheel_pos, wheelDirectionCS0, wheelAxleCS, wheel_tun.suspension_rest_length, wheel_tun.radius, bt_tuning, wheel_tun.front); wi.m_suspensionStiffness = wheel_tun.suspension_stiffness; wi.m_wheelsDampingCompression = k * 2.0 * btSqrt(wi.m_suspensionStiffness); // vehicleTuning.suspensionCompression; wi.m_wheelsDampingRelaxation = k * 3.3 * btSqrt(wi.m_suspensionStiffness); // vehicleTuning.suspensionDamping; wi.m_frictionSlip = wheel_tun.friction; wi.m_rollInfluence = wheel_tun.roll_influence; wi.m_maxSuspensionForce = wheel_tun.suspension_max_force; wi.m_maxSuspensionTravelCm = wheel_tun.suspension_travel * 100.0f; } auto& bt_world = world_.GetBtWorld(); bt_world.addRigidBody(body_.get(), collision::OG_DEFAULT, ~collision::OG_PROJECTILE); bt_world.addAction(vehicle_.get()); // make bullet hitbox auto col_mesh = model.GetModel()->GetColMesh(); if (col_mesh) { bullet_hitbox_ = std::make_unique(); bullet_hitbox_->setCollisionShape(col_mesh->GetShape()); collision::SetObjectInfo(bullet_hitbox_.get(), collision::OT_ENTITY, 0, &obj_cb); bt_world.addCollisionObject(bullet_hitbox_.get(), collision::OG_DEFAULT, collision::OG_PROJECTILE); UpdateBulletHitboxTransform(); } } void game::VehiclePhysics::Update() { UpdateBulletHitboxTransform(); } game::VehiclePhysics::~VehiclePhysics() { auto& bt_world = world_.GetBtWorld(); bt_world.removeRigidBody(body_.get()); bt_world.removeAction(vehicle_.get()); if (bullet_hitbox_) { bt_world.removeCollisionObject(bullet_hitbox_.get()); } } void game::VehiclePhysics::UpdateBulletHitboxTransform() { if (!bullet_hitbox_) return; bullet_hitbox_->setWorldTransform(body_->getWorldTransform()); }