article

AndresGenvid avatar image
AndresGenvid posted

05. Unreal C++ Commands & Events Guide   

Unreal Commands & Events Guide

This guide was written as a quick reference step by step guide for setting up a simple Unreal command/event. For more in depth information on each step please check the documentation at https://www.genvidtech.com/


Stop Command (C++)

  1. Create a C++ class that inherits from GameState if you have not already

    • Create a blueprint object of this GameState

    • Set the BP game state as the default game state in your game mode

  2. We need to reparent the clock BP to a C++ class. Create a C++ class of type Actor for your clock. Then reparent the clock BP to this class.

  3. In your Genvid Commands C++ class create the following functions:

    • Override “BeginPlay”

    • “StopCommand” which returns a FGenvidCommand and takes no parameters

    • “OnCommandStop” which returns void and takes two parameters - “const FString& CommandID” and “const FString& FullCommand”

  4. In “BeginPlay” add the following code:

  5.     Commands.Add(StopCommand());
        Super::BeginPlay();
    • We call the function we created called “StopCommand” and add the result to an existing property in the parent class called Commands. Then we call the parent BeginPlay function

  6. In “StopCommand” add the following code:

  7.     FGenvidCommand Stop;
        Stop.Replicated = true;
        Stop.Name = "Stop";
        Stop.OnGenvidCommandDelegate.BindUFunction(this, "OnCommandStop");
    
        return Stop;
  8. We create a FGenvidCommand and set some values on it before binding to the “OnCommandStop” function we created

    • The name will be the name of the command. See the documentation for more info on “replicated”

  9. In “OnCommandStop” add the following code:

  10.     AMyGameState* GameState = GetWorld()->GetGameState<AMyGameState>();
        GameState->MulticastConfirmCommandStop(FullCommand);
    • AMyGameState is whatever the game state you created is called

    • The function we are calling does not exist yet. We will create it next

  11. In your newly created Game State class create a function called “MultiCastConfirmCommandStop” with a return value of void and an input parmeter of “const FString& FullCommand”. Additionally give it a UFUNCTION tag with “NetMulticast” and “Reliable”.

    • See the documentation for what these tags are doing

  12. Because of the nature of RPC functions when actually implementing the function your implementation will be named “MulticastConfirmCommandStop_Implementation”. Add the following code to the implementation:

  13. UMyGameInstance* GameInstance = Cast<UMyGameInstance>(GetGameInstance());
    GameInstance->CommandStop();
    • Normally this is where you would check to make sure a valid command is being sent and also that it was sent from a valid location. But since this is just a test running locally we skip that and call a function that will take care of the actual command logic. This function will be created next

  14. In your Game Instance class create a function called “CommandStop”. It takes no parameters and has a return type of void. Add the following code:

  15.     TArray<AActor*> ClockActorArray;
        UGameplayStatics::GetAllActorsOfClass(GetWorld(), AMyClock::StaticClass(), ClockActorArray);
    
    
        if (ClockActorArray.Num())
        {
          
            Cast<AMyClock>(ClockActorArray[0])->StopClock = true;
        }
    • AMyClock is whatever C++ class you created for your clock

    • We look for any clocks in the level (there should be only one) then set a property to true. We will create this property next

  16. In your clock C++ class create a boolean called “StopClock”. Make it a Blueprint readable UPROPERTY.

  17. In your clock blueprint - open up the event graph and go to your tick. Add a check to stop clock before doing anything in the tick. If it’s true then don’t do anything. If it’s false then continue as normal

  18. 1668554764098.png

  19. Pulling this all together - we bind a function to a “Stop” command. That function eventually finds its way to the clock actor and flips a bool that causes it to stop ticking


Stop Command (Web)

  1. In your web/config folder open up the web.hcl and make sure the “admin” link templates all end in “/admin”. Then in your web/public folder create an “admin.html” file.

  2. Add the following code to the new html file:

  3. <!doctype html>
    <html>
    <head>
        <title>Genvid Overlay</title>
        <link rel="stylesheet" href="style.css">
        <script src="genvid.umd.js"></script>
        <script src="genvid-math.umd.js"></script>
        <script src="overlay-admin.js"></script>
    </head>
    <body style="background:black">
        <div id="video_player"></div>
        <div id="overlay">
            <tr> 
                <td><button class='commandButton' id='admin_stop_button'>STOP</button></td>
            </tr>
        </div> 
    </body>
    </html>
  4. We are creating a button with the text “STOP” on the admin link

  5. We reference a new .js file named “overlay-admin.js” file. We will create that next

  6. In the public folder create a file named “overlay-admin.js”. This will be the javascript file that will back the admin version of the steam

  7. Add the following code to the new js file:

  8. class AdminController {
         
      constructor(videoPlayerId) {
          
        this.videoPlayerId = videoPlayerId;
      }
    
      start() {
          
        fetch("/api/public/channels/join", {
          
          method: "POST",
        })
          .then((data) => data.json())
          .then((res) => this.onChannelJoin(res))
          .catch((error) => genvid.error(`Can't get the stream info: ${error}`));
      }
    
      onChannelJoin(joinRep) {
          
        this.client = genvid.createGenvidClient(
          joinRep.info,
          joinRep.uri,
          joinRep.token,
          this.videoPlayerId
        );
        let StopButton = document.getElementById(
            "admin_stop_button"
        );
                
        StopButton.addEventListener(
            "click",
            () => {
          
                this.stopClock();
           },
           false
        );
        this.client.start();
      }
      
      sendCommands(bodyCommands, successMessage, errorMessage) {
          
        fetch("/api/admin/commands/game", {
          
          method: "POST",
          body: JSON.stringify(bodyCommands),
          headers: {
          
            "Content-Type": "application/json",
          },
        })
      }
      
      stopClock(){
          
          const commands = {
          
              id: "Stop",
              value: "Clock:Stop",
          };
        const successMessage = "Command sent.";
        const errorMessage = "Stop Clock Error";
    
    
        this.sendCommands(commands, successMessage, errorMessage);
      }
    }
    
    let admin = new AdminController("video_player");
    admin.start();
  9. Notice this file is a bit different then the last js file. This version gives us more control over the controller class. We will be updating the other js file in a similar fashion later

  10. We bind a function that sends a command with the id “Stop” to a button click. Recall we named our command “Stop” in the project. This is how they connect

  11. Go through the steps of setting up the local cluster, package the project (or play in editor), and start up the services

  12. Click on the admin link. The username and password will both be “admin”

  13. Click on the stop command. The clock should immediately stop ticking in the project and eventually stop on stream


Admin stream with "STOP" button

Color Change Event (C++)

  1. In your Genvid Events C++ class create the following functions:

    • Override “BeginPlay”

    • “ColorChangeEvent” which takes no parameters and returns a “FGenvidEvent”

    • “OnEventColorChange” which takes two parameters - “const FString& eventName” and “const FGenvidEventSummary& summary” with a return of void

  2. In “BeginPlay” add the following code:

  3.     Events.Add(ColorChangeEvent());
        Super::BeginPlay();
  4. We add the return value from the “ColorChangeEvent” to a property that already exists on the parent Genvid Events class then run the parent begin play.

  5. In “ColorChangeEvent” add the following code:

  6.     FGenvidEvent ColorChange;
        ColorChange.Replicated = true;
        ColorChange.Name = "colorChange";
        ColorChange.OnGenvidEventDelegate.BindUFunction(this, "OnEventColorChange");
    
        return ColorChange;
  7. Create the Event and name it “colorChange”. Then bind it to the function “OnEventColorChange”

  8. See the documentation for more information on “Replicated”

  9. In “OnEventColorChange” add the following code:

  10.     AMyGameState* GameState = GetWorld()->GetGameState<AMyGameState>();
        GameState->MulticastConfirmEventColorChange(summary);
  11. AMyGameState is your game state C++ class

  12. The function we are calling does not exist yet. We will create it next

  13. In your game state class create the function “MulticastConfirmEventColorChange” with a return value of void and a single parameter of “const FGenvidEventSummary& summary”. Mark it with the UFUNCTION tags “NetMulticast” and “Reliable”

  14. Since this is an RPC call the implementation of the function will be called “MulticastConfirmEventColorChange_Implementation”. Add the following code to it:

  15.     int PinkEvents = 0;
        int BlackEvents = 0;
        for (FGenvidEventResult Result : summary.results)
        {
          
            if (Result.key.fields[0] == "pink")
            {
          
                for (FGenvidEventValue Values : Result.values)
                {
          
                    if (Values.reduce == EGenvidReduceOp::Count)
                    {
          
                        PinkEvents += Values.value;
                    }
                }
            }
            else if (Result.key.fields[0] == "black")
            {
          
                for (FGenvidEventValue Values : Result.values)
                {
          
                    if (Values.reduce == EGenvidReduceOp::Count)
                    {
          
                        BlackEvents += Values.value;
                    }
                }
            }
        }
    
        bool BlackColor = (BlackEvents > PinkEvents);
    
        UMyGameInstance* GameInstance = Cast<UMyGameInstance>(GetGameInstance());
        GameInstance->EventColorChange(BlackColor);
  16. Slightly more complicated than a command as these are map-reduced as they are scalable to mass amounts of people

  17. We check to see if the key of the “colorChange” event is “black” or “pink”. Then we add it’s count to a counter

  18. If Black is larger we set a boolean to true. Otherwise it’s false. Then we pass that boolean to a function we will create next

  19. In the GameInstance class create “EventColorChange” with a return of void and a parameter of “bool Black”. Add the following code:

  20.     TArray<AActor*> ClockActorArray;
        UGameplayStatics::GetAllActorsOfClass(GetWorld(), AMyClock::StaticClass(), ClockActorArray);
    
    
        if (ClockActorArray.Num())
        {
          
            Cast<AMyClock>(ClockActorArray[0])->ColorChange(Black);
        }
  21. Here we look for our clock and call the function “ColorChange” with the bool passed in. We will create this function next

  22. In the C++ class that the clock is parented to create a function named “ColorChange” with a return of void and a parameter of “bool Black”

    • Give it a UFUNCTION tag of “BlueprintImplementableEvent”

  23. In the unreal editor create a new material. Make it pink.

  24. In the clock blueprint implement “ColorChange”. Add the following blueprint code:

    • If the clock was imported you will only need to override the ColorChange event and attach to the existing blueprint code

Color Change Event (Web)

  1. Go to your web/public folder

  2. Open your index.html file. Add the following code after the existing streams, notifications, and annotations:

  3.         <div>
                <button class='eventButton' id='pink_button'>Pink</button>
            </div>
            <div>
                <button class='eventButton' id='black_button'>Black</button>
            </div>
  4. We Declare two buttons. One with the text “Pink” the other with the text “Black”

  5. Open your overlay.js file. Replace the contents with the following code:

  6. var genvidClient;
    class WebController {
          
        constructor(videoPlayerId) {
          
            this.videoPlayerId = videoPlayerId;
        }
        
        start() {
          
        fetch("/api/public/channels/join", {
          
          method: "POST",
        })
        .then(function (data) { return data.json() })
        .then((res) => this.onChannelJoin(res))
        .catch((error) => genvid.error(`Can't get the stream info: ${error}`));
        }
    
    
      onChannelJoin(joinRep) {
          
        this.genvidClient = genvid.createGenvidClient(
          joinRep.info,
          joinRep.uri,
          joinRep.token,
          this.videoPlayerId
        );
    
    
        this.genvidClient.onStreamsReceived(function (dataStreams) {
          
            for (let stream of [...dataStreams.streams, ...dataStreams.annotations]) {
          
                for (let frame of stream.frames) {
          
                    try {
          
                        frame.user = JSON.parse(frame.data);
                    }
                    catch (e) {
          
                        console.log(e, frame.data);
                    }
                }
            }
        });
                    
        this.genvidClient.onVideoPlayerReady(function () {
          
            console.log("Video Player is Ready");
        });
    
        this.genvidClient.onDraw(function (frame) {
          
            let gameDataFrame = frame.streams["ClockTime"];
            if (gameDataFrame && gameDataFrame.user) {
          
                update(gameDataFrame.user);
            }
            
            if ("ClockAnnotation" in frame.annotations && frame.annotations["ClockAnnotation"].length){
              
    
                for (let gameAnnotation of frame.annotations["ClockAnnotation"])
                {
          
                    if (gameAnnotation && gameAnnotation.user)
                    {
          
                        annotationUpdate(gameAnnotation.user);
                    }
                }
            }
        });
            
        this.genvidClient.onNotificationsReceived(function (notifications) {
          
            for (let notification of notifications.notifications) {
          
                try {
          
                    notification.user = JSON.parse(notification.data);
                    notificationUpdate(notification.user);
                }
                catch (e) {
          
                    console.log(e, notification);
                }
            }
        });    
        
        let PinkButton = document.getElementById(
            "pink_button"
        );
                
        PinkButton.addEventListener(
            "click",
            () => {
          
                this.onColorChange("pink");
           },
           false
        );
        
        let BlackButton = document.getElementById(
            "black_button"
        );
                
        BlackButton.addEventListener(
            "click",
            () => {
          
                this.onColorChange("black");
           },
           false
        );
        
        this.genvidClient.start();
      }
        
        onColorChange(color) {
          
        this.genvidClient.sendEventObject({
          
          colorChange: color,
        });
        }
    }
    
    function update(data) {
          
        document.getElementById("hour").innerHTML = "Hour: " + Math.round(data.hour);
        document.getElementById("minute").innerHTML = "Minute: " + Math.round(data.minute);
        document.getElementById("second").innerHTML = "Second: " + Math.round(data.second);
    }
    
    function notificationUpdate(data) {
          
        document.getElementById("notification").innerHTML = "Notification: " + Math.round(data.second);
    }
    
    function annotationUpdate(data) {
          
        document.getElementById("annotation").innerHTML = "Annotation: " + Math.round(data.second);
    }
    
    let web = new WebController("video_player");
    web.start();
  7. Don’t be intimidated by the changes. Most of it has remained the same - just restructured in a way that gives us more control.

  8. Note there is some new code related to events

    • We get the pink button and black button and bind to a function named “onColorChanged” but pass a different string value

      • This is the string value we look for in MulticastConfirmEventColorChange_Implementation

    • onColorChange sends the string with the key “colorChange”. Recall we named our event in the C++ “colorChange” as well.

  9. Go to your web/config folder and open events.json. Replace the contents with the following code:

  10. {
         
      "version": "1.7.0",
      "event": {
          
        "game": {
          
          "maps": [
            {
          
              "id": "colorChange",
              "source": "userinput",
              "where": {"key": ["colorChange"], "name": "<color>", "type": "string"},
              "key": ["colorChange", "<color>"], 
              "value": 1
            }
          ],
          "reductions": [
            {
          
              "id": "colorChange",
              "where": {"key": ["colorChange", "<color>"]},
              "key": ["<color>"],
              "value": ["$count"],
              "period": 250
            }
          ]
        }
      }
    }
  11. Update your “version” number if necessary

  12. This sets up the map reduce of the “colorChange” event

    • See the documentation for more info on map reduce. The important part is multiple instances of “pink” and “black” are put together and counted

  13. Copy the events.json file to your config folder that is one step above your project

    • This is where “game.hcl” and “sample.hcl” live

    • Both the web and client need to know about the event format

    • Go through the steps of setting up the local cluster, package the project (or play in editor), and start up the services

    • Click on the non-admin link

    • Click on the pink or black. The clock hands will shift color depending on what is pressed. If multiple sessions were connected and clicking at the same time, whatever had the most clicks per map-reduce would win out.


Stream with the pink and black event buttons

Events Improvements (C++)

  1. The event is functional. But we can do better.

  2. We will send notifications updating the current bid count of each event and instead only update when the clock strikes a 15 second interval

  3. Open your Game State C++ file

  4. Replace your “MulticastConfirmEventColorChange_Implementation” with the following code:

  5.     for (FGenvidEventResult Result : summary.results)
        {
          
            if (Result.key.fields[0] == "pink")
            {
          
                for (FGenvidEventValue Values : Result.values)
                {
          
                    if (Values.reduce == EGenvidReduceOp::Count)
                    {
          
                        PinkEvents += Values.value;
                    }
                }
            }
            else if (Result.key.fields[0] == "black")
            {
          
                for (FGenvidEventValue Values : Result.values)
                {
          
                    if (Values.reduce == EGenvidReduceOp::Count)
                    {
          
                        BlackEvents += Values.value;
                    }
                }
            }
        }
  6. We no longer reset the pink and black vote count

  7. We no longer call the instance function to update the color of the clock hands

  8. PinkEvents and BlackEvents are now class properties

  9. In your header file define “PinkEvents” and “BlackEvents” as public class properties of type int

  10. Create a new function called “ColorChange” that takes no inputs and returns void. Copy the following code into it:

  11.     if (BlackEvents == PinkEvents)
        {
          
            return;
        }
    
        bool BlackColor = (BlackEvents > PinkEvents);
    
        UMyGameInstance* GameInstance = Cast<UMyGameInstance>(GetGameInstance());
        GameInstance->EventColorChange(BlackColor);
  12. When called this function does nothing if the values are the same. Otherwise it updates to the greater of the two values

  13. In your Genvid Stream C++ class go to your “TickComponent”. Change the if statement that flips the notification and annotation booleans to the following:

  14.     if (PreviousUsedSecond != CurrentSecond && CurrentSecond % 15 == 0)
        {
          
            PreviousUsedSecond = CurrentSecond;
            ShouldSendNotification = true;
            ShouldSendAnnotation = true;
            GetWorld()->GetGameState<AMyGameState>()->ColorChange();
        }
  15. We now call the new function we just created on the game state to update the colors of the hands every 15 seconds

  16. Now we will add a notification stream for the bid numbers

  17. Create “ClockColorBidNotification” that takes in no parameters and returns an “FGenvidStream”. Add the following code to it:

  18.  FGenvidStream ColorBidNotification;
    ColorBidNotification.Name = "ColorBidNotification";
    ColorBidNotification.OnGenvidStreamDelegate.BindUFunction(this, "OnSubmitColorBidNotification");
     ColorBidNotification.Framerate = 30;
    
    
     return ColorBidNotification;
  19. We create a stream called “ColorBidNotification”

  20. We bind it to a function “OnSubmitColorBidNotification” which we will create later

  21. In “BeginPlay” add the new stream the same way the other streams are added “Streams.Add(ClockColorBidNotification());

  22. In the header file for your Genvid Streams create the following struct that will hold the data for this new notification:

  23. USTRUCT(BlueprintType)
    struct FColorBids
    {
          
        GENERATED_BODY()
    
    public:
        UPROPERTY(BlueprintReadWrite, EditAnywhere)
        int32 Pink = 0;
    
        UPROPERTY(BlueprintReadWrite, EditAnywhere)
        int32 Black = 0;
    
    };
  24. Create “OnSubmitColorBidNotification”. It takes in a parameter named “Id” of type FString& and returns void. Add the following code:

  25.     FColorBids ColorBids;
        AMyGameState* MyGameState = GetWorld()->GetGameState<AMyGameState>();
        ColorBids.Black = MyGameState->BlackEvents;
        ColorBids.Pink = MyGameState->PinkEvents;
        FString ColorBidsString;
        FJsonObjectConverter::UStructToJsonObjectString(ColorBids, ColorBidsString);
    
        SubmitNotification(Id, ColorBidsString);

Events Improvements (Web)

  1. We now have a notification being sent to us we want to intercept. However, first some general improvements.

  2. First open the style.css file and add the following classes:

  3. .btn {
         
        opacity: 0.7;
        border-radius: 50%;
        padding: 12px;
        margin: 12px;
        width:16px;
    }
    
    .black {
          
        background-color:rgb(0, 0, 0);
        border-color:rgb(251,72,196);
    }
    
    .pink {
          
        background-color:rgb(251,72,196);
        border-color:rgb(0, 0, 0);
    }
  4. We will now have colored buttons that users can click on for the black and pink bids

  5. Open index.html. Replace the contents with the following:

  6. <!doctype html>
    <html>
    <head>
        <title>Genvid Overlay</title>
        <link rel="stylesheet" href="style.css">
        <script src="genvid.umd.js"></script>
        <script src="genvid-math.umd.js"></script>
        <script src="overlay.js"></script>
    </head>
    <body style="background:black">
        <div id="video_player"></div>
        <div id="overlay">
            <div class="child" id="hour">Hour:</div>
            <div class="child" id="minute">Minute:</div>
            <div class="child" id="second">Second:</div>
            <div class="child" id="notification">Notification:</div>
            <div class="child" id="annotation">Annotation:</div>
            <div>
                <button class='btn pink' id='pink_button'></button>
            </div>
            <div class="child" id="PinkBid">Pink Bids:</div>
            <div>
                <button class='btn black' id='black_button'></button>
            </div>
            <div class="child" id="BlackBid">Black Bids:</div>
        </div>
    </html>
  7. We now have a place to display the number of bids for each color next to the buttons

  8. Additionally we added some default values to all the streams

  9. We are now ready to receive the new notification. Open “overlay.js”

  10. Replace the onNotificationsReceived override with the following:

  11.     this.genvidClient.onNotificationsReceived(function (notifications) {
         
            for (let notification of notifications.notifications) {
          
                try {
          
                    notification.user = JSON.parse(notification.data);
                    if (notification.id == "ClockNotification")
                    {
          
                        notificationUpdate(notification.user);
                    }
                    else if (notification.id == "ColorBidNotification")
                    {
          
                        colorBidUpdate(notification.user);
                    }
                }
                catch (e) {
          
                    console.log(e, notification);
                }
            }
        });    
  12. We now only call notificationUpdate if the id is “ClockNotification”

  13. There is a new call to a function we will create below called “colorBidUpdate” if the notification has the name “ColorBidNotification”

  14. Add the following function at the bottom of the overlay.js file:

  15. function colorBidUpdate(data) {
         
        document.getElementById("PinkBid").innerHTML = "Pink Bids: " + Math.round(data.pink);
        document.getElementById("BlackBid").innerHTML = "Black Bids: " + Math.round(data.black);
    }
  16. We use the notification to update our overlay with the current bid numbers

  17. Run through the usual steps detailed in the web guide to deploy the client and local cluster. You can now vote for the colors and have their values updated in real time. Additionally the colors only change when a clock hand hit’s a 15 second mark. However the votes are tallied later due to the stream delay.


Stream with event improvements

unreal engineunreal
1668554764098.png (19.1 KiB)
10 |600

Up to 8 attachments (including images) can be used with a maximum of 1.0 MiB each and 10.0 MiB total.

Article

Contributors

AndresGenvid contributed to this article