Jump to content
  • Advertisement


  • Content Count

  • Joined

  • Last visited

Community Reputation

262 Neutral

About FredrikHolmstr

  • Rank

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

  1. You do not need Unity Pro, no. It will function even on iOS and Android without Pro (check out the tutorial to see how it works). Bolt is developed for games of 2-128 players, it's not an "MMO SERVER" or anything like that.
  2. Calling folk som lyssnar på bra musik, 2000 eller senare bra hiphop man inte hört? Agnes Arpi Anders Holmström ;p
  3. Figured I would post a quick notice in here also, considering all of the asking and talking I've done here over the past two years or so . I just released my own networking solution, called Bolt, for Unity3D. You can find it over at http://www.boltengine.com
  4. Tror jag måste kolla igenom alla säsonger av Blue Mountain State igen
  5. 29 då, sedan nästa år blir det första årdagens av min 29:e födelsedag.
  6. FredrikHolmstr

    Threading and networking

    If you want to see a source-code of an implementation of the 2nd solution that hplus described, you can check out my UdpKit networking library: https://github.com/fholm/udpkit More specifically you can check out the chat example: https://github.com/fholm/udpkit/blob/master/src/managed/udpkit.example.chat/Program.cs as it implements this type of polling that hplus describes. All of this is in C# so maybe it doesnt apply for you.
  7. FredrikHolmstr

    Simulation in time vs. frames (steps/ticks)

    I think you're right, to keep nudging the clock back and forth is just going to cause issues. I think I over-complicated this a ton in my head. I just needed someone to agree with my initial idea (using ticks everywhere) to feel confident in it I think Thanks!
  8. So, like always, I have been working on my FPS game. A while back I switched the networking from using time (as in seconds) to using frames (as in simulation frames/steps/ticks/whatever you wanna call them). All state that is sent from both server to client and client to server are stamped with the frame the state is valid for, all actions which are taken (like "throw a grenade") are stamped with the frame they happened, etc.    This makes for a very tidy model, there is no floating point inaccuracies to deal with, everything always happens at an explicit frame, it's very easy to keep things in sync and looking proper compared when using time (as in seconds).   Now, the one thing that popped into my head recently was the fact that when you deal with explicitly numbered frames and the client start lagging behind or for some reason runs faster then the server, basically when you get into a state which the client needs to re-adjust its local estimate of the server frame (server time if you are using time instead). When dealing with "time" you can adjust/drift time at a very small level (usually in milliseconds) over several frames to re-adjust your local estimate on the client with the server, which gives next to no visible artifacts.    But when you are dealing with frames, your most fine grained level of control is a whole simulation frame, this causes two problems: If you are just drifting a tiny tiny bit (a few ms per second or so), the re-adjustment that happens when dealing with "time" is practically invisible as you can correct the tiny drift instantly and there is no time for it to accumulate. But when dealing with frames you wont even notice this until you have drifted an entire simulation frame worth of time. Also like I said, when you are going to re-adjust the clients estimate you have no smaller unit than "simulation frame" to deal with, so there is no way of interpolating, you just have to "hard step" an entire frame up/down which leaves visual artifacts.   All of this obviously only applies when you are doing small adjustments, but they are the most common ones to do. If you get huge discrepancies in the time between the client and server, every solution is going to give you visual artifacts, snapping, slowdown/speedup, etc. But that is pretty rare.   So, what's the solution here? I have been thinking about a model which still keeps most of my original design with frames, but when the state/actions are received on the remote end it's easy to transform the frame numbers into actual simulation time numbers, and then as things are displayed on remote machines you can nudge the time back/forward as needed. I'm just looking for ideas and input on how people have solved this.   Edit: But then again, maybe I am over-thinking this because since "time" will also be measured with the granularity of your simulation step size. So maybe there is no difference when you compare the two solutions? For example, if you nudge your local time -1 ms to "align" with the server better, but this pushes you over a frame boundary then everything will be delayed by a whole frame anyway...   Edit 2: Or actually, I suppose the case above where you nudge time to get closer to the server and it happens to be right on a frame boundary would require you to drift more then one frame worth of time without detecting it (you most likely got a ping spike or packet drop), because if you just drift a tiny bit away/towards the server, and you re-adjust back closer towards the server, you would never cross the frame boundary.   Edit 3: Also, thinking more of this, maybe the reasons I am confusing myself with this is that currently the updating of remote entities are done in my local simulation loop, when maybe it should be done in the render loop instead? I mean, I have no control over the remote entities what so ever anyway, since they are... well remote. On the other hand it feels a lot cleaner (in my mind) to step all remote entities at the same interval as they are stepped on the peer that controls them. On the other hand, maybe this is why it's getting convoluted as I'm mixing up my local simulation of the entities I control with the display of the remote entities.    Like you probably can tell I am pretty ambivalent on what route to go with here, really looking for some input from someone.
  9. Någon som spelar titanfall över helgen? Mitt origin handle är "fjholmstrom", adda om ni vill lira.
  10. The solutions which exist to this problem are deceptively simple really:   1) Ignore it and don't try to compensate for it, I'm pretty certain a lot of AAA titles do this as you can see the artifact you're talking about in for example Battlefield 4 2) Since you know how fast a player can move in a certain situation (walking, running, crouching, falling, etc. etc.) you can limit the speed a player entity can move with, and let it spill over into the next frame if he's moving to fast. If he's moving too slow you speed him up a little bit instead 3) You can combine 1) with implementing a de-jitter buffer on the server which does 1 UC de-jittering instead of the usual 2 world state dejittering on the client, it introduces a bit of extra lag but should smooth everything out enough that you don't have to think about this.   Also you should always send UC unreliably in a real time game and just send them redundantly instead. Honestly I think I would prefer approach 2) as it keeps the rest of the logic super clean and deals with the visual artifacts where appropriate (in the part of the code which is responsible for the rendering). A lot of your solutions are heavily convoluted with a lot of extra data to keep track of or to send back and forth, or both. And I just don't see a big gain in trying to get 100% perfect rendering of remote entities in every possible situation, it simply won't be possible, again just look at something like BF4 which has a ton of visual bugs/quirks for the remote players (animations heavily out of sync, etc. etc.) and you simply dont care about this when youre actually playing.   However, it would be interesting to get the input on this from someone which has released an AAA FPS/TPS title.   Edit: I mean, just try to play BF4 on a crappy connection with some packetloss/jitter and ~150ms ping, and then see how you move in the world for someone with ~50ping and little jitter/loss. Your avatar will move and animate like crap.
  11. FredrikHolmstr

    Client snapshot updates and fixed events

    I do it like this. I don't count "time", i count simulation frames - aka "ticks". Both my server and client run with 60 ticks / second, and I send data between them every third tick (~16.67 * 3 = ~50ms). The first four bytes in each packet is the current local tick of the machine at the time the packet was sent. I have three types of events: Reliable, Unreliable and UnreliableFrameSynchronized. Reliable and Unreliable dont care about what time they were sent, they are just executed whenever they arrive. The frame synced events are kept in "sync" with the senders tick, so it happens at the correct tick on the remote. I basically just write 2 bits for each frame synced event which tell how many frames "before" the packet tick this event was.
  12. FredrikHolmstr

    Network Tick rates

    From reading the replies in the whole thread it feels like you are over-complicating things in your head a lot, I did the same when I initially learned how to deal with synchronizing time between the client/server. A lot of the confusion comes from the fact that most people call it "synchronize", when in reality that's not what it's about. hplus, said something which is key for understanding this and for realizing how simple it actually is:   This is the only thing which actually matters, there is no need to try to keep the clients time in line with the servers time or try to forward-estimate the local client time with the remote server time by adding half the latency (rtt/2) on some local offset. The piece of code which I read that made it all "click" for me was the CL_AdjustTimeDelta function in the Quake 3 source code, more specifically line 822 to 877 in this file: https://github.com/id-Software/Quake-III-Arena/blob/master/code/client/cl_cgame.c this shows how incredibly simple time adjustment is. There are four cases which you need to handle:Are we off by a huge amount? reset the clock to the last time/tick received from the server (line 844)Are we off by a large amount? jump the clock towards the last time/tick received from the server (line 851)Are we slightly off? nudge the clock in the correct direction (line 857 - 871)Are we off by nothing/almost nothing? do nothingNow quake 3 uses "time" as the value which we try to sync the client against the server with, I have found it a lot easier to use "ticks" completely and just forgo any concept of "time" in my code completely. The process I am going to describe next is the same process i described (a bit long winded) in my earlier post, but I'll try to make it a bit more transparent/easier to grasp: Each peer have their own local tick which increments exactly like you would expect: +1 for every simulation frame we run locally. This is done on both the server and all the clients, individually and separately. The local tick is sent as the first four non-header bytes of each packet to every remote peer we are connected to. In reality the clients just send its own local tick to the server, and the server sends its own local tick to all clients. Each connection to a remote peer has what is called the remote tick of that connection, this is exists on the connection object from client->server and server->client. The remote tick of each connection is what we try to keep in sync with the other end of the connections local tick. This means that the remote tick of the connection to the server on the client tries to keep in sync with the servers local tick and vice versa. The remote tick of each connection is also stepped +1 for each local simulation tick. This allows our remote tick to step forward at roughly the same pace as the other end of the connection steps its local tick (which is what we are trying to stay in sync with). When we receive a packet we look at the four tick bytes of that packet and compare the remote tick we have for the connection, we check the same four conditions as is done in the Q3 sources in the CL_AdjustDeltaTime function, but with ticks instead. After we have adjust our remote tick we read all the data in the packet and put it in the connections simulation buffer, everything is indexed by the local tick off the remote end of the connection. When then simply run something like this for each connection to de-queue all the remote simulation data which should be processed: while (connection->simulationBuffer->next && connection->simulationBuffer->next->tick <= connection->remoteTick) { connection->simulationBuffer->next->process(); connection->simulationBuffer->next = connection->simulationBuffer->next->next; }
  13. Det finns inget ynkligare än en 1.5 åring med feber och hosta som inte förstår vad det är som händer.
  14. FredrikHolmstr

    Network Tick rates

    Since I have gotten a ton of help from a lot of people on this forum over the years, and especially hplus0603, I figured I could give some back and provide the code for the system for tick synchronization and simulation that I have come up with. This is by no means anything revolutionary or different from what everyone else seem to be doing, but maybe it will provide a good example for people learning. For reference I am using this for an FPS game. First some basic numbers: My simulation runs at 60 ticks per second on both the server and client. While it could be possible to run the server at some even multiplier of the client (say 30/60 or 20/60 or 30/120, etc.) I have chosen to keep both simulations at the same rate for simplicity's sake. Data transmission rates are also counted in ticks and happen directly after a simulation tick, the clients send data to the server 30 times per second (or every 2nd tick), the server sends data to the clients 20 times per second (or every 3rd tick). Here are first some constants we will use throughout the pseudo code:   #define SERVER_SEND_RATE 3 #define CLIENT_SEND_RATE 2 #define STEP_SIZE 1f/60f I use the canonical fixed simulation/variable rendering game loop, which looks like this (C-style pseudo code):   float time_now = get_time(); float time_old = time_now; float time_delta = 0; float time_acc = 0; int tick_counter = 0; int local_send_rate = is_server() ? SERVER_SEND_RATE : CLIENT_SEND_RATE; int remote_send_rate = is_server() ? CLIENT_SEND_RATE : SERVER_SEND_RATE; while (true) { time_now = get_time(); time_delta = time_now - time_old; time_old = time_now; if (time_delta > 0.5f) time_delta = 0.5f; time_acc += time_delta; while (time_acc >= STEP_SIZE) { tick_counter += 1; recv_packets(tick_counter); step_simulation_forward(tick_counter); if ((tick_counter % local_send_rate) == 0) { send_packet(tick_counter); } time_acc -= STEP_SIZE; } render_frame(time_delta); } This deal with the local simulation and the local ticks on both the client and the server, now how do we deal with the remote ticks? That is how do we handle the servers ticks on the client, and the clients ticks on the server. The first key to the puzzle is that the first four bytes in every packet which is sent over the network contains the local tick of the sender, and it's then received into a struct which looks like this:   struct Packet { int remoteTick; bool synchronized; char* data; int dataLength; Packet* next; } Exactly what's in the "data" pointer is going to be very game specific, so there is no point in describing it. The key is the "ticks" field, which is the local tick of the sending side at the time the packet was constructed and put on the wire. Before I show the "receive packet" function I need to show the Connection struct which encapsulates a remote connection, it looks like this:   struct Connection { int remoteTick = -1; // this is not valid C, just to show that remoteTick is initialized to -1 int remoteTickMax = -1; Packet* queueHead; Packet* queueTail; Connection* next; // .. a ton of more fields for sockets, ping, etc. etc. } The Connection struct contains two fields which are important too us (and like said in the comment, a ton more things in reality which are not important for this explanation): remoteTick, this is the estimated remote tick of the remote end of this connection (the first four bytes we get in each packet). queueHead and queueTail which forms the head/tail pointers of the currently packets in the receive buffer. So, when we receive a packet on both the client and the server, the following code executes:   void recv_packet(Connection c, Packet p) { // if this is our first packet (remoteTick is -1) // we should initialize our local synchronized remote tick // of the connection to the remote tick minus remotes send rate times 2 // this allows us to stay ~2 packets behind the remote on avarage, // which provides a nice de-jitter buffer. The code for // adjusting our expected remoteTick is done somewhere else (shown // further down) if (c->remoteTick == -1) { c->remoteTick = p.remoteTick - (remote_send_rate * 2); } // deliver data which should be "instant" (out of bounds // with the simulation), such as: reliable RPCs, ack/nack // to the remote for packets, ping-replies, etc. deliver_instant_data(c, p); // insert packet on the queue list_insert(c->queueHead, c->queueTail, p); } Now, on each connection we will have a list of packets in queueHead and queueTail, and also a remoteTick which gets initialized to the first packets remoteTick value minus how large of a jitter-buffer we want to keep. Now, inside the step_simulation_forward(int tick) function which moves our local simulation forward (the objects we control ourselves), but we also integrate the remote data we get from our connections and their packet queues. First lets just look at the step_local_simulation function for reference (it doesn't contain anything interesting, but just want to show the flow of logic):   void step_simulation_forward (int tick) { // synchronize/adjust remoteTick of all our remote connetions synchronize_connection_remote_ticks(); // de-queue incomming data and integrate it integrade_remote_simulations(); // move our local stuff forward step_local_objects(tick); } The first thing we should do is to calculate the new synchroznied remoteTick of each remote connection. Now this ia long function, but the goals are very simple: To give us some de-jittering and give us smooth playback, we want to stay remote_send_rate * 2 behind the last received packet. If we are closer to the received packet then < remote_send_rate or further away then remote_send_rate * 3, we want to adjust to get closer. Depending on how far/close we are we adjust one or a few frames up/down or if we are very far away we just reset-completely.   void synchronize_connection_remote_ticks() { // we go through each connection and adjust the tick // so we are as close to the last packet we received as possible // the end result of this algorithm is that we are trying to stay // as close to remote_send_rate * 2 ticks behind the last received packet // there is a sweetspot where our diff compared to the last // received packet is: > remote_send_rate and < (remote_send_rate * 3) // where we dont do any adjustments to our remoteTick value Connection* c = connectionList.head; while (c) { // increment our remote tick with one (we do this every simulation step) // so we move at the same rate forward as the remote end does. c->remoteTick += 1; // if we have a received packet, which has not had its tick synchronized // we should compare our expected c->remoteTick with the remoteTick of the packet. if (c->queueTail && c->queueTail->synchronized == false) { // difference between our expected remote tick and the // remoteTick of the last packet that arrived int diff = c->queueTail->remoteTick - c->remoteTick; // our goal is to stay remote_send_rate * 2 ticks behind // the last received packet // if we have drifted 3 or more packets behind // we should adjust our simulation slightly if (diff >= (remote_send_rate * 3)) { // step back our local simulation forward, at most two packets worth of ticks c->remoteTick += min(diff - (remote_send_rate * 2), (remote_send_rate * 4)); // if we have drifted closer to getting ahead of the // remote simulation ticks, we should stall one tick } else if (diff >= 0 && diff < remote_send_rate) { // stall a single tick c->remoteTick -= 1; // if we are ahead of the remote simulation, // but not more then two packets worth of ticks } else if (diff < 0 && abs(diff) <= remote_send_rate * 2) { // step back one packets worth of ticks c->remoteTick -= remote_send_rate; // if we are way out of sync (more then two packets ahead) // just re-initialize the connections remoteTick } else if (diff < 0 && abs(diff) > remote_send_rate * 2) { // perform same initialization as we did on first packet c->remoteTick = c->queueTail->remoteTick - (remote_send_rate * 2); } // only run this code once per packet c->queueTail->synchronized = true; // remoteTickMax contains the max tick we have stepped up to c->remoteTickMax = max(c->remoteTick, c->remoteTickMax); } c = c->next; } } The last piece of the puzzle is the function called integrade_remote_simulations, this function looks as the packets available in the queue for each connection, and if the current remote tick of the connection is >= remote_tick_of_packet - (remote_send_rate - 1). Why this weird remote_tick comparison? Because if the remote end of the connection sends packets every remote_send_rate tick, then each packet contains the data for remote_send_rate ticks, which means the tick number of the packet itself, and then the ticks at T-1 and T-2.   void integrade_remote_simulations() { Connection* c = connectionList.head; while (c) { while (c->queueHead && c-remoteTick >= c->queueHead->remoteTick - 2) { // integrate data into local sim, how this function looks depends on the game itself integrade_remote_data(c->queueHead->data); // remove packet list_remove_first(c->queueHead, c->queueTail); } c = c->next; } } I hope this helps someone
  15. FredrikHolmstr

    Game loop tick count

    time_old = get_time(); time_acc = 0; time_now = 0; time_delta = 0; while (true) { time_now = get_time(); time_delta = time_now - time_old; time_old = time_now; // limit max delta so we dont spiral away if (time_delta > 0.25) { time_delta = 0.25; } time_acc += time_delta; while (time_acc >= (1f / 60f)) { simulate(); time_acc -= (1f / 60f); } render(time_delta); } This is the canonical game-loop with a fixed simulation step as I learned it
  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!