1+ package frc .robot .extras .simulation .field ;
2+
3+ import edu .wpi .first .math .geometry .Pose2d ;
4+ import edu .wpi .first .math .geometry .Pose3d ;
5+ import edu .wpi .first .math .geometry .Translation2d ;
6+ import edu .wpi .first .math .geometry .Translation3d ;
7+ import edu .wpi .first .units .measure .Time ;
8+ import edu .wpi .first .wpilibj .IterativeRobotBase ;
9+ import edu .wpi .first .wpilibj .smartdashboard .SmartDashboard ;
10+ import frc .robot .extras .simulation .gamePiece .GamePieceSimulation ;
11+
12+ import java .util .List ;
13+ import java .util .Set ;
14+ import java .util .concurrent .ConcurrentHashMap ;
15+ import java .util .concurrent .locks .ReentrantLock ;
16+
17+ import static edu .wpi .first .units .Units .Seconds ;
18+
19+ import java .util .ArrayList ;
20+ import java .util .function .Consumer ;
21+ import java .util .function .Supplier ;
22+ import java .util .stream .Stream ;
23+
24+ import org .dyn4j .dynamics .Body ;
25+ import org .dyn4j .dynamics .BodyFixture ;
26+ import org .dyn4j .geometry .Convex ;
27+ import org .dyn4j .geometry .Geometry ;
28+ import org .dyn4j .geometry .MassType ;
29+ import org .dyn4j .world .PhysicsWorld ;
30+ import org .dyn4j .world .World ;
31+
32+ public abstract class SimArena {
33+ public static final class SimEnvTiming {
34+ public final Time period ;
35+ public final int ticksPerPeriod ;
36+ public final Time dt ;
37+
38+ private SimEnvTiming (double robotPeriodSeconds , int simulationSubTicksPerPeriod ) {
39+ this .period = Seconds .of (robotPeriodSeconds );
40+ this .ticksPerPeriod = simulationSubTicksPerPeriod ;
41+ this .dt = Seconds .of (robotPeriodSeconds / ((double ) ticksPerPeriod ));
42+ }
43+ }
44+
45+ protected final ReentrantLock worldLock = new ReentrantLock ();
46+ protected final World <Body > physicsWorld = new World <>();
47+ protected final Set <GamePieceSimulation > gamePieces = ConcurrentHashMap .newKeySet ();
48+ protected final Set <ShamRobot <?>> robots = ConcurrentHashMap .newKeySet ();
49+ public final SimEnvTiming timing ;
50+
51+ /**
52+ *
53+ *
54+ * <h2>Constructs a new simulation arena with the specified field map of obstacles.</h2>
55+ *
56+ * <p>This constructor initializes a physics world with zero gravity and adds the provided
57+ * obstacles to the world.
58+ *
59+ * <p>It also sets up the collections for drivetrain simulations, game pieces, projectiles, and
60+ * intake simulations.
61+ *
62+ * @param obstaclesMap the season-specific field map containing the layout of obstacles for the
63+ * simulation
64+ * @param period the duration of each simulation period in seconds
65+ * @param ticksPerPeriod the number of sub-ticks to execute in each simulation period
66+ */
67+ protected SimArena (FieldMap obstaclesMap , double period , int ticksPerPeriod ) {
68+ this .timing = new SimEnvTiming (period , ticksPerPeriod );
69+ this .physicsWorld .setGravity (PhysicsWorld .ZERO_GRAVITY );
70+ for (Body obstacle : obstaclesMap .obstacles ) {
71+ this .physicsWorld .addBody (obstacle );
72+ }
73+ }
74+
75+ void withWorld (Consumer <World <Body >> worldModifier ) {
76+ try {
77+ worldLock .lock ();
78+ worldModifier .accept (physicsWorld );
79+ } finally {
80+ worldLock .unlock ();
81+ }
82+ }
83+
84+ /**
85+ *
86+ *
87+ * <h2>Registers a {@link ShamGamePiece} to the Simulation.</h2>
88+ *
89+ * <p>The user will have to call one the following methods to add the game piece to the field:
90+ * <ul>
91+ * <li>{@link ShamGamePiece#place(Translation2d)}
92+ * <li>{@link ShamGamePiece#slide(Translation2d, Translation2d)}
93+ * <li>{@link ShamGamePiece#launch(Pose3d, Translation3d, ProjectileUtil.ProjectileDynamics)}
94+ * <li>{@link ShamGamePiece#intake(Supplier, Supplier)}
95+ * </ul>
96+ *
97+ * @param variant the variant of game piece to be registered
98+ * @return the game piece that was created and registered
99+ */
100+ public GamePieceSimulation createGamePiece (GamePieceVariant variant ) {
101+ GamePieceSimulation gamePiece = new GamePieceSimulation (variant , this );
102+ this .gamePieces .add (gamePiece );
103+ return gamePiece .userControlled ();
104+ }
105+
106+ /**
107+ *
108+ *
109+ * <h2>Update the simulation world.</h2>
110+ *
111+ * <p>This method should be called ONCE in {@link IterativeRobotBase#simulationPeriodic()}
112+ */
113+ public void simulationPeriodic () {
114+ final long t0 = System .nanoTime ();
115+ competitionPeriodic ();
116+ // move through a few sub-periods in each update
117+ for (int i = 0 ; i < timing .ticksPerPeriod ; i ++) {
118+ simulationSubTick ();
119+ }
120+
121+ SmartDashboard .putNumber (
122+ "MapleArenaSimulation/Dyn4jEngineCPUTimeMS" , (System .nanoTime () - t0 ) / 1000000.0 );
123+ }
124+
125+ private void simulationSubTick () {
126+ for (final ShamRobot <?> otherRobot : robots )
127+ otherRobot .simTick ();
128+
129+ for (final ShamGamePiece gamePiece : gamePieces )
130+ gamePiece .simulationSubTick ();
131+
132+ withWorld (world -> world .update (timing .dt .in (Seconds )));
133+ }
134+
135+ /**
136+ * Returns a stream of all game pieces tracked by the arena.
137+ *
138+ * There are some helpful methods to better utilize the stream.
139+ *
140+ * <pre><code>
141+ * // Shows off getting an array of the positions of all cubes on the field
142+ * Pose3d[] gamePiecePositions = arena.gamePieces()
143+ * .filter(GamePieceState.ON_FIELD::isInState) // Only get the game pieces that are on the field
144+ * .filter(CUBE_GP::isOfVariant) // Only get the game pieces that are cubes
145+ * .filter(GamePiece::isLibraryControlled) // Only get the game pieces that are controlled by the library
146+ * .collect(GamePiece.poseStreamCollector()); // Collect the poses of the game pieces into an array
147+ * </code></pre>
148+ */
149+ public Stream <ShamGamePiece > gamePieces () {
150+ return List .copyOf (gamePieces ).stream ();
151+ }
152+
153+ /**
154+ *
155+ *
156+ * <h2>Resets the Field for Autonomous Mode.</h2>
157+ *
158+ * <p>This method clears all current game pieces from the field and places new game pieces in
159+ * their starting positions for the autonomous mode.
160+ */
161+ public void resetFieldForAuto () {
162+ gamePieces .forEach (gp -> gp .withLib (gp2 -> gp2 .delete ()));
163+ gamePieces .clear ();
164+ placeGamePiecesOnField ();
165+ }
166+
167+ /**
168+ *
169+ *
170+ * <h2>Places Game Pieces on the Field for Autonomous Mode.</h2>
171+ *
172+ * <p>This method sets up the game pieces on the field, typically in their starting positions for
173+ * autonomous mode.
174+ *
175+ * <p>It should be implemented differently for each season-specific subclass of {@link
176+ * SimArena} to reflect the unique game piece placements for that season's game.
177+ */
178+ protected abstract void placeGamePiecesOnField ();
179+
180+ /**
181+ *
182+ *
183+ * <h2>Season-Specific Actions to Execute in {@link SimArena#simulationPeriodic()}.</h2>
184+ *
185+ * <p>This method defines season-specific tasks to be executed during the {@link
186+ * SimArena#simulationPeriodic()} method.
187+ *
188+ * <p>For example:
189+ *
190+ * <ul>
191+ * <li>Updating the score counts.
192+ * <li>Simulating human player activities.
193+ * </ul>
194+ *
195+ * <p>This method should be implemented in the season-specific subclass of {@link SimArena}
196+ * to reflect the unique aspects of that season's game.
197+ */
198+ protected abstract void competitionPeriodic ();
199+
200+ /**
201+ *
202+ *
203+ * <h1>Represents an Abstract Field Map</h1>
204+ *
205+ * <p>Stores the layout of obstacles and game pieces.
206+ *
207+ * <p>For each season-specific subclass of {@link SimArena}, there should be a corresponding
208+ * subclass of this class to store the field map for that specific season's game.
209+ */
210+ public static class FieldMap {
211+ private final List <Body > obstacles = new ArrayList <>();
212+
213+ protected void addBorderLine (Translation2d startingPoint , Translation2d endingPoint ) {
214+ addCustomObstacle (
215+ Geometry .createSegment (
216+ GeometryConvertor .toDyn4jVector2 (startingPoint ),
217+ GeometryConvertor .toDyn4jVector2 (endingPoint )),
218+ new Pose2d ());
219+ }
220+
221+ protected void addRectangularObstacle (
222+ double width , double height , Pose2d absolutePositionOnField ) {
223+ addCustomObstacle (Geometry .createRectangle (width , height ), absolutePositionOnField );
224+ }
225+
226+ protected void addCustomObstacle (Convex shape , Pose2d absolutePositionOnField ) {
227+ final Body obstacle = createObstacle (shape );
228+
229+ obstacle .getTransform ().set (GeometryConvertor .toDyn4jTransform (absolutePositionOnField ));
230+
231+ obstacles .add (obstacle );
232+ }
233+
234+ private static Body createObstacle (Convex shape ) {
235+ final Body obstacle = new Body ();
236+ obstacle .setMass (MassType .INFINITE );
237+ final BodyFixture fixture = obstacle .addFixture (shape );
238+ fixture .setFriction (0.6 );
239+ fixture .setRestitution (0.3 );
240+ return obstacle ;
241+ }
242+ }
243+ }
0 commit comments