Multiplayer scripting

From Bohemia Interactive Community
Jump to: navigation, search


Scripting for multiplayer missions is difficult for many scripters and mission editors to understand. Systems that work perfectly in the mission editor environment can fail miserably when run on a dedicated server, resulting in an inconsistent gameplay experience for clients and frustration for the editor. The keys to success at multiplayer scripting are learning the properties of every scripting command, devising ironclad methods for propagating messages, and testing in an appropriate environment.

Common Multiplayer Scripting Problems

Many scripts fail in a multiplayer environment because they rely on inconsistent variables. Inconsistent variables are conditions that are not guaranteed to be the same for each client in a multiplayer game:

  • Properties of soldiers, vehicles, or other objects (especially their position and velocity)
  • The time command
  • Event handlers that are not executed globally
  • Any random value
  • A unit taking a flag
  • Many others

Any scripted system relying on inconsistent variables (without proper multiplayer scripting techniques) will result in unpredictable states for each client at any given time. Common symptoms for clients include seeing different scores, seeing the mission end at different times, missing hint or radio messages, and seeing different states for flags and other objectives.

Another reason for scripts failing in a multiplayer environment is failure to account for global scripting commands. For example, a simple script that respawns a vehicle using createVehicle may work perfectly in the mission editor, but when run on a dedicated server, a new vehicle is created for every client. That means on a full 64 player server, 65 new vehicles will be created when a vehicle respawns (one for each client and one for the server entity). This kind of error will make a mission unplayable very quickly (especially when someone gets the bright idea of destroying all the extra vehicles, causing them to reproduce exponentially).

Local and Global Scripting Commands

Each scripting command can be described as either local or global. (more on this)

Server-Side Scripting

The best way to solve a problem in which you must rely on an inconsistent variable is to delegate the responsibility to one entity. The only entity we can trust for objective-related duties is the server. The server entity is guaranteed to be present for the length of the mission, and is the only entity with a "true" game state. By making sure that the server is the only entity tracking the status of objectives, we have a starting point for building a multiplayer-safe system.

The most useful commands for controlling multiplayer scripts are local, isServer, and player.

; at this point in the script, everyone except the server will exit
?(!isServer): exit
; only execute this function on the server
?(isServer): call _fnSendMsg
; OFP example: all game logic objects are local to the server at all times,
; so by placing a reference game logic we get the equivalent of isServer
; this code is deprecated in ArmA
?(!local someGameLogic): exit
; exit the script unless you are a client in _vehicle
?(vehicle player != _vehicle): exit

A multiplayer scripter must be fully aware of who is executing every line of code. No code should be executed unless it is necessary for the mission to function. For example, in a sector control mission, a client doesn't need to keep track of who is in a sector when the server is the point of reference for capturing sectors and updating each team's score. Instead, clients should run routines that "listen" for changes in variables broadcast by the server.

publicVariable is the preferred method of propagating messages from the server to all clients. (more about pV)


Single-shot event

; init.sqs
; start client and server monitoring scripts
[] exec "serverScript.sqs"
[] exec "clientScript.sqs"
; serverScript.sqs
; this server-side script waits for a truck to come within 50m of the flag,
; then broadcasts a message to each client and exits
?(!isServer): exit
?(TheTruck distance TheFlag < 50): goto "send"
goto "loop"
TruckInArea = true; publicVariable "TruckInArea"
; clientScript.sqs
; this client-side script waits for the server to broadcast a message,
; then displays a message and exits
hint "Truck is near the flag!"

The example above is a "single-shot" server-triggered event. Note that the client-side script is also run by the server. Otherwise, the system could break on non-dedicated servers. Also, an undeclared variable will never return true, so we don't need to declare TruckInArea ahead of time.

Repeating event

; init.sqs
; start client and server monitoring scripts
[] exec "serverScript.sqs"
[] exec "clientScript.sqs"
; serverScript.sqs
; broadcast a message every time TheMan comes within
; 50m of TheFlag
?(!isServer): exit
@TheMan distance TheFlag < 50
ManIsHere = true; publicVariable "ManIsHere"
@TheMan distance TheFlag > 50
goto "loop"
; clientScript.sqs
; display a message when ManIsDead is received
ManIsHere = false
hint "Man is near the flag!"
goto "loop"

The example above is a repeating server-triggered event. Every time the server sees TheMan approaching TheFlag, ManIsHere is set to true and broadcast. Each client waits for ManIsHere to become true, then sets it to false to prepare for the next broadcast. Note that each client resets the variable locally; setting ManIsHere to false without a following publicVariable does not affect other clients or the server.

Multiple events

; init.sqs
; initialize flag system
; start client and server monitoring scripts
SectorStatus = NO_CAPTURE
[] exec "serverScript.sqs"
[] exec "clientScript.sqs"
; serverScript.sqs
; broadcast a message every time someone takes TheFlag
?(!isServer): exit
@!isNull (flagOwner TheFlag)
?(side (flagOwner TheFlag) == WEST): goto "westCap"
?(side (flagOwner TheFlag) == EAST): goto "eastCap"
?(side (flagOwner TheFlag) == RESISTANCE): goto "guerCap"
; if we are here, bad side for flag owner
goto "resetflag"
SectorStatus = WEST_CAPTURE; publicVariable "SectorStatus"
TheFlag setFlagSide WEST
goto "resetflag"
SectorStatus = EAST_CAPTURE; publicVariable "SectorStatus"
TheFlag setFlagSide EAST
goto "resetflag"
SectorStatus = RES_CAPTURE; publicVariable "SectorStatus"
TheFlag setFlagSide RESISTANCE
TheFlag setFlagOwner objNull
goto "loop"
; clientScript.sqs
; display a message when the sector is captured
@SectorStatus != NO_CAPTURE
?(SectorStatus == WEST_CAPTURE): hint "West captured the sector!"; goto "reset"
?(SectorStatus == EAST_CAPTURE): hint "East captured the sector!"; goto "reset"
?(SectorStatus == RES_CAPTURE): hint "Resistance captured the sector!"; goto "reset"
SectorStatus = NO_CAPTURE
goto "loop"

This is an example of using a single variable to trigger multiple exclusive events repeatedly. This time, the variable and a series of status constants (think of it like an enumerated list in C) must be declared in init.sqs before the monitor scripts begin. When a unit takes the flag, the server changes SectorStatus to the appropriate numerical value and broadcasts it. When the client sees that SectorStatus is no longer in the null state (NO_CAPTURE), it interprets the message and resets SectorStatus to prepare for the next message.

Testing Environment

Most scripters will use a dedicated server for testing their scripts in multi player and 2 client machines. Some will run a copy of ArmA in -server mode on the same machine as a client. This is an ideal setup as you can make sure that what you're seeing on one client is the same on the other client. The purpose of this testing environment is to prepare a mission for public play on a dedicated server, which is the most common target for a multiplayer mission.

Some people will try and test their scripts by hosting a server in game, and while this might work it can sometimes produce inaccurate results when you transfer the missions over to a dedicated server.

A faster testing environment is described in the article here. You need to work disconnected from the Internet, or using the trick described there.


There are also the matter of what happens if a client is disconnected. With ArmA you have the opposite issue introduced by JIP - will that client have a state that will work with the rest?

There are a few ways of affecting the other computers: -publicVariable -CoC Network services -...

Addon Scripting