main()
function - here we create the shapes ( struct Shape
) and run an infinite loop, where we will update the simulation. Immediately you should pay attention to the design of the code - I write the function in a separate cpp file and declare it extern
in the place of use - so I don’t need to create a separate header file or even a separate type, which positively affects the compilation time and generally makes the code more readable: one function - one file.updateSimulation()
function. vector<Shape> updateSimulation(float const dt, vector<Shape> const shapes, float const width, float const height);
const reference
guarantees this. This is true, but the next most important principle is the purity of functions (pure function) - i.e. no side effects and a guarantee that the function will return the same values with the same input data. But, receiving the link, we cannot guarantee this. Let us consider an example. Suppose we have some function that accepts a constant link: int foo(vector<int> const & data) { return accumulate(data.begin(), data.end(), 0); }
vector<int> data{1, 1, 1}; int result{foo(data)};
foo()
accepts const &
, the data itself is not constant, which means that it can be changed before and at the time of the call to accumulate()
, for example, by another thread. That is why all the data should come as a copy. struct Vec2 { float const x; float const y; Vec2(float const x = 0.0f, float const y = 0.0f) : x{ x }, y{ y } {} // member functions }
updateSimulation()
function. It is called as follows: shapes = updateSimulation(dtStep, move(shapes), wSize.x, wSize.y);
std::move()
) - this allows you to get rid of unnecessary copies. In our case, however, this has no effect, since we operate on primitive types, and moving is equivalent to copying.shapes
, which, in fact, is a violation of the principle of lack of state. However, I believe that we can change the local variable without fear - it will not affect the result of the function, since this change remains encapsulated inside this function. vector<Shape> updateSimulation(float const dt, vector<Shape> const shapes, float const width, float const height) { // step 1 - update calculate current positions vector<Shape> const updatedShapes1{ calculatePositionsAndBounds(dt, move(shapes)) }; // step 2 - for each shape calculate cells it fits in uint32_t rows; uint32_t columns; tie(rows, columns) = getNumberOfCells(width, height); // auto [rows, columns] = getNumberOfCells(width, height); - c++17 structured bindings - not supported in vs2017 at the moment of writing vector<Shape> const updatedShapes2{ calculateCellsRanges(width, height, rows, columns, move(updatedShapes1)) }; // step 3 - put shapes in corresponding cells vector<vector<Shape>> const cellsWithShapes{ fillGrid(width, height, rows, columns, updatedShapes2) }; // step 4 - calculate collisions vector<VelocityAfterImpact> const velocityAfterImpact{ solveCollisions(move(cellsWithShapes), columns) }; // step 5- apply velocities vector<Shape> const updatedShapes3{ applyVelocities(move(updatedShapes2), velocityAfterImpact) }; return updatedShapes3; }
vector<Shape> calculatePositionsAndBounds(float const dt, vector<Shape> const shapes) { vector<Shape> updatedShapes; updatedShapes.reserve(shapes.size()); for_each(shapes.begin(), shapes.end(), [dt, &updatedShapes](Shape const shape) { Shape const newShape{ shape.id, shape.vertices, calculatePosition(shape, dt), shape.velocity, shape.bounds, shape.cellsRange, shape.color, shape.massInverse }; updatedShapes.emplace_back(newShape.id, newShape.vertices, newShape.position, newShape.velocity, calculateBounds(newShape), newShape.cellsRange, newShape.color, newShape.massInverse); }); return updatedShapes; }
for_each
algorithm is a higher order function, i.e. function that accepts other functions. In general, stl
very rich in algorithms, so knowledge of the library is very important if you write in a functional style.for
loop, but I went in the direction of visualization and readability. vector<Shape> updateOne(float const dt, vector<Shape> shapes, vector<Shape> updatedShapes) { if (shapes.size() > 0) { Shape shape{ shapes.back() }; shapes.pop_back(); Shape const newShape{ shape.id, shape.vertices, calculatePosition(shape, dt), shape.velocity, shape.bounds, shape.cellsRange, shape.color, shape.massInverse }; updatedShapes.emplace_back(newShape.id, newShape.vertices, newShape.position, newShape.velocity, calculateBounds(newShape), newShape.cellsRange, newShape.color, newShape.massInverse); } else { return updatedShapes; } return updateOne(dt, move(shapes), move(updatedShapes)); } vector<Shape> calculatePositionsAndBounds(float const dt, vector<Shape> const shapes) { return updateOne(dt, move(shapes), {}); }
calculateCellsRanges()
calculates the cells occupied by the figure and returns the changed data.fillGrid()
function, we fill each cell (in our example, the cell is just std::vector
) with corresponding shapes. Those. if the cell contains nothing, an empty vector will be returned. Later in the code, we will run through each cell, and check inside it every figure with each other for intersection. But in the figure you can see that figure a
and figure b
are (besides other cells) both in cell 2 and cell 5. This means that the check will be performed twice. Therefore, we will add logic that will say whether verification is necessary. Knowing the rows and columns make it trivial.a
and b
ceased to touch. This added a lot of complexity - you had to re-calculate the bounding box every time we moved an object. To avoid multiple rearrangements, we introduced a special battery, into which we put all the permutations, and later used this battery only once. One way or another, we had to introduce mutexes for synchronization, the code was complicated and in this form was not suitable for a functional approach. In a new attempt, we will not move objects at all, moreover, we will produce calculations only if they are really necessary. In the picture, for example, calculations are not needed, because figure b
moves faster than figure a
, i.e. they move away from each other, and sooner or later they will no longer come into contact without our participation. Of course, this is physically implausible, but if the speeds are small and / or a small simulation step is used, then it looks quite normal. If calculations are needed, we consider the changes in the velocities that occurred during the collision and return these speeds together with the figure identifier.applyVelocities()
function simply summarizes them and applies them to the object. Again, the plausibility is not in question and, quite possibly, artifacts will appear under certain conditions, but I did not notice problems with this approach on my test data. And the goal of the experiment was not at all plausible.Source: https://habr.com/ru/post/324518/
All Articles