A small 2D collision demo built with MonoGame, .NET 9, and the Separating Axis Theorem (SAT). The project includes rotatable rectangles, circles, collision states, and an obstacle that prevents the player-controlled rectangle from passing through it.
Use the arrow keys to move the player rectangle. Yellow indicates Touching, and red indicates Overlapping.
The Separating Axis Theorem is a technique for detecting collisions between convex polygons. For every relevant axis, usually an edge normal, the algorithm projects the vertices of both polygons onto that axis and compares the resulting intervals.
- If at least one axis has a gap between the intervals, the objects are
Separated. - If the intervals only meet at their boundaries, the state is
Touching. - If the intervals overlap on every tested axis, the state is
Overlapping.
Two rotated rectangles must test the axes from both rectangles. For circle–rectangle collision, the implementation checks whether the circle center is inside the rectangle and, if it is not, measures the distance to the nearest edge. Although the geometry calculations differ, both approaches return the same CollisionKind.
| State | Meaning |
|---|---|
Separated |
There is a gap between the colliders. |
Touching |
The colliders meet without penetration. |
Overlapping |
The colliders penetrate each other. |
SatCollider.Intersects is available when a boolean result is enough and any result other than Separated counts as an intersection.
- .NET SDK 9 (
9.0.200or later is required to build the.slnxfile from the command line) - An environment supported by MonoGame DesktopGL: Windows, macOS, or Linux
From the repository root, run:
cd XNA-Monogame-Sat-System
dotnet tool restore --tool-manifest Desktop/.config/dotnet-tools.json
dotnet run --project Desktop/Desktop.csprojThe first command restores MGCB, which builds the MonoGame content assets. It is local to the project, so no global MonoGame or MGCB installation is required.
The DesktopGL host is Desktop/Desktop.csproj and targets net9.0. The modern solution file is XNA Monogame Sat System/XNA Monogame Sat System.slnx; open it with a recent version of Visual Studio, Rider, or VS Code. Building the .slnx file from the command line requires .NET SDK 9.0.200 or later. With an earlier .NET 9 SDK, build or run the .csproj directly.
| Path | Responsibility |
|---|---|
Desktop/ |
DesktopGL host, manifest, icons, and local tools. |
XNA Monogame Sat System/GameTester.cs |
Creates, updates, and draws the demo scene. |
XNA Monogame Sat System/PlayerControlledRectangle.cs |
Controls the player rectangle and resolves movement blocking. |
XNA Monogame Sat System/VerticallyOscillatingCircle.cs |
Updates the vertically moving circle. |
XNA Monogame Sat System/Sat/ |
Colliders, SAT logic, collision states, and the collision world. |
The following sequence describes one frame of the demo and points to the source files that perform each part of the work:
GameTestercreates the player rectangle, the rotating rectangle, the static rectangle, and the circles.- The colliders are registered in
CollisionWorld.Instance. The static rectangle receivesBlocksMovement = true; the other objects still detect collisions but do not block the player. - Every frame,
PlayerControlledRectangle.Update()reads the arrow keys, tries to move the player along the X axis and then the Y axis, and applies its rotation. - Before accepting a movement or rotation,
PlayerControlledRectanglequeriesCollisionWorld. The world checks only colliders marked withBlocksMovementand callsGetCollisionKind. - If an attempted change would result in
Overlapping, the component performs a binary search between the previous and attempted values. It stops at the last valid value—usuallyTouching—which lets the player slide along an obstacle edge. - In
Sat/CollisionWorld.cs,HasBlockingOverlapcallscollider.GetCollisionKind(otherCollider). The concrete collider selects the appropriate collision calculation; this is the entry point into the SAT implementation. - For two rectangles,
Sat/SatRectangle.cscallsGetRectangleCollisionKind. It first callsRecalculateGeometryfor both rectangles, producing their four world-space vertices and two edge-normal axes. GetPolygonCollisionKindthen tests both sets of axes. This is the core SAT step: an axis from the first rectangle is tested, then an axis from the other rectangle is tested. Testing both is necessary because a separating gap may exist only on an axis from the other rotated rectangle.- For each axis,
GetProjectionCollisionKindcallsProject.Projectcalculates the dot product of every vertex with that axis and returns the smallest and largest values: the interval occupied by that polygon on the axis.ClassifySignedDistanceinSat/SatCollider.csconverts the interval comparison intoSeparated,Touching, orOverlapping. A singleSeparatedaxis ends the SAT test immediately. - Rectangle–circle collision uses
GetCircleCollisionKindinSat/SatRectangle.csinstead. It checks whether the circle center is inside the rectangle; otherwise, it finds the nearest rectangle edge and compares that distance with the circle radius. Circle–circle collision is handled byGetCircleCollisionKindinSat/SatCircle.cs, which compares the distance between centers with the combined radii. - Back in
GameTester,UpdateCollisionColorsevaluates the scene collisions and chooses a color for each object. Finally,Drawrenders the instructions, FPS counter, and colliders.
Steps 7 through 9 are where the Separating Axis Theorem is applied. Reading SatRectangle.GetRectangleCollisionKind, GetPolygonCollisionKind, GetProjectionCollisionKind, and Project in that order follows the complete rectangle–rectangle SAT path in the code.
PlayerControlledRectangle is a gameplay helper. It encapsulates a SatRectangle in its Collider property and keeps player-specific behavior out of GameTester:
- keyboard input;
- axis-by-axis movement;
- rotation;
CollisionWorldqueries;- prevention of
Overlappingagainst obstacles; - drawing and color changes for its collider.
Because of this, scene code does not have to pass an obstacle on every update:
_playerRectangle.Update();To make a collider block movement, mark it and register it in the world:
wall.BlocksMovement = true;
CollisionWorld.Instance.Register(wall);var firstRectangle = new SatRectangle(position, size, GraphicsDevice);
var secondRectangle = new SatRectangle(otherPosition, otherSize, GraphicsDevice);
var collisionKind = firstRectangle.GetCollisionKind(secondRectangle);
if (collisionKind == CollisionKind.Overlapping)
{
// React to the objects penetrating each other.
}SatCircle exposes the same public collision API. The concrete shape decides internally which geometry calculation to use.
Colors, on-screen text, and the FPS counter exist to make the collision logic easy to observe:
- yellow for
Touching; - red for
Overlapping; - default colors for
Separatedobjects.
These colors are not a required part of a game. In a real project, the same collision state could trigger damage, scoring, physics, sound, events, or removal of an element from the screen. This solution separates detection (SatCollider and CollisionWorld) from the visual response used by the demo.
MIT License.
- Mauricio Terra — mauriciomta@gmail.com
- Marcelo Belkiman — marcelobelkiman@gmail.com
