Verse Voting Script :)

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