Unpacking is an art, and as such there aren't clear steps to unpack any target.
I was in your situation a few years ago. I understood assembly, and code injections and hooking and whatnot, but I could not grasp unpacking.
The key is to approach unpacking analytically. You have a state A, which is a (possibly!) packed executable, and you have a desired state B which contains code you can analyze in IDA for example. You want to find where that state switch happens.
This sounds rather generic, and that's because it is. Especially in malware unpacking, it's not always 100% clear when code is unpacked. Sometimes there is no unpacked binary, but injected shellcode. Sometimes you get 3 stages of unpacking.
So then, this is not helpful. That's why you start with very simple approaches to learn the patterns.
That's where the ESP trick you mentioned comes into play. This is a trick that commonly allows you to skip code. Oftentimes, unpacking stubs are surrounded with pushad/popad to not taint the environment before the actual code runs. That's one pattern to learn - something starts with saving the execution state (more or less) - so we're interested in what happens afterwards.
Another idea: PE executables have a header. So if some unpacker unpacks the target code, does it restore the original header? The answer surprisingly is mostly yes! That's great, because now we can just watch for the header being changed and are close to the final form of the code. This can be done be setting a Hardware Breakpoint on Write on some field in the header - for example the AddressOfEntryPoint, because that's what we want to know!
Then there are so-called RunPE packers (also now referred to as 'process hollowing') - they broke up with the concept of unpacking as in a small stub that rebuilds the original executable in-placce but instead create a new (possibly unrelated) process, then rewrite that new process to the state of the original binary.
So how do we handle that? We watch for APIs that are used. We watch for CreateProcess(CREATE_SUSPENDED), we watch WriteProcessMemory, we watch ResumeThread. Of course, there are dozens of variations (ZwWriteVirtualMemory, ZwResumeProcess, ZwCreateProcess and so on) but the key steps are the same: Create some new process, modify it, resume execution.
Then there are examples that simply don't fit a scheme themselves - what to do with them? Follow their code flow! Look until you see something you can use. Sometimes you'll end up with some dumped shellcode from WriteProcessMemory, but that's the 'final' stage B, there is nothing else.
This is a long answer but my point is: Unpacking is simple - packed code is transformed into unpacked code. There is no magic between these steps, so follow the code and think outside the box.
Sometimes all you need is a breakpoint on VirtualFree() to catch the unpacked code being freed. Sometimes you just need a breakpoint on VirtualAlloc() and watch what is written there. By thinking about what CAN happen between state A and B you'll find points to 'ambush' the unpacked code so to speak.