They're typically not even handled by the same machine, much less the same codebase. The user profile is handing by a service that deals only with users. The simulation server deals with in-game things. There may even be another session server that ties the two together.
The simulation server has an ID that corresponds to each user, so its Player
class might look something like:
struct Player {
UserId _user;
PlayerId _player;
int _health;
// etc.
};
For anything that affects the user account, the server sends a message to the user service telling it what happened, e.g.
struct UserAchievementMessage : public UserMessage {
UserId _user;
AchievementId _achievement;
uint64 _timestamp;
// etc.
};
So if the user gets an achievement for something happening in game, the simulation server just lets the user service know (usually via some kind of messaging service, e.g. RabbitMQ or the like) and then forgets about it (since it's not the simulation server's job to care about users).
If the user service has to notify any in-game matches, it can again use a messaging service (or proxy through a match service) to notify any player instances of that user that something changed. For instance, the match service knows about all matches running, who's in those matches, etc., so a user service can just ask the match service to notify all simulation servers that have an instance of that user (a player) about whatever it was that changed. Clever use of RabbitMQ or the like can remove the need for a proxy service, too.
Samller/simpler games often just have one or two servers and overlap a lot of functionality, but this is due to a lack of time/budget and not because of good engineering.
In any case, User
and Player
(or whatever terms you use) are two entirely separate things. The coupling, if any, should be as light as possible. Unique IDs are typically the way to go. Each user could have a UUID, for instance, and any Player
instance just contains that UUID. If something that happens to a Player
affects a User
, call a method or send a message to the UserManager
with the affected UUID and other details. The UserManager
can then update internal data structures, send network messages, or do whatever other implementation details it needs to do.