I like to explain it as, whenever I write code I want to see it work and see it work immediately. Testing something inside the running program can take a lot of effort and time, firing up the program and getting it to the exact scenario and point that triggers the code, and so is done when the feature is all written to make sure it's actually working.
But along the way, I'm a serial recompiler. I want to see everything compile and work. Now, to do that quickly I want to isolate the code as I'm writing it and call the functions to make sure they behave as expected. So pre-encountering TDD I would be keeping a separate main function that let me run what I was working on and see it work.
Effectively, I was constantly writing and throwing away unit tests. Post-TDD I have a framework that helps me write and keep those tests, so I can keep them to document the expected behaviour of those functions and even run them when I make further changes to make sure I haven't broken any past behaviour (and naturally I can modify them when I want to break past behaviour).
It also helps that practising stricter TDD tends to self-enforce a lot of
SOLID principles. The real trick comes in learning to 'read the tests', namely accepting that if testing is becoming difficult there's a reason for that. Testing becoming difficult is a
code smell, and often is indicative of code hitting a point where it must be refactored and evolved.
And whilst very useful for business applications, client/server applications especially, for programs like games ATDD (automated testing of your acceptance criteria/high level specifications) is dark witchcraft and may actually be more effort than it's worth. I have heard there is a whole team at Microsoft dedicated to writing AIs to test people's games via friend-of-a-colleague, but that's really all I know on the subject.
The tricky stuff for me comes in how testing integration with 3rd party libraries is something of an art-form in and of itself. I've yet to find a way I'm happy with, especially for C++.
In languages like C# and Java there are libraries that (ab)use the reflective features of the language to let you mock out real classes instead of interfaces, and C++ has a couple of libraries that abuse the low level memory access and inherent understandings of how specific compilers work to achieve a similar result but it's not cross-platform and can break from compiler version to compiler version (especially if you like to work with the latest and greatest of everything).
Typcially the solution involves a facade later around the 3rd party library that must be extensively tested manually, or in passing delegates/individual functions (via std::function) that are easier to mock and test. Neither of these are ideal but they are the lesser evils in my mind, at least for C++.