Advertisement

rate/hate/appreciate my mog server update model

Started by September 28, 2014 02:53 AM
2 comments, last by hplus0603 10 years, 1 month ago

Just a hint, I'm a newb. I may be an older and somewhat educated newb, but still a newb so read what I place here with the understanding that I may have no clue what I'm talking about. :p

I'm currently working on a hobby mog client/server.

Its just for learning purposes and not meant to be a fullscale production game.

The client and server are written in C++ and share code (you compile with either #define is_server or #define is_client to build the seperate binaries).

I originally had a working UDP prototype but due to it being blocked by either ATT or my VPS provider I switched to TCP so that I could actually test it over the internet. I coded it in a way so that the TCP parts are encapsulated in an object that can be replaced with a UDP equiv with little hassle.

The network logic makes no assumptions of in-order packet arrival or fragmentation and in fact will multiplex multiple RPC/actions at the same time.

Anyways...

I'm going to describe two important parts:

Data: everything is a record- actually everything inherits from the record class. It doesn't matter what it is- a human character or even a lump of coal in inventory is a record (well actually an object that inherits from record). Basically all records are stored and retrieved using the same logic. When records are loaded there is a collection of record factories that are selected by record type so that the correct record is generated for each blob in the file(s) (or database in the future).

Each record has a record state. Record state is just using the state pattern to encapsulate stuff like monster AI into a hot-swappable object contained within the record. Basically each state will decide which next state to generate and return when that state is ran. This also makes handling game data updates simpler too since the same logic is used regardless of the record type to send the updates to the clients. Also, the client has the exact same code so it saves me from having to duplicate data handling code on the client and server. There are basically only a few differences between client and server. The client side code will have saving updates to disk disabled and it won't broadcast changes to data to the server.

Sessions: the session code is basically the same for the client and server. The client typically only runs one session at a time and sessions pass packets to the corresponding session at the other end of the network. The client has a few things disabled or blocked like data update pushes or attaching sessions to records but the entire session code base is used verbatim on client and server. Sessions contain a list of action receivers and action senders and handle login state.

RPC/Actions: This is where the work gets done. This can best be explained by an example. Lets say that I created an account from the client.

The client will take the input from the GUI and allocate a account_create_sender_action object. This action object will send the first packet to the server. The server will read the packet and get the action type (first it tests to see if this action was already created by comparing id with action id in packet) and find and evoke the correct action receiver factory. The action receiver is then given the packet and performs the work of creating the account. The action sender on the client may persist in cases where the action requires multiple packets of communication between sender and receiver or it can simply ask for itself to be deleted immediately depending on the action. In the case of creating an account, the receiver sends a sucess or fail packet back to the sender (client matches by action id- action id is the same on the server and client for each sender+receiver pair).

The account action receiver then also attaches the involved session to the account record for reasons that will be explained below.

Basically packets are sent and received between action objects. Game logic does not handle any packets directly and everything happens by creating an action sender. The action receiver is just what accepts the incoming packets from the sender (despite the names, both senders and receivers can send and receive packets and both may exist on server and client). Incoming packets are routed directly to the action receiver by action id if exists, or an action receiver is created by matching a factory on action type and then passing in the packet. Action senders are what is created by game logic to initiate the network conversation to evoke the action receiver to perform the necessary action.

Pushing updates to clients: After a data change occurs for any reason, the clients need to be informed. This is acheived by "attaching" sessions to records. When a session is attached, that record will check each session to see when it was last pushed and push a new update after each data change in that record. The record itself actually determines how sessions may be attached. An NPC fox will attach sessions based on proximity to its location. Also, sessions may attach themselves to records like when a player teleports to a new location on the map and many new objects become in close proximity. Some records are attached for other reasons like the account record is attached when a session logs in so that the client will see the account data. Each record object contains a list of record_session objects which handle maintain the attachment and initiating the data push by creating the data_sender_action on that session. Different attachment behaviors exist like permanent, timed (detach after x seconds), range(detach after distance between record's world location and session's location becomes larger than a supplied value) are handled by having different flavors record_session objects that inherit from the abstract one. The sessions don't handle data updates with the clients, the records do (well actually, the record_sessions create the sender action and bla bla bla).

Some of the reasons I went this route are:

I found myself writing the same code over and over for updates to clients and loading and saving data to disk on the server for all of the different entities- now every entity is treated the same in code but have overridden "doProcessing()", "attachSessions()", and "genFirstState()" methods.

I found myself writing duplicate code for the client and server for all entities. Now basically the client and server have the exact same code with a few methods overriden here and there in about 3 classes. There are "client" and "server" objects that override the "session_processor" object basically.

Encapsulating the work inside of "action" objects makes the code cleaner. I forgot to mention above that actions also have states and these state objects actually process and send packets. You override the base action class to override the method called by the default state to create the first state (well, technically second tongue.png ). Basically everything that isn't AI happens via action objects but even the AI will trigger data updates which invoke actions to push data to the clients.

The actual client side rendering code is not complete. Everything is being tested by a primitive command line I baked up.

The client will eventually have a layer that requests different record types and moves around objects in a rendering layer/scene graph and updates the UI. I wanted to build it from the ground up and get the most important parts working first. If the client/server layer sucks nobody will care about how snazzy your graphics are. I have had one primitive functioning MOG running several years back (way way back) but it was created when I was even more naive than I pretend not to be now.

So basically this is the route I am pursuing. Does it sound like a good idea?

Any constructive criticism is appreciated!

Thanks.

First - good luck. I truly mean that and not in a sarcastic manner.

Any input I would have at this point in time would be pure speculation because despite the effort to be detailed about what you are doing, it is difficult to really grok what is going on.

I will suggest on a much more basic and probably unrelated level however that TCP is the way to go until you find it is not - and then check that again. I work in telecom where some people still believe everything must be UDP because "it is faster" when really there are only a few pieces that actually need UDP (RTP for example) where others are actually much less complicated and more scalable over TCP (SIP for example). Of course I am probably getting ahead of myself because I have to build massively scalable and redundant systems with load balancers etc. where none of this will matter to you but if I can help someone avoid the pain I feel every day because of myths and half truths it will be worth it.

Evillive2
Advertisement

Well, actually the point of the thread wasn't TCP vs UDP originally.

I was asking for input on the whole [everything is-a record has-a record_session updates-a session] programming model.

This probably has a programming pattern name that I am unaware of.

When the sessions are attached to a record, the record itself will update sessions when the record is modified instead of having the sessions perform polling on all records to find new data to send down the wire.

However, UDP has some advantages over TCP once who figure out the following must be done:

Packets must have a prefix inserted with packet ID and session ID.

Packet ID prevents you from processing a packet you have already received when you receive duplicated packets and lets you send acknowledgements. Also you can just send several packets and process them out of order because they will arrive in random order. If you did fragmentation, you would also need a fragment id. My packets are too small to fragment.

This also allows what is called multiplexing. Because you make no assumptions about packet ordering and you aren't using a sequence number, you can send and process many packets at the same time in any order and without having to worry about waiting on each packet to finish processing or waiting on acknowledgments.

(my system sends ACKs with just the packet prefix and no payload and the sender does not wait on ACK before sending more but will resend any nonACK'ed packet after a timeout. UDP doesn't, and you don't have to. You could even have a do-not-ack field too I suppose and support both behaviors)

Session ID helps me keep the clients traffic seperated. You can simply just use IP:port for this purpose but for me it was easier to just do the lookups by a simple unsigned integer.

You can even do bounds testing and have this ID be an offset into your vector or array container of sessions for maximum efficiency. You can also build a b-tree with each node having 256 subnodes and treat each byte of your session id or IP:port as the lookup into the array of subnodes.

There is a benefit of using a seperate integer as the ID over just using IP:port. When the user is on a laptop or using a dynamic IP ISP or even an older SOHO router with UDP bugs, their IP:port can randomly change without warning after a few hours.

When you code for UDP, typically its less complicated aside from having to queue up sent packets to know what hasn't been ACK'ed, and queuing up received packets to know not to process the same packet twice.

You can just do a select() on a single socket followed by a call to recvfrom(). You can even make the select call replace your thread's usleep() for better efficiency.

With TCP you need to use select() on all of your sockets, or have some complicated async event code to avoid polling select() and then some way to lookup a session by socket.

Of course there are actual technical benefits to UDP like that it won't lag on you when a packet is lost. TCP has flow control and won't excessively flood your server during high traffic moments. UDP doesn't have flow control so it will not lag badly on highly saturated links. TCP is less likely to be blocked by network admins because to them UDP = P2P filesharing.

Anyways, back to "I was asking for input on the whole [everything is-a record has-a record_session updates-a session] programming model."

Thanks :)

I was asking for input on the whole [everything is-a record has-a record_session updates-a session] programming model.


That's one way of expressing the data-flow plumbing. I'm sure it can be made to work fine.

I find that what ends up being interesting in networked games is not the plumbing (important as it might be) but instead the game rules and how they collaborate with UI/display to give a particular "feel" to the game.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement