Repository: still published as
omonien/AetherOrbits(unchanged clone URL).
Collection name: FireMonkey Animation Demos — playable samples around one idea: smooth frame loops in Delphi 13.
Since Delphi 13, FireMonkey drives TAnimation through the platform Display Link (VSync). That means you can build buttery-smooth animation loops — including for games — out of the box, without a third-party engine and without fighting a coarse TTimer.
This collection shows that path end-to-end:
| Piece | Role |
|---|---|
TGameLoop (TAnimation subclass) |
The reusable frame clock — copy one unit into your own app |
| Skia | High-quality 2D / soft-3D drawing on GPU paths where available |
| Demos | Full apps that prove the loop feels right under real load |
No black-box game framework. The clock is FMX. The pixels are Skia. Your scene logic stays plain Delphi.
src/FMXAnimation.GameLoop.pas is intentionally demo-agnostic:
- Depends only on
System.*+FMX.Types/FMX.Ani(TAnimation) - No forms, no Skia, no scene types
- Fixed timestep + optional Preferred-FPS pacing on top of Display Link
That is the only unit you need to copy for a VSync-aligned loop in your own app.
| Unit | When you need it |
|---|---|
FMXAnimation.SystemInfo |
Platform / backend / CPU labels |
FMXAnimation.Stats.Hud |
Skia stats footer overlay |
FMXAnimation.DemoShell |
Client TSkPaintBox, HUD strip split, dark segment chrome, Preferred-FPS restart |
To reuse the loop in your project:
- Copy
FMXAnimation.GameLoop.pasinto your FMX app. - Create a
TGameLoop, assignOnUpdate/OnRender. - Call
StartLoopwhen the form is shown (Rootmust be set — parent the loop to the form). - (Optional) Copy
DemoShellif you want the same paint-box / chrome patterns as the demos.
Deep dive: docs/GameLoop.md
(Display Link vs Preferred FPS vs fixed simulation timestep, Windows DWM pacing, pitfalls.)
uses
FMXAnimation.GameLoop;
// Optional for demos-style UI:
// FMXAnimation.DemoShell, FMXAnimation.Stats.Hud, FMXAnimation.SystemInfo
type
TFormMain = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormShow(Sender: TObject);
private
FGameLoop: TGameLoop;
procedure DoUpdate(const ADeltaTime: Double);
procedure DoRender;
end;
procedure TFormMain.FormCreate(Sender: TObject);
begin
// Owner = form → Parent is set so Root <> nil (required for Display Link).
FGameLoop := TGameLoop.Create(Self);
FGameLoop.OnUpdate := DoUpdate;
FGameLoop.OnRender := DoRender;
// Optional: FGameLoop.FixedTimeStep := 1 / 60; // simulation step only
end;
procedure TFormMain.FormShow(Sender: TObject);
begin
// Start when visible — not only in OnCreate.
if not FGameLoop.Running then
FGameLoop.StartLoop;
end;
procedure TFormMain.FormDestroy(Sender: TObject);
begin
FGameLoop.StopLoop;
// FGameLoop is owned by Self; no Free needed if Owner = Self
end;
procedure TFormMain.DoUpdate(const ADeltaTime: Double);
begin
// Called 0..N times per display frame with a *fixed* step (default 1/60 s).
// Advance the world here — not the pixels:
//
// Player.X := Player.X + Player.SpeedX * ADeltaTime;
// ParticleSystem.Step(ADeltaTime);
//
// Keep this free of drawing code so the sim stays deterministic and testable.
end;
procedure TFormMain.DoRender;
begin
// Called once per processed frame *after* updates. Only show the result:
//
// PaintBox.Redraw; // draw sprites / HUD / background from current state
//
// Do not advance physics here — that belongs in DoUpdate.
end;Pitfalls
| Issue | Fix |
|---|---|
| Scene freezes after first frame | Root was nil — pass the form as Owner (or set Parent) and call StartLoop from OnShow |
| Preferred FPS 30 does nothing on Windows | Set GlobalPreferredFramesPerSecond (keep PaceToPreferredFps true — default); see docs/GameLoop.md |
| Simulation too fast/slow | Change FixedTimeStep, not Preferred FPS |
Two applications share the same loop + diagnostics. Each is a full FMX project under src/.
| Demo | Project | What it shows |
|---|---|---|
| Aether Orbits | src/AetherOrbits/AetherOrbits.dproj |
Particle field, glowing orbs, pointer forces, Preferred FPS |
| Helios | src/Helios/Helios.dproj |
Soft-3D solar system, sim speed, pause, trails, click-to-focus |
Aether Orbits · ≈8s capture · MP4 · PNG
| Control | Action |
|---|---|
| Mouse / touch move | Repels nearby particles (soft force field under the pointer) |
| Click / tap | Spawns a short particle burst at the pointer |
| Preferred FPS (top bar) | Request 30, 60, or 120 frames per second |
| Stats footer | Live FPS, frame ms, Preferred value, CPU, platform, render backend |
| Control | Action |
|---|---|
| Click / tap a body | Smooth camera focus on that body + info panel |
| Click empty space / Overview | Reset camera to system overview |
| Speed (0.25× / 1× / 5× / 20×) | Simulation time scale |
| Pause / Resume | Freeze orbits (camera ease still runs) |
| Trails | Toggle orbital trail ribbons |
| Preferred FPS | Same Display Link pacing as Aether Orbits |
| Stats footer | FPS, frame ms, sim speed %, body count, platform, backend |
| Setting | Typical result |
|---|---|
| 30 | ~30 FPS (paced; useful to save work or compare) |
| 60 | Default — matches most panels and FMX’s default preferred rate |
| 120 | Up to 120 only on ProMotion / 120 Hz displays; otherwise capped by the panel |
On iPhone 16 (non-Pro) the panel is 60 Hz — choosing 120 will not go above ~60.
On Windows, the OS display link often stays at monitor refresh; the demos pace the game loop so Preferred 30 still measures ~30 FPS in the footer.
git clone --recurse-submodules https://github.com/omonien/AetherOrbits.git
cd AetherOrbitsIf you already cloned without submodules:
git submodule update --init --recursiveOpen FMXAnimationDemos.groupproj in RAD Studio / Delphi 13 (project group for FireMonkey Animation Demos).
- Aether Orbits:
src/AetherOrbits/AetherOrbits.dproj - Helios:
src/Helios/Helios.dproj - Tests (both demos + shared units):
tests/AetherOrbits.Tests.dproj
Select a platform (e.g. Win64, OSX64, iOS Device 64-bit) and run.
.\build-scripts\DelphiBuildDPROJ.ps1 -Project src\AetherOrbits\AetherOrbits.dproj -Platform Win64 -Config Debug
.\build-scripts\DelphiBuildDPROJ.ps1 -Project src\Helios\Helios.dproj -Platform Win64 -Config Debug
.\build-scripts\DelphiBuildDPROJ.ps1 -Project tests\AetherOrbits.Tests.dproj -Platform Win64 -Config Debug
.\build\Win64\Debug\AetherOrbits.Tests.exeOutput goes to build/<Platform>/<Config>/ (git-ignored).
| Platform | Notes |
|---|---|
| Windows 64 | Skia via OpenGL (or GPU path) when PreferRaster is off at startup |
| macOS | Metal enabled for Skia GPU (GlobalUseMetal) |
| iOS | Same Display Link / Metal path; responsive stats HUD for narrow screens |
The footer’s Backend line shows what is actually active (e.g. Skia Metal, Skia OpenGL, Skia Raster).
Startup flags that matter are set in each demo’s .dpr (Skia on; Windows PreferRaster off; Metal on for Apple). Details: docs/FMX-Skia-Gotchas.md.
AetherOrbits/ # GitHub repo name (stable URL)
├── src/
│ ├── FMXAnimation.GameLoop.pas # ★ shared frame loop — copy this
│ ├── FMXAnimation.SystemInfo.pas # shared diagnostics
│ ├── FMXAnimation.Stats.Hud.pas # shared stats overlay
│ ├── FMXAnimation.DemoShell.pas # shared paint-box / chrome helpers
│ ├── AetherOrbits/ # demo: orbs + particles
│ └── Helios/ # demo: solar system
├── tests/ # DUnitX (shared + both demos)
├── build-scripts/
├── docs/
│ ├── GameLoop.md # loop design & FPS concepts
│ ├── FMX-Skia-Gotchas.md
│ └── images/
├── libs/DUnitX/ # Git submodule
├── FMXAnimationDemos.groupproj
├── LICENSE # MIT
└── README.md
Separation of concerns is intentional — each layer has one job:
TGameLoop (Display Link) ← frame clock only
│ OnUpdate → *.Scene.Update ← simulation / world state (no Skia)
│ OnRender → PaintBox.Redraw
└─ OnDraw → *.Scene.Renderer ← Skia paint of current state
→ Stats.Hud ← diagnostics strip
Main.Form ← FMX controls + wiring only
DemoShell ← optional shared surface/chrome helpers
| Layer | Unit | Role |
|---|---|---|
| Timing | src/FMXAnimation.GameLoop |
Display Link + fixed timestep + Preferred pacing (shared, copyable) |
| Diagnostics | src/FMXAnimation.SystemInfo |
Platform, backend, CPU samples (shared) |
| HUD | src/FMXAnimation.Stats.Hud |
Footer text + Skia overlay (shared) |
| Demo shell | src/FMXAnimation.DemoShell |
Paint box setup, HUD strip split, dark chip chrome (shared) |
| Simulation | AetherOrbits.Scene / Helios.Scene |
Demo state — no UI, no Skia |
| Drawing | *.Scene.Renderer |
Skia paint of scene state |
| Form UI | *.Main.Form |
Form events, demo-specific controls, scene wiring |
Skia draws pixels; it does not own the frame clock. The loop does not know about orbs, planets, or Skia.
- Delphi 13 (Florence) or newer — Display Link–driven
TAnimation - Integrated Skia for the demo renderers (not required by
GameLoopitself) - Git with submodule support for DUnitX tests
- Optional: macOS / iOS deploy targets and signing as usual for FMX
| Doc | Audience |
|---|---|
| docs/GameLoop.md | Reusing / understanding TGameLoop |
| docs/FMX-Skia-Gotchas.md | Metal, PreferRaster, Root, HUD under Skia |
| docs/Delphi Style Guide EN.md | Coding conventions used in this repo |
Copyright © 2026 Olaf Monien. Released under the MIT License.
