If you want to understand how things work, you can start programming on machine language for Z80 for example ( An 8 bit CPU ). On machine language there's no automations. You'll learn the registers of the CPU, instructions for loading, moving, manipulating data from the registers to RAM, to I/O, etc, working on binari,hexadecimal and a lot of more things just on the CPU. Depend on which system you would like to program, they have his own idiosyncrasy. On Amstad CPC, if you want to paint a pixel, you'll write from $C000 on RAM, on the other hand if you want to do the same thing on Master System or MSX(the same architecture) you'll have to talk to VDP(Visual display processor) from I/O ( $BF and $BE) and write more information on different regions on VRAM( sprite atribute table, screen map,etc). Those last one systems, have hardware sprites. That means they have automations for moving them for you.
Then, when you masters machine code you can jump to assembler. I good started point would be using WinAPE for Amstrad CPC so you can inject machine code easily on RAM.
There's a course from profesorRetroMan on youtube starting on machine code for z80 on amastradCPC. "Dez80 -- Dominando ensamblador". It's a course for learning machine code, the most low level a programmer can program, for learning the basics on retro video games.
Right now I'm programming little programs on assembler for the Master System as gif shows.
It's a very rewarding travel.