Results 1 to 1 of 1
  1. #1
    MSgt. Shooter Person
    Join Date
    Jun 2011
    Location
    Ulm, Germany
    Posts
    164

    Default Multiplayer Lobby - Replication Walkthrough

    Hey there!

    Introduction

    This tutorial shows how to build a simple chatsystem for a multiplayer game lobby.
    I will give you a code-snippet framework for calling functions on the server from a clients HUD and propagate information to all clients HUDs in a clean way.

    Overview

    When a client sends a chatmessage, multiple things will happen:

    • The swfs Actionscript passes the message to its GFxMoviePlayer class.
    • The GFxMoviepPlayer passes the message to the PlayerController owned by the client.
    • The PlayerController calls a reliable server function - this means it is processed on the server instead of the client.
    • The servers version of PlayerController knows GameInfo and gives it the Message.
    • GameInfo can do some logic on the Message now. It will add the players name to it.
    • GameInfo will pass the message to its GameReplicationInfo.
    • The GRI will then call a delegate function on all clients, containing the message.
    • All classes who listen to this delegate will receive the message, including the clients GFxMoviePlayer.
    • The GFxMoviePlayer passes the message to its Scaleform HUD.
    • The message gets plotted in every clients HUD
    • Done!


    So let's get started!

    Scaleform Input

    Place a TextInput CLIK-Component on the stage, call it chatInput.
    We want any text typed in here to be sent to Unrealscript. You could use a button to do so, but I preferred to send it on pressing return.

    Open the actions tab ('F9'), and place this code there:

    Code:
    import flash.external.ExternalInterface;
    
    /*------------------------------
    LOBBY CHAT
    ------------------------------*/
    //Send Chatmessages
    var chatInputListener:Object = new Object();
    	chatInputListener.onKeyUp = function() {
    		if (Key.getCode() == Key.ENTER) {
    			if (chatInput.focused==1&&chatInput.text!=""){
    				ExternalInterface.call("Unreal_SendChatMessage",chatInput.text);
    				chatInput.text = "";		
    			}
    		}
    	};
    Key.addListener(chatInputListener);
    In plain text:
    This will listen on any enter-keypresses. When called:
    If the chatInput component is focused, and if there is some text in there (I don't want to send empty messages),
    it will call the Unreal_SendChatMessage function in our GFxMoviePlayer-Class and passes the text contained in chatInput.
    After that it will clear our chatInput.

    Pass the Message to our PlayerController

    The GFxMoviePlayer recieved the message. It is even not an actor class, so there is no chance to get to the servers GameInfo from here.
    This makes sense anyway, because its purpose is to display information on screen, not to do any gamelogic stuff.

    But the GFxMoviePlayer has access to the local PlayerController via GetPC(), and from there we can move on!

    In YourLobbyMenu_GFxMoviePlayer Class:
    Code:
    function Unreal_SendChatMessage(String Message){
    	YourPlayerController(GetPC()).SendChatMessage(Message);
    }
    Playercontroller: Call a reliable server function

    When you run a local game, the PlayerController has direct access to GameInfo. When in a network game, it doesn't have any direct access anymore - calls to GameInfo return None (except if you are a listen-server). But we can force the server to run a function in its version of our PlayerController class, and it knows GameInfo.

    In YourPlayerController Class:
    Code:
    function SendChatMessage(String Message){
    	ServerSendChatMessage(Message);
    }
    reliable server function ServerSendChatMessage(String Message){
    	YourGame(WorldInfo.Game).NewChatMessage(PlayerReplicationInfo.PlayerName,Message);
    }
    I additionally sent the Players name to GameInfo. I will put it together there. You could also do this here, but most of the time, you want gameinfo to do some gamelogic related stuff with your inputs, so I tend to go this way.

    GameInfo: Process logic and pass results to GameReplicationInfo

    Our message arrived in the servers GameInfo. Let's do the logic part with it:

    In YourGameInfo class:
    Code:
    function NewChatMessage(String PlayerName, String Message){
    	Message=PlayerName@" :"@Message;
    	YourGameReplicationInfo(GameReplicationInfo).NewChatMessage(Message);
    }
    After rebuilding the message, it get's finally passed to GameReplicationInfo.

    GameReplicationInfo: Publish the Message!

    Okay, now this is the heavy lifting part if you are not a replication guru.

    In YourGameReplicationInfo class:
    Code:
    var repnotify Bool bNewChatMessage;
    var String ChatMessage;
    You will need an additional Bool - it will be our aha-something-changed switch. Note it is declared as "repnotify"

    Code:
    replication
    {
    	if ( bNetDirty && (Role == ROLE_Authority) )
    		bNewChatMessage,ChatMessage;
    }
    This is our replication statement. It says, if any values differ on clients and server(bNetDirty), and if I am the server(ROLE_Authority), replicate these variables to all clients. (change them on the clients version of gamereplicationinfo to the servers values)

    Code:
    simulated event ReplicatedEvent(name VarName){
    	if ( VarName == 'bNewChatMessage' ){
    		DOnNewChatMessage(ChatMessage);
            return;
        }
    	Super.ReplicatedEvent(VarName);
    }
    This event gets called when any "repnotify" variable changes on the targeted audience: The Client. This also explains why we need an aditional bool for our message system: If a client tries to send the same message two or more times, it will not change, so Unreal saves bandwidth and will not replicate it.

    If our bool gets replicated, we call DOnNewChatMessage:
    Code:
    delegate DOnNewChatMessage(String NewChatMessage);
    This is a delegate , means a pointer to a function.
    Every class that knows YourGameReplicationInfo can listen to it and call its own function when the delegate is fired.
    Someone could do some fiddling and call every class that needs to know about our new message from here, but I prefer this way.

    Code:
    function NewChatMessage(String Message){
    	ChatMessage=Message;
    	bNewChatMessage = !bNewChatMessage;
    	DOnNewChatMessage(Message); //Additional call for listen servers
    }
    This got called from Servers GameInfo in the last chapter.
    First, ChatMessage gets changed, meaning it instantly gets replicated to the other clients, if its content is not the same as the previous messages content.
    Second, switch the repnotify boolean. After replicated to the clients, this will call their YourGameReplicationInfo versions DOnNewChatMessage delegate.
    Third, call DOnNewChatMessage on serverside. You don't need to, if you use a dedicated server. But if your player is a listen server, it won't get notified.

    GFxMoviePlayer: Get the GameReplicationInfo

    Okay this needs some more explanation.

    The GFxMoviePlayer does not know YourGameReplicationInfo per se, but you can get a reference from your playercontroller.
    I found another thread where this is done by simply getting it from Clients PlayerController when the GFxMoviePlayers Start() function gets called.
    In most cases this will do the trick, at least if you initialize your GFxMoviePlayer from Kismet.

    But unfortunatly, Kismet did not work well for me. The OpenGFxMovieplayer Sequence did not open the movie on the clients viewports.
    I also wanted to open a different movie for the host and the clients, so the host has extended rights and may change maps and difficulty, for example.
    (Looking back this was probably a lame decision - better to build in some additional switches in flash and keep all the lobby in the same movie.)

    I decided to put this logic in my servers GameInfo. When a Player Logs in, the server checks if it is the host or a client, and passes the right movie to the Players PlayerController.

    And now the big downside:
    When PostLogin gets called in GameInfo, with my approach GFxMoviePlayers Start() will get called on the same time.
    But when PostLogin gets called, the logged in Client has no GameReplicationInfo yet! Referencing it from Start() will return None.

    I did not find any other good solutions so I did some ugly Timer stuff here. Well, this way it does work.

    In YourGameInfo class:
    Code:
    event PostLogin( PlayerController NewPlayer ){
    	Super.PostLogin(NewPlayer);
    	if(YourPlayerController(NewPlayer)!=None){
    		if(WorldInfo.GetMapName()=="LobbyMenuMap"){
    			YourPlayerController(NewPlayer).OpenMenu('Lobby');
    			NewPlayer.SetCinematicMode(true, true, true, true, true, true);
    		}
    		else {	//Open our inGame HUD
    		
    		}
    	}
    }
    This makes sure the Lobby Movie only gets opened when you entered LobbyMenuMap, which is my "Multiplayer-Hub" Map.

    In YourPlayerController class:

    Code:
    var GfxMoviePlayer Menu;
    
    reliable client function OpenMenu(Name MenuName){
    	local GFxMoviePlayer MoviePlayer;
    	switch(MenuName){
    		case 'Lobby':
    			MoviePlayer = new class'YourLobbyMenu_GFxMoviePlayer';
    			break;
    		//case 'inGame':
    		//	break;
    		}
    	Menu=MoviePlayer;
    	MoviePlayer.Start();
    	SetTimer(0.5,true,'CatchGRI');
    	}
    Okay, now with this snippet you can load in and display any GFxMoviePlayer you want. Be sure to save it to a class member, or it gets garbage collected and will display only for a minute. And don't forget to define MovieInfo in your GFxMoviePlayers DefaultProperties. It needs a path to your swf.

    In the last line I put my timer function. It will loop until GameReplicationInfo is known by PlayerController and then calls another delegate.

    Code:
    function CatchGRI(){
    	if(YourGameReplicationInfo(WorldInfo.GRI)!=None){
    		DCatchedGRI(YourGameReplicationInfo(WorldInfo.GRI));
    		ClearTimer('CatchGRI');
    		return;
    	}
    }
    delegate DCatchedGRI(YourGameReplicationInfo GRI);
    Any class listening to this delegate will receive a GRI Reference when it is defined.

    GFxMoviePlayer: Catch the delegate and pass the Message to our Scaleform HUD

    The GFxMoviePlayer listens to this delegate and can finally save its reference!
    In YourLobbyMenu_GFxMoviePlayer class:
    Code:
    var YourGameReplicationInfo YGRI;
    
    function bool Start(optional bool StartPaused=false)
    {
    	Super.Start();
    	Advance(0);
    	YourPlayerController(GetPC()).DCatchedGRI = StartListenToGRI;
    	return true;
    }
    
    function StartListenToGRI(YourGameReplicationInfo Info){
    	YGRI = Info;
    	YGRI.DOnNewChatMessage = ReceiveChatMessage;
    }
    In StartListenToGRI you can now define any Eventhandler functions according to delegates fired in GameReplicationInfo, Including the message delegate!

    Last thing to do is passing the message back to the clients swf movie:
    Code:
    function ReceiveChatMessage(String NewChatMessage){
    	AS_ReceiveChatMessage(NewChatMessage);
    }
    
    function AS_ReceiveChatMessage(String NewChatMessage){
    	ActionScriptVoid("unreal_ReceiveChatMessage");
    }
    This seems a little redundant but becomes handy if you need to preprocess values received from the Server before sending them to Actionscript.
    Note that the ActionScriptVoid function has no arguments but the function name to call within actionscript.
    It will pass all arguments of its sourrounding function - AS_ReceiveChatMessage - instead.

    Log the received message in the lobbys chatwindow

    Place a TextArea CLIK-Component on the stage (right above the chatInput).
    Call it chatLog. Uncheck editable. In the field scrollBar, type in chatLogScrollbar.
    Now place a ScrollBar CLIK-Component next to it. Call it chatLogScrollbar.

    Now open the Actions Tab again ('F9').

    Place this code under the send ChatMessages Code:

    Code:
    //Receive ChatMessages
    function unreal_ReceiveChatMessage(Message:String):Void{
    	chatLog.text+=Message;
    	chatLog.text+="\n";
    	chatLog.position=chatLog.maxscroll;
    }
    Done!
    Now you should have a working Chatsystem, and a good starting point for more complex replication tasks.
    Thank you for reading,

    Philipp
    Last edited by Misk84; 07-15-2013 at 08:08 AM.


 

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  
Copyright ©2009-2011 Epic Games, Inc. All Rights Reserved.
Digital Point modules: Sphinx-based search vBulletin skin by CompletevB.com.