Show Posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.


Messages - Proton

Pages: [1]
1
...even if they aren't in the channel. In TNGameServer.cs when processing Packet.RequestSetChannelData

  1. // Forward the packet to everyone in this channel
  2. for (int i = 0; i < mPlayerList.size; ++i)
  3. {
  4.         TcpPlayer tp = mPlayerList[i];
  5.         tp.SendTcpPacket(buffer);
  6. }

Should be more like this:
  1. // Forward the packet to everyone in this channel
  2. for (int i = 0; i < mPlayerList.size; ++i)
  3. {
  4.         TcpPlayer tp = mPlayerList[i];
  5.         // Don't send it back to the player that made the request
  6.         if( player == tp )
  7.                 continue;
  8.         // Only send to players in this channel
  9.         if( tp.IsInChannel(ch.id) == false )
  10.                 continue;
  11.  
  12.         tp.SendTcpPacket(buffer);
  13. }

It is handled client side (ignored), but can show up in a race condition when connecting. You could get a ResponseSetChannelData before ResponseID which causes the connection to fail. I haven't seen that happen, but it happened to a different custom packet that follows the same logic.

2
TNet 3 Support / Re: TNCounter Issues (Ticks & IBinarySerializable)
« on: February 23, 2017, 04:19:31 AM »
I'm curious about your game too. Can you tell me more about it?

We're working on Time Warpers, it's a sequel to Time Clickers which is in the 'cookie clicker' genre. Time Clickers started out as an adver-game for Time Rifters, but turned out to be even more popular than the game it was advertising :P. So Time Warpers combines the best aspects of both games + multi-player. Something like an incremental Borderlands.

3
TNet 3 Support / Re: TNCounter Issues (Ticks & IBinarySerializable)
« on: February 13, 2017, 10:32:15 PM »
Yeah, I also changed 'mTimestamp' from double to long.

The other change was to make the 'rate' in seconds instead of milliseconds. It seems much more natural to think of a counter/timer in change/sec. Kind of a hacky way, but I just changed the constructor to use 'this.rate = rate * 0.001;'. I never change rate directly after that, just replace the entire counter if I want to change it. I have the luxury of ignoring edge cases :P

So far we've used TNCounter as a network synchronized timer for the boss, he regenerates his health after the timer runs out (the blue bar): https://gfycat.com/SandyHandyIcelandicsheepdog

Using TNCounter is really efficient and makes the code easy to follow. I think it will be great for active abilities too!

4
TNet 3 Support / TNCounter Issues (Ticks & IBinarySerializable)
« on: February 09, 2017, 11:36:48 PM »
I'm still on 3.0.6, but looks the same in 3.0.7.

TNCounter.cs
var time = System.DateTime.UtcNow.Ticks * 0.0000001;

But there are 10,000 ticks per ms, so this should be:
var time = System.DateTime.UtcNow.Ticks * 0.0001;
Reference: https://msdn.microsoft.com/en-us/library/system.timespan.tickspermillisecond(v=vs.110).aspx

This causes the counter to get the wrong value the first time it is used on the client after being sent from the server.

It was tricky to figure out since it doesn't happen if you are in the room when a packet gets sent with a Counter in it (because the packet is just forwarded directly), bit it will happen if it is one that was saved on the server since the server serialization causes this issue.

It's also nice to have a ToString for TNCounter (helped with debugging & looks nice in the TNObject Unity inspector):
  1.     public override string ToString() {
  2.         return string.Format("{0:f2} [{1},{2}] {3}/sec", value, min, max, rate*1000);
  3.     }
   
   
I also had to modify TNSerialier.WriteObject:
else if (obj is IBinarySerializable && (obj is Counter)==false)

Otherwise the Counter just gets serialized like IBinarySerializable instad of using the special efficient GetPrefix identifier (24). Somehow IBinarySerializable didn't work either, perhaps because I had the type fully qualified as TNet.Counter, not sure didn't investigate that further.

Now everything seems to be working nicely :)

5
I got rid of some of the garbage:
  1.     /// <summary>
  2.     /// Set the object-specific data. Allows for types that the server can not parse by serializing it client side.
  3.     /// </summary>
  4.  
  5.     public void SetCustomType( string path, object data ) {
  6.         /*
  7.         // BEFORE
  8.         // 2.4 KB Garbage total when converting an enum via Reflection and wrapping in a DataNode
  9.         Profiler.BeginSample("TNObject.SetCustomType ToArray");
  10.            
  11.         DataNode node = new DataNode("", data);     // 32 B
  12.         byte[] arr = node.ToArray();                // 1.5 KB (arr.Length = 46)
  13.         Set(path, arr);                             // 0.9 KB
  14.         Profiler.EndSample();
  15.         */
  16.  
  17.         // AFTER
  18.         // 1.4 KB Garbage total when converting an enum without Reflection without wrapping in a DataNode
  19.         Profiler.BeginSample("TNObject.SetCustomType Without DataNode");        
  20.         MemoryStream stream = new MemoryStream();       //  40 B
  21.         BinaryWriter writer = new BinaryWriter(stream); //  56 B
  22.         writer.WriteObject( data );                     // 296 B (all garbage from BinaryWriter.Write(byte))
  23.         byte[] bytes = stream.ToArray();                //  42 B (all garbage from MemoryStream.ToArray)(bytes.Length = 2)
  24.         stream.Close();
  25.         Set(path, bytes);                               // 0.9 KB (all garbage from TcpProtocol.SendTcpPacket -> Socket.BeginSend)
  26.         Profiler.EndSample();
  27.     }
  28.  
  29.     /// <summary>
  30.     /// Get the object-specific data. Allows for types that the server can not parse by doing the deserialization client side.
  31.     /// </summary>
  32.  
  33.     public T GetCustomType<T>( string path ) {
  34.         return GetCustomType<T>(path, default(T));
  35.     }
  36.  
  37.     /// <summary>
  38.     /// Get the object-specific data. Allows for types that the server can not parse by doing the deserialization client side.
  39.     /// </summary>
  40.  
  41.     public T GetCustomType<T>( string path, T defVal ) {
  42.         byte[] bytes = Get<byte[]>(path);
  43.         if( bytes == null || bytes.Length==0 ) return defVal;
  44.  
  45.         MemoryStream stream = new MemoryStream(bytes);
  46.         BinaryReader reader = new BinaryReader(stream);
  47.         T obj = reader.ReadObject<T>();
  48.         reader.Close();
  49.         return obj;
  50.     }

Apparently UNET's NetworkWriter can do zero-garbage conversion of simple values. I didn't try it though.

I also got it to serialize enums more efficiently. First define the enum to only use a byte:
  1.     public enum CarType : byte {
  2.         Tesla,
  3.         PoliceCar
  4.     }

Then modify TNSerializer:
  1.     static int GetPrefix (Type type)
  2.     {
  3.         if( type.IsEnum ) {
  4.             type = Enum.GetUnderlyingType(type);
  5.         }

This gets the bytes sent over the network for an enum from 46 to 2 since it doesn't need to use reflection.

6
Good call. I did a few experiments and found a workaround.

You can do the serialization of custom types client-side and just send a byte array.

Additions to TNObject:
  1.     /// <summary>
  2.     /// Set the object-specific data. Allows for types that the server can not parse by wrapping it in a DataNode then serializing it client side.
  3.     /// </summary>
  4.  
  5.     public void SetCustomType( string path, object data ) {
  6.         DataNode node = new DataNode("", data);
  7.         Set(path, node.ToArray());
  8.     }
  9.  
  10.     /// <summary>
  11.     /// Get the object-specific data. Allows for types that the server can not parse by wrapping it in a DataNode and doing the deserialization client side.
  12.     /// </summary>
  13.  
  14.     public T GetCustomType<T>( string name ) {
  15.         return GetCustomType<T>(name, default(T));
  16.     }
  17.  
  18.     /// <summary>
  19.     /// Get the object-specific data. Allows for types that the server can not parse by wrapping it in a DataNode and doing the deserialization client side.
  20.     /// </summary>
  21.  
  22.     public T GetCustomType<T>( string name, T defVal ) {
  23.         byte[] bytes = Get<byte[]>(name);
  24.         if( bytes == null ) return defVal;
  25.  
  26.         DataNode node = DataNode.Read(bytes);
  27.         if( node == null ) return defVal;
  28.  
  29.         return node.Get<T>(defVal);
  30.     }
  31.  

An example of it in use:

https://gfycat.com/LivelySereneBrahmancow

  1. using UnityEngine;
  2. using TNet;
  3.  
  4. public class ExampleCarType : TNBehaviour {
  5.  
  6.     public enum CarType {
  7.         Tesla,
  8.         PoliceCar
  9.     }
  10.  
  11.     public CarType carType = CarType.Tesla;
  12.  
  13.     public GameObject policeLights;
  14.     public GameObject teslaLogo;
  15.  
  16.     public override void OnInit() {
  17.         tno.onDataChanged += OnDataChanged;
  18.     }
  19.  
  20.     void Update () {
  21.         if( tno.isMine == false )
  22.             return;
  23.  
  24.         if( Input.GetKeyDown(KeyCode.Alpha1) ) {
  25.             SetCarType(CarType.Tesla);
  26.         }
  27.         if( Input.GetKeyDown(KeyCode.Alpha2) ) {
  28.             SetCarType(CarType.PoliceCar);
  29.         }
  30.     }
  31.  
  32.     void SetCarType( CarType newCarType ) {
  33.         tno.SetCustomType("CarType", newCarType);
  34.     }
  35.  
  36.     void OnDataChanged( string path, DataNode node ) {
  37.         if( path == "" || path == "CarType" ) {
  38.             carType = tno.GetCustomType("CarType", CarType.Tesla);
  39.             RefreshCarType();
  40.         }
  41.     }
  42.  
  43.     void RefreshCarType() {
  44.         policeLights.gameObject.SetActive(carType == CarType.PoliceCar);
  45.         teslaLogo.gameObject.SetActive(carType == CarType.Tesla);
  46.     }
  47. }

Still, nothing is preventing me from adding a custom notification method for when only a part of the DataNode changes.
The entire DataNode would still be going over the network every time a single value changes though. Efficiency depends on the project I suppose, if TNObject.Set isn't called too often then it isn't a big problem.

The ExampleCarType script doesn't show any code advantages over RFC, I've been using UniRx & ECS for a while, this is what my code would actually look like:
  1. namespace TimeWarpers {
  2.     using UnityEngine;
  3.     using uFrame.Kernel;
  4.     using UniRx;
  5.     using System;
  6.  
  7.     public enum CarType {
  8.         Tesla,
  9.         PoliceCar
  10.     }
  11.  
  12.     public partial class CarTypeSystem {
  13.  
  14.         // Setup all the CarType behaviour. No need to think about Networking here.
  15.         // This logic happens for Local & Remote (owner and non-owner)
  16.         protected override void OnCarTypeSwitcherCreated( CarTypeSwitcher carTypeSwitcher ) {
  17.  
  18.             // Listen for the latest new value
  19.             carTypeSwitcher.CarTypeLatestUniqueObservable
  20.                 .Subscribe(carType => {
  21.                     carTypeSwitcher.TeslaLogo.SetActive(carType == CarType.Tesla);
  22.                     carTypeSwitcher.PoliceLights.SetActive(carType == CarType.PoliceCar);
  23.                 }).DisposeWith(carTypeSwitcher);
  24.         }
  25.  
  26.         // Local: Logic for the owner of the net object
  27.         protected override void OnLocalCarTypeSwitcherCreated( LocalCarTypeSwitcher group ) {
  28.  
  29.             // The Local player (object owner) can use keyboard input
  30.             RxInput.OnKeyDown(KeyCode.Alpha1)
  31.                 .Subscribe(_ => group.CarTypeSwitcher.CarType = CarType.Tesla)
  32.                 .DisposeWith(group);
  33.  
  34.             RxInput.OnKeyDown(KeyCode.Alpha2)
  35.                 .Subscribe(_ => group.CarTypeSwitcher.CarType = CarType.PoliceCar)
  36.                 .DisposeWith(group);
  37.  
  38.             // Send local changes to the server
  39.             // Only send if the value changes
  40.             // Don't send more often than every 50ms
  41.             group.CarTypeSwitcher.CarTypeLatestUniqueObservable
  42.                 .Sample( TimeSpan.FromMilliseconds(50) )
  43.                 .Subscribe(carType =>
  44.                     group.NetEntity.tno.SetCustomType("CarType", carType)
  45.                 ).DisposeWith(group);
  46.         }
  47.  
  48.         // Remote: Logic for the players that don't own the object
  49.         protected override void OnRemoteCarTypeSwitcherCreated( RemoteCarTypeSwitcher group ) {
  50.             // Listen for changes to the server variable and apply them locally
  51.             group.NetEntity.GetLatestCustomType("CarType", CarType.Tesla)
  52.                 .Subscribe(carType =>
  53.                     group.CarTypeSwitcher.CarType = carType
  54.                 ).DisposeWith(group);
  55.         }
  56.     }
  57. }
Doing data-driven reactive stuff might be another programming fad (and it looks a little strange at first), but Netflix is really embracing it (and I love the quality of their software), and I find it kind of fun :P

7
Cool, good to know I was on the right track.

I went ahead and made the changes, seems to work really nicely. I attached a git patch to this post.

It does break backwards compatibility in 2 places:
1. The client side RFC based DataNode is not used, so it kinda gets lost if limbo if it exists in a server save file.
2. TNChannel.SaveTo/LoadFrom:
Needed to save/load DataNode per CreatedObject. So that would be completely broken for an existing save file.
Idea: Backward & Forward compatibility if it used a DataNode instead of raw BinaryWriter?

I also noticed TNTcpProtocol.noDelay, so it looks like by default TNet uses Nagle's algorithm to optimize the sending of many small packets (at least I think it does). So that seems pretty cool!  ;D

I think the biggest concern I have is that after a TNet game is running in the wild for a while, the server-side database could become a tricky thing to manage (no standard db admin tools) and migrate forward if there are breaking changes like this one. DataNode seems to solve a lot of forward compatibility issues in theory, curious if it will work in practice. Limiting my persistence to only the RCC & Server/Channel/Player/Object DataNodes should reduce headaches down the line I think (no persistent RFCs). I imagine that an admin tool could be created in Unity to do some basic db admin.

8
Server, Channel & Player all have a server side DataNode, and you can set and listen to individual changes to that DataNode.
TNObject works differently though. The DataNode does technically exist server side as a parameter to an RFC, but you can't set and listen to partial changes. Any time you change 1 value, the entire DataNode is resent to the server then sent to everyone.

Why would I want to do this? To make the networking more data driven. Like how Google's Firebase Realtime Database works: https://firebase.google.com/docs/database/

For example, my FPS player TNObject could have a DataNode that contains the state of user input
{MoveH:float, MoveV:float, Jump:bool, Dash:bool, Look:Vector2}

Then calling tno.Set("Jump", true) would only need to send the data that changed. The server-side version of the DataNode would have all the latest data for when new players join. I also notice my code shrinks down in size without using RFCs ;D.

Possibly new packet types RequestSetObjectData, ResponseSetObjectData. Both with parameters [ChannelId,ObjectId,Path,Object/DataNode]
TNObject.OnDataChanged would need a path.

The goal would be network efficiency, and nicer code (IMO). But I can think of 1 possible downside with this approach.

If you want to set all 5 properties in 1 frame, it would create 5 TCP Packets instead of 1. It looks like TNTcpProtocol has an outgoing queue (mOut) but it doesn't appear to be used for batching multiple packets together.

Many games (Overwatch, Rainbow 6 Siege, Rocket League) and network libraries (Photon) have the idea of a client and server tick rate. So it would buffer up outgoing request and only send it 30 times per second for example. The server will also queue up outgoing events to specific players and send them as 1 TCP packet instead of a bunch of individual ones.

Perhaps these optimizations are no longer necessary with the massive bandwidth & low pings players have now?

Maybe there is some other downsides to making the TNObject DataNode server side that I am missing?

9
TNet 3 Support / Re: One frame of lag when joining a channel
« on: December 22, 2016, 09:18:12 PM »
(I don't see an edit post option, so new reply it is)
Another thought, it might be good to only sync the transform the first time the RFC is called. Changing transforms directly on rigidbodies can cause them to explode if they collide :o

10
TNet 3 Support / One frame of lag when joining a channel
« on: December 22, 2016, 08:58:54 PM »
I captured a video of what I mean: https://gfycat.com/ThunderousDearCaimanlizard

For 1 frame the player in Channel 60 is in the wrong spot (the spot where he spawned). But it doesn't happen every time.

I confirmed the packets are being processed in the correct order and all on the same frame.

It turned out the ExampleCar.SetRB RFC was only setting the Rigidbody Pos & Rot, but the TNAutoCreate.CreateAtPosition RCC was setting the Transform Pos & Rot.

Setting the Transform Pos & Rot in the SetRB RFC seems to fix it. I'm guessing the rigidbody only updates the transform on the FixedUpdate, that would explain why it wouldn't happen sometimes.

TNSyncRigidbody is the same (saw the same behavior with the cubes).

Hope this helps someone!

BTW, I recently switched from Photon to TNet and am very impressed! Server-side persistence wasn't something I thought I needed until I saw it. It feels like a bunch of possibilities opened up ;D

Pages: [1]