Should Client do same logic or just sync data from server?

Started by
9 comments, last by frob 2 years, 4 months ago

I'm working on an MMORPG,now I'm design about inventory system,and I m using untiy as my client engine,.net core as my server,so that I can easily share code from server to client.

Let talk about Drop logic,I have two implements,and I want to know which one is better.

1.once client send a DropMsg(int dropEntityID,vector3 pos) to server,then the server do logic like follow.

  • human packer change,remove the dropped item
  • entity state change to Dropped,and set the drop position for it
  • response a DropSuccMsg() back to client,this msg is very small even only take 4 bytes.
  • broadcast a HumanDropSuccMsg(int humanEntityID,data humanDropEntityData) to all client that in the area (aoi system)

when client get DropSuccMsg(),then the client do the same things like server did.If there is no bug in game,the client should get same result like server.this way save lots of bandwidth,but it is hard to code.

2.once client send a DropMsg(int dropEntityID,vector3 pos) to server,then the server do logic like follow.

  • human packer change,remove the dropped item
  • entity state change to Dropped,and set the drop position for it
  • response a DropSuccMsg(packerData humanPackerData,data humanDropEntityData) back to client,this msg is very large,it contains two datas,one is the player's inventory data,it is an array save the items entity id,another one is the drop entity data after it be dropped,in protobuf it will take more than 100 bytes some times.
  • broadcast a HumanDropSuccMsg(int humanEntityID,data humanDropEntityData) to all client that in the area (aoi system)

when the client get response,it will sync player's packer data,and then sync the drop entity data.without do any logic,only sync.this way is easy to code,but it will occupy lots of bandwidth of server.

We don't need to talk about safe,because both of the implement will verify data in server,so it is not important that if client be hacked.

so please tell me which one is better,or which one is widely used in online games.

Sorry for my English.

Advertisement

There is no universal better. Depending on your goals, both are valid alternatives.

At the very least, if you don't care about bandwidth at all, obviously making the server the central point will win (less coding time). If you care a lot about bandwidth, the other option wins. Do a few computations on how much bandwidth you'd roughly need for both options for a big value of "Massive". You can convert that to currency if you like by finding server hosting offers with specified bandwidth limits.

but it is hard to code

Yes, network programming is an instance of distributed simulation, which is a hard topic. This is why not every game is networked, and why some networked games have surprising networking bugs.

That being said, what I would do would be something more like:

  1. If there is currently a “drop pending” record for the object on the client, do not allow another drop request.
  2. Client sends DropObject(ObjectId, Position) to server using some form of reliable or “repeated until successful” messaging.
  3. Client locally tells the object that it's dropped at the position, and removes from inventory.
  4. Client keeps a record around of this message being pending.
  5. Server receives the message, and verifies that the object is in inventory and can be dropped, and if false, sends a drop reject only to the originating client, else sends a drop success to all viewing clients, using the same kind of messaging as the client. (If you use unreliable repeated messaging, there's a third state, which is “already in world.”)
  6. Client, when receiving drop success, removes the “pending” record.
  7. Client, when receiving drop reject, takes the object back into inventory, and then removes the “pending” record
  8. If using unreliable messaging, occasionally, while drop pending records exist, the client will re-send the request to the server.
  9. If client receives a “drop accept" or “drop reject” message for an object that doesn't have a “pending” record, you have a bug! (Unless you're using unreliable messaging, in which case it's OK as long as the object is now in the world.)
    Log the message and a snapshot of the player's inventory just to have some debug data. Then send a request to the server to get a full inventory state dump.
  10. Optionally, to recover from such a bug, if it's frequent: When server receives a full inventory state dump, it sends back its view of player inventory.
  11. Optionally, to recover from such a bug, if it's frequent: When player receives an inventory state dump, it removes any object that has a “drop pending” record from the world, then it removes all outstanding “drop pending” records, then it makes the inventory be a copy of the data received from server.

This shows the local player right away what's happening, and can still correct state if the server disagrees. It also sends the small amount of network data, as long as there's not a bug.

enum Bool { True, False, FileNotFound };

@Alberth hey thanks for your reply.

@hplus0603 thank you!i have learn all your post and I think it may be so complex for such a simple scene?

ununion said:
I think it may be so complex for such a simple scene?

The problem with anything “network" is that you have multiple users performing independent actions. You have independent computers updating themselves at arbitrary times, you have a network that may or may not loose messages. It may also swap order of messages (one you sent later arrives earlier). For extra fun the network may (hopefully temporarily) fail completely, and one or more computers may crash and loose all information they had in memory.

Depending on what you take into account as feasible possibilities that might happen, your programming problem goes from very difficult to near impossible.

As a simple example how difficult even simple problems can be: Imagine 2 computers A and B, and an unreliable network between them (it may drop messages silently). In this setting it is not possible to mutually agree between the computers that both sides are done talking to the other side.

You can send a “I am done” message from A to B, but you don't know if it arrived at B (the network may have dropped it). So A continues sending that message until it gets a confirmation from B. However, B doesn't know if the confirmation was received (the network may have dropped it). So how will B decide it can stop sending the confirmation?

(It has been proven as unsolvable, so there is no answer for this problem, see also https://en.wikipedia.org/wiki/Two_Generals%27_Problem .)

Alberth said:
So how will B decide it can stop sending the confirmation?

That's why we have keepalive packets, ack/nack, sliding windows with retries, and plenty of other similar technologies. If machines cannot communicate reliably the game can't be played, simple as that. Games (and distributed programs in generally) accept that failure is often a great option, so not having a perfect solution to communication is acceptable. The common approach is that after x seconds of communications difficulty the connection is presumed failed, which both rolls back the transaction and breaks the connection.

In either scenario #1 or scenario #2, disconnects aren't considered by either statement but they're a real-world problem. Depending on what's going on at the time that can mean a simple disconnect, a transaction rollback, or otherwise aborting whatever was being done. The hplus reply touches on that.

The topic was well researched over a half century ago for databases, both scenarios fall under the umbrella of concurrency control for replicated data. It is an enormous topic, but the basics are absolutely essential for network programming. At the very least network programmers need to understand how to ensure distributed ACID constraints. Few game messages require all four ACID requirements and it's certainly easier when you can stream updates that don't require them, but sometimes data is critical and all need to be present with the locking, blocking, and rollback requirements that come with it.

HPlus's listing is either optimistic or semi-optimistic (depending on how you decide to block or fail) with a two phase lock, which is pretty common. Set an exclusive remote lock or fail, message with retry or timeout with fail, after confirmation or failure release the lock, on failure rollback and log / on success commit and continue. As long as the locks are quite specific with a small scope they will rarely fail, so they can be mixed with all kinds of other communications in the data stream and flow freely. It might seem like a lot of work, but in practice it has very little overhead and in games can usually be tuned to completely avoid lock contention.

ununion said:
I think it may be so complex for such a simple scene?

It is the easiest possible solution that will actually WORK for networked games in practice.

If you don't care about it working, then simpler solutions exist :-)

enum Bool { True, False, FileNotFound };

this topic is only talk about sync logic,it is none about connection and others…

That “sync logic” is a set of rules. Those rules have been researched and studied, and are called “concurrency control”.

The two options you gave are error-prone. There are a bunch of details that people rarely think about, but computer scientists have studied and solved.

Inventory control systems like you describe are famous for ways to duplicate objects. They can be exploited for other players to send commands in an attack. Without much thought I can think of several broad classes of network attacks that would likely enable duplicating items, or deleting items from inventories.

This is a solved problem. There is no need to invent your own solutions. It's been studied for about 60 years, and the method described above is one of the better, and more common approaches. Optimistic two-phase locks are probably the easiest solution you'll find that will work in real-world environments.

This topic is closed to new replies.

Advertisement