alvyxaz / barebones-masterserver

Master Server framework for Unity
475 stars 106 forks source link

Create/Select a Profile/Character prior to entering Gameworld? #69

Open Specro777 opened 7 years ago

Specro777 commented 7 years ago

Hello! Instead of a problem this time around, I have a question about implementation. I'm currently on my last stretch of 'things I don't fully understand,' so I thought I'd ask for any advice.

While it's not exactly an MMORPG, for the simplicity of this example, let us say that I am making an MMORPG.

Like any MMORPG, there's two elements of a player's existence on my server. His or her account, and his or her character. The player has one account, but can have multiple characters. The account is already handled, but I am trying to better understand how to implement the characters.

I need to do the following:

  1. Create a character that is somehow attached to the account (perhaps with a reference to the account username or ID)
  2. Enter a Character Select Screen that retrieves, then displays all characters associated with the account
  3. Enter the game world with a selected character.

Now, I don't need help with actually implementing the GUI aspects of this procedure, but I do need assistance in understanding how to conceive of the characters in the first place. My first question is: Can you re-purpose Profiles to be interchangeable characters? This appears to be the closest approximation to characters that already exists in the framework. Can profiles simply be reused and re-purposed to be characters? I ask this because I get the feeling profiles aren't intended to be long-term persistent, or intended to be 1 Profile per Account.

If profiles can be used, then I have to ask: How would you obtain all Profiles in a database that identify with a certain account? I am using MySQL for this project but I imagine this can go for all database types. My thought was for each Profile to have an entry for the account Username or ID associated with it. My thought was to potentially get all rows that have (for example) account ID '4', and display that to the end user. They select one, then enter the game loading that particular profile. If this is possible, what classes might I need to look into to make the appropriate alterations?

If Profiles might not be used, how might I go about making a custom Database handler? Is it as easy as it looks? Naturally I would only need to send, and then receive SQL calls infrequently. A character save time of 5 minutes would be ideal, if not instantaneous on any changes. Any time the player would need to receive information from the database is on log in, or whenever a change is made (such as swapping an item in an inventory)

I am not sure if this purpose is fully in line with the intent of this product but considering this is essentially an alternate way of handling the Profiles database, I was hoping to get any information going forward before I attempted my own experiments with the framework.

Thank you very much for any support anyone is able to offer! -Specro

alvyxaz commented 7 years ago

Hey!

That's a fun question, haha

I think trying to use profiles for this case would be a bit troublesome. Possible, but probably not worth the effort.

Instead, I'd recommend you to create a new module, for example, you could call it "CharacterSelectionModule."

I'll add a documentation page about creating custom modules tomorrow.

I took a few minutes to write a quick example of the module:

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

public enum MyOpCodes
{
    // For client
    GetAllCharacters,
    SelectCharacter,
    CreateCharacter,

    // For game server
    GetCurrentCharacterData
}

/// <summary>
/// Quick, universal serializer
/// Not sure if it works, but it should :D
/// </summary>
public class Serializer : SerializablePacket
{
    private Action<EndianBinaryWriter> _writeAction;
    private Action<EndianBinaryReader> _readAction;

    public static SerializablePacket Write(Action<EndianBinaryWriter> writeAction)
    {
        return new Serializer()
        {
            _writeAction = writeAction
        };
    }

    public static SerializablePacket Read(Action<EndianBinaryReader> readAction)
    {
        return new Serializer()
        {
            _readAction = readAction
        };
    }

    public override void ToBinaryWriter(EndianBinaryWriter writer)
    {
        _writeAction.Invoke(writer);
    }

    public override void FromBinaryReader(EndianBinaryReader reader)
    {
        _readAction.Invoke(reader);
    }
}

public class CharacterData
{
    public string Name;
    public Dictionary<string, string> Properties;
}

public class CharacterSelectionModule : ServerModuleBehaviour
{
    public AuthModule AuthModule;

    void Awake()
    {
        AddDependency<AuthModule>();
    }

    public override void Initialize(IServer server)
    {
        base.Initialize(server);

        AuthModule = server.GetModule<AuthModule>();

        server.SetHandler((short) MyOpCodes.GetAllCharacters, HandleGetCharacters);
        server.SetHandler((short) MyOpCodes.SelectCharacter, HandleSelectCharacter);

        server.SetHandler((short) MyOpCodes.GetCurrentCharacterData, HandleGetCurrentCharData);
    }

    #region Message handlers

    /// <summary>
    /// 
    /// </summary>
    /// <param name="message"></param>
    private void HandleGetCharacters(IIncommingMessage message)
    {
        // On client, this is how you'd send a message which would be handled by this handler:
        //Msf.Client.Connection.SendMessage((short) MyOpCodes.GetAllCharacters, (status, response) =>
        //{
        //    if (status != ResponseStatus.Success)
        //    {
        //        Logs.Debug(response.AsString("Unknown Error"));
        //        return;
        //    }

        //    response.Deserialize(Serializer.Read(reader =>
        //    {
        //        var charCount = reader.ReadInt32();

        //        for (var i = 0; i > charCount; i++)
        //        {
        //            var charName = reader.ReadString();
        //            var properties = reader.ReadDictionary();
        //        }
        //    }));
        //});

        // Find out who's the user who sent this message
        var user = message.Peer.GetExtension<IUserExtension>();

        // TODO Get all characters, you'll probably need more data than that
        var characters = new List<CharacterData>()
        {
            new CharacterData()
            {
                Name = "Roflcopter",
                Properties = new Dictionary<string, string>()
            }
        };

        // Respond with the data
        message.Respond(Serializer.Write(writer =>
        {
            // Character count
            writer.Write(characters.Count); 

            // Write the characters
            foreach (var character in characters)
            {
                writer.Write(character.Name);
                writer.Write(character.Properties);
            }
        }), ResponseStatus.Success);
    }

    private void HandleSelectCharacter(IIncommingMessage message)
    {
        var user = message.Peer.GetExtension<IUserExtension>();

        var characterName = message.AsString();

        // TODO get all users
        // TODO Check if players owns this character

        // You'd probably load it from somewhere (like database)
        // With a direct SQL query or something else
        var character = new CharacterData()
        {
            Name = "Roflcopter",
            Properties = new Dictionary<string, string>()
        };

        if (character == null)
        {
            // If character doesn't exist
            message.Respond(ResponseStatus.Failed);
            return;
        }

        // Save selected character data in extensions
        message.Peer.AddExtension(character);

        message.Respond(ResponseStatus.Success);
    }

    /// <summary>
    /// This message would be sent by game server, when he asks which character
    /// a player has selected (you may or may not need this)
    /// </summary>
    /// <param name="message"></param>
    private void HandleGetCurrentCharData(IIncommingMessage message)
    {
        var accountUsername = message.AsString();

        var user = AuthModule.GetLoggedInUser(accountUsername);

        var selectedCharacter = user.Peer.GetExtension<CharacterData>();

        message.Respond(Serializer.Write(writer =>
        {
            writer.Write(selectedCharacter.Name);
            writer.Write(selectedCharacter.Properties);
        }));
    }

    #endregion

}
delmarle commented 7 years ago

i will give some feedback about the documentation and example:

what i miss from v1 and why:

-Working example in the project, with 3 classes that extend base behaviour like for example CharacterCreationModule: BaseServerModule, CharacterCreationClient:BaseClientModule,CharacterCreationUI: Monobehaviour. doing so just keep things clean and streamlined, its easier to debug for everyone. the code example you gave in this page are difficult to work with because no idea if its client or server code?

before we had the example with the coins for profiles but it has been removed, it was good enough: create default profile if none exist, change value inside profile, associate it with a player object to trigger coins pickup directly from environment. that could be a base to cover any kind of logic.

alvyxaz commented 7 years ago

@delmarle Thanks for suggestion!

The code I wrote above is just an example of how one could start working on the module (which is used on the server) - it doesn't cover any client logic at all, and it's in no way complete

The full tutorial on how to approach module creation and extend the framework will be written soon ;)

Specro777 commented 7 years ago

I wanted to check in with my current progress. I don't have anything substantial yet but I've made huge progress in creating this system. Your code example was incredibly helpful in assisting me in understanding how modules function in the framework, and how you're going about this data exchange.

I've been using the Account Database interfacing as a template to flesh out a system to interact with characters in a database, as they are essentially doing the same task, just for different aspects. If I could suggest something for either your current tutorial, or a future tutorial; it would be comments on database management beyond profiles, and how to best store, recollect, sanitize, and secure database information. I mention sanitation because from a glance, it doesn't seem like MSF doesn't support this right out of the box, at least for MySQL. (Things like translating all single quotations to tildes, and back again. Avoiding a "Little Bobby Tables" situation like in XKCD). I could easily write a function to handle this, but I wanted to make sure there weren't any hidden functions or anything that did this already.

Still though, thank you for all the assistance thus far. Granted, I don't know if anything I'm doing right now works yet, but so far I'm having a much easier time understanding how all the pieces fit together here. I'll check back in if and when everything works, or everything breaks horribly and burns to the ground.

RowellKataan commented 7 years ago

I too would like to see a full tutorial on this. In my project, I am storing player stats for each player (how many kills they've had, deaths, overall damage done over their career, etc). Having an easy way to load and save this information would be great.

I tried piecing together something from the Profiles tutorial on the Wiki, but it's not working at all.

RowellKataan commented 7 years ago

Something that I am noticing in regards to Player Profiles...they only seem to be available when connected to a GameServer; not just the MasterServer. My set up is such:

Player connects to master server. Player logs into master server. Player Profile (should be) loaded for the player. Log In panel disappears. Game List Panel and a Player Stats (profile) Panel appear. Player should see his overall kills and deaths, and other tid-bits of info saved in his profile. (does not) Player Creates New Game (or Joins Existing Game). Player connects to the GameServer. Player Profile Loaded (successfully) there. As player plays, changes are made to the profile (kills, deaths, etc). When player leaves, profile is updated (this part also seems to be working). Player returns the the Games List and Player Stats (profile) Panel. Player Stats panel is still all zeros, the profile is empty.

Does one NEED to connect to a game server in order to get their profile information? Or can the client get this information directly from the MasterServer?

alvyxaz commented 7 years ago

@RowellKataan Sorry! I somehow missed your comment.

Yes, it's possible to access player profile on client directly from master server. It's taking a bit long for me to write the tutorial on using profiles in real-world scenario, but it will be easier to see "how" when it's finished :)

RowellKataan commented 7 years ago

No worries.

My script is just a generic class with all the variables I need to track the stats, a serialize and deserialize (into a string) functions, and some functions to load and save to/from the profile from the master server, game server and client. All encapsulated in one place to make using profiles easy.

Specro777 commented 7 years ago

Quick update on how I've been approaching this topic.

Essentially, I took a look at how Authorization is handled, how it contacts the database, how it handles messages between the server, etc. I used all of this as a template and cloned it, using it for characters, and pulling a greater amount of information from the database. I then store the selected character information on the client like the framework does with accounts, for quick and easy future referencing. I may need to copy over a version of this character to the zone server later on so that it can handle alterations to character information, instead of the client doing so.

For the character list, I had to go a little bit further, and a little bit duct-tapey. I made a second version of the Reader to continuously save each iteration that it finds in the database for every character associated with an account. Now, I wasn't sure how to get a list or an array across the network, so I ended up saving two parts of the character information to a parsable string, which I sent back as a message. This saves the character ID, and character name (For display). This is really not a long term solution, but for now, it works. Once I can actually send full data, (if I actually have to), it'll be a perfect system.

I have run into a problem that I can't fully understand, and if no one can answer this issue here, I might start up a new thread about it because it seems pretty complex and oddly specific.

Namely, Changing a chat username is bizarre On first attempt, the chat module would respond that I cannot change my name to be "ChatUserExtension," because I am already named "ChatUserExtension." This error displays for two reasons. If the ChatUserExtension is not null (which it never seemingly is null) it will display the message, reading back the actual ChatUserExtension instead of ChatUserExtension.Username.

The second attempt changed my name accordingly, but it immediately booted me out of the Global Chat. I received the visual error: No Channel is set to be your local channel. After I type /join Global, I then have two versions of myself in the channel, speaking simultaneously.

Then I discovered, that if I kept the server online and left with that username, then logged back in, the username was already taken, and could not be changed again.

I'm guessing Usernames in the Chat system is not entirely complete? The goal of course, is for the player to be recognized and known as their selected character. When I select a character, I have the client automatically set my username to be that of the character.

Thank you for any advice and assistance!

DeadlyCobraXXX commented 7 years ago

I too would like to see a tutorial on this as well. The character selection/spawning in what was selected and the Profile Stats tied to MasterServer vs GameServer.

alvyxaz commented 7 years ago

@Specro777 Chat module was never intended to allow changing usernames in the same session - character selection use-case didn't cross my mind when I was first working on it.

I'll see why disconnection is not handled properly, fix it, write a method to change the username, and get back to you ;)

alvyxaz commented 7 years ago

@Specro777

Try this out: https://gist.github.com/alvyxaz/8dd1e30e76457321eb088b7b41459dc8

I've added a ChatModule.ChangeUsername method, and made sure that RemoveChatUser removes the user from chat channels (which was probably causing the logging out / leaving chat channel issues).

You'll need to call ChatModule.ChangeUsername from somewhere within your custom module which does character selection

Specro777 commented 7 years ago

Thank you very much for this method. I've implemented it, yet for whatever reason, it only worked once. I'm not sure what happened, but I'm heading to bed after I write this message and wanted to check in before I went, and while it was still fresh in my head.

As recommended, I called this new method after the character selection process is completed, inside the Character Module's handler function. At first, it worked, with a few bugs I'll write about below. The next few times however, I started getting this error:

InvalidOperationException: HashSet have been modified while it was iterated over System.Collections.Generic.HashSet1+Enumerator[Barebones.MasterServer.ChatChannel].CheckState () System.Collections.Generic.HashSet1+Enumerator[Barebones.MasterServer.ChatChannel].MoveNext () Barebones.MasterServer.ChatModule.RemoveChatUser (Barebones.MasterServer.ChatUserExtension user) (at Assets/Barebones/Msf/Scripts/Modules/Chat/ChatModule.cs:95) Barebones.MasterServer.ChatModule.ChangeUsername (IPeer peer, System.String newUsername, Boolean joinSameChannels) (at Assets/Barebones/Msf/Scripts/Modules/Chat/ChatModule.cs:200) CharacterModule.HandleSelectCharacter (IIncommingMessage message) (at Assets/_Primary/_Scripts/Server/CharacterModule.cs:176) Barebones.Networking.PacketHandler.Handle (IIncommingMessage message) (at Assets/Barebones/Networking/Scripts/PacketHandler.cs:28) Barebones.MasterServer.ServerBehaviour.OnMessageReceived (IIncommingMessage message) (at Assets/Barebones/Msf/Scripts/Server/ServerBehaviour.cs:259)

I'll keep fiddling around with that.

As for the issues with the function, I only had two notable bugs.

  1. Upon username change, the main channel the chat uses is no longer the default channel, requiring that you rejoin it with a slash command (though it does properly remove the user and prevents doubling)
  2. Once a username has been changed, it cannot be changed again. This wouldn't be a problem unless you considered cases where the player may wish to switch characters without needing to relog.

Thank you again for the help on this matter, it's really helped push the concept to be easily workable with the framework. I'll fiddle around with this more when I get back to work.

alvyxaz commented 7 years ago

Whoops, sorry, that was a bit sloppy - I didn't test before sending it.

The iteration error is the downside of having publicly accessible collections. I'll send a fix in a few minutes. I just realized that channels will probably get destroyed when last user leaves, so re-joining them is not so straightforward

On Wed, Apr 12, 2017 at 11:39 AM, Specro777 notifications@github.com wrote:

Thank you very much for this method. I've implemented it, yet for whatever reason, it only worked once. I'm not sure what happened, but I'm heading to bed after I write this message and wanted to check in before I went, and while it was still fresh in my head.

As recommended, I called this new method after the character selection process is completed, inside the Character Module's handler function. At first, it worked, with a few bugs I'll write about below. The next few times however, I started getting this error:

InvalidOperationException: HashSet have been modified while it was iterated over System.Collections.Generic.HashSet1+Enumerator[Barebones. MasterServer.ChatChannel].CheckState () System.Collections.Generic.HashSet 1+Enumerator[Barebones.MasterServer.ChatChannel].MoveNext () Barebones.MasterServer.ChatModule.RemoveChatUser (Barebones.MasterServer.ChatUserExtension user) (at Assets/Barebones/Msf/Scripts/Modules/Chat/ChatModule.cs:95) Barebones.MasterServer.ChatModule.ChangeUsername (IPeer peer, System.String newUsername, Boolean joinSameChannels) (at Assets/Barebones/Msf/Scripts/Modules/Chat/ChatModule.cs:200) CharacterModule.HandleSelectCharacter (IIncommingMessage message) (at Assets/_Primary/_Scripts/Server/CharacterModule.cs:176) Barebones.Networking.PacketHandler.Handle (IIncommingMessage message) (at Assets/Barebones/Networking/Scripts/PacketHandler.cs:28) Barebones.MasterServer.ServerBehaviour.OnMessageReceived (IIncommingMessage message) (at Assets/Barebones/Msf/Scripts/ Server/ServerBehaviour.cs:259)

I'll keep fiddling around with that.

As for the issues with the function, I only had two notable bugs.

  1. Upon username change, the main channel the chat uses is no longer the default channel, requiring that you rejoin it with a slash command (though it does properly remove the user and prevents doubling)
  2. Once a username has been changed, it cannot be changed again. This wouldn't be a problem unless you considered cases where the player may wish to switch characters without needing to relog.

Thank you again for the help on this matter, it's really helped push the concept to be easily workable with the framework. I'll fiddle around with this more when I get back to work.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/alvyxaz/barebones-masterserver/issues/69#issuecomment-293511926, or mute the thread https://github.com/notifications/unsubscribe-auth/ADFAUzzrdcKzvJ4VxRd7eQcJQhb1OfgWks5rvI3IgaJpZM4MxQIi .

alvyxaz commented 7 years ago

Alright, here it is: https://gist.github.com/alvyxaz/5fd067df2b034022fd7f6fb02442b8a8

I wrote a quick test, and it seems to be working. Let me know if there are other issues ;)

On Wed, Apr 12, 2017 at 12:21 PM, Alvydas alvyxaz@gmail.com wrote:

Whoops, sorry, that was a bit sloppy - I didn't test before sending it.

The iteration error is the downside of having publicly accessible collections. I'll send a fix in a few minutes. I just realized that channels will probably get destroyed when last user leaves, so re-joining them is not so straightforward

On Wed, Apr 12, 2017 at 11:39 AM, Specro777 notifications@github.com wrote:

Thank you very much for this method. I've implemented it, yet for whatever reason, it only worked once. I'm not sure what happened, but I'm heading to bed after I write this message and wanted to check in before I went, and while it was still fresh in my head.

As recommended, I called this new method after the character selection process is completed, inside the Character Module's handler function. At first, it worked, with a few bugs I'll write about below. The next few times however, I started getting this error:

InvalidOperationException: HashSet have been modified while it was iterated over System.Collections.Generic.HashSet1+Enumerator[Barebones.Mas terServer.ChatChannel].CheckState () System.Collections.Generic.HashSet 1+Enumerator[Barebones.MasterServer.ChatChannel].MoveNext () Barebones.MasterServer.ChatModule.RemoveChatUser (Barebones.MasterServer.ChatUserExtension user) (at Assets/Barebones/Msf/Scripts/Modules/Chat/ChatModule.cs:95) Barebones.MasterServer.ChatModule.ChangeUsername (IPeer peer, System.String newUsername, Boolean joinSameChannels) (at Assets/Barebones/Msf/Scripts/Modules/Chat/ChatModule.cs:200) CharacterModule.HandleSelectCharacter (IIncommingMessage message) (at Assets/_Primary/_Scripts/Server/CharacterModule.cs:176) Barebones.Networking.PacketHandler.Handle (IIncommingMessage message) (at Assets/Barebones/Networking/Scripts/PacketHandler.cs:28) Barebones.MasterServer.ServerBehaviour.OnMessageReceived (IIncommingMessage message) (at Assets/Barebones/Msf/Scripts/S erver/ServerBehaviour.cs:259)

I'll keep fiddling around with that.

As for the issues with the function, I only had two notable bugs.

  1. Upon username change, the main channel the chat uses is no longer the default channel, requiring that you rejoin it with a slash command (though it does properly remove the user and prevents doubling)
  2. Once a username has been changed, it cannot be changed again. This wouldn't be a problem unless you considered cases where the player may wish to switch characters without needing to relog.

Thank you again for the help on this matter, it's really helped push the concept to be easily workable with the framework. I'll fiddle around with this more when I get back to work.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/alvyxaz/barebones-masterserver/issues/69#issuecomment-293511926, or mute the thread https://github.com/notifications/unsubscribe-auth/ADFAUzzrdcKzvJ4VxRd7eQcJQhb1OfgWks5rvI3IgaJpZM4MxQIi .

Specro777 commented 7 years ago

Thank you very much! Everything, thus far, has been working smoothly and as intended. No error, no complaints, perfect as a peach.

I'm currently on track now, so if I run into another issue related to this particular implementation I'll bring it up in this thread. The next thing I will likely be looking at will be dealing with storing player inventories on the database with an association with the character. I have an idea of how to implement this with a string, but that seems unwieldy and might be better suited for storing a list to the database, if that's possible.

RowellKataan commented 7 years ago

Still having a bear of a time getting profiles to work properly.
If possible, and time allows, a tutorial on how profiles are used would be wonderful. I'm not grabbing how/when information is being saved/updated when changes are made to the data.

Thanks!

RowellKataan commented 7 years ago

Thanks Angusmf for that very helpful addition to the Wiki, pertaining to Profiles. After going through some code, and applying some of your suggestions, Profiles are finally working as intended for me.