Game modes and game states tend to have a 1:1 relationship. Think about it, if your game mode is capture the flag, your game state needs to keep track of where is the flag, is it being carried by someone, how much time until a dropped flag should be returned to its normal position, etc. If you change the mode to team deathmatch you don’t care about the above, but you need to track total kills per team to determine when the game ends. In a deathmatch game mode you want to track individual player scores to determine which single player is the winner. Each of these modes requires a different game state.
The game mode and game state are created when the level is created. So your idea of storing the pawn point in a game state and then loading a level won’t work.
Storing state in the game instance is not “cheating”. That’s actually the primary use for the game instance. The game instance is the only framework object that persists between levels (until programming subsystems were created, see 2). If you want to pass information to a new level, you have to do one of the following:
- Store info in game instance and read it from the game mode, game state, or level blueprint once the new level is loaded.
- Same as 1) but use a game instance subsystem (which was designed so that you don’t have to store anything in the game instance).
- Pass info via options string when opening the level, parse the options string in the game mode initialization.
- Something else that is actually hacky, e.g. save data to a file and then read the file within the new level.
To summarize, the current level (whether that logic is in the level blueprint, game mode or game state) determines what level to open next and what spawn point to use. Then you use one of the above options to send data to the next level and issue an OpenLevel call.