Verse Voting Script :)

I have been having a lot of fun learning verse this week! I am working on a few things I hope to share soon, but for now I wanted to make something that others might find useful to use or learn from.

Here is a simple voting system :slight_smile:

My version below has 3 vote/inputs with the end result of being teleported, in a game you could use this to choose between 3 areas/games between. In the event of a 2 way draw, the lowest of the 3 options is eliminated and the vote re-starts. In the event of a 3 way draw a random option is chosen.

I would love any feedback or advice on making this more efficient, or perhaps you spotted an edge case I had not yet considered? Any and all feedback welcome and appreciated :slight_smile:

using { /Verse.org/Native }
using { /Verse.org/Verse }
using { /Verse.org/Simulation }
using { /Fortnite.com/Devices}
using { /EpicGames.com/Temporary/Diagnostics }
using { /Verse.org/Random }

    <# ===
                UEFN Voting Script Version 1.0 by Kyle Boswell :) 
                Epic: Kyle Dev
                Twitter: @Dev__Kyle 
    === #>

log_vote_device:=class(log_channel){}

vote_device := class<concrete>(creative_device):
    Logger:log = log{Channel:=log_vote_device}

    # Devices:
    # (Buttons for voting)
    @editable
    Button_A:button_device = button_device{} # link button = Button_A
    @editable
    Button_B:button_device = button_device{} # link button = Button_B
    @editable
    Button_C:button_device = button_device{} # link button = Button_C
    @editable
    StartVote:button_device = button_device{} # link button = StartVote
    @editable
    VoteTimeDevice:timer_device = timer_device{} # link timer device = VoteTimeDevice
    # (Teleport to here when vote win)
    @editable
    TeleA:teleporter_device = teleporter_device{}
    @editable
    TeleB:teleporter_device = teleporter_device{}
    @editable
    TeleC:teleporter_device = teleporter_device{}
    # (Lights to visually see active votes)
    @editable
    LightA:customizable_light_device = customizable_light_device{} # link button = light A
    @editable
    LightB:customizable_light_device = customizable_light_device{} # link button = light B 
    @editable
    LightC:customizable_light_device = customizable_light_device{} # link button = light C 

    # Variables:
    var randomNumber: int = 0 # Var to hold random number
    # (Hold the totals of the vote)
    var totalA : int = 0
    var totalB : int = 0
    var totalC : int = 0
    # (True if draw betwwen 2 options)
    var drawAB : logic = false
    var drawBC : logic = false 
    var drawCA : logic = false
    # (True if A B or C win)
    var winA : logic = false
    var winB : logic = false 
    var winC : logic = false

    # FUNCTION: OnBegin - This will start at the start of the game.
    OnBegin<override>()<suspends>:void=
        Button_A.InteractedWithEvent.Subscribe(VotedA) # if player interacts with button A then run VotedA
        Button_B.InteractedWithEvent.Subscribe(VotedB) # if player interacts with button B then run VotedB
        Button_C.InteractedWithEvent.Subscribe(VotedC) # if player interacts with button A then run VotedC
        StartVote.InteractedWithEvent.Subscribe(VoteTimer) # if player interacts with StartVote button then run VoteTimer
        
    # FUNCTION: Voted A - This code will run when Button_A is interacted with by the player raising the total A count by 1.
    VotedA(Player:player):void=
        Logger.Print("=== VotedA Function Activated ===")
        set totalA = totalA + 1; # Add 1 to the total of A
        Logger.Print("=== | A = {totalA} | B = {totalB} | C = {totalC} | ===") # Write total to log

    # FUNCTION: Voted B - This code will run when Button_B is interacted with by the player raising the total B count by 1.
    VotedB(Player:player):void=
        Logger.Print("=== VotedB Function Activated ===")
        set totalB = totalB + 1; # Add 1 to the total of B
        Logger.Print("=== | A = {totalA} | B = {totalB} | C = {totalC} | ===") # Write total to log
    
    # FUNCTION: Voted C - This code will run when Button_C is interacted with by the player raising the total C count by 1.
    VotedC(Player:player):void=
        Logger.Print("=== VotedC Function Activated ===")
        set totalC = totalC + 1; # Add 1 to the total of C
        Logger.Print("=== | A = {totalA} | B = {totalB} | C = {totalC} | ===") # Write total to log
    
    # FUNCTION: Vote timer - This is a timer... when the timer ends it will run VoteResults()
    VoteTimer(Player:player):void=
        Logger.Print("=== VoteTimer Function Activated ===")
        VoteTimeDevice.Enable # Enable timer
        VoteTimeDevice.Start # Start timer
        VoteTimeDevice.SuccessEvent.Subscribe(VoteResults) # When timer ends, run the function VoteResults

    # FUNCTION: Vote results - This will calculate which vote is highest or if there is a draw or if there is no votes.
    VoteResults(Player:player):void=
        Logger.Print("=== VoteResults Function Activated ===")
        # Check if 3 way draw (or no one voted)
        if (totalA = totalB && totalB = totalC):
            Logger.Print("=== Its a 3 way draw ===")
            GetRandom(Player) # choose a random result
        # Check if 2 way draws
        else if ( totalA = totalB && totalA > totalC && totalB > totalC): # if A and B are equal, and A and B are higher than C
            Logger.Print("=== Its a draw between A and B ===")
            set drawAB = true # Confirm A and B is draw
            DrawVote(Player) # Run DrawVote Function
        else if ( totalB = totalC && totalB > totalA && totalC > totalA): # if B and C are equal, and B and C are higher than A
            Logger.Print("=== Its a draw between B and C ===")
            set drawBC = true # Confirm B and C is draw
            DrawVote(Player) # Run DrawVote Function
        else if ( totalC = totalA && totalC > totalB && totalA > totalB): # if C and A are equal, and C and A are higher than B
            Logger.Print("=== Its a draw between C and A ===")
            set drawCA = true # Confirm C and A is draw
            DrawVote(Player) # Run DrawVote Function
        # Check if wins
        else if ( totalA > totalB && totalA = totalC): # if A is highest
            Logger.Print("=== A is highest ===")
            set winA = true # set variable winA to true
            Logger.Print("=== Winner A ===")
            TeleA.Teleport(Player) # teleport players to TeleA
            DrawVote(Player) # run DrawVote to reset totals and timer
        else if ( totalB > totalA && totalB > totalC): # if B is highest
            Logger.Print("=== B is highest ===")
            set winB = true # set variable winB to true
            Logger.Print("=== Winner B ===")
            TeleB.Teleport(Player) # teleport players to TeleB
            DrawVote(Player) # run DrawVote to reset totals and timer
        else if ( totalC > totalB && totalC > totalA): # if C is highest
            Logger.Print("=== C is highest ===")
            set winC = true # set variable winC to true
            Logger.Print("=== Winner C ===")
            TeleC.Teleport(Player) # teleport players to TeleB
            DrawVote(Player) # run DrawVote to reset totals and timer
    
    # FUNCTION: DrawVote - This runs if there is a draw in the vote, the code resets the vote and the disables the lowest result so you can vote again between the two draw winners.
    DrawVote(Player:player):void=
        Logger.Print("=== DrawVote Function Activated ===")
        # Reset totals to 0 
        set totalA = 0;
        set totalB = 0;
        set totalC = 0;
        # If draw, disable the 3rd (lowest) option
        if (drawAB = true): # if A B draw           
            Button_C.Disable() # Disable button C
            LightC.TurnOff()
            Logger.Print("=== Button C disabled ===")
        else if (drawBC = true): # if B C draw
            Button_A.Disable() # Disable button A
            LightA.TurnOff()
            Logger.Print("=== Button A disabled ===")
        else if (drawCA = true): # if C A draw
            Button_B.Disable() # Disable button B
            LightB.TurnOff()
            Logger.Print("=== Button B disabled ===")
        VoteTimer(Player) # Restart the vote timer

    # FUNCTION: Choose random - This generates a random number betweeen 1 and 3 if there is a 3 way draw of if no one votes.
    GetRandom(Player:player):void=
        Logger.Print("=== GetRandom Function Activated ===")
        RandomNum:int = GetRandomInt(1, 3)
            Logger.Print("Picked Random Number. Number selected is {RandomNum}")
            set randomNumber = RandomNum
            if (randomNumber = 1):
                set winA = true
                TeleA.Teleport(Player)
                Logger.Print("=== Winner A ===")
            if (randomNumber = 2):
                set winB = true
                TeleB.Teleport(Player)
                Logger.Print("=== Winner Bb ===")
            if (randomNumber = 3):
                set winC = true
                TeleC.Teleport(Player)
                Logger.Print("=== Winner C ===")


    <# ===
                UEFN Voting Script Version 1.0 by Kyle Boswell :) 
                Epic: Kyle Dev
                Twitter: @Dev__Kyle 
    === #>
6 Likes

Hey Kyle, this is nice, thanks for posting!
Here are a couple of things:

  1. You should be able to use and instead of && (same with or instead of ||) in your conditional checks. It reads better and I believe using the && form will result in an error soon.
  2. In your GetRandom function, it seems you don’t need to save RandomNum to randomNumber. This would remove the need for the randomNumber variable altogether.
  3. Also in GetRandom, you may want to use else if for your conditions, since only one will be true. Another good option is a case expression:
case(RandomNum):
    1 =>
        set winA = true
        TeleA.Teleport(Player)
        Logger.Print("=== Winner A ===")
    2 =>
        set winB = true
        TeleB.Teleport(Player)
        Logger.Print("=== Winner Bb ===")
    3 =>
        set winC = true
        TeleC.Teleport(Player)
        Logger.Print("=== Winner C ===")
    _ => # Default case is needed to avoid failure
  1. We recommend naming variables in PascalCase (e.g. TotalA instead of totalA). We’re still working on a code-style guide to help the community align with best practices and common patterns.

More Voting Options using Arrays

Lastly, if you wanted to extend this to work with a customizable amount of buttons, you could convert the script to use arrays of device references.
Let’s see how you could do that.
Note: we have a gameplay example tutorial being published next week that explains a similar process in more detail, so I’ll go over this in less detail here.

First, you need an array (a “list”) of button references that you can modify from the editor.
Add these lines to your device class to expose an array of button devices:

@editable
Buttons:[]button_device = array{}

Note: you should do the same for other devices like your A, B, C lights

You’ll also need to store the total votes for each button. You can add another variable array of ints.

var TotalVotes:[]int = array{}

We’ll initialize this in a bit so the votes start from 0

A general Voting function

I see you’re familiar with functions already.
Let’s put these two pieces together by making a function that increases the votes for the button at a specific index. This function takes in an index and increases the TotalVotes at that index.

VotedButtonIndex(ButtonIndex:int):void=
    if:
        # Total will be the result of the set expression,
        # so we can print the total for this button.
        Total := set TotalVotes[ButtonIndex] += 1
    then:
        Logger.Print("Button{ButtonIndex} total votes: {Total}")

How do we pass in the correct ButtonIndex? We must find a way to subscribe an event handler to the InteractedWithEvent that also has knowledge about which button from the Buttons array called the handler.
That means we’ll have a mapping from Buttons[X] to TotalVotes[X].

I’m going to show two ways to achieve the same result, so you’re exposed to different approaches you could use.

Option A: Using a Custom Event Handler

As said earlier, we need a way to pass along which button (the index X of Buttons[X]) triggered the InteractedWithEvent.
Let’s create a class that encapsulates all this info.
I’m calling it event_handler here, but you’re free to name it however you’d like.
Add this outside the vote_device class.

event_handler := class:
    ButtonIndex:int
    # We need a reference to a vote_device to call functions on it.
    VoteDevice:vote_device
    HandlerFunction(Player:player):void=
        VoteDevice.VotedButtonIndex(ButtonIndex)

As you can see, when we create an object of this class, we’ll save in it a ButtonIndex, and a reference to a vote_device (the one placed in the level in this case).
The HandlerFunction is what we’ll Subscribe to each Button’s InteractedWithEvent, and that’s why its signature expects a Player:player passed in.
HandlerFunction just calls the VotedButtonIndex function of the VoteDevice we’ve defined earlier, passing along the ButtonIndex.

Almost there.

Creating a handler object for each button

Now, we just need to go through the Buttons and, for each one of them, InteractedWithEvent.Subscribe the HandlerFunction of an object of the custom event_handler class we’ve created.
We should do this when the game starts:

OnBegin<override>()<suspends>:void=
    for (X -> Button:Buttons): # X -> gives us the index of Button in the Buttons array.
        set TotalVotes += array{0} # Add a new element to the votes array initialized to 0.
        # Create a new handler that'll hold this button's data.
        Handler := event_handler:
            ButtonIndex := X
            # Self is a special keyword that references this device (the enclosing class) so the event handler can call functions on it.
            VoteDevice := Self
        Button.InteractedWithEvent.Subscribe(Handler.HandlerFunction)

That’s it! Each button will have its own custom event handler, to which you can add any data you need.

Option B: Using Concurrency

Note: I’m unsure if it’s available in the build you’re using yet. If not, it should come soon!

This can be a somewhat simpler approach, but it could take some time to wrap your head around it while you’re learning concurrency and thinking asynchronously (it sure took some time for me!).

We’ve recently exposed the ability to Await() device events like InteractedWithEvent. Awaiting means pausing the currently executing <suspends> context (like a function) until what we’re awaiting is ready.
In this case, we’ll Await() until the InteractedWithEvent is fired.

The idea is to spawn a function, passing in a Button and its ButtonIndex, and inside the function, Await() for the InteractedWithEvent to fire.
After that happens, we call the VotedButtonIndex created earlier.
We do this in a loop to keep awaiting InteractedWithEvent.

Effectively, what spawn does is create another execution context that “splits” off and runs concurrently to the function it’s called from.
Note: we must use spawn here for now, but in the future, we recommend using branch instead for performance reasons. branch can’t be used inside for and loop at the moment.

The function we’ll spawn looks like:

HandleVoteButton(Button:button_device, ButtonIndex:int)<suspends>:void=
    loop:
        Button.InteractedWithEvent.Await() # Execution stops here until the button is pressed
        Logger.Print("Button {ButtonIndex} pressed")
        VotedButtonIndex(ButtonIndex)

Notice the <suspends> effect: saying this is an async context. It means we can use Await inside of it.

Spawning HandleVoteButton

We can now use the HandleVoteButton instead of the custom event_handler, similar to what we did earlier.
We spawn a HandleVoteButton for each Button in the Buttons array:

OnBegin<override>()<suspends>:void=
    for (X -> Button:Buttons):
        set TotalVotes += array{0} # Add a new element to the votes array initialized to 0
        spawn{ HandleVoteButton(Button, X) }

Conclusion

Phew, we made it!
This is just part of what you’ll need if you want to implement a customizable voting system that can be extended to any number of options.
I’ll leave it to you to implement the missing parts if you’d like:

  • Lights and teleporters arrays
  • VoteResults

This system can actually make it easier to work with draws as you can iterate over the TotalVotes array to find either:

  • A single winner
  • Multiple draws

As a hint, I suggest looking at the for documentation page and imagine how you could use the for expression result and filtering.

I hope this was informative, please feel free to reach out if you have any questions or doubts :slight_smile:

Relevant code

event_handler := class:
    ButtonIndex:int
    # We need a reference to a vote_device to call functions on it.
    VoteDevice:vote_device
    HandlerFunction(Player:player):void=
        VoteDevice.VotedButtonIndex(ButtonIndex)
...
...
# Inside vote_device
@editable
Buttons:[]button_device = array{}
var TotalVotes:[]int = array{}

OnBegin<override>()<suspends>:void=
    for (X -> Button:Buttons):
        set TotalVotes += array{0} # Add a new element to the votes array initialized to 0
        # Create a new handler that'll hold this button's data.
        Handler := event_handler:
            ButtonIndex := X
            # Self is a reference to this device so the event handler can call functions on it.
            VoteDevice := Self
        Button.InteractedWithEvent.Subscribe(Handler.HandlerFunction)
        # spawn{ HandleVoteButton(Button, X)}
        
    # PlayerRef := StartVote.InteractedWithEvent.Await() # You could also use this ;)

HandleVoteButton(Button:button_device, ButtonIndex:int)<suspends>:void=
    loop:
        Button.InteractedWithEvent.Await()
        Logger.Print("Button {ButtonIndex} pressed")
        VotedButtonIndex(ButtonIndex)

VotedButtonIndex(ButtonIndex:int):void=
    if:
        # Total will be the result of the set expression,
        # so we can print the total for this button
        Total := set TotalVotes[ButtonIndex] += 1
    then:
        Logger.Print("Button{ButtonIndex} total votes: {Total}")

P.S. There are improvements coming that’ll make it easier to match the number of elements between all the arrays you need (Lights, Buttons, Teleporters); for now, you’ll need to match them in-editor.
P.P.S In the future, I’ll post a countdown timer class that can serve as a simple replacement for a timer device. All Verse, plus UI.

5 Likes

Thank you so much for this very detailed response and guide, this is all very helpful!

Good idea using case expressions for the random number, I will definitely implement that for ease of readability - is there any advantage to use case over if else? I guess it would run faster but how would that relate in terms of Fortnite? Would inefficient code cause a lower frame rate or anything alike?

Also is there any advantage/benefit in using… ( if then )

if:
    # condition = true
then:
    # do this
else:
    # do this

… instead of … ( if else )?

if (condition = true)
    # do this
else if 
    # do this
else:
    # do this

I understand most of what you write about the custom event handler I think… I will have a play around with it and read the docs to get a better grasp on how classes work in verse.

The for looping through arrays is so much easier in your example than how C# handles it like… for (initializer; condition; iterator)
Is there a way to control how we iterate through arrays, for example every 2nd element instead of each one (i+2 instead of i++)?

Option B, using concurrency seemed the simplest of the 2 options so I will give that a go and let you know how I get on.

I will update this with your suggestions and be back soon with an update, and probably more questions :slight_smile:

Thank you again!

1 Like

Hi Kyle,

The main difference here is just the syntax (what it looks like), rather than what it does.
Your first example used:

if:
    # condition = true
then:
    # do this
else:
    # do this

This form is handy if your ‘if’ condition is very complex and it’s nicer to read if you break it up into multiple lines. But it is functionally equivalent to

if (condition = true)
    #do this
else:
    #do this

In the second form you simply don’t have to write the ‘then’ as the entire condition is contained in the parentheses after the ‘if’ as a single-line form. These both do ‘exactly’ the same thing.

In your second example you wrote:

if (condition = true)
    # do this
else if (someothercondition = true)
    # do this
else:
    # do this

You could write this to do exactly the same thing like this also:

if: 
    condition = true
then:
    # do this
else if:
    SomeOtherCondition = true
then:
    # do this
else:
    # do this

Really it just comes down to what’s nicest to read for the code you’re writing. Personally I usually stick to the form without the ‘then’ clause, but it’s all functionally equivalent.

4 Likes

Hi! I was wondering if this code still works :thinking:

Unfortunately not, this was written during the Alpha testing phase a lot has changed since this time. Here is a recommended video to learn this from: https://www.youtube.com/watch?v=1UqqAxpXVhM&t=286s

I’ve ben looking for a while but have had no luck, I’m looking for a ui voting system to make a pop up system, i have 0 experience with coding so i cant make one. Is it possible for me to find a ready code? or is it allowed for me to pay someone to do it? i need some advice on this matter.