LANSessionManager

Feb 21, 2012 at 8:37 AM
Edited Feb 22, 2012 at 8:28 AM


Hi,

I was going to try out Local Network play in my game so I added this:

SunBurn.AddManager(new LidgrenSessionManager(SunBurn));
...
var properties = new SessionProperties();
Application.SunBurn.GetManager<ISessionManager>(true).CreateLanSession(2, null); 
...

private void OnSessionCreated(object sender, EventArgs e)       
{            
    SessionManager.CurrentSession.Started += OnSessionStarted;      
    SessionManager.CurrentSession.StartSession(); 
} 


But when it reaches StartSession(), it kinda hangs the game and OnSessionStarted is never called. Why is that? Is there something I'm missing?

Maybe you can give me a short explanation on how local network play works, and also how do I sync objects over network?

Thanks, Kosmo 

EDIT:
I get this debug output:
Server Status changed to RespondedConnect
Server Status changed to Connected 

EDIT 2:
I tried the latest changeset and same problem but more debug output. Also I looked through your blog and read about PlayerAgent and NonPlayerAgent, I guess that's what I use to sync stuff over network. 

Coordinator
Feb 22, 2012 at 8:56 AM

It's odd that the OnSessionStarted never gets called. I'll try to see what may be going on.

Just so that you know how it works, Session and its inherited classes implementing custom networking schemes have a set of states: Lobby, Starting, Playing, Ended and Closed.

First, when you create the Session instance, it moves its state to Lobby waiting for you to start.
When you make a call on SessionManager.CurrentSession.StartSession(), it actually changes its state to Starting and raises the Starting event. This is an important place to initialize your game session as it'll listen for all PlayerAgent and NonPlayerAgent instances created and associated with a SceneEntity so that they get created automatically on all clients and synchronized. You may also want to place in here any content loading.
Once synchronized, the Session will automatically move from Starting to Playing and raise the Started event letting you know that from now on, all PlayerAgent and NonPlayerAgent instances, their behaviors and commands will get processed.
Calling SessionManager.CurrentSession.EndSession() will change the Session state to Ended and raise the Ended event. Once the Ended delegates are executed (if any), the Session state moves to Lobby again waiting for a new game session start.
Finally, you can close the Session calling the SessionManager.CloseSession() method which will change the Session state to Closed, dispose all connections and network resources for you.

Now, on the synchronization question: How it works behind might be rather complex but it eases a lot how you code your game logic.

Basically, as soon as you called the Session.StartSession(), every PlayerAgent or NonPlayerAgent instance that will be created using the Session.CreatePlayerAgent(), Session.CreateNonPlayerAgent() or their generic versions will automatically register themselves into the session instance.

At this time, the Session will look at all their Behaviors and Commands and make sure they share the same internal ID acrross all connected clients.
When you then attach them to a SceneEntity or SceneObject instance, it'll register their IDs and also synchronize them accross all connected clients.

This simple process allows you to code your game logic without actually taking care of Server/Client execution because it's handled for you by the framework.

PlayerAgent Behaviors and Commands will get processed on each clients.
NonPlayerAgent Behaviors and Commands will only get processed on the server to avoid cheating.

All you actually have to do is code your PlayerAgent and NonPlayerAgent Behaviors and Commands, attach them to your SceneEntity or SceneObject instances and you're done.

Now, it'll be up to you to synchronize your SceneEntity or SceneObject properties and this is done quite easily with Commands.

If you want to synchronize for instance a SceneObject.World property accross all clients, you'll simply add a new PlayerAgent or NonPlayerAgent to it and a Behavior such as:

public class MyCustomBehavior : Behavior
{
     public MyCustomBehavior()
     {
           Frequency = ExecutionFrequency.OncePerFrame; // we tell the system to execute all Commands in this behavior only once per frame (defaults to every frame)

           // we now add the command which will handle the synchronization
           AddServerCommand(
                 RetrieveWorldOnServer,         // this is the method executed on the server 
                 ApplyWorldOnClients,             // this is the method executed on all clients when receiving data from the above server method
                 typeof(Matrix),                       // this tells the framework which Type will be used for the data exchanged between the server and clients methods
                 DatatTransferOptions.None);   // this tells the framework how reliable you want the data transfer to occur
           
     }

     private object RetrieveWorldOnServer(Command command)
     {
           // we simply retrieve the SceneObject this Behavior's Agent is attached to and return its World Matrix value
           return Agent.ParentObejct.World;
     }

     private void ApplyWorldOnClients(Command command, object networkValue)
     {
           // we first need to cast the network value to a Matrix
           Matrix world = (Matrix)networkValue;

           // and we simply set it to the SceneObject World property
           Agent.ParentObject.World = world;
     }
}

 

This approach has a lot of advantages:

First, you code your game logic as if it was running locally (but actually it is executed wherever it actually needs to be).
Second, you can create custom Behavior classes and share them across multiple Agents either they are PlayerAgent or NonPlayerAgents. The above example could be used to synchronize SceneObject world transforms for player or AI controlled objects.
Third, you still have a total control how it gets executed with several options: Conditionned based Behaviors or Commands, Execution frequency on Behaviors and Commands to optimize your AI CPU and Network bandwidth usages efficiency, and you may have Local only, Local & Server, Local Server & All Clients, Server only, Server & All Clients commands depending on what you need to achieve ;)
Fourth, once you made your game work using a Single player Session, making it work on a Multiplayer session is way easier since you may only have to change a few AddLocalCommand(...) to AddServerCommand(...) or AddLocalAndServerCommand(...) method calls instead.

The concepts may be complex to get at first but once you get the logic, it's really easy to code your game logic.

When Ace on Steroids gets done and I move on the video tutorials, there will be an entire video dedicated to this approach explaining how it works and what you can do with it.

After that, I may work on a dedicated tutorial to show how to move Ace on Steroids from a Single Player only to a Multi player session.

Feb 22, 2012 at 9:25 AM

Wow, thank you so much for that information. Really detailed and I understand better how it works now. :)

But is there something I have to do before StartSession() or something?
Also I only have one PlayerAgent which is a class named Player that Inherits from PlayerAgent, it's Empty, does it need any data? Also, do I need to have at least one command or behavior or something like that?

 I was thinking I could start my game normally with a single instance but instead of SinglePlayer I would use LanSession and it would work the same but with no clients connected.

Also when calling StartSession I see that it gets stuck in Lobby and OnSessionStarted is never called. Also as I mentioned earlier, something weird happens, it doesn't hang, but the fps goes down to like 1FPS or something like that.

Thanks again for helping me! 

Coordinator
Feb 22, 2012 at 1:06 PM

If you can share with me a simple project that reproduces the issue, it may help.

Normally, you could use a LAN Session as if you'd be playing a SinglePlayer one but I need to double check that it actually works ;)
Out of my head, I potentially put something up that would avoid starting a session if it has less than 2 players connected (which would be wrong) but it should normally at least throw an exception then...

I'll check that out...

Feb 22, 2012 at 3:05 PM

I'll see if I can make a minimal sample project for you. :)

Coordinator
Feb 23, 2012 at 9:40 AM

Ok, I believe I found out what was going wrong.

I had a check when creating a LAN session which would test if more than 1 player was identified before starting it which wasn't the right way to do ;)

The latest changeset should now solve your issue and let you play with it ;)

Feb 23, 2012 at 10:14 AM

Great!
Thanks a lot for the fast fix!
I'm going to try it out later today, I'll get back to you if I notice something more. :) 

Feb 24, 2012 at 8:18 AM

Hi!

I tried the changeset 12700. But it still doesn't seem to work.
I made a tiny project that only creates a network session for you so you can see for yourself. It doesn't draw anything and as soon as StartSession() is called, it drops my framerate to around 3-4 FPS and OnSessionStarted is never called.

Here check it out: http://www.winterspring.se/NetworkTest.zip

:) 

Coordinator
Feb 24, 2012 at 10:28 AM
Edited Feb 24, 2012 at 10:29 AM

Ok, got it and I believe this is due to the way you're actually initializing your session.

First, I see that you have setup your project to use SunBurn 2.0.18 which isn't yet supported by the framework. Use 2.0.17 instead or wait a few days for my next changeset and probably next stable release.

In the provided example, in the EditorGameState.cs file, you should change the OnLoadingCompleted() override to the following:

protected override void OnLoadingCompleted()
{
    base.OnLoadingCompleted();

    var sessionManager = Application.SunBurn.GetManager<ISessionManager>(true);

    //sessionManager.PlayerLogin += delegate { sessionManager.EndPlayerIdentification(); };
    //sessionManager.BeginPlayerIdentification();
    sessionManager.IdentifyPlayer(Application.Input.PlayerOne);

    Application.SunBurn.GetManager<ISessionManager>(true).CreateLanSession(2, null);

    //PreLoad(this);
}

First, you shouln't call PreLoad(this) in your OnLoadinCompleted() because it'll actually ask the GameState to Load again creating an infinite loop. You'd rather call it at the end of your Initialize() method override.

Second, if you call the BeginPlayerIdentification() method, you don't need to call the IdentifyPlayer() one. BeginPlayerIdentification() is a helper method that enables the framework to look for Start or A button presses to identify users (internally calling the IdentifyPlayer() method). It lets you implement the "Start screen" feature present in most games without having to worry about it.

You may therefore choose to manually call identify players or use the BeginPlayerIdentification method. In this case, I went for the manual identification.

It should then lets you play. 

 

Feb 24, 2012 at 10:37 AM

Ok, thanks!

The PreLoad(this); thing is a mistake, I combined some code and it got a bit messed up.

I don't know if it's cause I use 2.0.18 but I changed the code like you said and it's the same thing. Maybe I should try the .17 version.

Out of topic:
Btw. Is it Philippe I'm talking to? 
Do you work as a game programmer or is this just for fun? Would you like to work on a game? 

Coordinator
Feb 24, 2012 at 12:27 PM

Hmmm... That's strange you're still getting the issue... I'll have a look on it as I'm currently porting IGF to support SunBurn 2.0.18.7.

I actually setup my Ace on Steroids project to use LidgrenSessionManager and create a Lan session instead of using LocalSessionManager and create a SinglePlayerSession and it worked just fine. Must be something I'm missing here...

I'll look onto it and see if I can come up with something.

To reply to your out of topic question:

Yes, this is Philippe you're talking to ;)

I'm doing this just for fun. I once worked in the game industry (hoping to get back to it someday) but in the Marketing field (I used to be Internet Director at Monte Cristo games when working on Cities XL).

I'm currently seeking for a job so open to positions ;)

Feb 24, 2012 at 12:33 PM
Edited Feb 24, 2012 at 12:35 PM

Do you use any kind of instant messaging client or some way that I can contact you instead of using this board? :)
To talk about working on games and what I have in mind, to see if you're interested. 

EDIT: I'll use the Contact Form on your indiefreaks blog to send you my e-mail and details.

Coordinator
Feb 24, 2012 at 12:58 PM
KosmoKeLi wrote:

Do you use any kind of instant messaging client or some way that I can contact you instead of using this board? :)
To talk about working on games and what I have in mind, to see if you're interested. 

EDIT: I'll use the Contact Form on your indiefreaks blog to send you my e-mail and details.


Received your email and replied back ;)

Feb 28, 2012 at 8:27 AM

Hi!

I looked through your Lidgren stuff and I can't seem to find that it sets SessionState.Playing or calls OnSessionStarted.

Is the stuff missing perhaps? Or is it done in some core class? 

Coordinator
Feb 28, 2012 at 12:14 PM

I believe it is since SessionState.Playing gets changed in LidgrenSession.Server.cs file line 122 when the server gets notified that all clients are synchronized.

It then sends a network message which says the server SessionState changed to Playing. The client interprets this message in LidgrenSession.Client.cs file line 399 which therefore calls OnStarted() in Session.cs file line 274 which in turns raises the Started event which you registered with previously to calling SessionManager.CurrentSession.StartSession();

I believe you're not getting the Session start event because of the way it is initialized first. I don't have the time right now to go deeply in what could go wrong on your side since I need to get Ace on Steroids out asap to move on some other important tasks.

I'll see when I have some time if I can set up a short sample out of your provided project for you to start with.