I would advice a few things from my experience, I made very complex games that i never finished because it becomes unmanageable over time so I came up with some solutions doing mess after mess
1-Separate your UI logic , this is very tricky to do, but someday I am planning to put a video about it, I put all my UI outside my game and re-use it in all my new games, well this is kind of hard to explain without a video at this moment, but that’s one idea. Some use libraries, some use XML or similar for that, some use imGUI , I imagined something different. Having UI logic outside your game loop code simplifies it a lot, if you can try to make a different library to put your UI content, that way it force you to separate it completely, that’s what I do with my games as much as possible, for example button creation and clicks , resizing, coloring , texturing all is outside my main game, but when a button is clicked I can get a message or I can query if the button clicked is x, y or z to take some actions, in any case , you will not have hundreds of buttons to query since it is a game.
2-Make one small game a month and then think what could be done better the next month. This really help optimize and simplify all your code, I have already made 3 different games and I am about to release my 4th one this month. This really changed the way I make and think games now.
3-About Input , I abstracted all the input outside, and call actions, well the main point is you can say movePlayer to left , right etc, with an enum or variable, and the logic that handles your input will find out what keys or buttons will need to be pushed to do that action, so in that way, I can re-assign all keys and buttons in my games using the same UI logic and input logic, you can check my games, all of them have the same logic. I also created a static Config class that have all the action/key configurations that can be re-assigned at any time , I do not know how players want to play the game so that is very important.
4-Sprite management and animation, I am really working on it now, the last game I made was a mess in that area, too complicated for my taste and very hard to follow up, so I made a new one this month. That’s the beauty of making small games, you learn exponentially.
5-Simplify and make your things re-usable as much as possible, make a workflow to minimize your manual input, with each new game you will improve that. Try to not spend too much time on this until you finish your game, when finished you will see what really needs to be improved.
6- People spend ages refactoring code, I think is not bad, but it is better to release something than spending your life refactoring. Once you release your game, think about refactoring some of the code if you can re-use it, if not make a new game with the knowledge you got.
7- For screens, you need to have some way to communicate between screens to the main game loop, so some of the methods like pausing the game when you are in the settings screen for example will be needed, so screen management is very important, try to make a generic workflow with that, there are plenty of examples in XNA/Monogame for that. In my last few games I used static variables in my main game screen so all the other screens will be able to access things directly. Not very neat but allows me to write less code.
There are a lot more things I can write from my experience. I made also a generic pool manager for any of my objects, if you check my youtube videos you will find that, there is one about object pooling and there is a link to the code for free. You can find it here : Devlog 7 - Let's Pool with Object Pooling - YouTube