Friday, June 17, 2011

Foray into Photon - Part 10 - Creating our Disconnected state and wiring it into the game

The first thing we need to do is set up our Game.cs calls to our state so the state can handle them, then we will create our disconnected state and tell Game.cs to use it going forward. I have updated all the IPhotonPeerListener functions. They look exactly like the ones Exit Games uses only without the debug logging:


    public void DebugReturn(DebugLevel level, string message)
    {
        if (this.listener.IsDebugLogEnabled)
        {
            this.listener.LogDebug(this, string.Concat(this.avatar.Id, ": ", debug));
        }
    }


    public void EventAction(byte eventCode, Hashtable photonEvent)
    {
        stateStrategy.OnEventReceive(this, (EventCode)eventCode, photonEvent);
    }


    public void OperationResult(byte operationCode, int returnCode, Hashtable returnValues, short invocId)
    {
        try
        {
            stateStrategy.OnOperationReturn(this, (OperationCode)operationCode, returnCode, returnValues);
        }
        catch (Exception e)
        {
            listener.LogError(this, e);
        }
    }


    public void PeerStatusCallback(StatusCode statusCode)
    {
        try
        {
            stateStrategy.OnPeerStatusCallback(this, returnCode);
        }
        catch (Exception e)
        {
            listener.LogError(this, e);
        }
    }


    public void Update()
    {
        stateStrategy.OnUpdate(this);
    }


    public void SendOp(OperationCode operationCode, Hashtable parameter, bool sendReliable, byte channelId)
    {
        stateStrategy.SendOperation(this, operationCode, parameter, sendReliable, channelId);
    }


As you can see, each place we normally would have done something because we received a message through the peer, we now pass on to the state. The state will then determine if we need to process the message and if so, how.

Now to create our Disconnect state. It is a very simple state, it does nothing when asked to send an operation or to update. The other functions all call the error logs we created earlier.


using AegisBornCommon;


public class Disconnected : IGameState
{


    public static readonly IGameState Instance = new Disconnected();


    public GameState State
    {
        get { return GameState.Disconnected; }
    }


    public void OnEventReceive(Game gameLogic, AegisBornCommon.EventCode eventCode, System.Collections.Hashtable eventData)
    {
        gameLogic.OnUnexpectedEventReceive(eventCode, eventData);
    }


    public void OnOperationReturn(Game gameLogic, AegisBornCommon.OperationCode operationCode, int returnCode, System.Collections.Hashtable returnValues)
    {
        gameLogic.OnUnexpectedPhotonReturn(returnCode, operationCode, returnValues);
    }


    public void OnPeerStatusCallback(Game gameLogic, ExitGames.Client.Photon.StatusCode returnCode)
    {
        gameLogic.OnUnexpectedPhotonReturn((int)returnCode, OperationCode.Nil, null);
    }


    public void OnUpdate(Game gameLogic)
    {
        // Do nothing on updates because we are disconnected.
    }


    public void SendOperation(Game gameLogic, AegisBornCommon.OperationCode operationCode, System.Collections.Hashtable parameter, bool sendReliable, byte channelId)
    {
        // Do not send operations because we are disconnected.
    }
}


So now we have our disconnected state and we are almost back to where we were. It feels like a ton of work just to be right back where we started, but this is a very good setup. The time we take setting up this project the first time, means less time will be spent when we need to add new states or messages.

Next up we will get WaitingForConnect and CharacterSelect states up and running!

Foray into Photon - Part 9 - Adding IGameState and IGameListener to the game.

So we have our game states and our listener ready, but we don't reference them anywhere. We are now going to make some modifications to the Game.cs file that will make these calls.

First we add 2 new properties and a new constructor:


    private readonly IGameListener listener;


    private IGameState stateStrategy;


    public Game(IGameListener listener)
    {
        this.listener = listener;
        this.stateStrategy = Disconnected.Instance;
    }

Next we go into Login.cs and modify our Start function to say:

        _engine = new Game(this);

Now we can add some error handling to our Game.cs file:

        public void OnUnexpectedEventReceive(EventCode eventCode, Hashtable eventData)
        {
            this.listener.LogError(this, string.Format("unexpected event {0}", eventCode));
        }

        public void OnUnexpectedOperationError(OperationCode operationCode, ErrorCode errorCode, string debugMessage, Hashtable hashtable)
        {
            this.listener.LogError(this, string.Format("unexpected operation error {0} from operation {1} in state {2}",  errorCode, operationCode, this.stateStrategy.State));
        }

        public void OnUnexpectedPhotonReturn(int photonReturnCode, OperationCode operationCode, Hashtable hashtable)
        {
            this.listener.LogError(this, string.Format("unexpected return {0}", photonReturnCode));
        }

This gets us all caught up to where we need to be so we can begin to implement our states. We will cover our basic disconnected state and the functions in Game.cs that we need to modify in our next segment. After we get our states set up I will be posting the code to github so that you can follow along if needed.

Foray into Photon - Part 8 - IGameListener

So we have our interface defining our GameStates but I said we are missing something. Exit Games created another interface called the IGameListener. It's job is to listen to events from the game and update the player based on those events.


using System;
using ExitGames.Client.Photon;


public interface IGameListener
{
    bool IsDebugLogEnabled { get; }


    void LogDebug(Game game, string message);


    void LogError(Game game, string message);


    void LogError(Game game, Exception exception);


    void LogInfo(Game game, string message);


    void OnConnect(Game game);


    void OnDisconnect(Game game, StatusCode returnCode);


}

We can then add this interface to our Login.cs as an interface and inherit these functions. These are how they look in Login.cs

    #region Inherited Interfaces

    #region IGameListener
    public bool IsDebugLogEnabled
    {
        get { return true; }
    }

    public void LogDebug(Game game, string message)
    {
        Debug.Log(message);
    }

    public void LogError(Game game, string message)
    {
        Debug.Log(message);
    }

    public void LogError(Game game, Exception exception)
    {
        Debug.Log(exception.ToString());
    }

    public void LogInfo(Game game, string message)
    {
        Debug.Log(message);
    }

    public void OnConnect(Game game)
    {
        Debug.Log("connected");
    }

    public void OnDisconnect(Game game, StatusCode returnCode)
    {
        Debug.Log("disconnected");
    }

    #endregion

    #endregion

This allows us to log any errors that occur during the course of our program running. Next time we will connect our game state and listener into our game. Stay Tuned

Foray into Photon - Part 7 - IGameState

Last time we left off talking about the state pattern, but my title said strategy/state pattern. The reason is because the two patterns are closely related to each other. After reading the Design Patterns book, also called the "Gang of Four" or GoF book because of the 4 authors that wrote it, I determined that they are using the State pattern. The idea behind the State pattern is that the program will constantly change states. The idea behind the Strategy pattern is that you will use one strategy or algorithm and it won't change very often if at all unless the user or developer wants it to change. Most people will give the idea of a sort as being a strategy, the developer will chose which sorting strategy they want to use. A state however, changes frequently, hence why I call these states rather than strategies. 

Now on to the code. You will want to open your Unity3d project and create a folder called GameStates.

First we need to create our list of states as an enumeration. It's simply a file that contains a simple enum:

public enum GameState
{
    Disconnected,
    WaitingForConnect,
    CharacterSelect,
    CharacterCreate,
    WorldEntered
}

Next we are going to create our interface IGameState. This does nothing more than define the functions that all game states will use. Just like the Exit Games version, we will have a few functions, OnEventReceived, OnPeerStatusCallback, OnOperationReturn, OnUpdate, and SendOperation. The code for this class looks like this:

using System.Collections;
using AegisBornCommon;
using ExitGames.Client.Photon;

public interface IGameState
{
    GameState State { get; }

    void OnEventReceive(Game gameLogic, EventCode eventCode, Hashtable eventData);

    void OnOperationReturn(Game gameLogic, OperationCode operationCode, int returnCode, Hashtable returnValues);

    void OnPeerStatusCallback(Game gameLogic, StatusCode returnCode);

    void OnUpdate(Game gameLogic);

    void SendOperation(Game gameLogic, OperationCode operationCode, Hashtable parameter, bool sendReliable, byte channelId);
}

With this in place we are going to go ahead and end this segment. There are other things we need in place before we can implement these states. We need to modify the Game.cs file and create our game listener which will be a mono behavior. Stay Tuned.

Thursday, June 16, 2011

Foray into Photon - Part 6 Strategy/State Pattern

So far I've really only been covering the code and how to build up to where the demo is. I'm going to take a step back to my original post and discuss a few things on why they were done.

As I stated toward the end of my post on SmartFoxServer, there are a few patterns or philosophies I try to adhere to. One of them is to keep only what a class needs to do in the class that does it. The folks at Exit Games follow this same philosophy. They have an Operator class which only contains the data that operations need. The same holds for Events. The only place you will find a send is in the PhotonPeer and the only place you get events is in the PhotonPeerListener.

Would you be able to agree with me if I told you that we want to do the same with how we handle messages? I hope so. In the demo on the client portion you will find a series of classes in the GameStateStrategies. They all base themselves off of the IGameLogicStrategy. This class follows the Design Patterns called State and Strategy. If you are serious into programming and want to make headway into being a better programmer I would recommend you pick up Design Patterns: Elements of Reusable Object-Oriented Software. This book shows you 23 design patterns you will frequently find in programming and one of these is the Strategy pattern.

The book defines the state pattern like this: "Allow an object to alter its behavior when its internal state changes. The object will appear to change its class." Lets take a look at it in terms of how Exit Games used it. The object that appears to change its class is the Game.cs class. You have several behaviors that all do the same thing: Disconnected, WaitingForConnect, Connected, and WorldEntered. It appears to change its class because based on which state its in, is how the game reacts when it gets an operation response or an event, checks its status in the call back, or sends an operation. In disconnected or waiting for connect it doesn't send operations, and it reports errors when it receives events and operationresponses. But connected is able to listen for a few things like when the world is entered and the worldEntered state is there for when they are totally in the game.

So lets think about our what we did in our SmartFoxServer program. We would have several states. Disconnected for when they haven't clicked login. WaitingForConnect would be used when they have hit the login button but we aren't talking to the server. CharacterSelect would be used when they are at the character select screen. CharacterCreate would be used when they are on the character creation screen. Lastly, WorldEntered would be used when they have selected a character and are fully in the game. Each of these states would handle only specific messages and would report all others as errors. So when you are on world entered you would report an error if you got the operation stating that you are creating a character. Also you couldn't send an operation to perform a craft while waiting for the connection.

Next time we will create a set of classes: IGameState that will contain the same as the IGameLogicStrategy. Then we will set up the barebones for these states and modify our game to use these states.

Friday, June 10, 2011

Foray into Photon - Part 5 Operation Dispatcher

So Photon has some really nice features built into it for us to use. They have pre-built encryption, the Operation Dispatcher, and Operations and Events to base our code off of. This segment we will be covering the Operation Dispatcher. Its job is to use reflection to determine what functions get which operations. This reduces the amount of work compared to what we had to do with SmartFoxServer. At first it seems like the same thing. SmartFoxServer allows us to add handlers which then pass us the user and the datapacket. This is exactly the same thing Photon does except that we only have to add the class that holds each of the handler functions instead of adding a new handlers for each message. Lets take a look. All of this code will be going into AegisBornPeer. First we add 2 new properties to our class:


        private static readonly OperationMethodInfoCache Operations = new OperationMethodInfoCache();


        private readonly OperationDispatcher _dispatcher;

The cache provides all the function lookup data and the dispatcher makes the calls and passes the information necessary.

Next we need to add a static constructor that will instantiate our cache and add all the functions that belong to each class that will handle our operations:

        static AegisBornPeer()
        {
            Operations = new OperationMethodInfoCache();

            try
            {
                Operations.RegisterOperations(typeof(AegisBornPeer));
            }
            catch (Exception e)
            {
                ErrorHandler.OnUnexpectedException(Operations, e);
            }
        }

The cool thing about this setup is that you can create handlers in groups. I can create a class called CraftingHandler and put all crafting operations in it. We'll have to explore this option later.

Next we will add a the dispatcher creation code to the AegisBornPeer(PhotonPeer) constructor immediately above the SetCurrentOperationHandler call:

            _dispatcher = new OperationDispatcher(Operations, this);

The last thing we need to do is call the dispatcher when we receive an Operation. So drop down to our "We get signal" call and add the following above it:

            OperationResponse result;
            if (_dispatcher.DispatchOperationRequest(peer, operationRequest, out result))
            {
                return result;
            }

What this does is attempt to find a function that has our operation code to handle the operation. if it doesn't find it, it will call our "We get signal" return. If it does handle it, it returns the result of the function call. Thats all there is to it. Now we can start adding operations and handling them without adding much more code.

So that's it for this segment. The next segment we are going to take a look at how the demo handles connection states. This is a piece of the client to handle specific things and will lead us into the server version of the same thing.

Foray into Photon - Part 4 Common

So last time we left off we can connect to the server, send it an operation and get a response back. Originally I said we were going to look at the OperationDispatcher but instead we a taking a small detour to begin getting set up for it. First we want to create a new class library project in the same solution as our server code. Just right-click on the AegisBorn solution and add a new project. I called mine AegisBornCommon. In this project we will create things that are common to the server and the client. Once the project is created, go to the server project and add a reference to the project AegisBornCommon.

Now that we have our common class library ready we are going to create several enumerations. These will help us determine what data we are trying to find or put into return values. I'm going to run through each of the files real quick. You will want to create 4 new items, use the class wizard and call them ErrorCode, EventCode, OperationCode, and ParameterCode. Here is the code for those 4 classes:


    public enum ErrorCode
    {
        ///


        /// The ok code.
        ///

        Ok = 0,


        ///


        /// The fatal.
        ///

        Fatal = 1,
    }



    public enum EventCode : byte
    {
    }




    public enum OperationCode : short
    {
        ///


        /// The nil (nothing).
        ///

        Nil = 0,
    }



    public enum ParameterCode : short
    {
        ///


        /// The error code.
        ///

        ErrorCode = 0,


        ///


        /// The debug message.
        ///

        DebugMessage = 1,
    }

If you notice, these are exactly the same as the ones in the MMO Demo. ErrorCodes are so we know if anything went wrong. EventCodes are so we can figure out which event was sent to us by the server, it also makes it easy for the server to know which event it is sending. OperationCode is for operations, much like events, it is so that we can know which operation the client wants to send to the server and so that the server can quickly figure out what it needs to do. Lastly the ParameterCode is for the OperationRequest and OperationResponse parameters/returnValues.

With these 4 files in place you will want to build this project and then copy the dll it puts into bin/Debug into your Plugins directory of your Unity3d project. Each time you build it you will need to copy it unless you know how to modify the project settings to tell it to put it in there directly.

Next time we will set up our system to use the OperationDispatcher. This saves our server from having the ugly switch statement that you would have to modify any time you add a new message and anything that keeps us from doing more work is a good thing.