Event systems over a network

Started by
8 comments, last by Kylotan 1 year, 7 months ago

I have a question about designing event systems for network-enabled games. I'm writing it from scratch, for fun, and am working on the details in my head of how this system should work

If you have a character with a gun and they fire a gun, my assumption that this happens locally and remotely

The "fire event" occurs

local machine: The animation for the fun firing happens. Any secondary effects (sparks, sprites, ammo ejecting, etc) also occurs

local -> remote: The "fire event" is sent to the server, I'm guessing as a hard-coded integer, that represents the action that the user has taken

server -> local: The server tells all other connected players that it received the fire event and plays the same animation back on their side

It's simple in concept (assuming I'm right so far). I was hoping to ask about implementation details about this.

To minimize information needed to be sent to the server and to prevent cheating, I'm guessing you wouldn't want to send raw client-side values directly to the event system. I could totally see a cheater reverse engineering the event system to say "my bullet just did 9999999 damage".

And because there will be some latency between the point that the local user shooting the gun fires -> sent to server -> broadcasted to the other clients, it'd make sense to also store...

- The time that the event occurred

- The player that generated the event (their ID)

And when the code goes "server -> local", you'd diff the local machine's current time by the received time. If the recipient has a lot of latency or a poor connection, the fire animation starts not at frame 0 but maybe frame 3 (based on the diff). And you use the player ID from the event to determine who to apply the animation to.

I haven't thought much about how hitscan would work in this system. Maybe the event system prioritizes the shooter's local session. e.g. if they think they hit the other player (even if the other player has a different real position) then the shot counts. Stuff like that.

Anyway to wrap it up, I was thinking that an event would look like this:

struct FireEvent {
    // all events have these
    event_id: 1234;
    origin_time: date-time-here;  // epoch float value

    // player-originated events have these	
    string player_id;

    // Xformable events have these
    position: [x, y, z];
    orientation: some_quaternion;
	
    // FireEvent has this
    weapon_id: 5678;  // pistol
    hit: other_player_id,
};

And then I'd implement this system not just for firing but for jumping, sprinting, etc. Anything else that is an action that generates a change in a user's state. I'm not super sure when to not make something an Event but I guess I can feel that part out as I go.

Is that all sound or did I miss anything? There's other details I'm interested in, regarding anti-cheat, but this post is pretty long and those details aren't as important.

Advertisement

How do you decide you actually hit anyone (and/or globally agree on which one)? Everything is at a different position at different points in time at different computers.

__nostromo__ said:
Is that all sound or did I miss anything? There's other details I'm interested in, regarding anti-cheat, but this post is pretty long and those details aren't as important.

That looks like you've covered the basics.

Typically the client sends more information like where they think the player was aiming, in case the client was somewhat lagged or otherwise out of sync locally.

The server/host validates it, which is where an element of anti-cheat comes in. Always validate everything, and at the very least flag anything that looks suspicious. Verify that packets are the expected size, that the ID's and member values are within expected ranges, that indices are legal, etc. Then validate that the player could realistically be where they said they were, they could realistically aim at what they said they were aiming at, that they had ammunition and whatever else.

Once validated, the server broadcasts the information to others based on relevancy. In small games you can probably broadcast to everyone, but in bigger games it gets filtered based on relevance, such as proximity/visibility.

As the server/host sends out data about the shot such as hit/miss and location, it can also send time stamps and IDs as needed. But those are also details that tend to grow in time with the system and get sent with all communications.

@Alberth

> How do you decide you actually hit anyone (and/or globally agree on which one)? Everything is at a different position at different points in time at different computers.

I'm still new to all this but I'd imagine I'd either favor the local client and reject it if it's wrong, server-side, or just have the client send their position and rotation and compute the players hit over the server. Not sure which approach is better or if there's a 3rd, better choice.

@frob

> Verify that packets are the expected size

I assume by this you mean literally something like "check that if you expect a 12 byte object that you get a 12 byte object". Is there a situation where all of the member values are within expected ranges but the size is incorrect? What would that look like?


> In small games you can probably broadcast to everyone, but in bigger games it gets filtered based on relevance, such as proximity/visibility.

Oh that's smart. Like if you know someone is too far away to hear / see a shot, don't update? I guess you could also do smart things like, if a player is getting hit by multiple sources at once, subtract the damage from all sources and send that instead of updating the user repeatedly. Or if updating repeatedly, you can smooth that out over a few frames so the FPS doesn't tank.

@frob

> Verify that packets are the expected size

I assume by this you mean literally something like "check that if you expect a 12 byte object that you get a 12 byte object". Is there a situation where all of the member values are within expected ranges but the size is incorrect? What would that look like?

@frob I hope you don't mind If I add an answer with example for that question.

@__nostromo__ Yes if someone added some payload to the package. Or if someone changed the expected int32 to a int64 with the same value. If your receiver does not expect the package to have a 64bit datatype at this position, then it may read nonsense values best case. Worst case it's gonna crash, or anything in between. It's to make sure the packet is not mangled, tampered with or platform differences I imagine. But this all depends on how you handle the messaging. If you are sending raw bytes where size and placement matters, or if you are just sending i.e. comma separated strings and how you handle it.

I had problems with sending/receiving data at work between a Windows Machine (client) and C# code and Linux Machine (server) C++ code. This cost me 1-2 Days to figure out how to encode the strings properly so the two can communicate via TCP connection. I know it's something trivial. But when one isn't expecting it like me at the time then it's quite frustrating and puzzling.

An example I quickly put together on what can happen when you expect 12 bytes and get 24 bytes sent. I tried to simulate it via 2 structs and casting the bigger one to the smaller one. I know the code is not pretty, but I think this is enough to illustrate. In hindsight I probably should have just created a byte buffer and read/write from/to it.
https://www.jdoodle.com/iembed/v0/wBb

“It's a cruel and random world, but the chaos is all so beautiful.”
― Hiromu Arakawa

__nostromo__ said:
Is there a situation where all of the member values are within expected ranges but the size is incorrect? What would that look like?

You transfer a list of something, where the first byte is the number of elements in the list. Then you receive 1 byte with value 5.

Note that a network connection are at the level of a stream of bytes. There is no “start” or “end” notion built-in of what you think as “message” (it's a continuous stream that you have to make sense of). When you read from the network, you get an arbitrary number of bytes, including 0 bytes (if you read too fast) unless you block until some data arrives (at which point you may be waiting for an unknown amount of time).

If you expect 4 bytes, you may get 3, you may get 7 (the sender sent more data, perhaps the next message), you may get 100. The network connection may stall (you don't get any data for some time), it may be slow (few bytes / second), lag may change in time, it may terminate at any time including in the middle of getting a message.

If you support re-connecting a disconnected client, you need to exchange information to get the client in sync with the game again.

__nostromo__ said:

And then I'd implement this system not just for firing but for jumping, sprinting, etc. Anything else that is an action that generates a change in a user's state. I'm not super sure when to not make something an Event but I guess I can feel that part out as I go.

Is that all sound or did I miss anything? There's other details I'm interested in, regarding anti-cheat, but this post is pretty long and those details aren't as important.

I forgot to ask if you are planning to write the network transport code yourself too? Or are you using a solid network library? If you wanted to write it yourself, then I would advise against the pain of raw socket programming. But you do you.

And it's exactly like Alberth said, I seem to have misunderstood your question.

Sorry.

“It's a cruel and random world, but the chaos is all so beautiful.”
― Hiromu Arakawa

__nostromo__ said:
I'm still new to all this but I'd imagine I'd either favor the local client and reject it if it's wrong, server-side, or just have the client send their position and rotation and compute the players hit over the server. Not sure which approach is better or if there's a 3rd, better choice

How do you “favor the client” and “reject it if it's wrong, server-side” at the same time? These are contradictory.

The naive approach, which is ok for very slow-moving games, is just to have the server perform the logic based on its internal state and return the result.

The common approach, used for FPS games since the mid-90s, is to have the server attempt to reconstruct the scene as the client saw it and perform the logic based on that. This involves remembering what was sent to that client in the past and using an estimate of latency to deduce where the relevant entities were on the client when this event was generated.

This is tweaked to taste - for example, you wouldn't allow arbitrarily large latency in these calculations as it allows a certain type of exploit where clients can pretend to have bad lag when really they're just ‘backdating’ their shots so that they hit things that they wouldn't have otherwise hit.

Is that all sound or did I miss anything?

The only other thing I'd mention is that you're currently sending a floating point time value to timestamp your events. This is workable, but most games prefer to operate in terms of ‘ticks’ or ‘frames’ which have a fixed length. By quantizing time into these regular boxes, it's possible to store exactly where every entity was at every time, which makes reconstructing past positions (as above) more reliable. This also helps if you need to have the server correect a client's position (e.g. because something happened to the client's player entity on the server that the client did not forsee), because it's a smoother experience to be able to do that as a series of single steps from a known point.

Alberth said:

Note that a network connection are at the level of a stream of bytes. There is no “start” or “end” notion built-in of what you think as “message”

This is generally not true for game networking. It's uncommon to use TCP or any stream-oriented protocol. Usually we work with message-oriented protocols, i.e. UDP with an application level protocol layered on top.

It doesn't mean you don't have to worry about message lengths, because that matters during serialization. But it's rare to have to worry about coalescing packets or getting too much or too little data, outside of bugs and hackers.

This topic is closed to new replies.

Advertisement