In this tutorial we will use Client-side prediction and server reconciliation.
It's just a fancy way to say we use a local object and save all positions on the client.
We send the key press info as before to the server and he process it. Server send position info back.
The client compare his positions with the one from the server and check if they match.
First we need to create a local object that the client steer. We will make the server decide when we should create it.
obj_player>Create event "initialise"
if htme_isServer() { AddMultiple(id); }
This will prevent the client from creating the minime object. When obj_player is created on his side.
obj_server_handler>Create event "Collector"
Cut // Collect objects scr_Network_Do_Collector(0);
Pate it before scr_Network_Do_Collector(1);
We need the client to also hold the array. Because the client will use it too.
obj_server_handler>Begin step "Collector"
else { }
// Check if draw array got info if is_array(Draw_array) { }
// Loop array and step each chunk for (var i=0; i<array_length_1d(Draw_array); i+=1) { }
// Step chunk scr_Network_Do_Collector(8,Draw_array[i]);
Here we add a new step process. This will check the received data for events and other special commands to this client.
We will make the server send an event to create an obj_player on the client it will be link them with an id number (network number).
obj_server_handler>Draw event after "while (ds_queue_size(saved_arrays))..."
// Check if draw array got info if is_array(Draw_array) { }
// Loop array and step each chunk for (var i=0; i<array_length_1d(Draw_array); i+=1) { }
// Step chunk scr_Network_Do_Collector(8,Draw_array[i]);
Here we need to also process the step before we scrap the array and use the next one.
We also need some help scripts.
Scripts>Network
Create a new script
/// htme_get_my_player_number(); return htme_getPlayerNumber(global.htme_object.playerhash);
This will return the local player number.
Scripts>Network>Collector
New script
/// scr_Network_Do_Collector_Set_Number(); network_number=id-100000;
This will be the id link between the server and the client.
Game maker id start with 100000 so we just remove that amount so we can use a smaller buffer.
obj_player>Create event "Init for network"
// Set network number scr_Network_Do_Collector_Set_Number();
This will add a network number to this obj_player. We will make the server send this number when he create his obj_player to "link" them together.
New script
/// scr_Network_Do_Collector_Set_PlayerNumber(id); argument0.player_number=htme_getPlayerNumber(htme_get_instance_hash());
This will set the player_number for any instance. So we know who is the owner of it.
Missed "Do"
Create a new script
/// scr_Network_Do_Collector_Get_Object_Number(object_index);
with obj_server_handler { }
for (var i=0; i<array_length_1d(Collector_objects_array); i+=1) { }
if Collector_objects_array[i]=argument0 { }
// Return index return i;
return noone;
Here we take an object and check what index it got in the collect array. Also to save buffer space. Else we would need to send the object_index.
New script
/// scr_Network_Do_Collector_Event_Create_Object(id);
var id_=argument0; var data_array=noone; // Id for create event data_array[0]=-2;
// who should create this data_array[1]=id_.player_number // link id data_array[2]=id_.network_number
// x data_array[3]=id_.x; // y data_array[4]=id_.y;
// Object number data_array[5]=scr_Network_Do_Collector_Get_Object_Number(id_.object_index);
// Add in collection to send if data_array[5]>-1 { }
scr_Network_Do_Collector(9,data_array);
This will be our event script that creates a new special chunk -2.
-2 will be an "create object event" on the clients and use the network number (id-100000) as a link.
So the server can send info and just tell the network id to do something. This way we dont need the player_number anymore.
obj_client_handler>Begin step "Create player"
// Add create event on local scr_Network_Do_Collector_Event_Create_Object(PlayerID);
Replace PlayerID.player_number=htme_getPlayerNumber(htme_get_instance_hash());
scr_Network_Do_Collector_Set_PlayerNumber(PlayerID);
When the server create a clients obj_player we create an "create event" to make that client to also create an obj_player.
Scripts>scr_Network_Steer_player
/// scr_Network_Steer_player(data string, [local]);
var data_string=argument[0];
var temp_check=false;
if argument_count>1 { }
if htme_isLocal() temp_check=true;
else { }
if htme_get_instance_hash()=htme_get_last_rpc_hash() temp_check=true;
if temp_check
Here we make it possible for the clients to use this script for his local obj_player.
obj_player_keylogger>step event
// Handle client side if !htme_isServer() { }
scr_Network_Steer_player(SendString,true);
This will give our controls to our local obj_player.
We will also set the client to follow obj_player too. Because he will now have one.
obj_client_handler>Step Begin "Create player"
//view_object[0]=id;
By default the game follow obj_player.And now all players got one.
scr_Network_Do_Collector
temp_array[4]=buffer_u16; // network link number (0 to 65,535)
We dont need the player number for the view. We will make the client follow his local obj_player.
// Events // -1 Step Collector_buffer_event_array[1]=noone; temp_array=noone;
// -2 Create event temp_array[0]=buffer_u8; // player number temp_array[1]=buffer_u16; // network number
temp_array[2]=buffer_s16; // x temp_array[3]=buffer_s16; // y
temp_array[4]=buffer_u8; // object array index Collector_buffer_event_array[2]=temp_array;
temp_array=noone;
Here we setup our new event buffer array. We will use this to make the server force the client create a obj_player on his side.
network_number
We use network number instead.
if is_array(chunk_array) { }
Remove: scr_Network_Set_Pos_Of_Local(chunk_array[1],chunk_array[2],obj_client_handler,chunk_array[5]);
We must now check if the chunk is an array because when a event has run we will set it to noone in the draw array. This way it only happen once.
case -2:
// Chunk id is the buffer array index
var buffer_array=Collector_buffer_event_array[abs(chunk_id)];
case -2:
// Chunk id is the buffer array index
var buffer_array=Collector_buffer_event_array[abs(chunk_id)];
Here we make use of the event buffer arrays
case 8: break;
case 9: break;
// Step event var chunk_array=argument[1];
if is_array(chunk_array) { }
switch (chunk_array[0]) { }
case -2:
// Create event // Check if this is our
if chunk_array[1]=htme_get_my_player_number() { }
// Create object
with instance_create(chunk_array[3],chunk_array[4],Collector_objects_array[chunk_array[5]]) { }
// Set player number player_number=chunk_array[1]; // Set network link number network_number=chunk_array[2];
// Do object specific stuff switch (object_index) { }
case obj_player: break;
// Add player to local obj_client_handler (to make it responce to player control)
with obj_client_handler { }
if htme_isLocal() PlayerID=other.id;
// Only do this once chunk_array=noone; break;
// Return return chunk_array;
Ok. What do we do here:
We check what event we got
If this event is for us we process it.
Create the object and give it some values.
If it is a obj_player we check for our obj_client_handler and set the PlayerID. This gives us control over it and it will respond to our inputs.
// Add In Collector var Data_array=argument[1];
// Make it possible to call this from anywhere
with obj_server_handler { }
// Loop the provided array for (var j=0; j<array_length_1d(Data_array); j+=1) { }
// And in collector queue ds_queue_enqueue(Collector_que,Data_array[j]);
Here we just take an array an put it in the collector. Ex from the scr_Network_Do_Collector_Event_Create_Object.
Ok. We now can do a test run.
Create a server and connect P2.
When we move P2 we see two sets. Our local obj_player and the server one.
You notice that out local obj_player move instancly to our key presses.
And the server is a little behind. This is because of the simulated network lag.
We will now use the server info validate our local player. So the server info is always in command. If there is a differrence we use the server info. Because the server is king.
obj_client_handler>Create event
Add code block
/// Validator Collector_que=noone; Validator_list=noone;
if htme_isLocal() and !htme_isServer() { }
// Collect in queue Collector_que=ds_queue_create();
// Validator list Validator_list=ds_list_create();
// Collect objects scr_Network_Do_Collector(0);
We will reuse some of the collector code. We will start to log our local objects.
Destroy event
if Collector_que>-1 ds_queue_destroy(Collector_que); if Validator_list>-1 ds_list_destroy(Validator_list);
Here we just clean up.
Begin step add code block
/// Validator if htme_isLocal() and !htme_isServer() { }
// Loop objects for (var i=0; i<array_length_1d(Collector_objects_array); i+=1) { }
// Get info from instance with Collector_objects_array[i] { }
// Collect info chunk scr_Network_Do_Collector(2); // Turn queue to array and save in list var temp_array=noone;
var array_counter=0;
while !ds_queue_empty(other.Collector_que) { }
// Add to array temp_array[array_counter]=ds_queue_dequeue(other.Collector_que);
// Increase count array_counter+=1;
// Save array if is_array(temp_array) { }
// Add network number ds_list_add(other.Validator_list,network_number); // Add data ds_list_add(other.Validator_list,temp_array);
This will collect and save the same values the server do. We will now make the client compare these with the one we get from the server.
Notice that we save the network number first and then the array. This makes it possible to do a quick search for the network number in the list and we can then just grab the array in the next list index.
We will now create the script that compare the server data and client data and fix differences.
Scripts>Network>Collector
Create a new script
/// scr_Network_Do_Collector_Change_History(network number, diff, index to apply on);
var nn=argument0; var diff=argument1; var apply_index=argument2; var temp_array=-1;
for (var i=0; i<ds_list_size(Validator_list); i+=2) { }
// Check if same network number if Validator_list[| i]=nn { }
// Get the array temp_array=Validator_list[| i+1]; // Add diff temp_array[@ apply_index]+=diff;
Here we add a diff value to all the arrays in the list. The @ make sure we reference back to the original array.
Create a new script
/// scr_Network_Do_Collector_Validator_Check(object_index, network number, server array, client array);
var theObject=argument0; var nn=argument1; var server_array=argument2; var client_array=argument3;
var id_=noone;
// Find our instance with theObject { }
if network_number=nn { }
id_=id;
// Compare and fix if id_>-1 { }
for (var i=0; i<array_length_1d(server_array); i+=1) { }
switch (theObject) { }
case obj_player: break;
switch (i) { }
case 1: // x break;
// Compare if server_array[i]!=client_array[i] { }
// Resolve difference var diff_value=server_array[i]-client_array[i];
// Apply the diff to our historical collected info scr_Network_Do_Collector_Change_History(nn,diff_value,i);
// Apply the diff to the current instance id_.x+=diff_value*0.5;
Copy the case statement
Paste below
case 2: // y
id_.y+=diff_value*0.5;
Here we compare the server and client array.
If there is any diff we apply the diff to our historical collected info. Else it would detect a diff in the next step and add the diff to our player again.
Then we apply the diff to our current player. In a way we turn back the time for our player.
Ok. We will validate the data when we get it from the server.
Scripts>Network>Collector
Create a new script
/// scr_Network_Do_Collector_Validator(theValue_array);
var theValue_array=argument0; var count_total=0; var onlyDoOnce=true;
with obj_client_handler { }
if htme_isLocal() { }
switch (theValue_array[0]) { }
case 0: break;
// obj_player // Check if we got this network number
for (var j=0; j<ds_list_size(Validator_list); j+=2) { }
// Check if same network number if Validator_list[| j]=theValue_array[5] { }
if onlyDoOnce { }
// Compare and fix
scr_Network_Do_Collector_Validator_Check(obj_player,theValue_array[5],theValue_array,Validator_list[| j+1]);
// Hide server draw and use our local obj_player draw
// -128 is now our hidden chunk id theValue_array[@ 0]=-128;
// Remove processed client data // Network number
ds_list_delete(Validator_list,j); // Data array ds_list_delete(Validator_list,j);
// Only process the found onlyDoOnce=false;
// Count count_total+=1;
// Check if to many,if the server is slow we must reset out list else wi will get problems
if count_total>obj_server_handler.Collector_collect_steps*2 ds_list_clear(Validator_list);
return theValue_array;
Ok. What do this do?
We get the chunk id and if its a 0 aka obj_player we continue
We check if this obj_player is our by comparing the network numbers in our list.
Here we take the server and client array and compare them fix any differences.
When processed we can now remove the local data so the next in the list will process in the next step.
Note that we call ds_list_delete(Validator_list,j) twice. Because when we remove a index from a list the list will shift the index down by one. So j+1 becomes j.
scr_Network_Do_Collector
// Validator theValue_array=scr_Network_Do_Collector_Validator(theValue_array);
Ok. Now when the client receive info we validate it.
When using client side prediction we need values from the server. So we must force the server to send us updates.
obj_server_handler>Begin step "Collector"
//if data_chunk!=data_chunk_last //{ //}
Now the server will always send updates to the clients.
Ok. Time for a test.
Ok. You will notice that the p2 jitter in some cases. Even if the server and P2 process the same keystrokes the server may be some milliseconds off. So the results will we different on the server and client.
We can fix this with a tolerance value in the validator or use *0.25 instead of *0.5 when applying the diff value to the player.We will try both in the next tutorial.
We will also check what options we have with the follower. You notice that on P2 the follower is further away.
Không có nhận xét nào:
Đăng nhận xét