Nuclide
Software Development Kit for id Technology (BETA)
|
Networking in FTE is an extension of the design decisions made for QuakeWorld. All you have to know is that we use NACK feedback to network the changes from client to server. There are a lot of other great resources that document the QuakeWorld protocol, so we will focus on the additions in FTE that matter here.
In Nuclide, we make heavy use of custom entity networking. In FTE, you can set the server-side entity field .SendEntity
and .SendFlags
to control network updates for any given entity. You have to implement your own .SendEntity
function that communicates:
flChanged
in code a lot)To see the type of entity updates we handle internally in Nuclide, look no further than src/shared/entities.h
. For one-time event updates, you can check src/shared/events.h
.
Generally, those entity updates happen and are controlled by these three common methods that are implemented by any entity class that chooses to override networking:
Let's go over them, one by one.
Called once every frame. This is where we check if anything has changed. We commonly use the macros EVALUATE_FIELD( field, changedFlag )
and EVALUATE_VECTOR( field, xyz, changedFLAG )
to test if any particular field has changed since last frame.
For these macros to work, your entity needs to have declared attributes with the PREDICTED_INT( x )
, PREDICTED_FLOAT( x )
, PREDICTED_VECTOR( x )
, PREDICTED_BOOL( x )
, PREDICTED_ENTITY( x )
, or PREDICTED_STRING( x )
macros.
Once the frame ends, we will network the differences in the next method.
As already mentioned in the FTE section, this is where the actual networking gets done.
This is where we have helper macros for dealing with sending and flagging the updates reliably.
Most of them are self explanatory as the names refer to the size and datatype being sent, but in the case of SENDENTITY_ANGLE
we actually network a short, since we don't need full floating point precision for most types of angles. Keep that in mind if you're running into precision issues.
The ReceiveEntity method is called on the client-side for each respective entity class we want to handle. First however, we need to talk about the handler.
We allow any game to implement their own handler for entity updates in the function ClientGame_EntityUpdate(float type, bool isNew)
which you're encouraged to implement. This is where we check for a handler first, then Nuclide will attempt to handle it instead.
We usually just read the flags field (which we assume is a float for most entities) and then call the ReceiveEntity(float isNew, float flChanged)
method, in which we'll use the following macros to help read networked information:
As you can tell, it's the same setup as in SendEntity
- which is by design. This will make keeping fields in check much easier. A simple find-and-replace of the word SEND with READ will do the job most of the time.
We'd like to streamline a lot of this further in the future, so this may be subject to change.
The server will use the FTE supported SVC_CGAMEPACKET
packet type to network events.
An example of such an event is as follows, it can be called at any time, anywhere on the server:
This event will then be sent to ALL clients, reliably as indicated by the MULTICAST_ALL_R. On the client-side, we will handle any event in Event_Parse(float eventType)
that you are not handling yourself within ClientGame_EventParse(float eventType)
.
From there you can use the builtins readbyte()
, readshort()
etc. like so:
There's a few different means of networking things to the server as a client.
sendevent()
is the builtin you call from the client game to communicate, reliably, to the server game running on the other end.
The first parameter of sendevent()
specifies the function to call in server game:
However, it will not look for void() Myfunction
on the server game, there is a prefix reserved for all sendevent calls. That one is CSEv
. So in reality, the above command will execute this on the server game:
void CSEv_Myfunction ( void ) { }
Now, what about that second parameter?
.. , "" );
Well, sendevent()
has the ability to send data. 6 arguments can be passed, max. That second parameter string specifies the type of data you want to send.
For example, you want to ask the server to set the health of a player to a specific value...
Client game:
Server game:
As you can see, the f
indicates the type float. s
would indicate a string. v
would indicate a vector. i
would indicate an integer. e
would indicate an entity (more about that in the last chapter).
The second parameter specifying the arguments will append to the name of the function you're trying to call.
Client game:
Server game:
..is what a longer sendevent with multiple arguments would look like. 6 arguments are the maximum. This is because QuakeC supports 8 arguments max per function call. If you, however, only want to send floats and require more than 6 arguments, you can store them inside vectors:
Client game:
Server game:
If you pass an entity via sendevent, it'll in reality send only the entnum. The entnum is the essentially the entity-id. If the entity does not exist via the server game (like, it has been removed since or is a client-side only entity) then the entity parameter on the server game function call will return world
aka 0/__NULL__.
For protective reasons, entities that are removed have their entnums reserved for a specific amount of time before being able to be used again. Player entities, for example, will allow their entnums to be recycled after 2 seconds. Any other entity its entnum will be reusable after only half a second. This should avoid most entnum conflicts.
Basically, during this time, the parameter will not return world
.
Note: This is entirely unreliable behaviour. Check if the .classname is valid on the server before doing anything fancy on it.
Whenever the client issues a cmd
based command, say: cmd say foobar
in console or via the client game in general, the server will forward it to the active ncGameRules based game rule class within the ClientCommand(ncClient client, string command)
method. This is useful for things you do every once in a while.
ServerInfo keys can be set by server admins, or the game-logic. Those keys are networked to clients inside the game, as well as outside of the game. They can be queried through tools like qstat
or GameSpy
.
When in the console, you can set the key foo
to the value bar
like this:
The same thing can be done in the SSQC side with this line of code:
At any time, the server and clients can enter serverinfo
by itself into the console, and it will print out all known ServerInfo keys.
Some of the keys are set by the engine, noteworthy ones include *bspversion
and mapname
. I encourage you try it and see if there's any keys that seem interesting to you and to make note of them.
We have two builtins to query ServerInfo keys from the client-side. serverkey
and serverkeyfloat
. Their usage goes like this:
UserInfo keys are means of allowing clients (and the server, more on that later) to communicate bits of information about themselves to the server and all other clients. Much like ServerInfo. Those are networked regardless of whether another client is in the same PVS.
Clients can set their own custom infokey data at any time, using the setinfo
console command. For example: setinfo foo bar
will set the infokey foo
to the value of bar
.
The server can also assign its own infokeys to a player. These can be overriden by the client at any given time - unless the server prefixes them with an asterisk. Here is an example: forceinfokey(somePlayer, "*team", "1");
Client infokeys are visible to all other clients. That's how most of the scoreboard is filled in with information. So be careful about the type of data you store in them. Storing passwords or other sensitive information is not recommended.
On the client-side, if you wanted to query your own players' specific infokey value, you can query it like this:
And if you want to query a specific player entity, this is perfectly valid:
As to why we have to subtract 1 from an entity its entnum for this, is because the clients in the infokey table start at 0, and player entities on the server start at 1; 0 being reserved for world
.
The server can then read any given infokey on a given client like so: