Author Topic: Pre-Sale Questions  (Read 5914 times)

cmifwdll

  • Global Moderator
  • Sr. Member
  • *****
  • Thank You
  • -Given: 0
  • -Receive: 149
  • Posts: 285
  • TNet Alchemist
    • View Profile
Pre-Sale Questions
« on: July 13, 2015, 10:06:31 PM »
I've been fighting (big emphasis on "fighting") Unity's networking for a few weeks now, so I'm at the point where I need to start looking for other solutions. I'll try to convey what I need for my project, and hopefully with feedback I'll be able to determine if TNet is the right fit.

Some things which I feel may be relevant:
* The project is using the latest version of Unity (5.1.1p4)
* Standalone Desktop platform only
* Host-based multiplayer (one player acts as both client and server)
* The Host is the authority (performs all physics, attacks, damage, movement, etc. Clients are not trusted)
* Invasion mechanic (dark souls style) (will explain more below)
* I come from a programming and networking background, but not a game development background

Upon startup, players register with the master server and begin sending heartbeats at regular intervals. When invading, players tell the master server "hey, here's my latest info, find me a match". The master server does some processing to find a match, and tells both players when a match is found. In doing so, the master server acts as a NAT punchthrough facilitator, in addition to its role as a master server. I've got the master server set up, put it up on a VPS and tested using two separate WANs, and all is well. The problem comes with implementing this into Unity. Without going too much into it, the problem is that I can't make use of Unity's underlying Socket. I need to be able to send/receive packets to/from any destination from the same Socket the game is being hosted on. Or, at the very least, be able to handle NAT punchthrough and matchmaking from within the engine.

Additionally, Unity has some weird... technical limitations. Logical structure is very important for this project. It needs to make enough sense that junior coders will be able to add on to it. Following this paradigm, the Player class handles the Player stuff, the Inventory class handles the Inventory stuff, the Weapon classes handle the Weapon stuff. Of course, the Player class has an Inventory instance, and the Inventory instance holds an array of Weapon instances, but it all meshes well and each part handles its own stuff, as you'd expect. I really feel like this part is important to convey, so I'll give an example: The Player class handles Input, and when the Input for "attack" is detected, the Player class calls playerInventory.EquippedItems[SlotType.MainHand].Attack(). This will call the Attack function on the equipped Weapon. The Weapon class then handles animating the weapon swing, in addition to performing the hit detection and damage calculation server-side. Unity doesn't allow for this behaviour because my Weapon component exists on a child GameObject of the root Player GameObject. Calling a Command in the Weapon class from the Weapon class will result in a "Trying to send command for non-local player" error, because, according to Unity, a child GameObject of a parent GameObject that IS the local authority isn't the local authority. The parent is the local authority but the child isn't. I need to be able to declare and call networked functions from each Component, regardless of its position in the GameObject (Scene?) hierarchy. Unity's solution to this is to put all the logic in one place, the Player class, and have the Weapon class call that. This becomes really messy when there are multiple Weapon types, each with their own attack logic, all inheriting from a base Item class.

To summarize my needs:
* I need to be able to handle NAT punchthrough and communication with my own matchmaking server while *simultaneously hosting* a game
   * Or, if that's not possible, I need to be able to send/receive packets to/from any destination from the same Socket the game is being hosted on
* I need to be able to declare and call networked functions from any Component, regardless of its position in the hierarchy.
   * For example, a Weapon component is on a child GameObject of the GameObject that has local authority (the Player GameObject). That Weapon component needs to be able to declare and call [Command] and [ClientRpc] functions to perform hit detection and damage calculations on the server.
   
Going through the forums I see a few posts with the same issues, and the replies typically point out a design flaw. Since I lack game development experience, is there a flaw with my design? Should I be thinking about this differently?

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Pre-Sale Questions
« Reply #1 on: July 15, 2015, 12:42:29 AM »
TNet doesn't handle NAT punchthrough. NAT punchthrough is generally for UDP-based communication, while TNet primarily uses TCP.

You can be connected to both the lobby server and a game server at the same time, however I question whether this is what you want... I'd handle it differently, like so:

1. Main game server that everyone connects to, hosted in a public-accessible location, such as an Amazon EC2 cloud.

2. Players connect to this game server -- at this point you can communicate with other players by sending broadcasts.

3. For match-making, I'd do it like this: send a request for channels:
  1.                 TNManager.BeginSend(Packet.RequestChannelList);
  2.                 TNManager.EndSend();
assuming you set a listener for this packet like so:
  1. TNManager.client.packetHandlers[(byte)Packet.ResponseChannelList] = UpdateWorldData;
...you will receive a list of all the channels with detailed info such as the number of players, player limit, and custom string data you can use to store other custom information such as the expected level requirement.


4. When you get the list of channels, you should be able to see if any channels are open with fewer than maximum players that have the appropriate level requirement that you can join. If so, join one. If not, create your own channel via TNManager.CreateChannel (be sure to set the expected player limit, such as 2!)

5. At this point the channel's host (the player that created it) will wait for another player to join (OnNetworkPlayerJoin notification). Once one joins, close the channel so no one else can join and start your game, possibly by loading the actual game level (TNManager.LoadLevel) -- make sure to only do this on the host player (TNManager.isHosting).

6. To send messages such as "attack", use RFCs: tno.Send("SomeFunction", TNet.Target.All, ...); Do not call these functions directly. That's the main difference between making a single player game and a multi-player game. Instead of calling functions, you will be sending messages to call those functions. And that's it.

P.S. if you want to store player information, use TNManager.playerDataNode. It's an XML-like hierarchical structure that you can change at any time to store any info you need, such as what kind of weapon you have equipped. Each player should be managing their own data node. Use TNManager.SyncPlayerData() to sync it with other clients after any changes. Other players always have access to this struct via player.dataNode -- but make sure to only read, not write.

As a nice benefit, DataNode structs are easily save-able to a file (so this becomes your player's save file!). You can read them back in the same exact way. In Windward I simply read the DataNode player file then set it to TNManager.playerDataNode -- so all players have access to each other's full player data at all times. This includes equipment, inventory, talent tree unlocks, achievement unlocks, and more.

cmifwdll

  • Global Moderator
  • Sr. Member
  • *****
  • Thank You
  • -Given: 0
  • -Receive: 149
  • Posts: 285
  • TNet Alchemist
    • View Profile
Re: Pre-Sale Questions
« Reply #2 on: July 15, 2015, 02:21:34 AM »
Thanks for getting back to me so quickly.

I think I have a better understanding of your concept of channels now, and I think they'll make NAT a non-issue.

6. To send messages such as "attack", use RFCs: tno.Send("SomeFunction", TNet.Target.All, ...); Do not call these functions directly. That's the main difference between making a single player game and a multi-player game. Instead of calling functions, you will be sending messages to call those functions. And that's it.

My question wasn't how to invoke functions on other clients, rather, the restrictions on where we can send, receive, and process these messages. Will I be able to send (and receive) a RFC on a component attached to a gameobject that is a child of the root player gameobject?

For example, in the following scene hierarchy:


There is a Player gameobject with several child gameobjects. Each child gameobject would have a script attached to it, like so:


Would this script be able to send, receive, and process RFCs given its position in the object hierarchy? If yes: would this object require its own NetworkIdentity component (I think you call it TNObject)? If yes to that question: would having a TNObject on each child gameobject, along with one on the root player gameobject, cause problems? To elaborate on a (the) possible problem: using Unity's networking, each gameobject that makes use of networking must inherit from the NetworkBehaviour class (and have a NetworkIdentity component attached), and from there you use isLocalPlayer to determine if you own the gameobject. The problem is that isLocalPlayer only evaluates to true on the root player gameobject, NOT on its children.

edit: forgot to explain the importance of isLocalPlayer. isLocalPlayer, a property of the NetworkBehaviour class that all networked entities inherit, is used a lot in Unity's internal network functions. Most importantly, when calling any function with the [Command] attribute (similar to tnet's RFC system, but strictly client -> server), the *internal* Unity function (meaning something I have no control over), SendCommandInternal is called, and the very first line of this function checks to see if isLocalPlayer is true or not. If it's false (and it is on child gameobjects), the function returns and the [Command] is never sent over the network. So that's why I need to know if tnet handles child gameobjects differently, and if I'll be able to send and receive RFCs on these child gameobjects.

I hope I explained my question(s?) adequately. I really suck at articulation.
« Last Edit: July 16, 2015, 02:21:50 AM by cmifwdll »

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Pre-Sale Questions
« Reply #3 on: July 16, 2015, 11:13:33 AM »
Yes, as long as there is a TNObject, you can send and receive messages. Think of TNObject as a house address and RFCs as letters you send via your postal service. When you send a letter, you specify an address. The same thing here -- except instead of an address it automatically matches TNObject IDs. When you send an RFC from your script attached to a TNObject, that message gets forwarded to all clients, telling them to execute your function on a TNObject with that ID.

That said, I recommend keeping one TNObject per player if you instantiate the player object (on the root game object), because otherwise TNet won't be able to assign it a unique ID. If you have your player exist as a part of the scene and you don't instantiate it, then it's a non-issue and you can have many TNObjects in your hierarchy.

cmifwdll

  • Global Moderator
  • Sr. Member
  • *****
  • Thank You
  • -Given: 0
  • -Receive: 149
  • Posts: 285
  • TNet Alchemist
    • View Profile
Re: Pre-Sale Questions
« Reply #4 on: July 16, 2015, 05:56:49 PM »
This seems very limiting, and unfortunately won't work with my project. Because of the invasion-mechanic, players must be instantiated. Do you have any plans to change the way you assign IDs? Maybe you can traverse the gameobject when it's instantiated and assign IDs to each child traversed (that has a TNObject)?

It seems really wrong to me that every multiplayer game puts all their networking logic in a single class. Am I looking at it the wrong way? Should I just bite the bullet and clutter up my Player class?

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Pre-Sale Questions
« Reply #5 on: July 16, 2015, 11:31:04 PM »
No, because the server doesn't know anything about components or Unity at all, so it can't count how many TNObjects are needed. Plus it doesn't matter if you only have one TNObject per instantiated game object. The same way you can have multiple people living at the same address, you can have your scripts with RFCs be attached to child objects and they will still be found automatically. You may need to change TNBehaviour to this though, depending on your version:
  1. public abstract class TNBehaviour : MonoBehaviour
  2. {
  3.         [System.NonSerialized] TNObject mTNO;
  4.  
  5.         public TNObject tno
  6.         {
  7.                 get
  8.                 {
  9.                         if (mTNO == null) mTNO = GetComponentInParent<TNObject>();
  10.                         return mTNO;
  11.                 }
  12.         }
  13.  
  14.         protected virtual void OnEnable ()
  15.         {
  16.                 if (tno == null)
  17.                         mTNO = gameObject.AddComponent<TNObject>();
  18.  
  19.                 if (Application.isPlaying)
  20.                         tno.rebuildMethodList = true;
  21.         }
  22.  
  23.         /// <summary>
  24.         /// Destroy this game object.
  25.         /// </summary>
  26.  
  27.         public virtual void DestroySelf () { if (mTNO != null) mTNO.DestroySelf(); }
  28. }

cmifwdll

  • Global Moderator
  • Sr. Member
  • *****
  • Thank You
  • -Given: 0
  • -Receive: 149
  • Posts: 285
  • TNet Alchemist
    • View Profile
Re: Pre-Sale Questions
« Reply #6 on: July 24, 2015, 10:15:01 AM »
No, because the server doesn't know anything about components or Unity at all, so it can't count how many TNObjects are needed. Plus it doesn't matter if you only have one TNObject per instantiated game object. The same way you can have multiple people living at the same address, you can have your scripts with RFCs be attached to child objects and they will still be found automatically. You may need to change TNBehaviour to this though, depending on your version:
  1. public abstract class TNBehaviour : MonoBehaviour
  2. {
  3.         [System.NonSerialized] TNObject mTNO;
  4.  
  5.         public TNObject tno
  6.         {
  7.                 get
  8.                 {
  9.                         if (mTNO == null) mTNO = GetComponentInParent<TNObject>();
  10.                         return mTNO;
  11.                 }
  12.         }
  13.  
  14.         protected virtual void OnEnable ()
  15.         {
  16.                 if (tno == null)
  17.                         mTNO = gameObject.AddComponent<TNObject>();
  18.  
  19.                 if (Application.isPlaying)
  20.                         tno.rebuildMethodList = true;
  21.         }
  22.  
  23.         /// <summary>
  24.         /// Destroy this game object.
  25.         /// </summary>
  26.  
  27.         public virtual void DestroySelf () { if (mTNO != null) mTNO.DestroySelf(); }
  28. }

Thanks to this post we went ahead and purchased TNet. I've been porting the project over, and I'm really, really satisfied with TNet. The flexibility is insane. The custom object creation is insane. The serialization is insane. My Item class implements your IBinarySerializable interface and I can spawn a new Item with just ~45 bytes over the network. I highly recommend TNet to any prospective buyer.

However, I'm having a weird problem. When I attach an Item component (derives from TNBehaviour) to a child gameobject of my Player, a new TNObject is automatically added to that child gameobject.

The automatically added TNObject has the same ID (seen in the scene view) as the TNObject on the root Player gameobject, but there's some differences.
When debugging and viewing the properties of both TNObjects, the following are different:
id - matches the uid on the root Player gameobject, 0 on the child gameobject
mIsRegistered - true on the root Player gameobject, false on the child gameobject
mParent - null on the root Player gameobject, the Player gameobject on the child gameobject (as it should be)
mRFCs - 4 on the root Player gameobject (3 in the Player class, 1 in the AutoSync class), 0 (buffer is null) on the child gameobject (there are *2* RFCs in the Item class!)
rebuildMethodList - false on the root Player gameobject, true on the child gameobject

It's very odd that a new TNObject is even created. I looked for all calls to AddComponent<TNObject>, and the only place it can be added is TNBehaviour. So I set a breakpoint and it's not hit when adding the Item component. I think the TNObject is being added non-generically somewhere (AddComponent(Type) instead of AddComponent<TNObject>()), and the properties are being copied from the parent TNObject via reflection, but something is going wrong with the copy / update process.

This is all with the changes to TNBehaviour you posted.

I can workaround this by adding
  1. tno.rebuildMethodList = true;
  2. if (tno.parent != null)
  3.     tno.parent.rebuildMethodList = true;
  4.  
to the Start() function of my Item class, but this feels like a band-aid solution.

ArenMook

  • Administrator
  • Hero Member
  • *****
  • Thank You
  • -Given: 337
  • -Receive: 1171
  • Posts: 22,128
  • Toronto, Canada
    • View Profile
Re: Pre-Sale Questions
« Reply #7 on: July 24, 2015, 11:04:03 AM »
Well, note the script I posted. My guess is that yours still has [RequireComponent(TNObject)] at the top of it, right? Remove that.

cmifwdll

  • Global Moderator
  • Sr. Member
  • *****
  • Thank You
  • -Given: 0
  • -Receive: 149
  • Posts: 285
  • TNet Alchemist
    • View Profile
Re: Pre-Sale Questions
« Reply #8 on: July 24, 2015, 12:25:55 PM »
Wow... It's always the simple things overlooked. Thank you.