alvyxaz / barebones-masterserver

Master Server framework for Unity
476 stars 106 forks source link

Exclusive communication between one client and the server #3

Closed tatien closed 7 years ago

tatien commented 8 years ago

Hi Alvyxaz,

I'm trying to communicate only between the server and one specific client, so clientA will never know that clientB exists (even when the clientB connects to the server). Can you tell how I can do that ?

Best regards Tatien

alvyxaz commented 8 years ago

Hey!

Normally, clients will never know about other clients unless you program your servers / games to notify about them.

Even in demo, when multiple players log in to the master server, they have no way of knowing about each other.

Can you write a simplified "flow" of message exchanges you want to do? Or even exactly what you want to do? This way I'd be able to give you a better example :).

And do you want to "push" a message from master server to client, or from client to server? Or is it just a simple request from client, and response from server?

Check out the "Creating a module" page here: https://github.com/alvyxaz/barebones-masterserver/wiki/Creating-Modules

It covers basics of sending a simple message from client to master server, and handling/sending a response.

EDIT:

There's also some info on the matter in Networking API page: https://github.com/alvyxaz/barebones-masterserver/wiki/Networking-API

Or do you want the messages to be exchanged within the game server?

tatien commented 8 years ago

Thanks for your answer, I have been looking at the networking tutorial and even though I understood the concept, i can't find a concret use of it in your demo. In the demo you use Unet, is it compatible with my communication need (server send data to clientA and different to clientB) ?

This is what i want to do : 1) ClientA : Connect to the GameServer 2) ClientB : Connect to the GameServer 3) Server : Start game (for ClientA and ClientB) 4) Server : SpawnUnit (for ClientA and ClientB) 5) ClientA : Ask for increase spawn rate 6) ClientB : Ask spawn tower => Server : Spawn tower (for ClientA and ClientB) 7) Server : End of game (for ClientA and ClientB)

Simultaneously I have ClientC and ClientD but the server doesn't notify them for ClientA and ClientB game change. I want 2 games instances within the same GameServer.

alvyxaz commented 8 years ago

Oh, got it!

Links I've sent you were meant not for the game servers, but instead for master server/any and other server (except for game server). (feels like there'e are too many words "server", haha)

What you want to do is a great concept, and I'm planning to do something similar in one of my games in the near future. Despite of a few possible caveats, I don't think it will be too hard to achieve.

Since this whole thing will be managed mainly within a game server, the problem becomes not as much framework, but more uNET specific.

How to send a message to a specific user

If you base your game on the UnetGameServer class (like the example class RoomGameServer), after each client joins, a special method will be invoked:

protected override void OnServerUserJoined(UnetClient client)

UnetClient instance will have these properties:

You can use the Connection property to send a personal message like this:

client.Connection.Send(msgType, new StringMessage());

⚠️ Make sure you read this (More info about sending and receiving UNet messages): https://docs.unity3d.com/Manual/UNetMessages.html

While the GitHub was down I wrote a quick example. It wont work, and it's meant only as the guide on how you can tackle the problem of sending personal messages:

using System.Collections.Generic;
using Barebones.MasterServer;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Networking.NetworkSystem;

/// <summary>
/// Represents a single game "instance" within a game server
/// </summary>
public class GameInstance
{
    public Dictionary<string, UnetClient> ClientsInGame { get; private set; }

    public GameInstance()
    {
        ClientsInGame = new Dictionary<string, UnetClient>();
    }

    public void Broadcast(short messageType, MessageBase message)
    {
        foreach (var client in ClientsInGame.Values)
        {
            client.Connection.Send(messageType, message);
        }
    }
}

/// <summary>
/// A list of message types for unet
/// </summary>
public class MyCustomMsgTypes
{
    public const int SpawnTower = UnetMsgType.Highest + 1;
}

/// <summary>
/// Game server logic
/// </summary>
public class MultiGameServer : UnetGameServer
{
    // Key - uNET connection id
    private Dictionary<int, GameInstance> PlayerGameLookup;

    // Game instances
    private Dictionary<string, GameInstance> Games;

    public MultiGameServer()
    {
        PlayerGameLookup = new Dictionary<int, GameInstance>();
        Games = new Dictionary<string, GameInstance>();

        // Listen to a specific type of message (spawn tower)
        NetworkServer.RegisterHandler(MyCustomMsgTypes.SpawnTower, HandleSpawnRequest);
    }

    /// <summary>
    /// Invoked when client joins a game server
    /// </summary>
    /// <param name="client"></param>
    protected override void OnServerUserJoined(UnetClient client)
    {
        // This is just an example, you can assign player instances here, or somewhere
        // else after some action was taken by the player (like clicking a button)

        // 1. Determine which game instance a client belongs. 
        // For the purposes of this example, I'll just hardcore it 
        var gameName = "gameA";

        // 2. Find or create a new game instance
        var game = Games[gameName];

        // 3. Add player to that game
        game.ClientsInGame.Add(client.Username, client);

        // 4. Add player-game to the lookup
        PlayerGameLookup.Add(client.Connection.connectionId, game);
    }

    protected override void OnServerUserLeft(UnetClient client)
    {
        // Remove clients and etc.
    }

    /// <summary>
    /// This is called on game server, when it retrieves a message from client
    /// to spawn a tower
    /// </summary>
    /// <param name="message"></param>
    private void HandleSpawnRequest(NetworkMessage message)
    {
        // 1. Find out who sent the message
        UnetClient client = GetClient(message.conn);

        // 2. Find out which game instance the player plays in
        GameInstance game;
        PlayerGameLookup.TryGetValue(message.conn.connectionId, out game);

        // 3. Do the spawning

        // 4. Notify players in the game instance about the new tower
        game.Broadcast(MyCustomMsgTypes.SpawnTower, new EmptyMessage());
    }
}

Let me know if it makes things a bit more clear :).

Do you have any thoughts on how you're going to determine which game instance each client should play in, or what players should play together?

tatien commented 8 years ago

Thanks, I think I start to understand how to do it on my server. On the client I need to have the EventfulNetworkManager in the scene to send and receive my message, is that right ? What is the best way to handle message on the client ? Extends ClientSocketUnet with the RegisterHandler?

To determine the instance for a game I will put my matchmaking algorithm on the MasterServer, send my players on a GameServer and create a GameInstance for this game. For the matchmaking I didn't do anything yet.

alvyxaz commented 8 years ago

Awesome! If it's one of your first multiplayer games, getting a full grasp on how general game networking works might be a bit daunting (there's a large number of intertwining concepts), but you'll get through this.

Yes, EventfulNetworkManager is an extension of NetworkManager, which is necessary if you want to base your game on uNET's High Level API (HLAPI).

You don't really need to extend ClientSocketUnet, and you probably never will, because it's an implementation of what I call Networking API, which is used to communicate between Client and Master Server (one of the concrete implementations of abstract Networking API socket)

In uNET, you register handlers to your current connection (NetworkConnection). This means that to register a handler, you first need to have a connection to server, and then be able to access it in your code. There are multiple ways you can do it. Here's an example of the one I like the most:

using UnityEngine;
using Barebones.MasterServer;
using UnityEngine.Networking;
using UnityEngine.Networking.NetworkSystem;

/// <summary>
/// This script should be added anywhere in the game scene.
/// It represents "client logic".
/// </summary>
public class ClientScript : MonoBehaviour
{
    private EventfulNetworkManager _networkManager;

    // Use this for initialization
    void Awake ()
    {
        // Find the network manager in our scene
        _networkManager = FindObjectOfType<EventfulNetworkManager>();

        // Listen to an event which will notify us that client has connected to server
        _networkManager.OnClientConnectEvent += OnClientConnectedToServer;
    }

    private void OnClientConnectedToServer(NetworkConnection connection)
    {
        Debug.Log("I'm a client, and I've connected to the uNET game server!");

        // At this point, we are connected to game server, and we can register our handlers
        connection.RegisterHandler(777, HandleMySpecialMessageFromServer);
    }

    private void HandleMySpecialMessageFromServer(NetworkMessage message)
    {
        Debug.Log("I've received a special message: " + message.ReadMessage<StringMessage>().value);
    }

    void OnDestroy()
    {
        // Unregister the listener
        _networkManager.OnClientConnectEvent += OnClientConnectedToServer;
    }
}

You can have multiple scripts like these in your scene :)

tatien commented 8 years ago

Thanks, I succeed to send message to the server and get back another message. Do you know if I can create a message which contain a list of a custom class ?

By the way I also found another solution for the Socket denied on Mac OS, to avoid use root permissions. I change every port to 1XXXX for example client was 90 I set it to 10090. Do you think this is a good solution ?

alvyxaz commented 8 years ago

Hey,

Regarding the list serialization - it depends. If you're asking about serializing difficult objects - you'd need some kind of a serialization library.

However, messages you send "over the wire" should be in a relatively simple structure. If by "custom class" you meant a custom class which extends MessageBase (uNET message), then yes, there is a built-in solution.

According to uNET documentation (Network Messages) : Message classes can contain members that are basic types, structs, arrays, and most of the common Unity Engine types such as Vector3.

So you can have an array within your message class. I've tried a simple example, and it worked.

public class InnerMessage : MessageBase
    {
        public string Username;
        public string Address;
    }

    public class MainMessage : MessageBase
    {
        public string GroupName;
        public InnerMessage[] Members;
    }

(It's an array, so you'll need to convert your list to array first (list.ToArray()))

If you need something more complex, you'll need to override Deserialize and Serialize methods of the MessageBase.

Regarding the Mac OS solution with ports - woah, I didn't know about this! Yeah, I think it's a good solution. If I had known this, I might have had the ports default to something like 1xxxx+ :)

tatien commented 8 years ago

I was working on my game server but can you tell me how to launch the game server in editor? It is for debug purpose.

alvyxaz commented 8 years ago

Actually, if you try to open the GameRoom scene in the editor and hit a "Play" button, it should start the game server and get you into the game.

Getting started guide contains some information about how it works, and that shouldn't be too hard to replicate for other games: https://github.com/alvyxaz/barebones-masterserver/wiki/Getting-Started#joining-the-game

I'm not sure if that guide will be good enough, let me know if there's something that's hard to understand, or if you still can't get the game server running, and I'll help you out :)

tatien commented 7 years ago

I have already succeed to send message who extends MessageBase but now I'm trying to send a message with an array of object, but I'm stuck with errors.

My message class:

public class ListEntityMessage : MessageBase {

    public Entity[] entities;

    public ListEntityMessage() {
    }

    public ListEntityMessage(List<LivingEntity> livingEntities) {
        SetEntities(livingEntities);
    }

    public void SetEntities(List<LivingEntity> livingEntities) {
        Debug.Log("Set " + livingEntities.Count + " entities");
        entities = new Entity[livingEntities.Count];
        List<Entity> _entity = new List<Entity>();
        foreach (LivingEntity entity in livingEntities) {
            _entity.Add(new Entity(entity.id, entity.transform.position, (int)entity.health, entity.GetType()));
        }
        entities = _entity.ToArray();
    }

    public class Entity {

        public enum LE {
            Creep,
            Tower,
            QG}

        ;

        public string id;
        public LE type;
        public Vector3 pos;
        public int health;

        public Entity(string _id, Vector3 _pos, int _health, System.Type type) {
            id = _id;
            pos = _pos;
            health = _health;
            SetType(type);
        }

        private void SetType(System.Type _type) {
            if (_type == typeof(Creep)) {
                type = LE.Creep;

            } else if (_type == typeof(Tower)) {
                type = LE.Tower;

            } else if (_type == typeof(QG)) {
                type = LE.QG;

            }

        }
    }
}

I got this error on my client.

InvalidProgramException: Invalid IL code in Unity.GeneratedNetworkCode:_ReadEntity_ListEntityMessage (UnityEngine.Networking.NetworkReader): IL_0000: newobj    0x06000919

Unity.GeneratedNetworkCode._ReadArrayEntity_None (UnityEngine.Networking.NetworkReader reader)
ListEntityMessage.Deserialize (UnityEngine.Networking.NetworkReader reader)
UnityEngine.Networking.NetworkMessage.ReadMessage[ListEntityMessage] () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/UNetwork.cs:162)
ClientScript.HandleListEntityMessageFromServer (UnityEngine.Networking.NetworkMessage message) (at Assets/Arena/Scripts/Client/ClientScript.cs:100)
UnityEngine.Networking.NetworkConnection.HandleReader (UnityEngine.Networking.NetworkReader reader, Int32 receivedSize, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:453)
UnityEngine.Networking.NetworkConnection.HandleBytes (System.Byte[] buffer, Int32 receivedSize, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:409)
UnityEngine.Networking.NetworkConnection.TransportRecieve (System.Byte[] bytes, Int32 numBytes, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:561)
UnityEngine.Networking.NetworkClient.Update () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkClient.cs:734)
UnityEngine.Networking.NetworkClient.UpdateClients () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkClient.cs:949)
UnityEngine.Networking.NetworkIdentity.UNetStaticUpdate () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkIdentity.cs:1061)

So I add serialization to my message:

    public override void Serialize(NetworkWriter writer) {
        writer.Write(entities.Length);
        for (int i = 0; i < entities.Length; i++) {
            writer.Write(entities [i].id);
            writer.Write((int)entities [i].type);
            writer.Write(entities [i].pos);
            writer.Write(entities [i].health);

        }
    }

    public override void Deserialize(NetworkReader reader) {
        int size = 0;
        size = reader.ReadInt32();
        entities = new Entity[size];

        for (int i = 0; i < size; i++) {
            entities [i].id = reader.ReadString();
            entities [i].type = (Entity.LE)reader.ReadInt32();
            entities [i].pos = reader.ReadVector3();
            entities [i].health = reader.ReadInt32();
        }
    }

But I get another error :

NullReferenceException: Object reference not set to an instance of an object
ListEntityMessage.Deserialize (UnityEngine.Networking.NetworkReader reader) (at Assets/Arena/Scripts/Message/ListEntityMessage.cs:49)
UnityEngine.Networking.NetworkMessage.ReadMessage[ListEntityMessage] () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/UNetwork.cs:162)
ClientScript.HandleListEntityMessageFromServer (UnityEngine.Networking.NetworkMessage message) (at Assets/Arena/Scripts/Client/ClientScript.cs:100)
UnityEngine.Networking.NetworkConnection.HandleReader (UnityEngine.Networking.NetworkReader reader, Int32 receivedSize, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:453)
UnityEngine.Networking.NetworkConnection.HandleBytes (System.Byte[] buffer, Int32 receivedSize, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:409)
UnityEngine.Networking.NetworkConnection.TransportRecieve (System.Byte[] bytes, Int32 numBytes, Int32 channelId) (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkConnection.cs:561)
UnityEngine.Networking.NetworkClient.Update () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkClient.cs:734)
UnityEngine.Networking.NetworkClient.UpdateClients () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkClient.cs:949)
UnityEngine.Networking.NetworkIdentity.UNetStaticUpdate () (at C:/buildslave/unity/build/Extensions/Networking/Runtime/NetworkIdentity.cs:1061)

Do you know how I can fix it ? I'm also not sure to have done this on the right way.

alvyxaz commented 7 years ago

Hey!

I'll check it out and get back to you. From a quick glance, your serialize / deserialize methods seem good :)

alvyxaz commented 7 years ago

Which line is the ListEntityMessage.cs:49 ? (the one that throws the NullReferenceException

My guess would be that entities is empty when you call entities.Length. Or one of the elements within array is null.

tatien commented 7 years ago

It is the line entities [i].id = reader.ReadString(); in Deserialize method.

public override void Deserialize(NetworkReader reader) {
        int size = 0;
        size = reader.ReadInt32();
        entities = new Entity[size];

        for (int i = 0; i < size; i++) {
            Debug.Log("loop : " + i);
            entities [i].id = reader.ReadString();
            entities [i].type = (Entity.LE)reader.ReadInt32();
            entities [i].pos = reader.ReadVector3();
            entities [i].health = reader.ReadInt32();
        }
    }
alvyxaz commented 7 years ago

Oh, yeah! Not sure how I've missed that, haha.

The thing is, when you create an array with new Entity[size], it only creates a structure of "places for references".

After creation of the array, they refer to nothing, a.k.a - null. So your array actually looks like this:

{null, null, null...}

This is why when you call entities[i].id, it actually translates to null.id. null object (or the lack of object) has no id, so it causes an exception.

To fix this, in the for loop, create the actual object like this:

for (int i = 0; i < size; i++) {
    entities[i] = new Entity(); // Creating the actual instance
    entities[i].id = ....
tatien commented 7 years ago

Thank you , I don't know how I missed that. My message system works now. 👍

alvyxaz commented 7 years ago

Congratulations!

That happens, usually, more than you'd want to, haha . I've spent countless hours being stuck on something very simple, and thinking that the problem is impossible to solve. And it still happens to this day 😄