Surprise workshop |
Top Previous Next |
If you came to this page looking for a surprise, you won't be disappointed: we've got a huge surprise this month indeed. Ven from Mesetts Software, aka EpsiloN at the forum, has created a fantastic multiplayer workshop, and he has asked me if I'd like to share it with the AUM readers. Since his workshop is outstanding indeed, explaining the inner multiplayer works in great detail, you bet that I said YES! Don't worry, the AI workshop will be resumed in the next magazine issue.
So there you have it, a great multiplayer example that you can build on. You can get the fully functional demo project in this month's resources. And don't worry, Aum's multiplayer template hasn't been abandoned. In fact, it has been beefed once again, gaining a lot of new features. You'll find all the details about the new template version in the new "Templates step by step" section of the AUM.
Multiplayer - Basic Extensive Tutorial
What is this about?
This tutorial will show you how to make a client-server architecture with a proper interpolation (to a past position), a better way of packing client input into a single skill (or variable) and how to handle client joining/movement/turning and shooting.
It will extend further with a server list (at the very least) and hopefully, one day, to a FPS system.
I've been thinking also about a point & click isometric system (think Diablo 2), its even more simple, but I'm extremely limited of time right now.
This code will be used for my own project (Top-Down isometric zombie shooter) and I'm writing the tutorial while I'm developing my project, so don't be afraid to use it as is for full games.
A forum user pointed out the directory structure isn't clear.
Basically, its all in one folder, and it has only one sub-folder called 'Scripts', where you will keep two '.c' files named 'system.c' and 'def.c'. 'System.c' contains the MP system and 'def.c' contains the declaration for the project. 'main.c' in the Main folder is the file running our app (containing the 'main()' function) and that's it.
Player model, mouse pointer and level files are all in the main folder...
The beginning...
Lets start with the main() function. Create a file named main.c and add this function in it:
#include <acknex.h> #include <default.c>
function main() { fps_max = 60; vec_set(screen_size,vector(640,340,0)); vec_set(screen_color,vector(50,1,1)); // dark blue
wait(3); level_load("blank.wmb"); wait(3);
while(1) { if(connection == 0) { draw_text("Not connected",10,10,vector(0,115,210)); } if(connection == 2) { draw_text("Client",10,10,vector(0,115,210)); } if(connection == 3) { draw_text("Server",10,10,vector(0,115,210)); } wait(1); } }
We start with a simple main() function that sets the maximum Frames Per Second to 60, screen size to 640x340 pixels (you have to be able to put two windows side by side, for easier testing), background screen color to dark blue. We load a blank level consisting of a ground plane with a default texture, at Z=0. And then we write a string on the screen each frame, depending on the connection status (either not connected/ server or a client).
Now we're ready to start making the Multiplayer system.
First, add to the main() function, right before the while(1) loop:
on_x = initServer; on_c = initClient;
We will use key X to start a server and key C to start a client.
Server and Client
Before we continue, I want to explain in depth how this MP system is structured.
You have to understand the logic right, so you can extend this further, or debug what is currently running.
We have two functions called 'initServer()' and 'initClient()'. Unlike in single player, the client and server can run different functions from each other at the same time.
What is called on the server runs ONLY at the server. What is called on the client runs ONLY on THAT client.
So, from now on, everything called from the 'initServer()' function or any function called from a child function of 'initServer()' will run on the server.
The same goes for our 'initClient()' function. Anything called from that function will run ONLY on the client calling it, and any sub-function called from a function called by 'initClient()' will run ONLY on that client.
The only exceptions are the 'player_act' (for our player) and 'on_client_event' functions.
When you create an entity on the client, it runs its function on the server! You have to attach a function manually to it on the client. Otherwise, you cant control anything related to that entity.
The same goes for our 'on_client_event' function. Anything received from the server on our client will execute this function, which in turn can call other functions on our client (like attaching a function to every other client entity (not OUR PLAYER entity, but other Player entities of other clients).
So, we're using our 'initClient()' to create a player and control it. We're using our 'on_client_event' to control other clients entities(players) to control them. And the server uses 'initServer()' to control his player entity and the 'player_act' called by a client on the server to move & update the players entities on the server.
You can follow the logic from here, which function runs where.
Lst example, before we begin. The camera function called in 'initServer()' will move the camera with the player entity on the server. The camera function called in 'initClient()' will move the camera with the player entity on the client calling it. If you want to call another function from the camera function, it will run on the machine that's currently running that camera function. You might be running that camera function on the server only, or on a client only, at any given time...Its your choice. Always keep in mind the logic between client/server functions!
Using dplay_localfunction can be confusing and hard to debug later. Try to avoid that.
Now lets start a server! Create a new file in a new sub-folder "Scripts" called system.c where we will put our multiplayer system and add to main.c the declaration before the main() function:
#include "Scripts/system.c"
Open the system.c file and create a function for opening a session:
// Runs on SERVER, called from the 'main()' function. function initServer() { if(connection != 0) { session_close(); } wait(3); if(session_open("mpsession")) { level_load("blank.wmb"); wait(3); camera.z = 400; camera.tilt = -90;
set(camera, ISOMETRIC); } }
session_open("mpsession") starts a session with the given name "mpsession" and if successful will continue inside the 'if' block. This means, it will re-load the level 'blank.wmb', wait 3 frames and set the camera.z to 400, tilt to -90 (top-down) and ISOMETRIC flag on to remove the distortions of the screen.
Now, lets add a method to start as a client...
Add another function 'initClient' after the 'initServer' function.
// Runs on CLIENT, called from the 'main()' function. function initClient() { if(connection != 0) { session_close(); } wait(3); if(session_connect("mpsession","ip.add.re.ss")) { // load level anew level_load("blank.wmb"); wait(3); // wait until the level state is received from the server while (dplay_status < 6) { wait(1); } camera.z = 400; camera.tilt = -90;
set(camera, ISOMETRIC); } }
Here we try to connect with session_connect to a session named "mpsession" located at the ip address "ip.add.re.ss". You should write your own IP there...
For testing with latency, I downloaded a program called Network Emulator Client, which can add latency to a local connection, but I had to also connect the session to my router's external IP and forward the port (2300) to my laptop.
Now comes the boring part :D working blindly and creating dummy functions that will be used when we create the player.
Lets start with a custom debug panel, to monitor what we need. Add this to the beginning of system.c:
var debug_var[10];
PANEL* debug_pan = { pos_x = 10; pos_y = 30; layer = 999; flags = visible; digits(0,20,"%.3f",*,1,debug_var[0]); digits(0,30,"%.3f",*,1,debug_var[1]); digits(0,40,"%.3f",*,1,debug_var[2]); digits(0,50,"%.3f",*,1,debug_var[3]); digits(0,60,"%.3f",*,1,debug_var[4]); digits(0,70,"%.3f",*,1,debug_var[5]); }
Here we use a temporary array for displaying information called debug_var. It has 10 variables, but we're showing only 6. If we need more, we can always add more in the panel definition, but 6 is enough for now.
Now, let's add a camera function, following our player entity. Add this after the debug_pan definition:
// Runs on SERVER and CLIENT, called from the 'initServer()' and 'initClient()' functions. function cameraFollow(entity1) { proc_kill(4); me = entity1; while(me) { cam_zoom -= mickey.z * 5 * time_step; cam_zoom = clamp(cam_zoom,0,600); vec_set(camera.x, vector(my.x, my.y, my.z + cam_zoom + 48)); vec_set(camera.pan, vector(0, -90, 0)); wait(1); } }
Also, create a file called def.c and add a definition to load it in the main.c file, before the system.c file. We will use this file to add general var/string/bmap definitions to keep our system clean of global definitions and focus on the functions.
In main.c before system.c definition add:
#include "Scripts/def.c"
And, now define the first variable (cam_zoom) in our def.c file:
var cam_zoom = 400;
I don't think I should explain what the camera function does, but in case you're extremely new to 3DGS, here's a brief flow:
Take a pointer called entity1 to an entity, kill all other running camera functions, save the entity we're trying to point to in the 'my' pointer. While the entity exists do the following: Modify the cam_zoom with the mouse scroll with speed of 5 quants * time_step (which means 5 quants every tick, or 80 quants/sec.) Restrict the cam_zoom from 0 to 600 quants. Set the camera position to the entity position and add to Z the cam_zoom plus additional 48 quants between the camera and the entity. Rotate the camera to default angles and tilt to -90.
And that's it for the camera. Leave it for later.
Now, lets write the function that determines the input. We'll use a single number to represent the 8 directions the player can travel (forward/back/left/right and in-between). It will return a value from 1 to 8, 0 meaning no movement input. Add this after the camera function:
// Runs on SERVER and CLIENT, called from the 'initServer()' and 'initClient()' functions. function processInput() { if(key_w == 1 && key_s == 0) { if(key_a == 1 && key_d == 0) { return(8); } if(key_a == 0 && key_d == 1) { return(2); } if(key_a == 0 && key_d == 0) { return(1); } } if(key_w == 0 && key_s == 1) { if(key_a == 1 && key_d == 0) { return(6); } if(key_a == 0 && key_d == 1) { return(4); } if(key_a == 0 && key_d == 0) { return(5); } } if(key_w == 0 && key_s == 0) { if(key_a == 1 && key_d == 0) { return(7); } if(key_a == 0 && key_d == 1) { return(3); } if(key_a == 0 && key_d == 0) { return(0); } } return(0); }
Movement
Now we need a function to set the movement vector based in interpretation of this input above. Add it after the input processing function:
// Runs on SERVER and CLIENT, called from the 'initServer()' and 'initClient()' functions. function interpMovementInput(inputValue, var* speedVector, movementSpeed) { if(inputValue == 0) { speedVector[0] = 0; speedVector[1] = 0; } if(inputValue == 1) { speedVector[0] = movementSpeed * time_step; speedVector[1] = 0; } if(inputValue == 2) { speedVector[0] = (movementSpeed * 0.707) * time_step; speedVector[1] = -(movementSpeed * 0.707) * time_step; } if(inputValue == 3) { speedVector[0] = 0; speedVector[1] = -movementSpeed * time_step; } if(inputValue == 4) { speedVector[0] = -(movementSpeed * 0.707) * time_step; speedVector[1] = -(movementSpeed * 0.707) * time_step; } if(inputValue == 5) { speedVector[0] = -movementSpeed * time_step; speedVector[1] = 0; } if(inputValue == 6) { speedVector[0] = -(movementSpeed * 0.707) * time_step; speedVector[1] = (movementSpeed * 0.707) * time_step; } if(inputValue == 7) { speedVector[0] = 0; speedVector[1] = movementSpeed * time_step; } if(inputValue == 8) { speedVector[0] = (movementSpeed * 0.707) * time_step; speedVector[1] = (movementSpeed * 0.707) * time_step; } }
This function takes an input value, and based on it directly modifies the given speedVector with the movementSpeed supplied.
The movement speed is added multiplied by time_step to the X axis and Y axis and multiplied by 0.707 when it is added to both X and Y axis (diagonal movement).
Now we need a function to move the entity based on the input and interpretation of input. Add it after the interpretation function:
// Runs on SERVER and CLIENT, called from 'initServer()', 'initClient()', and other functions. function moveClient(entity1) { me = entity1; var myOldPan; myOldPan = my.pan; my.pan = 0; var distance_moved; distance_moved = c_move(me, my.skill1, nullvector, IGNORE_PASSABLE | GLIDE); my.pan = myOldPan; return(distance_moved); }
The movement function takes entity1 pointer and saves it to the my pointer. It stores the entity pan angle to a temporary variable, resets it to 0 (its a 8-way top-down shooter...), then moves the entity based on its skill1, skill2 and skill3 (used like a vector), resets the pan to the saved one and returns the local variable which holds the distance moved. Simple, right?
Now, we're ready to create a player!
The Player!
Next, add this to your 'initClient()' function, after 'set(camera, ISOMETRIC)':
player = ent_create("player.mdl",vector(random(400) - 200,random(400) - 200,32),player_act); while(player.client_id != dplay_id) { wait(1); } wait(-0.5); cameraFollow(player);
This will create "player.mdl" up to 200 quants random in X and Y axis at 32 quants above the ground and assign 'player_act()' function to the entity on the server.
Then we wait while the server returns 'created' with the 'player.client_id != dplay_id' and wait additional half a second so the entity is created on all clients properly.
Now, create the 'player_act()' function that the server will use. Add it after the 'moveClient()' function:
// Runs on SERVER, called from the 'initServer()' or 'initClient()' functions when creating a player entity. function player_act() { wait(1); // Wait until I'm created // If I'm a client , wait until I've been created everywhere if(player != me) { while(my.client_id == 0) { wait(1); } } my.smask |= NOSEND_ALPHA | NOSEND_AMBIENT | NOSEND_ANGLES | NOSEND_ATTACH | NOSEND_COLOR | NOSEND_FLAGS | NOSEND_FRAME | NOSEND_LIGHT | NOSEND_ORIGIN | NOSEND_Z | NOSEND_SCALE | NOSEND_SKIN | NOSEND_SOUND;
my.objectType = playerObject; // I'm a player send_skill(my.objectType, SEND_ALL); // Send I'm a player send_skill(my.x, SEND_VEC | SEND_ALL ); // Send my starting POS }
We wait 1 frame, so the ent is created and we check to see if player points to the entity attached to the function. This will be the case on the server, but will return false if it is attached to a client entity (because we assign player = ent_create on different machines localy, and it acts only locally for that machine!).
Next, we set all send flags to off. We set objectType to playerObject, which will help us distinguish players from items, props and enemies and we send the skill to all clients. Finally, we send the initial random position.
Before we can test this out, you should define the playerObject and objectType in the def.c file after the cam_zoom variable:
#define playerObject 1 #define objectType skill6
Start the app and test your connection. It should create one player entity for your client and nothing more...
Now, lets make the player appear on all clients after someone joins... Add some more lines to the 'player_act()' function:
// ASK EVERY CLIENT FOR INITIALIZATION INFO sendInitAllClients();
while(1) { //////////////////////////////////////////////// // IF A CLIENT IS INITIALIZING > SEND MY INFO // //////////////////////////////////////////////// if(my.sendInit == 1) { send_skill(my.x, SEND_VEC | SEND_ALL); send_skill(my.objectType, SEND_ALL); my.sendInit = 0; } wait(1); }
Now replace your whole def.c file with this:
#define playerObject 1
#define latency skill4 #define sendInit skill5 #define objectType skill6 #define clientInput skill7 #define initializedMovement skill8
// Prediction #define shouldBeAt_X skill31 #define shouldBeAt_Y skill32 #define shouldBeAt_Z skill33 #define correction_X skill34 #define correction_Y skill35 #define correction_Z skill36
var cam_zoom = 400; var initClients = 0; var updateInterval = 1; // Time between updates (in seconds)
You will use the other definitions later, but we can save def.c as final for now.
This is the 'sendInitAllClients()' function, add it before the 'player_act()' function:
// Runs on SERVER, called from the 'player_act()' function. // Cycle through all ents and send init information function sendInitAllClients() { initClients = 1; send_var(initClients);
you = ent_next(NULL); while(you) { if(you.objectType == playerObject && you != me) { you.sendInit = 1; } you = ent_next(you); } }
This function will set every entity's sendInit skill that is a playerObject to 1 and after this frame, all entities that are running 'player_act()' on the server will send their positions to all clients.
initClients variable will take care of all clients initializing their own copies of every player entity. You'll understand this when I explain the on_client_event function, add it before your 'initServer()' function:
// Runs on CLIENT, auto called when receiving data (variables, strings...). function on_client_event(void* serverData) { if(event_type == EVENT_VAR) { if(serverData == &initClients) { wait(-0.6); initOtherClients(); } } }
This function is triggered each time our game receives something from the server, and we're checking for a variable here.
using the pointer &initClients, we check if the server sent this variable and if correct, wait 0.6 seconds (for new entities to be created fully) and call 'initOtherClients()' function. This function will make all our copies of all players alive and moving. Here it is, add it before the 'sendInitAllClients()' function:
// Runs on CLIENT, called from the 'on_client_event()' function. // Initialization (function) of other clients on My client machine (to move, animate, control them...) function initOtherClients() { you = ent_next(NULL); while(you) { if(you.objectType == playerObject) { if(you.initializedMovement == 0) { you.initializedMovement = 1; //otherClient_act(you); } } you = ent_next(you); } }
We loop through all entities, check if its a playerObject and set initializedMovement to 1. Later we will use 'otherClient_act()' to control each copy of a player entity on our client machine. For now, leave it commented.
Now, lets take some input and move our player. In our 'initClient()', add this after the 'cameraFollow()' instruction:
var clientOldInput = 0; while(player) { player.clientInput = processInput(); interpMovementInput( player.clientInput , player.skill1 , 5 ); moveClient(player);
if(player.clientInput != clientOldInput) { send_skill(player.clientInput,0); clientOldInput = player.clientInput; } wait(1); }
This code receives our input from the keyboard, turns it into the appropriate number to be sent and modifies our skill1 and 2 of the player entity so we can move in the desired direction. It uses speed 5 quants per tick or 80 quants per second (16 ticks). Then, it moves our player according to its skill1 and 2 values and checks to see if our input has changed since the last frame. If it has, we send it to the server and save the change for reference for the next frame.
So far, we should have a proper connection between client and server, player creation and local player movement, but no updates on other clients and no synchronization with the server.
Do the same for the 'serverInit()' function, add this after 'set(camera, ISOMETRIC)' instruction:
player = ent_create("player.mdl",vector(random(400) - 200,random(400) - 200,32),player_act); while(!player) { wait(1); } wait(1);
cameraFollow(player);
var clientOldInput = 0; while(player) { player.clientInput = processInput(); interpMovementInput( player.clientInput , player.skill1 , 5 ); moveClient(player);
if(player.clientInput != clientOldInput) { send_skill(player.clientInput,0); clientOldInput = player.clientInput; } wait(1); }
This code will create and move the player character on the server. Test it to ensure it works.
Lets now update the clientInput to server and to all clients, to get our player moving everywhere...
Add this local variable right before the while() loop in the 'player_act()' function:
var distance_moved; // How much I moved in the last frame var myOldClientInput = 0;
Add this to the 'player_act()' function, after the sendInit if statement:
/////////////////////////////////////////////////////////// // IF I'M JUST A CLIENT HERE > MOVE ME BASED ON MY INPUT // /////////////////////////////////////////////////////////// if(player != me) { interpMovementInput( my.clientInput , my.skill1 , 5); distance_moved = moveClient(me); }
//////////////////////////////// // IF INPUT CHANGED > SEND IT // //////////////////////////////// if(my.clientInput != myOldClientInput) { send_skill(my.clientInput, SEND_ALL ); myOldClientInput = my.clientInput; }
Your clients should move, your server should move, but the only Multi-player should be between a single client and the server.
Right now, all your players have a local initClient function attached on them to their machines and no one else, and your server has player_act functions for each client to receive/move/send input.
The input is received and sent by the server correctly, your other clients just don't have a function attached for your player entity on their machines yet.
Updating a client
Let's add an update timer, to each client entity separately (this way you can modify it to send frequent to low latency players and less to high latency if you like...). Add this after the clientInput if statement in the 'player_act()' function:
/////////////////////////////////////////////////////////////// // IF UPDATE TIME INTERVAL PASSED > SEND MY CURRENT POSITION // /////////////////////////////////////////////////////////////// updateTime += 1 / (16 / time_step); if(updateTime >= updateInterval) { vec_set(my.shouldBeAt_X, my.x); send_skill(my.shouldBeAt_X, SEND_ALL | SEND_VEC); updateTime = 0; }
And add a local variable before the while loop for the updateTime counter:
var updateTime;
Now your server counts up to 0.5 seconds and sends the current position in 3 skills (shouldBeAt_X, shouldBeAt_Y and shouldBeAt_Z). We will use this position to interpolate to. It is the correct server position for that player.
This also ensures no hacking will be allowed, because of a difference between the server and client positions. You will just force the client to this position if it is too far away...
What's left?
The interpolation!
This will require some theory...lets discuss the method.
In order to interpolate to a correct position from the server, you always have to keep in mind it is a past position. Not only on the server, but on your client also. It is at least 10 ms old, and in a worst case (could be more, but lets assume) 250 ms old! That's 1/4 of a second, or 1.5 meters difference for an average male average running speed).
To correctly interpolate to a past position, you must know your own past position, and check the difference. Then, interpolate THIS difference to your current position. This way, you will get at the right position. Otherwise, you will always be interpolating backwards, to a position you already were at.
To do this, we need to save our player's position and all other clients position. Lets do it with an array or two...
Add this before the while(player) loop in your 'initClient()' function:
var clientOldLatency = 0; var myOldShouldBeAt[3]; var myOldState[2][360]; // My past positions list var correction_factor; // Correction % every frame of total distance var correction_distance[3]; // Total distance to be corrected var correction_distance_val; var framesBeforeLatency;
These variables will help us store our position for 360 frames back, update and send our latency, calculate a correction vector and calculate how many frames have passed since the server sent his last current position for our player entity.
The fun part begins! And the hard one! Add this after the clientInput if statement in your 'initClient()' function:
player.latency = dplay_latency; if(player.latency != clientOldLatency) { send_skill(player.latency,0); clientOldLatency = player.latency; }
////////////////////////////////////////// // STORE MY POSITION AND PAST POSITIONS // ////////////////////////////////////////// var i , j; for(i = 0; i < 2; i++) { for(j = 359; j > 0; j--) { myOldState[i][j] = myOldState[i][j - 1]; } } myOldState[0][0] = player.x; myOldState[1][0] = player.y;
You don't really need to send the latency to the server right now, but we might use it for a prediction in the future, so I added it for reference.
With this piece of code we browse through the myOldState list of variables (multi-dimensional for two X and Y axis) and we move all values one slot downwards. After that, we set the first slots to the current entity X and Y positions. This 'saves' our past position every frame for 360 frames back.
Test to see if everything runs ok so far.
Add this to the end of the while loop to see the difference between your player's position and the clients entity for your player:
if(vec_dist(player.shouldBeAt_X,myOldShouldBeAt[0]) != 0) { debug_var[0] = vec_dist(player.x, player.shouldBeAt_X); vec_set(myOldShouldBeAt[0], player.shouldBeAt_X); }
When you receive a shouldBeAt_X position, it will be different from the old stored position, and it will set the distance between the two positions to debug_var[0]. This way you will be able to see realtime the difference between the two positions when you receive it, until the next update comes.
Note that this distance might be 0 when you stop moving from the beginning, but will increase as you continue to move. This is the desynchronization caused by the latency and different running times of the c_move call on the server and on client.
This difference will be even greater when you use glide, because glide uses up to 6 c_move calls invisibly.
That's the reason why this interpolation is needed. To synchronize the server and the client positions.
Now that we got the idea, lets implement it. Replace the content in the 'vec_dist(player.shouldBeAt_X,myOldShouldBeAt[0]) != 0' if statement:
vec_set(myOldShouldBeAt[0], player.shouldBeAt_X);
framesBeforeLatency = round(((16 / time_step) * (player.latency / 1000))) + 1; vec_diff(player.correction_X, player.shouldBeAt_X, vector(myOldState[0][framesBeforeLatency], myOldState[1][framesBeforeLatency], player.z)); vec_set(correction_distance[0], player.correction_X); correction_factor = updateInterval / (16 / time_step); vec_scale(player.correction_X, correction_factor ); debug_var[0] = vec_dist(player.correction_X , NULLVECTOR); debug_var[1] = vec_dist(player.shouldBeAt_X, vector(myOldState[0][framesBeforeLatency], myOldState[1][framesBeforeLatency], player.z)); debug_var[5] = framesBeforeLatency;
Here we save the new position, calculate how many frames have passed since the update was sent (using player.latency and fps), we round the number to the closest whole and add one frame.
Then we set the difference of the old position (according to our X and Y positions list, using the framesBeforeLatency) and store it in correction_X skill vector.
We then set a correction factor (divide the updateInterval to the fps to get a value used for each frame, like a time_step thing) and scale the difference between the two positions with our correction_factor.
This way we add a fraction of the difference each frame, until a new update comes.
If this looks hard to understand, stop everything and think about it for half an hour, keeping in mind the difference in times between server sending and client receiving (imagine 100ms difference, its easier...).
Then, we set our debug variables to monitor what is happening. And we have our difference value for each frame to correct by until the next update comes.
Add the missing function for rounding numbers to the closest integer after the debug_pan definition:
function round(x) { return floor(x+0.5); }
One more thing remaining is the actual correction. Add this after the last if statement you just modified in the 'initClient()' function:
correction_distance_val = vec_dist(correction_distance[0], NULLVECTOR); if(correction_distance_val > 2) { if(correction_distance_val < 30) { if(player.clientInput != 0) { debug_var[2] = c_move(player , NULLVECTOR , player.correction_X , IGNORE_PASSABLE ); vec_sub(correction_distance[0], player.correction_X); } } else { vec_set(player.x , player.shouldBeAt_X ); vec_set(correction_distance[0] , NULLVECTOR ); } }
debug_var[3] = vec_dist(correction_distance[0] , NULLVECTOR ); debug_var[6] = player.latency; debug_var[7] = 16 / time_step;
Now we get the distance in a local variable, so we don't have to call vec_dist all the time, and check if its greater than some values.
If it is greater than two, but less than 30 > if we're moving the player > move our player further with the correction_X vector and subtract the vector used to move from our total correction_distance[0] vector saved.
If it is greater than 30 quants, directly set our player position to the updated position to prevent cheating and keep track of current events.
Finaly, we set some of our debug variables to the total correction distance remaining to be corrected, our latency and finally our fps.
What remains is to create a function for each other client on our machine, move him the same way based on input received and put the same code for interpolation, and we get a full working movement code.
This is done just by copying and modifying our existing interpolation code.
Remember we wanted to call a function named 'otherClient_act()' from the 'initOtherClients()' function? First, uncomment the 'otherClient_act()' function from the 'initOtherClients()' function and then add it before 'initOtherClients()'. Here it is:
// Runs on CLIENT, called from the 'initOtherClients()' function. // Other client's function on My client machine function otherClient_act(entity1) { me = entity1;
wait(1); if(player == me) { return; } // I shouldnt be a player
var myOldShouldBeAt[3]; var myOldState[2][360]; // My past positions list var correction_factor; // Correction % every frame of total distance var correction_distance[3]; // Total distance to be corrected var correction_distance_val; var framesBeforeLatency;
while(1) {
interpMovementInput( my.clientInput , my.skill1 , 5); // Interpret my movement input, set speed moveClient(me); // Move me based on my interpretated movement and speed
////////////////////////////////////////// // STORE MY POSITION AND PAST POSITIONS // ////////////////////////////////////////// var i , j; for(i = 0; i < 2; i++) { for(j = 359; j > 0; j--) { myOldState[i][j] = myOldState[i][j - 1]; } } myOldState[0][0] = my.x; myOldState[1][0] = my.y;
if(vec_dist(my.shouldBeAt_X,myOldShouldBeAt[0]) != 0) { vec_set(myOldShouldBeAt[0], my.shouldBeAt_X);
framesBeforeLatency = round(((16 / time_step) * (my.latency / 1000))) + 1; vec_diff(my.correction_X, my.shouldBeAt_X, vector(myOldState[0][framesBeforeLatency], myOldState[1][framesBeforeLatency], my.z)); vec_set(correction_distance[0], my.correction_X); correction_factor = updateInterval / (16 / time_step); vec_scale(my.correction_X, correction_factor ); debug_var[0] = vec_dist(my.correction_X , NULLVECTOR); debug_var[1] = vec_dist(my.shouldBeAt_X, vector(myOldState[0][framesBeforeLatency], myOldState[1][framesBeforeLatency], my.z)); debug_var[5] = framesBeforeLatency; }
correction_distance_val = vec_dist(correction_distance[0], NULLVECTOR); if(correction_distance_val > 2) { if(correction_distance_val < 30) { if(my.clientInput != 0) { debug_var[2] = c_move(me , NULLVECTOR , my.correction_X , IGNORE_PASSABLE ); vec_sub(correction_distance[0], my.correction_X); } } else { vec_set(my.x , my.shouldBeAt_X ); vec_set(correction_distance[0] , NULLVECTOR ); } }
debug_var[3] = vec_dist(correction_distance[0] , NULLVECTOR ); debug_var[6] = my.latency; debug_var[7] = 16 / time_step; wait(1); } }
This is the exactly same interpolation code as the one used for our player, but instead of 'player.' pointer we use 'my.' pointer. If it is called for the player entity, it will terminate (with 'return') so that only other clients will run this function.
The debug variables set here will show in the panel, because this function is called for the last 'other' client created. If it gets mixed up, comment or delete the other debug_var manipulations in the player function ('initClient()').
And, don't forget to send updated latency from 'player_act()' function, after the pan interpolation:
if(my.latency != myOldLatency) { send_skill_except_id( my.client_id , my.latency , 0 ); myOldLatency = my.latency; }
Also, add the local variable before the while loop:
var myOldLatency;
It's good to see the updates in the game here, because bugs that prevent updating can appear at any time. We'll create a simple function that changes the skin of the player every time an update arrives, and then it reverts back to the first skin after 16 frames (like flashing, its best to use a bright or red second skin).
// Runs on SERVER and CLIENT, called from the 'otherClient_act()', 'player_act()', 'initClient()' functions. function makeMeDifferent() { my.skin = 2; wait(16); my.skin = 1; }
And add this function in the update handling if statement in the 'initClient()' function, like this:
... if(vec_dist(player.shouldBeAt_X,myOldShouldBeAt[0]) != 0) { makeMeDifferent(); vec_set(myOldShouldBeAt[0], player.shouldBeAt_X); ...
Also, add it in the 'otherClient_act()' at the same place:
... if(vec_dist(my.shouldBeAt_X,myOldShouldBeAt[0]) != 0) { makeMeDifferent(); vec_set(myOldShouldBeAt[0], my.shouldBeAt_X); ...
And one more in the 'player_act()' in the update timer statement to see it acting on the server:
... updateTime += 1 / (16 / time_step); if(updateTime >= updateInterval) { makeMeDifferent(); ...
When you run this with a server and two clients, you'll see the ents flashing their second skin whenever they receive an update.
On the server, the flash indicates an update is sent to all clients.
Keep in mind, that the updated position has to differ from the current position on the client, in order to get a flash. This means that the update is handled only if it is different than the last time, like in our vec_dist check.
Now, let's add Pan change and update.
Turning around
First, in the def.c file add this after '#define initializedMovement skill8:'
#define panAng skill9
Now, in the 'initClient()' and 'initServer()' function add three local variables before the while loop:
var rotation_vec1[3]; var rotation_vec2[3]; var myOldPan;
And add this after the latency sending if statement in the 'initClient()' function and before the clientInput sending if statement in the 'initServer()' function:
rotation_vec2[0] = mouse_pos.x; rotation_vec2[1] = mouse_pos.y; rotation_vec2[2] = 1000; vec_for_screen(rotation_vec2[0], camera); rotation_vec2[2] = player.z;
vec_set(rotation_vec1[0], rotation_vec2[0]); // subtract from it my position: vec_sub(rotation_vec1[0], player.x); // rotate angle to the target position: vec_to_angle(player.pan, rotation_vec1[0]); player.tilt = 0; player.roll = 0; if(abs(ang(player.pan - player.panAng)) >= 2) { player.panAng = player.pan; send_skill(player.panAng, SEND_UNRELIABLE ); }
To save some traffic, we're sending in Unreliable mode, which means some packets will be dropped if the network cant handle the amount of traffic coming through, but it is ok to drop some of those packets, because it doesn't affect the movement of our top-down shooter, just the rotation of other players.
In the 'otherClient_act()' function add this after the 'moveClient()' instruction:
if(abs(ang(my.pan - my.panAng)) >= 1) { my.pan += ang(my.panAng-my.pan) * 0.5 * time_step; }
This takes care of the receiving pan angles for other players, now lets add the middle man in the 'player_act' function, after the clientInput updating if statement:
if(my.panAng != myOldPan) { send_skill(my.panAng, SEND_ALL | SEND_UNRELIABLE ); myOldPan = my.panAng; } if(abs(ang(my.pan - my.panAng)) >= 1) { my.pan += ang(my.panAng-my.pan) * 0.5 * time_step; }
And don't forget the local variable to hold the old pan in the 'player_act', add it before the while loop:
var myOldPan;
Now we should have a proper rotation for each client according to the mouse position on screen, but we still don't have a mouse on the screen...Lets add that now, in main.c, right in the 'main()' function, in the while loop before the wait(1) instruction:
vec_set(mouse_pos,mouse_cursor);
And before the while loop add this:
mouse_map = arrow; // use arrow as mouse pointer mouse_mode = 2;
And finally, we have to define the arrow bitmap in def.c file, add this somewhere:
BMAP* arrow = "arrow.bmp";
Go download a cursor image from somewhere and convert it, rename it and place it inside the folder. Run the app and you'll have a mouse pointer on the screen that the player turns to and updates to other clients.
Traffic optimization
After some testing with the debug vars, I've noticed this takes 500 bytes/client to update the pan every frame if it is changed.
For a normal Deathmatch shooter it will be ok, but for our top-down zombie shooter it is too much. If you put 32 clients in there and get 500 bytes/client you'll have 16kb per second receiving on the server and sending 16kb/client = 512kb per second. Some will be dropped probably, and also probably your server app will split those on different routes, so it wont be a problem, but we also have zombies...
Lots of zombies. Lets say 200 at any time on the screen... 200 zombies updating their position two times a second will generate some more traffic, for example 80 bytes per zombie per second = 16kb per client * 32 clients = another 512kb per second. This adds up to a MB per second. You'll need a server with a good connection.
My calculations could be wrong, but we still have to optimize this traffic monster.
The first thing that comes to mind is that, as I said, you have to update only the visible on screen zombies per client, which will be accomplished by sending for each client individually only the visible zombies from the whole zombie list.
So, thats already considered.
Next thing to consider is sending the pan angle less often so its not that much traffic.
First thought that comes to mind is SEND_RATE, and dplay_entrate, but after giving it a try it appears that the pan doesn't get sent sometimes, because it hasn't changed this frame...but the game still waits until a given frame is reached to send.
So, the next thought is to check and send the pan from a separate while loop that gets run as fast as we want it to, for example every 4 frames.
In theory, this will lessen the traffic to 125 bytes instead of 500, and will update 15 times/sec. I hope that wont be noticeable, but lets give it a try first.
Add this function before the 'moveClient()' function:
// Runs on SERVER and CLIENT, called from the 'initServer()' and 'initClient()' functions. function sendPanUpdates(entity1) { me = entity1; // Store the entity in the 'my' pointer while(me) { if(abs(ang(my.pan - my.panAng)) >= 2) { my.panAng = my.pan; send_skill(my.panAng, SEND_UNRELIABLE ); } wait(4); } }
This is the same code we have in 'initClient()' function. It just checks if the angle has changed and sends it, but it waits 4 frames (wait(4)) and checks again.
In order for this to work, we now have to remove that code from the 'initClient()' function and call that function. Remove that part from the 'initClient()' function:
if(abs(ang(my.pan - my.panAng)) >= 2) { my.panAng = my.pan; send_skill(my.panAng, SEND_UNRELIABLE ); }
Now call the function after the 'cameraFollow(player)' call in the 'initClient()' function and in the 'initServer()' function:
sendPanUpdates(player);
Now, after testing, each client sends around 125 bytes per second for constant pan changing and with two clients this adds up to 250bps. The server receives these and sends them to each client (two clients), so it sums up to 500bps going out of the server. We'll later restrict this to update only other clients and not the actual client sending the pan (which is pointless...).
After this line, you might have noticed we're also sending the input a client gives to the server back to that same client, which is also pointless...It doesn't waste that much traffic, but we will remove that too, with a custom function to send to everyone BUT client ID later.
We can also turn the data compression ON later, if it turns out to be beneficial.
So, lets open the calculator again...
A player sends around 125 bytes/sec. for constant pan change. 32 players * 125 = 4000 bytes per second received on the server. Our server sends out to 32 clients, so 4000 * 32 = 128000 bytes/sec, or 125kb/sec. We've shrinked the traffic to 1/4 now (not counting the zombies positions).
I haven't given any deep thought to this, but this traffic is split 32 ways, and it will only go hard on the server's link to the network (with 125kbps right now). After that it will be split in 32, so the network will be able handle it.
Now, we need a function that sends to everyone but a given client ID.
Add these two functions before the 'otherClient_act()' function:
// Runs on SERVER, called from the 'send_skill_except_id()' function, called from the 'player_act()' function. function find_number_of_clients() { var num = 0; while(client_find(++num)); return num-1; }
// Sends a skill to every client except the client specified with CL_ID // Works eighter in MODE=0 or MODE=SEND_VEC void send_skill_except_id(var cl_id, var* skill, var mode) { var i,val; if(mode&SEND_VEC) mode = SEND_VEC; // limit mode to 0 or SEND_VEC var total_clients; total_clients = find_number_of_clients(); for(i = 0; i < total_clients + 1; i++) { val = client_find(i+1); if(val > 0 && val != cl_id) send_skill_id(val,skill,mode); } }
The first function counts all clients and returns the number. The second function takes in client ID to ignore, its skill pointer and MODE (either 0 or SEND_VEC). It then loops through all clients connected, and if that client ID isn't the same as the ignored client ID or 0 (for server) it sends the skill or vector.
And now change the two send_skill instructions in the 'player_act()' function to 'send_skill_except_id()'. It should now look like this (don't forget the SEND MODE - '0'):
... if(my.clientInput != myOldClientInput) { send_skill_except_id( my.client_id , my.clientInput, 0 ); myOldClientInput = my.clientInput; } if(my.panAng != myOldPan) { send_skill_except_id( my.client_id , my.panAng, 0 ); myOldPan = my.panAng; } if(abs(ang(my.pan - my.panAng)) >= 1) { my.pan += ang(my.panAng-my.pan) * 0.5 * time_step; } ...
We wont use it for the sendInit instructions or the shouldBeAt instruction, because we still need them to send to the client creating/controlling the entity.
You're now sending to everyone, but the creator of the entity.
But, you're still sending position updates once a second, even if the position hasn't changed. Lets change that...
Replace your updateTime counter in the 'player_act()' function with this one:
updateTime += 1 / (16 / time_step); if(updateTime >= updateInterval) { if(vec_dist(my.shouldBeAt_X, my.x) > 0 || my.clientInput > 0) { makeMeDifferent(); vec_set(my.shouldBeAt_X, my.x); send_skill(my.shouldBeAt_X, SEND_VEC | SEND_UNRELIABLE | SEND_ALL ); } updateTime = 0; }
This just checks if our player moved since the last update was sent and sends if it is true. In case our player didn't move, but assuming the client is moving on his machine, we send if the input indicates movement too.
Now, after testing, you'll notice that our player position or other players position is off after joining. That's because the server isn't sending the shouldBeAt_X position.
Change the sendInit if statement to this one:
if(my.sendInit == 1) { send_skill(my.x, SEND_VEC | SEND_ALL); send_skill(my.objectType, SEND_ALL);
vec_set(my.shouldBeAt_X, my.x); send_skill(my.shouldBeAt_X, SEND_VEC | SEND_ALL);
send_skill_except_id( my.client_id , my.panAng, SEND_ALL | SEND_UNRELIABLE ); myOldPan = my.panAng; my.sendInit = 0; }
This forces the server to send the current pan angle of the player and his shouldBeAt position when someone joins our session. Now every client is where he should be.
After some more traffic testing, here's the final numbers (average rounded...)
While breaking my keyboard and mouse, turning 3 clients and a server and moving one of the clients constantly around, the debug variables show: BPS received from a client for constant movement and pan change = 375. 125 for pan Unreliable and 250 for input Reliable. The server takes this from each client and sends it to every other client, so, 32 clients * 375b = 12000b * 31 clients = 372000b or 364kbps traffic to update constant pan and input change for each player.
Not bad, considering this is the worst case scenario. On average, the traffic will be twice lower (meaning 180kbps).
Save all this code in a folder and call it a template.
This is your core system (or base, or whatever you want to call it) for joining and moving players around.
From here, you can implement character classes and skills, shooting and enemies and other stuff.
Now, I'll show you how to make your player shoot projectiles.
To use real projectiles and be easy on the traffic, we'll create the projectiles locally on each client. Otherwise we'd have to send 1000's of entities each second.
The local method might be a little off or with errors, but the tradeoff in traffic is huge.
We'll send only an input parameter, and even better, pack it into our existing clientInput variable.
Input compression
For this, we first have to change our 'processInput()' function to support more input values.
But, lets consider a different method, than the usual bit packing.
A var (variable) ranges from -999999.999 to +999999.999, and we can store up to 9 bits in this range. I haven't tested this, but, maybe if we use the negative value it could go to 18 bits, which means packing up to 18 values for 18 keys.
Going beyond that, we'll use the decimal system to store our input. We're already storing the movement input from 0 to 8. We'll store the shooting input in the next character by multiplying our shooting input by 10.
Let me give you an example. If we use 1 for shooting and 0 for not shooting, we'll have input value between 0 and 18 (meaning 0-8 for movement + 10 for shooting)
Later we can add a right mouse button and 'call it 2' and a combination of both right and left mouse buttons and 'call it 3'. In our current example, the client input will range from 0 to 38 meaning 0-8 for movement + 10 for LMB, 20 for RMB or 30 for both.
If you want to use other buttons too, you just have to pack them even further. The shooting input still has 6 free input values (4,5,6,7,8,9) and you can go beyond that, in the 9xx range, to pack other input (this time multiplying by 100).
The only condition here is that, whenever you want to get a certain input value, you'll have to save a temp of the input and then start removing characters one by one until you get to your desired character.
For example, you have input 316. You want to find out the movement input, so you'll first have to divide that number by 100 and use integer to remove the decimals. You'll be left with 3. Multiplying that 3 by 100 and removing the result from the temp input will leave you with 16. Now, you have your first input value 3 and the rest packed as 16.
Divide the rest by 10 and use integer to get 1 - our shooting input. Now, multiply that 1 by 10 and remove it from the temp input. This leaves you with 6 (our movement input).
Here's an example in action, to understand this better:
var movementInput; movementInput = inputValue - integer(inputValue / 10) * 10;
In the working example, we have inputValue from 0 to 99 (the first digit currently represents shooting, so its always 1 or 0, and the second digit represents our movement input from 0 to 8)
Again, take 16 as example. movementInput = 16 - integer(1.6) * 10; > movementInput = 16 - 1 * 10; Its clear now, isn't it? And not very complicated...
Using this method, you can pack up to 999999999 input values. I haven't tested this, but if you need it, you can store a pan value for the last 3 and add input before that. This way, you can send input and pan together...Or add input/pan/tilt together and get a FPS input into one variable every 2 frames for example...
Lets change our input function to process this new input form. After we add more input in the 100's we have to change it again, but for now use two digits.
// Runs on SERVER and CLIENT, called from the 'initServer()' and 'initClient()' functions. function processInput() { var movementInput = 0; var shootingInput = 0; var finalInput = 0; if(key_w == 1 && key_s == 0) { if(key_a == 1 && key_d == 0) { movementInput = 8; } if(key_a == 0 && key_d == 1) { movementInput = 2; } if(key_a == 0 && key_d == 0) { movementInput = 1; } } if(key_w == 0 && key_s == 1) { if(key_a == 1 && key_d == 0) { movementInput = 6; } if(key_a == 0 && key_d == 1) { movementInput = 4; } if(key_a == 0 && key_d == 0) { movementInput = 5; } } if(key_w == 0 && key_s == 0) { if(key_a == 1 && key_d == 0) { movementInput = 7; } if(key_a == 0 && key_d == 1) { movementInput = 3; } if(key_a == 0 && key_d == 0) { movementInput = 0; } } if(mouse_left == 1) { shootingInput = 1; } if(mouse_left == 0) { shootingInput = 0; } finalInput = (shootingInput * 10) + movementInput; return(finalInput); }
The change is that we use a variable for each input, and at the end multiply the first input by 10 (to move it as the first digit) and add the second digit.
Also, change our 'interpMovementInput()' function to support our new input:
// Runs on SERVER and CLIENT, called from the 'otherClient_act()', 'player_act()', 'initServer()' and 'initClient()' functions. function interpMovementInput(inputValue, var* speedVector, movementSpeed) { var movementInput; movementInput = inputValue - integer(inputValue / 10) * 10; if(movementInput == 0) { speedVector[0] = 0; speedVector[1] = 0; } if(movementInput == 1) { speedVector[0] = movementSpeed * time_step; speedVector[1] = 0; } if(movementInput == 2) { speedVector[0] = (movementSpeed * 0.707) * time_step; speedVector[1] = -(movementSpeed * 0.707) * time_step; } if(movementInput == 3) { speedVector[0] = 0; speedVector[1] = -movementSpeed * time_step; } if(movementInput == 4) { speedVector[0] = -(movementSpeed * 0.707) * time_step; speedVector[1] = -(movementSpeed * 0.707) * time_step; } if(movementInput == 5) { speedVector[0] = -movementSpeed * time_step; speedVector[1] = 0; } if(movementInput == 6) { speedVector[0] = -(movementSpeed * 0.707) * time_step; speedVector[1] = (movementSpeed * 0.707) * time_step; } if(movementInput == 7) { speedVector[0] = 0; speedVector[1] = movementSpeed * time_step; } if(movementInput == 8) { speedVector[0] = (movementSpeed * 0.707) * time_step; speedVector[1] = (movementSpeed * 0.707) * time_step; } }
Now, we're sending shooting input. But, we still need bullets...
In the 'initClient()' function add this after 'sendPanUpdates()' instruction:
initShooting(player);
And add almost the same to the 'player_act()' function, after 'sendInitAllClients()' instruction and in the 'otherClient_act()' function, after the 'if(player == me)' statement:
initShooting(me);
Don't add it in the 'initServer()' function, because it is already active for the player at the server, called by 'player_act()' that runs on the server!
Lets shoot!
And create the function after the 'moveClient()' function:
// Runs on SERVER and CLIENT, called from the 'otherClient_act()', 'player_act()' and 'initClient()' functions. function initShooting(entity1) { me = entity1; wait(-0.5); var tempvec1[3]; var shootingInput = 0; while(me) { shootingInput = integer(my.clientInput / 10); if(shootingInput == 1) { tempvec1[0] = my.x + 5 * cos(my.pan); tempvec1[1] = my.y + 5 * sin(my.pan); tempvec1[2] = my.z; you = ent_createlocal("bullet.mdl" , tempvec1[0], bullet_act); you.pan = my.pan; you.skill40 = handle(me); you.client_id = my.client_id; wait(7); } wait(1); } }
Here we're breaking down our clientInput skill to get the shooting input. tempvec1 gets a value in front of the entity given as an input parameter and creates a bullet, which runs on the client only! It sets its pan as the entity pan and stores a handle to the entity in skill40 and client_id. These are used to ignore the parent entity when the bullet moves.
All these initShooting functions run only on the machine they're called and act only for the caller's clientInput skill. So, other players on your machine will still use this function with their respective clientInput skills (called through the otherClient_act function).
Now, above that function create a function for the bullets, 'bullet_act()' like this:
// Runs on SERVER and CLIENT, called from the 'initShooting()' function. function bullet_act() { wait(1); var myfuel = 200; my.emask |= (ENABLE_BLOCK | ENABLE_ENTITY); // make entity sensitive for block and entity collision my.event = bullet_event;
while(myfuel > 0) { you = ptr_for_handle(my.skill40); if(you) { myfuel -= c_move(me, vector( 20 * time_step, 0, 0), NULLVECTOR, IGNORE_PASSABLE | IGNORE_CONTENT | USE_POLYGON | IGNORE_YOU ); } else { myfuel -= c_move(me, vector( 20 * time_step, 0, 0), NULLVECTOR, IGNORE_PASSABLE | IGNORE_CONTENT | USE_POLYGON ); } wait(1); } ent_remove(me); }
Waits 1 frame, sets a travel distance of 200 quants, and while it hasn't passed that distance, moves the bullet.
If its skill40 holds a working parent entity, it ignores it.
Now, above this function add the event 'bullet_event()' function:
// Runs on SERVER and CLIENT, auto called from the 'bullet_act()' function on event. function bullet_event() { switch(event_type) { case EVENT_BLOCK: wait(1); ent_remove(me); case EVENT_ENTITY: wait(1); ent_remove(me); } }
This event function just checks if the bullet hit something and removes it. Here we'll add our damage code later.
And, also change the player movement function to ignore the bullets:
// Runs on SERVER and CLIENT, called from various functions. function moveClient(entity1) { me = entity1; var myOldPan; myOldPan = my.pan; my.pan = 0; var distance_moved; distance_moved = c_move(me, my.skill1, nullvector, IGNORE_PASSABLE | IGNORE_PUSH | GLIDE); my.pan = myOldPan; return(distance_moved); }
And, finally, add this to the 'otherClient_act()', 'player_act()' functions after the entity has been initialized, right before the while() loop:
my.push = 50;
And add its equivalent using player. to the 'initClient()' and 'initServer()' functions:
player.push = 50;
This push value will make the c_move ignore the bullets (with default 0 push), but still collide with other players or objects with higher push (in the future).
Don't forget to add the IGNORE_PUSH flag in the interpolation c_move instruction (in the 'initClient()' function). It should look like this:
correction_distance_val = vec_dist(correction_distance[0], NULLVECTOR); if(correction_distance_val > 2) { if(correction_distance_val < 30) { if(player.clientInput != 0) { debug_var[2] = c_move(player , NULLVECTOR , player.correction_X , IGNORE_PASSABLE | IGNORE_PUSH ); vec_sub(correction_distance[0], player.correction_X); } } else { vec_set(player.x , player.shouldBeAt_X ); vec_set(correction_distance[0] , NULLVECTOR ); } }
Do the same for the 'otherClient_act()' function, in the interpolation section, like this (its the same code, but with the 'my' pointer):
correction_distance_val = vec_dist(correction_distance[0], NULLVECTOR); if(correction_distance_val > 2) { if(correction_distance_val < 30) { if(my.clientInput != 0) { debug_var[2] = c_move(me , NULLVECTOR , my.correction_X , IGNORE_PASSABLE | IGNORE_PUSH ); vec_sub(correction_distance[0], my.correction_X); } } else { vec_set(my.x , my.shouldBeAt_X ); vec_set(correction_distance[0] , NULLVECTOR ); } }
Running the app a few times, you'll notice this produced a strange bug. The player.push statement in the 'initClient()' function stops the player from interpolating his position with the server's actual position. Instead, we'll add the push value to our 'moveClient()' function, since all of our players must have a value of 50. Here's the updated 'moveClient()' function:
// Runs on SERVER and CLIENT, called from various functions. function moveClient(entity1) { me = entity1; var myOldPan; myOldPan = my.pan; my.pan = 0; if(my.push < 50) { my.push = 50; } var distance_moved; distance_moved = c_move(me, my.skill1, nullvector, IGNORE_PASSABLE | IGNORE_PUSH | GLIDE); my.pan = myOldPan; return(distance_moved); }
Test a few times. Interpolation should work properly now.
Now you should have a shooting system with a movement, turning system and interpolation system which sums up to a small shooter game, minus the damage...
Breaking the immortality
Lets add some damage. First, declare a health skill in the def.c file after the panAng skill:
#define health skill10
Now, change the 'bullet_event()' function to do some damage if it hit a player and if its running on the server:
// Runs on SERVER and CLIENT, auto called from 'bullet_act()' function on event. function bullet_event() { switch(event_type) { case EVENT_BLOCK: wait(1); ent_remove(me); case EVENT_ENTITY: if(connection == 1 || connection == 3) { if(you.objectType == playerObject) { you.health -= 20; } } wait(1); ent_remove(me); } }
We need to set the health of all players to 100 and reflect this health change. In the 'player_act()' function, add this after the send_skill(my.x...) instruction (packing all initialization values in one place):
my.health = 100; send_skill(my.health, SEND_ALL);
And also send it with every sendInit request in the 'player_act()' while loop. In the if statement for sendInit, add it before the 'my.sendInit = 0' instruction:
send_skill(my.health, SEND_ALL);
So, we have set everybody's health to 100 and we decrease it with every bullet that hits, but we aren't showing any changes yet.
For this tutorial, I've decided to respawn the player if he dies. My top-down shooter will have more complicated mechanics based on level type chosen, so I wont write it here.
To respawn our players when they die, we first need them to die.
Add this in the 'player_act()' function, before the update if statement:
if(my.health <= 0) { send_skill(my.health, SEND_ALL); my.z = -40000; wait(-5); my.health = 100; send_skill(my.health, SEND_ALL); my.z = 32; }
We're just checking the health, and sending it if it is below or equal to 0, and we're moving the entity underground 40000 quants. Then we wait for 5 seconds and move the entity on the ground, setting its health to 100 and sending it to everybody.
Add this in the 'otherClient_act()' function, after the interpolation:
if(my.health <= 0) { my.z = -40000; while(my.health <= 0) { wait(1); } my.z = 32; }
Add one more to the 'initClient()' function, after the interpolation (same code):
if(player.health <= 0) { player.z = -40000; while(player.health <= 0) { wait(1); } player.z = 32; }
If our entity has 0 health, move it underground and wait until the server sends a positive health value. Then, put our entity back above ground.
Because our 'otherClient_act()' function doesn't wait for the health and starts right away, that client could sometimes be shown below ground. So, we either have to wait for health, or set it manually to 100 in the beginning, just before 'initShooting(me)' statement:
my.health = 100;
And, add an if statement to our camera, to keep it above the ground:
// Runs on SERVER and CLIENT, called from the 'initServer()' and 'initClient()' functions. function cameraFollow(entity1) { proc_kill(4); me = entity1; while(me) { if(my.health > 0) { cam_zoom -= mickey.z * 5 * time_step; cam_zoom = clamp(cam_zoom,0,600); vec_set(camera.x, vector(my.x, my.y, my.z + cam_zoom + 48)); vec_set(camera.pan, vector(0, -90, 0)); } wait(1); } }
Make a custom level, and have fun! You can now fight your friends... A side note...
There is a bug appearing sometimes, which I haven't been able to pinpoint.
The shouldBeAt_X update doesnt get processed sometimes, either on a client's player or on other entities (each time different, but only player or only other entities).
There were some mistakes in the code, which I corrected and updated above, which were causing such a behaviour, but I don't see any other reason for this bug to appear.
As a last resort, I changed the shouldBeAt_X check to '!= 0' (was '> 1' before) and so far, the bug hasn't appeared yet, but I'm not convinced this was the issue.
And, a little more info, at a depth, the while loop appears 'not executing' when you use the debug vars and the bug happens. No vars get updated in the loop where the update isn't processed.
Plus, the bug appears/ disappears when you add more lines before the input processing or an else statement of the shouldBeAt_X processing... strange, right?
I don't have the time to get deeper. I will continue my project, and if it is an issue in the future, I will return to this. For now, consider this top-down tutorial complete.
Extending this monster...
I will, probably, update this tutorial with a server list and some other stuff in the future, so keep coming back, just in case.
You could try, on your own, to make different kind of weapons, add ammo checks (and update on reloading), health bars (probably a bad idea) and score keeping with player names.
Don't forget to add a chat, so you can brag about your kills! There are numerous examples in the forums or on the net about implementing chat in MP.
Last thing, I promise!
Don't forget to load new levels on EVERY client. This is important, and required! Always load the same level on every machine. And keep in mind the dplay_encrypt variable.
And hopefully a FPS tutorial will follow soon!
Some facts about Multiplayer
Each function called runs only on the machine calling it!
Only ent_create instructions run functions on the server instead on the client calling them.
Always keep in mind the data you're sending receiving is in the past!
Before you check if a player shot something, consider checking in the past, not the present.Otherwise your players will always have to lead their targets to hit.
Prediction is always wrong!
Use a perfect lag-behind method and the client will never know it. Counter-strike shows you 100ms past events so it can correct without you noticing it...
And, by the way, the age of the modems is long gone. Latency has dropped significantly and bandwidth has increased a lot, so you might not even need prediction.
Never trust the client, but never leave him with unresponsive game!
IMHO its better to have a responsive game filled with hackers, than to have a the best cheat-proofed system that no one plays.
|