Author Topic: Multiplayer Tutorial  (Read 2197 times)

0 Members and 1 Guest are viewing this topic.

Offline Builderboy

  • Physics Guru
  • CoT Emeritus
  • LV13 Extreme Addict (Next: 9001)
  • *
  • Posts: 5673
  • Rating: +613/-9
  • Would you kindly?
    • View Profile
Multiplayer Tutorial
« on: July 22, 2012, 01:57:12 pm »
So you want to implement multiplayer into your game?  Having trouble with all the specifics?  Well look no further because this tutorial will go over all the routines, ideas, and methods you will need in order to successfully implement multiplayer functionality into your Axe (or Asm!) game!

Ideas:

The hardest part of making a multiplayer game is to keep the two or more game experiences synced up.  Many different methods can be used to do this, and we should learn at least a little bit about all of them before deciding how to handle multiplayer on our calculator.  Traditionally in games like first person shooters, or other types of actions games for the computer, a method is used that relies on client and server based interactions.  The client is the master control computer, doing all the calculations for the game state.  The client computers simply send their inputs to the client, and get back the current game state to display.  Unfortunately this requires sending a fair amount of data, and most games today use advanced prediction algorithms to give the clients the illusion of less latency than they actually have.  This method would be an ok solution to any calculator game that doesn't have a lot going on, but sending information over the link port isn't the fastest thing in the world, and I think we can do better.

The method described in the previous paragraph is used almost exclusively in the gaming environment.  It works well for most cases, but is not used in one very specific case: RTS games.  In RTS games, there are hundreds, sometimes thousands of units on the screen and in the game at any given time.  This would require huge amounts of data to be sent each frame if the traditional client-serer method was used.  Instead, RTS games use a different method that is more suitable for cases when game states require a lot of data to represent.  Instead of using a client server approach, RTS games have all parties on equal footing, with no server, just clients.  Each state of the game, each client sends its input or commands to ever other computer.  After each computer has the input of the other clients, each computer uses the input to simulate the next game step.  This idea relies upon the fact that if the game is 100% deterministic, and if for each frame each game uses the exact same input, the game states should evolve in the same way, even though the actual game state is never being sent from client to client!  This is the method I will propose in this tutorial, as it is a method that allows for low amount of data to be sent over the link port, and a small amount of additional coding to further iterate the game state.

The game state cycle

Using our new approach, there are several things that need to happen during a single game frame update.  They would look something like this:

Code: [Select]
Get input from player 1
Get input from player 2
Simulate actions of player 1
Simulate actions of player 2

The order of the actions only matters if one of the actions can influence the results of another.  For example it does not matter in what order input is attained, because (for the most part) the input you give does not depend on what input your opponent gives (for a single frame).  However, simulating the actions of players can affect other players.  For example, lets say one calculator simulates player 1 and then player 2, and the other calculator does the opposite.  On calculator 1, player 1 might move forward to block the movement of player 2.  On calculator 2, player 2 would be simulated first, allowing player 2 to move before player 1 could block them.  This would be disastrous, as remember, we are not sending the game state, only inputs, and that entire concept relies upon the fact that the game states remain the same.  If the two games get out of sync even a little bit, the changes can cumulate until the two players are playing completely different games!  For this reason, the order of simulating actions must remain the same for both calculators.

Getting the input

There are two steps to getting the input for the game.  First you need to get player 1's input, and then player 2's.  But you can make a simplification assuming that one input does not affect the other.  Instead of getting player 1's input and then player 2's, first get your own input and then your opponents.  This way the code can stay more the same for both players, since they will both be doing much of the same thing.  Getting your own input is simple enough, it is the getting of your opponents input that is a bit tricky.  In the following example, we will assume the input to be sent is a simple key variable K:


Code: [Select]
GetKey->K          //Get input from my player

If MyPlayer = 1              //If i am player 1
  K->K1                        //store my input into the player 1 input var
  Repeat Send(K1,1000)  //and wait until I can send it to my opponent
  End
  Repeat Get->K2-1        //then wait until I get the input from my opponent
  End
Else
  K->K2
  Repeat Get->K1-1        //the other player needs to recieve first before sending!
  End
  Repeat Send(K2,1000)
  End
End

This code uses several variables.  K represents the input you are giving to your own calculator.  K1 and K2 represent the inputs of player 1 and player 2.  MyPlayer represents the player number of the calculator (1 or a 2).  Whenever one of the calculators reaches this piece of code, it will first get the input from the player controlling this calculator, and then it will wait and try to talk to the other calculator.  Once both calculators reach that part of the code, they will exchange their inputs and move on.  Also, note how similar the get and send parts are.  If your input was more complicated and required more code, it might be useful to put the code in a subroutine in order to cut down size.

Simulating The Game:

The previous routines and ideas will get you the input you need, but if you do not simulate the game correctly, your two simulations will diverge and the game will be lost.  The only thing you need to ensure is that given identical inputs, the simulation phase must produce identical results given identical and initial game state.  This is not too hard to attain, just make sure that instead of simulating your own player and then your opponents, you simulate player 1 and then player 2.  The harder part is simply keeping track of all your variables, and for that I will provide a quick suggestion for Axe based systems.

Set aside 2 spaces of 56 bytes, 1 space for each player.  We will call place 1 P1 and place 2 P2.  Both of these memory locations need to be separate from your regular variable space.  Our movement code will not do anything fancy with it's variable access, but we will use #Realloc() and Exch() to help us out.  Move() will also be our subroutine that handles the movement of a single player.

Code: [Select]
#Realloc(°P1)
Move()
Exch(°P1,°P2,56)
Move()
Exch(°P1,°P2,56)
#Realloc()

Now the Move() subroutine just uses default variables, but because we use #Realloc(), we can change its effect to modify a player's variables instead!  And when we exchange the player data, we can use the same Move() subroutine for both players!

Global Variables:

Now with all this memory swapping going on, we will end up with 3 different variable spaces.  One for the regular #Realloc() space, one for °P1 and one for °P2.  This can get a little annoying when trying to access data from a different sector, because you really don't have a simple way to access it.  That is why I propose the use of Axe custom variables, as Global Variables.  They are unaffected by the #Realloc() command, and so can always be accessed by any part of the program at any time, and they will always be the same variable.  

Now this comes in extra use when it comes to input.  Remember those variables K1 and K2 we were using before?  Those are actually special types of global variables that refer to actual variables in a specific variable space.  Specifically K1 refers to the K of °P1, and K2 refers to the K of °P2.  This way, when it comes time to Move() each player, the K in their variable space will already have the input data needed!  I recommend using these types of global variables frequently in order to help different variable spaces talk to each other.  It might even be useful to create a whole set of global variables for °P2, since those variables will always point to the opponent whenever Move() is called.  

Conclusion:

That's pretty much it!  Using this knowledge and coding techniques, it should be relatively simple to make a simple multiplayer game, and when you decide to start a detailed in depth game with fully featured multiplayer, you will have the ideas to help you along the way!
« Last Edit: July 22, 2012, 01:57:37 pm by Builderboy »