Search Unity

Getting really frustrated with implementing input handling for real-time strategy games.

Discussion in 'Scripting' started by asperatology, Jun 29, 2015.

  1. asperatology

    asperatology

    Joined:
    Mar 10, 2015
    Posts:
    981
    For TL;DR - Read just the boldfaced text. :)

    I have a simple setup of using A, S, and D keys with the combination of left and right mouse buttons. No scrolling with the mouse wheel, and no other keys needed (other than exiting the game).

    I've been tackling the issue of handling the input combinations in regards to the 3 keys and 2 buttons. A, S, and D all have their own respective commands the player can use, while the left mouse button is for selection /canceling selection only, and right mouse button is for issuing orders and targets.

    The code below is me handling only the inputs, and setting the variables around to accommodate the issuing of orders and the states the controlled unit is doing. States are split into pending and current action states.

    Code (CSharp):
    1. private void HandleOrders() {
    2.         bool waitOrders = this.commandState.Equals(UnitCommand.ATTACK_ORDER) || this.commandState.Equals(UnitCommand.SPLIT_ORDER) || this.commandState.Equals(UnitCommand.MERGE_ORDER);
    3.         if (Input.GetKeyDown(KeyCode.A) && !waitOrders) {
    4.             this.pendingActionState = UnitState.WAITING;
    5.             this.commandState = UnitCommand.ATTACK_ORDER;
    6.         }
    7.         else if (Input.GetKeyDown(KeyCode.S) && !waitOrders) {
    8.             this.pendingActionState = UnitState.WAITING;
    9.             this.commandState = UnitCommand.SPLIT_ORDER;
    10.         }
    11.         else if (Input.GetKeyDown(KeyCode.D) && !waitOrders) {
    12.             this.pendingActionState = UnitState.WAITING;
    13.             this.commandState = UnitCommand.MERGE_ORDER;
    14.         }
    15.         else if (Input.GetMouseButtonDown(0) && this.pendingActionState.Equals(UnitState.WAITING) && waitOrders) {
    16.             Debug.Log("Hit");
    17.             this.commandState = UnitCommand.NO_ORDERS;
    18.             this.pendingActionState = this.actionState;
    19.         }
    20.         else if (Input.GetMouseButtonUp(1)) {
    21.             if (this.pendingActionState.Equals(UnitState.WAITING) && waitOrders) {
    22.                 switch (this.commandState) {
    23.                     case UnitCommand.NO_ORDERS:
    24.                         this.pendingActionState = UnitState.MOVING;
    25.                         this.actionState = this.pendingActionState;
    26.                         Move();
    27.                         break;
    28.                     case UnitCommand.MOVE_ORDER:
    29.                         this.pendingActionState = UnitState.MOVING;
    30.                         this.actionState = this.pendingActionState;
    31.                         Move();
    32.                         break;
    33.                     case UnitCommand.ATTACK_ORDER:
    34.                         this.pendingActionState = UnitState.ATTACKING;
    35.                         this.actionState = this.pendingActionState;
    36.                         AttackAction();
    37.                         break;
    38.                     default:
    39.                         break;
    40.                 }
    41.             }
    42.             else if (!this.pendingActionState.Equals(UnitState.WAITING) && waitOrders){
    43.                 if (Input.GetKeyDown(KeyCode.A) && !waitOrders) {
    44.                     this.pendingActionState = UnitState.WAITING;
    45.                     this.commandState = UnitCommand.ATTACK_ORDER;
    46.                 }
    47.                 else if (Input.GetKeyDown(KeyCode.S) && !waitOrders) {
    48.                     this.pendingActionState = UnitState.WAITING;
    49.                     this.commandState = UnitCommand.SPLIT_ORDER;
    50.                 }
    51.                 else if (Input.GetKeyDown(KeyCode.D) && !waitOrders) {
    52.                     this.pendingActionState = UnitState.WAITING;
    53.                     this.commandState = UnitCommand.MERGE_ORDER;
    54.                 }
    55.                 else if (Input.GetMouseButtonDown(0) && this.pendingActionState.Equals(UnitState.WAITING) && waitOrders) {
    56.                     Debug.Log("Hit");
    57.                     this.commandState = UnitCommand.NO_ORDERS;
    58.                     this.pendingActionState = this.actionState;
    59.                 }
    60.             }
    61.             else {
    62.                 this.pendingActionState = UnitState.MOVING;
    63.                 this.actionState = this.pendingActionState;
    64.                 Move();
    65.             }
    66.         }
    67.     }
    Full code can be seen at the very end of this post.

    Not considering refactoring and other ways of better usages of if...else, or switch statements, I am defeated by the complexity of the input combinations, and trying to get it so the players can issue their orders/commands correctly. I am trying to handle corner cases and cases that players with high APM could easily do that my HandleOrders() can break. For instance, units are quickly issued in succession, attack, move, attack, attack, move, select, deselect, and attack. Just the first three commands will break the HandleOrders().

    All I am asking for is either a better way of handling input combinations, or some way to handle the aggressive inputs coming into their way. Does anyone know how?

    Full code:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public enum UnitState {
    5.     IDLING,
    6.     ATTACKING,
    7.     SCOUTING,
    8.     MOVING,
    9.     DIVIDING,
    10.     MERGING,
    11.     WAITING,
    12.     DYING
    13. }
    14.  
    15. public enum UnitCommand {
    16.     ATTACK_ORDER,
    17.     SPLIT_ORDER,
    18.     MERGE_ORDER,
    19.     MOVE_ORDER,
    20.     WAITING_FOR_ORDERS,
    21.     NO_ORDERS
    22. }
    23.  
    24. public class UnitStateManager : MonoBehaviour {
    25.     public UnitState actionState;
    26.     public UnitState pendingActionState;
    27.     public bool selectFlag;
    28.     public UnitCommand commandState;
    29.  
    30.     private GameObject attackee;
    31.     private NavMeshAgent agent;
    32.     private float countdown;
    33.  
    34.     private void Start() {
    35.         this.actionState = UnitState.IDLING;
    36.         this.agent = this.GetComponent<NavMeshAgent>();
    37.         if (this.agent == null) {
    38.             Debug.LogError(new System.NullReferenceException("No nav mesh agent detected."));
    39.         }
    40.         this.agent.updateRotation = true;
    41.         this.agent.stoppingDistance = 0.85f;
    42.         this.selectFlag = false;
    43.         this.commandState = UnitCommand.NO_ORDERS;
    44.     }
    45.  
    46.     private void Update() {
    47.         if (this.selectFlag) {
    48.             DetectInput();
    49.         }
    50.     }
    51.  
    52.     private void DetectInput() {
    53.         if (Input.GetMouseButtonDown(0)) {
    54.          
    55.         }
    56.         else if (Input.GetMouseButtonDown(1)) {
    57.  
    58.         }
    59.     }
    60.  
    61.  
    62.     private void WaitingOnOrders() {
    63.         if (this.commandState.Equals(UnitCommand.WAITING_FOR_ORDERS)) {
    64.             if (Input.GetMouseButtonUp(0)) {
    65.                 this.commandState = UnitCommand.NO_ORDERS;
    66.                 this.actionState = UnitState.IDLING;
    67.                 Debug.Log("CANCEL ORDER");
    68.             }
    69.         }
    70.     }
    71.  
    72.     private void HandleOrders() {
    73.         bool waitOrders = this.commandState.Equals(UnitCommand.ATTACK_ORDER) || this.commandState.Equals(UnitCommand.SPLIT_ORDER) || this.commandState.Equals(UnitCommand.MERGE_ORDER);
    74.         if (Input.GetKeyDown(KeyCode.A) && !waitOrders) {
    75.             this.pendingActionState = UnitState.WAITING;
    76.             this.commandState = UnitCommand.ATTACK_ORDER;
    77.         }
    78.         else if (Input.GetKeyDown(KeyCode.S) && !waitOrders) {
    79.             this.pendingActionState = UnitState.WAITING;
    80.             this.commandState = UnitCommand.SPLIT_ORDER;
    81.         }
    82.         else if (Input.GetKeyDown(KeyCode.D) && !waitOrders) {
    83.             this.pendingActionState = UnitState.WAITING;
    84.             this.commandState = UnitCommand.MERGE_ORDER;
    85.         }
    86.         else if (Input.GetMouseButtonDown(0) && this.pendingActionState.Equals(UnitState.WAITING) && waitOrders) {
    87.             Debug.Log("Hit");
    88.             this.commandState = UnitCommand.NO_ORDERS;
    89.             this.pendingActionState = this.actionState;
    90.         }
    91.         else if (Input.GetMouseButtonUp(1)) {
    92.             if (this.pendingActionState.Equals(UnitState.WAITING) && waitOrders) {
    93.                 switch (this.commandState) {
    94.                     case UnitCommand.NO_ORDERS:
    95.                         this.pendingActionState = UnitState.MOVING;
    96.                         this.actionState = this.pendingActionState;
    97.                         Move();
    98.                         break;
    99.                     case UnitCommand.MOVE_ORDER:
    100.                         this.pendingActionState = UnitState.MOVING;
    101.                         this.actionState = this.pendingActionState;
    102.                         Move();
    103.                         break;
    104.                     case UnitCommand.ATTACK_ORDER:
    105.                         this.pendingActionState = UnitState.ATTACKING;
    106.                         this.actionState = this.pendingActionState;
    107.                         AttackAction();
    108.                         break;
    109.                     default:
    110.                         break;
    111.                 }
    112.             }
    113.             else if (!this.pendingActionState.Equals(UnitState.WAITING) && waitOrders){
    114.                 if (Input.GetKeyDown(KeyCode.A) && !waitOrders) {
    115.                     this.pendingActionState = UnitState.WAITING;
    116.                     this.commandState = UnitCommand.ATTACK_ORDER;
    117.                 }
    118.                 else if (Input.GetKeyDown(KeyCode.S) && !waitOrders) {
    119.                     this.pendingActionState = UnitState.WAITING;
    120.                     this.commandState = UnitCommand.SPLIT_ORDER;
    121.                 }
    122.                 else if (Input.GetKeyDown(KeyCode.D) && !waitOrders) {
    123.                     this.pendingActionState = UnitState.WAITING;
    124.                     this.commandState = UnitCommand.MERGE_ORDER;
    125.                 }
    126.                 else if (Input.GetMouseButtonDown(0) && this.pendingActionState.Equals(UnitState.WAITING) && waitOrders) {
    127.                     Debug.Log("Hit");
    128.                     this.commandState = UnitCommand.NO_ORDERS;
    129.                     this.pendingActionState = this.actionState;
    130.                 }
    131.             }
    132.             else {
    133.                 this.pendingActionState = UnitState.MOVING;
    134.                 this.actionState = this.pendingActionState;
    135.                 Move();
    136.             }
    137.         }
    138.     }
    139.  
    140.     private void HandleAction() {
    141.         ReachingDestination();
    142.     }
    143.  
    144.     private void AttackAction() {
    145.         if (this.attackee != null) {
    146.             if (!this.agent.pathPending) {
    147.                 if (this.agent.remainingDistance <= this.agent.stoppingDistance) {
    148.                     if (this.agent.hasPath || this.agent.velocity.sqrMagnitude == 0f) {
    149.                         //Stopped.
    150.                         float distance = Vector3.Distance(this.transform.position, this.attackee.transform.position);
    151.                         if (distance >= 1f) {
    152.                             this.agent.stoppingDistance = 0.9f;
    153.                             this.agent.SetDestination(this.attackee.transform.position);
    154.                         }
    155.                     }
    156.                 }
    157.             }
    158.             //Do attack here.
    159.             UnitHealth health = this.attackee.GetComponent<UnitHealth>();
    160.             if (health != null && health.healthPoints > 0) {
    161.                 this.attackee.SendMessage("DoDamage");
    162.             }
    163.             return;
    164.         }
    165.         else {
    166.             CheckSurroundings();
    167.             if (this.attackee == null) {
    168.                 AttackMove();
    169.             }
    170.             ReachingDestination();
    171.         }
    172.     }
    173.  
    174.     private void AttackObject(GameObject victim) {
    175.         Debug.Log("Attacking the " + victim.name.ToString());
    176.         this.actionState = UnitState.ATTACKING;
    177.         Vector3 spacing = victim.transform.position - this.transform.position;
    178.         spacing *= 0.8f;
    179.         spacing += this.transform.position;
    180.         this.agent.SetDestination(spacing);
    181.     }
    182.  
    183.     private void AttackMove() {
    184.         this.actionState = UnitState.ATTACKING;
    185.         this.countdown = 2f;
    186.         if (Input.GetMouseButtonUp(1)) {
    187.             Debug.Log("Giving attack order and setting new destination.");
    188.             Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    189.             RaycastHit hitInfo;
    190.             if (Physics.Raycast(ray, out hitInfo)) {
    191.                 Debug.Log("New move target position: " + hitInfo.point);
    192.                 this.agent.SetDestination(hitInfo.point);
    193.             }
    194.         }
    195.     }
    196.  
    197.     private void DivideAction() {
    198.     }
    199.  
    200.     private void DeathAction() {
    201.     }
    202.  
    203.     private void IdleAction() {
    204.         CheckSurroundings();
    205.     }
    206.  
    207.     private void MergeAction() {
    208.     }
    209.  
    210.     private void ScoutAction() {
    211.     }
    212.  
    213.     private void MoveAction() {
    214.         ReachingDestination();
    215.     }
    216.  
    217.     private void CheckSurroundings() {
    218.         //Check if enemies are nearby.
    219.         bool currentState = this.actionState.Equals(UnitState.IDLING) || this.actionState.Equals(UnitState.ATTACKING);
    220.         if (currentState) {
    221.             Collider[] colliders = Physics.OverlapSphere(this.transform.position, 3f);
    222.             bool enemyNearby = false;
    223.             for (int i = 0; i < colliders.Length; i++) {
    224.                 if (!colliders[i].name.Equals("Floor") && !colliders[i].gameObject.Equals(this.gameObject)) {
    225.                     float distance = Vector3.Distance(colliders[i].transform.position, this.transform.position);
    226.                     if (distance < 4f) {
    227.                         this.attackee = colliders[i].gameObject;
    228.                         this.SendMessage("AttackObject", this.attackee);
    229.                         enemyNearby = true;
    230.                         break;
    231.                     }
    232.                 }
    233.             }
    234.             if (this.attackee != null && !enemyNearby) {
    235.                 this.attackee = null;
    236.             }
    237.         }
    238.     }
    239.  
    240.     private void Move() {
    241.         Debug.Log("Giving attack order and setting new destination.");
    242.         this.actionState = UnitState.MOVING;
    243.         this.commandState = UnitCommand.MOVE_ORDER;
    244.         this.countdown = 2f;
    245.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    246.         RaycastHit hitInfo;
    247.         if (Physics.Raycast(ray, out hitInfo)) {
    248.             Debug.Log("New move target position: " + hitInfo.point);
    249.             this.agent.SetDestination(hitInfo.point);
    250.         }
    251.     }
    252.  
    253.     private void ReachingDestination() {
    254.         if (!this.agent.pathPending) {
    255.             if (this.agent.remainingDistance <= this.agent.stoppingDistance) {
    256.                 if (this.agent.hasPath || this.agent.velocity.sqrMagnitude == 0f) {
    257.                     if (!this.pendingActionState.Equals(UnitState.WAITING)) {
    258.                         this.commandState = UnitCommand.NO_ORDERS;
    259.                         this.pendingActionState = UnitState.IDLING;
    260.                         if (this.countdown > 0f) {
    261.                             this.countdown -= Time.deltaTime;
    262.                         }
    263.                         else if (!this.actionState.Equals(UnitState.WAITING)) {
    264.                             this.actionState = this.pendingActionState;
    265.                         }
    266.                     }
    267.                 }
    268.             }
    269.         }
    270.     }
    271. }
     
  2. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Your input should be on a single MonoBehaviour, not every unit. Then you can throw out your whole pending action thing (which should be a finite state machine implementation BTW).

    We basically do this:
    If user presses left mouse button - register cursor location.
    If left mouse button is down - check if they moved the cursor far enough from initial location to start drawing a selection box
    If the user releases the left mouse button - are we drawing a box? If so get all the units in the box and add them to our selected units list. If not - see if there is a unit underneath the cursor. If we get a unit, select it. If we don't then clear the current selection list.

    If user presses right mouse button - register cursor location.
    If right mouse button is down - check if they moved the cursor far enough from initial location to allow the camera to drag-move.
    If user releases right mouse button - do we have any units selected? If so check for an enemy under the cursor. If we get an enemy change all the selected units to an attack state. If we don't change to a move state. If we have no selected units then don't do anything.
     
    Kiwasi likes this.
  3. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    As @KelsoMRK mentioned, it looks like you are trying to do too many jobs in a single script.

    Provide a single global "Input Manager" that handles all of the user input. This also makes it possible to do things like change keys. The input manager should keep a List of selected units, and also fire off events to the selected Units.

    Each unit should then handle its own state based on orders received. This can also include a list or queue of actions if you like.
     
  4. asperatology

    asperatology

    Joined:
    Mar 10, 2015
    Posts:
    981
    Oh. Well, it never occurred to me that I can separate input handling from a prefab to a single game object. But I have a few questions about that idea if implemented:

    @KelsoMRK For the input manager, do you specifically use SendMessage() so that it propagates through all other scripts in the Hierarchy? Or, I use it like setting static flags in that one InputManager class, and let all other units go check the flags (peeking)?

    @BoredMormon For the global InputManager, do I keep the "IF...ELSE" statements I have in my script in my first post? Like, I check for inputs in the Update() functions, and then I would try to set/toggle/flip flags and send it off to other game objects? Or, redesign it in a way that checks individual inputs, rather than a group of inputs altogether (the way I was using in the first place)?

    I have some trouble designing and coding how Unity handles individual inputs when I require a specific order of inputs to make it work, especially for high APM players. It's like chaining a certain combo that, when input correctly, it will send off a message to the unit telling the unit to do this order/command or that order/command.

    I'll figure something out, but I really am grateful for both of your replies. I really do appreciated your helps.
     
  5. KelsoMRK

    KelsoMRK

    Joined:
    Jul 18, 2010
    Posts:
    5,539
    Our list of selected units is a list of MonoBehaviours called UnitManager that expose methods to change a unit's state. So no, we don't use SendMessage. And I don't get why units would constantly check input. The input should go tell the units what to do directly. Don't mess with setting a million flags to track input state for what amounts to a single frame.

    I don't understand your concern about high APM players. The fastest pro SC players hit about 500 APM which is 1 action every 120ms. If your game is running at 60fps (hell, a lot less than that even) then it would be impossible, for the fastest RTS players on the planet, to issue multiple commands in a single frame.

    If you're worried about issuing conflicting commands (ie I right click and press A at *exactly* the same time) then it still doesn't matter because whatever command is processed last in Update() will win.
     
  6. asperatology

    asperatology

    Joined:
    Mar 10, 2015
    Posts:
    981
    A player with high APM means a player that simultaneously input multiple keys at similar times. So, yes, I didn't think of Update() seeing only 1 input per update tick. It now makes more sense than my rubbish "High APM player" explanation.

    I get it now. Thank you.