Author Topic: Syncronizing Players And Intialization  (Read 4526 times)

Patterrun

  • Newbie
  • *
  • Thank You
  • -Given: 0
  • -Receive: 1
  • Posts: 12
    • View Profile
Syncronizing Players And Intialization
« on: May 30, 2014, 12:29:31 AM »
Hi, I want to know how I can synchronize Players in the order they join, and when the level is done "loading". That is, after the level is loaded, the play field is randomized in Start(), like so:

  1.         public void Start()
  2.         {
  3.                 if (!TNManager.isHosting) return;
  4.  
  5.                 for (int row = 0; row < numberOfRows; row++)
  6.                 {
  7.                         for (int col = 0; col < numberOfColumns; col++)
  8.                         {
  9.                                 if(Random.value > 0.9) TNManager.Create(blockObject, new Vector3(row,0,col), Quaternion.identity);
  10.                         }
  11.                 }
  12.  
  13.                 StartCoroutine(PlayerWaitSpawn() );
  14.         }
  15.  

Cut the down code to what it is essentially like, it creates a random playing field each time the level is loaded. So that works fine. However, I do not know how to synchronize players properly. The two conditions I need fulfilled are

1) Wait for at least 2 players to be in the channel/level
2) Also, wait for the level to completely load, that is after the Create() functions called in Start() finish appearing for other clients

In the coroutine I start after calling the Create() functions in Start(), I tried a few things, with the closest resembling any form of success being:

First yield waits continually until TNManager.players.Count is greater than 0 (Why does this not include the host player by the way?)
Next, Spawn the host's player character (since the coroutine was called from Start() where the check ensures only the host player is doing logic, it does not create multiple characters from other clients)
Finally, in a loop, for each player send them an RFC telling them to Create() their player (So they have ownership. On a side note I spawn them in certain locations according to the order of the TNManger.players list, but this is only to simulate what I want to do mentioned further below. It won't well once players leave the level/channel in the middle of a game)

However, this does not really work well as sometimes the RFC reaches the client faster than the Create() calls from Start() apparently as the player will fall through the floor when it happens. Also, upon joining for the first time, clients do not spawn their players ever, only the host player does. The level must be reloaded via TNManager.LoadLevel() for clients to spawn players this way, and not sure why. I am not using SendQuickly() either, as it is important that the message sent be received. Note sure why I didn't just post it but here it is

  1.         IEnumerator PlayerWaitSpawn()
  2.         {
  3.                 while (TNManager.players.Count == 0)
  4.                 {
  5.                         yield return new WaitForSeconds(1f);
  6.                 }
  7.  
  8.                 if (TNManager.isHosting)
  9.                 {
  10.                         TNManager.Create(playerObject, new Vector3(0f ,0f, 0f), Quaternion.identity, false);
  11.                 }
  12.  
  13.                 int lastPlayerID = 0;
  14.                 foreach (Player p in TNManager.players)
  15.                 {
  16.                         //if (p.id > lastPlayerID) {StaticPlayerIDsDict.Add(lastPlayerID,true);}
  17.                         //if (StaticPlayersIDsDict[p.id]) ...
  18.                        
  19.                         lastPlayerID += 1;
  20.                        
  21.                         switch(lastPlayerID)
  22.                         {
  23.                                 case 1:
  24.                                 tno.Send(99, p, new Vector3(numberOfRows, 0f, 0f) );
  25.                                 break;
  26.                                 case 2:
  27.                                 tno.Send(99, p, new Vector3(numberOfRows/2, 0f, numberOfColumns) );
  28.                                 break;
  29.                                 case 3:
  30.                                 tno.Send(99, p, new Vector3(0f,0f,numberOfColumns/2) );
  31.                                 break;
  32.                                 //etc.. will finish later
  33.                         }
  34.                 }
  35.         }
  36.        
  37.         [RFC(99)]
  38.         void PersonalCreate(Vector3 vector)
  39.         {
  40.                 TNManager.Create(playerObject, vector, Quaternion.identity, false);
  41.         }
  42.  

Finally, if I get the above solved, I need to spawn players in the order they joined the channel/level (so I can spawn them in proper positions on the map, ie. the player that has second place in the room always spawns in the upper middle section of the map), and if they leave, a player must take their place (such as 2nd place of 8 players if first place, and third-eighth place is taken). I figure I could do something with the Player.id and compare the ids in the TNManager.players list and add them to a dictionary in order or something but then I don't know how to synchronize this properly or if it would even work for organizing players spawning like this.
« Last Edit: May 30, 2014, 12:49:18 AM by Patterrun »

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #1 on: May 30, 2014, 03:56:17 PM »
Don't do things in Start(). You should wait for OnNetworkJoinChannel first. What I generally do is check TManager.isConnected && TNManager.isInChannel in Start(). If yes, exit early. If no, call OnNetworkJoinChannel myself (as it means you're testing offline and OnNetworkJoinChannel won't be executed).

When other players join, OnNetworkPlayerJoin notification is sent out. You can watch for it instead of PlayerWaitSpawn. The other players can always be retrieved via TNManager.players. Note that this list won't include you (just other players).

Patterrun

  • Newbie
  • *
  • Thank You
  • -Given: 0
  • -Receive: 1
  • Posts: 12
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #2 on: May 31, 2014, 08:10:58 PM »
Hi Aren, the reason I didn't do it in OnNetworkJoinChannel() was because I am reloading the scene within the game. If I do it within OnNetworkJoinChannel(), it only executes once. Also, I need to be checking when there are 2 more players ready to play before simply creating the player's avatars right away.  I tried polling for that by checking when the TNManager.players list became populated and then sending calls to the players to create the avatars but it is very buggy the way I did it which I posted in the OP.

Patterrun

  • Newbie
  • *
  • Thank You
  • -Given: 0
  • -Receive: 1
  • Posts: 12
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #3 on: June 01, 2014, 03:21:10 AM »
Managed to more or less fix it by creating an additional invisible object that creates a player avatar (this object is created after the level is randomized, it is the last to be created. It is created by the host player, therefore host player also uses it. My problem is Create() and Send()s are not syncronized with each other so I changed the Send() to Create() and this basically fixed it.

I could probably do this with RFC's Send() by not using Create() to create the randomized level environment objects, but I realize that Create() takes care of ownership for me so I just focused on making that work for me since I would not know how to handle it otherwise. Anyway, is my way of doing this inefficient or wrong? Is it also okay for all objects created after the level is loaded to be a TNObject (I have a lot of them because they are placeholder cubes and make up the randomized game objects in the level)?

Also, speaking of ownership, I want to keep track of the order in which players enter and leave the room (so I can do some stuff like assigning spawn areas). I was thinking of using a dictionary or class to keep track of the information but I am not sure how I am going to synchronize this one. Maybe I will have the host create and take care of it but then I am not sure how to keep it in sync if the host leaves. How should I approach this?

My logic of ownership is not great as you can see. Just one more thing, since I do not want to clutter the forum with more threads, and I feel it's the same topic as the above two questions. I also want critique on my logic of ownership with projectile weapons. Currently, my way is a player creates the projectile when he presses the fire button, so ownership is his at this point (tno.isMine). If it collides with an obstacle, it destroys it and itself (obstacles are owned by host) and the obstacles may drops items if they are marked to do so. The items dropped are owned by the projectile's owner that destroyed them (again using tno.isMine). Finally when a player goes near and picksup/collides with the item, it calls TNManager.Destroy() on it. Is this way of doing it okay? I notice if I use TNManager.isHosting the results appear to be the same, but I think with actual lag conditions and not just testing it on my computer would show different results.
« Last Edit: June 01, 2014, 03:35:47 AM by Patterrun »

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #4 on: June 01, 2014, 06:41:39 PM »
Projectiles should not be owned by TNet. Owned implies that they are created via TNManager.Create, and it's not a good idea to do it via TNet because how frequently projectiles can be created (bullets fired several times per second? it would be absurd to have that many managed objects). What you should be doing instead of saying, tno.Send("FireWeapon", Target.All, ...), and then each client will take care of creating their own projectile with the correct position/rotation/direction. How to identify who fired the weapon? Just pass some ID as a part of your tno.Send call.

I don't understand the part about the dictionary.

Patterrun

  • Newbie
  • *
  • Thank You
  • -Given: 0
  • -Receive: 1
  • Posts: 12
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #5 on: June 03, 2014, 12:48:18 AM »
Well the projectiles aren't that quick or many created per second, they are more like cannonballs. I also have bomb objects ala Bomberman. And yes, I am using TNManager.Create() to create these slow projectiles and bombs. As for the dictionary, don't mind it, I think I need to approach that differently and I will after I tackle this immediate issue first.

When I did what you said:

Press Space to:
tno.Send("FireCannonballOrDropBomb",Target.All)

[RFC]
void FireCannonBallOrDropBomb()
{
GameObject.Instantiate(slowProjectileLikeObject,transform.position,Quaternion.identity);
}

With the above when a new player joins the channel, they are not aware the cannonballs or bombs laid even exist and there could be many on the screen waiting to detonate or cannons still traveling across the map still.

If I use Target.AllSaved as my target, only the latest cannonball/bomb is saved. And with that when objects are destroyed without another RFC, the last cannonball/bomb is recreated everytime a player joins a map.

While I tried various ways of logic with TNManager.Create() and TNManager.Destroy() instead and having each projectile be a tno, I got better behavior but I still haven't reached what I wanted to do. The closest to success I got for bombs was: they are created by anyone, but only host the ticks down the bomb counter. At the end of the timer it runs some logic such as if it the raycast it generates hits a destroyable object, it calls a CustomDestroy() on it which simply makes the objects drop items if possible and then calls TNManager.Destroy() on itself because destructible objects should disappear when hit. After this raycast logic is run, a Create() is called for an explosion effect and then Destroy()s itself.

The problems with this are that there is an unnecessary delay for everyone as to when the explosion effect occurs and the bomb is destroyed, as well the destroyable objects being destroyed. There is already normal latency lag for when the bomb is created (I am testing with 100-150ms of latency from my computer to the TNserver.exe I put on a cloud) and that is why I know I am not doing it right. Ideally I'd just have clients network create a bomb, and since I know when it will always explode, just have clients do the logic themselves and create the effects, and destroy the bombs and objects locally themselves. The thing is I need all the bombs to be present when players join (syncing the explosion times would be easy all I would have to do is within the coroutine just check and decrement the timer every frame and when players join, use OnNetworkPlayerJoin() to sync the timer, which I already do to keep track of the bomb timers in a GUI).
« Last Edit: June 03, 2014, 01:00:50 AM by Patterrun »

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #6 on: June 04, 2014, 02:20:15 AM »
You can stick with the RFC method if you add a OnNetworkPlayerJoin function to it, and inside it execute code that will send an RFC to the newly joined player that will create this bomb (but the RFC must be sent via something that has a TNObject on it). Only run this logic if the TNManager.isHosting, and pass the remaining time to the explosion.

In Windward I have a generic manager script attached to a static TNObject that's present for every player as soon as the game loads, before it even connects to the server (and it's marked as DontDestroyOnLoad). This means that I can use this object to communicate with other players regardless of which channel they are in, and indeed I use it for just that -- global chat in my case.

Another advantage of having such an ever-present object is the ability to send an RFC through it, such as when the ships fire their cannons or when an explosion gets created. The less stuff is tracked, the easier it is for the server.

In your case I'd suggest you have bombs destroy themselves and create explosions using their own local timers rather than waiting for the host to explode them. Loot should be created by the host though.

Patterrun

  • Newbie
  • *
  • Thank You
  • -Given: 0
  • -Receive: 1
  • Posts: 12
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #7 on: June 05, 2014, 03:06:54 PM »
I don't think I am explaining it very well but what you suggested is what I already tried, and even the your post about the timer with the bombs is what I did and said in my post (I synchronized the timers with OnNetworkPlayerJoin() so I could show it in the onGUI and see in realtime how the timers are counting down). This is my goal: I want new players to be able to join and see and know about all the latest action on the map so they can join rounds at any time in a relatively synchronized state.

The problem is when new players join, they only see the latest bomb from the tno that created them (player avatar) if I do not use Create() but Send(Target.AllSaved). But if I use Create(), I have to use Create() for the explosions and TNManager.Destroy() after the timer is over, and only by one person (I use the host because if I use tno.isMine and the player leaves, the logic never gets executed because no one owns the bomb object when that happen it seems. If I don't have the host or owner do the logic, every client creates an explosion. Then if I do the explosion locally instead, sometimes the explosion never occurs for some clients as the time between TNManager.Destroy() might reach them faster than their own ticking down of the timer. If I then change TNManager.Destroy() to a normal local destroy, new clients that join never get these messages. Anyway you can ignore all the stuff inside these parantheses but basically I have thought this through and made sure to actually try each thing. ). This way seems to work but introduces a lot of unnecessary delay (becomes apparent at above 100ms ping to server) in every step of the process from:

1) Lay down bomb
2) Bomb timer is over, raycast other objects and send a destroy message to them
3) Create explosion, destroy self (bomb)
4) Other objects that have received a destroy message (that then calls TNManager.Destroy after running their own logic like drop an item) destroy themselves
5) Item is dropped (use similar logic as the above two steps and TNManager.Destroy self after adding to player when someone touches it)

For example, it takes time to see the bomb laid down for everyone, making it feel laggy as soon as you press space. Next, explosion takes a little bit to appear. After, objects that have been sent a destroy message take a while to destroy even though the explosion has already occurred, making it look further laggy. Next items take a bit to appear. Finally when a faster player runs over an item, it takes a while to disappear. If I replace all the logic with Send() messages instead it's all fine but then new players do not see any of the action that may be currently happening and affect them. I suppose I could just cut out this realtime joining and change the game completely but I really, really do not want to do this. I know there has to be a way to get this done.

« Last Edit: June 05, 2014, 03:19:34 PM by Patterrun »

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #8 on: June 06, 2014, 03:02:12 AM »
tno.isMine should always be 'true' for at least one player. When the owner leaves, the host should become the new owner. Also remember, you can change ownership in OnNetworkPlayerLeave on the host.

I wouldn't have one player do a raycast then send destroy messages. Instead I would have each player destroy only themselves. This way you never run into a situation where a player rounds a corner at the last second, thus becoming "safe" and feeling all skilled and happy about it, only to die a split second later because the "host" saw the player in a different position. Remember: players pay more attention to their own avatars than to others. I advise having each player be responsible for killing themselves, basically.

The lag with Create calls is to be expected as it has to happen for all players. You can only eliminate this lag if you create a local object, followed by tno.Send to others. Then there is no visual delay on the client's side, but as we talked about this runs into other difficulties for you since you aren't using TNManager's Create calls in this case. You can never eliminate network lag. You can only make it invisible with a lot of forward thinking and clever tricks.

As for new players not seeing the bombs on join -- why? If you send them all the bombs from the host's computer in the OnNetworkPlayerJoin notification, then they will. There may be a short delay, but they will see them.

Patterrun

  • Newbie
  • *
  • Thank You
  • -Given: 0
  • -Receive: 1
  • Posts: 12
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #9 on: June 13, 2014, 03:33:47 AM »
I ended up scrapping a lot of code over the week and I decided to avoid using Create() calls for everything as much as possible except for players. I ended up synchronizing some things like powerups being picked up by sending arbitrary ids based on grid data I made and then sending RFC calls from the player that entered them to everybody else. Speaking of the grid data I decided to do that so everyone can receive the data and make local copies (via RFCs) when joining, and I based a lot of the code around this concept. Long story short, I did quite a bit for somethings which I thought were going to be simple.

About the new players seeing the bombs on join, when I was still using Create() calls and lots of TNManager.isHosting logic, it was initially for visual effect since it would look strange to be dropped in a match where bombs are exploding and such. If I don't have a kind of protection period for the new player either, then they could die and not know what hit them (host seeing they died) which would be frustrating.

However, now that I changed everything besides the players to local objects, if I don't sync the bombs on join, the game worlds will be desyncronized and different so it was actually important for me to do it now. I already did it though with a convoluted system involving my grid data and keeping track of the bombs.

As for this,
Quote
The lag with Create calls is to be expected as it has to happen for all players. You can only eliminate this lag if you create a local object, followed by tno.Send to others. Then there is no visual delay on the client's side, but as we talked about this runs into other difficulties for you since you aren't using TNManager's Create calls in this case. You can never eliminate network lag. You can only make it invisible with a lot of forward thinking and clever tricks.

Would this not cause de-syncs though? If I create the local object early on the player's screen, then it will explode earlier too on the player's screen but not everyone's elses. Ideally I would like to do this as now the only visual lag I have is when laying down bombs, I managed to hide everything else by doing what I said above which was making everything local and synchronizing player pickups at the very end.

Finally for the players deaths, I do see that way is much better but it's not too much of a problem right now as opposed to the laggy feeling of laying down bombs right now. Apart from a rare de-sync or two in game worlds, I managed to get everything else down.

However, one more thing, since I was feeling better about everything else, I went back to work on the smoothing of client's avatars since that was the most jarring. I don't know if I should open a new thread for this but for now I'll continue. Initially I just sent out position updates without lerping or anything. It's of course not good so I decided to use interpolation on it by keeping states on positions and lerping between them 100ms in the past. This hardly proved to be any better, but I've tried the same popular technique with Photon and such and it's not great there either. Using a simple lerp between the latest update position is smoother, but it slingshots the character which is not much better. I decided to then move everything to FixedUpdate() and it was a bit better but not much. Is there anything else I can do to improve the smoothing? Also, I am using a character controller, not a rigidbody for movement. Using a rigidbody is even worse, especially above 50ms of lag where there is no way to smooth it well even with all the stuff I read here before, re-syncing on collision events, syncing transform and rigidbody 4-5 times per second, etc. My character controller is jerky above 100ms but like the rigidbody is also visible above 50ms.
 

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Syncronizing Players And Intialization
« Reply #10 on: June 13, 2014, 06:33:26 AM »
Yes, there will be a fraction of a second delay, but that's to be expected. Do you expect to see two players play next to each other and stare at each other's screen, comparing this delay? When you talk with overseas via VOIP there is also a delay. It's the nature of all long-distance communication, and is the fact of life of any networked game.

Best way to get visual smoothing is to separate your renderer from your rigidbody, and have the renderer always smoothly follow the rigidbody. I do that in the second example (the one with the cubes you can create), as well as in my games -- and in the Multi-Purpose Game Starter Kit as well.