Here are a few points how complexity happens and what to do against it, the list is by far not complete, and the solutions are not cookbook-recipes. Your mileage may vary dramatically.
- Interface first, code second: This is probably the most important point. When starting to work on a new feature or subsystem, it's tempting to get the most complicated implementation details out of the way first. Resist!!! Instead concentrate on the interface first because that's the only thing that matters to the other programmers which are "honored" to use your creation in the future. Step back, look at the bigger picture, and ask yourself: If I were one of them, what functionality would I expect from the subsystem and what would I want the class interfaces to look like? Repeat this after you designed each class interface: "If I would need to work with this class, would I be happy with its interface or not?". Maybe let your fellow programmers look at your classes. Only when you are completely happy with your class interfaces, start to think about the implementation. The good thing of all this upfront work is, once you have a good class interface, the implementation behind it is usually obvious, and "writes itself".
- Solve the problem at hand, not future problems: Often programmers want to "do it right once and for all" and create super-flexible subsystems which solve all types of similar problems which may show up in the future. Forget it, never works. Just accept that you cannot foresee the future. Simply write clean code which solves exactly the one problem you have at the moment. Only care about future problem when they actually show up. Then reuse existing code if it makes sense. Writing reusable code is something completely different then writing code which intends to solve imaginary future problems.
- Pocket fluff adds up over time: Some subsystems have a trend to grow complex over time. The reason is that programmers that use the subsystem need to add little tweaks, like adding a few new methods, or making a method virtual, or making a method public or protected. This is almost inevitable, and it would be wrong to completely forbid such tweaks. However, it is very important to not let this get out of control! When a subsystem requires many of such tweaks, then this is an indication that something is wrong with the subsystem's design, and a refactoring may be in order. Don't be afraid to cut back superfluous functionality, or to throw the entire subsystem away and do a clean rewrite with the new requirements in mind. The earlier such a refactoring happens, the better. Also, if you intend to do such tweaks to a subsystem, first check back with the guy who maintains or originally wrote the subsystem so he knows what's going on.
- Newbie programmers love complex stuff: Unexperienced programmers have a strong tendency towards overcomplicated solutions. Even worse are unexperienced prodigy programmers, because they are convinced they are good, when in reality they still need 10 years of real-world-experience to be really good. There's no simple solution to this, experience cannot be injected but must be learned over many years.
- You'll need 3 versions to get it right: That's because you can plan and design ahead as much as you want, you won't know what you actually wanted to achieve until the result is in daily use. Only then will all the little imperfections, design issues and interface bugs show up. When this happens, don't hesitate to throw away and rewrite as soon as realistically possible (often minor nuisances like a "milestone plan" stand in the way, but fix the problem as soon as possible or it will haunt you until eternity).
- Know what's out there (and use it): Unexperienced programmers tend to waste too much time reinventing wheels (and making them square instead of round). Sometimes it actually IS better to reinvent a more specialized wheel, but for all the boring stuff, don't be proud and just use what's already there. Make sure you have a good toolbox of utility classes that simplify your daily work, like various container classes, or a good string class, and know how to use them! If you find yourself writing more lines code with mundane, repetitive stuff then actual feature-related code, then you need a better toolbox of low level utility classes!
- Don't rely on UML (too much): UML is good and fine for doing the very first design steps and getting a grasp of a problem. But don't use UML to design your subsystem down to the method and member level. You will most likely end up with terribly complex class interfaces on the source code level. What looks simple as an UML diagram is often still too complex as source code. Remember that your fellow programmers don't use your subsystem by drawing UML diagrams but by hacking lines of code into the machine! In fact, if your subsystem can only be understood by looking at its UML diagram, you already lost.
- Know your target audience and their work flows: When working as a programmer on a game project, the "target audience" for a feature may be other programmers, level designers, graphics artists or (most importantly) the gamer. Concentrate your first design efforts on that. How will the audience use your new feature? What will their work flow look like? Sit together with a level designer, graphics guy or tester and let them show you how they work or how they would like something to work. Don't expect them though to tell you exactly what to do. It's your job as a programmer to formalize their (usually completely unrealistic) expectations ;)