That really is the best way. Even with a multi-threaded engine, you want to use task batching, so your main loop will still be "update system A, then system B, etc."
So far as eating up CPU, you definitely want to be conscious of that, especially on mobile gaming (but even for PC gaming due to the rising popularity of mobile PCs).
First, always use sync-to-vblank. That'll keep your game loop from running faster than the display. You can use a custom frame rate controller if vblank syncing is for some reason not usable (or if you are using triple buffering). Basically put a microsecond-accurate sleep (not a busy loop) at the end of your loop after doing the buffer swap in order to keep the game from running faster than you need it to.
You can also replace the part that queries system events (the Win32 message pump, for instance) between blocking and non-blocking when you go into screens that have no animation running when the player isnt doing anything, such as a pause screen or the main menu.
getInput
instead ofwaitForInput
:) \$\endgroup\$