2
\$\begingroup\$

For practice, I'm trying to remake the character select screen from Street Fighter II.

Screenshot of recreated character select screen

I managed get it working with a Grid Layout Group, ScriptableObjects, and some scripts to have the 1P cursor move around and change the portrait and text on the left.

What I don't know is how to make it 2 Player. The white area on the right is where I'm intending to show the selection for the second player, however I can't get 2 players to work here.

Currently, the Player selection works using WASD or the Arrow Keys (and the mouse, though I'd like some way to turn that off for now since it causes weird things to happen).

I'd like P1 to work with WASD and P2 to work with the arrow keys.

I've tried to make 4 new axes for the X & Y of each player with different buttons and apply them to different Standalone Input Module components, but that doesn't work (one often ends up disabling the other).

I've also looked into Event Systems, but wasn't sure if this was applicable. I don't know much beyond the basics of UI programming and I've never really dealt with Event Systems and Event Triggers before this. Nothing I do seems to let a second player exist in this selection scene.

This is the script that I'm currently using for selection. It uses parameters from a Character ScriptableObject I made to control the graphics. However, my navigation inputs in the UI seem to be driven by the Standalone Input Module.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;

public class SelectionScript : Selectable
{

    //Our variables
    Image characterPortrait;
    Image characterNameSprite;
    Text characterName;
    CharDisplay character;
    public GameObject P1cursor;

    BaseEventData m_BaseEvent;//This is a way for us to track certain events in a Selectable object in the Event System.


    // Start is called before the first frame update
    void Start()
    {
        character = GetComponent<CharDisplay>(); //We're going to use this to access anything we want inside of the CharDisplay

        characterName = character.nameText; //The UI object in the CharDisplay Script we said we wanted to represent the name text
        characterNameSprite = character.charName; // The UI Object we want to show the Name Sprite
        characterPortrait = character.portrait; //The UI object we want to show the Portrait Art

        P1cursor = GameObject.FindGameObjectWithTag("P1Cursor"); // Automatically finds the P1cursor based on tag
    }

    // Update is called once per frame
    void Update()
    {

        if ((IsHighlighted(m_BaseEvent) == true)){ //If an object is highlighted...
            P1cursor.transform.position = gameObject.transform.position; //Move the cursor to it.
            characterPortrait.sprite = character.character.Portrait; //Make the sprite of the Portrait UI Image we're accessing become the CharDisplay Character's defined Portrait sprite.
            characterNameSprite.sprite = character.character.CharName; //Make the sprite of the Name Sprite UI Image we're accessing become the CharDisplay Character's defined Name sprite.
            characterName.text = character.character.name; //Make the name in the extra textbox we made equal the CharDisplay Character's defined name text.
        }

        //This let's us know what we are selecting.
        if ((IsHighlighted(m_BaseEvent)== true) && Input.GetKey(KeyCode.Space))
        {
            Debug.Log("You have selected " + characterName.text.ToString());//We need to translate the name of the character into a string for the console.
        }
    }
}

Anyone have any advice on making a 2-Player UI? I'm glad I was able to figure the 1P out, but I'd really appreciate being able to learn how to make a multiplayer UI.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

This is possible, but you have to keep track of one selection for each player separately, and handle reading each player's input/reacting to it yourself.

But you can still reap the benefits of the UI system's navigation mapping, highlighted state transitions, etc.

We can do this with a MonoBehaviour script that represents one player's selection state. Our scene can contain one or more instances of this script, or even spawn/enable more on the fly as players join.

This script is responsible for:

  • reading one player's inputs
  • updating the selected item based on those inputs
  • reflecting the current selection using that player's cursor and the UI selected states (think button highlights, etc)
  • notifying other scripts when this player's selection has changed, via an onSelect event

    (Your selected character name & portrait panel can subscribe to this event and peek at the selected item to determine what to show)

Here's a proof of concept:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;

public class IndividualSelection : MonoBehaviour
{
    // Use this to position a cursor or selection highlight for each player.
    public RectTransform cursor;

    // Choose which UI element this player should select by default.
    public Selectable defaultSelection;

    // Names for this player's axes & buttons, 
    // so they're not both listening to the same inputs.
    public string horizontalAxis;
    public string verticalAxis;
    public string acceptButton;
    public string cancelButton;

    // To make an analog stick "clicky" - you might need to soup-up the
    // sensitivity & gravity values beyond their defaults in the Input Manager.
    public float activationThreshold = 0.1f;

    // Debouncing / rejecting holds on the axis.
    Vector2 _axisMemory;

    // Keeping track of current selection state.
    Selectable _currentSelection;
    static Dictionary<Selectable, int> _selectionCount = new Dictionary<Selectable, int>();

    // Expose an event for when the player's selection changes.
    // Hook into this to update your portrait / etc.
    [System.Serializable]
    public class SelectionEvent : UnityEvent<Selectable> { }
    public SelectionEvent onSelect;

    public UnityEvent onAccept;

    public Selectable GetCurrentSelection() { return _currentSelection; }


    void Start() {
        // Selection has to start somewhere 
        // (you can also inject this from the script that spawns the player
        // or handles joining in progress, or choose a default selection randomly)
        Select(defaultSelection);
    }

    public void Select(Selectable target) {
        // A more complete solution might see us create our own EventSystem
        // or InputModule that's multiplayer-aware, but this will do for now.
        var fakeEvent = new BaseEventData(EventSystem.current);

        // Deselect the old selection if we're the last one to leave it.
        // (But don't deselect something another player is still hovering over)
        if (_currentSelection != null && --_selectionCount[_currentSelection] == 0) {
            _currentSelection.OnDeselect(fakeEvent);
            _selectionCount.Remove(_currentSelection);
        }

        // Handle highlighting our current selection.
        _currentSelection = target;    
        if (_selectionCount.ContainsKey(_currentSelection))
            _selectionCount[_currentSelection]++;
        else {
            _selectionCount.Add(_currentSelection, 1);
            target.OnSelect(fakeEvent);
        }

        // Position our cursor over the selected control.
        // There are nicer ways to animate this, but this served for a quick test.
        cursor.position = _currentSelection.transform.position;

        // Let listeners know that this player's selection has changed.
        if(onSelect != null)
            onSelect.Invoke(_currentSelection);
    }

    void Update() {
        Vector2 input = new Vector2(Input.GetAxis(horizontalAxis), Input.GetAxis(verticalAxis));

        Selectable nextSelection = null;

        // Don't keep bouncing down the list until the player releases/reverses.
        // TODO: handle repeat rate for holding in one direction, if desired.
        if (input.x * _axisMemory.x <= 0f) {
            _axisMemory.x = 0f;
            if (Mathf.Abs(input.x) > activationThreshold) {
                if (input.x > 0f) {
                    nextSelection = _currentSelection.FindSelectableOnRight();
                    _axisMemory.x = 1f;
                } else {
                    nextSelection = _currentSelection.FindSelectableOnLeft();
                    _axisMemory.x = -1f;
                }
            }
        }

        if (input.y * _axisMemory.y <= 0f) {
            _axisMemory.y = 0f;
            if (Mathf.Abs(input.y) > activationThreshold) {
                if (input.y > 0f) {
                    nextSelection = _currentSelection.FindSelectableOnUp();
                    _axisMemory.y = 1f;
                } else {
                    nextSelection = _currentSelection.FindSelectableOnDown();
                    _axisMemory.y = -1f;
                }
            }
        }

        if(nextSelection != null) {
            Select(nextSelection);
        } else if(Input.GetButtonDown(acceptButton)) {
            // If we're selecting something clickable,
            // handle the accept button like a click.
            var clickable = _currentSelection.GetComponent<IPointerClickHandler>();
            if (clickable != null) {
                if (_currentSelection.IsInteractable()) {
                    clickable.OnPointerClick(new PointerEventData(EventSystem.current));
                }
            // Otherwise, pass the accept on to any other interested listeners.
            } else if (onAccept != null) {
                onAccept.Invoke();
            }
        }
    }
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .