Sample

Overview

The Asmodee.net Unity SDK comes with a sample showing the full extent of the possibilities of the Asmodee.net Scalable Server.

This SDK provides an architecture which decouples the view from the logic.

View

The view contains only the visual elements of the game. A Unity scene contains a view. The view references all interactive elements present on the screen.

The dice is a visual prefab. Clicking the dice fire an event to the view. The view will then tell the logic that the dice has been clicked.

The view communicates directly with the instance of the logic, then it is notified of the action by a .Net events.

Logic

The logic manages the business of the game. In the sample, it’s the logic that determines the status of a clicked dice. Indeed, the dice can be selected / deselected in certain conditions (round #2 of the player).

The logic will then notify the view that a dice has been selected. Selecting a dice whether for the local player, the AI or a remote player is unified.

Domain (game)

The domain contains all the business entities of the game. The dice’s class is part of the domain. In a game with cards, instances of the cards would be part of the domain.

GameState, Events and Messages are part of the domain too. The domain is transverse to the view and logic.

Domain (Scalable Server)

Scalable Server domain contains the objects returned by the server. This may be the entity Player or the result of a query in Scalable Server.

Documentation of the entire domain is located here:

http://ssdoc.asmodee.net/request-reference.html

Diagram

class_diagram

Starting the sample

Setup

First of all, it’s very important that you override the default credentials of the sample to use you own credentials. Failing to do so will most likely lead to deconnections and errors, as the Scalable Server won’t allow the same player to be logged in more than once at a time.

So, in the Unity Editor, find the user assets in the Project tab: they are located in Assets > Data > TestUsers. They are called Alice, Bob and Carol (Don’t mind the *TUA files, they are for Unit Testing). Replace the Login and Password with yours. As far as testing the Scalable Server Sample goes, these are the only values that need to be changed.

_images/change_credentials.png

Then in the Game window, set the Screen Ratio to 16:10 or 16:9

_images/Screen_Ratio.png

Running the sample

Thanks to the PlayeReal menu, you can build and launch 2 instances of the sample and launch Unity editor at the same time. This is very useful to test multiplayer features.

  • The Unity instance will automatically connect with Alice’s account, the scene 1_MainMenu will be open.
  • The first window will automatically connect with Bob’s account and will be placed in the upper left corner.
  • The second window will automatically connect with Carol’s account and will be placed nearby Bob’s window.

You can resize the Unity window in order to view the 3 instances of the Sample.

Note: automatic placement of the window is only possible on Windows.

_images/Sample_3_Instances.PNG

Sample build Settings

After importing the package, the 3 scenes of the sample should be added in Build Settings.

For each of the 3 scenes:

  1. Open the scene from the directory /Assets/Samples/PlayReal/Scenes/
  2. Open the File > Build Settings window
_images/FileMenu_BuildSettings.png
  1. Click the [Add Open Scenes] button
_images/BuildSettings.png
  1. Close the window and go to the next scene

Account creation

The sample uses 3 Asmodee.net user accounts. To avoid conflicting with other developers using the sample at the same moment, you must provide a login and password for each of these users.

To create Days of Wonder user accounts please go to the Days of Wonder’s website

By convention, we named our 3 users Alice, Bob and Carol (A, B, C)

Management of test accounts in the sample is done through User data assets stored in the directory /Assets/Data/TestUsers/

TestUsers_Directory

You can now create 3 User data assets in that directory

TestUsers_Creation

Then simply fill in the fields of the user:

TestUsers_Fields

To use these accounts, just open the scene 1_MainMenu

MainMenu_Scene

Select View object in the hierarchy

MainMenu_View_Hierarchy

Then drag and drop the data assets created in the Users field instead of Alice, Bob and Carol

MainMenu_View_Inspector

When you start the scene 1_MainMenu, accounts used will be those that you created.

Obviously, this automatic connection system is only useful in the context of local tests and should not be deployed in production.

The User data assets are also used for the unit test.

_images/UnitTest_Sample_Directory.PNG

Just drag’n drop the user in the Alice, Bob and Carol fields of the unit test source code file int the inspector

_images/UnitTest_Sample_Inspector.PNG

LobbyPlayersView

image4

The lobby is accessible in the scene 2_Lobby by clicking the button image5

The lobby is divided into 2 views and 3 logics.

LobbyPlayersView that displays the chat and the lists of online / friends / ignored players.

This view contains the ChatLogic which handle chat and LobbyPlayersLogic which handle the players lists.

LobbyGameView that displays game creation, opened and ongoing games

This view contains the LobbyGameLogic which handle all these features thanks to the ServerConnection.

Initialization

private void Init()
{
    //---> Logics instanciations
    ChatLogic = new ChatLogic(NetworkManager.Instance.ServerConnection, PersistenceManager.Instance.Persistence);
    LobbyPlayersLogic = new LobbyPlayersLogic(NetworkManager.Instance.ServerConnection, PersistenceManager.Instance.Persistence);

    //---> Events subscriptions
    ChatLogic.ClientChatEvent += ChatLogic_ClientChatEvent;
    LobbyPlayersLogic.RefreshLobbyPlayerListEvent += LobbyLogic_RefreshLobbyPlayerListEvent;

    //---> Logics initialization
    LobbyPlayersLogic.Init();
    ChatLogic.Init();
}

Thanks to events, the view will be notified when:

  • A chat message is coming : ChatLogic.ClientChatEvent
  • The players lists must be refreshed : LobbyPlayersLogic.RefreshLobbyPlayerListEvent

Access to the lobby

Entering the lobby

image6

To enter the lobby LobbyPlayersLogic.EnterLobbyRequest is called.

ServerConnection
Send request
ServerConnection.EnterLobbyRequest();
Reception event
ServerConnection.LobbyEnteredEvent += ServerConnection_LobbyEnteredEvent;

private void ServerConnection_LobbyEnteredEvent(LobbyEnteredRequest lobbyEnteredRequest)
{
    [..]
}

Exiting the lobby

image7

The player exit the lobby when an other view is displayed.

It is triggered by the call of LobbyPlayersView.OnDestroy (automatically by Unity). The LobbyPlayersLogic is then destroyed, resulting in the call of ServerConnection.ExitLobbyRequest.

ServerConnection
Send request
ServerConnection.ExitLobbyRequest();
Reception event
ServerConnection.LobbyExitedEvent += ServerConnection_LobbyExitedEvent;

private void ServerConnection_ServerConnection_LobbyExitedEvent (LobbyExitedRequest lobbyExitedRequest)
{
    [..]
}

Chat

image8

Send a chat message in the lobby

image9

To send a chat message to the lobby, call ChatLogic.SendChatToLobby.

ServerConnection.MultiCastChatRequest is then called without specifying GameID (null).

ServerConnection
Send request
ServerConnection.MultiCastChatRequest (chat, gameId);

Receive a message

image10

The message reception is handled by the event ServerConnection.ClientChatEvent. ChatLogic then notifies the view that a message has arrived via the event ChatLogic.ClientChatEvent.

A chat message is encapsulated in a ChatEntry object containing the name of the sender, the message, the date time, a marker to determine whether it is the local player or not and a marker for censorship.

image11

ServerConnection
Reception event
ServerConnection.ClientChatEvent += ServerConnection_ClientChatEvent

private void ServerConnection_ClientChatEvent(ClientChatRequest clientChatRequest)
{
    [..]
}

Receiving a chat blocked message

image12

If the message does not pass the anti-profanity filter (handled by Scalable Server), then the ChatLogic receives the event ServerConnection.ClientChatBlockedEvent and sends a specific message to the view through ChatLogic.ClientChatEvent with IsCensored = true

ServerConnection
Reception event
ServerConnection.ClientChatBlockedEvent+= ServerConnection_ClientChatBlockedEvent;

private void ServerConnection_ClientChatBlockedEvent(ClientChatBlockedRequest clientChatBlockedRequest)
{
    [..]
}

Request / receive chat history in the lobby

image13

From the display of the lobby, the chat history is downloaded.

ChatLogic then call ServerConnection.GetChatHistoryRequest. GameID parameter is ignored (null)

When the chat history is received, the ServerConnection.ClientChatHistoryEvent is raised.

ChatLogic.ClientChatEvent sent each message unitarily to the view. Each message is formatted as a new message

Receiving chat history or a new message is unified for the view

ServerConnection
Send request
ServerConnection.GetChatHistoryRequest(gameId);
Reception event
ServerConnection.ClientChatHistoryEvent += ServerConnection_ClientChatHistoryEvent ;

private void ServerConnection_ClientChatHistoryEvent(ClientChatHistoryRequest clientChatHistoryRequest)
{
    [..]
}

Players lists

image14

The list of online / friends / ignored players is received automatically after entering the lobby.

Each of these lists is stored in the LobbyPlayersLogic.

IPlayer interface represents whether an online, friends or ignored player.

image15

Get the list of online players

image16

After being registered in the lobby through ServerConnection.EnterLobbyRequest, the online player list is automatically and regularly sent by the Scalable Server. To receive it you must subscribe to ServerConnection.LobbyPlayerListEvent

Whenever the online player list is received, it is stored in memory in LobbyPlayersLogic then the LobbyPlayersLogic.RefreshLobbyPlayerListEvent is called to force refresh of LobbyPlayersView

ServerConnection
Reception event
ServerConnection.LobbyPlayerListEvent += ServerConnection_LobbyPlayerListEvent;

private void ServerConnection_LobbyPlayerListEvent(List<SmallPlayer> players)
{
    [..]
}

Receive the buddy list

image17

After being registered in the lobby through ServerConnection.EnterLobbyRequest, the buddy list is automatically and regularly sent by the Scalable Server. To receive it you must subscribe to ServerConnection.AsyncBuddyListContentEvent

Whenever the buddy list is received, it is stored in memory in LobbyPlayersLogic then the LobbyPlayersLogic.RefreshLobbyPlayerListEvent is called to force refresh of LobbyPlayersView

ServerConnection
Reception event
ServerConnection.AsyncBuddyListContentEvent += ServerConnection_AsyncBuddyListContentEvent;

private void ServerConnection_AsyncBuddyListContentEvent(AsyncBuddyListContentRequest asyncBuddyListContentRequest)
{
    [..]
}

Get the list of ignored players

image18

After being registered in the lobby through ServerConnection.EnterLobbyRequest, the ignored player list is automatically and regularly sent by the Scalable Server. To receive it you must subscribe to ServerConnection.AsyncIgnoreListContentEvent

Whenever the online player list is received, it is stored in memory in LobbyPlayersLogic then the LobbyPlayersLogic.RefreshLobbyPlayerListEvent is called to force refresh of LobbyPlayersView

ServerConnection
Reception event
ServerConnection.AsyncIgnoreListContentEvent += ServerConnection_AsyncIgnoreListContentEvent;

private void ServerConnection_AsyncIgnoreListContentEvent(AsyncIgnoreListContentRequest asyncIgnoreListContentRequest)
{
    [..]
}

Add a player to the buddy list

image19

You can add a player to the buddy list by clicking the heart icon next to his name in the list of online players.

A popup is displayed and proposes to add the player to the buddy list by clicking the button [Add to buddy list]

image20

Call ServerConnection.AddBuddy with the Days of Wonder player’s account ID (DoWId, w_w_w_id).

The DoWId is contained in the IPlayer object selected in the list

ServerConnection
Send request
ServerConnection.AddBuddy(DoWId);
Reception event
ServerConnection.AsyncBuddyAddedEvent += ServerConnection_AsyncBuddyAddedEvent;

private void ServerConnection_AsyncBuddyAddedEvent(AsyncBuddyAddedRequest asyncBuddyAddedRequest)
{
    [..]
}

Remove a player from buddy list

image21

You can remove a friend from the buddy list by clicking the heart icon next to his name in the buddy list.

A popup is displayed and proposes to remove the player from the buddy list by clicking the button [Remove from buddy list]

image22

Call ServerConnection.RemoveFromBuddyList with the Days of Wonder player’s account ID (DoWID, w_w_w_id).

The DoWID is contained in the IPlayer object selected in the list

ServerConnection
Send request
ServerConnection.RemoveFromBuddyList (DoWId);
Reception event
ServerConnection.AsyncBuddyRemovedEvent += ServerConnection_AsyncBuddyRemovedEvent;

private void ServerConnection_AsyncBuddyRemovedEvent(AsyncBuddyRemovedRequest asyncBuddyRemovedRequest)
{
    [..]
}

Add a player to the ignored list

image23

You can add a player to the ignored list by clicking the heart icon next to his name in the list of online players.

A popup is displayed and proposes to add the player to the ignored list by clicking the button [Add to ignore list]

image24

Call ServerConnection.AddToIgnoreList with the Days of Wonder player’s account ID (DoWID, w_w_w_id).

The DoWID is contained in the IPlayer object selected in the list

ServerConnection
Send request
ServerConnection.AddToIgnoreList (DoWId);
Reception event
ServerConnection.AsyncIgnoreAddedEvent += ServerConnection_AsyncIgnoreAddedEvent;

private void ServerConnection_AsyncIgnoreAddedEvent (AsyncIgnoreAddedRequest asyncIgnoreAddedRequest)
{
    [..]
}

Remove a player from the ignore list

image25

You can remove a player from the ignored list by clicking the heart icon next to his name in the ignored list.

A popup is displayed and proposes to remove the player from the ignored list by clicking the button [Remove from ignore list]

image26

Call ServerConnection.RemoveFromIgnoreList with the Days of Wonder player’s account ID (DoWID, w_w_w_id).

The DoWID is contained in the IPlayer object selected in the list

ServerConnection
Send request
ServerConnection.RemoveFromIgnoreList (DoWId);
Reception event
ServerConnection.AsyncIgnoreRemovedEvent += ServerConnection_AsyncIgnoreRemovedEvent;

private void ServerConnection_AsyncIgnoreRemovedEvent (AsyncIgnoreRemovedRequest asyncIgnoreRemovedRequest)
{
    [..]
}

LobbyGameView

Initialization

/// <summary>
/// Initialize the view - Creating logic - Creating .Net events
/// </summary>
private void Init()
{
    LobbyGameLogic = new LobbyGameLogic(NetworkManager.Instance.ServerConnection, PersistenceManager.Instance.Persistence);

    LobbyGameLogic.LobbyOpenGameListEvent += LobbyGameLogic_LobbyGameListEvent;
    LobbyGameLogic.LobbyResumeGameListEvent += LobbyGameLogic_LobbyResumeGameListEvent;

    LobbyGameLogic.InvitationArrivedEvent += LobbyGameLogic_InvitationArrivedEvent;
    LobbyGameLogic.InvitationAcceptedEvent += LobbyGameLogic_InvitationAcceptedEvent;
    LobbyGameLogic.InvitationRejectedEvent += LobbyGameLogic_InvitationRejectedEvent;
    LobbyGameLogic.LobbyNewPlayerEvent += LobbyGameLogic_LobbyNewPlayerEvent;
    LobbyGameLogic.LobbyPlayerLeftGameEvent += LobbyGameLogic_LobbyPlayerLeftGameEvent;
    LobbyGameLogic.LobbyJoinDeniedEvent += LobbyGameLogic_LobbyJoinDeniedEvent;

    LobbyGameLogic.Init();
}

Thanks to events, the view will be notified when:

  • The list of opened games must be refreshed : LobbyGameLogic.LobbyOpenGameListEvent
  • The list of ongoing games must be refreshed : LobbyPlayersLogic.LobbyResumeGameListEvent
  • The player is invited to a game : LobbyGameLogic.InvitationArrivedEvent
  • The invited player accepted the invitation : LobbyGameLogic.InvitationAcceptedEvent
  • The invited player refused the invitation : LobbyGameLogic.InvitationRejectedEvent
  • A new player joins the game : LobbyGameLogic.LobbyNewPlayerEvent
  • A player leaves the game : LobbyGameLogic.LobbyPlayerLeftGameEvent
  • The player is not allowed to join the game LobbyGameLogic.LobbyJoinDeniedEvent

Creating a game

Creating a solo / pass and play game

image27

image28

To create a solo / pass and play game, the player must drag’n drop AI or Local Player icons in empty slots. he can name the local players and define who goes first.

LobbyGameLogic.CreateLocalGame method is then called, the following settings are stored in the persistence:

  • Persistence.GameType = GameType.Local
  • Persistence.PlayerSeats.AddRange (playerSeats)

The scene 3_Game is loaded and the game can begin.

ServerConnection is not used in a solo game, as there is no need of related features of the Scalable Server. It is, therefore, possible to create a solo game without authenticating the user.

Creating an open online game

image29

image30

You can create an online game done from the [QUICK] or [ADVANCED] tabs. It is possible to determine many parameters which some values ​​are predefined for Quick games.

LobbyGameLogic.CreateOnlineGame is then called with a GameConfiguration parameter.

When the Scalable Server receives the request it return a GameCreatedRequest which result in a ServerConnection.GameCreatedEvent on client side.

ServerConnection
Send request
ServerConnection.CreateOnlineGame(gameConf, password);
Reception event
ServerConnection.GameCreatedEvent += ServerConnection_GameCreatedEvent;

private void ServerConnection_GameCreatedEvent(GameCreatedRequest gameCreatedRequest)
{
    [..]
}

Inviting friends to join a game

image31

image32

You can invite friends to join a private game. It is necessary beforehand that these players are in the buddy list. Drag’n drop their portrait from the buddy list to a free slot. The [Create online game] button send the request to the Scalable Server.

LobbyGameLogic.CreateOnlineGame is then called with a GameConfiguration parameter.

When the Scalable Server receives the request it returns a GameCreatedRequest to all players involved which result in a ServerConnection.GameCreatedEvent on client side.

ServerConnection
Send request
ServerConnection.CreateInvitationGame (GameConfiguration, DoWIds);
Reception event
ServerConnection.GameCreatedEvent += ServerConnection_GameCreatedEvent;

private void ServerConnection_GameCreatedEvent(GameCreatedRequest gameCreatedRequest)
{
    [..]
}

Invitation

Receiving an invitation

image33

When a player receives an invitation, ServerConnection.GameCreatedEvent is called. LobbyGameLogic interprets this event and determines whether it is an invitation. If it’s the case, LobbyGameLogic.InvitationArrivedEvent is called. The invitation popup appears offering the choice to accept or decline the invitation.

ServerConnection
Reception event
ServerConnection.GameCreatedEvent += ServerConnection_GameCreatedEvent;

private void ServerConnection_GameCreatedEvent(GameCreatedRequest gameCreatedRequest)
{
    // If the field gameCreatedRequest.invited_by is not null, then it’s an invitation
    [..]
}

Accepting an invitation

image34

image35

When the player accepts the invitation, LobbyGameLogic.AcceptInvitation is called with the GameID of the game. DoWID of the player is stored in the list of players who accept the invitation. The invitation popup is displayed until all guests have not responded to the invitation.

The response to the invitation is sent to Scalable Server by sending AnswerInvitationRequest with the parameter accept = True

Persistence.InvitationAcceptedByPlayers.Add(Persistence.ConnectedPlayer.w_w_w_id);
ServerConnection
Send request
ServerConnection.AcceptInvitation(GameId);

Declining an invitation

image36

image37

When the player accepts the invitation, LobbyGameLogic.DeclineInvitation is called with the GameID of the game. The invitation popup is closed.

The response to the invitation is sent to Scalable Server by sending AnswerInvitationRequest with the parameter accept = False

ServerConnection
Send request
ServerConnection.DeclineInvitation(GameId);

Receiving the answer of friend to an invitation

image38

After a friend responded to an invitation, ServerConnection.InvitationAnsweredEvent is called. LobbyGameLogic détermines whether the invitation was accepted. If it’s the case LobbyGameLogic.InvitationAcceptedEvent is called. If it’s not LobbyGameLogic.InvitationRejectedEvent is called. The popup is then updated.

ServerConnection
Reception event
ServerConnection.InvitationAnsweredEvent+= ServerConnection_InvitationAnsweredEvent;

private void ServerConnection_InvitationAnsweredEvent(InvitationAnsweredRequest invitationAnsweredRequest)
{
    [..]
}

Joining a game

Viewing the list of open games

image39

image40

The list of open games is received regularly and automatically after entering the lobby.

ServerConnection
Reception event
ServerConnection.LobbyGameListEvent += ServerConnection_LobbyGameListEvent;

private void ServerConnection_LobbyGameListEvent(GameList gameList)
{
    [..]
}

Viewing the list of ongoing games

image41

image42

The list of ongoing game is received by calling ServerConnection.WhatsNewPussycat. This method is called upon initialization of LobbyGameLogic

ServerConnection
Send request
ServerConnection.WhatsNewPussycat
Reception event
ServerConnection.GameStatusReportAllEvent += ServerConnection_GameStatusReportAllEvent;

private void ServerConnection_GameStatusReportAllEvent(List<StatusReport> statusReports)
{
    [..]
}

Joining an open game

image43

To join a game, LobbyGameLogic.JoinGame is called, it takes one parameter, the GameDetails of the selected game to store it in persistence. Then ServerConnection.LobbyJoinGameRequest is called with the GameID and optional password.

ServerConnection
Send request
ServerConnection.LobbyJoinGameRequest(GameID, password)

Exiting a game

image44

image45

The player may leave the current waiting game by clicking the [Leave] button.

ServerConnection
Send request
ServerConnection.LobbyLeaveGameRequest(GameID)

A player joined the open game

image46

image47

When a player joins a game, ServerConnection.LobbyNewPlayerEvent is received by all players of the game, including the player that just connected. the popup is updated and displays the portrait of the new player

ServerConnection
Reception event
ServerConnection.LobbyNewPlayerEvent += ServerConnection_LobbyNewPlayerEvent;

private void ServerConnection_LobbyNewPlayerEvent(LobbyNewPlayerRequest lobbyNewPlayerRequest)
{
    [..]
}

A player leaves the open game

image48

When a player leaves a waiting game, ServerConnection.LobbyPlayerLeftGameEvent is received by all participating players, including the player who leaves the game.

ServerConnection
Reception event
ServerConnection.LobbyPlayerLeftGameEvent += ServerConnection_LobbyPlayerLeftGameEvent;

private void ServerConnection_LobbyPlayerLeftGameEvent(LobbyPlayerLeftGameRequest lobbyPlayerLeftGameRequest)
{
    [..]
}

Resuming a game

image49

image50

In the [RESUME] tab, the list of games in progress is displayed. A player can resume a game by selecting it and clicking [Join game] button.

LobbyGameLogic.ResumeSyncGame called, the parameter GameDetails of the selected game to store it in persistence. Then ServerConnection.ResumeSyncGameRequest is called with the GameID.

The scene 3_Game is then opened, the player receives all information from the game and can play when it’s his turn.

ServerConnection
Send request
ServerConnection.ResumeSyncGameRequest(GameID)

The player is not allowed to join the game

image51

image52

If the player is not allowed to join a game, it receives ServerConnection.LobbyJoinDeniedEvent. The errors can be found in the documentation of Scalable Server : LobbyJoinDeniedRequest.JoinError

ServerConnection
Reception event
ServerConnection.LobbyJoinDeniedEvent += ServerConnection_LobbyJoinDeniedEvent;

private void ServerConnection_LobbyJoinDeniedEvent(LobbyJoinDeniedRequest lobbyJoinDeniedRequest)
{
    [..]
}

GameView

image53

The scene 3_Game hosts the heart of the game. It contains GameView that instantiate the GameplayLogic and ChatLogic

Initialization

/// <summary>
/// Initialize the view - Creating Local or Distant Logic according to the parameter of the game - Creating .Net events
/// </summary>
private void Init()
{
    //--- Instantiate the GamePlayLogic depending on GameType (Distant, Local or Replay)
    switch (PlayRealManager.Instance.Persistence.GameType)
    {
        case GameType.Local:
            _gameplayLogic = new LocalGameplayLogic(PlayRealManager.Instance.Persistence,  ui.Dices);
            break;
        case GameType.Distant:
            _gameplayLogic = new DistantGameplayLogic(PlayRealManager.Instance.ServerConnection, PlayRealManager.Instance.Persistence, ui.Dices);
            break;
        case GameType.Replay:
            _gameplayLogic = new ReplayGameplayLogic(PlayRealManager.Instance.Persistence, ui.Dices);
            break;
        default:
            break;
    }

    _gameplayLogic.LogEnabled = PlayRealManager.Instance.LogLogicEnabled;
    //---

    //--- General local and online events
    _gameplayLogic.LocalPlayerTurnEvent += GameplayLogic_LocalPlayerTurnEvent;
    _gameplayLogic.OtherPlayerTurnEvent += GameplayLogic_OtherPlayerTurnEvent;
    _gameplayLogic.RoundEndedEvent += GameplayLogic_RoundEndedEvent;
    _gameplayLogic.GameOutcomeEvent += GameplayLogic_GameOutcomeEvent;
    //---

    //--- Dice game specific events
    _gameplayLogic.DicePickedIndexEvent += GameplayLogic_DicePickedIndexEvent;
    _gameplayLogic.RollDicesAnimationEvent += GameplayLogic_RollDicesAnimationEvent;
    //---

    //---> Online events
    if (PersistenceManager.Instance.Persistence.GameType == GameType.Distant)
    {
        ((DistantGameplayLogic)_gameplayLogic).PlayerPresenceUpdatedEvent += GameplayLogic_PlayerPresenceUpdatedEvent;
        ((DistantGameplayLogic)_gameplayLogic).RefreshGameStatusEvent += GameplayLogic_RefreshGameStatusEvent;
        ((DistantGameplayLogic)_gameplayLogic).GameAbortedEvent += GameplayLogic_GameAbortedEvent;
        ((DistantGameplayLogic)_gameplayLogic).OtherPlayerQuitGameEvent += GameplayLogic_PlayerQuitGameEvent;
        ((DistantGameplayLogic)_gameplayLogic).LocalPlayerQuitGameEvent += GameplayLogic_LocalPlayerQuitGameEvent;
        ((DistantGameplayLogic)_gameplayLogic).PlayerJoinGameEvent += GameplayLogic_PlayerJoinGameEvent;

        _chatLogic = new ChatLogic(NetworkManager.Instance.ServerConnection, PersistenceManager.Instance.Persistence);
        _chatLogic.ClientChatEvent += ChatLogic_ClientChatEvent;

        _chatLogic.Init();
    }
    else
    {
        //---> Hide the Chat button in local mode
        ui.ChatButton.gameObject.SetActive(false);
    }

    //---> Initialize the logic
    _gameplayLogic.Init();
}

According to the GameType parameter (Local, Distant or Replay) stored in the persistence, whether LocalGameplayLogic, DistantGameplayLogic or ReplayGameplayLogic is instantiated in the _gameplayLogic member of LobbyGameView. Also, if GameType == Distant, the ChatLogic is instantiated.

With events, the view will be notified when :

  • It’s local player turn : GameplayLogicBase.LocalPlayerTurnEvent
  • It’s the turn of another player (distant / AI) : GameplayLogicBase.OtherPlayerTurnEvent
  • This is the end of a round : GameplayLogicBase.RoundEndedEvent
  • This is the end of the game : GameplayLogicBase.GameOutcomeEvent
  • The dice selection animation must be played : GameplayLogicBase.DicePickedIndexEvent
  • The dice rotation animation must be played : GameplayLogicBase.RollDicesAnimationEvent

Exclusively in online mode :

  • The presence of a player has changed : DistantGameplayLogic.PlayerPresenceUpdatedEvent
  • The status of the game has changed : DistantGameplayLogic.RefreshGameStatusEvent
  • The game is aborted : DistantGameplayLogic.GameAbortedEvent
  • A distant player has left the game DistantGameplayLogic.OtherPlayerQuitGameEvent
  • The local player left the game : DistantGameplayLogic.LocalPlayerQuitGameEvent
  • A distant player joins the game : DistantGameplayLogic.PlayerJoinGameEvent
  • A chat message is received : ChatLogic.ClientChatEvent

Architecture

The SDK contains a set of class and interface that allow building a game architecture supporting the event sourcing, local game with AI and online game.

image54

The interfaces IGameplayLogic, IDistantGameplayLogic, IMessage and the classes GameStateBase and DistantGameplayComponent are part of the SDK. That means they are designed to be used as such to build game architecture.

image55

image56

  • With IGameplayLogic you can build the common base for the single player and online gameplay logic.
  • IDistantGameplayLogic complement the online gameplay class.
  • DistantGameplayComponent will manage a large part of technical features of online and drive the class deriving from IDistantGameplayLogicBase accordingly.
  • GameStateBase stores all the actions of the players. It is the object transiting between players and which determines the state of the game. In solo, it can be saved in a file to save the game.
  • IMessage is the model for multicast messages

Communications with Scalable Server

Communication with the Scalable Server is done through object serialization sent over TCP/IP with a socket. The format used is Protobuf.

To send events (IEvent) and messages (IMessage) to Scalable Server, we must serialize it. It’s the role of AsmodeeNetSerializer.

AsmodeeNetSerializer is a class that is embedded in the generated AsmodeeNetSerializer.dll

Serialization is automatically done in the ServerConnection component when CommitAction, MultiCastDataRequest are called and when new data are coming from the Scalable Server

Event sourcing

image57

As recommended by the documentation, the Asmodee.net Unity SDK is built on the concept of event sourcing. This will make the task for cheaters much more difficult and we can make replay of old games easily.

The principle of event sourcing is not to store results in memory but only the actions leading to results. Thereby each time a result is required (to display the values ​​of the dice for example) the entire stack is replayed by the GameEngine.

The sample store the stack of players actions in the GameState object. It is from this stack that value of the dice, score and the winners of each round are calculated. Each player action is stored in the GameState then it is sent to the Scalable server. It is possible to leave and return to the middle of a turn.

Example

In the case of the sample:

  1. The first event stored in the GameState is GameInitalized. The seed of the random number generator (RNG) and the number of players are saved into it. The GameState is sent to the Scalable Server.
  2. The first player rolls the 3 dices, the event DicesRolled is stored. Index and not values of rolled dices (0, 1 and 2) are saved in it. The GameState is sent to the Scalable Server.
  3. To view the results of the dice, GameEngine plays the stack of events from the beginning, by initializing the RNG then draw 3 times a random number between 1 and 6.
  4. The first player re-roll the dices 0 and 2. DicesRolled is stored with index 0 and 2. The GameState is sent to the Scalable Server.
  5. To view the result of the dice, as in #3, the entire event stack is played, there is 5 draw of random numbers.
  6. The first player finishes his turn, the TurnEnded events and is stored in the GameState
  7. GameState is sent to Scalable Server which will send it to next player.
  8. The last player of the round will add RoundEnded event to the stack, resulting in the end of the round and score calculation
  9. The players score is calculated by computing the stack of events from the beginning to ensure the reproducibility of the RNG

Creating your own events and messages

If you want to create your own events, you need to create a YourGameDTO c# project. This project is external to your unity project.

When you setup the Asmodee.net Serializer , you’ve unzipped the ScalableServerDTO solution.

  1. Open the ScalableServerDTO solution into Visual Studio or Monodevelop
  2. Create a new Class Library Project
_images/ScalableServerDTO_CreateNewProject.png _images/ScalableServerDTO_CreateNewProject_Window.PNG
  1. Set The target Framework of your Project to .NET Framework 2.0
_images/ScalableServerDTO_TargetFramework.png
  1. Remove the PlayRealSamplesDTO project from the solution
_images/ScalableServerDTO_Remove_Samples.png
  1. Add protobuf-net.dll reference to your new DTO project, by browsing it near the solution
_images/ScalableServerDTO_Add_References.png
  1. Add PlayRealDTO reference to your new DTO project, by selecting it in the projects tab
_images/ScalableServerDTO_Add_PlayRealDTO_References.png
  1. Remove the Class1.cs file in your project
  2. Create your own Event class inherited from IEvent
using AsmodeeDigital.PlayReal.Plugin.Domain.GameState;
using ProtoBuf;
using System;

namespace YourGameDTO
{
    [ProtoContract, Serializable]
    public class MyEvent : IEvent
    {
        /// <summary>
        /// Player local ID
        /// </summary>
        [ProtoMember(1)]
        public int PlayerLocalID { get; set; }

        /// <summary>
        /// UTC time
        /// </summary>
        [ProtoMember(2)]
        public long Time { get; set; }

        /// <summary>
        /// Value for documentation
        /// </summary>
        [ProtoMember(3)]
        public int MyValue { get; set; }

        public MyEvent()
        {
        }
    }
}
  1. Compile the solution in Release mode

9. Once the compilation done, change the YOUR_PROJECT_DTO and UNITY_PROJECT_FOLDER variable in the build_AsmodeeNetSerializer.cmd or build_AsmodeeNetSerializer.sh and launch it in order to create the AsmodeeNetSerializer.dll.

10. A new AsmodeeNetSerializer is compiled. PlayRealDTO.dll, YourGameDTO.dll and AsmodeeNetSerializer.dll are copied into the /Assets/Plugins folder of your Unity project

_images/AsmodeeNetSerializer_YourGameDTO_Log.png

Sending an action

image58

Whether in solo or online, the player’s actions are managed in GameplayLogicBase. DistantGameplayLogic overloads methods to send the GameState to the Scalable Server

Example - Roll of the dice:

In the abstract GameplayLogicBase:

/// <summary>
/// Create *dices rolled* event in game state. Store the list of selected dices to roll
/// </summary>
/// <param name="dicesIndices">List of selected dices to roll, by default all dices</param>
public virtual void CreateEventDicesRolled(List<int> dicesIndices = null)
{
    Log("Event : Dices rolled");

    State = States.DiceRolled;

    if (dicesIndices == null)
        dicesIndices = new List<int> { 0, 1, 2 };

    GameState.Events.Add(
        new DicesRolled(
            CurrentPlayer.LocalId,
            Turn, dicesIndices));
}

In DistantGameplayLogic:

/// <summary>
/// Create *dices rolled* event in game state. Store the list of selected dices to roll.
/// Then commits the game state to the server. The current player can continue to play
/// </summary>
/// <param name="dicesIndices">List of selected dices to roll</param>
public override void CreateEventDicesRolled(List<int> dicesIndices = null)
{
    //---> Call the GameplayLogicBase method
    base.CreateEventDicesRolled(dicesIndices);

    //---> Commit the action to the **Scalable Server**
    DistantGameplayComponent.CommitAction(false);

    //---> Send a multicast message so that other players see the animation of the dice
    SendRolledDicesMessage(dicesIndices);
}

DistantGameplayComponent.CommitAction(nextPlayerTurn) skips to the next player if the parameter nextPlayerTurn == True

Each sent action, the Scalable Server return ActionCommitedRequest and ActionRequiredRequest to the next player, which in our example is the current player (self).

ServerConnection
Send request
ServerConnection.CommitAction(GameID, localPlayerId, nextPlayers, gameState)
Reception event
ServerConnection.ActionCommitedEvent += ServerConnection_ActionCommitedEvent

private void ServerConnection_ActionCommitedEvent(ActionCommitedRequest actionCommitedRequest)
{
    [..]
}
ServerConnection.ActionRequiredEvent += ServerConnection_ActionRequiredEvent

private void ServerConnection_ActionRequiredEvent(ActionRequiredRequest actionRequiredRequest)
{
    [..]
}

Next player

image59

the game moves to the next player when the current player call DistantGameplayComponent.CommitAction(nextPlayerTurn) with nextPlayerTurn == True

The Scalable Server send the ActionRequiredRequest to the the next player, which will raise ServerConnection.ActionRequiredEvent

DistantGameplayComponent subscribes to ServerConnection.ActionRequiredEvent. It get the GameState sent by the Scalable Server and determines who is the next player by calling the DistantGameplayLogic.UpdateNextPlayer(nextPlayersIds)

Local player

image60

In Solo or online mode, the management of the local player remains the same. It is managed by GameplayLogicBase.LocalPlayerTurn

ServerConnection
Reception event
ServerConnection.ActionRequiredEvent += ServerConnection_ActionRequiredEvent

private void ServerConnection_ActionRequiredEvent(ActionRequiredRequest actionRequiredRequest)
{
    [..]
}

Distant player

image61

The turn of a distant player is reported to other players by multicast message when the current player commits his EndTurn

This message is then received by all the other players, thereby refresh the UI

ServerConnection
Reception event
ServerConnection.ClientDataEvent += ServerConnection_ClientDataEvent

private void ServerConnection_ClientDataEvent(ClientDataRequest clientDataRequest)
{
    [..]
}

Artificial intelligence

image62

The device of a player can play the AI ​​in the single player game or when a distant player was replaced by AI.

Solo game

image63

In solo, the AI ​​can play after the turn of a player. LocalGameplayLogic.EndOfTurn called GameLogicBase.UpdateNextPlayer with parameter canPlayAI == True

Online game

image64

Scalable Server determines who play the AI by sending a PlayerTimeoutRequest. DistantGameplayComponent subscribed to ServerConnection.PlayerTimeoutEvent, which allows it to call GameplayLogicBase.UpdateNextPlayer with the parameter ​​​canPlayAI == True when the device of a connected player must play the AI.

ServerConnection
Reception event
ServerConnection.PlayerTimeoutEvent += ServerConnection_PlayerTimeoutEvent

private void ServerConnection_PlayerTimeoutEvent (PlayerTimeoutRequest playerTimeoutRequest)
{
    [..]
}

GameEngine

GameEngine is the class (or set of classes) managing the rules and calculation of results from the events of GameState. It is this object that will be called to play the entire stack of events.

The IGameEngine interface offers a free design. In the sample it is considered as a database engine which must return a result from a given request and a set of data.

For each request, ComputeEvents is called

public T ComputeEvents<T>(GameStateBase gameState, int playerLocalId, Evaluation evaluation)

gameState parameter is the set of data from wich we want a result for the given player id and the evaluation. GameEngine will then compute the entire game in memory and stored temporarily the results. Then it will choose the result in the variable and return it. Thus, the results are a very short time in memory.

Example

/// <summary>
/// Get Score of defined player, by default the last player
/// </summary>
/// <param name="playerLocalId">Player local ID. By default the last player</param>
/// <param name="gameState">Game state whose the score is computed. By default the game state of the current game is used</param>
/// <returns></returns>
public int GetScore(int playerLocalId = -1, GameStateBase gameState = null)
{
    if (gameState == null)
        gameState = GameState;

    if (playerLocalId == -1)
        playerLocalId = CurrentPlayer.LocalId;

    return ((GameEngine)GameEngine).ComputeEvents<int>(gameState, playerLocalId, Evaluation.GetScore);
}

Messages

To notify the other players of the current player’s actions in real time, a multicast message system was put in place.

Sending and interpretation of messages are done in DistantGameplayLogic, their reception in DistantGameplayComponent

A message is a class which implement the interface IMessage. The message can then adopt any form. In the sample, it was decided to integrate the GameState and enumeration MessageType

DistantGameplayLogic is generic and takes two types in its construction DistantGameplayComponent<GameStateBase, iMessage>.

image65

Sending a message

image66

DistantGameplayLogic call ServerConnection.MultiCastDataRequest to send the desired message. It is possible to select the recipients, specifying the Local players Ids

ServerConnection
Send request
ServerConnection.MultiCastDataRequest (GameID, localPlayerId, Message)

Receiving a message

image67

DistantGameplayLogicComponent receives the message through its ServerConnnection.ClientDataEvent subscription. The message is deserialized in the defined type set at the instantiation of DistantGameplayLogicComponent. It is then transmitted to DistantGameplayLogic which interprets it thanks to DistantGameplayLogic.MessageReceived(message)

ServerConnection
Reception event
ServerConnection.ClientDataEvent += ServerConnection_ClientDataEvent

private void ServerConnection_ClientDataEvent(ClientDataRequest clientDataRequest)
{
    [..]
}

Exiting the game

In online mode, a player can leave the game by two different ways depending on the game mode :

  • Single session (synchronous): Forfeit
  • Multi Session (Asynchronous): Forfeit or Leave

Forfeit

image68 image69

When a player forfeit, he left the game for good, without the possibility of returning.

ServerConnection.GameForfeitRequest is called specifying the GameID

ServerConnection
Send request
ServerConnection.GameForfeitRequest (GameID)
Reception event
ServerConnection.GameForfeitedEvent += ServerConnection_GameForfeitedEvent;

private void ServerConnection_GameForfeitedEvent(GameForfeitedRequest gameForfeitedRequest)
{
    [..]
}

Leave a game temporarily

image70 image71

It is possible in a multi-session game to leave a game temporarily. The player can come back later as long as his player clock is not completely elapsed.

ServerConnection
Send request
ServerConnection.LeaveSyncGameRequest (GameID)

Chat in game

Sending a chat from a game

image72

image73

The chat in game uses the same ChatLogic class that in the lobby. To send a chat message to the current game you must call ChatLogic.SendChatToCurrentGame

ServerConnection.MultiCastChatRequest is called specifying the GameID

ServerConnection
Send request
ServerConnection.MultiCastChatRequest (Chat, GameID);

Savegame

In solo, it is possible to save and load a game.

This feature is compatible with Android, iOS, MacOSX, Linux, Windows and also directly from the Unity editor.

For the standalone version (MacOSX, Linux and Windows) the savegame is located in the user directory. This allows Steam Cloud synchronization

File path by platform:

  • Android : Persistent Data Path (/data/data/[com.mycompanyname]/[myapp]/files)
  • iOS : Persistent Data Path (/var/mobile/Applications/xxxxx-xx-x-xx-xxxx/Documents/)
  • Windows : Local application Data (C:\Users\[username]\appdata\local\[filePath])
  • MacOSX : Local application Data (/Applications/Unity/MonoDevelop.app/Contents/MacOS/../Frameworks/Mono.framework/Versions/Current/share/[filePath])
  • Linux : Local application Data (~/.[filePath])

Saving a game

To save the game, you must save the GameState. SaveGameState is generic, the first parameter accepts any subtype of GameStateBase. The second parameter to provide is the short file path. This path must not contain disk name (not an asbolute path). The SDK takes care to save the file to the appropriate location depending on the platform.

SaveManager.SaveGameState(GameState, @"AsmodeeDigital\PlayReal\save.sav");

Loading a game

Loading the savegame file is done by specifying the short file path and the generic output type.

GameState = SaveManager.LoadGameState<GameStateBase(@"AsmodeeDigital\PlayReal\save.sav");