Using Xenko as a server


#1

I’ve seen several people asking about networking in Xenko. I know this is a feature that is on the roadmap but is a bit distant out. Everyone has been very helpful to me thus far and I wanted to try to give back. To that end I wanted to share some notes I discovered getting Xenko to run as a server and getting a Xenko game client to connect.

My goal was to use features of the engine such as the entity manager and collision systems etc. without the pieces I don’t need on a server such as Rendering, Audio, Input. I haven’t figured out yet how to strip that stuff out and will update this post as I do or as people comment.

I hope that I have included everything, and that you find what I did useful. There are still a few things I need to work out which I’ll try to point out, but this should be a starting point. If you have ideas of suggestions for how I could do things better that would be welcomed.

I would like to note that the following post helped me immensely starting out Ogre3D and Xenko

Tools used
Windows 10
Visual Studio 2017
Lidgren
Xenko 2.0.2.1
Top Down RPG Template for the client

Notes:

  • You will need to have the x86 and x64 folders from Xenko in your servers bin\Debug or bin\Release folder otherwise you will get an error loading libcore.dll. The location of these files is in

  • I created a project called GameServer and included Lidgren.Network

  • <Install_path>\Xenko\GamePackages\Xenko.2.0.2.1\Bin\Windows. For my purposes, I just copied and pasted them, but ultimately this should probably be a build step.

  • As discussed in the link above I inherited a class from Game, EntityManager(Though I don’t use this right now)

  • I am still not sure how to set the renderer to Xenko.Null. I saw this in a few places, but I can’t tell where to do that. The code I provide will create a GameWindow for your server, but it will be a black screen.

  • As noted before I wanted this to be a starting point which means the code can likely be cleaned up substantially. As I go about that, I can update this post.

  • This does not require anything from the GameStudio to work. It’s a regular C# (not portable) console project.

  • I have not done extensive testing. I have confirmed that the client will connect to the server and send and receive messages. I don’t know anything related to performance or any limitations doing this.

  • when Xenko does supports networking this work will likely be obsolete.

Server Code
Program.cs

internal class Program
{
    //This can be whatever you want    
    private static string ServerId { get { return "GameServer"; } }

    //Hardcoding a port for the client to connect to
    private statis Int ServerPort { get { return 14545; } }

    static int Main(string[] args)
    {   
        //This sample will only use DiscoveryRequest, however for actual
        //game data you will be using UnconnectedData
        NetPeerConfiguration lConfig = new NetPeerConfiguration(ServerId);        
        lConfig.EnableMessageType(NetIncomingMessageType.DiscoveryRequest);
        lConfig.EnableMessageType(NetIncomingMessageType.UnconnectedData);
        lConfig.Port = ServerPort;

        //Spin up the server
        NetServer lServer = new NetServer(lConfig);
        lServer.Start();

        //This is an Async Script which will be described later
        NetworkScript lScript = new NetworkScript();
        lScript.Server = lServer;
        lScript.ServerId = ServerId;
        using (var World = new MyGame())
        {
            //I would prefer to just call Scripts.Add but it appears
            //that Initialize is called in World.Run
            //When I tried to manually call it earlier I getting
            //exceptions
            World.MyScript = lScript;

            //Right now the server won't exit because you need
            //to implement the some sort of Exit Game functionality
            World.Run();
        }
        return 0;
    }
}

NetworkScript.cs

public class NetworkScript : AsyncScript
{
    //This enum would go in a shared data class to share with the client
    public enum ServerMessageType
    {
        ClientInput
    }

    //Server Created in Program.cs
    public NetServer Server { get; set; }

    //Id for that server
    public String ServerId { get; set; }

    public override async Task Execute()
    {
        while(Game.IsRunning) {
            NetIncomingMessage lIncoming;
            while ((lIncoming = Server.ReadMessage()) != null)
            {
                switch (lIncoming.MessageType)
                {
                    case NetIncomingMessageType.DiscoveryRequest:
                        Console.WriteLine(String.Format("Discovery Requested {0}",
                            lIncoming.SenderEndPoint.Address.ToString()));
                        Console.WriteLine("Sending response");
                        NetOutgoingMessage lResponse = Server.CreateMessage();
                        lResponse.Write(ServerId);
                        Server.SendDiscoveryResponse(lResponse, lIncoming.SenderEndPoint);
                        Console.WriteLine(String.Format("Response sent to {0}",
                            lIncoming.SenderEndPoint.Address.ToString()));
                        break;
                    //I'm not showing this in the sample but here is where I capture input data sent from the client
                    case NetIncomingMessageType.UnconnectedData:
                        switch ((ServerMessageType)lIncoming.ReadByte())
                        {
                            case ServerMessageType.ClientInput:
                                Console.WriteLine(String.Format("Recieved input from {0}", lIncoming.SenderEndPoint));
                                break;
                        }
                        break;
                }
            }
            await Script.NextFrame();
        }
    }
}

MyGame.cs and WorldEntityManager.cs

internal class MyGame : Game
{
    //Taken from other example for adding entities
    private WorldEntityManager EntityManager { get; set; }

    //This would be better if it didn't have to be passed in
    //at the moment I don't know how to go about that.
    public  NetworkScript MyScript { get; set; }

    public MyGame()
    {
        EntityManager = new WorldEntityManager(Services);
    }

    //Ensure that the NetworkScript is added to the Game
    protected override void Initialize()
    {
        base.Initialize();
        Script.Initialize();
        Script.Add(MyScript);
    }
}

internal class WorldEntityManager : EntityManager
{
    public WorldEntityManager(IServiceRegistry registry) : base(registry) { }
}

Client Code (This was created with the TopDownRpg template. I am only going to show the code that was relevant to making it talk to the server.)

TopDownRpgApp.cs

class TopDownRpg
{
    //This will be described later
    private static GameClient Client { get; } = new GameClient();

    static int Main(string[] args)
    {
        Client.Initialize("LocalHost", 14545);
        using (var game = new Game()) {
            //In order to use the portable architecture of Xenko
            //This is how you can add a service.
            //
            //This does mean any other platform you need to support
            //Will also need to implement an INetworkInterface (Described later)
            //But, it does allow you to maintain portability
            game.Services.AddService(typeof(INetworkInterface), Client);
            game.Run();
        }

        return 0;
    }
}

GameClient.cs

internal class GameClient : INetworkInterface
{
    public GameClient()
    {
        IsInitialized = false;
        Client = null;
        Server = null;
    }

    public NetClient Client { get; private set; }
    public bool IsInitialized { get; private set; }

    private byte AppId { get; set; }
    private IPEndPoint Server { get; set; }

    public bool Initialize(string xServer, int xPort)
    {
        NetPeerConfiguration lConfig = new NetPeerConfiguration(AppId.ToString());
        lConfig.EnableMessageType(NetIncomingMessageType.UnconnectedData);
        lConfig.EnableMessageType(NetIncomingMessageType.DiscoveryResponse);

        Client = new NetClient(lConfig);
        Client.Start();
        Server = new IPEndPoint(NetUtility.Resolve(xServer), xPort);

        Client.DiscoverKnownPeer(Server);

        //I used this to await confirmation that the server connects.
        //In reality, there should be some sort of timer because this loop
        //won't end until the server responds.
        NetIncomingMessage lMessage;
        while (!IsInitialized) {
            while ((lMessage = Client.ReadMessage()) != null) {
                switch (lMessage.MessageType) {
                    case NetIncomingMessageType.DiscoveryResponse:
                        IsInitialized = true;
                        break;
                }
            }
        }

        return IsInitialized;
    }

    //The type passed in would come from the shared message enum
    //that was shown in the server code.
    //To simplify the sample I didn't include the shared code classes
    public void SendMessage(int xType)
    {
        if (!IsInitialized) {
            throw new Exception("GameClient hasn't been initialized");
        }

        NetOutgoingMessage lMessage = Client.CreateMessage();
        //Message Type
        lMessage.Write((byte)xType);

        //Who sent it
        lMessage.Write(AppId);

        //Extra information...

        //Send the data
        Client.SendUnconnectedMessage(lMessage, Server);
    }
}

INetworkInterface.cs

//This needs to be defined in the TopDownRpg.Game(Portable)
public interface INetworkInterface
{
    void SendMessage(ServerMessageType xType);
    //This should also be the interface that collects
    //server messages and feeds them back into the game
    //I've not included that in this sample because
    //I haven't ironed out how I want to do that.
}

PlayerInput.cs

//This is the class generated by Xenko
//I have replaced the generated code with "..." so you will want to merge this code
public class PlayerInput : SyncScript
{
    private INetworkInterface NetworkInterface { get; set; }
    ...
    
    public override void Update()
    {            
        INetworkInterface lNetworkInterface = 
            Game.Services.GetService(typeof(INetworkInterface)) as INetworkInterface;
        if(lNetworkInterface == null) {
            throw new Exception("Unable to find network interface");
        }
        
        if (Input.HasMouse)
        {
            ...
            // Character continuous moving
            if (isMoving)
            {
                ...
                //Here is where I tested sending a message from the game to the server
                lNetworkInterface.SendMessage(ServerMessageType.ClientInput);
            }
        }
        ...
    }
}