Rebel Fork Framework
|
The Network subsystem provides reliable and unreliable UDP messaging using SLikeNet. A server can be created that listens for incoming connections, and client connections can be made to the server. After connecting, code running on the server can assign the client into a scene to enable scene replication, provided that when connecting, the client specified a blank scene for receiving the updates.
Scene replication is one-directional: the server always has authority and sends scene updates to the client at a fixed update rate, by default 30 FPS. The client responds by sending controls updates (buttons, yaw and pitch + possible extra data) also at a fixed rate.
Bidirectional communication between the server and the client can happen either using raw network messages, which are binary-serialized data, or remote events, which operate like ordinary events, but are processed on the receiving end only. Code on the server can send messages or remote events either to one client, all clients assigned into a particular scene, or to all connected clients. In contrast the client can only send messages or remote events to the server, not directly to other clients.
Note that if a particular networked application does not need scene replication, network messages and remote events can also be transmitted without assigning the client to a scene. The Chat example does just that: it does not create a scene either on the server or the client.
Starting the server and connecting to it both happen through the Network subsystem. See StartServer() and Connect(). A UDP port must be chosen; the examples use the port 2345.
Note the scene (to be used for replication) and identity VariantMap supplied as parameters when connecting. The identity data can contain for example the user name or credentials, it is completely application-specified. The identity data is sent right after connecting and causes the E_CLIENTIDENTITY event to be sent on the server when received. By subscribing to this event, server code can examine incoming connections and accept or deny them. The default is to accept all connections.
After connecting successfully, client code can get the Connection object representing the server connection, see GetServerConnection(). Likewise, on the server a Connection object will be created for each connected client, and these can be iterated through. This object is used to send network messages or remote events to the remote peer, to assign the client into a scene (on the server only), or to disconnect.
It is possible to use NAT punchtrough functionality with network subsystem. This requires NAT punchtrough master server to be running on a public host. To set it up you first have to tell the networking subsystem the IP address or a domain name to NAT punchtrough master server, to do this, call SetNATServerInfo().
Server: Server should be started first by calling StartServer() and after that StartNATClient(). If connection to NAT punchtrough master server succeeds, unique GUID will be generated. Clients should use this generated GUID to connect to this server.
Client: When server has been successfully started and connected with NAT punchtrough master server, clients should connect to server by calling AttemptNATPunchtrough() and pass in the server generated GUID.
For a demonstration, check example 52_NATPunchtrough.
Network subsystem supports LAN discovery mode to search for running servers. When creating server you can set what data should be sent to the network when someone starts LAN discovery mode, see SetDiscoveryBeacon(). This data can contain any information about the server. To start LAN discovery mode you should call DiscoverHosts(). When server is found, "NetworkHostDiscovered" event will be sent out.
For a demonstration, check example 53_LANDiscovery.
Network replication of scene content has been implemented in a straightforward manner, using attributes. Nodes and components that have not been created in local mode - see the CreateMode parameter of CreateChild() or CreateComponent() - will be automatically replicated. Note that a replicated component created into a local node will not be replicated, as the node's locality is checked first.
The CreateMode translates into two different node and component ID ranges - replicated ID's range from 0x1 to 0xffffff, while local ID's range from 0x1000000 to 0xffffffff. This means there is a maximum of 16777215 replicated nodes or components in a scene.
If the scene was originally loaded from a file on the server, the client will also load the scene from the same file first. In this case all predefined, static objects such as the world geometry should be defined as local nodes, so that they are not needlessly retransmitted through the network during the initial update, and do not exhaust the more limited replicated ID range.
The server can be made to transmit needed resource packages to the client. This requires attaching the package files to the Scene by calling AddRequiredPackageFile(). On the client, a cache directory for the packages must be chosen before receiving them is possible: see SetPackageCacheDir().
There are some things to watch out for:
Scene replication includes a simple, distance-based interest management mechanism for reducing bandwidth use. To use, create the NetworkPriority component to a Node you wish to apply interest management to. The component can be created as local, as it is not important to the clients.
This component has three parameters for controlling the update frequency: base priority, distance factor, and minimum priority.
A current priority value is calculated on each server update as "base priority - distance factor * distance." Additionally, it can never go lower than the minimum priority. This value is then added to an update accumulator. Whenever the update accumulator reaches 100.0, the attribute changes to the node and its components are sent, and the accumulator is reset.
The default values are base priority 100.0, distance factor 0.0, and minimum priority 0.0. This means that by default an update is always sent (which is also the case if the node has no NetworkPriority component.) Additionally, there is a rule that the node's owner connection always receives updates at full frequency. This rule can be controlled by calling SetAlwaysUpdateOwner().
Calculating the distance requires the client to tell its current observer position (typically, either the camera's or the player character's world position.) This is accomplished by the client code calling SetPosition() on the server connection. The client can also tell its current observer rotation by calling SetRotation() but that will only be useful for custom logic, as it is not used by the NetworkPriority component.
For now, creation and removal of nodes is always sent immediately, without consulting interest management. This is based on the assumption that nodes' motion updates consume the most bandwidth.
The Controls structure is used to send controls information from the client to the server, by default also at 30 FPS. This includes held down buttons, which is an application-defined 32-bit bitfield, floating point yaw and pitch, and possible extra data (for example the currently selected weapon) stored within a VariantMap.
It is up to the client code to ensure they are kept up-to-date, by calling SetControls() on the server connection. The event E_NETWORKUPDDATE will be sent to remind of the impending update, and the event E_NETWORKUPDATESENT will be sent after the update. The controls can then be inspected on the server side by calling GetControls().
The controls update message also includes a running 8-bit timestamp, see GetTimeStamp(), and the client's observer position / rotation for interest management. To conserve bandwidth, the position and rotation values are left out if they have never been assigned.
Urho3D does not implement built-in client-side prediction for player controllable objects due to the difficulty of rewinding and resimulating a generic physics simulation. However, when not using physics for player movement, it is possible to intercept the authoritative network updates from the server and build a prediction system on the application level.
By calling SetInterceptNetworkUpdate() the update of an individual networked attribute is redirected to send an event (E_INTERCEPTNETWORKUPDATE) instead of applying the attribute value directly. This should be called on the client for the node or component that is to be predicted. For example to redirect a Node's position update:
The event includes the attribute name, index, new value as a Variant, and the latest 8-bit controls timestamp that the server has seen from the client. Typically, the event handler would store the value that arrived from the server and set an internal "update arrived" flag, which the application logic update code could use later on the same frame, by taking the server-sent value and replaying any user input on top of it. The timestamp value can be used to estimate how many client controls packets have been sent during the roundtrip time, and how much input needs to be replayed.
All network messages have an integer ID. The first ID you can use for custom messages is 153 (lower ID's are either reserved for SLikeNet's or the Network subsystem's internal use.) Messages can be sent either unreliably or reliably, in-order or unordered. The data payload is simply raw binary data that can be crafted by using for example VectorBuffer.
To send a message to a Connection, use its SendMessage() function. On the server, messages can also be broadcast to all client connections by calling the BroadcastMessage() function.
When a message is received, and it is not an internal protocol message, it will be forwarded as the E_NETWORKMESSAGE event. See the Chat example for details of sending and receiving.
For high performance, consider using unordered messages, because for in-order messages there is only a single channel within the connection, and all previous in-order messages must arrive first before a new one can be processed.
A remote event consists of its event type (name hash), a flag that tells whether it is to be sent in-order or unordered, and the event data VariantMap. It can optionally be set to originate from a specific Node in the receiver's scene ("remote node event.")
To send a remote event to a Connection, use its SendRemoteEvent() function. To broadcast remote events to several connections at once (server only), use Network's BroadcastRemoteEvent() function.
For safety, allowed remote event types must be registered. See RegisterRemoteEvent(). The registration affects only receiving events; sending whatever event is always allowed. There is a fixed blacklist of event types defined in Source/Urho3D/Network/Network.cpp that pose a security risk and are never allowed to be registered for reception; for example E_CONSOLECOMMAND.
Like with ordinary events, in script remote event types are strings instead of name hashes for convenience.
Remote events will always have the originating connection as a parameter in the event data. Here is how to get it in both C++ and script (in C++, include NetworkEvents.h):
C++:
Script:
In addition to UDP messaging, the network subsystem allows to make HTTP requests. Use the MakeHttpRequest() function for this. You can specify the URL, the verb to use (default GET if empty), optional headers and optional post data. The HttpRequest object that is returned acts like a Deserializer, and you can read the response data in suitably sized chunks. After the whole response is read, the connection closes. The connection can also be closed early by allowing the request object to expire.
The Network subsystem can optionally add delay to sending packets, as well as simulate packet loss. See SetSimulatedLatency() and SetSimulatedPacketLoss().