Advertisement

Code design for multi & single player

Started by November 01, 2018 09:36 PM
9 comments, last by udim 6 years ago

Hello guys! i am trying to make my first game that involve multiplayer and singleplayer. The game is a shooting game, you can move and shoot other players. in singleplayer you will shoot AI and in multiplayer you will shoot other real players.
Right now i am designing the code (UML) and i ran into a problem - I am trying to design the Player class, the problem is that this class is different between singleplayer and multiplayer. why? lets give an example:
The moving function on singleplayer will just move the player on the screen. in multiplayer it will move the player & send the new location to the server
Another example is when shooting players. In singleplayer when the player shoots another player It will just damage the other player, in multiplayer when shooting other player, the client will send to the server the action of shooting and the server will tell the client if the player actually hit the target and how much should it damage. In both single&multi of course you will play sound and trigger blood animation and so on

My point is that although you have similar code between multi&single the implementation is still different between single&multi
how can you design class like the Player class in these kind of games? Is inheritance is the correct answer? if so how would you design it?

Thank you :)

You can design games so that they are always handled the same way.  Whenever I review designs at work I frequently remind the team that if a game is single player, that means that TODAY it is single player, but might not be tomorrow.  I remind them that a single player game should still be written as a multi-player game, it happens to currently have a player count of one, but designs can change over time.

Advertisement

There's no reason not to use the same class, just keep track of whether or not the game is in single player mode or multi player mode, and if in single player mode, don't try to execute multi-player only code. 

e.g. use separate functions/methods for single and multiplayer mode(inside the same class) AND/OR simply check a bool IsModeSingle == true? inside singlular function calls.

 

If you're using a client-server architecture, the client and server will mostly want to run the same code - so the client can predict events before they're received from the server.

The difference is that server constantly serialises the entire game-state and synchronises it over to each client (usually as state-deltas/patches), while the client deserialises the game state and either rewinds into the past with interpolation, or fast-forwards into the future with extrapolation. Some small parts of the code might then be marked as client-only / server-only / not-run-during-extrapolation / etc... But most of the gameplay code is unaware of the networking. 

The bibles for this style are Quake 3, Source / Counter-Strike and Doom 3.

If you're doing a peer-to-peer model, you can use a similar architecture but with each peer being a mini-server that's authoritative over only some of the game objects, or you can use a completely different architecture. 

The architecture that you described, where each game object needs to contain specific network message passing code, is very error prone, plus probably quite inefficient. It's worth studying a broad range of different architectures before you commit to that style. 

You definitely don't want to put networking logic into you Player class. On this class just focus in dealing with the player state (position, health, isJumping?, isCrouched?, etc).

Ok, but you still need to send the changed state to the server, who will do that? Well, that depends on how you will model the rest of the game, but to pass the changes to the object responsible for sending the data I would advise you to use the Observer Pattern. This way, the player would broadcast his changes to any interested observer, including potential objects that want to send this info to the server ;D. PS.: You can even make your rendering layer observe the player to render the changes as well.

Also, is important to know that sending the player state to the server is a vulnerability that can be exploit by cheaters. The most safe way is to send the player inputs then the server make the calculations. To know more, search about Authoritative Server x non-Authoritative Server. ;)

There are many ways to do it. An observer is one, particularly when all your events go through a central message bus or similar hub. Adding a few function calls at key points of the architecture where commands are processed is another approach. Sending deltas of a large state can work when the game state fits a table or tree format. Turn based games can update every turn.

The details about if every game client should know about the entire world is highly dependent on the game you are making.  Expansive worlds can't handle it, and games where occlusion or "fog of war" equates to zero knowledge mean cheaters can exploit it.  But many game genres expose the entire world and the entire simulation to all participants, consider platformers or platform-combat like Smash Bros, sports games, racing games, and board games where the entire state is known and shared.

Advertisement
3 hours ago, hugolnx said:

You definitely don't want to put networking logic into you Player class. On this class just focus in dealing with the player state (position, health, isJumping?, isCrouched?, etc).

Ok, but you still need to send the changed state to the server, who will do that? Well, that depends on how you will model the rest of the game, but to pass the changes to the object responsible for sending the data I would advise you to use the Observer Pattern. This way, the player would broadcast his changes to any interested observer, including potential objects that want to send this info to the server ;D. PS.: You can even make your rendering layer observe the player to render the changes as well.

Also, is important to know that sending the player state to the server is a vulnerability that can be exploit by cheaters. The most safe way is to send the player inputs then the server make the calculations. To know more, search about Authoritative Server x non-Authoritative Server. ;)

So in the observer pattern, the Player class will send the input? What if the Player class depends on the server answer? how will it fit with the observer pattern and the singleplayer?
So for an example - shooting, the player will send its aiming direction and its position. So on multiplayer i guess that the client will wait for the server for its response whenever the player hit or not. when on singleplayer after taking the shot everything is done.

We can approach this also from a different direction, on multiplayer when shooting the Player will observer to the server about its input, and without waiting for the server response will damage the player's target and trigger the blood animation, sound affects etc.. But the server still need to let the client know if the player actually hit, how will you consider that on the design?

Maybe you know a tutorial or just a UML design to post here?

 

4 hours ago, Hodgman said:

If you're using a client-server architecture, the client and server will mostly want to run the same code - so the client can predict events before they're received from the server.

The difference is that server constantly serialises the entire game-state and synchronises it over to each client (usually as state-deltas/patches), while the client deserialises the game state and either rewinds into the past with interpolation, or fast-forwards into the future with extrapolation. Some small parts of the code might then be marked as client-only / server-only / not-run-during-extrapolation / etc... But most of the gameplay code is unaware of the networking. 

The bibles for this style are Quake 3, Source / Counter-Strike and Doom 3.

If you're doing a peer-to-peer model, you can use a similar architecture but with each peer being a mini-server that's authoritative over only some of the game objects, or you can use a completely different architecture. 

The architecture that you described, where each game object needs to contain specific network message passing code, is very error prone, plus probably quite inefficient. It's worth studying a broad range of different architectures before you commit to that style. 

Quote

The architecture that you described, where each game object needs to contain specific network message passing code, is very error prone, plus probably quite inefficient. It's worth studying a broad range of different architectures before you commit to that style.

Yeah i kind of figure it out when started on the design, can you give me a direction here on how can i approach this?

Split code in the player between understanding what the user is doing (that is translating user input to an action), and performing an action. You can use different methods in the same class, having a "part-of" object in the player that performs the action, or have more elaborate stuff there.

You may want to design it as a multi-player program, and then afterwards see how single-user play fits in it.

 

Quote

So in the observer pattern, the Player class will send the input? What if the Player class depends on the server answer? how will it fit with the observer pattern and the singleplayer?

You have to be careful to split the responsabilities well. Here I'm thinking of the player as the object that will represent what a player can or can not do. Thinking on the messages/methods a player could receive I thinking of:

  • Shoot()
  • Jump()
  • Crouch()
  • TakeHit(double damage)
  • RotateAim(double verticalAngle, double horizontalAngle)

So, in this way you can ensure that the state of the player will always be valid through those methods. As an example, on CounterStrike the player can't shoot while jumping, right? So, the object that will guarantee this is the player, the implementation options are many, you can do something like:


public boolean shoot() {
  if(this.state == State.JUMPING) {
    return false;
  } else {
    this.state = State.SHOOTING;
    // do some logic like decrease ammo or something
    return true;
  }
}

This way, you are protecting the player state, if the player press the shoot button while jumping, the player will return false, indicating the player could not do the action. Ok, so I'm saying this just to be clear about the responsibility of the player class, it basically encapsulates the player state management. Those concepts are related to Object Oriented Programming, but getting back to the original problem...

Quote

So for an example - shooting, the player will send its aiming direction and its position. So on multiplayer i guess that the client will wait for the server for its response whenever the player hit or not. when on singleplayer after taking the shot everything is done.

As @frob said, ideally most of your code should be the same besides being played singleplayer or multiplayer. That's also related to Object Oriented Programming principles, you must try to do your architecture in a way that you reuse the code that is common. Ok, but how to do that? I'll make a kind of pseudo-code implementation trying to illustrate. First let's think a single player game, than we expand the idea.

So, you will need an object that will know all the players and will handle the interactions with each other, let's call this class GameWorld. Also let's imagine a class responsible to update the screen, the PlayerRenderer class. And finally we will have a class that knows how to listen to the keyboard (let's imagine that shooting and aiming is also done through Keyboard and that we have multiple players on the same keyboard), we can name this class as PlayerInput. Ok, the responsibilities are there, but how tie them together?

First of all, the PlayerInput must know the Player in order to command him to shoot, jump or crouch when the proper key are pressed. So to do that we can just pass the player instance and the keys on the keyboard on PlayerInput constructor and trust that the PlayerInput instance will listen to the keyboard and call the proper player proper methods. Something like that:


public class PlayerInput {
  private Player player;
  private String shootKey;
  public PlayerInput(Player player, String shootKey) {
    this.player = player;
    this.shootKey = shootKey;
  }

  // This method may be called on gameloop update or something
  public void checkKeys() {
    if (Keyboard.isPressed(shootKey)) {
       this.player.shoot();
    }
  }
}

// Usage
PlayerInput p1Input = new PlayerInput(player1, "CTRL");
PlayerInput p2Input = new PlayerInput(player2, "ALT");
while(true) {
  p1Input.checkKeys();
  p2Input.checkKeys();
  sleep(1/60);
}

Ok, so our player is shooting now, but no one is being hit, neither the shooting animation are being played. The only one who know about all players is the GameWorld, so it would be a good idea to warn him too so he can check if any player was hit, we can do the same approach of making the player receive GameWorld on constructor and making him call a method on GameWorld like (playerShooting(player, direction)), but think that the PlayerRenderer will also need that information to play the animation, this means that the player would have to know PlayerRenderer and GameWorld. As multiple objects are interested on player events we can create a interface named PlayerObserver which will have the method shot(Player player, float direction). So now, GameWorld and PlayerRender will implement this interface and Player will broadcast the shoot to all interested objects. The implementation would be something like this:


public interface PlayerObserver {
  public void shoot(Player player, double direction);
}

public class Player {
  private double aimDirection;
  private List<PlayerObserver> observers;
  public Player(/* receive attributes */) {
    // assign those attributes (health, player name, etc)
  }
  
  public void notify(PlayerObserver observer) {
    this.observers.add(observer);
  }
  
  public void shoot() {
    if (this.state == State.JUMPING) {
      // do nothing
    } else {
      for(int i = 0; i < observers.getLength(); i++) {
        observers.get(i).shoot(this, aimDirection);
      }
    }
  }
}

public class GameWorld implements PlayerObserver {
  public void shoot(Player player, double direction) {
    Player damagedPlayer = this.findPlayerOnDirection(direction);
    if(damagedPlayer != null) {
      damagedPlayer.beingHit();
    }
  }
}

public class PlayerRenderer implements PlayerObserver {
  public void shoot(Player player, double direction) {
    this.renderShootingAnimation(direction);
  }
}

// Usage
Player player1 = new Player();
Player player2 = new Player();
PlayerInput p1Input = new PlayerInput(player1);
PlayerInput p2Input = new PlayerInput(player2);
GameWorld world = new GameWorld(player1, player2);
PlayerRenderer p1Renderer = new PlayerRenderer();
PlayerRenderer p2Renderer = new PlayerRenderer();
player1.notify(world);
player2.notify(world);
player1.notify(p1Renderer);
player2.notify(p2Renderer);

while(true) {
  p1Input.checkKeys();
  p2Input.checkKeys();
  gameWorld.update();
  sleep(1/60);
}

This is some kind of java pseudo-code that I wrote from my head just to illustrate it probably will not compile in java (It has been a while since I stop working with java). But the main idea is that player is not tied with PlayerRenderer nor GameWorld. What we gain from that? Now we can have other kinds of listeners, like GameServer, who will listen a Player shoot and will send to the server, now talking about a multiplayer game.

And about the input on a multiplayer game? You will not need the player2 PlayerInput anymore, instead you will have a NetworkPlayerInput, which will listen to the server (instead of listening to the Keyboard) and will call the shoot(), jump() and the other methods.

So as you can seen, just thinking a little bit on the architecture we can switch to a multiplayer game reusing almost all the code we have done except for the network specifics. But this example are not considering network delay, this would work if the two GameWorlds on the two computers are always synced. That not the case at all, so instead of the server just forward the player inputs, it will have a GameWorld instance that will be used as source of truth, the GameWorld on the player computer will have to obey what the server GameWorld is saying as the truth. This will add a new way that the GameWorld on the client will be updated because the server state will override the client state.

Quote

 We can approach this also from a different direction, on multiplayer when shooting the Player will observer to the server about its input, and without waiting for the server response will damage the player's target and trigger the blood animation, sound affects etc.. But the server still need to let the client know if the player actually hit, how will you consider that on the design?

I would adapt the client game world to receive state sync events which would basically override the current GameWorld state to the new one (sent by the game server). This way the NetworkPlayerInput will not be needed anymore.

Quote

Maybe you know a tutorial or just a UML design to post here?

The book Development and Deployment of Multiplayer Online Games explain in details the problems and most common techniques to deal with multiplayer online games.

The tutorial I know that explain in a simpler way is this one: http://buildnewgames.com/real-time-multiplayer/

 

This is a complicated topic, I suggest you start by a simpler game like a pacman online on a local network so you can learn how to propagate the state from server to the clients. After that you can try to put on a real server it will feel really laggy, then you use the book techniques to improve it. After understanding the techniques it will be much easier to create your FPS. :)

PS.: I'm not an expert on the subject, I just work with web applications for a long time and I'm studying game servers specific techniques right now. hehe

Hope it helped ;)

 

 

@ hugolnx

Wow thx alot! that helps, appreciate the effort!

This topic is closed to new replies.

Advertisement