HOW MULTIPLAYER WORKS
The multiplayer in Northgard is based on a simple host/client architecture. The host machine, from which a game was created, is responsible for managing all gameplay related actions, like moving units, activating abilities, controlling AI and counting scores. The client machines are the ones joining the lobby, reading the state of the game from the host and sending commands to the host each time the player does something.
In the case of scripting, this means most of the scripts that affect the gameplay must be executed only by the Host, while the parts that affect the UI or feedback can be executed by everyone.
Some examples of script affecting gameplay are :
- Changing a player’s resources,
- Spawning/killing a unit,
- Giving a move order to a unit,
- Updating a player’s objectives,
- Making a player win.
Some examples of script affecting UI or feedback :
- Showing a top-left corner notification,
- Showing a ping on the map,
- Starting a dialog,
- Using trace().
NEW FEATURES FOR MULTIPLAYER SCRIPTING IN 1.4
Who is the host?
function init() { if(isHost()){ // Write script for the host only }
// Write script for everyone }
Callback and feedback
Usually, when you call a script function as always, it is executed only on the machine running the script. Now that the host can have ownership of the gameplay script, it is important that it can communicate the results with the clients. For that, you now have the functions invoke() and invokeAll(). invoke() allows you to execute a function on a specific client, while invokeAll() will execute a function on every machine, including the host one.
Here is a simple example of a script where the host rolls a dice for each player and has them display the result.
function init() { if(isHost()){ for(player in state.players){ // Create a list of arguments var args : Array<Dynamic> = []; // Push the result of the roll args.push(math.irandom(6)); // Invoke displayResult on the player with the given argument invoke(p, “displayResult”, args); } } }
// This function will only be called using invoke function displayResult(result : Int){ trace(“You rolled “ + (result + 1)); }
Sending commands to the host
Now that we know how to invoke functions on the client, it is interesting to send commands to the host. The best situation to send commands is to allow the players to choose options using the objectives’ buttons.
For that, we now have the function invokeHost(), that will execute a function on the host at the demand of the client.
Since the function called by invokeHost() will always be executed on the host, you can call any [HostOnly] functions there to react to your client’s commands.
Network overload and infinite loop
When scripting for multiplayer, be really mindful of one thing : There is no security over when and how much you use invoke() and invokeHost(). If you invoke two functions that keep invoking each other, this will create an infinite loop that will slow down performances and might saturate your network.
OBJECTIVES IN 1.4
Previously, the objectives were local to a machine. Since 1.4, they are now owned by each player and managed by the host.
Thanks to these changes, each player in a multiplayer game can have his own sets of objectives. But that also means that only the host can validate an objective and change them. This means all objective operations must be wrapped inside a if(isHost()) condition!
Also note that Northgard’s AI player cannot interact with objective buttons. It is a good idea to not give objectives to AI players. You can test if a player is an AI with the Player.isAI() function.
Now that you have invoke() and invokeHost(), you can have your host and clients communicate both ways. The following script is an application of both. We just ask each player a question, and the host keeps track of all the answers.
var answers : Array<{p:Player, a:Int}> = [];
function init() { if (state.time == 0) onFirstLaunch(); }
function onFirstLaunch() { // Only the host setup the objectives if(isHost()){ // Each players has its own set of objectives for(p in state.players){ p.objectives.add("question", "Is Spidyy awesome?"); p.objectives.add("yes", "Yes ! A God !", null, {name:"Yes", action:"answerYes"}); p.objectives.add("no", "No. I mean, who's that?", null, {name:"No", action:"answerNo"}); }
// The host get an additional set of objectives to see the answer of each player. for(p in state.players){ me().objectives.add("answered" + playerId(p), p.name + " answered..."); } } }
// Just assign an index to a player function playerId(p : Player) : Int{
var i = 0; for(other in state.startPlayers){ if(other == p) return i; i++; } return -1; }
// Called from client, send a message to server function answerYes(){ // Create a list of arguments var args : Array<Dynamic> = []; // add the client's player. Could be the player ID instead. Needed for the callback. args.push(me()); // add the answer args.push(1); // send to the server invokeHost("clientAnswer", args); }
// Called from client, send a message to server function answerNo(){ var args : Array<Dynamic> = []; args.push(me()); args.push(0); invokeHost("clientAnswer", args); }
// Called on the server by the client using invokeHost function clientAnswer(p:Player, a:Int){ // Save the answers for(o in answers){ if(o.p == p) o.a = a; }
// Update the client's objectives p.objectives.setStatus("question", OStatus.Done); p.objectives.setStatus("yes", (a == 1)? OStatus.Done : OStatus.Missed); p.objectives.setStatus("no", (a == 0)? OStatus.Done : OStatus.Missed);
// Update our host objective for this player me().objectives.setStatus("answered" + playerId(p), (a == 1)? OStatus.Done : OStatus.Missed);
// Invoke a callback function so the client gets noticed that his answer got properly received. var args : Array<Dynamic> = []; args.push(a); invoke(p, "confirmation", args); }
// Called on the client by the server using invoke function confirmation(a:Int){ @async{ talk("You answered " + ((a == 1)? "Yes" : "No")); } }