An engine for describing games built around Xerces, exprtk, and SFML.
Status: implemented (swept object-against-object collision, edge-based screen collision, riding and rule exceptions; rough spots listed)
Separate the three things that were tangled in the first prototype:
CollisionDetector).Game, via typed Commands.)CommandExecutor).In the prototype a single function both detected an edge hit and executed the action, and it parsed action tokens by walking a vector of strings with an iterator, which was fragile and hard to extend. Now makeCommand() (command.cpp) is the only place that turns a command tag into a typed command (CmdBounce, CmdStick, CmdMove, and so on), and responses are one std::visit per context.
The declarative form of the idea: the XML does not spell out an if-the-ball-touches-the-edge test. It says the ball has a collision with the vertical edge and the response is bounce(). The conditional lives in the engine’s detection, not in the data.
overlap, rectangleRectangle, circleRectangle): two rectangles use axis-aligned overlap and report the edge on the axis with the smaller overlap; a circle uses the distance from its centre to the nearest point of the rectangle, reports the side it is furthest past, and for a centre inside the rectangle the nearest side. Used for objects that already overlap when a frame starts (a hit at time 0).CollisionDetector::sweep): the two objects’ motions over the frame are reduced to one by holding B still and moving A by the difference. Rectangle against rectangle: A’s top-left corner against B grown by A’s size, entering by the slab method. Circle against rectangle: the circle’s centre against the rectangle grown by the radius, which is exact along the flat sides; if it enters beside a corner, the corner is solved as a ray against a circle of that radius, which gives the rounded corner and a time. It returns the time (0 to 1 of the frame) and the edge of B that was hit. Touching and moving apart is not a hit.Game::moveObjects): the pairs worth testing are those where one object has a rule that answers to the other (class/object, or a rule with no selector), neither is a group-mate of the other, and one is moving. Each round, every pair is swept for the time left in the frame; the earliest hit anywhere wins, all shown objects advance to that moment, that pair’s rules run, and the round repeats for the time that remains, with whatever velocities the rules left. A pair reacts at most once per frame, which bounds the loop and also stops a bounce from immediately re-triggering. With no hits, every object simply moves by its velocity once, as before.aliens.3.2) to make that possible, and for games (Galaga, Galaxian) where members will leave a formation one by one.reset(), move.*, inc(), dec() and carry() work there too (they used to be ignored), so an object can say what touching a hazard or a goal costs or scores. CommandExecutor::executeObjectCollision is handed the other object as well as the edge, which is what carry() needs.carry()). A rule with carry() gives the object the other’s velocity for the frame (Object::carry), added to its own in movement, in the sweep and in the edge checks. It is cleared at the start of every frame’s move and worked out again by that frame’s touches, so stepping off is automatic and nothing has to be undone. Pairs are judged with the carried velocity, so an object sitting still on something moving is a moving pair.unless on a rule. A rule can be passed over while the object is also touching something of another class (Game::isTouchingClass, a plain overlap test against everything in play). It is a test of where things are now, so two rules for the same moment do not depend on the order the pairs were found in. This is what lets a wide, static river kill the frog anywhere except where a log is.stick() is axis-aware: it corrects position and cancels velocity only on the touched edge’s axis, so an object pushed into the bottom wall while holding left or right keeps sliding (the alternative, where any push into a wall freezes the object, feels bad to play). It is also re-applied after the move, because the pre-move edge checks only run for a moving object and would otherwise leave a stopped object overshooting the wall by up to one frame of velocity.A collision can say <type>pixel</type>. The box or circle sweep runs first, as always; only a pair it reports as touching is then walked in half-pixel steps, and bisected, to the first step where a drawn pixel meets a solid one. The response sees the same edge as for a box. Design 34 has the decisions.
bounce() just flips one velocity component).watch-style flag on a collision that must be looked at every frame is one way out, if a game needs it.die() now means the same everywhere: not drawn, moved or collided with), because a dead object is no longer part of anything.circleRectangle edge choice that compared a coordinate against a bound minus the same coordinate. The C++ reference Pong (17) had the swept test against a moving frame of reference; this is that idea, kept behind the same detector interface.basic="basic" was a stopgap spelling for “the one general object-against-object rule”. It is gone now: a <collision> with no edge, class or object means “anything”.TODO about colliding with invisible objects: today only shown objects take part, and a hidden object does not move at all. That is what lets a projectile wait unseen and still until fire() shows it; it also means there is no hidden-but-moving-and-colliding object (a trigger zone, an enemy still off-screen). If a game needs one, visibility and “in play” have to become separate flags; fire() and die() would then flip the second.<acceleration> (see 34).Detection alternatives.
Naming and targets.
Response alternatives.
game.cpp and its review.