JSim 2026.06.01-p(1)
Loading...
Searching...
No Matches
ball_gamepiece_sim.hpp
Go to the documentation of this file.
1// Copyright (c) JSim contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the LGPLv3 license file in the root directory of this project.
4
5#pragma once
6
7#include <algorithm>
8#include <cmath>
9#include <cstddef>
10#include <functional>
11#include <limits>
12#include <mutex>
13#include <string>
14#include <utility>
15#include <vector>
16
21
22namespace frcsim {
23
39 public:
42 enum class GamePieceType {
44 kFuel2026, // Fuel gamepiece type
49 };
50
52 static constexpr std::size_t kNoBall = static_cast<std::size_t>(-1);
53
55 struct FieldConfig {
57 Vector3 min_corner_m{0.0, 0.0, 0.0};
59 Vector3 max_corner_m{16.54, 8.21, 3.0};
61 double wall_restitution{0.25};
69 double wall_friction{0.2};
70
78 double net_velocity_decay{0.2};
80 double net_spin_decay{0.8};
83
102 double snap_distance_m{0.1};
118 bool ccd_enabled{true};
133 double baumgarte_beta{0.2};
135 double baumgarte_slop_m{0.005};
136 };
137
140 struct RobotState {
146 double yaw_rad{0.0};
147
149 double radius_m{0.45};
150
152 double intake_radius_m{0.5};
153
154 };
155
181
184
199
223
249
251 struct BallEntity {
255 bool scored_in_net{false};
257 bool sleeping{false};
260 };
261
264 BallGamepieceSim() = default;
265
270 explicit BallGamepieceSim(const FieldConfig& field) : field_(field) {}
271
277 void setFieldConfig(const FieldConfig& field) {
278 std::lock_guard<std::mutex> lock(state_mutex_);
279 field_ = field;
280 }
281
284 std::function<void(std::size_t robot_index, const RobotState& robot)>;
285
291 std::size_t addRobot(const RobotState& robot) {
292 std::lock_guard<std::mutex> lock(state_mutex_);
293 robots_.push_back(robot);
294 const std::size_t robot_index = robots_.size() - 1;
295 if (robot_added_callback_) {
296 robot_added_callback_(robot_index, robots_[robot_index]);
297 }
298 return robot_index;
299 }
300
306 robot_added_callback_ = callback;
307 }
308
316 std::size_t addBall(const BallPhysicsSim3D::BallState& state,
317 const BallPhysicsSim3D::Config& config,
318 const BallPhysicsSim3D::BallProperties& properties) {
319 std::lock_guard<std::mutex> lock(state_mutex_);
320 BallEntity entity{};
321 entity.sim = BallPhysicsSim3D(config, properties);
322 entity.sim.setState(state);
323 balls_.push_back(entity);
324 ball_types_.push_back(GamePieceType::kBall);
325 return balls_.size() - 1;
326 }
327
333 std::size_t addProjectile(const ProjectileEntity& projectile) {
334 std::lock_guard<std::mutex> lock(state_mutex_);
335 projectiles_.push_back(projectile);
336 return projectiles_.size() - 1;
337 }
338
344 GoalZone& addGoalZone(const GoalZone& goal_zone) {
345 std::lock_guard<std::mutex> lock(state_mutex_);
346 goals_.push_back(goal_zone);
347 return goals_.back();
348 }
349
356 std::lock_guard<std::mutex> lock(state_mutex_);
357 for (auto& existing : gamepiece_types_) {
358 if (existing.type == info.type) {
359 existing = info;
360 return existing;
361 }
362 }
363 gamepiece_types_.push_back(info);
364 return gamepiece_types_.back();
365 }
366
369 std::lock_guard<std::mutex> lock(state_mutex_);
370 gamepiece_types_.clear();
371 }
372
380 const EnvironmentalBoundary& boundary) {
381 std::lock_guard<std::mutex> lock(state_mutex_);
382 field_elements_.push_back(boundary);
383 return field_elements_.back();
384 }
385
388 std::vector<RobotState>& robots() { return robots_; }
391 const std::vector<RobotState>& robots() const { return robots_; }
392
395 std::vector<BallEntity>& balls() { return balls_; }
398 const std::vector<BallEntity>& balls() const { return balls_; }
399
402 std::vector<BallEntity>& gamepieces() { return balls_; }
404 const std::vector<BallEntity>& gamepieces() const { return balls_; }
405
407 static constexpr std::size_t kNoGamepiece = kNoBall;
408
410 GamePieceType gamepieceType(std::size_t idx) const { return ballType(idx); }
412 std::string gamepieceTypeName(std::size_t idx) const { return ballTypeName(idx); }
414 bool setGamepieceType(std::size_t idx, GamePieceType t) { return setBallType(idx, t); }
416 bool setGamepieceType(std::size_t idx, const std::string& n) { return setBallType(idx, n); }
418 bool removeGamepiece(std::size_t idx) { return removeBall(idx); }
420 std::size_t countGamepieces() const { return countBalls(); }
421
427 GamePieceType ballType(std::size_t ball_index) const {
428 if (ball_index >= ball_types_.size()) {
430 }
431 return ball_types_[ball_index];
432 }
433
436 std::string ballTypeName(std::size_t ball_index) const {
437 if (ball_index >= ball_types_.size()) {
438 return std::string();
439 }
440 return std::string(gamePieceTypeName(ball_types_[ball_index]));
441 }
442
449 bool setBallType(std::size_t ball_index, GamePieceType type) {
450 std::lock_guard<std::mutex> lock(state_mutex_);
451 if (ball_index >= balls_.size() || ball_index >= ball_types_.size()) {
452 return false;
453 }
454 ball_types_[ball_index] = type;
455 return true;
456 }
457
464 bool setBallType(std::size_t ball_index, const std::string& type_name) {
465 GamePieceType parsed_type{};
466 if (!tryParseGamePieceType(type_name, parsed_type)) {
467 return false;
468 }
469 return setBallType(ball_index, parsed_type);
470 }
471
477 bool removeBall(std::size_t ball_index) {
478 std::lock_guard<std::mutex> lock(state_mutex_);
479 if (ball_index >= balls_.size() || ball_index >= ball_types_.size()) {
480 return false;
481 }
482
483 balls_.erase(balls_.begin() + static_cast<std::ptrdiff_t>(ball_index));
484 ball_types_.erase(ball_types_.begin() +
485 static_cast<std::ptrdiff_t>(ball_index));
486 return true;
487 }
488
491 std::vector<ProjectileEntity>& projectiles() { return projectiles_; }
494 const std::vector<ProjectileEntity>& projectiles() const {
495 return projectiles_;
496 }
497
499 std::vector<GoalZone>& goals() { return goals_; }
501 const std::vector<GoalZone>& goals() const { return goals_; }
502
505 std::vector<EnvironmentalBoundary>& fieldElements() {
506 return field_elements_;
507 }
508
510 const std::vector<EnvironmentalBoundary>& fieldElements() const {
511 return field_elements_;
512 }
513
516 std::size_t countBalls() const {
517 std::lock_guard<std::mutex> lock(state_mutex_);
518 return balls_.size();
519 }
520
523 std::size_t countProjectiles() const {
524 std::lock_guard<std::mutex> lock(state_mutex_);
525 std::size_t count = 0;
526 for (const auto& projectile : projectiles_) {
527 if (projectile.active) {
528 ++count;
529 }
530 }
531 return count;
532 }
533
536 std::size_t countScoredBalls() const {
537 std::lock_guard<std::mutex> lock(state_mutex_);
538 std::size_t count = 0;
539 for (const auto& ball : balls_) {
540 if (ball.scored_in_net) {
541 ++count;
542 }
543 }
544 return count;
545 }
546
556 std::size_t fireProjectile(
557 std::size_t robot_index, const ExitTrajectoryParameters& command,
558 bool spawn_on_ground_after_touch = true,
559 const std::function<void()>& hit_target_callback = {}) {
560 std::lock_guard<std::mutex> lock(state_mutex_);
561 if (robot_index >= robots_.size()) {
562 return kNoBall;
563 }
564
565 const RobotState& robot = robots_[robot_index];
566 const double launch_speed =
567 command.estimated_exit_velocity_mps > 0.0
569 : std::max(0.0, command.mechanism_speed_mps);
570
571 const double yaw_world = robot.yaw_rad + command.yaw_offset_rad;
572 const double cos_pitch = std::cos(command.pitch_rad);
573 const Vector3 direction_world(std::cos(yaw_world) * cos_pitch,
574 std::sin(yaw_world) * cos_pitch,
575 std::sin(command.pitch_rad));
576
577 ProjectileEntity projectile{};
578 projectile.type = command.gamepiece_type;
579 projectile.position_m =
580 robot.position_m + rotateYaw(command.launch_offset_m, robot.yaw_rad);
581 projectile.velocity_mps =
582 robot.velocity_mps + direction_world * launch_speed;
583 projectile.spin_radps = command.spin_radps;
584 projectile.gravity_mps2 =
585 std::max(0.0, -fallbackBallConfig().gravity_mps2.z);
586 projectile.spawn_on_ground_after_touch = spawn_on_ground_after_touch;
587 projectile.hit_target_callback = hit_target_callback;
588
589 projectiles_.push_back(projectile);
590 return projectiles_.size() - 1;
591 }
592
597 void step(double dt_s) {
598 if (dt_s <= 0.0) {
599 return;
600 }
601
602 std::lock_guard<std::mutex> lock(state_mutex_);
603
604 const int substeps = std::max(1, simulation_substeps_);
605 const double dt_substep_s = dt_s / static_cast<double>(substeps);
606
607 for (int i = 0; i < substeps; ++i) {
608 stepSingle(dt_substep_s);
609 }
610 }
611
617 void setSimulationSubsteps(int simulation_substeps) {
618 simulation_substeps_ = std::max(1, simulation_substeps);
619 }
620
623 int simulationSubsteps() const { return simulation_substeps_; }
624
625 private:
635 void stepSingle(double dt_s) {
636 if (dt_s <= 0.0) {
637 return;
638 }
639
640 integrateRobots(dt_s);
641 resolveRobotRobotImpedance();
642 updateProjectiles(dt_s);
643
644 for (std::size_t i = 0; i < balls_.size(); ++i) {
645 BallEntity& ball = balls_[i];
646
647 const Vector3 pre_step_position = ball.sim.state().position_m;
648 const bool skip_integration =
649 field_.sleeping_enabled && ball.sleeping;
650 if (!skip_integration) {
651 ball.sim.step(dt_s);
652 }
653
654 auto state = ball.sim.state();
655 const double spin_decay =
656 std::max(0.0, 1.0 - field_.free_ball_spin_decay_per_s * dt_s);
657 state.spin_radps *= spin_decay;
658 ball.sim.setState(state);
659
660 resolveContinuousCollision(ball, pre_step_position, dt_s);
661 resolveRobotBallContacts(ball);
662 resolveFieldElements(ball, dt_s);
663 resolveFieldBounds(ball);
664 }
665
666 resolveBallBallContacts(dt_s);
667 updateSleepStates();
668 }
669
678 void updateProjectiles(double dt_s) {
679 const double floor_z = fallbackBallConfig().ground_height_m;
680
681 for (auto& projectile : projectiles_) {
682 if (!projectile.active) {
683 continue;
684 }
685
686 projectile.age_s += dt_s;
687 projectile.velocity_mps.z -= projectile.gravity_mps2 * dt_s;
688 projectile.position_m += projectile.velocity_mps * dt_s;
689
690 if (checkProjectileGoalHit(projectile)) {
691 projectile.hit_target = true;
692 projectile.active = false;
693 if (projectile.hit_target_callback) {
694 projectile.hit_target_callback();
695 }
696 continue;
697 }
698
699 if (projectile.position_m.z <= floor_z) {
700 if (projectile.spawn_on_ground_after_touch) {
701 const GamePieceInfo* info = findGamePieceInfo(projectile.type);
702 const auto& physics_cfg =
703 (info != nullptr) ? info->physics_config : fallbackBallConfig();
704 const auto& physics_props = (info != nullptr)
705 ? info->ball_properties
706 : fallbackBallProperties();
707
708 BallPhysicsSim3D::BallState state{};
709 state.position_m = Vector3(
710 projectile.position_m.x, projectile.position_m.y,
711 std::max(physics_props.radius_m, physics_cfg.ground_height_m));
712 state.velocity_mps = projectile.velocity_mps;
713 state.spin_radps = projectile.spin_radps;
714 addBall(state, physics_cfg, physics_props);
715 if (!ball_types_.empty()) {
716 ball_types_.back() = projectile.type;
717 }
718 }
719 projectile.active = false;
720 continue;
721 }
722
723 if (isProjectileOutOfField(projectile)) {
724 projectile.active = false;
725 }
726 }
727 }
728
729 bool checkProjectileGoalHit(const ProjectileEntity& projectile) const {
730 for (const auto& goal : goals_) {
731 if (!goal.accept_any_type && goal.accepted_type != projectile.type) {
732 continue;
733 }
734 if (goal.require_positive_vertical_velocity &&
735 projectile.velocity_mps.z <= 0.0) {
736 continue;
737 }
738 if (goal.custom_velocity_validator &&
739 !goal.custom_velocity_validator(projectile.velocity_mps)) {
740 continue;
741 }
742
743 if (goal.shape == GoalZone::Shape::kBox) {
744 const Vector3 delta = projectile.position_m - goal.center_m;
745 if (std::abs(delta.x) <= goal.half_extents_m.x &&
746 std::abs(delta.y) <= goal.half_extents_m.y &&
747 std::abs(delta.z) <= goal.half_extents_m.z) {
748 return true;
749 }
750 } else {
751 if ((projectile.position_m - goal.center_m).norm() <= goal.radius_m) {
752 return true;
753 }
754 }
755 }
756 return false;
757 }
758
759 bool isProjectileOutOfField(const ProjectileEntity& projectile) const {
760 const double tolerance_m = 2.0;
761 return projectile.position_m.x < field_.min_corner_m.x - tolerance_m ||
762 projectile.position_m.x > field_.max_corner_m.x + tolerance_m ||
763 projectile.position_m.y < field_.min_corner_m.y - tolerance_m ||
764 projectile.position_m.y > field_.max_corner_m.y + tolerance_m;
765 }
766
767 const GamePieceInfo* findGamePieceInfo(GamePieceType type) const {
768 for (const auto& info : gamepiece_types_) {
769 if (info.type == type) {
770 return &info;
771 }
772 }
773 return nullptr;
774 }
775
776 static const char* gamePieceTypeName(GamePieceType type) {
777 switch (type) {
779 return "Ball";
781 return "Custom1";
783 return "Custom2";
785 return "Custom3";
787 return "Custom4";
788 default:
789 return "Ball";
790 }
791 }
792
793 static bool tryParseGamePieceType(const std::string& type_name,
794 GamePieceType& out_type) {
795 if (type_name == "Ball") {
796 out_type = GamePieceType::kBall;
797 return true;
798 }
799 if (type_name == "Custom1") {
800 out_type = GamePieceType::kCustom1;
801 return true;
802 }
803 if (type_name == "Custom2") {
804 out_type = GamePieceType::kCustom2;
805 return true;
806 }
807 if (type_name == "Custom3") {
808 out_type = GamePieceType::kCustom3;
809 return true;
810 }
811 if (type_name == "Custom4") {
812 out_type = GamePieceType::kCustom4;
813 return true;
814 }
815 return false;
816 }
817
818 static BallPhysicsSim3D::BallProperties fallbackBallProperties() {
819 BallPhysicsSim3D::BallProperties properties{};
820 properties.mass_kg = 0.24;
821 properties.radius_m = 0.09;
822 properties.drag_coefficient = 0.50;
823 properties.reference_area_m2 =
824 3.14159265358979323846 * properties.radius_m * properties.radius_m;
825 properties.restitution = 0.48;
826 return properties;
827 }
828
829 static BallPhysicsSim3D::Config fallbackBallConfig() {
830 BallPhysicsSim3D::Config config{};
831 config.gravity_mps2 = Vector3(0.0, 0.0, -9.81);
832 config.air_density_kgpm3 = 1.225;
833 config.magnus_coefficient = 1.0e-4;
834 config.ground_height_m = 0.0;
835 config.rolling_friction_per_s = 1.5;
836 config.min_bounce_speed_mps = 0.06;
837 return config;
838 }
839
846 static Vector3 rotateYaw(const Vector3& local, double yaw_rad) {
847 const double c = std::cos(yaw_rad);
848 const double s = std::sin(yaw_rad);
849 return Vector3(local.x * c - local.y * s, local.x * s + local.y * c,
850 local.z);
851 }
852
858 static Vector3 boundaryNormalWorld(const EnvironmentalBoundary& boundary) {
859 return boundary.orientation.rotate(Vector3::unitZ()).normalized();
860 }
861
869 static void applyContactImpulse(Vector3& velocity, const Vector3& normal,
870 double restitution, double friction) {
871 const double vn = velocity.dot(normal);
872 if (vn >= 0.0) {
873 return;
874 }
875
876 velocity -= normal * ((1.0 + std::clamp(restitution, 0.0, 1.0)) * vn);
877 const Vector3 tangential = velocity - normal * velocity.dot(normal);
878 velocity -= tangential * std::clamp(friction, 0.0, 1.0);
879 }
880
885 static double velocityScaledRestitution(
886 double base_restitution, double impact_speed_mps,
887 double reference_speed_mps, double speed_exponent,
888 double min_scale) {
889 const double base = std::clamp(base_restitution, 0.0, 1.0);
890 if (!std::isfinite(impact_speed_mps) || impact_speed_mps <= 0.0) {
891 return base;
892 }
893
894 const double reference_speed = std::max(
895 1e-6,
896 (std::isfinite(reference_speed_mps) && reference_speed_mps >= 0.0)
897 ? reference_speed_mps
898 : 3.0);
899 const double exponent =
900 (std::isfinite(speed_exponent) && speed_exponent >= 0.0)
901 ? speed_exponent
902 : 0.15;
903 const double clamped_min_scale = std::clamp(
904 std::isfinite(min_scale) ? min_scale : 0.55, 0.0, 1.0);
905
906 if (impact_speed_mps <= reference_speed || exponent <= 0.0) {
907 return base;
908 }
909
910 const double speed_scale =
911 std::pow(reference_speed / impact_speed_mps, exponent);
912 const double restitution_scale =
913 std::clamp(speed_scale, clamped_min_scale, 1.0);
914 return base * restitution_scale;
915 }
916
918 static void wakeBall(BallEntity& ball) {
919 ball.sleeping = false;
920 ball.sleep_frame_count = 0;
921 }
922
924 void updateSleepStates() {
925 if (!field_.sleeping_enabled) {
926 for (auto& ball : balls_) {
927 wakeBall(ball);
928 }
929 return;
930 }
931
932 const double velocity_threshold = std::max(0.0, field_.sleep_velocity_threshold_mps);
933 const double spin_threshold = std::max(0.0, field_.sleep_spin_threshold_radps);
934 const int frame_threshold = std::max(1, field_.sleep_frame_threshold);
935
936 for (auto& ball : balls_) {
937 auto state = ball.sim.state();
938 if (state.held || ball.scored_in_net) {
939 wakeBall(ball);
940 continue;
941 }
942
943 const double floor_z =
944 ball.sim.config().ground_height_m + ball.sim.ballProperties().radius_m;
945 const bool near_ground = state.position_m.z <= floor_z + 0.01;
946 const bool sleep_candidate = near_ground &&
947 state.velocity_mps.norm() <= velocity_threshold &&
948 state.spin_radps.norm() <= spin_threshold;
949
950 if (!sleep_candidate) {
951 wakeBall(ball);
952 continue;
953 }
954
955 ball.sleep_frame_count += 1;
956 if (ball.sleep_frame_count < frame_threshold) {
957 continue;
958 }
959
960 ball.sleeping = true;
961 state.velocity_mps = Vector3::zero();
962 state.spin_radps = Vector3::zero();
963 ball.sim.setState(state);
964 }
965 }
966
973 void resolveContinuousCollision(BallEntity& ball,
974 const Vector3& previous_position,
975 double dt_s) const {
976 if (!field_.ccd_enabled || dt_s <= 0.0 || ball.sim.state().held) {
977 return;
978 }
979
980 auto state = ball.sim.state();
981 const double speed_mps =
982 (state.position_m - previous_position).norm() / std::max(1e-9, dt_s);
983 if (speed_mps < std::max(0.0, field_.ccd_speed_threshold_mps)) {
984 return;
985 }
986
987 const double radius = ball.sim.ballProperties().radius_m;
988 resolveSweptFieldBounds(state, previous_position, radius);
989
990 for (const auto& boundary : field_elements_) {
991 if (!boundary.is_active) {
992 continue;
993 }
994
995 switch (boundary.type) {
998 resolveSweptPlaneBoundary(state, previous_position, boundary, radius);
999 break;
1000 case BoundaryType::kBox:
1001 resolveSweptBoxBoundary(state, previous_position, boundary, radius);
1002 break;
1003 default:
1004 break;
1005 }
1006 }
1007
1008 ball.sim.setState(state);
1009 }
1010
1012 void resolveSweptFieldBounds(BallPhysicsSim3D::BallState& state,
1013 const Vector3& previous_position,
1014 double radius) const {
1015 const double min_x = field_.min_corner_m.x + radius;
1016 const double max_x = field_.max_corner_m.x - radius;
1017 const double min_y = field_.min_corner_m.y + radius;
1018 const double max_y = field_.max_corner_m.y - radius;
1019
1020 if (previous_position.x >= min_x && state.position_m.x < min_x) {
1021 state.position_m.x = min_x;
1022 state.velocity_mps.x = std::abs(state.velocity_mps.x) *
1023 scaledWallRestitution(
1024 field_.wall_restitution,
1025 std::abs(state.velocity_mps.x));
1026 state.velocity_mps.y *= (1.0 - field_.wall_friction);
1027 }
1028 if (previous_position.x <= max_x && state.position_m.x > max_x) {
1029 state.position_m.x = max_x;
1030 state.velocity_mps.x = -std::abs(state.velocity_mps.x) *
1031 scaledWallRestitution(
1032 field_.wall_restitution,
1033 std::abs(state.velocity_mps.x));
1034 state.velocity_mps.y *= (1.0 - field_.wall_friction);
1035 }
1036 if (previous_position.y >= min_y && state.position_m.y < min_y) {
1037 state.position_m.y = min_y;
1038 state.velocity_mps.y = std::abs(state.velocity_mps.y) *
1039 scaledWallRestitution(
1040 field_.wall_restitution,
1041 std::abs(state.velocity_mps.y));
1042 state.velocity_mps.x *= (1.0 - field_.wall_friction);
1043 }
1044 if (previous_position.y <= max_y && state.position_m.y > max_y) {
1045 state.position_m.y = max_y;
1046 state.velocity_mps.y = -std::abs(state.velocity_mps.y) *
1047 scaledWallRestitution(
1048 field_.wall_restitution,
1049 std::abs(state.velocity_mps.y));
1050 state.velocity_mps.x *= (1.0 - field_.wall_friction);
1051 }
1052 }
1053
1055 void resolveSweptPlaneBoundary(BallPhysicsSim3D::BallState& state,
1056 const Vector3& previous_position,
1057 const EnvironmentalBoundary& boundary,
1058 double ball_radius) const {
1059 Vector3 normal = boundaryNormalWorld(boundary);
1060 if (normal.isZero()) {
1061 normal = Vector3::unitZ();
1062 }
1063
1064 const double prev_distance =
1065 (previous_position - boundary.position_m).dot(normal);
1066 const double curr_distance =
1067 (state.position_m - boundary.position_m).dot(normal);
1068
1069 if (!(prev_distance >= ball_radius && curr_distance < ball_radius)) {
1070 return;
1071 }
1072
1073 state.position_m += normal * (ball_radius - curr_distance);
1074 const double impact_speed = std::max(0.0, -state.velocity_mps.dot(normal));
1075 applyContactImpulse(state.velocity_mps, normal,
1076 scaledWallRestitution(boundary.restitution,
1077 impact_speed),
1078 boundary.friction_coefficient);
1079 }
1080
1082 void resolveSweptBoxBoundary(BallPhysicsSim3D::BallState& state,
1083 const Vector3& previous_position,
1084 const EnvironmentalBoundary& boundary,
1085 double ball_radius) const {
1086 const Quaternion inverse = boundary.orientation.inverse();
1087 const Vector3 prev_local =
1088 inverse.rotate(previous_position - boundary.position_m);
1089 const Vector3 curr_local =
1090 inverse.rotate(state.position_m - boundary.position_m);
1091 const Vector3 delta_local = curr_local - prev_local;
1092
1093 const Vector3 expanded_min(-boundary.half_extents_m.x - ball_radius,
1094 -boundary.half_extents_m.y - ball_radius,
1095 -boundary.half_extents_m.z - ball_radius);
1096 const Vector3 expanded_max(boundary.half_extents_m.x + ball_radius,
1097 boundary.half_extents_m.y + ball_radius,
1098 boundary.half_extents_m.z + ball_radius);
1099
1100 auto inside_expanded = [&](const Vector3& p) {
1101 return p.x >= expanded_min.x && p.x <= expanded_max.x &&
1102 p.y >= expanded_min.y && p.y <= expanded_max.y &&
1103 p.z >= expanded_min.z && p.z <= expanded_max.z;
1104 };
1105
1106 if (inside_expanded(curr_local)) {
1107 return;
1108 }
1109
1110 double t_enter = 0.0;
1111 double t_exit = 1.0;
1112 Vector3 local_normal = Vector3::zero();
1113
1114 auto process_axis = [&](double p0, double d, double min_b, double max_b,
1115 const Vector3& min_normal,
1116 const Vector3& max_normal) -> bool {
1117 if (std::abs(d) < 1e-9) {
1118 return p0 >= min_b && p0 <= max_b;
1119 }
1120
1121 const double inv_d = 1.0 / d;
1122 double t0 = (min_b - p0) * inv_d;
1123 double t1 = (max_b - p0) * inv_d;
1124 Vector3 candidate_normal = min_normal;
1125 if (t0 > t1) {
1126 std::swap(t0, t1);
1127 candidate_normal = max_normal;
1128 }
1129
1130 if (t0 > t_enter) {
1131 t_enter = t0;
1132 local_normal = candidate_normal;
1133 }
1134 t_exit = std::min(t_exit, t1);
1135 return t_enter <= t_exit;
1136 };
1137
1138 if (!process_axis(prev_local.x, delta_local.x, expanded_min.x, expanded_max.x,
1139 Vector3(-1.0, 0.0, 0.0), Vector3(1.0, 0.0, 0.0))) {
1140 return;
1141 }
1142 if (!process_axis(prev_local.y, delta_local.y, expanded_min.y, expanded_max.y,
1143 Vector3(0.0, -1.0, 0.0), Vector3(0.0, 1.0, 0.0))) {
1144 return;
1145 }
1146 if (!process_axis(prev_local.z, delta_local.z, expanded_min.z, expanded_max.z,
1147 Vector3(0.0, 0.0, -1.0), Vector3(0.0, 0.0, 1.0))) {
1148 return;
1149 }
1150
1151 if (t_enter < 0.0 || t_enter > 1.0 || local_normal.isZero()) {
1152 return;
1153 }
1154
1155 const Vector3 hit_local = prev_local + delta_local * t_enter;
1156 const Vector3 corrected_local = hit_local + local_normal * 1e-4;
1157 const Vector3 world_normal =
1158 boundary.orientation.rotate(local_normal).normalized();
1159
1160 state.position_m = boundary.position_m + boundary.orientation.rotate(corrected_local);
1161 const double impact_speed = std::max(0.0, -state.velocity_mps.dot(world_normal));
1162 applyContactImpulse(state.velocity_mps, world_normal,
1163 scaledWallRestitution(boundary.restitution,
1164 impact_speed),
1165 boundary.friction_coefficient);
1166 }
1167
1169 double scaledWallRestitution(double base_restitution,
1170 double impact_speed_mps) const {
1171 return velocityScaledRestitution(
1172 base_restitution, impact_speed_mps,
1173 field_.wall_restitution_reference_speed_mps,
1174 field_.wall_restitution_speed_exponent,
1175 field_.wall_restitution_min_scale);
1176 }
1177
1179 double scaledRobotBallRestitution(double impact_speed_mps) const {
1180 return velocityScaledRestitution(
1181 field_.robot_ball_contact_restitution, impact_speed_mps,
1182 field_.robot_ball_restitution_reference_speed_mps,
1183 field_.robot_ball_restitution_speed_exponent,
1184 field_.robot_ball_restitution_min_scale);
1185 }
1186
1189 void integrateRobots(double dt_s) {
1190 for (auto& robot : robots_) {
1191 robot.position_m += robot.velocity_mps * dt_s;
1192
1193 // Ball simulation: decrement ball by consumption rate * dt_s
1194 if (robot.ball_consumption_rate > 0.0 && robot.ball_level > 0.0) {
1195 robot.ball_level -= robot.ball_consumption_rate * dt_s;
1196 if (robot.ball_level < 0.0) robot.ball_level = 0.0;
1197 if (robot.ball_level > 1.0) robot.ball_level = 1.0;
1198 }
1199
1200 const double min_x = field_.min_corner_m.x + robot.radius_m;
1201 const double max_x = field_.max_corner_m.x - robot.radius_m;
1202 const double min_y = field_.min_corner_m.y + robot.radius_m;
1203 const double max_y = field_.max_corner_m.y - robot.radius_m;
1204
1205 if (robot.position_m.x < min_x) {
1206 robot.position_m.x = min_x;
1207 robot.velocity_mps.x = 0.0;
1208 }
1209 if (robot.position_m.x > max_x) {
1210 robot.position_m.x = max_x;
1211 robot.velocity_mps.x = 0.0;
1212 }
1213 if (robot.position_m.y < min_y) {
1214 robot.position_m.y = min_y;
1215 robot.velocity_mps.y = 0.0;
1216 }
1217 if (robot.position_m.y > max_y) {
1218 robot.position_m.y = max_y;
1219 robot.velocity_mps.y = 0.0;
1220 }
1221 }
1222 }
1223
1225 void resolveRobotRobotImpedance() {
1226 for (std::size_t i = 0; i < robots_.size(); ++i) {
1227 for (std::size_t j = i + 1; j < robots_.size(); ++j) {
1228 RobotState& a = robots_[i];
1229 RobotState& b = robots_[j];
1230
1231 Vector3 delta = b.position_m - a.position_m;
1232 delta.z = 0.0;
1233 const double distance = delta.norm();
1234 const double minimum_distance = a.radius_m + b.radius_m;
1235 if (distance >= minimum_distance) {
1236 continue;
1237 }
1238
1239 Vector3 normal = distance > 1e-9 ? delta / distance : Vector3::unitX();
1240 const double overlap = minimum_distance - distance;
1241 a.position_m -= normal * (0.5 * overlap);
1242 b.position_m += normal * (0.5 * overlap);
1243
1244 const double relative_speed =
1245 (a.velocity_mps - b.velocity_mps).dot(normal);
1246 if (relative_speed > 0.0) {
1247 continue;
1248 }
1249
1250 const double impulse = -0.7 * relative_speed;
1251 a.velocity_mps += normal * impulse;
1252 b.velocity_mps -= normal * impulse;
1253 }
1254 }
1255 }
1256
1264 void resolveRobotBallContacts(BallEntity& ball) {
1265 std::lock_guard<std::recursive_mutex> lock(state_mutex_);
1266 auto state = ball.sim.state();
1267
1268 for (const auto& robot : robots_) {
1269 Vector3 robot_to_ball = state.position_m - robot.position_m;
1270 robot_to_ball.z = 0.0;
1271 const double distance = robot_to_ball.norm();
1272 const double ball_radius = ball.sim.ballProperties().radius_m;
1273 const double contact_distance = robot.radius_m + ball_radius;
1274 if (distance >= contact_distance) {
1275 continue;
1276 }
1277
1278 wakeBall(ball);
1279
1280 const Vector3 normal =
1281 distance > 1e-9 ? robot_to_ball / distance : Vector3::unitX();
1282 const double penetration = contact_distance - distance;
1283 state.position_m +=
1284 normal * std::max(0.0, penetration - field_.baumgarte_slop_m);
1285
1286 Vector3 relative_velocity = state.velocity_mps - robot.velocity_mps;
1287 const double normal_impact_speed = -relative_velocity.dot(normal);
1288 applyContactImpulse(relative_velocity, normal,
1289 scaledRobotBallRestitution(normal_impact_speed),
1290 field_.robot_ball_contact_friction);
1291 state.velocity_mps = robot.velocity_mps + relative_velocity;
1292 state.spin_radps = Vector3::zero();
1293 }
1294
1295 ball.sim.setState(state);
1296 }
1297
1302 void resolveBallBallContacts(double dt_s) {
1303 struct BallBallContact {
1304 std::size_t a_index{};
1305 std::size_t b_index{};
1306 Vector3 normal{};
1307 double penetration{0.0};
1308 double inv_mass_a{0.0};
1309 double inv_mass_b{0.0};
1310 double mass_a{0.0};
1311 double mass_b{0.0};
1312 double radius_a{0.0};
1313 double radius_b{0.0};
1314 double restitution{0.0};
1315 double friction{0.0};
1316 double normal_impulse_accum{0.0};
1317 double tangent_impulse_accum{0.0};
1318 };
1319
1320 std::vector<BallBallContact> contacts;
1321 contacts.reserve(balls_.size());
1322
1323 for (std::size_t i = 0; i < balls_.size(); ++i) {
1324 const auto& state_a = balls_[i].sim.state();
1325 if (state_a.held || balls_[i].scored_in_net) {
1326 continue;
1327 }
1328
1329 const double radius_a = balls_[i].sim.ballProperties().radius_m;
1330 const double mass_a =
1331 std::max(1e-9, balls_[i].sim.ballProperties().mass_kg);
1332
1333 for (std::size_t j = i + 1; j < balls_.size(); ++j) {
1334 const auto& state_b = balls_[j].sim.state();
1335 if (state_b.held || balls_[j].scored_in_net) {
1336 continue;
1337 }
1338
1339 const double radius_b = balls_[j].sim.ballProperties().radius_m;
1340 const double mass_b =
1341 std::max(1e-9, balls_[j].sim.ballProperties().mass_kg);
1342
1343 const Vector3 delta = state_b.position_m - state_a.position_m;
1344 const double distance = delta.norm();
1345 const double target_distance = radius_a + radius_b;
1346 if (distance >= target_distance) {
1347 continue;
1348 }
1349
1350 BallBallContact contact{};
1351 contact.a_index = i;
1352 contact.b_index = j;
1353 contact.normal = distance > 1e-9 ? delta / distance : Vector3::unitX();
1354 contact.penetration = target_distance - distance;
1355 contact.inv_mass_a = 1.0 / mass_a;
1356 contact.inv_mass_b = 1.0 / mass_b;
1357 contact.mass_a = mass_a;
1358 contact.mass_b = mass_b;
1359 contact.radius_a = radius_a;
1360 contact.radius_b = radius_b;
1361 const double approach_speed =
1362 std::max(0.0, (state_a.velocity_mps - state_b.velocity_mps).dot(contact.normal));
1363 contact.restitution = velocityScaledRestitution(
1364 field_.ball_ball_contact_restitution, approach_speed,
1365 field_.ball_ball_restitution_reference_speed_mps,
1366 field_.ball_ball_restitution_speed_exponent,
1367 field_.ball_ball_restitution_min_scale);
1368 contact.friction = std::clamp(field_.ball_ball_contact_friction, 0.0, 1.0);
1369 contacts.push_back(contact);
1370 wakeBall(balls_[i]);
1371 wakeBall(balls_[j]);
1372 }
1373 }
1374
1375 if (contacts.empty()) {
1376 return;
1377 }
1378
1379 const int solver_iterations = std::max(1, field_.solver_iterations);
1380 const double inv_dt = 1.0 / std::max(1e-9, dt_s);
1381
1382 for (int iteration = 0; iteration < solver_iterations; ++iteration) {
1383 for (auto& contact : contacts) {
1384 auto state_a = balls_[contact.a_index].sim.state();
1385 auto state_b = balls_[contact.b_index].sim.state();
1386
1387 Vector3 relative_velocity = state_b.velocity_mps - state_a.velocity_mps;
1388 const double normal_speed = relative_velocity.dot(contact.normal);
1389 const double position_bias =
1390 std::max(0.0, contact.penetration - field_.baumgarte_slop_m) *
1391 std::clamp(field_.baumgarte_beta, 0.0, 1.0) * inv_dt;
1392
1393 if (normal_speed < 0.0 || position_bias > 0.0) {
1394 const double inv_mass_sum = contact.inv_mass_a + contact.inv_mass_b;
1395 if (inv_mass_sum <= 1e-9) {
1396 continue;
1397 }
1398
1399 const double desired_normal_impulse =
1400 -((1.0 + contact.restitution) * normal_speed + position_bias) /
1401 inv_mass_sum;
1402 const double old_normal_accum = contact.normal_impulse_accum;
1403 contact.normal_impulse_accum = std::max(
1404 0.0, contact.normal_impulse_accum + desired_normal_impulse);
1405 const double applied_normal_impulse =
1406 contact.normal_impulse_accum - old_normal_accum;
1407 const Vector3 normal_impulse_vec = contact.normal * applied_normal_impulse;
1408
1409 state_a.velocity_mps -= normal_impulse_vec * contact.inv_mass_a;
1410 state_b.velocity_mps += normal_impulse_vec * contact.inv_mass_b;
1411
1412 relative_velocity = state_b.velocity_mps - state_a.velocity_mps;
1413 const Vector3 tangential_velocity =
1414 relative_velocity - contact.normal *
1415 relative_velocity.dot(contact.normal);
1416 const double tangential_speed = tangential_velocity.norm();
1417 if (tangential_speed > 1e-9 && contact.friction > 0.0) {
1418 const Vector3 tangent = tangential_velocity / tangential_speed;
1419 const double desired_tangent_impulse =
1420 -tangential_speed / inv_mass_sum;
1421 const double max_friction_impulse =
1422 contact.friction * contact.normal_impulse_accum;
1423 const double old_tangent_accum = contact.tangent_impulse_accum;
1424 contact.tangent_impulse_accum = std::clamp(
1425 contact.tangent_impulse_accum + desired_tangent_impulse,
1426 -max_friction_impulse, max_friction_impulse);
1427 const double applied_tangent_impulse =
1428 contact.tangent_impulse_accum - old_tangent_accum;
1429 const Vector3 friction_impulse = tangent * applied_tangent_impulse;
1430
1431 state_a.velocity_mps += friction_impulse * contact.inv_mass_a;
1432 state_b.velocity_mps -= friction_impulse * contact.inv_mass_b;
1433
1434 const double spin_transfer_gain =
1435 std::max(0.0, field_.ball_ball_spin_transfer_gain);
1436 if (spin_transfer_gain > 0.0) {
1437 const double inertia_a =
1438 std::max(1e-9, 0.4 * contact.mass_a * contact.radius_a * contact.radius_a);
1439 const double inertia_b =
1440 std::max(1e-9, 0.4 * contact.mass_b * contact.radius_b * contact.radius_b);
1441 state_a.spin_radps +=
1442 contact.normal.cross(friction_impulse) *
1443 (spin_transfer_gain / inertia_a) * contact.radius_a;
1444 state_b.spin_radps +=
1445 (-contact.normal).cross(-friction_impulse) *
1446 (spin_transfer_gain / inertia_b) * contact.radius_b;
1447 }
1448 }
1449
1450 balls_[contact.a_index].sim.setState(state_a);
1451 balls_[contact.b_index].sim.setState(state_b);
1452 }
1453 }
1454 }
1455 }
1456
1458 void resolveFieldBounds(BallEntity& ball) const {
1459 auto state = ball.sim.state();
1460 const double radius = ball.sim.ballProperties().radius_m;
1461
1462 const double min_x = field_.min_corner_m.x + radius;
1463 const double max_x = field_.max_corner_m.x - radius;
1464 const double min_y = field_.min_corner_m.y + radius;
1465 const double max_y = field_.max_corner_m.y - radius;
1466
1467 if (state.position_m.x < min_x) {
1468 state.position_m.x = min_x + field_.baumgarte_slop_m;
1469 state.velocity_mps.x = std::abs(state.velocity_mps.x) *
1470 scaledWallRestitution(
1471 field_.wall_restitution,
1472 std::abs(state.velocity_mps.x));
1473 state.velocity_mps.y *= (1.0 - field_.wall_friction);
1474 }
1475 if (state.position_m.x > max_x) {
1476 state.position_m.x = max_x - field_.baumgarte_slop_m;
1477 state.velocity_mps.x = -std::abs(state.velocity_mps.x) *
1478 scaledWallRestitution(
1479 field_.wall_restitution,
1480 std::abs(state.velocity_mps.x));
1481 state.velocity_mps.y *= (1.0 - field_.wall_friction);
1482 }
1483 if (state.position_m.y < min_y) {
1484 state.position_m.y = min_y + field_.baumgarte_slop_m;
1485 state.velocity_mps.y = std::abs(state.velocity_mps.y) *
1486 scaledWallRestitution(
1487 field_.wall_restitution,
1488 std::abs(state.velocity_mps.y));
1489 state.velocity_mps.x *= (1.0 - field_.wall_friction);
1490 }
1491 if (state.position_m.y > max_y) {
1492 state.position_m.y = max_y - field_.baumgarte_slop_m;
1493 state.velocity_mps.y = -std::abs(state.velocity_mps.y) *
1494 scaledWallRestitution(
1495 field_.wall_restitution,
1496 std::abs(state.velocity_mps.y));
1497 state.velocity_mps.x *= (1.0 - field_.wall_friction);
1498 }
1499
1500 state.spin_radps = Vector3::zero();
1501
1502 ball.sim.setState(state);
1503 }
1504
1510 void resolveFieldElements(BallEntity& ball, double dt_s) {
1511 const int solver_iterations = std::max(1, field_.solver_iterations);
1512 for (int iteration = 0; iteration < solver_iterations; ++iteration) {
1513 auto state = ball.sim.state();
1514 const double radius = ball.sim.ballProperties().radius_m;
1515
1516 // Check for pick-and-place snapping to goal zones
1517 if (field_.scoring_element_snapping_enabled) {
1518 for (const auto& goal : goals_) {
1519 Vector3 displacement = goal.center_m - state.position_m;
1520 double distance = displacement.norm();
1521 if (distance < field_.snap_distance_m) {
1522 // Snap element to scoring zone
1523 state.position_m = goal.center_m;
1524 state.velocity_mps = Vector3(0.0, 0.0, 0.0);
1525 state.spin_radps = Vector3(0.0, 0.0, 0.0);
1526 ball.sim.setState(state);
1527 return; // Ball is scored, stop processing
1528 }
1529 }
1530 }
1531
1532 for (const auto& boundary : field_elements_) {
1533 if (!boundary.is_active) {
1534 continue;
1535 }
1536
1537 if (field_.net_boundary_user_id >= 0 &&
1538 boundary.user_id == field_.net_boundary_user_id &&
1539 boundary.type == BoundaryType::kBox) {
1540 if (isInsideBoxBoundary(state.position_m, boundary,
1541 radius * field_.net_entry_slack_scale)) {
1542 ball.scored_in_net = true;
1543 state.velocity_mps.x *= field_.net_velocity_decay;
1544 state.velocity_mps.y *= field_.net_velocity_decay;
1545 state.velocity_mps.z = std::min(state.velocity_mps.z, 0.0);
1546 state.spin_radps *= field_.net_spin_decay;
1547 state.velocity_mps +=
1548 Vector3(0.0, 0.0, -field_.net_downward_bias_mps2 * dt_s);
1549 }
1550 continue;
1551 }
1552
1553 switch (boundary.type) {
1556 resolvePlaneBoundary(state, boundary, radius);
1557 break;
1558 case BoundaryType::kBox:
1559 resolveBoxBoundary(state, boundary, radius);
1560 break;
1562 resolveCylinderBoundary(state, boundary, radius);
1563 break;
1564 default:
1565 break;
1566 }
1567 }
1568
1569 ball.sim.setState(state);
1570 }
1571 }
1572
1575 static bool isInsideBoxBoundary(const Vector3& world_point,
1576 const EnvironmentalBoundary& boundary,
1577 double slack) {
1578 const Quaternion inverse = boundary.orientation.inverse();
1579 const Vector3 local = inverse.rotate(world_point - boundary.position_m);
1580 return std::abs(local.x) <= boundary.half_extents_m.x + slack &&
1581 std::abs(local.y) <= boundary.half_extents_m.y + slack &&
1582 std::abs(local.z) <= boundary.half_extents_m.z + slack;
1583 }
1584
1586 void resolvePlaneBoundary(BallPhysicsSim3D::BallState& state,
1587 const EnvironmentalBoundary& boundary,
1588 double ball_radius) const {
1589 Vector3 normal = boundaryNormalWorld(boundary);
1590 if (normal.isZero()) {
1591 normal = Vector3::unitZ();
1592 }
1593
1594 const double signed_distance =
1595 (state.position_m - boundary.position_m).dot(normal);
1596 if (signed_distance >= ball_radius) {
1597 return;
1598 }
1599
1600 state.position_m += normal *
1601 std::max(0.0, ball_radius - signed_distance - field_.baumgarte_slop_m);
1602 const double impact_speed = std::max(0.0, -state.velocity_mps.dot(normal));
1603 applyContactImpulse(state.velocity_mps, normal,
1604 scaledWallRestitution(boundary.restitution,
1605 impact_speed),
1606 boundary.friction_coefficient);
1607 state.spin_radps = Vector3::zero();
1608 }
1609
1612 void resolveBoxBoundary(BallPhysicsSim3D::BallState& state,
1613 const EnvironmentalBoundary& boundary,
1614 double ball_radius) const {
1615 const Quaternion inverse = boundary.orientation.inverse();
1616 const Vector3 local =
1617 inverse.rotate(state.position_m - boundary.position_m);
1618
1619 const Vector3 closest(std::clamp(local.x, -boundary.half_extents_m.x,
1620 boundary.half_extents_m.x),
1621 std::clamp(local.y, -boundary.half_extents_m.y,
1622 boundary.half_extents_m.y),
1623 std::clamp(local.z, -boundary.half_extents_m.z,
1624 boundary.half_extents_m.z));
1625
1626 Vector3 delta = local - closest;
1627 double distance = delta.norm();
1628 Vector3 local_normal =
1629 distance > 1e-9 ? delta / distance : Vector3::unitZ();
1630
1631 if (distance >= ball_radius) {
1632 return;
1633 }
1634
1635 if (distance <= 1e-9) {
1636 const double pen_x = boundary.half_extents_m.x - std::abs(local.x);
1637 const double pen_y = boundary.half_extents_m.y - std::abs(local.y);
1638 const double pen_z = boundary.half_extents_m.z - std::abs(local.z);
1639
1640 if (pen_x <= pen_y && pen_x <= pen_z) {
1641 local_normal = Vector3(local.x >= 0.0 ? 1.0 : -1.0, 0.0, 0.0);
1642 distance = pen_x;
1643 } else if (pen_y <= pen_z) {
1644 local_normal = Vector3(0.0, local.y >= 0.0 ? 1.0 : -1.0, 0.0);
1645 distance = pen_y;
1646 } else {
1647 local_normal = Vector3(0.0, 0.0, local.z >= 0.0 ? 1.0 : -1.0);
1648 distance = pen_z;
1649 }
1650 }
1651
1652 const Vector3 world_normal =
1653 boundary.orientation.rotate(local_normal).normalized();
1654 state.position_m += world_normal *
1655 std::max(0.0, ball_radius - distance - field_.baumgarte_slop_m);
1656 const double impact_speed = std::max(0.0, -state.velocity_mps.dot(world_normal));
1657 applyContactImpulse(state.velocity_mps, world_normal,
1658 scaledWallRestitution(boundary.restitution,
1659 impact_speed),
1660 boundary.friction_coefficient);
1661 state.spin_radps = Vector3::zero();
1662 }
1663
1666 void resolveCylinderBoundary(BallPhysicsSim3D::BallState& state,
1667 const EnvironmentalBoundary& boundary,
1668 double ball_radius) const {
1669 const Quaternion inverse = boundary.orientation.inverse();
1670 const Vector3 local =
1671 inverse.rotate(state.position_m - boundary.position_m);
1672
1673 const double half_height = std::max(0.0, boundary.half_extents_m.z);
1674 if (std::abs(local.z) > half_height + ball_radius) {
1675 return;
1676 }
1677
1678 const Vector3 radial(local.x, local.y, 0.0);
1679 const double radial_distance = radial.norm();
1680 const double contact_distance =
1681 std::max(0.0, boundary.radius_m) + ball_radius;
1682 if (radial_distance >= contact_distance) {
1683 return;
1684 }
1685
1686 const Vector3 radial_normal_local =
1687 radial_distance > 1e-9 ? radial / radial_distance : Vector3::unitX();
1688 const Vector3 radial_normal_world =
1689 boundary.orientation.rotate(radial_normal_local).normalized();
1690 state.position_m += radial_normal_world *
1691 std::max(0.0, contact_distance - radial_distance - field_.baumgarte_slop_m);
1692 const double impact_speed =
1693 std::max(0.0, -state.velocity_mps.dot(radial_normal_world));
1694 applyContactImpulse(state.velocity_mps, radial_normal_world,
1695 scaledWallRestitution(boundary.restitution,
1696 impact_speed),
1697 boundary.friction_coefficient);
1698 state.spin_radps = Vector3::zero();
1699 }
1700
1701 FieldConfig field_{};
1702 std::vector<RobotState> robots_{};
1703 std::vector<BallEntity> balls_{};
1704 std::vector<GamePieceType> ball_types_{};
1705 std::vector<ProjectileEntity> projectiles_{};
1706 std::vector<GoalZone> goals_{};
1707 std::vector<GamePieceInfo> gamepiece_types_{};
1708 std::vector<EnvironmentalBoundary> field_elements_{};
1709 RobotAddedCallback robot_added_callback_{};
1710 int simulation_substeps_{4};
1711 mutable std::recursive_mutex state_mutex_{};
1712};
1713
1714} // namespace frcsim
std::size_t countProjectiles() const
Returns count of currently active projectiles.
Definition ball_gamepiece_sim.hpp:523
std::vector< EnvironmentalBoundary > & fieldElements()
Mutable field-element list.
Definition ball_gamepiece_sim.hpp:505
ExitTrajectoryParameters FireCommand
Backward-compatible alias for older call sites.
Definition ball_gamepiece_sim.hpp:183
std::size_t addProjectile(const ProjectileEntity &projectile)
Adds a projectile entity and returns its index.
Definition ball_gamepiece_sim.hpp:333
std::vector< RobotState > & robots()
Mutable robot state list.
Definition ball_gamepiece_sim.hpp:388
std::vector< ProjectileEntity > & projectiles()
Mutable projectile list.
Definition ball_gamepiece_sim.hpp:491
bool setBallType(std::size_t ball_index, const std::string &type_name)
Backward-compatible setter that accepts a type name.
Definition ball_gamepiece_sim.hpp:464
GamePieceType gamepieceType(std::size_t idx) const
Definition ball_gamepiece_sim.hpp:410
GoalZone & addGoalZone(const GoalZone &goal_zone)
Adds a goal zone and returns a mutable reference to the stored copy.
Definition ball_gamepiece_sim.hpp:344
void setRobotAddedCallback(const RobotAddedCallback &callback)
Sets a callback invoked whenever addRobot inserts a robot.
Definition ball_gamepiece_sim.hpp:305
static constexpr std::size_t kNoBall
Definition ball_gamepiece_sim.hpp:52
const std::vector< ProjectileEntity > & projectiles() const
Immutable projectile list.
Definition ball_gamepiece_sim.hpp:494
std::vector< BallEntity > & gamepieces()
Definition ball_gamepiece_sim.hpp:402
bool setBallType(std::size_t ball_index, GamePieceType type)
Updates the type for an existing ball.
Definition ball_gamepiece_sim.hpp:449
std::size_t fireProjectile(std::size_t robot_index, const ExitTrajectoryParameters &command, bool spawn_on_ground_after_touch=true, const std::function< void()> &hit_target_callback={})
Spawns a projectile using robot pose and exit trajectory parameters.
Definition ball_gamepiece_sim.hpp:556
std::vector< GoalZone > & goals()
Mutable goal-zone list.
Definition ball_gamepiece_sim.hpp:499
bool removeGamepiece(std::size_t idx)
Definition ball_gamepiece_sim.hpp:418
void step(double dt_s)
Advances simulation by dt using configured internal substep count.
Definition ball_gamepiece_sim.hpp:597
std::size_t countScoredBalls() const
Returns count of grounded balls marked as scored in configured net volume(s).
Definition ball_gamepiece_sim.hpp:536
std::size_t countGamepieces() const
Definition ball_gamepiece_sim.hpp:420
BallGamepieceSim()=default
Constructs a simulator with default evergreen field bounds and contact tuning.
EnvironmentalBoundary & addFieldElement(const EnvironmentalBoundary &boundary)
Adds a field element and returns a mutable reference to the stored copy.
Definition ball_gamepiece_sim.hpp:379
void setSimulationSubsteps(int simulation_substeps)
Sets simulation substeps per external step; values below 1 clamp to 1.
Definition ball_gamepiece_sim.hpp:617
bool removeBall(std::size_t ball_index)
Removes a ball by index and remaps carried-ball indices accordingly.
Definition ball_gamepiece_sim.hpp:477
bool setGamepieceType(std::size_t idx, GamePieceType t)
Definition ball_gamepiece_sim.hpp:414
std::vector< BallEntity > & balls()
Mutable grounded-ball list.
Definition ball_gamepiece_sim.hpp:395
const std::vector< BallEntity > & gamepieces() const
Definition ball_gamepiece_sim.hpp:404
std::string ballTypeName(std::size_t ball_index) const
Returns the registered type label for a ball index, or empty string if out of range.
Definition ball_gamepiece_sim.hpp:436
const std::vector< RobotState > & robots() const
Immutable robot state list.
Definition ball_gamepiece_sim.hpp:391
int simulationSubsteps() const
Returns current simulation substeps-per-step value.
Definition ball_gamepiece_sim.hpp:623
bool setGamepieceType(std::size_t idx, const std::string &n)
Definition ball_gamepiece_sim.hpp:416
GamePieceType
Definition ball_gamepiece_sim.hpp:42
@ kCustom4
Definition ball_gamepiece_sim.hpp:48
@ kCustom1
Definition ball_gamepiece_sim.hpp:45
@ kCustom2
Definition ball_gamepiece_sim.hpp:46
@ kCustom3
Definition ball_gamepiece_sim.hpp:47
@ kFuel2026
Definition ball_gamepiece_sim.hpp:44
@ kBall
Definition ball_gamepiece_sim.hpp:43
std::function< void(std::size_t robot_index, const RobotState &robot)> RobotAddedCallback
Callback type invoked when a robot is added to this simulator.
Definition ball_gamepiece_sim.hpp:283
void clearGamePieceTypes()
Clears all registered game piece type definitions.
Definition ball_gamepiece_sim.hpp:368
std::size_t addRobot(const RobotState &robot)
Adds a robot and returns its index.
Definition ball_gamepiece_sim.hpp:291
std::size_t addBall(const BallPhysicsSim3D::BallState &state, const BallPhysicsSim3D::Config &config, const BallPhysicsSim3D::BallProperties &properties)
Adds a grounded ball with explicit physics configuration.
Definition ball_gamepiece_sim.hpp:316
const std::vector< EnvironmentalBoundary > & fieldElements() const
Immutable field-element list.
Definition ball_gamepiece_sim.hpp:510
BallGamepieceSim(const FieldConfig &field)
Constructs a simulator with the provided field configuration.
Definition ball_gamepiece_sim.hpp:270
std::size_t countBalls() const
Returns total grounded ball count (including scored-in-net balls).
Definition ball_gamepiece_sim.hpp:516
GamePieceInfo & registerGamePieceType(const GamePieceInfo &info)
Registers or replaces a named game piece type.
Definition ball_gamepiece_sim.hpp:355
static constexpr std::size_t kNoGamepiece
Definition ball_gamepiece_sim.hpp:407
const std::vector< BallEntity > & balls() const
Immutable grounded-ball list.
Definition ball_gamepiece_sim.hpp:398
std::string gamepieceTypeName(std::size_t idx) const
Definition ball_gamepiece_sim.hpp:412
GamePieceType ballType(std::size_t ball_index) const
Returns the registered type for a ball index.
Definition ball_gamepiece_sim.hpp:427
void setFieldConfig(const FieldConfig &field)
Replaces field configuration while preserving dynamic simulation entities.
Definition ball_gamepiece_sim.hpp:277
const std::vector< GoalZone > & goals() const
Immutable goal-zone list.
Definition ball_gamepiece_sim.hpp:501
3D rigid-body style ball simulator with drag, Magnus lift, and ground contact.
Definition ball_physics.hpp:22
void setState(const BallState &state)
Replaces state and sanitizes non-finite values.
Definition ball_physics.hpp:117
Definition vector.hpp:13
@ kBox
Box primitive (axis-aligned or oriented via quaternion).
Definition boundary.hpp:26
@ kWall
Finite wall-like barrier primitive.
Definition boundary.hpp:22
@ kPlane
Infinite plane constraint primitive.
Definition boundary.hpp:24
@ kCylinder
Cylindrical primitive (for posts, exclusion zones, etc.).
Definition boundary.hpp:28
Grounded game piece entity with its own ball physics instance.
Definition ball_gamepiece_sim.hpp:251
bool scored_in_net
Definition ball_gamepiece_sim.hpp:255
bool sleeping
Definition ball_gamepiece_sim.hpp:257
int sleep_frame_count
Definition ball_gamepiece_sim.hpp:259
BallPhysicsSim3D sim
Definition ball_gamepiece_sim.hpp:253
Exit trajectory and metadata used when launching a carried ball or a projectile.
Definition ball_gamepiece_sim.hpp:160
Vector3 spin_radps
Definition ball_gamepiece_sim.hpp:176
double yaw_offset_rad
Definition ball_gamepiece_sim.hpp:165
double pitch_rad
Definition ball_gamepiece_sim.hpp:167
double estimated_exit_velocity_mps
Definition ball_gamepiece_sim.hpp:173
Vector3 launch_offset_m
Definition ball_gamepiece_sim.hpp:162
GamePieceType gamepiece_type
Definition ball_gamepiece_sim.hpp:179
double mechanism_speed_mps
Definition ball_gamepiece_sim.hpp:171
Global field and contact tuning parameters.
Definition ball_gamepiece_sim.hpp:55
double sleep_velocity_threshold_mps
Definition ball_gamepiece_sim.hpp:125
double baumgarte_slop_m
Definition ball_gamepiece_sim.hpp:135
Vector3 min_corner_m
Definition ball_gamepiece_sim.hpp:57
double ball_ball_restitution_speed_exponent
Definition ball_gamepiece_sim.hpp:110
double net_velocity_decay
Definition ball_gamepiece_sim.hpp:78
double robot_ball_restitution_min_scale
Definition ball_gamepiece_sim.hpp:92
double robot_ball_contact_friction
Definition ball_gamepiece_sim.hpp:94
int solver_iterations
Definition ball_gamepiece_sim.hpp:131
double robot_ball_contact_restitution
Definition ball_gamepiece_sim.hpp:85
double ccd_speed_threshold_mps
Definition ball_gamepiece_sim.hpp:121
double ball_ball_contact_friction
Definition ball_gamepiece_sim.hpp:106
double sleep_spin_threshold_radps
Definition ball_gamepiece_sim.hpp:127
double net_downward_bias_mps2
Definition ball_gamepiece_sim.hpp:82
double wall_restitution_speed_exponent
Definition ball_gamepiece_sim.hpp:65
double ball_ball_restitution_min_scale
Definition ball_gamepiece_sim.hpp:112
double wall_restitution_min_scale
Definition ball_gamepiece_sim.hpp:67
double wall_friction
Definition ball_gamepiece_sim.hpp:69
bool sleeping_enabled
Definition ball_gamepiece_sim.hpp:123
int sleep_frame_threshold
Definition ball_gamepiece_sim.hpp:129
double ball_ball_contact_restitution
Definition ball_gamepiece_sim.hpp:104
double free_ball_spin_decay_per_s
Definition ball_gamepiece_sim.hpp:116
double net_entry_slack_scale
Definition ball_gamepiece_sim.hpp:75
Vector3 max_corner_m
Definition ball_gamepiece_sim.hpp:59
double ball_ball_restitution_reference_speed_mps
Definition ball_gamepiece_sim.hpp:108
double robot_ball_restitution_speed_exponent
Definition ball_gamepiece_sim.hpp:90
double ball_ball_spin_transfer_gain
Definition ball_gamepiece_sim.hpp:114
double net_spin_decay
Definition ball_gamepiece_sim.hpp:80
double baumgarte_beta
Definition ball_gamepiece_sim.hpp:133
double wall_restitution_reference_speed_mps
Definition ball_gamepiece_sim.hpp:63
bool scoring_element_snapping_enabled
Definition ball_gamepiece_sim.hpp:99
double robot_ball_restitution_reference_speed_mps
Definition ball_gamepiece_sim.hpp:87
double snap_distance_m
Definition ball_gamepiece_sim.hpp:102
double wall_restitution
Definition ball_gamepiece_sim.hpp:61
int net_boundary_user_id
Definition ball_gamepiece_sim.hpp:73
bool ccd_enabled
Definition ball_gamepiece_sim.hpp:118
Registration record for a named game piece type.
Definition ball_gamepiece_sim.hpp:186
BallPhysicsSim3D::Config physics_config
Definition ball_gamepiece_sim.hpp:191
bool spawn_on_ground_after_projectile
Definition ball_gamepiece_sim.hpp:197
GamePieceType type
Definition ball_gamepiece_sim.hpp:188
BallPhysicsSim3D::BallProperties ball_properties
Definition ball_gamepiece_sim.hpp:194
Goal capture region and validation logic.
Definition ball_gamepiece_sim.hpp:225
Shape
Supported built-in goal geometries.
Definition ball_gamepiece_sim.hpp:227
@ kBox
Definition ball_gamepiece_sim.hpp:228
@ kSphere
Definition ball_gamepiece_sim.hpp:229
double radius_m
Definition ball_gamepiece_sim.hpp:239
Shape shape
Definition ball_gamepiece_sim.hpp:233
bool require_positive_vertical_velocity
Definition ball_gamepiece_sim.hpp:245
std::function< bool(const Vector3 &)> custom_velocity_validator
Definition ball_gamepiece_sim.hpp:247
Vector3 half_extents_m
Definition ball_gamepiece_sim.hpp:237
GamePieceType accepted_type
Definition ball_gamepiece_sim.hpp:241
Vector3 center_m
Definition ball_gamepiece_sim.hpp:235
bool accept_any_type
Definition ball_gamepiece_sim.hpp:243
In-flight entity not currently represented by BallPhysicsSim3D.
Definition ball_gamepiece_sim.hpp:201
bool active
Definition ball_gamepiece_sim.hpp:215
double gravity_mps2
Definition ball_gamepiece_sim.hpp:211
Vector3 velocity_mps
Definition ball_gamepiece_sim.hpp:207
Vector3 spin_radps
Definition ball_gamepiece_sim.hpp:209
Vector3 position_m
Definition ball_gamepiece_sim.hpp:205
double age_s
Definition ball_gamepiece_sim.hpp:213
bool spawn_on_ground_after_touch
Definition ball_gamepiece_sim.hpp:217
bool hit_target
Definition ball_gamepiece_sim.hpp:219
GamePieceType type
Definition ball_gamepiece_sim.hpp:203
std::function< void()> hit_target_callback
Definition ball_gamepiece_sim.hpp:221
Per-robot state used by game piece interaction and collision routines.
Definition ball_gamepiece_sim.hpp:140
Vector3 velocity_mps
Definition ball_gamepiece_sim.hpp:144
Vector3 position_m
Definition ball_gamepiece_sim.hpp:142
double intake_radius_m
Definition ball_gamepiece_sim.hpp:152
double radius_m
Definition ball_gamepiece_sim.hpp:149
double yaw_rad
Definition ball_gamepiece_sim.hpp:146
Physical parameters for the ball body.
Definition ball_physics.hpp:49
Dynamic state advanced by step().
Definition ball_physics.hpp:66
Runtime physics environment parameters.
Definition ball_physics.hpp:25
Collision or constraint boundary definition used by PhysicsWorld.
Definition boundary.hpp:51
3D vector utility used throughout JSim physics.
Definition vector.hpp:22
double z
Z component.
Definition vector.hpp:28
static constexpr Vector3 unitX() noexcept
Returns the +X basis vector.
Definition vector.hpp:392
static constexpr Vector3 unitZ() noexcept
Returns the +Z basis vector.
Definition vector.hpp:396
static constexpr Vector3 zero() noexcept
Returns the zero vector.
Definition vector.hpp:360