Character Selection
Overview
The Fighting Sample implements a Character Selection mode inside Quantum itself to:
- Simplify the transition from Character Selection to Gameplay; and,
- Enforce character selection rules in a completely deterministic manner.
N.B.: This approach can be used for any game with rule based character selection (e.g.: MOBA, hero based shooters, etc…). It can also be adjusted for map selection votes at the end of a round.
Character Selection
The character selection’s visual feedback and rule set are implemented separately. The former is done in Unity and the latter in Quantum. This page will go through the different aspects of the character selection setup and how they relate to each other.
Unity
The game scene in Unity includes the following parts of the character selection:
- The selection UI;
- Character Selection entity prototypes; and,
- Callbacks for visual feedback reflecting the selection decisions made by the player.
All these elements can be found in the Scene under the CharacterSelect
object (Side Bar and UI Camera > Camera > Foreground UI Canvas > CharacterSelect
).
Selection UI
The character selection is presented to the player using character profile pictures. These are composed of:
- a circle mask
- a square picture of the available characters
- a colored circle to highlight the current selection
The selection UI is completely passive. The selection order and available characters it represents is driven by the Character Select prototypes and the selection highlighting is handled by the CharacterSelectCallback
scripts on the Character Select Callback object.
Character Selection Prototype
The character selection UI P1 and P2 in Unity are purely visual and do not impact the underlying logic found in the CharacterSelectSystem
on the Quantum side. The information needed by the CharacterSelectSystem
is held by the Character Select Prototypes P1 and P2 respectively.
The Character Select Prototype is made of only two components:
Entity Prototype
; and,CharacterSelect
component.
The Entity Prototype
component is required for any and all types of entity prototypes, while the CharacterSelect
component is specifically designed to hold the information about the selectable fighters and audio-visual feedback tied to the selection interactions.
Note: The Character Select Prototype is used to create an entity that will only ever be used to hold a game state in Quantum and thus does not need any visual representation in Unity; as such it does have an EntityView associated with it.
The fields configurable in the editor are:
Fighter Index
: the PlayerRef which will be able to control that Character Select Prototype. In the Fighting Sample the indices 0 and 1 are mapped to players 1 and 2 respectively. For more information on how PlayerRefs are assigned to players, please read the documentation in Manual > Player.Fighter Choices
: This field holds a list of FighterDataAsset. Each FighterDataAsset represents a selectable character. N.B.: The order in which they are listed is the selection order used by the CharacterSelectSystem in Quantum. Since the Selection UI is decoupled, the order of the character profiles in the Selection UI and the FighterDataAssets in the CharacterSelect component need to be matched manually!Toggle Left / Right SFX
: The SFX triggered when moving the selection left / right.Select SFX
: The SFX triggered when a player confirms their character selection.
All other fields are set and manipulated by the CharacterSelectSystem in the quantum simulation.
Visualization of Selection
The CharacterSelectCallback
script handles all player selection visualization feedback. It inherits from the SubscriptionBehaviour
base class which is a simple utility parent class that handles the interfacing with the Quantum Event and Callback (un)subscription.
UI Elements Setup
Before the CharacterSelectCallback
script can get to work, it needs to know of the UI elements it can / should manipulate at runtime.
First of all, the amount of CharacterSelectSet
s needs to be set; in the Fighting Sample it is set to 2 since it is meant for 2 players by default. Each CharacterSelectSet
takes a Main Object which is the object under which the selectable character pictures placed - P1 and P2 in the case at hand - and a list of Selection Components which is the highlighting circle placed in the form of a panel alongside the character picture.
N.B.: The character picture highlighting panels have to be sorted in the same as the Fighter Choices in the CharacterSelect component!
Finally, the CharacterSelectCallback
requires a reference to the Main Header and Spectating Text which will be displayed while the selection is ongoing.
- The Main Header will be presented until both players have finalized their selection.
- The Spectating Text will be displayed after the local player has confirmed their character selection and while they are waiting for the remote player to confirm theirs.
Logic
In the case of CharacterSelectCallback
, it is registering and listening to the CallbackUpdateView
callback. This particular callback is triggered whenever the simulation (Quantum) hands over control to the view (Unity). At this moment, the script grabs the latest verified Frame from Quantum.Game
. The frame can then be used to filter components, iterate over the resulting collection and poll their information.
N.B.: It is possible to poll from the frame state. It is, however, NOT allowed to write to it from Unity. This would be indeterministic and result in simulation desynchronisation.
Based on the information polled from the frame, the CharacterSelectCallback
script can then enable or disable the necessary UI elements.
C#
protected override void SubscriptionDispatch(CallbackUpdateView result){
var frame = result.Game.Frames.Verified;
var charSelect = frame.Filter<CharacterSelect>();
bool isSelecting = false;
bool isSpectating = false;
while (charSelect.Next(out var e, out var cs)) {
var obj = characterSelectSets[cs.fighterIndex];
obj.mainObject.SetActive(cs.isSelecting);
for (int i = 0; i < obj.selectionComponents.Length; i++){
obj.selectionComponents[i].SetActive(cs.highlightedCharacterIndex == i && frame.Number % 10 < 5);
}
if (cs.isSelecting != true) continue;
if (result.Game.PlayerIsLocal(cs.playerRef)){
isSelecting = true;
} else {
isSpectating = true;
}
}
mainHeader.SetActive(isSelecting);
spectatingText.SetActive(isSpectating && !isSelecting);
}
Quantum
On the quantum side, the character selection process takes place in the CharacterSelectSystem
:
- Players are added to a list of available players;
- The first two players in the list are allowed to chose their characters; and,
- The loser is replaced with the next available player in the list.
Player assignment
The reception of a player’s RuntimePlayer
data triggers the ISignalOnPlayerDataSet
signal.
C#
public void OnPlayerDataSet(Frame f, PlayerRef player){
var list = f.ResolveList(f.Global->playerOrderList);
list.Add(player);
var filter = f.Filter<CharacterSelect>();
while (filter.NextUnsafe(out var e, out var cs)) {
if (cs->isSelecting == true) continue;
list.Remove(player);
cs->playerRef = player;
cs->highlightedCharacterIndex = 0;
cs->isSelecting = true;
cs->lastInput = InputFlag.NONE;
break;
}
}
The CharacterSelectSystem
implements the signal and adds the player who triggered it to the global list playerOrderList
held in the frame. If there is an available spot, they are assigned control over a CharacterSelect
component and thus granted the ability to choose a character to participate in the next match. If none is available, they will have to wait their turn; see Winner Stays section.
Selection Validation
The CharacterSelectionSystem
enforces the character selection rules on the Quantum side.
The system first ensures the selection was made in a valid space by looping the selected character through the list depending on the selection index.
C#
if ((inputFlag & InputFlag.Left) != 0) {
cs->highlightedCharacterIndex--;
if (cs->highlightedCharacterIndex < 0)
cs->highlightedCharacterIndex = cs->fighterChoices.Length - 1;
f.Events.PlayAudioEffect(FPVector2.Left, cs->toggleLeftSFX);
} else if ((inputFlag & InputFlag.Right) != 0){
cs->highlightedCharacterIndex++;
if (cs->highlightedCharacterIndex >= cs->fighterChoices.Length)
cs->highlightedCharacterIndex = 0;
f.Events.PlayAudioEffect(FPVector2.Left, cs->toggleRightSFX);
}
Each character has multiple costumes. The costume variant that will be applied to the character depends on the action button used to confirm the character selection.
C#
if ((inputFlag & InputFlag.LP) != 0) {
SelectionMade(f, cs, 0);
} else if ((inputFlag & InputFlag.LK) != 0) {
SelectionMade(f, cs, 1);
} else if ((inputFlag & InputFlag.HP) != 0) {
SelectionMade(f, cs, 2);
} else if ((inputFlag & InputFlag.HK) != 0) {
SelectionMade(f, cs, 3);
}
The SelectionMade()
method triggers the confirmation events to update the view and fires ISignalOnFighterSelected
signal (implemented by the FighterSystem
). The ISignalOnFighterSelected
instantiates the selected character and sets it up (position, rotation, costume swap in case of identical twins, etc…) and set f.Global->ready
to reflect whether both players have made their selection and their characters setup.
Winner Stays
The Fighting Sample implements an arcade style “Winner Stays” match; i.e. the loser relinquishes control and the next player on the spectator / waiting list is given a shot at the crown. The order in which players are chosen is defined by the order in which they joined the game and were added to the f.Global->playerOrderList
- see the OnPlayerDataSet
method in the CharacterSelectSystem
.
This process is handled in the NextMatchState
asset. First it removes the losing player from the match by calling CharacterSelectSystem.RemoveFighter
; this also resets the CharacerSelect
component for that fighter so the new player is granted the ability to choose their own character. Finally, it resets the remaining fighter back to their original position with ResetRoundState.ResetFighters
.
As soon as the new player has chosen their character, the next match starts.
Add A New Selectable Character
To add a new selectable character follow these steps:
- Increase the size of the fighterChoices array in the
CharacterSelect
component’s DSL definition -array<asset_ref<FighterData>>[4] fighterChoices
; - Add the character’s
FighterData
asset to thefighterChoices
field in theCharacterSelect
component on both Character Select P1 & P2 Prototypes. - Add the character portrait to the Selection UI.
- Ensure the character order in the Selection UI for P1 and P2 matches the order of the
FighterData
assets in theCharacterSelect
component’sfighterChoices
arrays.